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,276 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+
10
+ // ============================================================================
11
+ // Var Panel — UI for managing plug variables
12
+ // Generated by khotan CLI · https://github.com/khotan-data
13
+ //
14
+ // This file is yours. Restyle, extend, or embed it however you like.
15
+ // It uses shadcn/ui primitives and fetches from /api/khotan/variables.
16
+ // ============================================================================
17
+
18
+ interface VarField {
19
+ key: string;
20
+ label: string;
21
+ type: "text" | "password" | "url";
22
+ secret?: boolean;
23
+ hidden?: boolean;
24
+ required?: boolean;
25
+ placeholder?: string;
26
+ defaultValue?: string;
27
+ }
28
+
29
+ interface VarPanelProps {
30
+ plugName: string;
31
+ label?: string;
32
+ basePath?: string;
33
+ }
34
+
35
+ export function VarPanel({
36
+ plugName,
37
+ label,
38
+ basePath = "/api/khotan",
39
+ }: VarPanelProps) {
40
+ const displayName = label ?? plugName;
41
+ const [fields, setFields] = useState<VarField[]>([]);
42
+ const [values, setValues] = useState<Record<string, string>>({});
43
+ const [formValues, setFormValues] = useState<Record<string, string>>({});
44
+ const [configured, setConfigured] = useState(false);
45
+ const [loading, setLoading] = useState(true);
46
+ const [saving, setSaving] = useState(false);
47
+ const [error, setError] = useState<string | null>(null);
48
+ const [success, setSuccess] = useState<string | null>(null);
49
+ const [editing, setEditing] = useState(false);
50
+ const [visibleSecrets, setVisibleSecrets] = useState<Record<string, boolean>>(
51
+ {},
52
+ );
53
+
54
+ const fetchVariables = useCallback(async () => {
55
+ try {
56
+ const res = await fetch(`${basePath}/variables/${plugName}`);
57
+ if (!res.ok) {
58
+ setFields([]);
59
+ setConfigured(false);
60
+ return;
61
+ }
62
+ const data = await res.json();
63
+ setFields((data.fields ?? []).filter((f: VarField) => !f.hidden));
64
+ setValues(data.values ?? {});
65
+ setConfigured(data.configured ?? false);
66
+ setFormValues(data.configured ? data.values : {});
67
+ setError(null);
68
+ } catch {
69
+ setError("Failed to load variables");
70
+ } finally {
71
+ setLoading(false);
72
+ }
73
+ }, [basePath, plugName]);
74
+
75
+ useEffect(() => {
76
+ fetchVariables();
77
+ }, [fetchVariables]);
78
+
79
+ async function handleSave() {
80
+ setSaving(true);
81
+ setError(null);
82
+ setSuccess(null);
83
+ try {
84
+ const res = await fetch(`${basePath}/variables/${plugName}`, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify(formValues),
88
+ });
89
+
90
+ if (!res.ok) {
91
+ const data = await res.json();
92
+ setError(data.error ?? "Failed to save variables");
93
+ return;
94
+ }
95
+
96
+ setSuccess("Variables saved");
97
+ setEditing(false);
98
+ await fetchVariables();
99
+ } catch (e) {
100
+ setError(e instanceof Error ? e.message : "Network error");
101
+ } finally {
102
+ setSaving(false);
103
+ }
104
+ }
105
+
106
+ async function handleClear() {
107
+ setSaving(true);
108
+ setError(null);
109
+ setSuccess(null);
110
+ try {
111
+ const res = await fetch(`${basePath}/variables/${plugName}`, {
112
+ method: "DELETE",
113
+ });
114
+
115
+ if (!res.ok && res.status !== 204) {
116
+ const data = await res.json();
117
+ setError(data.error ?? "Failed to clear variables");
118
+ return;
119
+ }
120
+
121
+ setSuccess("Variables cleared");
122
+ setFormValues({});
123
+ setEditing(false);
124
+ await fetchVariables();
125
+ } catch (e) {
126
+ setError(e instanceof Error ? e.message : "Network error");
127
+ } finally {
128
+ setSaving(false);
129
+ }
130
+ }
131
+
132
+ if (loading) {
133
+ return (
134
+ <Card>
135
+ <CardHeader>
136
+ <CardTitle className="text-sm font-medium capitalize">
137
+ {displayName} Variables
138
+ </CardTitle>
139
+ </CardHeader>
140
+ <CardContent>
141
+ <div className="h-4 w-2/3 animate-pulse rounded bg-muted" />
142
+ </CardContent>
143
+ </Card>
144
+ );
145
+ }
146
+
147
+ if (fields.length === 0) return null;
148
+
149
+ return (
150
+ <Card>
151
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
152
+ <CardTitle className="text-sm font-medium capitalize">
153
+ {displayName} Variables
154
+ </CardTitle>
155
+ <Badge variant={configured ? "default" : "outline"}>
156
+ {configured ? "Configured" : "Not Configured"}
157
+ </Badge>
158
+ </CardHeader>
159
+ <CardContent className="space-y-4">
160
+ {error && <p className="text-sm text-destructive">{error}</p>}
161
+ {success && <p className="text-sm text-green-600">{success}</p>}
162
+
163
+ {!editing && configured ? (
164
+ <div className="space-y-3">
165
+ <div className="text-xs text-muted-foreground space-y-1">
166
+ {fields.map((field) => (
167
+ <p key={field.key}>
168
+ <span className="font-medium">{field.label}:</span>{" "}
169
+ <code className="bg-muted px-1 py-0.5 rounded text-[11px]">
170
+ {field.secret ? "••••••••" : (values[field.key] ?? "—")}
171
+ </code>
172
+ </p>
173
+ ))}
174
+ </div>
175
+ <div className="flex gap-2">
176
+ <Button
177
+ size="sm"
178
+ variant="outline"
179
+ onClick={() => {
180
+ setEditing(true);
181
+ const prefilled: Record<string, string> = {};
182
+ for (const field of fields) {
183
+ if (!field.secret && values[field.key]) {
184
+ prefilled[field.key] = values[field.key];
185
+ }
186
+ }
187
+ setFormValues(prefilled);
188
+ setVisibleSecrets({});
189
+ setSuccess(null);
190
+ }}
191
+ >
192
+ Update
193
+ </Button>
194
+ <Button
195
+ size="sm"
196
+ variant="destructive"
197
+ onClick={handleClear}
198
+ disabled={saving}
199
+ >
200
+ {saving ? "Clearing..." : "Clear"}
201
+ </Button>
202
+ </div>
203
+ </div>
204
+ ) : (
205
+ <div className="space-y-3">
206
+ {fields.map((field) => (
207
+ <div key={field.key} className="space-y-1">
208
+ <Label
209
+ htmlFor={`var-${plugName}-${field.key}`}
210
+ className="text-xs"
211
+ >
212
+ {field.label}
213
+ {field.required !== false && (
214
+ <span className="text-destructive ml-0.5">*</span>
215
+ )}
216
+ </Label>
217
+ <div className="relative">
218
+ <Input
219
+ id={`var-${plugName}-${field.key}`}
220
+ type={
221
+ field.secret && !visibleSecrets[field.key]
222
+ ? "password"
223
+ : "text"
224
+ }
225
+ placeholder={
226
+ field.placeholder ??
227
+ (field.secret ? "Enter value..." : "")
228
+ }
229
+ value={formValues[field.key] ?? ""}
230
+ onChange={(e) =>
231
+ setFormValues((prev) => ({
232
+ ...prev,
233
+ [field.key]: e.target.value,
234
+ }))
235
+ }
236
+ />
237
+ {field.secret && (
238
+ <button
239
+ type="button"
240
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-muted-foreground hover:text-foreground"
241
+ onClick={() =>
242
+ setVisibleSecrets((prev) => ({
243
+ ...prev,
244
+ [field.key]: !prev[field.key],
245
+ }))
246
+ }
247
+ >
248
+ {visibleSecrets[field.key] ? "Hide" : "Show"}
249
+ </button>
250
+ )}
251
+ </div>
252
+ </div>
253
+ ))}
254
+ <div className="flex gap-2">
255
+ <Button size="sm" onClick={handleSave} disabled={saving}>
256
+ {saving ? "Saving..." : "Save Variables"}
257
+ </Button>
258
+ {editing && (
259
+ <Button
260
+ size="sm"
261
+ variant="outline"
262
+ onClick={() => {
263
+ setEditing(false);
264
+ setSuccess(null);
265
+ }}
266
+ >
267
+ Cancel
268
+ </Button>
269
+ )}
270
+ </div>
271
+ </div>
272
+ )}
273
+ </CardContent>
274
+ </Card>
275
+ );
276
+ }
@@ -0,0 +1,241 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ } from "@/components/ui/table";
15
+
16
+ interface WebhookEventItem {
17
+ id: string;
18
+ khotanRunId: string;
19
+ eventType: string;
20
+ payload: Record<string, unknown>;
21
+ headers: Record<string, string>;
22
+ receivedAt: string;
23
+ handlerName: string | null;
24
+ handlerType: "catch" | "pass" | null;
25
+ plugName: string | null;
26
+ workflowRunId: string | null;
27
+ runStatus:
28
+ | "pending"
29
+ | "running"
30
+ | "completed"
31
+ | "partial"
32
+ | "failed"
33
+ | "cancelled"
34
+ | null;
35
+ }
36
+
37
+ interface PageResponse<T> {
38
+ items: T[];
39
+ page: {
40
+ limit: number;
41
+ offset: number;
42
+ hasMore: boolean;
43
+ prevOffset: number;
44
+ nextOffset: number;
45
+ };
46
+ }
47
+
48
+ const statusVariant = {
49
+ pending: "outline",
50
+ running: "secondary",
51
+ completed: "default",
52
+ partial: "secondary",
53
+ failed: "destructive",
54
+ cancelled: "outline",
55
+ } as const;
56
+
57
+ function formatDateTime(value: string): string {
58
+ const date = new Date(value);
59
+ if (Number.isNaN(date.getTime())) return value;
60
+ return date
61
+ .toISOString()
62
+ .replace("T", " ")
63
+ .replace(/\.\d{3}Z$/, " UTC");
64
+ }
65
+
66
+ function formatHandler(item: WebhookEventItem): string {
67
+ if (!item.handlerName) return "Unknown";
68
+ if (!item.handlerType) return item.handlerName;
69
+ return `${item.handlerType}:${item.handlerName}`;
70
+ }
71
+
72
+ export function KhotanWebhookEventsTable({
73
+ pageSize = 10,
74
+ }: { pageSize?: number } = {}) {
75
+ const [data, setData] = useState<PageResponse<WebhookEventItem> | null>(null);
76
+ const [offset, setOffset] = useState(0);
77
+ const [loading, setLoading] = useState(true);
78
+ const [error, setError] = useState<string | null>(null);
79
+ const [refreshKey, setRefreshKey] = useState(0);
80
+
81
+ useEffect(() => {
82
+ let cancelled = false;
83
+
84
+ async function load() {
85
+ setLoading(true);
86
+ setError(null);
87
+ try {
88
+ const res = await fetch(
89
+ `/api/khotan/webhook-events?limit=${String(pageSize)}&offset=${String(offset)}`,
90
+ );
91
+ if (!res.ok) {
92
+ throw new Error("Failed to load webhook events");
93
+ }
94
+ const json = (await res.json()) as PageResponse<WebhookEventItem>;
95
+ if (!cancelled) {
96
+ setData(json);
97
+ }
98
+ } catch (err) {
99
+ if (!cancelled) {
100
+ setError(err instanceof Error ? err.message : "Unknown error");
101
+ }
102
+ } finally {
103
+ if (!cancelled) {
104
+ setLoading(false);
105
+ }
106
+ }
107
+ }
108
+
109
+ void load();
110
+ return () => {
111
+ cancelled = true;
112
+ };
113
+ }, [offset, pageSize, refreshKey]);
114
+
115
+ return (
116
+ <Card>
117
+ <CardHeader className="flex flex-row items-center justify-between gap-4">
118
+ <div>
119
+ <CardTitle>Webhook Events</CardTitle>
120
+ <p className="text-sm text-muted-foreground">
121
+ Recent inbound events captured by Khotan before workflow execution.
122
+ </p>
123
+ </div>
124
+ <Button
125
+ variant="outline"
126
+ size="sm"
127
+ onClick={() => setRefreshKey((v) => v + 1)}
128
+ >
129
+ Refresh
130
+ </Button>
131
+ </CardHeader>
132
+ <CardContent className="space-y-4">
133
+ {error ? (
134
+ <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
135
+ {error}
136
+ </div>
137
+ ) : null}
138
+
139
+ <Table>
140
+ <TableHeader>
141
+ <TableRow>
142
+ <TableHead>Received</TableHead>
143
+ <TableHead>Event</TableHead>
144
+ <TableHead>Handler</TableHead>
145
+ <TableHead>Plug</TableHead>
146
+ <TableHead>Run</TableHead>
147
+ <TableHead>Payload</TableHead>
148
+ </TableRow>
149
+ </TableHeader>
150
+ <TableBody>
151
+ {loading ? (
152
+ <TableRow>
153
+ <TableCell
154
+ colSpan={6}
155
+ className="text-sm text-muted-foreground"
156
+ >
157
+ Loading webhook events...
158
+ </TableCell>
159
+ </TableRow>
160
+ ) : data?.items.length ? (
161
+ data.items.map((item) => (
162
+ <TableRow key={item.id}>
163
+ <TableCell className="text-sm text-muted-foreground">
164
+ {formatDateTime(item.receivedAt)}
165
+ </TableCell>
166
+ <TableCell className="font-medium">
167
+ {item.eventType}
168
+ </TableCell>
169
+ <TableCell className="text-sm text-muted-foreground">
170
+ {formatHandler(item)}
171
+ </TableCell>
172
+ <TableCell className="text-muted-foreground">
173
+ {item.plugName ?? "-"}
174
+ </TableCell>
175
+ <TableCell className="space-y-1 text-xs">
176
+ <div className="font-mono text-muted-foreground">
177
+ {item.khotanRunId}
178
+ </div>
179
+ <div className="flex flex-wrap items-center gap-2">
180
+ {item.runStatus ? (
181
+ <Badge variant={statusVariant[item.runStatus]}>
182
+ {item.runStatus}
183
+ </Badge>
184
+ ) : null}
185
+ <span className="font-mono text-muted-foreground">
186
+ {item.workflowRunId ?? "-"}
187
+ </span>
188
+ </div>
189
+ </TableCell>
190
+ <TableCell className="max-w-80">
191
+ <details>
192
+ <summary className="cursor-pointer text-sm text-primary">
193
+ View payload
194
+ </summary>
195
+ <pre className="mt-2 max-h-64 overflow-auto rounded-md bg-muted p-3 text-xs">
196
+ {JSON.stringify(item.payload, null, 2)}
197
+ </pre>
198
+ </details>
199
+ </TableCell>
200
+ </TableRow>
201
+ ))
202
+ ) : (
203
+ <TableRow>
204
+ <TableCell
205
+ colSpan={6}
206
+ className="text-sm text-muted-foreground"
207
+ >
208
+ No webhook events recorded yet.
209
+ </TableCell>
210
+ </TableRow>
211
+ )}
212
+ </TableBody>
213
+ </Table>
214
+
215
+ <div className="flex items-center justify-between gap-3">
216
+ <p className="text-sm text-muted-foreground">
217
+ Page {Math.floor(offset / pageSize) + 1}
218
+ </p>
219
+ <div className="flex items-center gap-2">
220
+ <Button
221
+ variant="outline"
222
+ size="sm"
223
+ disabled={offset === 0 || loading}
224
+ onClick={() => setOffset(Math.max(offset - pageSize, 0))}
225
+ >
226
+ Previous
227
+ </Button>
228
+ <Button
229
+ variant="outline"
230
+ size="sm"
231
+ disabled={!data?.page.hasMore || loading}
232
+ onClick={() => setOffset(offset + pageSize)}
233
+ >
234
+ Next
235
+ </Button>
236
+ </div>
237
+ </div>
238
+ </CardContent>
239
+ </Card>
240
+ );
241
+ }