ivorleaf 1.1.0

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/dist/render.js ADDED
@@ -0,0 +1,600 @@
1
+ import chalk from "chalk";
2
+ import { marked } from "marked";
3
+ import { markedTerminal } from "marked-terminal";
4
+ import { getActiveSession } from "./utils/activeSession.js";
5
+ export const CLI_VERSION = "1.1.0";
6
+ const PRIMARY_KEYS = [
7
+ "markdown", "content", "lesson_markdown", "workflow_markdown", "quiz_markdown",
8
+ "flashcards_markdown", "table_markdown", "citations_markdown", "output_markdown",
9
+ "lesson", "output", "result", "text", "response", "data",
10
+ ];
11
+ const SOURCE_KEYS = new Set(["sources", "source_details", "references", "citations"]);
12
+ const WARNING_KEYS = new Set(["warnings", "citation_warnings", "citation_warning"]);
13
+ const WORKFLOW_KEYS = new Set(["available_actions", "workflows", "actions"]);
14
+ const METADATA_KEYS = new Set([
15
+ "session_id", "sessionId", "id", "action", "action_name", "type", "status",
16
+ "created_at", "updated_at", "metadata", "usage", "plan", "organization",
17
+ ...SOURCE_KEYS, ...WARNING_KEYS, ...WORKFLOW_KEYS,
18
+ ]);
19
+ const DEFAULT_SOURCE_ACTIONS = new Set(["citation_builder", "source_comparison"]);
20
+ export function printJson(value) {
21
+ console.log(JSON.stringify(value, null, 2));
22
+ }
23
+ export function extractMarkdown(value) {
24
+ return extractPrimary(value) || "No displayable content was returned.";
25
+ }
26
+ export function extractSources(value) {
27
+ return uniqueValues(collectNamedValues(value, SOURCE_KEYS).flatMap(normalizeCollection));
28
+ }
29
+ export function formatResponse(value, options = {}, context = {}) {
30
+ const sections = [];
31
+ const rawContent = extractMarkdown(value).trim();
32
+ const content = context.kind === "workflow" && context.actionName === "table"
33
+ ? formatTableWorkflowMarkdown(rawContent)
34
+ : rawContent;
35
+ const title = context.kind === "workflow" ? workflowTitle(value, context.actionName) : null;
36
+ if (title && !startsWithEquivalentHeading(content, title))
37
+ sections.push(`# ${title}`);
38
+ sections.push(content);
39
+ const sources = extractSources(value);
40
+ const warnings = uniqueValues(collectNamedValues(value, WARNING_KEYS).flatMap(normalizeCollection));
41
+ const showFullSources = options.sources || DEFAULT_SOURCE_ACTIONS.has(context.actionName || "");
42
+ if (showFullSources && sources.length)
43
+ sections.push(formatSources(sources));
44
+ else if (context.kind === "lesson") {
45
+ if (sources.length)
46
+ sections.push(`Sources: ${sources.length} available`);
47
+ if (warnings.length)
48
+ sections.push(formatWarnings(warnings));
49
+ }
50
+ if (context.sessionId)
51
+ sections.push(`Session: ${context.sessionId}`);
52
+ if (options.debugResponse)
53
+ sections.push(formatDebugResponse(value, context, sources.length, warnings.length));
54
+ return sections.filter(Boolean).join("\n\n");
55
+ }
56
+ export function formatTableWorkflowMarkdown(markdown, width = terminalMarkdownWidth()) {
57
+ const lines = markdown.split(/\r?\n/);
58
+ const formatted = [];
59
+ for (let index = 0; index < lines.length;) {
60
+ if (!isMarkdownTableAt(lines, index)) {
61
+ formatted.push(lines[index]);
62
+ index += 1;
63
+ continue;
64
+ }
65
+ const block = [lines[index], lines[index + 1]];
66
+ index += 2;
67
+ while (index < lines.length && isTableRow(lines[index])) {
68
+ block.push(lines[index]);
69
+ index += 1;
70
+ }
71
+ formatted.push(formatTableBlock(block, width));
72
+ }
73
+ return formatted.join("\n").trim();
74
+ }
75
+ export async function renderResponse(value, options, context = {}) {
76
+ if (options.json)
77
+ return printJson(value);
78
+ const markdown = formatResponse(value, options, context);
79
+ const summary = responseSummary(value, context);
80
+ const shouldPager = shouldUsePager(markdown, options);
81
+ if (options.plain || options.noPager)
82
+ return console.log(markdown);
83
+ if (!process.stdout.isTTY && !options.pager)
84
+ return console.log(markdown);
85
+ const rendered = renderTerminalMarkdown(markdown);
86
+ if (shouldPager) {
87
+ if (canUseInteractivePager()) {
88
+ try {
89
+ return await renderInteractivePager(rendered, summary);
90
+ }
91
+ catch {
92
+ return console.log(formatPaginatedOutput(rendered, summary, { forceMarkers: true }));
93
+ }
94
+ }
95
+ return console.log(formatPaginatedOutput(rendered, summary, { forceMarkers: true }));
96
+ }
97
+ console.log(rendered);
98
+ }
99
+ export function renderTerminalMarkdown(markdown) {
100
+ marked.use(markedTerminal({
101
+ reflowText: true,
102
+ width: Math.min(Math.max(process.stdout.columns || 80, 50), 100),
103
+ tab: 2,
104
+ firstHeading: chalk.bold.green,
105
+ heading: chalk.bold.cyan,
106
+ strong: chalk.bold,
107
+ em: chalk.italic,
108
+ codespan: chalk.yellow,
109
+ blockquote: chalk.gray,
110
+ hr: chalk.dim,
111
+ link: chalk.cyan,
112
+ href: chalk.dim,
113
+ table: chalk.white,
114
+ }));
115
+ return String(marked.parse(markdown)).trimEnd();
116
+ }
117
+ export function formatPaginatedOutput(content, summary, options = {}) {
118
+ const height = Math.max(8, options.height || process.stdout.rows || 24);
119
+ const width = Math.max(40, options.width || process.stdout.columns || 80);
120
+ const header = formatFixedStatus(summary, width);
121
+ const footer = formatStickyFooter();
122
+ const headerHeight = header.split("\n").length;
123
+ const footerHeight = footer.split("\n").length;
124
+ const paneHeight = Math.max(3, height - headerHeight - footerHeight - 2);
125
+ const lines = wrapOutputLines(content, width);
126
+ const pages = [];
127
+ for (let start = 0; start < Math.max(lines.length, 1); start += paneHeight) {
128
+ const pageLines = lines.slice(start, start + paneHeight);
129
+ const page = [
130
+ header,
131
+ "─".repeat(width),
132
+ pageLines.join("\n"),
133
+ "",
134
+ footer,
135
+ ].join("\n").trimEnd();
136
+ const hasMore = start + paneHeight < lines.length;
137
+ pages.push(hasMore || options.forceMarkers ? `${page}\n--More--` : page);
138
+ }
139
+ return pages.join("\n\n");
140
+ }
141
+ function extractPrimary(value, seen = new Set()) {
142
+ if (typeof value === "string")
143
+ return value;
144
+ if (typeof value === "number" || typeof value === "boolean")
145
+ return String(value);
146
+ if (!value || typeof value !== "object" || seen.has(value))
147
+ return "";
148
+ seen.add(value);
149
+ if (Array.isArray(value))
150
+ return formatArray(value, seen);
151
+ const object = value;
152
+ for (const key of PRIMARY_KEYS) {
153
+ if (object[key] === undefined)
154
+ continue;
155
+ const content = extractPrimary(object[key], seen);
156
+ if (content)
157
+ return content;
158
+ }
159
+ return formatObject(object, seen);
160
+ }
161
+ function formatArray(items, seen) {
162
+ return items.map((item, index) => {
163
+ if (isObject(item)) {
164
+ const question = stringValue(item.question ?? item.prompt ?? item.front ?? item.term);
165
+ const answer = stringValue(item.answer ?? item.correct_answer ?? item.back ?? item.definition);
166
+ const explanation = stringValue(item.explanation ?? item.rationale);
167
+ const options = Array.isArray(item.options) ? item.options : Array.isArray(item.choices) ? item.choices : [];
168
+ if (question || answer || options.length) {
169
+ const lines = [`## ${index + 1}. ${question || "Item"}`];
170
+ if (options.length)
171
+ lines.push(options.map(option => `- ${stringValue(option)}`).join("\n"));
172
+ if (answer)
173
+ lines.push(`**Answer:** ${answer}`);
174
+ if (explanation)
175
+ lines.push(explanation);
176
+ return lines.join("\n\n");
177
+ }
178
+ }
179
+ const rendered = extractPrimary(item, seen);
180
+ return rendered ? `- ${rendered.replace(/\n/g, "\n ")}` : "";
181
+ }).filter(Boolean).join("\n\n");
182
+ }
183
+ function formatObject(object, seen) {
184
+ const sections = [];
185
+ for (const [key, value] of Object.entries(object)) {
186
+ if (METADATA_KEYS.has(key) || value === null || value === undefined)
187
+ continue;
188
+ const rendered = extractPrimary(value, seen);
189
+ if (!rendered)
190
+ continue;
191
+ if (typeof value === "string" && Object.keys(object).length === 1)
192
+ sections.push(rendered);
193
+ else
194
+ sections.push(`## ${humanize(key)}\n\n${rendered}`);
195
+ }
196
+ return sections.join("\n\n");
197
+ }
198
+ function responseSummary(value, context) {
199
+ const sources = extractSources(value);
200
+ const warnings = uniqueValues(collectNamedValues(value, WARNING_KEYS).flatMap(normalizeCollection));
201
+ const workflows = uniqueValues(collectNamedValues(value, WORKFLOW_KEYS).flatMap(normalizeCollection))
202
+ .map(workflow => typeof workflow === "string" ? workflow : stringValue(workflow))
203
+ .filter(Boolean)
204
+ .slice(0, 8);
205
+ return {
206
+ version: process.env.npm_package_version || CLI_VERSION,
207
+ command: context.command || context.actionName || context.kind || "command",
208
+ sessionId: context.sessionId || "None",
209
+ title: responseTitle(value, context),
210
+ sourceCount: sources.length,
211
+ warningCount: warnings.length,
212
+ workflows,
213
+ };
214
+ }
215
+ function responseTitle(value, context) {
216
+ const named = findNamedString(value, ["title", "subject", "topic", "name"]);
217
+ if (named)
218
+ return named;
219
+ const prompt = findNamedString(value, ["prompt", "query", "rewritten_prompt", "rewritten_query"]);
220
+ if (prompt)
221
+ return truncateText(prompt, 72);
222
+ const firstHeading = extractMarkdown(value).split(/\r?\n/).find(line => /^#{1,3}\s+\S/.test(line));
223
+ if (firstHeading)
224
+ return truncateText(firstHeading.replace(/^#{1,3}\s+/, "").trim(), 72);
225
+ return context.kind === "workflow" ? humanize(context.actionName || "workflow") : "Lesson";
226
+ }
227
+ function shouldUsePager(markdown, options) {
228
+ if (options.noPager || options.plain || options.json)
229
+ return false;
230
+ if (options.pager)
231
+ return true;
232
+ if (!process.stdout.isTTY)
233
+ return false;
234
+ const height = Math.max(8, process.stdout.rows || 24);
235
+ return wrapOutputLines(markdown, Math.max(40, process.stdout.columns || 80)).length > height - 6;
236
+ }
237
+ function canUseInteractivePager() {
238
+ return Boolean(process.stdin.isTTY
239
+ && process.stdout.isTTY
240
+ && typeof process.stdin.setRawMode === "function"
241
+ && typeof process.stdin.on === "function");
242
+ }
243
+ async function renderInteractivePager(content, summary) {
244
+ const height = Math.max(8, process.stdout.rows || 24);
245
+ const width = Math.max(40, process.stdout.columns || 80);
246
+ const header = formatFixedStatus(summary, width);
247
+ const footer = formatStickyFooter();
248
+ const paneHeight = Math.max(3, height - header.split("\n").length - footer.split("\n").length - 2);
249
+ const lines = wrapOutputLines(content, width);
250
+ let offset = 0;
251
+ const renderPage = () => {
252
+ const pageLines = lines.slice(offset, offset + paneHeight);
253
+ process.stdout.write("\x1b[2J\x1b[H");
254
+ process.stdout.write(`${header}\n${"─".repeat(width)}\n`);
255
+ process.stdout.write(`${pageLines.join("\n")}\n\n${footer}`);
256
+ if (offset + paneHeight < lines.length)
257
+ process.stdout.write("\n--More--");
258
+ };
259
+ const stdin = process.stdin;
260
+ const wasRaw = Boolean(stdin.isRaw);
261
+ try {
262
+ stdin.setRawMode(true);
263
+ stdin.resume();
264
+ renderPage();
265
+ while (offset + paneHeight < lines.length) {
266
+ const key = await readPagerKey();
267
+ if (key === "q" || key === "\u0003")
268
+ break;
269
+ if (key === "\r" || key === "\n" || key === " " || key === "\x1b[B") {
270
+ offset = Math.min(offset + paneHeight, Math.max(lines.length - paneHeight, 0));
271
+ }
272
+ else if (key === "\x1b[A") {
273
+ offset = Math.max(offset - paneHeight, 0);
274
+ }
275
+ else if (key === "s" || key === "c" || key === "w") {
276
+ renderPagerHelp(summary, key, width);
277
+ break;
278
+ }
279
+ else {
280
+ continue;
281
+ }
282
+ renderPage();
283
+ }
284
+ process.stdout.write("\n");
285
+ }
286
+ finally {
287
+ try {
288
+ stdin.setRawMode(wasRaw);
289
+ }
290
+ catch {
291
+ // Ignore terminal cleanup failures; the caller falls back to non-interactive output.
292
+ }
293
+ stdin.pause();
294
+ }
295
+ }
296
+ function readPagerKey() {
297
+ return new Promise(resolve => {
298
+ const onData = (chunk) => {
299
+ cleanup();
300
+ resolve(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
301
+ };
302
+ const onError = () => {
303
+ cleanup();
304
+ resolve("q");
305
+ };
306
+ const cleanup = () => {
307
+ process.stdin.off("data", onData);
308
+ process.stdin.off("error", onError);
309
+ };
310
+ process.stdin.once("data", onData);
311
+ process.stdin.once("error", onError);
312
+ });
313
+ }
314
+ function renderPagerHelp(summary, key, width) {
315
+ const line = "─".repeat(width);
316
+ if (key === "s") {
317
+ process.stdout.write(`\n${line}\nSources: run ivorleaf sources\n`);
318
+ return;
319
+ }
320
+ if (key === "c") {
321
+ process.stdout.write(`\n${line}\nCitations: run ivorleaf citations${summary.sessionId !== "None" ? ` ${summary.sessionId}` : ""}\n`);
322
+ return;
323
+ }
324
+ const workflows = summary.workflows.length
325
+ ? summary.workflows.join(", ")
326
+ : "quiz, flashcards, citations, compare, continue";
327
+ process.stdout.write(`\n${line}\nWorkflows: ${workflows}\n`);
328
+ }
329
+ function formatFixedStatus(summary, width) {
330
+ const workflows = summary.workflows.length ? summary.workflows.join(", ") : "workflow menu";
331
+ const lines = [
332
+ `Ivorleaf CLI ${summary.version}`,
333
+ `Command: ${summary.command}`,
334
+ `Session: ${summary.sessionId}`,
335
+ `Topic: ${summary.title}`,
336
+ `Sources: ${summary.sourceCount} Warnings: ${summary.warningCount}`,
337
+ `Workflows: ${workflows}`,
338
+ ];
339
+ return lines.map(line => truncateText(line, width)).join("\n");
340
+ }
341
+ function formatStickyFooter() {
342
+ return "q quit | ↑/↓ scroll | Enter continue | s sources | c citations | w workflows";
343
+ }
344
+ function wrapOutputLines(content, width) {
345
+ const max = Math.max(20, width);
346
+ return content.split(/\r?\n/).flatMap(line => {
347
+ const stripped = stripAnsi(line);
348
+ if (stripped.length <= max)
349
+ return [line];
350
+ const chunks = [];
351
+ let remaining = line;
352
+ while (stripAnsi(remaining).length > max) {
353
+ chunks.push(remaining.slice(0, max));
354
+ remaining = remaining.slice(max);
355
+ }
356
+ chunks.push(remaining);
357
+ return chunks;
358
+ });
359
+ }
360
+ function collectNamedValues(value, keys, seen = new Set()) {
361
+ if (!value || typeof value !== "object" || seen.has(value))
362
+ return [];
363
+ seen.add(value);
364
+ if (Array.isArray(value))
365
+ return value.flatMap(item => collectNamedValues(item, keys, seen));
366
+ const found = [];
367
+ for (const [key, child] of Object.entries(value)) {
368
+ if (keys.has(key))
369
+ found.push(child);
370
+ else
371
+ found.push(...collectNamedValues(child, keys, seen));
372
+ }
373
+ return found;
374
+ }
375
+ function normalizeCollection(value) {
376
+ if (value === null || value === undefined || value === "")
377
+ return [];
378
+ if (Array.isArray(value))
379
+ return value;
380
+ if (isObject(value))
381
+ return Object.values(value);
382
+ if (typeof value === "number")
383
+ return Array.from({ length: value }, () => null);
384
+ return [value];
385
+ }
386
+ function uniqueValues(values) {
387
+ const seen = new Set();
388
+ return values.filter(value => {
389
+ // Null placeholders preserve count-only metadata such as `sources: 3`.
390
+ if (value === null)
391
+ return true;
392
+ const key = value && typeof value === "object" ? JSON.stringify(value) : String(value);
393
+ if (seen.has(key))
394
+ return false;
395
+ seen.add(key);
396
+ return true;
397
+ });
398
+ }
399
+ export function formatSources(sources) {
400
+ const lines = sources.map((source, index) => {
401
+ const fallbackLabel = `S${index + 1}`;
402
+ if (typeof source === "string")
403
+ return `${fallbackLabel}. ${source}`;
404
+ if (!isObject(source))
405
+ return `${fallbackLabel}. Source ${index + 1}`;
406
+ const label = stringValue(source.id ?? source.source_label ?? source.label) || fallbackLabel;
407
+ const title = stringValue(source.title ?? source.name ?? source.citation) || `Source ${index + 1}`;
408
+ const provider = stringValue(source.provider ?? source.source ?? source.source_type);
409
+ const url = stringValue(source.url ?? source.href ?? source.link ?? source.doi ?? source.openalex_id);
410
+ const linkUrl = /^https?:\/\//i.test(url) ? url : "";
411
+ const author = stringValue(source.author ?? source.authors);
412
+ const relevance = formatRelevance(source.citation_relevance);
413
+ const status = stringValue(source.status ?? source.citation_priority ?? source.type);
414
+ const detail = stringValue(source.excerpt ?? source.snippet ?? source.summary);
415
+ const heading = linkUrl ? `${label}. [${title}](${linkUrl})` : `${label}. ${title}`;
416
+ return [
417
+ heading,
418
+ provider && ` Provider: ${provider}`,
419
+ author && ` Authors: ${author}`,
420
+ url && ` URL/DOI: ${url}`,
421
+ relevance && ` Relevance: ${relevance}`,
422
+ status && ` Status: ${status}`,
423
+ detail && ` ${detail}`,
424
+ ].filter(Boolean).join("\n");
425
+ });
426
+ return `## Sources\n\n${lines.join("\n")}`;
427
+ }
428
+ function formatWarnings(warnings) {
429
+ const lines = warnings.map(warning => {
430
+ if (typeof warning === "string")
431
+ return `- ${warning}`;
432
+ if (!isObject(warning))
433
+ return `- ${stringValue(warning) || "Citation warning"}`;
434
+ const message = stringValue(warning.message ?? warning.warning ?? warning.detail ?? warning.reason ?? warning.text);
435
+ return `- ${message || JSON.stringify(warning)}`;
436
+ });
437
+ return `Warnings:\n${lines.join("\n")}`;
438
+ }
439
+ function formatDebugResponse(value, context, sourceCount, warningCount) {
440
+ const object = isObject(value) ? value : {};
441
+ const route = isObject(object.route) ? object.route : {};
442
+ const usage = isObject(object.usage) ? object.usage : {};
443
+ const sessionValue = object.session_id ?? object.sessionId;
444
+ const lines = [
445
+ `Root keys: ${Object.keys(object).join(", ") || "(none)"}`,
446
+ `Top-level session_id: ${sessionValue === null || sessionValue === undefined ? String(sessionValue) : typeof sessionValue}`,
447
+ `Discovered session: ${context.sessionId || "None"}`,
448
+ `Plan: ${context.plan || "Unknown"}`,
449
+ `Route keys: ${Object.keys(route).join(", ") || "(none)"}`,
450
+ `Route client: ${stringValue(route.client) || "Unknown"}`,
451
+ `Usage keys: ${Object.keys(usage).join(", ") || "(none)"}`,
452
+ `Sources discovered: ${sourceCount}`,
453
+ `Citation warnings discovered: ${warningCount}`,
454
+ ];
455
+ return `## Debug Response\n\n${lines.join("\n")}`;
456
+ }
457
+ function formatRelevance(value) {
458
+ if (!isObject(value))
459
+ return "";
460
+ const score = typeof value.score === "number" ? value.score : null;
461
+ const rejected = typeof value.rejected === "boolean" ? value.rejected : null;
462
+ const reason = stringValue(value.reason);
463
+ const parts = [
464
+ score !== null && `score ${Number.isInteger(score) ? score : score.toFixed(2)}`,
465
+ rejected !== null && (rejected ? "rejected" : "accepted"),
466
+ reason,
467
+ ].filter(Boolean);
468
+ return parts.join("; ");
469
+ }
470
+ function workflowTitle(value, actionName) {
471
+ const title = findNamedString(value, ["workflow_title", "title", "name"]);
472
+ return title || humanize(actionName || "workflow");
473
+ }
474
+ function findNamedString(value, keys, seen = new Set()) {
475
+ if (!value || typeof value !== "object" || seen.has(value))
476
+ return null;
477
+ seen.add(value);
478
+ if (Array.isArray(value)) {
479
+ for (const item of value) {
480
+ const found = findNamedString(item, keys, seen);
481
+ if (found)
482
+ return found;
483
+ }
484
+ return null;
485
+ }
486
+ const object = value;
487
+ for (const key of keys)
488
+ if (typeof object[key] === "string")
489
+ return object[key];
490
+ for (const [key, child] of Object.entries(object)) {
491
+ if (SOURCE_KEYS.has(key) || WARNING_KEYS.has(key) || key === "metadata")
492
+ continue;
493
+ const found = findNamedString(child, keys, seen);
494
+ if (found)
495
+ return found;
496
+ }
497
+ return null;
498
+ }
499
+ function startsWithEquivalentHeading(markdown, title) {
500
+ const first = markdown.split("\n", 1)[0].replace(/^#+\s*/, "").trim().toLowerCase();
501
+ return first === title.trim().toLowerCase();
502
+ }
503
+ function formatTableBlock(block, width) {
504
+ if (!isOversizedTable(block, width))
505
+ return block.join("\n");
506
+ const headers = splitTableRow(block[0]).map(cell => cell || "Field");
507
+ const records = block.slice(2).map(splitTableRow).filter(cells => cells.some(Boolean));
508
+ if (!headers.length || !records.length)
509
+ return block.join("\n");
510
+ return records.map((cells, index) => {
511
+ const fields = headers.map((header, cellIndex) => {
512
+ const value = cells[cellIndex]?.trim();
513
+ return value ? `**${header}:** ${value}` : "";
514
+ }).filter(Boolean);
515
+ return [`### Record ${index + 1}`, ...fields].join("\n\n");
516
+ }).join("\n\n---\n\n");
517
+ }
518
+ function isOversizedTable(block, width) {
519
+ const maxLineLength = Math.max(...block.map(line => stripAnsi(line).length));
520
+ if (maxLineLength > width)
521
+ return true;
522
+ const cells = block.filter((_, index) => index !== 1).flatMap(splitTableRow);
523
+ const longCellThreshold = Math.max(28, Math.floor(width * 0.35));
524
+ return cells.some(cell => cell.length > longCellThreshold);
525
+ }
526
+ function isMarkdownTableAt(lines, index) {
527
+ return index + 1 < lines.length && isTableRow(lines[index]) && isSeparatorRow(lines[index + 1]);
528
+ }
529
+ function isTableRow(line) {
530
+ const trimmed = line.trim();
531
+ return trimmed.includes("|") && splitTableRow(trimmed).length >= 2;
532
+ }
533
+ function isSeparatorRow(line) {
534
+ return splitTableRow(line).every(cell => /^:?-{3,}:?$/.test(cell.trim()));
535
+ }
536
+ function splitTableRow(line) {
537
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
538
+ const cells = [];
539
+ let current = "";
540
+ let escaped = false;
541
+ for (const char of trimmed) {
542
+ if (escaped) {
543
+ current += char;
544
+ escaped = false;
545
+ continue;
546
+ }
547
+ if (char === "\\") {
548
+ current += char;
549
+ escaped = true;
550
+ continue;
551
+ }
552
+ if (char === "|") {
553
+ cells.push(cleanTableCell(current));
554
+ current = "";
555
+ continue;
556
+ }
557
+ current += char;
558
+ }
559
+ cells.push(cleanTableCell(current));
560
+ return cells;
561
+ }
562
+ function cleanTableCell(cell) {
563
+ return cell.trim().replace(/\\\|/g, "|");
564
+ }
565
+ function terminalMarkdownWidth() {
566
+ return Math.min(Math.max(process.stdout.columns || 80, 50), 100);
567
+ }
568
+ function stripAnsi(value) {
569
+ return value.replace(/\u001b\[[0-9;]*m/g, "");
570
+ }
571
+ function humanize(value) {
572
+ return value.replace(/[_-]+/g, " ").replace(/\b\w/g, letter => letter.toUpperCase());
573
+ }
574
+ function truncateText(value, width) {
575
+ if (value.length <= width)
576
+ return value;
577
+ return `${value.slice(0, Math.max(0, width - 1))}…`;
578
+ }
579
+ function isObject(value) {
580
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
581
+ }
582
+ function stringValue(value) {
583
+ if (Array.isArray(value))
584
+ return value.map(stringValue).filter(Boolean).join(", ");
585
+ return typeof value === "string" || typeof value === "number" ? String(value) : "";
586
+ }
587
+ export async function renderHeader(command, options = {}) {
588
+ const headerOptions = typeof options === "string" || options === null
589
+ ? { plan: options }
590
+ : options;
591
+ const session = headerOptions.sessionId ?? await getActiveSession();
592
+ console.log(`${chalk.bold("Ivorleaf CLI")} ${process.env.npm_package_version || CLI_VERSION}`);
593
+ console.log("─".repeat(40));
594
+ console.log(`Plan: ${headerOptions.plan || "Unknown"}`);
595
+ console.log(`Session: ${session || "None"}`);
596
+ console.log(`Command: ${command}\n`);
597
+ }
598
+ export function success(text) { return chalk.green(`✓ ${text}`); }
599
+ export function failure(text) { return chalk.red(`✗ ${text}`); }
600
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1,58 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ export const ACTIVE_SESSION_PATH = join(homedir(), ".ivorleaf", "active-session");
5
+ export function getActiveSessionPath() {
6
+ return process.env.IVORLEAF_CONFIG_DIR
7
+ ? join(process.env.IVORLEAF_CONFIG_DIR, "active-session")
8
+ : ACTIVE_SESSION_PATH;
9
+ }
10
+ export function getActiveSessionMetadataPath() {
11
+ return join(dirname(getActiveSessionPath()), "active-session.json");
12
+ }
13
+ export async function getActiveSession() {
14
+ try {
15
+ return (await readFile(getActiveSessionPath(), "utf8")).trim() || null;
16
+ }
17
+ catch (error) {
18
+ if (error.code === "ENOENT")
19
+ return null;
20
+ throw error;
21
+ }
22
+ }
23
+ export async function setActiveSession(sessionId) {
24
+ const path = getActiveSessionPath();
25
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
26
+ await writeFile(path, `${sessionId}\n`, { mode: 0o600 });
27
+ }
28
+ export async function getActiveSessionMetadata() {
29
+ try {
30
+ const parsed = JSON.parse(await readFile(getActiveSessionMetadataPath(), "utf8"));
31
+ return parsed && typeof parsed === "object" && typeof parsed.sessionId === "string"
32
+ ? parsed
33
+ : null;
34
+ }
35
+ catch (error) {
36
+ if (error.code === "ENOENT")
37
+ return null;
38
+ throw error;
39
+ }
40
+ }
41
+ export async function updateActiveSessionMetadata(metadata) {
42
+ const sessionId = metadata.sessionId.trim();
43
+ if (!sessionId)
44
+ return;
45
+ await setActiveSession(sessionId);
46
+ const path = getActiveSessionMetadataPath();
47
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
48
+ await writeFile(path, `${JSON.stringify({
49
+ ...metadata,
50
+ sessionId,
51
+ updatedAt: metadata.updatedAt || new Date().toISOString(),
52
+ }, null, 2)}\n`, { mode: 0o600 });
53
+ }
54
+ export async function clearActiveSession() {
55
+ await rm(getActiveSessionPath(), { force: true });
56
+ await rm(getActiveSessionMetadataPath(), { force: true });
57
+ }
58
+ //# sourceMappingURL=activeSession.js.map
@@ -0,0 +1,4 @@
1
+ export function joinPromptParts(promptParts) {
2
+ return promptParts.join(" ").trim();
3
+ }
4
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1,27 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { getActiveSessionPath } from "./activeSession.js";
4
+ export function getRecentSourcesPath() {
5
+ return join(dirname(getActiveSessionPath()), "recent-sources.json");
6
+ }
7
+ export async function getRecentSources() {
8
+ try {
9
+ const text = await readFile(getRecentSourcesPath(), "utf8");
10
+ const parsed = JSON.parse(text);
11
+ return Array.isArray(parsed?.sources) ? parsed.sources : [];
12
+ }
13
+ catch (error) {
14
+ if (error.code === "ENOENT")
15
+ return [];
16
+ throw error;
17
+ }
18
+ }
19
+ export async function setRecentSources(sources) {
20
+ const path = getRecentSourcesPath();
21
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
22
+ await writeFile(path, `${JSON.stringify({ sources }, null, 2)}\n`, { mode: 0o600 });
23
+ }
24
+ export async function clearRecentSources() {
25
+ await rm(getRecentSourcesPath(), { force: true });
26
+ }
27
+ //# sourceMappingURL=recentSources.js.map