@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.
- package/README.md +256 -0
- package/assets/skill/SKILL.md +112 -0
- package/assets/skill/cli-reference.md +327 -0
- package/assets/skill/examples.md +234 -0
- package/assets/skill/mcp-reference.md +159 -0
- package/package.json +90 -0
- package/src/app/constants.ts +313 -0
- package/src/cli/colors.ts +65 -0
- package/src/cli/commands/ask.ts +545 -0
- package/src/cli/commands/cleanup.ts +105 -0
- package/src/cli/commands/collection/add.ts +120 -0
- package/src/cli/commands/collection/index.ts +10 -0
- package/src/cli/commands/collection/list.ts +108 -0
- package/src/cli/commands/collection/remove.ts +64 -0
- package/src/cli/commands/collection/rename.ts +95 -0
- package/src/cli/commands/context/add.ts +67 -0
- package/src/cli/commands/context/check.ts +153 -0
- package/src/cli/commands/context/index.ts +10 -0
- package/src/cli/commands/context/list.ts +109 -0
- package/src/cli/commands/context/rm.ts +52 -0
- package/src/cli/commands/doctor.ts +393 -0
- package/src/cli/commands/embed.ts +462 -0
- package/src/cli/commands/get.ts +356 -0
- package/src/cli/commands/index-cmd.ts +119 -0
- package/src/cli/commands/index.ts +102 -0
- package/src/cli/commands/init.ts +328 -0
- package/src/cli/commands/ls.ts +217 -0
- package/src/cli/commands/mcp/config.ts +300 -0
- package/src/cli/commands/mcp/index.ts +24 -0
- package/src/cli/commands/mcp/install.ts +203 -0
- package/src/cli/commands/mcp/paths.ts +470 -0
- package/src/cli/commands/mcp/status.ts +222 -0
- package/src/cli/commands/mcp/uninstall.ts +158 -0
- package/src/cli/commands/mcp.ts +20 -0
- package/src/cli/commands/models/clear.ts +103 -0
- package/src/cli/commands/models/index.ts +32 -0
- package/src/cli/commands/models/list.ts +214 -0
- package/src/cli/commands/models/path.ts +51 -0
- package/src/cli/commands/models/pull.ts +199 -0
- package/src/cli/commands/models/use.ts +85 -0
- package/src/cli/commands/multi-get.ts +400 -0
- package/src/cli/commands/query.ts +220 -0
- package/src/cli/commands/ref-parser.ts +108 -0
- package/src/cli/commands/reset.ts +191 -0
- package/src/cli/commands/search.ts +136 -0
- package/src/cli/commands/shared.ts +156 -0
- package/src/cli/commands/skill/index.ts +19 -0
- package/src/cli/commands/skill/install.ts +197 -0
- package/src/cli/commands/skill/paths-cmd.ts +81 -0
- package/src/cli/commands/skill/paths.ts +191 -0
- package/src/cli/commands/skill/show.ts +73 -0
- package/src/cli/commands/skill/uninstall.ts +141 -0
- package/src/cli/commands/status.ts +205 -0
- package/src/cli/commands/update.ts +68 -0
- package/src/cli/commands/vsearch.ts +188 -0
- package/src/cli/context.ts +64 -0
- package/src/cli/errors.ts +64 -0
- package/src/cli/format/search-results.ts +211 -0
- package/src/cli/options.ts +183 -0
- package/src/cli/program.ts +1330 -0
- package/src/cli/run.ts +213 -0
- package/src/cli/ui.ts +92 -0
- package/src/config/defaults.ts +20 -0
- package/src/config/index.ts +55 -0
- package/src/config/loader.ts +161 -0
- package/src/config/paths.ts +87 -0
- package/src/config/saver.ts +153 -0
- package/src/config/types.ts +280 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
- package/src/converters/adapters/officeparser/adapter.ts +126 -0
- package/src/converters/canonicalize.ts +89 -0
- package/src/converters/errors.ts +218 -0
- package/src/converters/index.ts +51 -0
- package/src/converters/mime.ts +163 -0
- package/src/converters/native/markdown.ts +115 -0
- package/src/converters/native/plaintext.ts +56 -0
- package/src/converters/path.ts +48 -0
- package/src/converters/pipeline.ts +159 -0
- package/src/converters/registry.ts +74 -0
- package/src/converters/types.ts +123 -0
- package/src/converters/versions.ts +24 -0
- package/src/index.ts +27 -0
- package/src/ingestion/chunker.ts +238 -0
- package/src/ingestion/index.ts +32 -0
- package/src/ingestion/language.ts +276 -0
- package/src/ingestion/sync.ts +671 -0
- package/src/ingestion/types.ts +219 -0
- package/src/ingestion/walker.ts +235 -0
- package/src/llm/cache.ts +467 -0
- package/src/llm/errors.ts +191 -0
- package/src/llm/index.ts +58 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
- package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
- package/src/llm/nodeLlamaCpp/generation.ts +88 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
- package/src/llm/registry.ts +86 -0
- package/src/llm/types.ts +129 -0
- package/src/mcp/resources/index.ts +151 -0
- package/src/mcp/server.ts +229 -0
- package/src/mcp/tools/get.ts +220 -0
- package/src/mcp/tools/index.ts +160 -0
- package/src/mcp/tools/multi-get.ts +263 -0
- package/src/mcp/tools/query.ts +226 -0
- package/src/mcp/tools/search.ts +119 -0
- package/src/mcp/tools/status.ts +81 -0
- package/src/mcp/tools/vsearch.ts +198 -0
- package/src/pipeline/chunk-lookup.ts +44 -0
- package/src/pipeline/expansion.ts +256 -0
- package/src/pipeline/explain.ts +115 -0
- package/src/pipeline/fusion.ts +185 -0
- package/src/pipeline/hybrid.ts +535 -0
- package/src/pipeline/index.ts +64 -0
- package/src/pipeline/query-language.ts +118 -0
- package/src/pipeline/rerank.ts +223 -0
- package/src/pipeline/search.ts +261 -0
- package/src/pipeline/types.ts +328 -0
- package/src/pipeline/vsearch.ts +348 -0
- package/src/store/index.ts +41 -0
- package/src/store/migrations/001-initial.ts +196 -0
- package/src/store/migrations/index.ts +20 -0
- package/src/store/migrations/runner.ts +187 -0
- package/src/store/sqlite/adapter.ts +1242 -0
- package/src/store/sqlite/index.ts +7 -0
- package/src/store/sqlite/setup.ts +129 -0
- package/src/store/sqlite/types.ts +28 -0
- package/src/store/types.ts +506 -0
- package/src/store/vector/index.ts +13 -0
- package/src/store/vector/sqlite-vec.ts +373 -0
- package/src/store/vector/stats.ts +152 -0
- package/src/store/vector/types.ts +115 -0
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commander program definition.
|
|
3
|
+
* Wires all CLI commands with lazy imports for fast --help.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/program
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import {
|
|
10
|
+
CLI_NAME,
|
|
11
|
+
DOCS_URL,
|
|
12
|
+
ISSUES_URL,
|
|
13
|
+
PRODUCT_NAME,
|
|
14
|
+
VERSION,
|
|
15
|
+
} from '../app/constants';
|
|
16
|
+
import { setColorsEnabled } from './colors';
|
|
17
|
+
import {
|
|
18
|
+
applyGlobalOptions,
|
|
19
|
+
type GlobalOptions,
|
|
20
|
+
parseGlobalOptions,
|
|
21
|
+
} from './context';
|
|
22
|
+
import { CliError } from './errors';
|
|
23
|
+
import {
|
|
24
|
+
assertFormatSupported,
|
|
25
|
+
CMD,
|
|
26
|
+
getDefaultLimit,
|
|
27
|
+
parseOptionalFloat,
|
|
28
|
+
parsePositiveInt,
|
|
29
|
+
} from './options';
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Global State (set by preAction hook)
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
// Using object wrapper to allow mutation while satisfying linter
|
|
36
|
+
const globalState: { current: GlobalOptions | null } = { current: null };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get resolved global options. Must be called after command parsing.
|
|
40
|
+
* Throws if called before preAction hook runs.
|
|
41
|
+
*/
|
|
42
|
+
export function getGlobals(): GlobalOptions {
|
|
43
|
+
if (!globalState.current) {
|
|
44
|
+
throw new Error('Global options not resolved - called before preAction?');
|
|
45
|
+
}
|
|
46
|
+
return globalState.current;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reset global state (for testing).
|
|
51
|
+
* Resets both option state and color state to avoid test pollution.
|
|
52
|
+
*/
|
|
53
|
+
export function resetGlobals(): void {
|
|
54
|
+
globalState.current = null;
|
|
55
|
+
// Reset colors to default (true) - will be set by applyGlobalOptions on next run
|
|
56
|
+
setColorsEnabled(true);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Select output format with explicit precedence.
|
|
61
|
+
* Precedence: local non-json format > local --json > global --json > terminal
|
|
62
|
+
*/
|
|
63
|
+
function getFormat(
|
|
64
|
+
cmdOpts: Record<string, unknown>
|
|
65
|
+
): 'terminal' | 'json' | 'files' | 'csv' | 'md' | 'xml' {
|
|
66
|
+
const globals = getGlobals();
|
|
67
|
+
|
|
68
|
+
const local = {
|
|
69
|
+
json: Boolean(cmdOpts.json),
|
|
70
|
+
files: Boolean(cmdOpts.files),
|
|
71
|
+
csv: Boolean(cmdOpts.csv),
|
|
72
|
+
md: Boolean(cmdOpts.md),
|
|
73
|
+
xml: Boolean(cmdOpts.xml),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Count local format flags
|
|
77
|
+
const localFormats = Object.entries(local).filter(([_, v]) => v);
|
|
78
|
+
if (localFormats.length > 1) {
|
|
79
|
+
throw new CliError(
|
|
80
|
+
'VALIDATION',
|
|
81
|
+
`Conflicting output formats: ${localFormats.map(([k]) => k).join(', ')}. Choose one.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Local non-json format wins (--md, --csv, --files, --xml)
|
|
86
|
+
if (local.files) {
|
|
87
|
+
return 'files';
|
|
88
|
+
}
|
|
89
|
+
if (local.csv) {
|
|
90
|
+
return 'csv';
|
|
91
|
+
}
|
|
92
|
+
if (local.md) {
|
|
93
|
+
return 'md';
|
|
94
|
+
}
|
|
95
|
+
if (local.xml) {
|
|
96
|
+
return 'xml';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Local --json wins over global
|
|
100
|
+
if (local.json) {
|
|
101
|
+
return 'json';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Global --json as fallback
|
|
105
|
+
if (globals.json) {
|
|
106
|
+
return 'json';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return 'terminal';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
// Program Factory
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export function createProgram(): Command {
|
|
117
|
+
const program = new Command();
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.name(CLI_NAME)
|
|
121
|
+
.description(`${PRODUCT_NAME} - Local Knowledge Index and Retrieval`)
|
|
122
|
+
.version(VERSION, '-V, --version', 'show version')
|
|
123
|
+
.exitOverride() // Prevent Commander from calling process.exit()
|
|
124
|
+
.showSuggestionAfterError(true)
|
|
125
|
+
.showHelpAfterError('(Use --help for available options)');
|
|
126
|
+
|
|
127
|
+
// Global flags - resolved via preAction hook
|
|
128
|
+
program
|
|
129
|
+
.option('--index <name>', 'index name', 'default')
|
|
130
|
+
.option('--config <path>', 'config file path')
|
|
131
|
+
.option('--no-color', 'disable colors')
|
|
132
|
+
.option('--verbose', 'verbose logging')
|
|
133
|
+
.option('--yes', 'non-interactive mode')
|
|
134
|
+
.option('-q, --quiet', 'suppress non-essential output')
|
|
135
|
+
.option('--json', 'JSON output (for errors and supported commands)');
|
|
136
|
+
|
|
137
|
+
// Resolve globals ONCE before any command runs (ensures consistency)
|
|
138
|
+
program.hook('preAction', (thisCommand) => {
|
|
139
|
+
const rootOpts = thisCommand.optsWithGlobals();
|
|
140
|
+
const globals = parseGlobalOptions(rootOpts);
|
|
141
|
+
applyGlobalOptions(globals);
|
|
142
|
+
globalState.current = globals;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Wire command groups
|
|
146
|
+
wireSearchCommands(program);
|
|
147
|
+
wireOnboardingCommands(program);
|
|
148
|
+
wireManagementCommands(program);
|
|
149
|
+
wireRetrievalCommands(program);
|
|
150
|
+
wireMcpCommand(program);
|
|
151
|
+
wireSkillCommands(program);
|
|
152
|
+
|
|
153
|
+
// Add docs/support links to help footer
|
|
154
|
+
program.addHelpText(
|
|
155
|
+
'after',
|
|
156
|
+
`
|
|
157
|
+
Documentation: ${DOCS_URL}
|
|
158
|
+
Report issues: ${ISSUES_URL}`
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return program;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// Search Commands (search, vsearch, query, ask)
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function wireSearchCommands(program: Command): void {
|
|
169
|
+
// search - BM25 keyword search
|
|
170
|
+
program
|
|
171
|
+
.command('search <query>')
|
|
172
|
+
.description('BM25 keyword search')
|
|
173
|
+
.option('-n, --limit <num>', 'max results')
|
|
174
|
+
.option('--min-score <num>', 'minimum score threshold')
|
|
175
|
+
.option('-c, --collection <name>', 'filter by collection')
|
|
176
|
+
.option('--lang <code>', 'language filter/hint (BCP-47)')
|
|
177
|
+
.option('--full', 'include full content')
|
|
178
|
+
.option('--line-numbers', 'include line numbers in output')
|
|
179
|
+
.option('--json', 'JSON output')
|
|
180
|
+
.option('--md', 'Markdown output')
|
|
181
|
+
.option('--csv', 'CSV output')
|
|
182
|
+
.option('--xml', 'XML output')
|
|
183
|
+
.option('--files', 'file paths only')
|
|
184
|
+
.action(async (queryText: string, cmdOpts: Record<string, unknown>) => {
|
|
185
|
+
const format = getFormat(cmdOpts);
|
|
186
|
+
assertFormatSupported(CMD.search, format);
|
|
187
|
+
|
|
188
|
+
// Validate empty query
|
|
189
|
+
if (!queryText.trim()) {
|
|
190
|
+
throw new CliError('VALIDATION', 'Query cannot be empty');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Validate minScore range
|
|
194
|
+
const minScore = parseOptionalFloat('min-score', cmdOpts.minScore);
|
|
195
|
+
if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
|
|
196
|
+
throw new CliError('VALIDATION', '--min-score must be between 0 and 1');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const limit = cmdOpts.limit
|
|
200
|
+
? parsePositiveInt('limit', cmdOpts.limit)
|
|
201
|
+
: getDefaultLimit(format);
|
|
202
|
+
|
|
203
|
+
const { search, formatSearch } = await import('./commands/search');
|
|
204
|
+
const result = await search(queryText, {
|
|
205
|
+
limit,
|
|
206
|
+
minScore,
|
|
207
|
+
collection: cmdOpts.collection as string | undefined,
|
|
208
|
+
lang: cmdOpts.lang as string | undefined,
|
|
209
|
+
full: Boolean(cmdOpts.full),
|
|
210
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
211
|
+
json: format === 'json',
|
|
212
|
+
md: format === 'md',
|
|
213
|
+
csv: format === 'csv',
|
|
214
|
+
xml: format === 'xml',
|
|
215
|
+
files: format === 'files',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Check success before printing - stdout is for successful outputs only
|
|
219
|
+
if (!result.success) {
|
|
220
|
+
// Map validation errors to exit code 1
|
|
221
|
+
throw new CliError(
|
|
222
|
+
result.isValidation ? 'VALIDATION' : 'RUNTIME',
|
|
223
|
+
result.error
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
process.stdout.write(
|
|
227
|
+
`${formatSearch(result, {
|
|
228
|
+
json: format === 'json',
|
|
229
|
+
md: format === 'md',
|
|
230
|
+
csv: format === 'csv',
|
|
231
|
+
xml: format === 'xml',
|
|
232
|
+
files: format === 'files',
|
|
233
|
+
full: Boolean(cmdOpts.full),
|
|
234
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
235
|
+
})}\n`
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// vsearch - Vector similarity search
|
|
240
|
+
program
|
|
241
|
+
.command('vsearch <query>')
|
|
242
|
+
.description('Vector similarity search')
|
|
243
|
+
.option('-n, --limit <num>', 'max results')
|
|
244
|
+
.option('--min-score <num>', 'minimum score threshold')
|
|
245
|
+
.option('-c, --collection <name>', 'filter by collection')
|
|
246
|
+
.option('--lang <code>', 'language filter/hint (BCP-47)')
|
|
247
|
+
.option('--full', 'include full content')
|
|
248
|
+
.option('--line-numbers', 'include line numbers in output')
|
|
249
|
+
.option('--json', 'JSON output')
|
|
250
|
+
.option('--md', 'Markdown output')
|
|
251
|
+
.option('--csv', 'CSV output')
|
|
252
|
+
.option('--xml', 'XML output')
|
|
253
|
+
.option('--files', 'file paths only')
|
|
254
|
+
.action(async (queryText: string, cmdOpts: Record<string, unknown>) => {
|
|
255
|
+
const format = getFormat(cmdOpts);
|
|
256
|
+
assertFormatSupported(CMD.vsearch, format);
|
|
257
|
+
|
|
258
|
+
// Validate empty query
|
|
259
|
+
if (!queryText.trim()) {
|
|
260
|
+
throw new CliError('VALIDATION', 'Query cannot be empty');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Validate minScore range
|
|
264
|
+
const minScore = parseOptionalFloat('min-score', cmdOpts.minScore);
|
|
265
|
+
if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
|
|
266
|
+
throw new CliError('VALIDATION', '--min-score must be between 0 and 1');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const limit = cmdOpts.limit
|
|
270
|
+
? parsePositiveInt('limit', cmdOpts.limit)
|
|
271
|
+
: getDefaultLimit(format);
|
|
272
|
+
|
|
273
|
+
const { vsearch, formatVsearch } = await import('./commands/vsearch');
|
|
274
|
+
const result = await vsearch(queryText, {
|
|
275
|
+
limit,
|
|
276
|
+
minScore,
|
|
277
|
+
collection: cmdOpts.collection as string | undefined,
|
|
278
|
+
lang: cmdOpts.lang as string | undefined,
|
|
279
|
+
full: Boolean(cmdOpts.full),
|
|
280
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
281
|
+
json: format === 'json',
|
|
282
|
+
md: format === 'md',
|
|
283
|
+
csv: format === 'csv',
|
|
284
|
+
xml: format === 'xml',
|
|
285
|
+
files: format === 'files',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!result.success) {
|
|
289
|
+
throw new CliError('RUNTIME', result.error);
|
|
290
|
+
}
|
|
291
|
+
process.stdout.write(
|
|
292
|
+
`${formatVsearch(result, {
|
|
293
|
+
json: format === 'json',
|
|
294
|
+
md: format === 'md',
|
|
295
|
+
csv: format === 'csv',
|
|
296
|
+
xml: format === 'xml',
|
|
297
|
+
files: format === 'files',
|
|
298
|
+
full: Boolean(cmdOpts.full),
|
|
299
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
300
|
+
})}\n`
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// query - Hybrid search with expansion and reranking
|
|
305
|
+
program
|
|
306
|
+
.command('query <query>')
|
|
307
|
+
.description('Hybrid search with expansion and reranking')
|
|
308
|
+
.option('-n, --limit <num>', 'max results')
|
|
309
|
+
.option('--min-score <num>', 'minimum score threshold')
|
|
310
|
+
.option('-c, --collection <name>', 'filter by collection')
|
|
311
|
+
.option('--lang <code>', 'language hint (BCP-47)')
|
|
312
|
+
.option('--full', 'include full content')
|
|
313
|
+
.option('--line-numbers', 'include line numbers in output')
|
|
314
|
+
.option('--no-expand', 'disable query expansion')
|
|
315
|
+
.option('--no-rerank', 'disable reranking')
|
|
316
|
+
.option('--explain', 'include scoring explanation')
|
|
317
|
+
.option('--json', 'JSON output')
|
|
318
|
+
.option('--md', 'Markdown output')
|
|
319
|
+
.option('--csv', 'CSV output')
|
|
320
|
+
.option('--xml', 'XML output')
|
|
321
|
+
.option('--files', 'file paths only')
|
|
322
|
+
.action(async (queryText: string, cmdOpts: Record<string, unknown>) => {
|
|
323
|
+
const format = getFormat(cmdOpts);
|
|
324
|
+
assertFormatSupported(CMD.query, format);
|
|
325
|
+
|
|
326
|
+
// Validate empty query
|
|
327
|
+
if (!queryText.trim()) {
|
|
328
|
+
throw new CliError('VALIDATION', 'Query cannot be empty');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Validate minScore range
|
|
332
|
+
const minScore = parseOptionalFloat('min-score', cmdOpts.minScore);
|
|
333
|
+
if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
|
|
334
|
+
throw new CliError('VALIDATION', '--min-score must be between 0 and 1');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const limit = cmdOpts.limit
|
|
338
|
+
? parsePositiveInt('limit', cmdOpts.limit)
|
|
339
|
+
: getDefaultLimit(format);
|
|
340
|
+
|
|
341
|
+
const { query, formatQuery } = await import('./commands/query');
|
|
342
|
+
const result = await query(queryText, {
|
|
343
|
+
limit,
|
|
344
|
+
minScore,
|
|
345
|
+
collection: cmdOpts.collection as string | undefined,
|
|
346
|
+
lang: cmdOpts.lang as string | undefined,
|
|
347
|
+
full: Boolean(cmdOpts.full),
|
|
348
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
349
|
+
noExpand: cmdOpts.expand === false,
|
|
350
|
+
noRerank: cmdOpts.rerank === false,
|
|
351
|
+
explain: Boolean(cmdOpts.explain),
|
|
352
|
+
json: format === 'json',
|
|
353
|
+
md: format === 'md',
|
|
354
|
+
csv: format === 'csv',
|
|
355
|
+
xml: format === 'xml',
|
|
356
|
+
files: format === 'files',
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (!result.success) {
|
|
360
|
+
throw new CliError('RUNTIME', result.error);
|
|
361
|
+
}
|
|
362
|
+
process.stdout.write(
|
|
363
|
+
`${formatQuery(result, {
|
|
364
|
+
format,
|
|
365
|
+
full: Boolean(cmdOpts.full),
|
|
366
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
367
|
+
})}\n`
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ask - Human-friendly query with grounded answer
|
|
372
|
+
program
|
|
373
|
+
.command('ask <query>')
|
|
374
|
+
.description('Human-friendly query with grounded answer')
|
|
375
|
+
.option('-n, --limit <num>', 'max source results')
|
|
376
|
+
.option('-c, --collection <name>', 'filter by collection')
|
|
377
|
+
.option('--lang <code>', 'language hint (BCP-47)')
|
|
378
|
+
.option('--answer', 'generate short grounded answer')
|
|
379
|
+
.option('--no-answer', 'force retrieval-only output')
|
|
380
|
+
.option('--max-answer-tokens <num>', 'max answer tokens')
|
|
381
|
+
.option('--show-sources', 'show all retrieved sources (not just cited)')
|
|
382
|
+
.option('--json', 'JSON output')
|
|
383
|
+
.option('--md', 'Markdown output')
|
|
384
|
+
.action(async (queryText: string, cmdOpts: Record<string, unknown>) => {
|
|
385
|
+
const format = getFormat(cmdOpts);
|
|
386
|
+
assertFormatSupported(CMD.ask, format);
|
|
387
|
+
|
|
388
|
+
// Validate empty query
|
|
389
|
+
if (!queryText.trim()) {
|
|
390
|
+
throw new CliError('VALIDATION', 'Query cannot be empty');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const limit = cmdOpts.limit
|
|
394
|
+
? parsePositiveInt('limit', cmdOpts.limit)
|
|
395
|
+
: getDefaultLimit(format);
|
|
396
|
+
|
|
397
|
+
// Parse max-answer-tokens (optional, defaults to 512 in command impl)
|
|
398
|
+
const maxAnswerTokens = cmdOpts.maxAnswerTokens
|
|
399
|
+
? parsePositiveInt('max-answer-tokens', cmdOpts.maxAnswerTokens)
|
|
400
|
+
: undefined;
|
|
401
|
+
|
|
402
|
+
const { ask, formatAsk } = await import('./commands/ask');
|
|
403
|
+
const showSources = Boolean(cmdOpts.showSources);
|
|
404
|
+
const result = await ask(queryText, {
|
|
405
|
+
limit,
|
|
406
|
+
collection: cmdOpts.collection as string | undefined,
|
|
407
|
+
lang: cmdOpts.lang as string | undefined,
|
|
408
|
+
// Per spec: --answer defaults to false, --no-answer forces retrieval-only
|
|
409
|
+
// Commander creates separate cmdOpts.noAnswer for --no-answer flag
|
|
410
|
+
answer: Boolean(cmdOpts.answer),
|
|
411
|
+
noAnswer: Boolean(cmdOpts.noAnswer),
|
|
412
|
+
maxAnswerTokens,
|
|
413
|
+
showSources,
|
|
414
|
+
json: format === 'json',
|
|
415
|
+
md: format === 'md',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (!result.success) {
|
|
419
|
+
throw new CliError('RUNTIME', result.error);
|
|
420
|
+
}
|
|
421
|
+
process.stdout.write(
|
|
422
|
+
`${formatAsk(result, { json: format === 'json', md: format === 'md', showSources })}\n`
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
428
|
+
// Onboarding Commands (init, index, status, doctor)
|
|
429
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
function wireOnboardingCommands(program: Command): void {
|
|
432
|
+
// init - Initialize GNO
|
|
433
|
+
program
|
|
434
|
+
.command('init [path]')
|
|
435
|
+
.description('Initialize GNO configuration')
|
|
436
|
+
.option('-n, --name <name>', 'collection name')
|
|
437
|
+
.option('--pattern <glob>', 'file matching pattern')
|
|
438
|
+
.option('--include <exts>', 'extension allowlist (CSV)')
|
|
439
|
+
.option('--exclude <patterns>', 'exclude patterns (CSV)')
|
|
440
|
+
.option('--update <cmd>', 'shell command to run before indexing')
|
|
441
|
+
.option('--tokenizer <type>', 'FTS tokenizer (unicode61, porter, trigram)')
|
|
442
|
+
.option('--language <code>', 'language hint (BCP-47)')
|
|
443
|
+
.action(
|
|
444
|
+
async (path: string | undefined, cmdOpts: Record<string, unknown>) => {
|
|
445
|
+
const globals = getGlobals();
|
|
446
|
+
const { init } = await import('./commands/init');
|
|
447
|
+
const result = await init({
|
|
448
|
+
path,
|
|
449
|
+
name: cmdOpts.name as string | undefined,
|
|
450
|
+
pattern: cmdOpts.pattern as string | undefined,
|
|
451
|
+
include: cmdOpts.include as string | undefined,
|
|
452
|
+
exclude: cmdOpts.exclude as string | undefined,
|
|
453
|
+
update: cmdOpts.update as string | undefined,
|
|
454
|
+
tokenizer: cmdOpts.tokenizer as
|
|
455
|
+
| 'unicode61'
|
|
456
|
+
| 'porter'
|
|
457
|
+
| 'trigram'
|
|
458
|
+
| undefined,
|
|
459
|
+
language: cmdOpts.language as string | undefined,
|
|
460
|
+
yes: globals.yes,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
if (!result.success) {
|
|
464
|
+
throw new CliError('RUNTIME', result.error ?? 'Init failed');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (result.alreadyInitialized) {
|
|
468
|
+
process.stdout.write('GNO already initialized.\n');
|
|
469
|
+
if (result.collectionAdded) {
|
|
470
|
+
process.stdout.write(
|
|
471
|
+
`Collection "${result.collectionAdded}" added.\n`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
process.stdout.write('GNO initialized successfully.\n');
|
|
476
|
+
process.stdout.write(`Config: ${result.configPath}\n`);
|
|
477
|
+
process.stdout.write(`Database: ${result.dbPath}\n`);
|
|
478
|
+
if (result.collectionAdded) {
|
|
479
|
+
process.stdout.write(
|
|
480
|
+
`Collection "${result.collectionAdded}" added.\n`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// index - Index collections
|
|
488
|
+
program
|
|
489
|
+
.command('index [collection]')
|
|
490
|
+
.description('Index files from collections')
|
|
491
|
+
.option('--no-embed', 'skip embedding after sync')
|
|
492
|
+
.option('--git-pull', 'run git pull in git repositories')
|
|
493
|
+
.option('--models-pull', 'download models if missing')
|
|
494
|
+
.action(
|
|
495
|
+
async (
|
|
496
|
+
collection: string | undefined,
|
|
497
|
+
cmdOpts: Record<string, unknown>
|
|
498
|
+
) => {
|
|
499
|
+
const globals = getGlobals();
|
|
500
|
+
const { index, formatIndex } = await import('./commands/index-cmd');
|
|
501
|
+
const opts = {
|
|
502
|
+
collection,
|
|
503
|
+
noEmbed: cmdOpts.embed === false,
|
|
504
|
+
gitPull: Boolean(cmdOpts.gitPull),
|
|
505
|
+
modelsPull: Boolean(cmdOpts.modelsPull),
|
|
506
|
+
yes: globals.yes,
|
|
507
|
+
verbose: globals.verbose,
|
|
508
|
+
};
|
|
509
|
+
const result = await index(opts);
|
|
510
|
+
|
|
511
|
+
if (!result.success) {
|
|
512
|
+
throw new CliError('RUNTIME', result.error ?? 'Index failed');
|
|
513
|
+
}
|
|
514
|
+
process.stdout.write(`${formatIndex(result, opts)}\n`);
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// status - Show index status
|
|
519
|
+
program
|
|
520
|
+
.command('status')
|
|
521
|
+
.description('Show index status')
|
|
522
|
+
.option('--json', 'JSON output')
|
|
523
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
524
|
+
const format = getFormat(cmdOpts);
|
|
525
|
+
assertFormatSupported(CMD.status, format);
|
|
526
|
+
|
|
527
|
+
const { status, formatStatus } = await import('./commands/status');
|
|
528
|
+
const result = await status({ json: format === 'json' });
|
|
529
|
+
|
|
530
|
+
if (!result.success) {
|
|
531
|
+
throw new CliError('RUNTIME', result.error ?? 'Status failed');
|
|
532
|
+
}
|
|
533
|
+
process.stdout.write(
|
|
534
|
+
`${formatStatus(result, { json: format === 'json' })}\n`
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// doctor - Diagnose configuration issues
|
|
539
|
+
program
|
|
540
|
+
.command('doctor')
|
|
541
|
+
.description('Diagnose configuration issues')
|
|
542
|
+
.option('--json', 'JSON output')
|
|
543
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
544
|
+
const format = getFormat(cmdOpts);
|
|
545
|
+
const { doctor, formatDoctor } = await import('./commands/doctor');
|
|
546
|
+
const result = await doctor({ json: format === 'json' });
|
|
547
|
+
|
|
548
|
+
// Doctor always succeeds but may report issues
|
|
549
|
+
process.stdout.write(
|
|
550
|
+
`${formatDoctor(result, { json: format === 'json' })}\n`
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
556
|
+
// Retrieval Commands (get, multi-get, ls)
|
|
557
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
function wireRetrievalCommands(program: Command): void {
|
|
560
|
+
// get - Retrieve document by URI or docid
|
|
561
|
+
program
|
|
562
|
+
.command('get <ref>')
|
|
563
|
+
.description('Get document by URI or docid')
|
|
564
|
+
.option(
|
|
565
|
+
'--from <line>',
|
|
566
|
+
'Start at line number',
|
|
567
|
+
parsePositiveInt.bind(null, 'from')
|
|
568
|
+
)
|
|
569
|
+
.option(
|
|
570
|
+
'-l, --limit <lines>',
|
|
571
|
+
'Limit to N lines',
|
|
572
|
+
parsePositiveInt.bind(null, 'limit')
|
|
573
|
+
)
|
|
574
|
+
.option('--line-numbers', 'Prefix lines with numbers')
|
|
575
|
+
.option('--source', 'Include source metadata')
|
|
576
|
+
.option('--json', 'JSON output')
|
|
577
|
+
.option('--md', 'Markdown output')
|
|
578
|
+
.action(async (ref: string, cmdOpts: Record<string, unknown>) => {
|
|
579
|
+
const format = getFormat(cmdOpts);
|
|
580
|
+
assertFormatSupported(CMD.get, format);
|
|
581
|
+
const globals = getGlobals();
|
|
582
|
+
|
|
583
|
+
const { get, formatGet } = await import('./commands/get');
|
|
584
|
+
const result = await get(ref, {
|
|
585
|
+
configPath: globals.config,
|
|
586
|
+
from: cmdOpts.from as number | undefined,
|
|
587
|
+
limit: cmdOpts.limit as number | undefined,
|
|
588
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
589
|
+
source: Boolean(cmdOpts.source),
|
|
590
|
+
json: format === 'json',
|
|
591
|
+
md: format === 'md',
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
if (!result.success) {
|
|
595
|
+
throw new CliError(
|
|
596
|
+
result.isValidation ? 'VALIDATION' : 'RUNTIME',
|
|
597
|
+
result.error
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
process.stdout.write(
|
|
602
|
+
`${formatGet(result, {
|
|
603
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
604
|
+
json: format === 'json',
|
|
605
|
+
md: format === 'md',
|
|
606
|
+
})}\n`
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// multi-get - Retrieve multiple documents
|
|
611
|
+
program
|
|
612
|
+
.command('multi-get <refs...>')
|
|
613
|
+
.description('Get multiple documents by URI or docid')
|
|
614
|
+
.option(
|
|
615
|
+
'--max-bytes <n>',
|
|
616
|
+
'Max bytes per document',
|
|
617
|
+
parsePositiveInt.bind(null, 'max-bytes')
|
|
618
|
+
)
|
|
619
|
+
.option('--line-numbers', 'Include line numbers')
|
|
620
|
+
.option('--json', 'JSON output')
|
|
621
|
+
.option('--files', 'File protocol output')
|
|
622
|
+
.option('--md', 'Markdown output')
|
|
623
|
+
.action(async (refs: string[], cmdOpts: Record<string, unknown>) => {
|
|
624
|
+
const format = getFormat(cmdOpts);
|
|
625
|
+
assertFormatSupported(CMD.multiGet, format);
|
|
626
|
+
const globals = getGlobals();
|
|
627
|
+
|
|
628
|
+
const { multiGet, formatMultiGet } = await import('./commands/multi-get');
|
|
629
|
+
const result = await multiGet(refs, {
|
|
630
|
+
configPath: globals.config,
|
|
631
|
+
maxBytes: cmdOpts.maxBytes as number | undefined,
|
|
632
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
633
|
+
json: format === 'json',
|
|
634
|
+
files: format === 'files',
|
|
635
|
+
md: format === 'md',
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (!result.success) {
|
|
639
|
+
throw new CliError(
|
|
640
|
+
result.isValidation ? 'VALIDATION' : 'RUNTIME',
|
|
641
|
+
result.error
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
process.stdout.write(
|
|
646
|
+
`${formatMultiGet(result, {
|
|
647
|
+
lineNumbers: Boolean(cmdOpts.lineNumbers),
|
|
648
|
+
json: format === 'json',
|
|
649
|
+
files: format === 'files',
|
|
650
|
+
md: format === 'md',
|
|
651
|
+
})}\n`
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// ls - List indexed documents
|
|
656
|
+
program
|
|
657
|
+
.command('ls [scope]')
|
|
658
|
+
.description('List indexed documents')
|
|
659
|
+
.option(
|
|
660
|
+
'-n, --limit <num>',
|
|
661
|
+
'Max results',
|
|
662
|
+
parsePositiveInt.bind(null, 'limit')
|
|
663
|
+
)
|
|
664
|
+
.option(
|
|
665
|
+
'--offset <num>',
|
|
666
|
+
'Skip first N results',
|
|
667
|
+
parsePositiveInt.bind(null, 'offset')
|
|
668
|
+
)
|
|
669
|
+
.option('--json', 'JSON output')
|
|
670
|
+
.option('--files', 'File protocol output')
|
|
671
|
+
.option('--md', 'Markdown output')
|
|
672
|
+
.action(
|
|
673
|
+
async (scope: string | undefined, cmdOpts: Record<string, unknown>) => {
|
|
674
|
+
const format = getFormat(cmdOpts);
|
|
675
|
+
assertFormatSupported(CMD.ls, format);
|
|
676
|
+
const globals = getGlobals();
|
|
677
|
+
|
|
678
|
+
const { ls, formatLs } = await import('./commands/ls');
|
|
679
|
+
const result = await ls(scope, {
|
|
680
|
+
configPath: globals.config,
|
|
681
|
+
limit: cmdOpts.limit as number | undefined,
|
|
682
|
+
offset: cmdOpts.offset as number | undefined,
|
|
683
|
+
json: format === 'json',
|
|
684
|
+
files: format === 'files',
|
|
685
|
+
md: format === 'md',
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
if (!result.success) {
|
|
689
|
+
throw new CliError(
|
|
690
|
+
result.isValidation ? 'VALIDATION' : 'RUNTIME',
|
|
691
|
+
result.error
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
process.stdout.write(
|
|
696
|
+
`${formatLs(result, {
|
|
697
|
+
json: format === 'json',
|
|
698
|
+
files: format === 'files',
|
|
699
|
+
md: format === 'md',
|
|
700
|
+
})}\n`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
707
|
+
// MCP Commands
|
|
708
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
709
|
+
|
|
710
|
+
function wireMcpCommand(program: Command): void {
|
|
711
|
+
// mcp - Start MCP server (stdio transport) or manage MCP configuration
|
|
712
|
+
// CRITICAL: helpOption(false) on server command prevents --help from writing
|
|
713
|
+
// to stdout which would corrupt the JSON-RPC stream
|
|
714
|
+
const mcpCmd = program
|
|
715
|
+
.command('mcp')
|
|
716
|
+
.description('MCP server and configuration');
|
|
717
|
+
|
|
718
|
+
// Default action: start MCP server
|
|
719
|
+
mcpCmd
|
|
720
|
+
.command('serve', { isDefault: true })
|
|
721
|
+
.description('Start MCP server (stdio transport)')
|
|
722
|
+
.helpOption(false)
|
|
723
|
+
.action(async () => {
|
|
724
|
+
const { mcpCommand } = await import('./commands/mcp.js');
|
|
725
|
+
const globalOpts = program.opts();
|
|
726
|
+
const globals = parseGlobalOptions(globalOpts);
|
|
727
|
+
await mcpCommand(globals);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// install - Install gno MCP server to client configs
|
|
731
|
+
mcpCmd
|
|
732
|
+
.command('install')
|
|
733
|
+
.description('Install gno as MCP server in client configuration')
|
|
734
|
+
.option(
|
|
735
|
+
'-t, --target <target>',
|
|
736
|
+
'target client (claude-desktop, cursor, zed, windsurf, opencode, amp, lmstudio, librechat, claude-code, codex)',
|
|
737
|
+
'claude-desktop'
|
|
738
|
+
)
|
|
739
|
+
.option(
|
|
740
|
+
'-s, --scope <scope>',
|
|
741
|
+
'scope (user, project) - project only for claude-code/codex/cursor/opencode',
|
|
742
|
+
'user'
|
|
743
|
+
)
|
|
744
|
+
.option('-f, --force', 'overwrite existing configuration')
|
|
745
|
+
.option('--dry-run', 'show what would be done without making changes')
|
|
746
|
+
.option('--json', 'JSON output')
|
|
747
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
748
|
+
const target = cmdOpts.target as string;
|
|
749
|
+
const scope = cmdOpts.scope as string;
|
|
750
|
+
|
|
751
|
+
// Import MCP_TARGETS for validation
|
|
752
|
+
const { MCP_TARGETS } = await import('./commands/mcp/paths.js');
|
|
753
|
+
|
|
754
|
+
// Validate target
|
|
755
|
+
if (!(MCP_TARGETS as string[]).includes(target)) {
|
|
756
|
+
throw new CliError(
|
|
757
|
+
'VALIDATION',
|
|
758
|
+
`Invalid target: ${target}. Must be one of: ${MCP_TARGETS.join(', ')}.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
// Validate scope
|
|
762
|
+
if (!['user', 'project'].includes(scope)) {
|
|
763
|
+
throw new CliError(
|
|
764
|
+
'VALIDATION',
|
|
765
|
+
`Invalid scope: ${scope}. Must be 'user' or 'project'.`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const { installMcp } = await import('./commands/mcp/install.js');
|
|
770
|
+
await installMcp({
|
|
771
|
+
target: target as NonNullable<
|
|
772
|
+
Parameters<typeof installMcp>[0]
|
|
773
|
+
>['target'],
|
|
774
|
+
scope: scope as 'user' | 'project',
|
|
775
|
+
force: Boolean(cmdOpts.force),
|
|
776
|
+
dryRun: Boolean(cmdOpts.dryRun),
|
|
777
|
+
// Pass undefined if not set, so global --json can take effect
|
|
778
|
+
json: cmdOpts.json === true ? true : undefined,
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// uninstall - Remove gno MCP server from client configs
|
|
783
|
+
mcpCmd
|
|
784
|
+
.command('uninstall')
|
|
785
|
+
.description('Remove gno MCP server from client configuration')
|
|
786
|
+
.option(
|
|
787
|
+
'-t, --target <target>',
|
|
788
|
+
'target client (claude-desktop, cursor, zed, windsurf, opencode, amp, lmstudio, librechat, claude-code, codex)',
|
|
789
|
+
'claude-desktop'
|
|
790
|
+
)
|
|
791
|
+
.option('-s, --scope <scope>', 'scope (user, project)', 'user')
|
|
792
|
+
.option('--json', 'JSON output')
|
|
793
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
794
|
+
const target = cmdOpts.target as string;
|
|
795
|
+
const scope = cmdOpts.scope as string;
|
|
796
|
+
|
|
797
|
+
// Import MCP_TARGETS for validation
|
|
798
|
+
const { MCP_TARGETS } = await import('./commands/mcp/paths.js');
|
|
799
|
+
|
|
800
|
+
// Validate target
|
|
801
|
+
if (!(MCP_TARGETS as string[]).includes(target)) {
|
|
802
|
+
throw new CliError(
|
|
803
|
+
'VALIDATION',
|
|
804
|
+
`Invalid target: ${target}. Must be one of: ${MCP_TARGETS.join(', ')}.`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
// Validate scope
|
|
808
|
+
if (!['user', 'project'].includes(scope)) {
|
|
809
|
+
throw new CliError(
|
|
810
|
+
'VALIDATION',
|
|
811
|
+
`Invalid scope: ${scope}. Must be 'user' or 'project'.`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const { uninstallMcp } = await import('./commands/mcp/uninstall.js');
|
|
816
|
+
await uninstallMcp({
|
|
817
|
+
target: target as NonNullable<
|
|
818
|
+
Parameters<typeof uninstallMcp>[0]
|
|
819
|
+
>['target'],
|
|
820
|
+
scope: scope as 'user' | 'project',
|
|
821
|
+
// Pass undefined if not set, so global --json can take effect
|
|
822
|
+
json: cmdOpts.json === true ? true : undefined,
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// status - Show MCP installation status
|
|
827
|
+
mcpCmd
|
|
828
|
+
.command('status')
|
|
829
|
+
.description('Show MCP server installation status')
|
|
830
|
+
.option(
|
|
831
|
+
'-t, --target <target>',
|
|
832
|
+
'filter by target (claude-desktop, cursor, zed, windsurf, opencode, amp, lmstudio, librechat, claude-code, codex, all)',
|
|
833
|
+
'all'
|
|
834
|
+
)
|
|
835
|
+
.option(
|
|
836
|
+
'-s, --scope <scope>',
|
|
837
|
+
'filter by scope (user, project, all)',
|
|
838
|
+
'all'
|
|
839
|
+
)
|
|
840
|
+
.option('--json', 'JSON output')
|
|
841
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
842
|
+
const target = cmdOpts.target as string;
|
|
843
|
+
const scope = cmdOpts.scope as string;
|
|
844
|
+
|
|
845
|
+
// Import MCP_TARGETS for validation
|
|
846
|
+
const { MCP_TARGETS, TARGETS_WITH_PROJECT_SCOPE } = await import(
|
|
847
|
+
'./commands/mcp/paths.js'
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// Validate target
|
|
851
|
+
if (target !== 'all' && !(MCP_TARGETS as string[]).includes(target)) {
|
|
852
|
+
throw new CliError(
|
|
853
|
+
'VALIDATION',
|
|
854
|
+
`Invalid target: ${target}. Must be one of: ${MCP_TARGETS.join(', ')}, all.`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
// Validate scope
|
|
858
|
+
if (!['user', 'project', 'all'].includes(scope)) {
|
|
859
|
+
throw new CliError(
|
|
860
|
+
'VALIDATION',
|
|
861
|
+
`Invalid scope: ${scope}. Must be 'user', 'project', or 'all'.`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
// Validate target/scope combination
|
|
865
|
+
if (
|
|
866
|
+
target !== 'all' &&
|
|
867
|
+
scope === 'project' &&
|
|
868
|
+
!(TARGETS_WITH_PROJECT_SCOPE as string[]).includes(target)
|
|
869
|
+
) {
|
|
870
|
+
throw new CliError(
|
|
871
|
+
'VALIDATION',
|
|
872
|
+
`${target} does not support project scope.`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const { statusMcp } = await import('./commands/mcp/status.js');
|
|
877
|
+
await statusMcp({
|
|
878
|
+
target: target as NonNullable<
|
|
879
|
+
Parameters<typeof statusMcp>[0]
|
|
880
|
+
>['target'],
|
|
881
|
+
scope: scope as 'user' | 'project' | 'all',
|
|
882
|
+
// Pass undefined if not set, so global --json can take effect
|
|
883
|
+
json: cmdOpts.json === true ? true : undefined,
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
889
|
+
// Management Commands (collection, context, models, update, embed, cleanup)
|
|
890
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
891
|
+
|
|
892
|
+
function wireManagementCommands(program: Command): void {
|
|
893
|
+
// collection subcommands
|
|
894
|
+
const collectionCmd = program
|
|
895
|
+
.command('collection')
|
|
896
|
+
.description('Manage collections');
|
|
897
|
+
|
|
898
|
+
collectionCmd
|
|
899
|
+
.command('add <path>')
|
|
900
|
+
.description('Add a collection')
|
|
901
|
+
.requiredOption('-n, --name <name>', 'collection name')
|
|
902
|
+
.option('--pattern <glob>', 'file matching pattern')
|
|
903
|
+
.option('--include <exts>', 'extension allowlist (CSV)')
|
|
904
|
+
.option('--exclude <patterns>', 'exclude patterns (CSV)')
|
|
905
|
+
.option('--update <cmd>', 'shell command to run before indexing')
|
|
906
|
+
.action(async (path: string, cmdOpts: Record<string, unknown>) => {
|
|
907
|
+
const { collectionAdd } = await import('./commands/collection');
|
|
908
|
+
await collectionAdd(path, {
|
|
909
|
+
name: cmdOpts.name as string,
|
|
910
|
+
pattern: cmdOpts.pattern as string | undefined,
|
|
911
|
+
include: cmdOpts.include as string | undefined,
|
|
912
|
+
exclude: cmdOpts.exclude as string | undefined,
|
|
913
|
+
update: cmdOpts.update as string | undefined,
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
collectionCmd
|
|
918
|
+
.command('list')
|
|
919
|
+
.description('List collections')
|
|
920
|
+
.option('--json', 'JSON output')
|
|
921
|
+
.option('--md', 'Markdown output')
|
|
922
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
923
|
+
const format = getFormat(cmdOpts);
|
|
924
|
+
assertFormatSupported(CMD.collectionList, format);
|
|
925
|
+
|
|
926
|
+
const { collectionList } = await import('./commands/collection');
|
|
927
|
+
await collectionList({
|
|
928
|
+
json: format === 'json',
|
|
929
|
+
md: format === 'md',
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
collectionCmd
|
|
934
|
+
.command('remove <name>')
|
|
935
|
+
.description('Remove a collection')
|
|
936
|
+
.action(async (name: string) => {
|
|
937
|
+
const { collectionRemove } = await import('./commands/collection');
|
|
938
|
+
await collectionRemove(name);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
collectionCmd
|
|
942
|
+
.command('rename <old> <new>')
|
|
943
|
+
.description('Rename a collection')
|
|
944
|
+
.action(async (oldName: string, newName: string) => {
|
|
945
|
+
const { collectionRename } = await import('./commands/collection');
|
|
946
|
+
await collectionRename(oldName, newName);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// context subcommands
|
|
950
|
+
const contextCmd = program
|
|
951
|
+
.command('context')
|
|
952
|
+
.description('Manage context items');
|
|
953
|
+
|
|
954
|
+
contextCmd
|
|
955
|
+
.command('add <scope> <text>')
|
|
956
|
+
.description('Add context metadata for a scope')
|
|
957
|
+
.action(async (scope: string, text: string) => {
|
|
958
|
+
const { contextAdd } = await import('./commands/context');
|
|
959
|
+
const exitCode = await contextAdd(scope, text);
|
|
960
|
+
if (exitCode !== 0) {
|
|
961
|
+
throw new CliError('RUNTIME', 'Failed to add context');
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
contextCmd
|
|
966
|
+
.command('list')
|
|
967
|
+
.description('List context items')
|
|
968
|
+
.option('--json', 'JSON output')
|
|
969
|
+
.option('--md', 'Markdown output')
|
|
970
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
971
|
+
const format = getFormat(cmdOpts);
|
|
972
|
+
assertFormatSupported(CMD.contextList, format);
|
|
973
|
+
|
|
974
|
+
const { contextList } = await import('./commands/context');
|
|
975
|
+
await contextList(format as 'terminal' | 'json' | 'md');
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
contextCmd
|
|
979
|
+
.command('check')
|
|
980
|
+
.description('Check context configuration')
|
|
981
|
+
.option('--json', 'JSON output')
|
|
982
|
+
.option('--md', 'Markdown output')
|
|
983
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
984
|
+
const format = getFormat(cmdOpts);
|
|
985
|
+
assertFormatSupported(CMD.contextCheck, format);
|
|
986
|
+
|
|
987
|
+
const { contextCheck } = await import('./commands/context');
|
|
988
|
+
await contextCheck(format as 'terminal' | 'json' | 'md');
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
contextCmd
|
|
992
|
+
.command('rm <uri>')
|
|
993
|
+
.description('Remove context item')
|
|
994
|
+
.action(async (uri: string) => {
|
|
995
|
+
const { contextRm } = await import('./commands/context');
|
|
996
|
+
await contextRm(uri);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// models subcommands
|
|
1000
|
+
const modelsCmd = program.command('models').description('Manage LLM models');
|
|
1001
|
+
|
|
1002
|
+
modelsCmd
|
|
1003
|
+
.command('list')
|
|
1004
|
+
.description('List available models')
|
|
1005
|
+
.option('--json', 'JSON output')
|
|
1006
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1007
|
+
const format = getFormat(cmdOpts);
|
|
1008
|
+
assertFormatSupported(CMD.modelsList, format);
|
|
1009
|
+
|
|
1010
|
+
const { modelsList, formatModelsList } = await import(
|
|
1011
|
+
'./commands/models'
|
|
1012
|
+
);
|
|
1013
|
+
const result = await modelsList({ json: format === 'json' });
|
|
1014
|
+
process.stdout.write(
|
|
1015
|
+
`${formatModelsList(result, { json: format === 'json' })}\n`
|
|
1016
|
+
);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
modelsCmd
|
|
1020
|
+
.command('use')
|
|
1021
|
+
.description('Switch active model preset')
|
|
1022
|
+
.argument('<preset>', 'preset ID (slim, balanced, quality)')
|
|
1023
|
+
.action(async (preset: string) => {
|
|
1024
|
+
const globals = getGlobals();
|
|
1025
|
+
const { modelsUse, formatModelsUse } = await import(
|
|
1026
|
+
'./commands/models/use'
|
|
1027
|
+
);
|
|
1028
|
+
const result = await modelsUse(preset, { configPath: globals.config });
|
|
1029
|
+
if (!result.success) {
|
|
1030
|
+
throw new CliError('VALIDATION', result.error);
|
|
1031
|
+
}
|
|
1032
|
+
process.stdout.write(`${formatModelsUse(result)}\n`);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
modelsCmd
|
|
1036
|
+
.command('pull')
|
|
1037
|
+
.description('Download models')
|
|
1038
|
+
.option('--all', 'download all configured models')
|
|
1039
|
+
.option('--embed', 'download embedding model')
|
|
1040
|
+
.option('--rerank', 'download reranker model')
|
|
1041
|
+
.option('--gen', 'download generation model')
|
|
1042
|
+
.option('--force', 'force re-download')
|
|
1043
|
+
.option('--no-progress', 'disable download progress')
|
|
1044
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1045
|
+
const globals = getGlobals();
|
|
1046
|
+
const { modelsPull, formatModelsPull, createProgressRenderer } =
|
|
1047
|
+
await import('./commands/models');
|
|
1048
|
+
|
|
1049
|
+
// Merge global quiet/json with local --no-progress
|
|
1050
|
+
const showProgress =
|
|
1051
|
+
(process.stderr.isTTY ?? false) &&
|
|
1052
|
+
!globals.quiet &&
|
|
1053
|
+
!globals.json &&
|
|
1054
|
+
cmdOpts.progress !== false;
|
|
1055
|
+
|
|
1056
|
+
const result = await modelsPull({
|
|
1057
|
+
all: Boolean(cmdOpts.all),
|
|
1058
|
+
embed: Boolean(cmdOpts.embed),
|
|
1059
|
+
rerank: Boolean(cmdOpts.rerank),
|
|
1060
|
+
gen: Boolean(cmdOpts.gen),
|
|
1061
|
+
force: Boolean(cmdOpts.force),
|
|
1062
|
+
onProgress: showProgress ? createProgressRenderer() : undefined,
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// For models pull, print result first, then check for failures
|
|
1066
|
+
// This allows partial success output before throwing
|
|
1067
|
+
process.stdout.write(`${formatModelsPull(result)}\n`);
|
|
1068
|
+
if (result.failed > 0) {
|
|
1069
|
+
throw new CliError('RUNTIME', `${result.failed} model(s) failed`);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
modelsCmd
|
|
1074
|
+
.command('clear')
|
|
1075
|
+
.description('Clear model cache')
|
|
1076
|
+
.action(async () => {
|
|
1077
|
+
const globals = getGlobals();
|
|
1078
|
+
const { modelsClear, formatModelsClear } = await import(
|
|
1079
|
+
'./commands/models'
|
|
1080
|
+
);
|
|
1081
|
+
const result = await modelsClear({ yes: globals.yes });
|
|
1082
|
+
process.stdout.write(`${formatModelsClear(result)}\n`);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
modelsCmd
|
|
1086
|
+
.command('path')
|
|
1087
|
+
.description('Show model cache path')
|
|
1088
|
+
.option('--json', 'JSON output')
|
|
1089
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1090
|
+
const format = getFormat(cmdOpts);
|
|
1091
|
+
const { modelsPath, formatModelsPath } = await import(
|
|
1092
|
+
'./commands/models'
|
|
1093
|
+
);
|
|
1094
|
+
const result = modelsPath();
|
|
1095
|
+
process.stdout.write(
|
|
1096
|
+
`${formatModelsPath(result, { json: format === 'json' })}\n`
|
|
1097
|
+
);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// update - Sync files from disk
|
|
1101
|
+
program
|
|
1102
|
+
.command('update')
|
|
1103
|
+
.description('Sync files from disk into the index')
|
|
1104
|
+
.option('--git-pull', 'run git pull in git repositories')
|
|
1105
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1106
|
+
const globals = getGlobals();
|
|
1107
|
+
const { update, formatUpdate } = await import('./commands/update');
|
|
1108
|
+
const opts = {
|
|
1109
|
+
gitPull: Boolean(cmdOpts.gitPull),
|
|
1110
|
+
verbose: globals.verbose,
|
|
1111
|
+
};
|
|
1112
|
+
const result = await update(opts);
|
|
1113
|
+
|
|
1114
|
+
if (!result.success) {
|
|
1115
|
+
throw new CliError('RUNTIME', result.error ?? 'Update failed');
|
|
1116
|
+
}
|
|
1117
|
+
process.stdout.write(`${formatUpdate(result, opts)}\n`);
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// embed - Generate embeddings
|
|
1121
|
+
program
|
|
1122
|
+
.command('embed')
|
|
1123
|
+
.description('Generate embeddings for indexed documents')
|
|
1124
|
+
.option('--model <uri>', 'embedding model URI')
|
|
1125
|
+
.option('--batch-size <num>', 'batch size', '32')
|
|
1126
|
+
.option('--force', 'regenerate all embeddings')
|
|
1127
|
+
.option('--dry-run', 'show what would be done')
|
|
1128
|
+
.option('--json', 'JSON output')
|
|
1129
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1130
|
+
const globals = getGlobals();
|
|
1131
|
+
const format = getFormat(cmdOpts);
|
|
1132
|
+
|
|
1133
|
+
const { embed, formatEmbed } = await import('./commands/embed');
|
|
1134
|
+
const opts = {
|
|
1135
|
+
model: cmdOpts.model as string | undefined,
|
|
1136
|
+
batchSize: parsePositiveInt('batch-size', cmdOpts.batchSize),
|
|
1137
|
+
force: Boolean(cmdOpts.force),
|
|
1138
|
+
dryRun: Boolean(cmdOpts.dryRun),
|
|
1139
|
+
yes: globals.yes,
|
|
1140
|
+
json: format === 'json',
|
|
1141
|
+
};
|
|
1142
|
+
const result = await embed(opts);
|
|
1143
|
+
|
|
1144
|
+
if (!result.success) {
|
|
1145
|
+
throw new CliError('RUNTIME', result.error ?? 'Embed failed');
|
|
1146
|
+
}
|
|
1147
|
+
process.stdout.write(`${formatEmbed(result, opts)}\n`);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// cleanup - Clean stale data
|
|
1151
|
+
program
|
|
1152
|
+
.command('cleanup')
|
|
1153
|
+
.description('Clean orphaned data from index')
|
|
1154
|
+
.action(async () => {
|
|
1155
|
+
const { cleanup, formatCleanup } = await import('./commands/cleanup');
|
|
1156
|
+
const result = await cleanup();
|
|
1157
|
+
|
|
1158
|
+
if (!result.success) {
|
|
1159
|
+
throw new CliError('RUNTIME', result.error ?? 'Cleanup failed');
|
|
1160
|
+
}
|
|
1161
|
+
process.stdout.write(`${formatCleanup(result)}\n`);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
// reset - Reset GNO to fresh state
|
|
1165
|
+
program
|
|
1166
|
+
.command('reset')
|
|
1167
|
+
.description('Delete all GNO data and start fresh')
|
|
1168
|
+
.option('--confirm', 'confirm destructive operation')
|
|
1169
|
+
.option('--keep-config', 'preserve config file')
|
|
1170
|
+
.option('--keep-cache', 'preserve model cache')
|
|
1171
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1172
|
+
const { reset, formatReset } = await import('./commands/reset');
|
|
1173
|
+
const globals = getGlobals();
|
|
1174
|
+
const result = await reset({
|
|
1175
|
+
// Accept either --confirm or global --yes
|
|
1176
|
+
confirm: Boolean(cmdOpts.confirm) || globals.yes,
|
|
1177
|
+
keepConfig: Boolean(cmdOpts.keepConfig),
|
|
1178
|
+
keepCache: Boolean(cmdOpts.keepCache),
|
|
1179
|
+
});
|
|
1180
|
+
process.stdout.write(`${formatReset(result)}\n`);
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1185
|
+
// Skill Commands (install, uninstall, show, paths)
|
|
1186
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1187
|
+
|
|
1188
|
+
function wireSkillCommands(program: Command): void {
|
|
1189
|
+
const skillCmd = program
|
|
1190
|
+
.command('skill')
|
|
1191
|
+
.description('Manage GNO agent skill');
|
|
1192
|
+
|
|
1193
|
+
skillCmd
|
|
1194
|
+
.command('install')
|
|
1195
|
+
.description('Install GNO skill to Claude Code or Codex')
|
|
1196
|
+
.option(
|
|
1197
|
+
'-s, --scope <scope>',
|
|
1198
|
+
'installation scope (project, user)',
|
|
1199
|
+
'project'
|
|
1200
|
+
)
|
|
1201
|
+
.option(
|
|
1202
|
+
'-t, --target <target>',
|
|
1203
|
+
'target agent (claude, codex, all)',
|
|
1204
|
+
'claude'
|
|
1205
|
+
)
|
|
1206
|
+
.option('-f, --force', 'overwrite existing installation')
|
|
1207
|
+
.option('--json', 'JSON output')
|
|
1208
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1209
|
+
const scope = cmdOpts.scope as string;
|
|
1210
|
+
const target = cmdOpts.target as string;
|
|
1211
|
+
|
|
1212
|
+
// Validate scope
|
|
1213
|
+
if (!['project', 'user'].includes(scope)) {
|
|
1214
|
+
throw new CliError(
|
|
1215
|
+
'VALIDATION',
|
|
1216
|
+
`Invalid scope: ${scope}. Must be 'project' or 'user'.`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
// Validate target
|
|
1220
|
+
if (!['claude', 'codex', 'all'].includes(target)) {
|
|
1221
|
+
throw new CliError(
|
|
1222
|
+
'VALIDATION',
|
|
1223
|
+
`Invalid target: ${target}. Must be 'claude', 'codex', or 'all'.`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const { installSkill } = await import('./commands/skill/install.js');
|
|
1228
|
+
await installSkill({
|
|
1229
|
+
scope: scope as 'project' | 'user',
|
|
1230
|
+
target: target as 'claude' | 'codex' | 'all',
|
|
1231
|
+
force: Boolean(cmdOpts.force),
|
|
1232
|
+
json: Boolean(cmdOpts.json),
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
skillCmd
|
|
1237
|
+
.command('uninstall')
|
|
1238
|
+
.description('Uninstall GNO skill')
|
|
1239
|
+
.option(
|
|
1240
|
+
'-s, --scope <scope>',
|
|
1241
|
+
'installation scope (project, user)',
|
|
1242
|
+
'project'
|
|
1243
|
+
)
|
|
1244
|
+
.option(
|
|
1245
|
+
'-t, --target <target>',
|
|
1246
|
+
'target agent (claude, codex, all)',
|
|
1247
|
+
'claude'
|
|
1248
|
+
)
|
|
1249
|
+
.option('--json', 'JSON output')
|
|
1250
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1251
|
+
const scope = cmdOpts.scope as string;
|
|
1252
|
+
const target = cmdOpts.target as string;
|
|
1253
|
+
|
|
1254
|
+
// Validate scope
|
|
1255
|
+
if (!['project', 'user'].includes(scope)) {
|
|
1256
|
+
throw new CliError(
|
|
1257
|
+
'VALIDATION',
|
|
1258
|
+
`Invalid scope: ${scope}. Must be 'project' or 'user'.`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
// Validate target
|
|
1262
|
+
if (!['claude', 'codex', 'all'].includes(target)) {
|
|
1263
|
+
throw new CliError(
|
|
1264
|
+
'VALIDATION',
|
|
1265
|
+
`Invalid target: ${target}. Must be 'claude', 'codex', or 'all'.`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const { uninstallSkill } = await import('./commands/skill/uninstall.js');
|
|
1270
|
+
await uninstallSkill({
|
|
1271
|
+
scope: scope as 'project' | 'user',
|
|
1272
|
+
target: target as 'claude' | 'codex' | 'all',
|
|
1273
|
+
json: Boolean(cmdOpts.json),
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
skillCmd
|
|
1278
|
+
.command('show')
|
|
1279
|
+
.description('Preview skill files without installing')
|
|
1280
|
+
.option('--file <name>', 'specific file to show')
|
|
1281
|
+
.option('--all', 'show all skill files')
|
|
1282
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1283
|
+
const { showSkill } = await import('./commands/skill/show.js');
|
|
1284
|
+
await showSkill({
|
|
1285
|
+
file: cmdOpts.file as string | undefined,
|
|
1286
|
+
all: Boolean(cmdOpts.all),
|
|
1287
|
+
});
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
skillCmd
|
|
1291
|
+
.command('paths')
|
|
1292
|
+
.description('Show resolved skill installation paths')
|
|
1293
|
+
.option(
|
|
1294
|
+
'-s, --scope <scope>',
|
|
1295
|
+
'filter by scope (project, user, all)',
|
|
1296
|
+
'all'
|
|
1297
|
+
)
|
|
1298
|
+
.option(
|
|
1299
|
+
'-t, --target <target>',
|
|
1300
|
+
'filter by target (claude, codex, all)',
|
|
1301
|
+
'all'
|
|
1302
|
+
)
|
|
1303
|
+
.option('--json', 'JSON output')
|
|
1304
|
+
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
1305
|
+
const scope = cmdOpts.scope as string;
|
|
1306
|
+
const target = cmdOpts.target as string;
|
|
1307
|
+
|
|
1308
|
+
// Validate scope
|
|
1309
|
+
if (!['project', 'user', 'all'].includes(scope)) {
|
|
1310
|
+
throw new CliError(
|
|
1311
|
+
'VALIDATION',
|
|
1312
|
+
`Invalid scope: ${scope}. Must be 'project', 'user', or 'all'.`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
// Validate target
|
|
1316
|
+
if (!['claude', 'codex', 'all'].includes(target)) {
|
|
1317
|
+
throw new CliError(
|
|
1318
|
+
'VALIDATION',
|
|
1319
|
+
`Invalid target: ${target}. Must be 'claude', 'codex', or 'all'.`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const { showPaths } = await import('./commands/skill/paths-cmd.js');
|
|
1324
|
+
await showPaths({
|
|
1325
|
+
scope: scope as 'project' | 'user' | 'all',
|
|
1326
|
+
target: target as 'claude' | 'codex' | 'all',
|
|
1327
|
+
json: Boolean(cmdOpts.json),
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
}
|