visualproof 0.2.0 → 0.4.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 +88 -33
- package/dist/assets/tips/phone-fingers.light.mp4 +0 -0
- package/dist/assets/tips/phone-fingers.light.webp +0 -0
- package/dist/assets/tips/tour-agent.webp +0 -0
- package/dist/assets/tips/tour-arrange.webp +0 -0
- package/dist/assets/tips/tour-artwork.webp +0 -0
- package/dist/assets/tips/tour-checks.webp +0 -0
- package/dist/assets/tips/tour-export.webp +0 -0
- package/dist/assets/tips/tour-keys.webp +0 -0
- package/dist/assets/tips/tour-library.webp +0 -0
- package/dist/assets/tips/tour-project.webp +0 -0
- package/dist/assets/tips/tour-review.webp +0 -0
- package/dist/assets/tips/tour-share.webp +0 -0
- package/dist/assets/tips/tour-source.webp +0 -0
- package/dist/assets/tips/tour-target.webp +0 -0
- package/dist/assets/tips/tour-view.webp +0 -0
- package/dist/assets/tips/tour.json +289 -0
- package/dist/assets/tips/tour.webp +0 -0
- package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
- package/dist/assets/webfonts/metrics.json +1 -0
- package/dist/assets/webfonts/ttf/jost-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/jost-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans-math.ttf +0 -0
- package/dist/assets/webfonts/ttf/noto-sans.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-400-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-500-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-600-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-sans-3-700-normal.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-400-italic.ttf +0 -0
- package/dist/assets/webfonts/ttf/source-serif-4-600-italic.ttf +0 -0
- package/dist/mcp/visualproof-mcp.mjs +3 -1
- package/dist/server/agent.js +94 -91
- package/dist/server/agentdocs.js +1 -1
- package/dist/server/artcmd.js +335 -0
- package/dist/server/children.js +85 -0
- package/dist/server/doctor.js +105 -42
- package/dist/server/events.js +10 -0
- package/dist/server/export.js +11 -4
- package/dist/server/extract.js +3 -2
- package/dist/server/figures.js +3 -2
- package/dist/server/film.js +380 -0
- package/dist/server/history.js +11 -1
- package/dist/server/hover.js +8 -5
- package/dist/server/live.js +72 -9
- package/dist/server/main.js +352 -23
- package/dist/server/mcptools.js +4 -1
- package/dist/server/media.js +114 -0
- package/dist/server/pipeline.js +21 -10
- package/dist/server/poppler.js +3 -2
- package/dist/server/readonlygate.js +4 -4
- package/dist/server/review.js +4 -1
- package/dist/server/reviewapi.js +35 -1
- package/dist/server/snapshot.js +10 -1
- package/dist/server/sourcestore.js +3 -2
- package/dist/server/status.js +3 -2
- package/dist/server/supervise.js +219 -0
- package/dist/server/updates.js +951 -0
- package/dist/server/usage.js +3 -1
- package/dist/server/version.js +183 -0
- package/dist/server/view.js +0 -7
- package/dist/server/views.js +24 -0
- package/dist/shared/artfonts.js +81 -0
- package/dist/shared/artrecipe.js +316 -0
- package/dist/shared/collab.js +7 -2
- package/dist/shared/declfind.js +95 -0
- package/dist/shared/lostfound.js +1 -0
- package/dist/shared/pastgraph.js +6 -6
- package/dist/shared/review.js +62 -5
- package/dist/shared/timeline.js +5 -0
- package/dist/web/bundle/app.js +3006 -413
- package/dist/web/bundle/edges.js +1 -1
- package/dist/web/index.html +13 -1
- package/dist/web/src/agentmark.js +14 -0
- package/dist/web/src/artwork.js +1000 -0
- package/dist/web/src/bundles.js +151 -0
- package/dist/web/src/canvas.js +3716 -0
- package/dist/web/src/drag.js +61 -0
- package/dist/web/src/edgequeue.js +204 -0
- package/dist/web/src/gl/gl.js +123 -0
- package/dist/web/src/gl/renderer.js +451 -0
- package/dist/web/src/halo.js +95 -0
- package/dist/web/src/highlight.js +108 -0
- package/dist/web/src/overview.js +393 -0
- package/dist/web/src/reviewqueue.js +250 -0
- package/dist/web/src/selection.js +272 -0
- package/dist/web/src/status.js +80 -0
- package/dist/web/src/visitors.js +318 -0
- package/dist/web/src/visits.js +174 -0
- package/dist/web/style.css +196 -1
- package/docs/agent-api/README.md +1 -1
- package/docs/agent-api/export.md +41 -2
- package/docs/agent-api/history.md +5 -3
- package/docs/agent-api/mcp.md +1 -1
- package/docs/agent-api/reading.md +2 -1
- package/package.json +5 -1
package/dist/server/main.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// visualproof serve <path> — read a built Lean project and serve its declaration graph
|
|
3
3
|
// visualproof export <path> — the same page, frozen into one HTML file
|
|
4
4
|
import { historyAnswer } from "./historyapi.js";
|
|
5
|
+
import { gitSource } from "./history.js";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { createServer } from "node:http";
|
|
8
|
-
import { extname, join, resolve } from "node:path";
|
|
9
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
9
11
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
10
12
|
import { WEB, WEBFONTS, bundleDir, bundleDirIn, ensureBundle, prepare, useBundleDir } from "./pipeline.js";
|
|
13
|
+
import { makeArtwork, pngOf, projectView } from "./artcmd.js";
|
|
11
14
|
import { SiteBusy, cacheHome, dataHome, lockSite, openSite, projectDirs } from "./dirs.js";
|
|
12
15
|
import { ProjectError, openProject, targetsInWords } from "./project.js";
|
|
13
16
|
import { Live } from "./live.js";
|
|
@@ -15,7 +18,7 @@ import { ReviewStore } from "./review.js";
|
|
|
15
18
|
import { isDeclName, LeanHover } from "./hover.js";
|
|
16
19
|
import { Identities, layoutAuthor } from "./identity.js";
|
|
17
20
|
import { LAYOUT_VERSION } from "../shared/graph.js";
|
|
18
|
-
import { MAX_MEMORY_BYTES, Views } from "./views.js";
|
|
21
|
+
import { MAX_MEMORY_BYTES, readProjectView, Views } from "./views.js";
|
|
19
22
|
import { cleanView, defaultAgentName, validSession, withoutArtwork } from "../shared/collab.js";
|
|
20
23
|
import { writeSnapshot } from "./snapshot.js";
|
|
21
24
|
import { lakeTextHash } from "./status.js";
|
|
@@ -36,6 +39,12 @@ import { UPLOAD_PATH } from "./agentlibrary.js";
|
|
|
36
39
|
import { DEFAULT_LIBRARY_LIMIT, validSha } from "../shared/library.js";
|
|
37
40
|
import { doctor, packageVersion, printReport } from "./doctor.js";
|
|
38
41
|
import { READONLY_PATH } from "./readonly.js";
|
|
42
|
+
import { sendMedia, TIP_PATH, TIPS_DIR, tipClips, tourData, tourVtt } from "./media.js";
|
|
43
|
+
import { withoutReasons } from "../shared/timeline.js";
|
|
44
|
+
import { FROM_PACKAGE, PACKAGE_ROOT, UPDATE_MODES, Updates } from "./updates.js";
|
|
45
|
+
import { RESTART_STATUS, supervise } from "./supervise.js";
|
|
46
|
+
import { stopChildren } from "./children.js";
|
|
47
|
+
import { filmStatus, serveFilm, useFilm } from "./film.js";
|
|
39
48
|
import { finalAnswer, readOnlyAllows, ReadOnlyBounds } from "./readonlygate.js";
|
|
40
49
|
/** The usage, to stderr with status 2 for a command line it cannot read; asked for (`--help`),
|
|
41
50
|
* to stdout with status 0. */
|
|
@@ -44,10 +53,12 @@ function usage(code = 2) {
|
|
|
44
53
|
[--site dir] [--cache dir] [--graph saved.json]
|
|
45
54
|
[--identity-header X-Forwarded-User] [--editor URL-template]
|
|
46
55
|
[--agents read|advise|full] [--access-key-file file] [--public] [--jobs ${DEFAULT_JOBS}]
|
|
47
|
-
[--public-url https://host] [--library-limit GB]
|
|
56
|
+
[--public-url https://host] [--library-limit GB] [--updates auto|notify|off]
|
|
48
57
|
visualproof purge-file <lean-project> <sha256> [--site dir] [--cache dir]
|
|
49
58
|
visualproof export <lean-project> -o page.html [--label text] [--root Name]... [--graph saved.json]
|
|
50
59
|
[--jobs ${DEFAULT_JOBS}] [--site dir] [--cache dir]
|
|
60
|
+
visualproof artwork <lean-project> --recipe recipe.json [-o name] [--png] [--svg] [--root Name]...
|
|
61
|
+
[--site dir] [--cache dir] [--jobs ${DEFAULT_JOBS}] [--screen 1920x1200] [--graph saved.json]
|
|
51
62
|
visualproof doctor [<lean-project>] [--root Name]... [--site dir] [--cache dir]
|
|
52
63
|
visualproof --version
|
|
53
64
|
|
|
@@ -65,13 +76,27 @@ function usage(code = 2) {
|
|
|
65
76
|
--cache the root of the caches, which a rebuild makes again: the project's (extractions, the
|
|
66
77
|
usage pass's results, prepared views, typeset documents, Lean's language server's
|
|
67
78
|
folder) in projects/<project>-<hash>/, the Lean programs compiled per toolchain in
|
|
68
|
-
lean/, the page's bundle in web
|
|
79
|
+
lean/, the page's bundle in web/, VisualProof's own releases in updates.json and the
|
|
80
|
+
patches downloaded in versions/ (code the servers run: keep the root yours alone),
|
|
81
|
+
and the tips' tour film in media/.
|
|
82
|
+
Default: ${cacheHome()}
|
|
69
83
|
(XDG_DATA_HOME and XDG_CACHE_HOME move both homes, on any platform)
|
|
70
84
|
|
|
71
85
|
--graph use a saved extraction (a projects/*/extract/*.json of the cache) instead of reading
|
|
72
86
|
the build; the project path still supplies the sources
|
|
73
87
|
export writes one self-contained HTML file: the page, its data and the sources, frozen
|
|
74
88
|
|
|
89
|
+
artwork makes the picture an artwork recipe says (the agents' API's, docs/agent-api/export.md:
|
|
90
|
+
the size, the look, names by a rule, the targets, the selection), as the page's
|
|
91
|
+
artwork mode makes it, without a browser: an SVG and a PNG (--png or --svg for one),
|
|
92
|
+
named by -o (a name, or a path without its extension), else the recipe's title, else
|
|
93
|
+
the project. With --site, the board as that site keeps it (its layout, pins and folds)
|
|
94
|
+
and its review's targets; without, the board as a new site would lay it out, and the
|
|
95
|
+
targets the recipe names. --screen: the screen the board is drawn on, as a reader's
|
|
96
|
+
(default 1920x1200, at least 640x480): how large names are in the picture. It reads
|
|
97
|
+
the site, never writes it; it writes the files and the cache's extraction. Anything
|
|
98
|
+
that fails ends it with status 1. Notes are not drawn yet.
|
|
99
|
+
|
|
75
100
|
--identity-header behind a proxy that authenticates (Tailscale, oauth2-proxy, …): take each
|
|
76
101
|
reader's name from this header instead of the name the page gives. Only use it when
|
|
77
102
|
the proxy is the only way to reach the server: the header is believed as it comes.
|
|
@@ -111,6 +136,16 @@ function usage(code = 2) {
|
|
|
111
136
|
--public-url the address people reach this server at, when it is not the one their browser
|
|
112
137
|
sends (a proxy in front): the prompt Share makes for an agent tells it this address.
|
|
113
138
|
|
|
139
|
+
--updates what the server does with VisualProof's own releases, asked of the npm registry
|
|
140
|
+
(through npm) at start and once a day: auto (the default) — a patch of the version it
|
|
141
|
+
runs (0.4.x) is downloaded into the cache and taken at a quiet moment, nobody on a page,
|
|
142
|
+
open pages reloading where they were; a newer version (0.5.0) is told of, in the log
|
|
143
|
+
and the help, with the command that installs it, after which the server restarts into
|
|
144
|
+
it by itself; notify — told of, never downloaded; off — never asked (and no
|
|
145
|
+
supervisor process), and the tips' tour film never fetched: the server reaches the
|
|
146
|
+
network for nothing of its own. VISUALPROOF_UPDATES says the same. From a checkout,
|
|
147
|
+
never asked.
|
|
148
|
+
|
|
114
149
|
--library-limit the most the project's library of papers and images may take, in GB (default
|
|
115
150
|
${DEFAULT_LIBRARY_LIMIT / 1024 ** 3}); its files are kept in the site directory, by their hash.
|
|
116
151
|
|
|
@@ -131,9 +166,14 @@ function parseArgs(argv) {
|
|
|
131
166
|
console.log(packageVersion());
|
|
132
167
|
process.exit(0);
|
|
133
168
|
}
|
|
134
|
-
if (cmd !== "serve" && cmd !== "export" && cmd !== "purge-file" && cmd !== "doctor")
|
|
169
|
+
if (cmd !== "serve" && cmd !== "export" && cmd !== "purge-file" && cmd !== "doctor" && cmd !== "artwork")
|
|
135
170
|
usage();
|
|
136
|
-
const
|
|
171
|
+
const fromEnv = process.env.VISUALPROOF_UPDATES;
|
|
172
|
+
if (cmd === "serve" && fromEnv !== undefined && !UPDATE_MODES.includes(fromEnv)) {
|
|
173
|
+
console.error(`visualproof: VISUALPROOF_UPDATES=${fromEnv}: say auto, notify or off`);
|
|
174
|
+
process.exit(2);
|
|
175
|
+
}
|
|
176
|
+
const o = { cmd, path: "", roots: [], port: 4173, host: "127.0.0.1", site: null, cache: null, graph: null, out: null, label: "VisualProof", identityHeader: null, editor: null, agents: "full", accessKeyFile: null, jobs: DEFAULT_JOBS, publicUrl: null, libraryLimit: DEFAULT_LIBRARY_LIMIT, sha: null, publicRead: false, recipe: null, png: false, svg: false, screen: { w: 1920, h: 1200 }, updates: UPDATE_MODES.includes(fromEnv) ? fromEnv : "auto" };
|
|
137
177
|
for (let i = 0; i < rest.length; i++) {
|
|
138
178
|
const a = rest[i];
|
|
139
179
|
const val = () => rest[++i] ?? usage();
|
|
@@ -161,6 +201,18 @@ function parseArgs(argv) {
|
|
|
161
201
|
o.accessKeyFile = resolve(val());
|
|
162
202
|
else if (a === "--public")
|
|
163
203
|
o.publicRead = true;
|
|
204
|
+
else if (a === "--recipe" && cmd === "artwork")
|
|
205
|
+
o.recipe = resolve(val());
|
|
206
|
+
else if (a === "--png" && cmd === "artwork")
|
|
207
|
+
o.png = true;
|
|
208
|
+
else if (a === "--svg" && cmd === "artwork")
|
|
209
|
+
o.svg = true;
|
|
210
|
+
else if (a === "--screen" && cmd === "artwork") {
|
|
211
|
+
const m = /^(\d+)x(\d+)$/.exec(val());
|
|
212
|
+
if (!m || +m[1] < 640 || +m[2] < 480 || +m[1] > 10000 || +m[2] > 10000)
|
|
213
|
+
usage();
|
|
214
|
+
o.screen = { w: +m[1], h: +m[2] };
|
|
215
|
+
}
|
|
164
216
|
else if (a === "--public-url") {
|
|
165
217
|
const u = val();
|
|
166
218
|
if (!/^https?:\/\/[^\s/?#]+\/?$/.test(u)) {
|
|
@@ -175,6 +227,12 @@ function parseArgs(argv) {
|
|
|
175
227
|
usage();
|
|
176
228
|
o.jobs = n;
|
|
177
229
|
}
|
|
230
|
+
else if (a === "--updates" && cmd === "serve") {
|
|
231
|
+
const mode = val();
|
|
232
|
+
if (!UPDATE_MODES.includes(mode))
|
|
233
|
+
usage();
|
|
234
|
+
o.updates = mode;
|
|
235
|
+
}
|
|
178
236
|
else if (a === "--agents") {
|
|
179
237
|
const level = val();
|
|
180
238
|
if (!AGENT_LEVELS.includes(level))
|
|
@@ -196,7 +254,7 @@ function parseArgs(argv) {
|
|
|
196
254
|
else
|
|
197
255
|
usage();
|
|
198
256
|
}
|
|
199
|
-
if ((!o.path && cmd !== "doctor") || (cmd === "export" && !o.out))
|
|
257
|
+
if ((!o.path && cmd !== "doctor") || (cmd === "export" && !o.out) || (cmd === "artwork" && !o.recipe))
|
|
200
258
|
usage();
|
|
201
259
|
// read-only for whoever comes, beside those the key or a link lets in: without a key, everyone is in
|
|
202
260
|
// already; behind a sign-in, there is nobody else
|
|
@@ -221,6 +279,11 @@ export class App {
|
|
|
221
279
|
live;
|
|
222
280
|
hover;
|
|
223
281
|
ids;
|
|
282
|
+
/** how long a read of the project's git (GET /api/git) is kept, whoever asks: git is run at most
|
|
283
|
+
* once in that time, so what readers cost the server is bounded (CollabSpec decision 11); 0 in
|
|
284
|
+
* the tests */
|
|
285
|
+
gitFreshMs = 3000;
|
|
286
|
+
gitRead = null;
|
|
224
287
|
/** the agents' API (server/agent.ts), and what agents may change */
|
|
225
288
|
agents;
|
|
226
289
|
/** who may come in at all (server/access.ts); null: anyone who reaches the port */
|
|
@@ -237,6 +300,9 @@ export class App {
|
|
|
237
300
|
readOnlyOf = new WeakMap();
|
|
238
301
|
/** what read-only readers may cost the server: Lean's answers, their documents */
|
|
239
302
|
bounds;
|
|
303
|
+
/** VisualProof's own releases as this server follows them (server/updates.ts); null: the
|
|
304
|
+
* version alone is told (a test's server) */
|
|
305
|
+
updates = null;
|
|
240
306
|
constructor(live, hover, ids, agents, access, exporter = null, publicUrl = null, publicRead = false) {
|
|
241
307
|
this.live = live;
|
|
242
308
|
this.hover = hover;
|
|
@@ -253,6 +319,11 @@ export class App {
|
|
|
253
319
|
this.publicRead = publicRead && access !== null;
|
|
254
320
|
this.bounds = new ReadOnlyBounds((id) => this.exporter?.job(id)?.state === "running");
|
|
255
321
|
}
|
|
322
|
+
/** Whether a request is told all of the server's own releases (UpdateSpec decision 11): someone
|
|
323
|
+
* let in fully; not a read-only reader, nor an agent. */
|
|
324
|
+
toldAll(req) {
|
|
325
|
+
return this.readOnlyOf.get(req) === undefined && this.ids.kind(req) !== "agent";
|
|
326
|
+
}
|
|
256
327
|
/** The exported document (ExportSpec decisions 5–16): POST /api/export starts typesetting the
|
|
257
328
|
* document of a view, `{view, settings, version?}` (a build the Time Machine shows the view at;
|
|
258
329
|
* left out, the view's own), and answers with its job at once; the page asks how the
|
|
@@ -825,6 +896,23 @@ export class App {
|
|
|
825
896
|
// the fonts an artwork is drawn in (ExportSpec decision 19)
|
|
826
897
|
if (FONT_PATH.test(path))
|
|
827
898
|
return sendFile(req, res, join(WEBFONTS, path.slice("/fonts/".length)));
|
|
899
|
+
// the tips' clips and their key frames (GuideSpec decision 6), and which there are
|
|
900
|
+
if (path === "/tips/index.json")
|
|
901
|
+
return sendJson(res, { clips: tipClips() });
|
|
902
|
+
// the tour film, fetched by this server on its first play and kept checked in the cache
|
|
903
|
+
// (server/film.ts, UpdateSpec decision 14): else 503 while it comes, or 404, and the page shows
|
|
904
|
+
// its stills and captions; and its captions as WebVTT, for assistive technology
|
|
905
|
+
if (path === "/tips/tour.mp4" && !refused.has(res))
|
|
906
|
+
return void serveFilm(req, res, sendMedia);
|
|
907
|
+
if (path === "/tips/tour.en.vtt") {
|
|
908
|
+
const vtt = tourVtt(tourData());
|
|
909
|
+
if (vtt === null)
|
|
910
|
+
return sendJson(res, { error: "no captions" }, 404);
|
|
911
|
+
res.writeHead(200, { "content-type": "text/vtt; charset=utf-8", "cache-control": "no-cache" });
|
|
912
|
+
return void res.end(vtt);
|
|
913
|
+
}
|
|
914
|
+
if (TIP_PATH.test(path) && !refused.has(res))
|
|
915
|
+
return sendMedia(req, res, join(TIPS_DIR, path.slice("/tips/".length)));
|
|
828
916
|
// the page, and its workers: the one that lays out the Time Machine's past graphs
|
|
829
917
|
// (web/src/layoutworker.ts), the one that chooses the overview's edges (web/src/edgeworker.ts),
|
|
830
918
|
// and pdf.js's, which reads the library's PDFs for the reference window (AttachmentSpec decision 11)
|
|
@@ -833,8 +921,19 @@ export class App {
|
|
|
833
921
|
if (path.startsWith("/pdfjs/"))
|
|
834
922
|
return pdfjsAsset(res, path);
|
|
835
923
|
// with Lean's language server as it is (server/hover.ts): what is open, what crashed
|
|
836
|
-
|
|
837
|
-
|
|
924
|
+
// a read-only reader is told how the server is, not its log nor where its review is kept
|
|
925
|
+
if (path === "/api/status") {
|
|
926
|
+
const st = { ...live.status(), lean: this.hover.state() };
|
|
927
|
+
if (this.readOnlyOf.get(req) !== undefined) {
|
|
928
|
+
delete st.log;
|
|
929
|
+
st.review = { ...st.review, folder: undefined };
|
|
930
|
+
}
|
|
931
|
+
return sendJson(res, st);
|
|
932
|
+
}
|
|
933
|
+
// VisualProof's own version, and what of its releases those let in fully are told (UpdateSpec
|
|
934
|
+
// decision 11): a read-only reader and an agent are told the version alone
|
|
935
|
+
if (path === "/api/updates")
|
|
936
|
+
return sendJson(res, this.updates?.status(this.toldAll(req)) ?? { running: packageVersion() });
|
|
838
937
|
// the link that lets someone in, for a page that shares where it is (web/src/share.ts); a
|
|
839
938
|
// request gets here only once it is in
|
|
840
939
|
// a read-only reader shares the link they came by, or the address alone (--public)
|
|
@@ -954,12 +1053,14 @@ export class App {
|
|
|
954
1053
|
}
|
|
955
1054
|
if (path === "/api/events") {
|
|
956
1055
|
const session = url.searchParams.get("session");
|
|
957
|
-
return live.subscribe(res, validSession(session) ? session : null, this.ids.person(req)?.id ?? null, reader ?? null);
|
|
1056
|
+
return live.subscribe(res, validSession(session) ? session : null, this.ids.person(req)?.id ?? null, reader ?? null, this.toldAll(req));
|
|
958
1057
|
}
|
|
959
1058
|
// what happened (CollabSpec decision 5): what the server stored; the review's own history the
|
|
960
1059
|
// page reads from the review
|
|
961
|
-
if (path === "/api/activity")
|
|
962
|
-
|
|
1060
|
+
if (path === "/api/activity") {
|
|
1061
|
+
const events = live.timeline.events.slice(-2000);
|
|
1062
|
+
return sendJson(res, { events: this.toldAll(req) ? events : events.map(withoutReasons) });
|
|
1063
|
+
}
|
|
963
1064
|
// a layout batch an agent made, undone (AgentSpec decision 4): from a page's Activity, by anyone;
|
|
964
1065
|
// by a private reader, without their name (CollabSpec decision 8)
|
|
965
1066
|
if (path === "/api/layout/undo" && req.method === "POST") {
|
|
@@ -976,14 +1077,23 @@ export class App {
|
|
|
976
1077
|
const a = this.agents.undo(me, agent, r.body?.id);
|
|
977
1078
|
return sendJson(res, a.body, a.status);
|
|
978
1079
|
}
|
|
1080
|
+
// the project's git source and commit as git says them now, not as the build was read (an
|
|
1081
|
+
// artwork's foot, ExportSpec decision 19; the owner's decision of 2026-10-15)
|
|
1082
|
+
if (path === "/api/git") {
|
|
1083
|
+
if (!this.gitRead || Date.now() - this.gitRead.at >= this.gitFreshMs)
|
|
1084
|
+
this.gitRead = { git: gitSource(live.project.root), at: Date.now() };
|
|
1085
|
+
return sendJson(res, { git: this.gitRead.git });
|
|
1086
|
+
}
|
|
979
1087
|
if (path === "/api/meta" || path === "/api/edges") {
|
|
980
1088
|
const g = live.graph;
|
|
981
1089
|
if (!g)
|
|
982
1090
|
return sendJson(res, { error: "not ready" }, 503);
|
|
983
1091
|
// a page asks for the version it was told about; an older one is gone
|
|
984
1092
|
const want = url.searchParams.get("v");
|
|
985
|
-
|
|
986
|
-
|
|
1093
|
+
// of this server's: one restarted since counts its versions again (UpdateSpec decision 8)
|
|
1094
|
+
const of = url.searchParams.get("i");
|
|
1095
|
+
if ((want && Number(want) !== g.version) || (of && of !== live.instance))
|
|
1096
|
+
return sendJson(res, { error: "superseded", version: g.version, instance: live.instance }, 409);
|
|
987
1097
|
// a page asks for the edges packed (shared/edgecodec.ts; `pack=2` with the grammar site, a
|
|
988
1098
|
// page from before it `pack=1`, which takes grammar for a proof); an agent gets them as they
|
|
989
1099
|
// are
|
|
@@ -1287,15 +1397,30 @@ function packedGz(edgesGz, format) {
|
|
|
1287
1397
|
}
|
|
1288
1398
|
return p;
|
|
1289
1399
|
}
|
|
1400
|
+
/** What a stop signal does to a server: end it at once, until it can stop gracefully (serve). */
|
|
1401
|
+
let stopNow = () => process.exit(0);
|
|
1290
1402
|
async function main() {
|
|
1291
1403
|
const o = parseArgs(process.argv.slice(2));
|
|
1404
|
+
// an installed package's server runs under the supervisor, which stays while it restarts into
|
|
1405
|
+
// another version (UpdateSpec decision 5); `--updates off`, and a checkout, run it as they are
|
|
1406
|
+
if (o.cmd === "serve" && FROM_PACKAGE && o.updates !== "off" && process.env.VISUALPROOF_SUPERVISED !== "1") {
|
|
1407
|
+
const site = projectDirs(resolve(o.path), o.site, o.cache).site;
|
|
1408
|
+
process.exit(await supervise({ installedRoot: PACKAGE_ROOT, cacheRoot: o.cache ? resolve(o.cache) : cacheHome(), argv: process.argv.slice(2), site, mode: o.updates }));
|
|
1409
|
+
}
|
|
1410
|
+
// a stop asked before the server can stop gracefully (it reads the project, takes the site) ends
|
|
1411
|
+
// it at once, with status 0; once it can, the graceful stop does (`stopNow`, below)
|
|
1412
|
+
if (o.cmd === "serve")
|
|
1413
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
1414
|
+
process.on(sig, () => stopNow());
|
|
1292
1415
|
if (o.cmd === "doctor") {
|
|
1293
1416
|
const report = doctor({ path: o.path || null, roots: o.roots, site: o.site, cache: o.cache });
|
|
1294
1417
|
console.log(printReport(report, o.path || null));
|
|
1295
1418
|
process.exit(report.checks.some((c) => c.level === "fail") ? 1 : 0);
|
|
1296
1419
|
}
|
|
1297
|
-
// a live server outlives any one failed task: log it and keep serving
|
|
1298
|
-
|
|
1420
|
+
// a live server outlives any one failed task: log it and keep serving (a picture is no server:
|
|
1421
|
+
// whatever fails ends it, with status 1)
|
|
1422
|
+
if (o.cmd !== "artwork")
|
|
1423
|
+
process.on("unhandledRejection", (e) => console.error("unhandled:", e instanceof Error ? e.message : e));
|
|
1299
1424
|
let project;
|
|
1300
1425
|
try {
|
|
1301
1426
|
project = openProject(o.path, o.roots, o.graph === null);
|
|
@@ -1303,10 +1428,13 @@ async function main() {
|
|
|
1303
1428
|
catch (e) {
|
|
1304
1429
|
if (e instanceof ProjectError) {
|
|
1305
1430
|
console.error(`visualproof: ${e.message}`);
|
|
1306
|
-
|
|
1431
|
+
return endNotTheCode(1);
|
|
1307
1432
|
}
|
|
1308
1433
|
throw e;
|
|
1309
1434
|
}
|
|
1435
|
+
// a picture: the site read, never made, locked or written (ExportSpec decision 21)
|
|
1436
|
+
if (o.cmd === "artwork")
|
|
1437
|
+
return artworkCommand(project, o);
|
|
1310
1438
|
const dirs = projectDirs(project.root, o.site, o.cache);
|
|
1311
1439
|
openSite(dirs.site);
|
|
1312
1440
|
mkdirSync(dirs.cache, { recursive: true });
|
|
@@ -1320,7 +1448,7 @@ async function main() {
|
|
|
1320
1448
|
if (!(e instanceof SiteBusy))
|
|
1321
1449
|
throw e;
|
|
1322
1450
|
console.error(`visualproof: ${e.message}`);
|
|
1323
|
-
|
|
1451
|
+
return endNotTheCode(1);
|
|
1324
1452
|
}
|
|
1325
1453
|
}
|
|
1326
1454
|
useBundleDir(bundleDirIn(o.cache ? resolve(o.cache) : cacheHome()));
|
|
@@ -1373,7 +1501,7 @@ async function main() {
|
|
|
1373
1501
|
}
|
|
1374
1502
|
catch (e) {
|
|
1375
1503
|
console.error(`visualproof: --access-key-file: ${e instanceof Error ? e.message : String(e)}`);
|
|
1376
|
-
|
|
1504
|
+
return endNotTheCode(2);
|
|
1377
1505
|
}
|
|
1378
1506
|
process.on("exit", () => {
|
|
1379
1507
|
hover.stop();
|
|
@@ -1384,10 +1512,77 @@ async function main() {
|
|
|
1384
1512
|
live.invites.close();
|
|
1385
1513
|
live.readOnly.close();
|
|
1386
1514
|
});
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
const app = new App(live, hover, ids, new AgentApi(live, hover, o.agents), key ? new Access(key.key) : null, new Exporter(dirs.cache), o.publicUrl, o.publicRead);
|
|
1515
|
+
const exporter = new Exporter(dirs.cache);
|
|
1516
|
+
const app = new App(live, hover, ids, new AgentApi(live, hover, o.agents), key ? new Access(key.key) : null, exporter, o.publicUrl, o.publicRead);
|
|
1390
1517
|
const server = createServer((req, res) => app.handle(req, res));
|
|
1518
|
+
const version = packageVersion();
|
|
1519
|
+
// run by the supervisor, which starts it again in another version (UpdateSpec decision 5)
|
|
1520
|
+
const supervised = typeof process.send === "function" && process.env.VISUALPROOF_SUPERVISED === "1";
|
|
1521
|
+
/** Stop (UpdateSpec decision 9): no new requests, the pages told why and their streams ended, the
|
|
1522
|
+
* programs the server runs stopped, then the process ends: status 0, or RESTART_STATUS for the
|
|
1523
|
+
* supervisor to start it again in another version. A second signal meanwhile changes nothing;
|
|
1524
|
+
* whatever still waits, it ends within 5 s. */
|
|
1525
|
+
let stopping = false;
|
|
1526
|
+
/** the status the process ends with: a stop asked during a restart makes it a stop (0), so that
|
|
1527
|
+
* the supervisor does not start it again */
|
|
1528
|
+
let exitStatus = 0;
|
|
1529
|
+
const stop = (status, why) => {
|
|
1530
|
+
if (stopping) {
|
|
1531
|
+
if ("stopped" in why)
|
|
1532
|
+
exitStatus = 0;
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
stopping = true;
|
|
1536
|
+
exitStatus = status;
|
|
1537
|
+
updates.stop();
|
|
1538
|
+
server.close();
|
|
1539
|
+
live.end(why);
|
|
1540
|
+
hover.stop();
|
|
1541
|
+
live.usage.cancel();
|
|
1542
|
+
server.closeIdleConnections();
|
|
1543
|
+
setTimeout(() => process.exit(exitStatus), 5000);
|
|
1544
|
+
void stopChildren(2000).then(() => {
|
|
1545
|
+
server.closeAllConnections();
|
|
1546
|
+
process.exit(exitStatus);
|
|
1547
|
+
});
|
|
1548
|
+
};
|
|
1549
|
+
const cacheRoot = o.cache ? resolve(o.cache) : cacheHome();
|
|
1550
|
+
const updates = new Updates({
|
|
1551
|
+
mode: o.updates,
|
|
1552
|
+
cacheRoot,
|
|
1553
|
+
canRestart: supervised && FROM_PACKAGE,
|
|
1554
|
+
check: FROM_PACKAGE,
|
|
1555
|
+
checkout: !FROM_PACKAGE,
|
|
1556
|
+
// nobody on a page, nothing running that a restart would cut short (UpdateSpec decision 6)
|
|
1557
|
+
quiet: () => live.pages === 0 && !live.busy && !exporter.busy,
|
|
1558
|
+
restart: (to) => {
|
|
1559
|
+
process.send?.({ vp: "restart", to });
|
|
1560
|
+
stop(RESTART_STATUS, { update: { from: version, to } });
|
|
1561
|
+
},
|
|
1562
|
+
// the server's console alone: the log pages read (GET /api/status) is no place for paths and
|
|
1563
|
+
// commands of this machine
|
|
1564
|
+
log: (l) => console.log(l),
|
|
1565
|
+
changed: () => live.updatesChanged(),
|
|
1566
|
+
running: version,
|
|
1567
|
+
// the supervisor's word, believed only from it
|
|
1568
|
+
installedRoot: (supervised && process.env.VISUALPROOF_INSTALLED) || PACKAGE_ROOT,
|
|
1569
|
+
site: dirs.site,
|
|
1570
|
+
// what the supervisor set aside without a record of it (a full disk), believed only from it
|
|
1571
|
+
also: new Set(supervised ? unrecordedOf(process.env.VISUALPROOF_UNRECORDED) : []),
|
|
1572
|
+
film: () => filmStatus(),
|
|
1573
|
+
});
|
|
1574
|
+
// the tips' tour film, fetched on its first play (UpdateSpec decision 14; never under --updates off)
|
|
1575
|
+
useFilm({ cacheRoot, mode: o.updates, log: (l) => console.log(l) });
|
|
1576
|
+
live.updateStatus = (full) => updates.status(full);
|
|
1577
|
+
app.updates = updates;
|
|
1578
|
+
stopNow = () => stop(0, { stopped: true });
|
|
1579
|
+
// the supervisor went: this server goes with it rather than serve on alone
|
|
1580
|
+
if (supervised)
|
|
1581
|
+
process.on("disconnect", () => stop(0, { stopped: true }));
|
|
1582
|
+
server.on("error", (e) => {
|
|
1583
|
+
console.error(`visualproof: ${e.code === "EADDRINUSE" ? `${o.host}:${o.port} is in use already: give --port another` : e.message}`);
|
|
1584
|
+
return endNotTheCode(1);
|
|
1585
|
+
});
|
|
1391
1586
|
server.listen(o.port, o.host, () => {
|
|
1392
1587
|
console.log(`VisualProof — ${project.name} (${targetsInWords(project)})`);
|
|
1393
1588
|
console.log(` http://${o.host}:${o.port}/`);
|
|
@@ -1398,9 +1593,143 @@ async function main() {
|
|
|
1398
1593
|
console.log(` agents: ${o.agents} — their API is http://${o.host}:${o.port}/api/agent`);
|
|
1399
1594
|
console.log(` usage pass: ${o.graph ? "none for a saved graph" : o.jobs > 0 ? `${o.jobs} module(s) at a time` : "off (--jobs 0)"}`);
|
|
1400
1595
|
console.log(` access: ${key ? `by its link only, /k/<the key in ${o.accessKeyFile}>${key.made ? " (a new key was made there)" : ""}${o.publicRead ? "; anyone else read-only (--public)" : ""}` : "anyone who reaches the port"}`);
|
|
1596
|
+
console.log(` version: ${version}; updates ${updatesInWords(o.updates, version, supervised)}`);
|
|
1597
|
+
recordVersion(live, version, supervised);
|
|
1598
|
+
process.send?.({ vp: "listening", version });
|
|
1599
|
+
updates.start();
|
|
1401
1600
|
});
|
|
1402
1601
|
live.start();
|
|
1403
1602
|
}
|
|
1603
|
+
/** What the server does with its releases, for its first lines (UpdateSpec decisions 2–4, 13). */
|
|
1604
|
+
function updatesInWords(mode, version, supervised) {
|
|
1605
|
+
if (mode === "off")
|
|
1606
|
+
return "off (--updates off): the registry is never asked";
|
|
1607
|
+
if (!FROM_PACKAGE)
|
|
1608
|
+
return "none from a checkout";
|
|
1609
|
+
const minor = version.split(".").slice(0, 2).join(".");
|
|
1610
|
+
if (mode === "notify")
|
|
1611
|
+
return `notify — newer versions told of, never downloaded${supervised ? "; one installed meanwhile is restarted into" : ""}`;
|
|
1612
|
+
return supervised ? `auto — a patch of ${minor} taken by itself at a quiet moment; a newer version told of` : "auto, but without the supervisor: newer versions told of";
|
|
1613
|
+
}
|
|
1614
|
+
/** The versions the supervisor says it set aside without a record (VISUALPROOF_UNRECORDED). */
|
|
1615
|
+
function unrecordedOf(text) {
|
|
1616
|
+
try {
|
|
1617
|
+
const v = JSON.parse(text ?? "[]");
|
|
1618
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
1619
|
+
}
|
|
1620
|
+
catch {
|
|
1621
|
+
return [];
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
/** The version that serves the site, recorded in the timeline where it is another than the last one
|
|
1625
|
+
* recorded (UpdateSpec decision 10), with what the supervisor set aside before this one started. */
|
|
1626
|
+
function recordVersion(live, version, supervised) {
|
|
1627
|
+
let setAside;
|
|
1628
|
+
try {
|
|
1629
|
+
const raw = JSON.parse((supervised && process.env.VISUALPROOF_SET_ASIDE) || "null");
|
|
1630
|
+
if (raw && typeof raw.version === "string" && typeof raw.why === "string")
|
|
1631
|
+
setAside = { version: raw.version, why: raw.why };
|
|
1632
|
+
}
|
|
1633
|
+
catch {
|
|
1634
|
+
// nothing set aside that can be read
|
|
1635
|
+
}
|
|
1636
|
+
const was = live.timeline.lastVersion() ?? live.history?.lastVersion() ?? null;
|
|
1637
|
+
if (was !== version || setAside)
|
|
1638
|
+
live.timeline.add({ kind: "tool", from: was, to: version, ...(setAside ? { setAside } : {}) });
|
|
1639
|
+
}
|
|
1640
|
+
/** End the server with `status`, the supervisor told first that what ends it is not its code (the
|
|
1641
|
+
* site is busy, the port is taken, the project cannot be read): a patch that ends so is not set
|
|
1642
|
+
* aside (UpdateSpec decision 7). The message is sent before the process ends, not lost with it. */
|
|
1643
|
+
function endNotTheCode(status) {
|
|
1644
|
+
process.exitCode = status;
|
|
1645
|
+
if (process.send)
|
|
1646
|
+
process.send({ vp: "environment" }, () => process.exit(status));
|
|
1647
|
+
else
|
|
1648
|
+
process.exit(status);
|
|
1649
|
+
}
|
|
1650
|
+
/** The artwork command (ExportSpec decision 21): the build read as an export reads it, the picture a
|
|
1651
|
+
* recipe says made of it (server/artcmd.ts), its files written; what it named said as the agents'
|
|
1652
|
+
* API says it; problems said, and exit status 1. */
|
|
1653
|
+
async function artworkCommand(project, o) {
|
|
1654
|
+
const fail = (why) => {
|
|
1655
|
+
console.error(`visualproof artwork: ${why}`);
|
|
1656
|
+
process.exit(1);
|
|
1657
|
+
};
|
|
1658
|
+
try {
|
|
1659
|
+
await artwork(project, o, fail);
|
|
1660
|
+
}
|
|
1661
|
+
catch (e) {
|
|
1662
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
async function artwork(project, o, fail) {
|
|
1666
|
+
const dirs = projectDirs(project.root, o.site, o.cache);
|
|
1667
|
+
const site = o.site ? dirs.site : null;
|
|
1668
|
+
if (site && !existsSync(site))
|
|
1669
|
+
fail(`there is no site at ${site}`);
|
|
1670
|
+
let recipe;
|
|
1671
|
+
try {
|
|
1672
|
+
recipe = JSON.parse(readFileSync(o.recipe, "utf8"));
|
|
1673
|
+
}
|
|
1674
|
+
catch (e) {
|
|
1675
|
+
fail(`the recipe ${o.recipe} cannot be read: ${e instanceof Error ? e.message : String(e)}`);
|
|
1676
|
+
}
|
|
1677
|
+
// -o: a name, or a path without its extension, in a folder that is there
|
|
1678
|
+
const outBase = o.out ? o.out.replace(/\.(png|svg)$/i, "") : null;
|
|
1679
|
+
const dir = outBase ? dirname(outBase) : process.cwd();
|
|
1680
|
+
if (!existsSync(dir))
|
|
1681
|
+
fail(`there is no folder ${dir} to write the files in`);
|
|
1682
|
+
mkdirSync(dirs.cache, { recursive: true });
|
|
1683
|
+
const log = (l) => console.log(l);
|
|
1684
|
+
let ex = o.graph ? undefined : await extract(project, dirs, log);
|
|
1685
|
+
if (ex && o.jobs > 0) {
|
|
1686
|
+
const pass = new UsagePass(project, dirs, o.jobs, log);
|
|
1687
|
+
const plan = pass.plan(ex.graph);
|
|
1688
|
+
if (plan.todo.length)
|
|
1689
|
+
log(`usage pass: elaborating ${plan.todo.length} of ${plan.modules.length} modules, ${o.jobs} at a time`);
|
|
1690
|
+
const results = await pass.run(plan);
|
|
1691
|
+
if (results) {
|
|
1692
|
+
ex = mergeUsage(ex, plan, results, pass.status.failed, pass.status.ms ?? 0);
|
|
1693
|
+
saveMerged(dirs.cache, ex);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
// the site's layout memory read and never written, nor the views laid out written to the cache;
|
|
1697
|
+
// without a site, none read: the board as a new site lays it out
|
|
1698
|
+
const prep = await prepare(project, dirs, o.graph, log, { ...(ex ? { extraction: ex } : {}), readOnlySite: true, ...(site ? {} : { freshLayout: true }) });
|
|
1699
|
+
const kept = site ? readProjectView(site, LAYOUT_VERSION) : { view: null };
|
|
1700
|
+
if (kept.why)
|
|
1701
|
+
console.error(`visualproof artwork: the site's views cannot be read (${kept.why}): the Project view as a new site has it`);
|
|
1702
|
+
const review = site ? new ReviewStore(project.root, site).data : null;
|
|
1703
|
+
const a = makeArtwork(prep, review, projectView(kept.view), { recipe, screen: o.screen, name: outBase ? basename(outBase) : null, root: project.root });
|
|
1704
|
+
if (!("svg" in a)) {
|
|
1705
|
+
for (const p of a.problems)
|
|
1706
|
+
console.error(`visualproof artwork: ${p}`);
|
|
1707
|
+
process.exit(1);
|
|
1708
|
+
}
|
|
1709
|
+
const list = (xs) => (xs.length ? xs.join(", ") : "none");
|
|
1710
|
+
console.log(`names written: ${list(a.names.written)}`);
|
|
1711
|
+
if (a.names.ruledSkipped.length)
|
|
1712
|
+
console.log(`names a rule chose, left out where they would cover another: ${list(a.names.ruledSkipped)}`);
|
|
1713
|
+
if (a.names.left)
|
|
1714
|
+
console.log(`names or targets left out for room: ${a.names.left}`);
|
|
1715
|
+
console.log(`targets marked: ${list(a.targets)}`);
|
|
1716
|
+
console.log(`selection: ${a.selection ? `${a.selection.mode} of ${a.selection.name}` : "none"}`);
|
|
1717
|
+
if (a.notesLeftOut)
|
|
1718
|
+
console.log(`notes: ${a.notesLeftOut} on the board, not drawn (the command draws no notes yet)`);
|
|
1719
|
+
const both = !o.png && !o.svg;
|
|
1720
|
+
const mb = (n) => `${(n / 1e6).toFixed(1)} MB`;
|
|
1721
|
+
if (both || o.svg) {
|
|
1722
|
+
const file = join(dir, a.file("svg"));
|
|
1723
|
+
writeFileSync(file, a.svg);
|
|
1724
|
+
console.log(`wrote ${file} (${mb(Buffer.byteLength(a.svg))})`);
|
|
1725
|
+
}
|
|
1726
|
+
if (both || o.png) {
|
|
1727
|
+
const png = await pngOf(a.svg);
|
|
1728
|
+
const file = join(dir, a.file("png"));
|
|
1729
|
+
writeFileSync(file, png);
|
|
1730
|
+
console.log(`wrote ${file} (${mb(png.length)}, ${a.look.w} × ${a.look.h})`);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1404
1733
|
// run as the command (`node src/server/main.ts`, the `visualproof` bin), not when a test imports App
|
|
1405
1734
|
if (import.meta.main)
|
|
1406
1735
|
void main();
|
package/dist/server/mcptools.js
CHANGED
|
@@ -373,12 +373,13 @@ export const TOOLS = [
|
|
|
373
373
|
},
|
|
374
374
|
{
|
|
375
375
|
name: "export",
|
|
376
|
-
about: "A view's document — every declaration of the project its board holds, in its sections and order: its model as JSON, or typeset as a PDF and saved on your machine.",
|
|
376
|
+
about: "A view's document — every declaration of the project its board holds, in its sections and order: its model as JSON, or typeset as a PDF and saved on your machine. Or a picture of the board: a link that opens it in the artwork mode as a recipe says.",
|
|
377
377
|
actions: {
|
|
378
378
|
model: { method: "GET", path: "/api/agent/export", says: "the document's model", args: ["view", "settings", "version"] },
|
|
379
379
|
typeset: { method: "POST", path: "/api/agent/export", says: "start typesetting: a job", args: ["view", "settings", "version"] },
|
|
380
380
|
job: { method: "GET", path: "/api/agent/export/job", says: "how the job is doing", args: ["id"] },
|
|
381
381
|
file: { method: "GET", path: "/api/agent/export/file", saves: true, says: "a file of it saved on your machine", args: ["id", "what", "saveTo"] },
|
|
382
|
+
artwork: { method: "POST", path: "/api/agent/artwork", says: "a link to a picture of the board, as a recipe says", args: ["recipe", "save"] },
|
|
382
383
|
},
|
|
383
384
|
args: {
|
|
384
385
|
view: str("the view's id: a target's Key view is open on key(<target>)"),
|
|
@@ -387,6 +388,8 @@ export const TOOLS = [
|
|
|
387
388
|
id: str("the job's id"),
|
|
388
389
|
what: str("pdf (default), tex, sty or zip", ["pdf", "tex", "sty", "zip"]),
|
|
389
390
|
saveTo: SAVE_TO,
|
|
391
|
+
recipe: { type: "object", description: "the picture: size, orientation, style, theme, names (e.g. {clusters: 1}), targets, selection {name, mode}, title, subtitle, edges… (docs: export)" },
|
|
392
|
+
save: str("png, svg or png,svg: saved by the page once ready"),
|
|
390
393
|
},
|
|
391
394
|
},
|
|
392
395
|
];
|