khotan-data 0.0.1 → 0.1.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 (51) hide show
  1. package/AGENTS.md +54 -0
  2. package/README.md +62 -0
  3. package/dist/cli.js +2585 -0
  4. package/dist/factory.cjs +2319 -0
  5. package/dist/factory.cjs.map +1 -0
  6. package/dist/factory.d.cts +475 -0
  7. package/dist/factory.d.ts +475 -0
  8. package/dist/factory.js +2311 -0
  9. package/dist/factory.js.map +1 -0
  10. package/dist/plug-client.cjs +99 -0
  11. package/dist/plug-client.cjs.map +1 -0
  12. package/dist/plug-client.d.cts +71 -0
  13. package/dist/plug-client.d.ts +71 -0
  14. package/dist/plug-client.js +96 -0
  15. package/dist/plug-client.js.map +1 -0
  16. package/dist/templates/agent-skill.md +73 -0
  17. package/dist/templates/agents.md +41 -0
  18. package/dist/templates/catch.example.ts +36 -0
  19. package/dist/templates/catch.ts +107 -0
  20. package/dist/templates/config-page.tsx +20 -0
  21. package/dist/templates/debug-index-page.tsx +101 -0
  22. package/dist/templates/debug-page.tsx +48 -0
  23. package/dist/templates/graph-page.tsx +11 -0
  24. package/dist/templates/hub.tsx +450 -0
  25. package/dist/templates/inflow.example.ts +61 -0
  26. package/dist/templates/inflow.ts +99 -0
  27. package/dist/templates/khotan-config.ts +40 -0
  28. package/dist/templates/khotan-route.ts +13 -0
  29. package/dist/templates/logs-page.tsx +9 -0
  30. package/dist/templates/logs.tsx +20 -0
  31. package/dist/templates/outflow.example.ts +52 -0
  32. package/dist/templates/outflow.ts +90 -0
  33. package/dist/templates/pass.example.ts +51 -0
  34. package/dist/templates/pass.ts +124 -0
  35. package/dist/templates/plug-debugger.tsx +1185 -0
  36. package/dist/templates/plug.example.ts +93 -0
  37. package/dist/templates/plug.ts +806 -0
  38. package/dist/templates/relay.example.ts +61 -0
  39. package/dist/templates/relay.ts +95 -0
  40. package/dist/templates/runs-table.tsx +592 -0
  41. package/dist/templates/schema.ts +424 -0
  42. package/dist/templates/skill-dashboard.md +144 -0
  43. package/dist/templates/skill-plug.md +193 -0
  44. package/dist/templates/skill-setup.md +119 -0
  45. package/dist/templates/skill-webhook.md +196 -0
  46. package/dist/templates/topology-canvas.tsx +1406 -0
  47. package/dist/templates/var-panel.tsx +276 -0
  48. package/dist/templates/webhook-events-table.tsx +241 -0
  49. package/dist/templates/wire-panel.tsx +216 -0
  50. package/dist/templates/wire.ts +155 -0
  51. package/package.json +46 -5
