@gmickel/gno 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +256 -0
  2. package/assets/skill/SKILL.md +112 -0
  3. package/assets/skill/cli-reference.md +327 -0
  4. package/assets/skill/examples.md +234 -0
  5. package/assets/skill/mcp-reference.md +159 -0
  6. package/package.json +90 -0
  7. package/src/app/constants.ts +313 -0
  8. package/src/cli/colors.ts +65 -0
  9. package/src/cli/commands/ask.ts +545 -0
  10. package/src/cli/commands/cleanup.ts +105 -0
  11. package/src/cli/commands/collection/add.ts +120 -0
  12. package/src/cli/commands/collection/index.ts +10 -0
  13. package/src/cli/commands/collection/list.ts +108 -0
  14. package/src/cli/commands/collection/remove.ts +64 -0
  15. package/src/cli/commands/collection/rename.ts +95 -0
  16. package/src/cli/commands/context/add.ts +67 -0
  17. package/src/cli/commands/context/check.ts +153 -0
  18. package/src/cli/commands/context/index.ts +10 -0
  19. package/src/cli/commands/context/list.ts +109 -0
  20. package/src/cli/commands/context/rm.ts +52 -0
  21. package/src/cli/commands/doctor.ts +393 -0
  22. package/src/cli/commands/embed.ts +462 -0
  23. package/src/cli/commands/get.ts +356 -0
  24. package/src/cli/commands/index-cmd.ts +119 -0
  25. package/src/cli/commands/index.ts +102 -0
  26. package/src/cli/commands/init.ts +328 -0
  27. package/src/cli/commands/ls.ts +217 -0
  28. package/src/cli/commands/mcp/config.ts +300 -0
  29. package/src/cli/commands/mcp/index.ts +24 -0
  30. package/src/cli/commands/mcp/install.ts +203 -0
  31. package/src/cli/commands/mcp/paths.ts +470 -0
  32. package/src/cli/commands/mcp/status.ts +222 -0
  33. package/src/cli/commands/mcp/uninstall.ts +158 -0
  34. package/src/cli/commands/mcp.ts +20 -0
  35. package/src/cli/commands/models/clear.ts +103 -0
  36. package/src/cli/commands/models/index.ts +32 -0
  37. package/src/cli/commands/models/list.ts +214 -0
  38. package/src/cli/commands/models/path.ts +51 -0
  39. package/src/cli/commands/models/pull.ts +199 -0
  40. package/src/cli/commands/models/use.ts +85 -0
  41. package/src/cli/commands/multi-get.ts +400 -0
  42. package/src/cli/commands/query.ts +220 -0
  43. package/src/cli/commands/ref-parser.ts +108 -0
  44. package/src/cli/commands/reset.ts +191 -0
  45. package/src/cli/commands/search.ts +136 -0
  46. package/src/cli/commands/shared.ts +156 -0
  47. package/src/cli/commands/skill/index.ts +19 -0
  48. package/src/cli/commands/skill/install.ts +197 -0
  49. package/src/cli/commands/skill/paths-cmd.ts +81 -0
  50. package/src/cli/commands/skill/paths.ts +191 -0
  51. package/src/cli/commands/skill/show.ts +73 -0
  52. package/src/cli/commands/skill/uninstall.ts +141 -0
  53. package/src/cli/commands/status.ts +205 -0
  54. package/src/cli/commands/update.ts +68 -0
  55. package/src/cli/commands/vsearch.ts +188 -0
  56. package/src/cli/context.ts +64 -0
  57. package/src/cli/errors.ts +64 -0
  58. package/src/cli/format/search-results.ts +211 -0
  59. package/src/cli/options.ts +183 -0
  60. package/src/cli/program.ts +1330 -0
  61. package/src/cli/run.ts +213 -0
  62. package/src/cli/ui.ts +92 -0
  63. package/src/config/defaults.ts +20 -0
  64. package/src/config/index.ts +55 -0
  65. package/src/config/loader.ts +161 -0
  66. package/src/config/paths.ts +87 -0
  67. package/src/config/saver.ts +153 -0
  68. package/src/config/types.ts +280 -0
  69. package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
  70. package/src/converters/adapters/officeparser/adapter.ts +126 -0
  71. package/src/converters/canonicalize.ts +89 -0
  72. package/src/converters/errors.ts +218 -0
  73. package/src/converters/index.ts +51 -0
  74. package/src/converters/mime.ts +163 -0
  75. package/src/converters/native/markdown.ts +115 -0
  76. package/src/converters/native/plaintext.ts +56 -0
  77. package/src/converters/path.ts +48 -0
  78. package/src/converters/pipeline.ts +159 -0
  79. package/src/converters/registry.ts +74 -0
  80. package/src/converters/types.ts +123 -0
  81. package/src/converters/versions.ts +24 -0
  82. package/src/index.ts +27 -0
  83. package/src/ingestion/chunker.ts +238 -0
  84. package/src/ingestion/index.ts +32 -0
  85. package/src/ingestion/language.ts +276 -0
  86. package/src/ingestion/sync.ts +671 -0
  87. package/src/ingestion/types.ts +219 -0
  88. package/src/ingestion/walker.ts +235 -0
  89. package/src/llm/cache.ts +467 -0
  90. package/src/llm/errors.ts +191 -0
  91. package/src/llm/index.ts +58 -0
  92. package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
  93. package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
  94. package/src/llm/nodeLlamaCpp/generation.ts +88 -0
  95. package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
  96. package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
  97. package/src/llm/registry.ts +86 -0
  98. package/src/llm/types.ts +129 -0
  99. package/src/mcp/resources/index.ts +151 -0
  100. package/src/mcp/server.ts +229 -0
  101. package/src/mcp/tools/get.ts +220 -0
  102. package/src/mcp/tools/index.ts +160 -0
  103. package/src/mcp/tools/multi-get.ts +263 -0
  104. package/src/mcp/tools/query.ts +226 -0
  105. package/src/mcp/tools/search.ts +119 -0
  106. package/src/mcp/tools/status.ts +81 -0
  107. package/src/mcp/tools/vsearch.ts +198 -0
  108. package/src/pipeline/chunk-lookup.ts +44 -0
  109. package/src/pipeline/expansion.ts +256 -0
  110. package/src/pipeline/explain.ts +115 -0
  111. package/src/pipeline/fusion.ts +185 -0
  112. package/src/pipeline/hybrid.ts +535 -0
  113. package/src/pipeline/index.ts +64 -0
  114. package/src/pipeline/query-language.ts +118 -0
  115. package/src/pipeline/rerank.ts +223 -0
  116. package/src/pipeline/search.ts +261 -0
  117. package/src/pipeline/types.ts +328 -0
  118. package/src/pipeline/vsearch.ts +348 -0
  119. package/src/store/index.ts +41 -0
  120. package/src/store/migrations/001-initial.ts +196 -0
  121. package/src/store/migrations/index.ts +20 -0
  122. package/src/store/migrations/runner.ts +187 -0
  123. package/src/store/sqlite/adapter.ts +1242 -0
  124. package/src/store/sqlite/index.ts +7 -0
  125. package/src/store/sqlite/setup.ts +129 -0
  126. package/src/store/sqlite/types.ts +28 -0
  127. package/src/store/types.ts +506 -0
  128. package/src/store/vector/index.ts +13 -0
  129. package/src/store/vector/sqlite-vec.ts +373 -0
  130. package/src/store/vector/stats.ts +152 -0
  131. package/src/store/vector/types.ts +115 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Show/preview GNO skill files without installing.
