@nordsym/apiclaw 1.3.3 → 1.3.5

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 (42) hide show
  1. package/convex/_generated/api.d.ts +6 -0
  2. package/convex/billing.ts +341 -0
  3. package/convex/email.ts +276 -0
  4. package/convex/http.ts +154 -0
  5. package/convex/schema.ts +43 -0
  6. package/convex/workspaces.ts +663 -0
  7. package/dist/cli.d.ts +7 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +272 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/index.js +396 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/session.d.ts +29 -0
  14. package/dist/session.d.ts.map +1 -0
  15. package/dist/session.js +87 -0
  16. package/dist/session.js.map +1 -0
  17. package/docs/PRD-agent-first-billing.md +525 -0
  18. package/docs/PRD-workspace-fixes.md +178 -0
  19. package/landing/package-lock.json +21 -3
  20. package/landing/package.json +2 -1
  21. package/landing/src/app/api/stripe/webhook/route.ts +178 -0
  22. package/landing/src/app/api/workspace-auth/magic-link/route.ts +84 -0
  23. package/landing/src/app/api/workspace-auth/session/route.ts +73 -0
  24. package/landing/src/app/api/workspace-auth/verify/route.ts +57 -0
  25. package/landing/src/app/auth/verify/page.tsx +292 -0
  26. package/landing/src/app/dashboard/layout.tsx +22 -0
  27. package/landing/src/app/dashboard/page.tsx +22 -0
  28. package/landing/src/app/dashboard/verify/page.tsx +108 -0
  29. package/landing/src/app/login/page.tsx +204 -0
  30. package/landing/src/app/page.tsx +23 -7
  31. package/landing/src/app/providers/dashboard/layout.tsx +5 -4
  32. package/landing/src/app/providers/dashboard/page.tsx +11 -641
  33. package/landing/src/app/upgrade/page.tsx +288 -0
  34. package/landing/src/app/workspace/layout.tsx +30 -0
  35. package/landing/src/app/workspace/page.tsx +1637 -0
  36. package/landing/src/lib/stats.json +14 -15
  37. package/landing/src/middleware.ts +50 -0
  38. package/landing/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +1 -1
  40. package/src/cli.ts +320 -0
  41. package/src/index.ts +444 -4
  42. package/src/session.ts +103 -0
package/src/index.ts CHANGED
@@ -41,10 +41,106 @@ import {
41
41
  validateParams
42
42
  } from './confirmation.js';
43
43
  import { executeCapability, listCapabilities, hasCapability } from './capability-router.js';
44
+ import { readSession, writeSession, clearSession, getMachineFingerprint, SessionData } from './session.js';
45
+ import { ConvexHttpClient } from 'convex/browser';
44
46
 
45
47
  // Default agent ID for MVP (in production, this would come from auth)
46
48
  const DEFAULT_AGENT_ID = 'agent_default';
47
49
 
