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,505 @@
1
+ // ============================================================================
2
+ // Schema — Drizzle table definitions for khotan plugs, flows, and runs
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // This file is yours. Edit anything — column types, defaults, indexes,
6
+ // relations. It has zero runtime dependencies on khotan-data.
7
+ //
8
+ // Re-export from your Drizzle schema barrel file:
9
+ // export * from "@/lib/khotan/schema";
10
+ // ============================================================================
11
+
12
+ import { relations } from "drizzle-orm";
13
+ import {
14
+ boolean,
15
+ index,
16
+ integer,
17
+ jsonb,
18
+ pgTable,
19
+ text,
20
+ timestamp,
21
+ unique,
22
+ } from "drizzle-orm/pg-core";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // khotan_plugs — one row per configured external service connection
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export const khotanPlugs = pgTable("khotan_plugs", {
29
+ id: text("id")
30
+ .primaryKey()
31
+ .$defaultFn(() => crypto.randomUUID()),
32
+ name: text("name").notNull().unique(),
33
+ baseUrl: text("base_url").notNull(),
34
+ authType: text("auth_type", {
35
+ enum: ["bearer", "basic", "apiKey", "custom"],
36
+ }).notNull(),
37
+ enabled: boolean("enabled").default(true).notNull(),
38
+ status: text("status", {
39
+ enum: ["connected", "error", "idle"],
40
+ })
41
+ .default("idle")
42
+ .notNull(),
43
+ statusMessage: text("status_message"),
44
+ encryptedVars: text("encrypted_vars"),
45
+ createdAt: timestamp("created_at", { withTimezone: true })
46
+ .defaultNow()
47
+ .notNull(),
48
+ updatedAt: timestamp("updated_at", { withTimezone: true })
49
+ .defaultNow()
50
+ .notNull(),
51
+ });
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // khotan_resources — one row per logical entity type (e.g. products, orders)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ export const khotanResources = pgTable("khotan_resources", {
58
+ id: text("id")
59
+ .primaryKey()
60
+ .$defaultFn(() => crypto.randomUUID()),
61
+ name: text("name").notNull().unique(),
62
+ connectField: text("connect_field").notNull(),
63
+ description: text("description"),
64
+ createdAt: timestamp("created_at", { withTimezone: true })
65
+ .defaultNow()
66
+ .notNull(),
67
+ updatedAt: timestamp("updated_at", { withTimezone: true })
68
+ .defaultNow()
69
+ .notNull(),
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // khotan_flows — one row per data flow tied to a plug
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export const khotanFlows = pgTable(
77
+ "khotan_flows",
78
+ {
79
+ id: text("id")
80
+ .primaryKey()
81
+ .$defaultFn(() => crypto.randomUUID()),
82
+ plugId: text("plug_id")
83
+ .notNull()
84
+ .references(() => khotanPlugs.id),
85
+ name: text("name").notNull(),
86
+ type: text("type", {
87
+ enum: ["inflow", "outflow", "relay", "webhook"],
88
+ }).notNull(),
89
+ enabled: boolean("enabled").default(true).notNull(),
90
+ schedule: text("schedule"),
91
+ resourceId: text("resource_id").references(() => khotanResources.id),
92
+ lastRunAt: timestamp("last_run_at", { withTimezone: true }),
93
+ lastRunStatus: text("last_run_status", {
94
+ enum: ["completed", "partial", "failed", "cancelled"],
95
+ }),
96
+ createdAt: timestamp("created_at", { withTimezone: true })
97
+ .defaultNow()
98
+ .notNull(),
99
+ updatedAt: timestamp("updated_at", { withTimezone: true })
100
+ .defaultNow()
101
+ .notNull(),
102
+ },
103
+ (table) => [
104
+ unique("khotan_flows_plug_id_name_unique").on(table.plugId, table.name),
105
+ index("khotan_flows_plug_id_idx").on(table.plugId),
106
+ index("khotan_flows_resource_id_idx").on(table.resourceId),
107
+ ],
108
+ );
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // khotan_wires — one row per webhook subscription managed by a plug
112
+ // ---------------------------------------------------------------------------
113
+
114
+ export const khotanWires = pgTable(
115
+ "khotan_wires",
116
+ {
117
+ id: text("id")
118
+ .primaryKey()
119
+ .$defaultFn(() => crypto.randomUUID()),
120
+ plugId: text("plug_id")
121
+ .notNull()
122
+ .references(() => khotanPlugs.id),
123
+ remoteId: text("remote_id").notNull(),
124
+ callbackUrl: text("callback_url").notNull(),
125
+ eventTypes: jsonb("event_types").notNull().$type<string[]>(),
126
+ status: text("status", {
127
+ enum: ["active", "disabled"],
128
+ })
129
+ .default("active")
130
+ .notNull(),
131
+ metadata: jsonb("metadata"),
132
+ createdAt: timestamp("created_at", { withTimezone: true })
133
+ .defaultNow()
134
+ .notNull(),
135
+ updatedAt: timestamp("updated_at", { withTimezone: true })
136
+ .defaultNow()
137
+ .notNull(),
138
+ },
139
+ (table) => [
140
+ index("khotan_wires_plug_id_idx").on(table.plugId),
141
+ index("khotan_wires_status_idx").on(table.status),
142
+ ],
143
+ );
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // khotan_webhook_handlers — catches and passes attached to a wire
147
+ // ---------------------------------------------------------------------------
148
+
149
+ export const khotanWebhookHandlers = pgTable(
150
+ "khotan_webhook_handlers",
151
+ {
152
+ id: text("id")
153
+ .primaryKey()
154
+ .$defaultFn(() => crypto.randomUUID()),
155
+ wireId: text("wire_id")
156
+ .notNull()
157
+ .references(() => khotanWires.id),
158
+ name: text("name").notNull(),
159
+ type: text("type", {
160
+ enum: ["catch", "pass"],
161
+ }).notNull(),
162
+ destinationPlugId: text("destination_plug_id"),
163
+ enabled: boolean("enabled").default(true).notNull(),
164
+ createdAt: timestamp("created_at", { withTimezone: true })
165
+ .defaultNow()
166
+ .notNull(),
167
+ updatedAt: timestamp("updated_at", { withTimezone: true })
168
+ .defaultNow()
169
+ .notNull(),
170
+ },
171
+ (table) => [
172
+ unique("khotan_webhook_handlers_wire_id_name_unique").on(
173
+ table.wireId,
174
+ table.name,
175
+ ),
176
+ index("khotan_webhook_handlers_wire_id_idx").on(table.wireId),
177
+ ],
178
+ );
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // khotan_webhook_events — one row per webhook event routed to a handler
182
+ // ---------------------------------------------------------------------------
183
+
184
+ export const khotanWebhookEvents = pgTable(
185
+ "khotan_webhook_events",
186
+ {
187
+ id: text("id")
188
+ .primaryKey()
189
+ .$defaultFn(() => crypto.randomUUID()),
190
+ wireId: text("wire_id")
191
+ .notNull()
192
+ .references(() => khotanWires.id),
193
+ webhookHandlerId: text("webhook_handler_id")
194
+ .notNull()
195
+ .references(() => khotanWebhookHandlers.id),
196
+ khotanRunId: text("khotan_run_id")
197
+ .notNull()
198
+ .references(() => khotanRuns.id),
199
+ eventType: text("event_type").notNull(),
200
+ payload: jsonb("payload").notNull().$type<Record<string, unknown>>(),
201
+ headers: jsonb("headers").notNull().$type<Record<string, string>>(),
202
+ receivedAt: timestamp("received_at", { withTimezone: true })
203
+ .defaultNow()
204
+ .notNull(),
205
+ },
206
+ (table) => [
207
+ index("khotan_webhook_events_wire_id_idx").on(table.wireId),
208
+ index("khotan_webhook_events_webhook_handler_id_idx").on(
209
+ table.webhookHandlerId,
210
+ ),
211
+ index("khotan_webhook_events_khotan_run_id_idx").on(table.khotanRunId),
212
+ index("khotan_webhook_events_received_at_idx").on(table.receivedAt.desc()),
213
+ ],
214
+ );
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // khotan_runs — one row per execution of a flow, wire, or webhook handler
218
+ // ---------------------------------------------------------------------------
219
+
220
+ export const khotanRuns = pgTable(
221
+ "khotan_runs",
222
+ {
223
+ id: text("id")
224
+ .primaryKey()
225
+ .$defaultFn(() => crypto.randomUUID()),
226
+ flowId: text("flow_id").references(() => khotanFlows.id),
227
+ wireId: text("wire_id").references(() => khotanWires.id),
228
+ webhookHandlerId: text("webhook_handler_id").references(
229
+ () => khotanWebhookHandlers.id,
230
+ ),
231
+ workflowRunId: text("workflow_run_id"),
232
+ runType: text("run_type", {
233
+ enum: ["full", "delta", "backfill", "reconcile", "dry-run", "webhook"],
234
+ }).notNull(),
235
+ status: text("status", {
236
+ enum: [
237
+ "pending",
238
+ "running",
239
+ "completed",
240
+ "partial",
241
+ "failed",
242
+ "cancelled",
243
+ ],
244
+ })
245
+ .default("pending")
246
+ .notNull(),
247
+ startedAt: timestamp("started_at", { withTimezone: true })
248
+ .defaultNow()
249
+ .notNull(),
250
+ completedAt: timestamp("completed_at", { withTimezone: true }),
251
+ durationMs: integer("duration_ms"),
252
+ extracted: integer("extracted").default(0).notNull(),
253
+ transformed: integer("transformed").default(0).notNull(),
254
+ created: integer("created").default(0).notNull(),
255
+ updated: integer("updated").default(0).notNull(),
256
+ deleted: integer("deleted").default(0).notNull(),
257
+ failed: integer("failed").default(0).notNull(),
258
+ error: text("error"),
259
+ metadata: jsonb("metadata"),
260
+ },
261
+ (table) => [
262
+ index("khotan_runs_flow_id_idx").on(table.flowId),
263
+ index("khotan_runs_wire_id_idx").on(table.wireId),
264
+ index("khotan_runs_webhook_handler_id_idx").on(table.webhookHandlerId),
265
+ index("khotan_runs_status_idx").on(table.status),
266
+ index("khotan_runs_flow_id_started_at_idx").on(
267
+ table.flowId,
268
+ table.startedAt.desc(),
269
+ ),
270
+ ],
271
+ );
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // khotan_mappings — one row per entity instance within a resource
275
+ // ---------------------------------------------------------------------------
276
+
277
+ export const khotanMappings = pgTable(
278
+ "khotan_mappings",
279
+ {
280
+ id: text("id")
281
+ .primaryKey()
282
+ .$defaultFn(() => crypto.randomUUID()),
283
+ resourceId: text("resource_id")
284
+ .notNull()
285
+ .references(() => khotanResources.id),
286
+ connectValue: text("connect_value").notNull(),
287
+ refs: jsonb("refs").notNull().$type<Record<string, string>>().default({}),
288
+ metadata: jsonb("metadata").$type<Record<string, unknown>>(),
289
+ createdAt: timestamp("created_at", { withTimezone: true })
290
+ .defaultNow()
291
+ .notNull(),
292
+ updatedAt: timestamp("updated_at", { withTimezone: true })
293
+ .defaultNow()
294
+ .notNull(),
295
+ },
296
+ (table) => [
297
+ unique("khotan_mappings_resource_id_connect_value_unique").on(
298
+ table.resourceId,
299
+ table.connectValue,
300
+ ),
301
+ index("khotan_mappings_resource_id_idx").on(table.resourceId),
302
+ index("khotan_mappings_refs_gin_idx").using("gin", table.refs),
303
+ ],
304
+ );
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // khotan_caches — one row per registered durable cache namespace
308
+ // ---------------------------------------------------------------------------
309
+
310
+ export const khotanCaches = pgTable(
311
+ "khotan_caches",
312
+ {
313
+ id: text("id")
314
+ .primaryKey()
315
+ .$defaultFn(() => crypto.randomUUID()),
316
+ name: text("name").notNull().unique(),
317
+ scope: jsonb("scope").$type<{
318
+ plug?: string;
319
+ resource?: string;
320
+ flow?: string;
321
+ }>(),
322
+ ttlSeconds: integer("ttl_seconds"),
323
+ createdAt: timestamp("created_at", { withTimezone: true })
324
+ .defaultNow()
325
+ .notNull(),
326
+ updatedAt: timestamp("updated_at", { withTimezone: true })
327
+ .defaultNow()
328
+ .notNull(),
329
+ },
330
+ (table) => [index("khotan_caches_name_idx").on(table.name)],
331
+ );
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // khotan_cache_entries — latest-value durable cache rows keyed by cache + key
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export const khotanCacheEntries = pgTable(
338
+ "khotan_cache_entries",
339
+ {
340
+ id: text("id")
341
+ .primaryKey()
342
+ .$defaultFn(() => crypto.randomUUID()),
343
+ cacheId: text("cache_id")
344
+ .notNull()
345
+ .references(() => khotanCaches.id),
346
+ key: text("key").notNull(),
347
+ value: jsonb("value").notNull(),
348
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
349
+ createdAt: timestamp("created_at", { withTimezone: true })
350
+ .defaultNow()
351
+ .notNull(),
352
+ updatedAt: timestamp("updated_at", { withTimezone: true })
353
+ .defaultNow()
354
+ .notNull(),
355
+ },
356
+ (table) => [
357
+ unique("khotan_cache_entries_cache_id_key_unique").on(
358
+ table.cacheId,
359
+ table.key,
360
+ ),
361
+ index("khotan_cache_entries_cache_id_idx").on(table.cacheId),
362
+ index("khotan_cache_entries_cache_id_key_idx").on(table.cacheId, table.key),
363
+ index("khotan_cache_entries_expires_at_idx").on(table.expiresAt),
364
+ ],
365
+ );
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Relations
369
+ // ---------------------------------------------------------------------------
370
+
371
+ export const khotanPlugsRelations = relations(khotanPlugs, ({ many }) => ({
372
+ flows: many(khotanFlows),
373
+ wires: many(khotanWires),
374
+ }));
375
+
376
+ export const khotanFlowsRelations = relations(khotanFlows, ({ one, many }) => ({
377
+ plug: one(khotanPlugs, {
378
+ fields: [khotanFlows.plugId],
379
+ references: [khotanPlugs.id],
380
+ }),
381
+ resource: one(khotanResources, {
382
+ fields: [khotanFlows.resourceId],
383
+ references: [khotanResources.id],
384
+ }),
385
+ runs: many(khotanRuns),
386
+ }));
387
+
388
+ export const khotanWiresRelations = relations(khotanWires, ({ one, many }) => ({
389
+ plug: one(khotanPlugs, {
390
+ fields: [khotanWires.plugId],
391
+ references: [khotanPlugs.id],
392
+ }),
393
+ webhookHandlers: many(khotanWebhookHandlers),
394
+ webhookEvents: many(khotanWebhookEvents),
395
+ runs: many(khotanRuns),
396
+ }));
397
+
398
+ export const khotanWebhookHandlersRelations = relations(
399
+ khotanWebhookHandlers,
400
+ ({ one, many }) => ({
401
+ wire: one(khotanWires, {
402
+ fields: [khotanWebhookHandlers.wireId],
403
+ references: [khotanWires.id],
404
+ }),
405
+ webhookEvents: many(khotanWebhookEvents),
406
+ runs: many(khotanRuns),
407
+ }),
408
+ );
409
+
410
+ export const khotanRunsRelations = relations(khotanRuns, ({ one, many }) => ({
411
+ flow: one(khotanFlows, {
412
+ fields: [khotanRuns.flowId],
413
+ references: [khotanFlows.id],
414
+ }),
415
+ wire: one(khotanWires, {
416
+ fields: [khotanRuns.wireId],
417
+ references: [khotanWires.id],
418
+ }),
419
+ webhookHandler: one(khotanWebhookHandlers, {
420
+ fields: [khotanRuns.webhookHandlerId],
421
+ references: [khotanWebhookHandlers.id],
422
+ }),
423
+ webhookEvents: many(khotanWebhookEvents),
424
+ }));
425
+
426
+ export const khotanWebhookEventsRelations = relations(
427
+ khotanWebhookEvents,
428
+ ({ one }) => ({
429
+ wire: one(khotanWires, {
430
+ fields: [khotanWebhookEvents.wireId],
431
+ references: [khotanWires.id],
432
+ }),
433
+ webhookHandler: one(khotanWebhookHandlers, {
434
+ fields: [khotanWebhookEvents.webhookHandlerId],
435
+ references: [khotanWebhookHandlers.id],
436
+ }),
437
+ khotanRun: one(khotanRuns, {
438
+ fields: [khotanWebhookEvents.khotanRunId],
439
+ references: [khotanRuns.id],
440
+ }),
441
+ }),
442
+ );
443
+
444
+ export const khotanResourcesRelations = relations(
445
+ khotanResources,
446
+ ({ many }) => ({
447
+ flows: many(khotanFlows),
448
+ mappings: many(khotanMappings),
449
+ }),
450
+ );
451
+
452
+ export const khotanMappingsRelations = relations(khotanMappings, ({ one }) => ({
453
+ resource: one(khotanResources, {
454
+ fields: [khotanMappings.resourceId],
455
+ references: [khotanResources.id],
456
+ }),
457
+ }));
458
+
459
+ export const khotanCachesRelations = relations(khotanCaches, ({ many }) => ({
460
+ entries: many(khotanCacheEntries),
461
+ }));
462
+
463
+ export const khotanCacheEntriesRelations = relations(
464
+ khotanCacheEntries,
465
+ ({ one }) => ({
466
+ cache: one(khotanCaches, {
467
+ fields: [khotanCacheEntries.cacheId],
468
+ references: [khotanCaches.id],
469
+ }),
470
+ }),
471
+ );
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // Type helpers
475
+ // ---------------------------------------------------------------------------
476
+
477
+ export type KhotanPlug = typeof khotanPlugs.$inferSelect;
478
+ export type NewKhotanPlug = typeof khotanPlugs.$inferInsert;
479
+
480
+ export type KhotanFlow = typeof khotanFlows.$inferSelect;
481
+ export type NewKhotanFlow = typeof khotanFlows.$inferInsert;
482
+
483
+ export type KhotanWire = typeof khotanWires.$inferSelect;
484
+ export type NewKhotanWire = typeof khotanWires.$inferInsert;
485
+
486
+ export type KhotanWebhookHandler = typeof khotanWebhookHandlers.$inferSelect;
487
+ export type NewKhotanWebhookHandler = typeof khotanWebhookHandlers.$inferInsert;
488
+
489
+ export type KhotanWebhookEvent = typeof khotanWebhookEvents.$inferSelect;
490
+ export type NewKhotanWebhookEvent = typeof khotanWebhookEvents.$inferInsert;
491
+
492
+ export type KhotanRun = typeof khotanRuns.$inferSelect;
493
+ export type NewKhotanRun = typeof khotanRuns.$inferInsert;
494
+
495
+ export type KhotanResource = typeof khotanResources.$inferSelect;
496
+ export type NewKhotanResource = typeof khotanResources.$inferInsert;
497
+
498
+ export type KhotanMapping = typeof khotanMappings.$inferSelect;
499
+ export type NewKhotanMapping = typeof khotanMappings.$inferInsert;
500
+
501
+ export type KhotanCache = typeof khotanCaches.$inferSelect;
502
+ export type NewKhotanCache = typeof khotanCaches.$inferInsert;
503
+
504
+ export type KhotanCacheEntry = typeof khotanCacheEntries.$inferSelect;
505
+ export type NewKhotanCacheEntry = typeof khotanCacheEntries.$inferInsert;
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: khotan-dashboard
3
+ description: >
4
+ Set up khotan dashboard UI — the Hub for managing plugs, flows,
5
+ variables, and webhooks, plus the Plug Debugger for testing API
6
+ requests. Use when adding a management interface, configuring plug
7
+ variables in the browser, or setting up debug pages.
8
+ ---
9
+
10
+ Set up khotan dashboard UI — the Hub for managing plugs, flows, variables, and webhooks, plus the Plug Debugger for testing API requests. Use when adding a management interface, configuring plug variables in the browser, or setting up debug pages.
11
+
12
+ ## Hub (Management Dashboard)
13
+
14
+ ```bash
15
+ npx khotan add hub --yes
16
+ npx khotan add config-page-1 --yes # Ready-made /config route
17
+ ```
18
+
19
+ The Hub scaffolds three components to `src/components/khotan/`:
20
+
21
+ | File | Purpose |
22
+ |------|---------|
23
+ | `hub.tsx` | Main `<KhotanHub />` — plug cards, flow table, enable/disable toggles |
24
+ | `var-panel.tsx` | Variables panel for configuring plug vars |
25
+ | `wire-panel.tsx` | Webhook subscription management (connect/disconnect) |
26
+
27
+ ### Rendering the Hub
28
+
29
+ ```tsx
30
+ import { KhotanHub } from "@/components/khotan/hub";
31
+
32
+ export default function ConfigPage() {
33
+ return (
34
+ <main className="container mx-auto max-w-5xl px-4 py-10">
35
+ <KhotanHub />
36
+ </main>
37
+ );
38
+ }
39
+ ```
40
+
41
+ Or use `npx khotan add config-page-1` to scaffold a `/config` page automatically.
42
+
43
+ ### Hub Features
44
+
45
+ - Lists all registered plugs with status badges (connected/error/idle)
46
+ - Click a plug to see its flows with enable/disable toggles
47
+ - VarPanel: configure plug variables (stored encrypted via `KHOTAN_SECRET`)
48
+ - WirePanel: manage webhook subscriptions (requires wires configured on plug)
49
+ - Debug button on each plug card (visible when `KHOTAN_DEBUG=1`)
50
+
51
+ ### Hub Props
52
+
53
+ ```tsx
54
+ <KhotanHub
55
+ webhookUrl="https://your-domain.com" // Base URL for wire callbacks
56
+ />
57
+ ```
58
+
59
+ ### API Endpoints Used by Hub
60
+
61
+ | Endpoint | Purpose |
62
+ |----------|---------|
63
+ | `GET /api/khotan/plugs` | List plugs with flow counts |
64
+ | `GET /api/khotan/flows` | List all flows |
65
+ | `PATCH /api/khotan/flows/:id` | Toggle flow enabled/disabled |
66
+ | `POST /api/khotan/flows/:id/runs` | Start a tracked flow run |
67
+ | `GET /api/khotan/runs/:id` | Get run detail with live Workflow status |
68
+ | `GET /api/khotan/runs/:id/stream` | Stream Workflow progress updates |
69
+ | `POST /api/khotan/runs/:id/cancel` | Cancel a running Workflow-backed run |
70
+ | `POST /api/khotan/runs/:id/retry` | Retry a flow run with the same run type |
71
+ | `PATCH /api/khotan/plugs/:id` | Toggle plug enabled/disabled |
72
+ | `GET /api/khotan/variables/:plugName` | Get var fields + masked values |
73
+ | `POST /api/khotan/variables/:plugName` | Save encrypted variables |
74
+ | `DELETE /api/khotan/variables/:plugName` | Clear variables |
75
+ | `GET /api/khotan/wires/:plugName` | Get wire status |
76
+ | `POST /api/khotan/wires/:plugName` | Create webhook subscription |
77
+ | `DELETE /api/khotan/wires/:plugName` | Remove webhook subscription |
78
+
79
+ From server code, prefer the Khotan-native starter instead of calling `workflow/api.start()` directly:
80
+
81
+ ```typescript
82
+ await khotanData.flow("products-inflow", { plugName: "shopify" }).start({
83
+ runType: "delta",
84
+ });
85
+ ```
86
+
87
+ ### Variables In Code And CLI
88
+
89
+ - Declare optional `defaultValue` on plug var fields to seed initial DB-backed values.
90
+ - Use `npx khotan plug vars <plugName>` to inspect masked values from the terminal.
91
+ - Use `npx khotan plug vars <plugName> set --json '{...}'` to update variables without opening the Hub.
92
+ - Use `npx khotan plug vars <plugName> clear` to remove all stored overrides for a plug.
93
+
94
+ ## Plug Debugger (Dev Testing UI)
95
+
96
+ ```bash
97
+ npx khotan add plug-debugger --yes
98
+ npx khotan add debug-page-1 --yes # Routes at /debug and /debug/[plugName]
99
+ ```
100
+
101
+ Requires `KHOTAN_DEBUG=1` in your environment.
102
+
103
+ ### Features
104
+
105
+ - Postman-like interface for testing plug requests
106
+ - Typed endpoint sidebar showing Zod schemas
107
+ - Path parameter interpolation
108
+ - Response validation (green/amber/red diff)
109
+ - Request history
110
+ - Auto-format JSON bodies
111
+ - Keyboard shortcut: Cmd+Enter to send
112
+
113
+ ### Debug Routes
114
+
115
+ | Route | Purpose |
116
+ |-------|---------|
117
+ | `/debug` | Index page listing all plugs |
118
+ | `/debug/[plugName]` | Per-plug debugger |
119
+
120
+ ### Debug API Endpoints
121
+
122
+ | Endpoint | Purpose |
123
+ |----------|---------|
124
+ | `GET /api/khotan/debug` | Check if debug mode is active |
125
+ | `GET /api/khotan/debug/:plugName` | Plug metadata + endpoint schemas |
126
+ | `POST /api/khotan/debug/:plugName` | Fire request through the real plug code path |
127
+
128
+ ## shadcn Dependencies
129
+
130
+ Both components require shadcn/ui. The CLI will offer to install missing components:
131
+
132
+ - **Hub**: card, badge, table, switch, button, input, label
133
+ - **Plug Debugger**: card, badge, button, input, label
134
+
135
+ Run `npx shadcn@latest init --defaults --yes` first if shadcn is not set up.
136
+
137
+ ## Skip UI
138
+
139
+ Use `--without-ui` to scaffold only the backend code without React components:
140
+
141
+ ```bash
142
+ npx khotan add hub --without-ui
143
+ npx khotan add wire --without-ui
144
+ ```