visualproof 0.0.0-stage → 0.2.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 (146) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +99 -2
  3. package/dist/assets/fonts/JuliaMono-Regular.ttf +0 -0
  4. package/dist/assets/fonts/JuliaMono-RegularItalic.ttf +0 -0
  5. package/dist/assets/fonts/JuliaMono-SemiBold.ttf +0 -0
  6. package/dist/assets/fonts/LICENSE-JuliaMono.txt +93 -0
  7. package/dist/assets/latex/visualproof.sty +308 -0
  8. package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
  9. package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
  10. package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
  11. package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
  12. package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
  13. package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
  14. package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
  15. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
  16. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
  17. package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  18. package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  19. package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
  20. package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
  21. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
  22. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
  23. package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
  24. package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
  25. package/dist/extractor/Extract.lean +1180 -0
  26. package/dist/extractor/Usage.lean +900 -0
  27. package/dist/mcp/visualproof-mcp.mjs +476 -0
  28. package/dist/server/access.js +95 -0
  29. package/dist/server/agent.js +3270 -0
  30. package/dist/server/agentdocs.js +35 -0
  31. package/dist/server/agentlibrary.js +202 -0
  32. package/dist/server/agentnotes.js +701 -0
  33. package/dist/server/agentviews.js +703 -0
  34. package/dist/server/askfollow.js +286 -0
  35. package/dist/server/boardspots.js +159 -0
  36. package/dist/server/dirs.js +175 -0
  37. package/dist/server/doctor.js +186 -0
  38. package/dist/server/events.js +198 -0
  39. package/dist/server/export.js +420 -0
  40. package/dist/server/extract.js +204 -0
  41. package/dist/server/fetchurl.js +321 -0
  42. package/dist/server/figures.js +158 -0
  43. package/dist/server/history.js +780 -0
  44. package/dist/server/historyapi.js +304 -0
  45. package/dist/server/hover.js +595 -0
  46. package/dist/server/identity.js +114 -0
  47. package/dist/server/invites.js +241 -0
  48. package/dist/server/layoutthread.js +51 -0
  49. package/dist/server/library.js +600 -0
  50. package/dist/server/libraryroute.js +211 -0
  51. package/dist/server/live.js +1101 -0
  52. package/dist/server/lostfound.js +199 -0
  53. package/dist/server/main.js +1406 -0
  54. package/dist/server/mcptools.js +496 -0
  55. package/dist/server/pastdoc.js +73 -0
  56. package/dist/server/pipeline.js +159 -0
  57. package/dist/server/poppler.js +251 -0
  58. package/dist/server/presence.js +252 -0
  59. package/dist/server/project.js +222 -0
  60. package/dist/server/prompt.js +53 -0
  61. package/dist/server/readonly.js +152 -0
  62. package/dist/server/readonlygate.js +123 -0
  63. package/dist/server/renames.js +268 -0
  64. package/dist/server/review.js +334 -0
  65. package/dist/server/reviewapi.js +188 -0
  66. package/dist/server/snapshot.js +50 -0
  67. package/dist/server/sourcestore.js +473 -0
  68. package/dist/server/status.js +360 -0
  69. package/dist/server/talk.js +476 -0
  70. package/dist/server/tokens.js +221 -0
  71. package/dist/server/usage.js +501 -0
  72. package/dist/server/versions.js +82 -0
  73. package/dist/server/view.js +164 -0
  74. package/dist/server/views.js +1167 -0
  75. package/dist/shared/askfollow.js +46 -0
  76. package/dist/shared/board.js +332 -0
  77. package/dist/shared/build.js +2 -0
  78. package/dist/shared/collab.js +241 -0
  79. package/dist/shared/comment.js +285 -0
  80. package/dist/shared/context.js +157 -0
  81. package/dist/shared/doc/authors.js +98 -0
  82. package/dist/shared/doc/build.js +358 -0
  83. package/dist/shared/doc/latex.js +455 -0
  84. package/dist/shared/doc/markdown.js +159 -0
  85. package/dist/shared/doc/model.js +37 -0
  86. package/dist/shared/doc/order.js +124 -0
  87. package/dist/shared/doc/outline.js +45 -0
  88. package/dist/shared/doc/proof.js +272 -0
  89. package/dist/shared/doc/sections.js +93 -0
  90. package/dist/shared/doc/settings.js +75 -0
  91. package/dist/shared/edgecodec.js +102 -0
  92. package/dist/shared/editor.js +64 -0
  93. package/dist/shared/excerpt.js +87 -0
  94. package/dist/shared/graph.js +16 -0
  95. package/dist/shared/items.js +92 -0
  96. package/dist/shared/keys.js +203 -0
  97. package/dist/shared/layout.js +926 -0
  98. package/dist/shared/lexer.js +130 -0
  99. package/dist/shared/library.js +711 -0
  100. package/dist/shared/lostfound.js +150 -0
  101. package/dist/shared/moves.js +244 -0
  102. package/dist/shared/notecard.js +295 -0
  103. package/dist/shared/noteplace.js +505 -0
  104. package/dist/shared/palette.js +46 -0
  105. package/dist/shared/pastgraph.js +103 -0
  106. package/dist/shared/places.js +80 -0
  107. package/dist/shared/projectnotes.js +244 -0
  108. package/dist/shared/query.js +818 -0
  109. package/dist/shared/review.js +642 -0
  110. package/dist/shared/reviewqueue.js +333 -0
  111. package/dist/shared/ring.js +366 -0
  112. package/dist/shared/scope.js +255 -0
  113. package/dist/shared/scopeedit.js +81 -0
  114. package/dist/shared/search.js +191 -0
  115. package/dist/shared/separate.js +178 -0
  116. package/dist/shared/sharesnap.js +281 -0
  117. package/dist/shared/sticky.js +501 -0
  118. package/dist/shared/timeline.js +73 -0
  119. package/dist/shared/titles.js +300 -0
  120. package/dist/shared/viewgraph.js +157 -0
  121. package/dist/shared/views.js +859 -0
  122. package/dist/web/bundle/app.js +52671 -0
  123. package/dist/web/bundle/edges.js +407 -0
  124. package/dist/web/bundle/layout.js +1010 -0
  125. package/dist/web/bundle/pdfworker.js +48322 -0
  126. package/dist/web/index.html +257 -0
  127. package/dist/web/src/motion.js +430 -0
  128. package/dist/web/src/past.js +224 -0
  129. package/dist/web/src/scene.js +804 -0
  130. package/dist/web/style.css +2548 -0
  131. package/docs/agent-api/README.md +91 -0
  132. package/docs/agent-api/advice.md +108 -0
  133. package/docs/agent-api/export.md +120 -0
  134. package/docs/agent-api/following.md +140 -0
  135. package/docs/agent-api/history.md +142 -0
  136. package/docs/agent-api/library.md +65 -0
  137. package/docs/agent-api/mcp.md +104 -0
  138. package/docs/agent-api/notes.md +93 -0
  139. package/docs/agent-api/reading.md +166 -0
  140. package/docs/agent-api/requests.md +94 -0
  141. package/docs/agent-api/talking.md +100 -0
  142. package/docs/agent-api/tcb.md +135 -0
  143. package/docs/agent-api/views.md +125 -0
  144. package/docs/agent-api/words.md +56 -0
  145. package/package.json +39 -4
  146. package/test/toolchains.json +5 -0
