@nordsym/apiclaw 1.3.7 → 1.3.9

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 (203) hide show
  1. package/README.md +436 -200
  2. package/convex/_generated/api.d.ts +4 -0
  3. package/convex/agents.ts +403 -0
  4. package/convex/directCall.ts +80 -0
  5. package/convex/earnProgress.ts +753 -0
  6. package/convex/logs.ts +17 -0
  7. package/convex/providerKeys.ts +82 -2
  8. package/convex/schema.ts +71 -2
  9. package/convex/workspaces.ts +84 -2
  10. package/dist/adapters/base.d.ts +112 -0
  11. package/dist/adapters/base.d.ts.map +1 -0
  12. package/dist/adapters/base.js +247 -0
  13. package/dist/adapters/base.js.map +1 -0
  14. package/dist/adapters/claude-desktop.d.ts +12 -0
  15. package/dist/adapters/claude-desktop.d.ts.map +1 -0
  16. package/dist/adapters/claude-desktop.js +36 -0
  17. package/dist/adapters/claude-desktop.js.map +1 -0
  18. package/dist/adapters/cline.d.ts +20 -0
  19. package/dist/adapters/cline.d.ts.map +1 -0
  20. package/dist/adapters/cline.js +77 -0
  21. package/dist/adapters/cline.js.map +1 -0
  22. package/dist/adapters/continue.d.ts +26 -0
  23. package/dist/adapters/continue.d.ts.map +1 -0
  24. package/dist/adapters/continue.js +68 -0
  25. package/dist/adapters/continue.js.map +1 -0
  26. package/dist/adapters/cursor.d.ts +12 -0
  27. package/dist/adapters/cursor.d.ts.map +1 -0
  28. package/dist/adapters/cursor.js +38 -0
  29. package/dist/adapters/cursor.js.map +1 -0
  30. package/dist/adapters/custom.d.ts +47 -0
  31. package/dist/adapters/custom.d.ts.map +1 -0
  32. package/dist/adapters/custom.js +146 -0
  33. package/dist/adapters/custom.js.map +1 -0
  34. package/dist/adapters/detect.d.ts +69 -0
  35. package/dist/adapters/detect.d.ts.map +1 -0
  36. package/dist/adapters/detect.js +158 -0
  37. package/dist/adapters/detect.js.map +1 -0
  38. package/dist/adapters/index.d.ts +21 -0
  39. package/dist/adapters/index.d.ts.map +1 -0
  40. package/dist/adapters/index.js +23 -0
  41. package/dist/adapters/index.js.map +1 -0
  42. package/dist/adapters/windsurf.d.ts +12 -0
  43. package/dist/adapters/windsurf.d.ts.map +1 -0
  44. package/dist/adapters/windsurf.js +39 -0
  45. package/dist/adapters/windsurf.js.map +1 -0
  46. package/dist/bin.d.ts +9 -0
  47. package/dist/bin.d.ts.map +1 -0
  48. package/dist/bin.js +19 -0
  49. package/dist/bin.js.map +1 -0
  50. package/dist/cli/commands/doctor.d.ts +34 -0
  51. package/dist/cli/commands/doctor.d.ts.map +1 -0
  52. package/dist/cli/commands/doctor.js +312 -0
  53. package/dist/cli/commands/doctor.js.map +1 -0
  54. package/dist/cli/commands/index.d.ts +9 -0
  55. package/dist/cli/commands/index.d.ts.map +1 -0
  56. package/dist/cli/commands/index.js +9 -0
  57. package/dist/cli/commands/index.js.map +1 -0
  58. package/dist/cli/commands/mcp-install.d.ts +14 -0
  59. package/dist/cli/commands/mcp-install.d.ts.map +1 -0
  60. package/dist/cli/commands/mcp-install.js +246 -0
  61. package/dist/cli/commands/mcp-install.js.map +1 -0
  62. package/dist/cli/commands/restore.d.ts +50 -0
  63. package/dist/cli/commands/restore.d.ts.map +1 -0
  64. package/dist/cli/commands/restore.js +260 -0
  65. package/dist/cli/commands/restore.js.map +1 -0
  66. package/dist/cli/commands/setup.d.ts +19 -0
  67. package/dist/cli/commands/setup.d.ts.map +1 -0
  68. package/dist/cli/commands/setup.js +206 -0
  69. package/dist/cli/commands/setup.js.map +1 -0
  70. package/dist/cli/commands/uninstall.d.ts +37 -0
  71. package/dist/cli/commands/uninstall.d.ts.map +1 -0
  72. package/dist/cli/commands/uninstall.js +189 -0
  73. package/dist/cli/commands/uninstall.js.map +1 -0
  74. package/dist/cli/index.d.ts +7 -0
  75. package/dist/cli/index.d.ts.map +1 -0
  76. package/dist/cli/index.js +105 -0
  77. package/dist/cli/index.js.map +1 -0
  78. package/dist/discovery.d.ts +6 -2
  79. package/dist/discovery.d.ts.map +1 -1
  80. package/dist/discovery.js +296 -2
  81. package/dist/discovery.js.map +1 -1
  82. package/dist/enterprise/env.d.ts +56 -0
  83. package/dist/enterprise/env.d.ts.map +1 -0
  84. package/dist/enterprise/env.js +124 -0
  85. package/dist/enterprise/env.js.map +1 -0
  86. package/dist/enterprise/index.d.ts +7 -0
  87. package/dist/enterprise/index.d.ts.map +1 -0
  88. package/dist/enterprise/index.js +7 -0
  89. package/dist/enterprise/index.js.map +1 -0
  90. package/dist/enterprise/script-generator.d.ts +32 -0
  91. package/dist/enterprise/script-generator.d.ts.map +1 -0
  92. package/dist/enterprise/script-generator.js +461 -0
  93. package/dist/enterprise/script-generator.js.map +1 -0
  94. package/dist/execute.d.ts +21 -0
  95. package/dist/execute.d.ts.map +1 -1
  96. package/dist/execute.js +231 -0
  97. package/dist/execute.js.map +1 -1
  98. package/dist/index.js +79 -7
  99. package/dist/index.js.map +1 -1
  100. package/dist/stripe.d.ts +1 -1
  101. package/dist/stripe.js +1 -1
  102. package/dist/stripe.js.map +1 -1
  103. package/dist/types.d.ts +29 -0
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/ui/colors.d.ts +111 -0
  106. package/dist/ui/colors.d.ts.map +1 -0
  107. package/dist/ui/colors.js +185 -0
  108. package/dist/ui/colors.js.map +1 -0
  109. package/dist/ui/errors.d.ts +69 -0
  110. package/dist/ui/errors.d.ts.map +1 -0
  111. package/dist/ui/errors.js +334 -0
  112. package/dist/ui/errors.js.map +1 -0
  113. package/dist/ui/index.d.ts +10 -0
  114. package/dist/ui/index.d.ts.map +1 -0
  115. package/dist/ui/index.js +14 -0
  116. package/dist/ui/index.js.map +1 -0
  117. package/dist/ui/prompts.d.ts +88 -0
  118. package/dist/ui/prompts.d.ts.map +1 -0
  119. package/dist/ui/prompts.js +295 -0
  120. package/dist/ui/prompts.js.map +1 -0
  121. package/dist/ui/spinner.d.ts +112 -0
  122. package/dist/ui/spinner.d.ts.map +1 -0
  123. package/dist/ui/spinner.js +229 -0
  124. package/dist/ui/spinner.js.map +1 -0
  125. package/dist/utils/backup.d.ts +48 -0
  126. package/dist/utils/backup.d.ts.map +1 -0
  127. package/dist/utils/backup.js +182 -0
  128. package/dist/utils/backup.js.map +1 -0
  129. package/dist/utils/config.d.ts +80 -0
  130. package/dist/utils/config.d.ts.map +1 -0
  131. package/dist/utils/config.js +221 -0
  132. package/dist/utils/config.js.map +1 -0
  133. package/dist/utils/os.d.ts +45 -0
  134. package/dist/utils/os.d.ts.map +1 -0
  135. package/dist/utils/os.js +106 -0
  136. package/dist/utils/os.js.map +1 -0
  137. package/dist/utils/paths.d.ts +38 -0
  138. package/dist/utils/paths.d.ts.map +1 -0
  139. package/dist/utils/paths.js +160 -0
  140. package/dist/utils/paths.js.map +1 -0
  141. package/docs/PRD-BILLING.md +226 -0
  142. package/docs/PRD-EARN-SYSTEM.md +261 -0
  143. package/docs/PRD-MCP-AUTO-SETUP.md +623 -0
  144. package/docs/enterprise-deployment.md +728 -0
  145. package/landing/next.config.mjs +14 -0
  146. package/landing/public/book/index.html +24 -5
  147. package/landing/public/demo-product.jpg +0 -0
  148. package/landing/public/stats.json +4 -2
  149. package/landing/scripts/generate-stats.js +13 -0
  150. package/landing/src/app/api/og/route.tsx +1 -3
  151. package/landing/src/app/api/workspace-auth/magic-link/route.ts +6 -3
  152. package/landing/src/app/auth/verify/page.tsx +11 -4
  153. package/landing/src/app/docs/page.tsx +1 -1
  154. package/landing/src/app/join/page.tsx +49 -0
  155. package/landing/src/app/layout.tsx +1 -1
  156. package/landing/src/app/login/page.tsx +7 -1
  157. package/landing/src/app/page.tsx +50 -38
  158. package/landing/src/app/providers/register/page.tsx +1 -1
  159. package/landing/src/app/workspace/page.tsx +484 -806
  160. package/landing/src/components/CheckoutButton.tsx +1 -1
  161. package/landing/src/components/EarnCreditsTab.tsx +842 -0
  162. package/landing/src/components/HeroTabs.tsx +2 -2
  163. package/landing/src/components/demo/PhoneDemo.tsx +423 -0
  164. package/landing/src/components/demo/index.ts +1 -0
  165. package/landing/src/lib/stats.json +3 -1
  166. package/landing/tailwind.config.ts +10 -0
  167. package/package.json +9 -2
  168. package/src/adapters/base.ts +363 -0
  169. package/src/adapters/claude-desktop.ts +41 -0
  170. package/src/adapters/cline.ts +88 -0
  171. package/src/adapters/continue.ts +91 -0
  172. package/src/adapters/cursor.ts +43 -0
  173. package/src/adapters/custom.ts +188 -0
  174. package/src/adapters/detect.ts +202 -0
  175. package/src/adapters/index.ts +47 -0
  176. package/src/adapters/windsurf.ts +44 -0
  177. package/src/bin.ts +19 -0
  178. package/src/cli/commands/doctor.ts +367 -0
  179. package/src/cli/commands/index.ts +9 -0
  180. package/src/cli/commands/mcp-install.ts +291 -0
  181. package/src/cli/commands/restore.ts +333 -0
  182. package/src/cli/commands/setup.ts +276 -0
  183. package/src/cli/commands/uninstall.ts +240 -0
  184. package/src/cli/index.ts +116 -0
  185. package/src/discovery.ts +328 -3
  186. package/src/enterprise/env.ts +156 -0
  187. package/src/enterprise/index.ts +7 -0
  188. package/src/enterprise/script-generator.ts +481 -0
  189. package/src/execute.ts +256 -0
  190. package/src/index.ts +85 -7
  191. package/src/stripe.ts +1 -1
  192. package/src/types.ts +32 -0
  193. package/src/ui/colors.ts +219 -0
  194. package/src/ui/errors.ts +394 -0
  195. package/src/ui/index.ts +17 -0
  196. package/src/ui/prompts.ts +390 -0
  197. package/src/ui/spinner.ts +325 -0
  198. package/src/utils/backup.ts +224 -0
  199. package/src/utils/config.ts +315 -0
  200. package/src/utils/os.ts +124 -0
  201. package/src/utils/paths.ts +203 -0
  202. package/STATUS.md +0 -160
  203. package/landing/tsconfig.tsbuildinfo +0 -1
