@cometchat/skills 4.4.2 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (518) hide show
  1. package/.claude-plugin/marketplace.json +55 -0
  2. package/.claude-plugin/plugin.json +14 -0
  3. package/CHANGELOG.md +44 -0
  4. package/LICENSE +21 -0
  5. package/MIGRATING.md +187 -0
  6. package/README.md +52 -156
  7. package/RULES.md +125 -0
  8. package/bin/cometchat-skills.mjs +1280 -0
  9. package/bin/legacy-skills.json +95 -0
  10. package/catalogs/android-v6.json +1646 -0
  11. package/catalogs/flutter-v6.json +651 -0
  12. package/catalogs/ios-v5.json +1383 -0
  13. package/catalogs/rn-calls-v5.json +134 -0
  14. package/catalogs/rn-calls-v5.symbols.txt +58 -0
  15. package/catalogs/rn-sdk-v4.json +672 -0
  16. package/catalogs/rn-v5.json +449 -0
  17. package/catalogs/rn-v5.symbols.txt +216 -0
  18. package/catalogs/sdk-android-v5.json +514 -0
  19. package/contracts.android-calls-v5.json +122 -0
  20. package/contracts.android-v6.json +135 -0
  21. package/contracts.angular-v5.json +694 -0
  22. package/contracts.flutter-calls-v5.json +77 -0
  23. package/contracts.flutter-v6.json +126 -0
  24. package/contracts.ios-calls-v5.json +67 -0
  25. package/contracts.ios-v5.json +436 -0
  26. package/contracts.json +82 -0
  27. package/contracts.rn-calls-v5.json +77 -0
  28. package/contracts.rn-v5.json +727 -0
  29. package/contracts.sdk-android-v5.json +15 -0
  30. package/contracts.web-calls-v5.json +45 -0
  31. package/features.android-calls-v5.json +246 -0
  32. package/features.android-v6.json +65 -0
  33. package/features.angular-v5.json +521 -0
  34. package/features.flutter-calls-v5.json +286 -0
  35. package/features.flutter-v6.json +484 -0
  36. package/features.ios-calls-v5.json +236 -0
  37. package/features.ios-v5.json +573 -0
  38. package/features.json +61 -0
  39. package/features.rn-calls-v5.json +187 -0
  40. package/features.rn-v5.json +614 -0
  41. package/features.sdk-android-v5.json +40 -0
  42. package/features.sdk-jsv4.json +24 -0
  43. package/features.web-calls-v5.json +108 -0
  44. package/package.json +156 -74
  45. package/peers.yaml +355 -0
  46. package/registry/README.md +31 -0
  47. package/registry/android-calls-v5.json +94 -0
  48. package/registry/android-v6.json +177 -0
  49. package/registry/angular-v5.json +154 -0
  50. package/registry/flutter-calls-v5.json +94 -0
  51. package/registry/flutter-v6.json +159 -0
  52. package/registry/ios-calls-v5.json +99 -0
  53. package/registry/ios-v5.json +152 -0
  54. package/registry/rn-calls-v5.json +99 -0
  55. package/registry/rn-v5.json +169 -0
  56. package/registry/sdk-android-v5.json +93 -0
  57. package/registry/sdk-jsv4.json +93 -0
  58. package/registry/web-calls-v5.json +94 -0
  59. package/registry/web-v7.json +154 -0
  60. package/skills/cometchat/SKILL.md +47 -2096
  61. package/skills/cometchat-a11y/SKILL.md +44 -472
  62. package/skills/cometchat-analytics/SKILL.md +61 -0
  63. package/skills/cometchat-android-v5-calls-sdk/SKILL.md +206 -0
  64. package/skills/cometchat-android-v5-calls-sdk/references/build-truth.md +63 -0
  65. package/skills/cometchat-android-v5-calls-sdk/references/docs-map.md +119 -0
  66. package/skills/cometchat-android-v5-calls-sdk/references/method-map.md +53 -0
  67. package/skills/cometchat-android-v5-calls-sdk/references/pitfalls.md +72 -0
  68. package/skills/cometchat-android-v5-calls-sdk/references/ringing.md +92 -0
  69. package/skills/cometchat-android-v5-calls-sdk/references/user-switch.md +90 -0
  70. package/skills/cometchat-android-v5-sdk/SKILL.md +127 -0
  71. package/skills/cometchat-android-v5-sdk/references/app-surface.md +88 -0
  72. package/skills/cometchat-android-v5-sdk/references/docs-map.md +91 -0
  73. package/skills/cometchat-android-v6-builder-settings/SKILL.md +51 -137
  74. package/skills/cometchat-android-v6-calls/SKILL.md +128 -413
  75. package/skills/cometchat-android-v6-compose-components/SKILL.md +84 -194
  76. package/skills/cometchat-android-v6-compose-customization/SKILL.md +66 -411
  77. package/skills/cometchat-android-v6-compose-placement/SKILL.md +154 -237
  78. package/skills/cometchat-android-v6-core/SKILL.md +236 -672
  79. package/skills/cometchat-android-v6-core/references/anti-patterns.md +31 -0
  80. package/skills/cometchat-android-v6-core/references/component-props.md +168 -0
  81. package/skills/cometchat-android-v6-core/references/docs-map.md +153 -0
  82. package/skills/cometchat-android-v6-core/references/layout.md +112 -0
  83. package/skills/cometchat-android-v6-core/references/lifecycle.md +75 -0
  84. package/skills/cometchat-android-v6-core/references/setup-credentials.md +105 -0
  85. package/skills/cometchat-android-v6-core/references/troubleshooting.md +36 -0
  86. package/skills/cometchat-android-v6-events/SKILL.md +46 -184
  87. package/skills/cometchat-android-v6-extensions/SKILL.md +39 -179
  88. package/skills/cometchat-android-v6-features/SKILL.md +74 -143
  89. package/skills/cometchat-android-v6-kotlin-components/SKILL.md +75 -193
  90. package/skills/cometchat-android-v6-kotlin-customization/SKILL.md +67 -419
  91. package/skills/cometchat-android-v6-kotlin-placement/SKILL.md +77 -223
  92. package/skills/cometchat-android-v6-migration/SKILL.md +67 -378
  93. package/skills/cometchat-android-v6-production/SKILL.md +41 -179
  94. package/skills/cometchat-android-v6-push/SKILL.md +39 -289
  95. package/skills/cometchat-android-v6-testing/SKILL.md +50 -265
  96. package/skills/cometchat-android-v6-troubleshooting/SKILL.md +70 -254
  97. package/skills/cometchat-angular-v5-calls/SKILL.md +193 -0
  98. package/skills/cometchat-angular-v5-components/SKILL.md +131 -0
  99. package/skills/cometchat-angular-v5-components/references/catalog.md +111 -0
  100. package/skills/cometchat-angular-v5-components/references/host-composed.md +46 -0
  101. package/skills/cometchat-angular-v5-core/SKILL.md +300 -0
  102. package/skills/cometchat-angular-v5-core/references/anti-patterns.md +49 -0
  103. package/skills/cometchat-angular-v5-core/references/component-props.md +55 -0
  104. package/skills/cometchat-angular-v5-core/references/dependencies.md +66 -0
  105. package/skills/cometchat-angular-v5-core/references/docs-map.md +132 -0
  106. package/skills/cometchat-angular-v5-core/references/layout.md +64 -0
  107. package/skills/cometchat-angular-v5-core/references/lifecycle.md +202 -0
  108. package/skills/cometchat-angular-v5-core/references/setup-credentials.md +137 -0
  109. package/skills/cometchat-angular-v5-core/references/theming.md +70 -0
  110. package/skills/cometchat-angular-v5-core/references/troubleshooting.md +27 -0
  111. package/skills/cometchat-angular-v5-customization/SKILL.md +94 -0
  112. package/skills/cometchat-angular-v5-features/SKILL.md +107 -0
  113. package/skills/cometchat-angular-v5-features/references/custom-messages.md +133 -0
  114. package/skills/cometchat-angular-v5-features/references/formatters.md +28 -0
  115. package/skills/cometchat-angular-v5-features/references/host-composed-extensions.md +85 -0
  116. package/skills/cometchat-angular-v5-migration/SKILL.md +40 -0
  117. package/skills/cometchat-angular-v5-migration/references/migration-guide.md +165 -0
  118. package/skills/cometchat-angular-v5-patterns/SKILL.md +107 -0
  119. package/skills/cometchat-angular-v5-placement/SKILL.md +133 -0
  120. package/skills/cometchat-angular-v5-placement/references/combined-app.md +174 -0
  121. package/skills/cometchat-angular-v5-placement/references/core-surface.md +199 -0
  122. package/skills/cometchat-angular-v5-production/SKILL.md +102 -0
  123. package/skills/cometchat-angular-v5-push/SKILL.md +61 -0
  124. package/skills/cometchat-angular-v5-testing/SKILL.md +112 -0
  125. package/skills/cometchat-angular-v5-troubleshooting/SKILL.md +96 -0
  126. package/skills/cometchat-audit/SKILL.md +61 -0
  127. package/skills/cometchat-compliance/SKILL.md +63 -0
  128. package/skills/cometchat-flutter-v5-sdk/SKILL.md +188 -0
  129. package/skills/cometchat-flutter-v5-sdk/references/doc-corrections.md +55 -0
  130. package/skills/cometchat-flutter-v5-sdk/references/docs-map.md +211 -0
  131. package/skills/cometchat-flutter-v5-sdk/references/platform-notes.md +48 -0
  132. package/skills/cometchat-flutter-v5-sdk/references/transcription.md +87 -0
  133. package/skills/cometchat-flutter-v6-calls/SKILL.md +75 -389
  134. package/skills/cometchat-flutter-v6-components/SKILL.md +88 -361
  135. package/skills/cometchat-flutter-v6-core/SKILL.md +154 -593
  136. package/skills/cometchat-flutter-v6-core/references/anti-patterns.md +55 -0
  137. package/skills/cometchat-flutter-v6-core/references/docs-map.md +143 -0
  138. package/skills/cometchat-flutter-v6-core/references/layout.md +45 -0
  139. package/skills/cometchat-flutter-v6-core/references/lifecycle.md +87 -0
  140. package/skills/cometchat-flutter-v6-core/references/setup-credentials.md +84 -0
  141. package/skills/cometchat-flutter-v6-core/references/troubleshooting.md +26 -0
  142. package/skills/cometchat-flutter-v6-customization/SKILL.md +70 -712
  143. package/skills/cometchat-flutter-v6-events/SKILL.md +79 -174
  144. package/skills/cometchat-flutter-v6-features/SKILL.md +55 -394
  145. package/skills/cometchat-flutter-v6-migration/SKILL.md +68 -367
  146. package/skills/cometchat-flutter-v6-migration/references/migration-guide.md +132 -0
  147. package/skills/cometchat-flutter-v6-patterns/SKILL.md +61 -0
  148. package/skills/cometchat-flutter-v6-placement/SKILL.md +94 -381
  149. package/skills/cometchat-flutter-v6-production/SKILL.md +59 -615
  150. package/skills/cometchat-flutter-v6-push/SKILL.md +46 -455
  151. package/skills/cometchat-flutter-v6-testing/SKILL.md +68 -299
  152. package/skills/cometchat-flutter-v6-troubleshooting/SKILL.md +54 -878
  153. package/skills/cometchat-i18n/SKILL.md +35 -404
  154. package/skills/cometchat-ios-calls/SKILL.md +112 -563
  155. package/skills/cometchat-ios-components/SKILL.md +44 -834
  156. package/skills/cometchat-ios-core/SKILL.md +182 -1068
  157. package/skills/cometchat-ios-core/references/anti-patterns.md +113 -0
  158. package/skills/cometchat-ios-core/references/docs-map.md +124 -0
  159. package/skills/cometchat-ios-core/references/install.md +23 -0
  160. package/skills/cometchat-ios-core/references/layout.md +219 -0
  161. package/skills/cometchat-ios-core/references/setup-credentials.md +197 -0
  162. package/skills/cometchat-ios-core/references/swiftui.md +91 -0
  163. package/skills/cometchat-ios-core/references/troubleshooting.md +100 -0
  164. package/skills/cometchat-ios-customization/SKILL.md +33 -968
  165. package/skills/cometchat-ios-features/SKILL.md +29 -738
  166. package/skills/cometchat-ios-migration/SKILL.md +39 -0
  167. package/skills/cometchat-ios-placement/SKILL.md +51 -729
  168. package/skills/cometchat-ios-production/SKILL.md +67 -723
  169. package/skills/cometchat-ios-push/SKILL.md +24 -602
  170. package/skills/cometchat-ios-testing/SKILL.md +36 -355
  171. package/skills/cometchat-ios-troubleshooting/SKILL.md +31 -989
  172. package/skills/cometchat-ios-v5-sdk/SKILL.md +161 -0
  173. package/skills/cometchat-ios-v5-sdk/references/docs-map.md +326 -0
  174. package/skills/cometchat-js-v5-sdk/SKILL.md +95 -0
  175. package/skills/cometchat-js-v5-sdk/references/docs-map.md +114 -0
  176. package/skills/cometchat-migrate-from-getstream/SKILL.md +65 -0
  177. package/skills/cometchat-migrate-from-getstream/references/concept-map.md +98 -0
  178. package/skills/cometchat-migrate-from-getstream/references/data-migration.md +74 -0
  179. package/skills/cometchat-migrate-from-getstream/references/feature-map.md +108 -0
  180. package/skills/cometchat-migrate-from-getstream/references/inventory.md +82 -0
  181. package/skills/cometchat-migrate-from-getstream/references/native-build.md +9 -0
  182. package/skills/cometchat-migrate-from-getstream/references/report-template.md +59 -0
  183. package/skills/cometchat-migrate-from-sendbird/SKILL.md +64 -0
  184. package/skills/cometchat-migrate-from-sendbird/references/concept-map.md +99 -0
  185. package/skills/cometchat-migrate-from-sendbird/references/data-migration.md +74 -0
  186. package/skills/cometchat-migrate-from-sendbird/references/feature-map.md +104 -0
  187. package/skills/cometchat-migrate-from-sendbird/references/inventory.md +86 -0
  188. package/skills/cometchat-migrate-from-sendbird/references/native-build.md +10 -0
  189. package/skills/cometchat-migrate-from-sendbird/references/report-template.md +59 -0
  190. package/skills/cometchat-migrate-from-twilio/SKILL.md +52 -0
  191. package/skills/cometchat-migrate-from-twilio/references/concept-map.md +49 -0
  192. package/skills/cometchat-migrate-from-twilio/references/data-migration.md +50 -0
  193. package/skills/cometchat-migrate-from-twilio/references/feature-map.md +27 -0
  194. package/skills/cometchat-migrate-from-twilio/references/inventory.md +53 -0
  195. package/skills/cometchat-migrate-from-twilio/references/report-template.md +52 -0
  196. package/skills/cometchat-moderation/SKILL.md +58 -0
  197. package/skills/cometchat-onboarding/SKILL.md +92 -0
  198. package/skills/cometchat-onboarding/references/archetypes.md +51 -0
  199. package/skills/cometchat-onboarding/references/calling-first.md +77 -0
  200. package/skills/cometchat-onboarding/references/not-in-this-pack.md +8 -0
  201. package/skills/cometchat-onboarding/references/plan-artifact.md +133 -0
  202. package/skills/cometchat-onboarding/references/platforms.md +105 -0
  203. package/skills/cometchat-react-native-bare-patterns/SKILL.md +120 -0
  204. package/skills/cometchat-react-native-calls/SKILL.md +200 -0
  205. package/skills/cometchat-react-native-components/SKILL.md +142 -0
  206. package/skills/cometchat-react-native-core/SKILL.md +335 -0
  207. package/skills/cometchat-react-native-core/references/anti-patterns.md +79 -0
  208. package/skills/cometchat-react-native-core/references/component-props.md +105 -0
  209. package/skills/cometchat-react-native-core/references/dependencies.md +62 -0
  210. package/skills/cometchat-react-native-core/references/docs-map.md +127 -0
  211. package/skills/cometchat-react-native-core/references/layout.md +61 -0
  212. package/skills/cometchat-react-native-core/references/lifecycle.md +64 -0
  213. package/skills/cometchat-react-native-core/references/setup-credentials.md +39 -0
  214. package/skills/cometchat-react-native-customization/SKILL.md +198 -0
  215. package/skills/cometchat-react-native-expo-patterns/SKILL.md +116 -0
  216. package/skills/cometchat-react-native-features/SKILL.md +122 -0
  217. package/skills/cometchat-react-native-migration/SKILL.md +103 -0
  218. package/skills/cometchat-react-native-placement/SKILL.md +158 -0
  219. package/skills/cometchat-react-native-production/SKILL.md +79 -0
  220. package/skills/cometchat-react-native-push/SKILL.md +88 -0
  221. package/skills/cometchat-react-native-sdk/SKILL.md +133 -0
  222. package/skills/cometchat-react-native-sdk/references/method-map.md +239 -0
  223. package/skills/cometchat-react-native-sdk/references/page-map.md +96 -0
  224. package/skills/cometchat-react-native-testing/SKILL.md +94 -0
  225. package/skills/cometchat-react-native-troubleshooting/SKILL.md +174 -0
  226. package/skills/cometchat-react-native-v5-sdk/SKILL.md +150 -0
  227. package/skills/cometchat-react-native-v5-sdk/references/docs-map.md +127 -0
  228. package/skills/cometchat-react-native-v5-sdk/references/ringing-voip.md +369 -0
  229. package/skills/cometchat-react-v7-calls/SKILL.md +75 -0
  230. package/skills/cometchat-react-v7-components/SKILL.md +72 -0
  231. package/skills/cometchat-react-v7-core/SKILL.md +101 -0
  232. package/skills/cometchat-react-v7-core/references/anti-patterns.md +19 -0
  233. package/skills/cometchat-react-v7-core/references/component-props.md +82 -0
  234. package/skills/cometchat-react-v7-core/references/dependencies.md +23 -0
  235. package/skills/cometchat-react-v7-core/references/docs-map.md +117 -0
  236. package/skills/cometchat-react-v7-core/references/layout.md +150 -0
  237. package/skills/cometchat-react-v7-core/references/lifecycle.md +74 -0
  238. package/skills/cometchat-react-v7-core/references/setup-credentials.md +58 -0
  239. package/skills/cometchat-react-v7-core/references/ssr.md +11 -0
  240. package/skills/cometchat-react-v7-core/references/theming.md +52 -0
  241. package/skills/cometchat-react-v7-customization/SKILL.md +84 -0
  242. package/skills/cometchat-react-v7-features/SKILL.md +92 -0
  243. package/skills/cometchat-react-v7-migration/SKILL.md +39 -0
  244. package/skills/cometchat-react-v7-migration/references/migration-guide.md +119 -0
  245. package/skills/cometchat-react-v7-patterns/SKILL.md +43 -0
  246. package/skills/cometchat-react-v7-placement/SKILL.md +106 -0
  247. package/skills/cometchat-react-v7-placement/references/recipes.md +354 -0
  248. package/skills/cometchat-react-v7-production/SKILL.md +84 -0
  249. package/skills/cometchat-react-v7-push/SKILL.md +53 -0
  250. package/skills/cometchat-react-v7-testing/SKILL.md +60 -0
  251. package/skills/cometchat-react-v7-troubleshooting/SKILL.md +47 -0
  252. package/skills/cometchat-security/SKILL.md +80 -0
  253. package/skills/cometchat-self-host/SKILL.md +56 -0
  254. package/test-suite/README.md +70 -0
  255. package/test-suite/catalogs/android-calls-v5.json +830 -0
  256. package/test-suite/catalogs/android-calls-v5.symbols.txt +499 -0
  257. package/test-suite/catalogs/android-v6.json +1646 -0
  258. package/test-suite/catalogs/android-v6.symbols.txt +298 -0
  259. package/test-suite/catalogs/angular-v5.json +7587 -0
  260. package/test-suite/catalogs/flutter-calls-v5.json +196 -0
  261. package/test-suite/catalogs/flutter-calls-v5.symbols.txt +59 -0
  262. package/test-suite/catalogs/flutter-v6.json +651 -0
  263. package/test-suite/catalogs/ios-calls-v5.json +158 -0
  264. package/test-suite/catalogs/ios-calls-v5.symbols.txt +70 -0
  265. package/test-suite/catalogs/ios-v5.json +1383 -0
  266. package/test-suite/catalogs/ios-v5.symbols.txt +699 -0
  267. package/test-suite/catalogs/rn-calls-v5.json +134 -0
  268. package/test-suite/catalogs/rn-sdk-v4.json +672 -0
  269. package/test-suite/catalogs/rn-v5.json +449 -0
  270. package/test-suite/catalogs/sdk-android-v5.json +514 -0
  271. package/test-suite/catalogs/sdk-android-v5.symbols.txt +254 -0
  272. package/test-suite/catalogs/sdk-jsv4.json +800 -0
  273. package/test-suite/catalogs/web-calls-v5.json +195 -0
  274. package/test-suite/catalogs/web-v7.json +1243 -0
  275. package/test-suite/scripts/build-registry.mjs +113 -0
  276. package/test-suite/scripts/catalog-refresh-dart.mjs +151 -0
  277. package/test-suite/scripts/catalog-refresh.mjs +100 -0
  278. package/test-suite/scripts/lint-skills.mjs +932 -0
  279. package/test-suite/scripts/measure-tokens.mjs +169 -0
  280. package/test-suite/scripts/typecheck-fences-dart.mjs +437 -0
  281. package/test-suite/scripts/typecheck-fences-kotlin.mjs +269 -0
  282. package/test-suite/scripts/typecheck-fences.mjs +206 -0
  283. package/test-suite/scripts/verify-catalog.mjs +242 -0
  284. package/test-suite/scripts/verify-contract-fences.mjs +94 -0
  285. package/test-suite/scripts/verify-detection.mjs +178 -0
  286. package/test-suite/scripts/verify-docs-base.mjs +91 -0
  287. package/test-suite/scripts/verify-family-agnostic.mjs +134 -0
  288. package/test-suite/scripts/verify-golden-path.mjs +138 -0
  289. package/test-suite/scripts/verify-oracle-components.mjs +133 -0
  290. package/test-suite/scripts/verify-template-bindings.mjs +148 -0
  291. package/test-suite/typecheck/angular-v5/package.json +21 -0
  292. package/test-suite/typecheck/angular-v5/tsconfig.json +21 -0
  293. package/test-suite/typecheck/flutter-v6/analysis_options.yaml +25 -0
  294. package/test-suite/typecheck/flutter-v6/pubspec.yaml +21 -0
  295. package/test-suite/typecheck/web-v7/package.json +18 -0
  296. package/test-suite/typecheck/web-v7/tsconfig.json +19 -0
  297. package/bin/install.js +0 -1003
  298. package/skills/.claude-plugin/marketplace.json +0 -86
  299. package/skills/cometchat/references/asking-questions.md +0 -48
  300. package/skills/cometchat-android-v5/SKILL.md +0 -121
  301. package/skills/cometchat-android-v5-calls/SKILL.md +0 -429
  302. package/skills/cometchat-android-v5-calls/references/README.md +0 -53
  303. package/skills/cometchat-android-v5-calls/references/add-calls-to-existing-chat.md +0 -197
  304. package/skills/cometchat-android-v5-calls/references/audio-controls.md +0 -91
  305. package/skills/cometchat-android-v5-calls/references/background-handling.md +0 -168
  306. package/skills/cometchat-android-v5-calls/references/call-layouts.md +0 -131
  307. package/skills/cometchat-android-v5-calls/references/call-logs.md +0 -113
  308. package/skills/cometchat-android-v5-calls/references/call-session.md +0 -274
  309. package/skills/cometchat-android-v5-calls/references/custom-ui.md +0 -150
  310. package/skills/cometchat-android-v5-calls/references/event-listeners.md +0 -123
  311. package/skills/cometchat-android-v5-calls/references/group-calls.md +0 -205
  312. package/skills/cometchat-android-v5-calls/references/idle-timeout.md +0 -117
  313. package/skills/cometchat-android-v5-calls/references/in-call-chat.md +0 -134
  314. package/skills/cometchat-android-v5-calls/references/join-session.md +0 -120
  315. package/skills/cometchat-android-v5-calls/references/migration-v4-to-v5.md +0 -222
  316. package/skills/cometchat-android-v5-calls/references/participant-management.md +0 -110
  317. package/skills/cometchat-android-v5-calls/references/picture-in-picture.md +0 -106
  318. package/skills/cometchat-android-v5-calls/references/raise-hand.md +0 -248
  319. package/skills/cometchat-android-v5-calls/references/recording.md +0 -101
  320. package/skills/cometchat-android-v5-calls/references/ringing-integration.md +0 -152
  321. package/skills/cometchat-android-v5-calls/references/screen-sharing.md +0 -78
  322. package/skills/cometchat-android-v5-calls/references/server-fcm-voip.md +0 -215
  323. package/skills/cometchat-android-v5-calls/references/session-settings.md +0 -121
  324. package/skills/cometchat-android-v5-calls/references/setup.md +0 -137
  325. package/skills/cometchat-android-v5-calls/references/share-invite.md +0 -156
  326. package/skills/cometchat-android-v5-calls/references/video-controls.md +0 -87
  327. package/skills/cometchat-android-v5-calls/references/voip-calling.md +0 -526
  328. package/skills/cometchat-android-v5-components/SKILL.md +0 -431
  329. package/skills/cometchat-android-v5-core/SKILL.md +0 -486
  330. package/skills/cometchat-android-v5-customization/SKILL.md +0 -469
  331. package/skills/cometchat-android-v5-extensions/SKILL.md +0 -127
  332. package/skills/cometchat-android-v5-features/SKILL.md +0 -131
  333. package/skills/cometchat-android-v5-placement/SKILL.md +0 -277
  334. package/skills/cometchat-android-v5-production/SKILL.md +0 -95
  335. package/skills/cometchat-android-v5-push/SKILL.md +0 -619
  336. package/skills/cometchat-android-v5-testing/SKILL.md +0 -188
  337. package/skills/cometchat-android-v5-theming/SKILL.md +0 -167
  338. package/skills/cometchat-android-v5-troubleshooting/SKILL.md +0 -74
  339. package/skills/cometchat-android-v6/SKILL.md +0 -108
  340. package/skills/cometchat-android-v6-calls/references/add-calls-to-existing-chat.md +0 -97
  341. package/skills/cometchat-android-v6-calls/references/advanced-features.md +0 -87
  342. package/skills/cometchat-android-v6-calls/references/call-layouts.md +0 -100
  343. package/skills/cometchat-android-v6-calls/references/call-session.md +0 -163
  344. package/skills/cometchat-android-v6-calls/references/device-management.md +0 -172
  345. package/skills/cometchat-android-v6-calls/references/group-calls.md +0 -191
  346. package/skills/cometchat-android-v6-calls/references/idle-timeout.md +0 -110
  347. package/skills/cometchat-android-v6-calls/references/in-call-chat.md +0 -141
  348. package/skills/cometchat-android-v6-calls/references/raise-hand.md +0 -188
  349. package/skills/cometchat-android-v6-calls/references/ringing-integration.md +0 -176
  350. package/skills/cometchat-android-v6-calls/references/server-fcm-voip.md +0 -127
  351. package/skills/cometchat-android-v6-calls/references/share-invite.md +0 -105
  352. package/skills/cometchat-android-v6-compose-theming/SKILL.md +0 -250
  353. package/skills/cometchat-android-v6-kotlin-theming/SKILL.md +0 -233
  354. package/skills/cometchat-angular-calls/SKILL.md +0 -380
  355. package/skills/cometchat-angular-calls/references/add-calls-to-existing-chat.md +0 -96
  356. package/skills/cometchat-angular-calls/references/call-layouts.md +0 -123
  357. package/skills/cometchat-angular-calls/references/call-session.md +0 -167
  358. package/skills/cometchat-angular-calls/references/custom-ui.md +0 -239
  359. package/skills/cometchat-angular-calls/references/device-management.md +0 -154
  360. package/skills/cometchat-angular-calls/references/group-calls.md +0 -433
  361. package/skills/cometchat-angular-calls/references/idle-timeout.md +0 -161
  362. package/skills/cometchat-angular-calls/references/in-call-chat.md +0 -148
  363. package/skills/cometchat-angular-calls/references/lazy-loading-pitfalls.md +0 -205
  364. package/skills/cometchat-angular-calls/references/migration-v4-to-v5.md +0 -53
  365. package/skills/cometchat-angular-calls/references/ngzone-and-async-callbacks.md +0 -184
  366. package/skills/cometchat-angular-calls/references/picture-in-picture.md +0 -285
  367. package/skills/cometchat-angular-calls/references/raise-hand.md +0 -172
  368. package/skills/cometchat-angular-calls/references/recording-screen-share.md +0 -260
  369. package/skills/cometchat-angular-calls/references/ringing-integration.md +0 -133
  370. package/skills/cometchat-angular-calls/references/server-web-push-vapid.md +0 -100
  371. package/skills/cometchat-angular-calls/references/share-invite.md +0 -122
  372. package/skills/cometchat-angular-calls/references/virtual-background.md +0 -29
  373. package/skills/cometchat-angular-components/SKILL.md +0 -235
  374. package/skills/cometchat-angular-core/SKILL.md +0 -386
  375. package/skills/cometchat-angular-customization/SKILL.md +0 -616
  376. package/skills/cometchat-angular-features/SKILL.md +0 -329
  377. package/skills/cometchat-angular-patterns/SKILL.md +0 -256
  378. package/skills/cometchat-angular-placement/SKILL.md +0 -314
  379. package/skills/cometchat-angular-production/SKILL.md +0 -564
  380. package/skills/cometchat-angular-push/SKILL.md +0 -389
  381. package/skills/cometchat-angular-testing/SKILL.md +0 -605
  382. package/skills/cometchat-angular-theming/SKILL.md +0 -290
  383. package/skills/cometchat-angular-troubleshooting/SKILL.md +0 -364
  384. package/skills/cometchat-astro-patterns/SKILL.md +0 -708
  385. package/skills/cometchat-calls/SKILL.md +0 -364
  386. package/skills/cometchat-calls/references/use-case-broadcast.md +0 -199
  387. package/skills/cometchat-calls/references/use-case-marketplace.md +0 -157
  388. package/skills/cometchat-calls/references/use-case-support.md +0 -189
  389. package/skills/cometchat-calls/references/use-case-team.md +0 -180
  390. package/skills/cometchat-calls/references/use-case-telehealth.md +0 -153
  391. package/skills/cometchat-components/SKILL.md +0 -1142
  392. package/skills/cometchat-core/SKILL.md +0 -1048
  393. package/skills/cometchat-customization/SKILL.md +0 -580
  394. package/skills/cometchat-customization/references/component-catalog.md +0 -236
  395. package/skills/cometchat-features/SKILL.md +0 -861
  396. package/skills/cometchat-flutter-v5/SKILL.md +0 -173
  397. package/skills/cometchat-flutter-v5-calls/SKILL.md +0 -455
  398. package/skills/cometchat-flutter-v5-calls/references/add-calls-to-existing-chat.md +0 -193
  399. package/skills/cometchat-flutter-v5-calls/references/call-layouts.md +0 -103
  400. package/skills/cometchat-flutter-v5-calls/references/call-session.md +0 -285
  401. package/skills/cometchat-flutter-v5-calls/references/device-management.md +0 -124
  402. package/skills/cometchat-flutter-v5-calls/references/group-calls.md +0 -181
  403. package/skills/cometchat-flutter-v5-calls/references/idle-timeout.md +0 -99
  404. package/skills/cometchat-flutter-v5-calls/references/in-call-chat.md +0 -135
  405. package/skills/cometchat-flutter-v5-calls/references/migration-v4-to-v5.md +0 -210
  406. package/skills/cometchat-flutter-v5-calls/references/raise-hand.md +0 -255
  407. package/skills/cometchat-flutter-v5-calls/references/ringing-integration.md +0 -196
  408. package/skills/cometchat-flutter-v5-calls/references/server-push-bridge.md +0 -101
  409. package/skills/cometchat-flutter-v5-calls/references/share-invite.md +0 -148
  410. package/skills/cometchat-flutter-v5-conversations/SKILL.md +0 -118
  411. package/skills/cometchat-flutter-v5-core/SKILL.md +0 -239
  412. package/skills/cometchat-flutter-v5-customization/SKILL.md +0 -358
  413. package/skills/cometchat-flutter-v5-events/SKILL.md +0 -156
  414. package/skills/cometchat-flutter-v5-messages/SKILL.md +0 -259
  415. package/skills/cometchat-flutter-v5-production/SKILL.md +0 -216
  416. package/skills/cometchat-flutter-v5-push/SKILL.md +0 -285
  417. package/skills/cometchat-flutter-v5-testing/SKILL.md +0 -401
  418. package/skills/cometchat-flutter-v5-theming/SKILL.md +0 -161
  419. package/skills/cometchat-flutter-v5-troubleshooting/SKILL.md +0 -130
  420. package/skills/cometchat-flutter-v5-users-groups/SKILL.md +0 -152
  421. package/skills/cometchat-flutter-v6/SKILL.md +0 -446
  422. package/skills/cometchat-flutter-v6-calls/references/add-calls-to-existing-chat.md +0 -136
  423. package/skills/cometchat-flutter-v6-calls/references/advanced-features.md +0 -89
  424. package/skills/cometchat-flutter-v6-calls/references/call-layouts.md +0 -105
  425. package/skills/cometchat-flutter-v6-calls/references/call-session.md +0 -229
  426. package/skills/cometchat-flutter-v6-calls/references/device-management.md +0 -105
  427. package/skills/cometchat-flutter-v6-calls/references/group-calls.md +0 -190
  428. package/skills/cometchat-flutter-v6-calls/references/idle-timeout.md +0 -88
  429. package/skills/cometchat-flutter-v6-calls/references/in-call-chat.md +0 -125
  430. package/skills/cometchat-flutter-v6-calls/references/raise-hand.md +0 -275
  431. package/skills/cometchat-flutter-v6-calls/references/ringing-integration.md +0 -171
  432. package/skills/cometchat-flutter-v6-calls/references/server-push-bridge.md +0 -93
  433. package/skills/cometchat-flutter-v6-calls/references/share-invite.md +0 -102
  434. package/skills/cometchat-flutter-v6-conversations/SKILL.md +0 -238
  435. package/skills/cometchat-flutter-v6-messages/SKILL.md +0 -384
  436. package/skills/cometchat-flutter-v6-theming/SKILL.md +0 -293
  437. package/skills/cometchat-flutter-v6-users-groups/SKILL.md +0 -224
  438. package/skills/cometchat-ios/SKILL.md +0 -325
  439. package/skills/cometchat-ios-calls/references/add-calls-to-existing-chat.md +0 -181
  440. package/skills/cometchat-ios-calls/references/audio-modes-and-controls.md +0 -45
  441. package/skills/cometchat-ios-calls/references/avaudiosession-routing.md +0 -188
  442. package/skills/cometchat-ios-calls/references/call-layouts.md +0 -76
  443. package/skills/cometchat-ios-calls/references/call-session.md +0 -218
  444. package/skills/cometchat-ios-calls/references/callkit-and-pushkit.md +0 -357
  445. package/skills/cometchat-ios-calls/references/custom-ui.md +0 -322
  446. package/skills/cometchat-ios-calls/references/device-management.md +0 -171
  447. package/skills/cometchat-ios-calls/references/group-calls.md +0 -359
  448. package/skills/cometchat-ios-calls/references/idle-timeout.md +0 -184
  449. package/skills/cometchat-ios-calls/references/in-call-chat.md +0 -179
  450. package/skills/cometchat-ios-calls/references/migration-v4-to-v5.md +0 -196
  451. package/skills/cometchat-ios-calls/references/picture-in-picture.md +0 -246
  452. package/skills/cometchat-ios-calls/references/raise-hand.md +0 -26
  453. package/skills/cometchat-ios-calls/references/recording-screen-share.md +0 -252
  454. package/skills/cometchat-ios-calls/references/ringing-integration.md +0 -143
  455. package/skills/cometchat-ios-calls/references/server-apns-pushkit.md +0 -267
  456. package/skills/cometchat-ios-calls/references/share-invite.md +0 -148
  457. package/skills/cometchat-ios-calls/references/swiftui-uikit-hosting.md +0 -246
  458. package/skills/cometchat-ios-calls/references/voip-cert-setup.md +0 -166
  459. package/skills/cometchat-ios-theming/SKILL.md +0 -667
  460. package/skills/cometchat-native-bare-patterns/SKILL.md +0 -550
  461. package/skills/cometchat-native-calls/SKILL.md +0 -671
  462. package/skills/cometchat-native-calls/references/add-calls-to-existing-chat.md +0 -203
  463. package/skills/cometchat-native-calls/references/call-layouts.md +0 -125
  464. package/skills/cometchat-native-calls/references/call-session.md +0 -182
  465. package/skills/cometchat-native-calls/references/custom-ui.md +0 -254
  466. package/skills/cometchat-native-calls/references/device-management.md +0 -158
  467. package/skills/cometchat-native-calls/references/expo-vs-bare.md +0 -179
  468. package/skills/cometchat-native-calls/references/group-calls.md +0 -296
  469. package/skills/cometchat-native-calls/references/idle-timeout.md +0 -128
  470. package/skills/cometchat-native-calls/references/in-call-chat.md +0 -145
  471. package/skills/cometchat-native-calls/references/migration-v4-to-v5.md +0 -72
  472. package/skills/cometchat-native-calls/references/picture-in-picture.md +0 -276
  473. package/skills/cometchat-native-calls/references/raise-hand.md +0 -205
  474. package/skills/cometchat-native-calls/references/recording-screen-share.md +0 -169
  475. package/skills/cometchat-native-calls/references/ringing-integration.md +0 -118
  476. package/skills/cometchat-native-calls/references/server-push-bridge.md +0 -135
  477. package/skills/cometchat-native-calls/references/server-push-payloads.md +0 -193
  478. package/skills/cometchat-native-calls/references/share-invite.md +0 -146
  479. package/skills/cometchat-native-calls/references/voip-push-end-to-end.md +0 -319
  480. package/skills/cometchat-native-components/SKILL.md +0 -628
  481. package/skills/cometchat-native-core/SKILL.md +0 -679
  482. package/skills/cometchat-native-customization/SKILL.md +0 -716
  483. package/skills/cometchat-native-expo-patterns/SKILL.md +0 -552
  484. package/skills/cometchat-native-features/SKILL.md +0 -638
  485. package/skills/cometchat-native-placement/SKILL.md +0 -509
  486. package/skills/cometchat-native-production/SKILL.md +0 -700
  487. package/skills/cometchat-native-push/SKILL.md +0 -594
  488. package/skills/cometchat-native-testing/SKILL.md +0 -582
  489. package/skills/cometchat-native-theming/SKILL.md +0 -681
  490. package/skills/cometchat-native-troubleshooting/SKILL.md +0 -501
  491. package/skills/cometchat-nextjs-patterns/SKILL.md +0 -899
  492. package/skills/cometchat-placement/SKILL.md +0 -1360
  493. package/skills/cometchat-production/SKILL.md +0 -1027
  494. package/skills/cometchat-react-calls/SKILL.md +0 -752
  495. package/skills/cometchat-react-calls/references/add-calls-to-existing-chat.md +0 -146
  496. package/skills/cometchat-react-calls/references/call-layouts.md +0 -160
  497. package/skills/cometchat-react-calls/references/call-session.md +0 -235
  498. package/skills/cometchat-react-calls/references/custom-ui.md +0 -247
  499. package/skills/cometchat-react-calls/references/device-management.md +0 -210
  500. package/skills/cometchat-react-calls/references/group-calls.md +0 -257
  501. package/skills/cometchat-react-calls/references/idle-timeout.md +0 -188
  502. package/skills/cometchat-react-calls/references/in-call-chat.md +0 -212
  503. package/skills/cometchat-react-calls/references/migration-v4-to-v5.md +0 -170
  504. package/skills/cometchat-react-calls/references/picture-in-picture.md +0 -245
  505. package/skills/cometchat-react-calls/references/raise-hand.md +0 -238
  506. package/skills/cometchat-react-calls/references/recording-screen-share.md +0 -144
  507. package/skills/cometchat-react-calls/references/ringing-integration.md +0 -255
  508. package/skills/cometchat-react-calls/references/server-web-push-vapid.md +0 -241
  509. package/skills/cometchat-react-calls/references/share-invite.md +0 -175
  510. package/skills/cometchat-react-calls/references/testing-calls-on-web.md +0 -216
  511. package/skills/cometchat-react-calls/references/virtual-background.md +0 -32
  512. package/skills/cometchat-react-calls/references/voip-and-web-push.md +0 -165
  513. package/skills/cometchat-react-patterns/SKILL.md +0 -628
  514. package/skills/cometchat-react-push/SKILL.md +0 -634
  515. package/skills/cometchat-react-router-patterns/SKILL.md +0 -758
  516. package/skills/cometchat-react-testing/SKILL.md +0 -413
  517. package/skills/cometchat-theming/SKILL.md +0 -498
  518. package/skills/cometchat-troubleshooting/SKILL.md +0 -294