50
+ // Convex client for workspace management
51
+ const CONVEX_URL = process.env.CONVEX_URL || 'https://adventurous-avocet-799.convex.cloud';
52
+ const convex = new ConvexHttpClient(CONVEX_URL);
53
+
54
+ // Global workspace context (set on startup if session is valid)
55
+ interface WorkspaceContext {
56
+ sessionToken: string;
57
+ workspaceId: string;
58
+ email: string;
59
+ tier: string;
60
+ usageRemaining: number;
61
+ status: string;
62
+ }
63
+
64
+ let workspaceContext: WorkspaceContext | null = null;
65
+
66
+ /**
67
+ * Validate session on startup
68
+ */
69
+ async function validateSession(): Promise<boolean> {
70
+ const session = readSession();
71
+ if (!session) {
72
+ console.error('[APIClaw] No session found. Use register_owner to authenticate.');
73
+ return false;
74
+ }
75
+
76
+ try {
77
+ const result = await convex.query("workspaces:getWorkspaceStatus" as any, {
78
+ sessionToken: session.sessionToken,
79
+ }) as { authenticated: boolean; email?: string; status?: string; tier?: string; usageCount?: number; usageLimit?: number; usageRemaining?: number };
80
+
81
+ if (!result.authenticated) {
82
+ console.error('[APIClaw] Session invalid or expired. Clearing...');
83
+ clearSession();
84
+ return false;
85
+ }
86
+
87
+ if (result.status !== 'active') {
88
+ console.error(`[APIClaw] Workspace status: ${result.status}. Please verify your email.`);
89
+ return false;
90
+ }
91
+
92
+ workspaceContext = {
93
+ sessionToken: session.sessionToken,
94
+ workspaceId: session.workspaceId,
95
+ email: result.email ?? '',
96
+ tier: result.tier ?? 'free',
97
+ usageRemaining: result.usageRemaining ?? 0,
98
+ status: result.status ?? 'unknown',
99
+ };
100
+
101
+ console.error(`[APIClaw] ✓ Authenticated as ${result.email} (${result.tier} tier)`);
102
+ console.error(`[APIClaw] ✓ Usage: ${result.usageCount}/${result.usageLimit === -1 ? '∞' : result.usageLimit} calls`);
103
+
104
+ // Touch session to update last used
105
+ await convex.mutation("workspaces:touchSession" as any, {
106
+ sessionToken: session.sessionToken,
107
+ });
108
+
109
+ return true;
110
+ } catch (error) {
111
+ console.error('[APIClaw] Error validating session:', error);
112
+ return false;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Check if workspace is active and has usage remaining
118
+ */
119
+ function checkWorkspaceAccess(): { allowed: boolean; error?: string } {
120
+ if (!workspaceContext) {
121
+ return {
122
+ allowed: false,
123
+ error: 'Not authenticated. Use register_owner to authenticate your workspace.'
124
+ };
125
+ }
126
+
127
+ if (workspaceContext.status !== 'active') {
128
+ return {
129
+ allowed: false,
130
+ error: `Workspace status: ${workspaceContext.status}. Please verify your email.`
131
+ };
132
+ }
133
+
134
+ if (workspaceContext.usageRemaining === 0) {
135
+ return {
136
+ allowed: false,
137
+ error: `Usage limit reached. Upgrade to ${workspaceContext.tier === 'free' ? 'Pro' : 'Enterprise'} for more calls.`
138
+ };
139
+ }
140
+
141
+ return { allowed: true };
142
+ }
143
+
48
144
  /**
49
145
  * Get customer API key from environment variable
50
146
  * Convention: {PROVIDER}_API_KEY (e.g., COACCEPT_API_KEY, ELKS_API_KEY)
@@ -263,6 +359,39 @@ const tools: Tool[] = [
263
359
  type: 'object',
264
360
  properties: {}
265
361
  }
362
+ },
363
+ // ============================================
364
+ // WORKSPACE TOOLS
365
+ // ============================================
366
+ {
367
+ name: 'register_owner',
368
+ description: 'Register your email to create a workspace. This authenticates your agent with APIClaw. You will receive a magic link to verify ownership.',
369
+ inputSchema: {
370
+ type: 'object',
371
+ properties: {
372
+ email: {
373
+ type: 'string',
374
+ description: 'Your email address (used for verification and account recovery)'
375
+ }
376
+ },
377
+ required: ['email']
378
+ }
379
+ },
380
+ {
381
+ name: 'check_workspace_status',
382
+ description: 'Check your workspace status, tier, and usage remaining.',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {}
386
+ }
387
+ },
388
+ {
389
+ name: 'remind_owner',
390
+ description: 'Send a reminder email to verify workspace ownership (if verification is pending).',
391
+ inputSchema: {
392
+ type: 'object',
393
+ properties: {}
394
+ }
266
395
  }
267
396
  ];
268
397
 
@@ -540,6 +669,25 @@ Docs: https://apiclaw.nordsym.com
540
669
  const params = (args?.params as Record<string, any>) || {};
541
670
  const confirmToken = args?.confirm_token as string | undefined;
542
671
 
672
+ // Check workspace access (skip for free/open APIs)
673
+ const isFreeAPI = isOpenAPI(provider);
674
+ if (!isFreeAPI) {
675
+ const access = checkWorkspaceAccess();
676
+ if (!access.allowed) {
677
+ return {
678
+ content: [{
679
+ type: 'text',
680
+ text: JSON.stringify({
681
+ status: 'error',
682
+ error: access.error,
683
+ hint: 'Use register_owner to authenticate your workspace.',
684
+ }, null, 2)
685
+ }],
686
+ isError: true
687
+ };
688
+ }
689
+ }
690
+
543
691
  const startTime = Date.now();
544
692
  let result: { success: boolean; provider: string; action: string; data?: any; error?: string; cost?: number };
545
693
  let apiType: 'direct' | 'open';
@@ -661,6 +809,20 @@ Docs: https://apiclaw.nordsym.com
661
809
  error: result.error,
662
810
  });
663
811
 
812
+ // Increment usage for workspace (non-free APIs only)
813
+ if (result.success && workspaceContext && !isFreeAPI) {
814
+ try {
815
+ const usageResult = await convex.mutation("workspaces:incrementUsage" as any, {
816
+ workspaceId: workspaceContext.workspaceId as any,
817
+ }) as { success: boolean; remaining?: number };
818
+ if (usageResult.success) {
819
+ workspaceContext.usageRemaining = usageResult.remaining ?? -1;
820
+ }
821
+ } catch (e) {
822
+ console.error('[APIClaw] Failed to track usage:', e);
823
+ }
824
+ }
825
+
664
826
  return {
665
827
  content: [
666
828
  {
@@ -774,6 +936,271 @@ Docs: https://apiclaw.nordsym.com
774
936
  };
775
937
  }
776
938
 
939
+ // ============================================
940
+ // WORKSPACE TOOLS
941
+ // ============================================
942
+
943
+ case 'register_owner': {
944
+ const email = args?.email as string;
945
+
946
+ if (!email || !email.includes('@')) {
947
+ return {
948
+ content: [{
949
+ type: 'text',
950
+ text: JSON.stringify({
951
+ status: 'error',
952
+ error: 'Invalid email address',
953
+ }, null, 2)
954
+ }],
955
+ isError: true
956
+ };
957
+ }
958
+
959
+ try {
960
+ // Check if workspace already exists
961
+ const existing = await convex.query("workspaces:getByEmail" as any, { email }) as { _id: string; status: string; tier: string; usageCount: number; usageLimit: number } | null;
962
+
963
+ if (existing && existing.status === 'active') {
964
+ // Workspace exists and is active - create session directly
965
+ const fingerprint = getMachineFingerprint();
966
+ const sessionResult = await convex.mutation("workspaces:createAgentSession" as any, {
967
+ workspaceId: existing._id,
968
+ fingerprint,
969
+ }) as { success: boolean; sessionToken?: string };
970
+
971
+ if (sessionResult.success) {
972
+ writeSession(sessionResult.sessionToken!, existing._id, email);
973
+
974
+ // Update global context
975
+ workspaceContext = {
976
+ sessionToken: sessionResult.sessionToken!,
977
+ workspaceId: existing._id,
978
+ email,
979
+ tier: existing.tier,
980
+ usageRemaining: existing.usageLimit - existing.usageCount,
981
+ status: existing.status,
982
+ };
983
+
984
+ return {
985
+ content: [{
986
+ type: 'text',
987
+ text: JSON.stringify({
988
+ status: 'success',
989
+ message: `Welcome back! Authenticated as ${email}`,
990
+ workspace: {
991
+ email,
992
+ tier: existing.tier,
993
+ usageCount: existing.usageCount,
994
+ usageLimit: existing.usageLimit,
995
+ },
996
+ }, null, 2)
997
+ }]
998
+ };
999
+ }
1000
+ }
1001
+
1002
+ // Create workspace and magic link
1003
+ const createResult = await convex.mutation("workspaces:createWorkspace" as any, { email }) as { success: boolean; workspaceId?: string; error?: string };
1004
+
1005
+ let workspaceId: string;
1006
+ if (createResult.success) {
1007
+ workspaceId = createResult.workspaceId!;
1008
+ } else if (createResult.error === 'workspace_exists') {
1009
+ workspaceId = createResult.workspaceId!;
1010
+ } else {
1011
+ throw new Error(createResult.error);
1012
+ }
1013
+
1014
+ // Create magic link
1015
+ const fingerprint = getMachineFingerprint();
1016
+ const magicLinkResult = await convex.mutation("workspaces:createMagicLink" as any, {
1017
+ email,
1018
+ fingerprint,
1019
+ }) as { token: string; expiresAt: number };
1020
+
1021
+ // TODO: Agent 2 will implement actual email sending
1022
+ // For now, return the verification link
1023
+ const verifyUrl = `https://apiclaw.nordsym.com/verify?token=${magicLinkResult.token}`;
1024
+
1025
+ return {
1026
+ content: [{
1027
+ type: 'text',
1028
+ text: JSON.stringify({
1029
+ status: 'pending_verification',
1030
+ message: 'Workspace created! Please verify your email.',
1031
+ email,
1032
+ verification_url: verifyUrl,
1033
+ expires_in_minutes: 15,
1034
+ next_step: 'Click the verification link, then run check_workspace_status',
1035
+ }, null, 2)
1036
+ }]
1037
+ };
1038
+ } catch (error) {
1039
+ return {
1040
+ content: [{
1041
+ type: 'text',
1042
+ text: JSON.stringify({
1043
+ status: 'error',
1044
+ error: error instanceof Error ? error.message : 'Registration failed',
1045
+ }, null, 2)
1046
+ }],
1047
+ isError: true
1048
+ };
1049
+ }
1050
+ }
1051
+
1052
+ case 'check_workspace_status': {
1053
+ // Check if we have a local session
1054
+ const session = readSession();
1055
+
1056
+ if (!session) {
1057
+ return {
1058
+ content: [{
1059
+ type: 'text',
1060
+ text: JSON.stringify({
1061
+ status: 'not_authenticated',
1062
+ message: 'No active session. Use register_owner to authenticate.',
1063
+ }, null, 2)
1064
+ }]
1065
+ };
1066
+ }
1067
+
1068
+ try {
1069
+ const result = await convex.query("workspaces:getWorkspaceStatus" as any, {
1070
+ sessionToken: session.sessionToken,
1071
+ }) as { authenticated: boolean; email?: string; status?: string; tier?: string; usageCount?: number; usageLimit?: number; usageRemaining?: number; hasStripe?: boolean; createdAt?: number };
1072
+
1073
+ if (!result.authenticated) {
1074
+ clearSession();
1075
+ workspaceContext = null;
1076
+
1077
+ return {
1078
+ content: [{
1079
+ type: 'text',
1080
+ text: JSON.stringify({
1081
+ status: 'session_expired',
1082
+ message: 'Session expired. Use register_owner to re-authenticate.',
1083
+ }, null, 2)
1084
+ }]
1085
+ };
1086
+ }
1087
+
1088
+ // Update global context
1089
+ workspaceContext = {
1090
+ sessionToken: session.sessionToken,
1091
+ workspaceId: session.workspaceId,
1092
+ email: result.email ?? '',
1093
+ tier: result.tier ?? 'free',
1094
+ usageRemaining: result.usageRemaining ?? 0,
1095
+ status: result.status ?? 'unknown',
1096
+ };
1097
+
1098
+ return {
1099
+ content: [{
1100
+ type: 'text',
1101
+ text: JSON.stringify({
1102
+ status: 'success',
1103
+ workspace: {
1104
+ email: result.email,
1105
+ status: result.status,
1106
+ tier: result.tier,
1107
+ usage: {
1108
+ count: result.usageCount,
1109
+ limit: result.usageLimit === -1 ? 'unlimited' : result.usageLimit,
1110
+ remaining: result.usageRemaining === -1 ? 'unlimited' : result.usageRemaining,
1111
+ },
1112
+ hasStripe: result.hasStripe,
1113
+ createdAt: result.createdAt ? new Date(result.createdAt).toISOString() : undefined,
1114
+ },
1115
+ }, null, 2)
1116
+ }]
1117
+ };
1118
+ } catch (error) {
1119
+ return {
1120
+ content: [{
1121
+ type: 'text',
1122
+ text: JSON.stringify({
1123
+ status: 'error',
1124
+ error: error instanceof Error ? error.message : 'Failed to check status',
1125
+ }, null, 2)
1126
+ }],
1127
+ isError: true
1128
+ };
1129
+ }
1130
+ }
1131
+
1132
+ case 'remind_owner': {
1133
+ const session = readSession();
1134
+
1135
+ if (!session) {
1136
+ return {
1137
+ content: [{
1138
+ type: 'text',
1139
+ text: JSON.stringify({
1140
+ status: 'error',
1141
+ error: 'No workspace found. Use register_owner first.',
1142
+ }, null, 2)
1143
+ }],
1144
+ isError: true
1145
+ };
1146
+ }
1147
+
1148
+ try {
1149
+ // Check current status
1150
+ const result = await convex.query("workspaces:getWorkspaceStatus" as any, {
1151
+ sessionToken: session.sessionToken,
1152
+ }) as { authenticated: boolean; email?: string; status?: string };
1153
+
1154
+ if (result.authenticated && result.status === 'active') {
1155
+ return {
1156
+ content: [{
1157
+ type: 'text',
1158
+ text: JSON.stringify({
1159
+ status: 'already_verified',
1160
+ message: 'Workspace is already verified and active!',
1161
+ email: result.email,
1162
+ }, null, 2)
1163
+ }]
1164
+ };
1165
+ }
1166
+
1167
+ // Create new magic link
1168
+ const fingerprint = getMachineFingerprint();
1169
+ const magicLinkResult = await convex.mutation("workspaces:createMagicLink" as any, {
1170
+ email: session.email,
1171
+ fingerprint,
1172
+ }) as { token: string; expiresAt: number };
1173
+
1174
+ // TODO: Agent 2 will implement actual email sending
1175
+ const verifyUrl = `https://apiclaw.nordsym.com/verify?token=${magicLinkResult.token}`;
1176
+
1177
+ return {
1178
+ content: [{
1179
+ type: 'text',
1180
+ text: JSON.stringify({
1181
+ status: 'reminder_sent',
1182
+ message: 'New verification link created.',
1183
+ email: session.email,
1184
+ verification_url: verifyUrl,
1185
+ expires_in_minutes: 15,
1186
+ note: 'Email sending will be implemented by Agent 2',
1187
+ }, null, 2)
1188
+ }]
1189
+ };
1190
+ } catch (error) {
1191
+ return {
1192
+ content: [{
1193
+ type: 'text',
1194
+ text: JSON.stringify({
1195
+ status: 'error',
1196
+ error: error instanceof Error ? error.message : 'Failed to send reminder',
1197
+ }, null, 2)
1198
+ }],
1199
+ isError: true
1200
+ };
1201
+ }
1202
+ }
1203
+
777
1204
  default:
778
1205
  return {
779
1206
  content: [
@@ -806,10 +1233,20 @@ Docs: https://apiclaw.nordsym.com
806
1233
 
807
1234
  // Start server
808
1235
  async function main() {
1236
+ // Check for CLI mode
1237
+ if (process.argv.includes('--cli') || process.argv.includes('-c')) {
1238
+ const { startCLI } = await import('./cli.js');
1239
+ await startCLI();
1240
+ return;
1241
+ }
1242
+
809
1243
  const transport = new StdioServerTransport();
810
1244
  await server.connect(transport);
811
1245
  trackStartup();
812
1246
 
1247
+ // Validate session on startup
1248
+ const hasValidSession = await validateSession();
1249
+
813
1250
  // Welcome message with onboarding
814
1251
  console.error(`
815
1252
  🦞 APIClaw v1.1.5 — The API Layer for AI Agents
@@ -818,15 +1255,18 @@ async function main() {
818
1255
  ✓ 19,000+ APIs indexed
819
1256
  ✓ 23 categories
820
1257
  ✓ 9 direct-call providers ready
1258
+ ${hasValidSession ? `✓ Authenticated as ${workspaceContext?.email}` : '⚠ Not authenticated - use register_owner'}
821
1259
 
822
1260
  Quick Start:
823
- discover_apis("send SMS to Sweden")
1261
+ ${!hasValidSession ? 'register_owner({ email: "you@example.com" }) # First, authenticate\n ' : ''}discover_apis("send SMS to Sweden")
824
1262
  discover_apis("search the web")
825
- discover_apis("generate speech from text")
1263
+ call_api({ provider: "brave_search", ... })
826
1264
 
827
1265
  Direct Call (no API key needed):
828
- get_connected_providers()
829
- call_api({ provider: "brave_search", ... })
1266
+ list_connected()
1267
+
1268
+ Interactive CLI mode:
1269
+ npx @nordsym/apiclaw --cli
830
1270
 
831
1271
  Docs: https://apiclaw.nordsym.com
832
1272
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
package/src/session.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Session management for APIClaw MCP server
3
+ * Stores session token locally at ~/.apiclaw/session
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+
10
+ export interface SessionData {
11
+ sessionToken: string;
12
+ workspaceId: string;
13
+ email: string;
14
+ createdAt: number;
15
+ }
16
+
17
+ const SESSION_DIR = path.join(os.homedir(), '.apiclaw');
18
+ const SESSION_FILE = path.join(SESSION_DIR, 'session');
19
+
20
+ /**
21
+ * Ensure the ~/.apiclaw directory exists
22
+ */
23
+ function ensureSessionDir(): void {
24
+ if (!fs.existsSync(SESSION_DIR)) {
25
+ fs.mkdirSync(SESSION_DIR, { mode: 0o700 });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Read session from ~/.apiclaw/session
31
+ * Returns null if no session file exists or if it's invalid
32
+ */
33
+ export function readSession(): SessionData | null {
34
+ try {
35
+ if (!fs.existsSync(SESSION_FILE)) {
36
+ return null;
37
+ }
38
+
39
+ const content = fs.readFileSync(SESSION_FILE, 'utf8');
40
+ const data = JSON.parse(content) as SessionData;
41
+
42
+ // Validate required fields
43
+ if (!data.sessionToken || !data.workspaceId || !data.email) {
44
+ console.error('[APIClaw] Invalid session file, clearing...');
45
+ clearSession();
46
+ return null;
47
+ }
48
+
49
+ return data;
50
+ } catch (error) {
51
+ console.error('[APIClaw] Error reading session:', error);
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Write session to ~/.apiclaw/session
58
+ */
59
+ export function writeSession(sessionToken: string, workspaceId: string, email: string): void {
60
+ try {
61
+ ensureSessionDir();
62
+
63
+ const data: SessionData = {
64
+ sessionToken,
65
+ workspaceId,
66
+ email,
67
+ createdAt: Date.now(),
68
+ };
69
+
70
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), {
71
+ mode: 0o600, // Read/write for owner only
72
+ });
73
+
74
+ console.error(`[APIClaw] Session saved for ${email}`);
75
+ } catch (error) {
76
+ console.error('[APIClaw] Error writing session:', error);
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Clear session file
83
+ */
84
+ export function clearSession(): void {
85
+ try {
86
+ if (fs.existsSync(SESSION_FILE)) {
87
+ fs.unlinkSync(SESSION_FILE);
88
+ console.error('[APIClaw] Session cleared');
89
+ }
90
+ } catch (error) {
91
+ console.error('[APIClaw] Error clearing session:', error);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get machine fingerprint (for session binding)
97
+ * Uses hostname + username as a simple fingerprint
98
+ */
99
+ export function getMachineFingerprint(): string {
100
+ const hostname = os.hostname();
101
+ const username = os.userInfo().username;
102
+ return `${hostname}:${username}`;
103
+ }