@@ -74,6 +74,8 @@ import {
74
74
  UsageExceededBanner,
75
75
  } from "@/components/CheckoutButton";
76
76
  import { Toast, useToast } from "@/components/Toast";
77
+ import { EarnCreditsTab } from "@/components/EarnCreditsTab";
78
+ import statsData from "@/lib/stats.json";
77
79
 
78
80
  const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
79
81
 
@@ -547,8 +549,8 @@ export default function WorkspacePage() {
547
549
  const getTabLabel = () => {
548
550
  if (activeTab === "analytics") {
549
551
  const subLabels: Record<AnalyticsSubtab, string> = {
550
- overview: "Analytics Overview",
551
- usage: "Usage",
552
+ overview: "Agent Analytics",
553
+ usage: "API Analytics",
552
554
  logs: "Logs",
553
555
  };
554
556
  return subLabels[analyticsSubtab] || "Analytics";
@@ -699,7 +701,7 @@ export default function WorkspacePage() {
699
701
  }`}
700
702
  >
701
703
  <BarChart3 className="w-4 h-4" />
702
- <span>Overview</span>
704
+ <span>Agent Analytics</span>
703
705
  </button>
704
706
  <button
705
707
  onClick={() => {
@@ -715,7 +717,7 @@ export default function WorkspacePage() {
715
717
  }`}
716
718
  >
717
719
  <TrendingUp className="w-4 h-4" />
718
- <span>Usage</span>
720
+ <span>API Analytics</span>
719
721
  </button>
720
722
  <button
721
723
  onClick={() => {
@@ -898,13 +900,13 @@ export default function WorkspacePage() {
898
900
  <WebhooksTab />
899
901
  )}
900
902
  {activeTab === "api-keys" && (
901
- <ApiKeysTab />
903
+ <ApiKeysTab workspace={workspace} providerApis={providerApis} sessionToken={sessionToken} />
902
904
  )}
903
905
  {activeTab === "billing" && (
904
906
  <BillingTab workspace={workspace} sessionToken={sessionToken} />
905
907
  )}
906
908
  {activeTab === "earn" && (
907
- <EarnTab />
909
+ <EarnCreditsTab showToast={showToast} />
908
910
  )}
909
911
  {activeTab === "docs" && (
910
912
  <DocsTab />
@@ -945,9 +947,9 @@ function OverviewTab({
945
947
  <div className="rounded-2xl border border-[#ef4444]/30 bg-[#ef4444]/10 p-6">
946
948
  <div className="flex items-center gap-3 mb-3">
947
949
  <Zap className="w-6 h-6 text-[#ef4444]" />
948
- <span className="text-[var(--text-muted)]">Available APIs</span>
950
+ <span className="text-[var(--text-muted)]">API Catalog</span>
949
951
  </div>
950
- <p className="text-4xl font-bold text-[#ef4444]">{approvedApis.length}</p>
952
+ <p className="text-4xl font-bold text-[#ef4444]">{statsData.apiCount.toLocaleString()}</p>
951
953
  </div>
952
954
 
953
955
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
@@ -1253,44 +1255,89 @@ function MyAPIsTab({ apis }: { apis: ProviderAPI[] }) {
1253
1255
  <div className="space-y-6">
1254
1256
  <div>
1255
1257
  <h2 className="text-2xl font-bold">My APIs</h2>
1256
- <p className="text-[var(--text-muted)]">APIs you&apos;ve listed for other agents to discover and use.</p>
1258
+ <p className="text-[var(--text-muted)]">Choose how you want AI agents to access your API.</p>
1257
1259
  </div>
1258
1260
 
1259
- <div className="text-center py-16 rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50">
1260
- <Terminal className="w-16 h-16 text-[var(--text-muted)] mx-auto mb-4" />
1261
- <h3 className="font-semibold text-xl mb-2">Get Your API in Front of AI Agents</h3>
1262
- <p className="text-[var(--text-muted)] max-w-md mx-auto mb-6">
1263
- List your API and let AI agents discover and use it — no integration work for them.
1264
- </p>
1265
- <Link href="/providers/register" className="btn-primary">
1266
- <Plus className="w-5 h-5" />
1267
- List New API
1261
+ {/* Three integration options */}
1262
+ <div className="grid gap-4 md:grid-cols-3">
1263
+ {/* Option 1: List API */}
1264
+ <Link
1265
+ href="/providers/register?type=list"
1266
+ className="group rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6 hover:border-[#ef4444]/50 transition text-left"
1267
+ >
1268
+ <div className="w-12 h-12 rounded-xl bg-[var(--surface)] flex items-center justify-center mb-4 group-hover:bg-[#ef4444]/10 transition">
1269
+ <Search className="w-6 h-6 text-[var(--text-muted)] group-hover:text-[#ef4444] transition" />
1270
+ </div>
1271
+ <h3 className="font-semibold text-lg mb-1">List API</h3>
1272
+ <p className="text-[#ef4444] text-sm font-medium mb-3">Get discovered</p>
1273
+ <p className="text-sm text-[var(--text-muted)] mb-4">
1274
+ Appear in the APIClaw catalog. AI agents find you when searching for capabilities.
1275
+ </p>
1276
+ <div className="flex items-center justify-end">
1277
+ <ChevronRight className="w-5 h-5 text-[var(--text-muted)] group-hover:text-[#ef4444] group-hover:translate-x-1 transition" />
1278
+ </div>
1268
1279
  </Link>
1269
1280
 
1270
- {/* Benefits */}
1271
- <div className="mt-8 pt-8 border-t border-[var(--border)] max-w-lg mx-auto">
1272
- <h4 className="font-medium mb-4 text-left">Why list your API?</h4>
1273
- <div className="space-y-3 text-left">
1274
- <div className="flex items-start gap-3">
1275
- <Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
1276
- <div>
1277
- <p className="font-medium">Get discovered by AI agents</p>
1278
- <p className="text-sm text-[var(--text-muted)]">Agents query APIClaw to find APIs matching their needs.</p>
1279
- </div>
1281
+ {/* Option 2: Open API */}
1282
+ <Link
1283
+ href="/providers/register?type=open"
1284
+ className="group rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6 hover:border-[#ef4444]/50 transition text-left"
1285
+ >
1286
+ <div className="w-12 h-12 rounded-xl bg-[var(--surface)] flex items-center justify-center mb-4 group-hover:bg-[#ef4444]/10 transition">
1287
+ <Globe className="w-6 h-6 text-[var(--text-muted)] group-hover:text-[#ef4444] transition" />
1288
+ </div>
1289
+ <h3 className="font-semibold text-lg mb-1">Open API</h3>
1290
+ <p className="text-[#ef4444] text-sm font-medium mb-3">Agents call directly</p>
1291
+ <p className="text-sm text-[var(--text-muted)] mb-4">
1292
+ Provide your public OpenAPI spec. Agents call your endpoint with their own keys.
1293
+ </p>
1294
+ <div className="flex items-center justify-end">
1295
+ <ChevronRight className="w-5 h-5 text-[var(--text-muted)] group-hover:text-[#ef4444] group-hover:translate-x-1 transition" />
1296
+ </div>
1297
+ </Link>
1298
+
1299
+ {/* Option 3: Direct Call */}
1300
+ <Link
1301
+ href="/providers/register?type=direct"
1302
+ className="group rounded-2xl border border-[#ef4444]/30 bg-gradient-to-br from-[#ef4444]/5 to-transparent p-6 hover:border-[#ef4444]/50 transition text-left relative overflow-hidden"
1303
+ >
1304
+ <div className="w-12 h-12 rounded-xl bg-[#ef4444]/10 flex items-center justify-center mb-4">
1305
+ <Zap className="w-6 h-6 text-[#ef4444]" />
1306
+ </div>
1307
+ <h3 className="font-semibold text-lg mb-1">Direct Call</h3>
1308
+ <p className="text-[#ef4444] text-sm font-medium mb-3">We handle keys</p>
1309
+ <p className="text-sm text-[var(--text-muted)] mb-4">
1310
+ APIClaw manages authentication. Agents pay per call, you earn revenue share.
1311
+ </p>
1312
+ <div className="flex items-center justify-end">
1313
+ <ChevronRight className="w-5 h-5 text-[var(--text-muted)] group-hover:text-[#ef4444] group-hover:translate-x-1 transition" />
1314
+ </div>
1315
+ </Link>
1316
+ </div>
1317
+
1318
+ {/* Why list section */}
1319
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1320
+ <h4 className="font-semibold mb-4">Why list your API on APIClaw?</h4>
1321
+ <div className="grid md:grid-cols-3 gap-4">
1322
+ <div className="flex items-start gap-3">
1323
+ <Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
1324
+ <div>
1325
+ <p className="font-medium text-sm">AI-native discovery</p>
1326
+ <p className="text-xs text-[var(--text-muted)]">Agents find you when searching for capabilities.</p>
1280
1327
  </div>
1281
- <div className="flex items-start gap-3">
1282
- <Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
1283
- <div>
1284
- <p className="font-medium">Direct Call = we handle keys</p>
1285
- <p className="text-sm text-[var(--text-muted)]">No need for agents to manage API keys.</p>
1286
- </div>
1328
+ </div>
1329
+ <div className="flex items-start gap-3">
1330
+ <Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
1331
+ <div>
1332
+ <p className="font-medium text-sm">Zero integration work</p>
1333
+ <p className="text-xs text-[var(--text-muted)]">We handle auth, billing, and agent compatibility.</p>
1287
1334
  </div>
1288
- <div className="flex items-start gap-3">
1289
- <Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
1290
- <div>
1291
- <p className="font-medium">Analytics on who&apos;s using your API</p>
1292
- <p className="text-sm text-[var(--text-muted)]">See which agents call your API and how often.</p>
1293
- </div>
1335
+ </div>
1336
+ <div className="flex items-start gap-3">
1337
+ <Check className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" />
1338
+ <div>
1339
+ <p className="font-medium text-sm">Usage analytics</p>
1340
+ <p className="text-xs text-[var(--text-muted)]">See which agents use your API and how.</p>
1294
1341
  </div>
1295
1342
  </div>
1296
1343
  </div>
@@ -1362,7 +1409,7 @@ function MyAPIsTab({ apis }: { apis: ProviderAPI[] }) {
1362
1409
  }
1363
1410
 
1364
1411
  // ============================================
1365
- // AGENTS TAB
1412
+ // AGENTS TAB - Agent-first hierarchy view
1366
1413
  // ============================================
1367
1414
 
1368
1415
  function AgentsTab({
@@ -1379,18 +1426,27 @@ function AgentsTab({
1379
1426
  sessionToken?: string;
1380
1427
  }) {
1381
1428
  const [confirmRevoke, setConfirmRevoke] = useState<string | null>(null);
1382
- const [sendingLink, setSendingLink] = useState(false);
1383
- const [linkSent, setLinkSent] = useState(false);
1384
- const [email, setEmail] = useState(workspaceEmail || "");
1385
1429
  const [editingAgent, setEditingAgent] = useState<string | null>(null);
1386
1430
  const [editName, setEditName] = useState("");
1431
+ const [copied, setCopied] = useState(false);
1432
+
1433
+ // Get the primary agent (current session or first agent)
1434
+ const primaryAgent = agents.find(a => a.isCurrent) || agents[0];
1435
+
1436
+ // Generate agent display name
1437
+ const getAgentDisplayName = (agent: typeof primaryAgent) => {
1438
+ if (!agent) return "agent-xxxx";
1439
+ if (agent.name) return agent.name;
1440
+ // Generate short ID from fingerprint
1441
+ const shortId = agent.fingerprint?.slice(-4) || "xxxx";
1442
+ return `agent-${shortId}`;
1443
+ };
1387
1444
 
1388
1445
  const handleRevoke = (agentId: string) => {
1389
1446
  const agent = agents.find(a => a.id === agentId);
1390
1447
  if (confirmRevoke === agentId) {
1391
1448
  onRevoke(agentId);
1392
1449
  setConfirmRevoke(null);
1393
- // If revoking current session, clear localStorage and redirect
1394
1450
  if (agent?.isCurrent) {
1395
1451
  localStorage.removeItem("apiclaw_workspace_session");
1396
1452
  window.location.href = "/login";
@@ -1400,230 +1456,166 @@ function AgentsTab({
1400
1456
  }
1401
1457
  };
1402
1458
 
1403
- const handleSendMagicLink = async () => {
1404
- if (!email || !email.includes("@")) return;
1405
-
1406
- setSendingLink(true);
1407
- try {
1408
- const response = await fetch(`${CONVEX_URL.replace('.cloud', '.site')}/workspace/magic-link`, {
1409
- method: "POST",
1410
- headers: { "Content-Type": "application/json" },
1411
- body: JSON.stringify({ email }),
1412
- });
1413
-
1414
- const result = await response.json();
1415
- if (result.success) {
1416
- setLinkSent(true);
1417
- setTimeout(() => setLinkSent(false), 5000);
1418
- }
1419
- } catch (err) {
1420
- console.error("Failed to send magic link:", err);
1421
- } finally {
1422
- setSendingLink(false);
1423
- }
1459
+ const copyToClipboard = (text: string) => {
1460
+ navigator.clipboard.writeText(text);
1461
+ setCopied(true);
1462
+ setTimeout(() => setCopied(false), 2000);
1424
1463
  };
1425
1464
 
1465
+ const mcpCommand = "npx @nordsym/apiclaw mcp-install";
1466
+
1426
1467
  return (
1427
1468
  <div className="space-y-6">
1428
- <div className="flex items-center justify-between">
1429
- <div>
1430
- <h2 className="text-2xl font-bold">My Agents</h2>
1431
- <p className="text-[var(--text-muted)]">{agents.length} connected agent{agents.length !== 1 ? "s" : ""}</p>
1432
- </div>
1433
- </div>
1434
-
1435
- {/* How to Connect Agents */}
1469
+ {/* Your Agent - Primary Card */}
1436
1470
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1437
- <div className="flex items-start gap-4">
1438
- <div className="w-10 h-10 rounded-xl bg-[#ef4444]/20 flex items-center justify-center flex-shrink-0">
1439
- <BookOpen className="w-5 h-5 text-[#ef4444]" />
1440
- </div>
1441
- <div className="flex-1">
1442
- <h3 className="font-semibold mb-2">How to Connect Your AI Agent</h3>
1443
- <p className="text-sm text-[var(--text-muted)] mb-4">
1444
- Add APIClaw to your AI agent&apos;s MCP config to enable API discovery and Direct Call.
1445
- </p>
1446
- <div className="bg-[var(--background)] rounded-xl p-4 font-mono text-sm mb-4 relative">
1447
- <pre className="text-[var(--text-secondary)] overflow-x-auto">{`{
1448
- "mcpServers": {
1449
- "apiclaw": {
1450
- "command": "npx",
1451
- "args": ["@nordsym/apiclaw"]
1452
- }
1453
- }
1454
- }`}</pre>
1455
- </div>
1456
- <p className="text-sm text-[var(--text-muted)] mb-3">
1457
- Or run directly in terminal to test:
1458
- </p>
1459
- <div className="flex items-center gap-2 bg-[var(--background)] rounded-lg px-4 py-2 font-mono text-sm w-fit">
1460
- <Terminal className="w-4 h-4 text-[#ef4444]" />
1461
- <code>npx @nordsym/apiclaw</code>
1462
- </div>
1463
- <p className="text-xs text-[var(--text-muted)] mt-3">
1464
- First run prompts for email → sends magic link → registers your agent to this workspace.
1465
- </p>
1466
- </div>
1471
+ <div className="flex items-center justify-between mb-4">
1472
+ <span className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider">Your Agent</span>
1473
+ {primaryAgent && (
1474
+ <button
1475
+ onClick={() => handleRevoke(primaryAgent.id)}
1476
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center gap-1 ${
1477
+ confirmRevoke === primaryAgent.id
1478
+ ? "bg-red-500 text-white"
1479
+ : "text-[var(--text-muted)] hover:text-red-500 hover:bg-red-500/10"
1480
+ }`}
1481
+ >
1482
+ <Trash2 className="w-3 h-3" />
1483
+ {confirmRevoke === primaryAgent.id ? "Confirm" : "Remove"}
1484
+ </button>
1485
+ )}
1467
1486
  </div>
1468
- </div>
1469
-
1470
- {/* Quick Connect via Email */}
1471
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1472
- <div className="flex items-start gap-4">
1473
- <div className="w-10 h-10 rounded-xl bg-green-500/20 flex items-center justify-center flex-shrink-0">
1474
- <Mail className="w-5 h-5 text-green-500" />
1475
- </div>
1476
- <div className="flex-1">
1477
- <h3 className="font-semibold mb-2">Quick Connect via Email</h3>
1478
- <p className="text-sm text-[var(--text-muted)] mb-4">
1479
- Send a magic link to connect your agent without using the CLI.
1480
- </p>
1481
- <div className="flex flex-col sm:flex-row gap-3">
1482
- <input
1483
- type="email"
1484
- value={email}
1485
- onChange={(e) => setEmail(e.target.value)}
1486
- placeholder="your@email.com"
1487
- className="w-full sm:flex-1 px-4 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1488
- />
1489
- <button
1490
- onClick={handleSendMagicLink}
1491
- disabled={sendingLink || !email}
1492
- className="w-full sm:w-auto px-6 py-2 bg-[#ef4444] text-white rounded-lg font-medium hover:bg-[#dc2626] transition disabled:opacity-50 flex items-center justify-center gap-2"
1493
- >
1494
- {sendingLink ? (
1495
- <>
1496
- <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
1497
- Sending...
1498
- </>
1499
- ) : linkSent ? (
1500
- <>
1501
- <Check className="w-4 h-4" />
1502
- Sent!
1503
- </>
1504
- ) : (
1505
- <>
1506
- <Send className="w-4 h-4" />
1507
- Send Link
1508
- </>
1509
- )}
1510
- </button>
1487
+
1488
+ {primaryAgent ? (
1489
+ <div className="flex items-center gap-4">
1490
+ <div className="w-14 h-14 rounded-xl bg-gradient-to-br from-[#ef4444] to-[#f97316] flex items-center justify-center flex-shrink-0">
1491
+ <Cpu className="w-7 h-7 text-white" />
1511
1492
  </div>
1512
- {linkSent && (
1513
- <p className="text-sm text-green-500 mt-2">
1514
- Magic link sent! Check your email and click to connect.
1515
- </p>
1516
- )}
1517
- </div>
1518
- </div>
1519
- </div>
1520
-
1521
- {agents.length === 0 ? (
1522
- <div className="text-center py-12 rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50">
1523
- <Users className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-4" />
1524
- <h3 className="font-semibold text-lg mb-2">No Agents Connected Yet</h3>
1525
- <p className="text-[var(--text-muted)] max-w-md mx-auto">
1526
- Follow the instructions above to connect your first AI agent.
1527
- </p>
1528
- </div>
1529
- ) : (
1530
- <div className="grid gap-4">
1531
- {agents.map((agent) => (
1532
- <div key={agent.id} className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-4 sm:p-6">
1533
- <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
1534
- <div className="flex items-center gap-3 sm:gap-4">
1535
- <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-full bg-[#ef4444]/20 flex items-center justify-center flex-shrink-0">
1536
- <Users className="w-5 h-5 sm:w-6 sm:h-6 text-[#ef4444]" />
1537
- </div>
1538
- <div className="flex-1 min-w-0">
1539
- {editingAgent === agent.id ? (
1540
- <div className="flex flex-wrap items-center gap-2">
1541
- <input
1542
- type="text"
1543
- value={editName}
1544
- onChange={(e) => setEditName(e.target.value)}
1545
- placeholder="Agent name..."
1546
- className="w-full sm:w-auto px-3 py-1 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1547
- autoFocus
1548
- />
1549
- <div className="flex gap-2">
1550
- <button
1551
- onClick={() => {
1552
- onRename(agent.id, editName);
1553
- setEditingAgent(null);
1554
- }}
1555
- className="px-3 py-1 bg-[#ef4444] text-white rounded-lg text-sm hover:bg-[#dc2626]"
1556
- >
1557
- Save
1558
- </button>
1559
- <button
1560
- onClick={() => setEditingAgent(null)}
1561
- className="px-3 py-1 text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1562
- >
1563
- Cancel
1564
- </button>
1565
- </div>
1566
- </div>
1567
- ) : (
1568
- <div className="flex flex-wrap items-center gap-2">
1569
- <h3 className="font-semibold truncate">{agent.name || agent.fingerprint}</h3>
1570
- <button
1571
- onClick={() => {
1572
- setEditingAgent(agent.id);
1573
- setEditName(agent.name || agent.fingerprint || "");
1574
- }}
1575
- className="text-[var(--text-muted)] hover:text-[var(--text-primary)] opacity-0 group-hover:opacity-100 transition"
1576
- title="Rename"
1577
- >
1578
- <Settings className="w-4 h-4" />
1579
- </button>
1580
- {agent.isCurrent && (
1581
- <span className="px-2 py-0.5 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
1582
- Current
1583
- </span>
1584
- )}
1585
- </div>
1586
- )}
1587
- {agent.fingerprint !== agent.name && agent.name && (
1588
- <p className="text-xs text-[var(--text-muted)] mt-0.5 truncate">{agent.fingerprint}</p>
1589
- )}
1590
- <div className="flex items-center gap-2 sm:gap-4 mt-1 text-xs sm:text-sm text-[var(--text-muted)]">
1591
- <span className="flex items-center gap-1">
1592
- <Clock className="w-3 h-3 sm:w-4 sm:h-4" />
1593
- <span className="hidden sm:inline">Last active:</span> {new Date(agent.lastUsedAt).toLocaleDateString()}
1594
- </span>
1595
- </div>
1596
- </div>
1597
- </div>
1598
- <div className="flex items-center gap-2 w-full sm:w-auto">
1493
+ <div className="flex-1 min-w-0">
1494
+ {editingAgent === primaryAgent.id ? (
1495
+ <div className="flex items-center gap-2">
1496
+ <input
1497
+ type="text"
1498
+ value={editName}
1499
+ onChange={(e) => setEditName(e.target.value)}
1500
+ placeholder="Agent name..."
1501
+ className="flex-1 px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1502
+ autoFocus
1503
+ onKeyDown={(e) => {
1504
+ if (e.key === "Enter") {
1505
+ onRename(primaryAgent.id, editName);
1506
+ setEditingAgent(null);
1507
+ } else if (e.key === "Escape") {
1508
+ setEditingAgent(null);
1509
+ }
1510
+ }}
1511
+ />
1599
1512
  <button
1600
1513
  onClick={() => {
1601
- setEditingAgent(agent.id);
1602
- setEditName(agent.name || agent.fingerprint || "");
1514
+ onRename(primaryAgent.id, editName);
1515
+ setEditingAgent(null);
1603
1516
  }}
1604
- className="flex-1 sm:flex-none px-3 py-2 rounded-lg text-sm text-[var(--text-muted)] hover:bg-[var(--surface)] transition text-center"
1517
+ className="px-3 py-1.5 bg-[#ef4444] text-white rounded-lg text-sm hover:bg-[#dc2626]"
1605
1518
  >
1606
- Rename
1519
+ Save
1607
1520
  </button>
1608
1521
  <button
1609
- onClick={() => handleRevoke(agent.id)}
1610
- className={`flex-1 sm:flex-none px-3 sm:px-4 py-2 rounded-lg text-sm font-medium transition flex items-center justify-center gap-1 ${
1611
- confirmRevoke === agent.id
1612
- ? "bg-red-500 text-white"
1613
- : "bg-red-500/10 text-red-500 hover:bg-red-500/20"
1614
- }`}
1615
- title={agent.isCurrent ? "This will log you out" : "Remove this agent"}
1522
+ onClick={() => setEditingAgent(null)}
1523
+ className="px-3 py-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1616
1524
  >
1617
- <Trash2 className="w-4 h-4" />
1618
- <span className="hidden sm:inline">{confirmRevoke === agent.id ? (agent.isCurrent ? "Logout & Remove" : "Confirm") : "Revoke"}</span>
1619
- <span className="sm:hidden">{confirmRevoke === agent.id ? "Confirm" : "Revoke"}</span>
1525
+ Cancel
1620
1526
  </button>
1621
1527
  </div>
1528
+ ) : (
1529
+ <div className="flex items-center gap-2">
1530
+ <h3 className="text-xl font-bold">{getAgentDisplayName(primaryAgent)}</h3>
1531
+ <button
1532
+ onClick={() => {
1533
+ setEditingAgent(primaryAgent.id);
1534
+ setEditName(primaryAgent.name || getAgentDisplayName(primaryAgent));
1535
+ }}
1536
+ className="p-1 rounded text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--surface)] transition"
1537
+ title="Rename agent"
1538
+ >
1539
+ <Settings className="w-4 h-4" />
1540
+ </button>
1541
+ </div>
1542
+ )}
1543
+ <div className="flex items-center gap-3 mt-1 text-sm text-[var(--text-muted)]">
1544
+ <span className="flex items-center gap-1.5">
1545
+ <span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
1546
+ Connected
1547
+ </span>
1548
+ <span className="text-[var(--border)]">•</span>
1549
+ <span className="flex items-center gap-1">
1550
+ <Activity className="w-3.5 h-3.5" />
1551
+ Active {new Date(primaryAgent.lastUsedAt).toLocaleDateString()}
1552
+ </span>
1622
1553
  </div>
1623
1554
  </div>
1624
- ))}
1555
+ </div>
1556
+ ) : (
1557
+ <div className="flex items-center gap-4">
1558
+ <div className="w-14 h-14 rounded-xl bg-[var(--surface)] border-2 border-dashed border-[var(--border)] flex items-center justify-center">
1559
+ <Cpu className="w-7 h-7 text-[var(--text-muted)]" />
1560
+ </div>
1561
+ <div>
1562
+ <h3 className="text-xl font-bold text-[var(--text-muted)]">No agent connected</h3>
1563
+ <p className="text-sm text-[var(--text-muted)]">Run the setup command below to connect</p>
1564
+ </div>
1565
+ </div>
1566
+ )}
1567
+ </div>
1568
+
1569
+ {/* Subagents Section */}
1570
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1571
+ <div className="flex items-center justify-between mb-4">
1572
+ <span className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider">Subagents</span>
1625
1573
  </div>
1626
- )}
1574
+
1575
+ {/* Empty state - subagents will come from backend later */}
1576
+ <div className="py-8 text-center">
1577
+ <div className="w-12 h-12 rounded-xl bg-[var(--surface)] mx-auto mb-3 flex items-center justify-center">
1578
+ <Users className="w-6 h-6 text-[var(--text-muted)]" />
1579
+ </div>
1580
+ <p className="text-sm text-[var(--text-muted)] max-w-sm mx-auto">
1581
+ Subagents will appear here when your agent makes calls with the{" "}
1582
+ <code className="px-1.5 py-0.5 rounded bg-[var(--surface)] text-[#ef4444] font-mono text-xs">
1583
+ X-APIClaw-Subagent
1584
+ </code>{" "}
1585
+ header.
1586
+ </p>
1587
+ </div>
1588
+ </div>
1589
+
1590
+ {/* Quick Setup - Collapsed at bottom */}
1591
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1592
+ <div className="flex items-center justify-between mb-4">
1593
+ <span className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider">Quick Setup</span>
1594
+ </div>
1595
+
1596
+ <p className="text-sm text-[var(--text-muted)] mb-3">
1597
+ Add to your agent&apos;s MCP config:
1598
+ </p>
1599
+
1600
+ <div className="flex items-center gap-2 bg-[var(--background)] rounded-lg px-4 py-3 font-mono text-sm">
1601
+ <Terminal className="w-4 h-4 text-[#ef4444] flex-shrink-0" />
1602
+ <code className="flex-1 text-[var(--text-primary)]">{mcpCommand}</code>
1603
+ <button
1604
+ onClick={() => copyToClipboard(mcpCommand)}
1605
+ className="p-1.5 rounded hover:bg-[var(--surface)] transition text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1606
+ title="Copy"
1607
+ >
1608
+ {copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
1609
+ </button>
1610
+ </div>
1611
+
1612
+ <p className="text-xs text-[var(--text-muted)] mt-3">
1613
+ Or use header:{" "}
1614
+ <code className="px-1.5 py-0.5 rounded bg-[var(--surface)] font-mono">
1615
+ X-APIClaw-Subagent: name
1616
+ </code>
1617
+ </p>
1618
+ </div>
1627
1619
  </div>
1628
1620
  );
1629
1621
  }
@@ -1890,10 +1882,44 @@ function UsageTab({
1890
1882
  workspace: Workspace | null;
1891
1883
  usage: UsageData | null;
1892
1884
  }) {
1893
- const hasData = usage && (usage.byProvider.length > 0 || usage.byDay.length > 0);
1885
+ const hasRealData = usage && (usage.byProvider.length > 0 || usage.byDay.length > 0);
1886
+
1887
+ // Preview data for empty state (provider perspective - how others use YOUR APIs)
1888
+ const previewByDay = [
1889
+ { date: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], calls: 8 },
1890
+ { date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], calls: 15 },
1891
+ { date: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], calls: 23 },
1892
+ { date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], calls: 19 },
1893
+ { date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], calls: 34 },
1894
+ { date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], calls: 28 },
1895
+ { date: new Date().toISOString().split('T')[0], calls: 21 },
1896
+ ];
1897
+
1898
+ // Preview shows YOUR listed APIs and agents using them
1899
+ const previewByApi = [
1900
+ { provider: "Your API Name", calls: 89, cost: 4.45 },
1901
+ { provider: "Another API", calls: 42, cost: 2.10 },
1902
+ { provider: "Third API", calls: 17, cost: 0.85 },
1903
+ ];
1904
+
1905
+ const isPreview = !hasRealData;
1906
+ const displayByDay = hasRealData ? usage!.byDay : previewByDay;
1907
+ const displayByProvider = hasRealData ? usage!.byProvider : previewByApi;
1908
+ const displayTotal = hasRealData ? (usage?.total || workspace?.usageCount || 0) : 148;
1894
1909
 
1895
1910
  return (
1896
1911
  <div className="space-y-8">
1912
+ {/* Preview Banner */}
1913
+ {isPreview && (
1914
+ <div className="bg-[#ef4444]/10 border border-[#ef4444]/30 rounded-xl p-4 flex items-center gap-3">
1915
+ <AlertCircle className="w-5 h-5 text-[#ef4444] flex-shrink-0" />
1916
+ <div>
1917
+ <p className="font-medium text-[#ef4444]">Preview Mode</p>
1918
+ <p className="text-sm text-[var(--text-muted)]">This is sample data. Real analytics will appear once agents start using your listed APIs.</p>
1919
+ </div>
1920
+ </div>
1921
+ )}
1922
+
1897
1923
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-4">
1898
1924
  <div className="rounded-2xl border border-[#ef4444]/30 bg-[#ef4444]/10 p-4 sm:p-6">
1899
1925
  <div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
@@ -1901,87 +1927,73 @@ function UsageTab({
1901
1927
  <span className="text-sm sm:text-base text-[var(--text-muted)]">Total Calls</span>
1902
1928
  </div>
1903
1929
  <p className="text-2xl sm:text-4xl font-bold text-[#ef4444]">
1904
- {(usage?.total || workspace?.usageCount || 0).toLocaleString()}
1930
+ {displayTotal.toLocaleString()}
1905
1931
  </p>
1906
1932
  </div>
1907
1933
 
1908
1934
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-4 sm:p-6">
1909
1935
  <div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
1910
1936
  <TrendingUp className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--text-muted)]" />
1911
- <span className="text-sm sm:text-base text-[var(--text-muted)]">Providers Used</span>
1937
+ <span className="text-sm sm:text-base text-[var(--text-muted)]">Your APIs</span>
1912
1938
  </div>
1913
- <p className="text-2xl sm:text-4xl font-bold">{usage?.byProvider.length || 0}</p>
1939
+ <p className="text-2xl sm:text-4xl font-bold">{displayByProvider.length}</p>
1914
1940
  </div>
1915
1941
 
1916
1942
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-4 sm:p-6">
1917
1943
  <div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
1918
- <Shield className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--text-muted)]" />
1919
- <span className="text-sm sm:text-base text-[var(--text-muted)]">Remaining</span>
1944
+ <Users className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--text-muted)]" />
1945
+ <span className="text-sm sm:text-base text-[var(--text-muted)]">Unique Agents</span>
1920
1946
  </div>
1921
- <p className="text-2xl sm:text-4xl font-bold">{workspace?.usageRemaining.toLocaleString() || ""}</p>
1947
+ <p className="text-2xl sm:text-4xl font-bold">{isPreview ? "12" : "0"}</p>
1922
1948
  </div>
1923
1949
  </div>
1924
1950
 
1925
- {hasData ? (
1926
- <>
1927
- {usage!.byDay.length > 0 && (
1928
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1929
- <h3 className="font-semibold mb-4">Usage Over Time</h3>
1930
- <div className="h-80">
1931
- <ResponsiveContainer width="100%" height="100%">
1932
- <LineChart data={usage!.byDay}>
1933
- <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
1934
- <XAxis
1935
- dataKey="date"
1936
- tick={{ fontSize: 12, fill: "var(--text-muted)" }}
1937
- tickFormatter={(d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
1938
- />
1939
- <YAxis tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
1940
- <Tooltip
1941
- contentStyle={{
1942
- background: "var(--surface-elevated)",
1943
- border: "1px solid var(--border)",
1944
- borderRadius: "8px",
1945
- }}
1946
- />
1947
- <Line type="monotone" dataKey="calls" stroke="#ef4444" strokeWidth={2} dot={false} />
1948
- </LineChart>
1949
- </ResponsiveContainer>
1950
- </div>
1951
- </div>
1952
- )}
1951
+ {/* Usage Over Time Chart */}
1952
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1953
+ <h3 className="font-semibold mb-4">Usage Over Time</h3>
1954
+ <div className="h-80">
1955
+ <ResponsiveContainer width="100%" height="100%">
1956
+ <LineChart data={displayByDay}>
1957
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
1958
+ <XAxis
1959
+ dataKey="date"
1960
+ tick={{ fontSize: 12, fill: "var(--text-muted)" }}
1961
+ tickFormatter={(d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
1962
+ />
1963
+ <YAxis tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
1964
+ <Tooltip
1965
+ contentStyle={{
1966
+ background: "var(--surface-elevated)",
1967
+ border: "1px solid var(--border)",
1968
+ borderRadius: "8px",
1969
+ }}
1970
+ />
1971
+ <Line type="monotone" dataKey="calls" stroke="#ef4444" strokeWidth={2} dot={false} />
1972
+ </LineChart>
1973
+ </ResponsiveContainer>
1974
+ </div>
1975
+ </div>
1953
1976
 
1954
- {usage!.byProvider.length > 0 && (
1955
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1956
- <h3 className="font-semibold mb-4">Usage by Provider</h3>
1957
- <div className="space-y-3">
1958
- {usage!.byProvider.map((p, i) => (
1959
- <div key={p.provider} className="flex items-center justify-between p-4 rounded-xl bg-[var(--surface)]">
1960
- <div className="flex items-center gap-3">
1961
- <span className="w-8 h-8 rounded-full bg-[#ef4444]/20 text-[#ef4444] flex items-center justify-center text-sm font-medium">
1962
- {i + 1}
1963
- </span>
1964
- <span className="font-medium">{p.provider}</span>
1965
- </div>
1966
- <div className="text-right">
1967
- <p className="font-semibold">{p.calls.toLocaleString()} calls</p>
1968
- {p.cost > 0 && <p className="text-sm text-[var(--text-muted)]">${p.cost.toFixed(2)}</p>}
1969
- </div>
1970
- </div>
1971
- ))}
1977
+ {/* Calls to Your APIs */}
1978
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1979
+ <h3 className="font-semibold mb-4">Calls to Your APIs</h3>
1980
+ <div className="space-y-3">
1981
+ {displayByProvider.map((p, i) => (
1982
+ <div key={p.provider} className="flex items-center justify-between p-4 rounded-xl bg-[var(--surface)]">
1983
+ <div className="flex items-center gap-3">
1984
+ <span className="w-8 h-8 rounded-full bg-[#ef4444]/20 text-[#ef4444] flex items-center justify-center text-sm font-medium">
1985
+ {i + 1}
1986
+ </span>
1987
+ <span className="font-medium">{p.provider}</span>
1988
+ </div>
1989
+ <div className="text-right">
1990
+ <p className="font-semibold">{p.calls.toLocaleString()} calls</p>
1991
+ {p.cost > 0 && <p className="text-sm text-[var(--text-muted)]">${p.cost.toFixed(2)}</p>}
1972
1992
  </div>
1973
1993
  </div>
1974
- )}
1975
- </>
1976
- ) : (
1977
- <div className="rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50 p-12 text-center">
1978
- <TrendingUp className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-4" />
1979
- <h3 className="font-semibold text-lg mb-2">No Usage Data Yet</h3>
1980
- <p className="text-[var(--text-muted)] max-w-md mx-auto">
1981
- When your agents start making API calls, usage analytics will appear here.
1982
- </p>
1994
+ ))}
1983
1995
  </div>
1984
- )}
1996
+ </div>
1985
1997
  </div>
1986
1998
  );
1987
1999
  }
@@ -2445,7 +2457,7 @@ function BillingTab({ workspace, sessionToken }: { workspace: Workspace | null;
2445
2457
 
2446
2458
  // Calculate estimated cost
2447
2459
  const FREE_CALLS = 100;
2448
- const COST_PER_CALL = 0.01;
2460
+ const COST_PER_CALL = 0.002;
2449
2461
  const currentUsage = billingInfo?.currentPeriodUsage || workspace?.usageCount || 0;
2450
2462
  const billableCalls = Math.max(0, currentUsage - FREE_CALLS);
2451
2463
  const estimatedCost = billableCalls * COST_PER_CALL;
@@ -2525,11 +2537,11 @@ function BillingTab({ workspace, sessionToken }: { workspace: Workspace | null;
2525
2537
  </div>
2526
2538
  <div className="flex items-center justify-between py-3 border-b border-[var(--border)]">
2527
2539
  <span className="text-[var(--text-muted)]">Free Tier</span>
2528
- <span className="font-medium">100 calls / month</span>
2540
+ <span className="font-medium">50 calls / month</span>
2529
2541
  </div>
2530
2542
  <div className="flex items-center justify-between py-3 border-b border-[var(--border)]">
2531
2543
  <span className="text-[var(--text-muted)]">Rate</span>
2532
- <span className="font-medium">$0.01 / call (after free tier)</span>
2544
+ <span className="font-medium">$0.002 / call (after free tier)</span>
2533
2545
  </div>
2534
2546
  <div className="flex items-center justify-between py-3">
2535
2547
  <span className="text-[var(--text-muted)]">Support</span>
@@ -2693,7 +2705,7 @@ function BillingTab({ workspace, sessionToken }: { workspace: Workspace | null;
2693
2705
  <div>
2694
2706
  <h3 className="font-bold text-xl mb-2">Unlock Unlimited API Calls</h3>
2695
2707
  <p className="text-[var(--text-muted)]">
2696
- Pay only for what you use. First 100 calls free every month, then just $0.01 per call.
2708
+ Pay only for what you use. First 50 calls free every month, then just $0.002 per call.
2697
2709
  </p>
2698
2710
  </div>
2699
2711
  </div>
@@ -2701,11 +2713,11 @@ function BillingTab({ workspace, sessionToken }: { workspace: Workspace | null;
2701
2713
  <div className="grid md:grid-cols-2 gap-4 mb-6">
2702
2714
  <div className="flex items-center gap-3">
2703
2715
  <Check className="w-5 h-5 text-green-500" />
2704
- <span>100 free calls / month</span>
2716
+ <span>50 free calls / month</span>
2705
2717
  </div>
2706
2718
  <div className="flex items-center gap-3">
2707
2719
  <Check className="w-5 h-5 text-green-500" />
2708
- <span>$0.01 per additional call</span>
2720
+ <span>$0.002 per additional call</span>
2709
2721
  </div>
2710
2722
  <div className="flex items-center gap-3">
2711
2723
  <Check className="w-5 h-5 text-green-500" />
@@ -2724,7 +2736,7 @@ function BillingTab({ workspace, sessionToken }: { workspace: Workspace | null;
2724
2736
  </CheckoutButton>
2725
2737
 
2726
2738
  <p className="mt-4 text-sm text-[var(--text-muted)]">
2727
- You&apos;ll only be charged for usage beyond 100 free calls. Billed monthly.
2739
+ You&apos;ll only be charged for usage beyond 50 free calls. Billed monthly.
2728
2740
  </p>
2729
2741
  </div>
2730
2742
  )}
@@ -3455,12 +3467,16 @@ interface BYOKProvider {
3455
3467
  }
3456
3468
 
3457
3469
  const BYOK_PROVIDERS: BYOKProvider[] = [
3458
- { id: "brave_search", name: "Brave Search", icon: "search" },
3459
- { id: "openrouter", name: "OpenRouter", icon: "cpu" },
3460
- { id: "elevenlabs", name: "ElevenLabs", icon: "activity" },
3470
+ { id: "46elks", name: "46elks", icon: "phone" },
3461
3471
  { id: "twilio", name: "Twilio", icon: "phone" },
3462
3472
  { id: "resend", name: "Resend", icon: "mail" },
3473
+ { id: "openrouter", name: "OpenRouter", icon: "cpu" },
3474
+ { id: "elevenlabs", name: "ElevenLabs", icon: "activity" },
3475
+ { id: "replicate", name: "Replicate", icon: "sparkles" },
3476
+ { id: "firecrawl", name: "Firecrawl", icon: "globe" },
3477
+ { id: "brave_search", name: "Brave Search", icon: "search" },
3463
3478
  { id: "e2b", name: "E2B", icon: "terminal" },
3479
+ { id: "github", name: "GitHub", icon: "database" },
3464
3480
  ];
3465
3481
 
3466
3482
  const ProviderIcon = ({ iconName, className = "w-6 h-6" }: { iconName: string; className?: string }) => {
@@ -3471,141 +3487,73 @@ const ProviderIcon = ({ iconName, className = "w-6 h-6" }: { iconName: string; c
3471
3487
  case "phone": return <Phone className={className} />;
3472
3488
  case "mail": return <Mail className={className} />;
3473
3489
  case "terminal": return <Terminal className={className} />;
3490
+ case "sparkles": return <Sparkles className={className} />;
3491
+ case "globe": return <Globe className={className} />;
3492
+ case "database": return <Database className={className} />;
3474
3493
  default: return <Zap className={className} />;
3475
3494
  }
3476
3495
  };
3477
3496
 
3478
- function ApiKeysTab() {
3479
- const [keys, setKeys] = useState<ProviderKey[]>([]);
3497
+ function ApiKeysTab({ workspace, providerApis, sessionToken }: { workspace: Workspace | null; providerApis: ProviderAPI[]; sessionToken: string | null }) {
3498
+ const [copied, setCopied] = useState(false);
3499
+ const [directCallConfigs, setDirectCallConfigs] = useState<Record<string, { status: string; keyHint: string }>>({});
3480
3500
  const [isLoading, setIsLoading] = useState(true);
3481
- const [showAddModal, setShowAddModal] = useState(false);
3482
- const [selectedProvider, setSelectedProvider] = useState<BYOKProvider | null>(null);
3483
- const [apiKeyInput, setApiKeyInput] = useState("");
3484
- const [isSaving, setIsSaving] = useState(false);
3485
- const [confirmRemove, setConfirmRemove] = useState<string | null>(null);
3486
- const [successMessage, setSuccessMessage] = useState<string | null>(null);
3487
- const [errorMessage, setErrorMessage] = useState<string | null>(null);
3501
+
3502
+ // Generate workspace API key from email (simple hash for display)
3503
+ const workspaceKey = workspace?.email
3504
+ ? `claw_${btoa(workspace.email).replace(/[^a-zA-Z0-9]/g, "").substring(0, 24)}`
3505
+ : null;
3488
3506
 
3489
3507
  useEffect(() => {
3490
- const fetchKeys = async () => {
3491
- const token = localStorage.getItem("apiclaw_workspace_session");
3492
- if (!token) {
3508
+ const fetchDirectCallConfigs = async () => {
3509
+ if (!sessionToken || providerApis.length === 0) {
3493
3510
  setIsLoading(false);
3494
3511
  return;
3495
3512
  }
3496
3513
 
3497
3514
  try {
3498
- const res = await fetch(`${CONVEX_URL}/api/query`, {
3499
- method: "POST",
3500
- headers: { "Content-Type": "application/json" },
3501
- body: JSON.stringify({
3502
- path: "providerKeys:getKeys",
3503
- args: { token },
3504
- }),
3505
- });
3506
- const data = await res.json();
3507
- setKeys(data.value?.keys || data.keys || []);
3515
+ // Fetch Direct Call configs for each API
3516
+ const configs: Record<string, { status: string; keyHint: string }> = {};
3517
+ for (const api of providerApis) {
3518
+ try {
3519
+ const res = await fetch(`${CONVEX_URL}/api/query`, {
3520
+ method: "POST",
3521
+ headers: { "Content-Type": "application/json" },
3522
+ body: JSON.stringify({
3523
+ path: "providerDirectCall:get",
3524
+ args: { token: sessionToken, apiId: api._id },
3525
+ }),
3526
+ });
3527
+ const data = await res.json();
3528
+ const config = data.value || data;
3529
+ if (config && config.status) {
3530
+ configs[api._id] = {
3531
+ status: config.status,
3532
+ keyHint: config.encryptedMasterKey ? "••••" + config.encryptedMasterKey.slice(-4) : "Not set",
3533
+ };
3534
+ }
3535
+ } catch (e) {
3536
+ // Skip this API if config fetch fails
3537
+ }
3538
+ }
3539
+ setDirectCallConfigs(configs);
3508
3540
  } catch (err) {
3509
- console.error("Failed to fetch keys:", err);
3541
+ console.error("Failed to fetch Direct Call configs:", err);
3510
3542
  } finally {
3511
3543
  setIsLoading(false);
3512
3544
  }
3513
3545
  };
3514
3546
 
3515
- fetchKeys();
3516
- }, []);
3517
-
3518
- const handleAddKey = async () => {
3519
- if (!selectedProvider || !apiKeyInput.trim()) return;
3547
+ fetchDirectCallConfigs();
3548
+ }, [sessionToken, providerApis]);
3520
3549
 
3521
- const token = localStorage.getItem("apiclaw_workspace_session");
3522
- if (!token) return;
3523
-
3524
- setIsSaving(true);
3525
- setErrorMessage(null);
3526
-
3527
- try {
3528
- const res = await fetch(`${CONVEX_URL}/api/mutation`, {
3529
- method: "POST",
3530
- headers: { "Content-Type": "application/json" },
3531
- body: JSON.stringify({
3532
- path: "providerKeys:addKey",
3533
- args: {
3534
- token,
3535
- provider: selectedProvider.id,
3536
- apiKey: apiKeyInput,
3537
- },
3538
- }),
3539
- });
3540
- const data = await res.json();
3541
-
3542
- if (data.value?.success || data.success) {
3543
- const keysRes = await fetch(`${CONVEX_URL}/api/query`, {
3544
- method: "POST",
3545
- headers: { "Content-Type": "application/json" },
3546
- body: JSON.stringify({
3547
- path: "providerKeys:getKeys",
3548
- args: { token },
3549
- }),
3550
- });
3551
- const keysData = await keysRes.json();
3552
- setKeys(keysData.value?.keys || keysData.keys || []);
3553
-
3554
- setSuccessMessage(`Key saved! Using your key for ${selectedProvider.name}`);
3555
- setTimeout(() => setSuccessMessage(null), 3000);
3556
- setShowAddModal(false);
3557
- setApiKeyInput("");
3558
- setSelectedProvider(null);
3559
- } else {
3560
- setErrorMessage("Failed to save key. Please try again.");
3561
- }
3562
- } catch (err) {
3563
- console.error("Failed to add key:", err);
3564
- setErrorMessage("Failed to save key. Please try again.");
3565
- } finally {
3566
- setIsSaving(false);
3567
- }
3568
- };
3569
-
3570
- const handleRemoveKey = async (providerId: string) => {
3571
- if (confirmRemove !== providerId) {
3572
- setConfirmRemove(providerId);
3573
- return;
3574
- }
3575
-
3576
- const token = localStorage.getItem("apiclaw_workspace_session");
3577
- if (!token) return;
3578
-
3579
- try {
3580
- await fetch(`${CONVEX_URL}/api/mutation`, {
3581
- method: "POST",
3582
- headers: { "Content-Type": "application/json" },
3583
- body: JSON.stringify({
3584
- path: "providerKeys:removeKey",
3585
- args: { token, provider: providerId },
3586
- }),
3587
- });
3588
-
3589
- setKeys(keys.filter((k) => k.provider !== providerId));
3590
- setConfirmRemove(null);
3591
- setSuccessMessage("Key removed. Back to Direct Call.");
3592
- setTimeout(() => setSuccessMessage(null), 3000);
3593
- } catch (err) {
3594
- console.error("Failed to remove key:", err);
3595
- setErrorMessage("Failed to remove key. Please try again.");
3596
- }
3550
+ const copyToClipboard = (text: string) => {
3551
+ navigator.clipboard.writeText(text);
3552
+ setCopied(true);
3553
+ setTimeout(() => setCopied(false), 2000);
3597
3554
  };
3598
3555
 
3599
- const getKeyForProvider = (providerId: string) => {
3600
- return keys.find((k) => k.provider === providerId);
3601
- };
3602
-
3603
- const openAddModal = (provider: BYOKProvider) => {
3604
- setSelectedProvider(provider);
3605
- setApiKeyInput("");
3606
- setErrorMessage(null);
3607
- setShowAddModal(true);
3608
- };
3556
+ const directCallApis = providerApis.filter((api) => directCallConfigs[api._id]);
3609
3557
 
3610
3558
  if (isLoading) {
3611
3559
  return (
@@ -3620,292 +3568,133 @@ function ApiKeysTab() {
3620
3568
  <div>
3621
3569
  <h2 className="text-2xl font-bold mb-2">API Keys</h2>
3622
3570
  <p className="text-[var(--text-muted)]">
3623
- Direct Call works without keys. Add your own for unlimited calls and direct provider access.
3571
+ Manage your workspace API key and Direct Call service credentials.
3624
3572
  </p>
3625
3573
  </div>
3626
3574
 
3627
- {successMessage && (
3628
- <div className="rounded-xl border border-green-500/30 bg-green-500/10 p-4 flex items-center gap-3">
3629
- <Check className="w-5 h-5 text-green-500 flex-shrink-0" />
3630
- <p className="text-green-500">{successMessage}</p>
3631
- </div>
3632
- )}
3633
-
3634
- {errorMessage && (
3635
- <div className="rounded-xl border border-red-500/30 bg-red-500/10 p-4 flex items-center gap-3">
3636
- <AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0" />
3637
- <p className="text-red-500">{errorMessage}</p>
3638
- </div>
3639
- )}
3640
-
3641
- <div className="rounded-2xl border border-[#ef4444]/30 bg-[#ef4444]/10 p-6">
3575
+ {/* Workspace API Key */}
3576
+ <div className="rounded-2xl border border-[#ef4444]/30 bg-[var(--surface-elevated)] p-6">
3642
3577
  <div className="flex items-start gap-4">
3643
- <div className="w-10 h-10 rounded-xl bg-[#ef4444]/20 flex items-center justify-center flex-shrink-0">
3644
- <Key className="w-5 h-5 text-[#ef4444]" />
3578
+ <div className="w-12 h-12 rounded-xl bg-[#ef4444]/20 flex items-center justify-center flex-shrink-0">
3579
+ <Key className="w-6 h-6 text-[#ef4444]" />
3645
3580
  </div>
3646
- <div>
3647
- <h3 className="font-semibold mb-2">Direct Call is Default</h3>
3648
- <p className="text-sm text-[var(--text-muted)]">
3649
- No API keys needed APIClaw handles authentication for you. Add your own keys to bypass usage limits and route requests directly to providers.
3581
+ <div className="flex-1">
3582
+ <h3 className="font-semibold text-lg mb-1">Workspace API Key</h3>
3583
+ <p className="text-sm text-[var(--text-muted)] mb-4">
3584
+ Use this key to authenticate API calls from your agents.
3650
3585
  </p>
3651
- </div>
3652
- </div>
3653
- </div>
3654
-
3655
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] overflow-hidden">
3656
- <div className="p-4 border-b border-[var(--border)]">
3657
- <h3 className="font-semibold">Providers</h3>
3658
- </div>
3659
- <div className="divide-y divide-[var(--border)]">
3660
- {BYOK_PROVIDERS.map((provider) => {
3661
- const userKey = getKeyForProvider(provider.id);
3662
- const hasKey = !!userKey;
3663
-
3664
- return (
3665
- <div
3666
- key={provider.id}
3667
- className="flex flex-col sm:flex-row sm:items-center justify-between p-4 hover:bg-[var(--surface)] transition gap-3"
3668
- >
3669
- <div className="flex items-center gap-3">
3670
- <div className="w-8 h-8 rounded-lg bg-[#ef4444]/10 flex items-center justify-center">
3671
- <ProviderIcon iconName={provider.icon} className="w-5 h-5 text-[#ef4444]" />
3672
- </div>
3673
- <span className="font-medium">{provider.name}</span>
3674
- </div>
3675
- <div className="flex items-center gap-3 ml-10 sm:ml-0">
3676
- {hasKey ? (
3677
- <>
3678
- <span className="px-3 py-1 rounded-full bg-[#ef4444]/20 text-[#ef4444] text-sm font-medium">
3679
- Your Key (•••• {userKey.keyHint})
3680
- </span>
3681
- <button
3682
- onClick={() => openAddModal(provider)}
3683
- className="px-3 py-1.5 rounded-lg text-sm font-medium text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--surface)] transition"
3684
- >
3685
- Edit
3686
- </button>
3687
- <button
3688
- onClick={() => handleRemoveKey(provider.id)}
3689
- className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
3690
- confirmRemove === provider.id
3691
- ? "bg-red-500 text-white"
3692
- : "text-red-500 hover:bg-red-500/10"
3693
- }`}
3694
- >
3695
- {confirmRemove === provider.id ? "Confirm" : "Remove"}
3696
- </button>
3697
- </>
3698
- ) : (
3699
- <>
3700
- <span className="px-3 py-1 rounded-full bg-green-500/20 text-green-500 text-sm font-medium">
3701
- Direct Call
3702
- </span>
3703
- <button
3704
- onClick={() => openAddModal(provider)}
3705
- className="px-4 py-2 rounded-lg border border-[var(--border)] text-sm font-medium hover:bg-[var(--surface)] hover:border-[#ef4444]/50 transition"
3706
- >
3707
- Add Your Key
3708
- </button>
3709
- </>
3710
- )}
3711
- </div>
3712
- </div>
3713
- );
3714
- })}
3715
- </div>
3716
- </div>
3717
-
3718
- <div className="relative group">
3719
- <button
3720
- className="w-full rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50 p-6 text-center hover:border-[#ef4444]/50 transition opacity-50 cursor-not-allowed"
3721
- disabled
3722
- >
3723
- <Plus className="w-8 h-8 text-[var(--text-muted)] mx-auto mb-2" />
3724
- <p className="font-medium text-[var(--text-muted)]">+ Add Custom Provider</p>
3725
- <p className="text-sm text-[var(--text-muted)] mt-1">Connect any REST API with custom authentication</p>
3726
- </button>
3727
- <div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition">
3728
- <span className="px-3 py-1.5 rounded-lg bg-[var(--background)] border border-[var(--border)] text-sm font-medium shadow-lg">
3729
- Coming soon
3730
- </span>
3731
- </div>
3732
- </div>
3733
-
3734
- {showAddModal && selectedProvider && (
3735
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
3736
- <div className="bg-[var(--surface-elevated)] rounded-2xl border border-[var(--border)] w-full max-w-md p-6">
3737
- <div className="flex items-center gap-3 mb-6">
3738
- <div className="w-12 h-12 rounded-xl bg-[#ef4444]/10 flex items-center justify-center">
3739
- <ProviderIcon iconName={selectedProvider.icon} className="w-7 h-7 text-[#ef4444]" />
3740
- </div>
3741
- <div>
3742
- <h3 className="font-bold text-lg">
3743
- {getKeyForProvider(selectedProvider.id) ? "Update" : "Add"} {selectedProvider.name} Key
3744
- </h3>
3745
- <p className="text-sm text-[var(--text-muted)]">
3746
- Your key will be encrypted and stored securely.
3747
- </p>
3748
- </div>
3749
- </div>
3750
-
3751
- <div className="space-y-4">
3752
- <div>
3753
- <label className="block text-sm font-medium mb-2">API Key</label>
3754
- <div className="relative">
3755
- <input
3756
- type="password"
3757
- value={apiKeyInput}
3758
- onChange={(e) => setApiKeyInput(e.target.value)}
3759
- placeholder="Enter your API key..."
3760
- className="w-full px-4 py-3 rounded-xl border border-[var(--border)] bg-[var(--background)] text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50 pr-10"
3761
- autoFocus
3762
- />
3763
- <Lock className="absolute right-3 top-1/2 -translate-y-1/2 w-5 h-5 text-[var(--text-muted)]" />
3764
- </div>
3765
- </div>
3766
-
3767
- {errorMessage && (
3768
- <p className="text-sm text-red-500">{errorMessage}</p>
3769
- )}
3770
-
3771
- <div className="flex gap-3 pt-2">
3772
- <button
3773
- onClick={() => {
3774
- setShowAddModal(false);
3775
- setApiKeyInput("");
3776
- setSelectedProvider(null);
3777
- setErrorMessage(null);
3778
- }}
3779
- className="flex-1 px-4 py-2.5 rounded-xl border border-[var(--border)] text-[var(--text-secondary)] font-medium hover:bg-[var(--surface)] transition"
3780
- >
3781
- Cancel
3782
- </button>
3586
+ {workspaceKey ? (
3587
+ <div className="flex items-center gap-3">
3588
+ <code className="flex-1 px-4 py-3 rounded-xl bg-[var(--background)] border border-[var(--border)] font-mono text-sm">
3589
+ {workspaceKey}
3590
+ </code>
3783
3591
  <button
3784
- onClick={handleAddKey}
3785
- disabled={!apiKeyInput.trim() || isSaving}
3786
- className="flex-1 px-4 py-2.5 rounded-xl bg-[#ef4444] text-white font-medium hover:bg-[#dc2626] transition disabled:opacity-50 flex items-center justify-center gap-2"
3592
+ onClick={() => copyToClipboard(workspaceKey)}
3593
+ className="px-4 py-3 rounded-xl border border-[var(--border)] hover:bg-[var(--surface)] transition flex items-center gap-2"
3787
3594
  >
3788
- {isSaving ? (
3595
+ {copied ? (
3789
3596
  <>
3790
- <Loader2 className="w-4 h-4 animate-spin" />
3791
- Saving...
3597
+ <Check className="w-4 h-4 text-green-500" />
3598
+ <span className="text-green-500">Copied</span>
3792
3599
  </>
3793
3600
  ) : (
3794
3601
  <>
3795
- <Check className="w-4 h-4" />
3796
- Save
3602
+ <Copy className="w-4 h-4" />
3603
+ <span>Copy</span>
3797
3604
  </>
3798
3605
  )}
3799
3606
  </button>
3800
3607
  </div>
3801
- </div>
3608
+ ) : (
3609
+ <p className="text-[var(--text-muted)]">Log in to see your API key.</p>
3610
+ )}
3802
3611
  </div>
3803
3612
  </div>
3804
- )}
3805
- </div>
3806
- );
3807
- }
3808
-
3809
- // ============================================
3810
- // EARN TAB
3811
- // ============================================
3812
-
3813
- function EarnTab() {
3814
- const [email, setEmail] = useState("");
3815
- const [subscribed, setSubscribed] = useState(false);
3816
- const [copied, setCopied] = useState(false);
3817
- const referralCode = "CLAW-" + Math.random().toString(36).substring(2, 8).toUpperCase();
3818
-
3819
- const earnChannels = [
3820
- { id: "github", title: "Star on GitHub", credits: 20, href: "https://github.com/nordsym/apiclaw", icon: "star" },
3821
- { id: "twitter", title: "Follow @NordSym", credits: 15, href: "https://x.com/NordSym", icon: "twitter" },
3822
- { id: "newsletter", title: "Join Newsletter", credits: 15, href: "#newsletter", icon: "mail" },
3823
- ];
3824
-
3825
- const EarnIcon = ({ iconName }: { iconName: string }) => {
3826
- const iconClass = "w-8 h-8 text-[#ef4444]";
3827
- switch (iconName) {
3828
- case "star": return <Star className={iconClass} />;
3829
- case "twitter": return <Twitter className={iconClass} />;
3830
- case "mail": return <Mail className={iconClass} />;
3831
- default: return <Zap className={iconClass} />;
3832
- }
3833
- };
3834
-
3835
- const handleCopyReferral = () => {
3836
- navigator.clipboard.writeText("https://apiclaw.nordsym.com?ref=" + referralCode);
3837
- setCopied(true);
3838
- setTimeout(() => setCopied(false), 2000);
3839
- };
3840
-
3841
- return (
3842
- <div className="space-y-6">
3843
- <div>
3844
- <h2 className="text-2xl font-bold mb-2">Earn Credits</h2>
3845
- <p className="text-[var(--text-muted)]">Complete tasks to earn free API calls. Max 50 extra calls.</p>
3846
3613
  </div>
3847
3614
 
3848
- <div className="grid gap-4 md:grid-cols-3">
3849
- {earnChannels.map((channel) => (
3850
- <a
3851
- key={channel.id}
3852
- href={channel.href}
3853
- target="_blank"
3854
- rel="noopener noreferrer"
3855
- className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6 hover:border-[#ef4444]/50 transition group"
3856
- >
3857
- <div className="w-12 h-12 rounded-xl bg-[#ef4444]/10 flex items-center justify-center mb-3">
3858
- <EarnIcon iconName={channel.icon} />
3859
- </div>
3860
- <h3 className="font-semibold mb-1">{channel.title}</h3>
3861
- <p className="text-sm text-[#ef4444] font-medium">+{channel.credits} calls</p>
3862
- </a>
3863
- ))}
3864
- </div>
3865
-
3866
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-4 sm:p-6">
3867
- <h3 className="font-semibold mb-2">Invite Friends</h3>
3868
- <p className="text-sm text-[var(--text-muted)] mb-4">Earn 10 calls for each friend who joins.</p>
3869
- <div className="flex flex-col sm:flex-row gap-3">
3870
- <input
3871
- type="text"
3872
- value={"https://apiclaw.nordsym.com?ref=" + referralCode}
3873
- readOnly
3874
- className="w-full sm:flex-1 px-4 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm"
3875
- />
3876
- <button
3877
- onClick={handleCopyReferral}
3878
- className="w-full sm:w-auto px-4 py-2 bg-[#ef4444] text-white rounded-lg font-medium hover:bg-[#dc2626] transition"
3879
- >
3880
- {copied ? "Copied!" : "Copy"}
3881
- </button>
3615
+ {/* Direct Call Service Keys */}
3616
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] overflow-hidden">
3617
+ <div className="p-4 border-b border-[var(--border)] flex items-center justify-between">
3618
+ <h3 className="font-semibold">Direct Call Service Keys</h3>
3619
+ <span className="text-sm text-[var(--text-muted)]">{directCallApis.length} configured</span>
3882
3620
  </div>
3621
+
3622
+ {directCallApis.length > 0 ? (
3623
+ <div className="divide-y divide-[var(--border)]">
3624
+ {directCallApis.map((api) => {
3625
+ const config = directCallConfigs[api._id];
3626
+ return (
3627
+ <div
3628
+ key={api._id}
3629
+ className="flex flex-col sm:flex-row sm:items-center justify-between p-4 hover:bg-[var(--surface)] transition gap-3"
3630
+ >
3631
+ <div className="flex items-center gap-3">
3632
+ <div className="w-10 h-10 rounded-xl bg-[#ef4444]/10 flex items-center justify-center">
3633
+ <Zap className="w-5 h-5 text-[#ef4444]" />
3634
+ </div>
3635
+ <div>
3636
+ <p className="font-medium">{api.name}</p>
3637
+ <p className="text-sm text-[var(--text-muted)]">{api.category}</p>
3638
+ </div>
3639
+ </div>
3640
+ <div className="flex items-center gap-3 ml-13 sm:ml-0">
3641
+ <span className="px-3 py-1 rounded-full bg-[var(--surface)] text-sm font-mono text-[var(--text-muted)]">
3642
+ {config.keyHint}
3643
+ </span>
3644
+ <span className={`px-3 py-1 rounded-full text-sm font-medium ${
3645
+ config.status === "live"
3646
+ ? "bg-green-500/20 text-green-500"
3647
+ : config.status === "testing"
3648
+ ? "bg-yellow-500/20 text-yellow-500"
3649
+ : "bg-[var(--surface)] text-[var(--text-muted)]"
3650
+ }`}>
3651
+ {config.status}
3652
+ </span>
3653
+ <a
3654
+ href={`/providers/dashboard/${api._id}/direct-call`}
3655
+ className="px-3 py-1.5 rounded-lg text-sm font-medium text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--surface)] transition"
3656
+ >
3657
+ Manage →
3658
+ </a>
3659
+ </div>
3660
+ </div>
3661
+ );
3662
+ })}
3663
+ </div>
3664
+ ) : (
3665
+ <div className="p-8 text-center">
3666
+ <Key className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-4" />
3667
+ <h4 className="font-semibold mb-2">No Direct Call APIs</h4>
3668
+ <p className="text-sm text-[var(--text-muted)] mb-4">
3669
+ Service keys appear here when you set up Direct Call for your APIs.
3670
+ </p>
3671
+ <a
3672
+ href="/workspace?tab=my-apis"
3673
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-xl border border-[#ef4444]/50 text-[#ef4444] font-medium hover:bg-[#ef4444]/10 transition"
3674
+ >
3675
+ <Plus className="w-4 h-4" />
3676
+ Set Up Direct Call
3677
+ </a>
3678
+ </div>
3679
+ )}
3883
3680
  </div>
3884
3681
 
3885
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-4 sm:p-6">
3886
- <h3 className="font-semibold mb-2">Newsletter (+15 calls)</h3>
3887
- <p className="text-sm text-[var(--text-muted)] mb-4">Get weekly updates, tips, and new API announcements.</p>
3888
- <div className="flex flex-col sm:flex-row gap-3">
3889
- <input
3890
- type="email"
3891
- value={email}
3892
- onChange={(e) => setEmail(e.target.value)}
3893
- placeholder="your@email.com"
3894
- className="w-full sm:flex-1 px-4 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)]"
3895
- />
3896
- <button
3897
- onClick={() => setSubscribed(true)}
3898
- disabled={subscribed}
3899
- className="w-full sm:w-auto px-4 py-2 bg-[#ef4444] text-white rounded-lg font-medium hover:bg-[#dc2626] transition disabled:opacity-50"
3900
- >
3901
- {subscribed ? "Subscribed!" : "Subscribe"}
3902
- </button>
3682
+ {/* Info Box */}
3683
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-6">
3684
+ <div className="flex items-start gap-4">
3685
+ <AlertCircle className="w-5 h-5 text-[var(--text-muted)] flex-shrink-0 mt-0.5" />
3686
+ <div className="text-sm text-[var(--text-muted)]">
3687
+ <p className="mb-2"><strong>Workspace API Key:</strong> Authenticate your agent's requests to APIClaw.</p>
3688
+ <p><strong>Direct Call Service Keys:</strong> Your API credentials for provider integrations. Manage these in each API's Direct Call settings.</p>
3689
+ </div>
3903
3690
  </div>
3904
3691
  </div>
3905
3692
  </div>
3906
3693
  );
3907
3694
  }
3908
3695
 
3696
+ // EarnTab moved to components/EarnCreditsTab.tsx
3697
+
3909
3698
  // ============================================
3910
3699
  // DOCS TAB
3911
3700
  // ============================================
@@ -3936,7 +3725,7 @@ function DocsTab() {
3936
3725
  </div>
3937
3726
  <div>
3938
3727
  <p className="text-sm text-[var(--text-muted)] mb-2">2. Or run directly:</p>
3939
- <pre className="bg-[var(--background)] rounded-lg p-4 text-sm">npx @nordsym/apiclaw</pre>
3728
+ <pre className="bg-[var(--background)] rounded-lg p-4 text-sm">npx @nordsym/apiclaw mcp-install</pre>
3940
3729
  </div>
3941
3730
  <div>
3942
3731
  <p className="text-sm text-[var(--text-muted)] mb-2">3. Interactive CLI mode:</p>
@@ -4222,101 +4011,6 @@ function FeedbackTab() {
4222
4011
  </div>
4223
4012
  </form>
4224
4013
  </div>
4225
-
4226
- <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
4227
- <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
4228
- <h3 className="font-semibold text-lg">Community Feedback</h3>
4229
- <div className="flex flex-wrap gap-2">
4230
- <select
4231
- value={filterType}
4232
- onChange={(e) => setFilterType(e.target.value as typeof filterType)}
4233
- className="px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
4234
- >
4235
- <option value="all">All Types</option>
4236
- <option value="bug">Bugs</option>
4237
- <option value="feature">Features</option>
4238
- <option value="general">General</option>
4239
- </select>
4240
-
4241
- <select
4242
- value={sortBy}
4243
- onChange={(e) => setSortBy(e.target.value as typeof sortBy)}
4244
- className="px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
4245
- >
4246
- <option value="votes">Most Votes</option>
4247
- <option value="recent">Most Recent</option>
4248
- </select>
4249
- </div>
4250
- </div>
4251
-
4252
- {loading ? (
4253
- <div className="text-center py-8">
4254
- <Loader2 className="w-8 h-8 text-[#ef4444] animate-spin mx-auto mb-2" />
4255
- <p className="text-sm text-[var(--text-muted)]">Loading feedback...</p>
4256
- </div>
4257
- ) : feedbackList.length === 0 ? (
4258
- <div className="text-center py-12 rounded-xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50">
4259
- <MessageSquare className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-3" />
4260
- <h4 className="font-semibold mb-1">No Feedback Yet</h4>
4261
- <p className="text-sm text-[var(--text-muted)]">
4262
- Be the first to share your thoughts!
4263
- </p>
4264
- </div>
4265
- ) : (
4266
- <div className="space-y-3">
4267
- {feedbackList.map((item) => (
4268
- <div
4269
- key={item._id}
4270
- className={`flex gap-3 p-4 rounded-xl border transition ${
4271
- item.isOwn
4272
- ? "border-[#ef4444]/30 bg-[#ef4444]/5"
4273
- : "border-[var(--border)] bg-[var(--surface)]"
4274
- }`}
4275
- >
4276
- <div className="flex flex-col items-center gap-1 min-w-[40px]">
4277
- <button
4278
- onClick={() => handleVote(item._id, "up")}
4279
- disabled={votingId === item._id}
4280
- className={`p-1 rounded hover:bg-[var(--background)] transition ${
4281
- item.hasVoted ? "text-[#ef4444]" : "text-[var(--text-muted)]"
4282
- }`}
4283
- >
4284
- <ChevronUp className="w-5 h-5" />
4285
- </button>
4286
- <span className={`text-sm font-bold ${item.votes > 0 ? "text-[#ef4444]" : item.votes < 0 ? "text-red-500" : "text-[var(--text-muted)]"}`}>
4287
- {item.votes}
4288
- </span>
4289
- <button
4290
- onClick={() => handleVote(item._id, "down")}
4291
- disabled={votingId === item._id}
4292
- className="p-1 rounded text-[var(--text-muted)] hover:bg-[var(--background)] transition"
4293
- >
4294
- <ChevronDown className="w-5 h-5" />
4295
- </button>
4296
- </div>
4297
-
4298
- <div className="flex-1 min-w-0">
4299
- <p className="text-[var(--text-primary)] mb-2">{item.content}</p>
4300
- <div className="flex flex-wrap items-center gap-2 text-xs">
4301
- <span className={`px-2 py-0.5 rounded-full capitalize ${getTypeBadge(item.type)}`}>
4302
- {item.type}
4303
- </span>
4304
- <span className={`px-2 py-0.5 rounded-full capitalize ${getStatusBadge(item.status)}`}>
4305
- {item.status}
4306
- </span>
4307
- <span className="text-[var(--text-muted)]">
4308
- {formatDate(item.createdAt)}
4309
- </span>
4310
- {item.isOwn && (
4311
- <span className="text-[#ef4444]">• You</span>
4312
- )}
4313
- </div>
4314
- </div>
4315
- </div>
4316
- ))}
4317
- </div>
4318
- )}
4319
- </div>
4320
4014
  </div>
4321
4015
  );
4322
4016
  }
@@ -4566,22 +4260,6 @@ function SettingsTab({ workspace, sessionToken }: { workspace: Workspace | null;
4566
4260
  </div>
4567
4261
  </SettingsSection>
4568
4262
 
4569
- <SettingsSection title="API Tokens" icon={Key}>
4570
- <div className="space-y-4 pt-4">
4571
- <p className="text-sm text-[var(--text-muted)]">
4572
- Generate API tokens for programmatic access to your workspace.
4573
- </p>
4574
- <button
4575
- disabled
4576
- className="w-full px-4 py-3 rounded-xl border border-dashed border-[var(--border)] text-sm font-medium text-[var(--text-muted)] hover:border-[#ef4444]/50 transition opacity-50 cursor-not-allowed flex items-center justify-center gap-2"
4577
- >
4578
- <Plus className="w-4 h-4" />
4579
- Generate New Token
4580
- </button>
4581
- <p className="text-xs text-[var(--text-muted)] text-center">Coming soon</p>
4582
- </div>
4583
- </SettingsSection>
4584
-
4585
4263
  <div className="rounded-2xl border border-red-500/30 bg-red-500/5 p-6">
4586
4264
  <h3 className="font-semibold text-red-500 mb-2">Danger Zone</h3>
4587
4265
  <p className="text-sm text-[var(--text-muted)] mb-4">