kenobi-pages 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.
package/bin/cli.mjs ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ var C=Object.defineProperty;var t=(e,o)=>C(e,"name",{value:o,configurable:!0});import{existsSync as P,mkdirSync as F,readFileSync as E,writeFileSync as I,appendFileSync as G}from"node:fs";import{readFile as j}from"node:fs/promises";import{homedir as L}from"node:os";import{join as _,resolve as x}from"node:path";import{createInterface as R}from"node:readline";var U="https://kenobi.ai",v=_(L(),".kenobi"),S=_(v,"config.json"),l="KENOBI_PAGES_KEY",$=t(()=>{try{return JSON.parse(E(S,"utf-8"))}catch{return{}}},"readGlobalConfig"),B=t(e=>{F(v,{recursive:!0}),I(S,JSON.stringify(e,null,2)+`
3
+ `)},"writeGlobalConfig"),J=t(()=>{let e=process.env[l]||$().apiKey;return e||(console.error("Error: No API key found."),console.error(""),console.error("Run 'npx kenobi-pages init' to set up your API key."),console.error(""),console.error("Or set the KENOBI_PAGES_KEY environment variable directly."),process.exit(1)),e},"getApiKey"),T=t(()=>(process.env.KENOBI_BASE_URL??$().baseUrl??U).replace(/\/+$/,""),"getBaseUrl"),k=t(async(e,o={})=>{let n=`${T()}${e}`,s=await fetch(n,{method:o.method??"GET",headers:{"x-kenobi-key":J(),"Content-Type":"application/json"},body:o.body?JSON.stringify(o.body):void 0});if(!s.ok){let r=await s.text().catch(()=>"Unknown error"),i=s.status===401||s.status===403?4:s.status===404?3:1;console.error(`Error ${s.status}: ${r}`),process.exit(i)}return s.json()},"fetchKenobi"),w=t(e=>new Promise(o=>{let n=R({input:process.stdin,output:process.stderr});n.question(e,s=>{n.close(),o(s.trim())})}),"prompt"),Y=t(()=>new Promise((e,o)=>{let n="";process.stdin.setEncoding("utf-8"),process.stdin.on("data",s=>{n+=s}),process.stdin.on("end",()=>e(n)),process.stdin.on("error",o),process.stdin.isTTY&&e("")}),"readStdin"),W=t(async e=>{let o=e.indexOf("--file");return o!==-1&&e[o+1]?j(e[o+1],"utf-8"):null},"parseFileArg"),D=t(async()=>{console.error("Kenobi Pages \u2014 Setup"),console.error(""),console.error("Find your Pages API key in the workflow builder:"),console.error(" https://kenobi.ai/testing/cortex"),console.error("");let e=await w("Paste your API key (pk_live_... or pk_test_...): ");e||(console.error("No key provided. Aborting."),process.exit(1)),!e.startsWith("pk_live_")&&!e.startsWith("pk_test_")&&(console.error(`Warning: "${e}" doesn't look like a Kenobi public key (expected pk_live_... or pk_test_...).`),(await w("Continue anyway? (y/N): ")).toLowerCase()!=="y"&&(console.error("Aborting."),process.exit(1)));let o=$();o.apiKey=e,B(o),console.error(`\u2713 Saved to ${S}`);let n=x(process.cwd(),".env.local"),s=x(process.cwd(),".env"),r=P(n)?n:P(s)?s:null;if(r){let i=E(r,"utf-8");if(i.includes(l))console.error(`\u2713 ${l} already present in ${r.split("/").pop()}`);else{let p=i.endsWith(`
4
+ `)?"":`
5
+ `;G(r,`${p}${l}="${e}"
6
+ `),console.error(`\u2713 Added ${l} to ${r.split("/").pop()}`)}}else(await w("No .env.local found. Create one with your API key? (Y/n): ")).toLowerCase()!=="n"&&(I(n,`${l}="${e}"
7
+ `),console.error(`\u2713 Created .env.local with ${l}`));console.error(""),console.error("Done! You can now use kenobi-pages commands."),console.error(` npx kenobi-pages schema push "My Page" '{"fields":{...}}'`),console.error(" npx kenobi-pages types <workflowId>")},"initCommand"),M=t(async e=>{let o=e[0];o||(console.error("Usage: kenobi-pages schema get <workflowId>"),process.exit(2));let n=await k(`/api/v1/pages/${o}/schema`);console.log(JSON.stringify(n,null,2))},"schemaGet"),q=t(async e=>{let o=e[0];o||(console.error("Usage: kenobi-pages schema push <name> '<json>'"),console.error(" kenobi-pages schema push <name> --file schema.json"),console.error(" echo '<json>' | kenobi-pages schema push <name>"),process.exit(2));let n=e.slice(1),s=await W(n),r=s?null:n.find(b=>b.startsWith("{")),i=!s&&!r?await Y():null,p=s??r??i;(!p||p.trim().length===0)&&(console.error("Error: No schema provided."),console.error("Pass inline JSON, --file <path>, or pipe to stdin."),process.exit(2));let f;try{f=JSON.parse(p)}catch{console.error("Error: Invalid JSON."),process.exit(2)}let m=await k("/api/v1/pages/schema",{method:"POST",body:{name:o,schema:f}});console.log(JSON.stringify(m,null,2)),console.error(`Schema "${m.name}" pushed successfully.`),console.error(`Source key: ${m.sourceKey}`),console.error("You can now select this schema as an output target in the Kenobi workflow builder.")},"schemaPush"),z=t(async e=>{let[o,n]=e;(!o||!n)&&(console.error("Usage: kenobi-pages page get <workflowId> <slug>"),process.exit(2));let s=await k(`/api/v1/pages/${o}/${n}`);console.log(JSON.stringify(s,null,2))},"pageGet"),H=t(async e=>{let o=e[0];o||(console.error("Usage: kenobi-pages types <workflowId>"),process.exit(2));let n=await k(`/api/v1/pages/${o}/schema`),s=n.schema;s?.fields||(console.error("Error: Workflow has no output schema with fields."),process.exit(1));let r=t(a=>" ".repeat(a),"indent"),i=t((a,c=1)=>{switch(a.type){case"string":case"url":return"string";case"number":return"number";case"boolean":return"boolean";case"enum":return a.values?.length?a.values.map(u=>`"${u}"`).join(" | "):"string";case"object":{let u=Object.entries(a.fields??{});return u.length===0?"Record<string, unknown>":`{
8
+ ${u.map(([N,O])=>{let K=O.optional?"?":"";return`${r(c)}${N}${K}: ${i(O,c+1)}`}).join(`
9
+ `)}
10
+ ${r(c-1)}}`}case"array":return`Array<${i(a.items,c)}>`;default:return"unknown"}},"fieldToTs"),f=Object.entries(s.fields).map(([a,c])=>{let u=c.optional?"?":"";return`${c.description?` /** ${c.description} */
11
+ `:""} ${a}${u}: ${i(c,2)}`}),b=`export interface ${n.title?n.title.replace(/[^a-zA-Z0-9]+/g,""):"PageContent"} {
12
+ ${f.join(`
13
+ `)}
14
+ }
15
+ `;console.log(b)},"typesGen"),V=`kenobi-pages \u2014 Kenobi Pages CLI
16
+
17
+ Commands:
18
+ kenobi-pages init Set up your API key (interactive)
19
+ kenobi-pages schema get <workflowId> Fetch a workflow's output schema
20
+ kenobi-pages schema push <name> '<json>' Push a schema to Kenobi (inline JSON)
21
+ kenobi-pages schema push <name> --file f Push a schema to Kenobi (from file)
22
+ kenobi-pages page get <workflowId> <slug> Fetch page content for a specific lead
23
+ kenobi-pages types <workflowId> Generate TypeScript interface from schema
24
+
25
+ Examples:
26
+ npx kenobi-pages init
27
+ npx kenobi-pages schema get 42
28
+ npx kenobi-pages schema push "My Page" '{"fields":{"headline":{"type":"string"}}}'
29
+ npx kenobi-pages types 42 > lib/kenobi-types.ts
30
+ npx kenobi-pages page get 42 acme-corp
31
+
32
+ Output:
33
+ All data is written as JSON to stdout. Status messages go to stderr.
34
+ Pipe stdout to jq, a file, or another command safely.
35
+
36
+ Exit codes:
37
+ 0 Success
38
+ 1 API or network error
39
+ 2 Invalid usage (bad arguments)
40
+ 3 Resource not found (404)
41
+ 4 Unauthorized (invalid API key)
42
+
43
+ Environment:
44
+ KENOBI_PAGES_KEY Org public key. Set via 'init' or manually.
45
+ KENOBI_BASE_URL Optional. Override the Kenobi API base URL.
46
+
47
+ Key resolution order:
48
+ 1. KENOBI_PAGES_KEY environment variable
49
+ 2. ~/.kenobi/config.json (written by 'init')
50
+ `,[,,...d]=process.argv;(d.length===0||d[0]==="--help"||d[0]==="-h")&&(console.log(V),process.exit(0));var[g,h,...y]=d;g==="init"?await D():g==="schema"&&h==="get"?await M(y):g==="schema"&&h==="push"?await q(y):g==="page"&&h==="get"?await z(y):g==="types"?await H([h,...y]):(console.error(`Unknown command: ${d.join(" ")}`),console.error("Run 'kenobi-pages --help' for usage."),process.exit(1));
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "kenobi-pages",
3
+ "version": "0.1.1",
4
+ "description": "Kenobi Pages SDK — fetch personalized page content from Kenobi workflows",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "bin": {
10
+ "kenobi-pages": "./bin/cli.mjs"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "tsx src/build.ts --prod",
21
+ "bump": "pnpm version patch",
22
+ "lint": "eslint src/",
23
+ "tsc": "tsc --noEmit"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^9.28.0",
27
+ "@types/node": "catalog:",
28
+ "esbuild": "^0.25.9",
29
+ "eslint": "catalog:",
30
+ "eslint-config-prettier": "^10.1.5",
31
+ "tsx": "^4.21.0",
32
+ "typescript": "catalog:",
33
+ "typescript-eslint": "^8.34.0"
34
+ }
35
+ }
package/src/build.ts ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import * as fs from "fs"
4
+ import * as path from "path"
5
+
6
+ import * as esbuild from "esbuild"
7
+
8
+ const isProd =
9
+ process.argv.includes("--prod") || process.env.NODE_ENV === "production"
10
+
11
+ const packageJsonPath = path.join(process.cwd(), "package.json")
12
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
13
+ const version = packageJson.version as string
14
+
15
+ const entryFile = path.join(process.cwd(), "src", "cli.ts")
16
+ const outFile = path.join(process.cwd(), "bin", "cli.mjs")
17
+
18
+ const build = async () => {
19
+ console.log(
20
+ `📦 Building CLI ${isProd ? "(production)" : "(development)"} v${version}...`
21
+ )
22
+
23
+ const result = await esbuild.build({
24
+ entryPoints: [entryFile],
25
+ bundle: true,
26
+ platform: "node",
27
+ target: ["node18"],
28
+ format: "esm",
29
+ treeShaking: true,
30
+ minify: isProd,
31
+ keepNames: true,
32
+ metafile: true,
33
+ logLevel: "info",
34
+ outfile: outFile,
35
+ banner: {
36
+ js: "#!/usr/bin/env node",
37
+ },
38
+ })
39
+
40
+ if (result.metafile) {
41
+ const outputs = Object.values(result.metafile.outputs)
42
+ const totalSize = outputs.reduce((acc, output) => acc + output.bytes, 0)
43
+ console.log(`\n✅ Build complete: ${(totalSize / 1024).toFixed(2)} KB → ${outFile}`)
44
+ }
45
+ }
46
+
47
+ build().catch((error) => {
48
+ console.error("Build failed:", error)
49
+ process.exit(1)
50
+ })
package/src/cli.ts ADDED
@@ -0,0 +1,396 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ appendFileSync,
7
+ } from "node:fs"
8
+ import { readFile } from "node:fs/promises"
9
+ import { homedir } from "node:os"
10
+ import { join, resolve } from "node:path"
11
+ import { createInterface } from "node:readline"
12
+
13
+ const DEFAULT_BASE_URL = "https://kenobi.ai"
14
+ const GLOBAL_CONFIG_DIR = join(homedir(), ".kenobi")
15
+ const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, "config.json")
16
+ const ENV_VAR_NAME = "KENOBI_PAGES_KEY"
17
+
18
+ // ── Types ──
19
+
20
+ interface GlobalConfig {
21
+ apiKey?: string
22
+ baseUrl?: string
23
+ }
24
+
25
+ interface FetchOptions {
26
+ method?: string
27
+ body?: Record<string, unknown>
28
+ }
29
+
30
+ interface OutputFieldSpec {
31
+ type: string
32
+ optional?: boolean
33
+ description?: string
34
+ values?: string[]
35
+ fields?: Record<string, OutputFieldSpec>
36
+ items?: OutputFieldSpec
37
+ }
38
+
39
+ // ── Global config ──
40
+
41
+ const readGlobalConfig = (): GlobalConfig => {
42
+ try {
43
+ return JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8")) as GlobalConfig
44
+ } catch {
45
+ return {}
46
+ }
47
+ }
48
+
49
+ const writeGlobalConfig = (config: GlobalConfig) => {
50
+ mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true })
51
+ writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n")
52
+ }
53
+
54
+ // ── Helpers ──
55
+
56
+ const getApiKey = (): string => {
57
+ const key = process.env[ENV_VAR_NAME] || readGlobalConfig().apiKey
58
+ if (!key) {
59
+ console.error("Error: No API key found.")
60
+ console.error("")
61
+ console.error("Run 'npx kenobi-pages init' to set up your API key.")
62
+ console.error("")
63
+ console.error("Or set the KENOBI_PAGES_KEY environment variable directly.")
64
+ process.exit(1)
65
+ }
66
+ return key
67
+ }
68
+
69
+ const getBaseUrl = (): string =>
70
+ (
71
+ process.env.KENOBI_BASE_URL ??
72
+ readGlobalConfig().baseUrl ??
73
+ DEFAULT_BASE_URL
74
+ ).replace(/\/+$/, "")
75
+
76
+ const fetchKenobi = async (path: string, opts: FetchOptions = {}) => {
77
+ const url = `${getBaseUrl()}${path}`
78
+ const res = await fetch(url, {
79
+ method: opts.method ?? "GET",
80
+ headers: {
81
+ "x-kenobi-key": getApiKey(),
82
+ "Content-Type": "application/json",
83
+ },
84
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
85
+ })
86
+
87
+ if (!res.ok) {
88
+ const text = await res.text().catch(() => "Unknown error")
89
+ const exitCode =
90
+ res.status === 401 || res.status === 403
91
+ ? 4
92
+ : res.status === 404
93
+ ? 3
94
+ : 1
95
+ console.error(`Error ${res.status}: ${text}`)
96
+ process.exit(exitCode)
97
+ }
98
+
99
+ return res.json()
100
+ }
101
+
102
+ const prompt = (question: string): Promise<string> =>
103
+ new Promise((resolve) => {
104
+ const rl = createInterface({ input: process.stdin, output: process.stderr })
105
+ rl.question(question, (answer) => {
106
+ rl.close()
107
+ resolve(answer.trim())
108
+ })
109
+ })
110
+
111
+ const readStdin = (): Promise<string> =>
112
+ new Promise((resolve, reject) => {
113
+ let data = ""
114
+ process.stdin.setEncoding("utf-8")
115
+ process.stdin.on("data", (chunk: string) => {
116
+ data += chunk
117
+ })
118
+ process.stdin.on("end", () => resolve(data))
119
+ process.stdin.on("error", reject)
120
+ if (process.stdin.isTTY) resolve("")
121
+ })
122
+
123
+ const parseFileArg = async (args: string[]): Promise<string | null> => {
124
+ const fileIdx = args.indexOf("--file")
125
+ if (fileIdx !== -1 && args[fileIdx + 1]) {
126
+ return readFile(args[fileIdx + 1], "utf-8")
127
+ }
128
+ return null
129
+ }
130
+
131
+ // ── Init ──
132
+
133
+ const initCommand = async () => {
134
+ console.error("Kenobi Pages — Setup")
135
+ console.error("")
136
+ console.error("Find your Pages API key in the workflow builder:")
137
+ console.error(" https://kenobi.ai/testing/cortex")
138
+ console.error("")
139
+
140
+ const key = await prompt("Paste your API key (pk_live_... or pk_test_...): ")
141
+
142
+ if (!key) {
143
+ console.error("No key provided. Aborting.")
144
+ process.exit(1)
145
+ }
146
+
147
+ if (!key.startsWith("pk_live_") && !key.startsWith("pk_test_")) {
148
+ console.error(
149
+ `Warning: "${key}" doesn't look like a Kenobi public key (expected pk_live_... or pk_test_...).`
150
+ )
151
+ const proceed = await prompt("Continue anyway? (y/N): ")
152
+ if (proceed.toLowerCase() !== "y") {
153
+ console.error("Aborting.")
154
+ process.exit(1)
155
+ }
156
+ }
157
+
158
+ const config = readGlobalConfig()
159
+ config.apiKey = key
160
+ writeGlobalConfig(config)
161
+ console.error(`✓ Saved to ${GLOBAL_CONFIG_PATH}`)
162
+
163
+ const envLocalPath = resolve(process.cwd(), ".env.local")
164
+ const envPath = resolve(process.cwd(), ".env")
165
+ const targetEnvFile = existsSync(envLocalPath)
166
+ ? envLocalPath
167
+ : existsSync(envPath)
168
+ ? envPath
169
+ : null
170
+
171
+ if (targetEnvFile) {
172
+ const contents = readFileSync(targetEnvFile, "utf-8")
173
+ if (contents.includes(ENV_VAR_NAME)) {
174
+ console.error(
175
+ `✓ ${ENV_VAR_NAME} already present in ${targetEnvFile.split("/").pop()}`
176
+ )
177
+ } else {
178
+ const suffix = contents.endsWith("\n") ? "" : "\n"
179
+ appendFileSync(targetEnvFile, `${suffix}${ENV_VAR_NAME}="${key}"\n`)
180
+ console.error(
181
+ `✓ Added ${ENV_VAR_NAME} to ${targetEnvFile.split("/").pop()}`
182
+ )
183
+ }
184
+ } else {
185
+ const createEnvLocal = await prompt(
186
+ "No .env.local found. Create one with your API key? (Y/n): "
187
+ )
188
+ if (createEnvLocal.toLowerCase() !== "n") {
189
+ writeFileSync(envLocalPath, `${ENV_VAR_NAME}="${key}"\n`)
190
+ console.error(`✓ Created .env.local with ${ENV_VAR_NAME}`)
191
+ }
192
+ }
193
+
194
+ console.error("")
195
+ console.error("Done! You can now use kenobi-pages commands.")
196
+ console.error(
197
+ ' npx kenobi-pages schema push "My Page" \'{"fields":{...}}\''
198
+ )
199
+ console.error(" npx kenobi-pages types <workflowId>")
200
+ }
201
+
202
+ // ── Subcommands ──
203
+
204
+ const schemaGet = async (args: string[]) => {
205
+ const workflowId = args[0]
206
+ if (!workflowId) {
207
+ console.error("Usage: kenobi-pages schema get <workflowId>")
208
+ process.exit(2)
209
+ }
210
+
211
+ const data = await fetchKenobi(`/api/v1/pages/${workflowId}/schema`)
212
+ console.log(JSON.stringify(data, null, 2))
213
+ }
214
+
215
+ const schemaPush = async (args: string[]) => {
216
+ const name = args[0]
217
+ if (!name) {
218
+ console.error("Usage: kenobi-pages schema push <name> '<json>'")
219
+ console.error(" kenobi-pages schema push <name> --file schema.json")
220
+ console.error(" echo '<json>' | kenobi-pages schema push <name>")
221
+ process.exit(2)
222
+ }
223
+
224
+ const remaining = args.slice(1)
225
+ const fileContent = await parseFileArg(remaining)
226
+
227
+ const inlineArg = !fileContent
228
+ ? remaining.find((a) => a.startsWith("{"))
229
+ : null
230
+ const stdinContent = !fileContent && !inlineArg ? await readStdin() : null
231
+ const rawJson = fileContent ?? inlineArg ?? stdinContent
232
+
233
+ if (!rawJson || rawJson.trim().length === 0) {
234
+ console.error("Error: No schema provided.")
235
+ console.error("Pass inline JSON, --file <path>, or pipe to stdin.")
236
+ process.exit(2)
237
+ }
238
+
239
+ let schema: Record<string, unknown>
240
+ try {
241
+ schema = JSON.parse(rawJson) as Record<string, unknown>
242
+ } catch {
243
+ console.error("Error: Invalid JSON.")
244
+ process.exit(2)
245
+ }
246
+
247
+ const data = (await fetchKenobi("/api/v1/pages/schema", {
248
+ method: "POST",
249
+ body: { name, schema },
250
+ })) as { name: string; sourceKey: string }
251
+
252
+ console.log(JSON.stringify(data, null, 2))
253
+ console.error(`Schema "${data.name}" pushed successfully.`)
254
+ console.error(`Source key: ${data.sourceKey}`)
255
+ console.error(
256
+ "You can now select this schema as an output target in the Kenobi workflow builder."
257
+ )
258
+ }
259
+
260
+ const pageGet = async (args: string[]) => {
261
+ const [workflowId, slug] = args
262
+ if (!workflowId || !slug) {
263
+ console.error("Usage: kenobi-pages page get <workflowId> <slug>")
264
+ process.exit(2)
265
+ }
266
+
267
+ const data = await fetchKenobi(`/api/v1/pages/${workflowId}/${slug}`)
268
+ console.log(JSON.stringify(data, null, 2))
269
+ }
270
+
271
+ const typesGen = async (args: string[]) => {
272
+ const workflowId = args[0]
273
+ if (!workflowId) {
274
+ console.error("Usage: kenobi-pages types <workflowId>")
275
+ process.exit(2)
276
+ }
277
+
278
+ const data = (await fetchKenobi(
279
+ `/api/v1/pages/${workflowId}/schema`
280
+ )) as { schema: { fields?: Record<string, OutputFieldSpec> }; title?: string }
281
+ const schema = data.schema
282
+
283
+ if (!schema?.fields) {
284
+ console.error("Error: Workflow has no output schema with fields.")
285
+ process.exit(1)
286
+ }
287
+
288
+ const indent = (level: number) => " ".repeat(level)
289
+
290
+ const fieldToTs = (spec: OutputFieldSpec, level = 1): string => {
291
+ switch (spec.type) {
292
+ case "string":
293
+ case "url":
294
+ return "string"
295
+ case "number":
296
+ return "number"
297
+ case "boolean":
298
+ return "boolean"
299
+ case "enum":
300
+ return spec.values?.length
301
+ ? spec.values.map((v) => `"${v}"`).join(" | ")
302
+ : "string"
303
+ case "object": {
304
+ const entries = Object.entries(spec.fields ?? {})
305
+ if (entries.length === 0) return "Record<string, unknown>"
306
+ const lines = entries.map(([k, v]) => {
307
+ const opt = v.optional ? "?" : ""
308
+ return `${indent(level)}${k}${opt}: ${fieldToTs(v, level + 1)}`
309
+ })
310
+ return `{\n${lines.join("\n")}\n${indent(level - 1)}}`
311
+ }
312
+ case "array":
313
+ return `Array<${fieldToTs(spec.items!, level)}>`
314
+ default:
315
+ return "unknown"
316
+ }
317
+ }
318
+
319
+ const entries = Object.entries(schema.fields)
320
+ const lines = entries.map(([k, v]) => {
321
+ const opt = v.optional ? "?" : ""
322
+ const comment = v.description ? ` /** ${v.description} */\n` : ""
323
+ return `${comment} ${k}${opt}: ${fieldToTs(v, 2)}`
324
+ })
325
+
326
+ const typeName = data.title
327
+ ? data.title.replace(/[^a-zA-Z0-9]+/g, "")
328
+ : "PageContent"
329
+
330
+ const output = `export interface ${typeName} {\n${lines.join("\n")}\n}\n`
331
+ console.log(output)
332
+ }
333
+
334
+ // ── Router ──
335
+
336
+ const HELP = `kenobi-pages — Kenobi Pages CLI
337
+
338
+ Commands:
339
+ kenobi-pages init Set up your API key (interactive)
340
+ kenobi-pages schema get <workflowId> Fetch a workflow's output schema
341
+ kenobi-pages schema push <name> '<json>' Push a schema to Kenobi (inline JSON)
342
+ kenobi-pages schema push <name> --file f Push a schema to Kenobi (from file)
343
+ kenobi-pages page get <workflowId> <slug> Fetch page content for a specific lead
344
+ kenobi-pages types <workflowId> Generate TypeScript interface from schema
345
+
346
+ Examples:
347
+ npx kenobi-pages init
348
+ npx kenobi-pages schema get 42
349
+ npx kenobi-pages schema push "My Page" '{"fields":{"headline":{"type":"string"}}}'
350
+ npx kenobi-pages types 42 > lib/kenobi-types.ts
351
+ npx kenobi-pages page get 42 acme-corp
352
+
353
+ Output:
354
+ All data is written as JSON to stdout. Status messages go to stderr.
355
+ Pipe stdout to jq, a file, or another command safely.
356
+
357
+ Exit codes:
358
+ 0 Success
359
+ 1 API or network error
360
+ 2 Invalid usage (bad arguments)
361
+ 3 Resource not found (404)
362
+ 4 Unauthorized (invalid API key)
363
+
364
+ Environment:
365
+ KENOBI_PAGES_KEY Org public key. Set via 'init' or manually.
366
+ KENOBI_BASE_URL Optional. Override the Kenobi API base URL.
367
+
368
+ Key resolution order:
369
+ 1. KENOBI_PAGES_KEY environment variable
370
+ 2. ~/.kenobi/config.json (written by 'init')
371
+ `
372
+
373
+ const [, , ...argv] = process.argv
374
+
375
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
376
+ console.log(HELP)
377
+ process.exit(0)
378
+ }
379
+
380
+ const [group, action, ...rest] = argv
381
+
382
+ if (group === "init") {
383
+ await initCommand()
384
+ } else if (group === "schema" && action === "get") {
385
+ await schemaGet(rest)
386
+ } else if (group === "schema" && action === "push") {
387
+ await schemaPush(rest)
388
+ } else if (group === "page" && action === "get") {
389
+ await pageGet(rest)
390
+ } else if (group === "types") {
391
+ await typesGen([action, ...rest])
392
+ } else {
393
+ console.error(`Unknown command: ${argv.join(" ")}`)
394
+ console.error("Run 'kenobi-pages --help' for usage.")
395
+ process.exit(1)
396
+ }
package/src/client.ts ADDED
@@ -0,0 +1,96 @@
1
+ import { KenobiPagesError, KenobiNotFoundError, KenobiUnauthorizedError } from "./errors"
2
+ import type {
3
+ KenobiPagesClient,
4
+ KenobiPagesConfig,
5
+ KenobiFetchOptions,
6
+ KenobiPageResponse,
7
+ KenobiPageSchema,
8
+ KenobiSchemaResponse,
9
+ PostSchemaResponse,
10
+ } from "./types"
11
+
12
+ const DEFAULT_BASE_URL = "https://kenobi.ai"
13
+
14
+ // ── Internal fetch helper ──
15
+
16
+ const fetchFromKenobi = async (
17
+ config: KenobiPagesConfig,
18
+ path: string,
19
+ opts?: KenobiFetchOptions & { method?: string; body?: unknown }
20
+ ): Promise<Response> => {
21
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "")
22
+ const url = `${baseUrl}${path}`
23
+
24
+ const headers: Record<string, string> = {
25
+ "x-kenobi-key": config.apiKey,
26
+ "Content-Type": "application/json",
27
+ }
28
+
29
+ const fetchInit: RequestInit & { next?: { revalidate?: number | false; tags?: readonly string[] } } = {
30
+ method: opts?.method ?? "GET",
31
+ headers,
32
+ }
33
+
34
+ if (opts?.body !== undefined) {
35
+ fetchInit.body = JSON.stringify(opts.body)
36
+ }
37
+
38
+ if (opts?.revalidate !== undefined || opts?.tags) {
39
+ fetchInit.next = {}
40
+ if (opts.revalidate !== undefined) fetchInit.next.revalidate = opts.revalidate
41
+ if (opts.tags) fetchInit.next.tags = opts.tags
42
+ }
43
+
44
+ const response = await fetch(url, fetchInit)
45
+
46
+ if (response.ok) return response
47
+
48
+ if (response.status === 401) {
49
+ throw new KenobiUnauthorizedError()
50
+ }
51
+
52
+ if (response.status === 404) {
53
+ throw new KenobiNotFoundError()
54
+ }
55
+
56
+ const errorBody = await response.text().catch(() => "Unknown error")
57
+ throw new KenobiPagesError(
58
+ `Kenobi API error (${response.status}): ${errorBody}`,
59
+ response.status,
60
+ "API_ERROR"
61
+ )
62
+ }
63
+
64
+ // ── Client factory ──
65
+
66
+ export const createKenobiPagesClient = (config: KenobiPagesConfig): KenobiPagesClient => ({
67
+ getPage: async (
68
+ workflowId: number,
69
+ slug: string,
70
+ opts?: KenobiFetchOptions
71
+ ): Promise<KenobiPageResponse | null> => {
72
+ try {
73
+ const response = await fetchFromKenobi(config, `/api/v1/pages/${workflowId}/${slug}`, opts)
74
+ return (await response.json()) as KenobiPageResponse
75
+ } catch (err) {
76
+ if (err instanceof KenobiNotFoundError) return null
77
+ throw err
78
+ }
79
+ },
80
+
81
+ getSchema: async (
82
+ workflowId: number,
83
+ opts?: KenobiFetchOptions
84
+ ): Promise<KenobiSchemaResponse> => {
85
+ const response = await fetchFromKenobi(config, `/api/v1/pages/${workflowId}/schema`, opts)
86
+ return (await response.json()) as KenobiSchemaResponse
87
+ },
88
+
89
+ postSchema: async (name: string, schema: KenobiPageSchema): Promise<PostSchemaResponse> => {
90
+ const response = await fetchFromKenobi(config, "/api/v1/pages/schema", {
91
+ method: "POST",
92
+ body: { name, schema },
93
+ })
94
+ return (await response.json()) as PostSchemaResponse
95
+ },
96
+ })
package/src/errors.ts ADDED
@@ -0,0 +1,25 @@
1
+ export class KenobiPagesError extends Error {
2
+ readonly status: number
3
+ readonly code: string
4
+
5
+ constructor(message: string, status: number, code: string) {
6
+ super(message)
7
+ this.name = "KenobiPagesError"
8
+ this.status = status
9
+ this.code = code
10
+ }
11
+ }
12
+
13
+ export class KenobiNotFoundError extends KenobiPagesError {
14
+ constructor(message = "Resource not found") {
15
+ super(message, 404, "NOT_FOUND")
16
+ this.name = "KenobiNotFoundError"
17
+ }
18
+ }
19
+
20
+ export class KenobiUnauthorizedError extends KenobiPagesError {
21
+ constructor(message = "Unauthorized — check your API key") {
22
+ super(message, 401, "UNAUTHORIZED")
23
+ this.name = "KenobiUnauthorizedError"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { createKenobiPagesClient } from "./client"
2
+ export { KenobiPagesError, KenobiNotFoundError, KenobiUnauthorizedError } from "./errors"
3
+ export type {
4
+ KenobiPagesClient,
5
+ KenobiPagesConfig,
6
+ KenobiFetchOptions,
7
+ KenobiPageResponse,
8
+ KenobiPageSchema,
9
+ KenobiSchemaResponse,
10
+ OutputFieldSpec,
11
+ PostSchemaResponse,
12
+ } from "./types"
package/src/types.ts ADDED
@@ -0,0 +1,84 @@
1
+ // ── Client configuration ──
2
+
3
+ export interface KenobiPagesConfig {
4
+ /** Org-scoped public key from the Kenobi workflow builder (pk_live_... or pk_test_...). */
5
+ readonly apiKey: string
6
+ /** Base URL of the Kenobi API. Defaults to https://kenobi.ai */
7
+ readonly baseUrl?: string
8
+ }
9
+
10
+ // ── Next.js fetch options ──
11
+
12
+ export interface KenobiFetchOptions {
13
+ /** Next.js ISR revalidation interval in seconds, or `false` to disable caching. */
14
+ readonly revalidate?: number | false
15
+ /** Next.js cache tags for on-demand revalidation via `revalidateTag()`. */
16
+ readonly tags?: readonly string[]
17
+ }
18
+
19
+ // ── API response types ──
20
+
21
+ export interface KenobiPageResponse {
22
+ readonly content: Record<string, unknown>
23
+ readonly metadata: Record<string, unknown> | null
24
+ readonly updatedAt: string
25
+ }
26
+
27
+ export interface KenobiSchemaResponse {
28
+ readonly schema: Record<string, unknown>
29
+ readonly provider: string
30
+ readonly title: string
31
+ }
32
+
33
+ export interface PostSchemaResponse {
34
+ readonly sourceKey: string
35
+ readonly name: string
36
+ }
37
+
38
+ // ── Schema DSL types (standalone — no @kenobi/cortex dependency) ──
39
+
40
+ interface FieldSpecBase {
41
+ readonly description?: string
42
+ readonly optional?: boolean
43
+ }
44
+
45
+ export type OutputFieldSpec =
46
+ | ({ readonly type: "string"; readonly min?: number; readonly max?: number } & FieldSpecBase)
47
+ | ({ readonly type: "url" } & FieldSpecBase)
48
+ | ({
49
+ readonly type: "number"
50
+ readonly min?: number
51
+ readonly max?: number
52
+ readonly integer?: boolean
53
+ } & FieldSpecBase)
54
+ | ({ readonly type: "boolean" } & FieldSpecBase)
55
+ | ({ readonly type: "enum"; readonly values?: readonly string[] } & FieldSpecBase)
56
+ | ({
57
+ readonly type: "object"
58
+ readonly fields: Readonly<Record<string, OutputFieldSpec>>
59
+ } & FieldSpecBase)
60
+ | ({
61
+ readonly type: "array"
62
+ readonly items: OutputFieldSpec
63
+ readonly min?: number
64
+ readonly max?: number
65
+ } & FieldSpecBase)
66
+
67
+ export interface KenobiPageSchema {
68
+ readonly fields: Readonly<Record<string, OutputFieldSpec>>
69
+ }
70
+
71
+ // ── Client interface ──
72
+
73
+ export interface KenobiPagesClient {
74
+ readonly getPage: (
75
+ workflowId: number,
76
+ slug: string,
77
+ opts?: KenobiFetchOptions
78
+ ) => Promise<KenobiPageResponse | null>
79
+ readonly getSchema: (
80
+ workflowId: number,
81
+ opts?: KenobiFetchOptions
82
+ ) => Promise<KenobiSchemaResponse>
83
+ readonly postSchema: (name: string, schema: KenobiPageSchema) => Promise<PostSchemaResponse>
84
+ }