@gmickel/gno 2.10.0 → 2.10.1

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 CHANGED
@@ -139,7 +139,7 @@ See the [guide](docs/COMPILED-CONTEXT.md).
139
139
 
140
140
  <!-- public-truth:current-version -->
141
141
 
142
- > Current source version: **v2.10.0**. See [CHANGELOG.md](./CHANGELOG.md).
142
+ > Current source version: **v2.10.1**. See [CHANGELOG.md](./CHANGELOG.md).
143
143
 
144
144
  <!-- /public-truth -->
145
145
 
@@ -0,0 +1 @@
1
+ e42897b28d0fc0fca7d1d7aca6ab016255cc3b5bf20d38bcac51535499eb7f25 gno-browser-clipper-v2.10.1.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "2.10.0"
24
+ "version": "2.10.1"
25
25
  }
package/bunfig.toml CHANGED
@@ -2,6 +2,10 @@
2
2
  plugins = ["bun-plugin-tailwind"]
3
3
 
4
4
  [test]
5
- preload = ["./test/preload/happy-dom.ts", "./test/preload/env-guard.ts"]
5
+ preload = [
6
+ "./test/preload/temp-dir.ts",
7
+ "./test/preload/happy-dom.ts",
8
+ "./test/preload/env-guard.ts",
9
+ ]
6
10
  # Frozen evidence and local experiments are not the executable regression suite.
