librian 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -2
- package/README.md +39 -12
- package/dist/cli.js +1050 -152
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7263,7 +7263,7 @@ import { resolve as resolve5 } from "node:path";
|
|
|
7263
7263
|
// package.json
|
|
7264
7264
|
var package_default = {
|
|
7265
7265
|
name: "librian",
|
|
7266
|
-
version: "0.
|
|
7266
|
+
version: "0.2.0",
|
|
7267
7267
|
description: "Find and read sections of a markdown docs folder over MCP, with outlines and line ranges that stay current as the docs change",
|
|
7268
7268
|
repository: {
|
|
7269
7269
|
type: "git",
|
|
@@ -7318,11 +7318,6 @@ var package_default = {
|
|
|
7318
7318
|
}
|
|
7319
7319
|
};
|
|
7320
7320
|
|
|
7321
|
-
// src/modules/catalog/catalog.ts
|
|
7322
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
7323
|
-
import { homedir } from "node:os";
|
|
7324
|
-
import { join } from "node:path";
|
|
7325
|
-
|
|
7326
7321
|
// src/_defs/limits.ts
|
|
7327
7322
|
var DEFAULT_MAX_BYTES = 16e3;
|
|
7328
7323
|
var MIN_MAX_BYTES = 4e3;
|
|
@@ -7333,8 +7328,12 @@ var SEARCH_LIMIT_DEFAULT = 20;
|
|
|
7333
7328
|
var SEARCH_LIMIT_MAX = 50;
|
|
7334
7329
|
var COUNT_MAX_FILES = 5e3;
|
|
7335
7330
|
var COUNT_MAX_MS = 2e3;
|
|
7331
|
+
var HISTORY_REFRESH_MS = 5 * 60 * 1e3;
|
|
7336
7332
|
|
|
7337
7333
|
// src/modules/catalog/catalog.ts
|
|
7334
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
7335
|
+
import { homedir } from "node:os";
|
|
7336
|
+
import { join } from "node:path";
|
|
7338
7337
|
var CATALOG_FILE_NAME = "catalog.json";
|
|
7339
7338
|
var ENTRY_NAME_PATTERN = /^[a-z][a-z0-9_]{0,23}$/;
|
|
7340
7339
|
var ENTRY_NAME_MAX_CHARS = 24;
|
|
@@ -7461,7 +7460,7 @@ function defaultEntryName(root, taken) {
|
|
|
7461
7460
|
// src/modules/cli/folder-check.ts
|
|
7462
7461
|
import { statSync as statSync2 } from "node:fs";
|
|
7463
7462
|
import { homedir as homedir2 } from "node:os";
|
|
7464
|
-
import { join as
|
|
7463
|
+
import { join as join4, parse, resolve as resolve2 } from "node:path";
|
|
7465
7464
|
|
|
7466
7465
|
// src/_libs/compare-text.ts
|
|
7467
7466
|
function compareText(left, right) {
|
|
@@ -7496,7 +7495,7 @@ function compareText(left, right) {
|
|
|
7496
7495
|
|
|
7497
7496
|
// src/modules/docs/library.ts
|
|
7498
7497
|
import { readFileSync as readFileSync2, readdirSync, realpathSync, statSync } from "node:fs";
|
|
7499
|
-
import { basename, join as
|
|
7498
|
+
import { basename, join as join3, resolve } from "node:path";
|
|
7500
7499
|
|
|
7501
7500
|
// src/_libs/is-inside.ts
|
|
7502
7501
|
import { isAbsolute, relative, sep } from "node:path";
|
|
@@ -7511,6 +7510,114 @@ function isInside(root, target) {
|
|
|
7511
7510
|
return true;
|
|
7512
7511
|
}
|
|
7513
7512
|
|
|
7513
|
+
// src/modules/docs/blocks.ts
|
|
7514
|
+
import { createHash } from "node:crypto";
|
|
7515
|
+
|
|
7516
|
+
// src/modules/tools/format.ts
|
|
7517
|
+
function textResult(text2) {
|
|
7518
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
7519
|
+
}
|
|
7520
|
+
function errorResult(message) {
|
|
7521
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
7522
|
+
}
|
|
7523
|
+
function byteLength(text2) {
|
|
7524
|
+
return Buffer.byteLength(text2, "utf8");
|
|
7525
|
+
}
|
|
7526
|
+
function formatSize(bytes) {
|
|
7527
|
+
if (bytes < 1e3) {
|
|
7528
|
+
return bytes + "B";
|
|
7529
|
+
}
|
|
7530
|
+
if (bytes < 99950) {
|
|
7531
|
+
return (bytes / 1e3).toFixed(1) + "K";
|
|
7532
|
+
}
|
|
7533
|
+
if (bytes < 999500) {
|
|
7534
|
+
return Math.round(bytes / 1e3) + "K";
|
|
7535
|
+
}
|
|
7536
|
+
return (bytes / 1e6).toFixed(2) + "M";
|
|
7537
|
+
}
|
|
7538
|
+
function pluralize(count, noun) {
|
|
7539
|
+
if (count == 1) {
|
|
7540
|
+
return count + " " + noun;
|
|
7541
|
+
}
|
|
7542
|
+
return count + " " + noun + "s";
|
|
7543
|
+
}
|
|
7544
|
+
function truncateChars(text2, maxChars) {
|
|
7545
|
+
const characters = Array.from(text2);
|
|
7546
|
+
if (characters.length <= maxChars) {
|
|
7547
|
+
return text2;
|
|
7548
|
+
}
|
|
7549
|
+
return characters.slice(0, maxChars - 1).join("") + "\u2026";
|
|
7550
|
+
}
|
|
7551
|
+
function takeBytes(text2, maxBytes) {
|
|
7552
|
+
let result = "";
|
|
7553
|
+
let used = 0;
|
|
7554
|
+
for (const character of text2) {
|
|
7555
|
+
const characterBytes = byteLength(character);
|
|
7556
|
+
if (used + characterBytes > maxBytes) {
|
|
7557
|
+
break;
|
|
7558
|
+
}
|
|
7559
|
+
result += character;
|
|
7560
|
+
used += characterBytes;
|
|
7561
|
+
}
|
|
7562
|
+
return result;
|
|
7563
|
+
}
|
|
7564
|
+
function headingLabel(document, headingIndex) {
|
|
7565
|
+
const heading = document.headings[headingIndex];
|
|
7566
|
+
return "#".repeat(heading.level) + " " + truncateChars(heading.title, TITLE_MAX_CHARS);
|
|
7567
|
+
}
|
|
7568
|
+
function headingChain(document, headingIndex) {
|
|
7569
|
+
const labels = [headingLabel(document, headingIndex)];
|
|
7570
|
+
let ancestor = document.headings[headingIndex].parent;
|
|
7571
|
+
while (ancestor != null) {
|
|
7572
|
+
if (ancestor != document.titleIndex) {
|
|
7573
|
+
labels.unshift(headingLabel(document, ancestor));
|
|
7574
|
+
}
|
|
7575
|
+
ancestor = document.headings[ancestor].parent;
|
|
7576
|
+
}
|
|
7577
|
+
return labels.join(" \u203A ");
|
|
7578
|
+
}
|
|
7579
|
+
function rangeHeader(document, start, end, headingIndex) {
|
|
7580
|
+
let header = document.path + ":" + start + "-" + end + " \xB7 " + formatSize(document.bytesOf(start, end));
|
|
7581
|
+
if (headingIndex != null) {
|
|
7582
|
+
header += " \xB7 " + headingChain(document, headingIndex);
|
|
7583
|
+
}
|
|
7584
|
+
return header;
|
|
7585
|
+
}
|
|
7586
|
+
function cutPage(document, start, end, budgetBytes) {
|
|
7587
|
+
const joinLines2 = (lastLine2) => document.lines.slice(start - 1, lastLine2).join("\n");
|
|
7588
|
+
if (document.bytesOf(start, end) - 1 <= budgetBytes) {
|
|
7589
|
+
return { text: joinLines2(end), lastLine: end };
|
|
7590
|
+
}
|
|
7591
|
+
let fitting = start - 1;
|
|
7592
|
+
let overflowing = end;
|
|
7593
|
+
while (overflowing - fitting > 1) {
|
|
7594
|
+
const middle = fitting + overflowing >> 1;
|
|
7595
|
+
if (document.bytesOf(start, middle) - 1 <= budgetBytes) {
|
|
7596
|
+
fitting = middle;
|
|
7597
|
+
} else {
|
|
7598
|
+
overflowing = middle;
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
if (fitting < start) {
|
|
7602
|
+
const line = document.lines[start - 1];
|
|
7603
|
+
const marker = "[line " + start + " cut: " + byteLength(line) + "B]";
|
|
7604
|
+
const allowed = budgetBytes - byteLength(marker) - 1;
|
|
7605
|
+
if (allowed <= 0) {
|
|
7606
|
+
return { text: marker, lastLine: start };
|
|
7607
|
+
}
|
|
7608
|
+
return { text: takeBytes(line, allowed) + " " + marker, lastLine: start };
|
|
7609
|
+
}
|
|
7610
|
+
let lastLine = fitting;
|
|
7611
|
+
const halfway = start + Math.floor((fitting - start) / 2);
|
|
7612
|
+
for (let line = fitting; line > halfway; line--) {
|
|
7613
|
+
if (document.lines[line - 1].trim() == "") {
|
|
7614
|
+
lastLine = line;
|
|
7615
|
+
break;
|
|
7616
|
+
}
|
|
7617
|
+
}
|
|
7618
|
+
return { text: joinLines2(lastLine), lastLine };
|
|
7619
|
+
}
|
|
7620
|
+
|
|
7514
7621
|
// src/modules/docs/parse.ts
|
|
7515
7622
|
var FENCE_OPENING = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
7516
7623
|
var FENCE_CLOSING = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
@@ -7684,6 +7791,353 @@ function blockAt(document, line) {
|
|
|
7684
7791
|
return { start: headings[found].line, end: headings[found].blockEnd, headingIndex: found };
|
|
7685
7792
|
}
|
|
7686
7793
|
|
|
7794
|
+
// src/modules/docs/blocks.ts
|
|
7795
|
+
var INTRO_LABEL = "(intro)";
|
|
7796
|
+
function shortHash(text2, length) {
|
|
7797
|
+
return createHash("sha1").update(text2).digest("base64url").slice(0, length);
|
|
7798
|
+
}
|
|
7799
|
+
function hashLines(document, start, end) {
|
|
7800
|
+
return shortHash(document.lines.slice(start - 1, end).join("\n"), 8);
|
|
7801
|
+
}
|
|
7802
|
+
function isBlank(document, start, end) {
|
|
7803
|
+
for (let line = start; line <= end; line += 1) {
|
|
7804
|
+
if (document.lines[line - 1].trim() != "") {
|
|
7805
|
+
return false;
|
|
7806
|
+
}
|
|
7807
|
+
}
|
|
7808
|
+
return true;
|
|
7809
|
+
}
|
|
7810
|
+
function encodeBlocks(blocks) {
|
|
7811
|
+
return JSON.stringify(blocks.map((block) => ({ h: block.key, x: block.hash })));
|
|
7812
|
+
}
|
|
7813
|
+
function decodeBlocks(text2) {
|
|
7814
|
+
const parsed = JSON.parse(text2);
|
|
7815
|
+
return parsed.map((entry) => ({ key: entry.h, hash: entry.x }));
|
|
7816
|
+
}
|
|
7817
|
+
function hashBlocks(blocks) {
|
|
7818
|
+
return shortHash(encodeBlocks(blocks), 16);
|
|
7819
|
+
}
|
|
7820
|
+
function listBlocks(document) {
|
|
7821
|
+
const blocks = [];
|
|
7822
|
+
const headings = document.headings;
|
|
7823
|
+
let introEnd = document.lines.length;
|
|
7824
|
+
if (headings.length > 0) {
|
|
7825
|
+
introEnd = headings[0].line - 1;
|
|
7826
|
+
}
|
|
7827
|
+
if (introEnd >= 1 && isBlank(document, 1, introEnd) == false) {
|
|
7828
|
+
blocks.push({ key: "", hash: hashLines(document, 1, introEnd), start: 1, end: introEnd, headingIndex: null });
|
|
7829
|
+
}
|
|
7830
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
7831
|
+
const heading = headings[index];
|
|
7832
|
+
blocks.push({
|
|
7833
|
+
key: "#".repeat(heading.level) + " " + heading.text,
|
|
7834
|
+
hash: hashLines(document, heading.line + 1, heading.blockEnd),
|
|
7835
|
+
start: heading.line,
|
|
7836
|
+
end: heading.blockEnd,
|
|
7837
|
+
headingIndex: index
|
|
7838
|
+
});
|
|
7839
|
+
}
|
|
7840
|
+
return { blocks, hash: hashBlocks(blocks) };
|
|
7841
|
+
}
|
|
7842
|
+
function labelOfBlock(document, block) {
|
|
7843
|
+
if (block.headingIndex == null) {
|
|
7844
|
+
return INTRO_LABEL;
|
|
7845
|
+
}
|
|
7846
|
+
return headingChain(document, block.headingIndex);
|
|
7847
|
+
}
|
|
7848
|
+
var KEY_PATTERN = /^(#{1,6}) (.*)$/;
|
|
7849
|
+
function keyLevel(block) {
|
|
7850
|
+
const match = KEY_PATTERN.exec(block.key);
|
|
7851
|
+
if (match == null) {
|
|
7852
|
+
return 0;
|
|
7853
|
+
}
|
|
7854
|
+
return match[1].length;
|
|
7855
|
+
}
|
|
7856
|
+
function keyLabel(block) {
|
|
7857
|
+
const match = KEY_PATTERN.exec(block.key);
|
|
7858
|
+
if (match == null) {
|
|
7859
|
+
return INTRO_LABEL;
|
|
7860
|
+
}
|
|
7861
|
+
return match[1] + " " + truncateChars(stripInlineMarkdown(match[2]), TITLE_MAX_CHARS);
|
|
7862
|
+
}
|
|
7863
|
+
function labelOfRecord(blocks, index) {
|
|
7864
|
+
if (blocks[index].key == "") {
|
|
7865
|
+
return INTRO_LABEL;
|
|
7866
|
+
}
|
|
7867
|
+
let titleIndex = -1;
|
|
7868
|
+
for (let position = 0; position < blocks.length; position += 1) {
|
|
7869
|
+
if (blocks[position].key == "") {
|
|
7870
|
+
continue;
|
|
7871
|
+
}
|
|
7872
|
+
if (keyLevel(blocks[position]) == 1) {
|
|
7873
|
+
titleIndex = position;
|
|
7874
|
+
}
|
|
7875
|
+
break;
|
|
7876
|
+
}
|
|
7877
|
+
const labels = [keyLabel(blocks[index])];
|
|
7878
|
+
let level = keyLevel(blocks[index]);
|
|
7879
|
+
for (let position = index - 1; position >= 0 && level > 1; position -= 1) {
|
|
7880
|
+
const ancestorLevel = keyLevel(blocks[position]);
|
|
7881
|
+
if (ancestorLevel == 0 || ancestorLevel >= level) {
|
|
7882
|
+
continue;
|
|
7883
|
+
}
|
|
7884
|
+
level = ancestorLevel;
|
|
7885
|
+
if (position != titleIndex) {
|
|
7886
|
+
labels.unshift(keyLabel(blocks[position]));
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
return labels.join(" \u203A ");
|
|
7890
|
+
}
|
|
7891
|
+
|
|
7892
|
+
// src/modules/docs/history-store.ts
|
|
7893
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
7894
|
+
import { dirname, join as join2 } from "node:path";
|
|
7895
|
+
var HISTORY_SCHEMA_VERSION = "1";
|
|
7896
|
+
var HISTORY_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
7897
|
+
var HISTORY_BUSY_TIMEOUT_MS = 1e3;
|
|
7898
|
+
var DELETED_HASH = "-";
|
|
7899
|
+
var HISTORY_REMOVED_MESSAGE = "History was removed. Open a new session to start a new one.";
|
|
7900
|
+
var HISTORY_BUSY_MESSAGE = "History is busy right now; try again in a moment.";
|
|
7901
|
+
function resolveHistoryFile(home, root) {
|
|
7902
|
+
void root;
|
|
7903
|
+
return join2(home, "history.db");
|
|
7904
|
+
}
|
|
7905
|
+
var HistoryBusyError = class extends Error {
|
|
7906
|
+
constructor() {
|
|
7907
|
+
super(HISTORY_BUSY_MESSAGE);
|
|
7908
|
+
}
|
|
7909
|
+
};
|
|
7910
|
+
function describeHistoryTrouble(file2, status) {
|
|
7911
|
+
if (status.removed == true) {
|
|
7912
|
+
return "librian: history.db was removed; changes is off for this session. Open a new session to start a new history.";
|
|
7913
|
+
}
|
|
7914
|
+
return `librian: history is unavailable: ${status.reason}; changes is off. Delete ${file2} to start over.`;
|
|
7915
|
+
}
|
|
7916
|
+
function historyToolMessage(file2, status) {
|
|
7917
|
+
if (status.removed == true) {
|
|
7918
|
+
return HISTORY_REMOVED_MESSAGE;
|
|
7919
|
+
}
|
|
7920
|
+
return `History is unavailable: ${status.reason}. Delete ${file2} to start over.`;
|
|
7921
|
+
}
|
|
7922
|
+
var sqliteModule = null;
|
|
7923
|
+
function loadSqlite() {
|
|
7924
|
+
if (sqliteModule != null) {
|
|
7925
|
+
return sqliteModule;
|
|
7926
|
+
}
|
|
7927
|
+
const originalEmitWarning = process.emitWarning;
|
|
7928
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
7929
|
+
const text2 = typeof warning == "string" ? warning : warning.message;
|
|
7930
|
+
if (/sqlite/i.test(text2) == true) {
|
|
7931
|
+
return;
|
|
7932
|
+
}
|
|
7933
|
+
return originalEmitWarning.call(process, warning, ...rest);
|
|
7934
|
+
});
|
|
7935
|
+
try {
|
|
7936
|
+
sqliteModule = process.getBuiltinModule("node:sqlite");
|
|
7937
|
+
} finally {
|
|
7938
|
+
process.emitWarning = originalEmitWarning;
|
|
7939
|
+
}
|
|
7940
|
+
return sqliteModule;
|
|
7941
|
+
}
|
|
7942
|
+
function describeError(error62) {
|
|
7943
|
+
if (error62 instanceof Error) {
|
|
7944
|
+
return error62.message;
|
|
7945
|
+
}
|
|
7946
|
+
return String(error62);
|
|
7947
|
+
}
|
|
7948
|
+
function isBusyError(error62) {
|
|
7949
|
+
if (error62 instanceof Error == false) {
|
|
7950
|
+
return false;
|
|
7951
|
+
}
|
|
7952
|
+
const code = error62.errcode;
|
|
7953
|
+
if (typeof code == "number" && ((code & 255) == 5 || (code & 255) == 6)) {
|
|
7954
|
+
return true;
|
|
7955
|
+
}
|
|
7956
|
+
return /database is locked|database table is locked|busy/i.test(error62.message);
|
|
7957
|
+
}
|
|
7958
|
+
function openHistoryStore(file2, now = Date.now) {
|
|
7959
|
+
let status = null;
|
|
7960
|
+
let hasWarnedBusy = false;
|
|
7961
|
+
const listeners = [];
|
|
7962
|
+
function say(line) {
|
|
7963
|
+
for (const listener of listeners) {
|
|
7964
|
+
listener(line);
|
|
7965
|
+
}
|
|
7966
|
+
}
|
|
7967
|
+
function disable(reason, removed) {
|
|
7968
|
+
if (status != null) {
|
|
7969
|
+
return;
|
|
7970
|
+
}
|
|
7971
|
+
status = { reason, removed };
|
|
7972
|
+
say(describeHistoryTrouble(file2, status));
|
|
7973
|
+
}
|
|
7974
|
+
try {
|
|
7975
|
+
mkdirSync2(dirname(file2), { recursive: true });
|
|
7976
|
+
const opened = new (loadSqlite()).DatabaseSync(file2);
|
|
7977
|
+
try {
|
|
7978
|
+
opened.exec(`PRAGMA busy_timeout=${HISTORY_BUSY_TIMEOUT_MS}`);
|
|
7979
|
+
opened.exec("PRAGMA journal_mode=WAL");
|
|
7980
|
+
opened.exec("CREATE TABLE IF NOT EXISTS meta (key text PRIMARY KEY, value text)");
|
|
7981
|
+
const knownVersion = opened.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
7982
|
+
if (knownVersion != null && knownVersion.value != HISTORY_SCHEMA_VERSION) {
|
|
7983
|
+
throw new Error(`unknown history schema version ${knownVersion.value}`);
|
|
7984
|
+
}
|
|
7985
|
+
opened.exec(`CREATE TABLE IF NOT EXISTS file_version (
|
|
7986
|
+
root text NOT NULL,
|
|
7987
|
+
path text NOT NULL,
|
|
7988
|
+
observed_at integer NOT NULL,
|
|
7989
|
+
blocks_hash text NOT NULL,
|
|
7990
|
+
blocks text,
|
|
7991
|
+
CONSTRAINT pk_file_version PRIMARY KEY (root, path, observed_at)
|
|
7992
|
+
)`);
|
|
7993
|
+
opened.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', ?)").run(HISTORY_SCHEMA_VERSION);
|
|
7994
|
+
opened.prepare(`DELETE FROM file_version
|
|
7995
|
+
WHERE observed_at < ?
|
|
7996
|
+
AND observed_at < (SELECT MAX(newer.observed_at) FROM file_version newer WHERE newer.root = file_version.root AND newer.path = file_version.path)`).run(now() - HISTORY_RETENTION_MS);
|
|
7997
|
+
} finally {
|
|
7998
|
+
opened.close();
|
|
7999
|
+
}
|
|
8000
|
+
} catch (error62) {
|
|
8001
|
+
status = { reason: describeError(error62), removed: false };
|
|
8002
|
+
}
|
|
8003
|
+
function use(work) {
|
|
8004
|
+
if (status != null) {
|
|
8005
|
+
throw new Error(status.reason);
|
|
8006
|
+
}
|
|
8007
|
+
if (existsSync2(file2) == false) {
|
|
8008
|
+
disable("history.db is gone", true);
|
|
8009
|
+
throw new Error("history.db is gone");
|
|
8010
|
+
}
|
|
8011
|
+
let opened = null;
|
|
8012
|
+
try {
|
|
8013
|
+
opened = new (loadSqlite()).DatabaseSync(file2);
|
|
8014
|
+
opened.exec(`PRAGMA busy_timeout=${HISTORY_BUSY_TIMEOUT_MS}`);
|
|
8015
|
+
return work(opened);
|
|
8016
|
+
} catch (error62) {
|
|
8017
|
+
if (isBusyError(error62) == true) {
|
|
8018
|
+
throw new HistoryBusyError();
|
|
8019
|
+
}
|
|
8020
|
+
disable(describeError(error62), false);
|
|
8021
|
+
throw error62;
|
|
8022
|
+
} finally {
|
|
8023
|
+
try {
|
|
8024
|
+
opened?.close();
|
|
8025
|
+
} catch {
|
|
8026
|
+
}
|
|
8027
|
+
}
|
|
8028
|
+
}
|
|
8029
|
+
function read(fallback, work) {
|
|
8030
|
+
if (status != null) {
|
|
8031
|
+
return fallback;
|
|
8032
|
+
}
|
|
8033
|
+
try {
|
|
8034
|
+
return use(work);
|
|
8035
|
+
} catch (error62) {
|
|
8036
|
+
if (error62 instanceof HistoryBusyError) {
|
|
8037
|
+
throw error62;
|
|
8038
|
+
}
|
|
8039
|
+
return fallback;
|
|
8040
|
+
}
|
|
8041
|
+
}
|
|
8042
|
+
function readBaselines(root, sinceMs) {
|
|
8043
|
+
return read(/* @__PURE__ */ new Map(), (opened) => {
|
|
8044
|
+
const rows = opened.prepare(`SELECT v.path AS path, v.observed_at AS observed_at, v.blocks_hash AS blocks_hash, v.blocks IS NULL AS deleted
|
|
8045
|
+
FROM file_version v
|
|
8046
|
+
JOIN (SELECT path, MAX(observed_at) AS at FROM file_version WHERE root = ? AND observed_at <= ? GROUP BY path) latest
|
|
8047
|
+
ON latest.path = v.path AND latest.at = v.observed_at
|
|
8048
|
+
WHERE v.root = ?`).all(root, sinceMs, root);
|
|
8049
|
+
const result = /* @__PURE__ */ new Map();
|
|
8050
|
+
for (const row of rows) {
|
|
8051
|
+
result.set(row.path, { observedMs: row.observed_at, hash: row.blocks_hash, deleted: row.deleted != 0 });
|
|
8052
|
+
}
|
|
8053
|
+
return result;
|
|
8054
|
+
});
|
|
8055
|
+
}
|
|
8056
|
+
return {
|
|
8057
|
+
file: file2,
|
|
8058
|
+
status: () => status,
|
|
8059
|
+
onTrouble(listener) {
|
|
8060
|
+
listeners.push(listener);
|
|
8061
|
+
},
|
|
8062
|
+
lastHashes(root) {
|
|
8063
|
+
const result = /* @__PURE__ */ new Map();
|
|
8064
|
+
for (const [path, baseline] of readBaselines(root, Number.MAX_SAFE_INTEGER)) {
|
|
8065
|
+
result.set(path, baseline.hash);
|
|
8066
|
+
}
|
|
8067
|
+
return result;
|
|
8068
|
+
},
|
|
8069
|
+
append(root, entries) {
|
|
8070
|
+
if (entries.length == 0) {
|
|
8071
|
+
return true;
|
|
8072
|
+
}
|
|
8073
|
+
try {
|
|
8074
|
+
use((opened) => {
|
|
8075
|
+
const selectLast = opened.prepare("SELECT blocks_hash FROM file_version WHERE root = ? AND path = ? ORDER BY observed_at DESC LIMIT 1");
|
|
8076
|
+
const insert = opened.prepare("INSERT INTO file_version (root, path, observed_at, blocks_hash, blocks) VALUES (?, ?, ?, ?, ?) ON CONFLICT (root, path, observed_at) DO NOTHING");
|
|
8077
|
+
opened.exec("BEGIN IMMEDIATE");
|
|
8078
|
+
try {
|
|
8079
|
+
for (const entry of entries) {
|
|
8080
|
+
const last = selectLast.get(root, entry.path);
|
|
8081
|
+
if (last != null && last.blocks_hash == entry.hash) {
|
|
8082
|
+
continue;
|
|
8083
|
+
}
|
|
8084
|
+
let blocksText = null;
|
|
8085
|
+
if (entry.blocks != null) {
|
|
8086
|
+
blocksText = encodeBlocks(entry.blocks);
|
|
8087
|
+
}
|
|
8088
|
+
insert.run(root, entry.path, entry.observedMs, entry.hash, blocksText);
|
|
8089
|
+
}
|
|
8090
|
+
opened.exec("COMMIT");
|
|
8091
|
+
} catch (error62) {
|
|
8092
|
+
try {
|
|
8093
|
+
opened.exec("ROLLBACK");
|
|
8094
|
+
} catch {
|
|
8095
|
+
}
|
|
8096
|
+
throw error62;
|
|
8097
|
+
}
|
|
8098
|
+
});
|
|
8099
|
+
return true;
|
|
8100
|
+
} catch (error62) {
|
|
8101
|
+
if (error62 instanceof HistoryBusyError && hasWarnedBusy == false) {
|
|
8102
|
+
hasWarnedBusy = true;
|
|
8103
|
+
say("librian: history.db is locked by another process; the change will be recorded on a later round.");
|
|
8104
|
+
}
|
|
8105
|
+
return false;
|
|
8106
|
+
}
|
|
8107
|
+
},
|
|
8108
|
+
firstObservedMs(root) {
|
|
8109
|
+
return read(null, (opened) => {
|
|
8110
|
+
const row = opened.prepare("SELECT MIN(observed_at) AS first FROM file_version WHERE root = ?").get(root);
|
|
8111
|
+
return row.first;
|
|
8112
|
+
});
|
|
8113
|
+
},
|
|
8114
|
+
baselines: readBaselines,
|
|
8115
|
+
blocksAt(root, path, observedMs) {
|
|
8116
|
+
return read(null, (opened) => {
|
|
8117
|
+
const row = opened.prepare("SELECT blocks FROM file_version WHERE root = ? AND path = ? AND observed_at = ?").get(root, path, observedMs);
|
|
8118
|
+
if (row == null || row.blocks == null) {
|
|
8119
|
+
return null;
|
|
8120
|
+
}
|
|
8121
|
+
return decodeBlocks(row.blocks);
|
|
8122
|
+
});
|
|
8123
|
+
},
|
|
8124
|
+
close() {
|
|
8125
|
+
if (status == null) {
|
|
8126
|
+
status = { reason: "the history was closed", removed: false };
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
};
|
|
8130
|
+
}
|
|
8131
|
+
var sharedStores = /* @__PURE__ */ new Map();
|
|
8132
|
+
function sharedHistoryStore(file2) {
|
|
8133
|
+
let store = sharedStores.get(file2);
|
|
8134
|
+
if (store == null) {
|
|
8135
|
+
store = openHistoryStore(file2);
|
|
8136
|
+
sharedStores.set(file2, store);
|
|
8137
|
+
}
|
|
8138
|
+
return store;
|
|
8139
|
+
}
|
|
8140
|
+
|
|
7687
8141
|
// src/modules/docs/library.ts
|
|
7688
8142
|
var ALWAYS_EXCLUDED_DIRECTORY = "node_modules";
|
|
7689
8143
|
function normalizeExclude(entry) {
|
|
@@ -7733,7 +8187,7 @@ function listMarkdownPaths(root, exclude, limits) {
|
|
|
7733
8187
|
if (entry.name.startsWith(".") == true || entry.name == ALWAYS_EXCLUDED_DIRECTORY) {
|
|
7734
8188
|
continue;
|
|
7735
8189
|
}
|
|
7736
|
-
walk(
|
|
8190
|
+
walk(join3(absoluteDirectory, entry.name), relativePath + "/");
|
|
7737
8191
|
continue;
|
|
7738
8192
|
}
|
|
7739
8193
|
if (entry.isFile() == true && /\.md$/i.test(entry.name) == true) {
|
|
@@ -7758,11 +8212,52 @@ function openLibrary(options) {
|
|
|
7758
8212
|
name = root;
|
|
7759
8213
|
}
|
|
7760
8214
|
const cache = /* @__PURE__ */ new Map();
|
|
8215
|
+
const store = options.history ?? null;
|
|
8216
|
+
const now = options.now ?? Date.now;
|
|
8217
|
+
let startedMs = null;
|
|
8218
|
+
let observedMs = null;
|
|
8219
|
+
let lastHashes = null;
|
|
8220
|
+
const unrecorded = /* @__PURE__ */ new Map();
|
|
7761
8221
|
function refresh() {
|
|
8222
|
+
let stamp = now();
|
|
8223
|
+
if (observedMs != null && stamp <= observedMs) {
|
|
8224
|
+
stamp = observedMs + 1;
|
|
8225
|
+
}
|
|
8226
|
+
observedMs = stamp;
|
|
8227
|
+
if (startedMs == null) {
|
|
8228
|
+
startedMs = stamp;
|
|
8229
|
+
}
|
|
8230
|
+
const pending = [];
|
|
8231
|
+
const isFirstRefresh = lastHashes == null;
|
|
8232
|
+
if (store != null && lastHashes == null) {
|
|
8233
|
+
try {
|
|
8234
|
+
lastHashes = store.lastHashes(root);
|
|
8235
|
+
} catch {
|
|
8236
|
+
lastHashes = /* @__PURE__ */ new Map();
|
|
8237
|
+
}
|
|
8238
|
+
}
|
|
8239
|
+
const noteVersion = (path, listed) => {
|
|
8240
|
+
if (lastHashes == null || lastHashes.get(path) == listed.hash) {
|
|
8241
|
+
return;
|
|
8242
|
+
}
|
|
8243
|
+
lastHashes.set(path, listed.hash);
|
|
8244
|
+
pending.push({ path, observedMs: stamp, blocks: listed.blocks, hash: listed.hash });
|
|
8245
|
+
};
|
|
8246
|
+
const noteDeleted = (path) => {
|
|
8247
|
+
if (lastHashes == null) {
|
|
8248
|
+
return;
|
|
8249
|
+
}
|
|
8250
|
+
const last = lastHashes.get(path);
|
|
8251
|
+
if (last == null || last == DELETED_HASH) {
|
|
8252
|
+
return;
|
|
8253
|
+
}
|
|
8254
|
+
lastHashes.set(path, DELETED_HASH);
|
|
8255
|
+
pending.push({ path, observedMs: stamp, blocks: null, hash: DELETED_HASH });
|
|
8256
|
+
};
|
|
7762
8257
|
const documents = [];
|
|
7763
8258
|
const seen = /* @__PURE__ */ new Set();
|
|
7764
8259
|
for (const path of listMarkdownPaths(root, options.exclude).paths.sort(compareText)) {
|
|
7765
|
-
const absolutePath =
|
|
8260
|
+
const absolutePath = join3(root, path);
|
|
7766
8261
|
let modifiedMs;
|
|
7767
8262
|
let size;
|
|
7768
8263
|
try {
|
|
@@ -7787,123 +8282,68 @@ function openLibrary(options) {
|
|
|
7787
8282
|
} catch {
|
|
7788
8283
|
continue;
|
|
7789
8284
|
}
|
|
7790
|
-
|
|
8285
|
+
let listed = null;
|
|
8286
|
+
if (store != null) {
|
|
8287
|
+
listed = listBlocks(document);
|
|
8288
|
+
noteVersion(path, listed);
|
|
8289
|
+
}
|
|
8290
|
+
cache.set(path, { modifiedMs, size, document, listed });
|
|
7791
8291
|
seen.add(path);
|
|
7792
8292
|
documents.push(document);
|
|
7793
8293
|
}
|
|
7794
8294
|
for (const path of [...cache.keys()]) {
|
|
7795
8295
|
if (seen.has(path) == false) {
|
|
7796
8296
|
cache.delete(path);
|
|
8297
|
+
noteDeleted(path);
|
|
7797
8298
|
}
|
|
7798
8299
|
}
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
function textResult(text2) {
|
|
7806
|
-
return { content: [{ type: "text", text: text2 }] };
|
|
7807
|
-
}
|
|
7808
|
-
function errorResult(message) {
|
|
7809
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
7810
|
-
}
|
|
7811
|
-
function byteLength(text2) {
|
|
7812
|
-
return Buffer.byteLength(text2, "utf8");
|
|
7813
|
-
}
|
|
7814
|
-
function formatSize(bytes) {
|
|
7815
|
-
if (bytes < 1e3) {
|
|
7816
|
-
return bytes + "B";
|
|
7817
|
-
}
|
|
7818
|
-
if (bytes < 99950) {
|
|
7819
|
-
return (bytes / 1e3).toFixed(1) + "K";
|
|
7820
|
-
}
|
|
7821
|
-
if (bytes < 999500) {
|
|
7822
|
-
return Math.round(bytes / 1e3) + "K";
|
|
7823
|
-
}
|
|
7824
|
-
return (bytes / 1e6).toFixed(2) + "M";
|
|
7825
|
-
}
|
|
7826
|
-
function pluralize(count, noun) {
|
|
7827
|
-
if (count == 1) {
|
|
7828
|
-
return count + " " + noun;
|
|
7829
|
-
}
|
|
7830
|
-
return count + " " + noun + "s";
|
|
7831
|
-
}
|
|
7832
|
-
function truncateChars(text2, maxChars) {
|
|
7833
|
-
const characters = Array.from(text2);
|
|
7834
|
-
if (characters.length <= maxChars) {
|
|
7835
|
-
return text2;
|
|
7836
|
-
}
|
|
7837
|
-
return characters.slice(0, maxChars - 1).join("") + "\u2026";
|
|
7838
|
-
}
|
|
7839
|
-
function takeBytes(text2, maxBytes) {
|
|
7840
|
-
let result = "";
|
|
7841
|
-
let used = 0;
|
|
7842
|
-
for (const character of text2) {
|
|
7843
|
-
const characterBytes = byteLength(character);
|
|
7844
|
-
if (used + characterBytes > maxBytes) {
|
|
7845
|
-
break;
|
|
7846
|
-
}
|
|
7847
|
-
result += character;
|
|
7848
|
-
used += characterBytes;
|
|
7849
|
-
}
|
|
7850
|
-
return result;
|
|
7851
|
-
}
|
|
7852
|
-
function headingLabel(document, headingIndex) {
|
|
7853
|
-
const heading = document.headings[headingIndex];
|
|
7854
|
-
return "#".repeat(heading.level) + " " + truncateChars(heading.title, TITLE_MAX_CHARS);
|
|
7855
|
-
}
|
|
7856
|
-
function headingChain(document, headingIndex) {
|
|
7857
|
-
const labels = [headingLabel(document, headingIndex)];
|
|
7858
|
-
let ancestor = document.headings[headingIndex].parent;
|
|
7859
|
-
while (ancestor != null) {
|
|
7860
|
-
if (ancestor != document.titleIndex) {
|
|
7861
|
-
labels.unshift(headingLabel(document, ancestor));
|
|
7862
|
-
}
|
|
7863
|
-
ancestor = document.headings[ancestor].parent;
|
|
7864
|
-
}
|
|
7865
|
-
return labels.join(" \u203A ");
|
|
7866
|
-
}
|
|
7867
|
-
function rangeHeader(document, start, end, headingIndex) {
|
|
7868
|
-
let header = document.path + ":" + start + "-" + end + " \xB7 " + formatSize(document.bytesOf(start, end));
|
|
7869
|
-
if (headingIndex != null) {
|
|
7870
|
-
header += " \xB7 " + headingChain(document, headingIndex);
|
|
7871
|
-
}
|
|
7872
|
-
return header;
|
|
7873
|
-
}
|
|
7874
|
-
function cutPage(document, start, end, budgetBytes) {
|
|
7875
|
-
const joinLines2 = (lastLine2) => document.lines.slice(start - 1, lastLine2).join("\n");
|
|
7876
|
-
if (document.bytesOf(start, end) - 1 <= budgetBytes) {
|
|
7877
|
-
return { text: joinLines2(end), lastLine: end };
|
|
7878
|
-
}
|
|
7879
|
-
let fitting = start - 1;
|
|
7880
|
-
let overflowing = end;
|
|
7881
|
-
while (overflowing - fitting > 1) {
|
|
7882
|
-
const middle = fitting + overflowing >> 1;
|
|
7883
|
-
if (document.bytesOf(start, middle) - 1 <= budgetBytes) {
|
|
7884
|
-
fitting = middle;
|
|
7885
|
-
} else {
|
|
7886
|
-
overflowing = middle;
|
|
8300
|
+
if (isFirstRefresh == true && lastHashes != null) {
|
|
8301
|
+
for (const path of [...lastHashes.keys()]) {
|
|
8302
|
+
if (seen.has(path) == false) {
|
|
8303
|
+
noteDeleted(path);
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
7887
8306
|
}
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
8307
|
+
if (store != null) {
|
|
8308
|
+
for (const entry of pending) {
|
|
8309
|
+
unrecorded.set(entry.path, entry);
|
|
8310
|
+
}
|
|
8311
|
+
if (store.status() != null) {
|
|
8312
|
+
unrecorded.clear();
|
|
8313
|
+
} else if (unrecorded.size > 0 && store.append(root, [...unrecorded.values()]) == true) {
|
|
8314
|
+
unrecorded.clear();
|
|
8315
|
+
}
|
|
7895
8316
|
}
|
|
7896
|
-
return
|
|
8317
|
+
return documents;
|
|
7897
8318
|
}
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
8319
|
+
const history = {
|
|
8320
|
+
file: store?.file ?? null,
|
|
8321
|
+
get startedMs() {
|
|
8322
|
+
return startedMs;
|
|
8323
|
+
},
|
|
8324
|
+
get observedMs() {
|
|
8325
|
+
return observedMs;
|
|
8326
|
+
},
|
|
8327
|
+
status() {
|
|
8328
|
+
if (store == null) {
|
|
8329
|
+
return { reason: "history is not enabled", removed: false };
|
|
8330
|
+
}
|
|
8331
|
+
return store.status();
|
|
8332
|
+
},
|
|
8333
|
+
currentBlocks(path) {
|
|
8334
|
+
return cache.get(path)?.listed ?? null;
|
|
8335
|
+
},
|
|
8336
|
+
firstObservedMs() {
|
|
8337
|
+
return store?.firstObservedMs(root) ?? null;
|
|
8338
|
+
},
|
|
8339
|
+
baselines(sinceMs) {
|
|
8340
|
+
return store?.baselines(root, sinceMs) ?? /* @__PURE__ */ new Map();
|
|
8341
|
+
},
|
|
8342
|
+
blocksAt(path, at) {
|
|
8343
|
+
return store?.blocksAt(root, path, at) ?? null;
|
|
7904
8344
|
}
|
|
7905
|
-
}
|
|
7906
|
-
return {
|
|
8345
|
+
};
|
|
8346
|
+
return { root, name, refresh, history };
|
|
7907
8347
|
}
|
|
7908
8348
|
|
|
7909
8349
|
// src/modules/cli/folder-check.ts
|
|
@@ -7969,7 +8409,7 @@ function checkFolder(root, exclude) {
|
|
|
7969
8409
|
const sized = scan.paths.map((path) => {
|
|
7970
8410
|
let bytes = 0;
|
|
7971
8411
|
try {
|
|
7972
|
-
bytes = statSync2(
|
|
8412
|
+
bytes = statSync2(join4(root, path)).size;
|
|
7973
8413
|
} catch {
|
|
7974
8414
|
bytes = 0;
|
|
7975
8415
|
}
|
|
@@ -9435,7 +9875,7 @@ var USAGE = [
|
|
|
9435
9875
|
"",
|
|
9436
9876
|
" --catalog Read the docs folders from <LIBRIAN_HOME>/catalog.json (default ~/.librian)",
|
|
9437
9877
|
" --root <folder> Markdown docs folder to serve",
|
|
9438
|
-
" --exclude <name> Skip a folder name (e.g. evidence) or a root-relative path (e.g.
|
|
9878
|
+
" --exclude <name> Skip a folder name (e.g. evidence) or a root-relative path (e.g. guide/archive); repeatable",
|
|
9439
9879
|
` --max-bytes <n> Response size limit in UTF-8 bytes (default ${DEFAULT_MAX_BYTES}, minimum ${MIN_MAX_BYTES})`,
|
|
9440
9880
|
" --help Show this help",
|
|
9441
9881
|
" --version Show the version"
|
|
@@ -38950,6 +39390,142 @@ var StdioServerTransport = class {
|
|
|
38950
39390
|
}
|
|
38951
39391
|
};
|
|
38952
39392
|
|
|
39393
|
+
// src/_libs/local-time.ts
|
|
39394
|
+
function pad(value, width) {
|
|
39395
|
+
return String(value).padStart(width, "0");
|
|
39396
|
+
}
|
|
39397
|
+
function formatLocalIso(ms) {
|
|
39398
|
+
const date5 = new Date(ms);
|
|
39399
|
+
const offsetMinutes = -date5.getTimezoneOffset();
|
|
39400
|
+
let sign = "+";
|
|
39401
|
+
if (offsetMinutes < 0) {
|
|
39402
|
+
sign = "-";
|
|
39403
|
+
}
|
|
39404
|
+
const absolute = Math.abs(offsetMinutes);
|
|
39405
|
+
const day = `${pad(date5.getFullYear(), 4)}-${pad(date5.getMonth() + 1, 2)}-${pad(date5.getDate(), 2)}`;
|
|
39406
|
+
const time3 = `${pad(date5.getHours(), 2)}:${pad(date5.getMinutes(), 2)}:${pad(date5.getSeconds(), 2)}.${pad(date5.getMilliseconds(), 3)}`;
|
|
39407
|
+
return `${day}T${time3}${sign}${pad(Math.floor(absolute / 60), 2)}:${pad(absolute % 60, 2)}`;
|
|
39408
|
+
}
|
|
39409
|
+
var SINCE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?(Z|z|[+-]\d{2}(?::?\d{2})?)?)?$/;
|
|
39410
|
+
function parseIsoTime(text2) {
|
|
39411
|
+
const match = SINCE_PATTERN.exec(text2.trim());
|
|
39412
|
+
if (match == null) {
|
|
39413
|
+
return null;
|
|
39414
|
+
}
|
|
39415
|
+
const year = Number(match[1]);
|
|
39416
|
+
const month = Number(match[2]);
|
|
39417
|
+
const day = Number(match[3]);
|
|
39418
|
+
const hour = Number(match[4] ?? "0");
|
|
39419
|
+
const minute = Number(match[5] ?? "0");
|
|
39420
|
+
const second = Number(match[6] ?? "0");
|
|
39421
|
+
const millisecond = Number((match[7] ?? "0").padEnd(3, "0").slice(0, 3));
|
|
39422
|
+
const zone = match[8];
|
|
39423
|
+
if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59) {
|
|
39424
|
+
return null;
|
|
39425
|
+
}
|
|
39426
|
+
const daysInMonth = new Date(Date.UTC(2e3 + year % 400, month, 0)).getUTCDate();
|
|
39427
|
+
if (day > daysInMonth) {
|
|
39428
|
+
return null;
|
|
39429
|
+
}
|
|
39430
|
+
if (zone == null) {
|
|
39431
|
+
const local = new Date(2e3, 0, 1);
|
|
39432
|
+
local.setFullYear(year, month - 1, day);
|
|
39433
|
+
local.setHours(hour, minute, second, millisecond);
|
|
39434
|
+
return local.getTime();
|
|
39435
|
+
}
|
|
39436
|
+
let offsetMinutes = 0;
|
|
39437
|
+
if (zone != "Z" && zone != "z") {
|
|
39438
|
+
const digits = zone.slice(1).replace(":", "");
|
|
39439
|
+
const offsetHours = Number(digits.slice(0, 2));
|
|
39440
|
+
const offsetRest = Number(digits.slice(2, 4) || "0");
|
|
39441
|
+
if (offsetHours > 23 || offsetRest > 59) {
|
|
39442
|
+
return null;
|
|
39443
|
+
}
|
|
39444
|
+
offsetMinutes = offsetHours * 60 + offsetRest;
|
|
39445
|
+
if (zone[0] == "-") {
|
|
39446
|
+
offsetMinutes = -offsetMinutes;
|
|
39447
|
+
}
|
|
39448
|
+
}
|
|
39449
|
+
const utc = new Date(Date.UTC(2e3, 0, 1));
|
|
39450
|
+
utc.setUTCFullYear(year, month - 1, day);
|
|
39451
|
+
utc.setUTCHours(hour, minute, second, millisecond);
|
|
39452
|
+
return utc.getTime() - offsetMinutes * 6e4;
|
|
39453
|
+
}
|
|
39454
|
+
|
|
39455
|
+
// src/modules/docs/block-diff.ts
|
|
39456
|
+
var MAX_DIFF_CELLS = 16e6;
|
|
39457
|
+
function compareMatched(before, after, changes) {
|
|
39458
|
+
if (before.hash != after.hash) {
|
|
39459
|
+
changes.push({ kind: "changed", before, after });
|
|
39460
|
+
}
|
|
39461
|
+
}
|
|
39462
|
+
function diffMiddle(before, offset, after, changes) {
|
|
39463
|
+
const beforeCount = before.length;
|
|
39464
|
+
const afterCount = after.length;
|
|
39465
|
+
const width = afterCount + 1;
|
|
39466
|
+
if (beforeCount == 0 || afterCount == 0 || (beforeCount + 1) * width > MAX_DIFF_CELLS) {
|
|
39467
|
+
for (let position = 0; position < before.length; position += 1) {
|
|
39468
|
+
changes.push({ kind: "removed", before: before[position], index: offset + position });
|
|
39469
|
+
}
|
|
39470
|
+
for (const block of after) {
|
|
39471
|
+
changes.push({ kind: "added", after: block });
|
|
39472
|
+
}
|
|
39473
|
+
return;
|
|
39474
|
+
}
|
|
39475
|
+
const lengths = new Uint16Array((beforeCount + 1) * width);
|
|
39476
|
+
for (let row2 = beforeCount - 1; row2 >= 0; row2 -= 1) {
|
|
39477
|
+
for (let column2 = afterCount - 1; column2 >= 0; column2 -= 1) {
|
|
39478
|
+
if (before[row2].key == after[column2].key) {
|
|
39479
|
+
lengths[row2 * width + column2] = lengths[(row2 + 1) * width + column2 + 1] + 1;
|
|
39480
|
+
} else {
|
|
39481
|
+
lengths[row2 * width + column2] = Math.max(lengths[(row2 + 1) * width + column2], lengths[row2 * width + column2 + 1]);
|
|
39482
|
+
}
|
|
39483
|
+
}
|
|
39484
|
+
}
|
|
39485
|
+
let row = 0;
|
|
39486
|
+
let column = 0;
|
|
39487
|
+
while (row < beforeCount && column < afterCount) {
|
|
39488
|
+
if (before[row].key == after[column].key) {
|
|
39489
|
+
compareMatched(before[row], after[column], changes);
|
|
39490
|
+
row += 1;
|
|
39491
|
+
column += 1;
|
|
39492
|
+
} else if (lengths[(row + 1) * width + column] >= lengths[row * width + column + 1]) {
|
|
39493
|
+
changes.push({ kind: "removed", before: before[row], index: offset + row });
|
|
39494
|
+
row += 1;
|
|
39495
|
+
} else {
|
|
39496
|
+
changes.push({ kind: "added", after: after[column] });
|
|
39497
|
+
column += 1;
|
|
39498
|
+
}
|
|
39499
|
+
}
|
|
39500
|
+
while (row < beforeCount) {
|
|
39501
|
+
changes.push({ kind: "removed", before: before[row], index: offset + row });
|
|
39502
|
+
row += 1;
|
|
39503
|
+
}
|
|
39504
|
+
while (column < afterCount) {
|
|
39505
|
+
changes.push({ kind: "added", after: after[column] });
|
|
39506
|
+
column += 1;
|
|
39507
|
+
}
|
|
39508
|
+
}
|
|
39509
|
+
function diffBlocks(before, after) {
|
|
39510
|
+
const changes = [];
|
|
39511
|
+
let prefix = 0;
|
|
39512
|
+
while (prefix < before.length && prefix < after.length && before[prefix].key == after[prefix].key) {
|
|
39513
|
+
compareMatched(before[prefix], after[prefix], changes);
|
|
39514
|
+
prefix += 1;
|
|
39515
|
+
}
|
|
39516
|
+
let beforeEnd = before.length;
|
|
39517
|
+
let afterEnd = after.length;
|
|
39518
|
+
while (beforeEnd > prefix && afterEnd > prefix && before[beforeEnd - 1].key == after[afterEnd - 1].key) {
|
|
39519
|
+
beforeEnd -= 1;
|
|
39520
|
+
afterEnd -= 1;
|
|
39521
|
+
}
|
|
39522
|
+
diffMiddle(before.slice(prefix, beforeEnd), prefix, after.slice(prefix, afterEnd), changes);
|
|
39523
|
+
for (let offset = 0; beforeEnd + offset < before.length; offset += 1) {
|
|
39524
|
+
compareMatched(before[beforeEnd + offset], after[afterEnd + offset], changes);
|
|
39525
|
+
}
|
|
39526
|
+
return changes;
|
|
39527
|
+
}
|
|
39528
|
+
|
|
38953
39529
|
// src/modules/tools/list.ts
|
|
38954
39530
|
var COLLAPSED_NOTE = "Folders are collapsed; call list with prefix to open one.";
|
|
38955
39531
|
function directoryOf(path) {
|
|
@@ -39044,7 +39620,7 @@ function normalizePrefix(prefix) {
|
|
|
39044
39620
|
}
|
|
39045
39621
|
function runList(library, args, maxBytes) {
|
|
39046
39622
|
if (args.prefix != null && typeof args.prefix != "string") {
|
|
39047
|
-
return errorResult('prefix must be a string: the start of a path such as "
|
|
39623
|
+
return errorResult('prefix must be a string: the start of a path such as "guide/api".');
|
|
39048
39624
|
}
|
|
39049
39625
|
let prefix = "";
|
|
39050
39626
|
if (typeof args.prefix == "string") {
|
|
@@ -39097,6 +39673,193 @@ function runList(library, args, maxBytes) {
|
|
|
39097
39673
|
].join("\n"));
|
|
39098
39674
|
}
|
|
39099
39675
|
|
|
39676
|
+
// src/modules/tools/since.ts
|
|
39677
|
+
var SINCE_ECHO_MAX_CHARS = 40;
|
|
39678
|
+
function readSince(value) {
|
|
39679
|
+
if (value == null) {
|
|
39680
|
+
return { ok: true, sinceMs: null };
|
|
39681
|
+
}
|
|
39682
|
+
if (typeof value == "string") {
|
|
39683
|
+
const sinceMs = parseIsoTime(value);
|
|
39684
|
+
if (sinceMs != null) {
|
|
39685
|
+
return { ok: true, sinceMs };
|
|
39686
|
+
}
|
|
39687
|
+
}
|
|
39688
|
+
const echo = JSON.stringify(truncateChars(String(value), SINCE_ECHO_MAX_CHARS));
|
|
39689
|
+
return {
|
|
39690
|
+
ok: false,
|
|
39691
|
+
result: errorResult(`since ${echo} is not an ISO 8601 time. Use 2026-11-08T09:00:00+09:00, 2026-11-08T09:00:00Z, 2026-11-08T09:00 (local time) or 2026-11-08 (local midnight).`)
|
|
39692
|
+
};
|
|
39693
|
+
}
|
|
39694
|
+
|
|
39695
|
+
// src/modules/tools/changes.ts
|
|
39696
|
+
var SESSION_START_NOTE = "(session start)";
|
|
39697
|
+
var ACTION_WIDTH = 7;
|
|
39698
|
+
function blockLine(change, document, before) {
|
|
39699
|
+
if (change.kind == "removed") {
|
|
39700
|
+
return " " + "removed".padEnd(ACTION_WIDTH) + " " + labelOfRecord(before, change.index);
|
|
39701
|
+
}
|
|
39702
|
+
const block = change.after;
|
|
39703
|
+
const current = document;
|
|
39704
|
+
const size = formatSize(current.bytesOf(block.start, block.end));
|
|
39705
|
+
return " " + change.kind.padEnd(ACTION_WIDTH) + " " + current.path + ":" + block.start + "-" + block.end + " \xB7 " + labelOfBlock(current, block) + " \xB7 " + size;
|
|
39706
|
+
}
|
|
39707
|
+
function reportFile(library, document, path, baseline) {
|
|
39708
|
+
const history = library.history;
|
|
39709
|
+
const hasBaseline = baseline != null && baseline.deleted == false;
|
|
39710
|
+
if (document == null) {
|
|
39711
|
+
if (baseline == null || hasBaseline == false) {
|
|
39712
|
+
return null;
|
|
39713
|
+
}
|
|
39714
|
+
const text2 = path + " \xB7 removed";
|
|
39715
|
+
const count = history.blocksAt(path, baseline.observedMs)?.length ?? 0;
|
|
39716
|
+
return { path, blockCount: count, text: text2, foldedText: text2 };
|
|
39717
|
+
}
|
|
39718
|
+
const current = history.currentBlocks(path);
|
|
39719
|
+
if (current == null) {
|
|
39720
|
+
return null;
|
|
39721
|
+
}
|
|
39722
|
+
if (baseline == null || hasBaseline == false) {
|
|
39723
|
+
const text2 = path + " \xB7 added \xB7 " + formatSize(document.totalBytes);
|
|
39724
|
+
return { path, blockCount: current.blocks.length, text: text2, foldedText: text2 };
|
|
39725
|
+
}
|
|
39726
|
+
if (baseline.hash == current.hash) {
|
|
39727
|
+
return null;
|
|
39728
|
+
}
|
|
39729
|
+
const before = history.blocksAt(path, baseline.observedMs);
|
|
39730
|
+
if (before == null) {
|
|
39731
|
+
return null;
|
|
39732
|
+
}
|
|
39733
|
+
const changes = diffBlocks(before, current.blocks);
|
|
39734
|
+
if (changes.length == 0) {
|
|
39735
|
+
return null;
|
|
39736
|
+
}
|
|
39737
|
+
const removedCount = changes.filter((change) => change.kind == "removed").length;
|
|
39738
|
+
return {
|
|
39739
|
+
path,
|
|
39740
|
+
blockCount: changes.length,
|
|
39741
|
+
text: [path, ...changes.map((change) => blockLine(change, document, before))].join("\n"),
|
|
39742
|
+
foldedText: path + " \xB7 changed \xB7 " + changes.length + " of " + (current.blocks.length + removedCount) + " blocks; outline it"
|
|
39743
|
+
};
|
|
39744
|
+
}
|
|
39745
|
+
function runChanges(library, args, maxBytes) {
|
|
39746
|
+
const sinceArgument = readSince(args.since);
|
|
39747
|
+
if (sinceArgument.ok == false) {
|
|
39748
|
+
return sinceArgument.result;
|
|
39749
|
+
}
|
|
39750
|
+
let prefix = "";
|
|
39751
|
+
if (typeof args.prefix == "string") {
|
|
39752
|
+
prefix = normalizePrefix(args.prefix);
|
|
39753
|
+
}
|
|
39754
|
+
let offset = 0;
|
|
39755
|
+
if (typeof args.offset == "number") {
|
|
39756
|
+
offset = args.offset;
|
|
39757
|
+
}
|
|
39758
|
+
const documents = library.refresh();
|
|
39759
|
+
const failedBefore = unavailableResult(library);
|
|
39760
|
+
if (failedBefore != null) {
|
|
39761
|
+
return failedBefore;
|
|
39762
|
+
}
|
|
39763
|
+
let result;
|
|
39764
|
+
try {
|
|
39765
|
+
result = buildReport(library, documents, { requestedSinceMs: sinceArgument.sinceMs, prefix, offset, maxBytes });
|
|
39766
|
+
} catch (error62) {
|
|
39767
|
+
if (error62 instanceof HistoryBusyError) {
|
|
39768
|
+
return errorResult(HISTORY_BUSY_MESSAGE);
|
|
39769
|
+
}
|
|
39770
|
+
throw error62;
|
|
39771
|
+
}
|
|
39772
|
+
return unavailableResult(library) ?? result;
|
|
39773
|
+
}
|
|
39774
|
+
function unavailableResult(library) {
|
|
39775
|
+
const status = library.history.status();
|
|
39776
|
+
if (status == null) {
|
|
39777
|
+
return null;
|
|
39778
|
+
}
|
|
39779
|
+
return errorResult(historyToolMessage(library.history.file ?? "history.db", status));
|
|
39780
|
+
}
|
|
39781
|
+
function buildReport(library, documents, request) {
|
|
39782
|
+
const { prefix, offset, maxBytes } = request;
|
|
39783
|
+
const history = library.history;
|
|
39784
|
+
const asOfMs = history.observedMs ?? Date.now();
|
|
39785
|
+
const startedMs = history.startedMs ?? asOfMs;
|
|
39786
|
+
const firstMs = history.firstObservedMs() ?? startedMs;
|
|
39787
|
+
let sinceMs = startedMs;
|
|
39788
|
+
if (request.requestedSinceMs != null) {
|
|
39789
|
+
sinceMs = request.requestedSinceMs;
|
|
39790
|
+
}
|
|
39791
|
+
const notes = [];
|
|
39792
|
+
if (sinceMs < firstMs) {
|
|
39793
|
+
sinceMs = firstMs;
|
|
39794
|
+
notes.push(`History starts ${formatLocalIso(firstMs)}; earlier edits were not recorded.`);
|
|
39795
|
+
}
|
|
39796
|
+
let sinceLabel = formatLocalIso(sinceMs);
|
|
39797
|
+
if (sinceMs == startedMs) {
|
|
39798
|
+
sinceLabel += " " + SESSION_START_NOTE;
|
|
39799
|
+
}
|
|
39800
|
+
const asOfLine = "As of " + formatLocalIso(asOfMs) + ". Pass this as `since` to see only later changes.";
|
|
39801
|
+
const documentByPath = /* @__PURE__ */ new Map();
|
|
39802
|
+
for (const document of documents) {
|
|
39803
|
+
documentByPath.set(document.path, document);
|
|
39804
|
+
}
|
|
39805
|
+
const baselines = history.baselines(sinceMs);
|
|
39806
|
+
const paths = new Set(documentByPath.keys());
|
|
39807
|
+
for (const path of baselines.keys()) {
|
|
39808
|
+
paths.add(path);
|
|
39809
|
+
}
|
|
39810
|
+
const reports = [];
|
|
39811
|
+
for (const path of [...paths].filter((candidate) => candidate.startsWith(prefix) == true).sort(compareText)) {
|
|
39812
|
+
const report = reportFile(library, documentByPath.get(path), path, baselines.get(path));
|
|
39813
|
+
if (report != null) {
|
|
39814
|
+
reports.push(report);
|
|
39815
|
+
}
|
|
39816
|
+
}
|
|
39817
|
+
if (reports.length == 0) {
|
|
39818
|
+
let scope = "";
|
|
39819
|
+
if (prefix != "") {
|
|
39820
|
+
scope = ` under "${prefix}"`;
|
|
39821
|
+
}
|
|
39822
|
+
return textResult([`No changes since ${sinceLabel}${scope}.`, asOfLine, ...notes].join("\n"));
|
|
39823
|
+
}
|
|
39824
|
+
const totalFiles = reports.length;
|
|
39825
|
+
const totalBlocks = reports.reduce((sum, report) => sum + report.blockCount, 0);
|
|
39826
|
+
const summary = `Changes since ${sinceLabel} \xB7 ${pluralize(totalFiles, "file")} \xB7 ${pluralize(totalBlocks, "block")}`;
|
|
39827
|
+
if (offset >= totalFiles) {
|
|
39828
|
+
return textResult([summary, asOfLine, ...notes, `Offset ${offset} is past the last file; ${pluralize(totalFiles, "file")} changed.`].join("\n"));
|
|
39829
|
+
}
|
|
39830
|
+
const fixedBytes = byteLength([`${summary} \xB7 files ${totalFiles}-${totalFiles}`, asOfLine, ...notes, ""].join("\n")) + 1;
|
|
39831
|
+
const moreReserve = byteLength(`More: offset=${totalFiles}`) + 1;
|
|
39832
|
+
const chunks = [];
|
|
39833
|
+
let used = fixedBytes;
|
|
39834
|
+
for (let position = offset; position < totalFiles; position += 1) {
|
|
39835
|
+
const report = reports[position];
|
|
39836
|
+
let reserve = 0;
|
|
39837
|
+
if (position < totalFiles - 1) {
|
|
39838
|
+
reserve = moreReserve;
|
|
39839
|
+
}
|
|
39840
|
+
let text2 = report.text;
|
|
39841
|
+
if (chunks.length == 0 && used + byteLength(text2) + 1 + moreReserve > maxBytes) {
|
|
39842
|
+
text2 = report.foldedText;
|
|
39843
|
+
}
|
|
39844
|
+
const chunkBytes = byteLength(text2) + 1;
|
|
39845
|
+
if (chunks.length > 0 && used + chunkBytes + reserve > maxBytes) {
|
|
39846
|
+
break;
|
|
39847
|
+
}
|
|
39848
|
+
chunks.push(text2);
|
|
39849
|
+
used += chunkBytes;
|
|
39850
|
+
}
|
|
39851
|
+
const shown = chunks.length;
|
|
39852
|
+
let header = summary;
|
|
39853
|
+
if (offset > 0 || offset + shown < totalFiles) {
|
|
39854
|
+
header += ` \xB7 files ${offset + 1}-${offset + shown}`;
|
|
39855
|
+
}
|
|
39856
|
+
const lines = [header, asOfLine, ...notes, "", ...chunks];
|
|
39857
|
+
if (offset + shown < totalFiles) {
|
|
39858
|
+
lines.push(`More: offset=${offset + shown}`);
|
|
39859
|
+
}
|
|
39860
|
+
return textResult(lines.join("\n"));
|
|
39861
|
+
}
|
|
39862
|
+
|
|
39100
39863
|
// src/modules/docs/reference.ts
|
|
39101
39864
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
39102
39865
|
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
@@ -39508,6 +40271,8 @@ var ELLIPSIS2 = "\u2026";
|
|
|
39508
40271
|
var ELLIPSIS_BYTES = Buffer.byteLength(ELLIPSIS2);
|
|
39509
40272
|
var QUERY_ECHO_MAX_CHARS = 80;
|
|
39510
40273
|
var WORD_CHARACTERS = /[a-z0-9_]/;
|
|
40274
|
+
var BM25_K1 = 1.2;
|
|
40275
|
+
var BM25_B = 0.75;
|
|
39511
40276
|
function normalize(text2) {
|
|
39512
40277
|
return text2.normalize("NFC").toLowerCase();
|
|
39513
40278
|
}
|
|
@@ -39576,7 +40341,7 @@ function chainIndices(document, index) {
|
|
|
39576
40341
|
}
|
|
39577
40342
|
return chain;
|
|
39578
40343
|
}
|
|
39579
|
-
function
|
|
40344
|
+
function isBlank2(document, start, end) {
|
|
39580
40345
|
for (let line = start; line <= end; line += 1) {
|
|
39581
40346
|
if (document.lines[line - 1].trim() != "") {
|
|
39582
40347
|
return false;
|
|
@@ -39605,14 +40370,18 @@ function matchBlock(document, start, end, chainText, label, bodyStart, terms) {
|
|
|
39605
40370
|
}
|
|
39606
40371
|
hits += counts[termIndex];
|
|
39607
40372
|
}
|
|
39608
|
-
return { document, start, end, label, bytes: document.bytesOf(start, end), hits, titleHits, bodyStart };
|
|
40373
|
+
return { document, start, end, label, bytes: document.bytesOf(start, end), termCounts: counts, hits, titleHits, score: 0, bodyStart };
|
|
39609
40374
|
}
|
|
39610
40375
|
function collectMatches(documents, terms) {
|
|
39611
40376
|
const matches = [];
|
|
40377
|
+
let scannedBytes = 0;
|
|
40378
|
+
let scannedBlocks = 0;
|
|
39612
40379
|
for (const document of documents) {
|
|
39613
40380
|
const headings = document.headings;
|
|
39614
40381
|
const introEnd = headings.length == 0 ? document.lines.length : headings[0].line - 1;
|
|
39615
|
-
if (introEnd >= 1 &&
|
|
40382
|
+
if (introEnd >= 1 && isBlank2(document, 1, introEnd) == false) {
|
|
40383
|
+
scannedBytes += document.bytesOf(1, introEnd);
|
|
40384
|
+
scannedBlocks += 1;
|
|
39616
40385
|
const introMatch = matchBlock(document, 1, introEnd, "", INTRO_TITLE, 1, terms);
|
|
39617
40386
|
if (introMatch != null) {
|
|
39618
40387
|
matches.push(introMatch);
|
|
@@ -39620,6 +40389,8 @@ function collectMatches(documents, terms) {
|
|
|
39620
40389
|
}
|
|
39621
40390
|
for (let index = 0; index < headings.length; index += 1) {
|
|
39622
40391
|
const heading = headings[index];
|
|
40392
|
+
scannedBytes += document.bytesOf(heading.line, heading.blockEnd);
|
|
40393
|
+
scannedBlocks += 1;
|
|
39623
40394
|
const chainText = chainIndices(document, index).map((position) => document.headings[position].title).join("\n");
|
|
39624
40395
|
const label = headingChain(document, index);
|
|
39625
40396
|
const match = matchBlock(document, heading.line, heading.blockEnd, chainText, label, heading.line + 1, terms);
|
|
@@ -39628,14 +40399,22 @@ function collectMatches(documents, terms) {
|
|
|
39628
40399
|
}
|
|
39629
40400
|
}
|
|
39630
40401
|
}
|
|
39631
|
-
return matches;
|
|
40402
|
+
return { matches, averageBytes: scannedBlocks == 0 ? 1 : scannedBytes / scannedBlocks };
|
|
40403
|
+
}
|
|
40404
|
+
function scoreMatch(match, averageBytes) {
|
|
40405
|
+
const lengthFactor = 1 - BM25_B + BM25_B * (match.bytes / averageBytes);
|
|
40406
|
+
let score = 0;
|
|
40407
|
+
for (const termCount of match.termCounts) {
|
|
40408
|
+
score += termCount * (BM25_K1 + 1) / (termCount + BM25_K1 * lengthFactor);
|
|
40409
|
+
}
|
|
40410
|
+
return score;
|
|
39632
40411
|
}
|
|
39633
40412
|
function compareMatches(left, right) {
|
|
39634
40413
|
if (left.titleHits != right.titleHits) {
|
|
39635
40414
|
return right.titleHits - left.titleHits;
|
|
39636
40415
|
}
|
|
39637
|
-
if (left.
|
|
39638
|
-
return right.
|
|
40416
|
+
if (left.score != right.score) {
|
|
40417
|
+
return right.score - left.score;
|
|
39639
40418
|
}
|
|
39640
40419
|
const byPath = compareText(left.document.path, right.document.path);
|
|
39641
40420
|
if (byPath != 0) {
|
|
@@ -39725,7 +40504,10 @@ function runSearch(library, args, maxBytes) {
|
|
|
39725
40504
|
return errorResult("Query is empty. Give one or more words; wrap a phrase in double quotes.");
|
|
39726
40505
|
}
|
|
39727
40506
|
const documents = library.refresh().filter((document) => document.path.startsWith(prefix));
|
|
39728
|
-
const matches = collectMatches(documents, terms);
|
|
40507
|
+
const { matches, averageBytes } = collectMatches(documents, terms);
|
|
40508
|
+
for (const match of matches) {
|
|
40509
|
+
match.score = scoreMatch(match, averageBytes);
|
|
40510
|
+
}
|
|
39729
40511
|
matches.sort(compareMatches);
|
|
39730
40512
|
const fileCount = new Set(matches.map((match) => match.document.path)).size;
|
|
39731
40513
|
const echo = JSON.stringify(truncateChars(query.trim(), QUERY_ECHO_MAX_CHARS));
|
|
@@ -39789,7 +40571,8 @@ var INSTRUCTIONS_USAGE = [
|
|
|
39789
40571
|
"Start with `list` (files with size and title) or `search` (blocks matching all words), `outline` a file to see headings with line ranges and sizes, then `read` only the part you need.",
|
|
39790
40572
|
"A ref is `path`, `path#heading` (heading text or GitHub anchor) or `path:start-end`.",
|
|
39791
40573
|
"Every call re-reads files changed on disk, so line numbers are always current.",
|
|
39792
|
-
"When you hand a ref to another agent, prefer `path#heading`: line ranges move when the file is edited."
|
|
40574
|
+
"When you hand a ref to another agent, prefer `path#heading`: line ranges move when the file is edited.",
|
|
40575
|
+
"To see what others edited while you worked, call `changes` (blocks added, changed or removed since the session start) and pass its `As of` time back as `since` to get only later edits."
|
|
39793
40576
|
].join(" ");
|
|
39794
40577
|
function buildRootInstructions(rootName) {
|
|
39795
40578
|
return `librian reads the markdown docs under \`${rootName}\` without loading whole files. ${INSTRUCTIONS_USAGE}`;
|
|
@@ -39801,9 +40584,9 @@ function describeTools(folder) {
|
|
|
39801
40584
|
return [
|
|
39802
40585
|
{
|
|
39803
40586
|
name: nameOf("list"),
|
|
39804
|
-
description: `${tag}List the markdown files under ${where} with size and document title, grouped by folder. Use prefix to open one folder: \`
|
|
40587
|
+
description: `${tag}List the markdown files under ${where} with size and document title, grouped by folder. Use prefix to open one folder: \`guide/api\` matches \`guide/api/\u2026\` and \`guide/api-notes.md\`, \`guide/api/\` matches the folder only.`,
|
|
39805
40588
|
inputSchema: {
|
|
39806
|
-
prefix: external_exports.string().optional().describe("Root-relative path prefix, e.g. `
|
|
40589
|
+
prefix: external_exports.string().optional().describe("Root-relative path prefix, e.g. `guide/api/`. Omit to list everything.")
|
|
39807
40590
|
},
|
|
39808
40591
|
runner: runList
|
|
39809
40592
|
},
|
|
@@ -39833,6 +40616,16 @@ function describeTools(folder) {
|
|
|
39833
40616
|
offset: external_exports.number().int().min(0).optional().describe("Results to skip, from the `More: offset=N` line of the previous page.")
|
|
39834
40617
|
},
|
|
39835
40618
|
runner: runSearch
|
|
40619
|
+
},
|
|
40620
|
+
{
|
|
40621
|
+
name: nameOf("changes"),
|
|
40622
|
+
description: `${tag}List the blocks (a heading and the text up to the next heading) that were added, changed or removed since this session started, or since a time you give, so you can read only what moved. Added and removed files appear as one line. Each result gives \`path:start-end\` to pass to read. The result ends with an \`As of\` time: pass it as since next time to get only later edits (when paging with offset, keep the same since). The history of block hashes is kept for 30 days across restarts, so an earlier since works too.`,
|
|
40623
|
+
inputSchema: {
|
|
40624
|
+
since: external_exports.string().optional().describe("ISO 8601 time, e.g. `2026-11-08T09:00:00+09:00`; local time when it has no zone. Omit for the session start (when this server started)."),
|
|
40625
|
+
prefix: external_exports.string().optional().describe("Root-relative path prefix to look under."),
|
|
40626
|
+
offset: external_exports.number().int().min(0).optional().describe("Files to skip, from the `More: offset=N` line of the previous page.")
|
|
40627
|
+
},
|
|
40628
|
+
runner: runChanges
|
|
39836
40629
|
}
|
|
39837
40630
|
];
|
|
39838
40631
|
}
|
|
@@ -39868,7 +40661,7 @@ function createLibrianServer(options) {
|
|
|
39868
40661
|
version: options.version
|
|
39869
40662
|
});
|
|
39870
40663
|
}
|
|
39871
|
-
async function serveOverStdio(server) {
|
|
40664
|
+
async function serveOverStdio(server, options = {}) {
|
|
39872
40665
|
let stopping = false;
|
|
39873
40666
|
let stdoutBroken = false;
|
|
39874
40667
|
function shutdown(exitCode) {
|
|
@@ -39876,6 +40669,7 @@ async function serveOverStdio(server) {
|
|
|
39876
40669
|
return;
|
|
39877
40670
|
}
|
|
39878
40671
|
stopping = true;
|
|
40672
|
+
options.onShutdown?.();
|
|
39879
40673
|
const deadline = setTimeout(() => {
|
|
39880
40674
|
process.exit(exitCode);
|
|
39881
40675
|
}, SHUTDOWN_DEADLINE_MS);
|
|
@@ -39901,11 +40695,18 @@ async function serveOverStdio(server) {
|
|
|
39901
40695
|
stdoutBroken = true;
|
|
39902
40696
|
shutdown(0);
|
|
39903
40697
|
});
|
|
40698
|
+
process.on("SIGINT", () => {
|
|
40699
|
+
shutdown(0);
|
|
40700
|
+
});
|
|
40701
|
+
process.on("SIGTERM", () => {
|
|
40702
|
+
shutdown(0);
|
|
40703
|
+
});
|
|
39904
40704
|
process.on("uncaughtException", (error62) => {
|
|
39905
40705
|
writeStderrLine(`librian: uncaught exception: ${error62.stack ?? error62.message}`);
|
|
39906
40706
|
shutdown(1);
|
|
39907
40707
|
});
|
|
39908
40708
|
await server.connect(new StdioServerTransport());
|
|
40709
|
+
return { shutdown };
|
|
39909
40710
|
}
|
|
39910
40711
|
|
|
39911
40712
|
// src/modules/mcp/catalog-server.ts
|
|
@@ -39916,7 +40717,7 @@ function isFolder2(path) {
|
|
|
39916
40717
|
return false;
|
|
39917
40718
|
}
|
|
39918
40719
|
}
|
|
39919
|
-
function openCatalogFolders(catalog) {
|
|
40720
|
+
function openCatalogFolders(catalog, historyFor) {
|
|
39920
40721
|
const folders = [];
|
|
39921
40722
|
const unavailable = [];
|
|
39922
40723
|
for (const entry of catalog.entries) {
|
|
@@ -39924,7 +40725,7 @@ function openCatalogFolders(catalog) {
|
|
|
39924
40725
|
unavailable.push({ name: entry.name, root: entry.root });
|
|
39925
40726
|
continue;
|
|
39926
40727
|
}
|
|
39927
|
-
const library = openLibrary({ root: entry.root, exclude: entry.exclude });
|
|
40728
|
+
const library = openLibrary({ root: entry.root, exclude: entry.exclude, history: historyFor?.(entry.root) });
|
|
39928
40729
|
folders.push({ name: entry.name, root: library.root, library, maxBytes: entry.maxBytes, fileCount: library.refresh().length });
|
|
39929
40730
|
}
|
|
39930
40731
|
return { folders, unavailable };
|
|
@@ -39976,14 +40777,62 @@ function catalogReadyLine(version2, home, catalog, opened) {
|
|
|
39976
40777
|
return parts.join(" \xB7 ");
|
|
39977
40778
|
}
|
|
39978
40779
|
|
|
40780
|
+
// src/modules/mcp/history-observer.ts
|
|
40781
|
+
function isParentProcessAlive() {
|
|
40782
|
+
try {
|
|
40783
|
+
process.kill(process.ppid, 0);
|
|
40784
|
+
return true;
|
|
40785
|
+
} catch (error62) {
|
|
40786
|
+
return error62.code != "ESRCH";
|
|
40787
|
+
}
|
|
40788
|
+
}
|
|
40789
|
+
function startHistoryObserver(options) {
|
|
40790
|
+
let isRunning = false;
|
|
40791
|
+
let isStopped = false;
|
|
40792
|
+
const isParentAlive = options.isParentAlive ?? isParentProcessAlive;
|
|
40793
|
+
const timer = setInterval(() => {
|
|
40794
|
+
if (isRunning == true || isStopped == true) {
|
|
40795
|
+
return;
|
|
40796
|
+
}
|
|
40797
|
+
if (isParentAlive() == false) {
|
|
40798
|
+
isStopped = true;
|
|
40799
|
+
clearInterval(timer);
|
|
40800
|
+
options.onParentGone?.();
|
|
40801
|
+
return;
|
|
40802
|
+
}
|
|
40803
|
+
isRunning = true;
|
|
40804
|
+
try {
|
|
40805
|
+
for (const library of options.libraries) {
|
|
40806
|
+
if (isStopped == true) {
|
|
40807
|
+
break;
|
|
40808
|
+
}
|
|
40809
|
+
try {
|
|
40810
|
+
library.refresh();
|
|
40811
|
+
} catch (error62) {
|
|
40812
|
+
options.writeError(`librian: history refresh of ${library.root} failed: ${error62 instanceof Error ? error62.message : String(error62)}`);
|
|
40813
|
+
}
|
|
40814
|
+
}
|
|
40815
|
+
} finally {
|
|
40816
|
+
isRunning = false;
|
|
40817
|
+
}
|
|
40818
|
+
}, options.intervalMs);
|
|
40819
|
+
timer.unref();
|
|
40820
|
+
return {
|
|
40821
|
+
stop() {
|
|
40822
|
+
isStopped = true;
|
|
40823
|
+
clearInterval(timer);
|
|
40824
|
+
}
|
|
40825
|
+
};
|
|
40826
|
+
}
|
|
40827
|
+
|
|
39979
40828
|
// src/modules/setup/runtime.ts
|
|
39980
|
-
import { existsSync as
|
|
39981
|
-
import { dirname, join as
|
|
40829
|
+
import { existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync as realpathSync3, rmSync as rmSync2 } from "node:fs";
|
|
40830
|
+
import { dirname as dirname2, join as join6, resolve as resolve4 } from "node:path";
|
|
39982
40831
|
|
|
39983
40832
|
// src/modules/setup/commands.ts
|
|
39984
40833
|
import { spawn } from "node:child_process";
|
|
39985
40834
|
import { accessSync, constants, statSync as statSync4 } from "node:fs";
|
|
39986
|
-
import { delimiter, join as
|
|
40835
|
+
import { delimiter, join as join5 } from "node:path";
|
|
39987
40836
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
39988
40837
|
var WINDOWS_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
|
|
39989
40838
|
function quotePart(part) {
|
|
@@ -40012,7 +40861,7 @@ function findOnPath(name, env = process.env, platform = process.platform) {
|
|
|
40012
40861
|
}
|
|
40013
40862
|
for (const directory of directories) {
|
|
40014
40863
|
for (const candidate of candidates) {
|
|
40015
|
-
const path =
|
|
40864
|
+
const path = join5(directory, candidate);
|
|
40016
40865
|
try {
|
|
40017
40866
|
if (statSync4(path).isFile() == false) {
|
|
40018
40867
|
continue;
|
|
@@ -40121,17 +40970,17 @@ var PACKAGE_NAME = "librian";
|
|
|
40121
40970
|
var INSTALL_TIMEOUT_MS = 18e4;
|
|
40122
40971
|
var VIEW_TIMEOUT_MS = 3e4;
|
|
40123
40972
|
function runtimeDirectory(home) {
|
|
40124
|
-
return
|
|
40973
|
+
return join6(home, "runtime");
|
|
40125
40974
|
}
|
|
40126
40975
|
function packageDirectory(home) {
|
|
40127
|
-
return
|
|
40976
|
+
return join6(runtimeDirectory(home), "node_modules", PACKAGE_NAME);
|
|
40128
40977
|
}
|
|
40129
40978
|
function runtimeCliPath(home) {
|
|
40130
|
-
return
|
|
40979
|
+
return join6(packageDirectory(home), "dist", "cli.js");
|
|
40131
40980
|
}
|
|
40132
40981
|
function readPackageVersion(directory) {
|
|
40133
40982
|
try {
|
|
40134
|
-
const parsed = JSON.parse(readFileSync3(
|
|
40983
|
+
const parsed = JSON.parse(readFileSync3(join6(directory, "package.json"), "utf8"));
|
|
40135
40984
|
return typeof parsed.version == "string" ? parsed.version : null;
|
|
40136
40985
|
} catch {
|
|
40137
40986
|
return null;
|
|
@@ -40139,7 +40988,7 @@ function readPackageVersion(directory) {
|
|
|
40139
40988
|
}
|
|
40140
40989
|
function runtimeStatus(home) {
|
|
40141
40990
|
const cliPath = runtimeCliPath(home);
|
|
40142
|
-
if (
|
|
40991
|
+
if (existsSync3(cliPath) == false) {
|
|
40143
40992
|
return { state: "missing", version: null, cliPath };
|
|
40144
40993
|
}
|
|
40145
40994
|
const directory = packageDirectory(home);
|
|
@@ -40161,13 +41010,13 @@ function compareVersions(left, right) {
|
|
|
40161
41010
|
return 0;
|
|
40162
41011
|
}
|
|
40163
41012
|
function findNpmCli(nodePath) {
|
|
40164
|
-
const nodeDirectory =
|
|
41013
|
+
const nodeDirectory = dirname2(nodePath);
|
|
40165
41014
|
const candidates = [
|
|
40166
|
-
|
|
40167
|
-
|
|
41015
|
+
join6(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
|
|
41016
|
+
join6(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
|
|
40168
41017
|
];
|
|
40169
41018
|
for (const candidate of candidates) {
|
|
40170
|
-
if (
|
|
41019
|
+
if (existsSync3(candidate) == true) {
|
|
40171
41020
|
return resolve4(candidate);
|
|
40172
41021
|
}
|
|
40173
41022
|
}
|
|
@@ -40199,7 +41048,7 @@ async function runNpm(toolchain, npmArguments, timeoutMs) {
|
|
|
40199
41048
|
}
|
|
40200
41049
|
async function installRuntime(home, spec, toolchain) {
|
|
40201
41050
|
const directory = runtimeDirectory(home);
|
|
40202
|
-
|
|
41051
|
+
mkdirSync3(directory, { recursive: true });
|
|
40203
41052
|
const installed = await runNpm(toolchain, ["install", "--prefix", directory, "--no-audit", "--no-fund", spec], INSTALL_TIMEOUT_MS);
|
|
40204
41053
|
if (installed.ok == false) {
|
|
40205
41054
|
return { ok: false, reason: installed.reason };
|
|
@@ -40228,10 +41077,21 @@ function uninstallRuntime(home) {
|
|
|
40228
41077
|
}
|
|
40229
41078
|
rmSync2(directory, { recursive: true, force: true });
|
|
40230
41079
|
}
|
|
41080
|
+
function removeHistoryFiles(home) {
|
|
41081
|
+
const file2 = resolveHistoryFile(home, "");
|
|
41082
|
+
for (const path of [file2, file2 + "-wal", file2 + "-shm"]) {
|
|
41083
|
+
try {
|
|
41084
|
+
rmSync2(path, { force: true });
|
|
41085
|
+
} catch (error62) {
|
|
41086
|
+
return `${path} could not be removed (${error62 instanceof Error ? error62.message : String(error62)}); stop the running AI sessions and delete it by hand`;
|
|
41087
|
+
}
|
|
41088
|
+
}
|
|
41089
|
+
return null;
|
|
41090
|
+
}
|
|
40231
41091
|
|
|
40232
41092
|
// src/modules/setup/services.ts
|
|
40233
41093
|
import { homedir as homedir3 } from "node:os";
|
|
40234
|
-
import { join as
|
|
41094
|
+
import { join as join7 } from "node:path";
|
|
40235
41095
|
|
|
40236
41096
|
// src/modules/setup/probe.ts
|
|
40237
41097
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -40338,7 +41198,7 @@ function defaultServiceDependencies() {
|
|
|
40338
41198
|
return { ...defaultToolchain(), probe: probeServer };
|
|
40339
41199
|
}
|
|
40340
41200
|
function isDefaultHome(home) {
|
|
40341
|
-
return normalizeForCompare(home) == normalizeForCompare(
|
|
41201
|
+
return normalizeForCompare(home) == normalizeForCompare(join7(homedir3(), ".librian"));
|
|
40342
41202
|
}
|
|
40343
41203
|
function expectedRegistration(home, toolchain) {
|
|
40344
41204
|
const environment = {};
|
|
@@ -40589,7 +41449,7 @@ async function askConfirm(question, defaultYes) {
|
|
|
40589
41449
|
}
|
|
40590
41450
|
}
|
|
40591
41451
|
async function uninstall(home, dependencies) {
|
|
40592
|
-
if (await askConfirm("Uninstall the runtime? Docs folders and the catalog are kept.", false) == false) {
|
|
41452
|
+
if (await askConfirm("Uninstall the runtime and the history (history.db)? Docs folders and the catalog are kept.", false) == false) {
|
|
40593
41453
|
return;
|
|
40594
41454
|
}
|
|
40595
41455
|
const registered = readCatalog(home).services;
|
|
@@ -40609,7 +41469,13 @@ async function uninstall(home, dependencies) {
|
|
|
40609
41469
|
}
|
|
40610
41470
|
}
|
|
40611
41471
|
uninstallRuntime(home);
|
|
41472
|
+
const historyProblem = removeHistoryFiles(home);
|
|
41473
|
+
if (historyProblem == null) {
|
|
41474
|
+
writeLine(`${styled("done", "green")} uninstall: the runtime folder and history.db are removed`);
|
|
41475
|
+
return;
|
|
41476
|
+
}
|
|
40612
41477
|
writeLine(`${styled("done", "green")} uninstall: the runtime folder is removed`);
|
|
41478
|
+
writeLine(styled(historyProblem, "yellow"));
|
|
40613
41479
|
}
|
|
40614
41480
|
async function openRuntimeScreen(home, dependencies = defaultServiceDependencies()) {
|
|
40615
41481
|
let latest = null;
|
|
@@ -40772,6 +41638,21 @@ async function openServicesScreen(home, dependencies = defaultServiceDependencie
|
|
|
40772
41638
|
|
|
40773
41639
|
// src/cli.ts
|
|
40774
41640
|
var EXIT_USAGE2 = 2;
|
|
41641
|
+
function watchHistory(stores) {
|
|
41642
|
+
for (const store of new Set(stores)) {
|
|
41643
|
+
const status = store.status();
|
|
41644
|
+
if (status != null) {
|
|
41645
|
+
writeStderrLine(describeHistoryTrouble(store.file, status));
|
|
41646
|
+
}
|
|
41647
|
+
store.onTrouble(writeStderrLine);
|
|
41648
|
+
}
|
|
41649
|
+
}
|
|
41650
|
+
function stopHistory(observer, stores) {
|
|
41651
|
+
observer?.stop();
|
|
41652
|
+
for (const store of stores) {
|
|
41653
|
+
store.close();
|
|
41654
|
+
}
|
|
41655
|
+
}
|
|
40775
41656
|
async function main() {
|
|
40776
41657
|
const parsed = parseArguments(process.argv.slice(2));
|
|
40777
41658
|
if (parsed.kind == "help") {
|
|
@@ -40823,13 +41704,26 @@ async function main() {
|
|
|
40823
41704
|
process.exitCode = 1;
|
|
40824
41705
|
return;
|
|
40825
41706
|
}
|
|
40826
|
-
const
|
|
41707
|
+
const stores = [];
|
|
41708
|
+
const opened = openCatalogFolders(catalog, (root2) => {
|
|
41709
|
+
const store2 = sharedHistoryStore(resolveHistoryFile(home, root2));
|
|
41710
|
+
stores.push(store2);
|
|
41711
|
+
return store2;
|
|
41712
|
+
});
|
|
40827
41713
|
for (const folder of opened.unavailable) {
|
|
40828
41714
|
writeStderrLine(`librian: docs folder "${folder.name}" (${folder.root}) does not exist or is not a folder; its tools are not offered.`);
|
|
40829
41715
|
}
|
|
40830
41716
|
const server2 = createCatalogServer(catalog, opened, package_default.version);
|
|
40831
|
-
|
|
41717
|
+
let observer2 = null;
|
|
41718
|
+
const serving2 = await serveOverStdio(server2, { onShutdown: () => stopHistory(observer2, stores) });
|
|
41719
|
+
observer2 = startHistoryObserver({
|
|
41720
|
+
libraries: opened.folders.map((folder) => folder.library),
|
|
41721
|
+
intervalMs: HISTORY_REFRESH_MS,
|
|
41722
|
+
writeError: writeStderrLine,
|
|
41723
|
+
onParentGone: () => serving2.shutdown(0)
|
|
41724
|
+
});
|
|
40832
41725
|
writeStderrLine(catalogReadyLine(package_default.version, home, catalog, opened));
|
|
41726
|
+
watchHistory(stores);
|
|
40833
41727
|
return;
|
|
40834
41728
|
}
|
|
40835
41729
|
const root = resolve5(parsed.root);
|
|
@@ -40844,11 +41738,15 @@ async function main() {
|
|
|
40844
41738
|
process.exitCode = EXIT_USAGE2;
|
|
40845
41739
|
return;
|
|
40846
41740
|
}
|
|
40847
|
-
const
|
|
41741
|
+
const store = sharedHistoryStore(resolveHistoryFile(librianHome(), root));
|
|
41742
|
+
const library = openLibrary({ root, exclude: parsed.exclude, history: store });
|
|
40848
41743
|
const fileCount = library.refresh().length;
|
|
40849
41744
|
const server = createLibrianServer({ library, maxBytes: parsed.maxBytes, version: package_default.version });
|
|
40850
|
-
|
|
41745
|
+
let observer = null;
|
|
41746
|
+
const serving = await serveOverStdio(server, { onShutdown: () => stopHistory(observer, [store]) });
|
|
41747
|
+
observer = startHistoryObserver({ libraries: [library], intervalMs: HISTORY_REFRESH_MS, writeError: writeStderrLine, onParentGone: () => serving.shutdown(0) });
|
|
40851
41748
|
writeStderrLine(`librian ${package_default.version} ready \xB7 root ${library.root} \xB7 ${fileCount} files \xB7 pid ${process.pid}`);
|
|
41749
|
+
watchHistory([store]);
|
|
40852
41750
|
}
|
|
40853
41751
|
main().catch((error62) => {
|
|
40854
41752
|
const message = error62 instanceof Error ? error62.stack ?? error62.message : String(error62);
|