librian 0.1.2 → 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.
Files changed (4) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +29 -4
  3. package/dist/cli.js +1022 -143
  4. 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.1.2",
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 join3, parse, resolve as resolve2 } from "node:path";
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 join2, resolve } from "node:path";
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(join2(absoluteDirectory, entry.name), relativePath + "/");
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 = join2(root, path);
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
- cache.set(path, { modifiedMs, size, document });
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
- return documents;
7800
- }
7801
- return { root, name, refresh };
7802
- }
7803
-
7804
- // src/modules/tools/format.ts
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
- if (fitting < start) {
7890
- const line = document.lines[start - 1];
7891
- const marker = "[line " + start + " cut: " + byteLength(line) + "B]";
7892
- const allowed = budgetBytes - byteLength(marker) - 1;
7893
- if (allowed <= 0) {
7894
- return { text: marker, lastLine: start };
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 { text: takeBytes(line, allowed) + " " + marker, lastLine: start };
8317
+ return documents;
7897
8318
  }
7898
- let lastLine = fitting;
7899
- const halfway = start + Math.floor((fitting - start) / 2);
7900
- for (let line = fitting; line > halfway; line--) {
7901
- if (document.lines[line - 1].trim() == "") {
7902
- lastLine = line;
7903
- break;
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 { text: joinLines2(lastLine), lastLine };
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(join3(root, path)).size;
8412
+ bytes = statSync2(join4(root, path)).size;
7973
8413
  } catch {
7974
8414
  bytes = 0;
7975
8415
  }
@@ -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) {
@@ -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";
@@ -39578,7 +40341,7 @@ function chainIndices(document, index) {
39578
40341
  }
39579
40342
  return chain;
39580
40343
  }
39581
- function isBlank(document, start, end) {
40344
+ function isBlank2(document, start, end) {
39582
40345
  for (let line = start; line <= end; line += 1) {
39583
40346
  if (document.lines[line - 1].trim() != "") {
39584
40347
  return false;
@@ -39616,7 +40379,7 @@ function collectMatches(documents, terms) {
39616
40379
  for (const document of documents) {
39617
40380
  const headings = document.headings;
39618
40381
  const introEnd = headings.length == 0 ? document.lines.length : headings[0].line - 1;
39619
- if (introEnd >= 1 && isBlank(document, 1, introEnd) == false) {
40382
+ if (introEnd >= 1 && isBlank2(document, 1, introEnd) == false) {
39620
40383
  scannedBytes += document.bytesOf(1, introEnd);
39621
40384
  scannedBlocks += 1;
39622
40385
  const introMatch = matchBlock(document, 1, introEnd, "", INTRO_TITLE, 1, terms);
@@ -39808,7 +40571,8 @@ var INSTRUCTIONS_USAGE = [
39808
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.",
39809
40572
  "A ref is `path`, `path#heading` (heading text or GitHub anchor) or `path:start-end`.",
39810
40573
  "Every call re-reads files changed on disk, so line numbers are always current.",
39811
- "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."
39812
40576
  ].join(" ");
39813
40577
  function buildRootInstructions(rootName) {
39814
40578
  return `librian reads the markdown docs under \`${rootName}\` without loading whole files. ${INSTRUCTIONS_USAGE}`;
@@ -39852,6 +40616,16 @@ function describeTools(folder) {
39852
40616
  offset: external_exports.number().int().min(0).optional().describe("Results to skip, from the `More: offset=N` line of the previous page.")
39853
40617
  },
39854
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
39855
40629
  }
39856
40630
  ];
39857
40631
  }
@@ -39887,7 +40661,7 @@ function createLibrianServer(options) {
39887
40661
  version: options.version
39888
40662
  });
39889
40663
  }
39890
- async function serveOverStdio(server) {
40664
+ async function serveOverStdio(server, options = {}) {
39891
40665
  let stopping = false;
39892
40666
  let stdoutBroken = false;
39893
40667
  function shutdown(exitCode) {
@@ -39895,6 +40669,7 @@ async function serveOverStdio(server) {
39895
40669
  return;
39896
40670
  }
39897
40671
  stopping = true;
40672
+ options.onShutdown?.();
39898
40673
  const deadline = setTimeout(() => {
39899
40674
  process.exit(exitCode);
39900
40675
  }, SHUTDOWN_DEADLINE_MS);
@@ -39920,11 +40695,18 @@ async function serveOverStdio(server) {
39920
40695
  stdoutBroken = true;
39921
40696
  shutdown(0);
39922
40697
  });
40698
+ process.on("SIGINT", () => {
40699
+ shutdown(0);
40700
+ });
40701
+ process.on("SIGTERM", () => {
40702
+ shutdown(0);
40703
+ });
39923
40704
  process.on("uncaughtException", (error62) => {
39924
40705
  writeStderrLine(`librian: uncaught exception: ${error62.stack ?? error62.message}`);
39925
40706
  shutdown(1);
39926
40707
  });
39927
40708
  await server.connect(new StdioServerTransport());
40709
+ return { shutdown };
39928
40710
  }
39929
40711
 
39930
40712
  // src/modules/mcp/catalog-server.ts
@@ -39935,7 +40717,7 @@ function isFolder2(path) {
39935
40717
  return false;
39936
40718
  }
39937
40719
  }
39938
- function openCatalogFolders(catalog) {
40720
+ function openCatalogFolders(catalog, historyFor) {
39939
40721
  const folders = [];
39940
40722
  const unavailable = [];
39941
40723
  for (const entry of catalog.entries) {
@@ -39943,7 +40725,7 @@ function openCatalogFolders(catalog) {
39943
40725
  unavailable.push({ name: entry.name, root: entry.root });
39944
40726
  continue;
39945
40727
  }
39946
- const library = openLibrary({ root: entry.root, exclude: entry.exclude });
40728
+ const library = openLibrary({ root: entry.root, exclude: entry.exclude, history: historyFor?.(entry.root) });
39947
40729
  folders.push({ name: entry.name, root: library.root, library, maxBytes: entry.maxBytes, fileCount: library.refresh().length });
39948
40730
  }
39949
40731
  return { folders, unavailable };
@@ -39995,14 +40777,62 @@ function catalogReadyLine(version2, home, catalog, opened) {
39995
40777
  return parts.join(" \xB7 ");
39996
40778
  }
39997
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
+
39998
40828
  // src/modules/setup/runtime.ts
39999
- import { existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync3, rmSync as rmSync2 } from "node:fs";
40000
- import { dirname, join as join5, resolve as resolve4 } from "node:path";
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";
40001
40831
 
40002
40832
  // src/modules/setup/commands.ts
40003
40833
  import { spawn } from "node:child_process";
40004
40834
  import { accessSync, constants, statSync as statSync4 } from "node:fs";
40005
- import { delimiter, join as join4 } from "node:path";
40835
+ import { delimiter, join as join5 } from "node:path";
40006
40836
  var DEFAULT_TIMEOUT_MS = 3e4;
40007
40837
  var WINDOWS_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
40008
40838
  function quotePart(part) {
@@ -40031,7 +40861,7 @@ function findOnPath(name, env = process.env, platform = process.platform) {
40031
40861
  }
40032
40862
  for (const directory of directories) {
40033
40863
  for (const candidate of candidates) {
40034
- const path = join4(directory, candidate);
40864
+ const path = join5(directory, candidate);
40035
40865
  try {
40036
40866
  if (statSync4(path).isFile() == false) {
40037
40867
  continue;
@@ -40140,17 +40970,17 @@ var PACKAGE_NAME = "librian";
40140
40970
  var INSTALL_TIMEOUT_MS = 18e4;
40141
40971
  var VIEW_TIMEOUT_MS = 3e4;
40142
40972
  function runtimeDirectory(home) {
40143
- return join5(home, "runtime");
40973
+ return join6(home, "runtime");
40144
40974
  }
40145
40975
  function packageDirectory(home) {
40146
- return join5(runtimeDirectory(home), "node_modules", PACKAGE_NAME);
40976
+ return join6(runtimeDirectory(home), "node_modules", PACKAGE_NAME);
40147
40977
  }
40148
40978
  function runtimeCliPath(home) {
40149
- return join5(packageDirectory(home), "dist", "cli.js");
40979
+ return join6(packageDirectory(home), "dist", "cli.js");
40150
40980
  }
40151
40981
  function readPackageVersion(directory) {
40152
40982
  try {
40153
- const parsed = JSON.parse(readFileSync3(join5(directory, "package.json"), "utf8"));
40983
+ const parsed = JSON.parse(readFileSync3(join6(directory, "package.json"), "utf8"));
40154
40984
  return typeof parsed.version == "string" ? parsed.version : null;
40155
40985
  } catch {
40156
40986
  return null;
@@ -40158,7 +40988,7 @@ function readPackageVersion(directory) {
40158
40988
  }
40159
40989
  function runtimeStatus(home) {
40160
40990
  const cliPath = runtimeCliPath(home);
40161
- if (existsSync2(cliPath) == false) {
40991
+ if (existsSync3(cliPath) == false) {
40162
40992
  return { state: "missing", version: null, cliPath };
40163
40993
  }
40164
40994
  const directory = packageDirectory(home);
@@ -40180,13 +41010,13 @@ function compareVersions(left, right) {
40180
41010
  return 0;
40181
41011
  }
40182
41012
  function findNpmCli(nodePath) {
40183
- const nodeDirectory = dirname(nodePath);
41013
+ const nodeDirectory = dirname2(nodePath);
40184
41014
  const candidates = [
40185
- join5(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
40186
- join5(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
41015
+ join6(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
41016
+ join6(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
40187
41017
  ];
40188
41018
  for (const candidate of candidates) {
40189
- if (existsSync2(candidate) == true) {
41019
+ if (existsSync3(candidate) == true) {
40190
41020
  return resolve4(candidate);
40191
41021
  }
40192
41022
  }
@@ -40218,7 +41048,7 @@ async function runNpm(toolchain, npmArguments, timeoutMs) {
40218
41048
  }
40219
41049
  async function installRuntime(home, spec, toolchain) {
40220
41050
  const directory = runtimeDirectory(home);
40221
- mkdirSync2(directory, { recursive: true });
41051
+ mkdirSync3(directory, { recursive: true });
40222
41052
  const installed = await runNpm(toolchain, ["install", "--prefix", directory, "--no-audit", "--no-fund", spec], INSTALL_TIMEOUT_MS);
40223
41053
  if (installed.ok == false) {
40224
41054
  return { ok: false, reason: installed.reason };
@@ -40247,10 +41077,21 @@ function uninstallRuntime(home) {
40247
41077
  }
40248
41078
  rmSync2(directory, { recursive: true, force: true });
40249
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
+ }
40250
41091
 
40251
41092
  // src/modules/setup/services.ts
40252
41093
  import { homedir as homedir3 } from "node:os";
40253
- import { join as join6 } from "node:path";
41094
+ import { join as join7 } from "node:path";
40254
41095
 
40255
41096
  // src/modules/setup/probe.ts
40256
41097
  import { spawn as spawn2 } from "node:child_process";
@@ -40357,7 +41198,7 @@ function defaultServiceDependencies() {
40357
41198
  return { ...defaultToolchain(), probe: probeServer };
40358
41199
  }
40359
41200
  function isDefaultHome(home) {
40360
- return normalizeForCompare(home) == normalizeForCompare(join6(homedir3(), ".librian"));
41201
+ return normalizeForCompare(home) == normalizeForCompare(join7(homedir3(), ".librian"));
40361
41202
  }
40362
41203
  function expectedRegistration(home, toolchain) {
40363
41204
  const environment = {};
@@ -40608,7 +41449,7 @@ async function askConfirm(question, defaultYes) {
40608
41449
  }
40609
41450
  }
40610
41451
  async function uninstall(home, dependencies) {
40611
- 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) {
40612
41453
  return;
40613
41454
  }
40614
41455
  const registered = readCatalog(home).services;
@@ -40628,7 +41469,13 @@ async function uninstall(home, dependencies) {
40628
41469
  }
40629
41470
  }
40630
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
+ }
40631
41477
  writeLine(`${styled("done", "green")} uninstall: the runtime folder is removed`);
41478
+ writeLine(styled(historyProblem, "yellow"));
40632
41479
  }
40633
41480
  async function openRuntimeScreen(home, dependencies = defaultServiceDependencies()) {
40634
41481
  let latest = null;
@@ -40791,6 +41638,21 @@ async function openServicesScreen(home, dependencies = defaultServiceDependencie
40791
41638
 
40792
41639
  // src/cli.ts
40793
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
+ }
40794
41656
  async function main() {
40795
41657
  const parsed = parseArguments(process.argv.slice(2));
40796
41658
  if (parsed.kind == "help") {
@@ -40842,13 +41704,26 @@ async function main() {
40842
41704
  process.exitCode = 1;
40843
41705
  return;
40844
41706
  }
40845
- const opened = openCatalogFolders(catalog);
41707
+ const stores = [];
41708
+ const opened = openCatalogFolders(catalog, (root2) => {
41709
+ const store2 = sharedHistoryStore(resolveHistoryFile(home, root2));
41710
+ stores.push(store2);
41711
+ return store2;
41712
+ });
40846
41713
  for (const folder of opened.unavailable) {
40847
41714
  writeStderrLine(`librian: docs folder "${folder.name}" (${folder.root}) does not exist or is not a folder; its tools are not offered.`);
40848
41715
  }
40849
41716
  const server2 = createCatalogServer(catalog, opened, package_default.version);
40850
- await serveOverStdio(server2);
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
+ });
40851
41725
  writeStderrLine(catalogReadyLine(package_default.version, home, catalog, opened));
41726
+ watchHistory(stores);
40852
41727
  return;
40853
41728
  }
40854
41729
  const root = resolve5(parsed.root);
@@ -40863,11 +41738,15 @@ async function main() {
40863
41738
  process.exitCode = EXIT_USAGE2;
40864
41739
  return;
40865
41740
  }
40866
- const library = openLibrary({ root, exclude: parsed.exclude });
41741
+ const store = sharedHistoryStore(resolveHistoryFile(librianHome(), root));
41742
+ const library = openLibrary({ root, exclude: parsed.exclude, history: store });
40867
41743
  const fileCount = library.refresh().length;
40868
41744
  const server = createLibrianServer({ library, maxBytes: parsed.maxBytes, version: package_default.version });
40869
- await serveOverStdio(server);
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) });
40870
41748
  writeStderrLine(`librian ${package_default.version} ready \xB7 root ${library.root} \xB7 ${fileCount} files \xB7 pid ${process.pid}`);
41749
+ watchHistory([store]);
40871
41750
  }
40872
41751
  main().catch((error62) => {
40873
41752
  const message = error62 instanceof Error ? error62.stack ?? error62.message : String(error62);