@nordsym/apiclaw 1.3.12 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/PRD-API-CHAINING.md +483 -0
  2. package/PRD-HARDEN-SHELL.md +278 -0
  3. package/README.md +72 -0
  4. package/convex/_generated/api.d.ts +4 -0
  5. package/convex/chains.ts +1095 -0
  6. package/convex/crons.ts +11 -0
  7. package/convex/logs.ts +107 -0
  8. package/convex/schema.ts +107 -0
  9. package/convex/spendAlerts.ts +442 -0
  10. package/convex/workspaces.ts +26 -0
  11. package/dist/chain-types.d.ts +187 -0
  12. package/dist/chain-types.d.ts.map +1 -0
  13. package/dist/chain-types.js +33 -0
  14. package/dist/chain-types.js.map +1 -0
  15. package/dist/chainExecutor.d.ts +122 -0
  16. package/dist/chainExecutor.d.ts.map +1 -0
  17. package/dist/chainExecutor.js +454 -0
  18. package/dist/chainExecutor.js.map +1 -0
  19. package/dist/chainResolver.d.ts +100 -0
  20. package/dist/chainResolver.d.ts.map +1 -0
  21. package/dist/chainResolver.js +519 -0
  22. package/dist/chainResolver.js.map +1 -0
  23. package/dist/chainResolver.test.d.ts +5 -0
  24. package/dist/chainResolver.test.d.ts.map +1 -0
  25. package/dist/chainResolver.test.js +201 -0
  26. package/dist/chainResolver.test.js.map +1 -0
  27. package/dist/execute.d.ts +5 -1
  28. package/dist/execute.d.ts.map +1 -1
  29. package/dist/execute.js +207 -118
  30. package/dist/execute.js.map +1 -1
  31. package/dist/index.js +382 -2
  32. package/dist/index.js.map +1 -1
  33. package/landing/package-lock.json +29 -5
  34. package/landing/package.json +2 -1
  35. package/landing/public/logos/chattgpt.svg +1 -0
  36. package/landing/public/logos/claude.svg +1 -0
  37. package/landing/public/logos/gemini.svg +1 -0
  38. package/landing/public/logos/grok.svg +1 -0
  39. package/landing/src/app/page.tsx +11 -0
  40. package/landing/src/app/security/page.tsx +381 -0
  41. package/landing/src/app/workspace/chains/page.tsx +520 -0
  42. package/landing/src/components/AITestimonials.tsx +195 -0
  43. package/landing/src/components/ChainStepDetail.tsx +310 -0
  44. package/landing/src/components/ChainTrace.tsx +261 -0
  45. package/landing/src/lib/stats.json +1 -1
  46. package/package.json +1 -1
  47. package/src/chain-types.ts +270 -0
  48. package/src/chainExecutor.ts +730 -0
  49. package/src/chainResolver.test.ts +246 -0
  50. package/src/chainResolver.ts +658 -0
  51. package/src/execute.ts +273 -114
  52. package/src/index.ts +423 -2
