llm-wiki-compiler 0.8.0 → 0.9.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/CHANGELOG.md +30 -0
- package/README.md +211 -24
- package/dist/cli.js +2943 -941
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1010 -0
- package/dist/index.js +7257 -0
- package/dist/index.js.map +1 -0
- package/dist/viewer/assets/viewer-rail.js +71 -0
- package/dist/viewer/assets/viewer-sidebar.js +115 -4
- package/dist/viewer/assets/viewer.css +69 -0
- package/dist/viewer/assets/viewer.js +48 -0
- package/package.json +16 -5
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
* timestamps, plus a "Warnings" block fed by `payload.warnings`
|
|
8
8
|
* (parser issues, unresolved citations, malformed citation entries).
|
|
9
9
|
*
|
|
10
|
+
* Freshness badges (STALE, ORPHANED, CONTRADICTED, ARCHIVED) are
|
|
11
|
+
* rendered from `payload.freshness`. The design rule: badge ONLY the
|
|
12
|
+
* two actionable source-freshness states (stale, orphaned) plus the
|
|
13
|
+
* two provenance flags (contradicted, archived). `fresh` and
|
|
14
|
+
* `unverified` get NO badge — badging neutral/default states would
|
|
15
|
+
* stamp nearly the whole wiki. A "Freshness as of <generatedAt>"
|
|
16
|
+
* caption is shown on every page (keyed on the always-present
|
|
17
|
+
* `generatedAt`), anchoring the displayed freshness to snapshot/server-
|
|
18
|
+
* start time because the viewer does not live-watch the filesystem.
|
|
19
|
+
*
|
|
10
20
|
* Fields render only when the frontmatter actually carries a value, so
|
|
11
21
|
* a legacy page with no provenance metadata shows a compact rail
|
|
12
22
|
* rather than a wall of `(none)` rows. Labels mirror `review show`
|
|
@@ -58,9 +68,11 @@ export function renderSupportRail(payload) {
|
|
|
58
68
|
const support = document.querySelector(SUPPORT_SELECTOR);
|
|
59
69
|
if (!support) return;
|
|
60
70
|
support.innerHTML = "";
|
|
71
|
+
appendFreshnessBadges(support, payload);
|
|
61
72
|
appendFrontmatterDl(support, extractFrontmatter(payload));
|
|
62
73
|
const warnings = extractWarnings(payload);
|
|
63
74
|
if (warnings.length > 0) support.appendChild(buildRailWarnings(warnings));
|
|
75
|
+
appendFreshnessCaption(support, payload);
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
/** Clear the support rail entirely (used on non-page routes). */
|
|
@@ -255,3 +267,62 @@ function buildWarningItem(warning) {
|
|
|
255
267
|
function warningText(safe) {
|
|
256
268
|
return safe.message || safe.code || "";
|
|
257
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Badge specs: each entry is [modifier, label, predicate(freshness)].
|
|
273
|
+
* Only entries whose predicate is truthy produce a badge. Ordered by
|
|
274
|
+
* axis: source-freshness first (stale, orphaned), then provenance
|
|
275
|
+
* (contradicted, archived).
|
|
276
|
+
*/
|
|
277
|
+
const BADGE_SPECS = [
|
|
278
|
+
["stale", "STALE", (f) => f.freshnessStatus === "stale"],
|
|
279
|
+
["orphaned", "ORPHANED", (f) => f.freshnessStatus === "orphaned"],
|
|
280
|
+
["contradicted","CONTRADICTED",(f) => f.contradicted],
|
|
281
|
+
["archived", "ARCHIVED", (f) => f.archived],
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Append freshness badges to the rail. Badges render ONLY for actionable
|
|
286
|
+
* states: STALE, ORPHANED (source-freshness axis) and CONTRADICTED,
|
|
287
|
+
* ARCHIVED (provenance axis). `fresh` and `unverified` get no badge —
|
|
288
|
+
* they are the neutral defaults and badging them would stamp nearly the
|
|
289
|
+
* whole wiki with noise.
|
|
290
|
+
*/
|
|
291
|
+
function appendFreshnessBadges(support, payload) {
|
|
292
|
+
const freshness = payload?.freshness;
|
|
293
|
+
if (!freshness) return;
|
|
294
|
+
const wrap = buildFreshnessBadgeWrap(freshness);
|
|
295
|
+
if (wrap) support.appendChild(wrap);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Build the badges container from the freshness object, or null if no badges apply. */
|
|
299
|
+
function buildFreshnessBadgeWrap(freshness) {
|
|
300
|
+
const activeBadges = BADGE_SPECS.filter(([, , pred]) => pred(freshness));
|
|
301
|
+
if (activeBadges.length === 0) return null;
|
|
302
|
+
const wrap = document.createElement("div");
|
|
303
|
+
wrap.className = "freshness-badges";
|
|
304
|
+
for (const [modifier, label] of activeBadges) wrap.appendChild(buildBadge(modifier, label));
|
|
305
|
+
return wrap;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Build one badge `<span>` with a modifier class and text label. */
|
|
309
|
+
function buildBadge(modifier, label) {
|
|
310
|
+
const span = document.createElement("span");
|
|
311
|
+
span.className = `freshness-badge badge-${modifier}`;
|
|
312
|
+
span.textContent = label;
|
|
313
|
+
return span;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Append the "Freshness as of <generatedAt>" caption when the payload
|
|
318
|
+
* carries a generatedAt timestamp. Explicitly honest: the viewer
|
|
319
|
+
* does not live-watch the filesystem, so this caption anchors the
|
|
320
|
+
* freshness data to the server-start time rather than implying live state.
|
|
321
|
+
*/
|
|
322
|
+
function appendFreshnessCaption(support, payload) {
|
|
323
|
+
if (!payload?.generatedAt) return;
|
|
324
|
+
const caption = document.createElement("p");
|
|
325
|
+
caption.className = "freshness-caption";
|
|
326
|
+
caption.textContent = `Freshness as of ${payload.generatedAt}`;
|
|
327
|
+
support.appendChild(caption);
|
|
328
|
+
}
|
|
@@ -11,12 +11,37 @@
|
|
|
11
11
|
* includes `kind` so the grouping is correct from the first byte);
|
|
12
12
|
* the full `/api/pages` envelope replaces the contents once it
|
|
13
13
|
* arrives.
|
|
14
|
+
*
|
|
15
|
+
* A per-axis freshness filter (all / stale / orphaned / contradicted /
|
|
16
|
+
* archived) narrows the page list client-side over the already-loaded
|
|
17
|
+
* `/api/pages` rows. No new endpoint is needed — the filter is pure
|
|
18
|
+
* DOM manipulation over the in-memory page list.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
const SIDEBAR_SELECTOR = "[data-sidebar]";
|
|
17
22
|
const DEFAULT_KIND = "concept";
|
|
18
23
|
const EMPTY_PLACEHOLDER_TEXT = "No pages yet — run `llmwiki compile`.";
|
|
19
24
|
|
|
25
|
+
/**
|
|
26
|
+
* CSS class on the standing "Project" section. Shared between
|
|
27
|
+
* `buildProjectSection` (which sets it) and `reRenderSidebarGroups`
|
|
28
|
+
* (whose keep-selector preserves it across filter re-renders) so a
|
|
29
|
+
* rename can't silently break the re-render keep-list.
|
|
30
|
+
*/
|
|
31
|
+
const PROJECT_SECTION_CLASS = "sidebar-health";
|
|
32
|
+
|
|
33
|
+
/** The active freshness filter. "all" means no narrowing. */
|
|
34
|
+
let activeFreshnessFilter = "all";
|
|
35
|
+
|
|
36
|
+
/** Available filter values and their human labels. */
|
|
37
|
+
const FRESHNESS_FILTER_OPTIONS = [
|
|
38
|
+
{ value: "all", label: "All" },
|
|
39
|
+
{ value: "stale", label: "Stale" },
|
|
40
|
+
{ value: "orphaned", label: "Orphaned" },
|
|
41
|
+
{ value: "contradicted", label: "Contradicted" },
|
|
42
|
+
{ value: "archived", label: "Archived" },
|
|
43
|
+
];
|
|
44
|
+
|
|
20
45
|
/**
|
|
21
46
|
* Static (non-page) hash routes that have a dedicated sidebar link.
|
|
22
47
|
* `markActive` highlights the entry via `a[data-route="<route>"]`
|
|
@@ -27,18 +52,43 @@ const STATIC_ROUTE_LINK_SELECTORS = new Map([
|
|
|
27
52
|
["#/health", 'a[data-route="health"]'],
|
|
28
53
|
]);
|
|
29
54
|
|
|
55
|
+
/** Full page list captured at the last renderSidebar call for filter re-renders. */
|
|
56
|
+
let lastPages = [];
|
|
57
|
+
|
|
30
58
|
/** Render the sidebar groups + standing Health entry, then mark active. */
|
|
31
59
|
export function renderSidebar(pages) {
|
|
60
|
+
lastPages = pages;
|
|
32
61
|
const sidebar = document.querySelector(SIDEBAR_SELECTOR);
|
|
33
62
|
if (!sidebar) return;
|
|
34
63
|
sidebar.innerHTML = "";
|
|
35
64
|
sidebar.appendChild(buildProjectSection());
|
|
36
|
-
|
|
37
|
-
|
|
65
|
+
sidebar.appendChild(buildFreshnessFilter());
|
|
66
|
+
renderFilteredGroups(sidebar, pages);
|
|
67
|
+
markActive();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Re-render only the page groups using the stored lastPages + active filter. */
|
|
71
|
+
function reRenderSidebarGroups() {
|
|
72
|
+
const sidebar = document.querySelector(SIDEBAR_SELECTOR);
|
|
73
|
+
if (!sidebar) return;
|
|
74
|
+
// Remove everything after the project section and filter control.
|
|
75
|
+
const keep = sidebar.querySelectorAll(`section.${PROJECT_SECTION_CLASS}, .freshness-filter`);
|
|
76
|
+
const keepSet = new Set(Array.from(keep));
|
|
77
|
+
Array.from(sidebar.children).forEach((child) => {
|
|
78
|
+
if (!keepSet.has(child)) child.remove();
|
|
79
|
+
});
|
|
80
|
+
renderFilteredGroups(sidebar, lastPages);
|
|
81
|
+
markActive();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Append the filtered concept/query groups and the empty placeholder if needed. */
|
|
85
|
+
function renderFilteredGroups(sidebar, pages) {
|
|
86
|
+
const filtered = applyFreshnessFilter(pages, activeFreshnessFilter);
|
|
87
|
+
const concepts = filterByDirectory(filtered, "concepts");
|
|
88
|
+
const queries = filterByDirectory(filtered, "queries");
|
|
38
89
|
appendConceptGroups(sidebar, concepts);
|
|
39
90
|
appendQueryGroup(sidebar, queries);
|
|
40
91
|
appendEmptyPlaceholderIfNeeded(sidebar, concepts, queries);
|
|
41
|
-
markActive();
|
|
42
92
|
}
|
|
43
93
|
|
|
44
94
|
/** Filter pages to those whose `pageDirectory` matches the given bucket. */
|
|
@@ -180,7 +230,7 @@ function buildPageListItem(page) {
|
|
|
180
230
|
/** Build the standing "Project" sidebar section with Health and Graph links. */
|
|
181
231
|
function buildProjectSection() {
|
|
182
232
|
const wrap = document.createElement("section");
|
|
183
|
-
wrap.className =
|
|
233
|
+
wrap.className = PROJECT_SECTION_CLASS;
|
|
184
234
|
const heading = document.createElement("h2");
|
|
185
235
|
heading.textContent = "Project";
|
|
186
236
|
wrap.appendChild(heading);
|
|
@@ -219,3 +269,64 @@ function parseExpectedPageId(hash) {
|
|
|
219
269
|
}
|
|
220
270
|
return `${match[1]}/${slug}`;
|
|
221
271
|
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Build the freshness-filter `<div>` with a `<select>` control. The
|
|
275
|
+
* filter is client-side over the already-loaded page rows — no new
|
|
276
|
+
* endpoint, no query params.
|
|
277
|
+
*/
|
|
278
|
+
function buildFreshnessFilter() {
|
|
279
|
+
const wrap = document.createElement("div");
|
|
280
|
+
wrap.className = "freshness-filter";
|
|
281
|
+
const label = document.createElement("label");
|
|
282
|
+
label.className = "freshness-filter-label";
|
|
283
|
+
label.setAttribute("for", "freshness-filter-select");
|
|
284
|
+
label.textContent = "Filter by freshness";
|
|
285
|
+
const select = document.createElement("select");
|
|
286
|
+
select.id = "freshness-filter-select";
|
|
287
|
+
select.className = "freshness-filter-select";
|
|
288
|
+
for (const { value, label: optLabel } of FRESHNESS_FILTER_OPTIONS) {
|
|
289
|
+
const option = document.createElement("option");
|
|
290
|
+
option.value = value;
|
|
291
|
+
option.textContent = optLabel;
|
|
292
|
+
if (value === activeFreshnessFilter) option.selected = true;
|
|
293
|
+
select.appendChild(option);
|
|
294
|
+
}
|
|
295
|
+
select.addEventListener("change", onFreshnessFilterChange);
|
|
296
|
+
wrap.appendChild(label);
|
|
297
|
+
wrap.appendChild(select);
|
|
298
|
+
return wrap;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Handle freshness filter selection change — update state and re-render. */
|
|
302
|
+
function onFreshnessFilterChange(event) {
|
|
303
|
+
activeFreshnessFilter = event.target.value;
|
|
304
|
+
reRenderSidebarGroups();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Lookup table: filter value → predicate over a freshness object.
|
|
309
|
+
* Keeps `matchesFreshnessFilter` branch-free.
|
|
310
|
+
*/
|
|
311
|
+
const FRESHNESS_PREDICATES = {
|
|
312
|
+
stale: (f) => f.freshnessStatus === "stale",
|
|
313
|
+
orphaned: (f) => f.freshnessStatus === "orphaned",
|
|
314
|
+
contradicted:(f) => f.contradicted === true,
|
|
315
|
+
archived: (f) => f.archived === true,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Apply the active freshness filter to the page list. "all" returns all
|
|
320
|
+
* pages; other values match the corresponding freshness flag on each page.
|
|
321
|
+
*/
|
|
322
|
+
function applyFreshnessFilter(pages, filter) {
|
|
323
|
+
if (filter === "all") return pages;
|
|
324
|
+
return pages.filter((page) => matchesFreshnessFilter(page, filter));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** True when the page's freshness satisfies the active filter. */
|
|
328
|
+
function matchesFreshnessFilter(page, filter) {
|
|
329
|
+
const f = page.freshness;
|
|
330
|
+
const predicate = FRESHNESS_PREDICATES[filter];
|
|
331
|
+
return f != null && predicate != null && predicate(f);
|
|
332
|
+
}
|
|
@@ -314,6 +314,75 @@ a:hover, a:focus-visible { text-decoration: underline; }
|
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
/* Freshness badges — shown in the support rail for STALE, ORPHANED,
|
|
318
|
+
* CONTRADICTED, ARCHIVED pages. fresh/unverified get no badge. */
|
|
319
|
+
.freshness-badges {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex-wrap: wrap;
|
|
322
|
+
gap: 0.35rem;
|
|
323
|
+
margin-bottom: 0.6rem;
|
|
324
|
+
}
|
|
325
|
+
.freshness-badge {
|
|
326
|
+
display: inline-block;
|
|
327
|
+
padding: 0.15rem 0.45rem;
|
|
328
|
+
border-radius: 999px;
|
|
329
|
+
font-size: 0.72rem;
|
|
330
|
+
font-weight: 700;
|
|
331
|
+
letter-spacing: 0.04em;
|
|
332
|
+
text-transform: uppercase;
|
|
333
|
+
}
|
|
334
|
+
.badge-stale { background: rgba(200, 90, 0, 0.12); color: #b85c00; }
|
|
335
|
+
.badge-orphaned { background: rgba(176, 0, 32, 0.10); color: var(--missing); }
|
|
336
|
+
.badge-contradicted { background: rgba(100, 0, 180, 0.10); color: #6400b4; }
|
|
337
|
+
.badge-archived { background: rgba(100, 100, 110, 0.12); color: var(--fg-muted); }
|
|
338
|
+
@media (prefers-color-scheme: dark) {
|
|
339
|
+
.badge-stale { background: rgba(255, 160, 60, 0.14); color: #ffa03c; }
|
|
340
|
+
.badge-contradicted { background: rgba(180, 100, 255, 0.14); color: #b464ff; }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Freshness caption — "Freshness as of <timestamp>" — always shown
|
|
344
|
+
* when freshness data is present to be honest about the frozen snapshot. */
|
|
345
|
+
.freshness-caption {
|
|
346
|
+
font-size: 0.76rem;
|
|
347
|
+
color: var(--fg-muted);
|
|
348
|
+
margin-top: 0.75rem;
|
|
349
|
+
font-style: italic;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* Corrupt-state banner — shown globally (and in the health pane) when stateStatus === "corrupt". */
|
|
353
|
+
.corrupt-state-banner {
|
|
354
|
+
background: rgba(176, 0, 32, 0.08);
|
|
355
|
+
color: var(--missing);
|
|
356
|
+
border: 1px solid rgba(176, 0, 32, 0.2);
|
|
357
|
+
padding: 0.6rem 0.85rem;
|
|
358
|
+
border-radius: 6px;
|
|
359
|
+
margin-bottom: 1rem;
|
|
360
|
+
font-weight: 500;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Freshness filter — sidebar select control for filtering by freshness axis. */
|
|
364
|
+
.freshness-filter {
|
|
365
|
+
display: flex;
|
|
366
|
+
flex-direction: column;
|
|
367
|
+
gap: 0.25rem;
|
|
368
|
+
margin: 0.5rem 0 0.75rem;
|
|
369
|
+
}
|
|
370
|
+
.freshness-filter-label {
|
|
371
|
+
font-size: 0.72rem;
|
|
372
|
+
text-transform: uppercase;
|
|
373
|
+
letter-spacing: 0.05em;
|
|
374
|
+
color: var(--fg-muted);
|
|
375
|
+
}
|
|
376
|
+
.freshness-filter-select {
|
|
377
|
+
width: 100%;
|
|
378
|
+
padding: 0.3rem 0.5rem;
|
|
379
|
+
background: var(--bg);
|
|
380
|
+
color: var(--fg);
|
|
381
|
+
border: 1px solid var(--border);
|
|
382
|
+
border-radius: 6px;
|
|
383
|
+
font-size: 0.9rem;
|
|
384
|
+
}
|
|
385
|
+
|
|
317
386
|
/* Graph pane */
|
|
318
387
|
.graph-pane {
|
|
319
388
|
width: 100%;
|
|
@@ -57,6 +57,8 @@ const HEALTH_METRIC_ROWS = [
|
|
|
57
57
|
["Saved queries", "queries"],
|
|
58
58
|
["Compiled sources", "sources"],
|
|
59
59
|
["Source files", "sourceFiles"],
|
|
60
|
+
["Stale pages", "stale"],
|
|
61
|
+
["Orphaned pages", "orphaned"],
|
|
60
62
|
["Pending reviews", "pendingReviews"],
|
|
61
63
|
];
|
|
62
64
|
|
|
@@ -268,6 +270,10 @@ async function renderHealthPane(main) {
|
|
|
268
270
|
function buildHealthDashboard(health) {
|
|
269
271
|
const wrap = document.createElement("section");
|
|
270
272
|
wrap.className = "health-dashboard";
|
|
273
|
+
// The global banner (injected at bootstrap) covers every route including health;
|
|
274
|
+
// only add it here if bootstrap didn't already inject one (e.g. if /api/pages
|
|
275
|
+
// was not yet fetched when navigating directly to #/health).
|
|
276
|
+
prependBannerIfNeeded(wrap, health?.stateStatus);
|
|
271
277
|
const rows = HEALTH_METRIC_ROWS.map(([label, key]) => [label, health?.[key] ?? 0]);
|
|
272
278
|
const metrics = buildDefinitionList(rows);
|
|
273
279
|
metrics.className = "metric-list";
|
|
@@ -276,6 +282,27 @@ function buildHealthDashboard(health) {
|
|
|
276
282
|
return wrap;
|
|
277
283
|
}
|
|
278
284
|
|
|
285
|
+
/** Prepend a corrupt-state banner to `container` if one is not already in the document. */
|
|
286
|
+
function prependBannerIfNeeded(container, stateStatus) {
|
|
287
|
+
if (stateStatus !== "corrupt") return;
|
|
288
|
+
if (document.querySelector(".corrupt-state-banner")) return;
|
|
289
|
+
container.prepend(buildCorruptStateBanner());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Build the corrupt-state warning banner. Displayed when `/api/health`
|
|
294
|
+
* reports `stateStatus === "corrupt"`, meaning the project's state.json
|
|
295
|
+
* could not be parsed at viewer startup and freshness data is unreliable.
|
|
296
|
+
*/
|
|
297
|
+
function buildCorruptStateBanner() {
|
|
298
|
+
const banner = document.createElement("div");
|
|
299
|
+
banner.className = "corrupt-state-banner";
|
|
300
|
+
banner.setAttribute("role", "alert");
|
|
301
|
+
banner.textContent =
|
|
302
|
+
"Warning: state.json is corrupt. Freshness data is unavailable. Re-run `llmwiki compile` to restore.";
|
|
303
|
+
return banner;
|
|
304
|
+
}
|
|
305
|
+
|
|
279
306
|
/** Render the lint summary, or a "lint has not been run yet" placeholder. */
|
|
280
307
|
function buildLintBlock(lint) {
|
|
281
308
|
const wrap = document.createElement("section");
|
|
@@ -305,6 +332,21 @@ function applyHomeEnvelope(envelope) {
|
|
|
305
332
|
titleEl.textContent = projectTitle(envelope);
|
|
306
333
|
renderSidebar(envelope?.pages || []);
|
|
307
334
|
renderHome(envelope);
|
|
335
|
+
// Inject into .app-layout (outside <main>) so the banner persists across route changes.
|
|
336
|
+
injectGlobalCorruptBanner(envelope?.stateStatus);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Inject the corrupt-state banner into the app-layout container (above `main`)
|
|
341
|
+
* so it persists across route changes. Runs once at app bootstrap from the
|
|
342
|
+
* /api/pages envelope. No-ops when not corrupt or already injected.
|
|
343
|
+
*/
|
|
344
|
+
function injectGlobalCorruptBanner(stateStatus) {
|
|
345
|
+
if (stateStatus !== "corrupt") return;
|
|
346
|
+
if (document.querySelector(".corrupt-state-banner")) return;
|
|
347
|
+
const layout = document.querySelector(".app-layout");
|
|
348
|
+
if (!layout) return;
|
|
349
|
+
layout.prepend(buildCorruptStateBanner());
|
|
308
350
|
}
|
|
309
351
|
|
|
310
352
|
/** Fetch /api/index and render the rendered HTML coming back from the server. */
|
|
@@ -467,6 +509,12 @@ function main() {
|
|
|
467
509
|
const embedded = readEmbeddedIndex();
|
|
468
510
|
renderSidebar(embedded.pages);
|
|
469
511
|
wireSearch({ fetchJson });
|
|
512
|
+
// Ensure the corrupt-state banner appears on every entry route, not just home.
|
|
513
|
+
// injectGlobalCorruptBanner is idempotent, so the home route's own /api/pages
|
|
514
|
+
// fetch won't double-render if both settle.
|
|
515
|
+
void fetchJson("/api/pages")
|
|
516
|
+
.then((env) => injectGlobalCorruptBanner(env?.stateStatus))
|
|
517
|
+
.catch(() => {});
|
|
470
518
|
window.addEventListener("hashchange", () => {
|
|
471
519
|
void renderRoute();
|
|
472
520
|
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-wiki-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A knowledge compiler CLI — raw sources in, interlinked wiki out",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
6
15
|
"bin": {
|
|
7
16
|
"llmwiki": "dist/cli.js"
|
|
8
17
|
},
|
|
@@ -16,7 +25,8 @@
|
|
|
16
25
|
"test:watch": "vitest",
|
|
17
26
|
"fallow:ci": "bash scripts/fallow-ci.sh",
|
|
18
27
|
"prepublishOnly": "npm run release:check-docs:current && npm run build && npm pack --dry-run --ignore-scripts",
|
|
19
|
-
"prepare": "husky"
|
|
28
|
+
"prepare": "husky",
|
|
29
|
+
"test:pack": "vitest run --config /dev/null test/sdk/packaging.test.ts"
|
|
20
30
|
},
|
|
21
31
|
"keywords": [
|
|
22
32
|
"knowledge",
|
|
@@ -46,7 +56,8 @@
|
|
|
46
56
|
"node": ">=24"
|
|
47
57
|
},
|
|
48
58
|
"dependencies": {
|
|
49
|
-
"@anthropic-ai/sdk": "^0.
|
|
59
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.163",
|
|
60
|
+
"@anthropic-ai/sdk": "^0.101.0",
|
|
50
61
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
62
|
"@mozilla/readability": "^0.5.0",
|
|
52
63
|
"chokidar": "^4.0.0",
|
|
@@ -55,13 +66,13 @@
|
|
|
55
66
|
"js-yaml": "^4.1.1",
|
|
56
67
|
"jsdom": "^25.0.0",
|
|
57
68
|
"markdown-it": "^14.1.1",
|
|
58
|
-
"openai": "^
|
|
69
|
+
"openai": "^6.42.0",
|
|
59
70
|
"p-limit": "^6.0.0",
|
|
60
71
|
"pdf-parse": "^2.4.5",
|
|
61
72
|
"sanitize-html": "^2.17.3",
|
|
62
73
|
"turndown": "^7.2.0",
|
|
63
74
|
"youtube-transcript": "^1.3.1",
|
|
64
|
-
"zod": "^
|
|
75
|
+
"zod": "^4.4.3"
|
|
65
76
|
},
|
|
66
77
|
"devDependencies": {
|
|
67
78
|
"@copilotkit/aimock": "^1.15.1",
|