@@ -0,0 +1,20 @@
1
+ import { KhotanHub } from "@/components/khotan/hub";
2
+
3
+ function getWebhookUrl(): string {
4
+ return (
5
+ process.env.KHOTAN_WEBHOOK_URL ||
6
+ process.env.NGROK_URL ||
7
+ process.env.NEXT_PUBLIC_APP_URL ||
8
+ "http://localhost:3000"
9
+ );
10
+ }
11
+
12
+ export default function KhotanConfigPage() {
13
+ const webhookUrl = getWebhookUrl();
14
+
15
+ return (
16
+ <main className="container mx-auto max-w-5xl px-4 py-10">
17
+ <KhotanHub webhookUrl={webhookUrl} />
18
+ </main>
19
+ );
20
+ }
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import Link from "next/link";
5
+
6
+ // ============================================================================
7
+ // Debug Index — Lists all registered plugs for debugging
8
+ // Generated by khotan CLI · https://github.com/khotan-data
9
+ //
10
+ // Place at: app/debug/page.tsx
11
+ // Requires KHOTAN_DEBUG env var to be set for the debug route to respond.
12
+ // ============================================================================
13
+
14
+ interface Plug {
15
+ id: string;
16
+ name: string;
17
+ baseUrl: string;
18
+ authType: string;
19
+ status: "connected" | "error" | "idle";
20
+ }
21
+
22
+ export default function DebugIndexPage() {
23
+ const [plugs, setPlugs] = useState<Plug[]>([]);
24
+ const [loading, setLoading] = useState(true);
25
+ const [debugEnabled, setDebugEnabled] = useState<boolean | null>(null);
26
+
27
+ useEffect(() => {
28
+ Promise.all([
29
+ fetch("/api/khotan/debug").then((r) => r.ok),
30
+ fetch("/api/khotan/plugs").then((r) => (r.ok ? r.json() : [])),
31
+ ])
32
+ .then(([enabled, data]) => {
33
+ setDebugEnabled(enabled);
34
+ setPlugs(data as Plug[]);
35
+ })
36
+ .catch(() => setDebugEnabled(false))
37
+ .finally(() => setLoading(false));
38
+ }, []);
39
+
40
+ if (loading) {
41
+ return (
42
+ <main className="container mx-auto max-w-3xl px-4 py-10">
43
+ <div className="h-8 w-48 animate-pulse rounded bg-muted" />
44
+ </main>
45
+ );
46
+ }
47
+
48
+ if (!debugEnabled) {
49
+ return (
50
+ <main className="container mx-auto max-w-3xl px-4 py-10">
51
+ <h1 className="text-2xl font-bold tracking-tight mb-4">Debug</h1>
52
+ <p className="text-muted-foreground">
53
+ Debug mode is not enabled. Set{" "}
54
+ <code className="bg-muted px-1.5 py-0.5 rounded text-sm">
55
+ KHOTAN_DEBUG=1
56
+ </code>{" "}
57
+ in your environment and restart the server.
58
+ </p>
59
+ </main>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <main className="container mx-auto max-w-3xl px-4 py-10">
65
+ <div className="mb-6">
66
+ <a
67
+ href="/config"
68
+ className="text-sm text-muted-foreground hover:text-foreground"
69
+ >
70
+ ← Back to Hub
71
+ </a>
72
+ </div>
73
+ <h1 className="text-2xl font-bold tracking-tight mb-6">Debug</h1>
74
+ <p className="text-muted-foreground mb-6">
75
+ Select a plug to test requests through its real code path.
76
+ </p>
77
+ <div className="grid gap-3">
78
+ {plugs.map((plug) => (
79
+ <Link
80
+ key={plug.id}
81
+ href={`/debug/${plug.name}`}
82
+ className="flex items-center justify-between rounded-lg border border-border p-4 transition-colors hover:border-foreground/30 hover:bg-muted/50"
83
+ >
84
+ <div>
85
+ <p className="font-medium">{plug.name}</p>
86
+ <p className="text-xs text-muted-foreground truncate">
87
+ {plug.baseUrl}
88
+ </p>
89
+ </div>
90
+ <div className="flex items-center gap-2">
91
+ <span className="text-xs text-muted-foreground">
92
+ {plug.authType}
93
+ </span>
94
+ <span className="text-muted-foreground">→</span>
95
+ </div>
96
+ </Link>
97
+ ))}
98
+ </div>
99
+ </main>
100
+ );
101
+ }
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import { useParams } from "next/navigation";
4
+ import { PlugDebugger } from "@/components/khotan/plug-debugger";
5
+
6
+ // ============================================================================
7
+ // Debug Page — Route for plug debugging
8
+ // Generated by khotan CLI · https://github.com/khotan-data
9
+ //
10
+ // Place at: app/debug/[plugName]/page.tsx
11
+ // Requires KHOTAN_DEBUG env var to be set for the debug route to respond.
12
+ // ============================================================================
13
+
14
+ export default function PlugDebugPage() {
15
+ const params = useParams<{ plugName: string }>();
16
+ const plugName = params.plugName;
17
+
18
+ if (!plugName) {
19
+ return (
20
+ <main className="container mx-auto max-w-6xl px-4 py-10">
21
+ <p className="text-muted-foreground">No plug specified.</p>
22
+ </main>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <main className="container mx-auto max-w-6xl px-4 py-10">
28
+ <div className="flex items-center justify-between mb-6">
29
+ <a
30
+ href="/debug"
31
+ className="text-sm text-muted-foreground hover:text-foreground"
32
+ >
33
+ ← All Plugs
34
+ </a>
35
+ <a
36
+ href="/config"
37
+ className="text-xs text-muted-foreground hover:text-foreground"
38
+ >
39
+ Hub
40
+ </a>
41
+ </div>
42
+ <h1 className="text-2xl font-bold tracking-tight mb-6">
43
+ Debug: {plugName}
44
+ </h1>
45
+ <PlugDebugger plugName={plugName} />
46
+ </main>
47
+ );
48
+ }
@@ -0,0 +1,11 @@
1
+ import { KhotanTopologyCanvas } from "@/components/khotan/topology-canvas";
2
+
3
+ export default function KhotanGraphPage() {
4
+ return (
5
+ <main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(248,250,252,0.94),_rgba(241,245,249,0.82)_38%,_rgba(226,232,240,0.7))]">
6
+ <div className="mx-auto max-w-[1720px] px-4 py-8 md:px-6 xl:py-10">
7
+ <KhotanTopologyCanvas />
8
+ </div>
9
+ </main>
10
+ );
11
+ }
@@ -0,0 +1,450 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "@/components/ui/table";
14
+ import { Switch } from "@/components/ui/switch";
15
+ import { Button } from "@/components/ui/button";
16
+ import { WirePanel } from "./wire";
17
+ import { VarPanel } from "./var-panel";
18
+
19
+ // ============================================================================
20
+ // Khotan Hub — Dashboard for configured plugs and flows
21
+ // Generated by khotan CLI · https://github.com/khotan-data
22
+ //
23
+ // This file is yours. Restyle, extend, or embed it however you like.
24
+ // It uses shadcn/ui primitives and fetches from /api/khotan.
25
+ // ============================================================================
26
+
27
+ interface Plug {
28
+ id: string;
29
+ name: string;
30
+ baseUrl: string;
31
+ authType: string;
32
+ enabled: boolean;
33
+ status: "connected" | "error" | "idle";
34
+ statusMessage: string | null;
35
+ flowCount: number;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }
39
+
40
+ interface Flow {
41
+ id: string;
42
+ plugId: string;
43
+ name: string;
44
+ type: "inflow" | "outflow" | "relay" | "webhook";
45
+ enabled: boolean;
46
+ schedule: string | null;
47
+ lastRunAt: string | null;
48
+ lastRunStatus: "completed" | "partial" | "failed" | "cancelled" | null;
49
+ plugName: string | null;
50
+ }
51
+
52
+ interface WebhookHandler {
53
+ id: string;
54
+ wireId: string;
55
+ name: string;
56
+ type: "catch" | "pass";
57
+ destinationPlugId: string | null;
58
+ events: string[] | null;
59
+ enabled: boolean;
60
+ lastRunAt: string | null;
61
+ lastRunStatus:
62
+ | "pending"
63
+ | "running"
64
+ | "completed"
65
+ | "partial"
66
+ | "failed"
67
+ | "cancelled"
68
+ | null;
69
+ createdAt: string;
70
+ updatedAt: string;
71
+ }
72
+
73
+ type StatusVariant = "default" | "secondary" | "destructive" | "outline";
74
+ type FlowTypeVariant = "default" | "secondary" | "outline";
75
+
76
+ const statusVariant: Record<string, StatusVariant> = {
77
+ connected: "default",
78
+ idle: "secondary",
79
+ error: "destructive",
80
+ };
81
+
82
+ const flowTypeVariant: Record<string, FlowTypeVariant> = {
83
+ inflow: "default",
84
+ outflow: "secondary",
85
+ relay: "outline",
86
+ webhook: "outline",
87
+ };
88
+
89
+ const runStatusVariant: Record<string, StatusVariant> = {
90
+ pending: "outline",
91
+ running: "secondary",
92
+ completed: "default",
93
+ partial: "secondary",
94
+ failed: "destructive",
95
+ cancelled: "outline",
96
+ };
97
+
98
+ export function KhotanHub({
99
+ webhookUrl,
100
+ debugHref,
101
+ logsHref = "/logs",
102
+ }: {
103
+ webhookUrl?: string;
104
+ debugHref?: (plugName: string) => string;
105
+ logsHref?: string;
106
+ } = {}) {
107
+ const [plugs, setPlugs] = useState<Plug[]>([]);
108
+ const [flows, setFlows] = useState<Flow[]>([]);
109
+ const [webhookHandlers, setWebhookHandlers] = useState<WebhookHandler[]>([]);
110
+ const [loading, setLoading] = useState(true);
111
+ const [error, setError] = useState<string | null>(null);
112
+ const [selectedPlugId, setSelectedPlugId] = useState<string | null>(null);
113
+ const [debugEnabled, setDebugEnabled] = useState(false);
114
+
115
+ useEffect(() => {
116
+ fetch("/api/khotan/debug")
117
+ .then((res) => setDebugEnabled(res.ok))
118
+ .catch(() => setDebugEnabled(false));
119
+ }, []);
120
+
121
+ async function fetchData() {
122
+ setLoading(true);
123
+ setError(null);
124
+ try {
125
+ const [plugsRes, flowsRes] = await Promise.all([
126
+ fetch("/api/khotan/plugs"),
127
+ fetch("/api/khotan/flows"),
128
+ ]);
129
+ if (!plugsRes.ok || !flowsRes.ok) {
130
+ throw new Error("Failed to fetch khotan data");
131
+ }
132
+ setPlugs(await plugsRes.json());
133
+ setFlows(await flowsRes.json());
134
+ } catch (err) {
135
+ setError(err instanceof Error ? err.message : "Unknown error");
136
+ } finally {
137
+ setLoading(false);
138
+ }
139
+ }
140
+
141
+ async function toggleFlow(flowId: string, enabled: boolean) {
142
+ setFlows((prev) =>
143
+ prev.map((flow) => (flow.id === flowId ? { ...flow, enabled } : flow)),
144
+ );
145
+ await fetch(`/api/khotan/flows/${flowId}`, {
146
+ method: "PATCH",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({ enabled }),
149
+ });
150
+ }
151
+
152
+ async function toggleWebhookHandler(handlerId: string, enabled: boolean) {
153
+ setWebhookHandlers((prev) =>
154
+ prev.map((h) => (h.id === handlerId ? { ...h, enabled } : h)),
155
+ );
156
+ await fetch(`/api/khotan/webhook-handlers/${handlerId}`, {
157
+ method: "PATCH",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ enabled }),
160
+ });
161
+ }
162
+
163
+ useEffect(() => {
164
+ fetchData();
165
+ }, []);
166
+
167
+ useEffect(() => {
168
+ if (!selectedPlugId) {
169
+ setWebhookHandlers([]);
170
+ return;
171
+ }
172
+ const plug = plugs.find((p) => p.id === selectedPlugId);
173
+ if (!plug) return;
174
+ fetch(`/api/khotan/webhook-handlers/${plug.name}`)
175
+ .then((res) => (res.ok ? res.json() : []))
176
+ .then((data) => setWebhookHandlers(data))
177
+ .catch(() => setWebhookHandlers([]));
178
+ }, [selectedPlugId, plugs]);
179
+
180
+ if (loading) {
181
+ return (
182
+ <div className="space-y-4">
183
+ <div className="h-8 w-48 animate-pulse rounded bg-muted" />
184
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
185
+ {[1, 2, 3].map((i) => (
186
+ <Card key={i}>
187
+ <CardHeader>
188
+ <div className="h-5 w-32 animate-pulse rounded bg-muted" />
189
+ </CardHeader>
190
+ <CardContent>
191
+ <div className="space-y-2">
192
+ <div className="h-4 w-full animate-pulse rounded bg-muted" />
193
+ <div className="h-4 w-2/3 animate-pulse rounded bg-muted" />
194
+ </div>
195
+ </CardContent>
196
+ </Card>
197
+ ))}
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+
203
+ if (error) {
204
+ return (
205
+ <Card>
206
+ <CardContent className="py-8 text-center">
207
+ <p className="text-destructive mb-4">{error}</p>
208
+ <button
209
+ onClick={fetchData}
210
+ className="text-sm underline hover:no-underline"
211
+ >
212
+ Retry
213
+ </button>
214
+ </CardContent>
215
+ </Card>
216
+ );
217
+ }
218
+
219
+ if (plugs.length === 0) {
220
+ return (
221
+ <Card>
222
+ <CardContent className="py-8 text-center">
223
+ <p className="text-muted-foreground mb-2">No plugs configured yet.</p>
224
+ <p className="text-sm text-muted-foreground">
225
+ Register plugs in your <code>khotan.ts</code> config file, then
226
+ restart the dev server.
227
+ </p>
228
+ </CardContent>
229
+ </Card>
230
+ );
231
+ }
232
+
233
+ const selectedPlug = selectedPlugId
234
+ ? plugs.find((p) => p.id === selectedPlugId)
235
+ : null;
236
+ const plugFlows = selectedPlugId
237
+ ? flows.filter((flow) => flow.plugId === selectedPlugId)
238
+ : [];
239
+
240
+ return (
241
+ <div className="space-y-6">
242
+ <div className="flex items-center justify-between gap-4">
243
+ <h2 className="text-2xl font-bold tracking-tight">Khotan Hub</h2>
244
+ <Button
245
+ variant="outline"
246
+ size="sm"
247
+ onClick={() => {
248
+ window.location.href = logsHref;
249
+ }}
250
+ >
251
+ Open Logs
252
+ </Button>
253
+ </div>
254
+
255
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
256
+ {plugs.map((plug) => (
257
+ <Card
258
+ key={plug.id}
259
+ className={`cursor-pointer transition-colors hover:border-primary ${
260
+ selectedPlugId === plug.id ? "border-primary" : ""
261
+ }`}
262
+ onClick={() =>
263
+ setSelectedPlugId(selectedPlugId === plug.id ? null : plug.id)
264
+ }
265
+ >
266
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
267
+ <CardTitle className="text-sm font-medium">{plug.name}</CardTitle>
268
+ <Badge variant={statusVariant[plug.status] ?? "secondary"}>
269
+ {plug.status}
270
+ </Badge>
271
+ </CardHeader>
272
+ <CardContent>
273
+ <p className="text-xs text-muted-foreground truncate">
274
+ {plug.baseUrl}
275
+ </p>
276
+ <div className="mt-2 flex items-center justify-between">
277
+ <p className="text-xs text-muted-foreground">
278
+ {plug.authType} · {plug.flowCount} flow
279
+ {plug.flowCount !== 1 ? "s" : ""}
280
+ </p>
281
+ {debugEnabled && (
282
+ <Button
283
+ size="sm"
284
+ variant="ghost"
285
+ className="h-6 px-2 text-[11px]"
286
+ onClick={(e) => {
287
+ e.stopPropagation();
288
+ if (debugHref) {
289
+ window.location.href = debugHref(plug.name);
290
+ } else {
291
+ window.location.href = `/debug/${plug.name}`;
292
+ }
293
+ }}
294
+ >
295
+ Debug
296
+ </Button>
297
+ )}
298
+ </div>
299
+ </CardContent>
300
+ </Card>
301
+ ))}
302
+ </div>
303
+
304
+ {selectedPlug && (
305
+ <div className="space-y-4">
306
+ <Card>
307
+ <CardHeader>
308
+ <CardTitle>{selectedPlug.name} — Flows</CardTitle>
309
+ </CardHeader>
310
+ <CardContent>
311
+ {plugFlows.length === 0 ? (
312
+ <p className="text-sm text-muted-foreground">
313
+ No flows configured for this plug.
314
+ </p>
315
+ ) : (
316
+ <Table>
317
+ <TableHeader>
318
+ <TableRow>
319
+ <TableHead>Name</TableHead>
320
+ <TableHead>Type</TableHead>
321
+ <TableHead>Schedule</TableHead>
322
+ <TableHead>Last Run</TableHead>
323
+ <TableHead>Enabled</TableHead>
324
+ </TableRow>
325
+ </TableHeader>
326
+ <TableBody>
327
+ {plugFlows.map((flow) => (
328
+ <TableRow key={flow.id}>
329
+ <TableCell className="font-medium">
330
+ {flow.name}
331
+ </TableCell>
332
+ <TableCell>
333
+ <Badge
334
+ variant={flowTypeVariant[flow.type] ?? "outline"}
335
+ >
336
+ {flow.type}
337
+ </Badge>
338
+ </TableCell>
339
+ <TableCell className="text-muted-foreground">
340
+ {flow.schedule ?? "—"}
341
+ </TableCell>
342
+ <TableCell>
343
+ {flow.lastRunStatus ? (
344
+ <Badge
345
+ variant={
346
+ runStatusVariant[flow.lastRunStatus] ??
347
+ "secondary"
348
+ }
349
+ >
350
+ {flow.lastRunStatus}
351
+ </Badge>
352
+ ) : (
353
+ <span className="text-muted-foreground">never</span>
354
+ )}
355
+ </TableCell>
356
+ <TableCell>
357
+ <Switch
358
+ checked={flow.enabled}
359
+ onCheckedChange={(v) => toggleFlow(flow.id, v)}
360
+ />
361
+ </TableCell>
362
+ </TableRow>
363
+ ))}
364
+ </TableBody>
365
+ </Table>
366
+ )}
367
+ </CardContent>
368
+ </Card>
369
+
370
+ {webhookHandlers.length > 0 && (
371
+ <Card>
372
+ <CardHeader>
373
+ <CardTitle>{selectedPlug.name} — Webhook Handlers</CardTitle>
374
+ </CardHeader>
375
+ <CardContent>
376
+ <Table>
377
+ <TableHeader>
378
+ <TableRow>
379
+ <TableHead>Name</TableHead>
380
+ <TableHead>Type</TableHead>
381
+ <TableHead>Events</TableHead>
382
+ <TableHead>Destination</TableHead>
383
+ <TableHead>Last Run</TableHead>
384
+ <TableHead>Enabled</TableHead>
385
+ </TableRow>
386
+ </TableHeader>
387
+ <TableBody>
388
+ {webhookHandlers.map((handler) => (
389
+ <TableRow key={handler.id}>
390
+ <TableCell className="font-medium">
391
+ {handler.name}
392
+ </TableCell>
393
+ <TableCell>
394
+ <Badge
395
+ variant={
396
+ handler.type === "catch" ? "default" : "secondary"
397
+ }
398
+ >
399
+ {handler.type}
400
+ </Badge>
401
+ </TableCell>
402
+ <TableCell className="text-muted-foreground">
403
+ {handler.events?.length
404
+ ? handler.events.join(", ")
405
+ : "from wire config"}
406
+ </TableCell>
407
+ <TableCell className="text-muted-foreground">
408
+ {handler.destinationPlugId
409
+ ? (plugs.find(
410
+ (p) => p.id === handler.destinationPlugId,
411
+ )?.name ?? handler.destinationPlugId)
412
+ : "—"}
413
+ </TableCell>
414
+ <TableCell>
415
+ {handler.lastRunStatus ? (
416
+ <Badge
417
+ variant={
418
+ runStatusVariant[handler.lastRunStatus] ??
419
+ "secondary"
420
+ }
421
+ >
422
+ {handler.lastRunStatus}
423
+ </Badge>
424
+ ) : (
425
+ <span className="text-muted-foreground">never</span>
426
+ )}
427
+ </TableCell>
428
+ <TableCell>
429
+ <Switch
430
+ checked={handler.enabled}
431
+ onCheckedChange={(v) =>
432
+ toggleWebhookHandler(handler.id, v)
433
+ }
434
+ />
435
+ </TableCell>
436
+ </TableRow>
437
+ ))}
438
+ </TableBody>
439
+ </Table>
440
+ </CardContent>
441
+ </Card>
442
+ )}
443
+
444
+ <VarPanel plugName={selectedPlug.name} />
445
+ <WirePanel plugName={selectedPlug.name} webhookUrl={webhookUrl} />
446
+ </div>
447
+ )}
448
+ </div>
449
+ );
450
+ }
@@ -0,0 +1,61 @@
1
+ // ============================================================================
2
+ // Example: Inflow
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // Copy this file, rename it for your source service/resource, and register the
6
+ // exported flow in {outputDir}/khotan.ts.
7
+ // ============================================================================
8
+
9
+ import { inflow, type InflowContext } from "./inflow";
10
+ import { sendUpdate } from "khotan-data/factory";
11
+
12
+ async function shopifyProductsWorkflow(ctx: InflowContext) {
13
+ "use workflow";
14
+
15
+ async function extractAndLoad() {
16
+ "use step";
17
+ console.log("Starting inflow", {
18
+ flow: ctx.flow.name,
19
+ khotanRunId: ctx.khotanRunId,
20
+ runType: ctx.runType,
21
+ });
22
+ await sendUpdate({
23
+ message: "Starting product inflow",
24
+ metadata: { flow: ctx.flow.name, runType: ctx.runType },
25
+ });
26
+
27
+ const response = await fetch("https://api.example.com/products", {
28
+ headers: {
29
+ Authorization: `Bearer ${ctx.vars["apiToken"] ?? ""}`,
30
+ },
31
+ });
32
+ const payload = (await response.json()) as {
33
+ data?: Array<Record<string, unknown>>;
34
+ };
35
+ const records = Array.isArray(payload.data) ? payload.data : [];
36
+
37
+ // Replace this with your app-specific transform and DB upsert.
38
+ console.log("Fetched records", records.length);
39
+ await sendUpdate({
40
+ message: `Fetched ${String(records.length)} products`,
41
+ extracted: records.length,
42
+ progress: 50,
43
+ });
44
+
45
+ return {
46
+ extracted: records.length,
47
+ transformed: records.length,
48
+ created: records.length,
49
+ metadata: { source: ctx.flow.name },
50
+ };
51
+ }
52
+
53
+ return extractAndLoad();
54
+ }
55
+
56
+ export const shopifyProductsInflow = inflow({
57
+ name: "shopify-products-inflow",
58
+ resource: "products",
59
+ schedule: "0 * * * *",
60
+ workflow: shopifyProductsWorkflow,
61
+ });