llm-wiki-compiler 0.6.0 → 0.7.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/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { readFile as readFile7 } from "fs/promises";
13
13
  import { writeFile, rename, readFile, mkdir } from "fs/promises";
14
14
  import path from "path";
15
15
  import yaml from "js-yaml";
16
+ var CITATION_MARKER_PATTERN = /\^\[([^\]]+)\]/g;
16
17
  var SPAN_SUFFIX_PATTERN = /^(?<file>[^:#]+)(?:(?::(?<colonStart>\d+)(?:-(?<colonEnd>\d+))?)|(?:#L(?<hashStart>\d+)(?:-L(?<hashEnd>\d+))?))?$/;
17
18
  var MIN_LINE_NUMBER = 1;
18
19
  var VALID_PROVENANCE_STATES = /* @__PURE__ */ new Set([
@@ -31,19 +32,27 @@ ${dumped}
31
32
  ---`;
32
33
  }
33
34
  function parseFrontmatter(content) {
35
+ const { meta, body } = parseFrontmatterStatus(content);
36
+ return { meta, body };
37
+ }
38
+ function parseFrontmatterStatus(content) {
34
39
  const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
35
40
  if (!match) {
36
- return { meta: {}, body: content };
41
+ return { meta: {}, body: content, hasFrontmatterBlock: false, malformedFrontmatter: false };
37
42
  }
38
43
  let meta = {};
44
+ let malformedFrontmatter = false;
39
45
  try {
40
46
  const parsed = yaml.load(match[1]);
41
47
  if (parsed && typeof parsed === "object") {
42
48
  meta = parsed;
49
+ } else if (parsed !== null && parsed !== void 0) {
50
+ malformedFrontmatter = true;
43
51
  }
44
52
  } catch {
53
+ malformedFrontmatter = true;
45
54
  }
46
- return { meta, body: match[2] };
55
+ return { meta, body: match[2], hasFrontmatterBlock: true, malformedFrontmatter };
47
56
  }
48
57
  async function atomicWrite(filePath, content) {
49
58
  await mkdir(path.dirname(filePath), { recursive: true });
@@ -51,6 +60,41 @@ async function atomicWrite(filePath, content) {
51
60
  await writeFile(tmpPath, content, "utf-8");
52
61
  await rename(tmpPath, filePath);
53
62
  }
63
+ function extractClaimCitations(body) {
64
+ const citations = [];
65
+ let match;
66
+ CITATION_MARKER_PATTERN.lastIndex = 0;
67
+ while ((match = CITATION_MARKER_PATTERN.exec(body)) !== null) {
68
+ const raw = match[1];
69
+ const spans = parseCitationEntries(raw);
70
+ if (spans.length > 0) citations.push({ raw, spans });
71
+ }
72
+ return citations;
73
+ }
74
+ function parseCitationEntries(inner) {
75
+ const spans = [];
76
+ for (const part of inner.split(",")) {
77
+ const trimmed = part.trim();
78
+ if (trimmed.length === 0) continue;
79
+ const span = parseSpanEntry(trimmed);
80
+ if (span !== void 0) spans.push(span);
81
+ }
82
+ return spans;
83
+ }
84
+ function parseSpanEntry(entry) {
85
+ const match = SPAN_SUFFIX_PATTERN.exec(entry);
86
+ if (!match || !match.groups) {
87
+ return { file: entry };
88
+ }
89
+ const { file, colonStart, colonEnd, hashStart, hashEnd } = match.groups;
90
+ const start = colonStart ?? hashStart;
91
+ const end = colonEnd ?? hashEnd;
92
+ if (start === void 0) return { file };
93
+ const startLine = Number(start);
94
+ const endLine = end === void 0 ? startLine : Number(end);
95
+ if (!isValidLineRange(startLine, endLine)) return void 0;
96
+ return { file, lines: { start: startLine, end: endLine } };
97
+ }
54
98
  function isValidLineRange(start, end) {
55
99
  return start >= MIN_LINE_NUMBER && end >= start;
56
100
  }
@@ -138,9 +182,11 @@ var PROVIDER_MODELS = {
138
182
  anthropic: "claude-sonnet-4-20250514",
139
183
  openai: "gpt-4o",
140
184
  ollama: "llama3.1",
141
- minimax: "MiniMax-M2.7"
185
+ minimax: "MiniMax-M2.7",
186
+ copilot: "gpt-4o"
142
187
  };
143
188
  var OLLAMA_DEFAULT_HOST = "http://localhost:11434/v1";
189
+ var COPILOT_BASE_URL = "https://api.githubcopilot.com";
144
190
  var OPENAI_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
145
191
  var OLLAMA_DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
146
192
  var SOURCES_DIR = "sources";
@@ -152,6 +198,7 @@ var LOCK_FILE = ".llmwiki/lock";
152
198
  var INDEX_FILE = "wiki/index.md";
153
199
  var MOC_FILE = "wiki/MOC.md";
154
200
  var EMBEDDINGS_FILE = ".llmwiki/embeddings.json";
201
+ var LAST_LINT_FILE = ".llmwiki/last-lint.json";
155
202
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".gif", ".webp"]);
156
203
  var TRANSCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".vtt", ".srt"]);
157
204
  var IMAGE_DESCRIBE_MAX_TOKENS = 2048;
@@ -1192,90 +1239,1387 @@ async function ingestDirectory(dirPath) {
1192
1239
  skipped++;
1193
1240
  }
1194
1241
  }
1195
- if (imported === 0) {
1242
+ if (imported === 0) {
1243
+ throw new Error(
1244
+ `No sessions imported from ${dirPath} (${skipped} file(s) skipped). Check that at least one file is in a supported session format.`
1245
+ );
1246
+ }
1247
+ status(
1248
+ "\u2192",
1249
+ dim(`Imported ${imported} session(s), skipped ${skipped}.`)
1250
+ );
1251
+ }
1252
+ async function ingestSession(targetPath) {
1253
+ const info2 = await stat(targetPath).catch(() => {
1254
+ throw new Error(`Path not found: ${targetPath}`);
1255
+ });
1256
+ if (info2.isDirectory()) {
1257
+ await ingestDirectory(targetPath);
1258
+ } else {
1259
+ await ingestSessionFile(targetPath);
1260
+ }
1261
+ status("\u2192", dim("Next: llmwiki compile"));
1262
+ }
1263
+
1264
+ // src/commands/view.ts
1265
+ import { spawn } from "child_process";
1266
+
1267
+ // src/viewer/server.ts
1268
+ import http from "http";
1269
+
1270
+ // src/linter/cache.ts
1271
+ import { mkdir as mkdir3, readFile as readFile11 } from "fs/promises";
1272
+ import path13 from "path";
1273
+ var LINT_CACHE_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
1274
+ async function writeLintCache(root, summary) {
1275
+ await mkdir3(path13.join(root, LLMWIKI_DIR), { recursive: true });
1276
+ const entry = {
1277
+ warnings: summary.warnings,
1278
+ errors: summary.errors,
1279
+ at: (/* @__PURE__ */ new Date()).toISOString()
1280
+ };
1281
+ await atomicWrite(path13.join(root, LAST_LINT_FILE), `${JSON.stringify(entry, null, 2)}
1282
+ `);
1283
+ }
1284
+ async function readLintCache(root) {
1285
+ let raw;
1286
+ try {
1287
+ raw = await readFile11(path13.join(root, LAST_LINT_FILE), "utf-8");
1288
+ } catch {
1289
+ return null;
1290
+ }
1291
+ let parsed;
1292
+ try {
1293
+ parsed = JSON.parse(raw);
1294
+ } catch {
1295
+ return null;
1296
+ }
1297
+ if (!isValidEntry(parsed)) return null;
1298
+ return { warnings: parsed.warnings, errors: parsed.errors, at: parsed.at };
1299
+ }
1300
+ function isNonNegativeInteger(value) {
1301
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
1302
+ }
1303
+ function isValidEntry(value) {
1304
+ if (typeof value !== "object" || value === null) return false;
1305
+ const candidate = value;
1306
+ return isNonNegativeInteger(candidate.warnings) && isNonNegativeInteger(candidate.errors) && typeof candidate.at === "string" && LINT_CACHE_TIMESTAMP_PATTERN.test(candidate.at);
1307
+ }
1308
+
1309
+ // src/viewer/health.ts
1310
+ async function buildHealthResponse(snapshot) {
1311
+ const lint2 = await readLintCache(snapshot.root);
1312
+ return {
1313
+ pendingReviews: snapshot.counts.pendingReviews,
1314
+ sources: snapshot.counts.compiledSources,
1315
+ sourceFiles: snapshot.counts.sourceFiles,
1316
+ concepts: snapshot.counts.concepts,
1317
+ queries: snapshot.counts.queries,
1318
+ lint: lint2
1319
+ };
1320
+ }
1321
+
1322
+ // src/viewer/shell.ts
1323
+ import { readFile as readFile12 } from "fs/promises";
1324
+ import path14 from "path";
1325
+ var PAGE_INDEX_MARKER = "<!--PAGE_INDEX-->";
1326
+ var templateCache = /* @__PURE__ */ new Map();
1327
+ async function loadShellTemplate(assetsDir) {
1328
+ const cached = templateCache.get(assetsDir);
1329
+ if (cached !== void 0) return cached;
1330
+ let bytes;
1331
+ try {
1332
+ bytes = await readFile12(path14.join(assetsDir, "index.html"), "utf-8");
1333
+ } catch {
1334
+ bytes = null;
1335
+ }
1336
+ templateCache.set(assetsDir, bytes);
1337
+ return bytes;
1338
+ }
1339
+ function substitutePageIndex(template, pages) {
1340
+ const embedded = pages.map((page) => ({
1341
+ id: page.id,
1342
+ pageDirectory: page.pageDirectory,
1343
+ slug: page.slug,
1344
+ title: page.title,
1345
+ kind: typeof page.frontmatter.kind === "string" && page.frontmatter.kind.length > 0 ? page.frontmatter.kind : "concept"
1346
+ }));
1347
+ const json = JSON.stringify({ pages: embedded }).replace(/</g, "\\u003c");
1348
+ const block = `<script type="application/json" id="page-index">${json}</script>`;
1349
+ return template.replace(PAGE_INDEX_MARKER, block);
1350
+ }
1351
+
1352
+ // src/viewer/static-assets.ts
1353
+ import { readFile as readFile13, realpath } from "fs/promises";
1354
+ import path16 from "path";
1355
+ import { fileURLToPath } from "url";
1356
+
1357
+ // src/viewer/path-safety.ts
1358
+ import path15 from "path";
1359
+ var PathSafetyError = class extends Error {
1360
+ constructor(message) {
1361
+ super(message);
1362
+ this.name = "PathSafetyError";
1363
+ }
1364
+ };
1365
+ function assertSafeSlug(decodedSlug) {
1366
+ if (typeof decodedSlug !== "string") {
1367
+ throw new PathSafetyError("slug must be a string");
1368
+ }
1369
+ if (decodedSlug.length === 0) {
1370
+ throw new PathSafetyError("slug must not be empty");
1371
+ }
1372
+ if (decodedSlug === "." || decodedSlug === "..") {
1373
+ throw new PathSafetyError(`slug must not be "${decodedSlug}"`);
1374
+ }
1375
+ if (decodedSlug.includes("/") || decodedSlug.includes("\\")) {
1376
+ throw new PathSafetyError("slug must not contain path separators");
1377
+ }
1378
+ if (decodedSlug.includes("\0")) {
1379
+ throw new PathSafetyError("slug must not contain NUL bytes");
1380
+ }
1381
+ if (path15.sep !== "/" && decodedSlug.includes(path15.sep)) {
1382
+ throw new PathSafetyError(`slug must not contain platform separator "${path15.sep}"`);
1383
+ }
1384
+ }
1385
+
1386
+ // src/viewer/static-assets.ts
1387
+ var ASSETS_DIR = path16.join(
1388
+ path16.dirname(fileURLToPath(import.meta.url)),
1389
+ "viewer/assets"
1390
+ );
1391
+ var ASSET_CONTENT_TYPES = {
1392
+ ".html": "text/html; charset=utf-8",
1393
+ ".css": "text/css; charset=utf-8",
1394
+ ".js": "application/javascript; charset=utf-8",
1395
+ ".svg": "image/svg+xml",
1396
+ ".png": "image/png"
1397
+ };
1398
+ async function handleAsset(res, pathname) {
1399
+ const segments = decodeAssetSegments(pathname);
1400
+ if (!segments) {
1401
+ writeAssetError(res, 400, "bad_asset_path", "Bad asset path.");
1402
+ return;
1403
+ }
1404
+ if (segments.length === 0) {
1405
+ writeAssetError(res, 404, "asset_not_found", "Asset not found.");
1406
+ return;
1407
+ }
1408
+ const contentType = ASSET_CONTENT_TYPES[path16.extname(segments[segments.length - 1]).toLowerCase()];
1409
+ if (!contentType) {
1410
+ writeAssetError(res, 404, "asset_not_found", "Asset not found.");
1411
+ return;
1412
+ }
1413
+ const resolved = await resolveAssetPath(segments);
1414
+ if (!resolved) {
1415
+ writeAssetError(res, 404, "asset_not_found", "Asset not found.");
1416
+ return;
1417
+ }
1418
+ try {
1419
+ const body = await readFile13(resolved);
1420
+ res.statusCode = 200;
1421
+ res.setHeader("Content-Type", contentType);
1422
+ res.end(body);
1423
+ } catch {
1424
+ writeAssetError(res, 404, "asset_not_found", "Asset not found.");
1425
+ }
1426
+ }
1427
+ function decodeAssetSegments(pathname) {
1428
+ const trimmed = pathname.replace(/^\/assets\//, "");
1429
+ if (trimmed.length === 0) return [];
1430
+ const decoded = [];
1431
+ for (const raw of trimmed.split("/")) {
1432
+ let segment;
1433
+ try {
1434
+ segment = decodeURIComponent(raw);
1435
+ } catch {
1436
+ return null;
1437
+ }
1438
+ try {
1439
+ assertSafeSlug(segment);
1440
+ } catch (err) {
1441
+ if (err instanceof PathSafetyError) return null;
1442
+ throw err;
1443
+ }
1444
+ decoded.push(segment);
1445
+ }
1446
+ return decoded;
1447
+ }
1448
+ async function resolveAssetPath(segments) {
1449
+ const candidate = path16.join(ASSETS_DIR, ...segments);
1450
+ let resolved;
1451
+ try {
1452
+ resolved = await realpath(candidate);
1453
+ } catch {
1454
+ return null;
1455
+ }
1456
+ const baseReal = await realpath(ASSETS_DIR).catch(() => ASSETS_DIR);
1457
+ if (resolved === baseReal) return resolved;
1458
+ const prefix = baseReal.endsWith(path16.sep) ? baseReal : baseReal + path16.sep;
1459
+ return resolved.startsWith(prefix) ? resolved : null;
1460
+ }
1461
+ function writeAssetError(res, status2, code, message) {
1462
+ res.statusCode = status2;
1463
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
1464
+ res.end(JSON.stringify({ error: { code, message } }));
1465
+ }
1466
+
1467
+ // src/viewer/render.ts
1468
+ import MarkdownIt from "markdown-it";
1469
+ import sanitizeHtml from "sanitize-html";
1470
+
1471
+ // src/wiki/collect.ts
1472
+ import { readdir as readdir2, readFile as readFile14, realpath as realpath2 } from "fs/promises";
1473
+ import path17 from "path";
1474
+ var WIKILINK_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
1475
+ function extractWikilinkSlugs(body) {
1476
+ const slugs = /* @__PURE__ */ new Set();
1477
+ WIKILINK_RE.lastIndex = 0;
1478
+ let match;
1479
+ while ((match = WIKILINK_RE.exec(body)) !== null) {
1480
+ slugs.add(slugify(match[1].trim()));
1481
+ }
1482
+ return [...slugs];
1483
+ }
1484
+ async function safeRealpath(p) {
1485
+ try {
1486
+ return await realpath2(p);
1487
+ } catch {
1488
+ return null;
1489
+ }
1490
+ }
1491
+ function isInsideDir(child, dir) {
1492
+ if (child === dir) return true;
1493
+ const prefix = dir.endsWith(path17.sep) ? dir : dir + path17.sep;
1494
+ return child.startsWith(prefix);
1495
+ }
1496
+ async function parsePageFile(filePath, slug, pageDirectory) {
1497
+ let raw;
1498
+ try {
1499
+ raw = await readFile14(filePath, "utf-8");
1500
+ } catch {
1501
+ return null;
1502
+ }
1503
+ const { meta, body, hasFrontmatterBlock, malformedFrontmatter } = parseFrontmatterStatus(raw);
1504
+ const title = typeof meta.title === "string" && meta.title.length > 0 ? meta.title : void 0;
1505
+ return {
1506
+ slug,
1507
+ pageDirectory,
1508
+ filePath,
1509
+ title,
1510
+ frontmatter: meta,
1511
+ body,
1512
+ parseStatus: {
1513
+ hasFrontmatterBlock,
1514
+ malformedFrontmatter,
1515
+ hasTitle: title !== void 0,
1516
+ orphaned: meta.orphaned === true
1517
+ }
1518
+ };
1519
+ }
1520
+ async function collectFromDir(canonicalRoot, pageDirectory, subdir) {
1521
+ const expectedDir = path17.join(canonicalRoot, subdir);
1522
+ const realDir = await safeRealpath(expectedDir);
1523
+ if (realDir !== expectedDir) return [];
1524
+ let files;
1525
+ try {
1526
+ files = await readdir2(realDir);
1527
+ } catch {
1528
+ return [];
1529
+ }
1530
+ const pages = [];
1531
+ for (const file of files.filter((f) => f.endsWith(".md"))) {
1532
+ const candidate = path17.join(realDir, file);
1533
+ const resolved = await safeRealpath(candidate);
1534
+ if (!resolved || !isInsideDir(resolved, realDir)) continue;
1535
+ const slug = file.replace(/\.md$/, "");
1536
+ const page = await parsePageFile(resolved, slug, pageDirectory);
1537
+ if (page) pages.push(page);
1538
+ }
1539
+ return pages;
1540
+ }
1541
+ async function collectRawWikiPages(root) {
1542
+ const canonicalRoot = await safeRealpath(root);
1543
+ if (!canonicalRoot) return [];
1544
+ const [concepts, queries] = await Promise.all([
1545
+ collectFromDir(canonicalRoot, "concepts", CONCEPTS_DIR),
1546
+ collectFromDir(canonicalRoot, "queries", QUERIES_DIR)
1547
+ ]);
1548
+ return [...concepts, ...queries];
1549
+ }
1550
+
1551
+ // src/viewer/collect.ts
1552
+ async function collectViewerPages(root) {
1553
+ const raw = await collectRawWikiPages(root);
1554
+ return decoratePages(raw);
1555
+ }
1556
+ function resolveBareSlug(slug, pages) {
1557
+ if (slug.length === 0) return null;
1558
+ const concept = pages.find((p) => p.pageDirectory === "concepts" && p.slug === slug);
1559
+ if (concept) return concept.id;
1560
+ const query = pages.find((p) => p.pageDirectory === "queries" && p.slug === slug);
1561
+ if (query) return query.id;
1562
+ return null;
1563
+ }
1564
+ function resolveBareSlugList(targets, pages) {
1565
+ const seen = /* @__PURE__ */ new Set();
1566
+ const ordered = [];
1567
+ for (const target of targets) {
1568
+ const resolved = resolveBareSlug(target, pages);
1569
+ if (resolved && !seen.has(resolved)) {
1570
+ seen.add(resolved);
1571
+ ordered.push(resolved);
1572
+ }
1573
+ }
1574
+ return ordered;
1575
+ }
1576
+ function decoratePages(raw) {
1577
+ const shells = raw.map(buildPageShell);
1578
+ for (const page of shells) {
1579
+ const targets = extractWikilinkSlugs(page.body);
1580
+ page.outgoingLinks = resolveBareSlugList(targets, shells);
1581
+ }
1582
+ return shells;
1583
+ }
1584
+ function buildPageShell(page) {
1585
+ const id = `${page.pageDirectory}/${page.slug}`;
1586
+ return {
1587
+ id,
1588
+ slug: page.slug,
1589
+ pageDirectory: page.pageDirectory,
1590
+ title: page.title ?? page.slug,
1591
+ filePath: page.filePath,
1592
+ frontmatter: page.frontmatter,
1593
+ body: page.body,
1594
+ outgoingLinks: [],
1595
+ citations: extractClaimCitations(page.body),
1596
+ warnings: warningsFromParseStatus(page)
1597
+ };
1598
+ }
1599
+ function warningsFromParseStatus(page) {
1600
+ const warnings = [];
1601
+ if (!page.parseStatus.hasFrontmatterBlock) {
1602
+ warnings.push({
1603
+ code: "missing_frontmatter",
1604
+ message: `Page "${page.slug}" has no frontmatter block.`
1605
+ });
1606
+ } else if (page.parseStatus.malformedFrontmatter) {
1607
+ warnings.push({
1608
+ code: "malformed_frontmatter",
1609
+ message: `Page "${page.slug}" has malformed YAML frontmatter.`
1610
+ });
1611
+ }
1612
+ if (!page.parseStatus.hasTitle) {
1613
+ warnings.push({
1614
+ code: "missing_title",
1615
+ message: `Page "${page.slug}" has no frontmatter title; displaying slug.`
1616
+ });
1617
+ }
1618
+ return warnings;
1619
+ }
1620
+
1621
+ // src/viewer/markdown-it-helpers.ts
1622
+ function escapeHtml(input) {
1623
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1624
+ }
1625
+ function currentLinkLevel(state) {
1626
+ const lifted = state;
1627
+ return typeof lifted.linkLevel === "number" ? lifted.linkLevel : 0;
1628
+ }
1629
+ function shouldDeferInlineRule(state, silent) {
1630
+ if (currentLinkLevel(state) > 0) return true;
1631
+ if (silent) return true;
1632
+ return false;
1633
+ }
1634
+
1635
+ // src/viewer/wikilink-rule.ts
1636
+ var OPEN = "[";
1637
+ var CHAR_OPEN_BRACKET = 91;
1638
+ function registerWikilink(md, context) {
1639
+ md.inline.ruler.after("link", "wikilink", buildParser(context));
1640
+ md.renderer.rules.wikilink = (tokens, idx) => renderWikilinkToken(tokens[idx]);
1641
+ }
1642
+ function buildParser(context) {
1643
+ return function parseWikilink(state, silent) {
1644
+ if (state.src.charCodeAt(state.pos) !== CHAR_OPEN_BRACKET) return false;
1645
+ if (state.src.charCodeAt(state.pos + 1) !== CHAR_OPEN_BRACKET) return false;
1646
+ if (shouldDeferInlineRule(state, silent)) return false;
1647
+ const closeAt = state.src.indexOf("]]", state.pos + 2);
1648
+ if (closeAt < 0) return false;
1649
+ const inner = state.src.slice(state.pos + 2, closeAt);
1650
+ if (inner.includes("\n") || inner.includes(OPEN)) return false;
1651
+ const { rawTarget, display } = splitTargetAndAlias(inner);
1652
+ const slug = slugify(rawTarget.trim());
1653
+ const resolved = resolveBareSlug(slug, context.pages);
1654
+ pushWikilinkToken(state, resolved, slug, display);
1655
+ state.pos = closeAt + 2;
1656
+ return true;
1657
+ };
1658
+ }
1659
+ function splitTargetAndAlias(inner) {
1660
+ const pipe = inner.indexOf("|");
1661
+ if (pipe < 0) return { rawTarget: inner, display: inner.trim() };
1662
+ return {
1663
+ rawTarget: inner.slice(0, pipe),
1664
+ display: inner.slice(pipe + 1).trim() || inner.slice(0, pipe).trim()
1665
+ };
1666
+ }
1667
+ function pushWikilinkToken(state, resolved, slug, display) {
1668
+ const token = state.push("wikilink", "", 0);
1669
+ token.meta = { resolved, slug, display };
1670
+ }
1671
+ function renderWikilinkToken(token) {
1672
+ const meta = token.meta;
1673
+ const display = escapeHtml(meta.display || meta.slug);
1674
+ if (!meta.resolved) {
1675
+ return `<span data-missing="true">[[${display}]]</span>`;
1676
+ }
1677
+ const href = `#/${encodeUriSegment(meta.resolved)}`;
1678
+ return `<a class="wikilink" data-page-id="${escapeHtml(meta.resolved)}" href="${escapeHtml(href)}">${display}</a>`;
1679
+ }
1680
+ function encodeUriSegment(id) {
1681
+ const [directory, slug] = id.split("/");
1682
+ return `${encodeURIComponent(directory)}/${encodeURIComponent(slug)}`;
1683
+ }
1684
+
1685
+ // src/viewer/citation-rule.ts
1686
+ import path18 from "path";
1687
+ import { pathToFileURL } from "url";
1688
+ var CHAR_CARET = 94;
1689
+ var CHAR_OPEN_BRACKET2 = 91;
1690
+ function registerCitation(md, context) {
1691
+ md.inline.ruler.after("link", "citation", buildParser2(context));
1692
+ md.renderer.rules.citation = (tokens, idx) => renderCitationToken(tokens[idx]);
1693
+ }
1694
+ function buildParser2(context) {
1695
+ return function parseCitation(state, silent) {
1696
+ if (state.src.charCodeAt(state.pos) !== CHAR_CARET) return false;
1697
+ if (state.src.charCodeAt(state.pos + 1) !== CHAR_OPEN_BRACKET2) return false;
1698
+ if (shouldDeferInlineRule(state, silent)) return false;
1699
+ const closeAt = state.src.indexOf("]", state.pos + 2);
1700
+ if (closeAt < 0) return false;
1701
+ const inner = state.src.slice(state.pos + 2, closeAt);
1702
+ if (inner.includes("\n")) return false;
1703
+ const citations = extractClaimCitations(`^[${inner}]`);
1704
+ pushChipTokens(state, citations, context);
1705
+ state.pos = closeAt + 1;
1706
+ return true;
1707
+ };
1708
+ }
1709
+ function pushChipTokens(state, citations, context) {
1710
+ for (const citation of citations) {
1711
+ for (const span of citation.spans) {
1712
+ const token = state.push("citation", "", 0);
1713
+ token.meta = buildChipMeta(span, context);
1714
+ }
1715
+ }
1716
+ }
1717
+ function buildChipMeta(span, context) {
1718
+ const meta = {
1719
+ file: span.file,
1720
+ lineStart: span.lines?.start,
1721
+ lineEnd: span.lines?.end,
1722
+ resolved: context.sourceFiles.has(span.file)
1723
+ };
1724
+ if (context.isLoopback && meta.resolved && isBareFilename(span.file)) {
1725
+ const absolutePath = path18.join(context.root, "sources", span.file);
1726
+ meta.absolutePath = absolutePath;
1727
+ meta.editorHref = buildEditorHref(absolutePath, meta.lineStart);
1728
+ }
1729
+ return meta;
1730
+ }
1731
+ function buildEditorHref(absolutePath, lineStart) {
1732
+ const encodedPath = pathToFileURL(absolutePath).pathname;
1733
+ if (lineStart === void 0) return `vscode://file${encodedPath}`;
1734
+ return `vscode://file${encodedPath}:${lineStart}`;
1735
+ }
1736
+ function isBareFilename(file) {
1737
+ if (file.length === 0) return false;
1738
+ if (file.includes("/") || file.includes("\\") || file.includes("\0")) return false;
1739
+ if (file === "." || file === "..") return false;
1740
+ return true;
1741
+ }
1742
+ function renderCitationToken(token) {
1743
+ const meta = token.meta;
1744
+ const label = formatChipLabel(meta);
1745
+ const attrs = chipAttributes(meta);
1746
+ return `<span ${attrs}>${escapeHtml(label)}</span>`;
1747
+ }
1748
+ function chipAttributes(meta) {
1749
+ const parts = [
1750
+ `class="citation-chip"`,
1751
+ `data-file="${escapeHtml(meta.file)}"`,
1752
+ `data-resolved="${meta.resolved ? "true" : "false"}"`
1753
+ ];
1754
+ if (meta.lineStart !== void 0) {
1755
+ parts.push(`data-line-start="${meta.lineStart}"`);
1756
+ }
1757
+ if (meta.lineEnd !== void 0) {
1758
+ parts.push(`data-line-end="${meta.lineEnd}"`);
1759
+ }
1760
+ if (meta.absolutePath !== void 0) {
1761
+ parts.push(`data-absolute-path="${escapeHtml(meta.absolutePath)}"`);
1762
+ }
1763
+ if (meta.editorHref !== void 0) {
1764
+ parts.push(`data-editor-href="${escapeHtml(meta.editorHref)}"`);
1765
+ }
1766
+ return parts.join(" ");
1767
+ }
1768
+ function formatChipLabel(meta) {
1769
+ if (meta.lineStart === void 0) return meta.file;
1770
+ if (meta.lineEnd === void 0 || meta.lineEnd === meta.lineStart) {
1771
+ return `${meta.file}:${meta.lineStart}`;
1772
+ }
1773
+ return `${meta.file}:${meta.lineStart}-${meta.lineEnd}`;
1774
+ }
1775
+
1776
+ // src/viewer/render.ts
1777
+ function renderPageHtml(body, snapshot, options) {
1778
+ const md = buildMarkdownIt(snapshot, options);
1779
+ const rendered = md.render(body);
1780
+ const html = sanitizeHtml(rendered, buildSanitizerPolicy(options));
1781
+ return { html };
1782
+ }
1783
+ function buildMarkdownIt(snapshot, options) {
1784
+ const md = new MarkdownIt({
1785
+ html: false,
1786
+ linkify: false,
1787
+ breaks: false
1788
+ });
1789
+ registerWikilink(md, { pages: snapshot.pages });
1790
+ registerCitation(md, {
1791
+ root: snapshot.root,
1792
+ sourceFiles: new Set(snapshot.sourceFilenames),
1793
+ isLoopback: options.isLoopback
1794
+ });
1795
+ return md;
1796
+ }
1797
+ function buildSanitizerPolicy(options) {
1798
+ const allowedSchemes = ["http", "https", "mailto"];
1799
+ const allowedSchemesAppliedToAttributes = ["href", "src", "cite"];
1800
+ return {
1801
+ allowedTags: [
1802
+ "h1",
1803
+ "h2",
1804
+ "h3",
1805
+ "h4",
1806
+ "h5",
1807
+ "h6",
1808
+ "p",
1809
+ "br",
1810
+ "hr",
1811
+ "ul",
1812
+ "ol",
1813
+ "li",
1814
+ "blockquote",
1815
+ "strong",
1816
+ "em",
1817
+ "b",
1818
+ "i",
1819
+ "s",
1820
+ "u",
1821
+ "code",
1822
+ "pre",
1823
+ "table",
1824
+ "thead",
1825
+ "tbody",
1826
+ "tfoot",
1827
+ "tr",
1828
+ "th",
1829
+ "td",
1830
+ "a",
1831
+ "img",
1832
+ "span",
1833
+ "div"
1834
+ ],
1835
+ disallowedTagsMode: "discard",
1836
+ allowedAttributes: {
1837
+ a: ["href", "title", "class", "id", "data-*", "aria-*"],
1838
+ img: ["src", "alt", "title", "class", "id"],
1839
+ span: ["class", "id", "data-*", "aria-*"],
1840
+ div: ["class", "id", "data-*", "aria-*"],
1841
+ th: ["scope", "colspan", "rowspan", "class", "id"],
1842
+ td: ["colspan", "rowspan", "class", "id"],
1843
+ table: ["class", "id"],
1844
+ code: ["class"],
1845
+ "*": ["class", "id"]
1846
+ },
1847
+ allowedSchemes,
1848
+ allowedSchemesByTag: {
1849
+ a: buildAnchorSchemes(),
1850
+ img: ["http", "https", "data"]
1851
+ },
1852
+ allowedSchemesAppliedToAttributes,
1853
+ allowProtocolRelative: false,
1854
+ // `allowedAttributes` above whitelists `class` everywhere via `*`,
1855
+ // so no further class-name allowlist is needed; leaving
1856
+ // `allowedClasses` unset lets every class value through.
1857
+ allowedStyles: {},
1858
+ allowedIframeHostnames: [],
1859
+ transformTags: {
1860
+ a: filterAnchorHref(),
1861
+ img: filterImgSrc,
1862
+ span: filterSpanForLanBind(options)
1863
+ }
1864
+ // sanitize-html's URL filter does not enforce hash-only links by
1865
+ // default; the anchor transform above whitelists `#/…` explicitly.
1866
+ };
1867
+ }
1868
+ function filterSpanForLanBind(options) {
1869
+ return function transformSpan(tagName, attribs) {
1870
+ if (options.isLoopback) return { tagName, attribs };
1871
+ if (!("data-absolute-path" in attribs) && !("data-editor-href" in attribs)) {
1872
+ return { tagName, attribs };
1873
+ }
1874
+ const stripped = {};
1875
+ for (const [key, value] of Object.entries(attribs)) {
1876
+ if (key === "data-absolute-path" || key === "data-editor-href") continue;
1877
+ stripped[key] = value;
1878
+ }
1879
+ return { tagName, attribs: stripped };
1880
+ };
1881
+ }
1882
+ function buildAnchorSchemes() {
1883
+ return ["http", "https", "mailto"];
1884
+ }
1885
+ function filterAnchorHref() {
1886
+ return function transformAnchor(tagName, attribs) {
1887
+ const href = attribs.href;
1888
+ if (typeof href !== "string" || href.length === 0) return { tagName, attribs };
1889
+ if (isAllowedAnchorHref(href)) return { tagName, attribs };
1890
+ const stripped = { ...attribs };
1891
+ delete stripped.href;
1892
+ return { tagName, attribs: stripped };
1893
+ };
1894
+ }
1895
+ function filterImgSrc(tagName, attribs) {
1896
+ const src = attribs.src;
1897
+ if (typeof src !== "string" || src.length === 0) return { tagName, attribs };
1898
+ if (isAllowedImgSrc(src)) return { tagName, attribs };
1899
+ const stripped = { ...attribs };
1900
+ delete stripped.src;
1901
+ return { tagName, attribs: stripped };
1902
+ }
1903
+ function isAllowedAnchorHref(href) {
1904
+ if (href.startsWith("#")) return true;
1905
+ if (href.startsWith("http://") || href.startsWith("https://")) return true;
1906
+ if (href.startsWith("mailto:")) return true;
1907
+ return false;
1908
+ }
1909
+ function isAllowedImgSrc(src) {
1910
+ if (src.startsWith("http://") || src.startsWith("https://")) return true;
1911
+ if (src.startsWith("data:image/")) return true;
1912
+ return false;
1913
+ }
1914
+
1915
+ // src/viewer/search.ts
1916
+ var MAX_QUERY_LENGTH = 200;
1917
+ var MAX_RESULTS = 50;
1918
+ var SNIPPET_RADIUS = 60;
1919
+ var SNIPPET_ELLIPSIS = "\u2026";
1920
+ function searchPages(snapshot, rawQuery) {
1921
+ const tokens = tokenizeQuery(rawQuery);
1922
+ if (tokens.length === 0) return { results: [] };
1923
+ const matches = collectMatches(snapshot.pages, tokens);
1924
+ matches.sort(compareResults);
1925
+ return { results: matches.slice(0, MAX_RESULTS) };
1926
+ }
1927
+ function tokenizeQuery(rawQuery) {
1928
+ if (typeof rawQuery !== "string") return [];
1929
+ const trimmed = rawQuery.trim();
1930
+ if (trimmed.length === 0) return [];
1931
+ const capped = trimmed.slice(0, MAX_QUERY_LENGTH).toLowerCase();
1932
+ return capped.split(/\s+/).filter((t) => t.length > 0);
1933
+ }
1934
+ function collectMatches(pages, tokens) {
1935
+ const matches = [];
1936
+ for (const page of pages) {
1937
+ const result = matchPage(page, tokens);
1938
+ if (result) matches.push(result);
1939
+ }
1940
+ return matches;
1941
+ }
1942
+ function matchPage(page, tokens) {
1943
+ const titleLower = page.title.toLowerCase();
1944
+ const bodyLower = page.body.toLowerCase();
1945
+ for (const token of tokens) {
1946
+ if (!titleLower.includes(token) && !bodyLower.includes(token)) return null;
1947
+ }
1948
+ const allInTitle = tokens.every((t) => titleLower.includes(t));
1949
+ if (allInTitle) return rowFromPage(page, page.title, "title");
1950
+ const snippet = buildBodySnippet(page.body, bodyLower, tokens);
1951
+ return rowFromPage(page, snippet, "body");
1952
+ }
1953
+ function rowFromPage(page, snippet, matchedIn) {
1954
+ return {
1955
+ id: page.id,
1956
+ pageDirectory: page.pageDirectory,
1957
+ title: page.title,
1958
+ snippet,
1959
+ matchedIn
1960
+ };
1961
+ }
1962
+ function buildBodySnippet(body, bodyLower, tokens) {
1963
+ const matchPos = earliestTokenPosition(bodyLower, tokens);
1964
+ const start = Math.max(0, matchPos - SNIPPET_RADIUS);
1965
+ const end = Math.min(body.length, matchPos + SNIPPET_RADIUS);
1966
+ const cleaned = stripInlineMarkdownNoise(body.slice(start, end)).replace(/\s+/g, " ").trim();
1967
+ const prefix = start > 0 ? SNIPPET_ELLIPSIS : "";
1968
+ const suffix = end < body.length ? SNIPPET_ELLIPSIS : "";
1969
+ return `${prefix}${cleaned}${suffix}`;
1970
+ }
1971
+ function stripInlineMarkdownNoise(text) {
1972
+ return text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/\[\[([^\]|\n]+)\|([^\]\n]+)\]\]/g, "$2").replace(/\[\[([^\]\n]+)\]\]/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/(?<!\w)\*([^*\n]+)\*(?!\w)/g, "$1").replace(/(?<!\w)_([^_\n]+)_(?!\w)/g, "$1").replace(/`([^`\n]+)`/g, "$1").replace(/~~([^~\n]+)~~/g, "$1");
1973
+ }
1974
+ function earliestTokenPosition(bodyLower, tokens) {
1975
+ let earliest = bodyLower.length;
1976
+ for (const token of tokens) {
1977
+ const idx = bodyLower.indexOf(token);
1978
+ if (idx >= 0 && idx < earliest) earliest = idx;
1979
+ }
1980
+ return earliest;
1981
+ }
1982
+ function compareResults(a, b) {
1983
+ if (a.matchedIn !== b.matchedIn) {
1984
+ return a.matchedIn === "title" ? -1 : 1;
1985
+ }
1986
+ return a.title.localeCompare(b.title);
1987
+ }
1988
+
1989
+ // src/viewer/server.ts
1990
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1"]);
1991
+ var CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'; form-action 'none'";
1992
+ async function startViewerServer(snapshot, config) {
1993
+ const boundConfig = { ...config };
1994
+ const server = http.createServer((req, res) => {
1995
+ handleRequest(req, res, snapshot, boundConfig).catch((err) => {
1996
+ void err;
1997
+ if (!res.headersSent) {
1998
+ writeJsonError(res, 500, "internal_error", "Unexpected server error.");
1999
+ }
2000
+ });
2001
+ });
2002
+ await new Promise((resolve, reject) => {
2003
+ const onError = (err) => {
2004
+ server.off("listening", onListening);
2005
+ reject(err);
2006
+ };
2007
+ const onListening = () => {
2008
+ server.off("error", onError);
2009
+ resolve();
2010
+ };
2011
+ server.once("error", onError);
2012
+ server.once("listening", onListening);
2013
+ server.listen(config.port, config.host);
2014
+ });
2015
+ const address = server.address();
2016
+ if (!address) throw new Error("server bound but address is null");
2017
+ boundConfig.port = address.port;
2018
+ return {
2019
+ host: config.host,
2020
+ port: address.port,
2021
+ close: () => new Promise((resolve) => server.close(() => resolve()))
2022
+ };
2023
+ }
2024
+ async function handleRequest(req, res, snapshot, config) {
2025
+ applySecurityHeaders(res);
2026
+ if (!validateOriginHeaders(req, config)) {
2027
+ writeJsonError(res, 403, "forbidden", "rejected by origin policy");
2028
+ return;
2029
+ }
2030
+ const url = new URL(req.url ?? "/", buildOriginBase(config));
2031
+ if (!isRouteRegistered(req.method, url.pathname)) {
2032
+ writeJsonError(res, 404, "not_found", `${req.method ?? "?"} ${url.pathname}`);
2033
+ return;
2034
+ }
2035
+ await routeRegistered(req, res, url, snapshot, LOOPBACK_HOSTS.has(config.host));
2036
+ }
2037
+ async function routeRegistered(req, res, parsedUrl, snapshot, isLoopback) {
2038
+ if (parsedUrl.pathname === "/") return handleShell(res, snapshot);
2039
+ if (parsedUrl.pathname.startsWith("/assets/")) return handleAsset(res, parsedUrl.pathname);
2040
+ if (parsedUrl.pathname === "/api/pages") return handleApiPages(res, snapshot);
2041
+ if (parsedUrl.pathname === "/api/index") return handleApiIndex(res, snapshot, isLoopback);
2042
+ if (parsedUrl.pathname === "/api/health") return handleApiHealth(res, snapshot);
2043
+ if (parsedUrl.pathname === "/api/search") return handleApiSearch(res, parsedUrl, snapshot);
2044
+ if (parsedUrl.pathname.startsWith("/api/page/")) {
2045
+ return handleApiPage(res, parsedUrl.pathname, snapshot, isLoopback);
2046
+ }
2047
+ throw new Error(`route registration drift: no handler for ${parsedUrl.pathname}`);
2048
+ }
2049
+ function isRouteRegistered(method, pathname) {
2050
+ if (method !== "GET") return false;
2051
+ if (pathname === "/") return true;
2052
+ if (pathname.startsWith("/assets/")) return true;
2053
+ if (pathname === "/api/pages") return true;
2054
+ if (pathname === "/api/index") return true;
2055
+ if (pathname === "/api/health") return true;
2056
+ if (pathname === "/api/search") return true;
2057
+ if (pathname.startsWith("/api/page/")) return true;
2058
+ return false;
2059
+ }
2060
+ function applySecurityHeaders(res) {
2061
+ res.setHeader("Content-Security-Policy", CONTENT_SECURITY_POLICY);
2062
+ res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
2063
+ res.setHeader("X-Content-Type-Options", "nosniff");
2064
+ res.setHeader("Referrer-Policy", "no-referrer");
2065
+ }
2066
+ function validateOriginHeaders(req, config) {
2067
+ const host = req.headers.host;
2068
+ if (!host || !isAcceptableHost(host, config)) return false;
2069
+ const origin = req.headers.origin;
2070
+ if (typeof origin === "string" && origin.length > 0) {
2071
+ if (!isSameOrigin(origin, config)) return false;
2072
+ }
2073
+ const fetchSite = req.headers["sec-fetch-site"];
2074
+ if (fetchSite === "cross-site") return false;
2075
+ return true;
2076
+ }
2077
+ function isAcceptableHost(hostHeader, config) {
2078
+ for (const acceptable of buildAcceptableHostHeaders(config)) {
2079
+ if (hostHeader === acceptable) return true;
2080
+ }
2081
+ return false;
2082
+ }
2083
+ function buildAcceptableHostHeaders(config) {
2084
+ const formattedBind = formatHostHeader(config.host, config.port);
2085
+ const accepted = [formattedBind];
2086
+ if (config.host === "127.0.0.1" || config.host === "::1") {
2087
+ accepted.push(`localhost:${config.port}`);
2088
+ }
2089
+ return accepted;
2090
+ }
2091
+ function isSameOrigin(origin, config) {
2092
+ try {
2093
+ const parsed = new URL(origin);
2094
+ const expectedHostname = normalizeHostnameForOrigin(config.host);
2095
+ const originHostname = normalizeHostnameForOrigin(parsed.hostname);
2096
+ return originHostname === expectedHostname && Number(parsed.port) === config.port;
2097
+ } catch {
2098
+ return false;
2099
+ }
2100
+ }
2101
+ function formatHostHeader(host, port) {
2102
+ if (host.includes(":")) return `[${host}]:${port}`;
2103
+ return `${host}:${port}`;
2104
+ }
2105
+ function buildOriginBase(config) {
2106
+ if (config.host.includes(":")) return `http://[${config.host}]:${config.port}`;
2107
+ return `http://${config.host}:${config.port}`;
2108
+ }
2109
+ function normalizeHostnameForOrigin(host) {
2110
+ let h = host.toLowerCase();
2111
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
2112
+ return h;
2113
+ }
2114
+ async function handleShell(res, snapshot) {
2115
+ const template = await loadShellTemplate(ASSETS_DIR);
2116
+ if (template === null) {
2117
+ writeJsonError(res, 500, "shell_missing", "Viewer shell template not found on disk.");
2118
+ return;
2119
+ }
2120
+ const body = substitutePageIndex(template, snapshot.pages);
2121
+ res.statusCode = 200;
2122
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
2123
+ res.end(body);
2124
+ }
2125
+ function handleApiPages(res, snapshot) {
2126
+ writeJson(res, 200, {
2127
+ project: snapshot.project,
2128
+ counts: {
2129
+ concepts: snapshot.counts.concepts,
2130
+ queries: snapshot.counts.queries,
2131
+ sourceFiles: snapshot.counts.sourceFiles,
2132
+ pendingReviews: snapshot.counts.pendingReviews
2133
+ },
2134
+ index: { available: snapshot.index.available, href: snapshot.index.href },
2135
+ recentPages: snapshot.recentPages,
2136
+ pages: snapshot.pages.map(pageListRow),
2137
+ updatedAt: snapshot.generatedAt
2138
+ });
2139
+ }
2140
+ function pageListRow(page) {
2141
+ return {
2142
+ id: page.id,
2143
+ pageDirectory: page.pageDirectory,
2144
+ slug: page.slug,
2145
+ title: page.title,
2146
+ kind: typeof page.frontmatter.kind === "string" ? page.frontmatter.kind : "concept",
2147
+ summary: typeof page.frontmatter.summary === "string" ? page.frontmatter.summary : "",
2148
+ updatedAt: typeof page.frontmatter.updatedAt === "string" ? page.frontmatter.updatedAt : "",
2149
+ warnings: page.warnings
2150
+ };
2151
+ }
2152
+ function handleApiIndex(res, snapshot, isLoopback) {
2153
+ if (!snapshot.index.available) {
2154
+ writeJsonError(res, 404, "index_unavailable", "wiki/index.md is not present.");
2155
+ return;
2156
+ }
2157
+ const rendered = tryRenderBody(snapshot.index.body, snapshot, isLoopback);
2158
+ if (rendered === null) {
2159
+ writeRenderFailed(res);
2160
+ return;
2161
+ }
2162
+ writeJson(res, 200, {
2163
+ html: rendered.html,
2164
+ outgoingLinks: snapshot.index.outgoingLinks,
2165
+ generatedAt: snapshot.generatedAt
2166
+ });
2167
+ }
2168
+ async function handleApiHealth(res, snapshot) {
2169
+ const health = await buildHealthResponse(snapshot);
2170
+ writeJson(res, 200, health);
2171
+ }
2172
+ function handleApiSearch(res, parsedUrl, snapshot) {
2173
+ const query = parsedUrl.searchParams.get("q") ?? "";
2174
+ writeJson(res, 200, searchPages(snapshot, query));
2175
+ }
2176
+ function handleApiPage(res, pathname, snapshot, isLoopback) {
2177
+ const segments = pathname.replace(/^\/api\/page\//, "").split("/");
2178
+ if (segments.length !== 2) {
2179
+ writeJsonError(res, 400, "bad_request", "Expected /api/page/:directory/:slug");
2180
+ return;
2181
+ }
2182
+ const [directorySegment, encodedSlug] = segments;
2183
+ const decodedSlug = safeDecodeSlug(directorySegment, encodedSlug);
2184
+ if (!decodedSlug) {
2185
+ writeJsonError(res, 400, "bad_request", "Invalid directory or slug.");
2186
+ return;
2187
+ }
2188
+ const page = snapshot.pages.find(
2189
+ (p) => p.pageDirectory === decodedSlug.directory && p.slug === decodedSlug.slug
2190
+ );
2191
+ if (!page) {
2192
+ writeJsonError(res, 404, "page_not_found", `${decodedSlug.directory}/${decodedSlug.slug}`);
2193
+ return;
2194
+ }
2195
+ const rendered = tryRenderBody(page.body, snapshot, isLoopback);
2196
+ if (rendered === null) {
2197
+ writeRenderFailed(res);
2198
+ return;
2199
+ }
2200
+ writeJson(res, 200, pagePayload(page, snapshot, rendered.html));
2201
+ }
2202
+ function safeDecodeSlug(directorySegment, encodedSlug) {
2203
+ if (directorySegment !== "concepts" && directorySegment !== "queries") return null;
2204
+ let decoded;
2205
+ try {
2206
+ decoded = decodeURIComponent(encodedSlug);
2207
+ } catch {
2208
+ return null;
2209
+ }
2210
+ try {
2211
+ assertSafeSlug(decoded);
2212
+ } catch (err) {
2213
+ if (err instanceof PathSafetyError) return null;
2214
+ throw err;
2215
+ }
2216
+ return { directory: directorySegment, slug: decoded };
2217
+ }
2218
+ function pagePayload(page, snapshot, renderedHtml) {
2219
+ return {
2220
+ id: page.id,
2221
+ title: page.title,
2222
+ pageDirectory: page.pageDirectory,
2223
+ slug: page.slug,
2224
+ html: renderedHtml,
2225
+ citations: page.citations,
2226
+ outgoingLinks: page.outgoingLinks,
2227
+ frontmatter: page.frontmatter,
2228
+ warnings: page.warnings,
2229
+ updatedAt: typeof page.frontmatter.updatedAt === "string" ? page.frontmatter.updatedAt : "",
2230
+ createdAt: typeof page.frontmatter.createdAt === "string" ? page.frontmatter.createdAt : "",
2231
+ generatedAt: snapshot.generatedAt
2232
+ };
2233
+ }
2234
+ function tryRenderBody(body, snapshot, isLoopback) {
2235
+ try {
2236
+ return renderPageHtml(body, snapshot, { isLoopback });
2237
+ } catch {
2238
+ return null;
2239
+ }
2240
+ }
2241
+ function writeRenderFailed(res) {
2242
+ writeJsonError(res, 500, "render_failed", "Could not render page.");
2243
+ }
2244
+ function writeJson(res, status2, body) {
2245
+ res.statusCode = status2;
2246
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
2247
+ res.end(JSON.stringify(body));
2248
+ }
2249
+ function writeJsonError(res, status2, code, message) {
2250
+ writeJson(res, status2, { error: { code, message } });
2251
+ }
2252
+
2253
+ // src/viewer/snapshot.ts
2254
+ import { readdir as readdir4, readFile as readFile16, realpath as realpath3 } from "fs/promises";
2255
+ import path21 from "path";
2256
+
2257
+ // src/compiler/candidates.ts
2258
+ import { readdir as readdir3, rename as rename2, unlink, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
2259
+ import { existsSync } from "fs";
2260
+ import path19 from "path";
2261
+ import { randomBytes } from "crypto";
2262
+ var ID_SUFFIX_BYTES = 4;
2263
+ var CANDIDATE_EXT = ".json";
2264
+ function buildCandidateId(slug) {
2265
+ const suffix = randomBytes(ID_SUFFIX_BYTES).toString("hex");
2266
+ return `${slug}-${suffix}`;
2267
+ }
2268
+ function candidatePath(root, id) {
2269
+ return path19.join(root, CANDIDATES_DIR, `${id}${CANDIDATE_EXT}`);
2270
+ }
2271
+ function archivePath(root, id) {
2272
+ return path19.join(root, CANDIDATES_ARCHIVE_DIR, `${id}${CANDIDATE_EXT}`);
2273
+ }
2274
+ async function writeCandidate(root, draft) {
2275
+ const candidate = {
2276
+ id: buildCandidateId(draft.slug),
2277
+ title: draft.title,
2278
+ slug: draft.slug,
2279
+ summary: draft.summary,
2280
+ sources: draft.sources,
2281
+ body: draft.body,
2282
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2283
+ ...draft.sourceStates ? { sourceStates: draft.sourceStates } : {},
2284
+ ...draft.schemaViolations ? { schemaViolations: draft.schemaViolations } : {},
2285
+ ...draft.provenanceViolations ? { provenanceViolations: draft.provenanceViolations } : {}
2286
+ };
2287
+ await atomicWrite(candidatePath(root, candidate.id), JSON.stringify(candidate, null, 2));
2288
+ return candidate;
2289
+ }
2290
+ function failWithError(message) {
2291
+ status("!", error(message));
2292
+ process.exitCode = 1;
2293
+ return null;
2294
+ }
2295
+ async function loadCandidateOrFail(root, id) {
2296
+ const candidate = await readCandidate(root, id);
2297
+ if (!candidate) return failWithError(`Candidate not found: ${id}`);
2298
+ return candidate;
2299
+ }
2300
+ async function loadCandidateUnderLockOrFail(root, id) {
2301
+ const candidate = await readCandidate(root, id);
2302
+ if (!candidate) {
2303
+ return failWithError(`Candidate ${id} was removed by another process during review.`);
2304
+ }
2305
+ return candidate;
2306
+ }
2307
+ async function readCandidate(root, id) {
2308
+ const raw = await safeReadFile(candidatePath(root, id));
2309
+ if (!raw) return null;
2310
+ try {
2311
+ const parsed = JSON.parse(raw);
2312
+ if (!isValidCandidate(parsed)) return null;
2313
+ return parsed;
2314
+ } catch {
2315
+ return null;
2316
+ }
2317
+ }
2318
+ function isValidCandidate(value) {
2319
+ if (!value || typeof value !== "object") return false;
2320
+ const candidate = value;
2321
+ return typeof candidate.id === "string" && typeof candidate.title === "string" && typeof candidate.slug === "string" && typeof candidate.body === "string" && Array.isArray(candidate.sources);
2322
+ }
2323
+ async function listCandidates(root) {
2324
+ const dir = path19.join(root, CANDIDATES_DIR);
2325
+ if (!existsSync(dir)) return [];
2326
+ const entries = await readdir3(dir, { withFileTypes: true });
2327
+ const candidates = [];
2328
+ for (const entry of entries) {
2329
+ if (!entry.isFile() || !entry.name.endsWith(CANDIDATE_EXT)) continue;
2330
+ const id = entry.name.slice(0, -CANDIDATE_EXT.length);
2331
+ const candidate = await readCandidate(root, id);
2332
+ if (candidate) candidates.push(candidate);
2333
+ }
2334
+ candidates.sort((a, b) => a.generatedAt.localeCompare(b.generatedAt));
2335
+ return candidates;
2336
+ }
2337
+ async function countCandidates(root) {
2338
+ const candidates = await listCandidates(root);
2339
+ return candidates.length;
2340
+ }
2341
+ async function deleteCandidate(root, id) {
2342
+ const filePath = candidatePath(root, id);
2343
+ if (!existsSync(filePath)) return false;
2344
+ await unlink(filePath);
2345
+ return true;
2346
+ }
2347
+ async function archiveCandidate(root, id) {
2348
+ const sourcePath = candidatePath(root, id);
2349
+ if (!existsSync(sourcePath)) return false;
2350
+ const target = archivePath(root, id);
2351
+ await mkdir4(path19.dirname(target), { recursive: true });
2352
+ try {
2353
+ await rename2(sourcePath, target);
2354
+ } catch {
2355
+ const raw = await safeReadFile(sourcePath);
2356
+ await writeFile3(target, raw, "utf-8");
2357
+ await unlink(sourcePath);
2358
+ }
2359
+ return true;
2360
+ }
2361
+
2362
+ // src/utils/state.ts
2363
+ import { readFile as readFile15, writeFile as writeFile4, rename as rename3, mkdir as mkdir5, copyFile } from "fs/promises";
2364
+ import { existsSync as existsSync2 } from "fs";
2365
+ import path20 from "path";
2366
+ function emptyState() {
2367
+ return { version: 1, indexHash: "", sources: {} };
2368
+ }
2369
+ async function readState(root) {
2370
+ const filePath = path20.join(root, STATE_FILE);
2371
+ if (!existsSync2(filePath)) {
2372
+ return emptyState();
2373
+ }
2374
+ try {
2375
+ const raw = await readFile15(filePath, "utf-8");
2376
+ return JSON.parse(raw);
2377
+ } catch {
2378
+ const bakPath = filePath + ".bak";
2379
+ console.warn(`\u26A0 Corrupt state.json \u2014 backed up to ${bakPath}, starting fresh.`);
2380
+ await copyFile(filePath, bakPath);
2381
+ return emptyState();
2382
+ }
2383
+ }
2384
+ async function writeState(root, state) {
2385
+ const dir = path20.join(root, LLMWIKI_DIR);
2386
+ await mkdir5(dir, { recursive: true });
2387
+ const filePath = path20.join(root, STATE_FILE);
2388
+ const tmpPath = filePath + ".tmp";
2389
+ await writeFile4(tmpPath, JSON.stringify(state, null, 2), "utf-8");
2390
+ await rename3(tmpPath, filePath);
2391
+ }
2392
+ async function updateSourceState(root, sourceFile, entry) {
2393
+ const state = await readState(root);
2394
+ state.sources[sourceFile] = entry;
2395
+ await writeState(root, state);
2396
+ }
2397
+ async function removeSourceState(root, sourceFile) {
2398
+ const state = await readState(root);
2399
+ delete state.sources[sourceFile];
2400
+ await writeState(root, state);
2401
+ }
2402
+
2403
+ // src/viewer/snapshot.ts
2404
+ var RECENT_PAGES_LIMIT = 8;
2405
+ var INDEX_HREF = "/#/index";
2406
+ async function buildViewerSnapshot(root) {
2407
+ const [pages, state, pendingReviews, sourceFilenames, index] = await Promise.all([
2408
+ collectViewerPages(root),
2409
+ readState(root),
2410
+ countCandidates(root),
2411
+ listSourceFiles(root),
2412
+ readIndexFile(root)
2413
+ ]);
2414
+ const project = buildProject(root);
2415
+ const counts = {
2416
+ concepts: pages.filter((p) => p.pageDirectory === "concepts").length,
2417
+ queries: pages.filter((p) => p.pageDirectory === "queries").length,
2418
+ sourceFiles: sourceFilenames.length,
2419
+ pendingReviews,
2420
+ compiledSources: Object.keys(state.sources).length
2421
+ };
2422
+ const fullIndex = {
2423
+ available: index.available,
2424
+ href: INDEX_HREF,
2425
+ body: index.body,
2426
+ outgoingLinks: resolveBareSlugList(extractWikilinkSlugs(index.body), pages)
2427
+ };
2428
+ const sourceFileSet = new Set(sourceFilenames);
2429
+ const annotatedPages = pages.map((page) => annotateCitationWarnings(page, sourceFileSet));
2430
+ return {
2431
+ root,
2432
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2433
+ project,
2434
+ counts,
2435
+ index: fullIndex,
2436
+ recentPages: buildRecentPages(annotatedPages),
2437
+ pages: annotatedPages,
2438
+ sourceFilenames
2439
+ };
2440
+ }
2441
+ function annotateCitationWarnings(page, sourceFiles) {
2442
+ const extra = [];
2443
+ const markerPattern = /\^\[([^\]\n]+)\]/g;
2444
+ let match;
2445
+ while ((match = markerPattern.exec(page.body)) !== null) {
2446
+ appendCitationWarningsForMarker(match[1], sourceFiles, extra);
2447
+ }
2448
+ if (extra.length === 0) return page;
2449
+ return { ...page, warnings: [...page.warnings, ...extra] };
2450
+ }
2451
+ function appendCitationWarningsForMarker(raw, sourceFiles, into) {
2452
+ for (const entry of raw.split(",")) {
2453
+ const trimmed = entry.trim();
2454
+ if (trimmed.length === 0) continue;
2455
+ if (isMalformedCitationEntry(trimmed)) {
2456
+ into.push({
2457
+ code: "malformed_citation",
2458
+ message: `Malformed citation entry: ${trimmed}`
2459
+ });
2460
+ continue;
2461
+ }
2462
+ const file = trimmed.split(/[:#]/)[0];
2463
+ if (file.length > 0 && !sourceFiles.has(file)) {
2464
+ into.push({
2465
+ code: "unresolved_citation",
2466
+ message: `Source not found: ${file}`
2467
+ });
2468
+ }
2469
+ }
2470
+ }
2471
+ function buildProject(root) {
2472
+ const rootName = path21.basename(root);
2473
+ return { title: rootName, rootName };
2474
+ }
2475
+ async function listSourceFiles(root) {
2476
+ let canonicalRoot;
2477
+ try {
2478
+ canonicalRoot = await realpath3(root);
2479
+ } catch {
2480
+ return [];
2481
+ }
2482
+ const expectedDir = path21.join(canonicalRoot, SOURCES_DIR);
2483
+ let realDir;
2484
+ try {
2485
+ realDir = await realpath3(expectedDir);
2486
+ } catch {
2487
+ return [];
2488
+ }
2489
+ if (realDir !== expectedDir) return [];
2490
+ try {
2491
+ const entries = await readdir4(realDir, { withFileTypes: true });
2492
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
2493
+ } catch {
2494
+ return [];
2495
+ }
2496
+ }
2497
+ async function readIndexFile(root) {
2498
+ let canonicalRoot;
2499
+ try {
2500
+ canonicalRoot = await realpath3(root);
2501
+ } catch {
2502
+ return { available: false, body: "" };
2503
+ }
2504
+ const expectedIndex = path21.join(canonicalRoot, "wiki", "index.md");
2505
+ let resolved;
2506
+ try {
2507
+ resolved = await realpath3(expectedIndex);
2508
+ } catch {
2509
+ return { available: false, body: "" };
2510
+ }
2511
+ if (resolved !== expectedIndex) {
2512
+ return { available: false, body: "" };
2513
+ }
2514
+ try {
2515
+ const body = await readFile16(resolved, "utf-8");
2516
+ return { available: true, body };
2517
+ } catch {
2518
+ return { available: false, body: "" };
2519
+ }
2520
+ }
2521
+ function buildRecentPages(pages) {
2522
+ const rows = pages.map((page) => ({
2523
+ id: page.id,
2524
+ pageDirectory: page.pageDirectory,
2525
+ slug: page.slug,
2526
+ title: page.title,
2527
+ updatedAt: typeof page.frontmatter.updatedAt === "string" ? page.frontmatter.updatedAt : ""
2528
+ }));
2529
+ rows.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2530
+ return rows.slice(0, RECENT_PAGES_LIMIT);
2531
+ }
2532
+
2533
+ // src/commands/view.ts
2534
+ var LOOPBACK_HOST = "127.0.0.1";
2535
+ var WILDCARD_HOSTS = /* @__PURE__ */ new Set([
2536
+ "0.0.0.0",
2537
+ "::",
2538
+ "0:0:0:0:0:0:0:0",
2539
+ "0000:0000:0000:0000:0000:0000:0000:0000",
2540
+ "*"
2541
+ ]);
2542
+ async function viewCommand(options) {
2543
+ const { host, port } = resolveBindConfig(options);
2544
+ const root = process.cwd();
2545
+ const snapshot = await buildViewerSnapshot(root);
2546
+ const handle = await startViewerServer(snapshot, { host, port });
2547
+ const url = buildReadyUrl(handle.host, handle.port);
2548
+ process.stdout.write(`Viewer ready at ${url}
2549
+ `);
2550
+ if (options.open) openInBrowser(url);
2551
+ registerShutdown(handle.close);
2552
+ }
2553
+ function openInBrowser(url) {
2554
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2555
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
2556
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
2557
+ child.on("error", () => void 0);
2558
+ child.unref();
2559
+ }
2560
+ function resolveBindConfig(options) {
2561
+ const hostFlag = typeof options.host === "string" && options.host.length > 0;
2562
+ const allowLan = options.allowLan === true;
2563
+ if (hostFlag !== allowLan) {
1196
2564
  throw new Error(
1197
- `No sessions imported from ${dirPath} (${skipped} file(s) skipped). Check that at least one file is in a supported session format.`
2565
+ "Privacy gate: --host and --allow-lan must be supplied together. Use both to bind beyond loopback, or neither to keep the viewer on 127.0.0.1."
1198
2566
  );
1199
2567
  }
1200
- status(
1201
- "\u2192",
1202
- dim(`Imported ${imported} session(s), skipped ${skipped}.`)
1203
- );
2568
+ const host = hostFlag ? options.host : LOOPBACK_HOST;
2569
+ if (WILDCARD_HOSTS.has(host)) {
2570
+ throw new Error(
2571
+ `--host ${host} is not supported: wildcard binds defeat the viewer's DNS-rebind protection. Use a specific interface IP (e.g. 192.168.1.10) instead.`
2572
+ );
2573
+ }
2574
+ const port = parsePort(options.port);
2575
+ return { host, port };
1204
2576
  }
1205
- async function ingestSession(targetPath) {
1206
- const info2 = await stat(targetPath).catch(() => {
1207
- throw new Error(`Path not found: ${targetPath}`);
1208
- });
1209
- if (info2.isDirectory()) {
1210
- await ingestDirectory(targetPath);
1211
- } else {
1212
- await ingestSessionFile(targetPath);
2577
+ function buildReadyUrl(host, port) {
2578
+ if (host.includes(":")) return `http://[${host}]:${port}`;
2579
+ return `http://${host}:${port}`;
2580
+ }
2581
+ function parsePort(raw) {
2582
+ if (raw === void 0) return 0;
2583
+ const value = typeof raw === "number" ? raw : Number(raw);
2584
+ if (!Number.isInteger(value) || value < 0 || value > 65535) {
2585
+ throw new Error(`Invalid --port value: ${raw}`);
1213
2586
  }
1214
- status("\u2192", dim("Next: llmwiki compile"));
2587
+ return value;
2588
+ }
2589
+ function registerShutdown(close) {
2590
+ const shutdown = async () => {
2591
+ try {
2592
+ await close();
2593
+ process.exit(0);
2594
+ } catch {
2595
+ process.exit(1);
2596
+ }
2597
+ };
2598
+ process.once("SIGINT", () => void shutdown());
2599
+ process.once("SIGTERM", () => void shutdown());
1215
2600
  }
1216
2601
 
1217
2602
  // src/commands/compile.ts
1218
2603
  import { existsSync as existsSync7 } from "fs";
1219
2604
 
1220
2605
  // src/compiler/index.ts
1221
- import { readFile as readFile18 } from "fs/promises";
1222
- import path26 from "path";
1223
-
1224
- // src/utils/state.ts
1225
- import { readFile as readFile11, writeFile as writeFile3, rename as rename2, mkdir as mkdir3, copyFile } from "fs/promises";
1226
- import { existsSync } from "fs";
1227
- import path13 from "path";
1228
- function emptyState() {
1229
- return { version: 1, indexHash: "", sources: {} };
1230
- }
1231
- async function readState(root) {
1232
- const filePath = path13.join(root, STATE_FILE);
1233
- if (!existsSync(filePath)) {
1234
- return emptyState();
1235
- }
1236
- try {
1237
- const raw = await readFile11(filePath, "utf-8");
1238
- return JSON.parse(raw);
1239
- } catch {
1240
- const bakPath = filePath + ".bak";
1241
- console.warn(`\u26A0 Corrupt state.json \u2014 backed up to ${bakPath}, starting fresh.`);
1242
- await copyFile(filePath, bakPath);
1243
- return emptyState();
1244
- }
1245
- }
1246
- async function writeState(root, state) {
1247
- const dir = path13.join(root, LLMWIKI_DIR);
1248
- await mkdir3(dir, { recursive: true });
1249
- const filePath = path13.join(root, STATE_FILE);
1250
- const tmpPath = filePath + ".tmp";
1251
- await writeFile3(tmpPath, JSON.stringify(state, null, 2), "utf-8");
1252
- await rename2(tmpPath, filePath);
1253
- }
1254
- async function updateSourceState(root, sourceFile, entry) {
1255
- const state = await readState(root);
1256
- state.sources[sourceFile] = entry;
1257
- await writeState(root, state);
1258
- }
1259
- async function removeSourceState(root, sourceFile) {
1260
- const state = await readState(root);
1261
- delete state.sources[sourceFile];
1262
- await writeState(root, state);
1263
- }
2606
+ import { readFile as readFile23 } from "fs/promises";
2607
+ import path33 from "path";
1264
2608
 
1265
2609
  // src/compiler/source-state.ts
1266
- import path15 from "path";
2610
+ import path23 from "path";
1267
2611
 
1268
2612
  // src/compiler/hasher.ts
1269
2613
  import { createHash as createHash2 } from "crypto";
1270
- import { readFile as readFile12, readdir as readdir2 } from "fs/promises";
1271
- import path14 from "path";
2614
+ import { readFile as readFile17, readdir as readdir5 } from "fs/promises";
2615
+ import path22 from "path";
1272
2616
  async function hashFile(filePath) {
1273
- const content = await readFile12(filePath, "utf-8");
2617
+ const content = await readFile17(filePath, "utf-8");
1274
2618
  return createHash2("sha256").update(content).digest("hex");
1275
2619
  }
1276
2620
  async function detectChanges(root, prevState) {
1277
- const sourcesPath = path14.join(root, SOURCES_DIR);
1278
- const currentFiles = await listSourceFiles(sourcesPath);
2621
+ const sourcesPath = path22.join(root, SOURCES_DIR);
2622
+ const currentFiles = await listSourceFiles2(sourcesPath);
1279
2623
  const changes = [];
1280
2624
  for (const file of currentFiles) {
1281
2625
  const status2 = await classifyFile(root, file, prevState);
@@ -1285,16 +2629,16 @@ async function detectChanges(root, prevState) {
1285
2629
  changes.push(...deletedChanges);
1286
2630
  return changes;
1287
2631
  }
1288
- async function listSourceFiles(sourcesPath) {
2632
+ async function listSourceFiles2(sourcesPath) {
1289
2633
  try {
1290
- const entries = await readdir2(sourcesPath);
2634
+ const entries = await readdir5(sourcesPath);
1291
2635
  return entries.filter((f) => f.endsWith(".md"));
1292
2636
  } catch {
1293
2637
  return [];
1294
2638
  }
1295
2639
  }
1296
2640
  async function classifyFile(root, file, prevState) {
1297
- const filePath = path14.join(root, SOURCES_DIR, file);
2641
+ const filePath = path22.join(root, SOURCES_DIR, file);
1298
2642
  const hash = await hashFile(filePath);
1299
2643
  const prev = prevState.sources[file];
1300
2644
  if (!prev) return "new";
@@ -1317,7 +2661,7 @@ async function buildExtractionSourceStates(root, extractions) {
1317
2661
  return snapshot;
1318
2662
  }
1319
2663
  async function buildEntry(root, result, compiledAt) {
1320
- const filePath = path15.join(root, SOURCES_DIR, result.sourceFile);
2664
+ const filePath = path23.join(root, SOURCES_DIR, result.sourceFile);
1321
2665
  const hash = await hashFile(filePath);
1322
2666
  return {
1323
2667
  hash,
@@ -1406,7 +2750,8 @@ var OpenAIProvider = class {
1406
2750
  model: this.model,
1407
2751
  max_tokens: maxTokens,
1408
2752
  messages: [{ role: "system", content: system }, ...messages],
1409
- tools: openaiTools
2753
+ tools: openaiTools,
2754
+ tool_choice: "required"
1410
2755
  });
1411
2756
  const toolCalls = response.choices[0]?.message?.tool_calls;
1412
2757
  if (toolCalls && toolCalls.length > 0) {
@@ -1463,8 +2808,24 @@ var MiniMaxProvider = class extends OpenAIProvider {
1463
2808
  }
1464
2809
  };
1465
2810
 
2811
+ // src/providers/copilot.ts
2812
+ var CopilotProvider = class extends OpenAIProvider {
2813
+ constructor(model, apiKey) {
2814
+ super(model, { baseURL: COPILOT_BASE_URL, apiKey });
2815
+ }
2816
+ /**
2817
+ * GitHub Copilot has no native embeddings API.
2818
+ * Throws an informative error directing the user to an alternative.
2819
+ */
2820
+ async embed(_text) {
2821
+ throw new Error(
2822
+ "GitHub Copilot does not support embeddings.\n For semantic search (llmwiki query), switch to the OpenAI provider:\n export LLMWIKI_PROVIDER=openai\n export OPENAI_API_KEY=sk-..."
2823
+ );
2824
+ }
2825
+ };
2826
+
1466
2827
  // src/utils/provider.ts
1467
- var SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "openai", "ollama", "minimax"]);
2828
+ var SUPPORTED_PROVIDERS = /* @__PURE__ */ new Set(["anthropic", "openai", "ollama", "minimax", "copilot"]);
1468
2829
  function getProvider() {
1469
2830
  const providerName = getProviderName();
1470
2831
  switch (providerName) {
@@ -1484,6 +2845,8 @@ function getProvider() {
1484
2845
  });
1485
2846
  case "minimax":
1486
2847
  return getMiniMaxProvider();
2848
+ case "copilot":
2849
+ return getCopilotProvider();
1487
2850
  default:
1488
2851
  throw new Error(`Unhandled provider: ${providerName}`);
1489
2852
  }
@@ -1504,6 +2867,15 @@ function getMiniMaxProvider() {
1504
2867
  }
1505
2868
  return new MiniMaxProvider(getModelForProvider("minimax"), apiKey);
1506
2869
  }
2870
+ function getCopilotProvider() {
2871
+ const apiKey = process.env.GITHUB_TOKEN;
2872
+ if (!apiKey) {
2873
+ throw new Error(
2874
+ "GitHub Copilot provider requires GITHUB_TOKEN environment variable.\n Run: gh auth refresh --scopes copilot\n Then set it with: export GITHUB_TOKEN=$(gh auth token)\n The token must belong to a GitHub account with an active Copilot subscription."
2875
+ );
2876
+ }
2877
+ return new CopilotProvider(getModelForProvider("copilot"), apiKey);
2878
+ }
1507
2879
  function getAnthropicProvider() {
1508
2880
  const model = resolveAnthropicModelFromEnv() ?? PROVIDER_MODELS.anthropic;
1509
2881
  const baseURL = resolveAnthropicBaseURLFromEnv();
@@ -1555,8 +2927,8 @@ async function callClaude(options) {
1555
2927
  }
1556
2928
 
1557
2929
  // src/utils/lock.ts
1558
- import { open, readFile as readFile13, unlink, mkdir as mkdir4 } from "fs/promises";
1559
- import path16 from "path";
2930
+ import { open, readFile as readFile18, unlink as unlink2, mkdir as mkdir6 } from "fs/promises";
2931
+ import path24 from "path";
1560
2932
  var RECLAIM_SUFFIX = ".reclaim";
1561
2933
  var MAX_ACQUIRE_ATTEMPTS = 2;
1562
2934
  function isProcessAlive(pid) {
@@ -1568,8 +2940,8 @@ function isProcessAlive(pid) {
1568
2940
  }
1569
2941
  }
1570
2942
  async function acquireLock(root) {
1571
- const lockPath = path16.join(root, LOCK_FILE);
1572
- await mkdir4(path16.join(root, LLMWIKI_DIR), { recursive: true });
2943
+ const lockPath = path24.join(root, LOCK_FILE);
2944
+ await mkdir6(path24.join(root, LLMWIKI_DIR), { recursive: true });
1573
2945
  for (let attempt = 0; attempt < MAX_ACQUIRE_ATTEMPTS; attempt++) {
1574
2946
  const created = await tryCreateLock(lockPath);
1575
2947
  if (created) return true;
@@ -1593,7 +2965,7 @@ async function reclaimStaleLock(root, lockPath) {
1593
2965
  return false;
1594
2966
  }
1595
2967
  try {
1596
- await unlink(lockPath);
2968
+ await unlink2(lockPath);
1597
2969
  } catch {
1598
2970
  }
1599
2971
  const acquired = await tryCreateLock(lockPath);
@@ -1603,7 +2975,7 @@ async function reclaimStaleLock(root, lockPath) {
1603
2975
  return acquired;
1604
2976
  } finally {
1605
2977
  try {
1606
- await unlink(reclaimPath);
2978
+ await unlink2(reclaimPath);
1607
2979
  } catch {
1608
2980
  }
1609
2981
  }
@@ -1612,7 +2984,7 @@ async function acquireReclaimLock(reclaimPath) {
1612
2984
  if (await tryCreateLock(reclaimPath)) return true;
1613
2985
  if (!await isLockStale(reclaimPath)) return false;
1614
2986
  try {
1615
- await unlink(reclaimPath);
2987
+ await unlink2(reclaimPath);
1616
2988
  } catch {
1617
2989
  }
1618
2990
  return false;
@@ -1632,7 +3004,7 @@ async function tryCreateLock(lockPath) {
1632
3004
  }
1633
3005
  async function isLockStale(lockPath) {
1634
3006
  try {
1635
- const content = await readFile13(lockPath, "utf-8");
3007
+ const content = await readFile18(lockPath, "utf-8");
1636
3008
  const pid = parseInt(content.trim(), 10);
1637
3009
  if (isNaN(pid)) return true;
1638
3010
  return !isProcessAlive(pid);
@@ -1641,9 +3013,9 @@ async function isLockStale(lockPath) {
1641
3013
  }
1642
3014
  }
1643
3015
  async function releaseLock(root) {
1644
- const lockPath = path16.join(root, LOCK_FILE);
3016
+ const lockPath = path24.join(root, LOCK_FILE);
1645
3017
  try {
1646
- await unlink(lockPath);
3018
+ await unlink2(lockPath);
1647
3019
  } catch {
1648
3020
  }
1649
3021
  }
@@ -1900,9 +3272,9 @@ function buildDefaultSchema() {
1900
3272
  }
1901
3273
 
1902
3274
  // src/schema/loader.ts
1903
- import { existsSync as existsSync2 } from "fs";
1904
- import { readFile as readFile14 } from "fs/promises";
1905
- import path17 from "path";
3275
+ import { existsSync as existsSync3 } from "fs";
3276
+ import { readFile as readFile19 } from "fs/promises";
3277
+ import path25 from "path";
1906
3278
  import yaml2 from "js-yaml";
1907
3279
  var SCHEMA_CANDIDATE_PATHS = [
1908
3280
  ".llmwiki/schema.json",
@@ -1913,8 +3285,8 @@ var SCHEMA_CANDIDATE_PATHS = [
1913
3285
  ];
1914
3286
  function findSchemaPath(root) {
1915
3287
  for (const candidate of SCHEMA_CANDIDATE_PATHS) {
1916
- const absolute = path17.join(root, candidate);
1917
- if (existsSync2(absolute)) return absolute;
3288
+ const absolute = path25.join(root, candidate);
3289
+ if (existsSync3(absolute)) return absolute;
1918
3290
  }
1919
3291
  return null;
1920
3292
  }
@@ -1966,12 +3338,12 @@ async function loadSchema(root) {
1966
3338
  const defaults = buildDefaultSchema();
1967
3339
  const schemaPath = findSchemaPath(root);
1968
3340
  if (!schemaPath) return defaults;
1969
- const raw = await readFile14(schemaPath, "utf-8");
3341
+ const raw = await readFile19(schemaPath, "utf-8");
1970
3342
  const parsed = parseSchemaFile(schemaPath, raw);
1971
3343
  return applyOverrides(defaults, parsed, schemaPath);
1972
3344
  }
1973
3345
  function defaultSchemaInitPath(root) {
1974
- return path17.join(root, SCHEMA_CANDIDATE_PATHS[0]);
3346
+ return path25.join(root, SCHEMA_CANDIDATE_PATHS[0]);
1975
3347
  }
1976
3348
 
1977
3349
  // src/schema/helpers.ts
@@ -2143,7 +3515,7 @@ async function freezeFailedExtractions(root, results, frozenSlugs) {
2143
3515
  }
2144
3516
 
2145
3517
  // src/compiler/orphan.ts
2146
- import path18 from "path";
3518
+ import path26 from "path";
2147
3519
  async function markOrphaned(root, sourceFile, state) {
2148
3520
  const sourceEntry = state.sources[sourceFile];
2149
3521
  if (!sourceEntry) return;
@@ -2169,7 +3541,7 @@ async function orphanUnownedFrozenPages(root, frozenSlugs) {
2169
3541
  }
2170
3542
  }
2171
3543
  async function orphanPage(root, slug, reason) {
2172
- const pagePath = path18.join(root, CONCEPTS_DIR, `${slug}.md`);
3544
+ const pagePath = path26.join(root, CONCEPTS_DIR, `${slug}.md`);
2173
3545
  const content = await safeReadFile(pagePath);
2174
3546
  if (!content) return;
2175
3547
  const { meta } = parseFrontmatter(content);
@@ -2180,18 +3552,18 @@ async function orphanPage(root, slug, reason) {
2180
3552
  }
2181
3553
 
2182
3554
  // src/compiler/resolver.ts
2183
- import { readdir as readdir3, readFile as readFile15 } from "fs/promises";
2184
- import path19 from "path";
2185
- import { existsSync as existsSync3 } from "fs";
3555
+ import { readdir as readdir6, readFile as readFile20 } from "fs/promises";
3556
+ import path27 from "path";
3557
+ import { existsSync as existsSync4 } from "fs";
2186
3558
  async function buildTitleIndex(root) {
2187
- const conceptsDir = path19.join(root, CONCEPTS_DIR);
2188
- if (!existsSync3(conceptsDir)) return [];
2189
- const files = await readdir3(conceptsDir);
3559
+ const conceptsDir = path27.join(root, CONCEPTS_DIR);
3560
+ if (!existsSync4(conceptsDir)) return [];
3561
+ const files = await readdir6(conceptsDir);
2190
3562
  const pages = [];
2191
3563
  for (const file of files) {
2192
3564
  if (!file.endsWith(".md")) continue;
2193
- const filePath = path19.join(conceptsDir, file);
2194
- const content = await readFile15(filePath, "utf-8");
3565
+ const filePath = path27.join(conceptsDir, file);
3566
+ const content = await readFile20(filePath, "utf-8");
2195
3567
  const { meta } = parseFrontmatter(content);
2196
3568
  if (meta.title && typeof meta.title === "string" && !meta.orphaned) {
2197
3569
  pages.push({
@@ -2277,7 +3649,7 @@ async function resolveInboundLinks(titleIndex, newSlugs) {
2277
3649
  let count = 0;
2278
3650
  for (const page of titleIndex) {
2279
3651
  if (newSlugs.includes(page.slug)) continue;
2280
- const content = await readFile15(page.filePath, "utf-8");
3652
+ const content = await readFile20(page.filePath, "utf-8");
2281
3653
  const { body } = parseFrontmatter(content);
2282
3654
  const linked = addWikilinks(body, newTitles, page.title);
2283
3655
  if (linked !== body) {
@@ -2289,7 +3661,7 @@ async function resolveInboundLinks(titleIndex, newSlugs) {
2289
3661
  return count;
2290
3662
  }
2291
3663
  async function linkPage(page, titleIndex) {
2292
- const content = await readFile15(page.filePath, "utf-8");
3664
+ const content = await readFile20(page.filePath, "utf-8");
2293
3665
  const { body } = parseFrontmatter(content);
2294
3666
  const linked = addWikilinks(body, titleIndex, page.title);
2295
3667
  if (linked === body) return false;
@@ -2299,18 +3671,18 @@ async function linkPage(page, titleIndex) {
2299
3671
  }
2300
3672
 
2301
3673
  // src/compiler/indexgen.ts
2302
- import { readdir as readdir4 } from "fs/promises";
2303
- import path20 from "path";
3674
+ import { readdir as readdir7 } from "fs/promises";
3675
+ import path28 from "path";
2304
3676
  async function generateIndex(root) {
2305
3677
  status("*", info("Generating index..."));
2306
- const conceptsPath = path20.join(root, CONCEPTS_DIR);
2307
- const queriesPath = path20.join(root, QUERIES_DIR);
3678
+ const conceptsPath = path28.join(root, CONCEPTS_DIR);
3679
+ const queriesPath = path28.join(root, QUERIES_DIR);
2308
3680
  const concepts = await collectPageSummaries(conceptsPath);
2309
3681
  const queries = await collectPageSummaries(queriesPath);
2310
3682
  concepts.sort((a, b) => a.title.localeCompare(b.title));
2311
3683
  queries.sort((a, b) => a.title.localeCompare(b.title));
2312
3684
  const indexContent = buildIndexContent(concepts, queries);
2313
- const indexPath = path20.join(root, INDEX_FILE);
3685
+ const indexPath = path28.join(root, INDEX_FILE);
2314
3686
  await atomicWrite(indexPath, indexContent);
2315
3687
  const total = concepts.length + queries.length;
2316
3688
  status("+", success(`Index updated with ${total} pages.`));
@@ -2318,13 +3690,13 @@ async function generateIndex(root) {
2318
3690
  async function scanWikiPages(dirPath) {
2319
3691
  let files;
2320
3692
  try {
2321
- files = await readdir4(dirPath);
3693
+ files = await readdir7(dirPath);
2322
3694
  } catch {
2323
3695
  return [];
2324
3696
  }
2325
3697
  const scanned = [];
2326
3698
  for (const file of files.filter((f) => f.endsWith(".md"))) {
2327
- const content = await safeReadFile(path20.join(dirPath, file));
3699
+ const content = await safeReadFile(path28.join(dirPath, file));
2328
3700
  const { meta } = parseFrontmatter(content);
2329
3701
  scanned.push({ slug: file.replace(/\.md$/, ""), meta });
2330
3702
  }
@@ -2396,8 +3768,8 @@ function warnTruncation(concept, totalRaw, sourceCount, perSource, budget) {
2396
3768
  }
2397
3769
 
2398
3770
  // src/compiler/obsidian.ts
2399
- import { readdir as readdir5 } from "fs/promises";
2400
- import path21 from "path";
3771
+ import { readdir as readdir8 } from "fs/promises";
3772
+ import path29 from "path";
2401
3773
  var ABBREVIATION_MIN_WORDS = 3;
2402
3774
  var SWAP_CONJUNCTIONS = [" and ", " or "];
2403
3775
  function addObsidianMeta(frontmatter, conceptTitle, tags) {
@@ -2439,23 +3811,23 @@ function generateAbbreviation(title) {
2439
3811
  return abbreviation;
2440
3812
  }
2441
3813
  async function generateMOC(root) {
2442
- const conceptsPath = path21.join(root, CONCEPTS_DIR);
3814
+ const conceptsPath = path29.join(root, CONCEPTS_DIR);
2443
3815
  const pages = await loadConceptPages(conceptsPath);
2444
3816
  const tagGroups = groupPagesByTag(pages);
2445
3817
  const content = buildMOCContent(tagGroups);
2446
- await atomicWrite(path21.join(root, MOC_FILE), content);
3818
+ await atomicWrite(path29.join(root, MOC_FILE), content);
2447
3819
  }
2448
3820
  async function loadConceptPages(conceptsPath) {
2449
3821
  let files;
2450
3822
  try {
2451
- files = await readdir5(conceptsPath);
3823
+ files = await readdir8(conceptsPath);
2452
3824
  } catch {
2453
3825
  return [];
2454
3826
  }
2455
3827
  const pages = [];
2456
3828
  for (const file of files) {
2457
3829
  if (!file.endsWith(".md")) continue;
2458
- const content = await safeReadFile(path21.join(conceptsPath, file));
3830
+ const content = await safeReadFile(path29.join(conceptsPath, file));
2459
3831
  if (!content) continue;
2460
3832
  const { meta } = parseFrontmatter(content);
2461
3833
  if (meta.orphaned) continue;
@@ -2506,9 +3878,9 @@ function buildMOCContent(tagGroups) {
2506
3878
  }
2507
3879
 
2508
3880
  // src/utils/embeddings.ts
2509
- import { readFile as readFile16, readdir as readdir6 } from "fs/promises";
2510
- import { existsSync as existsSync4 } from "fs";
2511
- import path22 from "path";
3881
+ import { readFile as readFile21, readdir as readdir9 } from "fs/promises";
3882
+ import { existsSync as existsSync5 } from "fs";
3883
+ import path30 from "path";
2512
3884
 
2513
3885
  // src/utils/retrieval.ts
2514
3886
  import { createHash as createHash3 } from "crypto";
@@ -2673,13 +4045,13 @@ function findTopKChunks(queryVec, chunks, k) {
2673
4045
  return scored.slice(0, k);
2674
4046
  }
2675
4047
  async function readEmbeddingStore(root) {
2676
- const filePath = path22.join(root, EMBEDDINGS_FILE);
2677
- if (!existsSync4(filePath)) return null;
2678
- const raw = await readFile16(filePath, "utf-8");
4048
+ const filePath = path30.join(root, EMBEDDINGS_FILE);
4049
+ if (!existsSync5(filePath)) return null;
4050
+ const raw = await readFile21(filePath, "utf-8");
2679
4051
  return JSON.parse(raw);
2680
4052
  }
2681
4053
  async function writeEmbeddingStore(root, store) {
2682
- const filePath = path22.join(root, EMBEDDINGS_FILE);
4054
+ const filePath = path30.join(root, EMBEDDINGS_FILE);
2683
4055
  await atomicWrite(filePath, JSON.stringify(store, null, 2));
2684
4056
  }
2685
4057
  async function findRelevantPages(root, question) {
@@ -2711,10 +4083,10 @@ async function loadActiveStore(root, hasContent) {
2711
4083
  async function collectPageRecords(root) {
2712
4084
  const records = [];
2713
4085
  for (const dir of [CONCEPTS_DIR, QUERIES_DIR]) {
2714
- const absDir = path22.join(root, dir);
4086
+ const absDir = path30.join(root, dir);
2715
4087
  let files;
2716
4088
  try {
2717
- files = await readdir6(absDir);
4089
+ files = await readdir9(absDir);
2718
4090
  } catch {
2719
4091
  continue;
2720
4092
  }
@@ -2726,7 +4098,7 @@ async function collectPageRecords(root) {
2726
4098
  return records;
2727
4099
  }
2728
4100
  async function readPageRecord(absDir, file) {
2729
- const content = await safeReadFile(path22.join(absDir, file));
4101
+ const content = await safeReadFile(path30.join(absDir, file));
2730
4102
  const { meta, body } = parseFrontmatter(content);
2731
4103
  if (meta.orphaned || typeof meta.title !== "string") return null;
2732
4104
  return {
@@ -2887,115 +4259,10 @@ function shouldRunEmbedding(modelChanged, toEmbed, previousEntries, previousChun
2887
4259
  return false;
2888
4260
  }
2889
4261
 
2890
- // src/compiler/candidates.ts
2891
- import { readdir as readdir7, rename as rename3, unlink as unlink2, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
2892
- import { existsSync as existsSync5 } from "fs";
2893
- import path23 from "path";
2894
- import { randomBytes } from "crypto";
2895
- var ID_SUFFIX_BYTES = 4;
2896
- var CANDIDATE_EXT = ".json";
2897
- function buildCandidateId(slug) {
2898
- const suffix = randomBytes(ID_SUFFIX_BYTES).toString("hex");
2899
- return `${slug}-${suffix}`;
2900
- }
2901
- function candidatePath(root, id) {
2902
- return path23.join(root, CANDIDATES_DIR, `${id}${CANDIDATE_EXT}`);
2903
- }
2904
- function archivePath(root, id) {
2905
- return path23.join(root, CANDIDATES_ARCHIVE_DIR, `${id}${CANDIDATE_EXT}`);
2906
- }
2907
- async function writeCandidate(root, draft) {
2908
- const candidate = {
2909
- id: buildCandidateId(draft.slug),
2910
- title: draft.title,
2911
- slug: draft.slug,
2912
- summary: draft.summary,
2913
- sources: draft.sources,
2914
- body: draft.body,
2915
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2916
- ...draft.sourceStates ? { sourceStates: draft.sourceStates } : {},
2917
- ...draft.schemaViolations ? { schemaViolations: draft.schemaViolations } : {},
2918
- ...draft.provenanceViolations ? { provenanceViolations: draft.provenanceViolations } : {}
2919
- };
2920
- await atomicWrite(candidatePath(root, candidate.id), JSON.stringify(candidate, null, 2));
2921
- return candidate;
2922
- }
2923
- function failWithError(message) {
2924
- status("!", error(message));
2925
- process.exitCode = 1;
2926
- return null;
2927
- }
2928
- async function loadCandidateOrFail(root, id) {
2929
- const candidate = await readCandidate(root, id);
2930
- if (!candidate) return failWithError(`Candidate not found: ${id}`);
2931
- return candidate;
2932
- }
2933
- async function loadCandidateUnderLockOrFail(root, id) {
2934
- const candidate = await readCandidate(root, id);
2935
- if (!candidate) {
2936
- return failWithError(`Candidate ${id} was removed by another process during review.`);
2937
- }
2938
- return candidate;
2939
- }
2940
- async function readCandidate(root, id) {
2941
- const raw = await safeReadFile(candidatePath(root, id));
2942
- if (!raw) return null;
2943
- try {
2944
- const parsed = JSON.parse(raw);
2945
- if (!isValidCandidate(parsed)) return null;
2946
- return parsed;
2947
- } catch {
2948
- return null;
2949
- }
2950
- }
2951
- function isValidCandidate(value) {
2952
- if (!value || typeof value !== "object") return false;
2953
- const candidate = value;
2954
- return typeof candidate.id === "string" && typeof candidate.title === "string" && typeof candidate.slug === "string" && typeof candidate.body === "string" && Array.isArray(candidate.sources);
2955
- }
2956
- async function listCandidates(root) {
2957
- const dir = path23.join(root, CANDIDATES_DIR);
2958
- if (!existsSync5(dir)) return [];
2959
- const entries = await readdir7(dir, { withFileTypes: true });
2960
- const candidates = [];
2961
- for (const entry of entries) {
2962
- if (!entry.isFile() || !entry.name.endsWith(CANDIDATE_EXT)) continue;
2963
- const id = entry.name.slice(0, -CANDIDATE_EXT.length);
2964
- const candidate = await readCandidate(root, id);
2965
- if (candidate) candidates.push(candidate);
2966
- }
2967
- candidates.sort((a, b) => a.generatedAt.localeCompare(b.generatedAt));
2968
- return candidates;
2969
- }
2970
- async function countCandidates(root) {
2971
- const candidates = await listCandidates(root);
2972
- return candidates.length;
2973
- }
2974
- async function deleteCandidate(root, id) {
2975
- const filePath = candidatePath(root, id);
2976
- if (!existsSync5(filePath)) return false;
2977
- await unlink2(filePath);
2978
- return true;
2979
- }
2980
- async function archiveCandidate(root, id) {
2981
- const sourcePath = candidatePath(root, id);
2982
- if (!existsSync5(sourcePath)) return false;
2983
- const target = archivePath(root, id);
2984
- await mkdir5(path23.dirname(target), { recursive: true });
2985
- try {
2986
- await rename3(sourcePath, target);
2987
- } catch {
2988
- const raw = await safeReadFile(sourcePath);
2989
- await writeFile4(target, raw, "utf-8");
2990
- await unlink2(sourcePath);
2991
- }
2992
- return true;
2993
- }
2994
-
2995
4262
  // src/linter/rules.ts
2996
- import { readdir as readdir8, readFile as readFile17 } from "fs/promises";
4263
+ import { readdir as readdir10, readFile as readFile22 } from "fs/promises";
2997
4264
  import { existsSync as existsSync6 } from "fs";
2998
- import path24 from "path";
4265
+ import path31 from "path";
2999
4266
  var MIN_BODY_LENGTH = 50;
3000
4267
  var WIKILINK_PATTERN2 = /\[\[([^\]]+)\]\]/g;
3001
4268
  var CITATION_PATTERN = /\^\[([^\]]+)\]/g;
@@ -3012,26 +4279,26 @@ function findMatchesInContent(content, pattern) {
3012
4279
  }
3013
4280
  async function readMarkdownFiles(dirPath) {
3014
4281
  if (!existsSync6(dirPath)) return [];
3015
- const entries = await readdir8(dirPath);
4282
+ const entries = await readdir10(dirPath);
3016
4283
  const mdFiles = entries.filter((f) => f.endsWith(".md"));
3017
4284
  const results = await Promise.all(
3018
4285
  mdFiles.map(async (fileName) => {
3019
- const filePath = path24.join(dirPath, fileName);
3020
- const content = await readFile17(filePath, "utf-8");
4286
+ const filePath = path31.join(dirPath, fileName);
4287
+ const content = await readFile22(filePath, "utf-8");
3021
4288
  return { filePath, content };
3022
4289
  })
3023
4290
  );
3024
4291
  return results;
3025
4292
  }
3026
4293
  async function collectAllPages(root) {
3027
- const conceptPages = await readMarkdownFiles(path24.join(root, CONCEPTS_DIR));
3028
- const queryPages = await readMarkdownFiles(path24.join(root, QUERIES_DIR));
4294
+ const conceptPages = await readMarkdownFiles(path31.join(root, CONCEPTS_DIR));
4295
+ const queryPages = await readMarkdownFiles(path31.join(root, QUERIES_DIR));
3029
4296
  return [...conceptPages, ...queryPages];
3030
4297
  }
3031
4298
  function buildPageSlugSet(pages) {
3032
4299
  const slugs = /* @__PURE__ */ new Set();
3033
4300
  for (const page of pages) {
3034
- const baseName = path24.basename(page.filePath, ".md");
4301
+ const baseName = path31.basename(page.filePath, ".md");
3035
4302
  slugs.add(baseName.toLowerCase());
3036
4303
  }
3037
4304
  return slugs;
@@ -3254,7 +4521,7 @@ function countLines(content) {
3254
4521
  }
3255
4522
  async function checkBrokenCitations(root) {
3256
4523
  const pages = await collectAllPages(root);
3257
- const sourcesDir = path24.join(root, SOURCES_DIR);
4524
+ const sourcesDir = path31.join(root, SOURCES_DIR);
3258
4525
  const results = [];
3259
4526
  const lineCountCache = /* @__PURE__ */ new Map();
3260
4527
  for (const page of pages) {
@@ -3280,7 +4547,7 @@ async function collectBrokenForMarker(captured, line, pageFile, sourcesDir, line
3280
4547
  const trimmed = part.trim();
3281
4548
  if (trimmed.length === 0) continue;
3282
4549
  const filename = stripSpanSuffix(trimmed);
3283
- const citedPath = path24.join(sourcesDir, filename);
4550
+ const citedPath = path31.join(sourcesDir, filename);
3284
4551
  if (!existsSync6(citedPath)) {
3285
4552
  out.push({
3286
4553
  rule: "broken-citation",
@@ -3338,8 +4605,8 @@ function checkPageMalformedCitations(content, filePath) {
3338
4605
  }
3339
4606
 
3340
4607
  // src/compiler/page-renderer.ts
3341
- import { readdir as readdir9 } from "fs/promises";
3342
- import path25 from "path";
4608
+ import { readdir as readdir11 } from "fs/promises";
4609
+ import path32 from "path";
3343
4610
 
3344
4611
  // src/compiler/provenance.ts
3345
4612
  function addProvenanceMeta(fields, concept) {
@@ -3366,7 +4633,7 @@ function reportContradictionWarnings(conceptTitle, concept) {
3366
4633
  // src/compiler/page-renderer.ts
3367
4634
  var RELATED_PAGE_CONTEXT_LIMIT = 5;
3368
4635
  async function renderMergedPageContent(root, entry, schema) {
3369
- const pagePath = path25.join(root, CONCEPTS_DIR, `${entry.slug}.md`);
4636
+ const pagePath = path32.join(root, CONCEPTS_DIR, `${entry.slug}.md`);
3370
4637
  const existingPage = await safeReadFile(pagePath);
3371
4638
  const relatedPages = await loadRelatedPages(root, entry.slug);
3372
4639
  const system = buildPagePrompt(
@@ -3405,17 +4672,17 @@ function buildMergedFrontmatter(entry, existingPage, schema) {
3405
4672
  return buildFrontmatter(frontmatterFields);
3406
4673
  }
3407
4674
  async function loadRelatedPages(root, excludeSlug) {
3408
- const conceptsPath = path25.join(root, CONCEPTS_DIR);
4675
+ const conceptsPath = path32.join(root, CONCEPTS_DIR);
3409
4676
  let files;
3410
4677
  try {
3411
- files = await readdir9(conceptsPath);
4678
+ files = await readdir11(conceptsPath);
3412
4679
  } catch {
3413
4680
  return "";
3414
4681
  }
3415
4682
  const related = files.filter((f) => f.endsWith(".md") && f !== `${excludeSlug}.md`).slice(0, RELATED_PAGE_CONTEXT_LIMIT);
3416
4683
  const contents = [];
3417
4684
  for (const f of related) {
3418
- const content = await safeReadFile(path25.join(conceptsPath, f));
4685
+ const content = await safeReadFile(path32.join(conceptsPath, f));
3419
4686
  if (!content) continue;
3420
4687
  const { meta } = parseFrontmatter(content);
3421
4688
  if (meta.orphaned) continue;
@@ -3628,9 +4895,9 @@ function printChangesSummary(changes) {
3628
4895
  }
3629
4896
  async function extractForSource(root, sourceFile) {
3630
4897
  status("*", info(`Extracting: ${sourceFile}`));
3631
- const sourcePath = path26.join(root, SOURCES_DIR, sourceFile);
3632
- const sourceContent = await readFile18(sourcePath, "utf-8");
3633
- const existingIndex = await safeReadFile(path26.join(root, INDEX_FILE));
4898
+ const sourcePath = path33.join(root, SOURCES_DIR, sourceFile);
4899
+ const sourceContent = await readFile23(sourcePath, "utf-8");
4900
+ const existingIndex = await safeReadFile(path33.join(root, INDEX_FILE));
3634
4901
  const concepts = await extractConcepts(sourceContent, existingIndex);
3635
4902
  if (concepts.length > 0) {
3636
4903
  const names = concepts.map((c) => c.concept).join(", ");
@@ -3696,7 +4963,7 @@ async function generateMergedPage(root, entry, schema, options, sourceStates) {
3696
4963
  if (options.review) {
3697
4964
  return await persistReviewCandidate(root, entry, fullPage, sourceStates, schema);
3698
4965
  }
3699
- const pagePath = path26.join(root, CONCEPTS_DIR, `${entry.slug}.md`);
4966
+ const pagePath = path33.join(root, CONCEPTS_DIR, `${entry.slug}.md`);
3700
4967
  const error2 = await writePageIfValid(pagePath, fullPage, entry.concept.concept);
3701
4968
  return { error: error2 ?? void 0 };
3702
4969
  }
@@ -3726,7 +4993,7 @@ async function collectCandidateProvenanceViolations(root, fullPage, virtualPath)
3726
4993
  const broken = await checkPageBrokenCitations(
3727
4994
  fullPage,
3728
4995
  virtualPath,
3729
- path26.join(root, SOURCES_DIR)
4996
+ path33.join(root, SOURCES_DIR)
3730
4997
  );
3731
4998
  return [...malformed, ...broken];
3732
4999
  }
@@ -3743,7 +5010,7 @@ async function generateSeedPages(root, schema, generation) {
3743
5010
  }
3744
5011
  async function generateSingleSeedPage(root, schema, seed) {
3745
5012
  const slug = slugify(seed.title);
3746
- const pagePath = path26.join(root, CONCEPTS_DIR, `${slug}.md`);
5013
+ const pagePath = path33.join(root, CONCEPTS_DIR, `${slug}.md`);
3747
5014
  const relatedContent = await loadSeedRelatedPages(root, seed.relatedSlugs ?? []);
3748
5015
  const rule = schema.kinds[seed.kind];
3749
5016
  const system = buildSeedPagePrompt(seed, rule, relatedContent);
@@ -3776,7 +5043,7 @@ async function loadSeedRelatedPages(root, slugs) {
3776
5043
  if (slugs.length === 0) return "";
3777
5044
  const contents = [];
3778
5045
  for (const slug of slugs) {
3779
- const pagePath = path26.join(root, CONCEPTS_DIR, `${slug}.md`);
5046
+ const pagePath = path33.join(root, CONCEPTS_DIR, `${slug}.md`);
3780
5047
  const content = await safeReadFile(pagePath);
3781
5048
  if (content) contents.push(content);
3782
5049
  }
@@ -3831,7 +5098,7 @@ async function compileCommand(options = {}) {
3831
5098
 
3832
5099
  // src/commands/query.ts
3833
5100
  import { existsSync as existsSync8 } from "fs";
3834
- import path27 from "path";
5101
+ import path34 from "path";
3835
5102
  var PAGE_DIRS = [CONCEPTS_DIR, QUERIES_DIR];
3836
5103
  var PAGE_SELECTION_TOOL = {
3837
5104
  name: "select_pages",
@@ -3888,7 +5155,7 @@ async function selectRelevantPages(root, question, debug) {
3888
5155
  const { pages: rawPages2, reasoning: reasoning2 } = await selectPages(question, filteredIndex);
3889
5156
  return { pages: rawPages2, rawPages: rawPages2, reasoning: reasoning2, chunks: [] };
3890
5157
  }
3891
- const indexContent = await safeReadFile(path27.join(root, INDEX_FILE));
5158
+ const indexContent = await safeReadFile(path34.join(root, INDEX_FILE));
3892
5159
  const { pages: rawPages, reasoning } = await selectPages(question, indexContent);
3893
5160
  return { pages: rawPages.map((p) => slugify(p)), rawPages, reasoning, chunks: [] };
3894
5161
  }
@@ -3980,7 +5247,7 @@ async function loadSelectedPages(root, slugs) {
3980
5247
  for (const slug of slugs) {
3981
5248
  let content = "";
3982
5249
  for (const dir of PAGE_DIRS) {
3983
- const candidate = await safeReadFile(path27.join(root, dir, `${slug}.md`));
5250
+ const candidate = await safeReadFile(path34.join(root, dir, `${slug}.md`));
3984
5251
  if (!candidate) continue;
3985
5252
  const { meta } = parseFrontmatter(candidate);
3986
5253
  if (meta.orphaned) continue;
@@ -4031,7 +5298,7 @@ function summarizeAnswer(answer) {
4031
5298
  }
4032
5299
  async function saveQueryPage(root, question, answer) {
4033
5300
  const slug = slugify(question);
4034
- const filePath = path27.join(root, QUERIES_DIR, `${slug}.md`);
5301
+ const filePath = path34.join(root, QUERIES_DIR, `${slug}.md`);
4035
5302
  const frontmatter = buildFrontmatter({
4036
5303
  title: question,
4037
5304
  summary: summarizeAnswer(answer),
@@ -4057,7 +5324,7 @@ ${answer}
4057
5324
  return slug;
4058
5325
  }
4059
5326
  async function generateAnswer(root, question, options = {}) {
4060
- if (!existsSync8(path27.join(root, INDEX_FILE))) {
5327
+ if (!existsSync8(path34.join(root, INDEX_FILE))) {
4061
5328
  throw new Error("Wiki index not found. Run `llmwiki compile` first.");
4062
5329
  }
4063
5330
  const selection = await selectRelevantPages(root, question, Boolean(options.debug));
@@ -4085,7 +5352,7 @@ function buildEmptyResult(selection) {
4085
5352
  };
4086
5353
  }
4087
5354
  async function queryCommand(root, question, options) {
4088
- if (!existsSync8(path27.join(root, INDEX_FILE))) {
5355
+ if (!existsSync8(path34.join(root, INDEX_FILE))) {
4089
5356
  status("!", error("Wiki index not found. Run `llmwiki compile` first."));
4090
5357
  return;
4091
5358
  }
@@ -4136,10 +5403,10 @@ var DEBUG_CHUNK_PREVIEW_CHARS = 120;
4136
5403
  // src/commands/watch.ts
4137
5404
  import { watch as chokidarWatch } from "chokidar";
4138
5405
  import { existsSync as existsSync9 } from "fs";
4139
- import path28 from "path";
5406
+ import path35 from "path";
4140
5407
  var DEBOUNCE_MS = 500;
4141
5408
  async function watchCommand() {
4142
- const sourcesPath = path28.resolve(SOURCES_DIR);
5409
+ const sourcesPath = path35.resolve(SOURCES_DIR);
4143
5410
  if (!existsSync9(sourcesPath)) {
4144
5411
  status(
4145
5412
  "!",
@@ -4174,7 +5441,7 @@ async function watchCommand() {
4174
5441
  const scheduleCompile = (eventPath, event) => {
4175
5442
  status(
4176
5443
  "~",
4177
- dim(`${event}: ${path28.basename(eventPath)}`)
5444
+ dim(`${event}: ${path35.basename(eventPath)}`)
4178
5445
  );
4179
5446
  if (debounceTimer) clearTimeout(debounceTimer);
4180
5447
  debounceTimer = setTimeout(triggerCompile, DEBOUNCE_MS);
@@ -4253,75 +5520,38 @@ async function lintCommand() {
4253
5520
  info(`${summary.info} info`)
4254
5521
  ].join(", ");
4255
5522
  status("*", summaryLine);
5523
+ await writeLintCache(process.cwd(), summary);
4256
5524
  if (summary.errors > 0) {
4257
5525
  process.exit(1);
4258
5526
  }
4259
5527
  }
4260
5528
 
4261
5529
  // src/commands/export.ts
4262
- import path30 from "path";
5530
+ import path36 from "path";
4263
5531
  import { createRequire } from "module";
4264
5532
 
4265
5533
  // src/export/collect.ts
4266
- import { readdir as readdir10, readFile as readFile19 } from "fs/promises";
4267
- import path29 from "path";
4268
- var WIKILINK_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
4269
- function extractWikilinkSlugs(body) {
4270
- const slugs = /* @__PURE__ */ new Set();
4271
- let match;
4272
- while ((match = WIKILINK_RE.exec(body)) !== null) {
4273
- slugs.add(slugify(match[1].trim()));
4274
- }
4275
- return [...slugs];
4276
- }
4277
- async function parsePageFile(filePath, slug, pageDirectory) {
4278
- let raw;
4279
- try {
4280
- raw = await readFile19(filePath, "utf-8");
4281
- } catch {
4282
- return null;
4283
- }
4284
- const { meta, body } = parseFrontmatter(raw);
4285
- if (!meta.title || typeof meta.title !== "string") return null;
4286
- if (meta.orphaned === true) return null;
5534
+ function toExportPage(raw) {
5535
+ const meta = raw.frontmatter;
4287
5536
  return {
4288
- title: meta.title,
4289
- slug,
4290
- pageDirectory,
5537
+ title: raw.title,
5538
+ slug: raw.slug,
5539
+ pageDirectory: raw.pageDirectory,
4291
5540
  summary: typeof meta.summary === "string" ? meta.summary : "",
4292
5541
  sources: Array.isArray(meta.sources) ? meta.sources.filter((s) => typeof s === "string") : [],
4293
5542
  tags: Array.isArray(meta.tags) ? meta.tags.filter((t) => typeof t === "string") : [],
4294
5543
  createdAt: typeof meta.createdAt === "string" ? meta.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
4295
5544
  updatedAt: typeof meta.updatedAt === "string" ? meta.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
4296
- links: extractWikilinkSlugs(body),
4297
- body
5545
+ links: extractWikilinkSlugs(raw.body),
5546
+ body: raw.body
4298
5547
  };
4299
5548
  }
4300
- async function collectFromDir(dirPath, pageDirectory) {
4301
- let files;
4302
- try {
4303
- files = await readdir10(dirPath);
4304
- } catch {
4305
- return [];
4306
- }
4307
- const pages = [];
4308
- for (const file of files.filter((f) => f.endsWith(".md"))) {
4309
- const slug = file.replace(/\.md$/, "");
4310
- const page = await parsePageFile(path29.join(dirPath, file), slug, pageDirectory);
4311
- if (page) pages.push(page);
4312
- }
4313
- return pages;
4314
- }
4315
5549
  async function collectExportPages(root) {
4316
- const conceptsPath = path29.join(root, CONCEPTS_DIR);
4317
- const queriesPath = path29.join(root, QUERIES_DIR);
4318
- const [concepts, queries] = await Promise.all([
4319
- collectFromDir(conceptsPath, "concepts"),
4320
- collectFromDir(queriesPath, "queries")
4321
- ]);
4322
- const all = [...concepts, ...queries];
4323
- all.sort((a, b) => a.title.localeCompare(b.title));
4324
- return all;
5550
+ const raw = await collectRawWikiPages(root);
5551
+ const kept = raw.filter((page) => page.parseStatus.hasTitle && !page.parseStatus.orphaned);
5552
+ const pages = kept.map(toExportPage);
5553
+ pages.sort((a, b) => a.title.localeCompare(b.title));
5554
+ return pages;
4325
5555
  }
4326
5556
 
4327
5557
  // src/export/llms-txt.ts
@@ -4559,7 +5789,7 @@ var TARGET_FILENAMES = {
4559
5789
  };
4560
5790
  function resolveProjectTitle(root) {
4561
5791
  try {
4562
- const pkg = require2(path30.join(root, "package.json"));
5792
+ const pkg = require2(path36.join(root, "package.json"));
4563
5793
  return typeof pkg.name === "string" ? pkg.name : "Knowledge Wiki";
4564
5794
  } catch {
4565
5795
  return "Knowledge Wiki";
@@ -4611,7 +5841,7 @@ async function runExport(root, options = {}) {
4611
5841
  const written = [];
4612
5842
  for (const target of targets) {
4613
5843
  const content = buildContent(target, pages, projectTitle, marpSource);
4614
- const outPath = path30.join(root, EXPORT_DIR, TARGET_FILENAMES[target]);
5844
+ const outPath = path36.join(root, EXPORT_DIR, TARGET_FILENAMES[target]);
4615
5845
  await atomicWrite(outPath, content);
4616
5846
  written.push(outPath);
4617
5847
  status("+", success(`Exported ${target} \u2192 ${source(outPath)}`));
@@ -4638,8 +5868,8 @@ async function exportCommand(root, options) {
4638
5868
 
4639
5869
  // src/commands/schema.ts
4640
5870
  import { existsSync as existsSync10 } from "fs";
4641
- import { mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
4642
- import path31 from "path";
5871
+ import { mkdir as mkdir7, writeFile as writeFile5 } from "fs/promises";
5872
+ import path37 from "path";
4643
5873
  async function schemaInitCommand() {
4644
5874
  const root = process.cwd();
4645
5875
  const defaults = buildDefaultSchema();
@@ -4648,7 +5878,7 @@ async function schemaInitCommand() {
4648
5878
  status("!", warn(`Schema file already exists at ${targetPath}`));
4649
5879
  return;
4650
5880
  }
4651
- await mkdir6(path31.dirname(targetPath), { recursive: true });
5881
+ await mkdir7(path37.dirname(targetPath), { recursive: true });
4652
5882
  const serializable = {
4653
5883
  version: defaults.version,
4654
5884
  defaultKind: defaults.defaultKind,
@@ -4714,7 +5944,7 @@ async function reviewShowCommand(id) {
4714
5944
  }
4715
5945
 
4716
5946
  // src/commands/review-approve.ts
4717
- import path32 from "path";
5947
+ import path38 from "path";
4718
5948
 
4719
5949
  // src/commands/review-helpers.ts
4720
5950
  async function runReviewUnderLock(id, underLock) {
@@ -4746,7 +5976,7 @@ async function approveUnderLock(root, id) {
4746
5976
  process.exitCode = 1;
4747
5977
  return;
4748
5978
  }
4749
- const pagePath = path32.join(root, CONCEPTS_DIR, `${candidate.slug}.md`);
5979
+ const pagePath = path38.join(root, CONCEPTS_DIR, `${candidate.slug}.md`);
4750
5980
  await atomicWrite(pagePath, candidate.body);
4751
5981
  status("+", success(`Approved \u2192 ${source(pagePath)}`));
4752
5982
  await persistCandidateSourceStates(root, candidate);
@@ -4806,7 +6036,7 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
4806
6036
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4807
6037
 
4808
6038
  // src/mcp/tools.ts
4809
- import path33 from "path";
6039
+ import path39 from "path";
4810
6040
  import { z } from "zod";
4811
6041
 
4812
6042
  // src/mcp/provider-check.ts
@@ -4814,7 +6044,8 @@ var PROVIDER_KEY_VARS = {
4814
6044
  anthropic: "ANTHROPIC_API_KEY",
4815
6045
  openai: "OPENAI_API_KEY",
4816
6046
  ollama: null,
4817
- minimax: "MINIMAX_API_KEY"
6047
+ minimax: "MINIMAX_API_KEY",
6048
+ copilot: "GITHUB_TOKEN"
4818
6049
  };
4819
6050
  function ensureProviderAvailable() {
4820
6051
  const provider = process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER;
@@ -4942,7 +6173,7 @@ async function pickSearchSlugs(root, question) {
4942
6173
  if (candidates.length > 0) return candidates.map((c) => c.slug);
4943
6174
  } catch {
4944
6175
  }
4945
- const indexContent = await safeReadFile(path33.join(root, INDEX_FILE));
6176
+ const indexContent = await safeReadFile(path39.join(root, INDEX_FILE));
4946
6177
  const { pages } = await selectPages(question, indexContent);
4947
6178
  return pages;
4948
6179
  }
@@ -5001,8 +6232,8 @@ function registerStatusTool(server, root) {
5001
6232
  );
5002
6233
  }
5003
6234
  async function collectStatus(root) {
5004
- const concepts = await collectPageSummaries(path33.join(root, CONCEPTS_DIR));
5005
- const queries = await collectPageSummaries(path33.join(root, QUERIES_DIR));
6235
+ const concepts = await collectPageSummaries(path39.join(root, CONCEPTS_DIR));
6236
+ const queries = await collectPageSummaries(path39.join(root, QUERIES_DIR));
5006
6237
  const state = await readState(root);
5007
6238
  const changes = await detectChanges(root, state);
5008
6239
  const orphans = await findOrphanedSlugs(root);
@@ -5019,7 +6250,7 @@ async function collectStatus(root) {
5019
6250
  };
5020
6251
  }
5021
6252
  async function findOrphanedSlugs(root) {
5022
- const scanned = await scanWikiPages(path33.join(root, CONCEPTS_DIR));
6253
+ const scanned = await scanWikiPages(path39.join(root, CONCEPTS_DIR));
5023
6254
  return scanned.filter(({ meta }) => meta.orphaned).map(({ slug }) => slug);
5024
6255
  }
5025
6256
  async function loadPageRecords(root, slugs) {
@@ -5032,7 +6263,7 @@ async function loadPageRecords(root, slugs) {
5032
6263
  }
5033
6264
  async function readPage(root, slug) {
5034
6265
  for (const dir of PAGE_DIRS2) {
5035
- const content = await safeReadFile(path33.join(root, dir, `${slug}.md`));
6266
+ const content = await safeReadFile(path39.join(root, dir, `${slug}.md`));
5036
6267
  if (!content) continue;
5037
6268
  const { meta, body } = parseFrontmatter(content);
5038
6269
  if (meta.orphaned) continue;
@@ -5047,8 +6278,8 @@ async function readPage(root, slug) {
5047
6278
  }
5048
6279
 
5049
6280
  // src/mcp/resources.ts
5050
- import path34 from "path";
5051
- import { readdir as readdir11 } from "fs/promises";
6281
+ import path40 from "path";
6282
+ import { readdir as readdir12 } from "fs/promises";
5052
6283
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5053
6284
  function jsonContent(uri, payload) {
5054
6285
  return {
@@ -5081,7 +6312,7 @@ function registerIndexResource(server, root) {
5081
6312
  mimeType: "text/markdown"
5082
6313
  },
5083
6314
  async (uri) => {
5084
- const content = await safeReadFile(path34.join(root, INDEX_FILE));
6315
+ const content = await safeReadFile(path40.join(root, INDEX_FILE));
5085
6316
  return { contents: [markdownContent(uri, content)] };
5086
6317
  }
5087
6318
  );
@@ -5148,23 +6379,23 @@ function registerQueryResource(server, root) {
5148
6379
  );
5149
6380
  }
5150
6381
  async function listSources(root) {
5151
- const sourcesPath = path34.join(root, SOURCES_DIR);
6382
+ const sourcesPath = path40.join(root, SOURCES_DIR);
5152
6383
  let files;
5153
6384
  try {
5154
- files = await readdir11(sourcesPath);
6385
+ files = await readdir12(sourcesPath);
5155
6386
  } catch {
5156
6387
  return [];
5157
6388
  }
5158
6389
  const records = [];
5159
6390
  for (const file of files.filter((f) => f.endsWith(".md"))) {
5160
- const content = await safeReadFile(path34.join(sourcesPath, file));
6391
+ const content = await safeReadFile(path40.join(sourcesPath, file));
5161
6392
  const { meta } = parseFrontmatter(content);
5162
6393
  records.push({ filename: file, ...meta });
5163
6394
  }
5164
6395
  return records;
5165
6396
  }
5166
6397
  async function loadPageWithMeta(root, dir, slug) {
5167
- const filePath = path34.join(root, dir, `${slug}.md`);
6398
+ const filePath = path40.join(root, dir, `${slug}.md`);
5168
6399
  const content = await safeReadFile(filePath);
5169
6400
  if (!content) {
5170
6401
  throw new Error(`Page not found: ${dir}/${slug}.md`);
@@ -5173,10 +6404,10 @@ async function loadPageWithMeta(root, dir, slug) {
5173
6404
  return { slug, meta, body: body.trim() };
5174
6405
  }
5175
6406
  async function listPagesUnder(root, dir, scheme) {
5176
- const pagesPath = path34.join(root, dir);
6407
+ const pagesPath = path40.join(root, dir);
5177
6408
  let files;
5178
6409
  try {
5179
- files = await readdir11(pagesPath);
6410
+ files = await readdir12(pagesPath);
5180
6411
  } catch {
5181
6412
  return { resources: [] };
5182
6413
  }
@@ -5220,6 +6451,14 @@ program.command("ingest-session <path>").description("Ingest a coding-agent sess
5220
6451
  process.exit(1);
5221
6452
  }
5222
6453
  });
6454
+ program.command("view").description("Start a local read-only web viewer for the current wiki project").option("--port <port>", "Port to bind (default 0 \u2014 OS-assigned)").option("--host <host>", "Host to bind (requires --allow-lan; default 127.0.0.1)").option("--allow-lan", "Bind beyond loopback (requires --host); off by default for privacy").option("--open", "Open the viewer in the default browser after startup").action(async (options) => {
6455
+ try {
6456
+ await viewCommand(options);
6457
+ } catch (err) {
6458
+ console.error(`\x1B[31mError:\x1B[0m ${err instanceof Error ? err.message : err}`);
6459
+ process.exit(1);
6460
+ }
6461
+ });
5223
6462
  program.command("compile").description("Compile sources/ into an interlinked wiki").option(
5224
6463
  "--review",
5225
6464
  "Write generated pages as review candidates under .llmwiki/candidates/ instead of mutating wiki/. Orphan-marking for deleted sources is deferred until the next non-review compile."
@@ -5346,7 +6585,8 @@ var PROVIDER_KEY_VARS2 = {
5346
6585
  anthropic: "ANTHROPIC_API_KEY",
5347
6586
  openai: "OPENAI_API_KEY",
5348
6587
  ollama: null,
5349
- minimax: "MINIMAX_API_KEY"
6588
+ minimax: "MINIMAX_API_KEY",
6589
+ copilot: "GITHUB_TOKEN"
5350
6590
  };
5351
6591
  function requireProvider() {
5352
6592
  const provider = process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER;