@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,199 @@
1
+ /**
2
+ * gno models pull command implementation.
3
+ * Download models to local cache.
4
+ *
5
+ * @module src/cli/commands/models/pull
6
+ */
7
+
8
+ import { getModelsCachePath } from '../../../app/constants';
9
+ import { loadConfig } from '../../../config';
10
+ import { ModelCache } from '../../../llm/cache';
11
+ import { getActivePreset } from '../../../llm/registry';
12
+ import type { DownloadProgress, ModelType } from '../../../llm/types';
13
+
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ // Types
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ export interface ModelsPullOptions {
19
+ /** Override config path */
20
+ configPath?: string;
21
+ /** Pull all models */
22
+ all?: boolean;
23
+ /** Pull embedding model */
24
+ embed?: boolean;
25
+ /** Pull reranker model */
26
+ rerank?: boolean;
27
+ /** Pull generation model */
28
+ gen?: boolean;
29
+ /** Force re-download */
30
+ force?: boolean;
31
+ /** Progress callback for UI (omit to disable progress) */
32
+ onProgress?: (type: ModelType, progress: DownloadProgress) => void;
33
+ }
34
+
35
+ export interface ModelPullResult {
36
+ type: ModelType;
37
+ uri: string;
38
+ ok: boolean;
39
+ error?: string;
40
+ path?: string;
41
+ skipped?: boolean;
42
+ }
43
+
44
+ export interface ModelsPullResult {
45
+ results: ModelPullResult[];
46
+ failed: number;
47
+ skipped: number;
48
+ }
49
+
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+ // Implementation
52
+ // ─────────────────────────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Determine which model types to pull based on options.
56
+ */
57
+ function getTypesToPull(options: ModelsPullOptions): ModelType[] {
58
+ if (options.all) {
59
+ return ['embed', 'rerank', 'gen'];
60
+ }
61
+ if (options.embed || options.rerank || options.gen) {
62
+ const types: ModelType[] = [];
63
+ if (options.embed) {
64
+ types.push('embed');
65
+ }
66
+ if (options.rerank) {
67
+ types.push('rerank');
68
+ }
69
+ if (options.gen) {
70
+ types.push('gen');
71
+ }
72
+ return types;
73
+ }
74
+ // Default: pull all
75
+ return ['embed', 'rerank', 'gen'];
76
+ }
77
+
78
+ /**
79
+ * Execute gno models pull command.
80
+ */
81
+ export async function modelsPull(
82
+ options: ModelsPullOptions = {}
83
+ ): Promise<ModelsPullResult> {
84
+ // Load config (use defaults if not initialized)
85
+ const { createDefaultConfig } = await import('../../../config');
86
+ const configResult = await loadConfig(options.configPath);
87
+ const config = configResult.ok ? configResult.value : createDefaultConfig();
88
+
89
+ const preset = getActivePreset(config);
90
+ const cache = new ModelCache(getModelsCachePath());
91
+ const types = getTypesToPull(options);
92
+
93
+ const results: ModelPullResult[] = [];
94
+ let failed = 0;
95
+ let skipped = 0;
96
+
97
+ for (const type of types) {
98
+ const uri = preset[type];
99
+
100
+ // Check if already cached (skip unless --force)
101
+ if (!options.force) {
102
+ const isCached = await cache.isCached(uri);
103
+ if (isCached) {
104
+ const path = await cache.getCachedPath(uri);
105
+ results.push({
106
+ type,
107
+ uri,
108
+ ok: true,
109
+ path: path ?? undefined,
110
+ skipped: true,
111
+ });
112
+ skipped += 1;
113
+ continue;
114
+ }
115
+ }
116
+
117
+ // Download the model
118
+ const result = await cache.download(
119
+ uri,
120
+ type,
121
+ (progress) => {
122
+ options.onProgress?.(type, progress);
123
+ },
124
+ options.force
125
+ );
126
+
127
+ if (result.ok) {
128
+ results.push({
129
+ type,
130
+ uri,
131
+ ok: true,
132
+ path: result.value,
133
+ });
134
+ } else {
135
+ results.push({
136
+ type,
137
+ uri,
138
+ ok: false,
139
+ error: result.error.message,
140
+ });
141
+ failed += 1;
142
+ }
143
+ }
144
+
145
+ return { results, failed, skipped };
146
+ }
147
+
148
+ // ─────────────────────────────────────────────────────────────────────────────
149
+ // Formatting
150
+ // ─────────────────────────────────────────────────────────────────────────────
151
+
152
+ /**
153
+ * Format models pull result for output.
154
+ */
155
+ export function formatModelsPull(result: ModelsPullResult): string {
156
+ const lines: string[] = [];
157
+
158
+ for (const r of result.results) {
159
+ if (r.ok) {
160
+ if (r.skipped) {
161
+ lines.push(`${r.type}: skipped (already cached)`);
162
+ } else {
163
+ lines.push(`${r.type}: downloaded`);
164
+ }
165
+ } else {
166
+ lines.push(`${r.type}: failed - ${r.error}`);
167
+ }
168
+ }
169
+
170
+ if (result.failed > 0) {
171
+ lines.push('');
172
+ lines.push(`${result.failed} model(s) failed to download.`);
173
+ } else if (result.skipped === result.results.length) {
174
+ lines.push('');
175
+ lines.push('All models already cached. Use --force to re-download.');
176
+ } else {
177
+ lines.push('');
178
+ lines.push('All models downloaded successfully.');
179
+ }
180
+
181
+ return lines.join('\n');
182
+ }
183
+
184
+ /**
185
+ * Create a terminal progress renderer.
186
+ */
187
+ export function createProgressRenderer(): (
188
+ type: ModelType,
189
+ progress: DownloadProgress
190
+ ) => void {
191
+ return (type, progress) => {
192
+ const percent = progress.percent.toFixed(1);
193
+ const downloaded = (progress.downloadedBytes / 1024 / 1024).toFixed(1);
194
+ const total = (progress.totalBytes / 1024 / 1024).toFixed(1);
195
+ process.stderr.write(
196
+ `\r${type}: ${percent}% (${downloaded}/${total} MB) `
197
+ );
198
+ };
199
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * gno models use command implementation.
3
+ * Switch active model preset.
4
+ *
5
+ * @module src/cli/commands/models/use
6
+ */
7
+
8
+ import { createDefaultConfig, loadConfig } from '../../../config';
9
+ import { saveConfig } from '../../../config/saver';
10
+ import { getPreset, listPresets } from '../../../llm/registry';
11
+
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Types
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ export interface ModelsUseOptions {
17
+ /** Override config path */
18
+ configPath?: string;
19
+ }
20
+
21
+ export type ModelsUseResult =
22
+ | { success: true; preset: string; name: string }
23
+ | { success: false; error: string };
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // Implementation
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Execute gno models use command.
31
+ */
32
+ export async function modelsUse(
33
+ presetId: string,
34
+ options: ModelsUseOptions = {}
35
+ ): Promise<ModelsUseResult> {
36
+ // Load existing config or create default
37
+ const configResult = await loadConfig(options.configPath);
38
+ const config = configResult.ok ? configResult.value : createDefaultConfig();
39
+
40
+ // Check if preset exists
41
+ const preset = getPreset(config, presetId);
42
+ if (!preset) {
43
+ const available = listPresets(config)
44
+ .map((p) => p.id)
45
+ .join(', ');
46
+ return {
47
+ success: false,
48
+ error: `Unknown preset: ${presetId}. Available: ${available}`,
49
+ };
50
+ }
51
+
52
+ // Update config with new active preset
53
+ const updatedConfig = {
54
+ ...config,
55
+ models: {
56
+ activePreset: presetId,
57
+ // Preserve existing presets or use defaults from code
58
+ presets: config.models?.presets ?? [],
59
+ loadTimeout: config.models?.loadTimeout ?? 60_000,
60
+ inferenceTimeout: config.models?.inferenceTimeout ?? 30_000,
61
+ warmModelTtl: config.models?.warmModelTtl ?? 300_000,
62
+ },
63
+ };
64
+
65
+ // Save updated config
66
+ const saveResult = await saveConfig(updatedConfig, options.configPath);
67
+ if (!saveResult.ok) {
68
+ return {
69
+ success: false,
70
+ error: `Failed to save config: ${saveResult.error.message}`,
71
+ };
72
+ }
73
+
74
+ return { success: true, preset: presetId, name: preset.name };
75
+ }
76
+
77
+ /**
78
+ * Format models use result for output.
79
+ */
80
+ export function formatModelsUse(result: ModelsUseResult): string {
81
+ if (!result.success) {
82
+ return `Error: ${result.error}`;
83
+ }
84
+ return `Switched to preset: ${result.preset} (${result.name})`;
85
+ }
@@ -0,0 +1,400 @@
1
+ /**
2
+ * gno multi-get command implementation.
3
+ * Retrieve multiple documents by reference.
4
+ *
5
+ * @module src/cli/commands/multi-get
6
+ */
7
+
8
+ import { minimatch } from 'minimatch';
9
+ import type { DocumentRow, StorePort, StoreResult } from '../../store/types';
10
+ import type { ParsedRef } from './ref-parser';
11
+ import { isGlobPattern, parseRef, splitRefs } from './ref-parser';
12
+ import { initStore } from './shared';
13
+
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+ // Types
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ export interface MultiGetCommandOptions {
19
+ /** Override config path */
20
+ configPath?: string;
21
+ /** Max bytes per document (default 10240) */
22
+ maxBytes?: number;
23
+ /** Include line numbers */
24
+ lineNumbers?: boolean;
25
+ /** JSON output */
26
+ json?: boolean;
27
+ /** File protocol output */
28
+ files?: boolean;
29
+ /** Markdown output */
30
+ md?: boolean;
31
+ }
32
+
33
+ export type MultiGetResult =
34
+ | { success: true; data: MultiGetResponse }
35
+ | { success: false; error: string; isValidation?: boolean };
36
+
37
+ export interface MultiGetDocument {
38
+ docid: string;
39
+ uri: string;
40
+ title?: string;
41
+ content: string;
42
+ truncated?: boolean;
43
+ totalLines?: number;
44
+ source: { absPath?: string; relPath: string; mime: string; ext: string };
45
+ }
46
+
47
+ export interface SkippedDoc {
48
+ ref: string;
49
+ reason: 'not_found' | 'conversion_error' | 'invalid_ref';
50
+ }
51
+
52
+ export interface MultiGetResponse {
53
+ documents: MultiGetDocument[];
54
+ skipped: SkippedDoc[];
55
+ meta: {
56
+ requested: number;
57
+ returned: number;
58
+ skipped: number;
59
+ maxBytes?: number;
60
+ };
61
+ }
62
+
63
+ interface ConfigLike {
64
+ collections: { name: string; path: string }[];
65
+ }
66
+
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+ // Document Lookup Helper
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+
71
+ function lookupDocument(
72
+ store: StorePort,
73
+ parsed: ParsedRef
74
+ ): Promise<StoreResult<DocumentRow | null>> {
75
+ switch (parsed.type) {
76
+ case 'docid':
77
+ return store.getDocumentByDocid(parsed.value);
78
+ case 'uri':
79
+ return store.getDocumentByUri(parsed.value);
80
+ case 'collPath':
81
+ if (!(parsed.collection && parsed.relPath)) {
82
+ return Promise.resolve({ ok: true as const, value: null });
83
+ }
84
+ return store.getDocument(parsed.collection, parsed.relPath);
85
+ default:
86
+ return Promise.resolve({ ok: true as const, value: null });
87
+ }
88
+ }
89
+
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ // Glob Expansion
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+
94
+ interface ExpandResult {
95
+ expanded: string[];
96
+ invalidRefs: string[];
97
+ }
98
+
99
+ async function expandGlobs(
100
+ refs: string[],
101
+ store: StorePort
102
+ ): Promise<ExpandResult> {
103
+ const expanded: string[] = [];
104
+ const invalidRefs: string[] = [];
105
+
106
+ for (const ref of refs) {
107
+ if (!isGlobPattern(ref)) {
108
+ expanded.push(ref);
109
+ continue;
110
+ }
111
+
112
+ const slashIdx = ref.indexOf('/');
113
+ if (slashIdx === -1) {
114
+ invalidRefs.push(ref);
115
+ continue;
116
+ }
117
+
118
+ const collection = ref.slice(0, slashIdx);
119
+ const pattern = ref.slice(slashIdx + 1);
120
+ const listResult = await store.listDocuments(collection);
121
+
122
+ if (listResult.ok) {
123
+ for (const doc of listResult.value) {
124
+ if (doc.active && minimatch(doc.relPath, pattern)) {
125
+ expanded.push(`${collection}/${doc.relPath}`);
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ return { expanded, invalidRefs };
132
+ }
133
+
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+ // Content Truncation
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+
138
+ const encoder = new TextEncoder();
139
+
140
+ function truncateContent(
141
+ content: string,
142
+ maxBytes: number
143
+ ): { content: string; truncated: boolean } {
144
+ if (encoder.encode(content).length <= maxBytes) {
145
+ return { content, truncated: false };
146
+ }
147
+
148
+ const lines = content.split('\n');
149
+ let accumulated = '';
150
+ let byteLen = 0;
151
+
152
+ for (const line of lines) {
153
+ const lineBytes = encoder.encode(`${line}\n`).length;
154
+ if (byteLen + lineBytes > maxBytes) {
155
+ return { content: accumulated.trimEnd(), truncated: true };
156
+ }
157
+ accumulated += `${line}\n`;
158
+ byteLen += lineBytes;
159
+ }
160
+
161
+ return { content: accumulated.trimEnd(), truncated: false };
162
+ }
163
+
164
+ // ─────────────────────────────────────────────────────────────────────────────
165
+ // Document Fetching
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+
168
+ interface FetchContext {
169
+ store: StorePort;
170
+ config: ConfigLike;
171
+ maxBytes: number;
172
+ documents: MultiGetDocument[];
173
+ skipped: SkippedDoc[];
174
+ seen: Set<string>;
175
+ }
176
+
177
+ async function fetchSingleDocument(
178
+ ref: string,
179
+ ctx: FetchContext
180
+ ): Promise<void> {
181
+ if (ctx.seen.has(ref)) {
182
+ return;
183
+ }
184
+ ctx.seen.add(ref);
185
+
186
+ const parsed = parseRef(ref);
187
+ if ('error' in parsed) {
188
+ ctx.skipped.push({ ref, reason: 'invalid_ref' });
189
+ return;
190
+ }
191
+
192
+ const docResult = await lookupDocument(ctx.store, parsed);
193
+ if (!docResult.ok) {
194
+ ctx.skipped.push({ ref, reason: 'not_found' });
195
+ return;
196
+ }
197
+
198
+ const doc = docResult.value;
199
+ if (!doc?.active) {
200
+ ctx.skipped.push({ ref, reason: 'not_found' });
201
+ return;
202
+ }
203
+
204
+ if (!doc.mirrorHash) {
205
+ ctx.skipped.push({ ref, reason: 'conversion_error' });
206
+ return;
207
+ }
208
+
209
+ const contentResult = await ctx.store.getContent(doc.mirrorHash);
210
+ if (!contentResult.ok || contentResult.value === null) {
211
+ ctx.skipped.push({ ref, reason: 'conversion_error' });
212
+ return;
213
+ }
214
+
215
+ const { content, truncated } = truncateContent(
216
+ contentResult.value,
217
+ ctx.maxBytes
218
+ );
219
+ const coll = ctx.config.collections.find((c) => c.name === doc.collection);
220
+
221
+ ctx.documents.push({
222
+ docid: doc.docid,
223
+ uri: doc.uri,
224
+ title: doc.title ?? undefined,
225
+ content,
226
+ truncated: truncated || undefined,
227
+ totalLines: content.split('\n').length,
228
+ source: {
229
+ absPath: coll ? `${coll.path}/${doc.relPath}` : undefined,
230
+ relPath: doc.relPath,
231
+ mime: doc.sourceMime,
232
+ ext: doc.sourceExt,
233
+ },
234
+ });
235
+ }
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ // Command Implementation
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+
241
+ /**
242
+ * Execute gno multi-get command.
243
+ */
244
+ export async function multiGet(
245
+ refs: string[],
246
+ options: MultiGetCommandOptions = {}
247
+ ): Promise<MultiGetResult> {
248
+ const maxBytes = options.maxBytes ?? 10_240;
249
+ const allRefs = splitRefs(refs);
250
+
251
+ const initResult = await initStore({ configPath: options.configPath });
252
+ if (!initResult.ok) {
253
+ return { success: false, error: initResult.error };
254
+ }
255
+ const { store, config } = initResult;
256
+
257
+ try {
258
+ const { expanded: expandedRefs, invalidRefs } = await expandGlobs(
259
+ allRefs,
260
+ store
261
+ );
262
+ const ctx: FetchContext = {
263
+ store,
264
+ config,
265
+ maxBytes,
266
+ documents: [],
267
+ skipped: [],
268
+ seen: new Set(),
269
+ };
270
+
271
+ // Track invalid refs as skipped
272
+ for (const ref of invalidRefs) {
273
+ ctx.skipped.push({ ref, reason: 'invalid_ref' });
274
+ }
275
+
276
+ for (const ref of expandedRefs) {
277
+ await fetchSingleDocument(ref, ctx);
278
+ }
279
+
280
+ const totalRequested = expandedRefs.length + invalidRefs.length;
281
+ return {
282
+ success: true,
283
+ data: {
284
+ documents: ctx.documents,
285
+ skipped: ctx.skipped,
286
+ meta: {
287
+ requested: totalRequested,
288
+ returned: ctx.documents.length,
289
+ skipped: ctx.skipped.length,
290
+ maxBytes,
291
+ },
292
+ },
293
+ };
294
+ } finally {
295
+ await store.close();
296
+ }
297
+ }
298
+
299
+ // ─────────────────────────────────────────────────────────────────────────────
300
+ // Formatter
301
+ // ─────────────────────────────────────────────────────────────────────────────
302
+
303
+ function addLineNumbers(text: string, startLine = 1): string {
304
+ return text
305
+ .split('\n')
306
+ .map((line, i) => `${startLine + i}: ${line}`)
307
+ .join('\n');
308
+ }
309
+
310
+ function formatContent(content: string, lineNumbers: boolean): string {
311
+ return lineNumbers ? addLineNumbers(content) : content;
312
+ }
313
+
314
+ function formatMarkdown(
315
+ data: MultiGetResponse,
316
+ options: MultiGetCommandOptions
317
+ ): string {
318
+ const lines: string[] = [];
319
+ lines.push('# Multi-Get Results');
320
+ lines.push('');
321
+ lines.push(`*${data.meta.returned} of ${data.meta.requested} documents*`);
322
+ lines.push('');
323
+
324
+ for (const doc of data.documents) {
325
+ lines.push(`## ${doc.title || doc.source.relPath}`);
326
+ lines.push(`- **URI**: \`${doc.uri}\``);
327
+ if (doc.truncated) {
328
+ lines.push(`- **Truncated**: yes (max ${data.meta.maxBytes} bytes)`);
329
+ }
330
+ lines.push('');
331
+ lines.push('```');
332
+ lines.push(formatContent(doc.content, Boolean(options.lineNumbers)));
333
+ lines.push('```');
334
+ lines.push('');
335
+ }
336
+
337
+ if (data.skipped.length > 0) {
338
+ lines.push('## Skipped');
339
+ for (const s of data.skipped) {
340
+ lines.push(`- ${s.ref}: ${s.reason}`);
341
+ }
342
+ }
343
+
344
+ return lines.join('\n');
345
+ }
346
+
347
+ function formatTerminal(
348
+ data: MultiGetResponse,
349
+ options: MultiGetCommandOptions
350
+ ): string {
351
+ const lines: string[] = [];
352
+
353
+ for (const doc of data.documents) {
354
+ lines.push(`=== ${doc.uri} ===`);
355
+ lines.push(formatContent(doc.content, Boolean(options.lineNumbers)));
356
+ lines.push('');
357
+ }
358
+
359
+ if (data.skipped.length > 0) {
360
+ lines.push(`Skipped: ${data.skipped.map((s) => s.ref).join(', ')}`);
361
+ }
362
+
363
+ lines.push(
364
+ `${data.meta.returned}/${data.meta.requested} documents retrieved`
365
+ );
366
+ return lines.join('\n');
367
+ }
368
+
369
+ /**
370
+ * Format multi-get result for output.
371
+ */
372
+ export function formatMultiGet(
373
+ result: MultiGetResult,
374
+ options: MultiGetCommandOptions
375
+ ): string {
376
+ if (!result.success) {
377
+ if (options.json) {
378
+ return JSON.stringify({
379
+ error: { code: 'MULTI_GET_FAILED', message: result.error },
380
+ });
381
+ }
382
+ return `Error: ${result.error}`;
383
+ }
384
+
385
+ const { data } = result;
386
+
387
+ if (options.json) {
388
+ return JSON.stringify(data, null, 2);
389
+ }
390
+
391
+ if (options.files) {
392
+ return data.documents.map((d) => `${d.docid},${d.uri}`).join('\n');
393
+ }
394
+
395
+ if (options.md) {
396
+ return formatMarkdown(data, options);
397
+ }
398
+
399
+ return formatTerminal(data, options);
400
+ }