khotan-data 0.0.1 → 0.1.1

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 (55) hide show
  1. package/AGENTS.md +54 -0
  2. package/README.md +117 -1
  3. package/dist/cli.js +2869 -0
  4. package/dist/factory.cjs +3303 -0
  5. package/dist/factory.cjs.map +1 -0
  6. package/dist/factory.d.cts +662 -0
  7. package/dist/factory.d.ts +662 -0
  8. package/dist/factory.js +3292 -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/cache.example.ts +11 -0
  19. package/dist/templates/cache.ts +58 -0
  20. package/dist/templates/catch.example.ts +36 -0
  21. package/dist/templates/catch.ts +119 -0
  22. package/dist/templates/config-page.tsx +20 -0
  23. package/dist/templates/debug-index-page.tsx +101 -0
  24. package/dist/templates/debug-page.tsx +48 -0
  25. package/dist/templates/graph-page.tsx +11 -0
  26. package/dist/templates/hub.tsx +450 -0
  27. package/dist/templates/inflow.example.ts +61 -0
  28. package/dist/templates/inflow.ts +98 -0
  29. package/dist/templates/khotan-config.ts +49 -0
  30. package/dist/templates/khotan-route.ts +13 -0
  31. package/dist/templates/logs-page.tsx +9 -0
  32. package/dist/templates/logs.tsx +20 -0
  33. package/dist/templates/mapping-browser.tsx +761 -0
  34. package/dist/templates/mappings-page.tsx +9 -0
  35. package/dist/templates/outflow.example.ts +52 -0
  36. package/dist/templates/outflow.ts +90 -0
  37. package/dist/templates/pass.example.ts +51 -0
  38. package/dist/templates/pass.ts +134 -0
  39. package/dist/templates/plug-debugger.tsx +1185 -0
  40. package/dist/templates/plug.example.ts +93 -0
  41. package/dist/templates/plug.ts +806 -0
  42. package/dist/templates/relay.example.ts +71 -0
  43. package/dist/templates/relay.ts +104 -0
  44. package/dist/templates/runs-table.tsx +592 -0
  45. package/dist/templates/schema.ts +505 -0
  46. package/dist/templates/skill-dashboard.md +144 -0
  47. package/dist/templates/skill-plug.md +216 -0
  48. package/dist/templates/skill-setup.md +161 -0
  49. package/dist/templates/skill-webhook.md +196 -0
  50. package/dist/templates/topology-canvas.tsx +1406 -0
  51. package/dist/templates/var-panel.tsx +276 -0
  52. package/dist/templates/webhook-events-table.tsx +241 -0
  53. package/dist/templates/wire-panel.tsx +216 -0
  54. package/dist/templates/wire.ts +155 -0
  55. package/package.json +46 -5
