@cometchat/skills 4.4.2 → 5.0.0

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