@gmickel/gno 1.12.4 → 1.13.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 +57 -30
- package/assets/skill/SKILL.md +5 -0
- package/assets/skill/cli-reference.md +16 -6
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +2 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +313 -53
- package/src/store/types.ts +118 -0
package/README.md
CHANGED
|
@@ -92,9 +92,13 @@ gno daemon --detach # headless continuous indexing (background; --status / --st
|
|
|
92
92
|
|
|
93
93
|
## What's New
|
|
94
94
|
|
|
95
|
-
> Latest release: [
|
|
95
|
+
> Latest release: see [CHANGELOG.md](./CHANGELOG.md)
|
|
96
96
|
> Full release history: [CHANGELOG.md](./CHANGELOG.md)
|
|
97
97
|
|
|
98
|
+
- **Retrieval-proven activation**: `gno status`, `gno doctor`, REST, and the
|
|
99
|
+
Web/Desktop dashboard now share a per-folder lexical retrieval proof. Local
|
|
100
|
+
semantic readiness remains independent, and installed MCP targets can run an
|
|
101
|
+
explicit read-only retrieval smoke from Connectors.
|
|
98
102
|
- **Second-brain capture**: `gno capture`, REST `/api/capture`, SDK
|
|
99
103
|
`client.capture()`, MCP `gno_capture`, and Web UI Quick Capture write
|
|
100
104
|
provenance-rich notes from text, stdin, or files, including typed presets for
|
|
@@ -209,12 +213,18 @@ bun install -g @gmickel/gno
|
|
|
209
213
|
brew install sqlite3
|
|
210
214
|
```
|
|
211
215
|
|
|
212
|
-
Verify
|
|
216
|
+
Verify the local installation and corpus-derived lexical retrieval:
|
|
213
217
|
|
|
214
218
|
```bash
|
|
215
219
|
gno doctor
|
|
220
|
+
gno status --json
|
|
216
221
|
```
|
|
217
222
|
|
|
223
|
+
`gno status` is passive with respect to models and connectors and exits 0 even
|
|
224
|
+
when its structured activation state is degraded. `gno doctor` exits 2 when any
|
|
225
|
+
configured folder fails the lexical proof; semantic models may still be pending
|
|
226
|
+
without blocking BM25 search.
|
|
227
|
+
|
|
218
228
|
**Windows**: current validated target is `windows-x64`, with a packaged
|
|
219
229
|
desktop beta zip now published on GitHub Releases. See
|
|
220
230
|
[docs/WINDOWS.md](./docs/WINDOWS.md) for support scope and validation notes.
|
|
@@ -248,9 +258,16 @@ gno mcp install --target codex # OpenAI Codex CLI
|
|
|
248
258
|
gno mcp install --target opencode # OpenCode
|
|
249
259
|
gno mcp install --target amp # Amp
|
|
250
260
|
gno mcp install --target lmstudio # LM Studio
|
|
251
|
-
gno mcp install --target librechat
|
|
261
|
+
gno mcp install --target librechat --scope project # LibreChat
|
|
252
262
|
```
|
|
253
263
|
|
|
264
|
+
Each install records an absolute Bun/package entrypoint plus the active index,
|
|
265
|
+
config, data directory, and model cache, so desktop clients open the same GNO
|
|
266
|
+
workspace without relying on shell `PATH` or `GNO_*` inheritance. Inspect the
|
|
267
|
+
exact command, arguments, and workspace values with
|
|
268
|
+
`gno mcp install --dry-run --json`. If GNO is already configured in that
|
|
269
|
+
target, add `--force` to preview the replacement without writing it.
|
|
270
|
+
|
|
254
271
|
Check status: `gno mcp status`
|
|
255
272
|
|
|
256
273
|
#### Skills (Claude Code, Codex, OpenCode, OpenClaw)
|
|
@@ -340,6 +357,7 @@ import { createGnoClient } from "@gmickel/gno";
|
|
|
340
357
|
|
|
341
358
|
const client = await createGnoClient({
|
|
342
359
|
configPath: "/Users/me/.config/gno/index.yml",
|
|
360
|
+
indexName: "research",
|
|
343
361
|
});
|
|
344
362
|
|
|
345
363
|
// Fast exact search
|
|
@@ -372,7 +390,7 @@ await client.close();
|
|
|
372
390
|
|
|
373
391
|
Core SDK surface:
|
|
374
392
|
|
|
375
|
-
- `createGnoClient({ config | configPath, dbPath? })`
|
|
393
|
+
- `createGnoClient({ config | configPath, dbPath?, indexName? })`
|
|
376
394
|
- `search`, `vsearch`, `query`, `ask`
|
|
377
395
|
- `get`, `multiGet`, `list`, `status`
|
|
378
396
|
- `update`, `embed`, `index`
|
|
@@ -556,7 +574,11 @@ Open `http://localhost:3000` to:
|
|
|
556
574
|
- **Same capture contract everywhere**: CLI, MCP `gno_capture`, REST `/api/capture`, SDK `client.capture()`, and Web UI Quick Capture return the same provenance receipt shape
|
|
557
575
|
- **Ask**: AI-powered Q&A with citations
|
|
558
576
|
- **Manage Collections**: Add, remove, and re-index collections
|
|
559
|
-
- **
|
|
577
|
+
- **Verify retrieval**: See each folder's lexical proof, exact failed stage,
|
|
578
|
+
and remediation without waiting for semantic models
|
|
579
|
+
- **Connect agents**: Install core Skill/MCP integrations; explicitly verify
|
|
580
|
+
configured MCP retrieval without changing client config. Skill installation
|
|
581
|
+
is visible, but client runtime execution cannot be proven automatically
|
|
560
582
|
- **Manage files safely**: Rename, reveal, or move editable files to Trash with explicit index-vs-disk semantics
|
|
561
583
|
- **Refactor files safely**: Move, duplicate, and organize editable notes with reference warnings
|
|
562
584
|
- **Switch presets**: Change models live without restart
|
|
@@ -697,32 +719,37 @@ curl -X POST http://localhost:3000/api/ask \
|
|
|
697
719
|
|
|
698
720
|
# Index status
|
|
699
721
|
curl http://localhost:3000/api/status
|
|
700
|
-
```
|
|
701
722
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
|
707
|
-
|
|
|
708
|
-
| `/api/
|
|
709
|
-
| `/api/
|
|
710
|
-
| `/api/
|
|
711
|
-
| `/api/docs
|
|
712
|
-
| `/api/docs
|
|
713
|
-
| `/api/docs/:id
|
|
714
|
-
| `/api/
|
|
715
|
-
| `/api/
|
|
716
|
-
| `/api/
|
|
717
|
-
| `/api/
|
|
718
|
-
| `/api/
|
|
719
|
-
| `/api/
|
|
720
|
-
| `/api/
|
|
721
|
-
| `/api/
|
|
722
|
-
| `/api/
|
|
723
|
-
| `/api/
|
|
724
|
-
| `/api/
|
|
725
|
-
| `/api/
|
|
723
|
+
# Process liveness only
|
|
724
|
+
curl http://localhost:3000/api/health
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
| Endpoint | Method | Description |
|
|
728
|
+
| :---------------------------- | :----- | :--------------------------- |
|
|
729
|
+
| `/api/query` | POST | Hybrid search (recommended) |
|
|
730
|
+
| `/api/search` | POST | BM25 keyword search |
|
|
731
|
+
| `/api/ask` | POST | AI-powered Q&A |
|
|
732
|
+
| `/api/docs` | GET | List documents |
|
|
733
|
+
| `/api/docs` | POST | Create document |
|
|
734
|
+
| `/api/docs/:id` | PUT | Update document content |
|
|
735
|
+
| `/api/docs/:id/move` | POST | Move editable document |
|
|
736
|
+
| `/api/docs/:id/duplicate` | POST | Duplicate editable document |
|
|
737
|
+
| `/api/docs/:id/refactor-plan` | POST | Preview file-op warnings |
|
|
738
|
+
| `/api/docs/:id/deactivate` | POST | Remove from index |
|
|
739
|
+
| `/api/doc` | GET | Get document content |
|
|
740
|
+
| `/api/doc/:id/sections` | GET | Get document sections |
|
|
741
|
+
| `/api/collections` | POST | Add collection |
|
|
742
|
+
| `/api/collections/:name` | DELETE | Remove collection |
|
|
743
|
+
| `/api/folders` | POST | Create folder |
|
|
744
|
+
| `/api/sync` | POST | Trigger re-index |
|
|
745
|
+
| `/api/status` | GET | Index and activation state |
|
|
746
|
+
| `/api/health` | GET | Process liveness only |
|
|
747
|
+
| `/api/connectors/verify` | POST | Explicit read-only MCP proof |
|
|
748
|
+
| `/api/note-presets` | GET | List note presets |
|
|
749
|
+
| `/api/presets` | GET | List model presets |
|
|
750
|
+
| `/api/presets` | POST | Switch preset |
|
|
751
|
+
| `/api/models/pull` | POST | Download models |
|
|
752
|
+
| `/api/models/status` | GET | Download progress |
|
|
726
753
|
|
|
727
754
|
No authentication. No rate limits. Build custom tools, automate workflows, integrate with any language.
|
|
728
755
|
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -226,6 +226,11 @@ gno graph --from gno://notes/a.md --to gno://notes/b.md
|
|
|
226
226
|
--no-pager Disable paging
|
|
227
227
|
```
|
|
228
228
|
|
|
229
|
+
Index names follow the CLI filesystem-identity contract: 1–64 UTF-16 code
|
|
230
|
+
units, letter/number first, no trailing space or `.`, no `..`, separators, or
|
|
231
|
+
platform-invalid punctuation. NFC/case-equivalent names share one identity.
|
|
232
|
+
See `docs/CLI.md` under Global Options for the complete byte limits.
|
|
233
|
+
|
|
229
234
|
Non-default index search results may include `?index=<name>` on `gno://` URIs.
|
|
230
235
|
Keep that query string when passing the URI to `gno get`, SDK `get()`, MCP
|
|
231
236
|
`gno_get`, or an MCP resource read: it selects the named database. Batch reads
|
|
@@ -17,6 +17,11 @@ All commands accept:
|
|
|
17
17
|
| `--no-pager` | Disable automatic paging |
|
|
18
18
|
| `--offline` | Use cached models only |
|
|
19
19
|
|
|
20
|
+
Index names use 1–64 UTF-16 code units, start with a letter or number, and
|
|
21
|
+
reject trailing space/dot, `..`, separators, controls, and platform-invalid
|
|
22
|
+
punctuation. NFC/case-equivalent spellings share one identity. See
|
|
23
|
+
`docs/CLI.md` under Global Options for the complete canonical-byte contract.
|
|
24
|
+
|
|
20
25
|
## Initialization
|
|
21
26
|
|
|
22
27
|
### gno init
|
|
@@ -523,12 +528,17 @@ Install GNO as MCP server in client configurations.
|
|
|
523
528
|
gno mcp install [options]
|
|
524
529
|
```
|
|
525
530
|
|
|
526
|
-
| Option | Default | Description
|
|
527
|
-
| -------------- | -------------- |
|
|
528
|
-
| `-t, --target` | claude-desktop | Target
|
|
529
|
-
| `-s, --scope` |
|
|
530
|
-
| `-f, --force` | false | Overwrite existing config
|
|
531
|
-
| `--dry-run` | false | Preview changes
|
|
531
|
+
| Option | Default | Description |
|
|
532
|
+
| -------------- | -------------- | ------------------------- |
|
|
533
|
+
| `-t, --target` | claude-desktop | Target client (see below) |
|
|
534
|
+
| `-s, --scope` | target default | Scope: `user`, `project` |
|
|
535
|
+
| `-f, --force` | false | Overwrite existing config |
|
|
536
|
+
| `--dry-run` | false | Preview changes |
|
|
537
|
+
|
|
538
|
+
Targets: `claude-desktop`, `claude-code`, `codex`, `cursor`, `zed`,
|
|
539
|
+
`windsurf`, `opencode`, `amp`, `lmstudio`, and `librechat`. Project scope is
|
|
540
|
+
supported by Claude Code, Codex, Cursor, OpenCode, and LibreChat.
|
|
541
|
+
LibreChat is project-only; all other targets default to user scope.
|
|
532
542
|
|
|
533
543
|
Examples:
|
|
534
544
|
|
|
@@ -21,14 +21,27 @@ gno mcp install --enable-write
|
|
|
21
21
|
|
|
22
22
|
### Claude Desktop
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Run `gno mcp install --dry-run --json`, then add the reported absolute values to
|
|
25
|
+
`claude_desktop_config.json`:
|
|
25
26
|
|
|
26
27
|
```json
|
|
27
28
|
{
|
|
28
29
|
"mcpServers": {
|
|
29
30
|
"gno": {
|
|
30
|
-
"command": "
|
|
31
|
-
"args": [
|
|
31
|
+
"command": "/absolute/path/to/bun",
|
|
32
|
+
"args": [
|
|
33
|
+
"run",
|
|
34
|
+
"/absolute/path/to/@gmickel/gno/src/index.ts",
|
|
35
|
+
"--index",
|
|
36
|
+
"default",
|
|
37
|
+
"--config",
|
|
38
|
+
"/absolute/path/to/index.yml",
|
|
39
|
+
"mcp"
|
|
40
|
+
],
|
|
41
|
+
"env": {
|
|
42
|
+
"GNO_DATA_DIR": "/absolute/path/to/data",
|
|
43
|
+
"GNO_CACHE_DIR": "/absolute/path/to/cache"
|
|
44
|
+
}
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
}
|
|
@@ -47,6 +60,12 @@ gno mcp install -t claude-code -s user # User scope
|
|
|
47
60
|
gno mcp install -t claude-code -s project # Project scope
|
|
48
61
|
```
|
|
49
62
|
|
|
63
|
+
Installed entries deliberately pin the current Bun/package entrypoint, active
|
|
64
|
+
index, absolute config, data directory, and model cache. Standard clients use
|
|
65
|
+
`env`; OpenCode uses `environment`; Codex uses `~/.codex/config.toml` or project
|
|
66
|
+
`.codex/config.toml` with `[mcp_servers.gno]` and `[mcp_servers.gno.env]`.
|
|
67
|
+
Do not shorten a generated entry to `gno mcp`.
|
|
68
|
+
|
|
50
69
|
## Check Status
|
|
51
70
|
|
|
52
71
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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
|
"embeddings",
|
|
@@ -160,6 +160,7 @@
|
|
|
160
160
|
"commander": "14.0.3",
|
|
161
161
|
"embla-carousel-react": "8.6.0",
|
|
162
162
|
"franc": "6.2.0",
|
|
163
|
+
"jsonc-parser": "3.3.1",
|
|
163
164
|
"lucide-react": "1.8.0",
|
|
164
165
|
"markitdown-ts": "0.0.9",
|
|
165
166
|
"minimatch": "10.2.3",
|
package/src/app/constants.ts
CHANGED
|
@@ -10,6 +10,11 @@ import { basename, join } from "node:path";
|
|
|
10
10
|
|
|
11
11
|
// Bun supports JSON imports natively - version single source of truth
|
|
12
12
|
import pkg from "../../package.json";
|
|
13
|
+
import {
|
|
14
|
+
assertValidIndexName,
|
|
15
|
+
indexNamesMatch,
|
|
16
|
+
resolveIndexDbFilename,
|
|
17
|
+
} from "./index-name";
|
|
13
18
|
|
|
14
19
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
20
|
// Brand / Product Identity
|
|
@@ -218,7 +223,26 @@ export function getIndexDbPath(
|
|
|
218
223
|
indexName: string = DEFAULT_INDEX_NAME,
|
|
219
224
|
dirs: ResolvedDirs = resolveDirs()
|
|
220
225
|
): string {
|
|
221
|
-
|
|
226
|
+
assertValidIndexName(indexName);
|
|
227
|
+
let existingFilenames: string[] = [];
|
|
228
|
+
try {
|
|
229
|
+
existingFilenames = [
|
|
230
|
+
...new Bun.Glob("index-*.sqlite").scanSync({
|
|
231
|
+
cwd: dirs.data,
|
|
232
|
+
onlyFiles: false,
|
|
233
|
+
}),
|
|
234
|
+
];
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (
|
|
237
|
+
!error ||
|
|
238
|
+
typeof error !== "object" ||
|
|
239
|
+
!("code" in error) ||
|
|
240
|
+
error.code !== "ENOENT"
|
|
241
|
+
) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return join(dirs.data, resolveIndexDbFilename(indexName, existingFilenames));
|
|
222
246
|
}
|
|
223
247
|
|
|
224
248
|
/**
|
|
@@ -265,8 +289,12 @@ export function buildUri(
|
|
|
265
289
|
.map((segment) => encodeURIComponent(segment))
|
|
266
290
|
.join("/");
|
|
267
291
|
const uri = `${URI_PREFIX}${collection}/${encodedPath}`;
|
|
268
|
-
const indexName = options.indexName
|
|
269
|
-
if (
|
|
292
|
+
const indexName = options.indexName;
|
|
293
|
+
if (indexName === undefined) {
|
|
294
|
+
return uri;
|
|
295
|
+
}
|
|
296
|
+
assertValidIndexName(indexName);
|
|
297
|
+
if (indexNamesMatch(indexName, DEFAULT_INDEX_NAME)) {
|
|
270
298
|
return uri;
|
|
271
299
|
}
|
|
272
300
|
return `${uri}?index=${encodeURIComponent(indexName)}`;
|
|
@@ -287,8 +315,8 @@ export function parseUri(uri: string): ParsedGnoUri | null {
|
|
|
287
315
|
if (slashIndex === -1) {
|
|
288
316
|
// gno://collection (no path)
|
|
289
317
|
const [collectionWithQuery, query = ""] = rest.split("?", 2);
|
|
290
|
-
const indexName = new URLSearchParams(query).get("index")
|
|
291
|
-
return indexName
|
|
318
|
+
const indexName = new URLSearchParams(query).get("index");
|
|
319
|
+
return indexName !== null
|
|
292
320
|
? {
|
|
293
321
|
collection: collectionWithQuery ?? rest,
|
|
294
322
|
path: "",
|
|
@@ -307,8 +335,10 @@ export function parseUri(uri: string): ParsedGnoUri | null {
|
|
|
307
335
|
// decodeURIComponent throws on malformed percent-encoding
|
|
308
336
|
try {
|
|
309
337
|
const path = decodeURIComponent(encodedPath);
|
|
310
|
-
const indexName = new URLSearchParams(query).get("index")
|
|
311
|
-
return indexName
|
|
338
|
+
const indexName = new URLSearchParams(query).get("index");
|
|
339
|
+
return indexName !== null
|
|
340
|
+
? { collection, path, indexName }
|
|
341
|
+
: { collection, path };
|
|
312
342
|
} catch {
|
|
313
343
|
return null;
|
|
314
344
|
}
|
|
@@ -319,12 +349,15 @@ export function parseUri(uri: string): ParsedGnoUri | null {
|
|
|
319
349
|
*/
|
|
320
350
|
export function decorateUriForIndex(uri: string, indexName?: string): string {
|
|
321
351
|
const parsed = parseUri(uri);
|
|
322
|
-
|
|
323
|
-
|
|
352
|
+
if (!parsed || indexName === undefined) {
|
|
353
|
+
return stripUriIndex(uri);
|
|
354
|
+
}
|
|
355
|
+
assertValidIndexName(indexName);
|
|
356
|
+
if (indexNamesMatch(indexName, DEFAULT_INDEX_NAME)) {
|
|
324
357
|
return stripUriIndex(uri);
|
|
325
358
|
}
|
|
326
359
|
return buildUri(parsed.collection, parsed.path, {
|
|
327
|
-
indexName
|
|
360
|
+
indexName,
|
|
328
361
|
});
|
|
329
362
|
}
|
|
330
363
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Shared index-name contract for filesystem paths and connector trust checks. */
|
|
2
|
+
|
|
3
|
+
export const MAX_INDEX_NAME_LENGTH = 64;
|
|
4
|
+
|
|
5
|
+
const SAFE_INDEX_NAME_REGEX = /^[\p{L}\p{N}][\p{L}\p{M}\p{N} ._-]*$/u;
|
|
6
|
+
const INDEX_DB_PREFIX = "index-";
|
|
7
|
+
const INDEX_DB_SUFFIX = ".sqlite";
|
|
8
|
+
const MAX_PORTABLE_FILENAME_COMPONENT_LENGTH = 255;
|
|
9
|
+
const MAX_INDEX_IDENTITY_STORAGE_LENGTH =
|
|
10
|
+
MAX_PORTABLE_FILENAME_COMPONENT_LENGTH -
|
|
11
|
+
INDEX_DB_PREFIX.length -
|
|
12
|
+
INDEX_DB_SUFFIX.length;
|
|
13
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
14
|
+
|
|
15
|
+
export const INDEX_NAME_REQUIREMENTS =
|
|
16
|
+
"use 1-64 letters, marks, numbers, internal spaces, '.', '_' or '-', start with a letter or number, do not end with a space or '.', do not include '..', and fit the portable database filename limit";
|
|
17
|
+
|
|
18
|
+
function hasSafeIndexNameSyntax(value: string): boolean {
|
|
19
|
+
return (
|
|
20
|
+
SAFE_INDEX_NAME_REGEX.test(value) &&
|
|
21
|
+
!/[ .]$/.test(value) &&
|
|
22
|
+
!value.includes("..")
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalizeIndexNameUnchecked(value: string): string {
|
|
27
|
+
return value
|
|
28
|
+
.normalize("NFC")
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.toUpperCase()
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.normalize("NFC");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fitsIndexIdentityStorage(value: string): boolean {
|
|
36
|
+
return (
|
|
37
|
+
value.length <= MAX_INDEX_IDENTITY_STORAGE_LENGTH &&
|
|
38
|
+
UTF8_ENCODER.encode(value).byteLength <= MAX_INDEX_IDENTITY_STORAGE_LENGTH
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Return whether a value is a canonical, filesystem-safe GNO index name. */
|
|
43
|
+
export function isValidIndexName(value: unknown): value is string {
|
|
44
|
+
if (
|
|
45
|
+
typeof value !== "string" ||
|
|
46
|
+
value.length > MAX_INDEX_NAME_LENGTH ||
|
|
47
|
+
!hasSafeIndexNameSyntax(value)
|
|
48
|
+
) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return fitsIndexIdentityStorage(canonicalizeIndexNameUnchecked(value));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Fail closed before an index name can influence a database path. */
|
|
55
|
+
export function assertValidIndexName(value: unknown): asserts value is string {
|
|
56
|
+
if (!isValidIndexName(value)) {
|
|
57
|
+
throw new TypeError(`Invalid index name: ${INDEX_NAME_REQUIREMENTS}.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Return the cross-platform logical identity for an index name.
|
|
63
|
+
*
|
|
64
|
+
* APFS and Windows filesystems collapse canonical Unicode and case variants,
|
|
65
|
+
* while common Linux filesystems do not. GNO applies one identity everywhere
|
|
66
|
+
* so URI routing and database selection cannot disagree by platform.
|
|
67
|
+
*/
|
|
68
|
+
export function canonicalizeIndexName(value: string): string {
|
|
69
|
+
assertValidIndexName(value);
|
|
70
|
+
// The lower/upper/lower closure covers multi-character folds (ß/SS),
|
|
71
|
+
// compatibility case pairs (ſ/S), and positional forms (ς/Σ) that a plain
|
|
72
|
+
// lowercase pass misses but case-insensitive APFS aliases on disk.
|
|
73
|
+
return canonicalizeIndexNameUnchecked(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Compare two validated names using GNO's cross-platform identity rules. */
|
|
77
|
+
export function indexNamesMatch(left: string, right: string): boolean {
|
|
78
|
+
return canonicalizeIndexName(left) === canonicalizeIndexName(right);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function indexNameFromDbFilename(filename: string): string | null {
|
|
82
|
+
if (
|
|
83
|
+
!filename.startsWith(INDEX_DB_PREFIX) ||
|
|
84
|
+
!filename.endsWith(INDEX_DB_SUFFIX)
|
|
85
|
+
) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const name = filename.slice(INDEX_DB_PREFIX.length, -INDEX_DB_SUFFIX.length);
|
|
89
|
+
const isCanonicalStoredIdentity =
|
|
90
|
+
hasSafeIndexNameSyntax(name) &&
|
|
91
|
+
fitsIndexIdentityStorage(name) &&
|
|
92
|
+
canonicalizeIndexNameUnchecked(name) === name;
|
|
93
|
+
return isValidIndexName(name) || isCanonicalStoredIdentity ? name : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Select one database filename for a logical index identity.
|
|
98
|
+
*
|
|
99
|
+
* New indexes use the canonical filename. A single existing mixed-case or
|
|
100
|
+
* pre-normalized filename remains addressable for backward compatibility.
|
|
101
|
+
* Multiple legacy files with the same identity are unsafe on case-sensitive
|
|
102
|
+
* filesystems and fail closed instead of selecting one by directory order.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveIndexDbFilename(
|
|
105
|
+
indexName: string,
|
|
106
|
+
existingFilenames: Iterable<string> = []
|
|
107
|
+
): string {
|
|
108
|
+
const identity = canonicalizeIndexName(indexName);
|
|
109
|
+
const matches: string[] = [];
|
|
110
|
+
for (const filename of existingFilenames) {
|
|
111
|
+
const existingName = indexNameFromDbFilename(filename);
|
|
112
|
+
if (
|
|
113
|
+
existingName !== null &&
|
|
114
|
+
canonicalizeIndexNameUnchecked(existingName) === identity
|
|
115
|
+
) {
|
|
116
|
+
matches.push(filename);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const uniqueMatches = [...new Set(matches)].sort();
|
|
121
|
+
if (uniqueMatches.length > 1) {
|
|
122
|
+
throw new TypeError(
|
|
123
|
+
`Ambiguous index name "${indexName}": multiple database files share its canonical identity (${uniqueMatches.join(", ")}).`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return uniqueMatches[0] ?? `${INDEX_DB_PREFIX}${identity}${INDEX_DB_SUFFIX}`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Passive retrieval activation diagnostics shared by `gno doctor`. */
|
|
2
|
+
|
|
3
|
+
import type { Config } from "../../config/types";
|
|
4
|
+
import type { ActivationStatus } from "../../core/activation-status";
|
|
5
|
+
import type { StorePort } from "../../store/types";
|
|
6
|
+
import type { DoctorCheck } from "./doctor";
|
|
7
|
+
|
|
8
|
+
import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
9
|
+
import { buildActivationStatus } from "../../core/activation-status";
|
|
10
|
+
import { ModelCache } from "../../llm/cache";
|
|
11
|
+
import { getActivePreset } from "../../llm/registry";
|
|
12
|
+
import { getConnectorVerificationTargets } from "../../serve/connectors";
|
|
13
|
+
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
14
|
+
|
|
15
|
+
export interface DoctorActivationOptions {
|
|
16
|
+
configPath?: string;
|
|
17
|
+
indexName?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function unavailableActivation(
|
|
21
|
+
config: Config
|
|
22
|
+
): Promise<ActivationStatus> {
|
|
23
|
+
return buildActivationStatus(
|
|
24
|
+
{} as StorePort,
|
|
25
|
+
config.collections.map(({ name }) => name),
|
|
26
|
+
{
|
|
27
|
+
verifyCollection: async () => ({
|
|
28
|
+
ok: false,
|
|
29
|
+
error: { code: "QUERY_FAILED", message: "Activation unavailable" },
|
|
30
|
+
}),
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function buildDoctorActivation(
|
|
36
|
+
config: Config,
|
|
37
|
+
options: DoctorActivationOptions
|
|
38
|
+
): Promise<ActivationStatus> {
|
|
39
|
+
const dbPath = getIndexDbPath(options.indexName);
|
|
40
|
+
if (!(await Bun.file(dbPath).exists())) {
|
|
41
|
+
return unavailableActivation(config);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const store = new SqliteAdapter();
|
|
45
|
+
store.setConfigPath(options.configPath ?? "");
|
|
46
|
+
const opened = await store.open(dbPath, config.ftsTokenizer);
|
|
47
|
+
if (!opened.ok) {
|
|
48
|
+
return unavailableActivation(config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const indexStatus = await store.getStatus();
|
|
53
|
+
const embedCached = await new ModelCache(getModelsCachePath()).isCached(
|
|
54
|
+
getActivePreset(config).embed
|
|
55
|
+
);
|
|
56
|
+
return await buildActivationStatus(
|
|
57
|
+
store,
|
|
58
|
+
config.collections.map(({ name }) => name),
|
|
59
|
+
{
|
|
60
|
+
semantic: {
|
|
61
|
+
modelsCached: embedCached,
|
|
62
|
+
embeddingBacklog: indexStatus.ok
|
|
63
|
+
? indexStatus.value.embeddingBacklog
|
|
64
|
+
: 0,
|
|
65
|
+
},
|
|
66
|
+
connectorTargets: await getConnectorVerificationTargets(),
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
} finally {
|
|
70
|
+
await store.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function checkRetrievalActivation(
|
|
75
|
+
activation: ActivationStatus
|
|
76
|
+
): DoctorCheck {
|
|
77
|
+
if (activation.healthy) {
|
|
78
|
+
const semanticStates = [
|
|
79
|
+
...new Set(
|
|
80
|
+
activation.collections.map(
|
|
81
|
+
({ semanticAvailability }) => semanticAvailability.code
|
|
82
|
+
)
|
|
83
|
+
),
|
|
84
|
+
];
|
|
85
|
+
return {
|
|
86
|
+
name: "retrieval-activation",
|
|
87
|
+
status: "ok",
|
|
88
|
+
message: `${activation.collections.length} collection${activation.collections.length === 1 ? "" : "s"} passed lexical retrieval proof`,
|
|
89
|
+
details: [
|
|
90
|
+
`Semantic retrieval remains separate (${semanticStates.join(", ")}).`,
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const failed = activation.collections.filter(({ ready }) => !ready);
|
|
96
|
+
const details = failed.flatMap(({ collection, remediation }) =>
|
|
97
|
+
remediation
|
|
98
|
+
? [
|
|
99
|
+
`${collection}: ${remediation.stage}/${remediation.code}`,
|
|
100
|
+
`Run: ${remediation.command}`,
|
|
101
|
+
]
|
|
102
|
+
: [`${collection}: activation unavailable`]
|
|
103
|
+
);
|
|
104
|
+
return {
|
|
105
|
+
name: "retrieval-activation",
|
|
106
|
+
status: "error",
|
|
107
|
+
message:
|
|
108
|
+
activation.collections.length === 0
|
|
109
|
+
? "No collections configured. Run: gno collection add"
|
|
110
|
+
: activation.usable
|
|
111
|
+
? `${failed.length} collection${failed.length === 1 ? "" : "s"} failed lexical retrieval proof`
|
|
112
|
+
: "No configured collection passed lexical retrieval proof",
|
|
113
|
+
details,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function checkConnectorActivation(
|
|
118
|
+
activation: ActivationStatus
|
|
119
|
+
): DoctorCheck | null {
|
|
120
|
+
const { projected, total, truncated } = activation.connectorProjection;
|
|
121
|
+
const omitted = total - projected;
|
|
122
|
+
const observed = activation.connectors.filter(
|
|
123
|
+
({ code }) =>
|
|
124
|
+
code !== "connector_not_configured" &&
|
|
125
|
+
code !== "target_runtime_unverifiable"
|
|
126
|
+
);
|
|
127
|
+
if (observed.length === 0 && !truncated) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const incomplete = observed.filter(({ status }) => status !== "passed");
|
|
131
|
+
const details = incomplete.map(
|
|
132
|
+
({ collection, target, status, code, remediation }) =>
|
|
133
|
+
`${target}/${collection}: ${status}${code ? `/${code}` : ""}${remediation ? `. ${remediation}` : ""}`
|
|
134
|
+
);
|
|
135
|
+
if (truncated) {
|
|
136
|
+
details.unshift(
|
|
137
|
+
`${omitted} target/collection checks were omitted by the bounded status projection; no result is claimed for them.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
name: "connector-activation",
|
|
142
|
+
status: incomplete.length > 0 || truncated ? "warn" : "ok",
|
|
143
|
+
message:
|
|
144
|
+
incomplete.length > 0
|
|
145
|
+
? `${incomplete.length} connector proof${incomplete.length === 1 ? "" : "s"} pending or failed`
|
|
146
|
+
: truncated
|
|
147
|
+
? `${projected} of ${total} connector target/collection checks projected`
|
|
148
|
+
: `${observed.length} connector proof${observed.length === 1 ? "" : "s"} passed`,
|
|
149
|
+
details,
|
|
150
|
+
};
|
|
151
|
+
}
|