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,216 @@
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
+
8
+ // ============================================================================
9
+ // Wire Panel — UI for managing webhook subscriptions
10
+ // Generated by khotan CLI · https://github.com/khotan-data
11
+ //
12
+ // This file is yours. Restyle, extend, or embed it however you like.
13
+ // It uses shadcn/ui primitives and fetches from /api/khotan/wires.
14
+ // ============================================================================
15
+
16
+ interface WireRecord {
17
+ id: string;
18
+ plugId: string;
19
+ remoteId: string;
20
+ callbackUrl: string;
21
+ eventTypes: string[];
22
+ status: "active" | "disabled" | "pending";
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ }
26
+
27
+ interface WirePanelProps {
28
+ plugName: string;
29
+ label?: string;
30
+ basePath?: string;
31
+ webhookUrl?: string;
32
+ }
33
+
34
+ export function WirePanel({
35
+ plugName,
36
+ label,
37
+ basePath = "/api/khotan",
38
+ webhookUrl,
39
+ }: WirePanelProps) {
40
+ const displayName = label ?? plugName;
41
+ const resolvedWebhookUrl =
42
+ webhookUrl || (typeof window !== "undefined" ? window.location.origin : "");
43
+ const [wire, setWire] = useState<WireRecord | null>(null);
44
+ const [configured, setConfigured] = useState(false);
45
+ const [loading, setLoading] = useState(true);
46
+ const [acting, setActing] = useState(false);
47
+ const [error, setError] = useState<string | null>(null);
48
+
49
+ const fetchWire = useCallback(async () => {
50
+ try {
51
+ const res = await fetch(`${basePath}/wires/${plugName}`);
52
+ if (!res.ok) {
53
+ setConfigured(false);
54
+ setWire(null);
55
+ return;
56
+ }
57
+ const data = await res.json();
58
+ setConfigured(data.configured ?? false);
59
+ setWire(data.wire ?? null);
60
+ setError(null);
61
+ } catch {
62
+ setConfigured(false);
63
+ } finally {
64
+ setLoading(false);
65
+ }
66
+ }, [basePath, plugName]);
67
+
68
+ useEffect(() => {
69
+ fetchWire();
70
+ }, [fetchWire]);
71
+
72
+ async function handleCreate() {
73
+ setActing(true);
74
+ setError(null);
75
+ try {
76
+ const callbackUrl = `${resolvedWebhookUrl}/api/khotan/webhook/${plugName}`;
77
+
78
+ const res = await fetch(`${basePath}/wires/${plugName}`, {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({ callbackUrl }),
82
+ });
83
+
84
+ if (!res.ok) {
85
+ const data = await res.json();
86
+ setError(data.error ?? "Failed to create wire");
87
+ return;
88
+ }
89
+
90
+ const data = await res.json();
91
+ setWire(data.wire ?? null);
92
+ } catch (e) {
93
+ setError(e instanceof Error ? e.message : "Network error");
94
+ } finally {
95
+ setActing(false);
96
+ }
97
+ }
98
+
99
+ async function handleDelete() {
100
+ if (!wire) return;
101
+ setActing(true);
102
+ setError(null);
103
+ try {
104
+ const res = await fetch(`${basePath}/wires/${plugName}`, {
105
+ method: "DELETE",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify({ wireId: wire.id }),
108
+ });
109
+
110
+ if (!res.ok && res.status !== 204) {
111
+ const data = await res.json();
112
+ setError(data.error ?? "Failed to disconnect");
113
+ return;
114
+ }
115
+
116
+ await fetchWire();
117
+ } catch (e) {
118
+ setError(e instanceof Error ? e.message : "Network error");
119
+ } finally {
120
+ setActing(false);
121
+ }
122
+ }
123
+
124
+ if (loading) {
125
+ return (
126
+ <Card>
127
+ <CardHeader>
128
+ <CardTitle className="text-sm font-medium capitalize">
129
+ {displayName} Wire
130
+ </CardTitle>
131
+ </CardHeader>
132
+ <CardContent>
133
+ <div className="h-4 w-2/3 animate-pulse rounded bg-muted" />
134
+ </CardContent>
135
+ </Card>
136
+ );
137
+ }
138
+
139
+ if (!configured) return null;
140
+
141
+ const isActive = wire?.status === "active";
142
+ const isPending = wire?.status === "pending";
143
+
144
+ return (
145
+ <Card>
146
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
147
+ <CardTitle className="text-sm font-medium capitalize">
148
+ {displayName} Wire
149
+ </CardTitle>
150
+ <Badge
151
+ variant={isActive ? "default" : isPending ? "outline" : "secondary"}
152
+ >
153
+ {isActive
154
+ ? "Connected"
155
+ : isPending
156
+ ? "Not Connected"
157
+ : "Disconnected"}
158
+ </Badge>
159
+ </CardHeader>
160
+ <CardContent className="space-y-3">
161
+ {error && <p className="text-sm text-destructive">{error}</p>}
162
+
163
+ {isActive ? (
164
+ <div className="space-y-2">
165
+ <div className="text-xs text-muted-foreground space-y-1">
166
+ <p>
167
+ <span className="font-medium">Callback:</span>{" "}
168
+ <code className="bg-muted px-1 py-0.5 rounded text-[11px]">
169
+ {wire!.callbackUrl}
170
+ </code>
171
+ </p>
172
+ <p>
173
+ <span className="font-medium">Remote ID:</span>{" "}
174
+ <code className="bg-muted px-1 py-0.5 rounded text-[11px]">
175
+ {wire!.remoteId}
176
+ </code>
177
+ </p>
178
+ <p>
179
+ <span className="font-medium">Events:</span>{" "}
180
+ {wire!.eventTypes.join(", ")}
181
+ </p>
182
+ <p>
183
+ <span className="font-medium">Created:</span>{" "}
184
+ {new Date(wire!.createdAt).toLocaleString()}
185
+ </p>
186
+ </div>
187
+ <Button
188
+ variant="destructive"
189
+ size="sm"
190
+ onClick={handleDelete}
191
+ disabled={acting}
192
+ >
193
+ {acting ? "Disconnecting..." : "Disconnect"}
194
+ </Button>
195
+ </div>
196
+ ) : (
197
+ <div className="space-y-2">
198
+ <p className="text-xs text-muted-foreground">
199
+ No active webhook subscription. Click below to register one with{" "}
200
+ <span className="capitalize">{displayName}</span>.
201
+ </p>
202
+ <p className="text-xs text-muted-foreground">
203
+ <span className="font-medium">Webhook URL:</span>{" "}
204
+ <code className="bg-muted px-1 py-0.5 rounded text-[11px]">
205
+ {resolvedWebhookUrl}/api/khotan/webhook/{plugName}
206
+ </code>
207
+ </p>
208
+ <Button size="sm" onClick={handleCreate} disabled={acting}>
209
+ {acting ? "Connecting..." : "Connect Webhook"}
210
+ </Button>
211
+ </div>
212
+ )}
213
+ </CardContent>
214
+ </Card>
215
+ );
216
+ }
@@ -0,0 +1,155 @@
1
+ // ============================================================================
2
+ // Wire — webhook subscription lifecycle hooks
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // This file defines the wire() builder and types. Create per-service wire
6
+ // files (e.g. pollinate-wire.ts) using this builder to handle subscribe,
7
+ // unsubscribe, and verification logic for each external service.
8
+ //
9
+ // Wire hooks receive a "bound plug" — the plug with vars and auth
10
+ // auto-injected by the factory. Just call ctx.plug.post/delete/etc.
11
+ // ============================================================================
12
+
13
+ import type { Plug } from "../plugs/plug";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Bound Plug — plug with vars auto-injected by the factory
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface BoundPlug {
20
+ readonly name: string;
21
+ readonly baseUrl: string;
22
+ get<T>(
23
+ path: string,
24
+ options?: {
25
+ params?: Record<string, unknown>;
26
+ headers?: Record<string, string>;
27
+ },
28
+ ): Promise<T>;
29
+ post<T>(
30
+ path: string,
31
+ options?: { body?: unknown; headers?: Record<string, string> },
32
+ ): Promise<T>;
33
+ put<T>(
34
+ path: string,
35
+ options?: { body?: unknown; headers?: Record<string, string> },
36
+ ): Promise<T>;
37
+ patch<T>(
38
+ path: string,
39
+ options?: { body?: unknown; headers?: Record<string, string> },
40
+ ): Promise<T>;
41
+ delete<T>(
42
+ path: string,
43
+ options?: { headers?: Record<string, string> },
44
+ ): Promise<T>;
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Hook Contexts
49
+ // ---------------------------------------------------------------------------
50
+
51
+ export interface WireSubscribeContext {
52
+ /** Plug instance with vars and auth auto-injected */
53
+ plug: BoundPlug;
54
+ /** The callback URL to register with the external service */
55
+ callbackUrl: string;
56
+ /** Event types this wire subscribes to */
57
+ events: string[];
58
+ /** Previously stored wire-specific vars (empty on first subscribe) */
59
+ wireVars: Record<string, string>;
60
+ /** Persist wire-specific vars (webhook secrets, tokens, etc.) */
61
+ setWireVars(updates: Record<string, string>): Promise<void>;
62
+ }
63
+
64
+ export interface WireUnsubscribeContext {
65
+ /** Plug instance with vars and auth auto-injected */
66
+ plug: BoundPlug;
67
+ /** The remote subscription ID returned from onSubscribe */
68
+ remoteId: string;
69
+ /** Wire-specific vars stored during subscribe */
70
+ wireVars: Record<string, string>;
71
+ /** Persist wire-specific var updates */
72
+ setWireVars(updates: Record<string, string>): Promise<void>;
73
+ }
74
+
75
+ export interface WireVerifyContext {
76
+ /** Incoming request headers as plain key-value pairs */
77
+ headers: Record<string, string>;
78
+ /** Raw request body as text (for signature verification) */
79
+ body: string;
80
+ /** Wire-specific vars (e.g. webhook signing secret) */
81
+ wireVars: Record<string, string>;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Wire Config
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export interface WireConfig {
89
+ /** Event types this wire subscribes to */
90
+ events: string[];
91
+
92
+ /**
93
+ * Called when the wire is being connected.
94
+ * Make the API call to register the webhook subscription and return the remoteId.
95
+ */
96
+ onSubscribe(ctx: WireSubscribeContext): Promise<{ remoteId: string }>;
97
+
98
+ /**
99
+ * Called when the wire is being disconnected.
100
+ * Make the API call to remove the webhook subscription.
101
+ */
102
+ onUnsubscribe(ctx: WireUnsubscribeContext): Promise<void>;
103
+
104
+ /**
105
+ * Optional: verify incoming webhook signatures.
106
+ * Return true if the signature is valid.
107
+ */
108
+ onVerify?(ctx: WireVerifyContext): Promise<boolean>;
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Builder
113
+ // ---------------------------------------------------------------------------
114
+
115
+ export function wire(config: WireConfig): WireConfig {
116
+ return config;
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Usage Example (create a file like wires/stripe-wire.ts)
121
+ // ---------------------------------------------------------------------------
122
+ //
123
+ // import { wire } from "./wire";
124
+ //
125
+ // export const stripeWire = wire({
126
+ // events: ["invoice.paid", "invoice.payment_failed"],
127
+ //
128
+ // async onSubscribe(ctx) {
129
+ // const res = await ctx.plug.post<{ id: string; secret: string }>(
130
+ // "/webhook_endpoints",
131
+ // {
132
+ // body: {
133
+ // url: ctx.callbackUrl,
134
+ // enabled_events: ctx.events,
135
+ // },
136
+ // },
137
+ // );
138
+ //
139
+ // // Store the webhook signing secret for verification
140
+ // await ctx.setWireVars({ webhookSecret: res.secret });
141
+ //
142
+ // return { remoteId: res.id };
143
+ // },
144
+ //
145
+ // async onUnsubscribe(ctx) {
146
+ // await ctx.plug.delete(`/webhook_endpoints/${ctx.remoteId}`);
147
+ // },
148
+ //
149
+ // async onVerify(ctx) {
150
+ // const signature = ctx.headers["stripe-signature"];
151
+ // if (!signature) return false;
152
+ // // Verify using ctx.wireVars.webhookSecret and ctx.body (raw text)
153
+ // return true;
154
+ // },
155
+ // });
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "khotan-data",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "Data primitives for TypeScript — ETL pipelines, transforms, and Drizzle Postgres integration.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "khotan": "./dist/cli.js"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "import": {
@@ -46,10 +49,32 @@
46
49
  "types": "./dist/drizzle.d.cts",
47
50
  "default": "./dist/drizzle.cjs"
48
51
  }
52
+ },
53
+ "./factory": {
54
+ "import": {
55
+ "types": "./dist/factory.d.ts",
56
+ "default": "./dist/factory.js"
57
+ },
58
+ "require": {
59
+ "types": "./dist/factory.d.cts",
60
+ "default": "./dist/factory.cjs"
61
+ }
62
+ },
63
+ "./plug": {
64
+ "import": {
65
+ "types": "./dist/plug-client.d.ts",
66
+ "default": "./dist/plug-client.js"
67
+ },
68
+ "require": {
69
+ "types": "./dist/plug-client.d.cts",
70
+ "default": "./dist/plug-client.cjs"
71
+ }
49
72
  }
50
73
  },
51
74
  "files": [
52
75
  "dist",
76
+ "dist/templates",
77
+ "AGENTS.md",
53
78
  "README.md",
54
79
  "LICENSE"
55
80
  ],
@@ -66,7 +91,7 @@
66
91
  "format:check": "prettier --check .",
67
92
  "typecheck": "tsc --noEmit",
68
93
  "check": "npm run typecheck && npm run lint && npm run format:check && npm run test",
69
- "prepublishOnly": "npm run check && npm run build",
94
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build",
70
95
  "changeset": "changeset",
71
96
  "release": "npm run build && changeset publish"
72
97
  },