7
11
  pathIgnorePatterns = [".flow/artifacts/**", "notes/**"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "2.10.0",
3
+ "version": "2.10.1",
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
  "embeddings",
@@ -72,6 +72,7 @@
72
72
  "test:e2e:install": "bunx playwright install chromium",
73
73
  "test:package": "bun scripts/package-smoke.ts",
74
74
  "test:package:clipper": "bun scripts/package-smoke-clipper-cli.ts",
75
+ "test:tmp-check": "bun scripts/test-tmp-check.ts",
75
76
  "test:watch": "bun test --watch",
76
77
  "test:coverage": "bun test --coverage",
77
78
  "test:coverage:html": "bun test --coverage --html",
package/spec/cli.md CHANGED
@@ -242,8 +242,10 @@ the model with `id`, `model`, `dimensions`, `state` (`active`|`shadow`),
242
242
  Terminal output prints a `Vector partitions:` block
243
243
  unless there is exactly one healthy partition. `collections`, `totalDocuments`
244
244
  and `totalChunks` cover configured collections only: a collection removed from
245
- config (for example by `gno collection remove`) is not reported even while its
246
- rows remain in the index until the next `gno update`. `totalChunks` counts the
245
+ config is not reported even while its rows remain in the index until the next
246
+ command that syncs collections (`gno update`, `gno index`, `gno cleanup`). The
247
+ embedding backlog likewise counts chunks of configured collections only.
248
+ `gno collection remove` syncs at once, so no rows remain. `totalChunks` counts the
247
249
  distinct chunks of active documents; chunks left by deleted documents do not
248
250
  count. Per-collection chunk totals remain deduplicated by canonical chunk;
249
251
  embedded counts require matching current inputs for every active owner within
@@ -1097,12 +1099,12 @@ for `updateCmd`; without it the command is stopped after 10 minutes.
1097
1099
 
1098
1100
  ### gno collection remove
1099
1101
 
1100
- Remove a collection from the index.
1102
+ Remove a collection from the config and the index.
1101
1103
 
1102
1104
  **Synopsis:**
1103
1105
 
1104
1106
  ```bash
1105
- gno collection remove <name>
1107
+ gno collection remove <name> [--lock-wait <duration>] [--no-wait]
1106
1108
  ```
1107
1109
 
1108
1110
  **Arguments:**
@@ -1113,12 +1115,19 @@ gno collection remove <name>
1113
1115
  **Behavior:**
1114
1116
 
1115
1117
  - Removes collection from config
1116
- - Marks documents as inactive (does not delete DB rows until `cleanup`)
1118
+ - Syncs the index in the same config write: the collection row and its
1119
+ documents are deleted at once, so status, search and the embedding backlog
1120
+ stop counting them without another command
1121
+ - Content, chunks and vectors no longer referenced by any document stay until
1122
+ `gno cleanup`
1123
+ - Waits for the index write lease (`--lock-wait`, default 120s; `--no-wait`
1124
+ fails at once)
1117
1125
 
1118
1126
  **Exit Codes:**
1119
1127
 
1120
1128
  - 0: Success
1121
- - 1: Collection not found
1129
+ - 1: Collection not found or referenced by contexts
1130
+ - 2: Config save or index sync failure, or write lease busy
1122
1131
 
1123
1132
  ---
1124
1133
 
@@ -1129,7 +1138,7 @@ Rename a collection.
1129
1138
  **Synopsis:**
1130
1139
 
1131
1140
  ```bash
1132
- gno collection rename <old> <new>
1141
+ gno collection rename <old> <new> [--lock-wait <duration>] [--no-wait]
1133
1142
  ```
1134
1143
 
1135
1144
  **Arguments:**
@@ -1138,10 +1147,22 @@ gno collection rename <old> <new>
1138
1147
  | `<old>` | string | Current collection name |
1139
1148
  | `<new>` | string | New collection name |
1140
1149
 
1150
+ **Behavior:**
1151
+
1152
+ - Renames the collection in config and rewrites contexts scoped to it
1153
+ - Syncs the index in the same config write. Documents are keyed by collection
1154
+ name, so the old name's documents are deleted at once and the collection is
1155
+ empty until the next `gno update` (or `gno index`) indexes its files under
1156
+ the new name; content and vectors not yet removed by `gno cleanup` are reused
1157
+ - Prints `Run: gno update to index it under the new name`
1158
+ - Waits for the index write lease (`--lock-wait`, default 120s; `--no-wait`
1159
+ fails at once)
1160
+
1141
1161
  **Exit Codes:**
1142
1162
 
1143
1163
  - 0: Success
1144
- - 1: Old name not found or new name already exists
1164
+ - 1: Old name not found, new name already exists, or new name invalid
1165
+ - 2: Config save or index sync failure, or write lease busy
1145
1166
 
1146
1167
  ---
1147
1168
 
@@ -2944,12 +2965,13 @@ included, is refused. JSON output is
2944
2965
 
2945
2966
  ### gno cleanup
2946
2967
 
2947
- Remove orphaned content, chunks, and vectors not referenced by active documents.
2968
+ Remove documents of collections no longer in the config, then orphaned
2969
+ content, chunks, and vectors not referenced by active documents.
2948
2970
 
2949
2971
  **Synopsis:**
2950
2972
 
2951
2973
  ```bash
2952
- gno cleanup
2974
+ gno cleanup [--lock-wait <duration>] [--no-wait]
2953
2975
  ```
2954
2976
 
2955
2977
  **Exit Codes:**
package/spec/mcp.md CHANGED
@@ -2830,7 +2830,7 @@ Full index: sync files + generate embeddings (write-enabled). Runs as background
2830
2830
 
2831
2831
  ### gno_remove_collection
2832
2832
 
2833
- Remove a collection from config (write-enabled). Indexed data is retained.
2833
+ Remove a collection from config and delete its documents from the index (write-enabled). Content, chunks and vectors stay until `gno cleanup`; `indexedDataRetained` is always `true` for that reason.
2834
2834
 
2835
2835
  **Input Schema:**
2836
2836
 
@@ -28,12 +28,12 @@
28
28
  },
29
29
  "indexedDataRetained": {
30
30
  "type": "boolean",
31
- "description": "Indexed data retention flag",
31
+ "description": "Always true: the collection's documents are deleted, but content, chunks and vectors stay in the index until gno cleanup",
32
32
  "const": true
33
33
  },
34
34
  "note": {
35
35
  "type": "string",
36
- "description": "Explanation of retained data"
36
+ "description": "What was deleted and what stays until gno cleanup"
37
37
  }
38
38
  }
39
39
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * gno cleanup command implementation.
3
- * Remove orphaned content, chunks, vectors not referenced by active documents.
3
+ * Remove documents of collections no longer configured, then orphaned
4
+ * content, chunks and vectors not referenced by active documents.
4
5
  *
5
6
  * @module src/cli/commands/cleanup
6
7
  */
@@ -61,6 +62,13 @@ export async function cleanup(
61
62
  }
62
63
 