@@ -0,0 +1,1280 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cometchat-skills — the deterministic "hands" layer of the pack.
4
+ *
5
+ * The v5 pivot kept the brain (skills/) and the materials (catalog/features/
6
+ * contracts/peers) but dropped the CLI the legacy pack had. This restores a
7
+ * small, dependency-free, DETERMINISTIC CLI so the steps an LLM should NOT
8
+ * improvise — detect the project, install skills, run the verification gates,
9
+ * refresh the oracle — are real commands with stable JSON contracts.
10
+ *
11
+ * npx @cometchat/skills <command> [options]
12
+ *
13
+ * Commands:
14
+ * detect Probe the CWD → framework / installed UI Kit / version_conflict (tools/detect.md contract)
15
+ * add [--ide <a>] Install THIS project's family skills for an agent (claude default; cursor/codex/copilot/… or `all`)
16
+ * list List the skills this pack ships (from peers.yaml + skills/)
17
+ * info <slug> Show one skill's triggers + compatibility
18
+ * verify [tier] Run the safety-net gates: lint | catalog | fences | predicate | all (default all offline)
19
+ * catalog [--check] Refresh the Tier-1 symbol catalog from the installed kit (oracle refresh)
20
+ * doctor Environment health check (node, kit install, catalog freshness, review state)
21
+ * version Print the pack version
22
+ * help This message
23
+ *
24
+ * Scope: every family in peers.yaml. Family-scoped paths/labels resolve from
25
+ * peers.yaml + families.json (RULES.md §22) — never a hardcoded family.
26
+ * Dependency-free (Node >=18 built-ins only). Every command supports --json.
27
+ */
28
+ import {
29
+ readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, cpSync, statSync, rmSync,
30
+ } from "node:fs";
31
+ import { join, dirname, resolve } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+ import { spawnSync } from "node:child_process";
34
+ import { homedir } from "node:os";
35
+
36
+ const PACK_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
37
+ const CWD = process.cwd();
38
+ const argv = process.argv.slice(2);
39
+ const CMD = argv[0];
40
+ const JSON_OUT = argv.includes("--json");
41
+ const has = (f) => argv.includes(f);
42
+ const flagVal = (f, d) => {
43
+ const i = argv.indexOf(f);
44
+ if (i >= 0) return argv[i + 1];
45
+ const eq = argv.find((a) => a.startsWith(f + "=")); // also accept --flag=value (e.g. --ide=cursor)
46
+ return eq ? eq.slice(f.length + 1) : d;
47
+ };
48
+ // A value-taking flag given WITHOUT a value — a bare trailing `--ide`, or `--ide --global` —
49
+ // used to fall through to the default and silently install for Claude. Refuse it instead.
50
+ function requireFlagValue(f, supported) {
51
+ const i = argv.indexOf(f);
52
+ if (i < 0) return;
53
+ const v = argv[i + 1];
54
+ if (v === undefined || v.startsWith("-")) {
55
+ console.error(`${f} needs a value\nsupported: ${supported.join(", ")}`);
56
+ process.exit(2);
57
+ }
58
+ }
59
+
60
+ function out(obj, human) {
61
+ if (JSON_OUT) console.log(JSON.stringify(obj, null, 0));
62
+ else human();
63
+ }
64
+ function readJSON(p) { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } }
65
+ // Resolve which family the CURRENT workspace belongs to, from families.json — the same single
66
+ // source the pack tools read (RULES.md §22). detect() already knows the framework; this maps it
67
+ // onto a family so no command has to name one.
68
+ // detect() -> the family THIS project actually uses. families.json is the dev
69
+ // oracle but is NOT packed (see package.json "files"), so an installed pack must
70
+ // resolve from peers.yaml, which ships. Hardcoding a family here is what put
71
+ // "React v7 UI Kit" atop every React Native AGENTS.md (AUDIT-101).
72
+ const FRAMEWORK_PLATFORM = {
73
+ "react-native": "react-native", expo: "react-native",
74
+ reactjs: "react", nextjs: "react", astro: "react", "react-router": "react",
75
+ angular: "angular",
76
+ // Native families detect() already recognises (pubspec / gradle / xcodeproj). Without
77
+ // these, `add` in a native iOS/Android/Flutter project resolved no family and exited 3
78
+ // with a false "no family" message even though detect() knew the framework.
79
+ flutter: "flutter", android: "android", ios: "ios",
80
+ };
81
+ // NO "nearest family" fallback. Defaulting to react is what titled an Angular
82
+ // project's router "React v7 UI Kit" and installed 9 React skills into it — the
83
+ // same silent-substitution defect as AUDIT-101, one framework over (AUDIT-105).
84
+ // An unresolved family is reported, never guessed. `--family <platform>` is the
85
+ // explicit escape hatch for a monorepo root, where detect() sees no framework.
86
+ function resolvePeer(fw) {
87
+ const live = (peers().peers || []).filter((p) => !p.parked && p.dirPrefix);
88
+ requireFlagValue("--family", live.map((p) => p.platform));
89
+ const forced = flagVal("--family");
90
+ if (forced) {
91
+ const hit = live.find((p) => p.platform === forced);
92
+ if (!hit) {
93
+ console.error(`unknown --family: ${forced}\nshippable: ${live.map((p) => p.platform).join(", ")}`);
94
+ process.exit(2);
95
+ }
96
+ return hit;
97
+ }
98
+ return live.find((p) => p.platform === FRAMEWORK_PLATFORM[fw])
99
+ || (live.length === 1 ? live[0] : null)
100
+ || null;
101
+ }
102
+ const FAMILIES_JSON = join(PACK_ROOT, ".claude/skills/cometchat-skill-reviewer/tools/families.json");
103
+ // The DEV-ONLY reviewer family for a peer (families.json is not packed → null on an npm install).
104
+ // Matched by CATALOG key — the one field both registries share. Matching by `platform` picked
105
+ // whichever family sorted first: an Angular project resolved to web-react-v7 (families.json's
106
+ // Angular platform is "web"), and React Native to rn-calls-v5 instead of rn-uikit-v5.
107
+ function reviewerFamily(peer) {
108
+ const F = readJSON(FAMILIES_JSON);
109
+ if (!peer || !F || !F.families) return null;
110
+ const key = (f) => (f.catalog || "").replace(/^catalog\//, "").replace(/\.json$/, "");
111
+ const hit = Object.entries(F.families)
112
+ .find(([, f]) => !/^template\b/i.test(f._status || "") && key(f) === peer.catalog);
113
+ if (!hit) return null;
114
+ const [name, f] = hit;
115
+ return { ...f, name, key: key(f), typecheckProject: f.typecheckProject || `test-suite/typecheck/${key(f)}` };
116
+ }
117
+ function resolveFamily() {
118
+ const det = detect() || {};
119
+ const fw = det.framework || "";
120
+ const peer = resolvePeer(fw);
121
+ if (!peer) failUnresolvedFamily(fw, det);
122
+ const base = { label: peer.label, dirPrefix: peer.dirPrefix, kit_package: peer.kit_package, major: peer.major };
123
+ const fam = reviewerFamily(peer);
124
+ if (fam) return { ...fam, ...base };
125
+ // ponytail: packed install — peers.yaml is the only shipped registry.
126
+ return { platform: peer.platform, key: null, typecheckProject: null, ...base };
127
+ }
128
+
129
+ function failUnresolvedFamily(fw, det = {}) {
130
+ {
131
+ const live = (peers().peers || []).filter((x) => !x.parked && x.dirPrefix);
132
+ console.error(
133
+ `cannot resolve a CometChat family for this project.\n` +
134
+ ` detected framework: ${fw || "(none)"}\n` +
135
+ (det.hint ? ` hint: ${det.hint}\n` : "") +
136
+ ` shippable families: ${live.map((x) => `${x.platform} (${x.label})`).join(", ") || "(none)"}\n\n` +
137
+ (fw
138
+ ? `There is no CometChat skills family for "${fw}" in this pack. Installing the nearest one\n` +
139
+ `would hand you another framework's APIs — so nothing was written.\n`
140
+ : `No framework was detected (a monorepo root looks like this). Run this inside the app\n` +
141
+ `package, or name the family explicitly.\n`) +
142
+ `\n npx @cometchat/skills add --ide <agent> --family <${live.map((x) => x.platform).join("|")}>`
143
+ );
144
+ process.exit(3);
145
+ }
146
+ }
147
+
148
+ function packVersion() { return (readJSON(join(PACK_ROOT, "package.json")) || {}).version || "0.0.0"; }
149
+
150
+ // ── detect — tools/detect.md contract (deterministic, offline) ───────────────
151
+ const TARGET_MAJOR = 7; // react UI Kit major these skills target (fallback when peers.yaml is absent)
152
+
153
+ // The UI Kit major EACH family targets, from peers.yaml `kit_package: "<npm-name>@<major>"`
154
+ // (react → 7, angular → 5, react-native → 5). "Not v7" is only a conflict for React: Angular v5
155
+ // and RN v5 are the CORRECT majors for their own families.
156
+ function jsKitTargets() {
157
+ const t = {};
158
+ for (const p of peers().peers || []) {
159
+ const m = /^(@cometchat\/chat-uikit-[a-z-]+)@(\d+)$/.exec(p.kit_package || "");
160
+ if (m && !p.parked) t[m[1]] = parseInt(m[2], 10);
161
+ }
162
+ if (!Object.keys(t).length) t["@cometchat/chat-uikit-react"] = TARGET_MAJOR;
163
+ return t;
164
+ }
165
+ const FRAMEWORK_KIT = { angular: "@cometchat/chat-uikit-angular", "react-native": "@cometchat/chat-uikit-react-native", expo: "@cometchat/chat-uikit-react-native" };
166
+
167
+ function ccVersion(pkg, dep) {
168
+ const v = (pkg.dependencies && pkg.dependencies[dep]) || (pkg.devDependencies && pkg.devDependencies[dep]);
169
+ if (!v) return null;
170
+ const m = v.match(/(\d+)\.(\d+)\.(\d+)/) || v.match(/(\d+)/);
171
+ return { range: v, major: m ? parseInt(m[1], 10) : null };
172
+ }
173
+
174
+ // A REAL product package -- kit/sdk/calls, any peer, from peers.yaml -- never
175
+ // the AI-tooling package itself. The old check used "any @cometchat/* dependency
176
+ // or a .cometchat/ dir", and BOTH halves were tool-installation signals, not
177
+ // integration signals: @cometchat/skills always matches "@cometchat/*" (every
178
+ // project that can run this CLI has it installed), and .cometchat/skills is
179
+ // literally NEUTRAL_SKILLS_DIR -- the tool's OWN install target, created by any
180
+ // --ide antigravity/cursor/codex/... install regardless of whether the product
181
+ // was ever integrated. So existing_cometchat had read true from the moment the
182
+ // TOOL was installed, on every project, forever -- which matters because it is
183
+ // the load-bearing signal for the dispatcher's "already integrated, skip
184
+ // onboarding" branch (AUDIT-108).
185
+ function productPackageNames() {
186
+ const y = existsSync(join(PACK_ROOT, "peers.yaml")) ? readFileSync(join(PACK_ROOT, "peers.yaml"), "utf8") : "";
187
+ const raw = [...y.matchAll(/(?:kit|sdk|calls)_package:\s*"([^"]+)"/g)].map((m) => m[1]);
188
+ return new Set(raw.map((pkg) => pkg.replace(/@[^@/]+$/, ""))); // strip trailing @<major>, keep the leading @scope
189
+ }
190
+
191
+ function detect() {
192
+ const pkgPath = join(CWD, "package.json");
193
+ const result = {
194
+ framework: null, flutter_version: null, android_variant: null,
195
+ installed_uikit: null, version_conflict: null, env_file: null, existing_cometchat: false,
196
+ // CometChat-relevant repo signals (for cometchat-onboarding). Additive: null when
197
+ // the evidence is absent (graceful degradation) — existing consumers ignore them.
198
+ auth: null, styling: null, state: null, routing: null, ssr: null,
199
+ };
200
+
201
+ // JS ecosystem
202
+ if (existsSync(pkgPath)) {
203
+ const pkg = readJSON(pkgPath) || {};
204
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
205
+ const hasDep = (d) => Object.prototype.hasOwnProperty.call(deps, d);
206
+ const hasAnyDep = (...ds) => ds.some(hasDep);
207
+ // some deps are namespaced (e.g. @clerk/*, @react-router/*) — match by prefix too.
208
+ const depKeys = Object.keys(deps);
209
+ const hasDepPrefix = (p) => depKeys.some((d) => d === p || d.startsWith(p));
210
+ const fileExists = (rel) => existsSync(join(CWD, rel));
211
+ const globish = (names) => names.some(fileExists);
212
+
213
+ let fwDep = null; // the dependency that decided the framework
214
+ if (hasDep("@angular/core")) { result.framework = "angular"; fwDep = "@angular/core"; }
215
+ else if (hasDep("expo")) { result.framework = "expo"; result.env_file = ".env"; fwDep = "expo"; }
216
+ else if (hasDep("react-native")) { result.framework = "react-native"; result.env_file = ".env"; fwDep = "react-native"; }
217
+ else if (hasDep("astro")) { result.framework = "astro"; result.env_file = ".env"; fwDep = "astro"; }
218
+ else if (hasDep("next")) { result.framework = "nextjs"; result.env_file = ".env.local"; fwDep = "next"; }
219
+ else if (hasDep("react-router-dom") || hasDep("@react-router/dev")) { result.framework = "react-router"; result.env_file = ".env"; fwDep = hasDep("react-router-dom") ? "react-router-dom" : "@react-router/dev"; }
220
+ else if (hasDep("react")) { result.framework = "reactjs"; result.env_file = ".env"; fwDep = "react"; }
221
+
222
+ if (result.framework === "angular") result.env_file = "src/environments/environment.ts";
223
+
224
+ // A WORKSPACE ROOT (package.json `workspaces`, or pnpm-workspace.yaml) whose framework dep is
225
+ // only a devDependency is tooling (storybook, lint, test config) — not an app. Auto-picking
226
+ // React there installed the React skills at a monorepo root. Report it; the user runs inside
227
+ // the app package or names the family. A root that lists the dep in `dependencies` IS an app.
228
+ const wsRoot = !!pkg.workspaces || fileExists("pnpm-workspace.yaml");
229
+ const inProdDeps = fwDep && pkg.dependencies && Object.prototype.hasOwnProperty.call(pkg.dependencies, fwDep);
230
+ if (result.framework && wsRoot && !inProdDeps) {
231
+ result.workspace_root = true;
232
+ result.hint = `workspace root (${pkg.workspaces ? 'package.json "workspaces"' : "pnpm-workspace.yaml"}) with ` +
233
+ `${fwDep} only in devDependencies — that is tooling, not an app. Run inside the app package ` +
234
+ `(e.g. apps/web), or pass --family <platform>.`;
235
+ result.framework = null; result.env_file = null;
236
+ }
237
+
238
+ // ── CometChat-relevant repo signals (deterministic, offline, graceful) ─────
239
+ // auth
240
+ if (hasDepPrefix("@auth/") || hasDep("next-auth")) result.auth = "nextauth";
241
+ else if (hasDepPrefix("@clerk/")) result.auth = "clerk";
242
+ else if (hasDep("firebase") || hasDepPrefix("@firebase/")) result.auth = "firebase";
243
+ else if (hasDep("auth0") || hasDepPrefix("@auth0/")) result.auth = "auth0";
244
+ else if (hasDep("@supabase/supabase-js") || hasDepPrefix("@supabase/")) result.auth = "supabase";
245
+ // else null — custom auth isn't inferable from deps alone.
246
+
247
+ // styling / design system
248
+ if (hasDep("tailwindcss") || globish(["tailwind.config.js", "tailwind.config.ts", "tailwind.config.cjs", "tailwind.config.mjs"])) result.styling = "tailwind";
249
+ else if (hasDepPrefix("@mui/")) result.styling = "mui";
250
+ else if (hasDep("styled-components")) result.styling = "styled-components";
251
+ else if (hasDepPrefix("@chakra-ui/")) result.styling = "chakra";
252
+ // css-modules is a convention, not a dep — leave to onboarding's repo read; null here.
253
+
254
+ // state management
255
+ if (hasDep("@reduxjs/toolkit") || hasDep("react-redux") || hasDep("redux")) result.state = "redux";
256
+ else if (hasDep("zustand")) result.state = "zustand";
257
+ else if (hasDep("mobx") || hasDepPrefix("mobx-react")) result.state = "mobx";
258
+ else if (hasDep("recoil")) result.state = "recoil";
259
+ else if (result.framework && /^(reactjs|nextjs|react-router|expo|react-native)$/.test(result.framework)) result.state = "context"; // React's built-in default
260
+ // else null (e.g. angular has its own DI; leave null unless a lib is present)
261
+
262
+ // routing
263
+ if (result.framework === "nextjs") result.routing = fileExists("app") || fileExists("src/app") ? "next-app-router" : (fileExists("pages") || fileExists("src/pages") ? "next-pages-router" : "next-app-router");
264
+ else if (hasDep("react-router-dom") || hasDepPrefix("@react-router/")) result.routing = "react-router";
265
+ else if (hasDepPrefix("@tanstack/react-router")) result.routing = "tanstack-router";
266
+ else if (hasDep("vue-router")) result.routing = "vue-router";
267
+ else if (hasDep("expo-router")) result.routing = "expo-router";
268
+ else if (hasDep("@angular/router") || result.framework === "angular") result.routing = "angular-router";
269
+ // else null
270
+
271
+ // SSR/CSR boundary
272
+ if (hasDep("next") || hasDep("nuxt") || hasDep("@remix-run/react") || hasDepPrefix("@sveltejs/kit")) result.ssr = "ssr";
273
+ else if (hasDep("astro")) result.ssr = "islands";
274
+ else if (result.framework && /^(reactjs|react-router|expo|react-native)$/.test(result.framework)) result.ssr = "csr";
275
+ // else null
276
+
277
+ if (result.workspace_root) { result.auth = result.styling = result.state = result.routing = result.ssr = null; }
278
+
279
+ // installed UI Kit — each JS family's kit, checked against THAT family's target major.
280
+ const kits = [];
281
+ for (const [name, target] of Object.entries(jsKitTargets())) {
282
+ const v = ccVersion(pkg, name);
283
+ if (!v) continue;
284
+ // prefer the actually-installed version if node_modules is present
285
+ const inst = readJSON(join(CWD, "node_modules", name, "package.json"));
286
+ const version = inst && inst.version ? inst.version : v.range;
287
+ const major = inst && inst.version ? parseInt(inst.version.split(".")[0], 10) : v.major;
288
+ kits.push({ name, target, version, major });
289
+ }
290
+ if (kits.length) {
291
+ const k = kits.find((x) => x.name === (FRAMEWORK_KIT[result.framework] || "@cometchat/chat-uikit-react")) || kits[0];
292
+ result.installed_uikit = { package: k.name, version: k.version };
293
+ if (k.major && k.major !== k.target) {
294
+ result.version_conflict = { detail: `project has ${k.name}@${k.major} but these skills target v${k.target}` };
295
+ }
296
+ // two different UI Kits (e.g. react + angular) declared at different majors
297
+ if (kits.length > 1 && new Set(kits.map((x) => x.major)).size > 1) {
298
+ const short = (x) => x.name.replace("@cometchat/chat-uikit-", "");
299
+ result.version_conflict = { detail: `two UI Kit majors declared (${kits.map((x) => `${short(x)} v${x.major}`).join(" + ")}) — pick one cohort` };
300
+ }
301
+ }
302
+ // A REAL product package as a project dependency is the only reliable "already
303
+ // integrated" signal. Do NOT also treat a `.cometchat/` dir as proof: it is this
304
+ // TOOL's own install target (NEUTRAL_SKILLS_DIR), created on every `add`, so it made
305
+ // every project read as integrated the moment the tooling was installed — the other
306
+ // half of the AUDIT-108 false positive (the dependency half was already narrowed to
307
+ // productPackageNames; this removes the dir half).
308
+ result.existing_cometchat =
309
+ Object.keys(deps).some((d) => productPackageNames().has(d));
310
+ }
311
+
312
+ // Non-JS scaffolds (light-touch — enough to fire version_conflict / routing)
313
+ if (!result.framework && existsSync(join(CWD, "pubspec.yaml"))) {
314
+ result.framework = "flutter";
315
+ const pub = readFileSync(join(CWD, "pubspec.yaml"), "utf8");
316
+ if (/cometchat_chat_uikit/.test(pub)) {
317
+ result.existing_cometchat = true;
318
+ const m = pub.match(/cometchat_chat_uikit:\s*["']?\^?(\d+)/);
319
+ if (m) result.flutter_version = parseInt(m[1], 10) >= 6 ? "v6" : "v5";
320
+ // installed_uikit was never populated on this branch, and version_conflict is
321
+ // derived from it — so a Flutter project sitting on the wrong major could never
322
+ // raise a conflict at all. Prefer the RESOLVED version in pubspec.lock over the
323
+ // declared range: "^6.1.0" says what is allowed, the lock says what is installed.
324
+ const declared = pub.match(/cometchat_chat_uikit:\s*["']?([^"'\s]+)/);
325
+ let version = declared ? declared[1] : null;
326
+ const lockPath = join(CWD, "pubspec.lock");
327
+ if (existsSync(lockPath)) {
328
+ const lock = readFileSync(lockPath, "utf8");
329
+ const lm = lock.match(/^\s{2}cometchat_chat_uikit:[\s\S]*?^\s{4}version:\s*["']?([\d.]+)/m);
330
+ if (lm) version = lm[1];
331
+ }
332
+ result.installed_uikit = { package: "cometchat_chat_uikit", version };
333
+ const major = version ? parseInt(String(version).replace(/^\D+/, ""), 10) : null;
334
+ if (major && major !== 6) {
335
+ result.version_conflict = {
336
+ detail: `project has cometchat_chat_uikit@${major} but these skills target v6`,
337
+ };
338
+ }
339
+ }
340
+ // The Flutter kit reads its credentials from a registered settings ASSET, not a dotenv
341
+ // file (commit a placeholder — a registered asset must exist for a fresh clone to build;
342
+ // cometchat-flutter-v6-core). Same reason the Android branch below names one. Leaving this
343
+ // null pointed the user at nothing on the one step that must precede any render.
344
+ result.env_file = "cometchat-settings.json";
345
+ }
346
+ // Android. BOTH DSLs — Groovy (.gradle) and Kotlin (.gradle.kts). Checking only the Groovy
347
+ // names missed every Kotlin-DSL project, which is the majority AND what this pack's own install
348
+ // block prescribes: `framework` came back null and the router could not route to Android at all.
349
+ const GRADLE_MARKERS = ["settings.gradle", "settings.gradle.kts", "app/build.gradle",
350
+ "app/build.gradle.kts", "build.gradle", "build.gradle.kts"];
351
+ if (!result.framework && GRADLE_MARKERS.some((f) => existsSync(join(CWD, f)))) {
352
+ result.framework = "android";
353
+ // Android credentials are NOT a dotenv file — they are the gitignored settings asset that
354
+ // `CometChatUIKit.initFromSettings` reads. Reporting "(n/a)" left the user with no pointer at
355
+ // all, on the one setup step that must happen before anything renders.
356
+ result.env_file = "app/src/main/assets/cometchat-settings.json";
357
+ const gradle = ["app/build.gradle", "app/build.gradle.kts", "build.gradle", "build.gradle.kts",
358
+ "gradle/libs.versions.toml"]
359
+ .map((f) => join(CWD, f)).filter(existsSync).map((f) => readFileSync(f, "utf8")).join("\n");
360
+
361
+ // Cohort. A bare /compose/ test matched "composer", the kit's own artifact name, and comments.
362
+ // Score STRUCTURAL signals instead, and report `mixed`/`unknown` honestly rather than guessing —
363
+ // emitting the wrong cohort is unrecoverable for the consumer.
364
+ const composeHits = [
365
+ /buildFeatures\s*\{[^}]*\bcompose\s*[= ]\s*true/s, // buildFeatures { compose = true }
366
+ /org\.jetbrains\.kotlin\.plugin\.compose/, // the compose compiler plugin
367
+ /androidx\.compose[.:]/, // androidx.compose.* dependency
368
+ /compose-bom/,
369
+ ].filter((re) => re.test(gradle)).length;
370
+ const viewsHits = [
371
+ /buildFeatures\s*\{[^}]*viewBinding\s*[= ]\s*true/s,
372
+ /buildFeatures\s*\{[^}]*dataBinding\s*[= ]\s*true/s,
373
+ /com\.android\.databinding/,
374
+ ].filter((re) => re.test(gradle)).length
375
+ + (existsSync(join(CWD, "app/src/main/res/layout")) ? 1 : 0); // XML layouts on disk
376
+
377
+ result.android_variant =
378
+ composeHits && !viewsHits ? "compose" :
379
+ viewsHits && !composeHits ? "views" :
380
+ composeHits && viewsHits ? "mixed" : "unknown";
381
+ result.android_cohort_signals = { composeHits, viewsHits };
382
+
383
+ // Existing CometChat dependency + major — drives version_conflict and the upgrade offer.
384
+ const kit = gradle.match(/com\.cometchat:(chatuikit-(?:kotlin|compose|core)-android|chat-uikit-android)[:"'\s]*([0-9]+)?/);
385
+ if (kit) {
386
+ result.existing_cometchat = true;
387
+ result.installed_uikit = kit[1];
388
+ const major = kit[2] ? parseInt(kit[2], 10) : null;
389
+ if (major) result.android_uikit_major = `v${major}`;
390
+ if (major && major < 6) {
391
+ result.version_conflict = { detail: `android UI Kit v${major} installed; this pack targets v6` };
392
+ }
393
+ }
394
+ }
395
+ // iOS. peers.yaml lists Package.swift / *.xcodeproj / Podfile / *.xcworkspace; checking only the
396
+ // Podfile + .xcodeproj left an SPM-only app and an .xcworkspace-only checkout detecting as nothing.
397
+ // A root Package.swift is AMBIGUOUS on its own — a Swift server/CLI package has one too — so it
398
+ // counts only with an iOS signal: an `.iOS(` platform, or a CometChat iOS product dependency.
399
+ if (!result.framework) {
400
+ const entries = readdirSync(CWD);
401
+ const read = (rel) => { try { return readFileSync(join(CWD, rel), "utf8"); } catch { return ""; } };
402
+ const xcode = entries.filter((f) => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"));
403
+ const spm = existsSync(join(CWD, "Package.swift")) ? read("Package.swift") : null;
404
+ const IOS_CC = /CometChatUIKitSwift|CometChatCallsSDK|CometChatSDK|cometchat-uikit-ios|chat-sdk-ios|calls-sdk-ios/;
405
+ const spmIsIos = spm !== null && (/\.iOS\s*\(/.test(spm) || IOS_CC.test(spm));
406
+ if (existsSync(join(CWD, "Podfile")) || xcode.length || spmIsIos) {
407
+ result.framework = "ios";
408
+ result.env_file = "Secrets.xcconfig";
409
+ // Existing integration: a CometChat iOS product in the Podfile, Package.swift, the Xcode
410
+ // project, or a resolved SPM pin.
411
+ const evidence = [read("Podfile"), spm || "", read("Package.resolved"),
412
+ ...xcode.flatMap((x) => [read(`${x}/project.pbxproj`),
413
+ read(`${x}/xcshareddata/swiftpm/Package.resolved`),
414
+ read(`${x}/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`)])].join("\n");
415
+ if (IOS_CC.test(evidence)) {
416
+ result.existing_cometchat = true;
417
+ // The kit id comes from peers.yaml (RULES §22 — no hardcoded family keys in tools).
418
+ const iosKit = ((peers().peers || []).find((p) => p.platform === "ios") || {}).kit_package;
419
+ if (iosKit && (evidence.includes(iosKit) || /cometchat-uikit-ios/.test(evidence))) result.installed_uikit = iosKit;
420
+ }
421
+ } else if (spm !== null) {
422
+ result.ambiguous = "ios";
423
+ result.hint = "Package.swift found, but no iOS signal (no .iOS(...) platform, no CometChat iOS dependency) — " +
424
+ "this looks like a Swift server/CLI package. If it IS an iOS app, pass --family ios.";
425
+ }
426
+ }
427
+ // Competitor SDKs (Sendbird / Stream) → the cross-family migration skill that replaces them.
428
+ // null when none — additive, so existing consumers of detect --json are unaffected.
429
+ result.migrate_from = detectMigrations();
430
+ return result;
431
+ }
432
+
433
+ // ── registry (derived from peers.yaml) ───────────────────────────────────────
434
+ function peers() {
435
+ const p = join(PACK_ROOT, "peers.yaml");
436
+ if (!existsSync(p)) return { entry: "cometchat", peers: [] };
437
+ const y = readFileSync(p, "utf8");
438
+ const entry = (y.match(/^entry:\s*(\S+)/m) || [])[1] || "cometchat";
439
+ const list = [];
440
+ for (const block of y.split(/^\s*-\s*platform:/m).slice(1)) {
441
+ const platform = (block.match(/^\s*(\S+)/) || [])[1];
442
+ const major = (block.match(/major:\s*(\S+)/) || [])[1];
443
+ const kit = (block.match(/kit_package:\s*"([^"]+)"/) || [])[1];
444
+ const skills = (block.match(/skills:\s*\[([^\]]*)\]/) || [])[1];
445
+ const verified = /^\s*verified:\s*true\b/m.test(block); // line-anchored: don't match "verified:true" inside a trailing comment on a `verified: false` line
446
+ const label = (block.match(/label:\s*"([^"]+)"/) || [])[1];
447
+ const dirPrefix = (block.match(/dir_prefix:\s*"([^"]+)"/) || [])[1];
448
+ const parked = /^\s*parked:\s*true\b/m.test(block); // line-anchored, like `verified`
449
+ list.push({ platform, major, kit_package: kit, label, dirPrefix, parked, skills: skills ? skills.split(",").map((s) => s.trim()) : [], verified });
450
+ }
451
+ // `catalog` + `env_file` make doctor/detect family-aware instead of hardcoding one family.
452
+ // Enriched in a SECOND pass, deliberately: the loop body above is shared with the React Native
453
+ // branch (PR #170), and editing it there turns a clean 3-way merge into a hand-resolved conflict.
454
+ y.split(/^\s*-\s*platform:/m).slice(1).forEach((block, i) => {
455
+ if (!list[i]) return;
456
+ list[i].catalog = (block.match(/catalog:\s*(\S+)/) || [])[1];
457
+ list[i].env_file = (block.match(/env_file:\s*"([^"]+)"/) || [])[1];
458
+ list[i].features = (block.match(/features:\s*(\S+\.json)/) || [])[1];
459
+ list[i].contracts = (block.match(/contracts:\s*(\S+\.json)/) || [])[1];
460
+ });
461
+ const gate = (y.match(/^gate:\s*(\S+)/m) || [])[1] || "cometchat-onboarding";
462
+ const entSec = (y.match(/^enterprise:\s*\n([\s\S]*?)(?=^\S)/m) || [])[1] || "";
463
+ const enterprise = [...entSec.matchAll(/^\s*-\s*(\S+)/gm)].map((m) => m[1].trim());
464
+ const cfSec = (y.match(/^crossFamily:\s*\n([\s\S]*?)(?=^\S)/m) || [])[1] || "";
465
+ const crossFamily = [...cfSec.matchAll(/^\s*-\s*(\S+)/gm)].map((m) => m[1].trim());
466
+ return { entry, gate, peers: list, migrations: migrationsFrom(y), enterprise, crossFamily };
467
+ }
468
+
469
+ // `migrations:` (peers.yaml, above `peers:`): one cross-family competitor-migration skill per
470
+ // vendor + the dependency patterns `detect` reports as `migrate_from`. Patterns are single-quoted
471
+ // YAML scalars, so backslashes arrive literally and go straight into RegExp.
472
+ function migrationsFrom(y) {
473
+ const sec = (y.match(/^migrations:\s*\n([\s\S]*?)(?=^\S)/m) || [])[1] || "";
474
+ return sec.split(/^\s*-\s*vendor:/m).slice(1).map((block) => {
475
+ const vendor = (block.match(/^\s*(\S+)/) || [])[1];
476
+ const q = (k) => (block.match(new RegExp(`^\\s*${k}:\\s*'([^']*)'`, "m")) || [])[1] || null;
477
+ const label = (block.match(/label:\s*"([^"]+)"/) || [])[1] || vendor;
478
+ const skill = (block.match(/skill:\s*(\S+)/) || [])[1];
479
+ return { vendor, label, skill, npm: q("npm"), pub: q("pub"), gradle: q("gradle"), ios: q("ios") };
480
+ }).filter((m) => m.vendor && m.skill);
481
+ }
482
+
483
+ // Platform-NEUTRAL skills: installed for every family (entry router, onboarding gate, and the
484
+ // competitor-migration skills). One list, read from peers.yaml, so `add`, the orienting router
485
+ // and the family filter never disagree about what is shared.
486
+ function neutralSkillDirs() {
487
+ const p = peers();
488
+ return new Set([p.entry || "cometchat", p.gate, ...(p.migrations || []).map((m) => m.skill), ...(p.enterprise || []), ...(p.crossFamily || [])].filter(Boolean));
489
+ }
490
+
491
+ // Workspace package dirs for a monorepo: npm/yarn `workspaces` + pnpm-workspace.yaml. A monorepo
492
+ // keeps the vendor dependency in apps/<x>/package.json, not the root. Globs are scanned one level
493
+ // only (`apps/*`; `apps/**` is treated the same, so a package nested deeper isn't found); `!`
494
+ // exclusions are honored; the result is capped.
495
+ function workspaceDirs() {
496
+ const pkg = readJSON(join(CWD, "package.json")) || {};
497
+ const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : (pkg.workspaces && pkg.workspaces.packages) || [];
498
+ let pnpm = "";
499
+ try { pnpm = readFileSync(join(CWD, "pnpm-workspace.yaml"), "utf8"); } catch { /* none */ }
500
+ const patterns = [...ws, ...[...pnpm.matchAll(/^\s*-\s*['"]?([^'"\n#]+?)['"]?\s*$/gm)].map((m) => m[1])];
501
+ const excluded = patterns.filter((g) => g.startsWith("!")).map((g) => g.slice(1).replace(/\/\*{1,2}$/, ""));
502
+ const dirs = [];
503
+ for (const g of patterns) {
504
+ if (g.startsWith("!")) continue;
505
+ const m = g.match(/^(.*?)\/\*{1,2}$/);
506
+ if (!m) { dirs.push(g); continue; }
507
+ try {
508
+ for (const d of readdirSync(join(CWD, m[1]), { withFileTypes: true }))
509
+ if (d.isDirectory() && d.name !== "node_modules") dirs.push(`${m[1]}/${d.name}`);
510
+ } catch { /* missing dir */ }
511
+ }
512
+ const kept = dirs.filter((d) => !excluded.some((x) => d === x || d.startsWith(`${x}/`)));
513
+ return [...new Set(kept)].slice(0, 50);
514
+ }
515
+
516
+ // Competitor SDKs present in THIS project (→ the migration skill to run). Reads dependency
517
+ // manifests only — names, not code — so it stays offline and cheap, like the rest of detect.
518
+ function detectMigrations() {
519
+ const read = (rel) => { try { return readFileSync(join(CWD, rel), "utf8"); } catch { return ""; } };
520
+ const depsOf = (p) => Object.keys({ ...((p && p.dependencies) || {}), ...((p && p.devDependencies) || {}) });
521
+ // Root package.json + every workspace package (monorepo): the vendor dep may live in apps/<x>.
522
+ const npmSources = [["package.json", readJSON(join(CWD, "package.json"))],
523
+ ...workspaceDirs().map((d) => [`${d}/package.json`, readJSON(join(CWD, d, "package.json"))])]
524
+ .map(([where, p]) => [where, depsOf(p)]);
525
+ const pub = read("pubspec.yaml");
526
+ const pubDeps = [...pub.matchAll(/^\s+([a-z0-9_]+):/gm)].map((m) => m[1]);
527
+ const gradle = ["app/build.gradle", "app/build.gradle.kts", "build.gradle", "build.gradle.kts",
528
+ "settings.gradle", "settings.gradle.kts", "gradle/libs.versions.toml"].map(read).join("\n");
529
+ let entries = [];
530
+ try { entries = readdirSync(CWD); } catch { /* unreadable cwd */ }
531
+ const xcode = entries.filter((f) => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"));
532
+ const ios = [read("Podfile"), read("Package.swift"), read("Package.resolved"),
533
+ ...xcode.flatMap((x) => [read(`${x}/project.pbxproj`),
534
+ read(`${x}/project.xcworkspace/xcshareddata/swiftpm/Package.resolved`),
535
+ read(`${x}/xcshareddata/swiftpm/Package.resolved`)])].join("\n");
536
+ const found = [];
537
+ for (const m of peers().migrations || []) {
538
+ const evidence = new Set();
539
+ const names = (pattern, list, where) => {
540
+ if (!pattern) return;
541
+ const re = new RegExp(pattern, "i");
542
+ for (const d of list) if (re.test(d)) evidence.add(`${where}:${d}`);
543
+ };
544
+ const text = (pattern, src, where) => {
545
+ if (!pattern || !src) return;
546
+ for (const hit of src.matchAll(new RegExp(pattern, "gi"))) evidence.add(`${where}:${hit[0].toLowerCase()}`);
547
+ };
548
+ for (const [where, deps] of npmSources) names(m.npm, deps, where);
549
+ names(m.pub, pubDeps, "pubspec.yaml");
550
+ text(m.gradle, gradle, "gradle");
551
+ text(m.ios, ios, "ios");
552
+ if (evidence.size) found.push({ vendor: m.vendor, label: m.label, skill: m.skill, evidence: [...evidence].slice(0, 8) });
553
+ }
554
+ return found.length ? found : null;
555
+ }
556
+
557
+ function shippedSkillDirs() {
558
+ const dir = join(PACK_ROOT, "skills");
559
+ if (!existsSync(dir)) return [];
560
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
561
+ }
562
+
563
+ // ── commands ──────────────────────────────────────────────────────────────────
564
+ function cmdDetect() {
565
+ const r = detect();
566
+ out(r, () => {
567
+ console.log(`framework: ${r.framework || "(none detected)"}`);
568
+ // The react branch sets `{package, version}`; the android branch sets a bare artifact STRING
569
+ // (there is no lockfile to read a version from), so the object form printed
570
+ // "undefined@undefined" on every Android project that already had the kit.
571
+ if (r.installed_uikit) {
572
+ const u = r.installed_uikit;
573
+ console.log(`installed UI Kit: ${typeof u === "string" ? u : `${u.package}@${u.version}`}`);
574
+ }
575
+ console.log(`env file: ${r.env_file || "(n/a)"}`);
576
+ console.log(`existing CometChat: ${r.existing_cometchat}`);
577
+ // repo signals for onboarding (only print the ones we found)
578
+ const sig = [["auth", r.auth], ["styling", r.styling], ["state", r.state], ["routing", r.routing], ["ssr", r.ssr]].filter(([, v]) => v);
579
+ if (sig.length) console.log(`repo signals: ${sig.map(([k, v]) => `${k}=${v}`).join(" ")}`);
580
+ if (r.version_conflict) console.log(`⚠️ version_conflict: ${r.version_conflict.detail} → STOP & reconcile (RULES §3)`);
581
+ if (r.hint) console.log(`hint: ${r.hint}`);
582
+ for (const m of r.migrate_from || []) {
583
+ console.log(`migrate from: ${m.label} (${m.evidence.join(", ")}) → skill ${m.skill}`);
584
+ }
585
+ });
586
+ }
587
+
588
+ function cmdList() {
589
+ const reg = peers();
590
+ const dirs = shippedSkillDirs();
591
+ out({ entry: reg.entry, peers: reg.peers, skills: dirs }, () => {
592
+ console.log(`Entry: /${reg.entry}`);
593
+ // Parked peers are a routing SPEC only (no installable skill) — listing them with a
594
+ // "verified" badge and "declared but not shipped" rows read as a broken pack.
595
+ const parked = reg.peers.filter((p) => p.parked);
596
+ for (const p of reg.peers.filter((x) => !x.parked)) {
597
+ console.log(`\n${p.platform} ${p.major} (${p.kit_package}) ${p.verified ? "✓ verified" : "unverified"}`);
598
+ for (const s of p.skills) {
599
+ // `dir_prefix` is the peer's own answer and always wins. Without it, composing
600
+ // `cometchat-<platform>-<major>-<skill>` invents names: the SDK peer carries its
601
+ // consumption in `platform` (android-sdk) while the dir does not, and a platform with only
602
+ // one live major ships no <major> segment at all (NAMING.md) — e.g. cometchat-ios-core.
603
+ const base = p.platform.replace(/-(sdk|uikit)$/, "");
604
+ const direct = `cometchat-${p.platform}-${p.major}-${s}`;
605
+ const slug = [p.dirPrefix && `${p.dirPrefix}${s}`, direct,
606
+ `cometchat-${base}-${p.major}-${s}`,
607
+ `cometchat-${p.platform}-${s}`, `cometchat-${base}-${s}`]
608
+ .filter(Boolean).find((c) => dirs.includes(c)) || null;
609
+ console.log(slug ? ` - ${slug}` : ` - ${direct} ⚠️ declared in peers.yaml but not shipped`);
610
+ }
611
+ }
612
+ if (parked.length) console.log(`\nparked (routing spec only — no installable skills): ${parked.map((p) => `${p.platform} (${p.label || p.kit_package})`).join(", ")}`);
613
+ });
614
+ }
615
+
616
+ function cmdInfo() {
617
+ const slug = argv[1];
618
+ if (!slug) { console.error("usage: cometchat-skills info <skill-dir>"); process.exit(2); }
619
+ const file = join(PACK_ROOT, "skills", slug, "SKILL.md");
620
+ if (!existsSync(file)) { console.error(`no such skill: ${slug}`); process.exit(2); }
621
+ const b = (readFileSync(file, "utf8").match(/^---\n([\s\S]*?)\n---/) || [])[1] || "";
622
+ const g = (k) => (b.match(new RegExp("^" + k + ':\\s*"?(.+?)"?$', "m")) || [])[1] || "";
623
+ out({ name: g("name"), description: g("description"), compatibility: g("compatibility") }, () => {
624
+ console.log(`${g("name")}\n\n${g("description")}\n\ncompatibility: ${g("compatibility")}`);
625
+ });
626
+ }
627
+
628
+ // Shared pack-root data files that skills reference by BARE name (RULES.md,
629
+ // features.json, …). The copy step used to ship only skill directories, so these
630
+ // references resolved to nothing post-install — the skills pointed at files that
631
+ // were never installed. Ship a copy alongside every skill so bare refs resolve.
632
+ // Discovered, not hard-coded. A per-family pack has one oracle pair PER FAMILY
633
+ // (features.json + contracts.json for web-react, features.rn-v5.json +
634
+ // contracts.rn-v5.json for react-native, …). A fixed list ships only the first family's,
635
+ // so every later family's skills point at oracles that were never installed — the exact
636
+ // bug this comment block was written about, recurring one family later.
637
+ const SHARED_ROOT_FILES = ["RULES.md", "peers.yaml"];
638
+ function sharedRootFiles() {
639
+ const dynamic = readdirSync(PACK_ROOT).filter(
640
+ (f) => /^(features|contracts)([.-][a-z0-9-]+)?\.json$/i.test(f)
641
+ );
642
+ return [...SHARED_ROOT_FILES, ...dynamic];
643
+ }
644
+
645
+ // Same problem as sharedRootFiles(), one level down: a skill's own prose can bake a
646
+ // literal runtime-fetchable path ("catalogs/rn-v5.json") as its anti-hallucination
647
+ // oracle -- "anything not in catalogs/rn-v5.json does not exist, do not emit it".
648
+ // sharedRootFiles()'s regex only ever matches top-level FILENAMES, so a directory
649
+ // referenced this way could never be discovered by it and never installed at all
650
+ // (AUDIT-107). Whole-directory shared items are rare and structural -- there is
651
+ // exactly one today -- so this is a short literal list, not a second regex scan.
652
+ const SHARED_ROOT_DIRS = ["catalogs"];
653
+ function sharedRootDirs() {
654
+ return SHARED_ROOT_DIRS.filter((d) => existsSync(join(PACK_ROOT, d)));
655
+ }
656
+
657
+ // Only THIS project's family (+ the platform-neutral skills: router, gate, competitor migrations) is installed.
658
+ // Installing every family let a React skill out-match its RN twin: React's
659
+ // features triggers are unqualified ('add polls'), RN's say "React Native", so an
660
+ // RN user asking "add polls" matched the WRONG framework's skill (AUDIT-101).
661
+ // The prefix match deliberately co-installs a family's headless-SDK sibling where the prefixes
662
+ // overlap: cometchat-ios-calls/-push route VoIP push to cometchat-ios-v5-sdk, and the Android
663
+ // calls-sdk skill's 1:1 ringing leans on the cometchat-android-v5-sdk recipe (and vice versa).
664
+ function familySkillDirs(fam) {
665
+ const all = shippedSkillDirs();
666
+ if (!fam || !fam.dirPrefix) return all;
667
+ const neutral = neutralSkillDirs();
668
+ return all.filter((d) => neutral.has(d) || d.startsWith(fam.dirPrefix));
669
+ }
670
+
671
+ // Remove-on-add: drop the skill folders an EARLIER generation shipped, so a v4→v5 upgrade
672
+ // never leaves stale skills behind (v4's cometchat-core/features/theming lingering next to
673
+ // the v5 set, with cometchat-core still advertising "React UI Kit v6"). Scoped to the exact
674
+ // legacy names in bin/legacy-skills.json — NOT every `cometchat*` dir: a current-pack skill
675
+ // may belong to another family, a calls peer, or a --global install from another project
676
+ // (installing a second family must not wipe the first), and any other cometchat-* dir is the
677
+ // user's own. The authoring/meta skills are never pruned either (dev tooling, not installed by `add`).
678
+ const PROTECTED_SKILLS = new Set(["cometchat-skill-creator", "cometchat-skill-reviewer", "cometchat-skill-explorer"]);
679
+ function legacySkillNames() {
680
+ try { return new Set(JSON.parse(readFileSync(join(PACK_ROOT, "bin", "legacy-skills.json"), "utf8")).skills || []); }
681
+ catch { return new Set(); }
682
+ }
683
+ function pruneLegacySkills(target) {
684
+ if (!existsSync(target)) return [];
685
+ const legacy = legacySkillNames();
686
+ const current = new Set(shippedSkillDirs());
687
+ const removed = [];
688
+ for (const name of readdirSync(target)) {
689
+ if (!legacy.has(name) || current.has(name) || PROTECTED_SKILLS.has(name)) continue;
690
+ const p = join(target, name);
691
+ try { if (statSync(p).isDirectory()) { rmSync(p, { recursive: true, force: true }); removed.push(name); } } catch { /* ignore */ }
692
+ }
693
+ return removed;
694
+ }
695
+
696
+ function copySkillsTo(target, fam) {
697
+ const src = join(PACK_ROOT, "skills");
698
+ mkdirSync(target, { recursive: true });
699
+ const removed = pruneLegacySkills(target); // remove-on-add: drop stale earlier-generation skills
700
+ const shared = sharedRootFiles().filter((f) => existsSync(join(PACK_ROOT, f)));
701
+ const sharedDirs = sharedRootDirs();
702
+ for (const f of shared) cpSync(join(PACK_ROOT, f), join(target, f)); // tree-root copy so <target>/RULES.md (the router's ref) resolves
703
+ for (const d of sharedDirs) cpSync(join(PACK_ROOT, d), join(target, d), { recursive: true });
704
+ // Per-skill ORACLES. The root features.json/contracts.json are the WEB pack's, so copying them
705
+ // into every skill shipped `family: "web-react", major: "v7"` into all 19 Android skills — while
706
+ // the correct android files sat uninstalled. The skills cite these as ground truth, so an Android
707
+ // agent was reading React's feature list (down to CometChatErrorBoundary, a web-only component).
708
+ // Resolve each skill's family from its peer's dir_prefix and install THAT peer's oracles under
709
+ // the bare names the skills reference.
710
+ const peerList = (peers().peers || []).filter((p) => p.dirPrefix);
711
+ const oracleFor = (skillDir) => {
712
+ // Resolve to the peer that actually SHIPS this skill dir — dirPrefix + a slug in its
713
+ // `skills` list — so android-sdk vs android-calls (which share `cometchat-android-v5-`)
714
+ // and react-native vs react-native-calls each get their OWN oracle, not whichever peer
715
+ // sorts first. Fall back to the longest matching prefix for shared/neutral dirs.
716
+ const owns = (p) => skillDir.startsWith(p.dirPrefix)
717
+ && (p.skills || []).includes(skillDir.slice(p.dirPrefix.length));
718
+ const peer = peerList.find(owns)
719
+ || peerList.filter((p) => skillDir.startsWith(p.dirPrefix))
720
+ .sort((a, b) => b.dirPrefix.length - a.dirPrefix.length)[0];
721
+ return {
722
+ "features.json": peer && peer.features,
723
+ "contracts.json": peer && peer.contracts,
724
+ };
725
+ };
726
+ // Per-skill copies are what a skill's RELATIVE refs resolve against ("features.json",
727
+ // "catalogs/android-v6.json"). Copy only what this install's skills actually cite — plus the
728
+ // bare family-correct oracles and RULES/peers — not all 25 oracle files and every catalog into
729
+ // every folder (that made a React install 8.9 MB and an Android one 14 MB). The tree root above
730
+ // keeps the full set.
731
+ const names = familySkillDirs(fam);
732
+ const cited = citedSharedFiles(names.map((n) => join(src, n)));
733
+ const always = new Set([...SHARED_ROOT_FILES, "features.json", "contracts.json"]);
734
+ const perSkill = shared.filter((f) => always.has(f) || cited.files.has(f));
735
+ const installed = [];
736
+ for (const name of names) {
737
+ const dest = join(target, name);
738
+ cpSync(join(src, name), dest, { recursive: true }); // overlay, not rm+copy: files a user added inside survive a re-install
739
+ const oracles = oracleFor(name);
740
+ for (const f of perSkill) {
741
+ const override = oracles[f];
742
+ const from = override && existsSync(join(PACK_ROOT, override)) ? override : f;
743
+ cpSync(join(PACK_ROOT, from), join(dest, f)); // bare name, family-correct content
744
+ }
745
+ for (const d of sharedDirs) { // AUDIT-107: a skill's "catalogs/<file>" reference resolves same-folder
746
+ const files = [...(cited.dirs.get(d) || [])].filter((f) => existsSync(join(PACK_ROOT, d, f)));
747
+ if (!files.length) continue;
748
+ mkdirSync(join(dest, d), { recursive: true });
749
+ for (const f of files) cpSync(join(PACK_ROOT, d, f), join(dest, d, f));
750
+ }
751
+ installed.push(name);
752
+ }
753
+ return { installed, removed };
754
+ }
755
+
756
+ // Every named oracle ("features.rn-v5.json") and shared-dir file ("catalogs/rn-v5.json") that the
757
+ // given skill dirs cite anywhere in their markdown.
758
+ function citedSharedFiles(skillDirs) {
759
+ const files = new Set();
760
+ const dirs = new Map(SHARED_ROOT_DIRS.map((d) => [d, new Set()]));
761
+ const dirRe = new RegExp(`\\b(${SHARED_ROOT_DIRS.join("|")})/([A-Za-z0-9._-]+\\.(?:json|txt))`, "g");
762
+ const walk = (p) => {
763
+ for (const e of readdirSync(p, { withFileTypes: true })) {
764
+ const q = join(p, e.name);
765
+ if (e.isDirectory()) { walk(q); continue; }
766
+ if (!e.name.endsWith(".md")) continue;
767
+ const text = readFileSync(q, "utf8");
768
+ for (const m of text.matchAll(/\b((?:features|contracts)\.[a-z0-9-]+\.json)\b/g)) files.add(m[1]);
769
+ for (const m of text.matchAll(dirRe)) dirs.get(m[1]).add(m[2]);
770
+ }
771
+ };
772
+ for (const d of skillDirs) if (existsSync(d)) walk(d);
773
+ return { files, dirs };
774
+ }
775
+
776
+ // ── multi-agent install targets ──────────────────────────────────────────────
777
+ // The v4 pack shipped a three-mode IDE_TARGETS map (multi / single / cli-only).
778
+ // v5 ports it forward, refreshed to 2026 conventions: AGENTS.md is now the
779
+ // near-universal single-file entrypoint, so Codex / Antigravity / Cursor /
780
+ // Continue read it and NO agent is left "CLI-only".
781
+ // • "multi" agents get the native SKILL.md folder tree (full progressive
782
+ // disclosure) at their own skills dir.
783
+ // • "single" agents get a short (~4KB) orienting ROUTER file at their rules
784
+ // location PLUS the full tree copied to a neutral ./.cometchat/skills so the
785
+ // router points at REAL files the agent reads ON DEMAND (keeps the token win;
786
+ // never a mega-concatenation — that burns tokens every turn).
787
+ // The skill CONTENT is identical across every target — only the delivery differs.
788
+ const NEUTRAL_SKILLS_DIR = ".cometchat/skills";
789
+ const IDE_TARGETS = {
790
+ claude: { mode: "multi", dir: ".claude/skills", globalDir: join(homedir(), ".claude", "skills") },
791
+ kiro: { mode: "multi", dir: ".kiro/skills" },
792
+ replit: { mode: "multi", dir: ".agents/skills" },
793
+ cursor: { mode: "single", base: ".", file: "AGENTS.md" },
794
+ codex: { mode: "single", base: ".", file: "AGENTS.md" },
795
+ antigravity: { mode: "single", base: ".", file: "AGENTS.md" },
796
+ continue: { mode: "single", base: ".", file: "AGENTS.md" },
797
+ windsurf: { mode: "single", base: ".", file: ".windsurfrules" },
798
+ copilot: { mode: "single", base: ".github", file: "copilot-instructions.md" },
799
+ cline: { mode: "single", base: ".clinerules", file: "cometchat.md" },
800
+ gemini: { mode: "single", base: ".", file: "GEMINI.md" },
801
+ aider: { mode: "single", base: ".", file: "CONVENTIONS.md" },
802
+ };
803
+
804
+ // ─── MCP auto-wiring (RULES.md "Canonical doc channel" tier 1) ──────────────
805
+ // Beyond placing skill files, wire the CometChat docs MCP into each agent's OWN
806
+ // config so live docs load automatically. Each agent's mechanism differs — JSON
807
+ // vs TOML, `url` vs `httpUrl` vs `url`+`type` — so there is NO single snippet.
808
+ // MERGE, never clobber: preserve the user's existing servers/settings, only add
809
+ // or refresh `cometchat-docs`. Skipped with `--no-mcp`. Claude also auto-connects
810
+ // via the plugin manifest (plugin-root .mcp.json); this covers the CLI path too.
811
+ const MCP_NAME = "cometchat-docs";
812
+ const MCP_URL = "https://mcp.cometchat.com/mcp";
813
+ // Claude's USER-level MCP lives in ~/.claude.json — a large file Claude Code itself rewrites
814
+ // while running, so the installer never edits it: `--global` prints the `claude mcp add
815
+ // --scope user` command instead of writing a project .mcp.json into whatever folder you ran from.
816
+ const CLAUDE_USER_MCP_CMD = `claude mcp add --scope user --transport http ${MCP_NAME} ${MCP_URL}`;
817
+ const MCP_TARGETS = {
818
+ claude: { kind: "json", proj: ".mcp.json", glob: null, shape: () => ({ type: "http", url: MCP_URL }) },
819
+ cursor: { kind: "json", proj: ".cursor/mcp.json", glob: join(homedir(), ".cursor", "mcp.json"), shape: () => ({ url: MCP_URL }) }, // Cursor: bare `url`, transport inferred
820
+ gemini: { kind: "json", proj: ".gemini/settings.json", glob: join(homedir(), ".gemini", "settings.json"), shape: () => ({ httpUrl: MCP_URL, timeout: 5000 }) }, // Gemini: `httpUrl` (its `url` = SSE!)
821
+ codex: { kind: "toml", proj: ".codex/config.toml", glob: join(homedir(), ".codex", "config.toml") }, // Codex: TOML + needs the RMCP flag
822
+ };
823
+ const isPlainObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
824
+
825
+ // How to add the server by hand when we could not (or must not) write the agent's config.
826
+ function manualMcpHint(ide, file) {
827
+ if (ide === "claude") return `claude mcp add --scope ${file ? "project" : "user"} --transport http ${MCP_NAME} ${MCP_URL}`;
828
+ if (ide === "codex") return `add to ${file}: experimental_use_rmcp_client = true (root) + [mcp_servers.${MCP_NAME}] url = "${MCP_URL}"`;
829
+ const m = MCP_TARGETS[ide];
830
+ return `add "${MCP_NAME}": ${JSON.stringify(m.shape())} under "mcpServers" in ${file}`;
831
+ }
832
+
833
+ // Merge the server into a JSON `mcpServers` map without touching anything else. Anything that is
834
+ // not a JSON OBJECT at the top level (null, a string, a number, an array) — or whose `mcpServers`
835
+ // is not an object — is left untouched and reported, never "fixed": assigning a property onto
836
+ // `null` crashed the install AFTER the skills were copied, and onto a string/array it silently did
837
+ // nothing while reporting "added".
838
+ function mergeJsonMcp(file, serverShape) {
839
+ let cfg = {};
840
+ if (existsSync(file)) {
841
+ let parsed;
842
+ try { parsed = JSON.parse(readFileSync(file, "utf8")); }
843
+ catch (e) { return { file, status: "skipped-unparseable", reason: e && e.code === "EISDIR" ? "is a directory" : "not valid JSON" }; } // never corrupt a file we can't parse
844
+ if (!isPlainObject(parsed)) return { file, status: "skipped-unparseable", reason: `top-level JSON is ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : `a ${typeof parsed}`}, not an object` };
845
+ cfg = parsed;
846
+ }
847
+ if (cfg.mcpServers === undefined || cfg.mcpServers === null) cfg.mcpServers = {};
848
+ else if (!isPlainObject(cfg.mcpServers)) return { file, status: "skipped-unparseable", reason: `"mcpServers" is ${Array.isArray(cfg.mcpServers) ? "an array" : `a ${typeof cfg.mcpServers}`}, not an object` };
849
+ const existed = !!cfg.mcpServers[MCP_NAME];
850
+ cfg.mcpServers[MCP_NAME] = serverShape;
851
+ mkdirSync(dirname(file), { recursive: true });
852
+ writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n", "utf8");
853
+ return { file, status: existed ? "updated" : "added" };
854
+ }
855
+
856
+ // Codex is TOML AND gates HTTP transport behind a ROOT-level flag that must
857
+ // precede the first table — so prepend the flag if missing, append the server
858
+ // table if missing. Idempotent; leaves an existing entry untouched.
859
+ function mergeCodexToml(file) {
860
+ const FLAG = "experimental_use_rmcp_client = true";
861
+ const TABLE = `[mcp_servers.${MCP_NAME}]`;
862
+ let txt = existsSync(file) ? readFileSync(file, "utf8") : "";
863
+ const hadTable = txt.includes(TABLE);
864
+ if (!/experimental_use_rmcp_client\s*=/.test(txt)) txt = `${FLAG}\n\n${txt}`; // root flag before any table
865
+ if (!hadTable) txt = `${txt.replace(/\s*$/, "")}\n\n${TABLE}\nurl = "${MCP_URL}"\n`;
866
+ mkdirSync(dirname(file), { recursive: true });
867
+ writeFileSync(file, txt, "utf8");
868
+ return { file, status: hadTable ? "present" : "added" };
869
+ }
870
+
871
+ function wireMcpForIde(ide, global) {
872
+ const m = MCP_TARGETS[ide];
873
+ if (!m) return null; // this agent has no supported HTTP-MCP wiring
874
+ if (global && !m.glob) return { file: null, status: "manual", command: CLAUDE_USER_MCP_CMD };
875
+ const file = global ? m.glob : join(CWD, m.proj);
876
+ // Never let a config-write failure (EACCES, a directory where the file should be, …) crash the
877
+ // run after the skills are already copied — report it with the by-hand fallback instead.
878
+ try { return m.kind === "toml" ? mergeCodexToml(file) : mergeJsonMcp(file, m.shape()); }
879
+ catch (e) { return { file, status: "skipped-error", reason: e.code || e.message }; }
880
+ }
881
+
882
+ // Read a shipped skill's frontmatter (name + description → summary + triggers).
883
+ // Drives the orienting router table from real data so it can't drift.
884
+ function skillMeta(name) {
885
+ const file = join(PACK_ROOT, "skills", name, "SKILL.md");
886
+ const b = existsSync(file) ? (readFileSync(file, "utf8").match(/^---\n([\s\S]*?)\n---/) || [])[1] || "" : "";
887
+ const g = (k) => (b.match(new RegExp("^" + k + ':\\s*"?(.+?)"?\\s*$', "m")) || [])[1] || "";
888
+ const desc = g("description");
889
+ const triggers = (desc.match(/Triggers?:\s*(.+)$/) || [])[1] || "";
890
+ const summary = desc.split(/Triggers?:/)[0].replace(/\s+/g, " ").trim();
891
+ return { name: g("name") || name, dir: name, summary, triggers };
892
+ }
893
+
894
+ // The short (~4KB) orienting router written for "single" agents. refBase is the
895
+ // on-disk location of the full skill tree the agent reads on demand.
896
+ function generateOrientingMd(reg, dirs, refBase, fam) {
897
+ const entry = reg.entry || "cometchat";
898
+ const label = (fam && fam.label) || "UI Kit";
899
+ const kit = (fam && fam.kit_package) || "";
900
+ // Scope the table to THIS family + the platform-neutral router/gate. Listing
901
+ // every family's skills is what let an RN agent read React rows (AUDIT-101).
902
+ const shared = neutralSkillDirs();
903
+ const scoped = (fam && fam.dirPrefix)
904
+ ? dirs.filter((d) => shared.has(d) || d.startsWith(fam.dirPrefix))
905
+ : dirs;
906
+ const metas = scoped.map(skillMeta);
907
+ let md = `# CometChat — AI Agent Instructions (${label})\n\n`;
908
+ md += `> Auto-generated by \`npx @cometchat/skills add --ide <agent>\` between the \`cometchat-skills\` markers — edits inside them are overwritten on re-run; anything outside them is yours and is kept.\n`;
909
+ md += `> ${metas.length} task-shaped skills ship as Markdown under \`${refBase}/\`. Read them ON DEMAND — do not inline them all (that burns tokens every turn).\n\n`;
910
+
911
+ md += `## For any CometChat chat / messaging / calls task\n\n`;
912
+ md += `This project integrates **CometChat** via the **${label}**${kit ? ` (\`${kit}\`)` : ""}. OPEN the matching skill file below and follow it — do **not** answer from training memory (the UI Kit API changes across majors; these skills are verified against the pinned kit).\n\n`;
913
+ md += `| When the user wants… | Read this skill |\n|---|---|\n`;
914
+ for (const m of metas) {
915
+ const purpose = (m.triggers || m.summary).replace(/\|/g, "\\|"); // ponytail: no slice — a cut trigger cannot match (AUDIT-102)
916
+ md += `| ${purpose} | \`${refBase}/${m.dir}/SKILL.md\` |\n`;
917
+ }
918
+ md += `\nEach skill lazily pulls deeper detail from its own \`references/*.md\` — open those only when the skill points you to them.\n\n`;
919
+
920
+ md += `## Start here\n\n`;
921
+ md += `1. **Detect** — \`npx @cometchat/skills detect --json\` (framework, installed UI Kit, version_conflict). If \`version_conflict\` is non-null, STOP and reconcile before touching code.\n`;
922
+ md += `2. **Already integrated?** If \`detect\` says \`existing_cometchat: true\`, this is a TASK, not an integration — go **straight** to the matching skill in the table above and skip onboarding. "Change my avatar", "add a logout button", "add polls", "restyle the bubble" are all tasks. Do NOT re-run the plan/approve journey, and do NOT fall back to reading \`node_modules\` when the phrasing is not an exact match — pick the closest row and open it.\n`;
923
+ md += `3. **Migrating off Sendbird or Stream?** If the user asks to migrate/switch/replace and \`detect\` reports \`migrate_from\` (or the user names the vendor), open that entry's \`skill\` from the table — it runs the whole migration in one pass (no onboarding) and ends with the list of removed features.\n`;
924
+ md += `4. **Route** — open \`${refBase}/${entry}/SKILL.md\` (the dispatcher); it points to the right task skill for the request.\n`;
925
+ md += `5. **Follow the skill** — it carries the exact init→login→render, the component props, and the verify steps.\n\n`;
926
+
927
+ md += `## Hard rules (full text in \`${refBase}/RULES.md\`)\n\n`;
928
+ md += `- **APPEND, never replace** the user's code — additive diffs; confirm before bulk edits.\n`;
929
+ md += `- **One package per platform**; never mix UI Kit majors (v5/v6/v7).\n`;
930
+ md += `- **Never hardcode credentials** — the Auth Key is dev-only; use a server-minted auth token for production.\n`;
931
+ md += `- **Give the chat container real height + width** or it collapses; **never render a raw localization key** as a label.\n`;
932
+ md += `- **Render custom controls in the component's own view slot**, not as a sibling on top of it.\n`;
933
+ md += `- **Verify against the catalog + a real build** before "done"; fetch signatures from live docs, never from memory or \`node_modules\`.\n`;
934
+ md += `- **Need a CometChat API that is not in the table above? FETCH THE DOCS** — open the family \`core\` skill's \`references/docs-map.md\` and follow it. **Never** grep \`node_modules\` and **never** read \`*.d.ts\`: they carry internal, unsupported and version-drifting surface. If the docs do not have it, say so — do not infer it from types.\n\n`;
935
+
936
+ md += `## Want the native Claude Code install instead?\n\n`;
937
+ md += "```bash\n";
938
+ md += `npx @cometchat/skills add --ide claude # writes the .claude/skills tree (auto progressive disclosure)\n`;
939
+ md += "```\n";
940
+ return md;
941
+ }
942
+
943
+ // The single-file targets (AGENTS.md, GEMINI.md, CONVENTIONS.md, .windsurfrules,
944
+ // .github/copilot-instructions.md, .clinerules/cometchat.md) are often the USER's own file —
945
+ // AGENTS.md especially is shared by many tools. Overwriting it wholesale destroyed their content.
946
+ // So the router lives in a MANAGED BLOCK: re-runs replace only what is between the markers.
947
+ // Every one of these files is Markdown (or read raw as Markdown-ish rules text), where an HTML
948
+ // comment is invisible when rendered and inert when read raw — a safe fence for all of them.
949
+ const MARK_START = "<!-- cometchat-skills:start -->";
950
+ const MARK_END = "<!-- cometchat-skills:end -->";
951
+ // The unmarked router an earlier installer version wrote (whole-file, "Do not hand-edit"): replace
952
+ // exactly that span on upgrade instead of appending a second router next to it.
953
+ const LEGACY_ROUTER_RE = /^# CometChat — AI Agent Instructions[^\r\n]*\r?\n[\s\S]*?\r?\n## Want the native Claude Code install instead\?\r?\n(?:\r?\n)*```bash\r?\n[^\r\n]*\r?\n```[ \t]*(?:\r?\n)?/m;
954
+ // The router the PUBLISHED v4.2–4.4.x installer wrote (npm `latest` users have it on disk): from its
955
+ // "Auto-generated … Do not hand-edit" header to its closing "That writes N per-skill markdown files"
956
+ // line. Anything a user appended after that line is left alone. (4.0/4.1 wrote no router file.)
957
+ const LEGACY_V4_ROUTER_RE = /^# CometChat — AI Agent Instructions\r?\n\r?\n> Auto-generated by `npx @cometchat\/skills add`\. Do not hand-edit;[\s\S]*?\r?\nThat writes \d+ per-skill markdown files to `\.claude\/skills\/cometchat-\*\/SKILL\.md`\.[^\r\n]*(?:\r?\n)?/m;
958
+ function writeManagedBlock(dest, body) {
959
+ const block = (eol) => [MARK_START, body.replace(/\n+$/, ""), MARK_END].join("\n").replace(/\n/g, eol);
960
+ if (!existsSync(dest)) { writeFileSync(dest, block("\n") + "\n", "utf8"); return "created"; }
961
+ if (statSync(dest).isDirectory()) return "skipped-not-a-file"; // e.g. an AGENTS.md FOLDER — never write into or over it
962
+ const text = readFileSync(dest, "utf8");
963
+ const legacy = [LEGACY_ROUTER_RE, LEGACY_V4_ROUTER_RE].find((re) => re.test(text));
964
+ const eol = /\r\n/.test(text) ? "\r\n" : "\n"; // keep a CRLF file CRLF
965
+ const s = text.indexOf(MARK_START);
966
+ const e = text.indexOf(MARK_END);
967
+ let next, status;
968
+ if (s >= 0 && e > s) { next = text.slice(0, s) + block(eol) + text.slice(e + MARK_END.length); status = "updated"; }
969
+ else if (s >= 0 || e >= 0) return "skipped-malformed"; // one marker without its partner: don't guess where the block ends
970
+ else if (legacy) { next = text.replace(legacy, block(eol) + eol); status = "migrated"; }
971
+ else { next = text.replace(/(\r?\n)*$/, "") + (text.trim() ? eol + eol : "") + block(eol) + eol; status = "appended"; }
972
+ if (next === text) return "unchanged";
973
+ writeFileSync(dest, next, "utf8");
974
+ return status;
975
+ }
976
+
977
+ // Install for one agent. "multi" → native SKILL.md tree; "single" → orienting
978
+ // router at the agent's rules file + the full tree at the neutral dir.
979
+ function installForIde(ide, global) {
980
+ const t = IDE_TARGETS[ide];
981
+ const fam = resolveFamily();
982
+ if (t.mode === "multi") {
983
+ const target = global && t.globalDir ? t.globalDir : join(CWD, t.dir);
984
+ const { installed, removed } = copySkillsTo(target, fam);
985
+ return { ide, mode: "multi", target, count: installed.length, removed };
986
+ }
987
+ const neutral = join(CWD, NEUTRAL_SKILLS_DIR);
988
+ const { installed, removed } = copySkillsTo(neutral, fam);
989
+ const base = join(CWD, t.base);
990
+ let dest = join(base, t.file), fileStatus;
991
+ try {
992
+ if (t.base !== "." && existsSync(base) && !statSync(base).isDirectory()) {
993
+ // The rules "folder" is a FILE. Cline's original single-file form is a `.clinerules` file, and
994
+ // Cline still reads it — so put the block there. Any other target: leave the user's file alone.
995
+ if (t.base === ".clinerules") dest = base;
996
+ else fileStatus = "skipped-not-a-directory";
997
+ } else mkdirSync(base, { recursive: true });
998
+ fileStatus ||= writeManagedBlock(dest, generateOrientingMd(peers(), shippedSkillDirs(), NEUTRAL_SKILLS_DIR, fam));
999
+ } catch (e) {
1000
+ fileStatus = "skipped-error"; // never crash after the skills are already copied
1001
+ return { ide, mode: "single", target: dest, neutral, count: installed.length, removed, file_status: fileStatus, reason: e.code || e.message };
1002
+ }
1003
+ return { ide, mode: "single", target: dest, neutral, count: installed.length, removed, file_status: fileStatus };
1004
+ }
1005
+
1006
+ function cmdAdd() {
1007
+ const global = has("--global");
1008
+ const supported = Object.keys(IDE_TARGETS);
1009
+ requireFlagValue("--ide", [...supported, "all"]);
1010
+ requireFlagValue("--target", [...supported, "all"]);
1011
+ const ide = (flagVal("--ide") || flagVal("--target") || "claude").toLowerCase();
1012
+ if (ide !== "all" && !IDE_TARGETS[ide]) {
1013
+ console.error(`unknown --ide: ${ide}\nsupported: ${supported.join(", ")}, all`);
1014
+ process.exit(2);
1015
+ }
1016
+ let ides = ide === "all" ? supported : [ide];
1017
+ // --global needs a USER-level location. Only claude has one here (~/.claude/skills); kiro/replit
1018
+ // and the single-file agents (AGENTS.md, GEMINI.md, …) are per-project by nature. They used to
1019
+ // ignore --global and write into whatever folder you ran from — now they are skipped, loudly.
1020
+ const noGlobal = global ? ides.filter((n) => !(IDE_TARGETS[n].mode === "multi" && IDE_TARGETS[n].globalDir)) : [];
1021
+ if (noGlobal.length) {
1022
+ ides = ides.filter((n) => !noGlobal.includes(n));
1023
+ const msg = `--global: no user-level install location for ${noGlobal.join(", ")} — skipped (nothing written to ${CWD}). ` +
1024
+ `Run \`npx @cometchat/skills add --ide <agent>\` inside the project for ${noGlobal.length > 1 ? "those" : "it"}.`;
1025
+ if (!ides.length) {
1026
+ if (JSON_OUT) console.log(JSON.stringify({ ok: false, error: msg, skipped: noGlobal }));
1027
+ else console.error(msg);
1028
+ process.exit(2);
1029
+ }
1030
+ if (!JSON_OUT) console.error(`↷ ${msg}`);
1031
+ }
1032
+ // Surface a version-conflict up front (non-fatal): a project already on a different UI Kit
1033
+ // major still gets skills, but the agent must reconcile before emitting code (RULES §3).
1034
+ const vc = (detect() || {}).version_conflict || null;
1035
+ if (vc && !JSON_OUT) console.error(`⚠️ version_conflict: ${vc.detail} — reconcile before generating code (RULES §3).`);
1036
+ const results = [];
1037
+ const writtenSingles = new Set();
1038
+ for (const name of ides) {
1039
+ const t = IDE_TARGETS[name];
1040
+ if (t.mode === "single") { // dedupe agents that share a file (cursor/codex/antigravity/continue → AGENTS.md)
1041
+ const key = join(t.base, t.file);
1042
+ if (writtenSingles.has(key)) { results.push({ ide: name, mode: "single", target: join(CWD, key), deduped: true }); continue; }
1043
+ writtenSingles.add(key);
1044
+ }
1045
+ results.push(installForIde(name, global));
1046
+ }
1047
+
1048
+ // Wire the docs MCP into each agent's own config (tier 1 auto-connect), unless opted out.
1049
+ const mcpResults = [];
1050
+ if (!has("--no-mcp")) {
1051
+ const wroteMcp = new Set();
1052
+ for (const name of ides) {
1053
+ const m = MCP_TARGETS[name];
1054
+ if (!m) continue; // agent has no supported HTTP-MCP wiring (e.g. copilot/windsurf/aider)
1055
+ const file = global ? (m.glob || `user:${name}`) : join(CWD, m.proj);
1056
+ if (wroteMcp.has(file)) continue;
1057
+ wroteMcp.add(file);
1058
+ const r = wireMcpForIde(name, global);
1059
+ if (r) mcpResults.push({ ide: name, ...r });
1060
+ }
1061
+ }
1062
+
1063
+ const FILE_VERB = { created: "created", updated: "updated (managed block)", migrated: "updated (replaced an earlier unmarked router)",
1064
+ appended: "appended (your content kept)", unchanged: "unchanged" };
1065
+ out({ ok: true, results, mcp: mcpResults, version_conflict: vc, ...(noGlobal.length ? { skipped: noGlobal } : {}) }, () => {
1066
+ for (const r of results) {
1067
+ if (r.deduped) console.log(`✅ ${r.ide.padEnd(11)} → shares ${r.target} (already written)`);
1068
+ else if (r.mode === "multi") console.log(`✅ ${r.ide.padEnd(11)} → ${r.count} skill(s) in ${r.target}`);
1069
+ else if (r.file_status === "skipped-malformed") console.log(`⚠️ ${r.ide.padEnd(11)} → ${r.target} has an unmatched cometchat-skills start/end marker — router NOT written; fix or remove the marker and re-run (+ ${r.count} skills in ${r.neutral})`);
1070
+ else if (String(r.file_status).startsWith("skipped")) {
1071
+ const why = { "skipped-not-a-file": "is a folder, not a file", "skipped-not-a-directory": "sits where a folder is expected" }[r.file_status] || `could not be written (${r.reason})`;
1072
+ console.log(`⚠️ ${r.ide.padEnd(11)} → ${r.target.replace(CWD + "/", "")} ${why} — router NOT written; the skills are installed (+ ${r.count} skills in ${r.neutral})`);
1073
+ }
1074
+ else console.log(`✅ ${r.ide.padEnd(11)} → router ${r.target} ${FILE_VERB[r.file_status] || r.file_status} (+ ${r.count} skills in ${r.neutral})`);
1075
+ if (r.removed && r.removed.length) console.log(` removed ${r.removed.length} stale skill(s) from an earlier version: ${r.removed.join(", ")}`);
1076
+ }
1077
+ for (const r of mcpResults) {
1078
+ const rel = r.file ? r.file.replace(CWD + "/", "") : null;
1079
+ if (r.status === "manual") console.log(`🔌 MCP ${r.ide.padEnd(8)} → --global: user-level MCP lives in ~/.claude.json (owned by Claude Code, not edited). Run:\n ${r.command}`);
1080
+ else if (r.status.startsWith("skipped")) console.log(`⚠️ MCP ${r.ide.padEnd(8)} → ${rel} left untouched (${r.reason}) — add by hand: ${manualMcpHint(r.ide, rel)}`);
1081
+ else console.log(`🔌 MCP ${r.ide.padEnd(8)} → ${MCP_NAME} ${r.status} in ${rel}`);
1082
+ }
1083
+ // Only point at the neutral tree when a router-file agent actually got one — a claude-only
1084
+ // install has no .cometchat/skills, and naming it sent people to a path that doesn't exist.
1085
+ const neutralDir = (results.find((r) => r.neutral) || {}).neutral;
1086
+ console.log(`\nEntry: /cometchat add chat to my app` +
1087
+ (neutralDir ? ` (agents without slash-skills: open ${neutralDir.replace(CWD + "/", "")}/cometchat/SKILL.md)` : ""));
1088
+ if (mcpResults.length) console.log(`Docs MCP: ${MCP_URL} (auto-loads live docs; restart the agent if it doesn't connect. Skip with --no-mcp.)`);
1089
+ // Claude Code gates a PROJECT .mcp.json server behind a one-time trust prompt (`claude mcp list`
1090
+ // shows "Pending approval" until then) — say so, or "auto-loads" reads as broken on first run.
1091
+ if (mcpResults.some((r) => r.ide === "claude" && (r.status === "added" || r.status === "updated")))
1092
+ console.log(`Claude Code: approve "${MCP_NAME}" when \`claude\` asks on first start in this project — until then it shows "Pending approval" in \`claude mcp list\`. Skills fall back to the docs site if you decline.`);
1093
+ });
1094
+ }
1095
+
1096
+ function runNode(scriptRel, extra = []) {
1097
+ const r = spawnSync("node", [join(PACK_ROOT, scriptRel), ...extra], { stdio: "inherit" });
1098
+ return r.status ?? 1;
1099
+ }
1100
+
1101
+ // The Tier-2 fence for a family: which engine, against which pinned-kit project, and whether that
1102
+ // project is set up. A packed (npm) install has no families.json, so `key`/`typecheckProject` are
1103
+ // null — this returns { skip } instead of handing the engine `--project null` (which printed
1104
+ // "npm --prefix null ci" and exited 1 on every npm install).
1105
+ function fencePlan(f) {
1106
+ const proj = f && f.key && f.typecheckProject && existsSync(join(PACK_ROOT, f.typecheckProject)) ? f.typecheckProject : null;
1107
+ if (!proj) return { skip: `no pinned-kit typecheck project for ${(f && f.label) || "this family"} in this install` };
1108
+ const P = join(PACK_ROOT, proj);
1109
+ const has = (rel) => existsSync(join(P, rel));
1110
+ let plan;
1111
+ if (has("tsconfig.json")) {
1112
+ const fam = f.fenceNeedles && f.skillDirPattern;
1113
+ if (!fam) return { skip: `no TypeScript fence declared for ${f.key}` };
1114
+ plan = { engine: "typecheck-fences.mjs", args: ["--project", proj, "--tsconfig", "tsconfig.json", "--discover", f.key],
1115
+ ready: has("node_modules/@cometchat"), setup: `npm --prefix ${proj} ci` };
1116
+ } else if (has("pubspec.yaml")) {
1117
+ plan = { engine: "typecheck-fences-dart.mjs", args: ["--project", proj, "--discover", f.key],
1118
+ ready: has(".dart_tool/package_config.json"), setup: `(cd ${proj} && flutter pub get)` };
1119
+ } else if (has("settings.gradle.kts") || has("settings.gradle")) {
1120
+ plan = { engine: "typecheck-fences-kotlin.mjs", args: ["--project", proj, "--discover", f.key],
1121
+ ready: has("local.properties"), setup: `write sdk.dir=<Android SDK path> into ${proj}/local.properties` };
1122
+ } else return { skip: `no fence engine for ${proj}` };
1123
+ if (!existsSync(join(PACK_ROOT, "test-suite/scripts", plan.engine))) return { skip: `${plan.engine} not in this install` };
1124
+ return { ...plan, proj };
1125
+ }
1126
+
1127
+ function cmdVerify() {
1128
+ const tier = argv[1] && !argv[1].startsWith("-") ? argv[1] : "all";
1129
+ // The predicate (review done-oracle) belongs to the reviewer, which is INTERNAL-DEV-ONLY
1130
+ // and NOT shipped in the published package (no e2e harness on npm). When it's absent —
1131
+ // i.e. running from an npm install rather than the dev repo — skip it cleanly instead of
1132
+ // erroring, so the end-user CLI never fails on the missing dev harness.
1133
+ const PREDICATE = ".claude/skills/cometchat-skill-reviewer/tools/predicate.mjs";
1134
+ const hasReviewer = existsSync(join(PACK_ROOT, PREDICATE));
1135
+ const gates = {
1136
+ lint: () => runNode("test-suite/scripts/lint-skills.mjs"),
1137
+ catalog: () => runNode("test-suite/scripts/verify-catalog.mjs"),
1138
+ // Family resolved from families.json (RULES.md §22). This hardcoded web-v7, so `fences`
1139
+ // silently checked only React regardless of which family the workspace actually used.
1140
+ fences: () => {
1141
+ if (!resolvePeer((detect() || {}).framework || "")) {
1142
+ console.log("↷ fences — DEV-ONLY Tier-2 gate: no family resolved for this directory (pass --family <platform>); skipped.");
1143
+ return 0;
1144
+ }
1145
+ const p = fencePlan(resolveFamily());
1146
+ if (p.skip) { console.log(`↷ fences — DEV-ONLY Tier-2 gate (${p.skip}); skipped.`); return 0; }
1147
+ if (!p.ready) { console.log(`↷ fences — DEV-ONLY Tier-2 gate: pinned kit not set up in ${p.proj}; skipped. Set up: ${p.setup}`); return 0; }
1148
+ return runNode(`test-suite/scripts/${p.engine}`, p.args);
1149
+ },
1150
+ predicate: () => {
1151
+ if (!hasReviewer) { console.log("↷ predicate — DEV-ONLY review gate (e2e harness not present in this install); skipped."); return 0; }
1152
+ return runNode(PREDICATE);
1153
+ },
1154
+ };
1155
+ let seq;
1156
+ if (tier === "all") seq = ["lint", "catalog", "predicate"]; // offline gates (fences needs the kit)
1157
+ else if (tier === "ci") seq = ["lint", "catalog", "fences", "predicate"];
1158
+ else if (gates[tier]) seq = [tier];
1159
+ else { console.error(`unknown tier: ${tier} (lint|catalog|fences|predicate|all|ci)`); process.exit(2); }
1160
+ let bad = 0;
1161
+ for (const g of seq) { const code = gates[g](); if (code !== 0) bad++; }
1162
+ process.exit(bad ? 1 : 0);
1163
+ }
1164
+
1165
+ function cmdCatalog() {
1166
+ process.exit(runNode("test-suite/scripts/catalog-refresh.mjs", has("--check") ? ["--check"] : []));
1167
+ }
1168
+
1169
+ function cmdDoctor() {
1170
+ const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
1171
+ // Family-aware. Reporting the WEB catalog (405 symbols) and "npm --prefix cometchat-react-app ci"
1172
+ // inside an Android or iOS project was worse than noise: it told the user their oracle was fine
1173
+ // while reading React's. Resolve the peer the project actually is.
1174
+ // Resolved exactly like `add` (FRAMEWORK_PLATFORM + --family): Expo → react-native, Next/Astro →
1175
+ // react. The old platform-string match fell through to React for Expo, and with NO family it
1176
+ // printed "harness/null". No family ⇒ say so; never substitute React's catalog.
1177
+ const det = detect();
1178
+ const peer = resolvePeer(det.framework || "");
1179
+ const fam = reviewerFamily(peer); // dev-only (families.json); null on an npm install
1180
+ const noFamily = "n/a — no family resolved (run inside the app package, or pass --family <platform>)";
1181
+ const catalog = peer && peer.catalog ? readJSON(join(PACK_ROOT, `test-suite/catalogs/${peer.catalog}.json`)) : null;
1182
+ // The pinned kit (Tier-2 typecheck project) and the review app/harness (Tier-3) for THIS family.
1183
+ let kit;
1184
+ if (!peer) kit = { ok: false, detail: noFamily };
1185
+ else if (!fam) kit = { ok: false, detail: "n/a — dev-only (pinned kit + review harness are not in this install)" };
1186
+ else {
1187
+ const plan = fencePlan({ ...fam, label: peer.label });
1188
+ const dirs = [plan.proj, fam.appDir].filter((d, i, a) => d && a.indexOf(d) === i);
1189
+ const installed = (d) => {
1190
+ const P = join(PACK_ROOT, d);
1191
+ if (!existsSync(P)) return false;
1192
+ if (existsSync(join(P, "package.json"))) return existsSync(join(P, "node_modules", "@cometchat"));
1193
+ if (existsSync(join(P, "pubspec.yaml"))) return existsSync(join(P, ".dart_tool"));
1194
+ return true; // native harness: present = usable (its own gate reports toolchain gaps)
1195
+ };
1196
+ const hit = dirs.find(installed);
1197
+ kit = hit ? { ok: true, detail: hit }
1198
+ : { ok: false, detail: plan.setup ? `not set up — ${plan.setup}` : dirs.length ? `${dirs.join(" / ")} not set up` : "no pinned kit or harness declared for this family" };
1199
+ }
1200
+ // Review state for THIS family only (reviews/<family>/review-state.*.json) — never React's as a fallback.
1201
+ let state = null;
1202
+ const reviewDir = fam ? join(PACK_ROOT, "reviews", fam.name) : null;
1203
+ if (reviewDir && existsSync(reviewDir)) {
1204
+ for (const f of readdirSync(reviewDir).filter((n) => n.startsWith("review-state."))) {
1205
+ const j = readJSON(join(reviewDir, f));
1206
+ if (j && (!state || ((j.totals || {}).passed || 0) > ((state.totals || {}).passed || 0))) state = j;
1207
+ }
1208
+ }
1209
+ // The pinned kit and the review harness/state are INTERNAL-DEV concerns — neither ships
1210
+ // in the published package. Mark those checks devOnly so `doctor` stays green on an npm
1211
+ // install (they surface as advisory ⚠️, not failures).
1212
+ const reviewerPresent = existsSync(join(PACK_ROOT, ".claude/skills/cometchat-skill-reviewer"));
1213
+ const checks = [
1214
+ { name: "node >= 18", ok: nodeMajor >= 18, detail: `v${process.versions.node}` },
1215
+ { name: `family`, ok: !!peer, detail: peer ? `${peer.platform} (${peer.label})` : noFamily, advisory: true },
1216
+ { name: "Tier-1 catalog present", ok: !!catalog,
1217
+ detail: !peer ? noFamily : catalog ? `${peer.catalog}: ${catalog.symbolCount} symbols` : `test-suite/catalogs/${peer.catalog}.json missing (npm run catalog:refresh)`,
1218
+ advisory: !peer },
1219
+ { name: "pinned kit / harness (dev-only Tier-2/3)", ok: kit.ok, detail: kit.detail, devOnly: true },
1220
+ { name: "review state (dev-only)", ok: !!state,
1221
+ detail: state ? `${(state.totals || {}).passed || 0}/${(state.totals || {}).count || 0} passed`
1222
+ : !peer ? noFamily : !reviewerPresent ? "n/a — e2e review harness is dev-only, not in this install" : "no review yet", devOnly: true },
1223
+ ];
1224
+ const ok = checks.every((c) => c.ok || c.devOnly || c.advisory); // kit-install + review state are dev-only; no-family is advisory
1225
+ out({ ok, checks }, () => {
1226
+ console.log(`cometchat-skills doctor (v${packVersion()})`);
1227
+ for (const c of checks) console.log(` ${c.ok ? "✅" : "⚠️ "} ${c.name} — ${c.detail}`);
1228
+ });
1229
+ process.exit(ok ? 0 : 1);
1230
+ }
1231
+
1232
+ function help() {
1233
+ console.log(`cometchat-skills v${packVersion()} — deterministic CLI for the CometChat skills pack
1234
+
1235
+ Usage: npx @cometchat/skills <command> [--json]
1236
+
1237
+ detect Probe the current project (framework, UI Kit, version_conflict)
1238
+ add [--ide <a>] Install the skills for an AI agent (default: claude)
1239
+ --ide claude|kiro|replit → native SKILL.md tree
1240
+ --ide cursor|codex|antigravity|continue|windsurf|copilot|cline|gemini|aider
1241
+ → orienting router + ./.cometchat/skills tree
1242
+ (router goes in a <!-- cometchat-skills:start/end -->
1243
+ block; your own content in that file is kept)
1244
+ --ide all → every supported agent
1245
+ --global → user-level dir: claude only (~/.claude/skills; prints
1246
+ the \`claude mcp add --scope user\` command instead of
1247
+ writing .mcp.json). Agents with no user-level location
1248
+ (kiro, replit, the router-file agents) are skipped.
1249
+ (also wires the docs MCP into the agent's config so live
1250
+ docs auto-load: claude .mcp.json · cursor .cursor/mcp.json ·
1251
+ codex .codex/config.toml · gemini .gemini/settings.json)
1252
+ --no-mcp → skip the MCP wiring
1253
+ --family <platform> → force the family (monorepo root / ambiguous detect;
1254
+ e.g. js-calls, ios-calls to add a headless calls skill
1255
+ alongside the UI Kit skills)
1256
+ list List the skills this pack ships
1257
+ info <slug> Show a skill's triggers + compatibility
1258
+ verify [tier] Run gates: lint | catalog | fences | predicate | all | ci
1259
+ catalog [--check] Refresh the Tier-1 catalog from the installed kit (oracle refresh)
1260
+ doctor Environment health check
1261
+ version | help
1262
+
1263
+ Notes:
1264
+ • Credentials/provisioning (auth, provision, config, features) are a
1265
+ SEPARATE published tool, @cometchat/skills-cli — see the core skill's
1266
+ references/setup-credentials.md. This CLI is the skill-management + verify layer.`);
1267
+ }
1268
+
1269
+ switch (CMD) {
1270
+ case "detect": cmdDetect(); break;
1271
+ case "add": case "install": cmdAdd(); break;
1272
+ case "list": cmdList(); break;
1273
+ case "info": cmdInfo(); break;
1274
+ case "verify": cmdVerify(); break;
1275
+ case "catalog": cmdCatalog(); break;
1276
+ case "doctor": cmdDoctor(); break;
1277
+ case "version": case "--version": case "-v": console.log(packVersion()); break;
1278
+ case "help": case "--help": case "-h": case undefined: help(); break;
1279
+ default: console.error(`unknown command: ${CMD}\n`); help(); process.exit(2);
1280
+ }