@@ -92,11 +117,22 @@
92
117
  "node": ">=18"
93
118
  },
94
119
  "peerDependencies": {
95
- "drizzle-orm": ">=0.35.0"
120
+ "drizzle-orm": ">=0.35.0",
121
+ "workflow": ">=0.1.0",
122
+ "zod": ">=3.22.0"
123
+ },
124
+ "peerDependenciesMeta": {
125
+ "workflow": {
126
+ "optional": true
127
+ },
128
+ "zod": {
129
+ "optional": true
130
+ }
96
131
  },
97
132
  "devDependencies": {
98
133
  "@changesets/cli": "^2.29.4",
99
134
  "@eslint/js": "^9.28.0",
135
+ "@types/prompts": "^2.4.9",
100
136
  "@vitest/coverage-v8": "^4.1.7",
101
137
  "drizzle-orm": "^0.45.2",
102
138
  "eslint": "^9.28.0",
@@ -104,7 +140,12 @@
104
140
  "tsup": "^8.5.1",
105
141
  "typescript": "^6.0.3",
106
142
  "typescript-eslint": "^8.34.0",
107
- "vitest": "^4.1.7"
143
+ "vitest": "^4.1.7",
144
+ "zod": "^3.25.76"
108
145
  },
109
- "packageManager": "npm@11.12.1"
146
+ "packageManager": "npm@11.12.1",
147
+ "dependencies": {
148
+ "commander": "^14.0.3",
149
+ "prompts": "^2.4.2"
150
+ }
110
151
  }