wiki-formant 0.107.0 → 0.112.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 +22 -6
- package/bin/check-pages.mjs +11 -4
- package/bin/indexnow.mjs +4 -3
- package/dist/analytics.d.ts +9 -3
- package/dist/analytics.js +20 -11
- 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.d.ts +4 -3
- package/dist/stats.js +6 -5
- package/dist/well-known.d.ts +2 -8
- package/dist/well-known.js +1 -8
- package/dist/wiki-tools.d.ts +6 -0
- package/dist/wiki-tools.js +3 -1
- package/package.json +6 -1
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.d.ts
CHANGED
|
@@ -26,9 +26,10 @@ export declare function statsQuery(params: Record<string, string | string[] | un
|
|
|
26
26
|
/**
|
|
27
27
|
* A site's own analytics on one page: the headline figures against the window
|
|
28
28
|
* before, people and agents apart, whichever of them is chosen per day, and every ranked list the
|
|
29
|
-
* digest carries, each row a link that narrows the page to it
|
|
30
|
-
* nothing in them are left out,
|
|
31
|
-
* which
|
|
29
|
+
* digest carries, each row a link that narrows the page to it, and what agents
|
|
30
|
+
* reported through `send_feedback`. Lists with nothing in them are left out,
|
|
31
|
+
* but for the searches and the feedback, which say they found none, so a quiet
|
|
32
|
+
* window never reads as one not counted. The chart and the bars are `currentColor`
|
|
32
33
|
* until the importing stylesheet gives them a colour.
|
|
33
34
|
*/
|
|
34
35
|
export declare function Stats({ digest: d, days, filter, ranges, link: Link }: StatsProps): import("react").JSX.Element;
|
package/dist/stats.js
CHANGED
|
@@ -52,9 +52,10 @@ function StatsList({ title, rows, label = v => v, to, empty, link: Link, }) {
|
|
|
52
52
|
/**
|
|
53
53
|
* A site's own analytics on one page: the headline figures against the window
|
|
54
54
|
* before, people and agents apart, whichever of them is chosen per day, and every ranked list the
|
|
55
|
-
* digest carries, each row a link that narrows the page to it
|
|
56
|
-
* nothing in them are left out,
|
|
57
|
-
* which
|
|
55
|
+
* digest carries, each row a link that narrows the page to it, and what agents
|
|
56
|
+
* reported through `send_feedback`. Lists with nothing in them are left out,
|
|
57
|
+
* but for the searches and the feedback, which say they found none, so a quiet
|
|
58
|
+
* window never reads as one not counted. The chart and the bars are `currentColor`
|
|
58
59
|
* until the importing stylesheet gives them a colour.
|
|
59
60
|
*/
|
|
60
61
|
export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES, link: Link = Anchor }) {
|
|
@@ -73,7 +74,7 @@ export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES, lin
|
|
|
73
74
|
const to = (k) => (v) => at({ ...filter, [k]: v });
|
|
74
75
|
// Each window is another digest, so a link rather than a button, pinned with the chart's own controls.
|
|
75
76
|
const controls = (_jsx("div", { role: "group", "aria-label": "Window", children: ranges.map(n => (_jsx(Link, { href: at(filter, n), "aria-current": n === days ? 'page' : undefined, prefetch: false, children: windowLabel(n) }, n))) }));
|
|
76
|
-
return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs(Link, { href: at({ ...filter, [k]: undefined }), title: "Remove", prefetch: false, children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, controls: controls }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { link: Link, title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { link: Link, title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { link: Link, title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => at({ ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { link: Link, title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { link: Link, title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { link: Link, title: "Agents", rows: rows(d.top_agents, 'agent', 'visitors') }), _jsx(StatsList, { link: Link, title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { link: Link, title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events'), empty: "None in this window." })] })] }));
|
|
77
|
+
return (_jsxs("div", { className: "stats", children: [narrowed.length > 0 && (_jsxs("div", { className: "stats-filters", children: [_jsx("ul", { "aria-label": "Narrowed to", children: narrowed.map(k => (_jsx("li", { children: _jsxs(Link, { href: at({ ...filter, [k]: undefined }), title: "Remove", prefetch: false, children: [NAMES[k], ": ", k === 'country' ? country(filter[k]) : filter[k], " \u00D7"] }) }, k))) }), start < counted && d.counted_from && (_jsxs("p", { children: ["Narrowed figures start on ", d.counted_from, ", the first day this site counted its own visits."] }))] })), _jsx(StatsFigures, { digest: d, first: first, controls: controls }), _jsxs("div", { className: "stats-lists", children: [_jsx(StatsList, { link: Link, title: "Pages", rows: rows(d.top_pages, 'page', 'visitors'), to: to('page') }), _jsx(StatsList, { link: Link, title: "Entry pages", rows: rows(d.entry_pages, 'page', 'visitors'), to: to('entry') }), _jsx(StatsList, { link: Link, title: "Sources", rows: rows(d.top_sources, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "From X posts", rows: rows(d.from_posts, 'page', 'visitors'), to: v => at({ ...filter, source: 'x', entry: v }) }), _jsx(StatsList, { link: Link, title: "Sibling sites", rows: rows(d.sibling_referrals, 'source', 'visitors'), to: to('source') }), _jsx(StatsList, { link: Link, title: "Countries", rows: rows(d.countries, 'country', 'visitors'), label: country, to: to('country') }), _jsx(StatsList, { link: Link, title: "Devices", rows: rows(d.devices, 'device', 'visitors'), to: to('device') }), _jsx(StatsList, { link: Link, title: "Agents", rows: rows(d.top_agents, 'agent', 'visitors') }), _jsx(StatsList, { link: Link, title: "Agent tool calls", rows: rows(d.agent_tool_calls, 'tool', 'events') }), _jsx(StatsList, { link: Link, title: "Searches", rows: rows(d.searches, 'query', 'events'), empty: "None in this window." }), _jsx(StatsList, { link: Link, title: "Agent searches", rows: rows(d.agent_searches, 'query', 'events'), empty: "None in this window." }), _jsx(StatsList, { link: Link, title: "Searches that found nothing", rows: rows(d.gaps, 'query', 'events'), empty: "None in this window." }), _jsxs("section", { className: "stats-list", children: [_jsx("h2", { children: "Agent feedback" }), d.agent_feedback.length ? (_jsx("ol", { children: d.agent_feedback.map((f, i) => (_jsxs("li", { children: [_jsxs("span", { children: [f.kind, ": ", f.message, f.page && ` (${f.page})`, " \u00B7 ", f.agent] }), _jsx("span", { className: "stats-count", children: f.date })] }, i))) })) : (_jsx("p", { children: "None in this window." }))] })] })] }));
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
80
|
* A stats page's door: `children` and a sign-out button for a signed-in
|
|
@@ -83,7 +84,7 @@ export function Stats({ digest: d, days, filter = {}, ranges = STATS_RANGES, lin
|
|
|
83
84
|
*/
|
|
84
85
|
export async function StatsGate({ gate, session, invite, host, endpoint = '/api/passkey', children, classNames: c = {} }) {
|
|
85
86
|
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 }) })] }));
|
|
87
|
+
return (_jsxs(_Fragment, { children: [await children(), _jsx("div", { className: c.actions, children: _jsx(PasskeySignOut, { endpoint: endpoint, className: c.button, errorClassName: c.error }) })] }));
|
|
87
88
|
}
|
|
88
89
|
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
90
|
}
|
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/dist/wiki-tools.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ export interface WikiToolsOptions<B extends DiffBlock> {
|
|
|
40
40
|
corpus?: (tagPath?: string) => Promise<CorpusSection[]>;
|
|
41
41
|
/** What an empty search says. */
|
|
42
42
|
noMatch?: (query: string, tagPath?: string) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Each `search_wiki` query, on its first page, with how many pages it found:
|
|
45
|
+
* `(request, q, n) => trackSearch(request.headers, q, n, MCP_SURFACE)`, so the
|
|
46
|
+
* stats page lists what agents searched for beside what people did.
|
|
47
|
+
*/
|
|
48
|
+
onSearch?: (request: Request, query: string, results: number) => void;
|
|
43
49
|
/** A2A skills by tool, beside the defaults. */
|
|
44
50
|
skills?: Partial<Record<ToolName, McpTool['skill']>>;
|
|
45
51
|
}
|
package/dist/wiki-tools.js
CHANGED
|
@@ -129,7 +129,7 @@ export function wikiReadTools(o) {
|
|
|
129
129
|
},
|
|
130
130
|
required: ['query'],
|
|
131
131
|
},
|
|
132
|
-
handler: async (args) => {
|
|
132
|
+
handler: async (args, ctx) => {
|
|
133
133
|
const r = readArgs(args);
|
|
134
134
|
const query = r.str('query');
|
|
135
135
|
const tagPath = r.str('tagPath') || undefined;
|
|
@@ -138,6 +138,8 @@ export function wikiReadTools(o) {
|
|
|
138
138
|
if (!query)
|
|
139
139
|
throw new McpToolError('`query` is empty. Pass a keyword to search for, or call list_pages to enumerate a branch instead.');
|
|
140
140
|
const { items, total, corrected } = await searchWiki(o, query, { tagPath, page, size });
|
|
141
|
+
if (page === 1 && ctx.request)
|
|
142
|
+
o.onSearch?.(ctx.request, query, total);
|
|
141
143
|
return withAdjustments(r, {
|
|
142
144
|
query,
|
|
143
145
|
...(corrected ? { corrected } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.112.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",
|