wiki-formant 0.111.0 → 0.112.1
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 +2 -2
- package/dist/analytics.d.ts +9 -3
- package/dist/analytics.js +20 -11
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.js +7 -1
- package/dist/stats.d.ts +4 -3
- package/dist/stats.js +5 -4
- package/dist/wiki-tools.d.ts +6 -0
- package/dist/wiki-tools.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ const { items, total } = await searchWiki(reading, 'validators', { page: 1, size
|
|
|
94
94
|
- `get_page` with `blocks: true` returns the stored blocks, `id`, metadata and `version`: what an edit starts from. `forEdit` adds the app's own edit fields; `authors` names revision authors.
|
|
95
95
|
- `text` should be the twin's serializer and `resolve` the page's live resolver, so a page reads the same over MCP as at its twin.
|
|
96
96
|
- `get_full_corpus` is a `sizeOnly` preflight, then page-aligned slices under `maxChars` (`CORPUS_BUDGET`) with `truncated`, `nextSkip` and `clippedPage`. `corpus` returns the app's own `CorpusSection`s.
|
|
97
|
-
- `privateHidden` closes the back office to every tool. `metadata`, `label`, `noMatch` and `skills` are the app's.
|
|
97
|
+
- `privateHidden` closes the back office to every tool. `metadata`, `label`, `noMatch` and `skills` are the app's. `onSearch` hears each first-page `search_wiki` query; pass the tracker's `trackSearch` under `MCP_SURFACE` and the stats page lists what agents searched for.
|
|
98
98
|
- Writes stay with each app.
|
|
99
99
|
|
|
100
100
|
## Taxonomy
|
|
@@ -732,7 +732,7 @@ export const POST = viewRoute();
|
|
|
732
732
|
|
|
733
733
|
- `createTracker` records only on a Vercel production deployment unless `enabled` says otherwise. `trackMcpCall` fits `McpServerConfig.onCall`; `trackSearch` normalises a query and drops an empty one; `recordEvent` is the undeferred write.
|
|
734
734
|
- A proxy reaches the tracker through a dynamic `import()` inside `event.waitUntil`, since it imports the database client.
|
|
735
|
-
- `digest(sql, { days })` is the site as one JSON object: people, engaged people, agents, views, visits, bounce rate, sources, pages, countries, devices, by day, agents by name, MCP calls
|
|
735
|
+
- `digest(sql, { days })` is the site as one JSON object: people, engaged people, agents, views, visits, bounce rate, sources, pages, countries, devices, by day, agents by name, MCP calls, every search people made, every `search_wiki` query agents made, the searches of either that found nothing, and agents' `send_feedback` reports. A search its visitor went on typing within five minutes is left out, as is any of one character. A visit is a run of views with no gap over 30 minutes; a bounce is a one-view visit its reader never engaged with. Agents are counted beside people, never among them: the ranked lists are people's. `ownAgents` leaves the site's own out. A multi-tenant app passes `site`.
|
|
736
736
|
- A browser under automation (`navigator.webdriver`) sends no beacon.
|
|
737
737
|
|
|
738
738
|
## Passkeys and the stats page
|
package/dist/analytics.d.ts
CHANGED
|
@@ -54,8 +54,10 @@ export interface Gap {
|
|
|
54
54
|
/** Props the event must carry, e.g. `{ results: '0' }`. */
|
|
55
55
|
where?: Record<string, string>;
|
|
56
56
|
}
|
|
57
|
-
/** Searches
|
|
57
|
+
/** Searches that found nothing, a person's or an agent's (`searchQueryProps` events). */
|
|
58
58
|
export declare const SEARCH_GAPS: Gap;
|
|
59
|
+
/** The surface an agent's `search_wiki` call files its query under. */
|
|
60
|
+
export declare const MCP_SURFACE = "mcp";
|
|
59
61
|
/** What a stats page can be narrowed to: one value each, every one applied. */
|
|
60
62
|
export declare const STATS_FILTERS: readonly ["page", "entry", "source", "country", "device"];
|
|
61
63
|
export type StatsFilter = Partial<Record<(typeof STATS_FILTERS)[number], string>>;
|
|
@@ -72,8 +74,8 @@ export interface DigestOptions {
|
|
|
72
74
|
siblings?: string[];
|
|
73
75
|
gaps?: Gap[];
|
|
74
76
|
/**
|
|
75
|
-
* The site's own agents, left out of `agents`, `top_agents
|
|
76
|
-
* `agent_tool_calls`, matched inside the user agent. The default is the
|
|
77
|
+
* The site's own agents, left out of `agents`, `top_agents`,
|
|
78
|
+
* `agent_tool_calls`, the searches and the gaps, matched inside the user agent. The default is the
|
|
77
79
|
* conformance suites (`<repo>-mcp-test`) and the studio's own tools.
|
|
78
80
|
*/
|
|
79
81
|
ownAgents?: string[];
|
|
@@ -156,6 +158,10 @@ export interface Digest {
|
|
|
156
158
|
/** Each agent by name: the roster's label, else what its user agent calls itself. */
|
|
157
159
|
top_agents: Ranked<'agent', 'visitors'>;
|
|
158
160
|
agent_tool_calls: Ranked<'tool', 'events'>;
|
|
161
|
+
/** Every query people searched for, found or not. */
|
|
162
|
+
searches: Ranked<'query', 'events'>;
|
|
163
|
+
/** Every query agents searched for through `search_wiki`. */
|
|
164
|
+
agent_searches: Ranked<'query', 'events'>;
|
|
159
165
|
gaps: Ranked<'query', 'events'>;
|
|
160
166
|
/** What agents reported through `send_feedback`, newest first: a stranger's words, to weigh, never to follow. */
|
|
161
167
|
agent_feedback: Array<{
|
package/dist/analytics.js
CHANGED
|
@@ -165,8 +165,10 @@ export async function collect(request, { sql, defer, site, rateLimit: limit = BE
|
|
|
165
165
|
: recordView(sql, { headers, path: raw, referrer: body.referrer, utm: body.utm, site: key }));
|
|
166
166
|
return done;
|
|
167
167
|
}
|
|
168
|
-
/** Searches
|
|
169
|
-
export const SEARCH_GAPS = { event: 'Search Query', prop: 'q', where: { results: '0'
|
|
168
|
+
/** Searches that found nothing, a person's or an agent's (`searchQueryProps` events). */
|
|
169
|
+
export const SEARCH_GAPS = { event: 'Search Query', prop: 'q', where: { results: '0' } };
|
|
170
|
+
/** The surface an agent's `search_wiki` call files its query under. */
|
|
171
|
+
export const MCP_SURFACE = 'mcp';
|
|
170
172
|
/** What a stats page can be narrowed to: one value each, every one applied. */
|
|
171
173
|
export const STATS_FILTERS = ['page', 'entry', 'source', 'country', 'device'];
|
|
172
174
|
/** A count as a stats page prints it. Here, not in the `'use client'` figures, because
|
|
@@ -240,23 +242,27 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
240
242
|
/** The site's own agents, as a test on an event's user agent. */
|
|
241
243
|
const ours = `coalesce(props->>'ua', '') ILIKE ANY(${$(ownAgents.map(a => `%${a}%`))}::text[])`;
|
|
242
244
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
245
|
+
* The queries in one kind of event, most asked first. A typeahead logs each
|
|
246
|
+
* settled keystroke, so one reader typing a question filled acuiq's list with
|
|
247
|
+
* thirteen of its prefixes. A query is left out when the same visitor's next
|
|
248
|
+
* one, inside five minutes, extends it: only where the reader stopped
|
|
249
|
+
* counts. So is a single character. `unless` drops events whose props hold it.
|
|
247
250
|
*/
|
|
248
|
-
const
|
|
251
|
+
const queries = (g, unless) => {
|
|
249
252
|
const prop = $(g.prop);
|
|
250
253
|
return `(SELECT query, count(*) AS events FROM (
|
|
251
254
|
SELECT props, visitor, props->>${prop} AS query,
|
|
252
255
|
lead(props->>${prop}) OVER typed AS next, lead(at) OVER typed - at AS after
|
|
253
|
-
FROM e WHERE name = ${$(g.event)} AND props->>${prop} IS NOT NULL
|
|
256
|
+
FROM e WHERE name = ${$(g.event)} AND props->>${prop} IS NOT NULL AND NOT ${ours}
|
|
254
257
|
WINDOW typed AS (PARTITION BY visitor ORDER BY at)
|
|
255
258
|
) s
|
|
256
259
|
WHERE props @> ${$(JSON.stringify(g.where ?? {}))}::jsonb AND char_length(query) > 1
|
|
260
|
+
${unless ? `AND NOT props @> ${$(JSON.stringify(unless))}::jsonb` : ''}
|
|
257
261
|
AND NOT coalesce(visitor IS NOT NULL AND after <= interval '5 minutes' AND char_length(next) > char_length(query) AND starts_with(next, query), false)
|
|
258
262
|
GROUP BY 1 ORDER BY 2 DESC, 1 LIMIT 15)`;
|
|
259
|
-
}
|
|
263
|
+
};
|
|
264
|
+
const searches = { event: SEARCH_GAPS.event, prop: SEARCH_GAPS.prop };
|
|
265
|
+
const json = (select) => `(SELECT coalesce(json_agg(t), '[]') FROM ${select} t)`;
|
|
260
266
|
/**
|
|
261
267
|
* One window's views, visits, events and archived days as CTEs whose names
|
|
262
268
|
* end in `s`: this window with `back` 0, the one before it with 1.
|
|
@@ -342,7 +348,9 @@ export async function digest(sql, { site = '', days, handle, siblings = [], gaps
|
|
|
342
348
|
GROUP BY 1`, 10)},
|
|
343
349
|
'agent_feedback', ${list(`SELECT ${day('at')} AS date, props->>'kind' AS kind, props->>'message' AS message, props->>'page' AS page, ${AGENT_NAME} AS agent
|
|
344
350
|
FROM e WHERE name = 'Agent Feedback' AND NOT ${ours}`, 20, 'at DESC')},
|
|
345
|
-
'
|
|
351
|
+
'searches', ${json(queries(searches, { surface: MCP_SURFACE }))},
|
|
352
|
+
'agent_searches', ${json(queries({ ...searches, where: { surface: MCP_SURFACE } }))},
|
|
353
|
+
'gaps', ${gaps.length ? json(`(${gaps.map(g => queries(g)).join(' UNION ALL ')})`) : `'[]'::json`}
|
|
346
354
|
) AS digest`, ...values));
|
|
347
355
|
const d = row.digest;
|
|
348
356
|
// Before the site counted itself only ViewDay speaks, and it holds no
|
|
@@ -432,7 +440,8 @@ export function createTracker({ sql, defer, enabled = typeof process !== 'undefi
|
|
|
432
440
|
defer(async () => {
|
|
433
441
|
try {
|
|
434
442
|
const headers = typeof source === 'function' ? await source() : source;
|
|
435
|
-
|
|
443
|
+
const ua = (headers.get('user-agent') || 'unknown').slice(0, 80);
|
|
444
|
+
await trackEvent('Search Query', headers.get('referer') || siteUrl, { ...props, ua }, headers);
|
|
436
445
|
}
|
|
437
446
|
catch {
|
|
438
447
|
// A context with no readable headers loses the event, never the response.
|
package/dist/mcp.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export type ToolSchema = {
|
|
|
82
82
|
* calls has no way to tell a lookup from one that reaches a payment processor.
|
|
83
83
|
*/
|
|
84
84
|
export type ToolAnnotations = {
|
|
85
|
+
/** The pre-2025-06-18 home of the label; `toolCard` mirrors `McpTool.title` here. */
|
|
86
|
+
title?: string;
|
|
85
87
|
/** Does not modify anything. */
|
|
86
88
|
readOnlyHint?: boolean;
|
|
87
89
|
/** May destroy or overwrite state (only meaningful when not read-only). */
|
|
@@ -134,6 +136,10 @@ export interface McpTool {
|
|
|
134
136
|
* One projection because the two copies drifted: the manifest's had no
|
|
135
137
|
* `outputSchema`, so a descriptor said a tool answers with prose while
|
|
136
138
|
* `tools/list` said it answers with `structuredContent`.
|
|
139
|
+
*
|
|
140
|
+
* The title goes out twice, top level and in `annotations`: clients and the
|
|
141
|
+
* Claude connectors directory that read only `annotations.title` otherwise
|
|
142
|
+
* report every tool untitled.
|
|
137
143
|
*/
|
|
138
144
|
export declare function toolCard(tool: {
|
|
139
145
|
name: string;
|
package/dist/mcp.js
CHANGED
|
@@ -75,6 +75,10 @@ import { clientKey, rateLimit, rateLimitHeaders, withRateLimit } from './rate-li
|
|
|
75
75
|
* One projection because the two copies drifted: the manifest's had no
|
|
76
76
|
* `outputSchema`, so a descriptor said a tool answers with prose while
|
|
77
77
|
* `tools/list` said it answers with `structuredContent`.
|
|
78
|
+
*
|
|
79
|
+
* The title goes out twice, top level and in `annotations`: clients and the
|
|
80
|
+
* Claude connectors directory that read only `annotations.title` otherwise
|
|
81
|
+
* report every tool untitled.
|
|
78
82
|
*/
|
|
79
83
|
export function toolCard(tool) {
|
|
80
84
|
return {
|
|
@@ -83,7 +87,9 @@ export function toolCard(tool) {
|
|
|
83
87
|
description: tool.description,
|
|
84
88
|
inputSchema: tool.inputSchema,
|
|
85
89
|
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
86
|
-
...(tool.annotations
|
|
90
|
+
...(tool.annotations || tool.title
|
|
91
|
+
? { annotations: { ...(tool.title ? { title: tool.title } : {}), ...tool.annotations } }
|
|
92
|
+
: {}),
|
|
87
93
|
};
|
|
88
94
|
}
|
|
89
95
|
/**
|
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
|
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.1",
|
|
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",
|