@gmickel/gno 1.30.6 → 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.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/assets/skill/SKILL.md +2 -0
  3. package/assets/skill/mcp-reference.md +6 -0
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.30.6.zip → gno-browser-clipper-v1.31.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +1 -1
  8. package/spec/mcp.md +62 -0
  9. package/spec/output-schemas/section-target-create-result.schema.json +20 -0
  10. package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
  11. package/spec/output-schemas/section-target.schema.json +118 -0
  12. package/spec/output-schemas/section.schema.json +113 -0
  13. package/src/core/section-parse.ts +187 -0
  14. package/src/core/section-target-link.ts +154 -0
  15. package/src/core/section-target-resolve.ts +351 -0
  16. package/src/core/section-target-transport.ts +519 -0
  17. package/src/core/section-target.ts +263 -0
  18. package/src/core/sections.ts +60 -115
  19. package/src/mcp/AGENTS.md +1 -0
  20. package/src/mcp/CLAUDE.md +1 -0
  21. package/src/mcp/http-egress.ts +1 -0
  22. package/src/mcp/tools/index.ts +19 -0
  23. package/src/mcp/tools/sections.ts +512 -0
  24. package/src/sdk/client.ts +71 -1
  25. package/src/sdk/index.ts +5 -0
  26. package/src/sdk/types.ts +29 -1
  27. package/src/serve/public/globals.built.css +1 -1
  28. package/src/serve/public/lib/section-links.ts +189 -0
  29. package/src/serve/public/pages/DocView.tsx +219 -36
  30. package/src/serve/routes/section-targets.ts +221 -0
  31. package/src/serve/server.ts +34 -0
  32. package/browser-extension/artifacts/gno-browser-clipper-v1.30.6.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
+ }
@@ -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
- 20b500022a0f94f573aab83b4c2dd1626263f66141ffb3630fd7fd82cbc4254d gno-browser-clipper-v1.30.6.zip