@gmickel/gno 0.3.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.
Files changed (131) hide show
  1. package/README.md +256 -0
  2. package/assets/skill/SKILL.md +112 -0
  3. package/assets/skill/cli-reference.md +327 -0
  4. package/assets/skill/examples.md +234 -0
  5. package/assets/skill/mcp-reference.md +159 -0
  6. package/package.json +90 -0
  7. package/src/app/constants.ts +313 -0
  8. package/src/cli/colors.ts +65 -0
  9. package/src/cli/commands/ask.ts +545 -0
  10. package/src/cli/commands/cleanup.ts +105 -0
  11. package/src/cli/commands/collection/add.ts +120 -0
  12. package/src/cli/commands/collection/index.ts +10 -0
  13. package/src/cli/commands/collection/list.ts +108 -0
  14. package/src/cli/commands/collection/remove.ts +64 -0
  15. package/src/cli/commands/collection/rename.ts +95 -0
  16. package/src/cli/commands/context/add.ts +67 -0
  17. package/src/cli/commands/context/check.ts +153 -0
  18. package/src/cli/commands/context/index.ts +10 -0
  19. package/src/cli/commands/context/list.ts +109 -0
  20. package/src/cli/commands/context/rm.ts +52 -0
  21. package/src/cli/commands/doctor.ts +393 -0
  22. package/src/cli/commands/embed.ts +462 -0
  23. package/src/cli/commands/get.ts +356 -0
  24. package/src/cli/commands/index-cmd.ts +119 -0
  25. package/src/cli/commands/index.ts +102 -0
  26. package/src/cli/commands/init.ts +328 -0
  27. package/src/cli/commands/ls.ts +217 -0
  28. package/src/cli/commands/mcp/config.ts +300 -0
  29. package/src/cli/commands/mcp/index.ts +24 -0
  30. package/src/cli/commands/mcp/install.ts +203 -0
  31. package/src/cli/commands/mcp/paths.ts +470 -0
  32. package/src/cli/commands/mcp/status.ts +222 -0
  33. package/src/cli/commands/mcp/uninstall.ts +158 -0
  34. package/src/cli/commands/mcp.ts +20 -0
  35. package/src/cli/commands/models/clear.ts +103 -0
  36. package/src/cli/commands/models/index.ts +32 -0
  37. package/src/cli/commands/models/list.ts +214 -0
  38. package/src/cli/commands/models/path.ts +51 -0
  39. package/src/cli/commands/models/pull.ts +199 -0
  40. package/src/cli/commands/models/use.ts +85 -0
  41. package/src/cli/commands/multi-get.ts +400 -0
  42. package/src/cli/commands/query.ts +220 -0
  43. package/src/cli/commands/ref-parser.ts +108 -0
  44. package/src/cli/commands/reset.ts +191 -0
  45. package/src/cli/commands/search.ts +136 -0
  46. package/src/cli/commands/shared.ts +156 -0
  47. package/src/cli/commands/skill/index.ts +19 -0
  48. package/src/cli/commands/skill/install.ts +197 -0
  49. package/src/cli/commands/skill/paths-cmd.ts +81 -0
  50. package/src/cli/commands/skill/paths.ts +191 -0
  51. package/src/cli/commands/skill/show.ts +73 -0
  52. package/src/cli/commands/skill/uninstall.ts +141 -0
  53. package/src/cli/commands/status.ts +205 -0
  54. package/src/cli/commands/update.ts +68 -0
  55. package/src/cli/commands/vsearch.ts +188 -0
  56. package/src/cli/context.ts +64 -0
  57. package/src/cli/errors.ts +64 -0
  58. package/src/cli/format/search-results.ts +211 -0
  59. package/src/cli/options.ts +183 -0
  60. package/src/cli/program.ts +1330 -0
  61. package/src/cli/run.ts +213 -0
  62. package/src/cli/ui.ts +92 -0
  63. package/src/config/defaults.ts +20 -0
  64. package/src/config/index.ts +55 -0
  65. package/src/config/loader.ts +161 -0
  66. package/src/config/paths.ts +87 -0
  67. package/src/config/saver.ts +153 -0
  68. package/src/config/types.ts +280 -0
  69. package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
  70. package/src/converters/adapters/officeparser/adapter.ts +126 -0
  71. package/src/converters/canonicalize.ts +89 -0
  72. package/src/converters/errors.ts +218 -0
  73. package/src/converters/index.ts +51 -0
  74. package/src/converters/mime.ts +163 -0
  75. package/src/converters/native/markdown.ts +115 -0
  76. package/src/converters/native/plaintext.ts +56 -0
  77. package/src/converters/path.ts +48 -0
  78. package/src/converters/pipeline.ts +159 -0
  79. package/src/converters/registry.ts +74 -0
  80. package/src/converters/types.ts +123 -0
  81. package/src/converters/versions.ts +24 -0
  82. package/src/index.ts +27 -0
  83. package/src/ingestion/chunker.ts +238 -0
  84. package/src/ingestion/index.ts +32 -0
  85. package/src/ingestion/language.ts +276 -0
  86. package/src/ingestion/sync.ts +671 -0
  87. package/src/ingestion/types.ts +219 -0
  88. package/src/ingestion/walker.ts +235 -0
  89. package/src/llm/cache.ts +467 -0
  90. package/src/llm/errors.ts +191 -0
  91. package/src/llm/index.ts +58 -0
  92. package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
  93. package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
  94. package/src/llm/nodeLlamaCpp/generation.ts +88 -0
  95. package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
  96. package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
  97. package/src/llm/registry.ts +86 -0
  98. package/src/llm/types.ts +129 -0
  99. package/src/mcp/resources/index.ts +151 -0
  100. package/src/mcp/server.ts +229 -0
  101. package/src/mcp/tools/get.ts +220 -0
  102. package/src/mcp/tools/index.ts +160 -0
  103. package/src/mcp/tools/multi-get.ts +263 -0
  104. package/src/mcp/tools/query.ts +226 -0
  105. package/src/mcp/tools/search.ts +119 -0
  106. package/src/mcp/tools/status.ts +81 -0
  107. package/src/mcp/tools/vsearch.ts +198 -0
  108. package/src/pipeline/chunk-lookup.ts +44 -0
  109. package/src/pipeline/expansion.ts +256 -0
  110. package/src/pipeline/explain.ts +115 -0
  111. package/src/pipeline/fusion.ts +185 -0
  112. package/src/pipeline/hybrid.ts +535 -0
  113. package/src/pipeline/index.ts +64 -0
  114. package/src/pipeline/query-language.ts +118 -0
  115. package/src/pipeline/rerank.ts +223 -0
  116. package/src/pipeline/search.ts +261 -0
  117. package/src/pipeline/types.ts +328 -0
  118. package/src/pipeline/vsearch.ts +348 -0
  119. package/src/store/index.ts +41 -0
  120. package/src/store/migrations/001-initial.ts +196 -0
  121. package/src/store/migrations/index.ts +20 -0
  122. package/src/store/migrations/runner.ts +187 -0
  123. package/src/store/sqlite/adapter.ts +1242 -0
  124. package/src/store/sqlite/index.ts +7 -0
  125. package/src/store/sqlite/setup.ts +129 -0
  126. package/src/store/sqlite/types.ts +28 -0
  127. package/src/store/types.ts +506 -0
  128. package/src/store/vector/index.ts +13 -0
  129. package/src/store/vector/sqlite-vec.ts +373 -0
  130. package/src/store/vector/stats.ts +152 -0
  131. package/src/store/vector/types.ts +115 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * gno init command implementation.
