wiki-formant 0.97.1 → 0.98.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 +409 -824
- package/dist/base.css +1 -0
- package/dist/category.d.ts +3 -2
- package/dist/category.js +7 -8
- package/dist/conformance.d.ts +63 -0
- package/dist/conformance.js +74 -0
- package/dist/editor.js +2 -2
- package/dist/headers.d.ts +13 -2
- package/dist/headers.js +13 -2
- package/dist/react.d.ts +11 -0
- package/dist/react.js +9 -0
- package/dist/rola.d.ts +39 -0
- package/dist/rola.js +81 -0
- package/dist/taxonomy.d.ts +2 -3
- package/dist/wiki-tools.d.ts +2 -1
- package/package.json +1 -11
package/dist/base.css
CHANGED
|
@@ -276,6 +276,7 @@ tbody > tr:nth-child(even) > :is(th, td):not(.chart *) { background-image: linea
|
|
|
276
276
|
.see-also-detail { font-size: var(--ui-small); color: var(--ui-muted); }
|
|
277
277
|
|
|
278
278
|
.link-preview-card { position: fixed; z-index: 50; width: 20rem; max-width: calc(100vw - 1rem); overflow: hidden; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); box-shadow: var(--ui-shadow, 0 8px 24px -4px rgb(0 0 0 / 0.25)); }
|
|
279
|
+
.link-preview-banner { display: block; width: 100%; height: 7rem; object-fit: cover; }
|
|
279
280
|
.link-preview-body { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.75rem; }
|
|
280
281
|
.link-preview-title { font-weight: 600; }
|
|
281
282
|
.link-preview-excerpt { display: -webkit-box; margin: 0; overflow: hidden; font-size: var(--ui-small); color: var(--ui-muted); -webkit-line-clamp: 4; -webkit-box-orient: vertical; }
|
package/dist/category.d.ts
CHANGED
|
@@ -33,8 +33,9 @@ export type CategoryQuery = Record<string, string | string[] | undefined>;
|
|
|
33
33
|
/**
|
|
34
34
|
* A category's pages narrowed by its facets and an A–Z letter, sorted A–Z or
|
|
35
35
|
* newest first (by a `date` key where the category declares one), and a card
|
|
36
|
-
* per visible child.
|
|
37
|
-
*
|
|
36
|
+
* per visible child. The order is the category's own until the query asks for
|
|
37
|
+
* the other, whatever the set's size: a blog of thirty posts is still newest
|
|
38
|
+
* first, though it is too short for an A–Z index.
|
|
38
39
|
*/
|
|
39
40
|
export declare function categoryView<P extends FacetablePage & {
|
|
40
41
|
tagPath: string;
|
package/dist/category.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// taxonomy and the store.
|
|
3
3
|
//
|
|
4
4
|
// radix-wiki assembled this inside its route and miow copied it line for line:
|
|
5
|
-
// the count, a sort
|
|
5
|
+
// the count, a sort toggle for any set with two pages to order, the facets
|
|
6
6
|
// its `select` keys give, a letter that exists, and a card per section listing
|
|
7
7
|
// what it holds. acuiq2 kept a subset that had drifted (no sort, no cards). The
|
|
8
8
|
// view is the app's; the arithmetic is here.
|
|
@@ -11,21 +11,20 @@ const one = (v) => (Array.isArray(v) ? v[0] : v);
|
|
|
11
11
|
/**
|
|
12
12
|
* A category's pages narrowed by its facets and an A–Z letter, sorted A–Z or
|
|
13
13
|
* newest first (by a `date` key where the category declares one), and a card
|
|
14
|
-
* per visible child.
|
|
15
|
-
*
|
|
14
|
+
* per visible child. The order is the category's own until the query asks for
|
|
15
|
+
* the other, whatever the set's size: a blog of thirty posts is still newest
|
|
16
|
+
* first, though it is too short for an A–Z index.
|
|
16
17
|
*/
|
|
17
18
|
export async function categoryView({ tree, taxonomy, pages: reader }, tagPath, query, { href = wikiPath, cardPages = 6, limit = 2000 } = {}) {
|
|
18
19
|
const resolved = tree.resolve(tagPath);
|
|
19
20
|
const order = (path, sort) => sort === 'recent' && tree.metadataKeys(path).some(k => k.key === 'date' && k.type === 'date') ? 'date' : sort;
|
|
20
21
|
const fallback = (path) => (tree.resolve(path).sort === 'title' ? 'title' : 'recent');
|
|
21
22
|
const asked = one(query.sort);
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const sort = taxonomy.needsAlphaIndex(fetched.length) ? requested : 'title';
|
|
25
|
-
const all = sort === requested ? fetched : await reader.list(tagPath, { sort: order(tagPath, sort), limit });
|
|
23
|
+
const sort = asked === 'title' || asked === 'recent' ? asked : fallback(tagPath);
|
|
24
|
+
const all = await reader.list(tagPath, { sort: order(tagPath, sort), limit });
|
|
26
25
|
const filters = taxonomy.facetFilters(tagPath, query);
|
|
27
26
|
const letter = taxonomy.resolveLetter(all, filters, one(query.letter)?.toUpperCase());
|
|
28
|
-
const sortable =
|
|
27
|
+
const sortable = all.length > 1;
|
|
29
28
|
// A link carries the sort only when it is not the category's own, so the bare
|
|
30
29
|
// category and its default are one address, not two.
|
|
31
30
|
const state = { sort: sortable && sort !== fallback(tagPath) ? sort : undefined, filters, letter };
|
package/dist/conformance.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface TesterOptions {
|
|
|
66
66
|
export interface Tester {
|
|
67
67
|
base: string;
|
|
68
68
|
endpoint: string;
|
|
69
|
+
clientName: string;
|
|
69
70
|
/** One JSON-RPC call. Waits out a 429 as `Retry-After` asks, up to four tries. */
|
|
70
71
|
rpc(method: string, params?: unknown): Promise<Rpc>;
|
|
71
72
|
/** `tools/call` shorthand. */
|
|
@@ -266,3 +267,65 @@ export declare function rolaOriginCheck(t: Tester, opts?: {
|
|
|
266
267
|
endpoint?: string;
|
|
267
268
|
tool?: string;
|
|
268
269
|
}): Promise<void>;
|
|
270
|
+
export interface StandardSuiteConfig {
|
|
271
|
+
/** `server.json`'s version, which `initialize` and every descriptor must state. */
|
|
272
|
+
version: string;
|
|
273
|
+
/** The capability keys `initialize` must declare. Default `['tools']`. */
|
|
274
|
+
capabilities?: readonly string[];
|
|
275
|
+
/** Every path the OpenAPI document is served at. */
|
|
276
|
+
openapi?: readonly string[];
|
|
277
|
+
/** The tools that write, which must not be marked read-only. */
|
|
278
|
+
writes?: readonly string[];
|
|
279
|
+
/** `manifestParity`'s options. */
|
|
280
|
+
manifest?: Parameters<typeof manifestParity>[1];
|
|
281
|
+
/** The calls `payloadBudget` weighs beyond the no-argument ones it finds itself; `[]` for those alone. None skips it. */
|
|
282
|
+
budget?: Parameters<typeof payloadBudget>[1];
|
|
283
|
+
/** Surfaces an agent recrawls, for `conditionalGetChecks`. None skips it. */
|
|
284
|
+
conditionalGet?: readonly string[];
|
|
285
|
+
/** JSON descriptors, for `descriptorChecks`. Default `S10_DESCRIPTORS`. */
|
|
286
|
+
descriptors?: readonly string[];
|
|
287
|
+
/** Paths the `User-agent: *` group must reach, for `robotsChecks`. None skips it. */
|
|
288
|
+
robots?: readonly string[];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The suites every surface's `scripts/_mcp-test.ts` runs, in the order they
|
|
292
|
+
* run them: transport, version coherence, the agent card, annotations, the
|
|
293
|
+
* manifest, payload budgets, conditional GETs, descriptors and robots. Returns
|
|
294
|
+
* the `initialize` response for the surface's own assertions.
|
|
295
|
+
*/
|
|
296
|
+
export declare function standardSuite(t: Tester, cfg: StandardSuiteConfig): Promise<Rpc>;
|
|
297
|
+
/** A row as `search_wiki` and `list_pages` return it. */
|
|
298
|
+
export interface WikiReadRow {
|
|
299
|
+
path: string;
|
|
300
|
+
tagPath?: string;
|
|
301
|
+
slug?: string;
|
|
302
|
+
id?: string;
|
|
303
|
+
url?: string;
|
|
304
|
+
}
|
|
305
|
+
export interface WikiReadCheckOptions {
|
|
306
|
+
/** A query with hits on this wiki. */
|
|
307
|
+
query: string;
|
|
308
|
+
/** `get_page`'s arguments for a row. Default `{ path }`. */
|
|
309
|
+
address?: (row: WikiReadRow) => Record<string, unknown>;
|
|
310
|
+
/** A row's markdown twin, for `twinParity`. None skips it. */
|
|
311
|
+
twin?: (row: WikiReadRow) => string;
|
|
312
|
+
/** The `maxChars` the corpus slice is asked for, at least `CORPUS_BUDGET.min`. Default 20,000. */
|
|
313
|
+
corpusChars?: number;
|
|
314
|
+
endpoint?: string;
|
|
315
|
+
label?: string;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The `wiki-formant/wiki-tools` read tools answer as an agent expects: the
|
|
319
|
+
* categories, a search and the page it finds, a listing, recent changes, a
|
|
320
|
+
* corpus preflight and a page-aligned slice that resumes, and the teaching
|
|
321
|
+
* answers to no hits, a missing argument, an unknown tool and a missing page.
|
|
322
|
+
* Returns the search rows and the page read, for the surface's own checks.
|
|
323
|
+
*/
|
|
324
|
+
export declare function wikiReadChecks(t: Tester, opts: WikiReadCheckOptions): Promise<{
|
|
325
|
+
rows: WikiReadRow[];
|
|
326
|
+
first: WikiReadRow | undefined;
|
|
327
|
+
page: {
|
|
328
|
+
title?: string;
|
|
329
|
+
content?: string;
|
|
330
|
+
} | undefined;
|
|
331
|
+
}>;
|
package/dist/conformance.js
CHANGED
|
@@ -65,6 +65,7 @@ export function createTester(opts) {
|
|
|
65
65
|
return {
|
|
66
66
|
base,
|
|
67
67
|
endpoint,
|
|
68
|
+
clientName: opts.clientName,
|
|
68
69
|
rpc,
|
|
69
70
|
call: (name, args = {}) => rpc('tools/call', { name, arguments: args }),
|
|
70
71
|
rpcAt,
|
|
@@ -625,3 +626,76 @@ export async function rolaOriginCheck(t, opts = {}) {
|
|
|
625
626
|
const message = answer?.sign?.message ?? '';
|
|
626
627
|
t.check('ROLA recipe names this origin', message.includes(`"${origin}"`), `expects "${origin}" in: …${message.slice(-90)}`);
|
|
627
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* The suites every surface's `scripts/_mcp-test.ts` runs, in the order they
|
|
631
|
+
* run them: transport, version coherence, the agent card, annotations, the
|
|
632
|
+
* manifest, payload budgets, conditional GETs, descriptors and robots. Returns
|
|
633
|
+
* the `initialize` response for the surface's own assertions.
|
|
634
|
+
*/
|
|
635
|
+
export async function standardSuite(t, cfg) {
|
|
636
|
+
const init = await transportChecks(t, t.clientName, cfg.capabilities);
|
|
637
|
+
await versionCoherence(t, cfg.version, standardVersionSources(t, cfg.openapi), init.result?.serverInfo?.version);
|
|
638
|
+
await agentCardParity(t);
|
|
639
|
+
await annotationChecks(t, { writes: cfg.writes });
|
|
640
|
+
await manifestParity(t, cfg.manifest);
|
|
641
|
+
if (cfg.budget)
|
|
642
|
+
await payloadBudget(t, cfg.budget);
|
|
643
|
+
if (cfg.conditionalGet)
|
|
644
|
+
await conditionalGetChecks(t, cfg.conditionalGet);
|
|
645
|
+
await descriptorChecks(t, cfg.descriptors);
|
|
646
|
+
if (cfg.robots)
|
|
647
|
+
await robotsChecks(t, cfg.robots);
|
|
648
|
+
return init;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* The `wiki-formant/wiki-tools` read tools answer as an agent expects: the
|
|
652
|
+
* categories, a search and the page it finds, a listing, recent changes, a
|
|
653
|
+
* corpus preflight and a page-aligned slice that resumes, and the teaching
|
|
654
|
+
* answers to no hits, a missing argument, an unknown tool and a missing page.
|
|
655
|
+
* Returns the search rows and the page read, for the surface's own checks.
|
|
656
|
+
*/
|
|
657
|
+
export async function wikiReadChecks(t, opts) {
|
|
658
|
+
const endpoint = opts.endpoint ?? t.endpoint;
|
|
659
|
+
const label = (s) => (opts.label ? `[${opts.label}] ${s}` : s);
|
|
660
|
+
const address = opts.address ?? (row => ({ path: row.path }));
|
|
661
|
+
const call = (name, args) => t.callAt(endpoint, name, args);
|
|
662
|
+
const read = async (name, args) => t.payload(await call(name, args));
|
|
663
|
+
const text = (r) => r.result?.content?.[0]?.text ?? '';
|
|
664
|
+
const cats = await read('get_categories');
|
|
665
|
+
t.check(label('get_categories'), (cats?.categories?.length ?? 0) > 0, `${cats?.categories?.length ?? 0} categories`);
|
|
666
|
+
const search = await read('search_wiki', { query: opts.query, pageSize: 5 });
|
|
667
|
+
const rows = search?.pages ?? [];
|
|
668
|
+
const first = rows[0];
|
|
669
|
+
t.check(label('search_wiki'), (search?.total ?? 0) > 0 && !!first?.path, `${search?.total ?? 0} hits for "${opts.query}", first="${first?.path}"`);
|
|
670
|
+
const page = first ? await read('get_page', address(first)) : undefined;
|
|
671
|
+
t.check(label('get_page'), !!page?.title, `"${page?.title}" ${page?.content?.length ?? 0} chars`);
|
|
672
|
+
if (first && opts.twin)
|
|
673
|
+
await twinParity(t, label('get_page is the .md twin'), opts.twin(first), page?.content ?? '');
|
|
674
|
+
const listed = await read('list_pages', { pageSize: 5 });
|
|
675
|
+
t.check(label('list_pages'), (listed?.pages?.length ?? 0) > 0, `${listed?.pages?.length ?? 0} of ${listed?.total ?? 0}`);
|
|
676
|
+
const recent = await read('get_recent_changes', { days: 30, limit: 5 });
|
|
677
|
+
t.check(label('get_recent_changes'), Array.isArray(recent?.pages), `${recent?.pages?.length ?? 0} pages changed in 30d`);
|
|
678
|
+
// A corpus under the budget comes back whole; one over it, cut on a page
|
|
679
|
+
// boundary with somewhere to resume from. The 1,000 is the heading and the
|
|
680
|
+
// separators the document carries beside its pages.
|
|
681
|
+
const maxChars = opts.corpusChars ?? 20_000;
|
|
682
|
+
const size = await read('get_full_corpus', { sizeOnly: true });
|
|
683
|
+
const characters = size?.characters ?? 0;
|
|
684
|
+
t.check(label('get_full_corpus(sizeOnly)'), characters > 0 && (size?.totalPages ?? 0) > 0, `${size?.totalPages} pages, ${characters.toLocaleString()} chars`);
|
|
685
|
+
const slice = await read('get_full_corpus', { maxChars });
|
|
686
|
+
const over = characters > maxChars;
|
|
687
|
+
t.check(label('get_full_corpus(bounded)'), (slice?.document?.length ?? Infinity) <= maxChars + 1_000 && slice?.truncated === over, `${(slice?.document?.length ?? 0).toLocaleString()} chars for maxChars=${maxChars.toLocaleString()}, ${slice?.includedPages} pages, truncated=${slice?.truncated}, nextSkip=${slice?.nextSkip}`);
|
|
688
|
+
if (typeof slice?.nextSkip === 'number') {
|
|
689
|
+
const resumed = await read('get_full_corpus', { maxChars, skip: slice.nextSkip });
|
|
690
|
+
t.check(label('get_full_corpus(resumes)'), resumed?.skip === slice.nextSkip && resumed?.document !== slice.document, `skip=${resumed?.skip} returns a different slice`);
|
|
691
|
+
}
|
|
692
|
+
const none = await read('search_wiki', { query: 'zzzqqqnotathing' });
|
|
693
|
+
t.check(label('no hits teach'), none?.total === 0 && !!none?.note, none?.note?.slice(0, 80) ?? 'no note on an empty result');
|
|
694
|
+
const missing = await call('search_wiki', {});
|
|
695
|
+
t.check(label('missing argument'), !!missing.result?.isError && text(missing).includes('"query"') && text(missing).includes('Expected schema'), text(missing).split('\n')[0] ?? '');
|
|
696
|
+
const unknown = await call('no_such_tool', {});
|
|
697
|
+
t.check(label('unknown tool'), unknown.error?.code === -32602 && (unknown.error.message ?? '').includes('search_wiki'), `${unknown.error?.code}: ${(unknown.error?.message ?? '').slice(0, 80)}`);
|
|
698
|
+
const miss = await call('get_page', address({ path: 'definitely/not/a-page', tagPath: 'definitely/not', slug: 'a-page' }));
|
|
699
|
+
t.check(label('get_page(404) teaches'), !!miss.result?.isError && text(miss).includes('didYouMean'), text(miss).slice(0, 80));
|
|
700
|
+
return { rows, first, page };
|
|
701
|
+
}
|
package/dist/editor.js
CHANGED
|
@@ -72,8 +72,8 @@ function cleanPastedHtml(html) {
|
|
|
72
72
|
* stored HTML, so a page edited once kept them for good. Nofollow on a site's
|
|
73
73
|
* own links throws away its link equity, and a new tab for in-site navigation
|
|
74
74
|
* is a bug. Whether a link is external depends on the host it is served from,
|
|
75
|
-
* which the editor cannot know, so the render path decides: `
|
|
76
|
-
*
|
|
75
|
+
* which the editor cannot know, so the render path decides: `articleHtml`'s
|
|
76
|
+
* `links` pass in `wiki-formant/headings`. Dropping both attributes from the mark also cleans
|
|
77
77
|
* a legacy link the next time its page is saved.
|
|
78
78
|
*
|
|
79
79
|
* @internal
|
package/dist/headers.d.ts
CHANGED
|
@@ -39,7 +39,14 @@ interface WikiNextConfig {
|
|
|
39
39
|
experimental?: object;
|
|
40
40
|
images?: object;
|
|
41
41
|
headers?: () => Promise<unknown[]> | unknown[];
|
|
42
|
+
rewrites?: () => Promise<WikiRewrites> | WikiRewrites;
|
|
42
43
|
}
|
|
44
|
+
/** A Next `rewrites()` answer: a bare list (run after the file system) or the three phases. */
|
|
45
|
+
type WikiRewrites = unknown[] | {
|
|
46
|
+
beforeFiles?: unknown[];
|
|
47
|
+
afterFiles?: unknown[];
|
|
48
|
+
fallback?: unknown[];
|
|
49
|
+
};
|
|
43
50
|
/**
|
|
44
51
|
* An app's Next config with the settings every app here wrote out the same way:
|
|
45
52
|
*
|
|
@@ -53,9 +60,13 @@ interface WikiNextConfig {
|
|
|
53
60
|
* - `securityHeaders` on every route, with `csp` enforced in production only:
|
|
54
61
|
* Next's dev runtime is inline and `eval`. An origin that sets its policy
|
|
55
62
|
* per request (a nonce) passes none.
|
|
63
|
+
* - `/openapi.json` rewritten to `/.well-known/openapi.json` ahead of the file
|
|
64
|
+
* system, so the spec answers at both paths S10 publishes it at.
|
|
56
65
|
*
|
|
57
|
-
* Anything the app sets wins, and its own `headers()` follow these
|
|
58
|
-
* `
|
|
66
|
+
* Anything the app sets wins, and its own `headers()` follow these; its
|
|
67
|
+
* `rewrites()`, in either form, keep their phases with this one first in
|
|
68
|
+
* `beforeFiles`. Pass `NextConfig` as `T` so the object is typed as Next's own
|
|
69
|
+
* config.
|
|
59
70
|
*/
|
|
60
71
|
export declare function withWikiDefaults<T extends WikiNextConfig>(config: T, { csp }?: {
|
|
61
72
|
csp?: string;
|
package/dist/headers.js
CHANGED
|
@@ -88,6 +88,8 @@ export function securityHeaders(csp) {
|
|
|
88
88
|
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
|
|
89
89
|
];
|
|
90
90
|
}
|
|
91
|
+
/** The OpenAPI document at the bare path an agent guesses beside `/.well-known/openapi.json`. */
|
|
92
|
+
const OPENAPI_REWRITE = { source: '/openapi.json', destination: '/.well-known/openapi.json' };
|
|
91
93
|
/**
|
|
92
94
|
* An app's Next config with the settings every app here wrote out the same way:
|
|
93
95
|
*
|
|
@@ -101,9 +103,13 @@ export function securityHeaders(csp) {
|
|
|
101
103
|
* - `securityHeaders` on every route, with `csp` enforced in production only:
|
|
102
104
|
* Next's dev runtime is inline and `eval`. An origin that sets its policy
|
|
103
105
|
* per request (a nonce) passes none.
|
|
106
|
+
* - `/openapi.json` rewritten to `/.well-known/openapi.json` ahead of the file
|
|
107
|
+
* system, so the spec answers at both paths S10 publishes it at.
|
|
104
108
|
*
|
|
105
|
-
* Anything the app sets wins, and its own `headers()` follow these
|
|
106
|
-
* `
|
|
109
|
+
* Anything the app sets wins, and its own `headers()` follow these; its
|
|
110
|
+
* `rewrites()`, in either form, keep their phases with this one first in
|
|
111
|
+
* `beforeFiles`. Pass `NextConfig` as `T` so the object is typed as Next's own
|
|
112
|
+
* config.
|
|
107
113
|
*/
|
|
108
114
|
export function withWikiDefaults(config, { csp } = {}) {
|
|
109
115
|
return {
|
|
@@ -117,5 +123,10 @@ export function withWikiDefaults(config, { csp } = {}) {
|
|
|
117
123
|
...((await config.headers?.()) ?? []),
|
|
118
124
|
];
|
|
119
125
|
},
|
|
126
|
+
async rewrites() {
|
|
127
|
+
const own = (await config.rewrites?.()) ?? {};
|
|
128
|
+
const phases = Array.isArray(own) ? { afterFiles: own } : own;
|
|
129
|
+
return { ...phases, beforeFiles: [OPENAPI_REWRITE, ...(phases.beforeFiles ?? [])] };
|
|
130
|
+
},
|
|
120
131
|
};
|
|
121
132
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -216,6 +216,17 @@ export interface LinkPreviewState<T> {
|
|
|
216
216
|
* three things, and those three are the options above.
|
|
217
217
|
*/
|
|
218
218
|
export declare function useLinkPreview<T>({ eligible, fetch, cardClassName, cardWidth, cardHeight, showDelay, hideDelay, margin, }: LinkPreviewOptions<T>): LinkPreviewState<T>;
|
|
219
|
+
/**
|
|
220
|
+
* The card `useLinkPreview` positions: an optional banner, the title and the
|
|
221
|
+
* excerpt, in base.css's `link-preview-*` classes. Spread `cardProps` onto it;
|
|
222
|
+
* a caller that has to move the card (out of a query container) passes its own
|
|
223
|
+
* `style`.
|
|
224
|
+
*/
|
|
225
|
+
export declare function LinkPreviewCard({ title, excerpt, bannerImage, ...card }: LinkPreviewState<unknown>['cardProps'] & {
|
|
226
|
+
title: string;
|
|
227
|
+
excerpt?: string | null;
|
|
228
|
+
bannerImage?: string | null;
|
|
229
|
+
}): import("react").JSX.Element;
|
|
219
230
|
export type SortDirection = 'asc' | 'desc';
|
|
220
231
|
export interface TableSortState<T, K extends string> {
|
|
221
232
|
sorted: T[];
|
package/dist/react.js
CHANGED
|
@@ -520,6 +520,15 @@ export function useLinkPreview({ eligible, fetch, cardClassName = 'link-preview-
|
|
|
520
520
|
},
|
|
521
521
|
};
|
|
522
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* The card `useLinkPreview` positions: an optional banner, the title and the
|
|
525
|
+
* excerpt, in base.css's `link-preview-*` classes. Spread `cardProps` onto it;
|
|
526
|
+
* a caller that has to move the card (out of a query container) passes its own
|
|
527
|
+
* `style`.
|
|
528
|
+
*/
|
|
529
|
+
export function LinkPreviewCard({ title, excerpt, bannerImage, ...card }) {
|
|
530
|
+
return (_jsxs("div", { className: "link-preview-card", ...card, children: [bannerImage && _jsx("img", { src: bannerImage, alt: "", className: "link-preview-banner", loading: "lazy" }), _jsxs("div", { className: "link-preview-body", children: [_jsx("div", { className: "link-preview-title", children: title }), excerpt && _jsx("p", { className: "link-preview-excerpt", children: excerpt })] })] }));
|
|
531
|
+
}
|
|
523
532
|
/**
|
|
524
533
|
* One active column, direction toggling on re-click, every table the same.
|
|
525
534
|
*
|
package/dist/rola.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type JWTPayload } from 'jose';
|
|
2
|
+
import { type McpTool } from './mcp.js';
|
|
2
3
|
export interface SignedChallenge {
|
|
3
4
|
challenge: string;
|
|
4
5
|
address: string;
|
|
@@ -72,3 +73,41 @@ export declare function verifySession<T extends object = JWTPayload>(token: stri
|
|
|
72
73
|
export declare function bearerToken(headers: {
|
|
73
74
|
get(name: string): string | null;
|
|
74
75
|
}): string | null;
|
|
76
|
+
export interface RolaLoginToolsOptions {
|
|
77
|
+
/** What a token writes to, as in "Step 1 of writing to the wiki". */
|
|
78
|
+
site: string;
|
|
79
|
+
dAppDefinitionAddress: string;
|
|
80
|
+
/** The origin the verifier checks, which the signed message names. */
|
|
81
|
+
origin: string;
|
|
82
|
+
generateChallenge: () => Promise<{
|
|
83
|
+
challenge: string;
|
|
84
|
+
expiresAt: Date | string;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Verify the proof and issue a token. Throw an `McpToolError` to refuse; the
|
|
88
|
+
* answer's other fields (the signed-in address) are passed through.
|
|
89
|
+
*/
|
|
90
|
+
login: (signed: SignedChallenge) => Promise<{
|
|
91
|
+
token: string;
|
|
92
|
+
expiresAt: Date | string;
|
|
93
|
+
} & Record<string, unknown>>;
|
|
94
|
+
/** Default 300, as `createRola`'s. */
|
|
95
|
+
challengeTtlSec?: number;
|
|
96
|
+
tokenTtlSec: number;
|
|
97
|
+
/** The calls the token is sent on, as in "create_page / edit_page". Default "tool". */
|
|
98
|
+
writeTools?: string;
|
|
99
|
+
notes?: {
|
|
100
|
+
/** A sentence added to `get_challenge`'s description. */
|
|
101
|
+
challenge?: string;
|
|
102
|
+
/** Lines added to, or replacing lines of, the signing recipe the challenge carries. */
|
|
103
|
+
sign?: Record<string, string>;
|
|
104
|
+
/** `login`'s `address` argument description. */
|
|
105
|
+
address?: string;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `get_challenge` and `login`, the two MCP tools that turn an agent's own key
|
|
110
|
+
* into a Bearer token: the challenge carries the exact message to sign, and a
|
|
111
|
+
* refused login says to start again, since a challenge is spent either way.
|
|
112
|
+
*/
|
|
113
|
+
export declare function rolaLoginTools(o: RolaLoginToolsOptions): Array<McpTool & Required<Pick<McpTool, 'title' | 'annotations'>>>;
|
package/dist/rola.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// `@radixdlt/rola` are optional peers, needed only by an app importing this.
|
|
18
18
|
import { SignJWT, jwtVerify } from 'jose';
|
|
19
19
|
import { Rola } from '@radixdlt/rola';
|
|
20
|
+
import { McpToolError } from './mcp.js';
|
|
20
21
|
const hex = (bytes) => Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
21
22
|
export function createRola({ challenges, challengeTtlSec = 300, ...identity }) {
|
|
22
23
|
const rola = Rola(identity);
|
|
@@ -101,3 +102,83 @@ export async function verifySession(token, secret) {
|
|
|
101
102
|
export function bearerToken(headers) {
|
|
102
103
|
return headers.get('authorization')?.match(/^Bearer\s+(.+)$/i)?.[1] ?? null;
|
|
103
104
|
}
|
|
105
|
+
/** "12h", or "7-day" for whole days past one. */
|
|
106
|
+
const span = (sec) => (sec % 86_400 === 0 && sec > 86_400 ? `${sec / 86_400}-day` : `${Math.round(sec / 3600)}h`);
|
|
107
|
+
/**
|
|
108
|
+
* `get_challenge` and `login`, the two MCP tools that turn an agent's own key
|
|
109
|
+
* into a Bearer token: the challenge carries the exact message to sign, and a
|
|
110
|
+
* refused login says to start again, since a challenge is spent either way.
|
|
111
|
+
*/
|
|
112
|
+
export function rolaLoginTools(o) {
|
|
113
|
+
const minutes = (o.challengeTtlSec ?? 300) / 60;
|
|
114
|
+
const carries = o.writeTools ?? 'tool';
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
name: 'get_challenge',
|
|
118
|
+
title: 'Start a login',
|
|
119
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
120
|
+
description: `Step 1 of writing to ${o.site}: a single-use ROLA challenge (${minutes}-minute expiry). ` +
|
|
121
|
+
'The response spells out the exact recipe for the message your Ed25519 key must sign. ' +
|
|
122
|
+
(o.notes?.challenge ? `${o.notes.challenge} ` : '') +
|
|
123
|
+
'You always sign with your OWN key — nothing here custodies anything. Takes no parameters.',
|
|
124
|
+
inputSchema: { type: 'object', properties: {} },
|
|
125
|
+
handler: async () => {
|
|
126
|
+
const { challenge, expiresAt } = await o.generateChallenge();
|
|
127
|
+
return {
|
|
128
|
+
challenge,
|
|
129
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
130
|
+
sign: {
|
|
131
|
+
message: `blake2b-256 of: "R" (ascii) + challenge (hex-decoded) + one length byte of the dApp definition address "${o.dAppDefinitionAddress}" + that same address (utf-8) + the origin "${o.origin}" (utf-8)`,
|
|
132
|
+
curve: 'curve25519 (Ed25519); sign the 32-byte hash, hex-encode the signature',
|
|
133
|
+
address: 'your virtual account address, derived from your public key',
|
|
134
|
+
...o.notes?.sign,
|
|
135
|
+
then: 'Call login with { challenge, address, publicKey, signature, curve }.',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'login',
|
|
142
|
+
title: 'Exchange a signed proof for a token',
|
|
143
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
144
|
+
description: `Step 2: exchange the signed ROLA proof for a ${span(o.tokenTtlSec)} Bearer token — the same verification the human wallet flow runs. ` +
|
|
145
|
+
`Send the returned token as an HTTP \`Authorization: Bearer <token>\` header on every later ${carries} call; tool arguments never carry it.`,
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
challenge: { type: 'string', description: 'The challenge from get_challenge' },
|
|
150
|
+
address: { type: 'string', description: o.notes?.address ?? 'Your account address (virtual account of the signing key)' },
|
|
151
|
+
publicKey: { type: 'string', description: 'Ed25519 public key, hex' },
|
|
152
|
+
signature: { type: 'string', description: 'Signature over the ROLA message, hex' },
|
|
153
|
+
curve: { type: 'string', enum: ['curve25519', 'secp256k1'], description: 'Signing curve (Ed25519 = "curve25519")' },
|
|
154
|
+
},
|
|
155
|
+
required: ['challenge', 'address', 'publicKey', 'signature', 'curve'],
|
|
156
|
+
},
|
|
157
|
+
handler: async (args) => {
|
|
158
|
+
const signed = {
|
|
159
|
+
challenge: String(args.challenge),
|
|
160
|
+
address: String(args.address),
|
|
161
|
+
proof: { publicKey: String(args.publicKey), signature: String(args.signature), curve: args.curve },
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
const { token, expiresAt, ...rest } = await o.login(signed);
|
|
165
|
+
return {
|
|
166
|
+
token,
|
|
167
|
+
tokenType: 'Bearer',
|
|
168
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
169
|
+
...rest,
|
|
170
|
+
note: `Send as an HTTP \`Authorization: Bearer <token>\` header on the POSTs carrying your ${carries} calls.`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
if (!(err instanceof McpToolError))
|
|
175
|
+
throw err;
|
|
176
|
+
throw new McpToolError(err.message, {
|
|
177
|
+
hint: `Challenges are single-use and expire after ${minutes} minutes — call get_challenge again. The signing recipe is in its response.`,
|
|
178
|
+
...err.details,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
package/dist/taxonomy.d.ts
CHANGED
|
@@ -150,9 +150,8 @@ export interface Taxonomy {
|
|
|
150
150
|
metadata?: unknown;
|
|
151
151
|
}) => MetadataRow[];
|
|
152
152
|
/**
|
|
153
|
-
* Whether a set has outgrown a grid
|
|
154
|
-
*
|
|
155
|
-
* the same unfiltered count, so the two appear and vanish together.
|
|
153
|
+
* Whether a set has outgrown a grid and needs an A–Z index. `alphaControls`
|
|
154
|
+
* applies it. The sort toggle does not: order matters to a short dated set too.
|
|
156
155
|
*/
|
|
157
156
|
needsAlphaIndex: (pageCount: number) => boolean;
|
|
158
157
|
href: (tagPath: string, state: CategoryState) => string;
|
package/dist/wiki-tools.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { type CorpusSection } from './corpus.js';
|
|
|
3
3
|
import { type WikiNode, type WikiTree } from './tree.js';
|
|
4
4
|
import type { WikiStore, WikiPageListing, WikiPageRow, PageMetadata } from './store.js';
|
|
5
5
|
import type { DiffBlock } from './revisions.js';
|
|
6
|
+
/** The section a `corpus` callback returns, from the slicer this module keeps internal. */
|
|
7
|
+
export type { CorpusSection };
|
|
6
8
|
type ToolName = 'search_wiki' | 'get_page' | 'get_pages' | 'list_pages' | 'get_categories' | 'get_recent_changes' | 'get_full_corpus' | 'get_page_history';
|
|
7
9
|
export interface WikiToolsOptions<B extends DiffBlock> {
|
|
8
10
|
store: WikiStore<B>;
|
|
@@ -75,4 +77,3 @@ export declare function searchWiki<B extends DiffBlock>(o: Pick<WikiToolsOptions
|
|
|
75
77
|
total: number;
|
|
76
78
|
}>;
|
|
77
79
|
export declare function wikiReadTools<B extends DiffBlock>(o: WikiToolsOptions<B>): WikiTool[];
|
|
78
|
-
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.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",
|
|
@@ -157,11 +157,6 @@
|
|
|
157
157
|
"import": "./dist/social.js",
|
|
158
158
|
"default": "./dist/social.js"
|
|
159
159
|
},
|
|
160
|
-
"./links": {
|
|
161
|
-
"types": "./dist/links.d.ts",
|
|
162
|
-
"import": "./dist/links.js",
|
|
163
|
-
"default": "./dist/links.js"
|
|
164
|
-
},
|
|
165
160
|
"./maps": {
|
|
166
161
|
"types": "./dist/maps.d.ts",
|
|
167
162
|
"import": "./dist/maps.js",
|
|
@@ -273,11 +268,6 @@
|
|
|
273
268
|
"import": "./dist/upload.js",
|
|
274
269
|
"default": "./dist/upload.js"
|
|
275
270
|
},
|
|
276
|
-
"./corpus": {
|
|
277
|
-
"types": "./dist/corpus.d.ts",
|
|
278
|
-
"import": "./dist/corpus.js",
|
|
279
|
-
"default": "./dist/corpus.js"
|
|
280
|
-
},
|
|
281
271
|
"./metadata": {
|
|
282
272
|
"types": "./dist/metadata.d.ts",
|
|
283
273
|
"import": "./dist/metadata.js",
|