@gmickel/gno 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "search",
@@ -33,7 +33,7 @@
33
33
  "vendor"
34
34
  ],
35
35
  "engines": {
36
- "bun": ">=1.0.0"
36
+ "bun": ">=1.3.0"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
@@ -113,11 +113,11 @@
113
113
  "evalite": "^1.0.0-beta.15",
114
114
  "exceljs": "^4.4.0",
115
115
  "lefthook": "^2.0.13",
116
- "oxlint-tsgolint": "^0.10.0",
116
+ "oxlint-tsgolint": "^0.10.1",
117
117
  "pdf-lib": "^1.17.1",
118
118
  "pptxgenjs": "^4.0.1",
119
119
  "tailwindcss": "^4.1.18",
120
- "ultracite": "6.5.0"
120
+ "ultracite": "7.0.4"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "typescript": "^5"
@@ -2,11 +2,8 @@
2
2
  * gno collection add - Add a new collection
3
3
  */
4
4
 
5
+ import { addCollection } from '../../../collection';
5
6
  import {
6
- type Collection,
7
- CollectionSchema,
8
- DEFAULT_EXCLUDES,
9
- DEFAULT_PATTERN,
10
7
  loadConfig,
11
8
  pathExists,
12
9
  saveConfig,
@@ -31,83 +28,45 @@ export async function collectionAdd(
31
28
  throw new CliError('VALIDATION', '--name is required');
32
29
  }
33
30
 
34
- const collectionName = options.name.toLowerCase();
35
-
36
- // Expand and validate path
31
+ // Validate path exists BEFORE loading config (user-friendly error ordering)
37
32
  const absolutePath = toAbsolutePath(path);
38
-
39
- // Check if path exists
40
33
  const exists = await pathExists(absolutePath);
41
34
  if (!exists) {
42
35
  throw new CliError('VALIDATION', `Path does not exist: ${absolutePath}`);
43
36
  }
44
37
 
45
38
  // Load config
46
- const result = await loadConfig();
47
- if (!result.ok) {
39
+ const configResult = await loadConfig();
40
+ if (!configResult.ok) {
48
41
  throw new CliError(
49
42
  'RUNTIME',
50
- `Failed to load config: ${result.error.message}`
43
+ `Failed to load config: ${configResult.error.message}`
51
44
  );
52
45
  }
53
46
 
54
- const config = result.value;
55
-
56
- // Check for duplicate name
57
- const existing = config.collections.find((c) => c.name === collectionName);
58
- if (existing) {
59
- throw new CliError(
60
- 'VALIDATION',
61
- `Collection "${collectionName}" already exists`
62
- );
63
- }
64
-
65
- // Parse options - filter empty, dedupe
66
- const includeList = options.include
67
- ? [
68
- ...new Set(
69
- options.include
70
- .split(',')
71
- .map((s) => s.trim())
72
- .filter(Boolean)
73
- ),
74
- ]
75
- : [];
76
- const excludeList = options.exclude
77
- ? [
78
- ...new Set(
79
- options.exclude
80
- .split(',')
81
- .map((s) => s.trim())
82
- .filter(Boolean)
83
- ),
84
- ]
85
- : [...DEFAULT_EXCLUDES];
86
-
87
- // Build collection
88
- const collection: Collection = {
89
- name: collectionName,
90
- path: absolutePath,
91
- pattern: options.pattern ?? DEFAULT_PATTERN,
92
- include: includeList,
93
- exclude: excludeList,
47
+ // Add collection using shared module
48
+ const result = await addCollection(configResult.value, {
49
+ path,
50
+ name: options.name,
51
+ pattern: options.pattern,
52
+ include: options.include,
53
+ exclude: options.exclude,
94
54
  updateCmd: options.update,
95
- };
55
+ });
96
56
 
97
- // Validate collection
98
- const validation = CollectionSchema.safeParse(collection);
99
- if (!validation.success) {
100
- throw new CliError(
101
- 'VALIDATION',
102
- `Invalid collection: ${validation.error.issues[0]?.message ?? 'unknown error'}`
103
- );
57
+ if (!result.ok) {
58
+ // Map collection error codes to CLI error codes
59
+ const cliCode =
60
+ result.code === 'VALIDATION' ||
61
+ result.code === 'PATH_NOT_FOUND' ||
62
+ result.code === 'DUPLICATE'
63
+ ? 'VALIDATION'
64
+ : 'RUNTIME';
65
+ throw new CliError(cliCode, result.message);
104
66
  }
105
67
 
106
- // Add to config
107
- config.collections.push(validation.data);
108
-
109
68
  // Save config
110
- const saveResult = await saveConfig(config);
69
+ const saveResult = await saveConfig(result.config);
111
70
  if (!saveResult.ok) {
112
71
  throw new CliError(
113
72
  'RUNTIME',
@@ -115,6 +74,8 @@ export async function collectionAdd(
115
74
  );
116
75
  }
117
76
 
118
- process.stdout.write(`Collection "${collectionName}" added successfully\n`);
119
- process.stdout.write(`Path: ${absolutePath}\n`);
77
+ process.stdout.write(
78
+ `Collection "${result.collection.name}" added successfully\n`
79
+ );
80
+ process.stdout.write(`Path: ${result.collection.path}\n`);
120
81
  }
@@ -2,57 +2,36 @@
2
2
  * gno collection remove - Remove a collection
3
3
  */
4
4
 
5
- import {
6
- getCollectionFromScope,
7
- loadConfig,
8
- saveConfig,
9
- } from '../../../config';
5
+ import { removeCollection } from '../../../collection';
6
+ import { loadConfig, saveConfig } from '../../../config';
10
7
  import { CliError } from '../../errors';
11
8
 
12
9
  export async function collectionRemove(name: string): Promise<void> {
13
- const collectionName = name.toLowerCase();
14
-
15
10
  // Load config
16
- const result = await loadConfig();
17
- if (!result.ok) {
11
+ const configResult = await loadConfig();
12
+ if (!configResult.ok) {
18
13
  throw new CliError(
19
14
  'RUNTIME',
20
- `Failed to load config: ${result.error.message}`
15
+ `Failed to load config: ${configResult.error.message}`
21
16
  );
22
17
  }
23
18
 
24
- const config = result.value;
25
-
26
- // Find collection
27
- const collectionIndex = config.collections.findIndex(
28
- (c) => c.name === collectionName
29
- );
30
- if (collectionIndex === -1) {
31
- throw new CliError(
32
- 'VALIDATION',
33
- `Collection "${collectionName}" not found`
34
- );
35
- }
19
+ // Remove collection using shared module
20
+ const result = removeCollection(configResult.value, { name });
36
21
 
37
- // Check if any contexts reference this collection
38
- const referencingContexts = config.contexts.filter((ctx) => {
39
- const collFromScope = getCollectionFromScope(ctx.scopeKey);
40
- return collFromScope === collectionName;
41
- });
42
-
43
- if (referencingContexts.length > 0) {
44
- const scopes = referencingContexts.map((ctx) => ctx.scopeKey).join(', ');
45
- throw new CliError(
46
- 'VALIDATION',
47
- `Collection "${collectionName}" is referenced by contexts: ${scopes}. Remove the contexts first or rename the collection.`
48
- );
22
+ if (!result.ok) {
23
+ // Map collection error codes to CLI error codes
24
+ const cliCode =
25
+ result.code === 'VALIDATION' ||
26
+ result.code === 'NOT_FOUND' ||
27
+ result.code === 'HAS_REFERENCES'
28
+ ? 'VALIDATION'
29
+ : 'RUNTIME';
30
+ throw new CliError(cliCode, result.message);
49
31
  }
50
32
 
51
- // Remove collection
52
- config.collections.splice(collectionIndex, 1);
53
-
54
33
  // Save config
55
- const saveResult = await saveConfig(config);
34
+ const saveResult = await saveConfig(result.config);
56
35
  if (!saveResult.ok) {
57
36
  throw new CliError(
58
37
  'RUNTIME',
@@ -60,5 +39,7 @@ export async function collectionRemove(name: string): Promise<void> {
60
39
  );
61
40
  }
62
41
 
63
- process.stdout.write(`Collection "${collectionName}" removed successfully\n`);
42
+ process.stdout.write(
43
+ `Collection "${result.collection.name}" removed successfully\n`
44
+ );
64
45
  }
@@ -7,7 +7,12 @@
7
7
 
8
8
  import type { Database } from 'bun:sqlite';
9
9
  import { getIndexDbPath } from '../../app/constants';
10
- import { getConfigPaths, isInitialized, loadConfig } from '../../config';
10
+ import {
11
+ type Config,
12
+ getConfigPaths,
13
+ isInitialized,
14
+ loadConfig,
15
+ } from '../../config';
11
16
  import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
12
17
  import { resolveDownloadPolicy } from '../../llm/policy';
13
18
  import { getActivePreset } from '../../llm/registry';
@@ -197,35 +202,36 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
197
202
  }
198
203
 
199
204
  // ─────────────────────────────────────────────────────────────────────────────
200
- // Main Command
205
+ // Helpers
201
206
  // ─────────────────────────────────────────────────────────────────────────────
202
207
 
208
+ interface EmbedContext {
209
+ config: Config;
210
+ modelUri: string;
211
+ store: SqliteAdapter;
212
+ }
213
+
203
214
  /**
204
- * Execute gno embed command.
215
+ * Initialize embed context: check init, load config, open store.
205
216
  */
206
- export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
207
- const batchSize = options.batchSize ?? 32;
208
- const force = options.force ?? false;
209
- const dryRun = options.dryRun ?? false;
210
-
211
- // Check initialization
212
- const initialized = await isInitialized(options.configPath);
217
+ async function initEmbedContext(
218
+ configPath?: string,
219
+ model?: string
220
+ ): Promise<({ ok: true } & EmbedContext) | { ok: false; error: string }> {
221
+ const initialized = await isInitialized(configPath);
213
222
  if (!initialized) {
214
- return { success: false, error: 'GNO not initialized. Run: gno init' };
223
+ return { ok: false, error: 'GNO not initialized. Run: gno init' };
215
224
  }
216
225
 
217
- // Load config
218
- const configResult = await loadConfig(options.configPath);
226
+ const configResult = await loadConfig(configPath);
219
227
  if (!configResult.ok) {
220
- return { success: false, error: configResult.error.message };
228
+ return { ok: false, error: configResult.error.message };
221
229
  }
222
230
  const config = configResult.value;
223
231
 
224
- // Get model URI
225
232
  const preset = getActivePreset(config);
226
- const modelUri = options.model ?? preset.embed;
233
+ const modelUri = model ?? preset.embed;
227
234
 
228
- // Open store
229
235
  const store = new SqliteAdapter();
230
236
  const dbPath = getIndexDbPath();
231
237
  const paths = getConfigPaths();
@@ -233,8 +239,30 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
233
239
 
234
240
  const openResult = await store.open(dbPath, config.ftsTokenizer);
235
241
  if (!openResult.ok) {
236
- return { success: false, error: openResult.error.message };
242
+ return { ok: false, error: openResult.error.message };
243
+ }
244
+
245
+ return { ok: true, config, modelUri, store };
246
+ }
247
+
248
+ // ─────────────────────────────────────────────────────────────────────────────
249
+ // Main Command
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+
252
+ /**
253
+ * Execute gno embed command.
254
+ */
255
+ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
256
+ const batchSize = options.batchSize ?? 32;
257
+ const force = options.force ?? false;
258
+ const dryRun = options.dryRun ?? false;
259
+
260
+ // Initialize config and store
261
+ const initResult = await initEmbedContext(options.configPath, options.model);
262
+ if (!initResult.ok) {
263
+ return { success: false, error: initResult.error };
237
264
  }
265
+ const { config, modelUri, store } = initResult;
238
266
 
239
267
  // Get raw DB for vector ops (SqliteAdapter always implements SqliteDbProvider)
240
268
  const db = store.getRawDb();
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Add collection core logic.
3
+ * Pure function that mutates config - caller handles I/O.
4
+ *
5
+ * @module src/collection/add
6
+ */
7
+
8
+ import {
9
+ type Collection,
10
+ CollectionSchema,
11
+ DEFAULT_EXCLUDES,
12
+ DEFAULT_PATTERN,
13
+ pathExists,
14
+ toAbsolutePath,
15
+ } from '../config';
16
+ import type { Config } from '../config/types';
17
+ import type { AddCollectionInput, CollectionResult } from './types';
18
+
19
+ /**
20
+ * Parse comma-separated string or array into deduplicated array.
21
+ */
22
+ function parseList(input: string[] | string | undefined): string[] {
23
+ if (!input) {
24
+ return [];
25
+ }
26
+ if (Array.isArray(input)) {
27
+ return [...new Set(input.map((s) => s.trim()).filter(Boolean))];
28
+ }
29
+ return [
30
+ ...new Set(
31
+ input
32
+ .split(',')
33
+ .map((s) => s.trim())
34
+ .filter(Boolean)
35
+ ),
36
+ ];
37
+ }
38
+
39
+ /**
40
+ * Add a collection to config.
41
+ *
42
+ * @param config - Current config (not mutated)
43
+ * @param input - Collection input
44
+ * @returns New config with collection added, or error
45
+ */
46
+ export async function addCollection(
47
+ config: Config,
48
+ input: AddCollectionInput
49
+ ): Promise<CollectionResult> {
50
+ const collectionName = input.name.toLowerCase();
51
+
52
+ // Expand and validate path
53
+ const absolutePath = toAbsolutePath(input.path);
54
+
55
+ // Check if path exists
56
+ const exists = await pathExists(absolutePath);
57
+ if (!exists) {
58
+ return {
59
+ ok: false,
60
+ code: 'PATH_NOT_FOUND',
61
+ message: `Path does not exist: ${absolutePath}`,
62
+ };
63
+ }
64
+
65
+ // Check for duplicate name
66
+ const existing = config.collections.find((c) => c.name === collectionName);
67
+ if (existing) {
68
+ return {
69
+ ok: false,
70
+ code: 'DUPLICATE',
71
+ message: `Collection "${collectionName}" already exists`,
72
+ };
73
+ }
74
+
75
+ // Parse include/exclude lists
76
+ const includeList = parseList(input.include);
77
+ const excludeList =
78
+ parseList(input.exclude).length > 0
79
+ ? parseList(input.exclude)
80
+ : [...DEFAULT_EXCLUDES];
81
+
82
+ // Build collection
83
+ const collection: Collection = {
84
+ name: collectionName,
85
+ path: absolutePath,
86
+ pattern: input.pattern ?? DEFAULT_PATTERN,
87
+ include: includeList,
88
+ exclude: excludeList,
89
+ updateCmd: input.updateCmd,
90
+ };
91
+
92
+ // Validate collection
93
+ const validation = CollectionSchema.safeParse(collection);
94
+ if (!validation.success) {
95
+ return {
96
+ ok: false,
97
+ code: 'VALIDATION',
98
+ message: `Invalid collection: ${validation.error.issues[0]?.message ?? 'unknown error'}`,
99
+ };
100
+ }
101
+
102
+ // Create new config with collection added
103
+ const newConfig: Config = {
104
+ ...config,
105
+ collections: [...config.collections, validation.data],
106
+ };
107
+
108
+ return {
109
+ ok: true,
110
+ config: newConfig,
111
+ collection: validation.data,
112
+ };
113
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Collection CRUD operations.
3
+ * Pure functions that mutate config - caller handles I/O.
4
+ *
5
+ * @module src/collection
6
+ */
7
+
8
+ export { addCollection } from './add';
9
+ export { removeCollection } from './remove';
10
+ export type {
11
+ AddCollectionInput,
12
+ CollectionError,
13
+ CollectionResult,
14
+ CollectionSuccess,
15
+ RemoveCollectionInput,
16
+ RenameCollectionInput,
17
+ } from './types';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Remove collection core logic.
3
+ * Pure function that mutates config - caller handles I/O.
4
+ *
5
+ * @module src/collection/remove
6
+ */
7
+
8
+ import { getCollectionFromScope } from '../config';
9
+ import type { Config } from '../config/types';
10
+ import type { CollectionResult, RemoveCollectionInput } from './types';
11
+
12
+ /**
13
+ * Remove a collection from config.
14
+ *
15
+ * @param config - Current config (not mutated)
16
+ * @param input - Collection name to remove
17
+ * @returns New config with collection removed, or error
18
+ */
19
+ export function removeCollection(
20
+ config: Config,
21
+ input: RemoveCollectionInput
22
+ ): CollectionResult {
23
+ const collectionName = input.name.toLowerCase();
24
+
25
+ // Find collection
26
+ const collection = config.collections.find((c) => c.name === collectionName);
27
+ if (!collection) {
28
+ return {
29
+ ok: false,
30
+ code: 'NOT_FOUND',
31
+ message: `Collection "${collectionName}" not found`,
32
+ };
33
+ }
34
+
35
+ // Check if any contexts reference this collection
36
+ const referencingContexts = (config.contexts ?? []).filter((ctx) => {
37
+ const collFromScope = getCollectionFromScope(ctx.scopeKey);
38
+ return collFromScope === collectionName;
39
+ });
40
+
41
+ if (referencingContexts.length > 0) {
42
+ const scopes = referencingContexts.map((ctx) => ctx.scopeKey).join(', ');
43
+ return {
44
+ ok: false,
45
+ code: 'HAS_REFERENCES',
46
+ message: `Collection "${collectionName}" is referenced by contexts: ${scopes}. Remove the contexts first.`,
47
+ };
48
+ }
49
+
50
+ // Create new config with collection removed (filter instead of splice)
51
+ const newCollections = config.collections.filter(
52
+ (c) => c.name !== collectionName
53
+ );
54
+
55
+ const newConfig: Config = {
56
+ ...config,
57
+ collections: newCollections,
58
+ };
59
+
60
+ return {
61
+ ok: true,
62
+ config: newConfig,
63
+ collection,
64
+ };
65
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Types for collection CRUD operations.
3
+ *
4
+ * @module src/collection/types
5
+ */
6
+
7
+ import type { Collection, Config } from '../config/types';
8
+
9
+ /**
10
+ * Input for adding a collection.
11
+ */
12
+ export interface AddCollectionInput {
13
+ /** Absolute path to the folder */
14
+ path: string;
15
+ /** Collection name (will be lowercased) */
16
+ name: string;
17
+ /** File pattern (default: DEFAULT_PATTERN) */
18
+ pattern?: string;
19
+ /** Include patterns (comma-separated or array) */
20
+ include?: string[] | string;
21
+ /** Exclude patterns (comma-separated or array) */
22
+ exclude?: string[] | string;
23
+ /** Update command to run before sync */
24
+ updateCmd?: string;
25
+ }
26
+
27
+ /**
28
+ * Input for removing a collection.
29
+ */
30
+ export interface RemoveCollectionInput {
31
+ /** Collection name (case-insensitive) */
32
+ name: string;
33
+ }
34
+
35
+ /**
36
+ * Input for renaming a collection.
37
+ */
38
+ export interface RenameCollectionInput {
39
+ /** Current collection name (case-insensitive) */
40
+ oldName: string;
41
+ /** New collection name (case-insensitive) */
42
+ newName: string;
43
+ }
44
+
45
+ /**
46
+ * Successful collection operation result.
47
+ */
48
+ export interface CollectionSuccess<T = Config> {
49
+ ok: true;
50
+ config: T;
51
+ collection: Collection;
52
+ }
53
+
54
+ /**
55
+ * Failed collection operation result.
56
+ */
57
+ export interface CollectionError {
58
+ ok: false;
59
+ code:
60
+ | 'VALIDATION'
61
+ | 'NOT_FOUND'
62
+ | 'DUPLICATE'
63
+ | 'PATH_NOT_FOUND'
64
+ | 'HAS_REFERENCES';
65
+ message: string;
66
+ }
67
+
68
+ export type CollectionResult<T = Config> =
69
+ | CollectionSuccess<T>
70
+ | CollectionError;