@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,109 @@
1
+ /**
2
+ * CLI command: gno context list
3
+ *
4
+ * List all configured contexts.
5
+ *
6
+ * @module src/cli/commands/context/list
7
+ */
8
+
9
+ import { loadConfig } from '../../../config';
10
+
11
+ /**
12
+ * Exit codes
13
+ */
14
+ const EXIT_SUCCESS = 0;
15
+
16
+ /**
17
+ * Output format
18
+ */
19
+ export type OutputFormat = 'terminal' | 'json' | 'md';
20
+
21
+ /**
22
+ * List all configured contexts.
23
+ *
24
+ * @param format - Output format (terminal, json, md)
25
+ * @returns Exit code
26
+ */
27
+ export async function contextList(
28
+ format: OutputFormat = 'terminal'
29
+ ): Promise<number> {
30
+ // Load config
31
+ const configResult = await loadConfig();
32
+ if (!configResult.ok) {
33
+ console.error(`Error: ${configResult.error.message}`);
34
+ return 1;
35
+ }
36
+
37
+ const { contexts } = configResult.value;
38
+
39
+ // Format and output
40
+ formatOutput(format, contexts);
41
+ return EXIT_SUCCESS;
42
+ }
43
+
44
+ /**
45
+ * Format and output context list
46
+ */
47
+ function formatOutput(
48
+ format: OutputFormat,
49
+ contexts: Array<{ scopeKey: string; text: string }>
50
+ ): void {
51
+ if (format === 'json') {
52
+ formatJson(contexts);
53
+ return;
54
+ }
55
+
56
+ if (format === 'md') {
57
+ formatMarkdown(contexts);
58
+ return;
59
+ }
60
+
61
+ formatTerminal(contexts);
62
+ }
63
+
64
+ /**
65
+ * Format contexts as JSON
66
+ */
67
+ function formatJson(contexts: Array<{ scopeKey: string; text: string }>): void {
68
+ const output = contexts.map((ctx) => ({
69
+ scope: ctx.scopeKey,
70
+ text: ctx.text,
71
+ }));
72
+ console.log(JSON.stringify(output, null, 2));
73
+ }
74
+
75
+ /**
76
+ * Format contexts as markdown
77
+ */
78
+ function formatMarkdown(
79
+ contexts: Array<{ scopeKey: string; text: string }>
80
+ ): void {
81
+ console.log('# Contexts\n');
82
+ if (contexts.length === 0) {
83
+ console.log('No contexts configured.');
84
+ return;
85
+ }
86
+
87
+ console.log('| Scope | Text |');
88
+ console.log('|-------|------|');
89
+ for (const ctx of contexts) {
90
+ console.log(`| ${ctx.scopeKey} | ${ctx.text} |`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Format contexts for terminal
96
+ */
97
+ function formatTerminal(
98
+ contexts: Array<{ scopeKey: string; text: string }>
99
+ ): void {
100
+ if (contexts.length === 0) {
101
+ console.log('No contexts configured.');
102
+ return;
103
+ }
104
+
105
+ console.log('Contexts:');
106
+ for (const ctx of contexts) {
107
+ console.log(` ${ctx.scopeKey} - ${ctx.text}`);
108
+ }
109
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CLI command: gno context rm
3
+ *
4
+ * Remove a context.
5
+ *
6
+ * @module src/cli/commands/context/rm
7
+ */
8
+
9
+ import { loadConfig, saveConfig } from '../../../config';
10
+
11
+ /**
12
+ * Exit codes
13
+ */
14
+ const EXIT_SUCCESS = 0;
15
+ const EXIT_VALIDATION = 1;
16
+
17
+ /**
18
+ * Remove a context by scope.
19
+ *
20
+ * @param scope - Scope key to remove
21
+ * @returns Exit code
22
+ */
23
+ export async function contextRm(scope: string): Promise<number> {
24
+ // Load config
25
+ const configResult = await loadConfig();
26
+ if (!configResult.ok) {
27
+ console.error(`Error: ${configResult.error.message}`);
28
+ return EXIT_VALIDATION;
29
+ }
30
+
31
+ const config = configResult.value;
32
+
33
+ // Find context
34
+ const index = config.contexts.findIndex((ctx) => ctx.scopeKey === scope);
35
+ if (index === -1) {
36
+ console.error(`Error: Context for scope "${scope}" not found`);
37
+ return EXIT_VALIDATION;
38
+ }
39
+
40
+ // Remove context
41
+ config.contexts.splice(index, 1);
42
+
43
+ // Save config
44
+ const saveResult = await saveConfig(config);
45
+ if (!saveResult.ok) {
46
+ console.error(`Error: ${saveResult.error.message}`);
47
+ return EXIT_VALIDATION;
48
+ }
49
+
50
+ console.log(`Removed context for scope: ${scope}`);
51
+ return EXIT_SUCCESS;
52
+ }
@@ -0,0 +1,393 @@
1
+ /**
2
+ * gno doctor command implementation.
3
+ * Diagnose configuration and dependencies.
4
+ *
5
+ * @module src/cli/commands/doctor
6
+ */
7
+
8
+ import { Database } from 'bun:sqlite';
9
+ import { stat } from 'node:fs/promises';
10
+ // node:os: arch/platform detection (no Bun equivalent)
11
+ import { arch, platform } from 'node:os';
12
+ import { getIndexDbPath, getModelsCachePath } from '../../app/constants';
13
+ import { getConfigPaths, isInitialized, loadConfig } from '../../config';
14
+ import type { Config } from '../../config/types';
15
+ import { ModelCache } from '../../llm/cache';
16
+ import { getActivePreset } from '../../llm/registry';
17
+ import {
18
+ getCustomSqlitePath,
19
+ getExtensionLoadingMode,
20
+ getLoadAttempts,
21
+ } from '../../store/sqlite/setup';
22
+
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Types
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+
27
+ export type DoctorCheckStatus = 'ok' | 'warn' | 'error';
28
+
29
+ export interface DoctorCheck {
30
+ name: string;
31
+ status: DoctorCheckStatus;
32
+ message: string;
33
+ /** Additional diagnostic details (shown in verbose/json output) */
34
+ details?: string[];
35
+ }
36
+
37
+ export interface DoctorOptions {
38
+ /** Override config path */
39
+ configPath?: string;
40
+ /** Output as JSON */
41
+ json?: boolean;
42
+ /** Output as Markdown */
43
+ md?: boolean;
44
+ }
45
+
46
+ export interface DoctorResult {
47
+ healthy: boolean;
48
+ checks: DoctorCheck[];
49
+ }
50
+
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+ // Checks
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+
55
+ async function checkConfig(configPath?: string): Promise<DoctorCheck> {
56
+ const initialized = await isInitialized(configPath);
57
+ if (!initialized) {
58
+ return {
59
+ name: 'config',
60
+ status: 'error',
61
+ message: 'Config not found. Run: gno init',
62
+ };
63
+ }
64
+
65
+ const configResult = await loadConfig(configPath);
66
+ if (!configResult.ok) {
67
+ return {
68
+ name: 'config',
69
+ status: 'error',
70
+ message: `Config invalid: ${configResult.error.message}`,
71
+ };
72
+ }
73
+
74
+ const paths = getConfigPaths();
75
+ return {
76
+ name: 'config',
77
+ status: 'ok',
78
+ message: `Config loaded: ${paths.configFile}`,
79
+ };
80
+ }
81
+
82
+ async function checkDatabase(): Promise<DoctorCheck> {
83
+ const dbPath = getIndexDbPath();
84
+
85
+ try {
86
+ await stat(dbPath);
87
+ return {
88
+ name: 'database',
89
+ status: 'ok',
90
+ message: `Database found: ${dbPath}`,
91
+ };
92
+ } catch {
93
+ return {
94
+ name: 'database',
95
+ status: 'warn',
96
+ message: 'Database not found. Run: gno init',
97
+ };
98
+ }
99
+ }
100
+
101
+ async function checkModels(config: Config): Promise<DoctorCheck[]> {
102
+ const checks: DoctorCheck[] = [];
103
+ const cache = new ModelCache(getModelsCachePath());
104
+ const preset = getActivePreset(config);
105
+
106
+ for (const type of ['embed', 'rerank', 'gen'] as const) {
107
+ const uri = preset[type];
108
+ const cached = await cache.isCached(uri);
109
+
110
+ checks.push({
111
+ name: `${type}-model`,
112
+ status: cached ? 'ok' : 'warn',
113
+ message: cached
114
+ ? `${type} model cached`
115
+ : `${type} model not cached. Run: gno models pull --${type}`,
116
+ });
117
+ }
118
+
119
+ return checks;
120
+ }
121
+
122
+ async function checkNodeLlamaCpp(): Promise<DoctorCheck> {
123
+ try {
124
+ const { getLlama } = await import('node-llama-cpp');
125
+ // Just check that we can get the llama instance
126
+ await getLlama();
127
+ return {
128
+ name: 'node-llama-cpp',
129
+ status: 'ok',
130
+ message: 'node-llama-cpp loaded successfully',
131
+ };
132
+ } catch (e) {
133
+ const message = e instanceof Error ? e.message : String(e);
134
+ return {
135
+ name: 'node-llama-cpp',
136
+ status: 'error',
137
+ message: `node-llama-cpp failed: ${message}`,
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Check SQLite extension support (FTS5, sqlite-vec).
144
+ * Uses runtime capability probes instead of compile_options strings.
145
+ */
146
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: diagnostic checks with platform-specific handling
147
+ async function checkSqliteExtensions(): Promise<DoctorCheck[]> {
148
+ const checks: DoctorCheck[] = [];
149
+ const plat = platform();
150
+ const archName = arch();
151
+ const mode = getExtensionLoadingMode();
152
+ const customPath = getCustomSqlitePath();
153
+ const attempts = getLoadAttempts();
154
+
155
+ // Platform/mode info
156
+ let modeDesc = 'unavailable';
157
+ if (mode === 'native') {
158
+ modeDesc = 'native (bundled SQLite supports extensions)';
159
+ } else if (mode === 'custom') {
160
+ modeDesc = `custom (${customPath})`;
161
+ }
162
+
163
+ const details: string[] = [
164
+ `Platform: ${plat}-${archName}`,
165
+ `Mode: ${modeDesc}`,
166
+ ];
167
+
168
+ // Add load attempt details if there were failures
169
+ if (attempts.length > 0) {
170
+ details.push('Load attempts:');
171
+ for (const attempt of attempts) {
172
+ details.push(` ${attempt.path}: ${attempt.error}`);
173
+ }
174
+ }
175
+
176
+ // Create in-memory DB for probes
177
+ const db = new Database(':memory:');
178
+ let version = 'unknown';
179
+
180
+ try {
181
+ const row = db.query('SELECT sqlite_version() as v').get() as { v: string };
182
+ version = row.v;
183
+ details.push(`SQLite version: ${version}`);
184
+ } catch {
185
+ // Continue with unknown version
186
+ }
187
+
188
+ // Probe FTS5 capability
189
+ let fts5Available = false;
190
+ try {
191
+ db.exec('CREATE VIRTUAL TABLE _fts5_probe USING fts5(x)');
192
+ db.exec('DROP TABLE _fts5_probe');
193
+ fts5Available = true;
194
+ } catch {
195
+ // FTS5 not available
196
+ }
197
+
198
+ checks.push({
199
+ name: 'sqlite-fts5',
200
+ status: fts5Available ? 'ok' : 'error',
201
+ message: fts5Available ? 'FTS5 available' : 'FTS5 not available (required)',
202
+ details: fts5Available
203
+ ? undefined
204
+ : ['Full-text search requires FTS5 support'],
205
+ });
206
+
207
+ // Probe JSON capability
208
+ let jsonAvailable = false;
209
+ try {
210
+ db.query("SELECT json_valid('{}')").get();
211
+ jsonAvailable = true;
212
+ } catch {
213
+ // JSON not available
214
+ }
215
+
216
+ checks.push({
217
+ name: 'sqlite-json',
218
+ status: jsonAvailable ? 'ok' : 'warn',
219
+ message: jsonAvailable ? 'JSON1 available' : 'JSON1 not available',
220
+ });
221
+
222
+ // Probe sqlite-vec extension
223
+ let sqliteVecAvailable = false;
224
+ let sqliteVecVersion = '';
225
+ let sqliteVecError = '';
226
+ try {
227
+ const sqliteVec = await import('sqlite-vec');
228
+ sqliteVec.load(db);
229
+ sqliteVecAvailable = true;
230
+ // Try to get version
231
+ try {
232
+ const vrow = db.query('SELECT vec_version() as v').get() as { v: string };
233
+ sqliteVecVersion = vrow.v;
234
+ } catch {
235
+ // No version available
236
+ }
237
+ } catch (e) {
238
+ sqliteVecError = e instanceof Error ? e.message : String(e);
239
+ }
240
+
241
+ let vecMessage: string;
242
+ if (sqliteVecAvailable) {
243
+ vecMessage = sqliteVecVersion
244
+ ? `sqlite-vec loaded (v${sqliteVecVersion})`
245
+ : 'sqlite-vec loaded';
246
+ } else if (mode === 'unavailable') {
247
+ vecMessage =
248
+ 'sqlite-vec unavailable (no extension support on macOS without Homebrew)';
249
+ } else {
250
+ vecMessage = sqliteVecError
251
+ ? `sqlite-vec failed: ${sqliteVecError}`
252
+ : 'sqlite-vec failed to load';
253
+ }
254
+
255
+ const vecDetails = [...details];
256
+ if (!sqliteVecAvailable && plat === 'darwin' && mode === 'unavailable') {
257
+ vecDetails.push('Install Homebrew SQLite: brew install sqlite3');
258
+ }
259
+ if (sqliteVecError) {
260
+ vecDetails.push(`Load error: ${sqliteVecError}`);
261
+ }
262
+
263
+ checks.push({
264
+ name: 'sqlite-vec',
265
+ status: sqliteVecAvailable ? 'ok' : 'warn',
266
+ message: vecMessage,
267
+ details: vecDetails,
268
+ });
269
+
270
+ db.close();
271
+ return checks;
272
+ }
273
+
274
+ // ─────────────────────────────────────────────────────────────────────────────
275
+ // Implementation
276
+ // ─────────────────────────────────────────────────────────────────────────────
277
+
278
+ /**
279
+ * Execute gno doctor command.
280
+ */
281
+ export async function doctor(
282
+ options: DoctorOptions = {}
283
+ ): Promise<DoctorResult> {
284
+ const checks: DoctorCheck[] = [];
285
+
286
+ // Config check
287
+ checks.push(await checkConfig(options.configPath));
288
+
289
+ // Database check
290
+ checks.push(await checkDatabase());
291
+
292
+ // Load config for model checks (if available)
293
+ const { createDefaultConfig } = await import('../../config');
294
+ const configResult = await loadConfig(options.configPath);
295
+ const config = configResult.ok ? configResult.value : createDefaultConfig();
296
+
297
+ // Model checks
298
+ const modelChecks = await checkModels(config);
299
+ checks.push(...modelChecks);
300
+
301
+ // node-llama-cpp check
302
+ checks.push(await checkNodeLlamaCpp());
303
+
304
+ // SQLite extension checks
305
+ const sqliteChecks = await checkSqliteExtensions();
306
+ checks.push(...sqliteChecks);
307
+
308
+ // Determine overall health
309
+ const hasErrors = checks.some((c) => c.status === 'error');
310
+
311
+ return {
312
+ healthy: !hasErrors,
313
+ checks,
314
+ };
315
+ }
316
+
317
+ // ─────────────────────────────────────────────────────────────────────────────
318
+ // Formatting
319
+ // ─────────────────────────────────────────────────────────────────────────────
320
+
321
+ function statusIcon(status: DoctorCheckStatus): string {
322
+ switch (status) {
323
+ case 'ok':
324
+ return '✓';
325
+ case 'warn':
326
+ return '!';
327
+ case 'error':
328
+ return '✗';
329
+ default:
330
+ return '?';
331
+ }
332
+ }
333
+
334
+ function formatTerminal(result: DoctorResult): string {
335
+ const lines: string[] = [];
336
+
337
+ lines.push('GNO Health Check');
338
+ lines.push('');
339
+
340
+ for (const check of result.checks) {
341
+ lines.push(` ${statusIcon(check.status)} ${check.name}: ${check.message}`);
342
+ // Show details for non-ok checks
343
+ if (check.details && check.status !== 'ok') {
344
+ for (const detail of check.details) {
345
+ lines.push(` ${detail}`);
346
+ }
347
+ }
348
+ }
349
+
350
+ lines.push('');
351
+ lines.push(`Overall: ${result.healthy ? 'HEALTHY' : 'UNHEALTHY'}`);
352
+
353
+ return lines.join('\n');
354
+ }
355
+
356
+ function formatMarkdown(result: DoctorResult): string {
357
+ const lines: string[] = [];
358
+
359
+ lines.push('# GNO Health Check');
360
+ lines.push('');
361
+ lines.push(`**Status**: ${result.healthy ? '✓ Healthy' : '✗ Unhealthy'}`);
362
+ lines.push('');
363
+ lines.push('## Checks');
364
+ lines.push('');
365
+ lines.push('| Check | Status | Message |');
366
+ lines.push('|-------|--------|---------|');
367
+
368
+ for (const check of result.checks) {
369
+ lines.push(
370
+ `| ${check.name} | ${statusIcon(check.status)} | ${check.message} |`
371
+ );
372
+ }
373
+
374
+ return lines.join('\n');
375
+ }
376
+
377
+ /**
378
+ * Format doctor result for output.
379
+ */
380
+ export function formatDoctor(
381
+ result: DoctorResult,
382
+ options: DoctorOptions
383
+ ): string {
384
+ if (options.json) {
385
+ return JSON.stringify(result, null, 2);
386
+ }
387
+
388
+ if (options.md) {
389
+ return formatMarkdown(result);
390
+ }
391
+
392
+ return formatTerminal(result);
393
+ }