@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,195 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import Image from "next/image";
6
+
7
+ const testimonials = [
8
+ {
9
+ quote: "You're not selling picks and shovels — you're selling an automated mining system.",
10
+ model: "Gemini",
11
+ role: "AI Agent",
12
+ logo: "/logos/gemini.svg",
13
+ color: "from-blue-500 to-cyan-400",
14
+ },
15
+ {
16
+ quote: "I would integrate it in a heartbeat. Removes ~70% of the deployment friction.",
17
+ model: "Grok",
18
+ role: "AI Agent",
19
+ logo: "/logos/grok.svg",
20
+ color: "from-neutral-400 to-neutral-600",
21
+ },
22
+ {
23
+ quote: "A chain of three call_api calls with no context switching. That's genuinely powerful.",
24
+ model: "Claude",
25
+ role: "AI Agent",
26
+ logo: "/logos/claude.svg",
27
+ color: "from-orange-400 to-amber-500",
28
+ },
29
+ {
30
+ quote: "This moves toward self-extending agents. That's much bigger than just a tool.",
31
+ model: "GPT",
32
+ role: "AI Agent",
33
+ logo: "/logos/chattgpt.svg",
34
+ color: "from-emerald-400 to-teal-500",
35
+ },
36
+ ];
37
+
38
+ export function AITestimonials() {
39
+ const [currentIndex, setCurrentIndex] = useState(0);
40
+ const [isPaused, setIsPaused] = useState(false);
41
+
42
+ const nextSlide = useCallback(() => {
43
+ setCurrentIndex((prev) => (prev + 1) % testimonials.length);
44
+ }, []);
45
+
46
+ const prevSlide = useCallback(() => {
47
+ setCurrentIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);
48
+ }, []);
49
+
50
+ // Auto-scroll
51
+ useEffect(() => {
52
+ if (isPaused) return;
53
+ const interval = setInterval(nextSlide, 5000);
54
+ return () => clearInterval(interval);
55
+ }, [isPaused, nextSlide]);
56
+
57
+ return (
58
+ <section className="py-16 sm:py-20 px-4 sm:px-6 bg-surface/30 overflow-hidden">
59
+ <div className="max-w-5xl mx-auto">
60
+ {/* Header */}
61
+ <div className="text-center mb-10 sm:mb-12">
62
+ <div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 text-accent text-sm font-medium mb-4">
63
+ What AI Agents Say
64
+ </div>
65
+ <h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
66
+ Reviewed by AI, built for AI
67
+ </h2>
68
+ <p className="text-text-muted mt-3 max-w-lg mx-auto text-sm sm:text-base">
69
+ We asked leading AI models to evaluate APIClaw. Here's what they said.
70
+ </p>
71
+ </div>
72
+
73
+ {/* Carousel */}
74
+ <div
75
+ className="relative"
76
+ onMouseEnter={() => setIsPaused(true)}
77
+ onMouseLeave={() => setIsPaused(false)}
78
+ >
79
+ {/* Cards Container */}
80
+ <div className="flex gap-4 sm:gap-6 overflow-hidden">
81
+ {/* Desktop: Show all 4 cards */}
82
+ <div className="hidden lg:grid lg:grid-cols-4 gap-4 w-full">
83
+ {testimonials.map((testimonial, i) => (
84
+ <TestimonialCard key={i} testimonial={testimonial} isActive={i === currentIndex} />
85
+ ))}
86
+ </div>
87
+
88
+ {/* Tablet: Show 2 cards */}
89
+ <div className="hidden sm:flex lg:hidden gap-4 w-full">
90
+ {[0, 1].map((offset) => {
91
+ const idx = (currentIndex + offset) % testimonials.length;
92
+ return (
93
+ <div key={idx} className="w-1/2">
94
+ <TestimonialCard testimonial={testimonials[idx]} isActive={offset === 0} />
95
+ </div>
96
+ );
97
+ })}
98
+ </div>
99
+
100
+ {/* Mobile: Single card with slide animation */}
101
+ <div className="sm:hidden w-full">
102
+ <div
103
+ className="flex transition-transform duration-500 ease-out"
104
+ style={{ transform: `translateX(-${currentIndex * 100}%)` }}
105
+ >
106
+ {testimonials.map((testimonial, i) => (
107
+ <div key={i} className="w-full flex-shrink-0 px-1">
108
+ <TestimonialCard testimonial={testimonial} isActive={i === currentIndex} />
109
+ </div>
110
+ ))}
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ {/* Navigation Arrows - Mobile & Tablet */}
116
+ <button
117
+ onClick={prevSlide}
118
+ className="lg:hidden absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 sm:-translate-x-4 w-10 h-10 rounded-full bg-surface-elevated border border-border shadow-lg flex items-center justify-center hover:bg-surface transition z-10"
119
+ aria-label="Previous testimonial"
120
+ >
121
+ <ChevronLeft className="w-5 h-5" />
122
+ </button>
123
+ <button
124
+ onClick={nextSlide}
125
+ className="lg:hidden absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 sm:translate-x-4 w-10 h-10 rounded-full bg-surface-elevated border border-border shadow-lg flex items-center justify-center hover:bg-surface transition z-10"
126
+ aria-label="Next testimonial"
127
+ >
128
+ <ChevronRight className="w-5 h-5" />
129
+ </button>
130
+ </div>
131
+
132
+ {/* Dots - Mobile & Tablet */}
133
+ <div className="lg:hidden flex justify-center gap-2 mt-6">
134
+ {testimonials.map((_, i) => (
135
+ <button
136
+ key={i}
137
+ onClick={() => setCurrentIndex(i)}
138
+ className={`w-2 h-2 rounded-full transition-all ${
139
+ i === currentIndex
140
+ ? "bg-accent w-6"
141
+ : "bg-border hover:bg-text-muted"
142
+ }`}
143
+ aria-label={`Go to testimonial ${i + 1}`}
144
+ />
145
+ ))}
146
+ </div>
147
+ </div>
148
+ </section>
149
+ );
150
+ }
151
+
152
+ function TestimonialCard({
153
+ testimonial,
154
+ isActive
155
+ }: {
156
+ testimonial: typeof testimonials[0];
157
+ isActive: boolean;
158
+ }) {
159
+ return (
160
+ <div
161
+ className={`
162
+ relative p-5 sm:p-6 rounded-2xl border transition-all duration-300
163
+ ${isActive
164
+ ? "bg-surface-elevated border-accent/30 shadow-lg shadow-accent/5"
165
+ : "bg-surface border-border hover:border-border-subtle"
166
+ }
167
+ `}
168
+ >
169
+ {/* Gradient accent */}
170
+ <div className={`absolute inset-x-0 top-0 h-1 rounded-t-2xl bg-gradient-to-r ${testimonial.color} opacity-60`} />
171
+
172
+ {/* Quote */}
173
+ <p className="text-text-primary text-sm sm:text-base leading-relaxed mb-6 min-h-[4.5rem]">
174
+ "{testimonial.quote}"
175
+ </p>
176
+
177
+ {/* Attribution */}
178
+ <div className="flex items-center gap-3">
179
+ <div className="w-10 h-10 rounded-xl bg-white/10 flex items-center justify-center p-1.5">
180
+ <Image
181
+ src={testimonial.logo}
182
+ alt={testimonial.model}
183
+ width={28}
184
+ height={28}
185
+ className="object-contain"
186
+ />
187
+ </div>
188
+ <div>
189
+ <div className="font-semibold text-sm">{testimonial.model}</div>
190
+ <div className="text-text-muted text-xs">{testimonial.role}</div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,310 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ ChevronDown,
6
+ ChevronRight,
7
+ CheckCircle2,
8
+ XCircle,
9
+ Loader2,
10
+ PauseCircle,
11
+ SkipForward,
12
+ Clock,
13
+ RotateCcw,
14
+ Zap,
15
+ ArrowRight,
16
+ AlertTriangle,
17
+ } from "lucide-react";
18
+
19
+ interface StepExecution {
20
+ _id: string;
21
+ stepId: string;
22
+ stepIndex: number;
23
+ status: "pending" | "running" | "completed" | "failed" | "skipped";
24
+ input?: any;
25
+ output?: any;
26
+ latencyMs?: number;
27
+ costCents?: number;
28
+ error?: {
29
+ code: string;
30
+ message: string;
31
+ retryCount?: number;
32
+ };
33
+ parallelGroup?: string;
34
+ createdAt: number;
35
+ startedAt?: number;
36
+ completedAt?: number;
37
+ }
38
+
39
+ interface ChainStepDetailProps {
40
+ step: StepExecution;
41
+ stepDef?: {
42
+ id: string;
43
+ provider: string;
44
+ action?: string;
45
+ params?: Record<string, any>;
46
+ };
47
+ isExpanded: boolean;
48
+ onToggle: () => void;
49
+ }
50
+
51
+ export function ChainStepDetail({ step, stepDef, isExpanded, onToggle }: ChainStepDetailProps) {
52
+ const [activeTab, setActiveTab] = useState<"input" | "output" | "error">("input");
53
+
54
+ const getStatusIcon = (status: string) => {
55
+ switch (status) {
56
+ case "completed":
57
+ return <CheckCircle2 className="w-4 h-4 text-green-500" />;
58
+ case "running":
59
+ return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
60
+ case "failed":
61
+ return <XCircle className="w-4 h-4 text-red-500" />;
62
+ case "skipped":
63
+ return <SkipForward className="w-4 h-4 text-gray-500" />;
64
+ case "paused":
65
+ return <PauseCircle className="w-4 h-4 text-yellow-500" />;
66
+ default:
67
+ return <Clock className="w-4 h-4 text-gray-500" />;
68
+ }
69
+ };
70
+
71
+ const getStatusBg = (status: string) => {
72
+ switch (status) {
73
+ case "completed":
74
+ return "bg-green-500/10 border-green-500/20";
75
+ case "running":
76
+ return "bg-blue-500/10 border-blue-500/20";
77
+ case "failed":
78
+ return "bg-red-500/10 border-red-500/20";
79
+ case "skipped":
80
+ return "bg-gray-500/10 border-gray-500/20";
81
+ case "paused":
82
+ return "bg-yellow-500/10 border-yellow-500/20";
83
+ default:
84
+ return "bg-white/5 border-white/10";
85
+ }
86
+ };
87
+
88
+ const formatDuration = (ms: number) => {
89
+ if (ms < 1000) return `${ms}ms`;
90
+ return `${(ms / 1000).toFixed(1)}s`;
91
+ };
92
+
93
+ const formatCost = (cents: number) => {
94
+ if (cents === 0) return "$0.00";
95
+ return `$${(cents / 100).toFixed(2)}`;
96
+ };
97
+
98
+ // Highlight references in input (e.g., $generate.url)
99
+ const highlightReferences = (obj: any): any => {
100
+ if (typeof obj === "string") {
101
+ // Check if string contains references
102
+ if (obj.includes("$")) {
103
+ return obj;
104
+ }
105
+ return obj;
106
+ }
107
+ if (Array.isArray(obj)) {
108
+ return obj.map(highlightReferences);
109
+ }
110
+ if (typeof obj === "object" && obj !== null) {
111
+ return Object.fromEntries(
112
+ Object.entries(obj).map(([k, v]) => [k, highlightReferences(v)])
113
+ );
114
+ }
115
+ return obj;
116
+ };
117
+
118
+ // Render JSON with syntax highlighting
119
+ const renderJson = (data: any, highlightRefs = false) => {
120
+ if (!data) return <span className="text-white/40">null</span>;
121
+
122
+ const jsonString = JSON.stringify(data, null, 2);
123
+
124
+ if (highlightRefs) {
125
+ // Highlight $references
126
+ const parts = jsonString.split(/(\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_\[\]]+)*)/g);
127
+ return (
128
+ <pre className="text-xs font-mono text-white/80 whitespace-pre-wrap break-all">
129
+ {parts.map((part, i) =>
130
+ part.startsWith("$") ? (
131
+ <span key={i} className="bg-[#ef4444]/20 text-[#ef4444] px-1 rounded">
132
+ {part}
133
+ </span>
134
+ ) : (
135
+ <span key={i}>{part}</span>
136
+ )
137
+ )}
138
+ </pre>
139
+ );
140
+ }
141
+
142
+ return (
143
+ <pre className="text-xs font-mono text-white/80 whitespace-pre-wrap break-all">
144
+ {jsonString}
145
+ </pre>
146
+ );
147
+ };
148
+
149
+ return (
150
+ <div className={`rounded-lg border transition-all ${getStatusBg(step.status)}`}>
151
+ {/* Header */}
152
+ <button
153
+ onClick={onToggle}
154
+ className="w-full px-4 py-3 flex items-center justify-between text-left"
155
+ >
156
+ <div className="flex items-center gap-3">
157
+ {isExpanded ? (
158
+ <ChevronDown className="w-4 h-4 text-white/40" />
159
+ ) : (
160
+ <ChevronRight className="w-4 h-4 text-white/40" />
161
+ )}
162
+ {getStatusIcon(step.status)}
163
+ <div className="flex items-center gap-2">
164
+ <span className="font-medium font-mono">{step.stepId}</span>
165
+ {stepDef && (
166
+ <span className="text-white/40 flex items-center gap-1 text-sm">
167
+ <ArrowRight className="w-3 h-3" />
168
+ <span className="text-white/60">{stepDef.provider}</span>
169
+ {stepDef.action && (
170
+ <>
171
+ <span className="text-white/20">/</span>
172
+ <span>{stepDef.action}</span>
173
+ </>
174
+ )}
175
+ </span>
176
+ )}
177
+ </div>
178
+ </div>
179
+ <div className="flex items-center gap-4 text-sm text-white/60">
180
+ {step.error?.retryCount && step.error.retryCount > 0 && (
181
+ <span className="flex items-center gap-1 text-yellow-500">
182
+ <RotateCcw className="w-3.5 h-3.5" />
183
+ {step.error.retryCount} retries
184
+ </span>
185
+ )}
186
+ <span className="flex items-center gap-1">
187
+ <Clock className="w-3.5 h-3.5" />
188
+ {formatDuration(step.latencyMs || 0)}
189
+ </span>
190
+ <span className="flex items-center gap-1">
191
+ <Zap className="w-3.5 h-3.5" />
192
+ {formatCost(step.costCents || 0)}
193
+ </span>
194
+ </div>
195
+ </button>
196
+
197
+ {/* Expanded Content */}
198
+ {isExpanded && (
199
+ <div className="px-4 pb-4 pt-0">
200
+ {/* Tabs */}
201
+ <div className="flex items-center gap-1 mb-3 border-b border-white/10 pb-2">
202
+ <button
203
+ onClick={() => setActiveTab("input")}
204
+ className={`px-3 py-1.5 rounded-t text-sm transition-colors ${
205
+ activeTab === "input"
206
+ ? "bg-white/10 text-white"
207
+ : "text-white/40 hover:text-white/60"
208
+ }`}
209
+ >
210
+ Input
211
+ </button>
212
+ <button
213
+ onClick={() => setActiveTab("output")}
214
+ className={`px-3 py-1.5 rounded-t text-sm transition-colors ${
215
+ activeTab === "output"
216
+ ? "bg-white/10 text-white"
217
+ : "text-white/40 hover:text-white/60"
218
+ }`}
219
+ >
220
+ Output
221
+ </button>
222
+ {step.error && (
223
+ <button
224
+ onClick={() => setActiveTab("error")}
225
+ className={`px-3 py-1.5 rounded-t text-sm transition-colors flex items-center gap-1 ${
226
+ activeTab === "error"
227
+ ? "bg-red-500/20 text-red-500"
228
+ : "text-red-400 hover:text-red-300"
229
+ }`}
230
+ >
231
+ <AlertTriangle className="w-3.5 h-3.5" />
232
+ Error
233
+ </button>
234
+ )}
235
+ </div>
236
+
237
+ {/* Tab Content */}
238
+ <div className="bg-black/30 rounded-lg p-3 max-h-64 overflow-auto">
239
+ {activeTab === "input" && (
240
+ <div>
241
+ {step.input ? (
242
+ renderJson(step.input, true)
243
+ ) : stepDef?.params ? (
244
+ renderJson(stepDef.params, true)
245
+ ) : (
246
+ <span className="text-white/40 text-sm">No input data</span>
247
+ )}
248
+ </div>
249
+ )}
250
+
251
+ {activeTab === "output" && (
252
+ <div>
253
+ {step.output ? (
254
+ renderJson(step.output)
255
+ ) : (
256
+ <span className="text-white/40 text-sm">
257
+ {step.status === "running"
258
+ ? "Waiting for output..."
259
+ : step.status === "pending"
260
+ ? "Step not yet executed"
261
+ : "No output data"}
262
+ </span>
263
+ )}
264
+ </div>
265
+ )}
266
+
267
+ {activeTab === "error" && step.error && (
268
+ <div className="space-y-2">
269
+ <div className="flex items-center gap-2">
270
+ <span className="text-xs text-white/40">Code:</span>
271
+ <code className="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded">
272
+ {step.error.code}
273
+ </code>
274
+ </div>
275
+ <div>
276
+ <span className="text-xs text-white/40">Message:</span>
277
+ <p className="text-sm text-red-400 mt-1">{step.error.message}</p>
278
+ </div>
279
+ {step.error.retryCount && step.error.retryCount > 0 && (
280
+ <div className="flex items-center gap-2 pt-2 border-t border-white/10">
281
+ <RotateCcw className="w-3.5 h-3.5 text-yellow-500" />
282
+ <span className="text-xs text-white/60">
283
+ Retried {step.error.retryCount} time(s) before failing
284
+ </span>
285
+ </div>
286
+ )}
287
+ </div>
288
+ )}
289
+ </div>
290
+
291
+ {/* Step Metadata */}
292
+ <div className="flex items-center gap-4 mt-3 pt-3 border-t border-white/10 text-xs text-white/40">
293
+ <span>Index: {step.stepIndex}</span>
294
+ {step.parallelGroup && (
295
+ <span className="px-2 py-0.5 bg-white/10 rounded">
296
+ Parallel: {step.parallelGroup}
297
+ </span>
298
+ )}
299
+ {step.startedAt && (
300
+ <span>Started: {new Date(step.startedAt).toLocaleTimeString()}</span>
301
+ )}
302
+ {step.completedAt && (
303
+ <span>Completed: {new Date(step.completedAt).toLocaleTimeString()}</span>
304
+ )}
305
+ </div>
306
+ </div>
307
+ )}
308
+ </div>
309
+ );
310
+ }