3
+ * Initializes GNO config, directories, and optionally adds a collection.
4
+ *
5
+ * @module src/cli/commands/init
6
+ */
7
+
8
+ import { basename } from 'node:path';
9
+ import { getIndexDbPath } from '../../app/constants';
10
+ import {
11
+ type Collection,
12
+ createDefaultConfig,
13
+ DEFAULT_EXCLUDES,
14
+ DEFAULT_PATTERN,
15
+ ensureDirectories,
16
+ FTS_TOKENIZERS,
17
+ type FtsTokenizer,
18
+ getConfigPaths,
19
+ isInitialized,
20
+ isValidLanguageHint,
21
+ loadConfigOrNull,
22
+ pathExists,
23
+ saveConfig,
24
+ toAbsolutePath,
25
+ } from '../../config';
26
+
27
+ /** Pattern to replace invalid chars in collection names with hyphens */
28
+ const INVALID_NAME_CHARS = /[^a-z0-9_-]/g;
29
+
30
+ /** Pattern to strip leading non-alphanumeric from collection names */
31
+ const LEADING_NON_ALPHANUMERIC = /^[^a-z0-9]+/;
32
+
33
+ /**
34
+ * Options for init command.
35
+ */
36
+ export interface InitOptions {
37
+ /** Optional path to add as collection */
38
+ path?: string;
39
+ /** Collection name (defaults to directory basename if path given) */
40
+ name?: string;
41
+ /** Glob pattern for file matching */
42
+ pattern?: string;
43
+ /** Extension allowlist CSV (e.g., ".md,.pdf") */
44
+ include?: string;
45
+ /** Exclude patterns CSV */
46
+ exclude?: string;
47
+ /** Shell command to run before indexing */
48
+ update?: string;
49
+ /** Skip prompts, accept defaults */
50
+ yes?: boolean;
51
+ /** Override config path */
52
+ configPath?: string;
53
+ /** FTS tokenizer (unicode61, porter, trigram) */
54
+ tokenizer?: FtsTokenizer;
55
+ /** BCP-47 language hint for collection */
56
+ language?: string;
57
+ }
58
+
59
+ /**
60
+ * Result of init command.
61
+ */
62
+ export interface InitResult {
63
+ success: boolean;
64
+ alreadyInitialized?: boolean;
65
+ configPath: string;
66
+ dataDir: string;
67
+ dbPath: string;
68
+ collectionAdded?: string;
69
+ error?: string;
70
+ }
71
+
72
+ /**
73
+ * Handle case when already initialized.
74
+ */
75
+ async function handleAlreadyInitialized(
76
+ options: InitOptions,
77
+ paths: ReturnType<typeof getConfigPaths>
78
+ ): Promise<InitResult> {
79
+ // Ensure directories exist (may have been deleted by reset)
80
+ await ensureDirectories();
81
+
82
+ const config = await loadConfigOrNull(options.configPath);
83
+ const dbPath = getIndexDbPath();
84
+
85
+ if (!options.path) {
86
+ return {
87
+ success: true,
88
+ alreadyInitialized: true,
89
+ configPath: paths.configFile,
90
+ dataDir: paths.dataDir,
91
+ dbPath,
92
+ };
93
+ }
94
+
95
+ if (!config) {
96
+ return {
97
+ success: false,
98
+ configPath: paths.configFile,
99
+ dataDir: paths.dataDir,
100
+ dbPath,
101
+ error: 'Config exists but could not be loaded',
102
+ };
103
+ }
104
+
105
+ const collectionResult = await addCollectionToConfig(config, options);
106
+ if (!collectionResult.success) {
107
+ return {
108
+ success: false,
109
+ configPath: paths.configFile,
110
+ dataDir: paths.dataDir,
111
+ dbPath,
112
+ error: collectionResult.error,
113
+ };
114
+ }
115
+
116
+ const saveResult = await saveConfig(config, options.configPath);
117
+ if (!saveResult.ok) {
118
+ return {
119
+ success: false,
120
+ configPath: paths.configFile,
121
+ dataDir: paths.dataDir,
122
+ dbPath,
123
+ error: saveResult.error.message,
124
+ };
125
+ }
126
+
127
+ return {
128
+ success: true,
129
+ alreadyInitialized: true,
130
+ configPath: paths.configFile,
131
+ dataDir: paths.dataDir,
132
+ dbPath,
133
+ collectionAdded: collectionResult.collectionName,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Execute gno init command.
139
+ */
140
+ export async function init(options: InitOptions = {}): Promise<InitResult> {
141
+ const paths = getConfigPaths();
142
+
143
+ // Check if already initialized
144
+ const initialized = await isInitialized(options.configPath);
145
+ if (initialized) {
146
+ return handleAlreadyInitialized(options, paths);
147
+ }
148
+
149
+ // Create directories
150
+ const dirResult = await ensureDirectories();
151
+ if (!dirResult.ok) {
152
+ return {
153
+ success: false,
154
+ configPath: paths.configFile,
155
+ dataDir: paths.dataDir,
156
+ dbPath: getIndexDbPath(),
157
+ error: dirResult.error.message,
158
+ };
159
+ }
160
+
161
+ // Validate tokenizer option if provided
162
+ if (options.tokenizer && !FTS_TOKENIZERS.includes(options.tokenizer)) {
163
+ return {
164
+ success: false,
165
+ configPath: paths.configFile,
166
+ dataDir: paths.dataDir,
167
+ dbPath: getIndexDbPath(),
168
+ error: `Invalid tokenizer: ${options.tokenizer}. Valid: ${FTS_TOKENIZERS.join(', ')}`,
169
+ };
170
+ }
171
+
172
+ // Create default config
173
+ const config = createDefaultConfig();
174
+
175
+ // Set tokenizer if provided
176
+ if (options.tokenizer) {
177
+ config.ftsTokenizer = options.tokenizer;
178
+ }
179
+
180
+ // Add collection if path provided
181
+ let collectionName: string | undefined;
182
+ if (options.path) {
183
+ const collectionResult = await addCollectionToConfig(config, options);
184
+ if (!collectionResult.success) {
185
+ return {
186
+ success: false,
187
+ configPath: paths.configFile,
188
+ dataDir: paths.dataDir,
189
+ dbPath: getIndexDbPath(),
190
+ error: collectionResult.error,
191
+ };
192
+ }
193
+ collectionName = collectionResult.collectionName;
194
+ }
195
+
196
+ // Save config
197
+ const saveResult = await saveConfig(config, options.configPath);
198
+ if (!saveResult.ok) {
199
+ return {
200
+ success: false,
201
+ configPath: paths.configFile,
202
+ dataDir: paths.dataDir,
203
+ dbPath: getIndexDbPath(),
204
+ error: saveResult.error.message,
205
+ };
206
+ }
207
+
208
+ // Create DB placeholder file only if it doesn't exist (don't truncate existing DB)
209
+ const dbPath = getIndexDbPath();
210
+ const dbFile = Bun.file(dbPath);
211
+ const dbExists = await dbFile.exists();
212
+ if (!dbExists) {
213
+ try {
214
+ await Bun.write(dbPath, '');
215
+ } catch (error) {
216
+ return {
217
+ success: false,
218
+ configPath: paths.configFile,
219
+ dataDir: paths.dataDir,
220
+ dbPath,
221
+ error: `Failed to create database file: ${error instanceof Error ? error.message : String(error)}`,
222
+ };
223
+ }
224
+ }
225
+
226
+ return {
227
+ success: true,
228
+ configPath: paths.configFile,
229
+ dataDir: paths.dataDir,
230
+ dbPath,
231
+ collectionAdded: collectionName,
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Helper to add collection to config.
237
+ */
238
+ async function addCollectionToConfig(
239
+ config: ReturnType<typeof createDefaultConfig>,
240
+ options: InitOptions
241
+ ): Promise<
242
+ { success: true; collectionName: string } | { success: false; error: string }
243
+ > {
244
+ if (!options.path) {
245
+ return { success: false, error: 'Path is required' };
246
+ }
247
+
248
+ // Convert to absolute path
249
+ const absolutePath = toAbsolutePath(options.path);
250
+
251
+ // Check if path exists (as directory or file)
252
+ const exists = await pathExists(absolutePath);
253
+ if (!exists) {
254
+ return {
255
+ success: false,
256
+ error: `Path does not exist: ${absolutePath}`,
257
+ };
258
+ }
259
+
260
+ // Determine collection name
261
+ let collectionName =
262
+ options.name ??
263
+ basename(absolutePath).toLowerCase().replace(INVALID_NAME_CHARS, '-');
264
+
265
+ // Ensure name starts with alphanumeric (strip leading non-alphanumeric)
266
+ collectionName = collectionName.replace(LEADING_NON_ALPHANUMERIC, '');
267
+
268
+ // Validate derived name
269
+ if (!collectionName || collectionName.length > 64) {
270
+ return {
271
+ success: false,
272
+ error:
273
+ 'Cannot derive valid collection name from path. Please specify --name explicitly.',
274
+ };
275
+ }
276
+
277
+ // Check for duplicate name
278
+ if (config.collections.some((c) => c.name === collectionName)) {
279
+ return {
280
+ success: false,
281
+ error: `Collection "${collectionName}" already exists`,
282
+ };
283
+ }
284
+
285
+ // Parse include/exclude CSV if provided (filter empty entries)
286
+ const include = options.include
287
+ ? options.include
288
+ .split(',')
289
+ .map((ext) => ext.trim())
290
+ .filter(Boolean)
291
+ : [];
292
+
293
+ const exclude = options.exclude
294
+ ? options.exclude
295
+ .split(',')
296
+ .map((pattern) => pattern.trim())
297
+ .filter(Boolean)
298
+ : [...DEFAULT_EXCLUDES];
299
+
300
+ // Create collection
301
+ const collection: Collection = {
302
+ name: collectionName,
303
+ path: absolutePath,
304
+ pattern: options.pattern ?? DEFAULT_PATTERN,
305
+ include,
306
+ exclude,
307
+ };
308
+
309
+ if (options.update) {
310
+ collection.updateCmd = options.update;
311
+ }
312
+
313
+ // Validate and set language hint if provided
314
+ if (options.language) {
315
+ if (!isValidLanguageHint(options.language)) {
316
+ return {
317
+ success: false,
318
+ error: `Invalid language hint: ${options.language}. Use BCP-47 format (e.g., en, de, zh-CN)`,
319
+ };
320
+ }
321
+ collection.languageHint = options.language;
322
+ }
323
+
324
+ // Add to config
325
+ config.collections.push(collection);
326
+
327
+ return { success: true, collectionName };
328
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * gno ls command implementation.
3
+ * List indexed documents.
4
+ *
5
+ * @module src/cli/commands/ls
6
+ */
7
+
8
+ import type { DocumentRow, StorePort, StoreResult } from '../../store/types';
9
+ import { initStore } from './shared';
10
+
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ // Types
13
+ // ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ export interface LsCommandOptions {
16
+ /** Override config path */
17
+ configPath?: string;
18
+ /** Max results (default 20) */
19
+ limit?: number;
20
+ /** Skip first N results */
21
+ offset?: number;
22
+ /** JSON output */
23
+ json?: boolean;
24
+ /** File protocol output */
25
+ files?: boolean;
26
+ /** Markdown output */
27
+ md?: boolean;
28
+ }
29
+
30
+ export type LsResult =
31
+ | { success: true; data: LsResponse }
32
+ | { success: false; error: string; isValidation?: boolean };
33
+
34
+ export interface LsDocument {
35
+ docid: string;
36
+ uri: string;
37
+ title?: string;
38
+ source: { relPath: string; mime: string; ext: string };
39
+ }
40
+
41
+ export interface LsResponse {
42
+ documents: LsDocument[];
43
+ meta: {
44
+ total: number;
45
+ returned: number;
46
+ offset: number;
47
+ };
48
+ }
49
+
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+ // Scope validation regex
52
+ // ─────────────────────────────────────────────────────────────────────────────
53
+
54
+ const URI_PREFIX_PATTERN = /^gno:\/\/[^/]+\//;
55
+
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+ // Document Fetching Helper
58
+ // ─────────────────────────────────────────────────────────────────────────────
59
+
60
+ async function fetchDocuments(
61
+ store: StorePort,
62
+ scope: string | undefined
63
+ ): Promise<StoreResult<DocumentRow[]>> {
64
+ if (!scope) {
65
+ return store.listDocuments();
66
+ }
67
+
68
+ if (scope.startsWith('gno://')) {
69
+ const allDocs = await store.listDocuments();
70
+ if (!allDocs.ok) {
71
+ return allDocs;
72
+ }
73
+ return {
74
+ ok: true,
75
+ value: allDocs.value.filter((d) => d.uri.startsWith(scope)),
76
+ };
77
+ }
78
+
79
+ return store.listDocuments(scope);
80
+ }
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // Command Implementation
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Execute gno ls command.
88
+ */
89
+ export async function ls(
90
+ scope: string | undefined,
91
+ options: LsCommandOptions = {}
92
+ ): Promise<LsResult> {
93
+ // Validate scope if it's a gno:// URI
94
+ if (scope?.startsWith('gno://')) {
95
+ if (scope === 'gno://') {
96
+ return {
97
+ success: false,
98
+ error: 'Invalid scope: missing collection',
99
+ isValidation: true,
100
+ };
101
+ }
102
+ if (!URI_PREFIX_PATTERN.test(scope)) {
103
+ return {
104
+ success: false,
105
+ error: 'Invalid scope: missing trailing path (use gno://collection/)',
106
+ isValidation: true,
107
+ };
108
+ }
109
+ }
110
+
111
+ const initResult = await initStore({ configPath: options.configPath });
112
+ if (!initResult.ok) {
113
+ return { success: false, error: initResult.error };
114
+ }
115
+ const { store } = initResult;
116
+
117
+ try {
118
+ const docs = await fetchDocuments(store, scope);
119
+ if (!docs.ok) {
120
+ return { success: false, error: docs.error.message };
121
+ }
122
+
123
+ // Filter active only, sort by URI
124
+ const allActive = docs.value
125
+ .filter((d) => d.active)
126
+ .map((d) => ({
127
+ docid: d.docid,
128
+ uri: d.uri,
129
+ title: d.title ?? undefined,
130
+ source: {
131
+ relPath: d.relPath,
132
+ mime: d.sourceMime,
133
+ ext: d.sourceExt,
134
+ },
135
+ }))
136
+ .sort((a, b) => a.uri.localeCompare(b.uri));
137
+
138
+ // Apply offset and limit
139
+ const offset = options.offset ?? 0;
140
+ const limit = options.limit ?? 20;
141
+ const paged = allActive.slice(offset, offset + limit);
142
+
143
+ return {
144
+ success: true,
145
+ data: {
146
+ documents: paged,
147
+ meta: {
148
+ total: allActive.length,
149
+ returned: paged.length,
150
+ offset,
151
+ },
152
+ },
153
+ };
154
+ } finally {
155
+ await store.close();
156
+ }
157
+ }
158
+
159
+ // ─────────────────────────────────────────────────────────────────────────────
160
+ // Formatter
161
+ // ─────────────────────────────────────────────────────────────────────────────
162
+
163
+ /**
164
+ * Format ls result for output.
165
+ */
166
+ export function formatLs(result: LsResult, options: LsCommandOptions): string {
167
+ if (!result.success) {
168
+ if (options.json) {
169
+ return JSON.stringify({
170
+ error: { code: 'LS_FAILED', message: result.error },
171
+ });
172
+ }
173
+ return `Error: ${result.error}`;
174
+ }
175
+
176
+ const { data } = result;
177
+ const docs = data.documents;
178
+
179
+ if (options.json) {
180
+ return JSON.stringify(data, null, 2);
181
+ }
182
+
183
+ if (options.files) {
184
+ return docs.map((d) => `${d.docid},${d.uri}`).join('\n');
185
+ }
186
+
187
+ if (options.md) {
188
+ if (docs.length === 0) {
189
+ return '# Documents\n\nNo documents found.';
190
+ }
191
+ const lines: string[] = [];
192
+ lines.push('# Documents');
193
+ lines.push('');
194
+ lines.push(
195
+ `*Showing ${data.meta.returned} of ${data.meta.total} documents*`
196
+ );
197
+ lines.push('');
198
+ lines.push('| DocID | URI | Title |');
199
+ lines.push('|-------|-----|-------|');
200
+ for (const d of docs) {
201
+ lines.push(`| \`${d.docid}\` | \`${d.uri}\` | ${d.title || '-'} |`);
202
+ }
203
+ return lines.join('\n');
204
+ }
205
+
206
+ // Terminal format
207
+ if (docs.length === 0) {
208
+ return 'No documents found.';
209
+ }
210
+ const lines = docs.map((d) => `${d.docid}\t${d.uri}`);
211
+ if (data.meta.returned < data.meta.total) {
212
+ lines.push(
213
+ `\n(${data.meta.returned} of ${data.meta.total} documents shown)`
214
+ );
215
+ }
216
+ return lines.join('\n');
217
+ }