@gmickel/gno 1.30.7 → 1.32.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 +6 -5
- package/assets/skill/SKILL.md +25 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +19 -0
- package/spec/db/schema.sql +55 -0
- package/spec/mcp.md +176 -11
- package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
- package/spec/output-schemas/file-refactor-preview.schema.json +393 -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/document-capabilities.ts +13 -0
- package/src/core/file-ops.ts +129 -1
- package/src/core/file-refactor-adapter.ts +329 -0
- package/src/core/file-refactor-apply-edits.ts +61 -0
- package/src/core/file-refactor-apply-fs.ts +512 -0
- package/src/core/file-refactor-apply-safety.ts +340 -0
- package/src/core/file-refactor-apply-validate.ts +401 -0
- package/src/core/file-refactor-contract.ts +486 -0
- package/src/core/file-refactor-destination.ts +123 -0
- package/src/core/file-refactor-from-snapshot.ts +148 -0
- package/src/core/file-refactor-journal-port.ts +150 -0
- package/src/core/file-refactor-journal.ts +347 -0
- package/src/core/file-refactor-paths.ts +60 -0
- package/src/core/file-refactor-plan-classify.ts +208 -0
- package/src/core/file-refactor-plan-validate.ts +169 -0
- package/src/core/file-refactor-planner-types.ts +62 -0
- package/src/core/file-refactor-planner.ts +423 -0
- package/src/core/file-refactor-resolve.ts +280 -0
- package/src/core/file-refactor-service.ts +468 -0
- package/src/core/file-refactors.ts +84 -56
- package/src/core/link-destination-parse.ts +275 -0
- package/src/core/link-inventory-markdown.ts +454 -0
- package/src/core/link-inventory-opaque.ts +244 -0
- package/src/core/link-inventory-types.ts +47 -0
- package/src/core/link-inventory.ts +182 -0
- package/src/core/link-relevance.ts +150 -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 +37 -17
- package/src/mcp/tools/sections.ts +512 -0
- package/src/mcp/tools/workspace-write.ts +215 -97
- package/src/sdk/client.ts +238 -116
- package/src/sdk/index.ts +12 -0
- package/src/sdk/types.ts +61 -3
- package/src/serve/file-refactor-http.ts +239 -0
- package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
- 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 +395 -77
- package/src/serve/routes/api.ts +191 -104
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +452 -0
- package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
- package/src/store/types.ts +84 -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 {
|
|
@@ -89,6 +95,10 @@ import {
|
|
|
89
95
|
handleDuplicateNote,
|
|
90
96
|
handleMoveNote,
|
|
91
97
|
handleRenameNote,
|
|
98
|
+
MOVE_NOTE_MCP_ANNOTATIONS,
|
|
99
|
+
moveNoteInputSchema,
|
|
100
|
+
RENAME_NOTE_MCP_ANNOTATIONS,
|
|
101
|
+
renameNoteInputSchema,
|
|
92
102
|
} from "./workspace-write";
|
|
93
103
|
|
|
94
104
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -116,6 +126,8 @@ export const MCP_TOOL_DESCRIPTIONS = {
|
|
|
116
126
|
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
127
|
multiGet:
|
|
118
128
|
"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.",
|
|
129
|
+
section:
|
|
130
|
+
"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
131
|
status:
|
|
120
132
|
"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
133
|
context:
|
|
@@ -390,17 +402,6 @@ const createFolderInputSchema = z.object({
|
|
|
390
402
|
parentPath: z.string().optional(),
|
|
391
403
|
});
|
|
392
404
|
|
|
393
|
-
const renameNoteInputSchema = z.object({
|
|
394
|
-
ref: z.string().min(1, "ref cannot be empty"),
|
|
395
|
-
name: z.string().min(1, "name cannot be empty"),
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const moveNoteInputSchema = z.object({
|
|
399
|
-
ref: z.string().min(1, "ref cannot be empty"),
|
|
400
|
-
folderPath: z.string().min(1, "folderPath cannot be empty"),
|
|
401
|
-
name: z.string().optional(),
|
|
402
|
-
});
|
|
403
|
-
|
|
404
405
|
const duplicateNoteInputSchema = z.object({
|
|
405
406
|
ref: z.string().min(1, "ref cannot be empty"),
|
|
406
407
|
folderPath: z.string().optional(),
|
|
@@ -1064,6 +1065,17 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1064
1065
|
(args) => handleGet(args, ctx)
|
|
1065
1066
|
);
|
|
1066
1067
|
|
|
1068
|
+
server.registerTool(
|
|
1069
|
+
"gno_section",
|
|
1070
|
+
{
|
|
1071
|
+
description: MCP_TOOL_DESCRIPTIONS.section,
|
|
1072
|
+
inputSchema: sectionInputSchema,
|
|
1073
|
+
outputSchema: sectionOutputSchema,
|
|
1074
|
+
annotations: SECTION_MCP_ANNOTATIONS,
|
|
1075
|
+
},
|
|
1076
|
+
(args) => handleSection(args, ctx)
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1067
1079
|
server.tool(
|
|
1068
1080
|
"gno_multi_get",
|
|
1069
1081
|
MCP_TOOL_DESCRIPTIONS.multiGet,
|
|
@@ -1332,17 +1344,25 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1332
1344
|
(args) => handleCreateFolder(args, ctx)
|
|
1333
1345
|
);
|
|
1334
1346
|
|
|
1335
|
-
server.
|
|
1347
|
+
server.registerTool(
|
|
1336
1348
|
"gno_rename_note",
|
|
1337
|
-
|
|
1338
|
-
|
|
1349
|
+
{
|
|
1350
|
+
description:
|
|
1351
|
+
'Preview or apply a reference-safe rename of an editable note. Use action=preview first, then action=apply with the exact planDigest, confirmation="apply", confirm=true, and schemaVersion="1.0". Annotations are hints only; --enable-write remains authoritative.',
|
|
1352
|
+
inputSchema: renameNoteInputSchema,
|
|
1353
|
+
annotations: RENAME_NOTE_MCP_ANNOTATIONS,
|
|
1354
|
+
},
|
|
1339
1355
|
(args) => handleRenameNote(args, ctx)
|
|
1340
1356
|
);
|
|
1341
1357
|
|
|
1342
|
-
server.
|
|
1358
|
+
server.registerTool(
|
|
1343
1359
|
"gno_move_note",
|
|
1344
|
-
|
|
1345
|
-
|
|
1360
|
+
{
|
|
1361
|
+
description:
|
|
1362
|
+
'Preview or apply a reference-safe same-collection move of an editable note. Use action=preview first, then action=apply with the exact planDigest, confirmation="apply", confirm=true, and schemaVersion="1.0". Annotations are hints only; --enable-write remains authoritative.',
|
|
1363
|
+
inputSchema: moveNoteInputSchema,
|
|
1364
|
+
annotations: MOVE_NOTE_MCP_ANNOTATIONS,
|
|
1365
|
+
},
|
|
1346
1366
|
(args) => handleMoveNote(args, ctx)
|
|
1347
1367
|
);
|
|
1348
1368
|
|