@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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST handlers for section target create/resolve.
|
|
3
|
+
*
|
|
4
|
+
* Consumes shared core create/resolve + transport projection.
|
|
5
|
+
* Does not change GET /api/doc/:id/sections.
|
|
6
|
+
*
|
|
7
|
+
* @module src/serve/routes/section-targets
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
11
|
+
import type { DocumentRow } from "../../store/types";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
|
|
15
|
+
SECTION_TARGET_BOUNDS,
|
|
16
|
+
createSectionTarget,
|
|
17
|
+
extractSections,
|
|
18
|
+
isTransportBoundedCanonicalUri,
|
|
19
|
+
parseSectionTargetCreateSelector,
|
|
20
|
+
parseSectionTargetResolveBody,
|
|
21
|
+
projectSectionTargetCreateResult,
|
|
22
|
+
projectSectionTargetResolveResult,
|
|
23
|
+
resolveSectionTarget,
|
|
24
|
+
} from "../../core/sections";
|
|
25
|
+
import { parseClosedJson } from "../closed-json";
|
|
26
|
+
|
|
27
|
+
/** Create body is tiny; resolve wraps a ≤2048-byte target. */
|
|
28
|
+
const CREATE_BODY_MAX_BYTES = 1024;
|
|
29
|
+
const RESOLVE_BODY_MAX_BYTES = SECTION_TARGET_BOUNDS.maxSerializedBytes + 1024;
|
|
30
|
+
|
|
31
|
+
function jsonResponse(data: unknown, status = 200): Response {
|
|
32
|
+
return Response.json(data, { status });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function errorResponse(code: string, message: string, status = 400): Response {
|
|
36
|
+
return jsonResponse({ error: { code, message } }, status);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readRequestedUriFromUrl(req: Request): string | undefined {
|
|
40
|
+
const value = new URL(req.url).searchParams.get("uri");
|
|
41
|
+
return value?.trim() ? value : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function resolveDocumentReference(
|
|
45
|
+
store: Pick<SqliteAdapter, "getDocumentByDocid" | "getDocumentByUri">,
|
|
46
|
+
docId: string,
|
|
47
|
+
requestedUri?: string
|
|
48
|
+
): Promise<
|
|
49
|
+
| { ok: true; value: DocumentRow | null }
|
|
50
|
+
| { ok: false; error: { message: string } }
|
|
51
|
+
> {
|
|
52
|
+
if (requestedUri) {
|
|
53
|
+
const byUri = await store.getDocumentByUri(requestedUri);
|
|
54
|
+
if (!byUri.ok) {
|
|
55
|
+
return { ok: false, error: byUri.error };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, value: byUri.value };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const byDocid = await store.getDocumentByDocid(docId);
|
|
61
|
+
if (!byDocid.ok) {
|
|
62
|
+
return { ok: false, error: byDocid.error };
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, value: byDocid.value };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadDocumentContent(
|
|
68
|
+
store: Pick<
|
|
69
|
+
SqliteAdapter,
|
|
70
|
+
"getDocumentByDocid" | "getDocumentByUri" | "getContent"
|
|
71
|
+
>,
|
|
72
|
+
docId: string,
|
|
73
|
+
req: Request
|
|
74
|
+
): Promise<
|
|
75
|
+
| { ok: true; doc: DocumentRow; content: string }
|
|
76
|
+
| { ok: false; response: Response }
|
|
77
|
+
> {
|
|
78
|
+
const docResult = await resolveDocumentReference(
|
|
79
|
+
store,
|
|
80
|
+
docId,
|
|
81
|
+
readRequestedUriFromUrl(req)
|
|
82
|
+
);
|
|
83
|
+
if (!docResult.ok) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
response: errorResponse("RUNTIME", docResult.error.message, 500),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!docResult.value) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
response: errorResponse("NOT_FOUND", "Document not found", 404),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const doc = docResult.value;
|
|
97
|
+
if (!doc.mirrorHash) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
response: errorResponse("NOT_FOUND", "Document content unavailable", 404),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const contentResult = await store.getContent(doc.mirrorHash);
|
|
105
|
+
if (!contentResult.ok || contentResult.value === null) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
response: errorResponse("RUNTIME", "Mirror content unavailable", 409),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { ok: true, doc, content: contentResult.value };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* POST /api/doc/:id/section-targets
|
|
117
|
+
* Body: { anchor?: string, line?: number } — exactly one selector.
|
|
118
|
+
* Canonical document URI always comes from the resolved stored document.
|
|
119
|
+
*/
|
|
120
|
+
export async function handleCreateSectionTarget(
|
|
121
|
+
store: Pick<
|
|
122
|
+
SqliteAdapter,
|
|
123
|
+
"getDocumentByDocid" | "getDocumentByUri" | "getContent"
|
|
124
|
+
>,
|
|
125
|
+
docId: string,
|
|
126
|
+
req: Request
|
|
127
|
+
): Promise<Response> {
|
|
128
|
+
const parsed = await parseClosedJson(req, CREATE_BODY_MAX_BYTES);
|
|
129
|
+
if (!parsed.ok) {
|
|
130
|
+
return errorResponse("VALIDATION", parsed.error, 400);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const selector = parseSectionTargetCreateSelector(parsed.value);
|
|
134
|
+
if (!selector.ok) {
|
|
135
|
+
return errorResponse("VALIDATION", selector.error, 400);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const loaded = await loadDocumentContent(store, docId, req);
|
|
139
|
+
if (!loaded.ok) return loaded.response;
|
|
140
|
+
|
|
141
|
+
// Top-level response uri shares schema maxLength — reject before create.
|
|
142
|
+
if (!isTransportBoundedCanonicalUri(loaded.doc.uri)) {
|
|
143
|
+
return errorResponse(
|
|
144
|
+
"VALIDATION",
|
|
145
|
+
CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
|
|
146
|
+
422
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Canonical identity from stored doc — never from caller.
|
|
151
|
+
const target = await createSectionTarget({
|
|
152
|
+
content: loaded.content,
|
|
153
|
+
uri: loaded.doc.uri,
|
|
154
|
+
...selector.value,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!target) {
|
|
158
|
+
const sections = extractSections(loaded.content);
|
|
159
|
+
const matched =
|
|
160
|
+
selector.value.anchor !== undefined
|
|
161
|
+
? sections.some((section) => section.anchor === selector.value.anchor)
|
|
162
|
+
: sections.some((section) => section.line === selector.value.line);
|
|
163
|
+
if (!matched) {
|
|
164
|
+
return errorResponse("NOT_FOUND", "Section not found", 404);
|
|
165
|
+
}
|
|
166
|
+
return errorResponse(
|
|
167
|
+
"VALIDATION",
|
|
168
|
+
"Section target exceeds size bounds",
|
|
169
|
+
422
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return jsonResponse(projectSectionTargetCreateResult(loaded.doc.uri, target));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* POST /api/doc/:id/section-targets/resolve
|
|
178
|
+
* Body: { target: SectionTargetV1 }
|
|
179
|
+
* Resolves against the stored document; URI mismatch yields status missing.
|
|
180
|
+
*/
|
|
181
|
+
export async function handleResolveSectionTarget(
|
|
182
|
+
store: Pick<
|
|
183
|
+
SqliteAdapter,
|
|
184
|
+
"getDocumentByDocid" | "getDocumentByUri" | "getContent"
|
|
185
|
+
>,
|
|
186
|
+
docId: string,
|
|
187
|
+
req: Request
|
|
188
|
+
): Promise<Response> {
|
|
189
|
+
const parsed = await parseClosedJson(req, RESOLVE_BODY_MAX_BYTES);
|
|
190
|
+
if (!parsed.ok) {
|
|
191
|
+
return errorResponse("VALIDATION", parsed.error, 400);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const body = parseSectionTargetResolveBody(parsed.value);
|
|
195
|
+
if (!body.ok) {
|
|
196
|
+
return errorResponse("VALIDATION", body.error, 400);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const loaded = await loadDocumentContent(store, docId, req);
|
|
200
|
+
if (!loaded.ok) return loaded.response;
|
|
201
|
+
|
|
202
|
+
// Top-level (and citation) uri must fit schema — reject before projection.
|
|
203
|
+
// Citation fail-closed does not repair an unbound top-level uri.
|
|
204
|
+
if (!isTransportBoundedCanonicalUri(loaded.doc.uri)) {
|
|
205
|
+
return errorResponse(
|
|
206
|
+
"VALIDATION",
|
|
207
|
+
CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
|
|
208
|
+
422
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const resolution = await resolveSectionTarget({
|
|
213
|
+
content: loaded.content,
|
|
214
|
+
target: body.value.target,
|
|
215
|
+
uri: loaded.doc.uri,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return jsonResponse(
|
|
219
|
+
projectSectionTargetResolveResult(loaded.doc.uri, resolution)
|
|
220
|
+
);
|
|
221
|
+
}
|
package/src/serve/server.ts
CHANGED
|
@@ -93,6 +93,10 @@ import {
|
|
|
93
93
|
handleDocSimilar,
|
|
94
94
|
} from "./routes/links";
|
|
95
95
|
import { createMcpHttpGateway } from "./routes/mcp";
|
|
96
|
+
import {
|
|
97
|
+
handleCreateSectionTarget,
|
|
98
|
+
handleResolveSectionTarget,
|
|
99
|
+
} from "./routes/section-targets";
|
|
96
100
|
import {
|
|
97
101
|
handleTraceDelete,
|
|
98
102
|
handleTraceExport,
|
|
@@ -1172,6 +1176,36 @@ export async function startServer(
|
|
|
1172
1176
|
);
|
|
1173
1177
|
},
|
|
1174
1178
|
},
|
|
1179
|
+
"/api/doc/:id/section-targets": {
|
|
1180
|
+
POST: async (req: Request) => {
|
|
1181
|
+
if (!isRequestAllowed(req, port)) {
|
|
1182
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
1183
|
+
}
|
|
1184
|
+
const parts = new URL(req.url).pathname.split("/");
|
|
1185
|
+
const id = decodeURIComponent(parts[3] || "");
|
|
1186
|
+
return withSecurityHeaders(
|
|
1187
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
1188
|
+
handleCreateSectionTarget(store, id, req)
|
|
1189
|
+
),
|
|
1190
|
+
isDev
|
|
1191
|
+
);
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
"/api/doc/:id/section-targets/resolve": {
|
|
1195
|
+
POST: async (req: Request) => {
|
|
1196
|
+
if (!isRequestAllowed(req, port)) {
|
|
1197
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
1198
|
+
}
|
|
1199
|
+
const parts = new URL(req.url).pathname.split("/");
|
|
1200
|
+
const id = decodeURIComponent(parts[3] || "");
|
|
1201
|
+
return withSecurityHeaders(
|
|
1202
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
1203
|
+
handleResolveSectionTarget(store, id, req)
|
|
1204
|
+
),
|
|
1205
|
+
isDev
|
|
1206
|
+
);
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1175
1209
|
"/api/doc/:id/backlinks": {
|
|
1176
1210
|
GET: async (req: Request) => {
|
|
1177
1211
|
const url = new URL(req.url);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
c8fbd761eeb10076ef5f05d917d7815000415fc869973341170cb45874e95133 gno-browser-clipper-v1.30.7.zip
|