3
+ *
4
+ * @module src/cli/commands/skill/show
5
+ */
6
+
7
+ import { readdir } from 'node:fs/promises';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { CliError } from '../../errors.js';
11
+
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+ // Source Path Resolution
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ function getSkillSourceDir(): string {
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ return join(__dirname, '../../../../assets/skill');
19
+ }
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Show Command
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ export interface ShowOptions {
26
+ file?: string;
27
+ all?: boolean;
28
+ }
29
+
30
+ const DEFAULT_FILE = 'SKILL.md';
31
+
32
+ /**
33
+ * Show skill file content.
34
+ */
35
+ export async function showSkill(opts: ShowOptions = {}): Promise<void> {
36
+ const sourceDir = getSkillSourceDir();
37
+
38
+ // Get available files
39
+ let files: string[];
40
+ try {
41
+ files = await readdir(sourceDir);
42
+ } catch {
43
+ throw new CliError('RUNTIME', `Skill files not found at ${sourceDir}`);
44
+ }
45
+
46
+ const mdFiles = files.filter((f) => f.endsWith('.md')).sort();
47
+
48
+ if (opts.all) {
49
+ // Show all files with separators
50
+ for (const file of mdFiles) {
51
+ process.stdout.write(`--- ${file} ---\n`);
52
+ const content = await Bun.file(join(sourceDir, file)).text();
53
+ process.stdout.write(`${content}\n`);
54
+ process.stdout.write('\n');
55
+ }
56
+ } else {
57
+ // Show single file
58
+ const fileName = opts.file ?? DEFAULT_FILE;
59
+
60
+ if (!mdFiles.includes(fileName)) {
61
+ throw new CliError(
62
+ 'VALIDATION',
63
+ `Unknown file: ${fileName}. Available: ${mdFiles.join(', ')}`
64
+ );
65
+ }
66
+
67
+ const content = await Bun.file(join(sourceDir, fileName)).text();
68
+ process.stdout.write(`${content}\n`);
69
+ }
70
+
71
+ // Always list available files at end
72
+ process.stdout.write(`\nFiles: ${mdFiles.join(', ')}\n`);
73
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Uninstall GNO agent skill from Claude Code or Codex.
3
+ * Includes safety checks before deletion.
4
+ *
5
+ * @module src/cli/commands/skill/uninstall
6
+ */
7
+
8
+ import { rm } from 'node:fs/promises';
9
+ import { join } from 'node:path';
10
+ import { CliError } from '../../errors.js';
11
+ import { getGlobals } from '../../program.js';
12
+ import {
13
+ resolveSkillPaths,
14
+ type SkillScope,
15
+ type SkillTarget,
16
+ validatePathForDeletion,
17
+ } from './paths.js';
18
+
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ // Uninstall Command
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ export interface UninstallOptions {
24
+ scope?: SkillScope;
25
+ target?: SkillTarget | 'all';
26
+ /** Override for testing */
27
+ cwd?: string;
28
+ /** Override for testing */
29
+ homeDir?: string;
30
+ /** JSON output (defaults to globals.json) */
31
+ json?: boolean;
32
+ /** Quiet mode (defaults to globals.quiet) */
33
+ quiet?: boolean;
34
+ }
35
+
36
+ interface UninstallResult {
37
+ target: SkillTarget;
38
+ scope: SkillScope;
39
+ path: string;
40
+ }
41
+
42
+ /**
43
+ * Uninstall skill from a single target.
44
+ */
45
+ async function uninstallFromTarget(
46
+ scope: SkillScope,
47
+ target: SkillTarget,
48
+ overrides?: { cwd?: string; homeDir?: string }
49
+ ): Promise<UninstallResult | null> {
50
+ const paths = resolveSkillPaths({ scope, target, ...overrides });
51
+
52
+ // Check if exists
53
+ const exists = await Bun.file(join(paths.gnoDir, 'SKILL.md')).exists();
54
+ if (!exists) {
55
+ return null;
56
+ }
57
+
58
+ // Safety validation
59
+ const validationError = validatePathForDeletion(paths.gnoDir, paths.base);
60
+ if (validationError) {
61
+ throw new CliError(
62
+ 'RUNTIME',
63
+ `Safety check failed for ${paths.gnoDir}: ${validationError}`
64
+ );
65
+ }
66
+
67
+ // Remove directory
68
+ try {
69
+ await rm(paths.gnoDir, { recursive: true, force: true });
70
+ return { target, scope, path: paths.gnoDir };
71
+ } catch (err) {
72
+ throw new CliError(
73
+ 'RUNTIME',
74
+ `Failed to remove skill: ${err instanceof Error ? err.message : String(err)}`
75
+ );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get globals with fallback for testing.
81
+ */
82
+ function safeGetGlobals(): { json: boolean; quiet: boolean } {
83
+ try {
84
+ return getGlobals();
85
+ } catch {
86
+ return { json: false, quiet: false };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Uninstall GNO skill.
92
+ */
93
+ export async function uninstallSkill(
94
+ opts: UninstallOptions = {}
95
+ ): Promise<void> {
96
+ const scope = opts.scope ?? 'project';
97
+ const target = opts.target ?? 'claude';
98
+ const globals = safeGetGlobals();
99
+ const json = opts.json ?? globals.json;
100
+ const quiet = opts.quiet ?? globals.quiet;
101
+
102
+ const targets: SkillTarget[] =
103
+ target === 'all' ? ['claude', 'codex'] : [target];
104
+
105
+ const results: UninstallResult[] = [];
106
+ const notFound: string[] = [];
107
+
108
+ for (const t of targets) {
109
+ const result = await uninstallFromTarget(scope, t, {
110
+ cwd: opts.cwd,
111
+ homeDir: opts.homeDir,
112
+ });
113
+ if (result) {
114
+ results.push(result);
115
+ } else {
116
+ notFound.push(t);
117
+ }
118
+ }
119
+
120
+ // If nothing was uninstalled
121
+ if (results.length === 0) {
122
+ throw new CliError(
123
+ 'VALIDATION',
124
+ `GNO skill not found for ${targets.join(', ')} (${scope} scope)`
125
+ );
126
+ }
127
+
128
+ // Output
129
+ if (json) {
130
+ process.stdout.write(
131
+ `${JSON.stringify({ uninstalled: results }, null, 2)}\n`
132
+ );
133
+ } else if (!quiet) {
134
+ for (const r of results) {
135
+ process.stdout.write(`Uninstalled GNO skill from ${r.path}\n`);
136
+ }
137
+ if (notFound.length > 0) {
138
+ process.stdout.write(`(Not found for: ${notFound.join(', ')})\n`);
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * gno status command implementation.
3
+ * Display index status and health information.
4
+ *
5
+ * @module src/cli/commands/status
6
+ */
7
+
8
+ import { getIndexDbPath } from '../../app/constants';
9
+ import { getConfigPaths, isInitialized, loadConfig } from '../../config';
10
+ import { SqliteAdapter } from '../../store/sqlite/adapter';
11
+ import type { IndexStatus } from '../../store/types';
12
+
13
+ /**
14
+ * Options for status command.
15
+ */
16
+ export interface StatusOptions {
17
+ /** Override config path */
18
+ configPath?: string;
19
+ /** Output as JSON */
20
+ json?: boolean;
21
+ /** Output as Markdown */
22
+ md?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Result of status command.
27
+ */
28
+ export type StatusResult =
29
+ | { success: true; status: IndexStatus }
30
+ | { success: false; error: string };
31
+
32
+ /**
33
+ * Format status as terminal output.
34
+ */
35
+ function formatTerminal(indexStatus: IndexStatus): string {
36
+ const lines: string[] = [];
37
+
38
+ lines.push(`Index: ${indexStatus.indexName}`);
39
+ lines.push(`Config: ${indexStatus.configPath}`);
40
+ lines.push(`Database: ${indexStatus.dbPath}`);
41
+ lines.push('');
42
+
43
+ if (indexStatus.collections.length === 0) {
44
+ lines.push('No collections configured.');
45
+ } else {
46
+ lines.push('Collections:');
47
+ for (const c of indexStatus.collections) {
48
+ lines.push(
49
+ ` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
50
+ (c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : '')
51
+ );
52
+ }
53
+ }
54
+
55
+ lines.push('');
56
+ lines.push(
57
+ `Total: ${indexStatus.activeDocuments} documents, ${indexStatus.totalChunks} chunks`
58
+ );
59
+
60
+ if (indexStatus.embeddingBacklog > 0) {
61
+ lines.push(`Embedding backlog: ${indexStatus.embeddingBacklog} chunks`);
62
+ }
63
+
64
+ if (indexStatus.recentErrors > 0) {
65
+ lines.push(`Recent errors: ${indexStatus.recentErrors} (last 24h)`);
66
+ }
67
+
68
+ if (indexStatus.lastUpdatedAt) {
69
+ lines.push(`Last updated: ${indexStatus.lastUpdatedAt}`);
70
+ }
71
+
72
+ lines.push(`Health: ${indexStatus.healthy ? 'OK' : 'DEGRADED'}`);
73
+
74
+ return lines.join('\n');
75
+ }
76
+
77
+ /**
78
+ * Format status as Markdown.
79
+ */
80
+ function formatMarkdown(indexStatus: IndexStatus): string {
81
+ const lines: string[] = [];
82
+
83
+ lines.push(`# Index Status: ${indexStatus.indexName}`);
84
+ lines.push('');
85
+ lines.push(`- **Config**: ${indexStatus.configPath}`);
86
+ lines.push(`- **Database**: ${indexStatus.dbPath}`);
87
+ lines.push(`- **Health**: ${indexStatus.healthy ? '✓ OK' : '⚠ DEGRADED'}`);
88
+ lines.push('');
89
+
90
+ if (indexStatus.collections.length > 0) {
91
+ lines.push('## Collections');
92
+ lines.push('');
93
+ lines.push('| Name | Path | Docs | Chunks | Embedded |');
94
+ lines.push('|------|------|------|--------|----------|');
95
+ for (const c of indexStatus.collections) {
96
+ lines.push(
97
+ `| ${c.name} | ${c.path} | ${c.activeDocuments} | ${c.totalChunks} | ${c.embeddedChunks} |`
98
+ );
99
+ }
100
+ lines.push('');
101
+ }
102
+
103
+ lines.push('## Summary');
104
+ lines.push('');
105
+ lines.push(`- **Documents**: ${indexStatus.activeDocuments}`);
106
+ lines.push(`- **Chunks**: ${indexStatus.totalChunks}`);
107
+ lines.push(`- **Embedding backlog**: ${indexStatus.embeddingBacklog}`);
108
+ lines.push(`- **Recent errors**: ${indexStatus.recentErrors}`);
109
+
110
+ if (indexStatus.lastUpdatedAt) {
111
+ lines.push(`- **Last updated**: ${indexStatus.lastUpdatedAt}`);
112
+ }
113
+
114
+ return lines.join('\n');
115
+ }
116
+
117
+ /**
118
+ * Execute gno status command.
119
+ */
120
+ export async function status(
121
+ options: StatusOptions = {}
122
+ ): Promise<StatusResult> {
123
+ // Check if initialized
124
+ const initialized = await isInitialized(options.configPath);
125
+ if (!initialized) {
126
+ return { success: false, error: 'GNO not initialized. Run: gno init' };
127
+ }
128
+
129
+ // Load config
130
+ const configResult = await loadConfig(options.configPath);
131
+ if (!configResult.ok) {
132
+ return { success: false, error: configResult.error.message };
133
+ }
134
+ const config = configResult.value;
135
+
136
+ // Open database
137
+ const store = new SqliteAdapter();
138
+ const dbPath = getIndexDbPath();
139
+ const paths = getConfigPaths();
140
+
141
+ // Set configPath for status output
142
+ store.setConfigPath(paths.configFile);
143
+
144
+ const openResult = await store.open(dbPath, config.ftsTokenizer);
145
+ if (!openResult.ok) {
146
+ return { success: false, error: openResult.error.message };
147
+ }
148
+
149
+ try {
150
+ const statusResult = await store.getStatus();
151
+ if (!statusResult.ok) {
152
+ return { success: false, error: statusResult.error.message };
153
+ }
154
+
155
+ return { success: true, status: statusResult.value };
156
+ } finally {
157
+ await store.close();
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Format status result for output.
163
+ */
164
+ export function formatStatus(
165
+ result: StatusResult,
166
+ options: StatusOptions
167
+ ): string {
168
+ if (!result.success) {
169
+ return options.json
170
+ ? JSON.stringify({ error: { code: 'RUNTIME', message: result.error } })
171
+ : `Error: ${result.error}`;
172
+ }
173
+
174
+ if (options.json) {
175
+ // Transform to match CLI spec output format
176
+ const s = result.status;
177
+ return JSON.stringify(
178
+ {
179
+ indexName: s.indexName,
180
+ configPath: s.configPath,
181
+ dbPath: s.dbPath,
182
+ collections: s.collections.map((c) => ({
183
+ name: c.name,
184
+ path: c.path,
185
+ documentCount: c.activeDocuments,
186
+ chunkCount: c.totalChunks,
187
+ embeddedCount: c.embeddedChunks,
188
+ })),
189
+ totalDocuments: s.activeDocuments,
190
+ totalChunks: s.totalChunks,
191
+ embeddingBacklog: s.embeddingBacklog,
192
+ lastUpdated: s.lastUpdatedAt,
193
+ healthy: s.healthy,
194
+ },
195
+ null,
196
+ 2
197
+ );
198
+ }
199
+
200
+ if (options.md) {
201
+ return formatMarkdown(result.status);
202
+ }
203
+
204
+ return formatTerminal(result.status);
205
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * gno update command implementation.
3
+ * Sync files from disk into the index (ingestion without embedding).
4
+ *
5
+ * @module src/cli/commands/update
6
+ */
7
+
8
+ import { defaultSyncService, type SyncResult } from '../../ingestion';
9
+ import { formatSyncResultLines, initStore } from './shared';
10
+
11
+ /**
12
+ * Options for update command.
13
+ */
14
+ export interface UpdateOptions {
15
+ /** Override config path */
16
+ configPath?: string;
17
+ /** Run git pull in git repositories before scanning */
18
+ gitPull?: boolean;
19
+ /** Verbose output */
20
+ verbose?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Result of update command.
25
+ */
26
+ export type UpdateResult =
27
+ | { success: true; result: SyncResult }
28
+ | { success: false; error: string };
29
+
30
+ /**
31
+ * Execute gno update command.
32
+ */
33
+ export async function update(
34
+ options: UpdateOptions = {}
35
+ ): Promise<UpdateResult> {
36
+ const initResult = await initStore({ configPath: options.configPath });
37
+ if (!initResult.ok) {
38
+ return { success: false, error: initResult.error };
39
+ }
40
+
41
+ const { store, collections } = initResult;
42
+
43
+ try {
44
+ // Run sync service
45
+ const result = await defaultSyncService.syncAll(collections, store, {
46
+ gitPull: options.gitPull,
47
+ runUpdateCmd: true,
48
+ });
49
+
50
+ return { success: true, result };
51
+ } finally {
52
+ await store.close();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Format update result for output.
58
+ */
59
+ export function formatUpdate(
60
+ result: UpdateResult,
61
+ options: UpdateOptions
62
+ ): string {
63
+ if (!result.success) {
64
+ return `Error: ${result.error}`;
65
+ }
66
+
67
+ return formatSyncResultLines(result.result, options).join('\n');
68
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * gno vsearch command implementation.
3
+ * Vector semantic search over indexed documents.
4
+ *
5
+ * @module src/cli/commands/vsearch
6
+ */
7
+
8
+ import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
9
+ import { getActivePreset } from '../../llm/registry';
10
+ import type { SearchOptions, SearchResults } from '../../pipeline/types';
11
+ import {
12
+ searchVectorWithEmbedding,
13
+ type VectorSearchDeps,
14
+ } from '../../pipeline/vsearch';
15
+ import { createVectorIndexPort } from '../../store/vector';
16
+ import {
17
+ type FormatOptions,
18
+ formatSearchResults,
19
+ } from '../format/search-results';
20
+ import { initStore } from './shared';
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // Types
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ export type VsearchCommandOptions = SearchOptions & {
27
+ /** Override config path */
28
+ configPath?: string;
29
+ /** Override model URI */
30
+ model?: string;
31
+ /** Output as JSON */
32
+ json?: boolean;
33
+ /** Output as Markdown */
34
+ md?: boolean;
35
+ /** Output as CSV */
36
+ csv?: boolean;
37
+ /** Output as XML */
38
+ xml?: boolean;
39
+ /** Output files only */
40
+ files?: boolean;
41
+ };
42
+
43
+ export type VsearchResult =
44
+ | { success: true; data: SearchResults }
45
+ | { success: false; error: string };
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+ // Command Implementation
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Execute gno vsearch command.
53
+ */
54
+ export async function vsearch(
55
+ query: string,
56
+ options: VsearchCommandOptions = {}
57
+ ): Promise<VsearchResult> {
58
+ // Adjust default limit based on output format
59
+ const isStructured =
60
+ options.json || options.files || options.csv || options.xml;
61
+ const limit = options.limit ?? (isStructured ? 20 : 5);
62
+
63
+ const initResult = await initStore({
64
+ configPath: options.configPath,
65
+ collection: options.collection,
66
+ });
67
+
68
+ if (!initResult.ok) {
69
+ return { success: false, error: initResult.error };
70
+ }
71
+
72
+ const { store, config } = initResult;
73
+
74
+ try {
75
+ // Get model URI from preset
76
+ const preset = getActivePreset(config);
77
+ const modelUri = options.model ?? preset.embed;
78
+
79
+ // Create LLM adapter for embeddings
80
+ const llm = new LlmAdapter(config);
81
+ const embedResult = await llm.createEmbeddingPort(modelUri);
82
+ if (!embedResult.ok) {
83
+ return { success: false, error: embedResult.error.message };
84
+ }
85
+
86
+ const embedPort = embedResult.value;
87
+
88
+ try {
89
+ // Embed query (also determines dimensions - avoids double embed)
90
+ const queryEmbedResult = await embedPort.embed(query);
91
+ if (!queryEmbedResult.ok) {
92
+ return { success: false, error: queryEmbedResult.error.message };
93
+ }
94
+ const queryEmbedding = new Float32Array(queryEmbedResult.value);
95
+ const dimensions = queryEmbedding.length;
96
+
97
+ // Create vector index port
98
+ const db = store.getRawDb();
99
+ const vectorResult = await createVectorIndexPort(db, {
100
+ model: modelUri,
101
+ dimensions,
102
+ });
103
+
104
+ if (!vectorResult.ok) {
105
+ return { success: false, error: vectorResult.error.message };
106
+ }
107
+
108
+ const vectorIndex = vectorResult.value;
109
+
110
+ const deps: VectorSearchDeps = {
111
+ store,
112
+ vectorIndex,
113
+ embedPort,
114
+ config,
115
+ };
116
+
117
+ // Pass pre-computed embedding to avoid double-embed
118
+ const result = await searchVectorWithEmbedding(
119
+ deps,
120
+ query,
121
+ queryEmbedding,
122
+ { ...options, limit }
123
+ );
124
+
125
+ if (!result.ok) {
126
+ return { success: false, error: result.error.message };
127
+ }
128
+
129
+ return { success: true, data: result.value };
130
+ } finally {
131
+ await embedPort.dispose();
132
+ }
133
+ } finally {
134
+ await store.close();
135
+ }
136
+ }
137
+
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ // Formatter
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+
142
+ /**
143
+ * Get output format from options.
144
+ */
145
+ function getFormatType(
146
+ options: VsearchCommandOptions
147
+ ): FormatOptions['format'] {
148
+ if (options.json) {
149
+ return 'json';
150
+ }
151
+ if (options.files) {
152
+ return 'files';
153
+ }
154
+ if (options.csv) {
155
+ return 'csv';
156
+ }
157
+ if (options.md) {
158
+ return 'md';
159
+ }
160
+ if (options.xml) {
161
+ return 'xml';
162
+ }
163
+ return 'terminal';
164
+ }
165
+
166
+ /**
167
+ * Format vsearch result for output.
168
+ */
169
+ export function formatVsearch(
170
+ result: VsearchResult,
171
+ options: VsearchCommandOptions
172
+ ): string {
173
+ if (!result.success) {
174
+ return options.json
175
+ ? JSON.stringify({
176
+ error: { code: 'QUERY_FAILED', message: result.error },
177
+ })
178
+ : `Error: ${result.error}`;
179
+ }
180
+
181
+ const formatOpts: FormatOptions = {
182
+ format: getFormatType(options),
183
+ full: options.full,
184
+ lineNumbers: options.lineNumbers,
185
+ };
186
+
187
+ return formatSearchResults(result.data, formatOpts);
188
+ }