@gmickel/gno 2.12.2 → 2.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 +1 -1
- package/assets/skill/SKILL.md +1 -1
- package/assets/skill/cli-reference.md +23 -10
- package/assets/skill/mcp-reference.md +2 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.12.2.zip → gno-browser-clipper-v2.13.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.13.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +66 -5
- package/spec/mcp.md +96 -1
- package/spec/output-schemas/convert.schema.json +97 -0
- package/spec/output-schemas/mcp-job-status.schema.json +17 -1
- package/spec/output-schemas/mcp-sync-result.schema.json +9 -2
- package/spec/output-schemas/search-results.schema.json +7 -0
- package/src/cli/commands/index-cmd.ts +50 -12
- package/src/cli/commands/query.ts +21 -8
- package/src/cli/commands/search.ts +11 -0
- package/src/cli/commands/vsearch.ts +17 -4
- package/src/cli/options.ts +19 -0
- package/src/cli/program.ts +79 -10
- package/src/core/collection-scope.ts +105 -0
- package/src/core/errors.ts +5 -0
- package/src/core/job-manager.ts +238 -19
- package/src/core/reindex-paths.ts +167 -0
- package/src/core/retrieval-trace-request.ts +1 -0
- package/src/ingestion/convert-file.ts +181 -0
- package/src/llm/registry.ts +20 -0
- package/src/mcp/http-egress.ts +21 -2
- package/src/mcp/tools/collection-scope.ts +25 -0
- package/src/mcp/tools/index-cmd.ts +66 -48
- package/src/mcp/tools/index.ts +74 -10
- package/src/mcp/tools/job-status.ts +7 -0
- package/src/mcp/tools/query.ts +28 -7
- package/src/mcp/tools/search.ts +4 -0
- package/src/mcp/tools/sync.ts +68 -60
- package/src/mcp/tools/vsearch.ts +9 -3
- package/src/pipeline/filters.ts +18 -3
- package/src/pipeline/graph-retrieval.ts +1 -0
- package/src/pipeline/hybrid.ts +15 -2
- package/src/pipeline/hydration.ts +7 -2
- package/src/pipeline/owner-fusion.ts +1 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +7 -0
- package/src/pipeline/vsearch.ts +10 -5
- package/src/sdk/client.ts +39 -11
- package/src/sdk/convert.ts +27 -0
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +1 -1
- package/src/serve/jobs.ts +4 -1
- package/src/serve/public/components/IndexingProgress.tsx +25 -3
- package/src/serve/routes/api.ts +164 -24
- package/src/store/sqlite/adapter.ts +97 -27
- package/src/store/sqlite/eligibility.ts +4 -0
- package/src/store/types.ts +7 -0
- package/src/store/vector/sqlite-vec.ts +67 -15
- package/src/store/vector/variant-search.ts +41 -4
- package/browser-extension/artifacts/gno-browser-clipper-v2.12.2.zip.sha256 +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert one file to the Markdown GNO would index for it, with nothing
|
|
3
|
+
* persisted (`gno convert`, SDK `convertFile`).
|
|
4
|
+
*
|
|
5
|
+
* The file goes through the indexing path: extension MIME detection and the
|
|
6
|
+
* record-adapter check, then a sniffed MIME and `prepareFile` in the
|
|
7
|
+
* terminable file processor under the default time and memory budget. The
|
|
8
|
+
* Markdown and mirror hash therefore equal what `gno update` stores for the
|
|
9
|
+
* same bytes. No config, index, store or cache is read or written.
|
|
10
|
+
*
|
|
11
|
+
* @module src/ingestion/convert-file
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// node:fs/promises for stat (no Bun equivalent for file stats)
|
|
15
|
+
import { stat } from "node:fs/promises";
|
|
16
|
+
// node:path for basename/resolve (no Bun path utils)
|
|
17
|
+
import { basename, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { getDefaultMimeDetector } from "../converters/mime";
|
|
20
|
+
import { markdownConverter } from "../converters/native/markdown";
|
|
21
|
+
import { getDefaultPipeline } from "../converters/pipeline";
|
|
22
|
+
import { DEFAULT_LIMITS } from "../converters/types";
|
|
23
|
+
import {
|
|
24
|
+
defaultConversionMemoryMb,
|
|
25
|
+
prepareInProcessor,
|
|
26
|
+
} from "./file-processor";
|
|
27
|
+
import { DEFAULT_CHUNK_PARAMS } from "./types";
|
|
28
|
+
|
|
29
|
+
export const CONVERT_SCHEMA_VERSION = "convert@1.0";
|
|
30
|
+
|
|
31
|
+
const BYTES_PER_MB = 1_048_576;
|
|
32
|
+
const SNIFF_BYTES = 512;
|
|
33
|
+
|
|
34
|
+
export interface ConvertFileResult {
|
|
35
|
+
schemaVersion: typeof CONVERT_SCHEMA_VERSION;
|
|
36
|
+
source: { path: string; mime: string; ext: string; sizeBytes: number };
|
|
37
|
+
converter: { id: string; version: string };
|
|
38
|
+
title: string | null;
|
|
39
|
+
languageHint: string | null;
|
|
40
|
+
markdown: string;
|
|
41
|
+
mirrorHash: string;
|
|
42
|
+
warnings: { code: string; message: string }[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Why a file was not converted. `validation` failures (missing file, over
|
|
47
|
+
* the size limit, no converter, multi-record format) exit 1 on the CLI;
|
|
48
|
+
* converter and processor failures exit 2.
|
|
49
|
+
*/
|
|
50
|
+
export interface ConvertFileFailure {
|
|
51
|
+
reason: string;
|
|
52
|
+
message: string;
|
|
53
|
+
validation: boolean;
|
|
54
|
+
details?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ConvertFileOutcome =
|
|
58
|
+
| { ok: true; value: ConvertFileResult }
|
|
59
|
+
| { ok: false; error: ConvertFileFailure };
|
|
60
|
+
|
|
61
|
+
const fail = (
|
|
62
|
+
reason: string,
|
|
63
|
+
message: string,
|
|
64
|
+
validation: boolean,
|
|
65
|
+
details?: Record<string, unknown>
|
|
66
|
+
): ConvertFileOutcome => ({
|
|
67
|
+
ok: false,
|
|
68
|
+
error: { reason, message, validation, ...(details ? { details } : {}) },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Convert one file; see the module comment. */
|
|
72
|
+
export async function convertFile(path: string): Promise<ConvertFileOutcome> {
|
|
73
|
+
const absPath = resolve(path);
|
|
74
|
+
let sizeBytes: number;
|
|
75
|
+
try {
|
|
76
|
+
// stat follows symlinks: a link to a regular file converts, a link to
|
|
77
|
+
// a directory does not.
|
|
78
|
+
const stats = await stat(absPath);
|
|
79
|
+
if (!stats.isFile()) {
|
|
80
|
+
return fail("NOT_FOUND", `Not a regular file: ${path}`, true);
|
|
81
|
+
}
|
|
82
|
+
sizeBytes = stats.size;
|
|
83
|
+
} catch {
|
|
84
|
+
return fail("NOT_FOUND", `File not found: ${path}`, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Refused before reading, so an oversized file is never loaded.
|
|
88
|
+
if (sizeBytes > DEFAULT_LIMITS.maxBytes) {
|
|
89
|
+
return fail(
|
|
90
|
+
"TOO_LARGE",
|
|
91
|
+
`File size ${sizeBytes} exceeds the indexing limit ${DEFAULT_LIMITS.maxBytes}`,
|
|
92
|
+
true,
|
|
93
|
+
{ sizeBytes, limitBytes: DEFAULT_LIMITS.maxBytes }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const detector = getDefaultMimeDetector();
|
|
98
|
+
const byExtension = detector.detect(absPath, new Uint8Array());
|
|
99
|
+
const recordAdapter = await getDefaultPipeline().selectRecordAdapter(
|
|
100
|
+
byExtension.mime,
|
|
101
|
+
byExtension.ext
|
|
102
|
+
);
|
|
103
|
+
if (recordAdapter) {
|
|
104
|
+
return fail(
|
|
105
|
+
"MULTI_DOCUMENT",
|
|
106
|
+
`${recordAdapter.id} files hold several records; index them in a collection instead`,
|
|
107
|
+
true,
|
|
108
|
+
{ format: recordAdapter.id }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let bytes: Uint8Array;
|
|
113
|
+
try {
|
|
114
|
+
bytes = await Bun.file(absPath).bytes();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// Readable at stat but not at read: removed meanwhile, or no permission.
|
|
117
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
118
|
+
if (code === "ENOENT") {
|
|
119
|
+
return fail("NOT_FOUND", `File not found: ${path}`, true);
|
|
120
|
+
}
|
|
121
|
+
const denied = code === "EACCES" || code === "EPERM";
|
|
122
|
+
return fail(
|
|
123
|
+
denied ? "PERMISSION" : "IO",
|
|
124
|
+
`Cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
125
|
+
false
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const { mime, ext } = detector.detect(
|
|
129
|
+
absPath,
|
|
130
|
+
bytes.subarray(0, SNIFF_BYTES)
|
|
131
|
+
);
|
|
132
|
+
const prepared = await prepareInProcessor(
|
|
133
|
+
{
|
|
134
|
+
input: {
|
|
135
|
+
sourcePath: absPath,
|
|
136
|
+
relativePath: basename(absPath),
|
|
137
|
+
collection: "",
|
|
138
|
+
bytes,
|
|
139
|
+
mime,
|
|
140
|
+
ext,
|
|
141
|
+
limits: { ...DEFAULT_LIMITS },
|
|
142
|
+
},
|
|
143
|
+
metadataExt: ext,
|
|
144
|
+
contentTypeRules: [],
|
|
145
|
+
chunkParams: DEFAULT_CHUNK_PARAMS,
|
|
146
|
+
memoryManaged: false,
|
|
147
|
+
markdownSource: markdownConverter.canHandle(mime, ext),
|
|
148
|
+
previous: null,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
timeoutMs: DEFAULT_LIMITS.timeoutMs,
|
|
152
|
+
maxMemoryBytes: defaultConversionMemoryMb() * BYTES_PER_MB,
|
|
153
|
+
},
|
|
154
|
+
{ onPrevious: () => undefined }
|
|
155
|
+
);
|
|
156
|
+
if (!prepared.ok) {
|
|
157
|
+
const { code, message, details } = prepared.error;
|
|
158
|
+
return fail(code, message, code === "UNSUPPORTED", details);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { artifact } = prepared.value;
|
|
162
|
+
return {
|
|
163
|
+
ok: true,
|
|
164
|
+
value: {
|
|
165
|
+
schemaVersion: CONVERT_SCHEMA_VERSION,
|
|
166
|
+
source: { path: absPath, mime, ext, sizeBytes },
|
|
167
|
+
converter: {
|
|
168
|
+
id: artifact.meta.converterId,
|
|
169
|
+
version: artifact.meta.converterVersion,
|
|
170
|
+
},
|
|
171
|
+
title: artifact.title ?? null,
|
|
172
|
+
languageHint: artifact.languageHint ?? null,
|
|
173
|
+
markdown: artifact.markdown,
|
|
174
|
+
mirrorHash: artifact.mirrorHash,
|
|
175
|
+
warnings: (artifact.meta.warnings ?? []).map(({ code, message }) => ({
|
|
176
|
+
code,
|
|
177
|
+
message,
|
|
178
|
+
})),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
package/src/llm/registry.ts
CHANGED
|
@@ -134,6 +134,26 @@ export function resolveModelUri(
|
|
|
134
134
|
return preset[type];
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Collection to resolve models for when a request lists several collections:
|
|
139
|
+
* the first listed one when every listed collection resolves `type` to the
|
|
140
|
+
* same model, otherwise undefined (the unscoped resolution).
|
|
141
|
+
*/
|
|
142
|
+
export function modelCollectionForList(
|
|
143
|
+
config: Config,
|
|
144
|
+
type: ModelType,
|
|
145
|
+
collections: readonly string[] | undefined
|
|
146
|
+
): string | undefined {
|
|
147
|
+
const [first] = collections ?? [];
|
|
148
|
+
if (first === undefined) return undefined;
|
|
149
|
+
const uri = resolveModelUri(config, type, undefined, first);
|
|
150
|
+
return collections?.every(
|
|
151
|
+
(collection) => resolveModelUri(config, type, undefined, collection) === uri
|
|
152
|
+
)
|
|
153
|
+
? first
|
|
154
|
+
: undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
export function resolveModelSource(
|
|
138
158
|
config: Config,
|
|
139
159
|
type: ModelType,
|
package/src/mcp/http-egress.ts
CHANGED
|
@@ -140,6 +140,19 @@ const GRAPH_RESULT_TOOLS = new Set([
|
|
|
140
140
|
"gno_impact",
|
|
141
141
|
]);
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Tools whose `collections` argument bounds the collections they read. The
|
|
145
|
+
* retrieval tools reject `collection` with `collections`, so the union of both
|
|
146
|
+
* is never wider than what the handler serves.
|
|
147
|
+
*/
|
|
148
|
+
const COLLECTIONS_SCOPE_TOOLS = new Set([
|
|
149
|
+
"gno_audit",
|
|
150
|
+
"gno_impact",
|
|
151
|
+
"gno_query",
|
|
152
|
+
"gno_search",
|
|
153
|
+
"gno_vsearch",
|
|
154
|
+
]);
|
|
155
|
+
|
|
143
156
|
const requestedCollections = (
|
|
144
157
|
params: unknown,
|
|
145
158
|
collections: readonly Collection[]
|
|
@@ -155,7 +168,8 @@ const requestedCollections = (
|
|
|
155
168
|
if (typeof direct === "string" && direct.trim())
|
|
156
169
|
names.add(direct.trim().toLowerCase());
|
|
157
170
|
if (
|
|
158
|
-
|
|
171
|
+
typeof record?.name === "string" &&
|
|
172
|
+
COLLECTIONS_SCOPE_TOOLS.has(record.name) &&
|
|
159
173
|
Array.isArray(args.collections)
|
|
160
174
|
) {
|
|
161
175
|
for (const value of args.collections) {
|
|
@@ -192,7 +206,12 @@ const requestedCollections = (
|
|
|
192
206
|
if (collection) names.add(collection);
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
|
-
|
|
209
|
+
// A name that is not configured serves nothing (the handler refuses it), so
|
|
210
|
+
// it never narrows or widens the check; with no configured name left, every
|
|
211
|
+
// collection is checked (fail closed).
|
|
212
|
+
const configured = new Set(collections.map(({ name }) => name.toLowerCase()));
|
|
213
|
+
const scoped = [...names].filter((name) => configured.has(name));
|
|
214
|
+
return scoped.length > 0 ? scoped : collections.map(({ name }) => name);
|
|
196
215
|
};
|
|
197
216
|
|
|
198
217
|
const enforceMessage = (
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP adapter for the shared request collection allowlist.
|
|
3
|
+
*
|
|
4
|
+
* @module src/mcp/tools/collection-scope
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToolContext } from "../server";
|
|
8
|
+
|
|
9
|
+
import { resolveCollectionScope } from "../../core/collection-scope";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve `collections` for gno_search, gno_vsearch and gno_query. Throws a
|
|
13
|
+
* `VALIDATION:` error for malformed input or `collection` plus `collections`,
|
|
14
|
+
* and the existing unknown-collection error for names not configured.
|
|
15
|
+
*/
|
|
16
|
+
export function requestCollections(
|
|
17
|
+
args: { collection?: string; collections?: string[] },
|
|
18
|
+
ctx: Pick<ToolContext, "collections">
|
|
19
|
+
): string[] | undefined {
|
|
20
|
+
const scope = resolveCollectionScope(args, ctx.collections);
|
|
21
|
+
if (scope.ok) return scope.collections;
|
|
22
|
+
throw new Error(
|
|
23
|
+
scope.kind === "validation" ? `VALIDATION: ${scope.message}` : scope.message
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -8,12 +8,12 @@ import type { Collection } from "../../config/types";
|
|
|
8
8
|
import type { ToolContext } from "../server";
|
|
9
9
|
|
|
10
10
|
import { MCP_ERRORS } from "../../core/errors";
|
|
11
|
-
import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
|
|
12
11
|
import { JobError } from "../../core/job-manager";
|
|
13
12
|
import {
|
|
14
13
|
recordContentMutation,
|
|
15
14
|
recordIndexMutation,
|
|
16
15
|
} from "../../core/mutation-generations";
|
|
16
|
+
import { resolveReindexScope } from "../../core/reindex-paths";
|
|
17
17
|
import { normalizeCollectionName } from "../../core/validation";
|
|
18
18
|
import { embedBacklog } from "../../embed";
|
|
19
19
|
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
@@ -29,12 +29,14 @@ import { runTool, type ToolResult } from "./index";
|
|
|
29
29
|
interface IndexInput {
|
|
30
30
|
collection?: string;
|
|
31
31
|
gitPull?: boolean;
|
|
32
|
+
paths?: string[];
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
interface IndexResultOutput {
|
|
35
36
|
jobId: string;
|
|
36
|
-
status: "started";
|
|
37
|
+
status: "started" | "queued";
|
|
37
38
|
collections: string[];
|
|
39
|
+
paths?: string[];
|
|
38
40
|
phases: ["sync", "embed"];
|
|
39
41
|
options: {
|
|
40
42
|
gitPull: boolean;
|
|
@@ -47,6 +49,7 @@ function formatIndexResult(result: IndexResultOutput): string {
|
|
|
47
49
|
lines.push(`Job: ${result.jobId}`);
|
|
48
50
|
lines.push(`Status: ${result.status}`);
|
|
49
51
|
lines.push(`Collections: ${result.collections.join(", ") || "(none)"}`);
|
|
52
|
+
if (result.paths) lines.push(`Paths: ${result.paths.join(", ")}`);
|
|
50
53
|
lines.push(`Phases: ${result.phases.join(" → ")}`);
|
|
51
54
|
lines.push(`Git pull: ${result.options.gitPull ? "yes" : "no"}`);
|
|
52
55
|
return lines.join("\n");
|
|
@@ -74,17 +77,7 @@ export function handleIndex(
|
|
|
74
77
|
throw new Error("Write tools disabled. Start MCP with --enable-write.");
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
let lock: WriteLockHandle | null = null;
|
|
78
|
-
let handedOff = false;
|
|
79
|
-
|
|
80
80
|
try {
|
|
81
|
-
lock = await acquireWriteLock(ctx.writeLockPath);
|
|
82
|
-
if (!lock) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`${MCP_ERRORS.LOCKED.code}: ${MCP_ERRORS.LOCKED.message}`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
81
|
const requestedCollection = args.collection?.trim();
|
|
89
82
|
const collection = requestedCollection
|
|
90
83
|
? resolveCollection(requestedCollection, ctx.collections)
|
|
@@ -108,47 +101,77 @@ export function handleIndex(
|
|
|
108
101
|
},
|
|
109
102
|
ctx.config
|
|
110
103
|
);
|
|
104
|
+
const paths = await resolveReindexScope(
|
|
105
|
+
collection,
|
|
106
|
+
args.paths,
|
|
107
|
+
options
|
|
108
|
+
);
|
|
111
109
|
enforceSyncCommandEgress(ctx, {
|
|
112
110
|
collectionNames: collections,
|
|
113
111
|
gitPull: options.gitPull,
|
|
114
112
|
runUpdateCmd: options.runUpdateCmd,
|
|
115
113
|
});
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
// Captured now: a queued pass runs after this call's context is gone.
|
|
116
|
+
const config = ctx.config;
|
|
117
|
+
const allCollections = ctx.collections;
|
|
118
|
+
const accepted = await ctx.jobManager.submitReindex({
|
|
119
|
+
type: "index",
|
|
120
|
+
collection: collection?.name ?? null,
|
|
121
|
+
paths,
|
|
122
|
+
options: {
|
|
123
|
+
gitPull: options.gitPull ?? false,
|
|
124
|
+
runUpdateCmd: options.runUpdateCmd ?? false,
|
|
125
|
+
},
|
|
126
|
+
run: async (scope) => {
|
|
128
127
|
const lease = ctx.acquireModelLease?.();
|
|
128
|
+
const passOptions = withContentTypeRules(
|
|
129
|
+
{ ...scope.options },
|
|
130
|
+
config
|
|
131
|
+
);
|
|
129
132
|
try {
|
|
130
133
|
// Phase 1: Sync
|
|
131
134
|
const syncResult = collection
|
|
132
|
-
? await
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
135
|
+
? await (
|
|
136
|
+
scope.paths
|
|
137
|
+
? defaultSyncService.syncPaths(
|
|
138
|
+
collection,
|
|
139
|
+
ctx.store,
|
|
140
|
+
scope.paths,
|
|
141
|
+
passOptions
|
|
142
|
+
)
|
|
143
|
+
: defaultSyncService.syncCollection(
|
|
144
|
+
collection,
|
|
145
|
+
ctx.store,
|
|
146
|
+
passOptions
|
|
147
|
+
)
|
|
148
|
+
).then((r) => ({
|
|
149
|
+
collections: [r],
|
|
150
|
+
totalDurationMs: r.durationMs,
|
|
151
|
+
totalFilesProcessed: r.filesProcessed,
|
|
152
|
+
totalFilesAdded: r.filesAdded,
|
|
153
|
+
totalFilesUpdated: r.filesUpdated,
|
|
154
|
+
totalFilesErrored: r.filesErrored,
|
|
155
|
+
totalFilesSkipped: r.filesSkipped,
|
|
156
|
+
}))
|
|
143
157
|
: await defaultSyncService.syncAll(
|
|
144
|
-
|
|
158
|
+
allCollections,
|
|
145
159
|
ctx.store,
|
|
146
|
-
|
|
160
|
+
passOptions
|
|
147
161
|
);
|
|
148
162
|
recordContentMutation(syncResult, ctx.markContentMutation);
|
|
149
163
|
|
|
150
|
-
// Phase 2: Embed
|
|
151
|
-
|
|
164
|
+
// Phase 2: Embed. The model is looked up when the pass runs: a
|
|
165
|
+
// queued pass outlives a server reload, which disposes the
|
|
166
|
+
// adapter that was current at submit.
|
|
167
|
+
const liveConfig = ctx.config;
|
|
168
|
+
const modelUri = resolveModelUri(
|
|
169
|
+
liveConfig,
|
|
170
|
+
"embed",
|
|
171
|
+
undefined,
|
|
172
|
+
collection?.name
|
|
173
|
+
);
|
|
174
|
+
const llm = ctx.getModelAdapter?.() ?? new LlmAdapter(liveConfig);
|
|
152
175
|
const embedResult = await llm.createEmbeddingPort(modelUri, {
|
|
153
176
|
egressCollections: collections,
|
|
154
177
|
policy: { offline: true, allowDownload: false },
|
|
@@ -217,15 +240,14 @@ export function handleIndex(
|
|
|
217
240
|
} finally {
|
|
218
241
|
lease?.release();
|
|
219
242
|
}
|
|
220
|
-
}
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
handedOff = true;
|
|
243
|
+
},
|
|
244
|
+
});
|
|
224
245
|
|
|
225
246
|
const result: IndexResultOutput = {
|
|
226
|
-
jobId,
|
|
247
|
+
jobId: accepted.jobId,
|
|
227
248
|
collections,
|
|
228
|
-
status: "started",
|
|
249
|
+
status: accepted.status === "running" ? "started" : "queued",
|
|
250
|
+
...(paths ? { paths } : {}),
|
|
229
251
|
phases: ["sync", "embed"],
|
|
230
252
|
options: {
|
|
231
253
|
gitPull: options.gitPull ?? false,
|
|
@@ -239,10 +261,6 @@ export function handleIndex(
|
|
|
239
261
|
throw new Error(`${error.code}: ${error.message}`);
|
|
240
262
|
}
|
|
241
263
|
throw error;
|
|
242
|
-
} finally {
|
|
243
|
-
if (lock && !handedOff) {
|
|
244
|
-
await lock.release();
|
|
245
|
-
}
|
|
246
264
|
}
|
|
247
265
|
},
|
|
248
266
|
formatIndexResult
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
readMetadataSummary,
|
|
28
28
|
} from "../../core/metadata-discovery";
|
|
29
29
|
import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
|
|
30
|
+
import { MAX_REINDEX_PATHS } from "../../core/reindex-paths";
|
|
30
31
|
import { RETRIEVAL_TRACE_METADATA } from "../../core/retrieval-trace-session";
|
|
31
32
|
import { normalizeTag } from "../../core/tags";
|
|
32
33
|
import {
|
|
@@ -255,6 +256,12 @@ export const searchInputSchema = z.object({
|
|
|
255
256
|
.string()
|
|
256
257
|
.optional()
|
|
257
258
|
.describe("Filter to a single collection name"),
|
|
259
|
+
collections: z
|
|
260
|
+
.array(z.string())
|
|
261
|
+
.optional()
|
|
262
|
+
.describe(
|
|
263
|
+
"Filter to any of these collection names (1-256); use instead of collection"
|
|
264
|
+
),
|
|
258
265
|
limit: z
|
|
259
266
|
.number()
|
|
260
267
|
.int()
|
|
@@ -437,6 +444,13 @@ const syncInputSchema = z.object({
|
|
|
437
444
|
.boolean()
|
|
438
445
|
.default(false)
|
|
439
446
|
.describe("Run the collection's configured update command before syncing"),
|
|
447
|
+
paths: z
|
|
448
|
+
.array(z.string())
|
|
449
|
+
.max(MAX_REINDEX_PATHS)
|
|
450
|
+
.optional()
|
|
451
|
+
.describe(
|
|
452
|
+
"Reindex only these files (1-256, relative to the collection root, or absolute inside it). Requires collection"
|
|
453
|
+
),
|
|
440
454
|
});
|
|
441
455
|
|
|
442
456
|
const embedInputSchema = z.object({
|
|
@@ -452,6 +466,13 @@ const indexInputSchema = z.object({
|
|
|
452
466
|
.optional()
|
|
453
467
|
.describe("Collection name to index. Omit to index all collections"),
|
|
454
468
|
gitPull: z.boolean().default(false).describe("Run git pull before indexing"),
|
|
469
|
+
paths: z
|
|
470
|
+
.array(z.string())
|
|
471
|
+
.max(MAX_REINDEX_PATHS)
|
|
472
|
+
.optional()
|
|
473
|
+
.describe(
|
|
474
|
+
"Reindex only these files (1-256, relative to the collection root, or absolute inside it). Requires collection"
|
|
475
|
+
),
|
|
455
476
|
});
|
|
456
477
|
|
|
457
478
|
const removeCollectionInputSchema = z.object({
|
|
@@ -496,6 +517,12 @@ export const vsearchInputSchema = z.object({
|
|
|
496
517
|
.string()
|
|
497
518
|
.optional()
|
|
498
519
|
.describe("Filter to a single collection name"),
|
|
520
|
+
collections: z
|
|
521
|
+
.array(z.string())
|
|
522
|
+
.optional()
|
|
523
|
+
.describe(
|
|
524
|
+
"Filter to any of these collection names (1-256); use instead of collection"
|
|
525
|
+
),
|
|
499
526
|
limit: z
|
|
500
527
|
.number()
|
|
501
528
|
.int()
|
|
@@ -570,6 +597,12 @@ export const queryInputSchema = z.object({
|
|
|
570
597
|
.string()
|
|
571
598
|
.optional()
|
|
572
599
|
.describe("Filter to a single collection name"),
|
|
600
|
+
collections: z
|
|
601
|
+
.array(z.string())
|
|
602
|
+
.optional()
|
|
603
|
+
.describe(
|
|
604
|
+
"Filter to any of these collection names (1-256); use instead of collection"
|
|
605
|
+
),
|
|
573
606
|
limit: z
|
|
574
607
|
.number()
|
|
575
608
|
.int()
|
|
@@ -672,15 +705,17 @@ export const queryInputSchema = z.object({
|
|
|
672
705
|
tagsAny: z.array(z.string()).optional().describe("Require ANY of these tags"),
|
|
673
706
|
});
|
|
674
707
|
|
|
675
|
-
export const queryDiagnoseInputSchema = queryInputSchema
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
"Target
|
|
682
|
-
|
|
683
|
-
|
|
708
|
+
export const queryDiagnoseInputSchema = queryInputSchema
|
|
709
|
+
.omit({ collections: true })
|
|
710
|
+
.extend({
|
|
711
|
+
target: z
|
|
712
|
+
.string()
|
|
713
|
+
.trim()
|
|
714
|
+
.min(1, "Target reference cannot be empty")
|
|
715
|
+
.describe(
|
|
716
|
+
"Target document reference to diagnose (gno URI, docid, or collection/path)"
|
|
717
|
+
),
|
|
718
|
+
});
|
|
684
719
|
|
|
685
720
|
const getInputSchema = z.object({
|
|
686
721
|
ref: z
|
|
@@ -1019,7 +1054,8 @@ export async function runTool<T>(
|
|
|
1019
1054
|
try {
|
|
1020
1055
|
assertConfiguredCollections(ctx, name);
|
|
1021
1056
|
assertInferenceActive();
|
|
1022
|
-
const
|
|
1057
|
+
const read = READ_CONNECTION_TOOLS.has(name) ? readConnection(ctx, fn) : fn;
|
|
1058
|
+
const raw = await (ctx.runWithSnapshot?.(read) ?? read());
|
|
1023
1059
|
assertInferenceActive();
|
|
1024
1060
|
const traceMetadata =
|
|
1025
1061
|
raw !== null && typeof raw === "object"
|
|
@@ -1048,6 +1084,34 @@ export async function runTool<T>(
|
|
|
1048
1084
|
}
|
|
1049
1085
|
}
|
|
1050
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* Read-only retrieval tools. They read on the store's read connection, so a
|
|
1089
|
+
* sync batch that is still writing is never visible to them.
|
|
1090
|
+
*/
|
|
1091
|
+
const READ_CONNECTION_TOOLS = new Set([
|
|
1092
|
+
"gno_search",
|
|
1093
|
+
"gno_vsearch",
|
|
1094
|
+
"gno_query",
|
|
1095
|
+
"gno_get",
|
|
1096
|
+
"gno_multi_get",
|
|
1097
|
+
"gno_status",
|
|
1098
|
+
"gno_list_tags",
|
|
1099
|
+
"gno_links",
|
|
1100
|
+
"gno_backlinks",
|
|
1101
|
+
"gno_similar",
|
|
1102
|
+
]);
|
|
1103
|
+
|
|
1104
|
+
/** Store test doubles may lack the read connection; they read as before. */
|
|
1105
|
+
function readConnection<T>(
|
|
1106
|
+
ctx: ToolContext,
|
|
1107
|
+
fn: () => Promise<T>
|
|
1108
|
+
): () => Promise<T> {
|
|
1109
|
+
const store = ctx.store as Partial<ToolContext["store"]> | undefined;
|
|
1110
|
+
return typeof store?.withReadConnection === "function"
|
|
1111
|
+
? () => ctx.store.withReadConnection(fn)
|
|
1112
|
+
: fn;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1051
1115
|
/**
|
|
1052
1116
|
* Run a tool without acquiring the mutex.
|
|
1053
1117
|
* For read-only in-memory tools (job_status, list_jobs) that should not block.
|
|
@@ -24,6 +24,7 @@ interface JobStatusResult {
|
|
|
24
24
|
result?: SyncResult;
|
|
25
25
|
typedResult?: JobResult;
|
|
26
26
|
error?: string;
|
|
27
|
+
scope?: JobRecord["scope"];
|
|
27
28
|
serverInstanceId: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -34,6 +35,11 @@ function formatJobStatus(result: JobStatusResult): string {
|
|
|
34
35
|
lines.push(`Type: ${result.type}`);
|
|
35
36
|
lines.push(`Status: ${result.status}`);
|
|
36
37
|
lines.push(`Started: ${result.startedAt}`);
|
|
38
|
+
if (result.scope) {
|
|
39
|
+
lines.push(`Collection: ${result.scope.collection ?? "(all)"}`);
|
|
40
|
+
if (result.scope.paths)
|
|
41
|
+
lines.push(`Paths: ${result.scope.paths.join(", ")}`);
|
|
42
|
+
}
|
|
37
43
|
|
|
38
44
|
if (result.completedAt) {
|
|
39
45
|
lines.push(`Completed: ${result.completedAt}`);
|
|
@@ -92,6 +98,7 @@ function toJobStatusResult(job: JobRecord): JobStatusResult {
|
|
|
92
98
|
result: job.result,
|
|
93
99
|
typedResult: job.typedResult,
|
|
94
100
|
error: job.error,
|
|
101
|
+
...(job.scope ? { scope: job.scope } : {}),
|
|
95
102
|
serverInstanceId: job.serverInstanceId,
|
|
96
103
|
};
|
|
97
104
|
}
|