@gmickel/gno 1.12.2 → 1.12.3
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 +1 -1
- package/assets/skill/SKILL.md +3 -1
- package/package.json +2 -1
- package/src/core/indexed-reference.ts +68 -0
- package/src/core/ref-parser.ts +6 -1
- package/src/index.ts +11 -2
- package/src/ingestion/sync.ts +182 -15
- package/src/ingestion/types.ts +2 -0
- package/src/mcp/resources/index.ts +71 -47
- package/src/mcp/tools/get.ts +108 -93
- package/src/mcp/tools/multi-get.ts +116 -99
- package/src/sdk/client.ts +56 -31
- package/src/serve/public/components/AIModelSelector.tsx +22 -7
- package/src/serve/public/pages/Dashboard.tsx +1 -1
- package/src/serve/routes/api.ts +26 -1
- package/src/serve/server.ts +11 -2
- package/src/serve/status.ts +24 -0
- package/src/serve/watch-service.ts +2 -1
- package/src/store/sqlite/adapter.ts +99 -49
- package/src/store/sqlite/scoped-index.ts +68 -0
- package/src/store/types.ts +3 -1
package/README.md
CHANGED
package/assets/skill/SKILL.md
CHANGED
|
@@ -227,7 +227,9 @@ gno graph --from gno://notes/a.md --to gno://notes/b.md
|
|
|
227
227
|
```
|
|
228
228
|
|
|
229
229
|
Non-default index search results may include `?index=<name>` on `gno://` URIs.
|
|
230
|
-
Keep that query string when passing the URI to `gno get
|
|
230
|
+
Keep that query string when passing the URI to `gno get`, SDK `get()`, MCP
|
|
231
|
+
`gno_get`, or an MCP resource read: it selects the named database. Batch reads
|
|
232
|
+
must contain refs for one index; split mixed-index results before `multi-get`.
|
|
231
233
|
|
|
232
234
|
## Important: Embedding After Changes
|
|
233
235
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.3",
|
|
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",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"eval:hybrid:baseline": "bun scripts/hybrid-benchmark.ts --write",
|
|
72
72
|
"eval:hybrid:delta": "bun scripts/hybrid-benchmark.ts --delta",
|
|
73
73
|
"bench:ast-chunking": "bun scripts/ast-chunking-benchmark.ts",
|
|
74
|
+
"smoke:serve-shutdown": "bun scripts/serve-shutdown-smoke.ts",
|
|
74
75
|
"bench:code-embeddings": "bun scripts/code-embedding-benchmark.ts",
|
|
75
76
|
"bench:code-embeddings:write": "bun scripts/code-embedding-benchmark.ts --write",
|
|
76
77
|
"bench:general-embeddings": "bun scripts/general-embedding-benchmark.ts",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DEFAULT_INDEX_NAME, parseUri } from "../app/constants";
|
|
2
|
+
import { parseRef } from "./ref-parser";
|
|
3
|
+
|
|
4
|
+
export interface EffectiveIndexResolution {
|
|
5
|
+
indexName?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeIndexName(indexName?: string): string {
|
|
9
|
+
const normalized = indexName?.trim();
|
|
10
|
+
return normalized || DEFAULT_INDEX_NAME;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function indexesMatch(left?: string, right?: string): boolean {
|
|
14
|
+
return normalizeIndexName(left) === normalizeIndexName(right);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getExplicitRefIndex(ref: string): string | undefined {
|
|
18
|
+
const parsed = parseRef(ref);
|
|
19
|
+
if ("error" in parsed || parsed.type !== "uri") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
return parseUri(parsed.value)?.indexName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveEffectiveIndex(
|
|
26
|
+
refs: string[],
|
|
27
|
+
activeIndexName?: string
|
|
28
|
+
):
|
|
29
|
+
| { ok: true; value: EffectiveIndexResolution }
|
|
30
|
+
| { ok: false; error: string } {
|
|
31
|
+
const explicitIndexes = new Set<string>();
|
|
32
|
+
let hasUnindexedRef = false;
|
|
33
|
+
|
|
34
|
+
for (const ref of refs) {
|
|
35
|
+
const explicitIndex = getExplicitRefIndex(ref);
|
|
36
|
+
if (explicitIndex) {
|
|
37
|
+
explicitIndexes.add(explicitIndex);
|
|
38
|
+
} else {
|
|
39
|
+
hasUnindexedRef = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (explicitIndexes.size > 1) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: `References cannot mix explicit indexes: ${[...explicitIndexes]
|
|
47
|
+
.sort()
|
|
48
|
+
.join(", ")}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const explicitIndex = [...explicitIndexes][0];
|
|
53
|
+
if (
|
|
54
|
+
explicitIndex &&
|
|
55
|
+
hasUnindexedRef &&
|
|
56
|
+
!indexesMatch(explicitIndex, activeIndexName)
|
|
57
|
+
) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: `References cannot mix indexed refs (${explicitIndex}) with unindexed refs while the active index is ${normalizeIndexName(activeIndexName)}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
value: { indexName: explicitIndex ?? activeIndexName },
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/core/ref-parser.ts
CHANGED
|
@@ -141,5 +141,10 @@ export function splitRefs(refs: string[]): string[] {
|
|
|
141
141
|
* Check if a ref contains glob characters.
|
|
142
142
|
*/
|
|
143
143
|
export function isGlobPattern(ref: string): boolean {
|
|
144
|
-
|
|
144
|
+
const queryIndex = ref.indexOf("?");
|
|
145
|
+
const globCandidate =
|
|
146
|
+
ref.startsWith("gno://") && queryIndex >= 0
|
|
147
|
+
? ref.slice(0, queryIndex)
|
|
148
|
+
: ref;
|
|
149
|
+
return GLOB_PATTERN.test(globCandidate);
|
|
145
150
|
}
|
package/src/index.ts
CHANGED
|
@@ -20,8 +20,17 @@ async function cleanupAndExit(code: number): Promise<never> {
|
|
|
20
20
|
process.exit(code);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
let interruptExitCode: 0 | 130 = 0;
|
|
24
|
+
|
|
25
|
+
// Long-running commands install their own SIGINT handler and must finish their
|
|
26
|
+
// resource teardown before this bootstrap exits. Short-lived commands have no
|
|
27
|
+
// owner, so retain the immediate interrupt behavior for them.
|
|
24
28
|
process.on("SIGINT", () => {
|
|
29
|
+
if (process.listenerCount("SIGINT") > 1) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interruptExitCode = 130;
|
|
25
34
|
process.stderr.write("\nInterrupted\n");
|
|
26
35
|
cleanupAndExit(130).catch(() => {
|
|
27
36
|
// Ignore cleanup errors on exit
|
|
@@ -30,7 +39,7 @@ process.on("SIGINT", () => {
|
|
|
30
39
|
|
|
31
40
|
// Run CLI and exit
|
|
32
41
|
runCli(process.argv)
|
|
33
|
-
.then((code) => cleanupAndExit(code))
|
|
42
|
+
.then((code) => cleanupAndExit(interruptExitCode || code))
|
|
34
43
|
.catch((err) => {
|
|
35
44
|
process.stderr.write(
|
|
36
45
|
`Fatal error: ${err instanceof Error ? err.message : String(err)}\n`
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -75,6 +75,7 @@ const MAX_CONCURRENCY = 16;
|
|
|
75
75
|
export const INGEST_VERSION = 6;
|
|
76
76
|
const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT = fingerprintContentTypeRules([]);
|
|
77
77
|
const RELATION_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
78
|
+
const PROJECTION_YIELD_INTERVAL = 25;
|
|
78
79
|
const NON_RETRYABLE_CONVERSION_ERROR_CODES = new Set([
|
|
79
80
|
"CORRUPT",
|
|
80
81
|
"PERMISSION",
|
|
@@ -1000,19 +1001,83 @@ export class SyncService {
|
|
|
1000
1001
|
relPaths: string[],
|
|
1001
1002
|
options: SyncOptions = {}
|
|
1002
1003
|
): Promise<FileSyncResult[]> {
|
|
1004
|
+
const result = await this.syncPaths(collection, store, relPaths, options);
|
|
1005
|
+
return result.files ?? [];
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async syncPaths(
|
|
1009
|
+
collection: Collection,
|
|
1010
|
+
store: StorePort,
|
|
1011
|
+
relPaths: string[],
|
|
1012
|
+
options: SyncOptions = {}
|
|
1013
|
+
): Promise<CollectionSyncResult> {
|
|
1014
|
+
const startedAt = Date.now();
|
|
1015
|
+
const syncOptions: SyncOptions = {
|
|
1016
|
+
...options,
|
|
1017
|
+
contentTypeRules: options.contentTypeRules ?? [],
|
|
1018
|
+
contentTypeRulesFingerprint:
|
|
1019
|
+
options.contentTypeRulesFingerprint ??
|
|
1020
|
+
fingerprintContentTypeRules(options.contentTypeRules ?? []),
|
|
1021
|
+
};
|
|
1003
1022
|
const results: FileSyncResult[] = [];
|
|
1023
|
+
const projectionSourceIds = new Set<number>();
|
|
1024
|
+
let markedInactive = 0;
|
|
1004
1025
|
|
|
1005
1026
|
for (const relPath of relPaths) {
|
|
1027
|
+
const existingResult = await store.getDocument(collection.name, relPath);
|
|
1028
|
+
const existingDoc = existingResult.ok ? existingResult.value : null;
|
|
1029
|
+
if (existingDoc) {
|
|
1030
|
+
await this.collectProjectionSourceIds(
|
|
1031
|
+
store,
|
|
1032
|
+
existingDoc.id,
|
|
1033
|
+
projectionSourceIds
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1006
1037
|
const absPath = join(collection.path, relPath);
|
|
1007
1038
|
let stats: Awaited<ReturnType<typeof stat>>;
|
|
1008
1039
|
try {
|
|
1009
1040
|
stats = await stat(absPath);
|
|
1010
|
-
} catch {
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
const errorCode =
|
|
1043
|
+
error && typeof error === "object" && "code" in error
|
|
1044
|
+
? String(error.code)
|
|
1045
|
+
: undefined;
|
|
1046
|
+
if (errorCode !== "ENOENT") {
|
|
1047
|
+
results.push({
|
|
1048
|
+
relPath,
|
|
1049
|
+
status: "error",
|
|
1050
|
+
errorCode: "STAT_FAILED",
|
|
1051
|
+
errorMessage:
|
|
1052
|
+
error instanceof Error ? error.message : "Failed to stat file",
|
|
1053
|
+
});
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
if (existingDoc?.active) {
|
|
1057
|
+
const inactiveResult = await store.markInactive(collection.name, [
|
|
1058
|
+
relPath,
|
|
1059
|
+
]);
|
|
1060
|
+
if (!inactiveResult.ok) {
|
|
1061
|
+
results.push({
|
|
1062
|
+
relPath,
|
|
1063
|
+
status: "error",
|
|
1064
|
+
errorCode: inactiveResult.error.code,
|
|
1065
|
+
errorMessage: inactiveResult.error.message,
|
|
1066
|
+
});
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
markedInactive += inactiveResult.value;
|
|
1070
|
+
results.push({
|
|
1071
|
+
relPath,
|
|
1072
|
+
status: "updated",
|
|
1073
|
+
docid: existingDoc.docid,
|
|
1074
|
+
});
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1011
1077
|
results.push({
|
|
1012
1078
|
relPath,
|
|
1013
|
-
status: "
|
|
1014
|
-
|
|
1015
|
-
errorMessage: "File not found",
|
|
1079
|
+
status: existingDoc ? "unchanged" : "skipped",
|
|
1080
|
+
docid: existingDoc?.docid,
|
|
1016
1081
|
});
|
|
1017
1082
|
continue;
|
|
1018
1083
|
}
|
|
@@ -1035,24 +1100,91 @@ export class SyncService {
|
|
|
1035
1100
|
ctime: (stats.birthtime ?? stats.ctime ?? stats.mtime).toISOString(),
|
|
1036
1101
|
};
|
|
1037
1102
|
|
|
1038
|
-
const result = await this.processFile(
|
|
1103
|
+
const result = await this.processFile(
|
|
1104
|
+
collection,
|
|
1105
|
+
entry,
|
|
1106
|
+
store,
|
|
1107
|
+
syncOptions
|
|
1108
|
+
);
|
|
1039
1109
|
results.push(result);
|
|
1110
|
+
const currentResult = await store.getDocument(collection.name, relPath);
|
|
1111
|
+
const currentDoc = currentResult.ok ? currentResult.value : null;
|
|
1112
|
+
if (currentDoc?.active) {
|
|
1113
|
+
await this.collectProjectionSourceIds(
|
|
1114
|
+
store,
|
|
1115
|
+
currentDoc.id,
|
|
1116
|
+
projectionSourceIds
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1040
1119
|
}
|
|
1041
1120
|
|
|
1042
|
-
|
|
1121
|
+
const errors =
|
|
1122
|
+
syncOptions.projectTypedEdges === false
|
|
1123
|
+
? []
|
|
1124
|
+
: await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
|
|
1125
|
+
const added = results.filter((result) => result.status === "added").length;
|
|
1126
|
+
const updated = results.filter(
|
|
1127
|
+
(result) => result.status === "updated"
|
|
1128
|
+
).length;
|
|
1129
|
+
const unchanged = results.filter(
|
|
1130
|
+
(result) => result.status === "unchanged"
|
|
1131
|
+
).length;
|
|
1132
|
+
const errored = results.filter(
|
|
1133
|
+
(result) => result.status === "error"
|
|
1134
|
+
).length;
|
|
1135
|
+
const skipped = results.filter(
|
|
1136
|
+
(result) => result.status === "skipped"
|
|
1137
|
+
).length;
|
|
1043
1138
|
|
|
1044
|
-
return
|
|
1139
|
+
return {
|
|
1140
|
+
collection: collection.name,
|
|
1141
|
+
filesProcessed: results.length,
|
|
1142
|
+
filesAdded: added,
|
|
1143
|
+
filesUpdated: updated,
|
|
1144
|
+
filesUnchanged: unchanged,
|
|
1145
|
+
filesErrored: errored,
|
|
1146
|
+
filesSkipped: skipped,
|
|
1147
|
+
filesMarkedInactive: markedInactive,
|
|
1148
|
+
durationMs: Date.now() - startedAt,
|
|
1149
|
+
files: results,
|
|
1150
|
+
errors,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
private async collectProjectionSourceIds(
|
|
1155
|
+
store: StorePort,
|
|
1156
|
+
documentId: number,
|
|
1157
|
+
sourceIds: Set<number>
|
|
1158
|
+
): Promise<void> {
|
|
1159
|
+
sourceIds.add(documentId);
|
|
1160
|
+
const [linkBacklinks, edgeBacklinks] = await Promise.all([
|
|
1161
|
+
store.getBacklinksForDoc(documentId),
|
|
1162
|
+
store.getEdgeBacklinksForDoc(documentId),
|
|
1163
|
+
]);
|
|
1164
|
+
if (linkBacklinks.ok) {
|
|
1165
|
+
for (const backlink of linkBacklinks.value) {
|
|
1166
|
+
sourceIds.add(backlink.sourceDocId);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (edgeBacklinks.ok) {
|
|
1170
|
+
for (const backlink of edgeBacklinks.value) {
|
|
1171
|
+
sourceIds.add(backlink.sourceDocId);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1045
1174
|
}
|
|
1046
1175
|
|
|
1047
1176
|
private async projectTypedEdges(
|
|
1048
|
-
collection: Collection,
|
|
1049
1177
|
store: StorePort,
|
|
1050
|
-
options: SyncOptions
|
|
1178
|
+
options: SyncOptions,
|
|
1179
|
+
sourceDocumentIds?: Set<number>
|
|
1051
1180
|
): Promise<Array<{ relPath: string; code: string; message: string }>> {
|
|
1052
1181
|
const errors: Array<{ relPath: string; code: string; message: string }> =
|
|
1053
1182
|
[];
|
|
1054
1183
|
|
|
1055
|
-
const
|
|
1184
|
+
const selectedSourceIds = sourceDocumentIds
|
|
1185
|
+
? [...sourceDocumentIds]
|
|
1186
|
+
: undefined;
|
|
1187
|
+
const backfillResult = await store.backfillDocEdges(selectedSourceIds);
|
|
1056
1188
|
if (!backfillResult.ok) {
|
|
1057
1189
|
return [
|
|
1058
1190
|
{
|
|
@@ -1075,8 +1207,22 @@ export class SyncService {
|
|
|
1075
1207
|
}
|
|
1076
1208
|
|
|
1077
1209
|
const activeDocs = docsResult.value.filter((doc) => doc.active);
|
|
1210
|
+
const activeIds = new Set(activeDocs.map((doc) => doc.id));
|
|
1211
|
+
if (selectedSourceIds) {
|
|
1212
|
+
for (const documentId of selectedSourceIds) {
|
|
1213
|
+
if (!activeIds.has(documentId)) {
|
|
1214
|
+
await store.setDocEdges(documentId, [], "frontmatter-relation");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const projectedDocs = sourceDocumentIds
|
|
1219
|
+
? activeDocs.filter((doc) => sourceDocumentIds.has(doc.id))
|
|
1220
|
+
: activeDocs;
|
|
1078
1221
|
|
|
1079
|
-
for (const doc of
|
|
1222
|
+
for (const [docIndex, doc] of projectedDocs.entries()) {
|
|
1223
|
+
if (docIndex > 0 && docIndex % PROJECTION_YIELD_INTERVAL === 0) {
|
|
1224
|
+
await Bun.sleep(0);
|
|
1225
|
+
}
|
|
1080
1226
|
if (!doc.mirrorHash) {
|
|
1081
1227
|
continue;
|
|
1082
1228
|
}
|
|
@@ -1193,6 +1339,14 @@ export class SyncService {
|
|
|
1193
1339
|
return errors;
|
|
1194
1340
|
}
|
|
1195
1341
|
|
|
1342
|
+
/** Run an exact global typed-edge reconciliation with cooperative yields. */
|
|
1343
|
+
reconcileTypedEdges(
|
|
1344
|
+
store: StorePort,
|
|
1345
|
+
options: SyncOptions = {}
|
|
1346
|
+
): Promise<Array<{ relPath: string; code: string; message: string }>> {
|
|
1347
|
+
return this.projectTypedEdges(store, options);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1196
1350
|
/**
|
|
1197
1351
|
* Sync a single collection.
|
|
1198
1352
|
*/
|
|
@@ -1419,9 +1573,9 @@ export class SyncService {
|
|
|
1419
1573
|
}
|
|
1420
1574
|
}
|
|
1421
1575
|
|
|
1422
|
-
|
|
1423
|
-
...(await this.projectTypedEdges(
|
|
1424
|
-
|
|
1576
|
+
if (syncOptions.projectTypedEdges !== false) {
|
|
1577
|
+
errors.push(...(await this.projectTypedEdges(store, syncOptions)));
|
|
1578
|
+
}
|
|
1425
1579
|
|
|
1426
1580
|
return {
|
|
1427
1581
|
collection: collection.name,
|
|
@@ -1448,12 +1602,25 @@ export class SyncService {
|
|
|
1448
1602
|
): Promise<SyncResult> {
|
|
1449
1603
|
const startTime = Date.now();
|
|
1450
1604
|
const results: CollectionSyncResult[] = [];
|
|
1605
|
+
const deferredProjectionOptions: SyncOptions = {
|
|
1606
|
+
...options,
|
|
1607
|
+
projectTypedEdges: false,
|
|
1608
|
+
};
|
|
1451
1609
|
|
|
1452
1610
|
for (const collection of collections) {
|
|
1453
|
-
const result = await this.syncCollection(
|
|
1611
|
+
const result = await this.syncCollection(
|
|
1612
|
+
collection,
|
|
1613
|
+
store,
|
|
1614
|
+
deferredProjectionOptions
|
|
1615
|
+
);
|
|
1454
1616
|
results.push(result);
|
|
1455
1617
|
}
|
|
1456
1618
|
|
|
1619
|
+
if (results.length > 0) {
|
|
1620
|
+
const projectionErrors = await this.projectTypedEdges(store, options);
|
|
1621
|
+
results.at(-1)?.errors.push(...projectionErrors);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1457
1624
|
// Aggregate totals
|
|
1458
1625
|
const totals = results.reduce(
|
|
1459
1626
|
(acc, r) => ({
|
package/src/ingestion/types.ts
CHANGED
|
@@ -137,6 +137,8 @@ export interface SyncOptions {
|
|
|
137
137
|
contentTypeRules?: NormalizedContentTypeRule[];
|
|
138
138
|
/** Stable hash of the normalized content type rules, used for re-derivation. */
|
|
139
139
|
contentTypeRulesFingerprint?: string;
|
|
140
|
+
/** Internal orchestration flag: defer graph projection to an outer sync. */
|
|
141
|
+
projectTypedEdges?: boolean;
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
export type ContentTypeSource =
|
|
@@ -20,8 +20,10 @@ import {
|
|
|
20
20
|
URI_PREFIX,
|
|
21
21
|
} from "../../app/constants";
|
|
22
22
|
import { MCP_ERRORS } from "../../core/errors";
|
|
23
|
+
import { resolveEffectiveIndex } from "../../core/indexed-reference";
|
|
23
24
|
import { normalizeTag, validateTag } from "../../core/tags";
|
|
24
25
|
import { normalizeCollectionName } from "../../core/validation";
|
|
26
|
+
import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
|
|
25
27
|
|
|
26
28
|
// Tags resource URI prefix
|
|
27
29
|
const TAGS_URI = `${URI_PREFIX}tags`;
|
|
@@ -51,7 +53,8 @@ function formatTagsContent(
|
|
|
51
53
|
function formatResourceContent(
|
|
52
54
|
doc: DocumentRow,
|
|
53
55
|
content: string,
|
|
54
|
-
ctx: ToolContext
|
|
56
|
+
ctx: ToolContext,
|
|
57
|
+
indexName = ctx.indexName
|
|
55
58
|
): string {
|
|
56
59
|
// Find collection for absPath
|
|
57
60
|
const uriParsed = parseUri(doc.uri);
|
|
@@ -69,7 +72,7 @@ function formatResourceContent(
|
|
|
69
72
|
const langLine = doc.languageHint
|
|
70
73
|
? `\n language: ${doc.languageHint}`
|
|
71
74
|
: "";
|
|
72
|
-
const displayUri = decorateUriForIndex(doc.uri,
|
|
75
|
+
const displayUri = decorateUriForIndex(doc.uri, indexName);
|
|
73
76
|
const header = `<!-- ${displayUri}
|
|
74
77
|
docid: ${doc.docid}
|
|
75
78
|
source: ${absPath}
|
|
@@ -125,61 +128,82 @@ export function registerResources(server: McpServer, ctx: ToolContext): void {
|
|
|
125
128
|
throw new Error(`Invalid gno:// URI: ${uri.href}`);
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const collectionExists = ctx.collections.some(
|
|
132
|
-
(c) => c.name === collection
|
|
133
|
-
);
|
|
134
|
-
if (!collectionExists) {
|
|
135
|
-
throw new Error(`Collection not found: ${collection}`);
|
|
131
|
+
const resolution = resolveEffectiveIndex([uri.href], ctx.indexName);
|
|
132
|
+
if (!resolution.ok) {
|
|
133
|
+
throw new Error(resolution.error);
|
|
136
134
|
}
|
|
135
|
+
const scoped = await openScopedIndexStore({
|
|
136
|
+
activeStore: ctx.store,
|
|
137
|
+
activeIndexName: ctx.indexName,
|
|
138
|
+
requestedIndexName: resolution.value.indexName,
|
|
139
|
+
config: ctx.config,
|
|
140
|
+
configPath: ctx.actualConfigPath,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const { collection, path } = parsed;
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Failed to lookup document: ${docResult.error.message}`
|
|
146
|
+
// Validate collection exists
|
|
147
|
+
const collectionExists = ctx.collections.some(
|
|
148
|
+
(c) => c.name === collection
|
|
143
149
|
);
|
|
144
|
-
|
|
150
|
+
if (!collectionExists) {
|
|
151
|
+
throw new Error(`Collection not found: ${collection}`);
|
|
152
|
+
}
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
// Look up document (path is properly decoded by parseUri)
|
|
155
|
+
const docResult = await scoped.store.getDocument(collection, path);
|
|
156
|
+
if (!docResult.ok) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Failed to lookup document: ${docResult.error.message}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
150
161
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
const doc = docResult.value;
|
|
163
|
+
if (!doc) {
|
|
164
|
+
throw new Error(`Document not found: ${uri.href}`);
|
|
165
|
+
}
|
|
155
166
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
}
|
|
167
|
+
// Get content
|
|
168
|
+
if (!doc.mirrorHash) {
|
|
169
|
+
throw new Error(`Document has no indexed content: ${uri.href}`);
|
|
170
|
+
}
|
|
162
171
|
|
|
163
|
-
|
|
172
|
+
const contentResult = await scoped.store.getContent(doc.mirrorHash);
|
|
173
|
+
if (!contentResult.ok) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Failed to read content: ${contentResult.error.message}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
164
178
|
|
|
165
|
-
|
|
166
|
-
const formattedContent = formatResourceContent(doc, content, ctx);
|
|
179
|
+
const content = contentResult.value ?? "";
|
|
167
180
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
// Format with header and line numbers
|
|
182
|
+
const formattedContent = formatResourceContent(
|
|
183
|
+
doc,
|
|
184
|
+
content,
|
|
185
|
+
ctx,
|
|
186
|
+
scoped.indexName
|
|
187
|
+
);
|
|
173
188
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
// Build canonical URI
|
|
190
|
+
const canonicalUri = decorateUriForIndex(
|
|
191
|
+
buildUri(collection, path),
|
|
192
|
+
scoped.indexName
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
contents: [
|
|
197
|
+
{
|
|
198
|
+
uri: canonicalUri,
|
|
199
|
+
mimeType: "text/markdown",
|
|
200
|
+
text: formattedContent,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
await scoped.close();
|
|
206
|
+
}
|
|
183
207
|
} finally {
|
|
184
208
|
release();
|
|
185
209
|
}
|