@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,470 @@
1
+ /**
2
+ * Path resolution for MCP server configuration.
3
+ * Supports Claude Desktop, Claude Code, and Codex targets.
4
+ *
5
+ * @module src/cli/commands/mcp/paths
6
+ */
7
+
8
+ import { existsSync } from 'node:fs';
9
+ import { homedir, platform } from 'node:os';
10
+ import { join } from 'node:path';
11
+
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Types
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ export type McpTarget =
17
+ | 'claude-desktop'
18
+ | 'claude-code'
19
+ | 'codex'
20
+ | 'cursor'
21
+ | 'zed'
22
+ | 'windsurf'
23
+ | 'opencode'
24
+ | 'amp'
25
+ | 'lmstudio'
26
+ | 'librechat';
27
+
28
+ export type McpScope = 'user' | 'project';
29
+
30
+ /**
31
+ * Config format varies by target.
32
+ * - standard: mcpServers key (Claude Desktop, Cursor, Windsurf, LM Studio)
33
+ * - context_servers: Zed uses context_servers key
34
+ * - mcp: OpenCode uses mcp key with array command
35
+ * - amp_mcp: Amp uses amp.mcpServers key
36
+ * - yaml_standard: YAML file with mcpServers key (LibreChat)
37
+ */
38
+ export type McpConfigFormat =
39
+ | 'standard'
40
+ | 'context_servers'
41
+ | 'mcp'
42
+ | 'amp_mcp'
43
+ | 'yaml_standard';
44
+
45
+ export interface McpConfigPaths {
46
+ /** Config file path */
47
+ configPath: string;
48
+ /** Whether this target supports project scope */
49
+ supportsProjectScope: boolean;
50
+ /** Config format for this target */
51
+ configFormat: McpConfigFormat;
52
+ }
53
+
54
+ export interface McpServerEntry {
55
+ command: string;
56
+ args: string[];
57
+ }
58
+
59
+ export interface McpPathOptions {
60
+ target: McpTarget;
61
+ scope?: McpScope;
62
+ /** Override cwd for project scope (testing) */
63
+ cwd?: string;
64
+ /** Override home dir (testing) */
65
+ homeDir?: string;
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+ // Constants
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+
72
+ export const MCP_SERVER_NAME = 'gno';
73
+
74
+ /** All supported MCP targets */
75
+ export const MCP_TARGETS: McpTarget[] = [
76
+ 'claude-desktop',
77
+ 'claude-code',
78
+ 'codex',
79
+ 'cursor',
80
+ 'zed',
81
+ 'windsurf',
82
+ 'opencode',
83
+ 'amp',
84
+ 'lmstudio',
85
+ 'librechat',
86
+ ];
87
+
88
+ /** Targets that support project scope */
89
+ export const TARGETS_WITH_PROJECT_SCOPE: McpTarget[] = [
90
+ 'claude-code',
91
+ 'codex',
92
+ 'cursor',
93
+ 'opencode',
94
+ 'librechat',
95
+ ];
96
+
97
+ /** Get config format for a target */
98
+ export function getTargetConfigFormat(target: McpTarget): McpConfigFormat {
99
+ switch (target) {
100
+ case 'zed':
101
+ return 'context_servers';
102
+ case 'opencode':
103
+ return 'mcp';
104
+ case 'amp':
105
+ return 'amp_mcp';
106
+ case 'librechat':
107
+ return 'yaml_standard';
108
+ default:
109
+ return 'standard';
110
+ }
111
+ }
112
+
113
+ /** Regex to extract entry script path from command path */
114
+ const COMMANDS_PATH_PATTERN = /\/commands\/.*$/;
115
+
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+ // Config Path Resolution
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Resolve Claude Desktop config path based on platform.
122
+ */
123
+ function resolveClaudeDesktopPath(home: string): string {
124
+ const plat = platform();
125
+
126
+ if (plat === 'darwin') {
127
+ return join(
128
+ home,
129
+ 'Library/Application Support/Claude/claude_desktop_config.json'
130
+ );
131
+ }
132
+ if (plat === 'win32') {
133
+ const appData = process.env.APPDATA || join(home, 'AppData/Roaming');
134
+ return join(appData, 'Claude/claude_desktop_config.json');
135
+ }
136
+ // Linux and other Unix
137
+ return join(home, '.config/Claude/claude_desktop_config.json');
138
+ }
139
+
140
+ /**
141
+ * Resolve Claude Code config path.
142
+ */
143
+ function resolveClaudeCodePath(
144
+ scope: McpScope,
145
+ home: string,
146
+ cwd: string
147
+ ): string {
148
+ if (scope === 'user') {
149
+ return join(home, '.claude.json');
150
+ }
151
+ return join(cwd, '.mcp.json');
152
+ }
153
+
154
+ /**
155
+ * Resolve Codex config path.
156
+ */
157
+ function resolveCodexPath(scope: McpScope, home: string, cwd: string): string {
158
+ if (scope === 'user') {
159
+ return join(home, '.codex.json');
160
+ }
161
+ return join(cwd, '.codex/.mcp.json');
162
+ }
163
+
164
+ /**
165
+ * Resolve Cursor config path.
166
+ */
167
+ function resolveCursorPath(scope: McpScope, home: string, cwd: string): string {
168
+ if (scope === 'user') {
169
+ const plat = platform();
170
+ if (plat === 'win32') {
171
+ return join(home, '.cursor', 'mcp.json');
172
+ }
173
+ return join(home, '.cursor/mcp.json');
174
+ }
175
+ return join(cwd, '.cursor/mcp.json');
176
+ }
177
+
178
+ /**
179
+ * Resolve Zed config path (macOS/Linux only, no project scope).
180
+ */
181
+ function resolveZedPath(home: string): string {
182
+ const plat = platform();
183
+ if (plat === 'win32') {
184
+ // Zed not available on Windows, but provide path anyway
185
+ return join(home, '.config/zed/settings.json');
186
+ }
187
+ // macOS and Linux use XDG or fallback
188
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
189
+ if (xdgConfig) {
190
+ return join(xdgConfig, 'zed/settings.json');
191
+ }
192
+ return join(home, '.config/zed/settings.json');
193
+ }
194
+
195
+ /**
196
+ * Resolve Windsurf config path.
197
+ */
198
+ function resolveWindsurfPath(home: string): string {
199
+ const plat = platform();
200
+ if (plat === 'win32') {
201
+ return join(home, '.codeium', 'windsurf', 'mcp_config.json');
202
+ }
203
+ return join(home, '.codeium/windsurf/mcp_config.json');
204
+ }
205
+
206
+ /**
207
+ * Resolve OpenCode config path.
208
+ */
209
+ function resolveOpenCodePath(
210
+ scope: McpScope,
211
+ home: string,
212
+ cwd: string
213
+ ): string {
214
+ if (scope === 'user') {
215
+ const plat = platform();
216
+ if (plat === 'win32') {
217
+ return join(home, '.config', 'opencode', 'config.json');
218
+ }
219
+ return join(home, '.config/opencode/config.json');
220
+ }
221
+ // Project scope: opencode.json in project root
222
+ return join(cwd, 'opencode.json');
223
+ }
224
+
225
+ /**
226
+ * Resolve Amp config path.
227
+ */
228
+ function resolveAmpPath(home: string): string {
229
+ const plat = platform();
230
+ if (plat === 'win32') {
231
+ return join(home, '.config', 'amp', 'settings.json');
232
+ }
233
+ return join(home, '.config/amp/settings.json');
234
+ }
235
+
236
+ /**
237
+ * Resolve LM Studio config path.
238
+ */
239
+ function resolveLmStudioPath(home: string): string {
240
+ const plat = platform();
241
+ if (plat === 'win32') {
242
+ return join(home, '.lmstudio', 'mcp.json');
243
+ }
244
+ return join(home, '.lmstudio/mcp.json');
245
+ }
246
+
247
+ /**
248
+ * Resolve LibreChat config path.
249
+ * LibreChat uses librechat.yaml in project root (project scope only).
250
+ */
251
+ function resolveLibreChatPath(cwd: string): string {
252
+ return join(cwd, 'librechat.yaml');
253
+ }
254
+
255
+ /**
256
+ * Resolve MCP config path for a given target and scope.
257
+ */
258
+ export function resolveMcpConfigPath(opts: McpPathOptions): McpConfigPaths {
259
+ const {
260
+ target,
261
+ scope = 'user',
262
+ cwd = process.cwd(),
263
+ homeDir = homedir(),
264
+ } = opts;
265
+
266
+ const configFormat = getTargetConfigFormat(target);
267
+ const supportsProjectScope = TARGETS_WITH_PROJECT_SCOPE.includes(target);
268
+
269
+ switch (target) {
270
+ case 'claude-desktop':
271
+ return {
272
+ configPath: resolveClaudeDesktopPath(homeDir),
273
+ supportsProjectScope,
274
+ configFormat,
275
+ };
276
+ case 'claude-code':
277
+ return {
278
+ configPath: resolveClaudeCodePath(scope, homeDir, cwd),
279
+ supportsProjectScope,
280
+ configFormat,
281
+ };
282
+ case 'codex':
283
+ return {
284
+ configPath: resolveCodexPath(scope, homeDir, cwd),
285
+ supportsProjectScope,
286
+ configFormat,
287
+ };
288
+ case 'cursor':
289
+ return {
290
+ configPath: resolveCursorPath(scope, homeDir, cwd),
291
+ supportsProjectScope,
292
+ configFormat,
293
+ };
294
+ case 'zed':
295
+ return {
296
+ configPath: resolveZedPath(homeDir),
297
+ supportsProjectScope,
298
+ configFormat,
299
+ };
300
+ case 'windsurf':
301
+ return {
302
+ configPath: resolveWindsurfPath(homeDir),
303
+ supportsProjectScope,
304
+ configFormat,
305
+ };
306
+ case 'opencode':
307
+ return {
308
+ configPath: resolveOpenCodePath(scope, homeDir, cwd),
309
+ supportsProjectScope,
310
+ configFormat,
311
+ };
312
+ case 'amp':
313
+ return {
314
+ configPath: resolveAmpPath(homeDir),
315
+ supportsProjectScope,
316
+ configFormat,
317
+ };
318
+ case 'lmstudio':
319
+ return {
320
+ configPath: resolveLmStudioPath(homeDir),
321
+ supportsProjectScope,
322
+ configFormat,
323
+ };
324
+ case 'librechat':
325
+ return {
326
+ configPath: resolveLibreChatPath(cwd),
327
+ supportsProjectScope,
328
+ configFormat,
329
+ };
330
+ default: {
331
+ const _exhaustive: never = target;
332
+ throw new Error(`Unknown target: ${_exhaustive}`);
333
+ }
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Resolve paths for all targets.
339
+ */
340
+ export function resolveAllMcpPaths(
341
+ scope: McpScope | 'all' = 'all',
342
+ target: McpTarget | 'all' = 'all',
343
+ overrides?: { cwd?: string; homeDir?: string }
344
+ ): Array<{ target: McpTarget; scope: McpScope; paths: McpConfigPaths }> {
345
+ const targets: McpTarget[] = target === 'all' ? MCP_TARGETS : [target];
346
+ const results: Array<{
347
+ target: McpTarget;
348
+ scope: McpScope;
349
+ paths: McpConfigPaths;
350
+ }> = [];
351
+
352
+ for (const t of targets) {
353
+ const supportsProject = TARGETS_WITH_PROJECT_SCOPE.includes(t);
354
+
355
+ if (supportsProject) {
356
+ // Targets that support both scopes
357
+ const scopes: McpScope[] =
358
+ scope === 'all' ? ['user', 'project'] : [scope];
359
+ for (const s of scopes) {
360
+ results.push({
361
+ target: t,
362
+ scope: s,
363
+ paths: resolveMcpConfigPath({ target: t, scope: s, ...overrides }),
364
+ });
365
+ }
366
+ } else {
367
+ // User scope only - skip if filtering by project
368
+ if (scope === 'project') {
369
+ continue;
370
+ }
371
+ results.push({
372
+ target: t,
373
+ scope: 'user',
374
+ paths: resolveMcpConfigPath({ target: t, scope: 'user', ...overrides }),
375
+ });
376
+ }
377
+ }
378
+
379
+ return results;
380
+ }
381
+
382
+ // ─────────────────────────────────────────────────────────────────────────────
383
+ // Bun & GNO Path Discovery
384
+ // ─────────────────────────────────────────────────────────────────────────────
385
+
386
+ /**
387
+ * Find absolute path to bun executable.
388
+ * Cross-platform: uses process.execPath since we're already running under Bun.
389
+ */
390
+ export function findBunPath(): string {
391
+ // process.execPath is the absolute path to the Bun binary running this process.
392
+ // This is cross-platform and doesn't require shelling out to `which`.
393
+ return process.execPath;
394
+ }
395
+
396
+ /**
397
+ * Detect how gno should be invoked and return the MCP server entry.
398
+ * Uses absolute paths because Claude Desktop has a limited PATH.
399
+ * Cross-platform: avoids shelling out to `which`.
400
+ */
401
+ export function buildMcpServerEntry(): McpServerEntry {
402
+ const bunPath = findBunPath();
403
+ const home = homedir();
404
+ const isWindows = platform() === 'win32';
405
+
406
+ // 1. Check if running from source (dev mode)
407
+ const scriptPath = process.argv[1];
408
+ if (
409
+ scriptPath?.includes('/gno/src/cli/') ||
410
+ scriptPath?.includes('\\gno\\src\\cli\\')
411
+ ) {
412
+ // Dev mode: run the entry script directly with bun
413
+ const entryScript = scriptPath.replace(COMMANDS_PATH_PATTERN, '/index.ts');
414
+ return { command: bunPath, args: ['run', entryScript, 'mcp'] };
415
+ }
416
+
417
+ // 2. Check common gno install locations (cross-platform)
418
+ const gnoCandidates = isWindows
419
+ ? [
420
+ join(home, '.bun\\bin\\gno.exe'),
421
+ join(home, 'AppData\\Roaming\\npm\\gno.cmd'),
422
+ ]
423
+ : [
424
+ join(home, '.bun/bin/gno'),
425
+ '/usr/local/bin/gno',
426
+ '/opt/homebrew/bin/gno',
427
+ ];
428
+
429
+ for (const gnoPath of gnoCandidates) {
430
+ if (existsSync(gnoPath)) {
431
+ return { command: bunPath, args: [gnoPath, 'mcp'] };
432
+ }
433
+ }
434
+
435
+ // 3. Fallback to bunx (works if gno is published to npm)
436
+ // Note: This may trigger network access on first run
437
+ return { command: bunPath, args: ['x', '@gmickel/gno', 'mcp'] };
438
+ }
439
+
440
+ /**
441
+ * Get display name for a target.
442
+ */
443
+ export function getTargetDisplayName(target: McpTarget): string {
444
+ switch (target) {
445
+ case 'claude-desktop':
446
+ return 'Claude Desktop';
447
+ case 'claude-code':
448
+ return 'Claude Code';
449
+ case 'codex':
450
+ return 'Codex';
451
+ case 'cursor':
452
+ return 'Cursor';
453
+ case 'zed':
454
+ return 'Zed';
455
+ case 'windsurf':
456
+ return 'Windsurf';
457
+ case 'opencode':
458
+ return 'OpenCode';
459
+ case 'amp':
460
+ return 'Amp';
461
+ case 'lmstudio':
462
+ return 'LM Studio';
463
+ case 'librechat':
464
+ return 'LibreChat';
465
+ default: {
466
+ const _exhaustive: never = target;
467
+ throw new Error(`Unknown target: ${_exhaustive}`);
468
+ }
469
+ }
470
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Show MCP server installation status across all targets.
3
+ *
4
+ * @module src/cli/commands/mcp/status
5
+ */
6
+
7
+ import { getGlobals } from '../../program.js';
8
+ import {
9
+ type AnyMcpConfig,
10
+ getServerEntry,
11
+ isYamlFormat,
12
+ type OpenCodeMcpEntry,
13
+ type StandardMcpEntry,
14
+ } from './config.js';
15
+ import {
16
+ getTargetDisplayName,
17
+ MCP_SERVER_NAME,
18
+ MCP_TARGETS,
19
+ type McpScope,
20
+ type McpTarget,
21
+ resolveMcpConfigPath,
22
+ TARGETS_WITH_PROJECT_SCOPE,
23
+ } from './paths.js';
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // Types
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ export interface StatusOptions {
30
+ target?: McpTarget | 'all';
31
+ scope?: McpScope | 'all';
32
+ /** Override cwd (testing) */
33
+ cwd?: string;
34
+ /** Override home dir (testing) */
35
+ homeDir?: string;
36
+ /** JSON output */
37
+ json?: boolean;
38
+ }
39
+
40
+ interface TargetStatus {
41
+ target: McpTarget;
42
+ scope: McpScope;
43
+ configPath: string;
44
+ configured: boolean;
45
+ serverEntry?: { command: string; args: string[] };
46
+ error?: string;
47
+ }
48
+
49
+ interface StatusResult {
50
+ targets: TargetStatus[];
51
+ summary: {
52
+ configured: number;
53
+ total: number;
54
+ };
55
+ }
56
+
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+ // Config Reading
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Normalize entry to standard format for display.
63
+ */
64
+ function normalizeEntry(
65
+ entry: StandardMcpEntry | OpenCodeMcpEntry
66
+ ): StandardMcpEntry {
67
+ if ('type' in entry && entry.type === 'local') {
68
+ // OpenCode format: command is array [command, ...args]
69
+ const [command = '', ...args] = entry.command;
70
+ return { command, args };
71
+ }
72
+ return entry as StandardMcpEntry;
73
+ }
74
+
75
+ async function checkTargetStatus(
76
+ target: McpTarget,
77
+ scope: McpScope,
78
+ options: { cwd?: string; homeDir?: string }
79
+ ): Promise<TargetStatus> {
80
+ const { cwd, homeDir } = options;
81
+
82
+ const { configPath, configFormat } = resolveMcpConfigPath({
83
+ target,
84
+ scope,
85
+ cwd,
86
+ homeDir,
87
+ });
88
+
89
+ const file = Bun.file(configPath);
90
+ const exists = await file.exists();
91
+
92
+ if (!exists) {
93
+ return { target, scope, configPath, configured: false };
94
+ }
95
+
96
+ try {
97
+ const content = await file.text();
98
+ if (!content.trim()) {
99
+ return { target, scope, configPath, configured: false };
100
+ }
101
+
102
+ const useYaml = isYamlFormat(configFormat);
103
+ const config = useYaml
104
+ ? (Bun.YAML.parse(content) as AnyMcpConfig)
105
+ : (JSON.parse(content) as AnyMcpConfig);
106
+ const entry = getServerEntry(config, MCP_SERVER_NAME, configFormat);
107
+
108
+ if (entry) {
109
+ const serverEntry = normalizeEntry(entry);
110
+ return { target, scope, configPath, configured: true, serverEntry };
111
+ }
112
+
113
+ return { target, scope, configPath, configured: false };
114
+ } catch {
115
+ const format = isYamlFormat(configFormat) ? 'YAML' : 'JSON';
116
+ return {
117
+ target,
118
+ scope,
119
+ configPath,
120
+ configured: false,
121
+ error: `Malformed ${format}`,
122
+ };
123
+ }
124
+ }
125
+
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+ // Status Command
128
+ // ─────────────────────────────────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Get globals safely.
132
+ */
133
+ function safeGetGlobals(): { json: boolean } {
134
+ try {
135
+ return getGlobals();
136
+ } catch {
137
+ return { json: false };
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Show MCP installation status.
143
+ */
144
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: status display with multiple targets and scopes
145
+ export async function statusMcp(opts: StatusOptions = {}): Promise<void> {
146
+ const targetFilter = opts.target ?? 'all';
147
+ const scopeFilter = opts.scope ?? 'all';
148
+ const globals = safeGetGlobals();
149
+ const json = opts.json ?? globals.json;
150
+
151
+ const targets: McpTarget[] =
152
+ targetFilter === 'all' ? MCP_TARGETS : [targetFilter];
153
+
154
+ const results: TargetStatus[] = [];
155
+
156
+ for (const target of targets) {
157
+ const supportsProject = TARGETS_WITH_PROJECT_SCOPE.includes(target);
158
+
159
+ if (supportsProject) {
160
+ // Targets that support both scopes
161
+ const scopes: McpScope[] =
162
+ scopeFilter === 'all' ? ['user', 'project'] : [scopeFilter];
163
+
164
+ for (const scope of scopes) {
165
+ results.push(
166
+ await checkTargetStatus(target, scope, {
167
+ cwd: opts.cwd,
168
+ homeDir: opts.homeDir,
169
+ })
170
+ );
171
+ }
172
+ } else if (scopeFilter === 'all' || scopeFilter === 'user') {
173
+ // User scope only - skip if filtering by project
174
+ results.push(
175
+ await checkTargetStatus(target, 'user', {
176
+ cwd: opts.cwd,
177
+ homeDir: opts.homeDir,
178
+ })
179
+ );
180
+ }
181
+ }
182
+
183
+ const configured = results.filter((r) => r.configured).length;
184
+ const statusResult: StatusResult = {
185
+ targets: results,
186
+ summary: { configured, total: results.length },
187
+ };
188
+
189
+ // Output
190
+ if (json) {
191
+ process.stdout.write(`${JSON.stringify(statusResult, null, 2)}\n`);
192
+ return;
193
+ }
194
+
195
+ // Terminal output
196
+ process.stdout.write('MCP Server Status\n');
197
+ process.stdout.write(`${'─'.repeat(50)}\n\n`);
198
+
199
+ for (const status of results) {
200
+ const targetName = getTargetDisplayName(status.target);
201
+ const scopeLabel = status.scope === 'project' ? ' (project)' : '';
202
+ const statusIcon = status.configured ? '✓' : '✗';
203
+ const statusText = status.configured ? 'configured' : 'not configured';
204
+
205
+ process.stdout.write(
206
+ `${statusIcon} ${targetName}${scopeLabel}: ${statusText}\n`
207
+ );
208
+
209
+ if (status.configured && status.serverEntry) {
210
+ process.stdout.write(` Command: ${status.serverEntry.command}\n`);
211
+ process.stdout.write(` Args: ${status.serverEntry.args.join(' ')}\n`);
212
+ }
213
+
214
+ if (status.error) {
215
+ process.stdout.write(` Error: ${status.error}\n`);
216
+ }
217
+
218
+ process.stdout.write(` Config: ${status.configPath}\n\n`);
219
+ }
220
+
221
+ process.stdout.write(`${configured}/${results.length} targets configured\n`);
222
+ }