@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,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global CLI context resolution.
|
|
3
|
+
* Handles global options, NO_COLOR support, etc.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/context
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { setColorsEnabled } from './colors';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Types
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface GlobalOptions {
|
|
15
|
+
index: string;
|
|
16
|
+
config?: string;
|
|
17
|
+
color: boolean;
|
|
18
|
+
verbose: boolean;
|
|
19
|
+
yes: boolean;
|
|
20
|
+
quiet: boolean;
|
|
21
|
+
json: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Parsing (pure - no side effects)
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse global options from Commander raw opts (pure function).
|
|
30
|
+
* Supports NO_COLOR env var (https://no-color.org/).
|
|
31
|
+
*/
|
|
32
|
+
export function parseGlobalOptions(
|
|
33
|
+
raw: Record<string, unknown>,
|
|
34
|
+
env = process.env
|
|
35
|
+
): GlobalOptions {
|
|
36
|
+
// NO_COLOR env var support (https://no-color.org/)
|
|
37
|
+
const noColorEnv = env.NO_COLOR !== undefined && env.NO_COLOR !== '';
|
|
38
|
+
// --no-color sets color to false in Commander
|
|
39
|
+
const noColorFlag = raw.color === false;
|
|
40
|
+
|
|
41
|
+
const colorEnabled = !(noColorEnv || noColorFlag);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
index: (raw.index as string) ?? 'default',
|
|
45
|
+
config: raw.config as string | undefined,
|
|
46
|
+
color: colorEnabled,
|
|
47
|
+
verbose: Boolean(raw.verbose),
|
|
48
|
+
yes: Boolean(raw.yes),
|
|
49
|
+
quiet: Boolean(raw.quiet),
|
|
50
|
+
json: Boolean(raw.json),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Side Effects
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Apply global options side effects (colors, etc).
|
|
60
|
+
* Should be called exactly once per CLI invocation.
|
|
61
|
+
*/
|
|
62
|
+
export function applyGlobalOptions(globals: GlobalOptions): void {
|
|
63
|
+
setColorsEnabled(globals.color);
|
|
64
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI error model aligned to spec.
|
|
3
|
+
* Exit codes: 0=success, 1=validation, 2=runtime
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/errors
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Error Types
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type CliErrorCode = 'VALIDATION' | 'RUNTIME';
|
|
13
|
+
|
|
14
|
+
export class CliError extends Error {
|
|
15
|
+
readonly code: CliErrorCode;
|
|
16
|
+
readonly details?: Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
code: CliErrorCode,
|
|
20
|
+
message: string,
|
|
21
|
+
details?: Record<string, unknown>
|
|
22
|
+
) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.details = details;
|
|
26
|
+
this.name = 'CliError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Exit Codes
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export function exitCodeFor(err: CliError): 1 | 2 {
|
|
35
|
+
return err.code === 'VALIDATION' ? 1 : 2;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
// Error Formatting
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface ErrorFormatOptions {
|
|
43
|
+
json?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format error for output.
|
|
48
|
+
* JSON mode returns { error: { code, message, details } } envelope.
|
|
49
|
+
*/
|
|
50
|
+
export function formatErrorForOutput(
|
|
51
|
+
err: CliError,
|
|
52
|
+
options: ErrorFormatOptions = {}
|
|
53
|
+
): string {
|
|
54
|
+
if (options.json) {
|
|
55
|
+
return JSON.stringify({
|
|
56
|
+
error: {
|
|
57
|
+
code: err.code,
|
|
58
|
+
message: err.message,
|
|
59
|
+
...(err.details && { details: err.details }),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return `Error: ${err.message}`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatters for search/vsearch commands.
|
|
3
|
+
* Centralizes output formatting to avoid duplication.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/format/searchResults
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SearchResults } from '../../pipeline/types';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Types
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export interface FormatOptions {
|
|
15
|
+
full?: boolean;
|
|
16
|
+
lineNumbers?: boolean;
|
|
17
|
+
format: 'terminal' | 'json' | 'files' | 'csv' | 'md' | 'xml';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Constants
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const SNIPPET_LIMIT_TERMINAL = 200;
|
|
25
|
+
const SNIPPET_LIMIT_STRUCTURED = 500;
|
|
26
|
+
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// Main Formatter
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format search results based on output format and options.
|
|
33
|
+
*/
|
|
34
|
+
export function formatSearchResults(
|
|
35
|
+
data: SearchResults,
|
|
36
|
+
options: FormatOptions
|
|
37
|
+
): string {
|
|
38
|
+
switch (options.format) {
|
|
39
|
+
case 'json':
|
|
40
|
+
return JSON.stringify(data, null, 2);
|
|
41
|
+
case 'files':
|
|
42
|
+
return formatFiles(data);
|
|
43
|
+
case 'csv':
|
|
44
|
+
return formatCsv(data);
|
|
45
|
+
case 'md':
|
|
46
|
+
return formatMarkdown(data, options);
|
|
47
|
+
case 'xml':
|
|
48
|
+
return formatXml(data, options);
|
|
49
|
+
default:
|
|
50
|
+
return formatTerminal(data, options);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Format Helpers
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format as line protocol per spec.
|
|
60
|
+
* Output: #docid,score,gno://collection/path
|
|
61
|
+
*/
|
|
62
|
+
function formatFiles(data: SearchResults): string {
|
|
63
|
+
return data.results
|
|
64
|
+
.map((r) => {
|
|
65
|
+
// Defensive: ensure docid starts with #
|
|
66
|
+
const docid = r.docid.startsWith('#') ? r.docid : `#${r.docid}`;
|
|
67
|
+
return `${docid},${r.score.toFixed(4)},${r.uri}`;
|
|
68
|
+
})
|
|
69
|
+
.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatTerminal(data: SearchResults, options: FormatOptions): string {
|
|
73
|
+
if (data.results.length === 0) {
|
|
74
|
+
return 'No results found.';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
for (const r of data.results) {
|
|
79
|
+
lines.push(`[${r.docid}] ${r.uri} (score: ${r.score.toFixed(2)})`);
|
|
80
|
+
if (r.title) {
|
|
81
|
+
lines.push(` ${r.title}`);
|
|
82
|
+
}
|
|
83
|
+
if (r.snippet) {
|
|
84
|
+
const content = options.full
|
|
85
|
+
? r.snippet
|
|
86
|
+
: truncate(r.snippet, SNIPPET_LIMIT_TERMINAL);
|
|
87
|
+
// For --full, snippetRange is undefined; start at line 1
|
|
88
|
+
const startLine = r.snippetRange?.startLine ?? 1;
|
|
89
|
+
const formatted = options.lineNumbers
|
|
90
|
+
? addLineNumbers(content, startLine)
|
|
91
|
+
: content;
|
|
92
|
+
// Indent multiline snippets
|
|
93
|
+
lines.push(` ${formatted.replace(/\n/g, '\n ')}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
lines.push(
|
|
98
|
+
`${data.meta.totalResults} result(s) for "${data.meta.query}" (${data.meta.mode})`
|
|
99
|
+
);
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatMarkdown(data: SearchResults, options: FormatOptions): string {
|
|
104
|
+
const modeLabel = data.meta.mode === 'vector' ? 'Vector ' : '';
|
|
105
|
+
if (data.results.length === 0) {
|
|
106
|
+
return `# ${modeLabel}Search Results\n\nNo results found for "${data.meta.query}".`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines: string[] = [];
|
|
110
|
+
lines.push(`# ${modeLabel}Search Results for "${data.meta.query}"`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(`*${data.meta.totalResults} result(s)*`);
|
|
113
|
+
lines.push('');
|
|
114
|
+
|
|
115
|
+
for (const r of data.results) {
|
|
116
|
+
lines.push(`## ${r.title || r.source.relPath}`);
|
|
117
|
+
lines.push('');
|
|
118
|
+
lines.push(`- **URI**: \`${r.uri}\``);
|
|
119
|
+
lines.push(`- **Score**: ${r.score.toFixed(2)}`);
|
|
120
|
+
lines.push(`- **DocID**: \`${r.docid}\``);
|
|
121
|
+
if (r.snippet) {
|
|
122
|
+
const content = options.full
|
|
123
|
+
? r.snippet
|
|
124
|
+
: truncate(r.snippet, SNIPPET_LIMIT_STRUCTURED);
|
|
125
|
+
const startLine = r.snippetRange?.startLine ?? 1;
|
|
126
|
+
const formatted = options.lineNumbers
|
|
127
|
+
? addLineNumbers(content, startLine)
|
|
128
|
+
: content;
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('```');
|
|
131
|
+
lines.push(formatted);
|
|
132
|
+
lines.push('```');
|
|
133
|
+
}
|
|
134
|
+
lines.push('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatCsv(data: SearchResults): string {
|
|
141
|
+
const lines: string[] = [];
|
|
142
|
+
lines.push('docid,score,uri,title,relPath');
|
|
143
|
+
for (const r of data.results) {
|
|
144
|
+
const title = escapeCsv(r.title ?? '');
|
|
145
|
+
const relPath = escapeCsv(r.source.relPath);
|
|
146
|
+
lines.push(
|
|
147
|
+
`"${r.docid}",${r.score.toFixed(4)},"${r.uri}","${title}","${relPath}"`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatXml(data: SearchResults, options: FormatOptions): string {
|
|
154
|
+
const lines: string[] = [];
|
|
155
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
156
|
+
lines.push('<searchResults>');
|
|
157
|
+
lines.push(
|
|
158
|
+
` <meta query="${escapeXml(data.meta.query)}" mode="${data.meta.mode}" total="${data.meta.totalResults}"/>`
|
|
159
|
+
);
|
|
160
|
+
for (const r of data.results) {
|
|
161
|
+
lines.push(' <result>');
|
|
162
|
+
lines.push(` <docid>${escapeXml(r.docid)}</docid>`);
|
|
163
|
+
lines.push(` <score>${r.score}</score>`);
|
|
164
|
+
lines.push(` <uri>${escapeXml(r.uri)}</uri>`);
|
|
165
|
+
if (r.title) {
|
|
166
|
+
lines.push(` <title>${escapeXml(r.title)}</title>`);
|
|
167
|
+
}
|
|
168
|
+
lines.push(` <relPath>${escapeXml(r.source.relPath)}</relPath>`);
|
|
169
|
+
if (r.snippet) {
|
|
170
|
+
const content = options.full
|
|
171
|
+
? r.snippet
|
|
172
|
+
: truncate(r.snippet, SNIPPET_LIMIT_STRUCTURED);
|
|
173
|
+
const startLine = r.snippetRange?.startLine ?? 1;
|
|
174
|
+
const formatted = options.lineNumbers
|
|
175
|
+
? addLineNumbers(content, startLine)
|
|
176
|
+
: content;
|
|
177
|
+
lines.push(` <snippet>${escapeXml(formatted)}</snippet>`);
|
|
178
|
+
}
|
|
179
|
+
lines.push(' </result>');
|
|
180
|
+
}
|
|
181
|
+
lines.push('</searchResults>');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
// Utility Functions
|
|
187
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function addLineNumbers(text: string, startLine: number): string {
|
|
190
|
+
return text
|
|
191
|
+
.split('\n')
|
|
192
|
+
.map((line, i) => `${startLine + i}: ${line}`)
|
|
193
|
+
.join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function truncate(text: string, limit: number): string {
|
|
197
|
+
return text.length > limit ? `${text.slice(0, limit)}...` : text;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function escapeCsv(str: string): string {
|
|
201
|
+
return str.replace(/"/g, '""');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function escapeXml(str: string): string {
|
|
205
|
+
return str
|
|
206
|
+
.replace(/&/g, '&')
|
|
207
|
+
.replace(/</g, '<')
|
|
208
|
+
.replace(/>/g, '>')
|
|
209
|
+
.replace(/"/g, '"')
|
|
210
|
+
.replace(/'/g, ''');
|
|
211
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output format selection and validation.
|
|
3
|
+
* Implements conditional defaults per spec.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/options
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CliError } from './errors';
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Types
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export type OutputFormat = 'terminal' | 'json' | 'files' | 'csv' | 'md' | 'xml';
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Format Support Matrix (per spec/cli.md)
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
// Command IDs for consistent referencing
|
|
21
|
+
export const CMD = {
|
|
22
|
+
search: 'search',
|
|
23
|
+
vsearch: 'vsearch',
|
|
24
|
+
query: 'query',
|
|
25
|
+
ask: 'ask',
|
|
26
|
+
get: 'get',
|
|
27
|
+
multiGet: 'multi-get',
|
|
28
|
+
ls: 'ls',
|
|
29
|
+
status: 'status',
|
|
30
|
+
collectionList: 'collection.list',
|
|
31
|
+
contextList: 'context.list',
|
|
32
|
+
contextCheck: 'context.check',
|
|
33
|
+
modelsList: 'models.list',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type CommandId = (typeof CMD)[keyof typeof CMD];
|
|
37
|
+
|
|
38
|
+
const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
|
|
39
|
+
[CMD.search]: ['terminal', 'json', 'files', 'csv', 'md', 'xml'],
|
|
40
|
+
[CMD.vsearch]: ['terminal', 'json', 'files', 'csv', 'md', 'xml'],
|
|
41
|
+
[CMD.query]: ['terminal', 'json', 'files', 'csv', 'md', 'xml'],
|
|
42
|
+
[CMD.ask]: ['terminal', 'json', 'md'],
|
|
43
|
+
[CMD.get]: ['terminal', 'json', 'md'],
|
|
44
|
+
[CMD.multiGet]: ['terminal', 'json', 'files', 'md'],
|
|
45
|
+
[CMD.ls]: ['terminal', 'json', 'files', 'md'],
|
|
46
|
+
[CMD.status]: ['terminal', 'json'],
|
|
47
|
+
[CMD.collectionList]: ['terminal', 'json', 'md'],
|
|
48
|
+
[CMD.contextList]: ['terminal', 'json', 'md'],
|
|
49
|
+
[CMD.contextCheck]: ['terminal', 'json', 'md'],
|
|
50
|
+
[CMD.modelsList]: ['terminal', 'json'],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Format Selection
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface FormatFlags {
|
|
58
|
+
json?: boolean;
|
|
59
|
+
files?: boolean;
|
|
60
|
+
csv?: boolean;
|
|
61
|
+
md?: boolean;
|
|
62
|
+
xml?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Select output format from flags.
|
|
67
|
+
* Throws if multiple format flags are set.
|
|
68
|
+
*/
|
|
69
|
+
export function selectOutputFormat(flags: FormatFlags): OutputFormat {
|
|
70
|
+
const selected: OutputFormat[] = [];
|
|
71
|
+
if (flags.json) {
|
|
72
|
+
selected.push('json');
|
|
73
|
+
}
|
|
74
|
+
if (flags.files) {
|
|
75
|
+
selected.push('files');
|
|
76
|
+
}
|
|
77
|
+
if (flags.csv) {
|
|
78
|
+
selected.push('csv');
|
|
79
|
+
}
|
|
80
|
+
if (flags.md) {
|
|
81
|
+
selected.push('md');
|
|
82
|
+
}
|
|
83
|
+
if (flags.xml) {
|
|
84
|
+
selected.push('xml');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (selected.length > 1) {
|
|
88
|
+
throw new CliError(
|
|
89
|
+
'VALIDATION',
|
|
90
|
+
`Conflicting output formats: ${selected.join(', ')}. Choose one.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return selected[0] ?? 'terminal';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Assert format is supported for command.
|
|
99
|
+
*/
|
|
100
|
+
export function assertFormatSupported(
|
|
101
|
+
cmd: CommandId,
|
|
102
|
+
format: OutputFormat
|
|
103
|
+
): void {
|
|
104
|
+
const supported = FORMAT_SUPPORT[cmd];
|
|
105
|
+
if (!supported.includes(format)) {
|
|
106
|
+
throw new CliError(
|
|
107
|
+
'VALIDATION',
|
|
108
|
+
`Format --${format} is not supported by '${cmd}'. Supported: ${supported.join(', ')}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
// Conditional Defaults
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get default limit based on format.
|
|
119
|
+
* Spec: 5 for terminal, 20 for structured output.
|
|
120
|
+
*/
|
|
121
|
+
export function getDefaultLimit(format: OutputFormat): number {
|
|
122
|
+
return format === 'terminal' ? 5 : 20;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
// Numeric Option Parsing
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parse and validate a positive integer option.
|
|
131
|
+
* Throws CliError on invalid input.
|
|
132
|
+
*/
|
|
133
|
+
export function parsePositiveInt(name: string, value: unknown): number {
|
|
134
|
+
if (value === undefined || value === null) {
|
|
135
|
+
throw new CliError('VALIDATION', `--${name} requires a value`);
|
|
136
|
+
}
|
|
137
|
+
const strValue = String(value);
|
|
138
|
+
const num = Number.parseInt(strValue, 10);
|
|
139
|
+
if (Number.isNaN(num)) {
|
|
140
|
+
throw new CliError(
|
|
141
|
+
'VALIDATION',
|
|
142
|
+
`--${name} must be a number, got: ${strValue}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (num < 1) {
|
|
146
|
+
throw new CliError('VALIDATION', `--${name} must be positive, got: ${num}`);
|
|
147
|
+
}
|
|
148
|
+
return num;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse optional positive integer, returning undefined if not provided.
|
|
153
|
+
*/
|
|
154
|
+
export function parseOptionalPositiveInt(
|
|
155
|
+
name: string,
|
|
156
|
+
value: unknown
|
|
157
|
+
): number | undefined {
|
|
158
|
+
if (value === undefined || value === null) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
return parsePositiveInt(name, value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse optional float, returning undefined if not provided.
|
|
166
|
+
*/
|
|
167
|
+
export function parseOptionalFloat(
|
|
168
|
+
name: string,
|
|
169
|
+
value: unknown
|
|
170
|
+
): number | undefined {
|
|
171
|
+
if (value === undefined || value === null) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const strValue = String(value);
|
|
175
|
+
const num = Number.parseFloat(strValue);
|
|
176
|
+
if (Number.isNaN(num)) {
|
|
177
|
+
throw new CliError(
|
|
178
|
+
'VALIDATION',
|
|
179
|
+
`--${name} must be a number, got: ${strValue}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return num;
|
|
183
|
+
}
|