@@ -0,0 +1,592 @@
1
+ "use client";
2
+
3
+ import { Fragment, useCallback, useEffect, useState } from "react";
4
+ import { RefreshCw } from "lucide-react";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { Switch } from "@/components/ui/switch";
9
+ import {
10
+ Table,
11
+ TableBody,
12
+ TableCell,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow,
16
+ } from "@/components/ui/table";
17
+
18
+ interface RunLogItem {
19
+ id: string;
20
+ runType: string;
21
+ status:
22
+ | "pending"
23
+ | "running"
24
+ | "completed"
25
+ | "partial"
26
+ | "failed"
27
+ | "cancelled";
28
+ workflowRunId: string | null;
29
+ sourceType: "flow" | "webhook" | "unknown";
30
+ sourceName: string | null;
31
+ sourceKind: "catch" | "pass" | null;
32
+ plugName: string | null;
33
+ startedAt: string;
34
+ completedAt: string | null;
35
+ extracted: number;
36
+ transformed: number;
37
+ created: number;
38
+ updated: number;
39
+ deleted: number;
40
+ failed: number;
41
+ error: string | null;
42
+ metadata?: Record<string, unknown> | null;
43
+ }
44
+
45
+ interface PageResponse<T> {
46
+ items: T[];
47
+ page: {
48
+ limit: number;
49
+ offset: number;
50
+ hasMore: boolean;
51
+ prevOffset: number;
52
+ nextOffset: number;
53
+ };
54
+ }
55
+
56
+ const statusVariant = {
57
+ pending: "outline",
58
+ running: "secondary",
59
+ completed: "default",
60
+ partial: "secondary",
61
+ failed: "destructive",
62
+ cancelled: "outline",
63
+ } as const;
64
+
65
+ const statusLabel = {
66
+ pending: "pending",
67
+ running: "running",
68
+ completed: "completed",
69
+ partial: "partial",
70
+ failed: "failed",
71
+ cancelled: "cancelled",
72
+ } as const;
73
+
74
+ function formatDateTime(value: string | null): string {
75
+ if (!value) return "Never";
76
+ const date = new Date(value);
77
+ if (Number.isNaN(date.getTime())) return value;
78
+ return date
79
+ .toISOString()
80
+ .replace("T", " ")
81
+ .replace(/\.\d{3}Z$/, " UTC");
82
+ }
83
+
84
+ function formatSource(item: RunLogItem): string {
85
+ if (!item.sourceName) return "Unknown";
86
+ if (item.sourceType !== "webhook" || !item.sourceKind) return item.sourceName;
87
+ return `${item.sourceKind}:${item.sourceName}`;
88
+ }
89
+
90
+ function formatCounts(item: RunLogItem): string {
91
+ const parts = [
92
+ item.extracted > 0 ? `extracted ${String(item.extracted)}` : null,
93
+ item.transformed > 0 ? `transformed ${String(item.transformed)}` : null,
94
+ item.created > 0 ? `created ${String(item.created)}` : null,
95
+ item.updated > 0 ? `updated ${String(item.updated)}` : null,
96
+ item.deleted > 0 ? `deleted ${String(item.deleted)}` : null,
97
+ item.failed > 0 ? `failed ${String(item.failed)}` : null,
98
+ ].filter(Boolean);
99
+ return parts.length > 0 ? parts.join(" - ") : "-";
100
+ }
101
+
102
+ function formatStreamLine(line: string): string {
103
+ try {
104
+ const parsed = JSON.parse(line) as {
105
+ timestamp?: string;
106
+ message?: string;
107
+ type?: string;
108
+ };
109
+ const parsedDate = parsed.timestamp ? new Date(parsed.timestamp) : null;
110
+ const prefix =
111
+ parsedDate && !Number.isNaN(parsedDate.getTime())
112
+ ? `[${parsedDate.toISOString().slice(11, 19)} UTC] `
113
+ : "";
114
+ const type = parsed.type ? `${parsed.type}: ` : "";
115
+ return `${prefix}${type}${parsed.message ?? line}`;
116
+ } catch {
117
+ return line;
118
+ }
119
+ }
120
+
121
+ function RunDetails({
122
+ run,
123
+ streamingEnabled,
124
+ onChanged,
125
+ onStreamInbound,
126
+ }: {
127
+ run: RunLogItem;
128
+ streamingEnabled: boolean;
129
+ onChanged(): void;
130
+ onStreamInbound(): void;
131
+ }) {
132
+ const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
133
+ const [streamLines, setStreamLines] = useState<string[]>([]);
134
+ const [error, setError] = useState<string | null>(null);
135
+ const [busy, setBusy] = useState<"cancel" | "retry" | null>(null);
136
+ const [lastUpdatedAt, setLastUpdatedAt] = useState<string | null>(null);
137
+
138
+ const fetchDetail = useCallback(async (): Promise<
139
+ Record<string, unknown>
140
+ > => {
141
+ const res = await fetch(`/api/khotan/runs/${run.id}`);
142
+ if (!res.ok) throw new Error("Failed to load run detail");
143
+ return (await res.json()) as Record<string, unknown>;
144
+ }, [run.id]);
145
+
146
+ useEffect(() => {
147
+ let cancelled = false;
148
+
149
+ async function loadDetail() {
150
+ try {
151
+ const json = await fetchDetail();
152
+ if (!cancelled) setDetail(json);
153
+ if (!cancelled) setLastUpdatedAt(new Date().toISOString());
154
+ } catch (err) {
155
+ if (!cancelled)
156
+ setError(err instanceof Error ? err.message : "Unknown error");
157
+ }
158
+ }
159
+
160
+ void loadDetail();
161
+ if (!streamingEnabled) {
162
+ return () => {
163
+ cancelled = true;
164
+ };
165
+ }
166
+
167
+ const interval = window.setInterval(() => {
168
+ void loadDetail();
169
+ }, 5000);
170
+ return () => {
171
+ cancelled = true;
172
+ window.clearInterval(interval);
173
+ };
174
+ }, [fetchDetail, streamingEnabled]);
175
+
176
+ useEffect(() => {
177
+ if (!run.workflowRunId) return;
178
+ const isLiveRun = run.status === "pending" || run.status === "running";
179
+ if (!streamingEnabled && isLiveRun) return;
180
+
181
+ const controller = new AbortController();
182
+ let buffer = "";
183
+
184
+ async function readStream() {
185
+ try {
186
+ const res = await fetch(
187
+ `/api/khotan/runs/${run.id}/stream?startIndex=-50`,
188
+ {
189
+ signal: controller.signal,
190
+ },
191
+ );
192
+ if (!res.ok || !res.body) return;
193
+
194
+ const reader = res.body.getReader();
195
+ const decoder = new TextDecoder();
196
+ while (true) {
197
+ const { done, value } = await reader.read();
198
+ if (done) break;
199
+ buffer += decoder.decode(value, { stream: true });
200
+ const lines = buffer.split("\n");
201
+ buffer = lines.pop() ?? "";
202
+ const parsed = lines
203
+ .map((line) => line.trim())
204
+ .filter(Boolean)
205
+ .map(formatStreamLine);
206
+ if (parsed.length > 0) {
207
+ setStreamLines((prev) => [...prev, ...parsed].slice(-100));
208
+ setLastUpdatedAt(new Date().toISOString());
209
+ if (streamingEnabled) onStreamInbound();
210
+ }
211
+ }
212
+ } catch (err) {
213
+ if (!controller.signal.aborted) {
214
+ setError(err instanceof Error ? err.message : "Unknown stream error");
215
+ }
216
+ }
217
+ }
218
+
219
+ void readStream();
220
+ if (streamingEnabled) {
221
+ return () => controller.abort();
222
+ }
223
+
224
+ const timeout = window.setTimeout(() => controller.abort(), 2000);
225
+ return () => {
226
+ window.clearTimeout(timeout);
227
+ controller.abort();
228
+ };
229
+ }, [
230
+ onStreamInbound,
231
+ run.id,
232
+ run.status,
233
+ run.workflowRunId,
234
+ streamingEnabled,
235
+ ]);
236
+
237
+ async function refreshDetail() {
238
+ setError(null);
239
+ try {
240
+ const json = await fetchDetail();
241
+ setDetail(json);
242
+ setLastUpdatedAt(new Date().toISOString());
243
+ } catch (err) {
244
+ setError(err instanceof Error ? err.message : "Unknown error");
245
+ }
246
+ }
247
+
248
+ async function postAction(action: "cancel" | "retry") {
249
+ setBusy(action);
250
+ setError(null);
251
+ try {
252
+ const res = await fetch(`/api/khotan/runs/${run.id}/${action}`, {
253
+ method: "POST",
254
+ });
255
+ if (!res.ok) {
256
+ const data = (await res.json().catch(() => ({}))) as { error?: string };
257
+ throw new Error(data.error ?? `Failed to ${action} run`);
258
+ }
259
+ onChanged();
260
+ } catch (err) {
261
+ setError(err instanceof Error ? err.message : "Unknown error");
262
+ } finally {
263
+ setBusy(null);
264
+ }
265
+ }
266
+
267
+ const workflowStatus =
268
+ typeof detail?.["workflowStatus"] === "string"
269
+ ? detail["workflowStatus"]
270
+ : null;
271
+
272
+ return (
273
+ <div className="space-y-3 rounded-md border bg-muted/20 p-3">
274
+ <div className="flex flex-wrap items-center justify-between gap-3">
275
+ <div className="space-y-1 text-sm">
276
+ <div>
277
+ <span className="font-medium">Khotan run:</span>{" "}
278
+ <code className="text-xs">{run.id}</code>
279
+ </div>
280
+ <div>
281
+ <span className="font-medium">Workflow status:</span>{" "}
282
+ {workflowStatus ?? "unknown"}
283
+ </div>
284
+ {run.workflowRunId ? (
285
+ <div>
286
+ <span className="font-medium">Workflow run:</span>{" "}
287
+ <code className="text-xs">{run.workflowRunId}</code>
288
+ </div>
289
+ ) : null}
290
+ <div className="text-xs text-muted-foreground">
291
+ Last updated:{" "}
292
+ {lastUpdatedAt ? formatDateTime(lastUpdatedAt) : "Not loaded yet"}
293
+ </div>
294
+ </div>
295
+ <div className="flex items-center gap-2">
296
+ <Button
297
+ variant="outline"
298
+ size="sm"
299
+ disabled={streamingEnabled}
300
+ onClick={() => void refreshDetail()}
301
+ >
302
+ Refresh
303
+ </Button>
304
+ <Button
305
+ variant="outline"
306
+ size="sm"
307
+ disabled={
308
+ !run.workflowRunId || busy !== null || run.status !== "running"
309
+ }
310
+ onClick={() => void postAction("cancel")}
311
+ >
312
+ {busy === "cancel" ? "Cancelling..." : "Cancel"}
313
+ </Button>
314
+ <Button
315
+ variant="outline"
316
+ size="sm"
317
+ disabled={busy !== null || run.sourceType !== "flow"}
318
+ onClick={() => void postAction("retry")}
319
+ >
320
+ {busy === "retry" ? "Retrying..." : "Retry"}
321
+ </Button>
322
+ </div>
323
+ </div>
324
+
325
+ {error ? (
326
+ <div className="rounded-md border border-destructive/30 bg-destructive/5 p-2 text-sm text-destructive">
327
+ {error}
328
+ </div>
329
+ ) : null}
330
+
331
+ <div className="rounded-md bg-background p-3">
332
+ <div className="mb-2 text-xs font-medium uppercase text-muted-foreground">
333
+ Workflow stream
334
+ </div>
335
+ {streamLines.length > 0 ? (
336
+ <pre className="max-h-56 overflow-auto whitespace-pre-wrap text-xs">
337
+ {streamLines.join("\n")}
338
+ </pre>
339
+ ) : (
340
+ <p className="text-sm text-muted-foreground">
341
+ {streamingEnabled
342
+ ? "No stream updates yet. Use sendUpdate() inside Workflow steps to emit progress."
343
+ : run.status === "pending" || run.status === "running"
344
+ ? "Streaming is off. Turn it on to follow live Workflow updates."
345
+ : "No stream logs found for this completed Workflow run."}
346
+ </p>
347
+ )}
348
+ {!streamingEnabled && streamLines.length > 0 ? (
349
+ <p className="mt-2 text-xs text-muted-foreground">
350
+ Streaming is off. Showing the last loaded Workflow logs.
351
+ </p>
352
+ ) : null}
353
+ </div>
354
+ </div>
355
+ );
356
+ }
357
+
358
+ export function KhotanRunsTable({ pageSize = 10 }: { pageSize?: number } = {}) {
359
+ const [data, setData] = useState<PageResponse<RunLogItem> | null>(null);
360
+ const [offset, setOffset] = useState(0);
361
+ const [loading, setLoading] = useState(true);
362
+ const [error, setError] = useState<string | null>(null);
363
+ const [refreshKey, setRefreshKey] = useState(0);
364
+ const [expandedRunId, setExpandedRunId] = useState<string | null>(null);
365
+ const [streamingEnabled, setStreamingEnabled] = useState(false);
366
+ const [lastUpdatedAt, setLastUpdatedAt] = useState<string | null>(null);
367
+ const [streamPulse, setStreamPulse] = useState(false);
368
+
369
+ const pulseLiveIndicator = useCallback(() => {
370
+ setStreamPulse(true);
371
+ window.setTimeout(() => setStreamPulse(false), 700);
372
+ }, []);
373
+
374
+ useEffect(() => {
375
+ let cancelled = false;
376
+
377
+ async function load() {
378
+ setLoading(true);
379
+ setError(null);
380
+ try {
381
+ const res = await fetch(
382
+ `/api/khotan/runs?limit=${String(pageSize)}&offset=${String(offset)}`,
383
+ );
384
+ if (!res.ok) {
385
+ throw new Error("Failed to load runs");
386
+ }
387
+ const json = (await res.json()) as PageResponse<RunLogItem>;
388
+ if (!cancelled) {
389
+ setData(json);
390
+ setLastUpdatedAt(new Date().toISOString());
391
+ }
392
+ } catch (err) {
393
+ if (!cancelled) {
394
+ setError(err instanceof Error ? err.message : "Unknown error");
395
+ }
396
+ } finally {
397
+ if (!cancelled) {
398
+ setLoading(false);
399
+ }
400
+ }
401
+ }
402
+
403
+ void load();
404
+ return () => {
405
+ cancelled = true;
406
+ };
407
+ }, [offset, pageSize, refreshKey]);
408
+
409
+ return (
410
+ <Card>
411
+ <CardHeader className="flex flex-row items-center justify-between gap-4">
412
+ <div>
413
+ <CardTitle>Runs</CardTitle>
414
+ <p className="text-sm text-muted-foreground">
415
+ Recent flow and webhook execution history.
416
+ </p>
417
+ <p className="text-xs text-muted-foreground">
418
+ Last updated:{" "}
419
+ {lastUpdatedAt ? formatDateTime(lastUpdatedAt) : "Not loaded yet"}
420
+ </p>
421
+ </div>
422
+ <div className="flex items-center gap-3">
423
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
424
+ <span className="relative flex h-2.5 w-2.5">
425
+ {streamingEnabled && streamPulse ? (
426
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
427
+ ) : null}
428
+ <span
429
+ className={`relative inline-flex h-2.5 w-2.5 rounded-full ${
430
+ streamingEnabled ? "bg-emerald-500" : "bg-muted-foreground/40"
431
+ }`}
432
+ />
433
+ </span>
434
+ {streamingEnabled ? "Live" : "Idle"}
435
+ </div>
436
+ <Button
437
+ aria-label="Refresh runs"
438
+ title="Refresh runs"
439
+ variant="outline"
440
+ size="sm"
441
+ onClick={() => setRefreshKey((v) => v + 1)}
442
+ >
443
+ <RefreshCw aria-hidden="true" className="h-4 w-4" />
444
+ </Button>
445
+ <div className="flex items-center gap-2 text-sm">
446
+ <span>Streaming</span>
447
+ <Switch
448
+ checked={streamingEnabled}
449
+ onCheckedChange={setStreamingEnabled}
450
+ aria-label="Toggle run streaming"
451
+ />
452
+ </div>
453
+ </div>
454
+ </CardHeader>
455
+ <CardContent className="space-y-4">
456
+ {error ? (
457
+ <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
458
+ {error}
459
+ </div>
460
+ ) : null}
461
+
462
+ <Table>
463
+ <TableHeader>
464
+ <TableRow>
465
+ <TableHead>Started</TableHead>
466
+ <TableHead>Status</TableHead>
467
+ <TableHead>Source</TableHead>
468
+ <TableHead>Plug</TableHead>
469
+ <TableHead>Run Type</TableHead>
470
+ <TableHead>Counts</TableHead>
471
+ <TableHead>Workflow</TableHead>
472
+ <TableHead />
473
+ </TableRow>
474
+ </TableHeader>
475
+ <TableBody>
476
+ {loading ? (
477
+ <TableRow>
478
+ <TableCell
479
+ colSpan={8}
480
+ className="text-sm text-muted-foreground"
481
+ >
482
+ Loading runs...
483
+ </TableCell>
484
+ </TableRow>
485
+ ) : data?.items.length ? (
486
+ data.items.map((item) => (
487
+ <Fragment key={item.id}>
488
+ <TableRow>
489
+ <TableCell className="text-sm text-muted-foreground">
490
+ <div>{formatDateTime(item.startedAt)}</div>
491
+ <div className="text-xs">
492
+ {item.completedAt
493
+ ? `completed ${formatDateTime(item.completedAt)}`
494
+ : "in progress"}
495
+ </div>
496
+ </TableCell>
497
+ <TableCell>
498
+ <Badge variant={statusVariant[item.status]}>
499
+ {statusLabel[item.status]}
500
+ </Badge>
501
+ {item.error ? (
502
+ <div
503
+ className="mt-1 max-w-56 truncate text-xs text-destructive"
504
+ title={item.error}
505
+ >
506
+ {item.error}
507
+ </div>
508
+ ) : null}
509
+ </TableCell>
510
+ <TableCell className="font-medium">
511
+ {formatSource(item)}
512
+ </TableCell>
513
+ <TableCell className="text-muted-foreground">
514
+ {item.plugName ?? "-"}
515
+ </TableCell>
516
+ <TableCell className="font-mono text-xs">
517
+ {item.runType}
518
+ </TableCell>
519
+ <TableCell className="max-w-64 text-xs text-muted-foreground">
520
+ {formatCounts(item)}
521
+ </TableCell>
522
+ <TableCell className="font-mono text-xs text-muted-foreground">
523
+ {item.workflowRunId ?? "-"}
524
+ </TableCell>
525
+ <TableCell className="text-right">
526
+ <Button
527
+ variant="outline"
528
+ size="sm"
529
+ onClick={() =>
530
+ setExpandedRunId((current) =>
531
+ current === item.id ? null : item.id,
532
+ )
533
+ }
534
+ >
535
+ {expandedRunId === item.id ? "Hide" : "Details"}
536
+ </Button>
537
+ </TableCell>
538
+ </TableRow>
539
+ {expandedRunId === item.id ? (
540
+ <TableRow>
541
+ <TableCell colSpan={8}>
542
+ <RunDetails
543
+ run={item}
544
+ streamingEnabled={streamingEnabled}
545
+ onChanged={() => setRefreshKey((v) => v + 1)}
546
+ onStreamInbound={pulseLiveIndicator}
547
+ />
548
+ </TableCell>
549
+ </TableRow>
550
+ ) : null}
551
+ </Fragment>
552
+ ))
553
+ ) : (
554
+ <TableRow>
555
+ <TableCell
556
+ colSpan={8}
557
+ className="text-sm text-muted-foreground"
558
+ >
559
+ No runs recorded yet.
560
+ </TableCell>
561
+ </TableRow>
562
+ )}
563
+ </TableBody>
564
+ </Table>
565
+
566
+ <div className="flex items-center justify-between gap-3">
567
+ <p className="text-sm text-muted-foreground">
568
+ Page {Math.floor(offset / pageSize) + 1}
569
+ </p>
570
+ <div className="flex items-center gap-2">
571
+ <Button
572
+ variant="outline"
573
+ size="sm"
574
+ disabled={offset === 0 || loading}
575
+ onClick={() => setOffset(Math.max(offset - pageSize, 0))}
576
+ >
577
+ Previous
578
+ </Button>
579
+ <Button
580
+ variant="outline"
581
+ size="sm"
582
+ disabled={!data?.page.hasMore || loading}
583
+ onClick={() => setOffset(offset + pageSize)}
584
+ >
585
+ Next
586
+ </Button>
587
+ </div>
588
+ </div>
589
+ </CardContent>
590
+ </Card>
591
+ );
592
+ }