@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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gno query command implementation.
|
|
3
|
+
* Hybrid search with expansion, fusion, and reranking.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/query
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
|
|
9
|
+
import { getActivePreset } from '../../llm/registry';
|
|
10
|
+
import type {
|
|
11
|
+
EmbeddingPort,
|
|
12
|
+
GenerationPort,
|
|
13
|
+
RerankPort,
|
|
14
|
+
} from '../../llm/types';
|
|
15
|
+
import { type HybridSearchDeps, searchHybrid } from '../../pipeline/hybrid';
|
|
16
|
+
import type { HybridSearchOptions, SearchResults } from '../../pipeline/types';
|
|
17
|
+
import {
|
|
18
|
+
createVectorIndexPort,
|
|
19
|
+
type VectorIndexPort,
|
|
20
|
+
} from '../../store/vector';
|
|
21
|
+
import { initStore } from './shared';
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Types
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export type QueryCommandOptions = HybridSearchOptions & {
|
|
28
|
+
/** Override config path */
|
|
29
|
+
configPath?: string;
|
|
30
|
+
/** Override embedding model */
|
|
31
|
+
embedModel?: string;
|
|
32
|
+
/** Override generation model */
|
|
33
|
+
genModel?: string;
|
|
34
|
+
/** Override rerank model */
|
|
35
|
+
rerankModel?: string;
|
|
36
|
+
/** Output as JSON */
|
|
37
|
+
json?: boolean;
|
|
38
|
+
/** Output as Markdown */
|
|
39
|
+
md?: boolean;
|
|
40
|
+
/** Output as CSV */
|
|
41
|
+
csv?: boolean;
|
|
42
|
+
/** Output as XML */
|
|
43
|
+
xml?: boolean;
|
|
44
|
+
/** Output files only */
|
|
45
|
+
files?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface QueryFormatOptions {
|
|
49
|
+
format: 'terminal' | 'json' | 'files' | 'csv' | 'md' | 'xml';
|
|
50
|
+
full?: boolean;
|
|
51
|
+
lineNumbers?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type QueryResult =
|
|
55
|
+
| { success: true; data: SearchResults }
|
|
56
|
+
| { success: false; error: string };
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
// Command Implementation
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute gno query command.
|
|
64
|
+
*/
|
|
65
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: CLI orchestration with multiple output formats
|
|
66
|
+
export async function query(
|
|
67
|
+
queryText: string,
|
|
68
|
+
options: QueryCommandOptions = {}
|
|
69
|
+
): Promise<QueryResult> {
|
|
70
|
+
const isStructured =
|
|
71
|
+
options.json || options.files || options.csv || options.xml;
|
|
72
|
+
const limit = options.limit ?? (isStructured ? 20 : 5);
|
|
73
|
+
|
|
74
|
+
const initResult = await initStore({
|
|
75
|
+
configPath: options.configPath,
|
|
76
|
+
collection: options.collection,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!initResult.ok) {
|
|
80
|
+
return { success: false, error: initResult.error };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { store, config } = initResult;
|
|
84
|
+
|
|
85
|
+
let embedPort: EmbeddingPort | null = null;
|
|
86
|
+
let genPort: GenerationPort | null = null;
|
|
87
|
+
let rerankPort: RerankPort | null = null;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const preset = getActivePreset(config);
|
|
91
|
+
const llm = new LlmAdapter(config);
|
|
92
|
+
|
|
93
|
+
// Create embedding port (for vector search)
|
|
94
|
+
const embedUri = options.embedModel ?? preset.embed;
|
|
95
|
+
const embedResult = await llm.createEmbeddingPort(embedUri);
|
|
96
|
+
if (embedResult.ok) {
|
|
97
|
+
embedPort = embedResult.value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create generation port (for expansion) - optional
|
|
101
|
+
if (!options.noExpand) {
|
|
102
|
+
const genUri = options.genModel ?? preset.gen;
|
|
103
|
+
const genResult = await llm.createGenerationPort(genUri);
|
|
104
|
+
if (genResult.ok) {
|
|
105
|
+
genPort = genResult.value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Create rerank port - optional
|
|
110
|
+
if (!options.noRerank) {
|
|
111
|
+
const rerankUri = options.rerankModel ?? preset.rerank;
|
|
112
|
+
const rerankResult = await llm.createRerankPort(rerankUri);
|
|
113
|
+
if (rerankResult.ok) {
|
|
114
|
+
rerankPort = rerankResult.value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create vector index (optional)
|
|
119
|
+
let vectorIndex: VectorIndexPort | null = null;
|
|
120
|
+
if (embedPort) {
|
|
121
|
+
const embedInitResult = await embedPort.init();
|
|
122
|
+
if (embedInitResult.ok) {
|
|
123
|
+
const dimensions = embedPort.dimensions();
|
|
124
|
+
const db = store.getRawDb();
|
|
125
|
+
const vectorResult = await createVectorIndexPort(db, {
|
|
126
|
+
model: embedUri,
|
|
127
|
+
dimensions,
|
|
128
|
+
});
|
|
129
|
+
if (vectorResult.ok) {
|
|
130
|
+
vectorIndex = vectorResult.value;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const deps: HybridSearchDeps = {
|
|
136
|
+
store,
|
|
137
|
+
config,
|
|
138
|
+
vectorIndex,
|
|
139
|
+
embedPort,
|
|
140
|
+
genPort,
|
|
141
|
+
rerankPort,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const result = await searchHybrid(deps, queryText, {
|
|
145
|
+
...options,
|
|
146
|
+
limit,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!result.ok) {
|
|
150
|
+
return { success: false, error: result.error.message };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { success: true, data: result.value };
|
|
154
|
+
} finally {
|
|
155
|
+
if (embedPort) {
|
|
156
|
+
await embedPort.dispose();
|
|
157
|
+
}
|
|
158
|
+
if (genPort) {
|
|
159
|
+
await genPort.dispose();
|
|
160
|
+
}
|
|
161
|
+
if (rerankPort) {
|
|
162
|
+
await rerankPort.dispose();
|
|
163
|
+
}
|
|
164
|
+
await store.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// Formatters
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
// Import shared formatters dynamically to keep module loading fast
|
|
173
|
+
// and avoid circular dependencies
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Output explain data to stderr using pipeline formatters.
|
|
177
|
+
*/
|
|
178
|
+
function outputExplainToStderr(data: SearchResults): void {
|
|
179
|
+
const explain = data.meta.explain;
|
|
180
|
+
if (!explain) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Import pipeline formatters synchronously (they're lightweight)
|
|
185
|
+
const {
|
|
186
|
+
formatExplain,
|
|
187
|
+
formatResultExplain,
|
|
188
|
+
} = require('../../pipeline/explain');
|
|
189
|
+
process.stderr.write(`${formatExplain(explain.lines)}\n`);
|
|
190
|
+
process.stderr.write(`${formatResultExplain(explain.results)}\n`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Format query result for output.
|
|
195
|
+
* Uses shared formatSearchResults for consistent output across search commands.
|
|
196
|
+
*/
|
|
197
|
+
export function formatQuery(
|
|
198
|
+
result: QueryResult,
|
|
199
|
+
options: QueryFormatOptions
|
|
200
|
+
): string {
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
return options.format === 'json'
|
|
203
|
+
? JSON.stringify({
|
|
204
|
+
error: { code: 'QUERY_FAILED', message: result.error },
|
|
205
|
+
})
|
|
206
|
+
: `Error: ${result.error}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Output explain to stderr if present (async but best-effort)
|
|
210
|
+
outputExplainToStderr(result.data);
|
|
211
|
+
|
|
212
|
+
// Use shared formatter for consistent output
|
|
213
|
+
// Dynamic import to keep module loading fast
|
|
214
|
+
const { formatSearchResults } = require('../format/search-results');
|
|
215
|
+
return formatSearchResults(result.data, {
|
|
216
|
+
format: options.format,
|
|
217
|
+
full: options.full,
|
|
218
|
+
lineNumbers: options.lineNumbers,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference parser for document refs.
|
|
3
|
+
* Pure lexical parsing - NO store/config access.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/ref-parser
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Types
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type RefType = 'docid' | 'uri' | 'collPath';
|
|
13
|
+
|
|
14
|
+
export interface ParsedRef {
|
|
15
|
+
type: RefType;
|
|
16
|
+
/** Normalized ref (without :line suffix) */
|
|
17
|
+
value: string;
|
|
18
|
+
/** For collPath type */
|
|
19
|
+
collection?: string;
|
|
20
|
+
/** For collPath type */
|
|
21
|
+
relPath?: string;
|
|
22
|
+
/** Parsed :line suffix (1-indexed) */
|
|
23
|
+
line?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ParseRefResult = ParsedRef | { error: string };
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Top-level regex patterns (perf: avoid recreating in functions)
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const DOCID_PATTERN = /^#[a-f0-9]{6,8}$/;
|
|
33
|
+
const LINE_SUFFIX_PATTERN = /:(\d+)$/;
|
|
34
|
+
const GLOB_PATTERN = /[*?[\]]/;
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
// Parser Functions
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a single ref string.
|
|
42
|
+
* - Docid: starts with # (no :line suffix allowed)
|
|
43
|
+
* - URI: starts with gno:// (optional :N suffix)
|
|
44
|
+
* - Else: collection/path (optional :N suffix)
|
|
45
|
+
*/
|
|
46
|
+
export function parseRef(ref: string): ParseRefResult {
|
|
47
|
+
// 1. DocID: starts with #, validate pattern
|
|
48
|
+
if (ref.startsWith('#')) {
|
|
49
|
+
if (ref.includes(':')) {
|
|
50
|
+
return { error: 'Docid refs cannot have :line suffix' };
|
|
51
|
+
}
|
|
52
|
+
if (!DOCID_PATTERN.test(ref)) {
|
|
53
|
+
return { error: `Invalid docid format: ${ref}` };
|
|
54
|
+
}
|
|
55
|
+
return { type: 'docid', value: ref };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Parse optional :line suffix for URI and collPath
|
|
59
|
+
let line: number | undefined;
|
|
60
|
+
let baseRef = ref;
|
|
61
|
+
const lineMatch = ref.match(LINE_SUFFIX_PATTERN);
|
|
62
|
+
if (lineMatch?.[1]) {
|
|
63
|
+
const parsed = Number.parseInt(lineMatch[1], 10);
|
|
64
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
65
|
+
return { error: `Invalid line suffix (must be >= 1): ${ref}` };
|
|
66
|
+
}
|
|
67
|
+
line = parsed;
|
|
68
|
+
baseRef = ref.slice(0, -lineMatch[0].length);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. URI: starts with gno://
|
|
72
|
+
if (baseRef.startsWith('gno://')) {
|
|
73
|
+
return { type: 'uri', value: baseRef, line };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 4. Collection/path: must contain /
|
|
77
|
+
const slashIdx = baseRef.indexOf('/');
|
|
78
|
+
if (slashIdx === -1) {
|
|
79
|
+
return { error: `Invalid ref format (missing /): ${ref}` };
|
|
80
|
+
}
|
|
81
|
+
const collection = baseRef.slice(0, slashIdx);
|
|
82
|
+
const relPath = baseRef.slice(slashIdx + 1);
|
|
83
|
+
|
|
84
|
+
return { type: 'collPath', value: baseRef, collection, relPath, line };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Split comma-separated refs. Does NOT expand globs.
|
|
89
|
+
*/
|
|
90
|
+
export function splitRefs(refs: string[]): string[] {
|
|
91
|
+
const result: string[] = [];
|
|
92
|
+
for (const r of refs) {
|
|
93
|
+
for (const part of r.split(',')) {
|
|
94
|
+
const trimmed = part.trim();
|
|
95
|
+
if (trimmed) {
|
|
96
|
+
result.push(trimmed);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a ref contains glob characters.
|
|
105
|
+
*/
|
|
106
|
+
export function isGlobPattern(ref: string): boolean {
|
|
107
|
+
return GLOB_PATTERN.test(ref);
|
|
108
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gno reset - Reset GNO to fresh state
|
|
3
|
+
*
|
|
4
|
+
* Deletes all config, data, and cache directories.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// node:fs/promises: rm and stat for recursive directory deletion (no Bun equivalent)
|
|
8
|
+
import { rm, stat } from 'node:fs/promises';
|
|
9
|
+
// node:os: homedir for platform-agnostic home directory (no Bun equivalent)
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
// node:path: path manipulation utilities (no Bun equivalent)
|
|
12
|
+
import { isAbsolute, normalize, sep } from 'node:path';
|
|
13
|
+
import { resolveDirs } from '../../app/constants';
|
|
14
|
+
import { CliError } from '../errors';
|
|
15
|
+
|
|
16
|
+
interface ResetOptions {
|
|
17
|
+
confirm?: boolean;
|
|
18
|
+
keepConfig?: boolean;
|
|
19
|
+
keepCache?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DirResult {
|
|
23
|
+
path: string;
|
|
24
|
+
status: 'deleted' | 'missing' | 'kept';
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ResetResult {
|
|
29
|
+
results: DirResult[];
|
|
30
|
+
errors: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Forbidden paths that should never be deleted
|
|
34
|
+
const FORBIDDEN_PATHS = new Set([
|
|
35
|
+
'/',
|
|
36
|
+
'/Users',
|
|
37
|
+
'/home',
|
|
38
|
+
'/var',
|
|
39
|
+
'/etc',
|
|
40
|
+
'/tmp',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate a path is safe to delete.
|
|
45
|
+
* Must be absolute, under home directory, and not a system path.
|
|
46
|
+
*/
|
|
47
|
+
function assertSafePath(path: string, label: string): void {
|
|
48
|
+
const normalized = normalize(path);
|
|
49
|
+
|
|
50
|
+
// Must be absolute
|
|
51
|
+
if (!isAbsolute(normalized)) {
|
|
52
|
+
throw new CliError('VALIDATION', `${label} path must be absolute: ${path}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Must not be a forbidden system path
|
|
56
|
+
if (
|
|
57
|
+
FORBIDDEN_PATHS.has(normalized) ||
|
|
58
|
+
FORBIDDEN_PATHS.has(`${normalized}${sep}`)
|
|
59
|
+
) {
|
|
60
|
+
throw new CliError('VALIDATION', `Refusing to delete system path: ${path}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Must be under home directory (for user safety)
|
|
64
|
+
const home = homedir();
|
|
65
|
+
if (!normalized.startsWith(home + sep) && normalized !== home) {
|
|
66
|
+
throw new CliError(
|
|
67
|
+
'VALIDATION',
|
|
68
|
+
`${label} path must be under home directory: ${path}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Must not be home directory itself
|
|
73
|
+
if (normalized === home || normalized === home + sep) {
|
|
74
|
+
throw new CliError(
|
|
75
|
+
'VALIDATION',
|
|
76
|
+
`Refusing to delete home directory: ${path}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reset GNO by deleting directories.
|
|
83
|
+
*/
|
|
84
|
+
export async function reset(options: ResetOptions): Promise<ResetResult> {
|
|
85
|
+
if (!options.confirm) {
|
|
86
|
+
throw new CliError(
|
|
87
|
+
'VALIDATION',
|
|
88
|
+
'Reset requires --confirm or --yes flag to prevent accidental data loss'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const dirs = resolveDirs();
|
|
93
|
+
const results: DirResult[] = [];
|
|
94
|
+
const errors: string[] = [];
|
|
95
|
+
|
|
96
|
+
// Validate all paths before deleting anything
|
|
97
|
+
assertSafePath(dirs.data, 'Data');
|
|
98
|
+
if (!options.keepConfig) {
|
|
99
|
+
assertSafePath(dirs.config, 'Config');
|
|
100
|
+
}
|
|
101
|
+
if (!options.keepCache) {
|
|
102
|
+
assertSafePath(dirs.cache, 'Cache');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Delete data directory (always, contains index DB)
|
|
106
|
+
results.push(await rmDir(dirs.data));
|
|
107
|
+
|
|
108
|
+
// Delete config unless --keep-config
|
|
109
|
+
if (options.keepConfig) {
|
|
110
|
+
results.push({ path: dirs.config, status: 'kept' });
|
|
111
|
+
} else {
|
|
112
|
+
results.push(await rmDir(dirs.config));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Delete cache unless --keep-cache
|
|
116
|
+
if (options.keepCache) {
|
|
117
|
+
results.push({ path: dirs.cache, status: 'kept' });
|
|
118
|
+
} else {
|
|
119
|
+
results.push(await rmDir(dirs.cache));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Collect errors
|
|
123
|
+
for (const r of results) {
|
|
124
|
+
if (r.error) {
|
|
125
|
+
errors.push(`${r.path}: ${r.error}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (errors.length > 0) {
|
|
130
|
+
throw new CliError('RUNTIME', `Reset failed:\n${errors.join('\n')}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { results, errors };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function rmDir(path: string): Promise<DirResult> {
|
|
137
|
+
try {
|
|
138
|
+
// Check if exists first
|
|
139
|
+
await stat(path);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
const err = e as NodeJS.ErrnoException;
|
|
142
|
+
if (err.code === 'ENOENT') {
|
|
143
|
+
return { path, status: 'missing' };
|
|
144
|
+
}
|
|
145
|
+
return { path, status: 'missing', error: err.message };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await rm(path, { recursive: true, force: true });
|
|
150
|
+
return { path, status: 'deleted' };
|
|
151
|
+
} catch (e) {
|
|
152
|
+
const err = e as NodeJS.ErrnoException;
|
|
153
|
+
return { path, status: 'missing', error: err.message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format reset result for terminal output.
|
|
159
|
+
*/
|
|
160
|
+
export function formatReset(result: ResetResult): string {
|
|
161
|
+
const deleted = result.results.filter((r) => r.status === 'deleted');
|
|
162
|
+
const missing = result.results.filter((r) => r.status === 'missing');
|
|
163
|
+
const kept = result.results.filter((r) => r.status === 'kept');
|
|
164
|
+
|
|
165
|
+
const lines: string[] = ['GNO reset complete.', ''];
|
|
166
|
+
|
|
167
|
+
if (deleted.length > 0) {
|
|
168
|
+
lines.push('Deleted:');
|
|
169
|
+
for (const r of deleted) {
|
|
170
|
+
lines.push(` ${r.path}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (missing.length > 0) {
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push('Already missing:');
|
|
177
|
+
for (const r of missing) {
|
|
178
|
+
lines.push(` ${r.path}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (kept.length > 0) {
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push('Kept:');
|
|
185
|
+
for (const r of kept) {
|
|
186
|
+
lines.push(` ${r.path}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gno search command implementation.
|
|
3
|
+
* BM25 keyword search over indexed documents.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/search
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { searchBm25 } from '../../pipeline/search';
|
|
9
|
+
import type { SearchOptions, SearchResults } from '../../pipeline/types';
|
|
10
|
+
import {
|
|
11
|
+
type FormatOptions,
|
|
12
|
+
formatSearchResults,
|
|
13
|
+
} from '../format/search-results';
|
|
14
|
+
import { initStore } from './shared';
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Types
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type SearchCommandOptions = SearchOptions & {
|
|
21
|
+
/** Override config path */
|
|
22
|
+
configPath?: string;
|
|
23
|
+
/** Output as JSON */
|
|
24
|
+
json?: boolean;
|
|
25
|
+
/** Output as Markdown */
|
|
26
|
+
md?: boolean;
|
|
27
|
+
/** Output as CSV */
|
|
28
|
+
csv?: boolean;
|
|
29
|
+
/** Output as XML */
|
|
30
|
+
xml?: boolean;
|
|
31
|
+
/** Output files only */
|
|
32
|
+
files?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type SearchResult =
|
|
36
|
+
| { success: true; data: SearchResults }
|
|
37
|
+
| { success: false; error: string; isValidation?: boolean };
|
|
38
|
+
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
// Command Implementation
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute gno search command.
|
|
45
|
+
*/
|
|
46
|
+
export async function search(
|
|
47
|
+
query: string,
|
|
48
|
+
options: SearchCommandOptions = {}
|
|
49
|
+
): Promise<SearchResult> {
|
|
50
|
+
// Adjust default limit based on output format
|
|
51
|
+
const isStructured =
|
|
52
|
+
options.json || options.files || options.csv || options.xml;
|
|
53
|
+
const limit = options.limit ?? (isStructured ? 20 : 5);
|
|
54
|
+
|
|
55
|
+
const initResult = await initStore({
|
|
56
|
+
configPath: options.configPath,
|
|
57
|
+
collection: options.collection,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!initResult.ok) {
|
|
61
|
+
return { success: false, error: initResult.error };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { store } = initResult;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await searchBm25(store, query, {
|
|
68
|
+
...options,
|
|
69
|
+
limit,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
// Map INVALID_INPUT to validation error for proper exit code
|
|
74
|
+
const isValidation = result.error.code === 'INVALID_INPUT';
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: result.error.message,
|
|
78
|
+
isValidation,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { success: true, data: result.value };
|
|
83
|
+
} finally {
|
|
84
|
+
await store.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Formatter
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get output format from options.
|
|
94
|
+
*/
|
|
95
|
+
function getFormatType(options: SearchCommandOptions): FormatOptions['format'] {
|
|
96
|
+
if (options.json) {
|
|
97
|
+
return 'json';
|
|
98
|
+
}
|
|
99
|
+
if (options.files) {
|
|
100
|
+
return 'files';
|
|
101
|
+
}
|
|
102
|
+
if (options.csv) {
|
|
103
|
+
return 'csv';
|
|
104
|
+
}
|
|
105
|
+
if (options.md) {
|
|
106
|
+
return 'md';
|
|
107
|
+
}
|
|
108
|
+
if (options.xml) {
|
|
109
|
+
return 'xml';
|
|
110
|
+
}
|
|
111
|
+
return 'terminal';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format search result for output.
|
|
116
|
+
*/
|
|
117
|
+
export function formatSearch(
|
|
118
|
+
result: SearchResult,
|
|
119
|
+
options: SearchCommandOptions
|
|
120
|
+
): string {
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
return options.json
|
|
123
|
+
? JSON.stringify({
|
|
124
|
+
error: { code: 'QUERY_FAILED', message: result.error },
|
|
125
|
+
})
|
|
126
|
+
: `Error: ${result.error}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const formatOpts: FormatOptions = {
|
|
130
|
+
format: getFormatType(options),
|
|
131
|
+
full: options.full,
|
|
132
|
+
lineNumbers: options.lineNumbers,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return formatSearchResults(result.data, formatOpts);
|
|
136
|
+
}
|