@ambicuity/kindx 0.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.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * renderer.ts - Output formatting utilities for QMD
3
+ *
4
+ * Provides methods to format search results and documents into various output formats:
5
+ * JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
6
+ */
7
+ import type { SearchResult, MultiGetResult, DocumentResult } from "./repository.js";
8
+ export type { SearchResult, MultiGetResult, DocumentResult };
9
+ export type MultiGetFile = {
10
+ filepath: string;
11
+ displayPath: string;
12
+ title: string;
13
+ body: string;
14
+ context?: string | null;
15
+ skipped: false;
16
+ } | {
17
+ filepath: string;
18
+ displayPath: string;
19
+ title: string;
20
+ body: string;
21
+ context?: string | null;
22
+ skipped: true;
23
+ skipReason: string;
24
+ };
25
+ export type OutputFormat = "cli" | "csv" | "md" | "xml" | "files" | "json";
26
+ export type FormatOptions = {
27
+ full?: boolean;
28
+ query?: string;
29
+ useColor?: boolean;
30
+ lineNumbers?: boolean;
31
+ };
32
+ /**
33
+ * Add line numbers to text content.
34
+ * Each line becomes: "{lineNum}: {content}"
35
+ * @param text The text to add line numbers to
36
+ * @param startLine Optional starting line number (default: 1)
37
+ */
38
+ export declare function addLineNumbers(text: string, startLine?: number): string;
39
+ /**
40
+ * Extract short docid from a full hash (first 6 characters).
41
+ */
42
+ export declare function getDocid(hash: string): string;
43
+ export declare function escapeCSV(value: string | null | number): string;
44
+ export declare function escapeXml(str: string): string;
45
+ /**
46
+ * Format search results as JSON
47
+ */
48
+ export declare function searchResultsToJson(results: SearchResult[], opts?: FormatOptions): string;
49
+ /**
50
+ * Format search results as CSV
51
+ */
52
+ export declare function searchResultsToCsv(results: SearchResult[], opts?: FormatOptions): string;
53
+ /**
54
+ * Format search results as simple files list (docid,score,filepath,context)
55
+ */
56
+ export declare function searchResultsToFiles(results: SearchResult[]): string;
57
+ /**
58
+ * Format search results as Markdown
59
+ */
60
+ export declare function searchResultsToMarkdown(results: SearchResult[], opts?: FormatOptions): string;
61
+ /**
62
+ * Format search results as XML
63
+ */
64
+ export declare function searchResultsToXml(results: SearchResult[], opts?: FormatOptions): string;
65
+ /**
66
+ * Format search results for MCP (simpler CSV format with pre-extracted snippets)
67
+ */
68
+ export declare function searchResultsToMcpCsv(results: {
69
+ docid: string;
70
+ file: string;
71
+ title: string;
72
+ score: number;
73
+ context: string | null;
74
+ snippet: string;
75
+ }[]): string;
76
+ /**
77
+ * Format documents as JSON
78
+ */
79
+ export declare function documentsToJson(results: MultiGetFile[]): string;
80
+ /**
81
+ * Format documents as CSV
82
+ */
83
+ export declare function documentsToCsv(results: MultiGetFile[]): string;
84
+ /**
85
+ * Format documents as files list
86
+ */
87
+ export declare function documentsToFiles(results: MultiGetFile[]): string;
88
+ /**
89
+ * Format documents as Markdown
90
+ */
91
+ export declare function documentsToMarkdown(results: MultiGetFile[]): string;
92
+ /**
93
+ * Format documents as XML
94
+ */
95
+ export declare function documentsToXml(results: MultiGetFile[]): string;
96
+ /**
97
+ * Format a single DocumentResult as JSON
98
+ */
99
+ export declare function documentToJson(doc: DocumentResult): string;
100
+ /**
101
+ * Format a single DocumentResult as Markdown
102
+ */
103
+ export declare function documentToMarkdown(doc: DocumentResult): string;
104
+ /**
105
+ * Format a single DocumentResult as XML
106
+ */
107
+ export declare function documentToXml(doc: DocumentResult): string;
108
+ /**
109
+ * Format a single document to the specified format
110
+ */
111
+ export declare function formatDocument(doc: DocumentResult, format: OutputFormat): string;
112
+ /**
113
+ * Format search results to the specified output format
114
+ */
115
+ export declare function formatSearchResults(results: SearchResult[], format: OutputFormat, opts?: FormatOptions): string;
116
+ /**
117
+ * Format documents to the specified output format
118
+ */
119
+ export declare function formatDocuments(results: MultiGetFile[], format: OutputFormat): string;
@@ -0,0 +1,350 @@
1
+ /**
2
+ * renderer.ts - Output formatting utilities for QMD
3
+ *
4
+ * Provides methods to format search results and documents into various output formats:
5
+ * JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
6
+ */
7
+ import { extractSnippet } from "./repository.js";
8
+ // =============================================================================
9
+ // Helper Functions
10
+ // =============================================================================
11
+ /**
12
+ * Add line numbers to text content.
13
+ * Each line becomes: "{lineNum}: {content}"
14
+ * @param text The text to add line numbers to
15
+ * @param startLine Optional starting line number (default: 1)
16
+ */
17
+ export function addLineNumbers(text, startLine = 1) {
18
+ const lines = text.split('\n');
19
+ return lines.map((line, i) => `${startLine + i}: ${line}`).join('\n');
20
+ }
21
+ /**
22
+ * Extract short docid from a full hash (first 6 characters).
23
+ */
24
+ export function getDocid(hash) {
25
+ return hash.slice(0, 6);
26
+ }
27
+ // =============================================================================
28
+ // Escape Helpers
29
+ // =============================================================================
30
+ export function escapeCSV(value) {
31
+ if (value === null || value === undefined)
32
+ return "";
33
+ const str = String(value);
34
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
35
+ return `"${str.replace(/"/g, '""')}"`;
36
+ }
37
+ return str;
38
+ }
39
+ export function escapeXml(str) {
40
+ return str
41
+ .replace(/&/g, "&")
42
+ .replace(/</g, "&lt;")
43
+ .replace(/>/g, "&gt;")
44
+ .replace(/"/g, "&quot;")
45
+ .replace(/'/g, "&apos;");
46
+ }
47
+ // =============================================================================
48
+ // Search Results Formatters
49
+ // =============================================================================
50
+ /**
51
+ * Format search results as JSON
52
+ */
53
+ export function searchResultsToJson(results, opts = {}) {
54
+ const query = opts.query || "";
55
+ const output = results.map(row => {
56
+ const bodyStr = row.body || "";
57
+ let body = opts.full ? bodyStr : undefined;
58
+ let snippet = !opts.full ? extractSnippet(bodyStr, query, 300, row.chunkPos).snippet : undefined;
59
+ if (opts.lineNumbers) {
60
+ if (body)
61
+ body = addLineNumbers(body);
62
+ if (snippet)
63
+ snippet = addLineNumbers(snippet);
64
+ }
65
+ return {
66
+ docid: `#${row.docid}`,
67
+ score: Math.round(row.score * 100) / 100,
68
+ file: row.displayPath,
69
+ title: row.title,
70
+ ...(row.context && { context: row.context }),
71
+ ...(body && { body }),
72
+ ...(snippet && { snippet }),
73
+ };
74
+ });
75
+ return JSON.stringify(output, null, 2);
76
+ }
77
+ /**
78
+ * Format search results as CSV
79
+ */
80
+ export function searchResultsToCsv(results, opts = {}) {
81
+ const query = opts.query || "";
82
+ const header = "docid,score,file,title,context,line,snippet";
83
+ const rows = results.map(row => {
84
+ const bodyStr = row.body || "";
85
+ const { line, snippet } = extractSnippet(bodyStr, query, 500, row.chunkPos);
86
+ let content = opts.full ? bodyStr : snippet;
87
+ if (opts.lineNumbers && content) {
88
+ content = addLineNumbers(content);
89
+ }
90
+ return [
91
+ `#${row.docid}`,
92
+ row.score.toFixed(4),
93
+ escapeCSV(row.displayPath),
94
+ escapeCSV(row.title),
95
+ escapeCSV(row.context || ""),
96
+ line,
97
+ escapeCSV(content),
98
+ ].join(",");
99
+ });
100
+ return [header, ...rows].join("\n");
101
+ }
102
+ /**
103
+ * Format search results as simple files list (docid,score,filepath,context)
104
+ */
105
+ export function searchResultsToFiles(results) {
106
+ return results.map(row => {
107
+ const ctx = row.context ? `,"${row.context.replace(/"/g, '""')}"` : "";
108
+ return `#${row.docid},${row.score.toFixed(2)},${row.displayPath}${ctx}`;
109
+ }).join("\n");
110
+ }
111
+ /**
112
+ * Format search results as Markdown
113
+ */
114
+ export function searchResultsToMarkdown(results, opts = {}) {
115
+ const query = opts.query || "";
116
+ return results.map(row => {
117
+ const heading = row.title || row.displayPath;
118
+ const bodyStr = row.body || "";
119
+ let content;
120
+ if (opts.full) {
121
+ content = bodyStr;
122
+ }
123
+ else {
124
+ content = extractSnippet(bodyStr, query, 500, row.chunkPos).snippet;
125
+ }
126
+ if (opts.lineNumbers) {
127
+ content = addLineNumbers(content);
128
+ }
129
+ const contextLine = row.context ? `**context:** ${row.context}\n` : "";
130
+ return `---\n# ${heading}\n\n**docid:** \`#${row.docid}\`\n${contextLine}\n${content}\n`;
131
+ }).join("\n");
132
+ }
133
+ /**
134
+ * Format search results as XML
135
+ */
136
+ export function searchResultsToXml(results, opts = {}) {
137
+ const query = opts.query || "";
138
+ const items = results.map(row => {
139
+ const titleAttr = row.title ? ` title="${escapeXml(row.title)}"` : "";
140
+ const bodyStr = row.body || "";
141
+ let content = opts.full ? bodyStr : extractSnippet(bodyStr, query, 500, row.chunkPos).snippet;
142
+ if (opts.lineNumbers) {
143
+ content = addLineNumbers(content);
144
+ }
145
+ const contextAttr = row.context ? ` context="${escapeXml(row.context)}"` : "";
146
+ return `<file docid="#${row.docid}" name="${escapeXml(row.displayPath)}"${titleAttr}${contextAttr}>\n${escapeXml(content)}\n</file>`;
147
+ });
148
+ return items.join("\n\n");
149
+ }
150
+ /**
151
+ * Format search results for MCP (simpler CSV format with pre-extracted snippets)
152
+ */
153
+ export function searchResultsToMcpCsv(results) {
154
+ const header = "docid,file,title,score,context,snippet";
155
+ const rows = results.map(r => [`#${r.docid}`, r.file, r.title, r.score, r.context || "", r.snippet].map(escapeCSV).join(","));
156
+ return [header, ...rows].join("\n");
157
+ }
158
+ // =============================================================================
159
+ // Document Formatters (for multi-get using MultiGetFile from store)
160
+ // =============================================================================
161
+ /**
162
+ * Format documents as JSON
163
+ */
164
+ export function documentsToJson(results) {
165
+ const output = results.map(r => ({
166
+ file: r.displayPath,
167
+ title: r.title,
168
+ ...(r.context && { context: r.context }),
169
+ ...(r.skipped ? { skipped: true, reason: r.skipReason } : { body: r.body }),
170
+ }));
171
+ return JSON.stringify(output, null, 2);
172
+ }
173
+ /**
174
+ * Format documents as CSV
175
+ */
176
+ export function documentsToCsv(results) {
177
+ const header = "file,title,context,skipped,body";
178
+ const rows = results.map(r => [
179
+ r.displayPath,
180
+ r.title,
181
+ r.context || "",
182
+ r.skipped ? "true" : "false",
183
+ r.skipped ? (r.skipReason || "") : r.body
184
+ ].map(escapeCSV).join(","));
185
+ return [header, ...rows].join("\n");
186
+ }
187
+ /**
188
+ * Format documents as files list
189
+ */
190
+ export function documentsToFiles(results) {
191
+ return results.map(r => {
192
+ const ctx = r.context ? `,"${r.context.replace(/"/g, '""')}"` : "";
193
+ const status = r.skipped ? ",[SKIPPED]" : "";
194
+ return `${r.displayPath}${ctx}${status}`;
195
+ }).join("\n");
196
+ }
197
+ /**
198
+ * Format documents as Markdown
199
+ */
200
+ export function documentsToMarkdown(results) {
201
+ return results.map(r => {
202
+ let md = `## ${r.displayPath}\n\n`;
203
+ if (r.title && r.title !== r.displayPath)
204
+ md += `**Title:** ${r.title}\n\n`;
205
+ if (r.context)
206
+ md += `**Context:** ${r.context}\n\n`;
207
+ if (r.skipped) {
208
+ md += `> ${r.skipReason}\n`;
209
+ }
210
+ else {
211
+ md += "```\n" + r.body + "\n```\n";
212
+ }
213
+ return md;
214
+ }).join("\n");
215
+ }
216
+ /**
217
+ * Format documents as XML
218
+ */
219
+ export function documentsToXml(results) {
220
+ const items = results.map(r => {
221
+ let xml = " <document>\n";
222
+ xml += ` <file>${escapeXml(r.displayPath)}</file>\n`;
223
+ xml += ` <title>${escapeXml(r.title)}</title>\n`;
224
+ if (r.context)
225
+ xml += ` <context>${escapeXml(r.context)}</context>\n`;
226
+ if (r.skipped) {
227
+ xml += ` <skipped>true</skipped>\n`;
228
+ xml += ` <reason>${escapeXml(r.skipReason || "")}</reason>\n`;
229
+ }
230
+ else {
231
+ xml += ` <body>${escapeXml(r.body)}</body>\n`;
232
+ }
233
+ xml += " </document>";
234
+ return xml;
235
+ });
236
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<documents>\n${items.join("\n")}\n</documents>`;
237
+ }
238
+ // =============================================================================
239
+ // Single Document Formatters
240
+ // =============================================================================
241
+ /**
242
+ * Format a single DocumentResult as JSON
243
+ */
244
+ export function documentToJson(doc) {
245
+ return JSON.stringify({
246
+ file: doc.displayPath,
247
+ title: doc.title,
248
+ ...(doc.context && { context: doc.context }),
249
+ hash: doc.hash,
250
+ modifiedAt: doc.modifiedAt,
251
+ bodyLength: doc.bodyLength,
252
+ ...(doc.body !== undefined && { body: doc.body }),
253
+ }, null, 2);
254
+ }
255
+ /**
256
+ * Format a single DocumentResult as Markdown
257
+ */
258
+ export function documentToMarkdown(doc) {
259
+ let md = `# ${doc.title || doc.displayPath}\n\n`;
260
+ if (doc.context)
261
+ md += `**Context:** ${doc.context}\n\n`;
262
+ md += `**File:** ${doc.displayPath}\n`;
263
+ md += `**Modified:** ${doc.modifiedAt}\n\n`;
264
+ if (doc.body !== undefined) {
265
+ md += "---\n\n" + doc.body + "\n";
266
+ }
267
+ return md;
268
+ }
269
+ /**
270
+ * Format a single DocumentResult as XML
271
+ */
272
+ export function documentToXml(doc) {
273
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<document>\n`;
274
+ xml += ` <file>${escapeXml(doc.displayPath)}</file>\n`;
275
+ xml += ` <title>${escapeXml(doc.title)}</title>\n`;
276
+ if (doc.context)
277
+ xml += ` <context>${escapeXml(doc.context)}</context>\n`;
278
+ xml += ` <hash>${escapeXml(doc.hash)}</hash>\n`;
279
+ xml += ` <modifiedAt>${escapeXml(doc.modifiedAt)}</modifiedAt>\n`;
280
+ xml += ` <bodyLength>${doc.bodyLength}</bodyLength>\n`;
281
+ if (doc.body !== undefined) {
282
+ xml += ` <body>${escapeXml(doc.body)}</body>\n`;
283
+ }
284
+ xml += `</document>`;
285
+ return xml;
286
+ }
287
+ /**
288
+ * Format a single document to the specified format
289
+ */
290
+ export function formatDocument(doc, format) {
291
+ switch (format) {
292
+ case "json":
293
+ return documentToJson(doc);
294
+ case "md":
295
+ return documentToMarkdown(doc);
296
+ case "xml":
297
+ return documentToXml(doc);
298
+ default:
299
+ // Default to markdown for CLI and other formats
300
+ return documentToMarkdown(doc);
301
+ }
302
+ }
303
+ // =============================================================================
304
+ // Universal Format Function
305
+ // =============================================================================
306
+ /**
307
+ * Format search results to the specified output format
308
+ */
309
+ export function formatSearchResults(results, format, opts = {}) {
310
+ switch (format) {
311
+ case "json":
312
+ return searchResultsToJson(results, opts);
313
+ case "csv":
314
+ return searchResultsToCsv(results, opts);
315
+ case "files":
316
+ return searchResultsToFiles(results);
317
+ case "md":
318
+ return searchResultsToMarkdown(results, opts);
319
+ case "xml":
320
+ return searchResultsToXml(results, opts);
321
+ case "cli":
322
+ // CLI format should be handled separately with colors
323
+ // Return a simple text version as fallback
324
+ return searchResultsToMarkdown(results, opts);
325
+ default:
326
+ return searchResultsToJson(results, opts);
327
+ }
328
+ }
329
+ /**
330
+ * Format documents to the specified output format
331
+ */
332
+ export function formatDocuments(results, format) {
333
+ switch (format) {
334
+ case "json":
335
+ return documentsToJson(results);
336
+ case "csv":
337
+ return documentsToCsv(results);
338
+ case "files":
339
+ return documentsToFiles(results);
340
+ case "md":
341
+ return documentsToMarkdown(results);
342
+ case "xml":
343
+ return documentsToXml(results);
344
+ case "cli":
345
+ // CLI format should be handled separately with colors
346
+ return documentsToMarkdown(results);
347
+ default:
348
+ return documentsToJson(results);
349
+ }
350
+ }