@gmickel/gno 1.30.7 → 1.31.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 +2 -2
- package/assets/skill/SKILL.md +2 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.31.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/mcp.md +62 -0
- package/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -0
- package/src/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +19 -0
- package/src/mcp/tools/sections.ts +512 -0
- package/src/sdk/client.ts +71 -1
- package/src/sdk/index.ts +5 -0
- package/src/sdk/types.ts +29 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +219 -36
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable SectionTargetV1 types, bounds, and creation.
|
|
3
|
+
* Private helper module — import public API from `./sections`.
|
|
4
|
+
*
|
|
5
|
+
* @module src/core/section-target
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
extractSectionRecords,
|
|
10
|
+
lineStartOffsets,
|
|
11
|
+
type SectionRecord,
|
|
12
|
+
} from "./section-parse";
|
|
13
|
+
|
|
14
|
+
/** Versioned, bounded, content-derived section locator (v1). */
|
|
15
|
+
export interface SectionTargetV1 {
|
|
16
|
+
schemaVersion: "1";
|
|
17
|
+
document: {
|
|
18
|
+
uri: string;
|
|
19
|
+
};
|
|
20
|
+
/** Human-readable heading anchor at capture time. */
|
|
21
|
+
anchor: string;
|
|
22
|
+
/** NFC-normalized heading ancestry including the section title. */
|
|
23
|
+
headingPath: string[];
|
|
24
|
+
/** 1-based occurrence among sections with the same headingPath. */
|
|
25
|
+
occurrence: number;
|
|
26
|
+
quote: {
|
|
27
|
+
exact: string;
|
|
28
|
+
prefix: string;
|
|
29
|
+
suffix: string;
|
|
30
|
+
};
|
|
31
|
+
/** SHA-256 hex fingerprint of the full source document content. */
|
|
32
|
+
sourceFingerprint: string;
|
|
33
|
+
/** Line/offset hints only — never identity. */
|
|
34
|
+
hints: {
|
|
35
|
+
line: number;
|
|
36
|
+
startOffset: number;
|
|
37
|
+
endOffset: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const SECTION_TARGET_SCHEMA_VERSION = "1" as const;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hard bounds so targets never embed full section bodies and remain
|
|
45
|
+
* schema-faithful. Identity fields (uri/anchor/headingPath) are never
|
|
46
|
+
* truncated — creation fails closed when they cannot fit.
|
|
47
|
+
*/
|
|
48
|
+
export const SECTION_TARGET_BOUNDS = {
|
|
49
|
+
exactMaxChars: 96,
|
|
50
|
+
prefixMaxChars: 32,
|
|
51
|
+
suffixMaxChars: 32,
|
|
52
|
+
uriMaxChars: 1024,
|
|
53
|
+
anchorMaxChars: 512,
|
|
54
|
+
headingPathItemMaxChars: 512,
|
|
55
|
+
headingPathMaxItems: 6,
|
|
56
|
+
maxSerializedBytes: 2048,
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
const UTF8 = new TextEncoder();
|
|
60
|
+
|
|
61
|
+
/** Structural section record with bounded quote evidence. */
|
|
62
|
+
export interface QuotedSectionRecord extends SectionRecord {
|
|
63
|
+
quote: {
|
|
64
|
+
exact: string;
|
|
65
|
+
prefix: string;
|
|
66
|
+
suffix: string;
|
|
67
|
+
};
|
|
68
|
+
quoteStartOffset: number;
|
|
69
|
+
quoteEndOffset: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const clipStart = (value: string, maxChars: number): string =>
|
|
73
|
+
value.length <= maxChars ? value : value.slice(0, maxChars);
|
|
74
|
+
|
|
75
|
+
const clipEnd = (value: string, maxChars: number): string =>
|
|
76
|
+
value.length <= maxChars ? value : value.slice(value.length - maxChars);
|
|
77
|
+
|
|
78
|
+
const bytesOf = (value: string): number => UTF8.encode(value).byteLength;
|
|
79
|
+
|
|
80
|
+
/** SHA-256 hex digest via Web Crypto (browser- and Bun-safe). */
|
|
81
|
+
export async function fingerprintSourceContent(
|
|
82
|
+
content: string
|
|
83
|
+
): Promise<string> {
|
|
84
|
+
const digest = new Uint8Array(
|
|
85
|
+
await crypto.subtle.digest("SHA-256", UTF8.encode(content))
|
|
86
|
+
);
|
|
87
|
+
return [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const chooseQuoteRange = (
|
|
91
|
+
content: string,
|
|
92
|
+
recordTitleStart: number,
|
|
93
|
+
recordTitleEnd: number,
|
|
94
|
+
bodyStartOffset: number,
|
|
95
|
+
bodyEndOffset: number
|
|
96
|
+
): { start: number; end: number } => {
|
|
97
|
+
const body = content.slice(bodyStartOffset, bodyEndOffset);
|
|
98
|
+
const trimmedLead = body.match(/^\s*/)?.[0]?.length ?? 0;
|
|
99
|
+
const trimmedTrail = body.match(/\s*$/)?.[0]?.length ?? 0;
|
|
100
|
+
const bodyContentStart = bodyStartOffset + trimmedLead;
|
|
101
|
+
const bodyContentEnd = Math.max(
|
|
102
|
+
bodyContentStart,
|
|
103
|
+
bodyEndOffset - trimmedTrail
|
|
104
|
+
);
|
|
105
|
+
if (bodyContentStart < bodyContentEnd) {
|
|
106
|
+
const exact = clipStart(
|
|
107
|
+
content.slice(bodyContentStart, bodyContentEnd),
|
|
108
|
+
SECTION_TARGET_BOUNDS.exactMaxChars
|
|
109
|
+
);
|
|
110
|
+
if (exact.length > 0) {
|
|
111
|
+
return {
|
|
112
|
+
start: bodyContentStart,
|
|
113
|
+
end: bodyContentStart + exact.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { start: recordTitleStart, end: recordTitleEnd };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Prefer local context so heading renames / insertions do not poison prefix
|
|
122
|
+
* evidence. Falls back to a short global clip only when local context is empty.
|
|
123
|
+
*/
|
|
124
|
+
const buildBoundedQuote = (
|
|
125
|
+
content: string,
|
|
126
|
+
exactStart: number,
|
|
127
|
+
exactEnd: number,
|
|
128
|
+
localPrefixFloor: number
|
|
129
|
+
): { exact: string; prefix: string; suffix: string } => {
|
|
130
|
+
const exact = clipStart(
|
|
131
|
+
content.slice(exactStart, exactEnd),
|
|
132
|
+
SECTION_TARGET_BOUNDS.exactMaxChars
|
|
133
|
+
);
|
|
134
|
+
const boundedEnd = exactStart + exact.length;
|
|
135
|
+
const localPrefix = content.slice(localPrefixFloor, exactStart);
|
|
136
|
+
const prefixSource =
|
|
137
|
+
localPrefix.length > 0 ? localPrefix : content.slice(0, exactStart);
|
|
138
|
+
return {
|
|
139
|
+
exact,
|
|
140
|
+
prefix: clipEnd(prefixSource, SECTION_TARGET_BOUNDS.prefixMaxChars),
|
|
141
|
+
suffix: clipStart(
|
|
142
|
+
content.slice(boundedEnd),
|
|
143
|
+
SECTION_TARGET_BOUNDS.suffixMaxChars
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Attach bounded quote evidence to structural section records. */
|
|
149
|
+
export const withQuotes = (
|
|
150
|
+
content: string,
|
|
151
|
+
records: readonly SectionRecord[]
|
|
152
|
+
): QuotedSectionRecord[] => {
|
|
153
|
+
const starts = lineStartOffsets(content);
|
|
154
|
+
return records.map((record) => {
|
|
155
|
+
const bodyStartOffset = starts[record.section.line] ?? content.length;
|
|
156
|
+
const bodyEndOffset =
|
|
157
|
+
record.endLine >= starts.length
|
|
158
|
+
? content.length
|
|
159
|
+
: (starts[record.endLine] ?? content.length);
|
|
160
|
+
const quoteRange = chooseQuoteRange(
|
|
161
|
+
content,
|
|
162
|
+
record.titleStartOffset,
|
|
163
|
+
record.titleEndOffset,
|
|
164
|
+
bodyStartOffset,
|
|
165
|
+
bodyEndOffset
|
|
166
|
+
);
|
|
167
|
+
// Keep prefix local to the gap after the heading title (or markers before
|
|
168
|
+
// a title-only quote) so renames do not poison quote recovery.
|
|
169
|
+
const prefixFloor =
|
|
170
|
+
quoteRange.start >= record.titleEndOffset
|
|
171
|
+
? record.titleEndOffset
|
|
172
|
+
: (starts[record.section.line - 1] ?? record.titleStartOffset);
|
|
173
|
+
return {
|
|
174
|
+
...record,
|
|
175
|
+
quoteStartOffset: quoteRange.start,
|
|
176
|
+
quoteEndOffset: quoteRange.end,
|
|
177
|
+
quote: buildBoundedQuote(
|
|
178
|
+
content,
|
|
179
|
+
quoteRange.start,
|
|
180
|
+
quoteRange.end,
|
|
181
|
+
prefixFloor
|
|
182
|
+
),
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* True when every identity/quote field and the UTF-8 JSON encoding satisfy
|
|
189
|
+
* runtime bounds. Identity fields must be present faithfully — never truncated.
|
|
190
|
+
*/
|
|
191
|
+
export const isBoundedSectionTarget = (target: SectionTargetV1): boolean => {
|
|
192
|
+
const { uri } = target.document;
|
|
193
|
+
if (
|
|
194
|
+
uri.length < 1 ||
|
|
195
|
+
uri.length > SECTION_TARGET_BOUNDS.uriMaxChars ||
|
|
196
|
+
target.anchor.length < 1 ||
|
|
197
|
+
target.anchor.length > SECTION_TARGET_BOUNDS.anchorMaxChars ||
|
|
198
|
+
target.headingPath.length < 1 ||
|
|
199
|
+
target.headingPath.length > SECTION_TARGET_BOUNDS.headingPathMaxItems ||
|
|
200
|
+
target.headingPath.some(
|
|
201
|
+
(item) =>
|
|
202
|
+
item.length < 1 ||
|
|
203
|
+
item.length > SECTION_TARGET_BOUNDS.headingPathItemMaxChars
|
|
204
|
+
) ||
|
|
205
|
+
target.quote.exact.length > SECTION_TARGET_BOUNDS.exactMaxChars ||
|
|
206
|
+
target.quote.prefix.length > SECTION_TARGET_BOUNDS.prefixMaxChars ||
|
|
207
|
+
target.quote.suffix.length > SECTION_TARGET_BOUNDS.suffixMaxChars
|
|
208
|
+
) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return (
|
|
212
|
+
bytesOf(JSON.stringify(target)) <= SECTION_TARGET_BOUNDS.maxSerializedBytes
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export interface CreateSectionTargetInput {
|
|
217
|
+
content: string;
|
|
218
|
+
uri: string;
|
|
219
|
+
/** Capture by current readable anchor, or by 1-based heading line. */
|
|
220
|
+
anchor?: string;
|
|
221
|
+
line?: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create a bounded SectionTargetV1 for a heading in `content`.
|
|
226
|
+
* Returns null when the section cannot be found, or when a faithful
|
|
227
|
+
* bounded target cannot be produced (oversized uri/anchor/path/serialized).
|
|
228
|
+
*/
|
|
229
|
+
export async function createSectionTarget(
|
|
230
|
+
input: CreateSectionTargetInput
|
|
231
|
+
): Promise<SectionTargetV1 | null> {
|
|
232
|
+
const records = withQuotes(
|
|
233
|
+
input.content,
|
|
234
|
+
extractSectionRecords(input.content)
|
|
235
|
+
);
|
|
236
|
+
const record =
|
|
237
|
+
input.anchor !== undefined
|
|
238
|
+
? records.find((entry) => entry.section.anchor === input.anchor)
|
|
239
|
+
: input.line !== undefined
|
|
240
|
+
? records.find((entry) => entry.section.line === input.line)
|
|
241
|
+
: undefined;
|
|
242
|
+
if (!record) return null;
|
|
243
|
+
|
|
244
|
+
const sourceFingerprint = await fingerprintSourceContent(input.content);
|
|
245
|
+
const target: SectionTargetV1 = {
|
|
246
|
+
schemaVersion: SECTION_TARGET_SCHEMA_VERSION,
|
|
247
|
+
document: { uri: input.uri },
|
|
248
|
+
anchor: record.section.anchor,
|
|
249
|
+
headingPath: [...record.headingPath],
|
|
250
|
+
occurrence: record.occurrence,
|
|
251
|
+
quote: { ...record.quote },
|
|
252
|
+
sourceFingerprint,
|
|
253
|
+
hints: {
|
|
254
|
+
line: record.section.line,
|
|
255
|
+
startOffset: record.quoteStartOffset,
|
|
256
|
+
endOffset: record.quoteEndOffset,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Fail closed: never truncate identity-bearing uri/anchor/path.
|
|
261
|
+
if (!isBoundedSectionTarget(target)) return null;
|
|
262
|
+
return target;
|
|
263
|
+
}
|
package/src/core/sections.ts
CHANGED
|
@@ -1,125 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared section extraction and
|
|
2
|
+
* Shared section extraction, anchors, and durable section targets.
|
|
3
3
|
*
|
|
4
|
-
* Browser-safe.
|
|
4
|
+
* Browser-safe (Web Crypto + standard string APIs only).
|
|
5
5
|
*
|
|
6
6
|
* @module src/core/sections
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
export type { DocumentSection } from "./section-parse";
|
|
10
|
+
export {
|
|
11
|
+
extractInclusiveLines,
|
|
12
|
+
extractSections,
|
|
13
|
+
headingForLine,
|
|
14
|
+
slugifySectionTitle,
|
|
15
|
+
} from "./section-parse";
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
export type {
|
|
18
|
+
CreateSectionTargetInput,
|
|
19
|
+
SectionTargetV1,
|
|
20
|
+
} from "./section-target";
|
|
21
|
+
export {
|
|
22
|
+
SECTION_TARGET_BOUNDS,
|
|
23
|
+
SECTION_TARGET_SCHEMA_VERSION,
|
|
24
|
+
createSectionTarget,
|
|
25
|
+
fingerprintSourceContent,
|
|
26
|
+
isBoundedSectionTarget,
|
|
27
|
+
} from "./section-target";
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
export type {
|
|
30
|
+
ResolveSectionTargetInput,
|
|
31
|
+
SectionResolution,
|
|
32
|
+
SectionResolutionCandidate,
|
|
33
|
+
SectionResolutionStatus,
|
|
34
|
+
} from "./section-target-resolve";
|
|
35
|
+
export {
|
|
36
|
+
isNavigableSectionResolution,
|
|
37
|
+
resolveSectionTarget,
|
|
38
|
+
} from "./section-target-resolve";
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
export type {
|
|
41
|
+
SectionCitationV1,
|
|
42
|
+
SectionResolutionDiagnostics,
|
|
43
|
+
SectionTargetCreateResult,
|
|
44
|
+
SectionTargetCreateSelector,
|
|
45
|
+
SectionTargetResolveResult,
|
|
46
|
+
TransportValidationResult,
|
|
47
|
+
} from "./section-target-transport";
|
|
48
|
+
export {
|
|
49
|
+
CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
|
|
50
|
+
CITATION_EXCEEDS_TRANSPORT_BOUNDS,
|
|
51
|
+
SECTION_TARGET_TRANSPORT_BOUNDS,
|
|
52
|
+
isTransportBoundedCandidate,
|
|
53
|
+
isTransportBoundedCanonicalUri,
|
|
54
|
+
isTransportBoundedCitation,
|
|
55
|
+
parseSectionTargetCreateSelector,
|
|
56
|
+
parseSectionTargetResolveBody,
|
|
57
|
+
parseSectionTargetV1,
|
|
58
|
+
projectSectionTargetCreateResult,
|
|
59
|
+
projectSectionTargetResolveResult,
|
|
60
|
+
} from "./section-target-transport";
|
|
32
61
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.toLowerCase()
|
|
43
|
-
.trim()
|
|
44
|
-
.replaceAll(/[^\p{L}\p{N}\s-]/gu, "")
|
|
45
|
-
.replaceAll(/\s+/g, "-")
|
|
46
|
-
.replaceAll(/-+/g, "-")
|
|
47
|
-
.replace(/^-|-$/g, "") || "section"
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function extractSections(content: string): DocumentSection[] {
|
|
52
|
-
const sections: DocumentSection[] = [];
|
|
53
|
-
const counts = new Map<string, number>();
|
|
54
|
-
const lines = content.split("\n");
|
|
55
|
-
let openFence: OpenFence | null = null;
|
|
56
|
-
|
|
57
|
-
for (const [index, line] of lines.entries()) {
|
|
58
|
-
if (openFence) {
|
|
59
|
-
if (closesFence(line, openFence)) openFence = null;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const opener = fenceOpener(line);
|
|
63
|
-
if (opener) {
|
|
64
|
-
openFence = opener;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const match = HEADING_REGEX.exec(line);
|
|
68
|
-
if (!match) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const level = match[1]?.length ?? 0;
|
|
73
|
-
const title = match[2]?.trim() ?? "";
|
|
74
|
-
if (!title) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const baseAnchor = slugifySectionTitle(title);
|
|
79
|
-
const count = (counts.get(baseAnchor) ?? 0) + 1;
|
|
80
|
-
counts.set(baseAnchor, count);
|
|
81
|
-
const anchor = count === 1 ? baseAnchor : `${baseAnchor}-${count}`;
|
|
82
|
-
|
|
83
|
-
sections.push({
|
|
84
|
-
anchor,
|
|
85
|
-
level,
|
|
86
|
-
line: index + 1,
|
|
87
|
-
title,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return sections;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Extract one inclusive, 1-based line range without normalizing source bytes. */
|
|
95
|
-
export function extractInclusiveLines(
|
|
96
|
-
content: string,
|
|
97
|
-
startLine: number,
|
|
98
|
-
endLine: number
|
|
99
|
-
): string | null {
|
|
100
|
-
if (
|
|
101
|
-
content.includes("\r") ||
|
|
102
|
-
!Number.isSafeInteger(startLine) ||
|
|
103
|
-
!Number.isSafeInteger(endLine) ||
|
|
104
|
-
startLine < 1 ||
|
|
105
|
-
endLine < startLine
|
|
106
|
-
) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
const lines = content.split("\n");
|
|
110
|
-
if (endLine > lines.length) return null;
|
|
111
|
-
return lines.slice(startLine - 1, endLine).join("\n");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Find the nearest Markdown heading governing a 1-based source line. */
|
|
115
|
-
export function headingForLine(
|
|
116
|
-
sections: readonly DocumentSection[],
|
|
117
|
-
line: number
|
|
118
|
-
): string | null {
|
|
119
|
-
let heading: string | null = null;
|
|
120
|
-
for (const section of sections) {
|
|
121
|
-
if (section.line > line) break;
|
|
122
|
-
heading = section.title;
|
|
123
|
-
}
|
|
124
|
-
return heading;
|
|
125
|
-
}
|
|
62
|
+
export type { SectionTargetLinkDecodeFailure } from "./section-target-link";
|
|
63
|
+
export {
|
|
64
|
+
SECTION_TARGET_LINK_MAX_ENCODED_CHARS,
|
|
65
|
+
SECTION_TARGET_LINK_PARAM,
|
|
66
|
+
SECTION_TARGET_LINK_VERSION,
|
|
67
|
+
classifySectionTargetLinkDecodeFailure,
|
|
68
|
+
decodeSectionTargetLinkParam,
|
|
69
|
+
encodeSectionTargetLinkParam,
|
|
70
|
+
} from "./section-target-link";
|
package/src/mcp/AGENTS.md
CHANGED
|
@@ -13,6 +13,7 @@ src/mcp/
|
|
|
13
13
|
│ ├── vsearch.ts # gno_vsearch (vector)
|
|
14
14
|
│ ├── query.ts # gno_query (hybrid)
|
|
15
15
|
│ ├── get.ts # gno_get (single doc)
|
|
16
|
+
│ ├── sections.ts # gno_section (section target create/resolve)
|
|
16
17
|
│ ├── multi-get.ts # gno_multi_get (batch)
|
|
17
18
|
│ └── status.ts # gno_status
|
|
18
19
|
└── resources/ # Resource implementations
|
package/src/mcp/CLAUDE.md
CHANGED
|
@@ -13,6 +13,7 @@ src/mcp/
|
|
|
13
13
|
│ ├── vsearch.ts # gno_vsearch (vector)
|
|
14
14
|
│ ├── query.ts # gno_query (hybrid)
|
|
15
15
|
│ ├── get.ts # gno_get (single doc)
|
|
16
|
+
│ ├── sections.ts # gno_section (section target create/resolve)
|
|
16
17
|
│ ├── multi-get.ts # gno_multi_get (batch)
|
|
17
18
|
│ └── status.ts # gno_status
|
|
18
19
|
└── resources/ # Resource implementations
|
package/src/mcp/http-egress.ts
CHANGED
package/src/mcp/tools/index.ts
CHANGED
|
@@ -67,6 +67,12 @@ import { handleMultiGet } from "./multi-get";
|
|
|
67
67
|
import { handleQuery, handleQueryDiagnose } from "./query";
|
|
68
68
|
import { handleRemoveCollection } from "./remove-collection";
|
|
69
69
|
import { handleSearch } from "./search";
|
|
70
|
+
import {
|
|
71
|
+
handleSection,
|
|
72
|
+
SECTION_MCP_ANNOTATIONS,
|
|
73
|
+
sectionInputSchema,
|
|
74
|
+
sectionOutputSchema,
|
|
75
|
+
} from "./sections";
|
|
70
76
|
import { handleStatus } from "./status";
|
|
71
77
|
import { handleSync } from "./sync";
|
|
72
78
|
import {
|
|
@@ -116,6 +122,8 @@ export const MCP_TOOL_DESCRIPTIONS = {
|
|
|
116
122
|
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.",
|
|
117
123
|
multiGet:
|
|
118
124
|
"Retrieve multiple documents by refs array or glob pattern. Use after gno_search/gno_query to batch top result URIs/docids; set maxBytes and lineNumbers to control context size.",
|
|
125
|
+
section:
|
|
126
|
+
"Create or resolve a durable SectionTargetV1 against one indexed document. action=create needs ref plus exactly one of anchor|line; action=resolve needs ref plus target. Exact/recovered include citation (uri, anchor, title, inclusive lines, fingerprint); ambiguous/stale/missing omit citation and are not safe to navigate or cite. Read-only — does not write or persist targets. Follow navigable ranges with gno_get fromLine/lineCount.",
|
|
119
127
|
status:
|
|
120
128
|
"Get index health: collection count, document count, chunk count, embedding backlog, and per-collection stats. Check first when vector/hybrid results look stale or unavailable.",
|
|
121
129
|
context:
|
|
@@ -1064,6 +1072,17 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1064
1072
|
(args) => handleGet(args, ctx)
|
|
1065
1073
|
);
|
|
1066
1074
|
|
|
1075
|
+
server.registerTool(
|
|
1076
|
+
"gno_section",
|
|
1077
|
+
{
|
|
1078
|
+
description: MCP_TOOL_DESCRIPTIONS.section,
|
|
1079
|
+
inputSchema: sectionInputSchema,
|
|
1080
|
+
outputSchema: sectionOutputSchema,
|
|
1081
|
+
annotations: SECTION_MCP_ANNOTATIONS,
|
|
1082
|
+
},
|
|
1083
|
+
(args) => handleSection(args, ctx)
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1067
1086
|
server.tool(
|
|
1068
1087
|
"gno_multi_get",
|
|
1069
1088
|
MCP_TOOL_DESCRIPTIONS.multiGet,
|