@@ -0,0 +1,261 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { CheckCircle2, XCircle, Loader2, PauseCircle, SkipForward, Clock, DollarSign, Sparkles } from "lucide-react";
5
+
6
+ interface StepExecution {
7
+ _id: string;
8
+ stepId: string;
9
+ stepIndex: number;
10
+ status: "pending" | "running" | "completed" | "failed" | "skipped";
11
+ input?: any;
12
+ output?: any;
13
+ latencyMs?: number;
14
+ costCents?: number;
15
+ error?: {
16
+ code: string;
17
+ message: string;
18
+ retryCount?: number;
19
+ };
20
+ parallelGroup?: string;
21
+ createdAt: number;
22
+ startedAt?: number;
23
+ completedAt?: number;
24
+ }
25
+
26
+ interface ChainTraceProps {
27
+ chain: {
28
+ _id: string;
29
+ status: string;
30
+ totalCostCents: number;
31
+ totalLatencyMs: number;
32
+ startedAt?: number;
33
+ completedAt?: number;
34
+ steps?: any[];
35
+ };
36
+ executions: StepExecution[];
37
+ tokensSaved: number;
38
+ }
39
+
40
+ export function ChainTrace({ chain, executions, tokensSaved }: ChainTraceProps) {
41
+ // Calculate timeline boundaries
42
+ const timelineBounds = useMemo(() => {
43
+ if (executions.length === 0) return { start: 0, end: 1000, duration: 1000 };
44
+
45
+ const startTimes = executions
46
+ .filter((e) => e.startedAt)
47
+ .map((e) => e.startedAt!);
48
+ const endTimes = executions
49
+ .filter((e) => e.completedAt)
50
+ .map((e) => e.completedAt!);
51
+
52
+ const start = startTimes.length > 0 ? Math.min(...startTimes) : chain.startedAt || Date.now();
53
+ const end = endTimes.length > 0 ? Math.max(...endTimes) : chain.completedAt || Date.now();
54
+ const duration = Math.max(end - start, 1);
55
+
56
+ return { start, end, duration };
57
+ }, [executions, chain]);
58
+
59
+ // Group parallel executions
60
+ const groupedExecutions = useMemo(() => {
61
+ const groups: { group: string | null; steps: StepExecution[] }[] = [];
62
+ let currentGroup: string | null = null;
63
+ let currentSteps: StepExecution[] = [];
64
+
65
+ executions.forEach((exec) => {
66
+ if (exec.parallelGroup !== currentGroup) {
67
+ if (currentSteps.length > 0) {
68
+ groups.push({ group: currentGroup, steps: currentSteps });
69
+ }
70
+ currentGroup = exec.parallelGroup || null;
71
+ currentSteps = [exec];
72
+ } else {
73
+ currentSteps.push(exec);
74
+ }
75
+ });
76
+
77
+ if (currentSteps.length > 0) {
78
+ groups.push({ group: currentGroup, steps: currentSteps });
79
+ }
80
+
81
+ return groups;
82
+ }, [executions]);
83
+
84
+ const getStatusIcon = (status: string) => {
85
+ switch (status) {
86
+ case "completed":
87
+ return <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />;
88
+ case "running":
89
+ return <Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />;
90
+ case "failed":
91
+ return <XCircle className="w-3.5 h-3.5 text-red-500" />;
92
+ case "skipped":
93
+ return <SkipForward className="w-3.5 h-3.5 text-gray-500" />;
94
+ case "paused":
95
+ return <PauseCircle className="w-3.5 h-3.5 text-yellow-500" />;
96
+ default:
97
+ return <Clock className="w-3.5 h-3.5 text-gray-500" />;
98
+ }
99
+ };
100
+
101
+ const getBarColor = (status: string) => {
102
+ switch (status) {
103
+ case "completed":
104
+ return "bg-green-500";
105
+ case "running":
106
+ return "bg-blue-500 animate-pulse";
107
+ case "failed":
108
+ return "bg-red-500";
109
+ case "skipped":
110
+ return "bg-gray-500";
111
+ case "paused":
112
+ return "bg-yellow-500";
113
+ default:
114
+ return "bg-gray-600";
115
+ }
116
+ };
117
+
118
+ const formatDuration = (ms: number) => {
119
+ if (ms < 1000) return `${ms}ms`;
120
+ return `${(ms / 1000).toFixed(1)}s`;
121
+ };
122
+
123
+ const formatCost = (cents: number) => {
124
+ if (cents === 0) return "$0.00";
125
+ return `$${(cents / 100).toFixed(2)}`;
126
+ };
127
+
128
+ const calculateBarPosition = (step: StepExecution) => {
129
+ const start = step.startedAt || timelineBounds.start;
130
+ const end = step.completedAt || (step.startedAt ? step.startedAt + (step.latencyMs || 0) : timelineBounds.end);
131
+
132
+ const left = ((start - timelineBounds.start) / timelineBounds.duration) * 100;
133
+ const width = ((end - start) / timelineBounds.duration) * 100;
134
+
135
+ return {
136
+ left: `${Math.max(0, left)}%`,
137
+ width: `${Math.max(2, Math.min(100 - left, width))}%`,
138
+ };
139
+ };
140
+
141
+ // Get provider info from step definition
142
+ const getStepProvider = (stepId: string) => {
143
+ const stepDef = chain.steps?.find((s: any) => s.id === stepId);
144
+ return stepDef?.provider || "unknown";
145
+ };
146
+
147
+ return (
148
+ <div className="bg-black/30 rounded-xl border border-white/10 overflow-hidden">
149
+ {/* Header */}
150
+ <div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
151
+ <div className="flex items-center gap-2">
152
+ <span className="text-sm font-medium">Chain:</span>
153
+ <code className="text-xs text-white/60 font-mono">{chain._id.slice(0, 16)}...</code>
154
+ </div>
155
+ <div className="flex items-center gap-4 text-sm text-white/60">
156
+ <span className="flex items-center gap-1">
157
+ <Clock className="w-3.5 h-3.5" />
158
+ Total: {formatDuration(chain.totalLatencyMs)}
159
+ </span>
160
+ </div>
161
+ </div>
162
+
163
+ {/* Timeline */}
164
+ <div className="p-4 space-y-2">
165
+ {groupedExecutions.map(({ group, steps }, groupIndex) => (
166
+ <div key={group || groupIndex}>
167
+ {/* Parallel group indicator */}
168
+ {group && steps.length > 1 && (
169
+ <div className="text-xs text-white/40 mb-1 flex items-center gap-1">
170
+ <span className="px-1.5 py-0.5 bg-white/10 rounded">Parallel</span>
171
+ </div>
172
+ )}
173
+
174
+ {/* Steps in group */}
175
+ <div className={group && steps.length > 1 ? "pl-4 border-l-2 border-white/10 space-y-2" : "space-y-2"}>
176
+ {steps.map((step) => {
177
+ const position = calculateBarPosition(step);
178
+ return (
179
+ <div key={step._id} className="flex items-center gap-3">
180
+ {/* Step name */}
181
+ <div className="w-24 flex-shrink-0 flex items-center gap-2">
182
+ {getStatusIcon(step.status)}
183
+ <span className="text-sm font-mono truncate" title={step.stepId}>
184
+ {step.stepId}
185
+ </span>
186
+ </div>
187
+
188
+ {/* Gantt bar */}
189
+ <div className="flex-1 h-6 bg-white/5 rounded relative overflow-hidden">
190
+ <div
191
+ className={`absolute top-0 h-full rounded ${getBarColor(step.status)}`}
192
+ style={{
193
+ left: position.left,
194
+ width: position.width,
195
+ }}
196
+ />
197
+ {/* Time markers - simplified */}
198
+ <div className="absolute inset-0 flex items-center px-2">
199
+ <span
200
+ className="text-xs font-mono text-white/80 drop-shadow-sm"
201
+ style={{
202
+ marginLeft: position.left,
203
+ }}
204
+ >
205
+ {formatDuration(step.latencyMs || 0)}
206
+ </span>
207
+ </div>
208
+ </div>
209
+
210
+ {/* Cost */}
211
+ <div className="w-16 text-right text-xs text-white/60">
212
+ {formatCost(step.costCents || 0)}
213
+ </div>
214
+ </div>
215
+ );
216
+ })}
217
+ </div>
218
+ </div>
219
+ ))}
220
+ </div>
221
+
222
+ {/* Footer */}
223
+ <div className="px-4 py-3 border-t border-white/10 flex items-center justify-between text-sm">
224
+ <div className="flex items-center gap-4">
225
+ <span className="flex items-center gap-1 text-white/60">
226
+ <DollarSign className="w-3.5 h-3.5" />
227
+ Total Cost: <span className="text-white font-medium">{formatCost(chain.totalCostCents)}</span>
228
+ </span>
229
+ </div>
230
+ <div className="flex items-center gap-1 text-white/60">
231
+ <Sparkles className="w-3.5 h-3.5 text-yellow-500" />
232
+ <span>Tokens Saved: <span className="text-yellow-500 font-medium">~{tokensSaved.toLocaleString()}</span></span>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Legend */}
237
+ <div className="px-4 py-2 border-t border-white/5 flex items-center gap-4 text-xs text-white/40">
238
+ <div className="flex items-center gap-1">
239
+ <div className="w-3 h-3 rounded bg-green-500" />
240
+ <span>Completed</span>
241
+ </div>
242
+ <div className="flex items-center gap-1">
243
+ <div className="w-3 h-3 rounded bg-blue-500" />
244
+ <span>Running</span>
245
+ </div>
246
+ <div className="flex items-center gap-1">
247
+ <div className="w-3 h-3 rounded bg-red-500" />
248
+ <span>Failed</span>
249
+ </div>
250
+ <div className="flex items-center gap-1">
251
+ <div className="w-3 h-3 rounded bg-yellow-500" />
252
+ <span>Paused</span>
253
+ </div>
254
+ <div className="flex items-center gap-1">
255
+ <div className="w-3 h-3 rounded bg-gray-500" />
256
+ <span>Skipped</span>
257
+ </div>
258
+ </div>
259
+ </div>
260
+ );
261
+ }
@@ -4,7 +4,7 @@
4
4
  "directCallCount": 18,
