-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcli.js
More file actions
executable file
·308 lines (280 loc) · 11.2 KB
/
Copy pathcli.js
File metadata and controls
executable file
·308 lines (280 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
#!/usr/bin/env node
// CODEX terminal client. Read scripture + run Oracle/panels from the shell.
// Pure Node 18+, no deps. Talks to bible-api.com for verses and to a
// locally-running CODEX server (default http://localhost:3001) for AI calls.
const SERVER = process.env.CODEX_SERVER || "http://localhost:3001";
const BIBLE_API = "https://bible-api.com";
// Tiny ANSI helpers — no chalk, no deps.
const c = {
bold: s => `\x1b[1m${s}\x1b[22m`,
dim: s => `\x1b[2m${s}\x1b[22m`,
cyan: s => `\x1b[36m${s}\x1b[39m`,
white: s => `\x1b[37m${s}\x1b[39m`,
red: s => `\x1b[31m${s}\x1b[39m`,
green: s => `\x1b[32m${s}\x1b[39m`,
yellow:s => `\x1b[33m${s}\x1b[39m`,
};
const HELP = `${c.bold("CODEX cli")} — read scripture and run AI panels from the terminal.
${c.bold("USAGE")}
node cli.js <reference> [--translation <id>] [--panels <a,b,c>]
node cli.js --oracle <question>
node cli.js --search <query>
node cli.js --help
${c.bold("EXAMPLES")}
node cli.js "John 3:16"
node cli.js "John 3:16" --translation kjv
node cli.js "Gen 1" --panels commentary,talmud
node cli.js --oracle "What is grace?"
node cli.js --search "love your enemies"
${c.bold("PANELS")} commentary, talmud, gematria, gnosis, crossRefs (comma-separated, or 'all')
${c.bold("ENV")} CODEX_SERVER (default ${SERVER})
`;
// Hand-rolled arg parser. Boolean & string flags, plus positional ref.
function parseArgs(argv) {
const out = { _: [], flags: {} };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--help" || a === "-h") { out.flags.help = true; continue; }
if (a.startsWith("--")) {
const key = a.slice(2);
const next = argv[i + 1];
if (next === undefined || next.startsWith("--")) out.flags[key] = true;
else { out.flags[key] = next; i++; }
} else {
out._.push(a);
}
}
return out;
}
async function fetchVerse(ref, translation) {
const t = (translation || "web").toLowerCase();
const url = `${BIBLE_API}/${encodeURIComponent(ref)}?translation=${encodeURIComponent(t)}`;
const r = await fetch(url);
if (!r.ok) throw new Error(`bible-api HTTP ${r.status} for "${ref}" (${t})`);
return r.json();
}
function renderPassage(data, translation) {
const ref = data.reference || "(passage)";
const t = (data.translation_id || translation || "web").toUpperCase();
console.log("");
console.log(c.bold(c.cyan(ref)) + " " + c.dim(`[${t}]`));
console.log(c.dim("─".repeat(Math.min(60, ref.length + t.length + 4))));
const verses = Array.isArray(data.verses) ? data.verses : [];
if (verses.length === 0 && data.text) {
console.log(c.white(data.text.trim()));
} else {
for (const v of verses) {
const num = c.dim(`${v.chapter}:${v.verse}`);
console.log(` ${num} ${c.white((v.text || "").trim())}`);
}
}
console.log("");
}
async function callChat({ system, messages, max_tokens }) {
let r;
try {
r = await fetch(`${SERVER}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ system, messages, max_tokens: max_tokens || 1024 }),
});
} catch (e) {
throw new Error(`Cannot reach CODEX server at ${SERVER} — is it running? (start with: node server.js)\n underlying: ${e.message}`);
}
const data = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(data.error || `server HTTP ${r.status}`);
return data;
}
async function runOracle(question) {
const system = "You are the CODEX ORACLE — a scholarly, multi-tradition Bible study companion. Answer clearly and concisely, drawing on Christian, Jewish, and esoteric traditions where relevant. No proselytising.";
console.log(c.dim(`\nOracle ← ${question}\n`));
const data = await callChat({
system,
messages: [{ role: "user", content: question }],
max_tokens: 1200,
});
console.log(c.white((data.text || "").trim()));
console.log("");
if (data.usage) console.log(c.dim(` tokens: in=${data.usage.input_tokens||0} out=${data.usage.output_tokens||0}`));
}
// System prompt replicated from panels-gen.js. Kept in-sync manually.
const PANELS_SYSTEM = `You are the CODEX PANEL DRAFTER. Output a single JSON object describing companion study material for a Bible passage. Scholarly, multi-tradition, never proselytising.
OUTPUT FORMAT — RETURN ONLY a single JSON object, no prose, no fences. Be COMPACT. Schema:
{
"title": "4-6 words, may use Greek/Hebrew",
"subtitle": "one short clause naming the passage's main theme",
"talmud": [
{ "ref":"e.g. b. Berakhot 7a / Genesis Rabbah 1:1", "heading":"short heading",
"body":"40-70 words of scholarly Talmudic/midrashic parallel",
"tag":"short Hebrew/Aramaic + transliteration in 'quotes'" }
],
"commentary": [
{ "from":"Patristic|Reformation|Modern|Devotional",
"author":"specific commentator + work",
"body":"40-60 words" }
],
"gematria": [
{ "term":"word in native script", "translit":"...",
"meaning":"2-4 word gloss", "value":<int>,
"system":"Mispar Hechrachi|Greek isopsephy" }
],
"gematriaNotes": [ "..." ],
"gnosis": [
{ "sigil":"single unicode glyph", "title":"esoteric reading title",
"body":"40-70 words, gnostic/hermetic/kabbalistic/perennialist lens" }
],
"crossRefs": [
{ "ref":"Book ch:vv", "note":"under 10 words" }
]
}
Rules:
- Use accurate citations when known; otherwise pick plausible tractates for the topic.
- Calm scholarly tone. No exclamations. No emoji (sigils OK).
- Real gematria values (אהבה=13, λόγος=373, etc.).
- Return ONLY the JSON. No commentary outside it. Stay compact so the response completes.`;
function extractJSON(text) {
if (!text) throw new Error("empty response");
let s = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "").trim();
const i = s.indexOf("{");
const j = s.lastIndexOf("}");
if (i === -1) throw new Error("no JSON found in model output");
return JSON.parse(s.slice(i, j + 1));
}
function wrap(text, width, indent) {
const pad = " ".repeat(indent);
const words = String(text || "").split(/\s+/);
const lines = [];
let line = "";
for (const w of words) {
if ((line + " " + w).trim().length > width) { lines.push(pad + line.trim()); line = w; }
else line += " " + w;
}
if (line.trim()) lines.push(pad + line.trim());
return lines.join("\n");
}
function renderPanels(obj, which) {
const want = new Set(which);
const all = want.has("all");
console.log("");
console.log(c.bold(c.cyan(obj.title || "Untitled")));
if (obj.subtitle) console.log(c.dim(obj.subtitle));
console.log("");
const section = (key, label) => {
if (!all && !want.has(key)) return;
const arr = obj[key] || [];
if (!arr.length && !(key === "gematria" && (obj.gematriaNotes || []).length)) return;
console.log(c.bold(c.yellow("▸ " + label.toUpperCase())));
console.log("");
if (key === "talmud") {
for (const t of arr) {
console.log(" " + c.cyan(t.ref || "") + " " + c.dim(t.tag || ""));
if (t.heading) console.log(" " + c.bold(t.heading));
console.log(wrap(t.body, 76, 2));
console.log("");
}
} else if (key === "commentary") {
for (const cm of arr) {
console.log(" " + c.cyan(`[${cm.from || "?"}]`) + " " + c.bold(cm.author || ""));
console.log(wrap(cm.body, 76, 2));
console.log("");
}
} else if (key === "gematria") {
for (const g of arr) {
console.log(` ${c.bold(g.term || "")} ${c.dim(g.translit || "")} = ${c.yellow(String(g.value))} ${c.dim(`(${g.meaning || ""} · ${g.system || ""})`)}`);
}
if ((obj.gematriaNotes || []).length) {
console.log("");
for (const n of obj.gematriaNotes) console.log(wrap("• " + n, 76, 2));
}
console.log("");
} else if (key === "gnosis") {
for (const g of arr) {
console.log(" " + c.cyan(g.sigil || "•") + " " + c.bold(g.title || ""));
console.log(wrap(g.body, 76, 2));
console.log("");
}
} else if (key === "crossRefs") {
for (const x of arr) console.log(" " + c.cyan(x.ref) + " " + c.dim(x.note || ""));
console.log("");
}
};
section("commentary", "Commentary");
section("talmud", "Talmud / Midrash");
section("gematria", "Gematria");
section("gnosis", "Gnosis");
section("crossRefs", "Cross References");
}
async function runPanels(ref, panelList) {
const which = (panelList === true || !panelList ? ["all"] : String(panelList).split(",").map(s => s.trim()).filter(Boolean));
console.log(c.dim(`\nGenerating panels for ${ref}…`));
const data = await callChat({
system: PANELS_SYSTEM,
messages: [{ role: "user", content: `Draft the CODEX panels for: ${ref}.\nReturn ONLY the JSON object as specified in the system instructions.` }],
max_tokens: 3000,
});
let parsed;
try { parsed = extractJSON(data.text || ""); }
catch (e) {
console.error(c.red("Failed to parse panel JSON: " + e.message));
console.error(c.dim((data.text || "").slice(0, 500)));
process.exit(1);
}
renderPanels(parsed, which);
}
async function runSearch(query) {
const system = "You are a Bible scripture-search helper. The user gives a phrase or theme; return a JSON array of up to 8 matching passages. Schema: [{\"ref\":\"Book ch:vv\",\"translation\":\"WEB\",\"text\":\"verse text\",\"why\":\"under 12 words why this matches\"}]. Return ONLY the JSON array.";
console.log(c.dim(`\nSearching: ${query}\n`));
const data = await callChat({
system,
messages: [{ role: "user", content: query }],
max_tokens: 1500,
});
let arr;
try {
const s = (data.text || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "");
const i = s.indexOf("["), j = s.lastIndexOf("]");
arr = JSON.parse(s.slice(i, j + 1));
} catch (e) {
console.error(c.red("Failed to parse search results: " + e.message));
console.error(c.dim((data.text || "").slice(0, 500)));
process.exit(1);
}
for (const hit of arr) {
console.log(c.bold(c.cyan(hit.ref || "?")) + " " + c.dim(`[${hit.translation || "WEB"}]`));
console.log(wrap(hit.text || "", 76, 2));
if (hit.why) console.log(c.dim(" → " + hit.why));
console.log("");
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.flags.help || (args._.length === 0 && !args.flags.oracle && !args.flags.search)) {
console.log(HELP);
process.exit(args.flags.help ? 0 : 1);
}
try {
if (args.flags.oracle) {
const q = args.flags.oracle === true ? args._.join(" ") : args.flags.oracle;
if (!q) throw new Error("--oracle needs a question");
await runOracle(q);
return;
}
if (args.flags.search) {
const q = args.flags.search === true ? args._.join(" ") : args.flags.search;
if (!q) throw new Error("--search needs a query");
await runSearch(q);
return;
}
const ref = args._.join(" ").trim();
if (!ref) throw new Error("missing scripture reference");
const translation = args.flags.translation || "web";
// Always fetch + print the passage first.
const data = await fetchVerse(ref, translation);
renderPassage(data, translation);
if (args.flags.panels) await runPanels(ref, args.flags.panels);
} catch (e) {
console.error(c.red("Error: ") + (e.message || String(e)));
process.exit(1);
}
}
main();