@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.
- package/convex/_generated/api.d.ts +6 -0
- package/convex/billing.ts +341 -0
- package/convex/email.ts +276 -0
- package/convex/http.ts +154 -0
- package/convex/schema.ts +43 -0
- package/convex/workspaces.ts +663 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +396 -4
- package/dist/index.js.map +1 -1
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +87 -0
- package/dist/session.js.map +1 -0
- package/docs/PRD-agent-first-billing.md +525 -0
- package/docs/PRD-workspace-fixes.md +178 -0
- package/landing/package-lock.json +21 -3
- package/landing/package.json +2 -1
- package/landing/src/app/api/stripe/webhook/route.ts +178 -0
- package/landing/src/app/api/workspace-auth/magic-link/route.ts +84 -0
- package/landing/src/app/api/workspace-auth/session/route.ts +73 -0
- package/landing/src/app/api/workspace-auth/verify/route.ts +57 -0
- package/landing/src/app/auth/verify/page.tsx +292 -0
- package/landing/src/app/dashboard/layout.tsx +22 -0
- package/landing/src/app/dashboard/page.tsx +22 -0
- package/landing/src/app/dashboard/verify/page.tsx +108 -0
- package/landing/src/app/login/page.tsx +204 -0
- package/landing/src/app/page.tsx +23 -7
- package/landing/src/app/providers/dashboard/layout.tsx +5 -4
- package/landing/src/app/providers/dashboard/page.tsx +11 -641
- package/landing/src/app/upgrade/page.tsx +288 -0
- package/landing/src/app/workspace/layout.tsx +30 -0
- package/landing/src/app/workspace/page.tsx +1637 -0
- package/landing/src/lib/stats.json +14 -15
- package/landing/src/middleware.ts +50 -0
- package/landing/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/cli.ts +320 -0
- package/src/index.ts +444 -4
- 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
|
-
|
|
1263
|
+
call_api({ provider: "brave_search", ... })
|
|
826
1264
|
|
|
827
1265
|
Direct Call (no API key needed):
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
}
|