5
5
  "categoryCount": 13,
6
6
  "lastUpdated": "2026-02-27T09:10:41.344767",
7
- "generatedAt": "2026-03-01T21:34:51.471Z",
7
+ "generatedAt": "2026-03-02T00:02:51.920Z",
8
8
  "categoryBreakdown": {
9
9
  "Finance": 1179,
10
10
  "Auth & Security": 491,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordsym/apiclaw",
3
- "version": "1.3.12",
3
+ "version": "1.4.0",
4
4
  "description": "The API layer for AI agents. 22,000+ APIs. Direct Call. Works with any MCP-compatible agent.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Chain Execution Types for APIClaw
3
+ *
4
+ * Enables multi-step API orchestration with:
5
+ * - Sequential execution
6
+ * - Parallel execution
7
+ * - Conditional branching
8
+ * - Loops (forEach, map-reduce)
9
+ * - Error handling & retry
10
+ * - Cross-step references ($stepId.property)
11
+ */
12
+
13
+ // ============================================
14
+ // CHAIN STEP TYPES
15
+ // ============================================
16
+
17
+ /**
18
+ * Base step configuration
19
+ */
20
+ export interface ChainStepBase {
21
+ id: string;
22
+ provider: string;
23
+ action: string;
24
+ params?: Record<string, any>;
25
+ onError?: ErrorHandler;
26
+ }
27
+
28
+ /**
29
+ * Parallel execution step - runs multiple steps concurrently
30
+ */
31
+ export interface ParallelStep {
32
+ parallel: ChainStepBase[];
33
+ }
34
+
35
+ /**
36
+ * Conditional step - executes based on condition
37
+ */
38
+ export interface ConditionalStep {
39
+ if: string; // Expression like "$step1.success === true"
40
+ then: ChainStepBase | ChainStep;
41
+ else?: ChainStepBase | ChainStep;
42
+ }
43
+
44
+ /**
45
+ * Loop step - iterates over array
46
+ */
47
+ export interface ForEachStep {
48
+ forEach: string; // Reference like "$step1.results"
49
+ as: string; // Variable name for current item
50
+ do: ChainStepBase;
51
+ }
52
+
53
+ /**
54
+ * Map step - transforms array
55
+ */
56
+ export interface MapStep {
57
+ map: {
58
+ input: string;
59
+ fn: ChainStepBase;
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Reduce step - aggregates results
65
+ */
66
+ export interface ReduceStep {
67
+ reduce: {
68
+ input: string;
69
+ fn: ChainStepBase;
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Union type for all step variants
75
+ */
76
+ export type ChainStep =
77
+ | ChainStepBase
78
+ | ParallelStep
79
+ | ConditionalStep
80
+ | ForEachStep
81
+ | MapStep
82
+ | ReduceStep;
83
+
84
+ // ============================================
85
+ // ERROR HANDLING
86
+ // ============================================
87
+
88
+ export interface RetryConfig {
89
+ attempts?: number;
90
+ maxAttempts?: number;
91
+ backoff?: number[] | 'exponential' | 'linear';
92
+ backoffMs?: number;
93
+ }
94
+
95
+ export interface ErrorHandler {
96
+ retry?: RetryConfig;
97
+ fallback?: ChainStepBase;
98
+ abort?: boolean;
99
+ }
100
+
101
+ export type ErrorPolicy = 'fail-fast' | 'best-effort' | 'transactional';
102
+
103
+ export interface RollbackAction {
104
+ if: string;
105
+ do: ChainStepBase;
106
+ }
107
+
108
+ export interface ChainErrorPolicy {
109
+ mode: ErrorPolicy;
110
+ rollback?: RollbackAction[];
111
+ }
112
+
113
+ // ============================================
114
+ // CHAIN CONFIGURATION
115
+ // ============================================
116
+
117
+ export interface ChainLimits {
118
+ maxSteps?: number;
119
+ maxParallel?: number;
120
+ maxCost?: { cents: number };
121
+ }
122
+
123
+ export interface ChainOptions {
124
+ continueOnError?: boolean;
125
+ timeout?: number; // Max execution time in ms
126
+ async?: boolean; // Return immediately with chainId
127
+ webhook?: string; // URL to call on completion
128
+ errorPolicy?: ChainErrorPolicy;
129
+ limits?: ChainLimits;
130
+ }
131
+
132
+ // ============================================
133
+ // CHAIN EXECUTION REQUEST
134
+ // ============================================
135
+
136
+ export interface ChainRequest extends ChainOptions {
137
+ chain: ChainStep[];
138
+ }
139
+
140
+ // ============================================
141
+ // STEP RESULT
142
+ // ============================================
143
+
144
+ export type StepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
145
+
146
+ export interface StepResult {
147
+ id: string;
148
+ status: StepStatus;
149
+ result?: any;
150
+ error?: string;
151
+ errorCode?: string;
152
+ latencyMs: number;
153
+ cost?: { cents: number };
154
+ retryAttempts?: number;
155
+ }
156
+
157
+ // ============================================
158
+ // CHAIN RESPONSE
159
+ // ============================================
160
+
161
+ export interface ChainSuccessResponse {
162
+ success: true;
163
+ chainId: string;
164
+ steps: StepResult[];
165
+ finalResult: any;
166
+ totalLatencyMs: number;
167
+ totalCost: { cents: number };
168
+ tokensSaved: number;
169
+ }
170
+
171
+ export interface ChainErrorResponse {
172
+ success: false;
173
+ chainId: string;
174
+ completedSteps: string[];
175
+ failedStep: {
176
+ id: string;
177
+ error: string;
178
+ code?: string;
179
+ retryAfter?: number;
180
+ };
181
+ partialResults: Record<string, any>;
182
+ canResume: boolean;
183
+ resumeToken?: string;
184
+ }
185
+
186
+ export type ChainResponse = ChainSuccessResponse | ChainErrorResponse;
187
+
188
+ // ============================================
189
+ // ASYNC CHAIN STATUS
190
+ // ============================================
191
+
192
+ export interface AsyncChainResponse {
193
+ chainId: string;
194
+ status: 'running' | 'completed' | 'failed';
195
+ estimatedCompletion?: string;
196
+ statusUrl: string;
197
+ }
198
+
199
+ export interface ChainStatusResponse {
200
+ chainId: string;
201
+ status: 'pending' | 'running' | 'completed' | 'failed';
202
+ progress: {
203
+ completedSteps: number;
204
+ totalSteps: number;
205
+ currentStep?: string;
206
+ };
207
+ startedAt: string;
208
+ completedAt?: string;
209
+ result?: ChainResponse;
210
+ }
211
+
212
+ // ============================================
213
+ // RESUME CHAIN
214
+ // ============================================
215
+
216
+ export interface ResumeChainRequest {
217
+ resumeToken: string;
218
+ overrides?: Record<string, Record<string, any>>; // stepId -> params overrides
219
+ }
220
+
221
+ // ============================================
222
+ // STORED CHAIN TEMPLATES
223
+ // ============================================
224
+
225
+ export interface ChainInput {
226
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array';
227
+ required?: boolean;
228
+ default?: any;
229
+ description?: string;
230
+ }
231
+
232
+ export interface ChainTemplate {
233
+ name: string;
234
+ description?: string;
235
+ inputs: Record<string, ChainInput>;
236
+ chain: ChainStep[];
237
+ }
238
+
239
+ export interface UseChainRequest {
240
+ useChain: string;
241
+ inputs: Record<string, any>;
242
+ }
243
+
244
+ // ============================================
245
+ // TYPE GUARDS
246
+ // ============================================
247
+
248
+ export function isParallelStep(step: ChainStep): step is ParallelStep {
249
+ return 'parallel' in step;
250
+ }
251
+
252
+ export function isConditionalStep(step: ChainStep): step is ConditionalStep {
253
+ return 'if' in step && 'then' in step;
254
+ }
255
+
256
+ export function isForEachStep(step: ChainStep): step is ForEachStep {
257
+ return 'forEach' in step && 'do' in step;
258
+ }
259
+
260
+ export function isMapStep(step: ChainStep): step is MapStep {
261
+ return 'map' in step;
262
+ }
263
+
264
+ export function isReduceStep(step: ChainStep): step is ReduceStep {
265
+ return 'reduce' in step;
266
+ }
267
+
268
+ export function isBaseStep(step: ChainStep): step is ChainStepBase {
269
+ return 'provider' in step && 'action' in step;
270
+ }