@cometchat/skills-cli 2.2.0 → 2.3.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.
package/dist/index.js CHANGED
@@ -283,6 +283,9 @@ var init_state = __esm({
283
283
  // src/commands/detect.ts
284
284
  import { resolve } from "node:path";
285
285
 
286
+ // src/detectors/framework.ts
287
+ import { readdirSync } from "node:fs";
288
+
286
289
  // src/utils/version.ts
287
290
  function stripRange(versionRange) {
288
291
  return versionRange.replace(/^[\^~>=<\s]+/, "").trim();
@@ -312,7 +315,9 @@ var EMPTY = {
312
315
  env_prefix: null,
313
316
  expo_mode: null,
314
317
  expo_version: null,
315
- react_native_version: null
318
+ react_native_version: null,
319
+ android_version: null,
320
+ flutter_version: null
316
321
  };
317
322
  function allDeps(pkg) {
318
323
  return {
@@ -434,6 +439,144 @@ function detectAstro(_root, deps) {
434
439
  env_prefix: "PUBLIC_"
435
440
  };
436
441
  }
442
+ function detectIOS(root) {
443
+ let dirEntries = [];
444
+ try {
445
+ dirEntries = readdirSync(root);
446
+ } catch {
447
+ }
448
+ const hasXcode = dirEntries.some(
449
+ (n) => n.endsWith(".xcodeproj") || n.endsWith(".xcworkspace")
450
+ );
451
+ const podfile = readFileOrNull(p(root, "Podfile"));
452
+ const hasIOSPodfile = !!podfile && /platform\s+:ios/.test(podfile);
453
+ const packageSwift = readFileOrNull(p(root, "Package.swift"));
454
+ const hasIOSPackage = !!packageSwift && /\.iOS\s*\(/.test(packageSwift);
455
+ if (!hasXcode && !hasIOSPodfile && !hasIOSPackage) return null;
456
+ const podfileLock = readFileOrNull(p(root, "Podfile.lock")) ?? "";
457
+ const packageResolved = readFileOrNull(p(root, "Package.resolved")) ?? readFileOrNull(p(root, ".swiftpm/Package.resolved")) ?? "";
458
+ const cocoapodSources = (podfile ?? "") + podfileLock;
459
+ const spmSources = (packageSwift ?? "") + packageResolved;
460
+ let ios_version = null;
461
+ if (
462
+ // Podfile.lock: `- CometChatUIKitSwift (5.1.12)` / Podfile: `pod 'CometChatUIKitSwift', '~> 5.0'`
463
+ /CometChatUIKitSwift[^,\n]*?[\s'"(]5\./.test(cocoapodSources) || // Package.resolved: `"identity" : "cometchatuikitswift", ... "version" : "5.x.x"`
464
+ /cometchatuikitswift[\s\S]{0,300}"version"\s*:\s*"5\./i.test(spmSources) || // Package.swift: `.package(url: "https://github.com/cometchat/...CometChatUIKitSwift", from: "5.0.0")`
465
+ /CometChatUIKitSwift[\s\S]{0,200}from:\s*"5\./.test(spmSources)
466
+ ) {
467
+ ios_version = "v5";
468
+ }
469
+ return {
470
+ framework: "ios",
471
+ framework_version: null,
472
+ // Swift / iOS deployment target isn't load-bearing for the dispatcher
473
+ router: null,
474
+ bundler: null,
475
+ ssr_strategy: null,
476
+ env_prefix: "",
477
+ // No public-env-prefix; Secrets.swift / xcconfig is the convention
478
+ ios_version
479
+ };
480
+ }
481
+ function detectFlutter(root) {
482
+ const pubspecRaw = readFileOrNull(p(root, "pubspec.yaml"));
483
+ if (!pubspecRaw) return null;
484
+ const isFlutter = /\n\s*flutter:\s*\n\s*sdk:\s*flutter/.test(pubspecRaw) || /\n\s*flutter:\s*$/m.test(pubspecRaw) || /\n\s*flutter:\s*\n\s*uses-material-design/.test(pubspecRaw);
485
+ if (!isFlutter) return null;
486
+ let flutter_version = null;
487
+ if (/cometchat_chat_uikit\s*:\s*[\^~]?6\./.test(pubspecRaw)) {
488
+ flutter_version = "v6";
489
+ } else if (/cometchat_chat_uikit\s*:\s*[\^~]?5\./.test(pubspecRaw) || /cometchat_calls_uikit\s*:\s*[\^~]?5\./.test(pubspecRaw)) {
490
+ flutter_version = "v5";
491
+ }
492
+ const sdkMatch = pubspecRaw.match(/\n\s*environment:\s*\n\s*sdk:\s*['"]?([^'"\n]+)/);
493
+ const framework_version = sdkMatch ? sdkMatch[1].trim() : null;
494
+ return {
495
+ framework: "flutter",
496
+ framework_version,
497
+ router: null,
498
+ // Flutter routing is in code (Navigator/GoRouter); not modeled here
499
+ bundler: null,
500
+ // Dart compiler — no JS bundler
501
+ ssr_strategy: null,
502
+ env_prefix: "",
503
+ // No public-env-prefix; --dart-define or .env-derived constants
504
+ flutter_version
505
+ };
506
+ }
507
+ function detectAndroid(root) {
508
+ const hasSettingsGradle = pathExists(p(root, "settings.gradle")) || pathExists(p(root, "settings.gradle.kts"));
509
+ const hasBuildGradle = pathExists(p(root, "build.gradle")) || pathExists(p(root, "build.gradle.kts"));
510
+ if (!hasSettingsGradle && !hasBuildGradle) return null;
511
+ const hasAppModule = pathExists(p(root, "app/build.gradle")) || pathExists(p(root, "app/build.gradle.kts"));
512
+ const rootGradle = readFileOrNull(p(root, "build.gradle")) ?? readFileOrNull(p(root, "build.gradle.kts")) ?? "";
513
+ const settingsGradle = readFileOrNull(p(root, "settings.gradle")) ?? readFileOrNull(p(root, "settings.gradle.kts")) ?? "";
514
+ const hasAndroidPlugin = /com\.android\.(application|library)/.test(
515
+ rootGradle + settingsGradle
516
+ );
517
+ if (!hasAppModule && !hasAndroidPlugin) return null;
518
+ const buildFiles = [
519
+ "build.gradle",
520
+ "build.gradle.kts",
521
+ "app/build.gradle",
522
+ "app/build.gradle.kts"
523
+ ];
524
+ let allBuildContent = "";
525
+ for (const f of buildFiles) {
526
+ const c = readFileOrNull(p(root, f));
527
+ if (c) allBuildContent += "\n" + c;
528
+ }
529
+ let android_version = null;
530
+ if (/com\.cometchat:chatuikit-(compose|kotlin)-android:6/.test(allBuildContent)) {
531
+ android_version = "v6";
532
+ } else if (/com\.cometchat:chat-uikit-android:5/.test(allBuildContent)) {
533
+ android_version = "v5";
534
+ }
535
+ let android_language = null;
536
+ const usesKtsGradle = pathExists(p(root, "build.gradle.kts")) || pathExists(p(root, "app/build.gradle.kts")) || pathExists(p(root, "settings.gradle.kts"));
537
+ const hasKotlinPlugin = /(?:apply\s+plugin\s*:\s*['"]kotlin-android['"]|id\s*\(?\s*['"]org\.jetbrains\.kotlin\.android['"]|id\s*\(?\s*['"]kotlin-android['"])/.test(
538
+ allBuildContent
539
+ );
540
+ if (usesKtsGradle || hasKotlinPlugin) {
541
+ android_language = "kotlin";
542
+ } else {
543
+ const javaDir = p(root, "app/src/main/java");
544
+ const kotlinDir = p(root, "app/src/main/kotlin");
545
+ if (pathExists(kotlinDir)) android_language = "kotlin";
546
+ else if (pathExists(javaDir)) android_language = "java";
547
+ }
548
+ return {
549
+ framework: "android",
550
+ framework_version: null,
551
+ // Gradle/AGP version isn't load-bearing for the dispatcher
552
+ router: null,
553
+ bundler: null,
554
+ // No JS bundler for native Android
555
+ ssr_strategy: null,
556
+ env_prefix: "",
557
+ // No public-env-prefix — local.properties + BuildConfig is the convention
558
+ android_version,
559
+ android_language
560
+ };
561
+ }
562
+ function detectAngular(root, deps) {
563
+ const hasAngularJson = pathExists(p(root, "angular.json"));
564
+ const hasAngularCore = !!deps["@angular/core"];
565
+ if (!hasAngularJson && !hasAngularCore) return null;
566
+ const version = extractVersion(deps["@angular/core"] ?? "");
567
+ return {
568
+ framework: "angular",
569
+ framework_version: version,
570
+ router: null,
571
+ // Angular Router is a separate concern; not modeled here yet
572
+ bundler: "webpack",
573
+ // Angular CLI ships with webpack; esbuild for v17+ but irrelevant to integration
574
+ ssr_strategy: "spa-no-ssr",
575
+ // Angular Universal exists but the integration path is SPA
576
+ env_prefix: ""
577
+ // No public-env-prefix — environment.ts is the convention
578
+ };
579
+ }
437
580
  function detectReactjs(root, deps) {
438
581
  if (!deps["react"]) return null;
439
582
  const version = extractVersion(deps["react"]);
@@ -465,10 +608,16 @@ function detectUsesJsx(root) {
465
608
  return false;
466
609
  }
467
610
  function detectFramework(root) {
611
+ const flutterInfo = detectFlutter(root);
612
+ if (flutterInfo) return flutterInfo;
613
+ const iosInfo = detectIOS(root);
614
+ if (iosInfo) return iosInfo;
615
+ const androidInfo = detectAndroid(root);
616
+ if (androidInfo) return androidInfo;
468
617
  const pkg = readJsonOrNull(p(root, "package.json"));
469
618
  if (!pkg) return EMPTY;
470
619
  const deps = allDeps(pkg);
471
- return detectExpo(root, deps, pkg) ?? detectBareReactNative(root, deps) ?? detectNextjs(root, deps) ?? detectAstro(root, deps) ?? detectReactRouter(root, deps) ?? detectReactjs(root, deps) ?? EMPTY;
620
+ return detectExpo(root, deps, pkg) ?? detectBareReactNative(root, deps) ?? detectNextjs(root, deps) ?? detectAstro(root, deps) ?? detectReactRouter(root, deps) ?? detectAngular(root, deps) ?? detectReactjs(root, deps) ?? EMPTY;
472
621
  }
473
622
 
474
623
  // src/detectors/package-manager.ts
@@ -483,7 +632,7 @@ function detectPackageManager(root) {
483
632
 
484
633
  // src/detectors/credentials.ts
485
634
  init_fs();
486
- import { readdirSync, statSync } from "node:fs";
635
+ import { readdirSync as readdirSync2, statSync } from "node:fs";
487
636
  import { join as join2, extname } from "node:path";
488
637
  function envVarsForPrefix(prefix) {
489
638
  const p2 = prefix ?? "";
@@ -551,7 +700,7 @@ function searchForInit(dir, maxDepth) {
551
700
  if (maxDepth < 0) return false;
552
701
  let entries;
553
702
  try {
554
- entries = readdirSync(dir);
703
+ entries = readdirSync2(dir);
555
704
  } catch {
556
705
  return false;
557
706
  }
@@ -603,7 +752,7 @@ function detectCredentials(root, envPrefix) {
603
752
  }
604
753
 
605
754
  // src/detectors/integration.ts
606
- import { readdirSync as readdirSync2 } from "node:fs";
755
+ import { readdirSync as readdirSync3 } from "node:fs";
607
756
  init_fs();
608
757
  function getInstalledVersion(root) {
609
758
  const pkg = readJsonOrNull(p(root, "package.json"));
@@ -626,7 +775,7 @@ function findCometchatDirs(root) {
626
775
  const fullPath = p(root, dir);
627
776
  if (!pathExists(fullPath)) continue;
628
777
  try {
629
- const entries = readdirSync2(fullPath);
778
+ const entries = readdirSync3(fullPath);
630
779
  for (const entry of entries) {
631
780
  found.push(`${dir}/${entry}`);
632
781
  }
@@ -818,6 +967,34 @@ function detectArchitectureContext(root) {
818
967
  };
819
968
  }
820
969
 
970
+ // src/detectors/project-name.ts
971
+ init_fs();
972
+ function detectProjectName(root) {
973
+ const pkg = readJsonOrNull(p(root, "package.json"));
974
+ if (pkg?.name) return cleanName(pkg.name);
975
+ const pubspec = readFileOrNull(p(root, "pubspec.yaml"));
976
+ if (pubspec) {
977
+ const m = pubspec.match(/^\s*name\s*:\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*$/m);
978
+ if (m) return cleanName(m[1]);
979
+ }
980
+ const packageSwift = readFileOrNull(p(root, "Package.swift"));
981
+ if (packageSwift) {
982
+ const m = packageSwift.match(/Package\s*\(\s*name\s*:\s*"([^"]+)"/);
983
+ if (m) return cleanName(m[1]);
984
+ }
985
+ for (const file of ["settings.gradle.kts", "settings.gradle"]) {
986
+ const content = readFileOrNull(p(root, file));
987
+ if (!content) continue;
988
+ const m = content.match(/rootProject\.name\s*=\s*['"]([^'"\n]+)['"]/);
989
+ if (m) return cleanName(m[1]);
990
+ }
991
+ return null;
992
+ }
993
+ function cleanName(raw) {
994
+ const trimmed = raw.trim().slice(0, 64);
995
+ return trimmed.length > 0 ? trimmed : null;
996
+ }
997
+
821
998
  // src/detectors/index.ts
822
999
  async function runDetectors(root) {
823
1000
  const fw = detectFramework(root);
@@ -826,6 +1003,7 @@ async function runDetectors(root) {
826
1003
  const existing_integration = detectExistingIntegration(root);
827
1004
  const compatibility = computeCompatibility(fw);
828
1005
  const architecture_context = detectArchitectureContext(root);
1006
+ const project_name = detectProjectName(root);
829
1007
  return {
830
1008
  project_root: root,
831
1009
  framework: fw.framework,
@@ -839,6 +1017,11 @@ async function runDetectors(root) {
839
1017
  expo_mode: fw.expo_mode,
840
1018
  expo_version: fw.expo_version,
841
1019
  react_native_version: fw.react_native_version,
1020
+ android_version: fw.android_version,
1021
+ android_language: fw.android_language,
1022
+ flutter_version: fw.flutter_version,
1023
+ ios_version: fw.ios_version,
1024
+ project_name,
842
1025
  package_manager,
843
1026
  credentials,
844
1027
  existing_integration,
@@ -919,6 +1102,12 @@ function printHumanReadable(r) {
919
1102
  if (r.react_native_version && r.framework === "expo") {
920
1103
  lines.push(` React Native: ${r.react_native_version}`);
921
1104
  }
1105
+ if (r.framework === "android") {
1106
+ lines.push(` Android UIKit: ${r.android_version ?? "(none yet \u2014 greenfield)"}`);
1107
+ }
1108
+ if (r.framework === "flutter") {
1109
+ lines.push(` Flutter UIKit: ${r.flutter_version ?? "(none yet \u2014 greenfield)"}`);
1110
+ }
922
1111
  if (r.router !== null) lines.push(` Router: ${r.router}`);
923
1112
  if (r.bundler !== null) lines.push(` Bundler: ${r.bundler}`);
924
1113
  if (r.ssr_strategy !== null) lines.push(` SSR strategy: ${r.ssr_strategy}`);
@@ -3134,7 +3323,7 @@ function printHumanReadable5(r) {
3134
3323
  // src/commands/uninstall.ts
3135
3324
  init_state();
3136
3325
  init_fs();
3137
- import { rmSync as rmSync2, statSync as statSync3, rmdirSync, readdirSync as readdirSync3 } from "node:fs";
3326
+ import { rmSync as rmSync2, statSync as statSync3, rmdirSync, readdirSync as readdirSync4 } from "node:fs";
3138
3327
  import { dirname as dirname6, join as join9, resolve as resolve7 } from "node:path";
3139
3328
  var HELP6 = `
3140
3329
  cometchat uninstall \u2014 remove the CometChat integration cleanly
@@ -3168,7 +3357,7 @@ function projectPath6(args) {
3168
3357
  }
3169
3358
  function maybeRemoveEmptyDir(dir) {
3170
3359
  try {
3171
- const entries = readdirSync3(dir);
3360
+ const entries = readdirSync4(dir);
3172
3361
  if (entries.length === 0) {
3173
3362
  rmdirSync(dir);
3174
3363
  maybeRemoveEmptyDir(dirname6(dir));
@@ -3245,7 +3434,7 @@ async function uninstall(args) {
3245
3434
  try {
3246
3435
  const cometchatDir = p(root, ".cometchat");
3247
3436
  if (statSync3(cometchatDir).isDirectory()) {
3248
- const entries = readdirSync3(cometchatDir);
3437
+ const entries = readdirSync4(cometchatDir);
3249
3438
  if (entries.length === 0) rmdirSync(cometchatDir);
3250
3439
  }
3251
3440
  } catch {
@@ -5191,8 +5380,19 @@ async function listApps(host, token) {
5191
5380
  const name = String(entry.name ?? id);
5192
5381
  const region = String(entry.region ?? "us");
5193
5382
  const plan = typeof entry.plan === "string" ? entry.plan : void 0;
5194
- const base = { id, name, region };
5195
- return plan ? { ...base, plan } : base;
5383
+ const meta = entry.metadata && typeof entry.metadata === "object" ? entry.metadata : {};
5384
+ const industry = typeof meta.industry === "string" ? meta.industry : void 0;
5385
+ const technology = typeof meta.technology === "string" ? meta.technology : void 0;
5386
+ const product = typeof meta.product === "string" ? meta.product : void 0;
5387
+ const createdAtRaw = entry.createdAt ?? entry.created_at;
5388
+ const createdAt = typeof createdAtRaw === "number" ? createdAtRaw : typeof createdAtRaw === "string" ? Number(new Date(createdAtRaw)) || void 0 : void 0;
5389
+ const out = { id, name, region };
5390
+ if (plan) out.plan = plan;
5391
+ if (industry) out.industry = industry;
5392
+ if (technology) out.technology = technology;
5393
+ if (product) out.product = product;
5394
+ if (createdAt !== void 0) out.createdAt = createdAt;
5395
+ return out;
5196
5396
  });
5197
5397
  }
5198
5398
  async function getAppCredentials(host, token, appId) {
@@ -5219,6 +5419,40 @@ async function getAppCredentials(host, token, appId) {
5219
5419
  }
5220
5420
  return { appId: id, authKey, region };
5221
5421
  }
5422
+ async function getCurrentUser(host, token) {
5423
+ const res = await request(host, "/me", {
5424
+ method: "GET",
5425
+ headers: { Authorization: `Bearer ${token}` }
5426
+ });
5427
+ const body = await parseBody(res);
5428
+ if (!res.ok) {
5429
+ const { code, message } = extractErrorCode(body);
5430
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5431
+ throw new Error(`API_ERROR: ${code} ${message}`);
5432
+ }
5433
+ const data = body?.data;
5434
+ if (!data || typeof data !== "object") {
5435
+ throw new Error(`API_ERROR: UNEXPECTED_SHAPE ${JSON.stringify(body)}`);
5436
+ }
5437
+ return data;
5438
+ }
5439
+ async function getCurrentUserWithLastApp(host, token) {
5440
+ const user = await getCurrentUser(host, token);
5441
+ let last_app = null;
5442
+ try {
5443
+ const apps = await listApps(host, token);
5444
+ if (apps.length > 0) {
5445
+ const sorted = [...apps].sort((a, b) => {
5446
+ const aT = a.createdAt ?? 0;
5447
+ const bT = b.createdAt ?? 0;
5448
+ return bT - aT;
5449
+ });
5450
+ last_app = sorted[0];
5451
+ }
5452
+ } catch {
5453
+ }
5454
+ return { ...user, last_app };
5455
+ }
5222
5456
  async function listInstalledExtensions(host, token, appId) {
5223
5457
  const res = await request(
5224
5458
  host,
@@ -5249,6 +5483,49 @@ async function toggleExtension(host, token, appId, extensionId, action) {
5249
5483
  throw new Error(`API_ERROR: ${code} ${message}`);
5250
5484
  }
5251
5485
  }
5486
+ async function toggleAiFeature(host, token, appId, featureKey, action) {
5487
+ const path = `/apps/${encodeURIComponent(appId)}/features/ai.${encodeURIComponent(featureKey)}/enabled`;
5488
+ const res = await request(host, path, {
5489
+ method: action === "enable" ? "POST" : "DELETE",
5490
+ headers: { Authorization: `Bearer ${token}` },
5491
+ body: action === "enable" ? "{}" : void 0
5492
+ });
5493
+ if (!res.ok) {
5494
+ const body = await parseBody(res);
5495
+ const { code, message } = extractErrorCode(body);
5496
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5497
+ throw new Error(`API_ERROR: ${code} ${message}`);
5498
+ }
5499
+ }
5500
+ async function getAiSettings(host, token, appId) {
5501
+ const res = await request(host, `/apps/${encodeURIComponent(appId)}/ai/settings`, {
5502
+ method: "GET",
5503
+ headers: { Authorization: `Bearer ${token}` }
5504
+ });
5505
+ const body = await parseBody(res);
5506
+ if (!res.ok) {
5507
+ const { code, message } = extractErrorCode(body);
5508
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5509
+ if (res.status === 404) return null;
5510
+ throw new Error(`API_ERROR: ${code} ${message}`);
5511
+ }
5512
+ const data = body?.data;
5513
+ if (!data || typeof data !== "object") return null;
5514
+ return data;
5515
+ }
5516
+ async function updateAiSettings(host, token, appId, settings) {
5517
+ const res = await request(host, `/apps/${encodeURIComponent(appId)}/ai/settings`, {
5518
+ method: "PUT",
5519
+ headers: { Authorization: `Bearer ${token}` },
5520
+ body: JSON.stringify(settings)
5521
+ });
5522
+ if (!res.ok) {
5523
+ const body = await parseBody(res);
5524
+ const { code, message } = extractErrorCode(body);
5525
+ if (res.status === 401) throw new Error("AUTH_FAILED");
5526
+ throw new Error(`API_ERROR: ${code} ${message}`);
5527
+ }
5528
+ }
5252
5529
  async function createApp(host, token, params) {
5253
5530
  const res = await request(host, "/apps", {
5254
5531
  method: "POST",
@@ -5347,40 +5624,58 @@ function nextStepsForFeature(feature, framework = "reactjs") {
5347
5624
  feature.requires_dashboard_setup ? "Some features (like moderation) need rules configured in the CometChat dashboard before they activate. Check the dashboard." : "No code changes needed \u2014 it's already there.",
5348
5625
  ...feature.docs_topic ? [`For details, query the docs MCP for "${feature.docs_topic}" or visit https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
5349
5626
  ];
5350
- case "dashboard-toggle": {
5627
+ case "extension": {
5351
5628
  const docsUrl = feature.docs_topic ? `https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}` : "https://www.cometchat.com/docs/ui-kit/react/extensions";
5352
5629
  const lines = [];
5353
5630
  if (feature.auto_wired_in_uikit) {
5354
5631
  lines.push(
5355
- `${feature.name} is in the UI Kit's defaultExtensions[] \u2014 its UI decorator is attached automatically by initiateAfterLogin(). The only thing you need to do is flip the dashboard toggle.`,
5632
+ `${feature.name} is in the UI Kit's defaultExtensions[] \u2014 its UI decorator is attached automatically by initiateAfterLogin().`,
5356
5633
  "",
5357
5634
  "Steps to enable:",
5358
- ` 1. Go to https://app.cometchat.com and select your app`,
5359
- ` 2. Navigate to: ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}`,
5360
- ` (this is a hint \u2014 if the dashboard UI has changed, see the canonical docs link below)`,
5361
- ` 3. Toggle the feature on (and configure any required values)`,
5362
- ` 4. Refresh your dev server \u2014 no code changes required`,
5635
+ ` 1. Run \`cometchat apply-feature ${feature.id}\` \u2014 the CLI flips the dashboard toggle via API.`,
5636
+ ` 2. Refresh your dev server \u2014 no code changes required.`,
5363
5637
  "",
5364
- ` \u{1F4D6} Canonical docs (always-current \u2014 use this if the dashboard navigation above doesn't match what you see):`,
5365
- ` ${docsUrl}`,
5366
- ` For exact, up-to-date integration steps, query the cometchat-docs MCP for "${feature.id}".`
5638
+ ` Manual fallback (if API isn't available): https://app.cometchat.com \u2192 ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}`,
5639
+ ` \u{1F4D6} Canonical docs: ${docsUrl}`
5367
5640
  );
5368
5641
  } else {
5369
5642
  lines.push(
5370
5643
  `${feature.name} is a dashboard-toggle extension that ALSO requires explicit registration in your UIKitSettingsBuilder. The UI Kit only auto-attaches the 7 extensions in defaultExtensions[]; this one is not in that list.`,
5371
5644
  "",
5372
5645
  "Steps to enable:",
5373
- ` 1. Go to https://app.cometchat.com \u2192 ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}, toggle on`,
5374
- ` (dashboard hint \u2014 see canonical docs link below if the navigation has changed)`,
5646
+ ` 1. Run \`cometchat apply-feature ${feature.id}\` \u2014 the CLI flips the dashboard toggle via API.`,
5375
5647
  ` 2. Register the extension via UIKitSettingsBuilder.setExtensions([...]) in your CometChat init.`,
5376
5648
  ` For the exact import path + extension class name + builder syntax, query the cometchat-docs MCP for "${feature.id}" \u2014 DO NOT invent the API from memory.`,
5377
- ` 3. Restart your dev server`,
5649
+ ` 3. Restart your dev server.`,
5378
5650
  "",
5379
5651
  ` \u{1F4D6} Canonical docs: ${docsUrl}`
5380
5652
  );
5381
5653
  }
5382
5654
  return lines;
5383
5655
  }
5656
+ case "ai-feature": {
5657
+ const docsUrl = feature.docs_topic ? `https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}` : "https://www.cometchat.com/docs/ui-kit/react/ai-features";
5658
+ return [
5659
+ `${feature.name} is an AI feature that requires an OpenAI API key on the app's AI settings.`,
5660
+ "",
5661
+ "Steps to enable:",
5662
+ ` 1. Run \`cometchat apply-feature ${feature.id} --openai-key sk-...\` \u2014 the CLI stores the key and flips the AI toggle via API.`,
5663
+ ` 2. Refresh your dev server.`,
5664
+ "",
5665
+ ` Get an OpenAI key: https://platform.openai.com/api-keys`,
5666
+ ` \u{1F4D6} Canonical docs: ${docsUrl}`
5667
+ ];
5668
+ }
5669
+ case "dashboard-only":
5670
+ return [
5671
+ `${feature.name} requires third-party config (API key, webhook, etc.) that only you can provide. The CLI cannot automate this.`,
5672
+ "",
5673
+ "Steps to enable:",
5674
+ ` 1. Open https://app.cometchat.com \u2192 ${feature.dashboard_path ?? "Extensions \u2192 " + feature.name}`,
5675
+ ` 2. Enter the required configuration values.`,
5676
+ ` 3. Toggle the feature on; refresh your dev server.`,
5677
+ ...feature.docs_topic ? ["", ` \u{1F4D6} Canonical docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
5678
+ ];
5384
5679
  case "package-install": {
5385
5680
  const pkg = rn && feature.package_native ? feature.package_native : feature.package;
5386
5681
  const peers = rn ? feature.package_native_peers ?? [] : [];
@@ -5498,7 +5793,7 @@ async function featuresToggle(args, featureName, action) {
5498
5793
  if (json) {
5499
5794
  console.log(JSON.stringify({
5500
5795
  ...result2,
5501
- available: catalog.features.filter((f) => f.type === "dashboard-toggle").map((f) => f.id)
5796
+ available: catalog.features.filter((f) => f.type === "extension").map((f) => f.id)
5502
5797
  }, null, 2));
5503
5798
  } else {
5504
5799
  console.error(`\u2717 Feature "${featureName}" not found.`);
@@ -5506,11 +5801,12 @@ async function featuresToggle(args, featureName, action) {
5506
5801
  }
5507
5802
  return 1;
5508
5803
  }
5509
- if (match.type !== "dashboard-toggle") {
5804
+ if (match.type !== "extension") {
5805
+ const hint = match.type === "ai-feature" ? `Use \`cometchat apply-feature ${match.id} --openai-key <sk-\u2026>\` to enable AI features.` : `Run \`cometchat features info ${match.id}\` for the right instructions.`;
5510
5806
  const result2 = {
5511
5807
  status: "unsupported-type",
5512
5808
  feature: match,
5513
- error: `"${match.name}" is type "${match.type}" \u2014 not a dashboard toggle. Run \`cometchat features info ${match.id}\` for the right instructions.`
5809
+ error: `"${match.name}" is type "${match.type}" \u2014 not a dashboard toggle. ${hint}`
5514
5810
  };
5515
5811
  if (json) {
5516
5812
  console.log(JSON.stringify(result2, null, 2));
@@ -5651,7 +5947,9 @@ function featuresList(args) {
5651
5947
  }
5652
5948
  const order = [
5653
5949
  "default",
5654
- "dashboard-toggle",
5950
+ "extension",
5951
+ "ai-feature",
5952
+ "dashboard-only",
5655
5953
  "package-install",
5656
5954
  "component-swap"
5657
5955
  ];
@@ -6804,21 +7102,52 @@ init_state();
6804
7102
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
6805
7103
  import { join as join15, resolve as resolve16 } from "node:path";
6806
7104
  var HELP15 = `
6807
- cometchat apply-feature \u2014 apply a component-swap feature on an integration
7105
+ cometchat apply-feature \u2014 apply a feature on top of an existing integration
6808
7106
 
6809
7107
  Usage:
6810
- cometchat apply-feature <feature-id> [--path <p>] [--json]
7108
+ cometchat apply-feature <feature-id> [--app-id <id>] [--openai-key <k>] [--path <p>] [--json]
6811
7109
 
6812
7110
  What this does:
6813
- Applies a component-swap feature (e.g. rich-text-formatting) on top of
6814
- an existing integration. Walks state.files_owned, runs a word-boundary
6815
- regex replace of swap_from \u2192 swap_to, updates checksums, and records the
6816
- feature id in state.applied_features. Idempotent.
7111
+ Dispatches by feature type from the catalog:
7112
+ extension Calls the dashboard API to flip the toggle. No code change.
7113
+ ai-feature Same, plus prompts/accepts an OpenAI key once per app.
7114
+ component-swap Walks files_owned and runs a word-boundary identifier
7115
+ swap (e.g. CometChatMessageComposer \u2192
7116
+ CometChatCompactMessageComposer). Updates checksums.
7117
+ default No-op \u2014 feature is already enabled by the UI Kit.
7118
+ dashboard-only Prints the dashboard path. Cannot automate (third-party
7119
+ keys, multi-field config).
7120
+ package-install Prints the npm install command. Cannot automate.
7121
+
7122
+ In stateful mode (default for web/RN where \`cometchat apply\` was run):
7123
+ - Reads .cometchat/state.json
7124
+ - Resolves the App ID from the project's env file
7125
+ - Records applied features in state.json for idempotency
7126
+
7127
+ In stateless mode (\`--app-id\` passed; for native iOS/Android/Flutter/Angular
7128
+ projects that don't go through \`cometchat apply\`):
7129
+ - Skips state.json
7130
+ - Uses the explicit --app-id value
7131
+ - Skips applied_features recording (server-side toggles are idempotent)
7132
+ - component-swap is unavailable (no files_owned to walk)
6817
7133
 
6818
7134
  Flags:
6819
- --path <p> Project root (defaults to cwd).
6820
- --json Machine-readable JSON output.
6821
- --help, -h Show this help.
7135
+ --app-id <id> Stateless mode \u2014 toggle the feature on this app via the
7136
+ dashboard API without requiring \`cometchat apply\` to
7137
+ have been run first. Required for native cohorts
7138
+ (iOS / Android / Flutter / Angular).
7139
+ --openai-key <k> Pre-fill the OpenAI API key for ai-feature applies. If
7140
+ omitted, the command checks the app's existing AI
7141
+ settings; if no key is set, returns an error asking
7142
+ the user to pass --openai-key.
7143
+ --path <p> Project root (defaults to cwd).
7144
+ --json Machine-readable JSON output.
7145
+ --help, -h Show this help.
7146
+
7147
+ Auth:
7148
+ extension and ai-feature applies require a dashboard bearer token.
7149
+ Run \`cometchat auth login\` first if you haven't already. The token is
7150
+ stored in the OS keychain (or ~/.cometchat-cli/credentials.json fallback).
6822
7151
  `;
6823
7152
  function isJsonMode16(args) {
6824
7153
  return args.flags.json === true || args.flags.json === "true";
@@ -6834,6 +7163,24 @@ function catalogPath2() {
6834
7163
  function loadCatalog2() {
6835
7164
  return JSON.parse(readFileSync12(catalogPath2(), "utf8"));
6836
7165
  }
7166
+ function resolveAppId2(root, state2) {
7167
+ const envFileName = envFileNameFor(state2.framework);
7168
+ if (!envFileName) return null;
7169
+ const envPath = join15(root, envFileName);
7170
+ let content;
7171
+ try {
7172
+ content = readFileSync12(envPath, "utf8");
7173
+ } catch {
7174
+ return null;
7175
+ }
7176
+ const pairs = parseEnvPairs(content);
7177
+ const prefix = state2.env_var_prefix ?? "";
7178
+ const prefixed = pairs.get(`${prefix}COMETCHAT_APP_ID`);
7179
+ const bare = pairs.get("COMETCHAT_APP_ID");
7180
+ const value = (prefixed ?? bare ?? "").trim();
7181
+ if (value.length === 0 || isPlaceholder(value)) return null;
7182
+ return value;
7183
+ }
6837
7184
  function swapIdentifier(content, from, to) {
6838
7185
  const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6839
7186
  const re = new RegExp(`\\b${escaped}\\b`, "g");
@@ -6862,15 +7209,22 @@ async function applyFeature(args) {
6862
7209
  result.error = "Usage: cometchat apply-feature <feature-id>";
6863
7210
  return outputResult11(args, result, 1);
6864
7211
  }
6865
- if (!hasState(root)) {
7212
+ const appIdFlag = args.flags["app-id"];
7213
+ const explicitAppId = typeof appIdFlag === "string" ? appIdFlag.trim() : "";
7214
+ let state2 = null;
7215
+ let stateless = false;
7216
+ if (explicitAppId.length > 0) {
7217
+ stateless = true;
7218
+ } else if (hasState(root)) {
7219
+ state2 = readState(root);
7220
+ if (!state2) {
7221
+ result.status = "error";
7222
+ result.error = "state.json exists but could not be parsed (schema mismatch?)";
7223
+ return outputResult11(args, result, 1);
7224
+ }
7225
+ } else {
6866
7226
  result.status = "no-integration";
6867
- result.error = "No CometChat integration found. Run `cometchat apply` first.";
6868
- return outputResult11(args, result, 1);
6869
- }
6870
- const state2 = readState(root);
6871
- if (!state2) {
6872
- result.status = "error";
6873
- result.error = "state.json exists but could not be parsed (schema mismatch?)";
7227
+ result.error = "No CometChat integration found. Run `cometchat apply` first, or pass `--app-id <id>` for stateless API toggles (native cohorts: iOS/Android/Flutter/Angular).";
6874
7228
  return outputResult11(args, result, 1);
6875
7229
  }
6876
7230
  let catalog;
@@ -6887,26 +7241,192 @@ async function applyFeature(args) {
6887
7241
  result.error = `Feature "${featureId}" not in the catalog. Run \`cometchat features list\` to see available features.`;
6888
7242
  return outputResult11(args, result, 1);
6889
7243
  }
6890
- if (feature.type !== "component-swap") {
6891
- result.status = "unsupported-feature-type";
6892
- result.error = `Feature "${featureId}" is type "${feature.type}", not "component-swap". Only component-swap features can be applied via this command.`;
7244
+ result.feature_type = feature.type;
7245
+ if (state2?.applied_features?.includes(featureId)) {
7246
+ result.status = "already-applied";
7247
+ result.next_steps = [`${feature.name} is already applied to this integration.`];
7248
+ return outputResult11(args, result, 0);
7249
+ }
7250
+ switch (feature.type) {
7251
+ case "default":
7252
+ return applyDefault(args, result, feature);
7253
+ case "component-swap":
7254
+ if (stateless || !state2) {
7255
+ result.status = "state-required";
7256
+ result.error = `Feature "${feature.id}" is type component-swap and requires \`cometchat apply\` to have run first (so we know which files to swap). \`--app-id\` mode does not support component-swap features.`;
7257
+ return outputResult11(args, result, 1);
7258
+ }
7259
+ return applyComponentSwap(args, result, root, state2, feature);
7260
+ case "extension":
7261
+ return applyExtensionToggle(args, result, root, state2, explicitAppId, feature);
7262
+ case "ai-feature":
7263
+ return applyAiFeatureToggle(args, result, root, state2, explicitAppId, feature);
7264
+ case "dashboard-only":
7265
+ return applyDashboardOnly(args, result, feature);
7266
+ case "package-install":
7267
+ return applyPackageInstall(args, result, feature);
7268
+ default: {
7269
+ result.status = "unsupported-feature-type";
7270
+ result.error = `Feature "${featureId}" has unknown type "${feature.type}".`;
7271
+ return outputResult11(args, result, 1);
7272
+ }
7273
+ }
7274
+ }
7275
+ function applyDefault(args, result, feature) {
7276
+ result.status = "already-applied";
7277
+ result.next_steps = [
7278
+ `${feature.name} is enabled by default in the UI Kit. No action needed.`,
7279
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7280
+ ];
7281
+ return outputResult11(args, result, 0);
7282
+ }
7283
+ async function applyExtensionToggle(args, result, root, state2, explicitAppId, feature) {
7284
+ const host = getApiHost();
7285
+ const creds = await loadToken(host);
7286
+ if (!creds) {
7287
+ result.status = "auth-required";
7288
+ result.error = "No dashboard bearer token found.";
6893
7289
  result.next_steps = [
6894
- feature.type === "default" ? `${feature.name} is already enabled \u2014 no action needed.` : feature.type === "dashboard-toggle" ? `${feature.name} is enabled in the CometChat dashboard, not via this command. Run \`cometchat features info ${featureId}\`.` : feature.type === "package-install" ? `${feature.name} requires \`npm install\` of an extra package. Run \`cometchat features info ${featureId}\`.` : `Run \`cometchat features info ${featureId}\` for instructions.`
7290
+ "Run `cometchat auth login` to authorize the CLI, then re-run this command."
6895
7291
  ];
6896
7292
  return outputResult11(args, result, 1);
6897
7293
  }
6898
- if (!feature.swap_from || !feature.swap_to) {
7294
+ const appId = explicitAppId || (state2 ? resolveAppId2(root, state2) : null);
7295
+ if (!appId) {
6899
7296
  result.status = "error";
6900
- result.error = `Feature "${featureId}" is missing swap_from / swap_to in the catalog.`;
7297
+ result.error = state2 ? "Could not read COMETCHAT_APP_ID from the project's env file." : "App ID is required. Pass `--app-id <id>`.";
7298
+ result.next_steps = state2 ? [`Confirm \`${state2.env_var_prefix ?? ""}COMETCHAT_APP_ID\` is set in your env file.`] : ["Run `cometchat apply-feature <id> --app-id <your-app-id>`."];
6901
7299
  return outputResult11(args, result, 1);
6902
7300
  }
6903
- if (state2.applied_features?.includes(featureId)) {
6904
- result.status = "already-applied";
7301
+ try {
7302
+ await toggleExtension(host, creds.token, appId, feature.id, "enable");
7303
+ } catch (err) {
7304
+ return handleApiError(args, result, err, feature);
7305
+ }
7306
+ if (state2) {
7307
+ state2.applied_features = [...state2.applied_features ?? [], feature.id];
7308
+ writeState(root, state2);
7309
+ appendAuditEntry(root, {
7310
+ command: `apply-feature ${feature.id}`,
7311
+ summary: `Enabled extension "${feature.name}" via dashboard API on app ${appId}`,
7312
+ inputs: { feature: feature.id, appId },
7313
+ decisions: {
7314
+ api_endpoint: `POST /apps/${appId}/extensions { enabled: ["${feature.id}"] }`,
7315
+ auto_wired: feature.auto_wired_in_uikit ? "yes" : "no"
7316
+ },
7317
+ files_modified: [],
7318
+ next_actions: [`Refresh the chat in the browser. ${feature.name} is now active.`]
7319
+ });
7320
+ }
7321
+ result.next_steps = [
7322
+ `Enabled ${feature.name} on app ${appId}.`,
7323
+ feature.auto_wired_in_uikit ? "The UI Kit's defaultExtensions[] picks this up automatically \u2014 refresh the browser." : "Refresh the chat in the browser. If the feature requires UIKitSettingsBuilder.setExtensions([...]), see the docs link below.",
7324
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7325
+ ];
7326
+ return outputResult11(args, result, 0);
7327
+ }
7328
+ async function applyAiFeatureToggle(args, result, root, state2, explicitAppId, feature) {
7329
+ if (!feature.ai_key) {
7330
+ result.status = "error";
7331
+ result.error = `Feature "${feature.id}" is type ai-feature but has no ai_key in the catalog.`;
7332
+ return outputResult11(args, result, 1);
7333
+ }
7334
+ const host = getApiHost();
7335
+ const creds = await loadToken(host);
7336
+ if (!creds) {
7337
+ result.status = "auth-required";
7338
+ result.error = "No dashboard bearer token found.";
6905
7339
  result.next_steps = [
6906
- `${feature.name} is already applied to this integration.`,
6907
- `Files using ${feature.swap_to}: scan with \`grep -r "${feature.swap_to}" .\`.`
7340
+ "Run `cometchat auth login` to authorize the CLI, then re-run this command."
6908
7341
  ];
6909
- return outputResult11(args, result, 0);
7342
+ return outputResult11(args, result, 1);
7343
+ }
7344
+ const appId = explicitAppId || (state2 ? resolveAppId2(root, state2) : null);
7345
+ if (!appId) {
7346
+ result.status = "error";
7347
+ result.error = state2 ? "Could not read COMETCHAT_APP_ID from the project's env file." : "App ID is required. Pass `--app-id <id>`.";
7348
+ result.next_steps = state2 ? [`Confirm \`${state2.env_var_prefix ?? ""}COMETCHAT_APP_ID\` is set in your env file.`] : ["Run `cometchat apply-feature <id> --app-id <your-app-id> --openai-key <sk-\u2026>`."];
7349
+ return outputResult11(args, result, 1);
7350
+ }
7351
+ const openaiFlag = args.flags["openai-key"];
7352
+ const explicitKey = typeof openaiFlag === "string" ? openaiFlag.trim() : "";
7353
+ let needsKeyWrite = false;
7354
+ if (explicitKey.length > 0) {
7355
+ needsKeyWrite = true;
7356
+ } else {
7357
+ let existing;
7358
+ try {
7359
+ existing = await getAiSettings(host, creds.token, appId);
7360
+ } catch (err) {
7361
+ return handleApiError(args, result, err, feature);
7362
+ }
7363
+ const currentKey = (existing?.openAIKey ?? "").trim();
7364
+ if (currentKey.length === 0) {
7365
+ result.status = "openai-key-required";
7366
+ result.error = `${feature.name} requires an OpenAI API key. None is set on app ${appId}.`;
7367
+ result.next_steps = [
7368
+ "Pass `--openai-key <sk-\u2026>` to set the key and enable the feature in one step.",
7369
+ "Get a key from https://platform.openai.com/api-keys"
7370
+ ];
7371
+ return outputResult11(args, result, 1);
7372
+ }
7373
+ }
7374
+ try {
7375
+ if (needsKeyWrite) {
7376
+ await updateAiSettings(host, creds.token, appId, { openAIKey: explicitKey });
7377
+ }
7378
+ await toggleAiFeature(host, creds.token, appId, feature.ai_key, "enable");
7379
+ } catch (err) {
7380
+ return handleApiError(args, result, err, feature);
7381
+ }
7382
+ if (state2) {
7383
+ state2.applied_features = [...state2.applied_features ?? [], feature.id];
7384
+ writeState(root, state2);
7385
+ appendAuditEntry(root, {
7386
+ command: `apply-feature ${feature.id}`,
7387
+ summary: `Enabled AI feature "${feature.name}" via dashboard API on app ${appId}`,
7388
+ inputs: { feature: feature.id, appId, openai_key_written: needsKeyWrite },
7389
+ decisions: {
7390
+ settings_endpoint: needsKeyWrite ? `PUT /apps/${appId}/ai/settings { openAIKey: "(redacted)" }` : "skipped (key already set)",
7391
+ toggle_endpoint: `POST /apps/${appId}/features/ai.${feature.ai_key}/enabled`
7392
+ },
7393
+ files_modified: [],
7394
+ next_actions: [`Refresh the chat. AI ${feature.name} is now active.`]
7395
+ });
7396
+ }
7397
+ result.next_steps = [
7398
+ `Enabled ${feature.name} on app ${appId}.`,
7399
+ needsKeyWrite ? "OpenAI key stored on the app's AI settings." : "OpenAI key was already set.",
7400
+ "Refresh the chat in the browser to see the feature in action.",
7401
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7402
+ ];
7403
+ return outputResult11(args, result, 0);
7404
+ }
7405
+ function applyDashboardOnly(args, result, feature) {
7406
+ result.status = "manual-action-required";
7407
+ result.error = `${feature.name} requires config beyond a boolean toggle (third-party API key or multi-field setup).`;
7408
+ result.next_steps = [
7409
+ feature.dashboard_path ? `In the dashboard at https://app.cometchat.com \u2192 ${feature.dashboard_path}` : `Open https://app.cometchat.com and configure ${feature.name} on the Extensions page.`,
7410
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7411
+ ];
7412
+ return outputResult11(args, result, 1);
7413
+ }
7414
+ function applyPackageInstall(args, result, feature) {
7415
+ result.status = "manual-action-required";
7416
+ result.error = `${feature.name} requires installing an additional package.`;
7417
+ const pkg = feature.package ?? "the calls SDK";
7418
+ result.next_steps = [
7419
+ `Run \`npm install ${pkg}\` and restart your dev server.`,
7420
+ "The UI Kit's initiateAfterLogin() detects the SDK and wires up call buttons automatically.",
7421
+ ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
7422
+ ];
7423
+ return outputResult11(args, result, 1);
7424
+ }
7425
+ function applyComponentSwap(args, result, root, state2, feature) {
7426
+ if (!feature.swap_from || !feature.swap_to) {
7427
+ result.status = "error";
7428
+ result.error = `Feature "${feature.id}" is missing swap_from / swap_to in the catalog.`;
7429
+ return outputResult11(args, result, 1);
6910
7430
  }
6911
7431
  const newChecksums = { ...state2.checksums };
6912
7432
  const modified = [];
@@ -6939,7 +7459,7 @@ async function applyFeature(args) {
6939
7459
  const nextState = {
6940
7460
  ...state2,
6941
7461
  checksums: newChecksums,
6942
- applied_features: [...state2.applied_features ?? [], featureId]
7462
+ applied_features: [...state2.applied_features ?? [], feature.id]
6943
7463
  };
6944
7464
  writeState(root, nextState);
6945
7465
  result.files_modified = modified;
@@ -6950,9 +7470,9 @@ async function applyFeature(args) {
6950
7470
  ...feature.docs_topic ? [`Docs: https://www.cometchat.com/docs/ui-kit/react/${feature.docs_topic}`] : []
6951
7471
  ];
6952
7472
  appendAuditEntry(root, {
6953
- command: `apply-feature ${featureId}`,
7473
+ command: `apply-feature ${feature.id}`,
6954
7474
  summary: `Applied component-swap feature "${feature.name}" \u2014 replaced ${feature.swap_from} with ${feature.swap_to} in ${modified.length} owned file(s)`,
6955
- inputs: { feature: featureId },
7475
+ inputs: { feature: feature.id },
6956
7476
  decisions: {
6957
7477
  swap_target: `${feature.swap_from} \u2192 ${feature.swap_to} (canonical drop-in variant from @cometchat/chat-uikit-react)`,
6958
7478
  file_selection: `walked state.files_owned and ran a word-boundary regex replace; only files that contained the canonical token were touched`,
@@ -6963,6 +7483,24 @@ async function applyFeature(args) {
6963
7483
  });
6964
7484
  return outputResult11(args, result, 0);
6965
7485
  }
7486
+ function handleApiError(args, result, err, feature) {
7487
+ const msg = err instanceof Error ? err.message : String(err);
7488
+ if (msg === "AUTH_FAILED") {
7489
+ result.status = "auth-required";
7490
+ result.error = "Bearer token rejected (expired or invalid).";
7491
+ result.next_steps = ["Run `cometchat auth login` to refresh the token, then re-run this command."];
7492
+ return outputResult11(args, result, 1);
7493
+ }
7494
+ if (msg === "NETWORK") {
7495
+ result.status = "error";
7496
+ result.error = "Network error reaching the dashboard API.";
7497
+ result.next_steps = ["Check your connection and re-run."];
7498
+ return outputResult11(args, result, 1);
7499
+ }
7500
+ result.status = "error";
7501
+ result.error = `Failed to enable ${feature.name}: ${msg}`;
7502
+ return outputResult11(args, result, 1);
7503
+ }
6966
7504
  function outputResult11(args, result, exitCode) {
6967
7505
  if (isJsonMode16(args)) {
6968
7506
  console.log(JSON.stringify(enrichResultWithError(result), null, 2));
@@ -6976,9 +7514,11 @@ function printHumanReadable14(r) {
6976
7514
  lines.push("");
6977
7515
  if (r.status === "applied") {
6978
7516
  lines.push(` \u2713 Applied feature: ${r.feature}`);
6979
- lines.push("");
6980
- lines.push(" Files modified:");
6981
- for (const f of r.files_modified) lines.push(` ${f}`);
7517
+ if (r.files_modified.length > 0) {
7518
+ lines.push("");
7519
+ lines.push(" Files modified:");
7520
+ for (const f of r.files_modified) lines.push(` ${f}`);
7521
+ }
6982
7522
  } else if (r.status === "already-applied") {
6983
7523
  lines.push(` \u25E6 ${r.feature} is already applied \u2014 nothing to do.`);
6984
7524
  } else {
@@ -7551,6 +8091,7 @@ Usage:
7551
8091
  cometchat auth login [--token <bearer>] [--json]
7552
8092
  cometchat auth signup [--json]
7553
8093
  cometchat auth status [--json]
8094
+ cometchat auth me [--json]
7554
8095
  cometchat auth logout [--json]
7555
8096
 
7556
8097
  \`auth login\` and \`auth signup\` open your browser at the CometChat
@@ -7589,6 +8130,8 @@ async function auth(args) {
7589
8130
  return authLogin(args);
7590
8131
  case "status":
7591
8132
  return authStatus(args);
8133
+ case "me":
8134
+ return authMe(args);
7592
8135
  case "logout":
7593
8136
  return authLogout(args);
7594
8137
  case "signup":
@@ -7747,6 +8290,81 @@ async function authStatus(args) {
7747
8290
  console.log("");
7748
8291
  return 0;
7749
8292
  }
8293
+ async function authMe(args) {
8294
+ const json = isJsonMode18(args);
8295
+ const apiHost = getApiHost();
8296
+ const creds = await loadToken(apiHost);
8297
+ if (!creds) {
8298
+ return printError(json, {
8299
+ status: "logged-out",
8300
+ error: "No bearer token found. Run `cometchat auth login` first."
8301
+ });
8302
+ }
8303
+ let user;
8304
+ try {
8305
+ user = await getCurrentUserWithLastApp(apiHost, creds.token);
8306
+ } catch (err) {
8307
+ const msg = err instanceof Error ? err.message : String(err);
8308
+ if (msg === "AUTH_FAILED") {
8309
+ return printError(json, {
8310
+ status: "auth-required",
8311
+ error: "Bearer token rejected (expired or invalid).",
8312
+ next_step: "Run `cometchat auth login` to refresh the token."
8313
+ });
8314
+ }
8315
+ return printError(json, { status: "error", error: msg });
8316
+ }
8317
+ const meta = user.meta ?? {};
8318
+ const lastApp = user.last_app ?? null;
8319
+ const payload = {
8320
+ status: "logged-in",
8321
+ email: user.email ?? creds.email ?? null,
8322
+ name: user.name ?? null,
8323
+ role: typeof meta.role === "string" ? meta.role : null,
8324
+ other_role: typeof meta.otherRole === "string" && meta.otherRole.length > 0 ? meta.otherRole : null,
8325
+ intent: typeof meta.intent === "string" ? meta.intent : null,
8326
+ last_app: lastApp ? {
8327
+ id: lastApp.id,
8328
+ name: lastApp.name,
8329
+ region: lastApp.region,
8330
+ industry: lastApp.industry ?? null,
8331
+ technology: lastApp.technology ?? null,
8332
+ product: lastApp.product ?? null
8333
+ } : null
8334
+ };
8335
+ if (json) {
8336
+ console.log(JSON.stringify(payload, null, 2));
8337
+ return 0;
8338
+ }
8339
+ console.log("");
8340
+ console.log(` \u2713 ${payload.email ?? "(no email on profile)"}`);
8341
+ if (payload.name) console.log(` Name: ${payload.name}`);
8342
+ console.log(` Role: ${payload.role ?? "(not set)"}${payload.other_role ? ` (${payload.other_role})` : ""}`);
8343
+ console.log(` Intent: ${payload.intent ?? "(not set)"}`);
8344
+ if (payload.last_app) {
8345
+ const la = payload.last_app;
8346
+ console.log(` Last app: ${la.name} (${la.region})`);
8347
+ if (la.industry || la.technology || la.product) {
8348
+ const bits = [
8349
+ la.industry && `industry=${la.industry}`,
8350
+ la.technology && `tech=${la.technology}`,
8351
+ la.product && `product=${la.product}`
8352
+ ].filter(Boolean);
8353
+ console.log(` ${bits.join(", ")}`);
8354
+ }
8355
+ }
8356
+ console.log("");
8357
+ return 0;
8358
+ }
8359
+ function printError(json, payload) {
8360
+ if (json) {
8361
+ console.log(JSON.stringify(payload, null, 2));
8362
+ } else {
8363
+ console.error(`\u2717 ${payload.error ?? payload.status}`);
8364
+ if (payload.next_step) console.error(` ${payload.next_step}`);
8365
+ }
8366
+ return 1;
8367
+ }
7750
8368
  async function authLogout(args) {
7751
8369
  const json = isJsonMode18(args);
7752
8370
  const verbose = args.flags.verbose === true || args.flags.verbose === "true";
@@ -7934,7 +8552,7 @@ async function provisionSetup(args) {
7934
8552
  return 1;
7935
8553
  }
7936
8554
  if (!frameworkFlag) {
7937
- const msg = "Missing --framework. One of: reactjs, nextjs, react-router, astro, expo, react-native.";
8555
+ const msg = "Missing --framework. One of: reactjs, nextjs, react-router, astro, expo, react-native, angular, android, flutter, ios.";
7938
8556
  if (json) return emitError(true, msg);
7939
8557
  console.error(msg);
7940
8558
  return 1;
@@ -8025,8 +8643,29 @@ var ENV_PREFIX_BY_FRAMEWORK = {
8025
8643
  "react-router": "VITE_",
8026
8644
  astro: "PUBLIC_",
8027
8645
  expo: "EXPO_PUBLIC_",
8028
- "react-native": ""
8646
+ "react-native": "",
8029
8647
  // bare RN uses react-native-dotenv — no prefix
8648
+ angular: "",
8649
+ // Angular reads from src/environments/environment.ts; CLI writes
8650
+ // a no-prefix .env as a credentials handoff, and the skill
8651
+ // migrates the values into environment.ts during integration
8652
+ android: "",
8653
+ // Android reads from local.properties → BuildConfig at compile
8654
+ // time. CLI writes a no-prefix .env handoff; the skill teaches
8655
+ // the user to mirror values into local.properties + add
8656
+ // buildConfigField entries to app/build.gradle.
8657
+ flutter: "",
8658
+ // Flutter has no runtime .env. Credentials come from
8659
+ // --dart-define flags or a generated config Dart file. CLI
8660
+ // writes a no-prefix .env handoff; the skill teaches the
8661
+ // integration agent how to wire creds (typically a
8662
+ // const-class file + runner script with --dart-define).
8663
+ ios: ""
8664
+ // iOS has no runtime .env. Credentials live in a
8665
+ // Secrets.swift const file (gitignored) or get injected
8666
+ // via xcconfig build settings. CLI writes a no-prefix
8667
+ // .env handoff for the agent; the skill teaches the
8668
+ // Secrets.swift / xcconfig migration.
8030
8669
  };
8031
8670
  async function provisionList(args) {
8032
8671
  const json = isJsonMode19(args);
@@ -5,7 +5,9 @@
5
5
  "description": "Catalog of CometChat features available in the React UI Kit. Categorized by what work is needed to enable each one. NOTE: this catalog stores OUR taxonomy (default vs dashboard-toggle vs package-install vs component-swap) plus a `dashboard_path` HINT for navigation. The hint is short and intentionally fragile — the canonical source is always the docs URL constructed from `docs_topic`. Agents must query the cometchat-docs MCP at runtime for current dashboard navigation, prop signatures, builder methods, and SDK reference. This catalog is for routing, not for SDK reference.",
6
6
  "feature_types": {
7
7
  "default": "Already enabled by default in the UI Kit components your integration uses. Zero code changes needed — the skill just shows you which component renders it.",
8
- "dashboard-toggle": "Requires flipping a toggle in the CometChat Dashboard at https://app.cometchat.com. Some have their UI decorator auto-attached by the UI Kit (auto_wired_in_uikit: true) — the dashboard flip is the only thing needed. Others additionally require passing the extension to UIKitSettingsBuilder.setExtensions([...]) before init.",
8
+ "extension": "Pure boolean toggle on a backend extension. The CLI's `apply-feature` command calls POST /apps/{appId}/extensions to enable, no further config needed. Some have their UI decorator auto-attached by the UI Kit (auto_wired_in_uikit: true). Others additionally require passing the extension to UIKitSettingsBuilder.setExtensions([...]) before init.",
9
+ "ai-feature": "Backend AI feature toggle. Requires an OpenAI API key (PUT /apps/{appId}/ai/settings) before the feature can be enabled (POST /apps/{appId}/features/ai.{ai_key}/enabled). The CLI prompts for the key once, stores it on the app's settings, then enables the feature. The `ai_key` field is the API suffix.",
10
+ "dashboard-only": "Requires entering third-party config the user has to fetch themselves (Giphy/Stipop/Tenor API key, Chatwoot webhook URL, Intercom token, etc.) or making a multi-field configuration choice (Disappearing Messages interval, Message Shortcuts list). The CLI cannot automate these — falls back to opening the dashboard. `dashboard_path` is load-bearing for these entries.",
9
11
  "package-install": "Requires installing an additional npm package. The UI Kit's initiateAfterLogin() auto-calls enableCalling() which detects the calls SDK and wires up call buttons. Currently calls only.",
10
12
  "component-swap": "Requires replacing one UI Kit component with a drop-in variant. One-line change. The SDK currently ships exactly one such variant (CometChatCompactMessageComposer for rich text formatting); more variants will become catalog entries here as the SDK adds them."
11
13
  },
@@ -126,7 +128,7 @@
126
128
  {
127
129
  "id": "bitly",
128
130
  "name": "Bitly URL Shortening",
129
- "type": "dashboard-toggle",
131
+ "type": "extension",
130
132
  "description": "Automatically shorten long URLs in messages using Bitly.",
131
133
  "dashboard_path": "Extensions → Bitly → Toggle on",
132
134
  "docs_topic": "extensions#bitly"
@@ -134,7 +136,7 @@
134
136
  {
135
137
  "id": "link-preview",
136
138
  "name": "Link Preview",
137
- "type": "dashboard-toggle",
139
+ "type": "extension",
138
140
  "auto_wired_in_uikit": true,
139
141
  "description": "Show URL previews with title, description, and thumbnail. UI decorator auto-attached by the UI Kit (in defaultExtensions[]).",
140
142
  "dashboard_path": "Extensions → Link Preview → Toggle on",
@@ -143,7 +145,7 @@
143
145
  {
144
146
  "id": "message-shortcuts",
145
147
  "name": "Message Shortcuts",
146
- "type": "dashboard-toggle",
148
+ "type": "dashboard-only",
147
149
  "description": "Expand predefined short codes into full messages.",
148
150
  "dashboard_path": "Extensions → Message Shortcuts → Configure shortcuts",
149
151
  "docs_topic": "extensions#message-shortcuts"
@@ -151,7 +153,7 @@
151
153
  {
152
154
  "id": "pin-message",
153
155
  "name": "Pin Message",
154
- "type": "dashboard-toggle",
156
+ "type": "extension",
155
157
  "description": "Pin important messages for easy access.",
156
158
  "dashboard_path": "Extensions → Pin Message → Toggle on",
157
159
  "docs_topic": "extensions#pin-message"
@@ -159,7 +161,7 @@
159
161
  {
160
162
  "id": "rich-media-preview",
161
163
  "name": "Rich Media Preview",
162
- "type": "dashboard-toggle",
164
+ "type": "extension",
163
165
  "description": "Generate rich preview panels for URLs via iFramely.",
164
166
  "dashboard_path": "Extensions → Rich Media Preview → Toggle on",
165
167
  "docs_topic": "extensions#rich-media-preview"
@@ -167,7 +169,7 @@
167
169
  {
168
170
  "id": "save-message",
169
171
  "name": "Save Message",
170
- "type": "dashboard-toggle",
172
+ "type": "extension",
171
173
  "description": "Bookmark messages privately for later.",
172
174
  "dashboard_path": "Extensions → Save Message → Toggle on",
173
175
  "docs_topic": "extensions#save-message"
@@ -175,7 +177,7 @@
175
177
  {
176
178
  "id": "thumbnail-generation",
177
179
  "name": "Thumbnail Generation",
178
- "type": "dashboard-toggle",
180
+ "type": "extension",
179
181
  "auto_wired_in_uikit": true,
180
182
  "description": "Auto-generate thumbnails for shared media to reduce bandwidth. UI decorator auto-attached by the UI Kit (in defaultExtensions[]).",
181
183
  "dashboard_path": "Extensions → Thumbnail Generation → Toggle on",
@@ -184,7 +186,7 @@
184
186
  {
185
187
  "id": "tinyurl",
186
188
  "name": "TinyURL Shortening",
187
- "type": "dashboard-toggle",
189
+ "type": "extension",
188
190
  "description": "Shorten URLs using TinyURL service.",
189
191
  "dashboard_path": "Extensions → TinyURL → Toggle on",
190
192
  "docs_topic": "extensions#tinyurl"
@@ -192,7 +194,7 @@
192
194
  {
193
195
  "id": "voice-transcription",
194
196
  "name": "Voice Transcription",
195
- "type": "dashboard-toggle",
197
+ "type": "extension",
196
198
  "description": "Convert audio messages to text automatically.",
197
199
  "dashboard_path": "Extensions → Voice Transcription → Toggle on",
198
200
  "docs_topic": "extensions#voice-transcription"
@@ -200,7 +202,7 @@
200
202
  {
201
203
  "id": "giphy",
202
204
  "name": "Giphy GIFs",
203
- "type": "dashboard-toggle",
205
+ "type": "dashboard-only",
204
206
  "description": "Search and share GIFs from Giphy.",
205
207
  "dashboard_path": "Extensions → Giphy → Configure API key",
206
208
  "docs_topic": "extensions#giphy"
@@ -208,7 +210,7 @@
208
210
  {
209
211
  "id": "message-translation",
210
212
  "name": "Message Translation",
211
- "type": "dashboard-toggle",
213
+ "type": "extension",
212
214
  "auto_wired_in_uikit": true,
213
215
  "description": "Translate messages into the user's locale. UI decorator auto-attached by the UI Kit (in defaultExtensions[]).",
214
216
  "dashboard_path": "Extensions → Message Translation → Toggle on",
@@ -217,7 +219,7 @@
217
219
  {
218
220
  "id": "polls",
219
221
  "name": "Polls",
220
- "type": "dashboard-toggle",
222
+ "type": "extension",
221
223
  "auto_wired_in_uikit": true,
222
224
  "description": "Create polls in group discussions with preset answers. UI decorator auto-attached by the UI Kit (in defaultExtensions[]).",
223
225
  "dashboard_path": "Extensions → Polls → Toggle on",
@@ -226,7 +228,7 @@
226
228
  {
227
229
  "id": "reminders",
228
230
  "name": "Reminders",
229
- "type": "dashboard-toggle",
231
+ "type": "extension",
230
232
  "description": "Set message reminders or personal notifications via bot.",
231
233
  "dashboard_path": "Extensions → Reminders → Toggle on",
232
234
  "docs_topic": "extensions#reminders"
@@ -234,7 +236,7 @@
234
236
  {
235
237
  "id": "stickers",
236
238
  "name": "Stickers",
237
- "type": "dashboard-toggle",
239
+ "type": "extension",
238
240
  "auto_wired_in_uikit": true,
239
241
  "description": "Send pre-designed stickers in conversations. UI decorator auto-attached by the UI Kit (in defaultExtensions[]).",
240
242
  "dashboard_path": "Extensions → Stickers → Toggle on",
@@ -243,7 +245,7 @@
243
245
  {
244
246
  "id": "stipop",
245
247
  "name": "Stipop Sticker Library",
246
- "type": "dashboard-toggle",
248
+ "type": "dashboard-only",
247
249
  "description": "Integrates the Stipop sticker library.",
248
250
  "dashboard_path": "Extensions → Stipop → Configure API key",
249
251
  "docs_topic": "extensions#stipop"
@@ -251,7 +253,7 @@
251
253
  {
252
254
  "id": "tenor",
253
255
  "name": "Tenor GIFs",
254
- "type": "dashboard-toggle",
256
+ "type": "dashboard-only",
255
257
  "description": "Search and share GIFs from Tenor.",
256
258
  "dashboard_path": "Extensions → Tenor → Configure API key",
257
259
  "docs_topic": "extensions#tenor"
@@ -259,7 +261,7 @@
259
261
  {
260
262
  "id": "collaborative-document",
261
263
  "name": "Collaborative Document",
262
- "type": "dashboard-toggle",
264
+ "type": "extension",
263
265
  "auto_wired_in_uikit": true,
264
266
  "description": "Real-time shared document editing. UI decorator auto-attached by the UI Kit (in defaultExtensions[]).",
265
267
  "dashboard_path": "Extensions → Collaborative Document → Toggle on",
@@ -268,7 +270,7 @@
268
270
  {
269
271
  "id": "collaborative-whiteboard",
270
272
  "name": "Collaborative Whiteboard",
271
- "type": "dashboard-toggle",
273
+ "type": "extension",
272
274
  "auto_wired_in_uikit": true,
273
275
  "description": "Shared whiteboard for drawing and brainstorming. UI decorator auto-attached by the UI Kit (in defaultExtensions[]).",
274
276
  "dashboard_path": "Extensions → Collaborative Whiteboard → Toggle on",
@@ -277,7 +279,7 @@
277
279
  {
278
280
  "id": "disappearing-messages",
279
281
  "name": "Disappearing Messages",
280
- "type": "dashboard-toggle",
282
+ "type": "dashboard-only",
281
283
  "description": "Messages auto-delete after a specified interval.",
282
284
  "dashboard_path": "Extensions → Disappearing Messages → Configure interval",
283
285
  "docs_topic": "extensions#disappearing-messages"
@@ -285,7 +287,7 @@
285
287
  {
286
288
  "id": "chatwoot",
287
289
  "name": "Chatwoot Integration",
288
- "type": "dashboard-toggle",
290
+ "type": "dashboard-only",
289
291
  "description": "Route messages to Chatwoot for customer support.",
290
292
  "dashboard_path": "Extensions → Chatwoot → Configure webhook",
291
293
  "docs_topic": "extensions#chatwoot"
@@ -293,7 +295,7 @@
293
295
  {
294
296
  "id": "intercom",
295
297
  "name": "Intercom Integration",
296
- "type": "dashboard-toggle",
298
+ "type": "dashboard-only",
297
299
  "description": "Integrate Intercom for in-app customer support.",
298
300
  "dashboard_path": "Extensions → Intercom → Configure API key",
299
301
  "docs_topic": "extensions#intercom"
@@ -301,7 +303,9 @@
301
303
  {
302
304
  "id": "conversation-starter",
303
305
  "name": "AI Conversation Starter",
304
- "type": "dashboard-toggle",
306
+ "type": "ai-feature",
307
+ "ai_key": "conversation-starter",
308
+ "requires": "openai-key",
305
309
  "description": "AI-generated opening messages for new chats.",
306
310
  "dashboard_path": "AI Features → Conversation Starter → Toggle on",
307
311
  "docs_topic": "ai-features#conversation-starter"
@@ -309,7 +313,9 @@
309
313
  {
310
314
  "id": "smart-replies",
311
315
  "name": "AI Smart Replies",
312
- "type": "dashboard-toggle",
316
+ "type": "ai-feature",
317
+ "ai_key": "smart-replies",
318
+ "requires": "openai-key",
313
319
  "description": "AI-generated contextual response suggestions.",
314
320
  "dashboard_path": "AI Features → Smart Replies → Toggle on",
315
321
  "docs_topic": "ai-features#smart-replies"
@@ -317,7 +323,9 @@
317
323
  {
318
324
  "id": "conversation-summary",
319
325
  "name": "AI Conversation Summary",
320
- "type": "dashboard-toggle",
326
+ "type": "ai-feature",
327
+ "ai_key": "conversation-summary",
328
+ "requires": "openai-key",
321
329
  "description": "AI-generated recaps of extended conversations.",
322
330
  "dashboard_path": "AI Features → Conversation Summary → Toggle on",
323
331
  "docs_topic": "ai-features#conversation-summary"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cometchat/skills-cli",
3
- "version": "2.2.0",
4
- "description": "CLI for the CometChat skills v3 architecture — auth, provision, detect, apply, verify CometChat integrations in React/Next.js/React-Router/Astro/Expo/React-Native projects.",
3
+ "version": "2.3.0",
4
+ "description": "CLI for the CometChat skills v3 architecture — auth, provision, detect, apply, verify CometChat integrations in React/Next.js/React-Router/Astro/Expo/React-Native/Angular/Android/iOS/Flutter projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -31,8 +31,8 @@
31
31
  "node": ">=18.0.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "^20.0.0",
35
- "esbuild": "^0.25.0",
34
+ "@types/node": "^25.6.0",
35
+ "esbuild": "^0.28.0",
36
36
  "tsx": "^4.19.0",
37
37
  "typescript": "^5.6.0"
38
38
  },