63
64
  try {
65
+ // Drop collections no longer in the config first (their documents follow
66
+ // by cascade), so their content, chunks and vectors count as orphans.
67
+ const syncResult = await store.syncCollections(config.collections);
68
+ if (!syncResult.ok) {
69
+ return { success: false, error: syncResult.error.message };
70
+ }
71
+
64
72
  const cleanupResult = await store.cleanupOrphans();
65
73
  if (!cleanupResult.ok) {
66
74
  return { success: false, error: cleanupResult.error.message };
@@ -3,43 +3,62 @@
3
3
  */
4
4
 
5
5
  import { removeCollection } from "../../../collection";
6
- import { applyConfigFileChange } from "../../../core/config-mutation";
6
+ import { applyConfigChange } from "../../../core/config-mutation";
7
7
  import { CliError } from "../../errors";
8
+ import { initStore } from "../shared";
8
9
 
9
10
  export async function collectionRemove(
10
11
  name: string,
11
- options: { configPath?: string } = {}
12
+ options: { configPath?: string; indexName?: string } = {}
12
13
  ): Promise<void> {
13
- const mutation = await applyConfigFileChange(
14
- { configPath: options.configPath },
15
- (config) => {
16
- const result = removeCollection(config, { name });
17
- return result.ok
18
- ? {
19
- ok: true as const,
20
- config: result.config,
21
- value: result.collection,
22
- }
23
- : {
24
- ok: false as const,
25
- error: result.message,
26
- code: result.code,
27
- };
14
+ // Open the index so the removal reaches the store in the same locked
15
+ // boundary: syncing collections deletes the collection row, and its
16
+ // documents follow by cascade. A config-only edit left them active (and
17
+ // counted in the embedding backlog) until some later command synced.
18
+ const initialized = await initStore({
19
+ configPath: options.configPath,
20
+ indexName: options.indexName,
21
+ syncConfig: false,
22
+ allowEmptyCollections: true,
23
+ });
24
+ if (!initialized.ok) {
25
+ throw new CliError("RUNTIME", initialized.error);
26
+ }
27
+
28
+ try {
29
+ const mutation = await applyConfigChange(
30
+ { store: initialized.store, configPath: initialized.actualConfigPath },
31
+ (config) => {
32
+ const result = removeCollection(config, { name });
33
+ return result.ok
34
+ ? {
35
+ ok: true as const,
36
+ config: result.config,
37
+ value: result.collection,
38
+ }
39
+ : {
40
+ ok: false as const,
41
+ error: result.message,
42
+ code: result.code,
43
+ };
44
+ }
45
+ );
46
+
47
+ if (!mutation.ok) {
48
+ // Map collection error codes to CLI error codes
49
+ const cliCode =
50
+ mutation.code === "VALIDATION" ||
51
+ mutation.code === "NOT_FOUND" ||
52
+ mutation.code === "HAS_REFERENCES"
53
+ ? "VALIDATION"
54
+ : "RUNTIME";
55
+ throw new CliError(cliCode, mutation.error);
28
56
  }
29
- );
30
57
 
31
- if (!mutation.ok) {
32
- // Map collection error codes to CLI error codes
33
- const cliCode =
34
- mutation.code === "VALIDATION" ||
35
- mutation.code === "NOT_FOUND" ||
36
- mutation.code === "HAS_REFERENCES"
37
- ? "VALIDATION"
38
- : "RUNTIME";
39
- throw new CliError(cliCode, mutation.error);
58
+ process.stdout.write(
59
+ `Collection "${mutation.value?.name}" removed successfully\n`
60
+ );
61
+ } finally {
62
+ await initialized.store.close();
40
63
  }
41
-
42
- process.stdout.write(
43
- `Collection "${mutation.value?.name}" removed successfully\n`
44
- );
45
64
  }
@@ -3,19 +3,51 @@
3
3
  */
4
4
 
5
5
  import { CollectionSchema, getCollectionFromScope } from "../../../config";
6
- import { applyConfigFileChange } from "../../../core/config-mutation";
6
+ import { applyConfigChange } from "../../../core/config-mutation";
7
7
  import { CliError } from "../../errors";
8
+ import { initStore, type InitStoreResult } from "../shared";
8
9
 
9
10
  export async function collectionRename(
10
11
  oldName: string,
11
12
  newName: string,
12
- options: { configPath?: string } = {}
13
+ options: { configPath?: string; indexName?: string } = {}
13
14
  ): Promise<void> {
14
15
  const oldCollectionName = oldName.toLowerCase();
15
16
  const newCollectionName = newName.toLowerCase();
16
17
 
17
- const mutation = await applyConfigFileChange(
18
- { configPath: options.configPath },
18
+ // Documents are keyed by collection name (URIs, FTS paths, links), so the
19
+ // index cannot rename them in place. Sync in the same locked boundary: the
20
+ // old name's documents go now instead of at some later sync, and the next
21
+ // update indexes the files under the new name.
22
+ const initialized = await initStore({
23
+ configPath: options.configPath,
24
+ indexName: options.indexName,
25
+ syncConfig: false,
26
+ allowEmptyCollections: true,
27
+ });
28
+ if (!initialized.ok) {
29
+ throw new CliError("RUNTIME", initialized.error);
30
+ }
31
+
32
+ try {
33
+ await renameAndSync(initialized, oldCollectionName, newCollectionName);
34
+ } finally {
35
+ await initialized.store.close();
36
+ }
37
+
38
+ process.stdout.write(
39
+ `Collection "${oldCollectionName}" renamed to "${newCollectionName}"\n` +
40
+ "Run: gno update to index it under the new name\n"
41
+ );
42
+ }
43
+
44
+ async function renameAndSync(
45
+ initialized: Extract<InitStoreResult, { ok: true }>,
46
+ oldCollectionName: string,
47
+ newCollectionName: string
48
+ ): Promise<void> {
49
+ const mutation = await applyConfigChange(
50
+ { store: initialized.store, configPath: initialized.actualConfigPath },
19
51
  (config) => {
20
52
  const collection = config.collections.find(
21
53
  (item) => item.name === oldCollectionName
@@ -70,8 +102,4 @@ export async function collectionRename(
70
102
  mutation.error
71
103
  );
72
104
  }
73
-
74
- process.stdout.write(
75
- `Collection "${oldCollectionName}" renamed to "${newCollectionName}"\n`
76
- );
77
105
  }
@@ -2870,23 +2870,59 @@ function wireManagementCommands(program: Command): void {
2870
2870
  });
2871
2871
  });
2872
2872
 
2873
- collectionCmd
2874
- .command("remove <name>")
2875
- .description("Remove a collection")
2876
- .action(async (name: string) => {
2877
- const { collectionRemove } = await import("./commands/collection");
2878
- await collectionRemove(name, { configPath: getGlobals().config });
2879
- });
2873
+ addWriteLeaseFlags(
2874
+ collectionCmd
2875
+ .command("remove <name>")
2876
+ .description("Remove a collection and its indexed documents")
2877
+ ).action(async (name: string, cmdOpts: Record<string, unknown>) => {
2878
+ const { collectionRemove } = await import("./commands/collection");
2879
+ const lease = parseWriteLeaseFlags(cmdOpts);
2880
+ const result = await withCliWriteLease(
2881
+ {
2882
+ indexName: getGlobals().index,
2883
+ lockWaitMs: lease.lockWaitMs,
2884
+ noWait: lease.noWait,
2885
+ },
2886
+ async () => {
2887
+ await collectionRemove(name, {
2888
+ configPath: getGlobals().config,
2889
+ indexName: getGlobals().index,
2890
+ });
2891
+ return { success: true as const };
2892
+ }
2893
+ );
2894
+ throwIfWriteLeaseBusy(result, false);
2895
+ });
2880
2896
 
2881
- collectionCmd
2882
- .command("rename <old> <new>")
2883
- .description("Rename a collection")
2884
- .action(async (oldName: string, newName: string) => {
2897
+ addWriteLeaseFlags(
2898
+ collectionCmd
2899
+ .command("rename <old> <new>")
2900
+ .description("Rename a collection (run gno update afterwards)")
2901
+ ).action(
2902
+ async (
2903
+ oldName: string,
2904
+ newName: string,
2905
+ cmdOpts: Record<string, unknown>
2906
+ ) => {
2885
2907
  const { collectionRename } = await import("./commands/collection");
2886
- await collectionRename(oldName, newName, {
2887
- configPath: getGlobals().config,
2888
- });
2889
- });
2908
+ const lease = parseWriteLeaseFlags(cmdOpts);
2909
+ const result = await withCliWriteLease(
2910
+ {
2911
+ indexName: getGlobals().index,
2912
+ lockWaitMs: lease.lockWaitMs,
2913
+ noWait: lease.noWait,
2914
+ },
2915
+ async () => {
2916
+ await collectionRename(oldName, newName, {
2917
+ configPath: getGlobals().config,
2918
+ indexName: getGlobals().index,
2919
+ });
2920
+ return { success: true as const };
2921
+ }
2922
+ );
2923
+ throwIfWriteLeaseBusy(result, false);
2924
+ }
2925
+ );
2890
2926
 
2891
2927
  addWriteLeaseFlags(
2892
2928
  collectionCmd
@@ -3620,7 +3656,11 @@ function wireManagementCommands(program: Command): void {
3620
3656
  lockWaitMs: lease.lockWaitMs,
3621
3657
  noWait: lease.noWait,
3622
3658
  },
3623
- () => cleanup({ indexName: getGlobals().index })
3659
+ () =>
3660
+ cleanup({
3661
+ configPath: getGlobals().config,
3662
+ indexName: getGlobals().index,
3663
+ })
3624
3664
  );
3625
3665
  throwIfWriteLeaseBusy(result, false);
3626
3666
 
@@ -21,6 +21,7 @@ interface RemoveCollectionResult {
21
21
  removed: true;
22
22
  collection: string;
23
23
  configUpdated: true;
24
+ /** Documents are deleted; content, chunks and vectors stay until cleanup. */
24
25
  indexedDataRetained: true;
25
26
  note: string;
26
27
  }
@@ -101,7 +102,7 @@ export function handleRemoveCollection(
101
102
  collection: name,
102
103
  configUpdated: true,
103
104
  indexedDataRetained: true,
104
- note: "Documents remain in index until next full reindex or manual cleanup",
105
+ note: "Collection removed from config and its documents deleted from the index. Content, chunks and vectors stay until gno cleanup.",
105
106
  };
106
107
 
107
108
  return result;
@@ -1574,9 +1574,8 @@ export async function handleImportPreview(
1574
1574
 
1575
1575
  /**
1576
1576
  * DELETE /api/collections/:name
1577
- * Remove a collection from config.
1578
- * Note: Does NOT remove indexed documents - they remain in DB until re-sync
1579
- * or manual cleanup. This preserves data for potential recovery.
1577
+ * Remove a collection from config. The config sync deletes the collection
1578
+ * row and its documents; content, chunks and vectors stay until cleanup.
1580
1579
  */
1581
1580
  export async function handleDeleteCollection(
1582
1581
  ctxHolder: ContextHolder,
@@ -1612,7 +1611,7 @@ export async function handleDeleteCollection(
1612
1611
  return jsonResponse({
1613
1612
  success: true,
1614
1613
  collection: name,
1615
- note: "Collection removed from config. Indexed documents remain in DB.",
1614
+ note: "Collection removed from config and its documents deleted from the index. Content, chunks and vectors stay until gno cleanup.",
1616
1615
  });
1617
1616
  }
1618
1617
 
@@ -6124,18 +6124,25 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
6124
6124
  )
6125
6125
  .get(configuredJson, configuredJson)?.count ?? 0;
6126
6126
 
6127
- // Embedding backlog: chunks from active docs without vectors
6128
- // Uses EXISTS to avoid duplicates when multiple docs share mirror_hash
6127
+ // Embedding backlog: chunks from active docs of configured collections
6128
+ // without vectors. DISTINCT avoids duplicates when docs share mirror_hash.
6129
6129
  const backlogRow = db
6130
6130
  .query<
6131
6131
  { count: number },
6132
- [string | null, string | null, string | null]
6132
+ [
6133
+ string | null,
6134
+ string | null,
6135
+ string | null,
6136
+ string | null,
6137
+ string | null,
6138
+ ]
6133
6139
  >(
6134
6140
  `
6135
6141
  WITH active_mirrors AS (
6136
6142
  SELECT DISTINCT mirror_hash
6137
6143
  FROM documents
6138
6144
  WHERE active = 1 AND mirror_hash IS NOT NULL
6145
+ AND (? IS NULL OR collection IN (SELECT value FROM json_each(?)))
6139
6146
  ),
6140
6147
  matching_vectors AS (
6141
6148
  SELECT mirror_hash, seq, MAX(embedded_at) AS embedded_at
@@ -6155,7 +6162,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
6155
6162
  WHERE mv.mirror_hash IS NULL
6156
6163
  `
6157
6164
  )
6158
- .get(embedModel, embedModel, embedFingerprint);
6165
+ .get(
6166
+ configuredJson,
6167
+ configuredJson,
6168
+ embedModel,
6169
+ embedModel,
6170
+ embedFingerprint
6171
+ );
6159
6172
 
6160
6173
  // Recent errors (last 24h)
6161
6174
  const recentErrorsRow = db
@@ -1 +0,0 @@
1
- 56174f38783259cef05d81ae53adceebc8467799b735c2f3026b928f7070ae76 gno-browser-clipper-v2.10.0.zip