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 +50 -0
- package/package.json +35 -0
- package/src/build.ts +50 -0
- package/src/cli.ts +396 -0
- package/src/client.ts +96 -0
- package/src/errors.ts +25 -0
- package/src/index.ts +12 -0
- package/src/types.ts +84 -0
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
|
+
}
|