@@ -0,0 +1,35 @@
1
+ // The agents' documentation (docs/agent-api/): an index, README.md — the rules and the topics — and
2
+ // one Markdown file per topic, served as GET /api/agent/docs and GET /api/agent/docs/<topic>
3
+ // (server/agent.ts), and by the MCP server as its docs tool and its resources. The topics below are
4
+ // what the entry, the docs tool and the resources name; a test keeps them and the folder the same.
5
+ import { readFileSync } from "node:fs";
6
+ /** the documentation, beside the server */
7
+ const DOCS_DIR = new URL("../../docs/agent-api/", import.meta.url);
8
+ /** The topics, in the order a newcomer reads them. */
9
+ export const DOC_TOPICS = [
10
+ { topic: "requests", title: "Requests, answers and tokens", about: "headers, tokens, errors, the entry, every endpoint" },
11
+ { topic: "words", title: "Words", about: "declaration, unit, TCB, trusted, review order, status, key" },
12
+ { topic: "reading", title: "Reading declarations", about: "search, declaration, source, context, module, Lean" },
13
+ { topic: "tcb", title: "Targets and their TCB", about: "targets, review sets, keys, why, uses; marks, trust" },
14
+ { topic: "advice", title: "Advice and the pre-review", about: "opinions, comments, a pre-review step by step" },
15
+ { topic: "notes", title: "Notes", about: "notes on the boards" },
16
+ { topic: "following", title: "Following", about: "where people are, your view, links to it, what people see" },
17
+ { topic: "views", title: "Views and the layout", about: "queries, views, places, pins" },
18
+ { topic: "library", title: "The library and links", about: "papers and images, their pages, links to them" },
19
+ { topic: "export", title: "Exported documents", about: "a view's document, typeset" },
20
+ { topic: "history", title: "History and activity", about: "the timeline, builds, renames, the lost-and-found" },
21
+ { topic: "talking", title: "Talking with people", about: "your inbox, replies, your state" },
22
+ { topic: "mcp", title: "VisualProof's MCP server", about: "installing it, its tools and their actions" },
23
+ ];
24
+ /** A topic's Markdown (null: the index), or null where there is no such topic or no file. */
25
+ export function readDoc(topic) {
26
+ const file = topic === null ? "README" : DOC_TOPICS.some((t) => t.topic === topic) ? topic : null;
27
+ if (!file)
28
+ return null;
29
+ try {
30
+ return readFileSync(new URL(`${file}.md`, DOCS_DIR), "utf8");
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
@@ -0,0 +1,202 @@
1
+ // The library and its links for agents (AttachmentSpec decision 20, the owner's of 2026-10-10:
2
+ // "everything through the API"): what the page does with the library, its files' pages and text and
3
+ // the links, an agent does through these endpoints, and through VisualProof's MCP server's tools,
4
+ // which relay them (server/mcptools.ts). They are server/agent.ts's, in its shape
5
+ // (server/agentviews.ts, AgentEndpoint), and call the operations the page's routes call
6
+ // (server/library.ts), so that both doors keep the same rules. At level advise an agent adds files
7
+ // and makes links, and edits, archives or removes its own; at level full also what others made
8
+ // (AgentSpec decision 9). Everything it does is recorded in its own log, marked as an agent's.
9
+ //
10
+ // A file is uploaded as its bytes, the body of the request (the MCP tool library's action add_file
11
+ // reads a file on the agent's machine and sends it so); a page and a clip are answered as PNG images, which
12
+ // the MCP server hands an agent that reads images as an image.
13
+ import { MIME, MAX_FILE } from "../shared/library.js";
14
+ import { fail } from "./agent.js";
15
+ import { graphOf } from "./agentviews.js";
16
+ import { libraryApiOf } from "./library.js";
17
+ /** Where an agent uploads a file's bytes: main.ts reads the body of this one as bytes, not JSON. */
18
+ export const UPLOAD_PATH = "/api/agent/library/upload";
19
+ /** The library's operations for the agents' API: the same object as the page's routes use. */
20
+ export function libraryFor(world) {
21
+ const lib = world.library;
22
+ if (!lib)
23
+ return null;
24
+ return libraryApiOf(world, () => ({
25
+ review: world.review,
26
+ project: world.project,
27
+ library: lib,
28
+ graph: () => graphOf(world),
29
+ // every file the library takes shows on the Project: a note carries it, on the ring
30
+ fileAdded: (f, who, note) => void world.views.fileAdded(f, { by: who.name, agent: who.agent, person: who.person ?? null }, note),
31
+ }));
32
+ }
33
+ /** An answer of the library as the agents' API answers: JSON, or bytes to download (a file, a page
34
+ * or a clip as an image). */
35
+ function answer(a, name = "file") {
36
+ if (a.bytes) {
37
+ const ext = Object.entries(MIME).find(([, m]) => m === a.bytes.type)?.[0] ?? "bin";
38
+ return { status: a.status, body: null, file: { body: a.bytes.data, type: a.bytes.type, name: `${name}.${ext === "jpeg" ? "jpg" : ext}` } };
39
+ }
40
+ const b = a.body;
41
+ // every refusal says what to do instead (AgentSpec, "The API")
42
+ if (a.status >= 400 && b && typeof b === "object" && !b.hint)
43
+ return { status: a.status, body: { ...b, hint: "GET /api/agent explains the library's endpoints." } };
44
+ // the review's change is for pages, which apply it at once; an agent reads what it made
45
+ if (b && typeof b === "object" && "change" in b) {
46
+ const { change: _change, ...rest } = b;
47
+ return { status: a.status, body: rest };
48
+ }
49
+ return { status: a.status, body: a.body };
50
+ }
51
+ /** The agents' endpoints about the library and its links. `refuse` says why an agent may not do
52
+ * what needs `need`. */
53
+ export function libraryEndpoints(world, refuse) {
54
+ const body = (a) => (a.body && typeof a.body === "object" && !Array.isArray(a.body) && !Buffer.isBuffer(a.body) ? a.body : {});
55
+ const api = () => libraryFor(world) ?? fail(404, "This server keeps no library.", "");
56
+ /** Writes say who makes them, and that it is an agent; then what their level allows: adding at
57
+ * advise, and at full changing what others made (`full` in the asker). */
58
+ const asker = (a) => {
59
+ if (a.caller.kind !== "agent")
60
+ return fail(400, "This API records what agents do: say you are one.", "Send X-VisualProof-Kind: agent with every request, beside X-VisualProof-Name.");
61
+ if (!a.caller.name)
62
+ return fail(401, "Say who you are first.", "Send X-VisualProof-Name: <your name>, e.g. Claude.", { who: true });
63
+ const no = refuse(a.caller, "advise");
64
+ if (no)
65
+ return no;
66
+ return { name: a.caller.name, agent: true, person: null, full: refuse(a.caller, "full") === null };
67
+ };
68
+ const q = (a, k) => a.query.get(k);
69
+ const sha = "a file's SHA-256 (64 hex digits), as GET /api/agent/library lists it";
70
+ const subject = "decl(<full name>) for a declaration, ns(<namespace>) or file(<module prefix>) for a cluster, as a view's scope names them";
71
+ const place = "page: the page, from 1 (a PDF's links are at a page; an image has none); region: [left, top, right, bottom] in fractions of the page from its top left (0–1; the word boxes of GET library/text are in the same units); quote: the text quoted, or {text, before?, after?} with a little of the text around it";
72
+ const read = (run, name) => async (a) => {
73
+ const l = api();
74
+ if (!("list" in l))
75
+ return l;
76
+ return answer(await run(l, a), name?.(a));
77
+ };
78
+ const write = (run) => async (a) => {
79
+ const l = api();
80
+ if (!("list" in l))
81
+ return l;
82
+ const who = asker(a);
83
+ if (!("full" in who))
84
+ return who;
85
+ return answer(await run(l, a, who));
86
+ };
87
+ return [
88
+ {
89
+ method: "GET", path: "/api/agent/library",
90
+ about: "The project's library (AttachmentSpec decision 1): the PDFs and images people and agents added, each with its record — title (the PDF's own, else the file's name), type, size, pages, who added it and when, the URL it came from, how many links point into it, and whether this server has its bytes (have: false — a link's file added elsewhere; it can be added again, only a file of that hash) — the archived apart, and how full the library is.",
91
+ params: {},
92
+ returns: "{files: [{sha, title, type, size, pages?, by, at, agent?, url?, name?, have, links}], archived: […], used, limit, maxFile}",
93
+ run: read((l) => l.list()),
94
+ },
95
+ {
96
+ method: "GET", path: "/api/agent/library/file",
97
+ about: "One file's record and every link into it.",
98
+ params: { sha },
99
+ returns: "{file, links: [{id, subject, file, page?, region?, quote?, label, by, at, agent?}]}",
100
+ run: read((l, a) => l.file(q(a, "sha"))),
101
+ },
102
+ {
103
+ method: "GET", path: "/api/agent/library/bytes",
104
+ about: "A file's bytes, as it was added (files never change): a PDF or an image. The MCP tool library (action save) writes them to a file on your machine.",
105
+ params: { sha },
106
+ returns: "the file (application/pdf, image/png, image/jpeg, image/webp or image/gif)",
107
+ run: read((l, a) => l.bytes(q(a, "sha")), (a) => (q(a, "sha") ?? "file").slice(0, 12)),
108
+ },
109
+ {
110
+ method: "POST", path: "/api/agent/library/url", needs: "advise",
111
+ about: `Add the file at an address to the library: the server fetches it once and keeps it (an arXiv abstract's address, arxiv.org/abs/…, gives its PDF). http and https only; never an address of a private or local network; at most ${MAX_FILE / (1024 * 1024)} MB; the type is read from the bytes: a PDF, PNG, JPEG, WebP or GIF, never SVG. The same bytes are stored once: a file already in the library is answered as it is (existed: true), restored where it was archived.`,
112
+ params: { url: "the file's address" },
113
+ returns: "{ok, file, existed}",
114
+ run: write((l, a, who) => l.addUrl(body(a).url, who)),
115
+ },
116
+ {
117
+ method: "POST", path: UPLOAD_PATH, needs: "advise",
118
+ about: `Upload a file to the library: its bytes are the body of the request (not JSON; e.g. curl --data-binary @paper.pdf), its name in the query. A PDF, PNG, JPEG, WebP or GIF, at most ${MAX_FILE / (1024 * 1024)} MB, its type read from the bytes. With sha, the bytes of a file a link names that this server lacks, and no other. The MCP tool library (action add_file) reads a file on your machine and sends it.`,
119
+ params: { name: "the file's name, in the query", sha: "optional, in the query: the file a link names, whose bytes these must be" },
120
+ returns: "{ok, file, existed, filled?}",
121
+ run: write(async (l, a, who) => {
122
+ const b = a.body;
123
+ if (!b || !Buffer.isBuffer(b.bytes))
124
+ return { status: 400, body: { error: "The body is the file's bytes.", hint: "Send them as they are (curl --data-binary @file), the name in the query; the MCP tool library (action add_file) does it from a path on your machine." } };
125
+ // an MCP server from before uploads sends the path as JSON: it cannot read files
126
+ if (b.bytes[0] === 0x7b && b.bytes.length < 65536 && /"file"\s*:/.test(b.bytes.toString("utf8")))
127
+ return { status: 400, body: { error: "The body is the file's bytes, not JSON naming a path.", hint: "Your MCP server is older than uploads: download it again (GET /api/agent/mcp) and restart your session; or send the bytes with curl --data-binary @file." } };
128
+ return l.add(b.bytes, { name: q(a, "name"), expect: q(a, "sha") }, who);
129
+ }),
130
+ },
131
+ {
132
+ method: "POST", path: "/api/agent/library/edit", needs: "advise",
133
+ about: "Edit a file's title. Your own at level advise; anyone's at full. A note that carries the file alone and whose text is its title, or the name it was added under, takes the new title by itself, as an edit by VisualProof; words someone wrote stay.",
134
+ params: { sha, title: "the new title" },
135
+ returns: "{ok, changed, file}",
136
+ run: write((l, a, who) => l.editFile(body(a).sha, body(a).title, who)),
137
+ },
138
+ {
139
+ method: "POST", path: "/api/agent/library/archive", needs: "advise",
140
+ about: "Archive a file — nothing is deleted: it leaves the library's list, its links say so, and it is restored from the archive — or restore it (on: false). Your own at level advise; anyone's at full.",
141
+ params: { sha, on: "true (default): archive; false: restore" },
142
+ returns: "{ok, changed, file}",
143
+ run: write((l, a, who) => l.archive(body(a).sha, body(a).on, who)),
144
+ },
145
+ {
146
+ method: "GET", path: "/api/agent/library/text",
147
+ about: "A PDF's page as text, with the place of every word: [text, left, top, right, bottom] in fractions of the page from its top left, the units a link's region is in, so that a region can be made from words' boxes; lines says where each line starts among the words.",
148
+ params: { sha, page: "the page, from 1 (default 1)" },
149
+ returns: "{file, title, page, pages, size: {width, height} (points), text, words: [[text, x0, y0, x1, y1]], lines}",
150
+ run: read((l, a) => l.text(q(a, "sha"), q(a, "page"))),
151
+ },
152
+ {
153
+ method: "GET", path: "/api/agent/library/search",
154
+ about: "Search a PDF's text: every match's page, the boxes of its words (fractions of the page) and the text around it — what a link's page, region and quote are made from.",
155
+ params: { sha, q: "the text to find (case and line breaks do not matter)", limit: "how many matches (default 50, at most 500)" },
156
+ returns: "{file, title, q, matches: [{page, boxes, text, before, after}], more}",
157
+ run: read((l, a) => l.search(q(a, "sha"), q(a, "q"), q(a, "limit"))),
158
+ },
159
+ {
160
+ method: "GET", path: "/api/agent/library/page",
161
+ about: "A PDF's page drawn as a PNG image, for an agent that reads images (the MCP tool library, action page, hands it over as an image).",
162
+ params: { sha, page: "the page, from 1", width: "its width in pixels: 400, 800, 1200 (default), 1600 or 2000" },
163
+ returns: "image/png",
164
+ run: read((l, a) => l.page(q(a, "sha"), q(a, "page"), q(a, "width")), (a) => `page-${q(a, "page") ?? 1}`),
165
+ },
166
+ {
167
+ method: "GET", path: "/api/agent/library/clip",
168
+ about: "A region of a PDF's page drawn as a PNG image: a figure, a theorem's statement.",
169
+ params: { sha, page: "the page, from 1", region: "left,top,right,bottom: fractions of the page from its top left", width: "its width in pixels: 400, 800 (default), 1200, 1600 or 2000" },
170
+ returns: "image/png",
171
+ run: read((l, a) => l.clip(q(a, "sha"), q(a, "page"), q(a, "region"), q(a, "width")), (a) => `clip-${q(a, "page") ?? 1}`),
172
+ },
173
+ {
174
+ method: "GET", path: "/api/agent/links",
175
+ about: "Links (AttachmentSpec decision 4): those of a subject — a declaration or a cluster — or into a file, in the order they were made; all of them with neither. A declaration's own links come first when the page opens its source beside the paper (decision 8); one with none shows the innermost cluster around it that has one.",
176
+ params: { subject, file: sha },
177
+ returns: "{links: [{id, subject, file, fileTitle, page?, region?, quote?, label, by, at, agent?, archived?, have}]}",
178
+ run: read((l, a) => l.links({ subject: q(a, "subject"), file: q(a, "file") })),
179
+ },
180
+ {
181
+ method: "POST", path: "/api/agent/link", needs: "advise",
182
+ about: "Link a declaration or a cluster to a place in a file of the library: a page with quoted text, a region, or both; an image whole or a region of it. Find the place with library/search or library/text. The label is what the page lists (\"Theorem 3, p. 12\"); left out, the page and the quote's first words. Recorded in your review log, marked as an agent's.",
183
+ params: { subject, file: sha, page: place, region: "see page", quote: "see page", label: "optional: e.g. Theorem 3, p. 12" },
184
+ returns: "{ok, link}",
185
+ run: write((l, a, who) => l.link(body(a), who)),
186
+ },
187
+ {
188
+ method: "POST", path: "/api/agent/link/edit", needs: "advise",
189
+ about: "Edit a link: its label, its place (page, region, quote; null takes a part away) or its subject. Your own at level advise; anyone's at full.",
190
+ params: { id: "the link's id", label: "optional", page: "optional", region: "optional", quote: "optional", subject: "optional" },
191
+ returns: "{ok, changed, link}",
192
+ run: write((l, a, who) => l.editLink(body(a), who)),
193
+ },
194
+ {
195
+ method: "POST", path: "/api/agent/unlink", needs: "advise",
196
+ about: "Remove a link. Your own at level advise; anyone's at full.",
197
+ params: { id: "the link's id" },
198
+ returns: "{ok, removed}",
199
+ run: write((l, a, who) => l.unlink(body(a).id, who)),
200
+ },
201
+ ];
202
+ }