@gmickel/gno 2.10.1 → 2.11.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 +11 -11
- package/assets/skill/SKILL.md +25 -14
- package/assets/skill/cli-reference.md +6 -5
- package/assets/skill/examples.md +16 -12
- package/assets/skill/mcp-reference.md +2 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v2.10.1.zip → gno-browser-clipper-v2.11.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.11.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +3 -1
- package/spec/cli.md +27 -1
- package/spec/mcp.md +27 -3
- package/spec/output-schemas/ask.schema.json +8 -0
- package/spec/output-schemas/claim-verification.schema.json +8 -2
- package/spec/output-schemas/context-capsule-v1.schema.json +6 -1
- package/spec/output-schemas/context-capsule-verification.schema.json +4 -2
- package/spec/output-schemas/get.schema.json +4 -0
- package/spec/output-schemas/metadata-summary.schema.json +60 -0
- package/spec/output-schemas/multi-get.schema.json +4 -0
- package/spec/output-schemas/search-result.schema.json +4 -0
- package/spec/output-schemas/search-results.schema.json +4 -0
- package/src/app/constants.ts +16 -0
- package/src/app/context-format.ts +1 -0
- package/src/app/verified-ask.ts +2 -0
- package/src/cli/commands/agents/block.ts +9 -8
- package/src/cli/commands/ask-format.ts +18 -1
- package/src/cli/commands/ask.ts +4 -5
- package/src/cli/commands/daemon.ts +5 -1
- package/src/cli/commands/get.ts +11 -1
- package/src/cli/commands/metadata.ts +79 -0
- package/src/cli/commands/multi-get.ts +13 -0
- package/src/cli/commands/shared.ts +9 -7
- package/src/cli/format/search-results.ts +11 -0
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +45 -0
- package/src/core/context-capsule-schema.ts +2 -0
- package/src/core/context-capsule-verification.ts +2 -0
- package/src/core/context-compiler.ts +2 -5
- package/src/core/context-evidence.ts +1 -0
- package/src/core/context-verifier.ts +14 -1
- package/src/core/metadata-discovery.ts +169 -0
- package/src/ingestion/sync.ts +49 -1
- package/src/ingestion/walker.ts +11 -3
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tool-descriptions-core.ts +2 -2
- package/src/mcp/tool-profile.ts +16 -1
- package/src/mcp/tools/get.ts +10 -0
- package/src/mcp/tools/index.ts +39 -4
- package/src/mcp/tools/multi-get.ts +15 -0
- package/src/mcp/tools/query.ts +5 -3
- package/src/mcp/tools/search.ts +5 -3
- package/src/mcp/tools/vsearch.ts +5 -3
- package/src/mcp/tools/workspace-write.ts +42 -34
- package/src/pipeline/answer-prompt.ts +15 -2
- package/src/pipeline/answer.ts +3 -0
- package/src/pipeline/claim-verification-schema.ts +6 -1
- package/src/pipeline/claim-verification.ts +54 -1
- package/src/pipeline/explain.ts +1 -1
- package/src/pipeline/fusion.ts +24 -4
- package/src/pipeline/hybrid.ts +5 -1
- package/src/pipeline/result-context.ts +29 -0
- package/src/pipeline/search.ts +5 -1
- package/src/pipeline/types.ts +4 -0
- package/src/pipeline/vsearch.ts +5 -1
- package/src/sdk/client.ts +32 -4
- package/src/sdk/documents.ts +5 -0
- package/src/sdk/types.ts +5 -0
- package/src/serve/routes/api.ts +29 -1
- package/src/serve/server.ts +12 -0
- package/src/store/sqlite/adapter.ts +82 -2
- package/src/store/types.ts +16 -0
- package/src/store/vector/variant-search.ts +112 -74
- package/browser-extension/artifacts/gno-browser-clipper-v2.10.1.zip.sha256 +0 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed metadata discovery: which keys the indexed documents carry, with
|
|
3
|
+
* their observed types, document counts and most common values, so a caller
|
|
4
|
+
* can write a `filter` predicate for metadata it has not seen.
|
|
5
|
+
*
|
|
6
|
+
* @module src/core/metadata-discovery
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type MetadataValue = string | number | boolean;
|
|
10
|
+
|
|
11
|
+
export interface MetadataKeySummary {
|
|
12
|
+
key: string;
|
|
13
|
+
/** Observed value types, e.g. `string`, `number`, `string[]`. */
|
|
14
|
+
types: string[];
|
|
15
|
+
/** Documents that carry the key. */
|
|
16
|
+
documents: number;
|
|
17
|
+
/** Most common values (array members count individually), count desc. */
|
|
18
|
+
values: { value: MetadataValue; documents: number }[];
|
|
19
|
+
/** True when more distinct values exist than `values` lists. */
|
|
20
|
+
valuesTruncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MetadataSummary {
|
|
24
|
+
collection: string | null;
|
|
25
|
+
/** Active documents in scope, and how many carry typed metadata. */
|
|
26
|
+
documents: number;
|
|
27
|
+
documentsWithMetadata: number;
|
|
28
|
+
keys: MetadataKeySummary[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_METADATA_VALUE_LIMIT = 10;
|
|
32
|
+
export const MAX_METADATA_VALUE_LIMIT = 100;
|
|
33
|
+
|
|
34
|
+
const typeOf = (value: unknown): string | null => {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const member = value.length > 0 ? typeOf(value[0]) : "string";
|
|
37
|
+
return member ? `${member}[]` : null;
|
|
38
|
+
}
|
|
39
|
+
return typeof value === "string" ||
|
|
40
|
+
typeof value === "number" ||
|
|
41
|
+
typeof value === "boolean"
|
|
42
|
+
? typeof value
|
|
43
|
+
: null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Summarize stored typed metadata JSON. Keys sort by document count then
|
|
48
|
+
* name; values by document count then their JSON text, so output is stable.
|
|
49
|
+
*/
|
|
50
|
+
export function summarizeTypedMetadata(
|
|
51
|
+
input: {
|
|
52
|
+
collection: string | null;
|
|
53
|
+
documents: number;
|
|
54
|
+
metadata: string[];
|
|
55
|
+
},
|
|
56
|
+
valueLimit = DEFAULT_METADATA_VALUE_LIMIT
|
|
57
|
+
): MetadataSummary {
|
|
58
|
+
const keys = new Map<
|
|
59
|
+
string,
|
|
60
|
+
{ types: Set<string>; documents: number; values: Map<string, number> }
|
|
61
|
+
>();
|
|
62
|
+
let documentsWithMetadata = 0;
|
|
63
|
+
for (const json of input.metadata) {
|
|
64
|
+
let parsed: unknown;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(json);
|
|
67
|
+
} catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
71
|
+
continue;
|
|
72
|
+
const entries = Object.entries(parsed);
|
|
73
|
+
if (entries.length === 0) continue;
|
|
74
|
+
documentsWithMetadata += 1;
|
|
75
|
+
for (const [key, value] of entries) {
|
|
76
|
+
const type = typeOf(value);
|
|
77
|
+
if (!type) continue;
|
|
78
|
+
const summary = keys.get(key) ?? {
|
|
79
|
+
types: new Set<string>(),
|
|
80
|
+
documents: 0,
|
|
81
|
+
values: new Map<string, number>(),
|
|
82
|
+
};
|
|
83
|
+
summary.types.add(type);
|
|
84
|
+
summary.documents += 1;
|
|
85
|
+
const members = new Set(
|
|
86
|
+
(Array.isArray(value) ? value : [value]).map((member) =>
|
|
87
|
+
JSON.stringify(member)
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
for (const member of members)
|
|
91
|
+
summary.values.set(member, (summary.values.get(member) ?? 0) + 1);
|
|
92
|
+
keys.set(key, summary);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
collection: input.collection,
|
|
97
|
+
documents: input.documents,
|
|
98
|
+
documentsWithMetadata,
|
|
99
|
+
keys: [...keys.entries()]
|
|
100
|
+
.map(([key, summary]) => {
|
|
101
|
+
const values = [...summary.values.entries()].sort(
|
|
102
|
+
([leftText, left], [rightText, right]) =>
|
|
103
|
+
right - left || (leftText < rightText ? -1 : 1)
|
|
104
|
+
);
|
|
105
|
+
return {
|
|
106
|
+
key,
|
|
107
|
+
types: [...summary.types].sort(),
|
|
108
|
+
documents: summary.documents,
|
|
109
|
+
values: values.slice(0, valueLimit).map(([text, documents]) => ({
|
|
110
|
+
value: JSON.parse(text) as MetadataValue,
|
|
111
|
+
documents,
|
|
112
|
+
})),
|
|
113
|
+
valuesTruncated: values.length > valueLimit,
|
|
114
|
+
};
|
|
115
|
+
})
|
|
116
|
+
.sort(
|
|
117
|
+
(left, right) =>
|
|
118
|
+
right.documents - left.documents || (left.key < right.key ? -1 : 1)
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Shared read for every surface; `values` caps listed values per key. */
|
|
124
|
+
export async function readMetadataSummary(
|
|
125
|
+
store: {
|
|
126
|
+
getTypedMetadataDocuments(options?: {
|
|
127
|
+
collection?: string;
|
|
128
|
+
}): Promise<
|
|
129
|
+
| { ok: true; value: { documents: number; metadata: string[] } }
|
|
130
|
+
| { ok: false; error: { message: string } }
|
|
131
|
+
>;
|
|
132
|
+
},
|
|
133
|
+
options: { collection?: string; values?: number } = {}
|
|
134
|
+
): Promise<MetadataSummary> {
|
|
135
|
+
const values = options.values ?? DEFAULT_METADATA_VALUE_LIMIT;
|
|
136
|
+
if (
|
|
137
|
+
!Number.isInteger(values) ||
|
|
138
|
+
values < 1 ||
|
|
139
|
+
values > MAX_METADATA_VALUE_LIMIT
|
|
140
|
+
)
|
|
141
|
+
throw new RangeError(
|
|
142
|
+
`values must be an integer from 1 to ${MAX_METADATA_VALUE_LIMIT}`
|
|
143
|
+
);
|
|
144
|
+
const result = await store.getTypedMetadataDocuments({
|
|
145
|
+
collection: options.collection,
|
|
146
|
+
});
|
|
147
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
148
|
+
return summarizeTypedMetadata(
|
|
149
|
+
{ collection: options.collection ?? null, ...result.value },
|
|
150
|
+
values
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Plain-text rendering shared by the CLI terminal output and MCP text. */
|
|
155
|
+
export function formatMetadataSummary(summary: MetadataSummary): string {
|
|
156
|
+
const scope = summary.collection ? ` in ${summary.collection}` : "";
|
|
157
|
+
const header = `${summary.documentsWithMetadata} of ${summary.documents} documents${scope} carry typed metadata`;
|
|
158
|
+
if (summary.keys.length === 0) return `${header}.`;
|
|
159
|
+
const lines = [`${header}:`, ""];
|
|
160
|
+
for (const key of summary.keys) {
|
|
161
|
+
const values = key.values
|
|
162
|
+
.map((item) => `${JSON.stringify(item.value)} (${item.documents})`)
|
|
163
|
+
.join(", ");
|
|
164
|
+
lines.push(
|
|
165
|
+
`${key.key} [${key.types.join(", ")}] ${key.documents} ${key.documents === 1 ? "doc" : "docs"}${values ? `: ${values}${key.valuesTruncated ? ", ..." : ""}` : ""}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// node:fs/promises for realpath/stat (no Bun equivalent for canonical paths or file stats)
|
|
2
|
-
|
|
2
|
+
// opendir proves a root can be listed; Bun has no directory-handle API.
|
|
3
|
+
import { opendir, realpath, stat } from "node:fs/promises";
|
|
3
4
|
/**
|
|
4
5
|
* Sync service - orchestrates file ingestion.
|
|
5
6
|
* Walks collections, converts files, chunks content, updates store.
|
|
@@ -389,6 +390,22 @@ class Semaphore {
|
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
|
|
393
|
+
/** Null when the collection root can be listed; otherwise why it cannot. */
|
|
394
|
+
async function unavailableRootMessage(root: string): Promise<string | null> {
|
|
395
|
+
try {
|
|
396
|
+
const directory = await opendir(root);
|
|
397
|
+
try {
|
|
398
|
+
// The open is lazy; the first read is what needs list permission.
|
|
399
|
+
await directory.read();
|
|
400
|
+
} finally {
|
|
401
|
+
await directory.close();
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
} catch (cause) {
|
|
405
|
+
return `Collection root could not be resolved (${cause instanceof Error ? cause.message : String(cause)}); indexed documents were kept`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
392
409
|
function summarizePathResults(
|
|
393
410
|
collection: string,
|
|
394
411
|
results: FileSyncResult[],
|
|
@@ -1174,6 +1191,7 @@ export class SyncService {
|
|
|
1174
1191
|
const directoryAvailability = memoizeDirectoryAvailability(
|
|
1175
1192
|
this.directoryAvailabilityFactory(availabilityMode)
|
|
1176
1193
|
);
|
|
1194
|
+
let rootUnavailable: string | null = null;
|
|
1177
1195
|
|
|
1178
1196
|
for (const relPath of relPaths) {
|
|
1179
1197
|
const recordDocumentsResult = await store.listRecordDocuments(
|
|
@@ -1285,6 +1303,19 @@ export class SyncService {
|
|
|
1285
1303
|
});
|
|
1286
1304
|
continue;
|
|
1287
1305
|
}
|
|
1306
|
+
// A file is only proven deleted while its collection root can be
|
|
1307
|
+
// listed. Checked per absence: the root can vanish mid-batch.
|
|
1308
|
+
const rootError = await unavailableRootMessage(collection.path);
|
|
1309
|
+
if (rootError) {
|
|
1310
|
+
rootUnavailable = rootError;
|
|
1311
|
+
results.push({
|
|
1312
|
+
relPath,
|
|
1313
|
+
status: "error",
|
|
1314
|
+
errorCode: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
1315
|
+
errorMessage: rootError,
|
|
1316
|
+
});
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1288
1319
|
const inactive = await this.inactivateOneAbsentSource(
|
|
1289
1320
|
collection,
|
|
1290
1321
|
store,
|
|
@@ -1436,6 +1467,23 @@ export class SyncService {
|
|
|
1436
1467
|
syncOptions.projectTypedEdges === false
|
|
1437
1468
|
? []
|
|
1438
1469
|
: await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
|
|
1470
|
+
if (rootUnavailable) {
|
|
1471
|
+
// Collection-level, as a full sync reports it, so status, summaries and
|
|
1472
|
+
// the watcher's error path all carry the root and the original error.
|
|
1473
|
+
errors.unshift({
|
|
1474
|
+
relPath: "",
|
|
1475
|
+
code: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
1476
|
+
message: rootUnavailable,
|
|
1477
|
+
});
|
|
1478
|
+
await store
|
|
1479
|
+
.recordError({
|
|
1480
|
+
collection: collection.name,
|
|
1481
|
+
relPath: "",
|
|
1482
|
+
code: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
1483
|
+
message: rootUnavailable,
|
|
1484
|
+
})
|
|
1485
|
+
.catch(() => undefined);
|
|
1486
|
+
}
|
|
1439
1487
|
return {
|
|
1440
1488
|
...summarizePathResults(
|
|
1441
1489
|
collection.name,
|
package/src/ingestion/walker.ts
CHANGED
|
@@ -289,9 +289,17 @@ export class FileWalker implements WalkerPort {
|
|
|
289
289
|
let rootReal: string;
|
|
290
290
|
try {
|
|
291
291
|
rootReal = await realpath(rootAbs);
|
|
292
|
-
} catch {
|
|
293
|
-
//
|
|
294
|
-
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// An unmounted drive or offline sync folder proves nothing about its
|
|
294
|
+
// files, so the root is an unproven prefix, never an empty listing.
|
|
295
|
+
skipped.push({
|
|
296
|
+
absPath: rootAbs,
|
|
297
|
+
relPath: "",
|
|
298
|
+
reason: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
299
|
+
unprovenPrefix: true,
|
|
300
|
+
message: `Collection root could not be resolved (${error instanceof Error ? error.message : String(error)}); indexed documents were kept`,
|
|
301
|
+
});
|
|
302
|
+
return { entries, skipped };
|
|
295
303
|
}
|
|
296
304
|
|
|
297
305
|
const matches = new Set<string>();
|
package/src/mcp/http-egress.ts
CHANGED
|
@@ -18,9 +18,9 @@ import { type McpToolProfile, mcpToolProfileAllowlist } from "./tool-profile";
|
|
|
18
18
|
|
|
19
19
|
export const MCP_CORE_TOOL_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
|
20
20
|
gno_query:
|
|
21
|
-
"Call first for a question about the indexed documents when the answer may be worded differently from the question. Runs hybrid retrieval: BM25 plus vector search, fused, with bounded one-hop graph expansion and optional query expansion and reranking. Returns ranked results, each with uri, docid, score, snippet, and usually a line anchor to read next with gno_get fromLine/lineCount; meta reports the mode actually used (bm25_only when vectors are unavailable) and whether expansion and reranking ran.
|
|
21
|
+
"Call first for a question about the indexed documents when the answer may be worded differently from the question. Runs hybrid retrieval: BM25 plus vector search, fused, with bounded one-hop graph expansion and optional query expansion and reranking. Returns ranked results, each with uri, docid, score, snippet, and usually a line anchor to read next with gno_get fromLine/lineCount; meta reports the mode actually used (bm25_only when vectors are unavailable) and whether expansion and reranking ran. Start with fast=true (no expansion or reranking, typically under a second on a warm server); when that misses, call again without fast to add expansion and reranking; set thorough=true when recall matters and intent to disambiguate a short term. A context field on a result is configured guidance; cite the source lines.",
|
|
22
22
|
gno_search:
|
|
23
|
-
"Call when you know the exact words: a name, identifier, filename, error message, or quoted phrase. Runs BM25 keyword matching only, so it needs no model and answers fast. Returns ranked results with uri, docid, score, snippet, and a line anchor when available; the match sits at that line, so read it with gno_get fromLine/lineCount.
|
|
23
|
+
"Call when you know the exact words: a name, identifier, filename, error message, or quoted phrase. Runs BM25 keyword matching only, so it needs no model and answers fast. Returns ranked results with uri, docid, score, snippet, and a line anchor when available; the match sits at that line, so read it with gno_get fromLine/lineCount. For a question asked in words, call gno_query with fast=true instead.",
|
|
24
24
|
gno_get:
|
|
25
25
|
"Call to read one document a result named: pass its gno:// URI, #docid, or collection/path as ref. With fromLine and lineCount it returns only that range; start from the result's line anchor with a small count before fetching the whole file. Returns the content with line numbers plus uri, docid, title, totalLines, returnedLines, and the source path and modifiedAt.",
|
|
26
26
|
gno_multi_get:
|
package/src/mcp/tool-profile.ts
CHANGED
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
* @module src/mcp/tool-profile
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
20
22
|
import { withInferenceScope } from "../llm/inference-scope";
|
|
21
23
|
|
|
22
24
|
export const MCP_TOOL_PROFILES = ["core", "full"] as const;
|
|
@@ -129,6 +131,19 @@ export function createProfileToolRegistrar(
|
|
|
129
131
|
: (cb as ToolCallback<StandardSchemaWithJSON>)(args[0], request)
|
|
130
132
|
);
|
|
131
133
|
};
|
|
132
|
-
server.registerTool(
|
|
134
|
+
server.registerTool(
|
|
135
|
+
name,
|
|
136
|
+
{ ...config, inputSchema: rejectUnknownKeys(config.inputSchema) },
|
|
137
|
+
wrapped as unknown as typeof cb
|
|
138
|
+
);
|
|
133
139
|
};
|
|
134
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Unknown argument keys fail validation with the key named instead of being
|
|
144
|
+
* dropped: a misspelled scope or receipt key must not silently widen a read
|
|
145
|
+
* or skip a write fence.
|
|
146
|
+
*/
|
|
147
|
+
function rejectUnknownKeys<T>(schema: T): T {
|
|
148
|
+
return (schema instanceof z.ZodObject ? schema.strict() : schema) as T;
|
|
149
|
+
}
|
package/src/mcp/tools/get.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
evidenceFromExactDocument,
|
|
26
26
|
RetrievalTraceSession,
|
|
27
27
|
} from "../../core/retrieval-trace-session";
|
|
28
|
+
import { supersedingUri } from "../../pipeline/result-context";
|
|
28
29
|
import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
|
|
29
30
|
import { runTool, type ToolResult } from "./index";
|
|
30
31
|
|
|
@@ -59,6 +60,8 @@ interface GetResponse {
|
|
|
59
60
|
mirrorHash?: string;
|
|
60
61
|
};
|
|
61
62
|
record?: RecordEvidenceMetadata;
|
|
63
|
+
/** URI of the current fact when a newer memory fact superseded this one. */
|
|
64
|
+
supersededBy?: string;
|
|
62
65
|
capabilities: DocumentCapabilities;
|
|
63
66
|
}
|
|
64
67
|
|
|
@@ -104,6 +107,9 @@ function formatGetResponse(data: GetResponse): string {
|
|
|
104
107
|
if (data.title) {
|
|
105
108
|
lines.push(`Title: ${data.title}`);
|
|
106
109
|
}
|
|
110
|
+
if (data.supersededBy) {
|
|
111
|
+
lines.push(`Superseded by: ${data.supersededBy}`);
|
|
112
|
+
}
|
|
107
113
|
lines.push(`Lines: ${data.totalLines}`);
|
|
108
114
|
if (data.source.absPath) {
|
|
109
115
|
lines.push(`Path: ${data.source.absPath}`);
|
|
@@ -226,7 +232,11 @@ export function handleGet(
|
|
|
226
232
|
}
|
|
227
233
|
}
|
|
228
234
|
|
|
235
|
+
const successor = await supersedingUri(scoped.store, doc.uri);
|
|
229
236
|
const response: GetResponse = {
|
|
237
|
+
...(successor
|
|
238
|
+
? { supersededBy: decorateUriForIndex(successor, scoped.indexName) }
|
|
239
|
+
: {}),
|
|
230
240
|
docid: doc.docid,
|
|
231
241
|
uri: decorateUriForIndex(doc.uri, scoped.indexName),
|
|
232
242
|
title: doc.title ?? undefined,
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
compiledContextCheckSchema,
|
|
22
22
|
} from "../../core/compiled-context";
|
|
23
23
|
import { withoutHostPaths } from "../../core/host-paths";
|
|
24
|
+
import {
|
|
25
|
+
formatMetadataSummary,
|
|
26
|
+
MAX_METADATA_VALUE_LIMIT,
|
|
27
|
+
readMetadataSummary,
|
|
28
|
+
} from "../../core/metadata-discovery";
|
|
24
29
|
import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
|
|
25
30
|
import { RETRIEVAL_TRACE_METADATA } from "../../core/retrieval-trace-session";
|
|
26
31
|
import { normalizeTag } from "../../core/tags";
|
|
@@ -170,11 +175,11 @@ export function normalizeTagFilters(tags?: string[]): string[] | undefined {
|
|
|
170
175
|
|
|
171
176
|
export const MCP_TOOL_DESCRIPTIONS = {
|
|
172
177
|
search:
|
|
173
|
-
"BM25 keyword search. Fast exact-term lookup for names, identifiers, error text, and known phrases. Structured results include uri/docid, line when available, and optional user-configured context guidance; use gno_get with fromLine/lineCount or gno_multi_get for full context.
|
|
178
|
+
"BM25 keyword search. Fast exact-term lookup for names, identifiers, error text, and known phrases. Structured results include uri/docid, line when available, and optional user-configured context guidance; use gno_get with fromLine/lineCount or gno_multi_get for full context. For natural-language questions use gno_query (fast=true first).",
|
|
174
179
|
vsearch:
|
|
175
|
-
"Vector semantic search. Finds conceptually similar docs with different wording. Structured results preserve optional user-configured context guidance. Best after embeddings are current; use intent to disambiguate short terms.
|
|
180
|
+
"Vector semantic search. Finds conceptually similar docs with different wording. Structured results preserve optional user-configured context guidance. Best after embeddings are current; use intent to disambiguate short terms. For questions, gno_query with fast=true adds keyword matching at about the same cost.",
|
|
176
181
|
query:
|
|
177
|
-
"Hybrid search (BM25 + vector + default bounded graph expansion + optional query expansion/reranking). Recommended default. Structured results preserve optional user-configured context guidance with source identity. Use intent for ambiguous terms, queryModes to combine term/intent/hyde strategies,
|
|
182
|
+
"Hybrid search (BM25 + vector + default bounded graph expansion + optional query expansion/reranking). Recommended default for natural-language questions: start with fast=true (hybrid without expansion/rerank, typically under a second on a warm server); when that misses, retry without fast (adds expansion and reranking, a few seconds); thorough=true when recall matters. Structured results preserve optional user-configured context guidance with source identity. Use intent for ambiguous terms, queryModes to combine term/intent/hyde strategies, graph=false to skip graph expansion, and candidateLimit to trade latency for coverage.",
|
|
178
183
|
queryDiagnose:
|
|
179
184
|
"Diagnose why one target document does or does not appear for a query. Use when an important doc is missing, a filter may exclude it, or you need stage-by-stage BM25/vector/fusion/graph/rerank evidence before changing retrieval strategy.",
|
|
180
185
|
get: "Retrieve one document by gno:// URI, docid (#abc123), or collection/path. After search results include line, pass fromLine and lineCount to fetch only the relevant range before expanding to the full document.",
|
|
@@ -632,7 +637,7 @@ export const queryInputSchema = z.object({
|
|
|
632
637
|
.boolean()
|
|
633
638
|
.default(false)
|
|
634
639
|
.describe(
|
|
635
|
-
"Skip query expansion, graph expansion, and reranking (
|
|
640
|
+
"Skip query expansion, graph expansion, and reranking (typically under a second on a warm server). Use first for natural-language questions"
|
|
636
641
|
),
|
|
637
642
|
thorough: z
|
|
638
643
|
.boolean()
|
|
@@ -756,6 +761,20 @@ const listJobsInputSchema = z.object({
|
|
|
756
761
|
.describe("Max jobs to return"),
|
|
757
762
|
});
|
|
758
763
|
|
|
764
|
+
const listMetadataInputSchema = z.object({
|
|
765
|
+
collection: z
|
|
766
|
+
.string()
|
|
767
|
+
.optional()
|
|
768
|
+
.describe("Only documents in this collection"),
|
|
769
|
+
values: z
|
|
770
|
+
.number()
|
|
771
|
+
.int()
|
|
772
|
+
.min(1)
|
|
773
|
+
.max(MAX_METADATA_VALUE_LIMIT)
|
|
774
|
+
.optional()
|
|
775
|
+
.describe("Common values listed per key (default 10)"),
|
|
776
|
+
});
|
|
777
|
+
|
|
759
778
|
const listTagsInputSchema = z.object({
|
|
760
779
|
collection: z
|
|
761
780
|
.string()
|
|
@@ -1373,6 +1392,22 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1373
1392
|
(args) => handleTraceShow(args, ctx)
|
|
1374
1393
|
);
|
|
1375
1394
|
|
|
1395
|
+
registerTool(
|
|
1396
|
+
"gno_list_metadata",
|
|
1397
|
+
{
|
|
1398
|
+
description:
|
|
1399
|
+
"List the typed metadata keys indexed documents carry, with observed types, document counts and the most common values. Call before writing a filter predicate on custom metadata you have not seen.",
|
|
1400
|
+
inputSchema: listMetadataInputSchema,
|
|
1401
|
+
},
|
|
1402
|
+
(args) =>
|
|
1403
|
+
runTool(
|
|
1404
|
+
ctx,
|
|
1405
|
+
"gno_list_metadata",
|
|
1406
|
+
() => readMetadataSummary(ctx.store, args),
|
|
1407
|
+
formatMetadataSummary
|
|
1408
|
+
)
|
|
1409
|
+
);
|
|
1410
|
+
|
|
1376
1411
|
registerTool(
|
|
1377
1412
|
"gno_list_tags",
|
|
1378
1413
|
{
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type RecordEvidenceMetadata,
|
|
17
17
|
} from "../../core/record-metadata";
|
|
18
18
|
import { parseRef } from "../../core/ref-parser";
|
|
19
|
+
import { supersedingUri } from "../../pipeline/result-context";
|
|
19
20
|
import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
|
|
20
21
|
import { runTool, type ToolResult } from "./index";
|
|
21
22
|
|
|
@@ -27,6 +28,8 @@ interface MultiGetInput {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
interface DocumentResult {
|
|
31
|
+
/** URI of the current fact when a newer memory fact superseded this one. */
|
|
32
|
+
supersededBy?: string;
|
|
30
33
|
docid: string;
|
|
31
34
|
uri: string;
|
|
32
35
|
title?: string;
|
|
@@ -108,6 +111,9 @@ function formatMultiGetResponse(data: MultiGetResponse): string {
|
|
|
108
111
|
if (doc.title) {
|
|
109
112
|
lines.push(`Title: ${doc.title}`);
|
|
110
113
|
}
|
|
114
|
+
if (doc.supersededBy) {
|
|
115
|
+
lines.push(`Superseded by: ${doc.supersededBy}`);
|
|
116
|
+
}
|
|
111
117
|
lines.push(
|
|
112
118
|
`Lines: ${doc.totalLines}${doc.truncated ? " (truncated)" : ""}`
|
|
113
119
|
);
|
|
@@ -250,7 +256,16 @@ export function handleMultiGet(
|
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
|
|
259
|
+
const successor = await supersedingUri(scoped.store, doc.uri);
|
|
253
260
|
documents.push({
|
|
261
|
+
...(successor
|
|
262
|
+
? {
|
|
263
|
+
supersededBy: decorateUriForIndex(
|
|
264
|
+
successor,
|
|
265
|
+
scoped.indexName
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
254
269
|
docid: doc.docid,
|
|
255
270
|
uri: decorateUriForIndex(doc.uri, scoped.indexName),
|
|
256
271
|
title: doc.title ?? undefined,
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
import type { VectorIndexPort } from "../../store/vector";
|
|
21
21
|
import type { ToolContext } from "../server";
|
|
22
22
|
|
|
23
|
-
import {
|
|
23
|
+
import { decorateResultUris, parseUri } from "../../app/constants";
|
|
24
24
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
25
25
|
import {
|
|
26
26
|
fingerprintContentTypeMetadataRules,
|
|
@@ -103,8 +103,7 @@ function enrichWithAbsPath(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
return {
|
|
106
|
-
...r,
|
|
107
|
-
uri: decorateUriForIndex(r.uri, ctx.indexName),
|
|
106
|
+
...decorateResultUris(r, ctx.indexName),
|
|
108
107
|
source: {
|
|
109
108
|
...r.source,
|
|
110
109
|
absPath: pathJoin(collection.path, r.source.relPath),
|
|
@@ -144,6 +143,9 @@ function formatSearchResults(data: SearchResults): string {
|
|
|
144
143
|
if (r.title) {
|
|
145
144
|
lines.push(` Title: ${r.title}`);
|
|
146
145
|
}
|
|
146
|
+
if (r.supersededBy) {
|
|
147
|
+
lines.push(` Superseded by: ${r.supersededBy}`);
|
|
148
|
+
}
|
|
147
149
|
if (r.snippet) {
|
|
148
150
|
const snippetPreview = r.snippet.slice(0, 200).replace(/\n/g, " ");
|
|
149
151
|
lines.push(` ${snippetPreview}${r.snippet.length > 200 ? "..." : ""}`);
|
package/src/mcp/tools/search.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
|
|
|
10
10
|
import type { SearchResult, SearchResults } from "../../pipeline/types";
|
|
11
11
|
import type { ToolContext } from "../server";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { decorateResultUris, parseUri } from "../../app/constants";
|
|
14
14
|
import { normalizeContentTypes } from "../../config";
|
|
15
15
|
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
16
16
|
import {
|
|
@@ -66,8 +66,7 @@ function enrichWithAbsPath(
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return {
|
|
69
|
-
...r,
|
|
70
|
-
uri: decorateUriForIndex(r.uri, ctx.indexName),
|
|
69
|
+
...decorateResultUris(r, ctx.indexName),
|
|
71
70
|
source: {
|
|
72
71
|
...r.source,
|
|
73
72
|
absPath: pathJoin(collection.path, r.source.relPath),
|
|
@@ -93,6 +92,9 @@ function formatSearchResults(data: SearchResults): string {
|
|
|
93
92
|
if (r.title) {
|
|
94
93
|
lines.push(` Title: ${r.title}`);
|
|
95
94
|
}
|
|
95
|
+
if (r.supersededBy) {
|
|
96
|
+
lines.push(` Superseded by: ${r.supersededBy}`);
|
|
97
|
+
}
|
|
96
98
|
if (r.snippet) {
|
|
97
99
|
const snippetPreview = r.snippet.slice(0, 200).replace(/\n/g, " ");
|
|
98
100
|
lines.push(` ${snippetPreview}${r.snippet.length > 200 ? "..." : ""}`);
|
package/src/mcp/tools/vsearch.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { EmbeddingPort } from "../../llm/types";
|
|
|
10
10
|
import type { SearchResult, SearchResults } from "../../pipeline/types";
|
|
11
11
|
import type { ToolContext } from "../server";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { decorateResultUris, parseUri } from "../../app/constants";
|
|
14
14
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
15
15
|
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
16
16
|
import {
|
|
@@ -77,8 +77,7 @@ function enrichWithAbsPath(
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
return {
|
|
80
|
-
...r,
|
|
81
|
-
uri: decorateUriForIndex(r.uri, ctx.indexName),
|
|
80
|
+
...decorateResultUris(r, ctx.indexName),
|
|
82
81
|
source: {
|
|
83
82
|
...r.source,
|
|
84
83
|
absPath: pathJoin(collection.path, r.source.relPath),
|
|
@@ -106,6 +105,9 @@ function formatSearchResults(data: SearchResults): string {
|
|
|
106
105
|
if (r.title) {
|
|
107
106
|
lines.push(` Title: ${r.title}`);
|
|
108
107
|
}
|
|
108
|
+
if (r.supersededBy) {
|
|
109
|
+
lines.push(` Superseded by: ${r.supersededBy}`);
|
|
110
|
+
}
|
|
109
111
|
if (r.snippet) {
|
|
110
112
|
const snippetPreview = r.snippet.slice(0, 200).replace(/\n/g, " ");
|
|
111
113
|
lines.push(` ${snippetPreview}${r.snippet.length > 200 ? "..." : ""}`);
|
|
@@ -53,43 +53,51 @@ const refactorPreviewActionSchema = z.literal("preview");
|
|
|
53
53
|
const refactorApplyActionSchema = z.literal("apply");
|
|
54
54
|
|
|
55
55
|
export const renameNoteInputSchema = z.discriminatedUnion("action", [
|
|
56
|
-
z
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.string()
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
z
|
|
57
|
+
.object({
|
|
58
|
+
action: refactorPreviewActionSchema,
|
|
59
|
+
ref: z.string().min(1, "ref cannot be empty"),
|
|
60
|
+
name: z.string().min(1, "name cannot be empty"),
|
|
61
|
+
})
|
|
62
|
+
.strict(),
|
|
63
|
+
z
|
|
64
|
+
.object({
|
|
65
|
+
action: refactorApplyActionSchema,
|
|
66
|
+
ref: z.string().min(1, "ref cannot be empty"),
|
|
67
|
+
name: z.string().min(1, "name cannot be empty"),
|
|
68
|
+
schemaVersion: z.literal(FILE_REFACTOR_SCHEMA_VERSION),
|
|
69
|
+
planDigest: z
|
|
70
|
+
.string()
|
|
71
|
+
.regex(/^[a-f0-9]{64}$/, "planDigest must be a lowercase SHA-256"),
|
|
72
|
+
confirmation: z.literal(FILE_REFACTOR_APPLY_CONFIRMATION),
|
|
73
|
+
confirm: z.literal(true),
|
|
74
|
+
})
|
|
75
|
+
.strict(),
|
|
72
76
|
]);
|
|
73
77
|
|
|
74
78
|
export const moveNoteInputSchema = z.discriminatedUnion("action", [
|
|
75
|
-
z
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.string()
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
z
|
|
80
|
+
.object({
|
|
81
|
+
action: refactorPreviewActionSchema,
|
|
82
|
+
ref: z.string().min(1, "ref cannot be empty"),
|
|
83
|
+
folderPath: z.string().min(1, "folderPath cannot be empty"),
|
|
84
|
+
name: z.string().optional(),
|
|
85
|
+
})
|
|
86
|
+
.strict(),
|
|
87
|
+
z
|
|
88
|
+
.object({
|
|
89
|
+
action: refactorApplyActionSchema,
|
|
90
|
+
ref: z.string().min(1, "ref cannot be empty"),
|
|
91
|
+
folderPath: z.string().min(1, "folderPath cannot be empty"),
|
|
92
|
+
name: z.string().optional(),
|
|
93
|
+
schemaVersion: z.literal(FILE_REFACTOR_SCHEMA_VERSION),
|
|
94
|
+
planDigest: z
|
|
95
|
+
.string()
|
|
96
|
+
.regex(/^[a-f0-9]{64}$/, "planDigest must be a lowercase SHA-256"),
|
|
97
|
+
confirmation: z.literal(FILE_REFACTOR_APPLY_CONFIRMATION),
|
|
98
|
+
confirm: z.literal(true),
|
|
99
|
+
})
|
|
100
|
+
.strict(),
|
|
93
101
|
]);
|
|
94
102
|
|
|
95
103
|
export type RenameNoteInput = z.infer<typeof renameNoteInputSchema>;
|