wiki-formant 0.107.0 → 0.111.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 +20 -4
- package/bin/check-pages.mjs +11 -4
- package/bin/indexnow.mjs +4 -3
- package/dist/base.css +5 -1
- package/dist/chart.js +15 -14
- package/dist/conformance.d.ts +3 -8
- package/dist/conformance.js +6 -18
- package/dist/editor.js +14 -11
- package/dist/http.d.ts +3 -2
- package/dist/http.js +5 -2
- package/dist/indexnow.d.ts +12 -2
- package/dist/indexnow.js +16 -6
- package/dist/knip.js +1 -3
- package/dist/link-check.js +4 -1
- package/dist/openapi.d.ts +139 -0
- package/dist/openapi.js +75 -0
- package/dist/react.d.ts +12 -13
- package/dist/react.js +27 -22
- package/dist/stats-figures.d.ts +7 -2
- package/dist/stats-figures.js +20 -13
- package/dist/stats.js +1 -1
- package/dist/well-known.d.ts +2 -8
- package/dist/well-known.js +1 -8
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -196,7 +196,7 @@ const { first } = await wikiReadChecks(t, { query: 'radix', address: r => ({ tag
|
|
|
196
196
|
process.exit(t.summary());
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
`standardSuite(t, cfg)` runs the suites every surface runs, in order: `transportChecks` (CORS preflight, `GET`→405, 202 for a notification, `-32700`, the batch cap, honest capabilities, version negotiation), `versionCoherence` over `standardVersionSources`, `
|
|
199
|
+
`standardSuite(t, cfg)` runs the suites every surface runs, in order: `transportChecks` (CORS preflight, `GET`→405, 202 for a notification, `-32700`, the batch cap, honest capabilities, version negotiation), `versionCoherence` over `standardVersionSources`, `agentCardChecks`, `annotationChecks`, `manifestParity`, `payloadBudget`, `conditionalGetChecks`, `descriptorChecks` (default `S10_DESCRIPTORS`) and `robotsChecks`. `budget`, `conditionalGet` and `robots` run only when given, for a surface whose fixtures exist only later in its run. It returns the `initialize` response. Each suite is exported for a surface that needs it alone.
|
|
200
200
|
|
|
201
201
|
`wikiReadChecks(t, opts)` checks the `wiki-tools` read tools: categories, a search and the page it finds (by `address`, default `{ path }`), its twin (given `twin`), a listing, recent changes, a corpus preflight and a slice that is cut and resumes when the corpus exceeds `corpusChars`, and the teaching answers to no hits, a missing argument, an unknown tool and a missing page. `endpoint` and `label` run it against a second server. It returns `{ rows, first, page }`.
|
|
202
202
|
|
|
@@ -260,7 +260,7 @@ export const POST = (req: Request) => handleRoute(async () => {
|
|
|
260
260
|
- `corpusEtag`, `notModified` and `textHeaders` are the parts; `corpusRoute` skips the build on a 304.
|
|
261
261
|
- A URL answering JSON or markdown by `Accept` asks `wantsMarkdown(request)` and sends `VARY_ACCEPT` on both branches.
|
|
262
262
|
- `descriptorResponse` serves a JSON descriptor with its own ETag and `Access-Control-Allow-Origin: *` (`descriptorHandler` in `wiki-formant/well-known` is the route).
|
|
263
|
-
- Every error body is `{ error, ...extra }`. A thrown `HttpError` answers with its status; anything else is logged and answered with a 500 carrying only the handler's message (`'Search failed'`), so no exception text reaches a caller. `errors.tooManyRequests(sec)` states `Retry-After`.
|
|
263
|
+
- Every error body is `{ error, ...extra }`. A thrown `HttpError` answers with its status, a thrown `Response` (`errors.tooManyRequests(sec)`, say) is the answer as built; anything else is logged and answered with a 500 carrying only the handler's message (`'Search failed'`), so no exception text reaches a caller. `errors.tooManyRequests(sec)` states `Retry-After`.
|
|
264
264
|
|
|
265
265
|
## Response headers and the Next config
|
|
266
266
|
|
|
@@ -511,7 +511,7 @@ const block = licenseBlock({ license, scope: 'The protocol compilation and prose
|
|
|
511
511
|
export const X = xAccount({ siteName: 'AcuiQ', handle: '@acu_iq', posts: "a named trial's prescription with its citation" });
|
|
512
512
|
```
|
|
513
513
|
|
|
514
|
-
- One `License` goes to `licenseBlock`, `agentCard({ license, licenseScope })` and `
|
|
514
|
+
- One `License` goes to `licenseBlock`, `agentCard({ license, licenseScope })` and `openApiDocument`, which sets `info.license` from it.
|
|
515
515
|
- `X.url` is the `sameAs`, `X.handle` the card's, `followLines(X)` goes into llms.txt and AGENTS.md, `followNote(X)` into the MCP instructions, and `<a {...followLinkProps(X)}>` is the footer link with `rel="me"`.
|
|
516
516
|
|
|
517
517
|
## Agent descriptors
|
|
@@ -526,7 +526,23 @@ const AGENT_CARD = agentCard({ name, description, url: `${SITE_URL}/api/mcp`, ve
|
|
|
526
526
|
export const GET = wellKnownHandler({ agentCard: AGENT_CARD, mcpManifest: mcpManifest({ … }), openapi: SPEC });
|
|
527
527
|
```
|
|
528
528
|
|
|
529
|
-
The card is served
|
|
529
|
+
The card is served at `agent-card.json`, and its `url` is the MCP endpoint, which answers A2A. `registryAuthHandler` serves `MCP_REGISTRY_PUBLIC_KEY` at `/.well-known/mcp-registry-auth`, 404 when unset.
|
|
530
|
+
|
|
531
|
+
## OpenAPI
|
|
532
|
+
|
|
533
|
+
`wiki-formant/openapi`: the frame of the spec `wellKnownHandler` serves, and the paths every site shares. A spec writes only its own paths.
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
import { openApiDocument, jsonBody, jsonResponse, toolBody, llmsTxtPath, mcpPath } from 'wiki-formant/openapi';
|
|
537
|
+
|
|
538
|
+
export const SPEC = openApiDocument({ title, version, description, license, siteUrl: SITE_URL }, {
|
|
539
|
+
'/api/wiki/{path}': { put: { requestBody: toolBody(toolInput('edit_page'), { inPath: ['tagPath', 'slug'], refs: { Block: BLOCK_SCHEMA } }), … } },
|
|
540
|
+
'/llms.txt': llmsTxtPath('Index of every page (plain text)'),
|
|
541
|
+
'/api/mcp': mcpPath(TOOLS.map(t => t.name)),
|
|
542
|
+
}, { schemas: { Block: BLOCK_SCHEMA } });
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`toolBody` is a write tool's input schema as the REST body, so `openapiWriteParity` holds by construction. `mcpPath` states the protocol versions, the batch cap and `MCP_RATE_LIMIT_TEXT`; `llmsTxtPath` the `If-None-Match` and its 304.
|
|
530
546
|
|
|
531
547
|
## AI crawlers
|
|
532
548
|
|
package/bin/check-pages.mjs
CHANGED
|
@@ -58,12 +58,12 @@ const get = async url => {
|
|
|
58
58
|
|
|
59
59
|
// The sitemap is the URL list.
|
|
60
60
|
async function sitemapUrls(origin) {
|
|
61
|
-
const
|
|
61
|
+
const { entries, unread } = await readSitemap(`${origin}/sitemap.xml`, url => fetch(url, { headers: { 'user-agent': UA } }));
|
|
62
62
|
// A sitemap states absolute production URLs by definition, so pointing
|
|
63
63
|
// --site at a dev server would otherwise crawl production and report the
|
|
64
64
|
// build you were trying to replace. The path is what the sitemap contributes;
|
|
65
65
|
// the origin is what you asked for.
|
|
66
|
-
|
|
66
|
+
const urls = entries.map(({ url: u }) => {
|
|
67
67
|
try {
|
|
68
68
|
const { pathname, search } = new URL(u);
|
|
69
69
|
return `${origin}${pathname}${search}`;
|
|
@@ -71,6 +71,7 @@ async function sitemapUrls(origin) {
|
|
|
71
71
|
return u;
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
+
return { urls, unread };
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
const titleOf = html => {
|
|
@@ -84,17 +85,23 @@ const descriptionOf = html => {
|
|
|
84
85
|
|
|
85
86
|
// A sitemap that does not answer is a fault, not an empty site: read as no
|
|
86
87
|
// URLs, a down sitemap passed the gate it exists to hold.
|
|
87
|
-
const urls = await sitemapUrls(site).catch(e => {
|
|
88
|
+
const { urls, unread } = await sitemapUrls(site).catch(e => {
|
|
88
89
|
console.error(`check-pages: ${site}/sitemap.xml could not be read: ${e.message}`);
|
|
89
90
|
process.exit(WARN_ONLY ? 0 : 1);
|
|
90
91
|
});
|
|
92
|
+
// A child sitemap that does not answer hides its pages from every check below,
|
|
93
|
+
// so it is a fault of its own (`sitemapUnread`), never a site with fewer pages.
|
|
94
|
+
if (!urls.length && unread.length) {
|
|
95
|
+
for (const { url, error } of unread) console.error(`check-pages: ${url} could not be read: ${error}`);
|
|
96
|
+
process.exit(WARN_ONLY ? 0 : 1);
|
|
97
|
+
}
|
|
91
98
|
if (!urls.length) {
|
|
92
99
|
console.log(`check-pages: no URLs in ${site}/sitemap.xml — nothing to check. Skipping.`);
|
|
93
100
|
process.exit(0);
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
const checked = urls.slice(0, limit);
|
|
97
|
-
const faults =
|
|
104
|
+
const faults = unread.map(({ url, error }) => ({ url, fault: 'sitemapUnread', detail: error }));
|
|
98
105
|
const titles = new Map();
|
|
99
106
|
|
|
100
107
|
for (const url of checked) {
|
package/bin/indexnow.mjs
CHANGED
|
@@ -40,11 +40,12 @@ if (live.trim() !== key) {
|
|
|
40
40
|
process.exit(1);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const entries = await readSitemap(`${origin}/sitemap.xml`).catch(e => {
|
|
43
|
+
const { entries, unread } = await readSitemap(`${origin}/sitemap.xml`).catch(e => {
|
|
44
44
|
console.error(e.message);
|
|
45
45
|
process.exit(1);
|
|
46
46
|
});
|
|
47
47
|
const urls = args.includes('--all') ? entries.map(e => e.url) : changedUrls(entries, Date.now() - hours * 3_600_000);
|
|
48
48
|
const statuses = await submitIndexNow(key, urls);
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// What was read is submitted; a child sitemap that did not answer fails the run, since its pages were not.
|
|
50
|
+
console.log(JSON.stringify({ origin, submitted: urls.length, statuses, ...(unread.length && { unread }) }));
|
|
51
|
+
if (unread.length || !statuses.every(s => s === 200 || s === 202)) process.exit(1);
|
package/dist/base.css
CHANGED
|
@@ -263,6 +263,10 @@ tbody > tr:nth-child(even) > :is(th, td):not(.chart *) { background-image: linea
|
|
|
263
263
|
.infobox-series { align-items: center; gap: 0.125rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--ui-border); text-align: center; font-size: var(--ui-small); color: var(--ui-muted); }
|
|
264
264
|
.page-with-infobox { gap: 1.5rem; }
|
|
265
265
|
|
|
266
|
+
/* `Cite` in a meta line: the summary reads as a link, the citation as a line of its own. */
|
|
267
|
+
.article-cite summary { cursor: pointer; color: var(--ui-accent); }
|
|
268
|
+
.article-cite code { display: block; margin-top: 0.25rem; white-space: normal; }
|
|
269
|
+
|
|
266
270
|
.section-card { display: flex; flex-direction: column; gap: 0.5rem; padding: var(--ui-pad); transition: border-color 150ms; }
|
|
267
271
|
.section-card:hover { border-color: var(--ui-accent); }
|
|
268
272
|
|
|
@@ -698,7 +702,7 @@ pre:hover > .code-copy-btn,
|
|
|
698
702
|
position: absolute;
|
|
699
703
|
inset: 0 auto 0 0;
|
|
700
704
|
width: calc(var(--share) * 100%);
|
|
701
|
-
background:
|
|
705
|
+
background: var(--ui-accent);
|
|
702
706
|
opacity: 0.08;
|
|
703
707
|
pointer-events: none;
|
|
704
708
|
}
|
package/dist/chart.js
CHANGED
|
@@ -167,12 +167,13 @@ const MARGIN_TOP = 0.15;
|
|
|
167
167
|
*/
|
|
168
168
|
export function TimeChart({ series, label, ranges = typeof series === 'function' ? CHART_RANGES : DAILY_RANGES, range: initial = ranges.includes('30d') ? '30d' : ranges[0], aggregate = 'last', fromZero = false, format = formatChartValue, candles: initialCandles = false, height, controls, markers, step: initialStep, onView, className, }) {
|
|
169
169
|
const box = useRef(null);
|
|
170
|
-
const chart = useRef(null);
|
|
171
170
|
const formatRef = useRef(format);
|
|
172
171
|
formatRef.current = format;
|
|
173
172
|
const onViewRef = useRef(onView);
|
|
174
173
|
onViewRef.current = onView;
|
|
175
|
-
|
|
174
|
+
// The drawn chart once the library has loaded, else null: the handle is the
|
|
175
|
+
// readiness, so no flag can say ready over a chart that is gone.
|
|
176
|
+
const [chart, setChart] = useState(null);
|
|
176
177
|
const [range, setRange] = useState(initial);
|
|
177
178
|
const [step, setStep] = useState(initialStep ?? null);
|
|
178
179
|
const [candles, setCandles] = useState(initialCandles);
|
|
@@ -186,6 +187,7 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
186
187
|
if (!el)
|
|
187
188
|
return;
|
|
188
189
|
let disposed = false;
|
|
190
|
+
let built = null;
|
|
189
191
|
import('lightweight-charts').then(({ createChart, createSeriesMarkers, AreaSeries, CandlestickSeries, HistogramSeries, ColorType, CrosshairMode, LineStyle, LineType }) => {
|
|
190
192
|
if (disposed)
|
|
191
193
|
return;
|
|
@@ -258,29 +260,28 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
258
260
|
api.panes()[1]?.setStretchFactor(1);
|
|
259
261
|
return volume;
|
|
260
262
|
};
|
|
261
|
-
|
|
263
|
+
built = {
|
|
262
264
|
api, area, candle, volume: null, addVolume, up, down, ink,
|
|
263
265
|
curved: LineType.Curved, straight: LineType.Simple,
|
|
264
266
|
areaMarkers: createSeriesMarkers(area, []),
|
|
265
267
|
candleMarkers: createSeriesMarkers(candle, []),
|
|
266
268
|
};
|
|
267
|
-
|
|
269
|
+
setChart(built);
|
|
268
270
|
});
|
|
269
271
|
return () => {
|
|
270
272
|
disposed = true;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
setReady(false);
|
|
273
|
+
built?.api.remove();
|
|
274
|
+
setChart(null);
|
|
274
275
|
};
|
|
275
276
|
}, []);
|
|
276
277
|
// Applied to the chart in place: rebuilding it would leave a new chart with nothing drawn on it.
|
|
277
278
|
useEffect(() => {
|
|
278
|
-
const c = chart
|
|
279
|
-
if (!
|
|
279
|
+
const c = chart;
|
|
280
|
+
if (!c)
|
|
280
281
|
return;
|
|
281
282
|
c.api.priceScale('right').applyOptions({ scaleMargins: { top: MARGIN_TOP, bottom: fromZero ? 0 : 0.1 } });
|
|
282
283
|
c.area.applyOptions({ lineType: aggregate === 'last' ? c.straight : c.curved, autoscaleInfoProvider: fromZero ? fromZeroScale : ownScale });
|
|
283
|
-
}, [
|
|
284
|
+
}, [chart, fromZero, aggregate]);
|
|
284
285
|
// One request per resolution for the life of the loader: every range from 30D up reads the same days.
|
|
285
286
|
const cache = useRef(new Map());
|
|
286
287
|
useEffect(() => {
|
|
@@ -330,8 +331,8 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
330
331
|
}, [points, range, ranges.length, resolution, step, aggregate]);
|
|
331
332
|
// While a resolution loads, the last one stays under the skeleton; an empty or failed one clears it.
|
|
332
333
|
useEffect(() => {
|
|
333
|
-
const c = chart
|
|
334
|
-
if (!
|
|
334
|
+
const c = chart;
|
|
335
|
+
if (!c || !source)
|
|
335
336
|
return;
|
|
336
337
|
const shown = view?.shown ?? [];
|
|
337
338
|
const asCandles = candles && !!view?.ohlc;
|
|
@@ -364,7 +365,7 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
364
365
|
c.areaMarkers.setMarkers(asCandles ? [] : drawn);
|
|
365
366
|
c.candleMarkers.setMarkers(asCandles ? drawn : []);
|
|
366
367
|
c.api.timeScale().fitContent();
|
|
367
|
-
}, [
|
|
368
|
+
}, [chart, source, view, candles, markers]);
|
|
368
369
|
// Reported once there are points to step, so a step read from a URL is not cleared while they load.
|
|
369
370
|
const shown = !!view;
|
|
370
371
|
const picked = step && step === view?.step ? step : null;
|
|
@@ -382,5 +383,5 @@ export function TimeChart({ series, label, ranges = typeof series === 'function'
|
|
|
382
383
|
top: box.current.offsetTop + tip.y,
|
|
383
384
|
...(tip.x < box.current.clientWidth / 2 ? { left: tip.x } : { right: box.current.clientWidth - tip.x }),
|
|
384
385
|
translate: tip.y < 48 ? '0 1rem' : '0 calc(-100% - 0.75rem)',
|
|
385
|
-
}, children: tip.label })), (!
|
|
386
|
+
}, children: tip.label })), (!chart || (!points && !status)) && _jsx("div", { className: "chart-status skeleton", "aria-hidden": true }), status && _jsx("p", { className: "chart-status", children: status }), (controls || ranges.length > 1 || daily || view?.ohlc) && (_jsxs("div", { className: "chart-controls", children: [controls, ranges.length > 1 && (_jsx("div", { role: "group", "aria-label": "Range", children: ranges.map(r => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => setRange(r), children: RANGE[r].label }, r))) })), daily && (_jsx("div", { role: "group", "aria-label": "Step", children: STEPS.map(s => (_jsx("button", { type: "button", "aria-pressed": s === view?.step, disabled: !view?.steps.includes(s), onClick: () => setStep(s), title: `One point per ${s}`, children: STEP[s].label }, s))) })), view?.ohlc && (_jsx("div", { children: _jsx("button", { type: "button", "aria-pressed": candles, "aria-label": "Candles", title: "Candles", onClick: () => setCandles(c => !c), children: _jsxs("svg", { className: "chart-icon", viewBox: "0 0 12 12", "aria-hidden": true, children: [_jsx("path", { d: "M3.5 1v2M3.5 9v2M8.5 2v3M8.5 9v1", stroke: "currentColor" }), _jsx("rect", { x: "2", y: "3", width: "3", height: "6", rx: "0.5", fill: "currentColor" }), _jsx("rect", { x: "7", y: "5", width: "3", height: "4", rx: "0.5", fill: "currentColor" })] }) }) }))] }))] }));
|
|
386
387
|
}
|
package/dist/conformance.d.ts
CHANGED
|
@@ -123,13 +123,8 @@ openapi?: readonly string[]): Record<string, {
|
|
|
123
123
|
url: string;
|
|
124
124
|
at: (json: Record<string, unknown>) => unknown;
|
|
125
125
|
}>;
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
* agent.json to agent-card.json and defined no fallback either way, so an origin
|
|
129
|
-
* that lets them drift answers two different questions to two halves of its
|
|
130
|
-
* callers.
|
|
131
|
-
*/
|
|
132
|
-
export declare function agentCardParity(t: Tester): Promise<void>;
|
|
126
|
+
/** The A2A card at `agent-card.json`: complete, and its `url` an endpoint that answers. */
|
|
127
|
+
export declare function agentCardChecks(t: Tester): Promise<void>;
|
|
133
128
|
/**
|
|
134
129
|
* Any surface an agent recrawls should answer a conditional GET with a 304.
|
|
135
130
|
*
|
|
@@ -140,7 +135,7 @@ export declare function agentCardParity(t: Tester): Promise<void>;
|
|
|
140
135
|
* will pass is a check that measures nothing.
|
|
141
136
|
*/
|
|
142
137
|
export declare function conditionalGetChecks(t: Tester, paths: readonly string[]): Promise<void>;
|
|
143
|
-
/** The JSON descriptors every S10 origin serves: the
|
|
138
|
+
/** The JSON descriptors every S10 origin serves: the agent card, the OpenAPI pair, the MCP manifest and the server card. */
|
|
144
139
|
export declare const S10_DESCRIPTORS: readonly string[];
|
|
145
140
|
/**
|
|
146
141
|
* A JSON descriptor is revalidatable — an agent card, an OpenAPI document, a
|
package/dist/conformance.js
CHANGED
|
@@ -252,7 +252,6 @@ openapi = ['/openapi.json', '/.well-known/openapi.json']) {
|
|
|
252
252
|
const version = (j) => j.version;
|
|
253
253
|
return {
|
|
254
254
|
card: { url: `${t.base}/.well-known/agent-card.json`, at: version },
|
|
255
|
-
legacyCard: { url: `${t.base}/.well-known/agent.json`, at: version },
|
|
256
255
|
mcpManifest: { url: `${t.base}/.well-known/mcp.json`, at: version },
|
|
257
256
|
serverCard: { url: `${t.endpoint}/server-card`, at: version },
|
|
258
257
|
...Object.fromEntries(openapi.map(path => [
|
|
@@ -261,20 +260,10 @@ openapi = ['/openapi.json', '/.well-known/openapi.json']) {
|
|
|
261
260
|
])),
|
|
262
261
|
};
|
|
263
262
|
}
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
* callers.
|
|
269
|
-
*/
|
|
270
|
-
export async function agentCardParity(t) {
|
|
271
|
-
const [a, b] = await Promise.all([
|
|
272
|
-
fetch(`${t.base}/.well-known/agent-card.json`).then(r => r.json()),
|
|
273
|
-
fetch(`${t.base}/.well-known/agent.json`).then(r => r.json()),
|
|
274
|
-
]);
|
|
275
|
-
t.check('agent card parity', JSON.stringify(a) === JSON.stringify(b), 'agent.json === agent-card.json');
|
|
276
|
-
// A card missing a required field is a card a spec-current client rejects, and
|
|
277
|
-
// parity alone happily reports two identical unusable ones.
|
|
263
|
+
/** The A2A card at `agent-card.json`: complete, and its `url` an endpoint that answers. */
|
|
264
|
+
export async function agentCardChecks(t) {
|
|
265
|
+
const a = await fetch(`${t.base}/.well-known/agent-card.json`).then(r => r.json());
|
|
266
|
+
// A card missing a required field is a card a spec-current client rejects.
|
|
278
267
|
const required = [
|
|
279
268
|
'protocolVersion',
|
|
280
269
|
'name',
|
|
@@ -335,10 +324,9 @@ export async function conditionalGetChecks(t, paths) {
|
|
|
335
324
|
}
|
|
336
325
|
}
|
|
337
326
|
}
|
|
338
|
-
/** The JSON descriptors every S10 origin serves: the
|
|
327
|
+
/** The JSON descriptors every S10 origin serves: the agent card, the OpenAPI pair, the MCP manifest and the server card. */
|
|
339
328
|
export const S10_DESCRIPTORS = [
|
|
340
329
|
'.well-known/agent-card.json',
|
|
341
|
-
'.well-known/agent.json',
|
|
342
330
|
'.well-known/openapi.json',
|
|
343
331
|
'openapi.json',
|
|
344
332
|
'.well-known/mcp.json',
|
|
@@ -635,7 +623,7 @@ export async function rolaOriginCheck(t, opts = {}) {
|
|
|
635
623
|
export async function standardSuite(t, cfg) {
|
|
636
624
|
const init = await transportChecks(t, t.clientName, cfg.capabilities);
|
|
637
625
|
await versionCoherence(t, cfg.version, standardVersionSources(t, cfg.openapi), init.result?.serverInfo?.version);
|
|
638
|
-
await
|
|
626
|
+
await agentCardChecks(t);
|
|
639
627
|
await annotationChecks(t, { writes: cfg.writes });
|
|
640
628
|
await manifestParity(t, cfg.manifest);
|
|
641
629
|
if (cfg.budget)
|
package/dist/editor.js
CHANGED
|
@@ -421,10 +421,10 @@ export function useWikiEditor({ value, onChange, placeholder = '', nodes = [], p
|
|
|
421
421
|
const uploadAttempt = useRef(0);
|
|
422
422
|
const initialValueRef = useRef(value);
|
|
423
423
|
const onChangeRef = useRef(onChange);
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
const pendingRef = useRef(
|
|
424
|
+
// The timer of an edit made and not yet reported, or null when every edit
|
|
425
|
+
// has been. `onUpdate` fires for edits only, never for the `setContent` that
|
|
426
|
+
// loads the stored value.
|
|
427
|
+
const pendingRef = useRef(null);
|
|
428
428
|
// Written AFTER render, not during: a ref mutated mid-render can tear under
|
|
429
429
|
// concurrent rendering. Only read from events and timeouts, which run later.
|
|
430
430
|
useEffect(() => {
|
|
@@ -441,10 +441,10 @@ export function useWikiEditor({ value, onChange, placeholder = '', nodes = [], p
|
|
|
441
441
|
transformPastedHTML: cleanPastedHtml,
|
|
442
442
|
},
|
|
443
443
|
onUpdate: ({ editor }) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
pendingRef.current =
|
|
444
|
+
if (pendingRef.current)
|
|
445
|
+
clearTimeout(pendingRef.current);
|
|
446
|
+
pendingRef.current = setTimeout(() => {
|
|
447
|
+
pendingRef.current = null;
|
|
448
448
|
onChangeRef.current(editor.getHTML());
|
|
449
449
|
}, debounceMs);
|
|
450
450
|
},
|
|
@@ -452,10 +452,10 @@ export function useWikiEditor({ value, onChange, placeholder = '', nodes = [], p
|
|
|
452
452
|
// before the click fires — never drops the final keystroke. A blur with no
|
|
453
453
|
// edit reports nothing.
|
|
454
454
|
onBlur: ({ editor }) => {
|
|
455
|
-
clearTimeout(debounceRef.current);
|
|
456
455
|
if (!pendingRef.current)
|
|
457
456
|
return;
|
|
458
|
-
pendingRef.current
|
|
457
|
+
clearTimeout(pendingRef.current);
|
|
458
|
+
pendingRef.current = null;
|
|
459
459
|
onChangeRef.current(editor.getHTML());
|
|
460
460
|
},
|
|
461
461
|
immediatelyRender: false,
|
|
@@ -465,7 +465,10 @@ export function useWikiEditor({ value, onChange, placeholder = '', nodes = [], p
|
|
|
465
465
|
}
|
|
466
466
|
},
|
|
467
467
|
});
|
|
468
|
-
useEffect(() => () =>
|
|
468
|
+
useEffect(() => () => {
|
|
469
|
+
if (pendingRef.current)
|
|
470
|
+
clearTimeout(pendingRef.current);
|
|
471
|
+
}, []);
|
|
469
472
|
// Accept an outside change, but never while the author is typing into it.
|
|
470
473
|
useEffect(() => {
|
|
471
474
|
if (!editor || editor.isFocused)
|
package/dist/http.d.ts
CHANGED
|
@@ -188,8 +188,9 @@ export declare class HttpError extends Error {
|
|
|
188
188
|
constructor(status: number, message: string, extra?: Record<string, unknown> | undefined);
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
|
-
* Run a handler; a thrown `HttpError` becomes its own answer,
|
|
192
|
-
*
|
|
191
|
+
* Run a handler; a thrown `HttpError` becomes its own answer, a thrown
|
|
192
|
+
* `Response` is the answer as built (a 429 keeps its `Retry-After`), anything
|
|
193
|
+
* else a logged 500 that names nothing internal. One handler here echoed every
|
|
193
194
|
* exception's message to the caller, which is how a database error's text
|
|
194
195
|
* becomes public.
|
|
195
196
|
*/
|
package/dist/http.js
CHANGED
|
@@ -300,8 +300,9 @@ export class HttpError extends Error {
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
/**
|
|
303
|
-
* Run a handler; a thrown `HttpError` becomes its own answer,
|
|
304
|
-
*
|
|
303
|
+
* Run a handler; a thrown `HttpError` becomes its own answer, a thrown
|
|
304
|
+
* `Response` is the answer as built (a 429 keeps its `Retry-After`), anything
|
|
305
|
+
* else a logged 500 that names nothing internal. One handler here echoed every
|
|
305
306
|
* exception's message to the caller, which is how a database error's text
|
|
306
307
|
* becomes public.
|
|
307
308
|
*/
|
|
@@ -310,6 +311,8 @@ export async function handleRoute(fn, errorMsg = 'Internal server error') {
|
|
|
310
311
|
return await fn();
|
|
311
312
|
}
|
|
312
313
|
catch (error) {
|
|
314
|
+
if (error instanceof Response)
|
|
315
|
+
return error;
|
|
313
316
|
if (error instanceof HttpError)
|
|
314
317
|
return json({ error: error.message, ...error.extra }, error.status);
|
|
315
318
|
console.error(errorMsg, error);
|
package/dist/indexnow.d.ts
CHANGED
|
@@ -7,13 +7,23 @@ export interface IndexNowEntry {
|
|
|
7
7
|
}
|
|
8
8
|
/** The `<loc>` and `<lastmod>` of every `<url>` in a sitemap. */
|
|
9
9
|
export declare function sitemapEntries(xml: string): IndexNowEntry[];
|
|
10
|
+
/** A sitemap read: its entries, and each child sitemap that did not answer and why. */
|
|
11
|
+
export interface SitemapRead {
|
|
12
|
+
entries: IndexNowEntry[];
|
|
13
|
+
unread: {
|
|
14
|
+
url: string;
|
|
15
|
+
error: string;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
10
18
|
/**
|
|
11
19
|
* Every entry of the sitemap at `url`, following a sitemap index one level down
|
|
12
20
|
* to at most 20 children. The `indexnow` and `check-pages` bins each parsed
|
|
13
21
|
* sitemaps, and only one of them read an index. Throws when `url` itself does
|
|
14
|
-
* not answer 200
|
|
22
|
+
* not answer 200. A child that fails is listed in `unread`, never read as a
|
|
23
|
+
* sitemap of no pages: skipped silently, a dead child passed `check-pages` with
|
|
24
|
+
* its pages unchecked.
|
|
15
25
|
*/
|
|
16
|
-
export declare function readSitemap(url: string, fetchImpl?: typeof fetch): Promise<
|
|
26
|
+
export declare function readSitemap(url: string, fetchImpl?: typeof fetch): Promise<SitemapRead>;
|
|
17
27
|
/**
|
|
18
28
|
* The URLs modified at or after `since`. An entry with no `lastModified` is
|
|
19
29
|
* left out: nothing says it changed, and the protocol asks for changed pages
|
package/dist/indexnow.js
CHANGED
|
@@ -35,7 +35,9 @@ export function sitemapEntries(xml) {
|
|
|
35
35
|
* Every entry of the sitemap at `url`, following a sitemap index one level down
|
|
36
36
|
* to at most 20 children. The `indexnow` and `check-pages` bins each parsed
|
|
37
37
|
* sitemaps, and only one of them read an index. Throws when `url` itself does
|
|
38
|
-
* not answer 200
|
|
38
|
+
* not answer 200. A child that fails is listed in `unread`, never read as a
|
|
39
|
+
* sitemap of no pages: skipped silently, a dead child passed `check-pages` with
|
|
40
|
+
* its pages unchecked.
|
|
39
41
|
*/
|
|
40
42
|
export async function readSitemap(url, fetchImpl = fetch) {
|
|
41
43
|
const res = await fetchImpl(url);
|
|
@@ -43,12 +45,20 @@ export async function readSitemap(url, fetchImpl = fetch) {
|
|
|
43
45
|
throw new Error(`${url} answered ${res.status}`);
|
|
44
46
|
const xml = await res.text();
|
|
45
47
|
if (!/<sitemapindex/i.test(xml))
|
|
46
|
-
return sitemapEntries(xml);
|
|
48
|
+
return { entries: sitemapEntries(xml), unread: [] };
|
|
47
49
|
const children = [...xml.matchAll(/<loc>([\s\S]*?)<\/loc>/g)].slice(0, 20).map(m => decodeEntities(m[1].trim()));
|
|
48
|
-
const
|
|
49
|
-
for (const child of children)
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const read = { entries: [], unread: [] };
|
|
51
|
+
for (const child of children) {
|
|
52
|
+
try {
|
|
53
|
+
const sub = await readSitemap(child, fetchImpl);
|
|
54
|
+
read.entries.push(...sub.entries);
|
|
55
|
+
read.unread.push(...sub.unread);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
read.unread.push({ url: child, error: e instanceof Error ? e.message : String(e) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return read;
|
|
52
62
|
}
|
|
53
63
|
/**
|
|
54
64
|
* The URLs modified at or after `since`. An entry with no `lastModified` is
|
package/dist/knip.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// knip.ts — the knip configuration every app here shares: what the App
|
|
2
2
|
// Router loads by convention and nothing imports, the scripts the routines
|
|
3
|
-
// run from the command line, and the
|
|
3
|
+
// run from the command line, and the dependency no TS import names.
|
|
4
4
|
// An app's `knip.ts` spreads it and adds what is its own:
|
|
5
5
|
//
|
|
6
6
|
// import base from 'wiki-formant/knip';
|
|
@@ -23,8 +23,6 @@ const config = {
|
|
|
23
23
|
ignoreDependencies: [
|
|
24
24
|
// Reached through `@tailwindcss/postcss` and the `@import` in globals.css.
|
|
25
25
|
'tailwindcss',
|
|
26
|
-
// Imported by the generated Prisma client.
|
|
27
|
-
'@prisma/client',
|
|
28
26
|
],
|
|
29
27
|
// The Vercel CLI the scripts shell out to for a cache tag; a global, never a dependency.
|
|
30
28
|
ignoreBinaries: ['vercel'],
|
package/dist/link-check.js
CHANGED
|
@@ -193,6 +193,9 @@ const YOUTUBE_WATCH = /^https?:\/\/(?:(?:www\.)?youtube\.com\/(?:watch\?(?:[^#]*
|
|
|
193
193
|
* video. oEmbed can:
|
|
194
194
|
*
|
|
195
195
|
* 200 → public and embeddable | 404 → deleted | 401/403 → private or embedding off
|
|
196
|
+
*
|
|
197
|
+
* A 429 is YouTube rationing the sweep, walled as `attempt` walls it: read off
|
|
198
|
+
* its status alone, `verdictOf` called every video in a throttled run dead.
|
|
196
199
|
*/
|
|
197
200
|
async function probeYouTube(videoId, opts = {}) {
|
|
198
201
|
const { timeoutMs } = { ...DEFAULTS, ...opts };
|
|
@@ -208,7 +211,7 @@ async function probeYouTube(videoId, opts = {}) {
|
|
|
208
211
|
if (res.status === 401 || res.status === 403) {
|
|
209
212
|
return { status: res.status, ok: false, reason: 'restricted (private or embedding disabled)' };
|
|
210
213
|
}
|
|
211
|
-
return { status: res.status, ok: res.status < 400 };
|
|
214
|
+
return { status: res.status, ok: res.status < 400, ...(res.status === 429 && { walled: true }) };
|
|
212
215
|
}
|
|
213
216
|
catch (err) {
|
|
214
217
|
return { status: 0, ok: false, ...describeFailure(err) };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type License } from './license.js';
|
|
2
|
+
import { type ToolSchema } from './mcp.js';
|
|
3
|
+
type Schema = Record<string, unknown>;
|
|
4
|
+
export interface OpenApiInfo {
|
|
5
|
+
title: string;
|
|
6
|
+
/** The server version, the number the agent card and MCP `initialize` carry. */
|
|
7
|
+
version: string;
|
|
8
|
+
description: string;
|
|
9
|
+
license: License;
|
|
10
|
+
siteUrl: string;
|
|
11
|
+
contact?: {
|
|
12
|
+
name: string;
|
|
13
|
+
url: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** The document: version, info, the one server, the paths, and components when there are any. */
|
|
17
|
+
export declare function openApiDocument<P extends object, C extends object>(info: OpenApiInfo, paths: P, components?: C): {
|
|
18
|
+
components?: C | undefined;
|
|
19
|
+
openapi: "3.1.0";
|
|
20
|
+
info: {
|
|
21
|
+
license: {
|
|
22
|
+
name: string;
|
|
23
|
+
identifier: string;
|
|
24
|
+
};
|
|
25
|
+
contact?: {
|
|
26
|
+
name: string;
|
|
27
|
+
url: string;
|
|
28
|
+
} | undefined;
|
|
29
|
+
title: string;
|
|
30
|
+
version: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
servers: {
|
|
34
|
+
url: string;
|
|
35
|
+
}[];
|
|
36
|
+
paths: P;
|
|
37
|
+
};
|
|
38
|
+
/** A required JSON request body. */
|
|
39
|
+
export declare const jsonBody: (schema: Schema) => {
|
|
40
|
+
required: boolean;
|
|
41
|
+
content: {
|
|
42
|
+
'application/json': {
|
|
43
|
+
schema: Schema;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/** A JSON response, its schema left open when none is given. */
|
|
48
|
+
export declare const jsonResponse: (description: string, schema?: Schema) => {
|
|
49
|
+
description: string;
|
|
50
|
+
content: {
|
|
51
|
+
'application/json': {
|
|
52
|
+
schema: Schema;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A tool's input schema as a REST body, less the fields the path carries. A
|
|
58
|
+
* property whose `items` is one of `refs` points at its component instead of
|
|
59
|
+
* inlining it, so a block schema is written once.
|
|
60
|
+
*/
|
|
61
|
+
export declare function toolBody(input: Pick<ToolSchema, 'properties' | 'required'>, { inPath, refs }?: {
|
|
62
|
+
inPath?: readonly string[];
|
|
63
|
+
refs?: Record<string, Schema>;
|
|
64
|
+
}): {
|
|
65
|
+
required: boolean;
|
|
66
|
+
content: {
|
|
67
|
+
'application/json': {
|
|
68
|
+
schema: Schema;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/** The validator a recrawling agent sends: a match costs a 304 rather than the body. */
|
|
73
|
+
export declare const IF_NONE_MATCH: {
|
|
74
|
+
name: string;
|
|
75
|
+
in: string;
|
|
76
|
+
required: boolean;
|
|
77
|
+
schema: {
|
|
78
|
+
type: string;
|
|
79
|
+
};
|
|
80
|
+
description: string;
|
|
81
|
+
};
|
|
82
|
+
export declare const NOT_MODIFIED: {
|
|
83
|
+
description: string;
|
|
84
|
+
};
|
|
85
|
+
/** `/llms.txt`, answered from one corpus validator. */
|
|
86
|
+
export declare const llmsTxtPath: (summary: string) => {
|
|
87
|
+
get: {
|
|
88
|
+
summary: string;
|
|
89
|
+
parameters: {
|
|
90
|
+
name: string;
|
|
91
|
+
in: string;
|
|
92
|
+
required: boolean;
|
|
93
|
+
schema: {
|
|
94
|
+
type: string;
|
|
95
|
+
};
|
|
96
|
+
description: string;
|
|
97
|
+
}[];
|
|
98
|
+
responses: {
|
|
99
|
+
'200': {
|
|
100
|
+
description: string;
|
|
101
|
+
content: {
|
|
102
|
+
'text/plain': {};
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
'304': {
|
|
106
|
+
description: string;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/** An MCP endpoint: the tools it serves, and what every endpoint `mcpResponse` serves has in common. */
|
|
112
|
+
export declare function mcpPath(tools: readonly string[], opts?: {
|
|
113
|
+
summary?: string;
|
|
114
|
+
note?: string;
|
|
115
|
+
}): {
|
|
116
|
+
post: {
|
|
117
|
+
summary: string;
|
|
118
|
+
description: string;
|
|
119
|
+
requestBody: {
|
|
120
|
+
required: boolean;
|
|
121
|
+
content: {
|
|
122
|
+
'application/json': {
|
|
123
|
+
schema: Schema;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
responses: {
|
|
128
|
+
'200': {
|
|
129
|
+
description: string;
|
|
130
|
+
content: {
|
|
131
|
+
'application/json': {
|
|
132
|
+
schema: Schema;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
export {};
|
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// openapi.ts — the OpenAPI 3.1 frame every agent surface serves at
|
|
2
|
+
// /.well-known/openapi.json.
|
|
3
|
+
//
|
|
4
|
+
// What is per-site is the paths only it has. The frame, the body and response
|
|
5
|
+
// shapes, the conditional GET, the MCP endpoint and /llms.txt read the same on
|
|
6
|
+
// every site, so they are built here and a spec carries only its own paths.
|
|
7
|
+
import { openApiLicense } from './license.js';
|
|
8
|
+
import { DEFAULT_MAX_BATCH, MCP_PROTOCOL_VERSIONS } from './mcp.js';
|
|
9
|
+
import { MCP_RATE_LIMIT_TEXT } from './rate-limit.js';
|
|
10
|
+
/** The document: version, info, the one server, the paths, and components when there are any. */
|
|
11
|
+
export function openApiDocument(info, paths, components) {
|
|
12
|
+
const { title, version, description, license, siteUrl, contact } = info;
|
|
13
|
+
return {
|
|
14
|
+
openapi: '3.1.0',
|
|
15
|
+
info: { title, version, description, ...(contact ? { contact } : {}), license: openApiLicense(license) },
|
|
16
|
+
servers: [{ url: siteUrl }],
|
|
17
|
+
paths,
|
|
18
|
+
...(components ? { components } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** A required JSON request body. */
|
|
22
|
+
export const jsonBody = (schema) => ({ required: true, content: { 'application/json': { schema } } });
|
|
23
|
+
/** A JSON response, its schema left open when none is given. */
|
|
24
|
+
export const jsonResponse = (description, schema = {}) => ({
|
|
25
|
+
description,
|
|
26
|
+
content: { 'application/json': { schema } },
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* A tool's input schema as a REST body, less the fields the path carries. A
|
|
30
|
+
* property whose `items` is one of `refs` points at its component instead of
|
|
31
|
+
* inlining it, so a block schema is written once.
|
|
32
|
+
*/
|
|
33
|
+
export function toolBody(input, { inPath = [], refs = {} } = {}) {
|
|
34
|
+
const kept = (k) => !inPath.includes(k);
|
|
35
|
+
const ref = (items) => Object.entries(refs).find(([, s]) => s === items)?.[0];
|
|
36
|
+
return jsonBody({
|
|
37
|
+
type: 'object',
|
|
38
|
+
required: input.required?.filter(kept),
|
|
39
|
+
properties: Object.fromEntries(Object.entries(input.properties)
|
|
40
|
+
.filter(([k]) => kept(k))
|
|
41
|
+
.map(([k, v]) => {
|
|
42
|
+
const name = ref(v.items);
|
|
43
|
+
return [k, name ? { ...v, items: { $ref: `#/components/schemas/${name}` } } : v];
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** The validator a recrawling agent sends: a match costs a 304 rather than the body. */
|
|
48
|
+
export const IF_NONE_MATCH = {
|
|
49
|
+
name: 'If-None-Match',
|
|
50
|
+
in: 'header',
|
|
51
|
+
required: false,
|
|
52
|
+
schema: { type: 'string' },
|
|
53
|
+
description: 'ETag from an earlier fetch. A match returns 304 with no body.',
|
|
54
|
+
};
|
|
55
|
+
export const NOT_MODIFIED = { description: 'Unchanged since the supplied validator' };
|
|
56
|
+
/** `/llms.txt`, answered from one corpus validator. */
|
|
57
|
+
export const llmsTxtPath = (summary) => ({
|
|
58
|
+
get: {
|
|
59
|
+
summary,
|
|
60
|
+
parameters: [IF_NONE_MATCH],
|
|
61
|
+
responses: { '200': { description: 'OK', content: { 'text/plain': {} } }, '304': NOT_MODIFIED },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
/** An MCP endpoint: the tools it serves, and what every endpoint `mcpResponse` serves has in common. */
|
|
65
|
+
export function mcpPath(tools, opts = {}) {
|
|
66
|
+
return {
|
|
67
|
+
post: {
|
|
68
|
+
summary: opts.summary ?? 'MCP server (JSON-RPC 2.0, Streamable HTTP)',
|
|
69
|
+
description: `Model Context Protocol ${MCP_PROTOCOL_VERSIONS.join(', ')}. Tools: ${tools.join(', ')}. ` +
|
|
70
|
+
`${opts.note ? `${opts.note} ` : ''}Batches are capped at ${DEFAULT_MAX_BATCH}; rate limit ${MCP_RATE_LIMIT_TEXT}.`,
|
|
71
|
+
requestBody: jsonBody({ type: 'object', description: 'JSON-RPC 2.0 request or batch' }),
|
|
72
|
+
responses: { '200': jsonResponse('JSON-RPC 2.0 response') },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/react.d.ts
CHANGED
|
@@ -385,7 +385,8 @@ export declare function restoreViaPost<Id>(endpoint: string): RestoreRevision<Id
|
|
|
385
385
|
* Confirm, restore, report. Of the three copies one asked no confirmation, one
|
|
386
386
|
* dropped a failure silently, and one reported it through `alert`. A restore
|
|
387
387
|
* writes the old state forward as a new revision in all three, which is what
|
|
388
|
-
* the default question says.
|
|
388
|
+
* the default question says. A `restore` that throws is a failure like one
|
|
389
|
+
* that answers with its reason: it shows as `error`, never as nothing.
|
|
389
390
|
*/
|
|
390
391
|
export declare function useRevisionRestore<Id>(restore: RestoreRevision<Id>, options: {
|
|
391
392
|
onRestored: () => void;
|
|
@@ -433,6 +434,13 @@ export interface SnapRow<T extends HTMLElement> {
|
|
|
433
434
|
* there reads as broken. Motion honours `prefers-reduced-motion`.
|
|
434
435
|
*/
|
|
435
436
|
export declare function useSnapRow<T extends HTMLElement = HTMLDivElement>(): SnapRow<T>;
|
|
437
|
+
/** The boundary's children render, or what they threw does: a thrown `null` is still a failure. */
|
|
438
|
+
type BoundaryState = {
|
|
439
|
+
status: 'ok';
|
|
440
|
+
} | {
|
|
441
|
+
status: 'failed';
|
|
442
|
+
error: unknown;
|
|
443
|
+
};
|
|
436
444
|
/**
|
|
437
445
|
* The one thing hooks cannot express, so it is worth owning once.
|
|
438
446
|
*
|
|
@@ -449,18 +457,9 @@ export declare class ErrorBoundary extends Component<{
|
|
|
449
457
|
*/
|
|
450
458
|
fallback: (retry: () => void, error: unknown) => ReactNode;
|
|
451
459
|
onError?: (error: unknown) => void;
|
|
452
|
-
}, {
|
|
453
|
-
|
|
454
|
-
error: unknown;
|
|
455
|
-
}> {
|
|
456
|
-
state: {
|
|
457
|
-
failed: boolean;
|
|
458
|
-
error: unknown;
|
|
459
|
-
};
|
|
460
|
-
static getDerivedStateFromError(error: unknown): {
|
|
461
|
-
failed: boolean;
|
|
462
|
-
error: unknown;
|
|
463
|
-
};
|
|
460
|
+
}, BoundaryState> {
|
|
461
|
+
state: BoundaryState;
|
|
462
|
+
static getDerivedStateFromError(error: unknown): BoundaryState;
|
|
464
463
|
componentDidCatch(error: unknown): void;
|
|
465
464
|
render(): ReactNode;
|
|
466
465
|
}
|
package/dist/react.js
CHANGED
|
@@ -52,8 +52,10 @@ function useCollapsibleSidebar(options = {}) {
|
|
|
52
52
|
// Start open on the server and on the first client paint. SSR has no viewport
|
|
53
53
|
// and no storage, so any other guess is a guaranteed mismatch on some loads.
|
|
54
54
|
const [open, setOpenState] = useState(true);
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Unknown until the effect has read matchMedia: one value, so the rail is
|
|
56
|
+
// never mobile before it is ready, and `ready` is never set apart from it.
|
|
57
|
+
const [viewport, setViewport] = useState('unknown');
|
|
58
|
+
const ready = viewport !== 'unknown';
|
|
57
59
|
// Set as soon as the reader touches the control, so a later viewport change
|
|
58
60
|
// cannot overwrite what they asked for.
|
|
59
61
|
const chosen = useRef(false);
|
|
@@ -61,7 +63,7 @@ function useCollapsibleSidebar(options = {}) {
|
|
|
61
63
|
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
62
64
|
const stored = readStored(storageKey);
|
|
63
65
|
const apply = (matches) => {
|
|
64
|
-
|
|
66
|
+
setViewport(matches ? 'mobile' : 'desktop');
|
|
65
67
|
// The breakpoint supplies a default, never an override. This is bug 1.
|
|
66
68
|
setOpenState(current => resolveSidebarOpen({ chosen: chosen.current, current, stored, isMobile: matches }));
|
|
67
69
|
// The same number lives in the stylesheet, and must. Say so out loud the
|
|
@@ -79,7 +81,6 @@ function useCollapsibleSidebar(options = {}) {
|
|
|
79
81
|
}
|
|
80
82
|
};
|
|
81
83
|
apply(mql.matches);
|
|
82
|
-
setReady(true);
|
|
83
84
|
const handler = (e) => apply(e.matches);
|
|
84
85
|
mql.addEventListener('change', handler);
|
|
85
86
|
return () => mql.removeEventListener('change', handler);
|
|
@@ -106,7 +107,7 @@ function useCollapsibleSidebar(options = {}) {
|
|
|
106
107
|
return;
|
|
107
108
|
document.documentElement.setAttribute(SIDEBAR_ATTRIBUTE, open ? 'open' : 'closed');
|
|
108
109
|
}, [open, ready]);
|
|
109
|
-
return { open, setOpen, toggle, isMobile, ready };
|
|
110
|
+
return { open, setOpen, toggle, isMobile: viewport === 'mobile', ready };
|
|
110
111
|
}
|
|
111
112
|
// ---- sharing that state across the layout -----------------------------------
|
|
112
113
|
// The rail and the button that toggles it are rarely siblings — the button
|
|
@@ -756,11 +757,11 @@ export function restoreViaPost(endpoint) {
|
|
|
756
757
|
* Confirm, restore, report. Of the three copies one asked no confirmation, one
|
|
757
758
|
* dropped a failure silently, and one reported it through `alert`. A restore
|
|
758
759
|
* writes the old state forward as a new revision in all three, which is what
|
|
759
|
-
* the default question says.
|
|
760
|
+
* the default question says. A `restore` that throws is a failure like one
|
|
761
|
+
* that answers with its reason: it shows as `error`, never as nothing.
|
|
760
762
|
*/
|
|
761
763
|
export function useRevisionRestore(restore, options) {
|
|
762
|
-
const [
|
|
763
|
-
const [error, setError] = useState(null);
|
|
764
|
+
const [state, setState] = useState({ status: 'idle' });
|
|
764
765
|
// One restore at a time: a second, started while the first is in flight,
|
|
765
766
|
// would have its "Restoring…" cleared by the first one finishing.
|
|
766
767
|
const inFlight = useRef(false);
|
|
@@ -772,21 +773,26 @@ export function useRevisionRestore(restore, options) {
|
|
|
772
773
|
if (question && !window.confirm(question))
|
|
773
774
|
return;
|
|
774
775
|
inFlight.current = true;
|
|
775
|
-
|
|
776
|
-
|
|
776
|
+
setState({ status: 'restoring', id });
|
|
777
|
+
let failure;
|
|
777
778
|
try {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
onRestored();
|
|
779
|
+
failure = await restore(id);
|
|
780
|
+
}
|
|
781
|
+
catch (err) {
|
|
782
|
+
failure = err instanceof Error && err.message ? err.message : 'Restore failed.';
|
|
783
783
|
}
|
|
784
784
|
finally {
|
|
785
785
|
inFlight.current = false;
|
|
786
|
-
setBusyId(null);
|
|
787
786
|
}
|
|
787
|
+
setState(failure ? { status: 'failed', error: failure } : { status: 'idle' });
|
|
788
|
+
if (!failure)
|
|
789
|
+
onRestored();
|
|
788
790
|
}, [restore, onRestored, confirm]);
|
|
789
|
-
return {
|
|
791
|
+
return {
|
|
792
|
+
restore: run,
|
|
793
|
+
busyId: state.status === 'restoring' ? state.id : null,
|
|
794
|
+
error: state.status === 'failed' ? state.error : null,
|
|
795
|
+
};
|
|
790
796
|
}
|
|
791
797
|
// ---- copy to clipboard ------------------------------------------------------
|
|
792
798
|
/**
|
|
@@ -866,7 +872,6 @@ export function useSnapRow() {
|
|
|
866
872
|
}
|
|
867
873
|
const snapEnd = (row) => row.scrollWidth - row.clientWidth;
|
|
868
874
|
const snapTo = (row, left, smooth = true) => row.scrollTo({ left, behavior: smooth && !matchMedia('(prefers-reduced-motion: reduce)').matches ? 'smooth' : 'auto' });
|
|
869
|
-
// ---- error boundary ---------------------------------------------------------
|
|
870
875
|
/**
|
|
871
876
|
* The one thing hooks cannot express, so it is worth owning once.
|
|
872
877
|
*
|
|
@@ -876,16 +881,16 @@ const snapTo = (row, left, smooth = true) => row.scrollTo({ left, behavior: smoo
|
|
|
876
881
|
* caller's, and the boundary only decides when to offer it.
|
|
877
882
|
*/
|
|
878
883
|
export class ErrorBoundary extends Component {
|
|
879
|
-
state = {
|
|
884
|
+
state = { status: 'ok' };
|
|
880
885
|
static getDerivedStateFromError(error) {
|
|
881
|
-
return {
|
|
886
|
+
return { status: 'failed', error };
|
|
882
887
|
}
|
|
883
888
|
componentDidCatch(error) {
|
|
884
889
|
this.props.onError?.(error);
|
|
885
890
|
}
|
|
886
891
|
render() {
|
|
887
|
-
if (this.state.failed) {
|
|
888
|
-
return this.props.fallback(() => this.setState({
|
|
892
|
+
if (this.state.status === 'failed') {
|
|
893
|
+
return this.props.fallback(() => this.setState({ status: 'ok' }), this.state.error);
|
|
889
894
|
}
|
|
890
895
|
return this.props.children;
|
|
891
896
|
}
|
package/dist/stats-figures.d.ts
CHANGED
|
@@ -20,8 +20,13 @@ export interface PasskeyButtonProps {
|
|
|
20
20
|
errorClassName?: string;
|
|
21
21
|
}
|
|
22
22
|
export declare function PasskeyButton({ endpoint, invite, className, errorClassName }: PasskeyButtonProps): import("react").JSX.Element;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Ends the session at `endpoint` (`gate.signOut` as DELETE) and reloads the
|
|
25
|
+
* page. A sign-out the server did not take says so: reloaded, it showed the
|
|
26
|
+
* stats again with no word of why.
|
|
27
|
+
*/
|
|
28
|
+
export declare function PasskeySignOut({ endpoint, className, errorClassName }: {
|
|
25
29
|
endpoint: string;
|
|
26
30
|
className?: string;
|
|
31
|
+
errorClassName?: string;
|
|
27
32
|
}): import("react").JSX.Element;
|
package/dist/stats-figures.js
CHANGED
|
@@ -96,13 +96,12 @@ async function post(endpoint, body) {
|
|
|
96
96
|
throw new Error((await res.json().catch(() => null))?.error ?? 'Sign-in failed.');
|
|
97
97
|
return res.status === 204 ? null : res.json();
|
|
98
98
|
}
|
|
99
|
+
const IDLE = { status: 'idle' };
|
|
99
100
|
export function PasskeyButton({ endpoint, invite, className, errorClassName }) {
|
|
100
101
|
const saving = !!invite;
|
|
101
|
-
const [
|
|
102
|
-
const [error, setError] = useState(null);
|
|
102
|
+
const [attempt, setAttempt] = useState(IDLE);
|
|
103
103
|
async function go() {
|
|
104
|
-
|
|
105
|
-
setError(null);
|
|
104
|
+
setAttempt({ status: 'busy' });
|
|
106
105
|
try {
|
|
107
106
|
const optionsJSON = await post(endpoint, { invite });
|
|
108
107
|
const response = saving ? await startRegistration({ optionsJSON }) : await startAuthentication({ optionsJSON });
|
|
@@ -111,16 +110,24 @@ export function PasskeyButton({ endpoint, invite, className, errorClassName }) {
|
|
|
111
110
|
}
|
|
112
111
|
catch (e) {
|
|
113
112
|
// A prompt the person closed throws NotAllowedError, which needs no message.
|
|
114
|
-
|
|
115
|
-
setBusy(false);
|
|
113
|
+
setAttempt(e instanceof Error && e.name !== 'NotAllowedError' ? { status: 'failed', error: e.message } : IDLE);
|
|
116
114
|
}
|
|
117
115
|
}
|
|
118
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: className, onClick: go, disabled: busy, children: saving ? 'Save a passkey' : 'Sign in with a passkey' }),
|
|
116
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: className, onClick: go, disabled: attempt.status === 'busy', children: saving ? 'Save a passkey' : 'Sign in with a passkey' }), attempt.status === 'failed' && (_jsx("p", { role: "alert", className: errorClassName, children: attempt.error }))] }));
|
|
119
117
|
}
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Ends the session at `endpoint` (`gate.signOut` as DELETE) and reloads the
|
|
120
|
+
* page. A sign-out the server did not take says so: reloaded, it showed the
|
|
121
|
+
* stats again with no word of why.
|
|
122
|
+
*/
|
|
123
|
+
export function PasskeySignOut({ endpoint, className, errorClassName }) {
|
|
124
|
+
const [attempt, setAttempt] = useState(IDLE);
|
|
125
|
+
async function go() {
|
|
126
|
+
setAttempt({ status: 'busy' });
|
|
127
|
+
const res = await fetch(endpoint, { method: 'DELETE' }).catch(() => null);
|
|
128
|
+
if (res?.ok)
|
|
129
|
+
return location.reload();
|
|
130
|
+
setAttempt({ status: 'failed', error: res ? `Sign-out failed (${res.status}). Try again.` : 'Sign-out did not reach the server. Try again.' });
|
|
131
|
+
}
|
|
132
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: className, onClick: go, disabled: attempt.status === 'busy', children: "Sign out" }), attempt.status === 'failed' && (_jsx("p", { role: "alert", className: errorClassName, children: attempt.error }))] }));
|
|
126
133
|
}
|
package/dist/stats.js
CHANGED
|
@@ -83,7 +83,7 @@ export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES, lin
|
|
|
83
83
|
*/
|
|
84
84
|
export async function StatsGate({ gate, session, invite, host, endpoint = '/api/passkey', children, classNames: c = {} }) {
|
|
85
85
|
if (!invite && (await gate.signedIn(session))) {
|
|
86
|
-
return (_jsxs(_Fragment, { children: [await children(), _jsx("div", { className: c.actions, children: _jsx(PasskeySignOut, { endpoint: endpoint, className: c.button }) })] }));
|
|
86
|
+
return (_jsxs(_Fragment, { children: [await children(), _jsx("div", { className: c.actions, children: _jsx(PasskeySignOut, { endpoint: endpoint, className: c.button, errorClassName: c.error }) })] }));
|
|
87
87
|
}
|
|
88
88
|
return (_jsxs(_Fragment, { children: [_jsx("p", { className: c.text, children: invite ? `Save a passkey on this device to open ${host}'s stats.` : 'Sign in with a passkey saved for this site.' }), _jsx("div", { className: c.actions, children: _jsx(PasskeyButton, { endpoint: endpoint, invite: invite, className: c.button, errorClassName: c.error }) })] }));
|
|
89
89
|
}
|
package/dist/well-known.d.ts
CHANGED
|
@@ -50,13 +50,7 @@ export interface AgentCardConfig {
|
|
|
50
50
|
/** Anything this origin advertises that the others do not. */
|
|
51
51
|
extra?: Record<string, unknown>;
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
* An A2A Agent Card. Serve the same object at both `/.well-known/agent.json`
|
|
55
|
-
* and `/.well-known/agent-card.json`: v0.3 renamed the path and defined no
|
|
56
|
-
* fallback in either direction, so a spec-current client probes only the new
|
|
57
|
-
* one and a client on an older SDK probes only the old one. Serving one path
|
|
58
|
-
* means half the callers conclude the origin has no agent at all.
|
|
59
|
-
*/
|
|
53
|
+
/** An A2A Agent Card, served at `/.well-known/agent-card.json`, the path A2A v0.3 names. */
|
|
60
54
|
export declare function agentCard(config: AgentCardConfig): Record<string, unknown>;
|
|
61
55
|
/** The registry manifest (`server.json`) fields a card projects. */
|
|
62
56
|
export interface ServerManifest {
|
|
@@ -150,7 +144,7 @@ export declare function descriptorHandler(body: unknown, opts?: {
|
|
|
150
144
|
/** The whole `<mcp-url>/server-card` route, projected from server.json. */
|
|
151
145
|
export declare const serverCardHandler: (manifest: ServerManifest) => ((request: Request) => Response);
|
|
152
146
|
export interface WellKnownDocs {
|
|
153
|
-
/**
|
|
147
|
+
/** `agent-card.json`. */
|
|
154
148
|
agentCard?: unknown;
|
|
155
149
|
/** `mcp.json`. */
|
|
156
150
|
mcpManifest?: unknown;
|
package/dist/well-known.js
CHANGED
|
@@ -67,13 +67,7 @@ export function skillsFromTools(tools) {
|
|
|
67
67
|
* origins here were serving one.
|
|
68
68
|
*/
|
|
69
69
|
const A2A_PROTOCOL_VERSION = '0.3.0';
|
|
70
|
-
/**
|
|
71
|
-
* An A2A Agent Card. Serve the same object at both `/.well-known/agent.json`
|
|
72
|
-
* and `/.well-known/agent-card.json`: v0.3 renamed the path and defined no
|
|
73
|
-
* fallback in either direction, so a spec-current client probes only the new
|
|
74
|
-
* one and a client on an older SDK probes only the old one. Serving one path
|
|
75
|
-
* means half the callers conclude the origin has no agent at all.
|
|
76
|
-
*/
|
|
70
|
+
/** An A2A Agent Card, served at `/.well-known/agent-card.json`, the path A2A v0.3 names. */
|
|
77
71
|
export function agentCard(config) {
|
|
78
72
|
const { name, description, url, version, skills, license, licenseScope, organization, documentationUrl, mcpEndpoint, extra } = config;
|
|
79
73
|
const endpoint = mcpEndpoint === null ? url : (mcpEndpoint ?? `${url}/api/mcp`);
|
|
@@ -234,7 +228,6 @@ export function wellKnownHandler(docs) {
|
|
|
234
228
|
: typeof auth === 'function' ? auth
|
|
235
229
|
: registryAuthHandler(auth?.publicKey ?? process.env.MCP_REGISTRY_PUBLIC_KEY, auth?.keyType ?? process.env.MCP_REGISTRY_KEY_TYPE);
|
|
236
230
|
const served = {
|
|
237
|
-
'agent.json': docs.agentCard === undefined ? undefined : descriptorHandler(docs.agentCard),
|
|
238
231
|
'agent-card.json': docs.agentCard === undefined ? undefined : descriptorHandler(docs.agentCard),
|
|
239
232
|
'mcp.json': docs.mcpManifest === undefined ? undefined : descriptorHandler(docs.mcpManifest),
|
|
240
233
|
'openapi.json': docs.openapi === undefined ? undefined : descriptorHandler(docs.openapi),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.111.0",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -258,6 +258,11 @@
|
|
|
258
258
|
"import": "./dist/og.js",
|
|
259
259
|
"default": "./dist/og.js"
|
|
260
260
|
},
|
|
261
|
+
"./openapi": {
|
|
262
|
+
"types": "./dist/openapi.d.ts",
|
|
263
|
+
"import": "./dist/openapi.js",
|
|
264
|
+
"default": "./dist/openapi.js"
|
|
265
|
+
},
|
|
261
266
|
"./sanitize": {
|
|
262
267
|
"types": "./dist/sanitize.d.ts",
|
|
263
268
|
"import": "./dist/sanitize.js",
|