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,99 @@
1
+ 'use strict';
2
+
3
+ // src/plug-client.ts
4
+ function defineContract(contract) {
5
+ return contract;
6
+ }
7
+ function isPlugError(err) {
8
+ return err instanceof Error && "status" in err && typeof err.status === "number" && "body" in err;
9
+ }
10
+ function parseBody(body) {
11
+ if (typeof body === "string") {
12
+ try {
13
+ return JSON.parse(body);
14
+ } catch {
15
+ return body;
16
+ }
17
+ }
18
+ return body;
19
+ }
20
+ function interpolatePath(path, params) {
21
+ return path.replace(/:([^/]+)/g, (_, key) => {
22
+ const value = params[key];
23
+ if (value === void 0) {
24
+ throw new Error(`Missing path parameter: "${key}"`);
25
+ }
26
+ return encodeURIComponent(value);
27
+ });
28
+ }
29
+ function createPlugClient(contract, plug, options) {
30
+ const globalValidateResponse = options?.validateResponse ?? true;
31
+ function buildClient(router) {
32
+ const client = {};
33
+ for (const [key, value] of Object.entries(router)) {
34
+ const route = value;
35
+ if ("method" in route && "path" in route) {
36
+ client[key] = async (input) => {
37
+ const def = route;
38
+ const params = input?.["params"] ?? {};
39
+ const query = input?.["query"];
40
+ const body = input?.["body"];
41
+ const headers = input?.["headers"];
42
+ const shouldValidateResponse = input?.["validateResponse"] ?? globalValidateResponse;
43
+ if (def.pathParams) {
44
+ def.pathParams.parse(params);
45
+ }
46
+ if (def.query && query !== void 0) {
47
+ def.query.parse(query);
48
+ }
49
+ if (def.body && body !== void 0) {
50
+ def.body.parse(body);
51
+ }
52
+ const interpolatedPath = interpolatePath(def.path, params);
53
+ const responses = def.responses;
54
+ try {
55
+ const responseBody = await plug.request(
56
+ def.method,
57
+ interpolatedPath,
58
+ {
59
+ ...query !== void 0 ? { params: query } : {},
60
+ ...body !== void 0 ? { body } : {},
61
+ ...headers !== void 0 ? { headers } : {}
62
+ }
63
+ );
64
+ const status = 200;
65
+ if (!responses[status]) {
66
+ return { status, body: responseBody };
67
+ }
68
+ if (shouldValidateResponse) {
69
+ const validated = responses[status].parse(responseBody);
70
+ return { status, body: validated };
71
+ }
72
+ return { status, body: responseBody };
73
+ } catch (err) {
74
+ if (!isPlugError(err)) throw err;
75
+ const status = err.status;
76
+ if (responses[status]) {
77
+ const parsed = parseBody(err.body);
78
+ if (shouldValidateResponse) {
79
+ const validated = responses[status].parse(parsed);
80
+ return { status, body: validated };
81
+ }
82
+ return { status, body: parsed };
83
+ }
84
+ throw err;
85
+ }
86
+ };
87
+ } else {
88
+ client[key] = buildClient(route);
89
+ }
90
+ }
91
+ return client;
92
+ }
93
+ return buildClient(contract);
94
+ }
95
+
96
+ exports.createPlugClient = createPlugClient;
97
+ exports.defineContract = defineContract;
98
+ //# sourceMappingURL=plug-client.cjs.map
99
+ //# sourceMappingURL=plug-client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plug-client.ts"],"names":[],"mappings":"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","file":"plug-client.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// Schema interface — works with zod v3, v4, or any validator with .parse()\n// ---------------------------------------------------------------------------\n\nexport interface Schema<TOutput = unknown, TInput = unknown> {\n parse(data: unknown): TOutput;\n _input?: TInput;\n _output?: TOutput;\n}\n\n// ---------------------------------------------------------------------------\n// Contract types\n// ---------------------------------------------------------------------------\n\nexport interface RouteDefinition {\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n path: string;\n query?: Schema;\n body?: Schema;\n pathParams?: Schema;\n responses: Record<number, Schema>;\n}\n\nexport interface ContractRouter {\n [key: string]: RouteDefinition | ContractRouter;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level utilities\n// ---------------------------------------------------------------------------\n\ntype SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;\ntype SchemaOutput<T> = T extends Schema<infer O> ? O : never;\n\ntype PathParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Record<Param | keyof PathParams<Rest>, string>\n : T extends `${string}:${infer Param}`\n ? Record<Param, string>\n : never;\n\ntype HasKeys<T> = keyof T extends never ? false : true;\n\ntype EndpointInput<TRoute extends RouteDefinition> = (HasKeys<\n PathParams<TRoute[\"path\"]>\n> extends true\n ? { params: PathParams<TRoute[\"path\"]> }\n : { params?: never }) &\n (TRoute extends { query: Schema }\n ? { query: SchemaInput<TRoute[\"query\"]> }\n : { query?: never }) &\n (TRoute extends { body: Schema }\n ? { body: SchemaInput<TRoute[\"body\"]> }\n : { body?: never }) & {\n headers?: Record<string, string>;\n validateResponse?: boolean;\n };\n\ntype ResponseSchemas<TRoute extends RouteDefinition> = {\n [K in keyof TRoute[\"responses\"]]: K extends number\n ? { status: K; body: SchemaOutput<TRoute[\"responses\"][K]> }\n : never;\n}[keyof TRoute[\"responses\"]];\n\ntype PlugClient<T extends ContractRouter> = {\n [K in keyof T]: T[K] extends RouteDefinition\n ? HasKeys<PathParams<T[K][\"path\"]>> extends true\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { query: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { body: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends ContractRouter\n ? PlugClient<T[K]>\n : never;\n};\n\n// ---------------------------------------------------------------------------\n// defineContract — type-narrowing identity function\n// ---------------------------------------------------------------------------\n\nexport function defineContract<const T extends ContractRouter>(contract: T): T {\n return contract;\n}\n\n// ---------------------------------------------------------------------------\n// PlugLike interface\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal interface expected from a Plug instance.\n * Matches the scaffolded Plug class's public API.\n */\nexport interface PlugLike {\n request<T>(\n method: string,\n path: string,\n options?: {\n params?: Record<string, unknown>;\n body?: unknown;\n headers?: Record<string, string>;\n },\n ): Promise<T>;\n}\n\nexport interface PlugClientOptions {\n validateResponse?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// PlugError detection — works without importing the user's PlugError class\n// ---------------------------------------------------------------------------\n\ninterface PlugErrorLike {\n name: string;\n status: number;\n body: unknown;\n}\n\nfunction isPlugError(err: unknown): err is PlugErrorLike {\n return (\n err instanceof Error &&\n \"status\" in err &&\n typeof (err as unknown as PlugErrorLike).status === \"number\" &&\n \"body\" in err\n );\n}\n\nfunction parseBody(body: unknown): unknown {\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body);\n } catch {\n return body;\n }\n }\n return body;\n}\n\n// ---------------------------------------------------------------------------\n// Path interpolation\n// ---------------------------------------------------------------------------\n\nfunction interpolatePath(path: string, params: Record<string, string>): string {\n return path.replace(/:([^/]+)/g, (_, key: string) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: \"${key}\"`);\n }\n return encodeURIComponent(value);\n });\n}\n\n// ---------------------------------------------------------------------------\n// createPlugClient\n// ---------------------------------------------------------------------------\n\nexport function createPlugClient<T extends ContractRouter>(\n contract: T,\n plug: PlugLike,\n options?: PlugClientOptions,\n): PlugClient<T> {\n const globalValidateResponse = options?.validateResponse ?? true;\n\n function buildClient(router: ContractRouter): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(router)) {\n const route = value;\n\n if (\"method\" in route && \"path\" in route) {\n client[key] = async (input?: Record<string, unknown>) => {\n const def = route as RouteDefinition;\n const params = (input?.[\"params\"] ?? {}) as Record<string, string>;\n const query = input?.[\"query\"] as Record<string, unknown> | undefined;\n const body = input?.[\"body\"];\n const headers = input?.[\"headers\"] as\n | Record<string, string>\n | undefined;\n const shouldValidateResponse =\n (input?.[\"validateResponse\"] as boolean | undefined) ??\n globalValidateResponse;\n\n if (def.pathParams) {\n def.pathParams.parse(params);\n }\n if (def.query && query !== undefined) {\n def.query.parse(query);\n }\n if (def.body && body !== undefined) {\n def.body.parse(body);\n }\n\n const interpolatedPath = interpolatePath(def.path, params);\n const responses = def.responses;\n\n try {\n const responseBody = await plug.request<unknown>(\n def.method,\n interpolatedPath,\n {\n ...(query !== undefined ? { params: query } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(headers !== undefined ? { headers } : {}),\n },\n );\n\n const status = 200;\n if (!responses[status]) {\n return { status, body: responseBody };\n }\n\n if (shouldValidateResponse) {\n const validated = responses[status].parse(responseBody);\n return { status, body: validated };\n }\n\n return { status, body: responseBody };\n } catch (err) {\n if (!isPlugError(err)) throw err;\n\n const status = err.status;\n if (responses[status]) {\n const parsed = parseBody(err.body);\n if (shouldValidateResponse) {\n const validated = responses[status].parse(parsed);\n return { status, body: validated };\n }\n return { status, body: parsed };\n }\n\n throw err;\n }\n };\n } else {\n client[key] = buildClient(route);\n }\n }\n\n return client;\n }\n\n return buildClient(contract) as PlugClient<T>;\n}\n"]}
@@ -0,0 +1,71 @@
1
+ interface Schema<TOutput = unknown, TInput = unknown> {
2
+ parse(data: unknown): TOutput;
3
+ _input?: TInput;
4
+ _output?: TOutput;
5
+ }
6
+ interface RouteDefinition {
7
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
8
+ path: string;
9
+ query?: Schema;
10
+ body?: Schema;
11
+ pathParams?: Schema;
12
+ responses: Record<number, Schema>;
13
+ }
14
+ interface ContractRouter {
15
+ [key: string]: RouteDefinition | ContractRouter;
16
+ }
17
+ type SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;
18
+ type SchemaOutput<T> = T extends Schema<infer O> ? O : never;
19
+ type PathParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Record<Param | keyof PathParams<Rest>, string> : T extends `${string}:${infer Param}` ? Record<Param, string> : never;
20
+ type HasKeys<T> = keyof T extends never ? false : true;
21
+ type EndpointInput<TRoute extends RouteDefinition> = (HasKeys<PathParams<TRoute["path"]>> extends true ? {
22
+ params: PathParams<TRoute["path"]>;
23
+ } : {
24
+ params?: never;
25
+ }) & (TRoute extends {
26
+ query: Schema;
27
+ } ? {
28
+ query: SchemaInput<TRoute["query"]>;
29
+ } : {
30
+ query?: never;
31
+ }) & (TRoute extends {
32
+ body: Schema;
33
+ } ? {
34
+ body: SchemaInput<TRoute["body"]>;
35
+ } : {
36
+ body?: never;
37
+ }) & {
38
+ headers?: Record<string, string>;
39
+ validateResponse?: boolean;
40
+ };
41
+ type ResponseSchemas<TRoute extends RouteDefinition> = {
42
+ [K in keyof TRoute["responses"]]: K extends number ? {
43
+ status: K;
44
+ body: SchemaOutput<TRoute["responses"][K]>;
45
+ } : never;
46
+ }[keyof TRoute["responses"]];
47
+ type PlugClient<T extends ContractRouter> = {
48
+ [K in keyof T]: T[K] extends RouteDefinition ? HasKeys<PathParams<T[K]["path"]>> extends true ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
49
+ query: Schema;
50
+ } ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
51
+ body: Schema;
52
+ } ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends ContractRouter ? PlugClient<T[K]> : never;
53
+ };
54
+ declare function defineContract<const T extends ContractRouter>(contract: T): T;
55
+ /**
56
+ * Minimal interface expected from a Plug instance.
57
+ * Matches the scaffolded Plug class's public API.
58
+ */
59
+ interface PlugLike {
60
+ request<T>(method: string, path: string, options?: {
61
+ params?: Record<string, unknown>;
62
+ body?: unknown;
63
+ headers?: Record<string, string>;
64
+ }): Promise<T>;
65
+ }
66
+ interface PlugClientOptions {
67
+ validateResponse?: boolean;
68
+ }
69
+ declare function createPlugClient<T extends ContractRouter>(contract: T, plug: PlugLike, options?: PlugClientOptions): PlugClient<T>;
70
+
71
+ export { type ContractRouter, type PlugClientOptions, type PlugLike, type RouteDefinition, type Schema, createPlugClient, defineContract };
@@ -0,0 +1,71 @@
1
+ interface Schema<TOutput = unknown, TInput = unknown> {
2
+ parse(data: unknown): TOutput;
3
+ _input?: TInput;
4
+ _output?: TOutput;
5
+ }
6
+ interface RouteDefinition {
7
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
8
+ path: string;
9
+ query?: Schema;
10
+ body?: Schema;
11
+ pathParams?: Schema;
12
+ responses: Record<number, Schema>;
13
+ }
14
+ interface ContractRouter {
15
+ [key: string]: RouteDefinition | ContractRouter;
16
+ }
17
+ type SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;
18
+ type SchemaOutput<T> = T extends Schema<infer O> ? O : never;
19
+ type PathParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Record<Param | keyof PathParams<Rest>, string> : T extends `${string}:${infer Param}` ? Record<Param, string> : never;
20
+ type HasKeys<T> = keyof T extends never ? false : true;
21
+ type EndpointInput<TRoute extends RouteDefinition> = (HasKeys<PathParams<TRoute["path"]>> extends true ? {
22
+ params: PathParams<TRoute["path"]>;
23
+ } : {
24
+ params?: never;
25
+ }) & (TRoute extends {
26
+ query: Schema;
27
+ } ? {
28
+ query: SchemaInput<TRoute["query"]>;
29
+ } : {
30
+ query?: never;
31
+ }) & (TRoute extends {
32
+ body: Schema;
33
+ } ? {
34
+ body: SchemaInput<TRoute["body"]>;
35
+ } : {
36
+ body?: never;
37
+ }) & {
38
+ headers?: Record<string, string>;
39
+ validateResponse?: boolean;
40
+ };
41
+ type ResponseSchemas<TRoute extends RouteDefinition> = {
42
+ [K in keyof TRoute["responses"]]: K extends number ? {
43
+ status: K;
44
+ body: SchemaOutput<TRoute["responses"][K]>;
45
+ } : never;
46
+ }[keyof TRoute["responses"]];
47
+ type PlugClient<T extends ContractRouter> = {
48
+ [K in keyof T]: T[K] extends RouteDefinition ? HasKeys<PathParams<T[K]["path"]>> extends true ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
49
+ query: Schema;
50
+ } ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends {
51
+ body: Schema;
52
+ } ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>> : T[K] extends ContractRouter ? PlugClient<T[K]> : never;
53
+ };
54
+ declare function defineContract<const T extends ContractRouter>(contract: T): T;
55
+ /**
56
+ * Minimal interface expected from a Plug instance.
57
+ * Matches the scaffolded Plug class's public API.
58
+ */
59
+ interface PlugLike {
60
+ request<T>(method: string, path: string, options?: {
61
+ params?: Record<string, unknown>;
62
+ body?: unknown;
63
+ headers?: Record<string, string>;
64
+ }): Promise<T>;
65
+ }
66
+ interface PlugClientOptions {
67
+ validateResponse?: boolean;
68
+ }
69
+ declare function createPlugClient<T extends ContractRouter>(contract: T, plug: PlugLike, options?: PlugClientOptions): PlugClient<T>;
70
+
71
+ export { type ContractRouter, type PlugClientOptions, type PlugLike, type RouteDefinition, type Schema, createPlugClient, defineContract };
@@ -0,0 +1,96 @@
1
+ // src/plug-client.ts
2
+ function defineContract(contract) {
3
+ return contract;
4
+ }
5
+ function isPlugError(err) {
6
+ return err instanceof Error && "status" in err && typeof err.status === "number" && "body" in err;
7
+ }
8
+ function parseBody(body) {
9
+ if (typeof body === "string") {
10
+ try {
11
+ return JSON.parse(body);
12
+ } catch {
13
+ return body;
14
+ }
15
+ }
16
+ return body;
17
+ }
18
+ function interpolatePath(path, params) {
19
+ return path.replace(/:([^/]+)/g, (_, key) => {
20
+ const value = params[key];
21
+ if (value === void 0) {
22
+ throw new Error(`Missing path parameter: "${key}"`);
23
+ }
24
+ return encodeURIComponent(value);
25
+ });
26
+ }
27
+ function createPlugClient(contract, plug, options) {
28
+ const globalValidateResponse = options?.validateResponse ?? true;
29
+ function buildClient(router) {
30
+ const client = {};
31
+ for (const [key, value] of Object.entries(router)) {
32
+ const route = value;
33
+ if ("method" in route && "path" in route) {
34
+ client[key] = async (input) => {
35
+ const def = route;
36
+ const params = input?.["params"] ?? {};
37
+ const query = input?.["query"];
38
+ const body = input?.["body"];
39
+ const headers = input?.["headers"];
40
+ const shouldValidateResponse = input?.["validateResponse"] ?? globalValidateResponse;
41
+ if (def.pathParams) {
42
+ def.pathParams.parse(params);
43
+ }
44
+ if (def.query && query !== void 0) {
45
+ def.query.parse(query);
46
+ }
47
+ if (def.body && body !== void 0) {
48
+ def.body.parse(body);
49
+ }
50
+ const interpolatedPath = interpolatePath(def.path, params);
51
+ const responses = def.responses;
52
+ try {
53
+ const responseBody = await plug.request(
54
+ def.method,
55
+ interpolatedPath,
56
+ {
57
+ ...query !== void 0 ? { params: query } : {},
58
+ ...body !== void 0 ? { body } : {},
59
+ ...headers !== void 0 ? { headers } : {}
60
+ }
61
+ );
62
+ const status = 200;
63
+ if (!responses[status]) {
64
+ return { status, body: responseBody };
65
+ }
66
+ if (shouldValidateResponse) {
67
+ const validated = responses[status].parse(responseBody);
68
+ return { status, body: validated };
69
+ }
70
+ return { status, body: responseBody };
71
+ } catch (err) {
72
+ if (!isPlugError(err)) throw err;
73
+ const status = err.status;
74
+ if (responses[status]) {
75
+ const parsed = parseBody(err.body);
76
+ if (shouldValidateResponse) {
77
+ const validated = responses[status].parse(parsed);
78
+ return { status, body: validated };
79
+ }
80
+ return { status, body: parsed };
81
+ }
82
+ throw err;
83
+ }
84
+ };
85
+ } else {
86
+ client[key] = buildClient(route);
87
+ }
88
+ }
89
+ return client;
90
+ }
91
+ return buildClient(contract);
92
+ }
93
+
94
+ export { createPlugClient, defineContract };
95
+ //# sourceMappingURL=plug-client.js.map
96
+ //# sourceMappingURL=plug-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plug-client.ts"],"names":[],"mappings":"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","file":"plug-client.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Schema interface — works with zod v3, v4, or any validator with .parse()\n// ---------------------------------------------------------------------------\n\nexport interface Schema<TOutput = unknown, TInput = unknown> {\n parse(data: unknown): TOutput;\n _input?: TInput;\n _output?: TOutput;\n}\n\n// ---------------------------------------------------------------------------\n// Contract types\n// ---------------------------------------------------------------------------\n\nexport interface RouteDefinition {\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n path: string;\n query?: Schema;\n body?: Schema;\n pathParams?: Schema;\n responses: Record<number, Schema>;\n}\n\nexport interface ContractRouter {\n [key: string]: RouteDefinition | ContractRouter;\n}\n\n// ---------------------------------------------------------------------------\n// Type-level utilities\n// ---------------------------------------------------------------------------\n\ntype SchemaInput<T> = T extends Schema<unknown, infer I> ? I : never;\ntype SchemaOutput<T> = T extends Schema<infer O> ? O : never;\n\ntype PathParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Record<Param | keyof PathParams<Rest>, string>\n : T extends `${string}:${infer Param}`\n ? Record<Param, string>\n : never;\n\ntype HasKeys<T> = keyof T extends never ? false : true;\n\ntype EndpointInput<TRoute extends RouteDefinition> = (HasKeys<\n PathParams<TRoute[\"path\"]>\n> extends true\n ? { params: PathParams<TRoute[\"path\"]> }\n : { params?: never }) &\n (TRoute extends { query: Schema }\n ? { query: SchemaInput<TRoute[\"query\"]> }\n : { query?: never }) &\n (TRoute extends { body: Schema }\n ? { body: SchemaInput<TRoute[\"body\"]> }\n : { body?: never }) & {\n headers?: Record<string, string>;\n validateResponse?: boolean;\n };\n\ntype ResponseSchemas<TRoute extends RouteDefinition> = {\n [K in keyof TRoute[\"responses\"]]: K extends number\n ? { status: K; body: SchemaOutput<TRoute[\"responses\"][K]> }\n : never;\n}[keyof TRoute[\"responses\"]];\n\ntype PlugClient<T extends ContractRouter> = {\n [K in keyof T]: T[K] extends RouteDefinition\n ? HasKeys<PathParams<T[K][\"path\"]>> extends true\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { query: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends { body: Schema }\n ? (input: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : (input?: EndpointInput<T[K]>) => Promise<ResponseSchemas<T[K]>>\n : T[K] extends ContractRouter\n ? PlugClient<T[K]>\n : never;\n};\n\n// ---------------------------------------------------------------------------\n// defineContract — type-narrowing identity function\n// ---------------------------------------------------------------------------\n\nexport function defineContract<const T extends ContractRouter>(contract: T): T {\n return contract;\n}\n\n// ---------------------------------------------------------------------------\n// PlugLike interface\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal interface expected from a Plug instance.\n * Matches the scaffolded Plug class's public API.\n */\nexport interface PlugLike {\n request<T>(\n method: string,\n path: string,\n options?: {\n params?: Record<string, unknown>;\n body?: unknown;\n headers?: Record<string, string>;\n },\n ): Promise<T>;\n}\n\nexport interface PlugClientOptions {\n validateResponse?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// PlugError detection — works without importing the user's PlugError class\n// ---------------------------------------------------------------------------\n\ninterface PlugErrorLike {\n name: string;\n status: number;\n body: unknown;\n}\n\nfunction isPlugError(err: unknown): err is PlugErrorLike {\n return (\n err instanceof Error &&\n \"status\" in err &&\n typeof (err as unknown as PlugErrorLike).status === \"number\" &&\n \"body\" in err\n );\n}\n\nfunction parseBody(body: unknown): unknown {\n if (typeof body === \"string\") {\n try {\n return JSON.parse(body);\n } catch {\n return body;\n }\n }\n return body;\n}\n\n// ---------------------------------------------------------------------------\n// Path interpolation\n// ---------------------------------------------------------------------------\n\nfunction interpolatePath(path: string, params: Record<string, string>): string {\n return path.replace(/:([^/]+)/g, (_, key: string) => {\n const value = params[key];\n if (value === undefined) {\n throw new Error(`Missing path parameter: \"${key}\"`);\n }\n return encodeURIComponent(value);\n });\n}\n\n// ---------------------------------------------------------------------------\n// createPlugClient\n// ---------------------------------------------------------------------------\n\nexport function createPlugClient<T extends ContractRouter>(\n contract: T,\n plug: PlugLike,\n options?: PlugClientOptions,\n): PlugClient<T> {\n const globalValidateResponse = options?.validateResponse ?? true;\n\n function buildClient(router: ContractRouter): Record<string, unknown> {\n const client: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(router)) {\n const route = value;\n\n if (\"method\" in route && \"path\" in route) {\n client[key] = async (input?: Record<string, unknown>) => {\n const def = route as RouteDefinition;\n const params = (input?.[\"params\"] ?? {}) as Record<string, string>;\n const query = input?.[\"query\"] as Record<string, unknown> | undefined;\n const body = input?.[\"body\"];\n const headers = input?.[\"headers\"] as\n | Record<string, string>\n | undefined;\n const shouldValidateResponse =\n (input?.[\"validateResponse\"] as boolean | undefined) ??\n globalValidateResponse;\n\n if (def.pathParams) {\n def.pathParams.parse(params);\n }\n if (def.query && query !== undefined) {\n def.query.parse(query);\n }\n if (def.body && body !== undefined) {\n def.body.parse(body);\n }\n\n const interpolatedPath = interpolatePath(def.path, params);\n const responses = def.responses;\n\n try {\n const responseBody = await plug.request<unknown>(\n def.method,\n interpolatedPath,\n {\n ...(query !== undefined ? { params: query } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(headers !== undefined ? { headers } : {}),\n },\n );\n\n const status = 200;\n if (!responses[status]) {\n return { status, body: responseBody };\n }\n\n if (shouldValidateResponse) {\n const validated = responses[status].parse(responseBody);\n return { status, body: validated };\n }\n\n return { status, body: responseBody };\n } catch (err) {\n if (!isPlugError(err)) throw err;\n\n const status = err.status;\n if (responses[status]) {\n const parsed = parseBody(err.body);\n if (shouldValidateResponse) {\n const validated = responses[status].parse(parsed);\n return { status, body: validated };\n }\n return { status, body: parsed };\n }\n\n throw err;\n }\n };\n } else {\n client[key] = buildClient(route);\n }\n }\n\n return client;\n }\n\n return buildClient(contract) as PlugClient<T>;\n}\n"]}
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: khotan-probe
3
+ description: >
4
+ Inspect and debug khotan plugs via the CLI. Prefer `khotan plug`
5
+ (legacy alias: `khotan probe`). Use when verifying API
6
+ response shapes against typed endpoint definitions, debugging type
7
+ mismatches between declared schemas and actual responses, or exploring
8
+ available plugs and their endpoints.
9
+ ---
10
+
11
+ Inspect and debug khotan plugs via the CLI. Prefer `khotan plug` (legacy alias: `khotan probe`). Use when verifying API response shapes against typed endpoint definitions, debugging type mismatches between declared schemas and actual responses, or exploring available plugs and their endpoints.
12
+
13
+ **Requires**: A running dev server with `KHOTAN_DEBUG=1` set.
14
+
15
+ ## Commands
16
+
17
+ ### List all plugs
18
+ ```bash
19
+ npx khotan plug --list
20
+ ```
21
+ Returns: `{ ok, plugs: [{ name, baseUrl, authType, varsConfigured }] }`
22
+
23
+ ### Show plug info and endpoints
24
+ ```bash
25
+ npx khotan plug <plugName> --info
26
+ ```
27
+ Returns: `{ ok, plug: { name, baseUrl, authType, vars, endpoints } }`
28
+
29
+ ### Fire a request through a plug
30
+ ```bash
31
+ npx khotan plug <plugName> GET /products
32
+ npx khotan plug <plugName> POST /subscriptions --body '{"url":"https://example.com"}'
33
+ npx khotan plug <plugName> GET /products --params '{"limit":"10"}'
34
+ npx khotan plug <plugName> GET /products --headers '{"X-Custom":"value"}'
35
+ ```
36
+ Returns: `{ ok, request, response: { status, timing, size, body }, matchedEndpoint }`
37
+
38
+ ### Fire via named endpoint
39
+ ```bash
40
+ npx khotan plug <plugName> --endpoint listProducts
41
+ ```
42
+ Resolves method and path from the endpoint definition automatically.
43
+
44
+ ### Compare response against schema
45
+ ```bash
46
+ npx khotan plug <plugName> --endpoint listProducts --compare
47
+ ```
48
+ Returns: `{ ..., comparison: { match, expected, actual, mismatches } }`
49
+
50
+ Each mismatch has `{ path, issue, note }` where issue is `missing`, `extra`, or `type_mismatch` and path uses JSONPath notation (e.g. `$.items[].sku`).
51
+
52
+ ## Options
53
+
54
+ | Flag | Description |
55
+ |------|-------------|
56
+ | `--port <n>` | Dev server port (default: from .env.local → .env → 3000) |
57
+ | `--base-path <p>` | API base path (default: `/api/khotan`) |
58
+ | `--list` | List registered plugs |
59
+ | `--info` | Show plug metadata |
60
+ | `--endpoint <name>` | Fire using named endpoint |
61
+ | `--compare` | Diff response against schema |
62
+ | `--body <json>` | Request body |
63
+ | `--params <json>` | Query params |
64
+ | `--headers <json>` | Extra headers |
65
+
66
+ ## Workflow
67
+
68
+ 1. **Discover**: `--list` to find plugs, `--info` to see endpoints
69
+ 2. **Probe**: Fire a request via endpoint name or raw method/path
70
+ 3. **Compare**: Add `--compare` to check response shape against Zod schema
71
+ 4. **Fix**: If mismatches found, update the endpoint's response schema or adjust API expectations
72
+
73
+ All output is JSON on stdout — parse with standard JSON tools.
@@ -0,0 +1,41 @@
1
+ # khotan-data
2
+
3
+ Production-grade data flow, ETL, and webhook components for Next.js + Drizzle + Postgres.
4
+
5
+ ## Skills
6
+
7
+ | Skill | When to use |
8
+ |-------|-------------|
9
+ | [khotan-setup](skills/khotan-setup/SKILL.md) | Initializing khotan in a new project, adding the database schema, configuring the factory |
10
+ | [khotan-plug](skills/khotan-plug/SKILL.md) | Connecting to a new API, defining endpoint contracts, configuring authentication |
11
+ | [khotan-dashboard](skills/khotan-dashboard/SKILL.md) | Adding a management interface, configuring plug variables in the browser |
12
+ | [khotan-webhook](skills/khotan-webhook/SKILL.md) | Receiving webhooks, registering callback URLs, processing incoming events |
13
+ | [khotan-probe](skills/khotan-probe/SKILL.md) | Debugging plugs via CLI with `khotan plug` (legacy alias: `probe`) |
14
+
15
+ ## Quick Reference
16
+
17
+ ```bash
18
+ npx khotan init # Initialize project
19
+ npx khotan add plug --yes # Add HTTP client component
20
+ npx khotan add hub --yes # Add dashboard UI
21
+ npx khotan generate # Scaffold Drizzle schema
22
+ npx khotan migrate # Apply database migrations
23
+ npx khotan plug --list # Debug: list registered plugs
24
+ ```
25
+
26
+ ## Key Files
27
+
28
+ | File | Purpose |
29
+ |------|---------|
30
+ | `khotan.config.ts` | CLI config — sets outputDir |
31
+ | `{outputDir}/khotan.ts` | Factory config — register plugs, resources, adapter |
32
+ | `src/app/api/khotan/[...all]/route.ts` | Catch-all API route |
33
+
34
+ ## Environment Variables
35
+
36
+ | Variable | Purpose |
37
+ |----------|---------|
38
+ | `DATABASE_URL` | Postgres connection (Drizzle) |
39
+ | `KHOTAN_SECRET` | AES-256-GCM key for encrypting plug variables |
40
+ | `KHOTAN_DEBUG` | Enables debug routes and the `plug` CLI (`probe` alias) |
41
+ | `KHOTAN_WEBHOOK_URL` | Public URL for webhook callbacks |
@@ -0,0 +1,11 @@
1
+ import { cache } from "./cache";
2
+
3
+ export const cin7ProductsSnapshotCache = cache({
4
+ name: "cin7-products-snapshot",
5
+ scope: {
6
+ plug: "cin7",
7
+ resource: "products",
8
+ flow: "cin7-to-pollinate-products-relay",
9
+ },
10
+ ttl: "6h",
11
+ });
@@ -0,0 +1,58 @@
1
+ // ============================================================================
2
+ // Cache — durable sync-state storage for flows, relays, and webhooks
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // This file defines the cache() builder and types. Create named cache
6
+ // definitions for expensive upstream snapshots, checkpoints, and dedupe markers,
7
+ // then register them in your khotan.ts factory config.
8
+ // ============================================================================
9
+
10
+ import type { CacheRegistration, CacheScope } from "khotan-data/factory";
11
+
12
+ export interface CacheConfig {
13
+ /** Unique cache name used by runtime helpers and DB registration */
14
+ name: string;
15
+ /** Optional ownership metadata for humans and runtime validation */
16
+ scope?: CacheScope;
17
+ /** Optional default TTL like "30m", "6h", or 86400 */
18
+ ttl?: string | number;
19
+ }
20
+
21
+ export function cache(config: CacheConfig): CacheRegistration {
22
+ return {
23
+ name: config.name,
24
+ scope: config.scope,
25
+ ttl: config.ttl,
26
+ };
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Usage Example (create a file like caches/cin7-products-snapshot.ts)
31
+ // ---------------------------------------------------------------------------
32
+ //
33
+ // import { cache } from "./cache";
34
+ //
35
+ // export const cin7ProductsSnapshotCache = cache({
36
+ // name: "cin7-products-snapshot",
37
+ // scope: {
38
+ // plug: "cin7",
39
+ // resource: "products",
40
+ // flow: "cin7-to-pollinate-products-relay",
41
+ // },
42
+ // ttl: "6h",
43
+ // });
44
+ //
45
+ // Then register in your khotan config:
46
+ //
47
+ // import { cin7ProductsSnapshotCache } from "./caches/cin7-products-snapshot";
48
+ //
49
+ // const khotanData = khotan({
50
+ // adapter: drizzleAdapter(db),
51
+ // caches: [cin7ProductsSnapshotCache],
52
+ // plugs: [...],
53
+ // });
54
+ //
55
+ // Flow, relay, catch, and pass workflows can then use:
56
+ // await khotanCache(ctx, "cin7-products-snapshot").get("all-products");
57
+ // await khotanCache(ctx, "cin7-products-snapshot").set("all-products", payload);
58
+ // await khotanCache(ctx, "cin7-products-snapshot").delete("all-products");
@@ -0,0 +1,36 @@
1
+ // ============================================================================
2
+ // Example: Catch
3
+ // Generated by khotan CLI · https://github.com/khotan-data
4
+ //
5
+ // Copy this file, rename it for your webhook source/event, and register the
6
+ // exported catch handler in {outputDir}/khotan.ts.
7
+ // ============================================================================
8
+
9
+ import { catchEvent, type CatchContext } from "./catch";
10
+
11
+ async function stripeInvoiceCatchWorkflow(ctx: CatchContext) {
12
+ "use workflow";
13
+
14
+ async function persistEvent() {
15
+ "use step";
16
+ console.log("Handling webhook event", {
17
+ eventType: ctx.eventType,
18
+ khotanRunId: ctx.khotanRunId,
19
+ });
20
+
21
+ // Khotan already records webhook deliveries. Add app-specific side effects
22
+ // here, such as updating a local table or enqueueing downstream work.
23
+ console.log("Webhook payload", {
24
+ event: ctx.event,
25
+ headers: ctx.headers,
26
+ });
27
+ }
28
+
29
+ await persistEvent();
30
+ }
31
+
32
+ export const stripeInvoiceCatch = catchEvent({
33
+ name: "stripe-invoices",
34
+ events: ["invoice.paid"],
35
+ workflow: stripeInvoiceCatchWorkflow,
36
+ });