librian 0.1.0 → 0.1.2

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 +47 -1
  2. package/README.md +35 -10
  3. package/dist/cli.js +429 -271
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2985,7 +2985,7 @@ var require_compile = __commonJS({
2985
2985
  const schOrFunc = root.refs[ref];
2986
2986
  if (schOrFunc)
2987
2987
  return schOrFunc;
2988
- let _sch = resolve7.call(this, root, ref);
2988
+ let _sch = resolve6.call(this, root, ref);
2989
2989
  if (_sch === void 0) {
2990
2990
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2991
2991
  const { schemaId } = this.opts;
@@ -3012,7 +3012,7 @@ var require_compile = __commonJS({
3012
3012
  function sameSchemaEnv(s1, s2) {
3013
3013
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3014
3014
  }
3015
- function resolve7(root, ref) {
3015
+ function resolve6(root, ref) {
3016
3016
  let sch;
3017
3017
  while (typeof (sch = this.refs[ref]) == "string")
3018
3018
  ref = sch;
@@ -3872,11 +3872,11 @@ var require_fast_uri = __commonJS({
3872
3872
  normalizeString(uri, options);
3873
3873
  } else if (typeof uri === "object") {
3874
3874
  uri = /** @type {T} */
3875
- parse3(serialize(uri, options), options);
3875
+ parse4(serialize(uri, options), options);
3876
3876
  }
3877
3877
  return uri;
3878
3878
  }
3879
- function resolve7(baseURI, relativeURI, options) {
3879
+ function resolve6(baseURI, relativeURI, options) {
3880
3880
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3881
3881
  const {
3882
3882
  parsed: baseParsed,
@@ -3915,8 +3915,8 @@ var require_fast_uri = __commonJS({
3915
3915
  function resolveComponent(base, relative3, options, skipNormalization) {
3916
3916
  const target = {};
3917
3917
  if (!skipNormalization) {
3918
- base = parse3(serialize(base, options), options);
3919
- relative3 = parse3(serialize(relative3, options), options);
3918
+ base = parse4(serialize(base, options), options);
3919
+ relative3 = parse4(serialize(relative3, options), options);
3920
3920
  }
3921
3921
  options = options || {};
3922
3922
  if (!options.tolerant && relative3.scheme) {
@@ -4234,7 +4234,7 @@ var require_fast_uri = __commonJS({
4234
4234
  }
4235
4235
  return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
4236
4236
  }
4237
- function parse3(uri, opts) {
4237
+ function parse4(uri, opts) {
4238
4238
  return parseWithStatus(uri, opts).parsed;
4239
4239
  }
4240
4240
  function normalizeString(uri, opts) {
@@ -4267,11 +4267,11 @@ var require_fast_uri = __commonJS({
4267
4267
  var fastUri = {
4268
4268
  SCHEMES,
4269
4269
  normalize: normalize2,
4270
- resolve: resolve7,
4270
+ resolve: resolve6,
4271
4271
  resolveComponent,
4272
4272
  equal,
4273
4273
  serialize,
4274
- parse: parse3
4274
+ parse: parse4
4275
4275
  };
4276
4276
  module.exports = fastUri;
4277
4277
  module.exports.default = fastUri;
@@ -7258,12 +7258,12 @@ var require_dist = __commonJS({
7258
7258
 
7259
7259
  // src/cli.ts
7260
7260
  import { statSync as statSync5 } from "node:fs";
7261
- import { resolve as resolve6 } from "node:path";
7261
+ import { resolve as resolve5 } from "node:path";
7262
7262
 
7263
7263
  // package.json
7264
7264
  var package_default = {
7265
7265
  name: "librian",
7266
- version: "0.1.0",
7266
+ version: "0.1.2",
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",
@@ -7331,6 +7331,8 @@ var TITLE_MAX_CHARS = 80;
7331
7331
  var EXCERPT_BYTES = 160;
7332
7332
  var SEARCH_LIMIT_DEFAULT = 20;
7333
7333
  var SEARCH_LIMIT_MAX = 50;
7334
+ var COUNT_MAX_FILES = 5e3;
7335
+ var COUNT_MAX_MS = 2e3;
7334
7336
 
7335
7337
  // src/modules/catalog/catalog.ts
7336
7338
  var CATALOG_FILE_NAME = "catalog.json";
@@ -7456,9 +7458,10 @@ function defaultEntryName(root, taken) {
7456
7458
  }
7457
7459
  }
7458
7460
 
7459
- // src/modules/docs/library.ts
7460
- import { readFileSync as readFileSync2, readdirSync, realpathSync, statSync } from "node:fs";
7461
- import { basename, join as join2, resolve } from "node:path";
7461
+ // src/modules/cli/folder-check.ts
7462
+ import { statSync as statSync2 } from "node:fs";
7463
+ import { homedir as homedir2 } from "node:os";
7464
+ import { join as join3, parse, resolve as resolve2 } from "node:path";
7462
7465
 
7463
7466
  // src/_libs/compare-text.ts
7464
7467
  function compareText(left, right) {
@@ -7491,6 +7494,10 @@ function compareText(left, right) {
7491
7494
  return 0;
7492
7495
  }
7493
7496
 
7497
+ // src/modules/docs/library.ts
7498
+ import { readFileSync as readFileSync2, readdirSync, realpathSync, statSync } from "node:fs";
7499
+ import { basename, join as join2, resolve } from "node:path";
7500
+
7494
7501
  // src/_libs/is-inside.ts
7495
7502
  import { isAbsolute, relative, sep } from "node:path";
7496
7503
  function isInside(root, target) {
@@ -7682,18 +7689,10 @@ var ALWAYS_EXCLUDED_DIRECTORY = "node_modules";
7682
7689
  function normalizeExclude(entry) {
7683
7690
  return entry.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
7684
7691
  }
7685
- function openLibrary(options) {
7686
- const root = realpathSync.native(resolve(options.root));
7687
- if (statSync(root).isDirectory() == false) {
7688
- throw new Error("root is not a folder: " + root);
7689
- }
7690
- let name = basename(root);
7691
- if (name == "") {
7692
- name = root;
7693
- }
7692
+ function listMarkdownPaths(root, exclude, limits) {
7694
7693
  const excludedNames = /* @__PURE__ */ new Set();
7695
7694
  const excludedPaths = /* @__PURE__ */ new Set();
7696
- for (const entry of options.exclude) {
7695
+ for (const entry of exclude) {
7697
7696
  const normalized = normalizeExclude(entry);
7698
7697
  if (normalized == "") {
7699
7698
  continue;
@@ -7704,43 +7703,65 @@ function openLibrary(options) {
7704
7703
  excludedNames.add(normalized);
7705
7704
  }
7706
7705
  }
7707
- const cache = /* @__PURE__ */ new Map();
7708
- function listMarkdownPaths() {
7709
- const found = [];
7710
- const walk = (absoluteDirectory, relativePrefix) => {
7711
- let entries;
7712
- try {
7713
- entries = readdirSync(absoluteDirectory, { withFileTypes: true });
7714
- } catch {
7706
+ const deadline = Date.now() + (limits?.maxMs ?? Number.POSITIVE_INFINITY);
7707
+ const found = [];
7708
+ let truncated = false;
7709
+ const isOverLimit = () => limits != null && (found.length >= limits.maxFiles || Date.now() > deadline);
7710
+ const walk = (absoluteDirectory, relativePrefix) => {
7711
+ if (isOverLimit() == true) {
7712
+ truncated = true;
7713
+ return;
7714
+ }
7715
+ let entries;
7716
+ try {
7717
+ entries = readdirSync(absoluteDirectory, { withFileTypes: true });
7718
+ } catch {
7719
+ return;
7720
+ }
7721
+ for (const entry of entries) {
7722
+ if (truncated == true) {
7715
7723
  return;
7716
7724
  }
7717
- for (const entry of entries) {
7718
- if (entry.isSymbolicLink() == true) {
7719
- continue;
7720
- }
7721
- const relativePath = relativePrefix + entry.name;
7722
- if (excludedNames.has(entry.name) == true || excludedPaths.has(relativePath) == true) {
7723
- continue;
7724
- }
7725
- if (entry.isDirectory() == true) {
7726
- if (entry.name.startsWith(".") == true || entry.name == ALWAYS_EXCLUDED_DIRECTORY) {
7727
- continue;
7728
- }
7729
- walk(join2(absoluteDirectory, entry.name), relativePath + "/");
7725
+ if (entry.isSymbolicLink() == true) {
7726
+ continue;
7727
+ }
7728
+ const relativePath = relativePrefix + entry.name;
7729
+ if (excludedNames.has(entry.name) == true || excludedPaths.has(relativePath) == true) {
7730
+ continue;
7731
+ }
7732
+ if (entry.isDirectory() == true) {
7733
+ if (entry.name.startsWith(".") == true || entry.name == ALWAYS_EXCLUDED_DIRECTORY) {
7730
7734
  continue;
7731
7735
  }
7732
- if (entry.isFile() == true && /\.md$/i.test(entry.name) == true) {
7733
- found.push(relativePath);
7736
+ walk(join2(absoluteDirectory, entry.name), relativePath + "/");
7737
+ continue;
7738
+ }
7739
+ if (entry.isFile() == true && /\.md$/i.test(entry.name) == true) {
7740
+ found.push(relativePath);
7741
+ if (isOverLimit() == true) {
7742
+ truncated = true;
7743
+ return;
7734
7744
  }
7735
7745
  }
7736
- };
7737
- walk(root, "");
7738
- return found;
7746
+ }
7747
+ };
7748
+ walk(root, "");
7749
+ return { paths: found, truncated };
7750
+ }
7751
+ function openLibrary(options) {
7752
+ const root = realpathSync.native(resolve(options.root));
7753
+ if (statSync(root).isDirectory() == false) {
7754
+ throw new Error("root is not a folder: " + root);
7739
7755
  }
7756
+ let name = basename(root);
7757
+ if (name == "") {
7758
+ name = root;
7759
+ }
7760
+ const cache = /* @__PURE__ */ new Map();
7740
7761
  function refresh() {
7741
7762
  const documents = [];
7742
7763
  const seen = /* @__PURE__ */ new Set();
7743
- for (const path of listMarkdownPaths().sort(compareText)) {
7764
+ for (const path of listMarkdownPaths(root, options.exclude).paths.sort(compareText)) {
7744
7765
  const absolutePath = join2(root, path);
7745
7766
  let modifiedMs;
7746
7767
  let size;
@@ -7885,6 +7906,113 @@ function cutPage(document, start, end, budgetBytes) {
7885
7906
  return { text: joinLines2(lastLine), lastLine };
7886
7907
  }
7887
7908
 
7909
+ // src/modules/cli/folder-check.ts
7910
+ var LARGEST_FILE_COUNT = 3;
7911
+ var COUNT_LIMITS = { maxFiles: COUNT_MAX_FILES, maxMs: COUNT_MAX_MS };
7912
+ function isFolder(path) {
7913
+ try {
7914
+ return statSync2(path).isDirectory();
7915
+ } catch {
7916
+ return false;
7917
+ }
7918
+ }
7919
+ function comparable(path) {
7920
+ const resolved = resolve2(path);
7921
+ if (process.platform == "win32") {
7922
+ return resolved.toLowerCase();
7923
+ }
7924
+ return resolved;
7925
+ }
7926
+ function isSameFolder(left, right) {
7927
+ return comparable(left) == comparable(right);
7928
+ }
7929
+ function isHomeOrDriveRoot(path) {
7930
+ const resolved = resolve2(path);
7931
+ return isSameFolder(resolved, homedir2()) || isSameFolder(resolved, parse(resolved).root);
7932
+ }
7933
+ function normalizeFolderInput(input2, base = process.cwd()) {
7934
+ let folder = input2.trim();
7935
+ const quoted = /^(["'])(.*)\1$/.exec(folder);
7936
+ if (quoted != null) {
7937
+ folder = quoted[2].trim();
7938
+ }
7939
+ if (folder == "") {
7940
+ return "";
7941
+ }
7942
+ if (folder == "~" || folder.startsWith("~/") || folder.startsWith("~\\")) {
7943
+ folder = homedir2() + folder.slice(1);
7944
+ }
7945
+ return resolve2(base, folder);
7946
+ }
7947
+ function formatFileCount(files, truncated) {
7948
+ let suffix = "";
7949
+ if (truncated == true) {
7950
+ suffix = "+";
7951
+ }
7952
+ if (files == 1 && truncated == false) {
7953
+ return "1 file";
7954
+ }
7955
+ return `${files}${suffix} files`;
7956
+ }
7957
+ function countMarkdownFiles(root, exclude) {
7958
+ if (isFolder(root) == false) {
7959
+ return null;
7960
+ }
7961
+ const scan = listMarkdownPaths(root, exclude, COUNT_LIMITS);
7962
+ return { files: scan.paths.length, truncated: scan.truncated };
7963
+ }
7964
+ function checkFolder(root, exclude) {
7965
+ if (isFolder(root) == false) {
7966
+ return { ok: false, message: `The docs folder does not exist or is not a folder: ${root}` };
7967
+ }
7968
+ const scan = listMarkdownPaths(root, exclude, COUNT_LIMITS);
7969
+ const sized = scan.paths.map((path) => {
7970
+ let bytes = 0;
7971
+ try {
7972
+ bytes = statSync2(join3(root, path)).size;
7973
+ } catch {
7974
+ bytes = 0;
7975
+ }
7976
+ return { path, bytes };
7977
+ });
7978
+ sized.sort((left, right) => right.bytes - left.bytes || compareText(left.path, right.path));
7979
+ return {
7980
+ ok: true,
7981
+ files: sized.length,
7982
+ truncated: scan.truncated,
7983
+ totalBytes: sized.reduce((sum, file2) => sum + file2.bytes, 0),
7984
+ largest: sized.slice(0, LARGEST_FILE_COUNT)
7985
+ };
7986
+ }
7987
+ function summarizeCheck(check2) {
7988
+ if (check2.ok == false) {
7989
+ return "folder not found";
7990
+ }
7991
+ if (check2.truncated == true) {
7992
+ return formatFileCount(check2.files, true);
7993
+ }
7994
+ return `${formatFileCount(check2.files, false)} \xB7 ${formatSize(check2.totalBytes)}`;
7995
+ }
7996
+ function describeCheck(check2) {
7997
+ if (check2.ok == false) {
7998
+ return [check2.message];
7999
+ }
8000
+ const lines = [summarizeCheck(check2)];
8001
+ if (check2.truncated == true) {
8002
+ lines.push(`Stopped counting after ${COUNT_MAX_FILES} files or ${COUNT_MAX_MS / 1e3} seconds. Is this the docs folder?`);
8003
+ return lines;
8004
+ }
8005
+ if (check2.files == 0) {
8006
+ lines.push("No .md files found. Check the folder and the Exclude list.");
8007
+ return lines;
8008
+ }
8009
+ lines.push("Largest:");
8010
+ for (const file2 of check2.largest) {
8011
+ lines.push(` ${file2.path} ${formatSize(file2.bytes)}`);
8012
+ }
8013
+ return lines;
8014
+ }
8015
+
7888
8016
  // src/modules/cli/display.ts
7889
8017
  var STYLE_CODES = {
7890
8018
  dim: "\x1B[2m",
@@ -8022,67 +8150,6 @@ function renderParts(parts, maxWidth, color) {
8022
8150
  return line;
8023
8151
  }
8024
8152
 
8025
- // src/modules/cli/folder-check.ts
8026
- import { statSync as statSync2 } from "node:fs";
8027
- import { homedir as homedir2 } from "node:os";
8028
- import { resolve as resolve2 } from "node:path";
8029
- var LARGEST_FILE_COUNT = 3;
8030
- function isFolder(path) {
8031
- try {
8032
- return statSync2(path).isDirectory();
8033
- } catch {
8034
- return false;
8035
- }
8036
- }
8037
- function normalizeFolderInput(input2) {
8038
- let folder = input2.trim();
8039
- const quoted = /^(["'])(.*)\1$/.exec(folder);
8040
- if (quoted != null) {
8041
- folder = quoted[2].trim();
8042
- }
8043
- if (folder == "") {
8044
- return "";
8045
- }
8046
- if (folder == "~" || folder.startsWith("~/") || folder.startsWith("~\\")) {
8047
- folder = homedir2() + folder.slice(1);
8048
- }
8049
- return resolve2(folder);
8050
- }
8051
- function checkFolder(root, exclude) {
8052
- if (isFolder(root) == false) {
8053
- return { ok: false, message: `The docs folder does not exist or is not a folder: ${root}` };
8054
- }
8055
- const documents = openLibrary({ root, exclude }).refresh();
8056
- const bySize = [...documents].sort((left, right) => right.totalBytes - left.totalBytes || compareText(left.path, right.path));
8057
- return {
8058
- ok: true,
8059
- files: documents.length,
8060
- totalBytes: documents.reduce((sum, document) => sum + document.totalBytes, 0),
8061
- largest: bySize.slice(0, LARGEST_FILE_COUNT).map((document) => ({ path: document.path, bytes: document.totalBytes }))
8062
- };
8063
- }
8064
- function summarizeCheck(check2) {
8065
- if (check2.ok == false) {
8066
- return "folder not found";
8067
- }
8068
- return `${pluralize(check2.files, "file")} \xB7 ${formatSize(check2.totalBytes)}`;
8069
- }
8070
- function describeCheck(check2) {
8071
- if (check2.ok == false) {
8072
- return [check2.message];
8073
- }
8074
- const lines = [summarizeCheck(check2)];
8075
- if (check2.files == 0) {
8076
- lines.push("No .md files found. Check the folder and the Exclude list.");
8077
- return lines;
8078
- }
8079
- lines.push("Largest:");
8080
- for (const file2 of check2.largest) {
8081
- lines.push(` ${file2.path} ${formatSize(file2.bytes)}`);
8082
- }
8083
- return lines;
8084
- }
8085
-
8086
8153
  // src/modules/cli/input-owner.ts
8087
8154
  var InputOwner = class {
8088
8155
  constructor(input2) {
@@ -8138,8 +8205,8 @@ var InputOwner = class {
8138
8205
  this.pendingText = "";
8139
8206
  return rest;
8140
8207
  }
8141
- await new Promise((resolve7) => {
8142
- this.lineWaiter = resolve7;
8208
+ await new Promise((resolve6) => {
8209
+ this.lineWaiter = resolve6;
8143
8210
  this.input.resume();
8144
8211
  });
8145
8212
  this.lineWaiter = null;
@@ -8304,6 +8371,9 @@ function writeLine(text2 = "") {
8304
8371
  function styled(text2, style) {
8305
8372
  return styleText(text2, style, getTerminal().color);
8306
8373
  }
8374
+ function tickMark() {
8375
+ return styleText(symbolsOf(getTerminal()).tick, "green", getTerminal().color);
8376
+ }
8307
8377
  function columnsOf(terminal) {
8308
8378
  return terminal.output.columns ?? DEFAULT_COLUMNS;
8309
8379
  }
@@ -8366,7 +8436,7 @@ async function withRawMode(terminal, hideCursor, frame, body) {
8366
8436
  }
8367
8437
  }
8368
8438
  function readKeys(terminal, onKey) {
8369
- return new Promise((resolve7, reject) => {
8439
+ return new Promise((resolve6, reject) => {
8370
8440
  let finished = false;
8371
8441
  const stop = terminal.owner.beginKeys(
8372
8442
  (chunk) => {
@@ -8379,7 +8449,7 @@ function readKeys(terminal, onKey) {
8379
8449
  if (outcome != void 0) {
8380
8450
  finished = true;
8381
8451
  stop();
8382
- resolve7(outcome.result);
8452
+ resolve6(outcome.result);
8383
8453
  return;
8384
8454
  }
8385
8455
  } catch (error62) {
@@ -8847,6 +8917,122 @@ async function confirm(question, defaultYes, options = {}) {
8847
8917
  return confirmWithLines(terminal, question, defaultYes);
8848
8918
  }
8849
8919
 
8920
+ // src/modules/cli/quick-add.ts
8921
+ function initialFolderFor(currentFolder) {
8922
+ if (isHomeOrDriveRoot(currentFolder) == true) {
8923
+ return "";
8924
+ }
8925
+ return currentFolder;
8926
+ }
8927
+ async function addFolderAsEntry(home, root) {
8928
+ const catalog = readCatalog(home);
8929
+ const existing = catalog.entries.find((entry) => isSameFolder(entry.root, root));
8930
+ if (existing != null) {
8931
+ writeLine(`Already added as "${existing.name}".`);
8932
+ return { kind: "exists", name: existing.name };
8933
+ }
8934
+ const check2 = checkFolder(root, []);
8935
+ if (check2.ok == false) {
8936
+ writeLine(styled(check2.message, "red"));
8937
+ return { kind: "declined" };
8938
+ }
8939
+ let summary = formatFileCount(check2.files, check2.truncated).replace(/ files?$/, (unit) => ` markdown${unit}`);
8940
+ if (check2.truncated == false) {
8941
+ summary += ` \xB7 ${formatSize(check2.totalBytes)}`;
8942
+ }
8943
+ writeLine(` ${summary}`);
8944
+ if (check2.files == 0) {
8945
+ writeLine(styled("No markdown files were found in this folder.", "yellow"));
8946
+ }
8947
+ const name = defaultEntryName(root, catalog.entries.map((entry) => entry.name));
8948
+ const accepted = await confirm(`Add it as "${name}"?`, check2.files > 0, { transient: true });
8949
+ if (accepted == false) {
8950
+ return { kind: "declined" };
8951
+ }
8952
+ const latest = readCatalog(home);
8953
+ const finalName = defaultEntryName(root, latest.entries.map((entry) => entry.name));
8954
+ latest.entries.push({ name: finalName, root, exclude: [], maxBytes: DEFAULT_MAX_BYTES });
8955
+ writeCatalog(home, latest);
8956
+ writeLine(`${tickMark()} Added ${finalName}. Open a new session to use ${finalName}__read and the other tools.`);
8957
+ return { kind: "added", name: finalName };
8958
+ }
8959
+ async function quickAddFromMenu(home, initialFolder, base) {
8960
+ let candidate = initialFolder;
8961
+ for (; ; ) {
8962
+ let typed;
8963
+ try {
8964
+ typed = await text("Docs folder", { initial: candidate, hint: "the folder that holds your .md files", transient: true });
8965
+ } catch (error62) {
8966
+ if (error62 instanceof PromptBackError) {
8967
+ return { kind: "declined" };
8968
+ }
8969
+ throw error62;
8970
+ }
8971
+ const root = normalizeFolderInput(typed, base);
8972
+ if (root == "") {
8973
+ writeLine(styled("Enter the path of a docs folder.", "red"));
8974
+ candidate = typed;
8975
+ continue;
8976
+ }
8977
+ if (isFolder(root) == false) {
8978
+ writeLine(styled(`The folder does not exist or is not a folder: ${root}`, "red"));
8979
+ candidate = typed;
8980
+ continue;
8981
+ }
8982
+ writeLine(`${tickMark()} Docs folder \xB7 ${root}`);
8983
+ try {
8984
+ return await addFolderAsEntry(home, root);
8985
+ } catch (error62) {
8986
+ if (error62 instanceof PromptBackError) {
8987
+ return { kind: "declined" };
8988
+ }
8989
+ throw error62;
8990
+ }
8991
+ }
8992
+ }
8993
+
8994
+ // src/modules/cli/add-command.ts
8995
+ var EXIT_USAGE = 2;
8996
+ async function runAddCommand(options) {
8997
+ let root = normalizeFolderInput(options.cwd, options.cwd);
8998
+ if (options.folder != null) {
8999
+ root = normalizeFolderInput(options.folder, options.cwd);
9000
+ }
9001
+ if (isHomeOrDriveRoot(root) == true) {
9002
+ options.writeError(`"${root}" is your home folder or a drive root, not a docs folder. Run librian add inside a docs folder, or pass its path: librian add <folder>`);
9003
+ return EXIT_USAGE;
9004
+ }
9005
+ if (isFolder(root) == false) {
9006
+ options.writeError(`The folder does not exist or is not a folder: ${root}`);
9007
+ return EXIT_USAGE;
9008
+ }
9009
+ try {
9010
+ writeLine(`Docs folder \xB7 ${root}`);
9011
+ const outcome = await addFolderAsEntry(options.home, root);
9012
+ if (outcome.kind == "declined") {
9013
+ writeLine("Nothing added.");
9014
+ }
9015
+ if (outcome.kind == "added" && readCatalog(options.home).services.length == 0) {
9016
+ writeLine("Run librian and choose Register to use it in Claude Code or Codex.");
9017
+ }
9018
+ return 0;
9019
+ } catch (error62) {
9020
+ if (error62 instanceof PromptClosedError) {
9021
+ return 0;
9022
+ }
9023
+ if (error62 instanceof PromptBackError) {
9024
+ writeLine("Nothing added.");
9025
+ return 0;
9026
+ }
9027
+ let reason = String(error62);
9028
+ if (error62 instanceof Error) {
9029
+ reason = error62.message;
9030
+ }
9031
+ options.writeError(reason);
9032
+ return 1;
9033
+ }
9034
+ }
9035
+
8850
9036
  // src/modules/cli/entry-form.ts
8851
9037
  var FORM_ORDER = ["name", "folder", "exclude", "max", "check", "save", "cancel"];
8852
9038
  function snapshotOf(draft) {
@@ -8860,14 +9046,6 @@ function nextRow(current) {
8860
9046
  return FORM_ORDER[Math.min(position + 1, FORM_ORDER.length - 1)];
8861
9047
  }
8862
9048
  function buildRows(draft, home, lastCheck) {
8863
- let nameDetail = draft.name;
8864
- if (nameDetail == "") {
8865
- nameDetail = "(from the folder)";
8866
- }
8867
- let nameHint = "";
8868
- if (draft.nameTouched == false) {
8869
- nameHint = "follows the folder until you change it";
8870
- }
8871
9049
  let folderDetail = draft.root;
8872
9050
  let folderKind = "path";
8873
9051
  let folderHint = "";
@@ -8886,7 +9064,7 @@ function buildRows(draft, home, lastCheck) {
8886
9064
  checkDetail = lastCheck.summary;
8887
9065
  }
8888
9066
  return [
8889
- { value: "name", key: "name", label: "Name", detail: nameDetail, hint: nameHint },
9067
+ { value: "name", key: "name", label: "Name", detail: draft.name, hint: "prefixes the tool names" },
8890
9068
  { value: "folder", key: "folder", label: "Docs folder", detail: folderDetail, detailKind: folderKind, hint: folderHint },
8891
9069
  { value: "exclude", key: "exclude", label: "Exclude", detail: excludeDetail, hint: "folder names or paths to skip" },
8892
9070
  { value: "max", key: "max", label: "Max bytes", detail: String(draft.maxBytes), hint: "response size limit" },
@@ -8899,33 +9077,24 @@ function buildRows(draft, home, lastCheck) {
8899
9077
  async function openEntryForm(home, editing) {
8900
9078
  const catalog = readCatalog(home);
8901
9079
  const existing = catalog.entries.find((entry) => entry.name == editing);
8902
- const otherNames = () => readCatalog(home).entries.filter((entry) => entry.name != editing).map((entry) => entry.name);
8903
- let draft = { name: "", nameTouched: false, root: "", exclude: [], maxBytes: DEFAULT_MAX_BYTES };
8904
- let cursor = "folder";
8905
- if (existing != null) {
8906
- draft = { name: existing.name, nameTouched: true, root: existing.root, exclude: [...existing.exclude], maxBytes: existing.maxBytes };
8907
- cursor = "name";
9080
+ if (existing == null) {
9081
+ writeLine(styled(`There is no docs folder named "${editing}" any more.`, "red"));
9082
+ return "cancelled";
8908
9083
  }
9084
+ const otherNames = () => readCatalog(home).entries.filter((entry) => entry.name != editing).map((entry) => entry.name);
9085
+ let draft = { name: existing.name, root: existing.root, exclude: [...existing.exclude], maxBytes: existing.maxBytes };
9086
+ let cursor = "name";
8909
9087
  const openingSnapshot = snapshotOf(draft);
8910
9088
  let lastCheck = null;
8911
- let verifiedKey = null;
8912
- if (existing != null) {
8913
- verifiedKey = checkKeyOf(draft);
8914
- }
8915
- let title = "Add docs folder";
8916
- if (existing != null) {
8917
- title = `Edit "${existing.name}"`;
8918
- }
9089
+ let verifiedKey = checkKeyOf(draft);
9090
+ const title = `Edit "${existing.name}"`;
8919
9091
  const askName = async () => {
8920
9092
  let candidate = draft.name;
8921
9093
  for (; ; ) {
8922
9094
  const typed = (await text("Name", { initial: candidate, hint: "lowercase letters, digits and _, up to 24", transient: true })).trim();
8923
9095
  const problem = checkEntryName(typed, otherNames());
8924
9096
  if (problem == null) {
8925
- if (typed != draft.name) {
8926
- draft.name = typed;
8927
- draft.nameTouched = true;
8928
- }
9097
+ draft.name = typed;
8929
9098
  return;
8930
9099
  }
8931
9100
  writeLine(styled(problem, "red"));
@@ -8935,12 +9104,6 @@ async function openEntryForm(home, editing) {
8935
9104
  const askFolder = async () => {
8936
9105
  const typed = await text("Docs folder", { initial: draft.root, hint: "the folder that holds your .md files", transient: true });
8937
9106
  draft.root = normalizeFolderInput(typed);
8938
- if (draft.nameTouched == false) {
8939
- draft.name = "";
8940
- if (draft.root != "") {
8941
- draft.name = defaultEntryName(draft.root, otherNames());
8942
- }
8943
- }
8944
9107
  };
8945
9108
  const askExclude = async () => {
8946
9109
  const typed = await text("Exclude", { initial: draft.exclude.join(", "), hint: "comma separated, empty for none", transient: true });
@@ -9004,7 +9167,7 @@ async function openEntryForm(home, editing) {
9004
9167
  const entry = { name: draft.name, root: draft.root, exclude: draft.exclude, maxBytes: draft.maxBytes };
9005
9168
  try {
9006
9169
  const latest = readCatalog(home);
9007
- const position = latest.entries.findIndex((item) => item.name == editing);
9170
+ const position = latest.entries.findIndex((item) => item.name == existing.name);
9008
9171
  if (position >= 0) {
9009
9172
  latest.entries[position] = entry;
9010
9173
  } else {
@@ -9098,21 +9261,6 @@ var SERVICE_NAMES = { claude: "Claude Code", codex: "Codex" };
9098
9261
  var ITEM_PREFIX = "item:";
9099
9262
  var ENTRY_PATH_WIDTH = 50;
9100
9263
  var HOME_PATH_WIDTH = 40;
9101
- var LibraryCache = class {
9102
- libraries = /* @__PURE__ */ new Map();
9103
- countFiles(entry) {
9104
- if (isFolder(entry.root) == false) {
9105
- return null;
9106
- }
9107
- const key = JSON.stringify([entry.root, entry.exclude]);
9108
- let library = this.libraries.get(key);
9109
- if (library == null) {
9110
- library = openLibrary({ root: entry.root, exclude: entry.exclude });
9111
- this.libraries.set(key, library);
9112
- }
9113
- return library.refresh().length;
9114
- }
9115
- };
9116
9264
  function describeServices(catalog) {
9117
9265
  if (catalog.services.length == 0) {
9118
9266
  return "none registered";
@@ -9133,13 +9281,13 @@ function describeRuntime(home, context) {
9133
9281
  return "status unknown";
9134
9282
  }
9135
9283
  }
9136
- function buildMainRows(home, context, catalog, cache) {
9284
+ function buildMainRows(home, context, catalog) {
9137
9285
  const rows = [];
9138
9286
  for (const entry of catalog.entries) {
9139
- const files = cache.countFiles(entry);
9287
+ const counted = countMarkdownFiles(entry.root, entry.exclude);
9140
9288
  let hint = "folder not found";
9141
- if (files != null) {
9142
- hint = pluralize(files, "file");
9289
+ if (counted != null) {
9290
+ hint = formatFileCount(counted.files, counted.truncated);
9143
9291
  }
9144
9292
  rows.push({ value: ITEM_PREFIX + entry.name, label: entry.name, detail: entry.root, detailKind: "path", hint });
9145
9293
  }
@@ -9147,7 +9295,7 @@ function buildMainRows(home, context, catalog, cache) {
9147
9295
  rows.push("separator");
9148
9296
  }
9149
9297
  rows.push({ value: "add", key: "add", label: "Add docs folder" });
9150
- rows.push({ value: "services", key: "services", label: "AI services", hint: describeServices(catalog) });
9298
+ rows.push({ value: "services", key: "services", label: "Register", hint: describeServices(catalog) });
9151
9299
  rows.push({ value: "runtime", key: "runtime", label: "Runtime", hint: describeRuntime(home, context) });
9152
9300
  rows.push({ value: "quit", key: "quit", label: "Quit" });
9153
9301
  return rows;
@@ -9225,7 +9373,6 @@ async function runScreen(open2) {
9225
9373
  }
9226
9374
  }
9227
9375
  async function runMainMenu(home, context) {
9228
- const cache = new LibraryCache();
9229
9376
  let cursor;
9230
9377
  for (; ; ) {
9231
9378
  let catalog;
@@ -9241,7 +9388,7 @@ async function runMainMenu(home, context) {
9241
9388
  }
9242
9389
  let choice;
9243
9390
  try {
9244
- choice = await select(`librian ${context.version} \xB7 ${shortenPath(home, HOME_PATH_WIDTH)}`, buildMainRows(home, context, catalog, cache), { initial: cursor, transient: true });
9391
+ choice = await select(`librian ${context.version} \xB7 ${shortenPath(home, HOME_PATH_WIDTH)}`, buildMainRows(home, context, catalog), { initial: cursor, transient: true });
9245
9392
  } catch (error62) {
9246
9393
  if (error62 instanceof PromptBackError) {
9247
9394
  return 0;
@@ -9253,11 +9400,9 @@ async function runMainMenu(home, context) {
9253
9400
  return 0;
9254
9401
  }
9255
9402
  if (choice == "add") {
9256
- if (await openEntryForm(home, null) == "saved") {
9257
- const newest = readCatalog(home).entries.at(-1);
9258
- if (newest != null) {
9259
- cursor = ITEM_PREFIX + newest.name;
9260
- }
9403
+ const outcome = await quickAddFromMenu(home, initialFolderFor(context.cwd ?? process.cwd()), context.cwd ?? process.cwd());
9404
+ if (outcome.kind != "declined") {
9405
+ cursor = ITEM_PREFIX + outcome.name;
9261
9406
  }
9262
9407
  } else if (choice == "services") {
9263
9408
  await runScreen(() => context.openServicesScreen(home));
@@ -9281,7 +9426,8 @@ async function openMainMenu(home, context) {
9281
9426
 
9282
9427
  // src/modules/mcp/arguments.ts
9283
9428
  var USAGE = [
9284
- "Usage: librian Open the menu (docs folders, AI services, runtime)",
9429
+ "Usage: librian Open the menu (docs folders, register, runtime)",
9430
+ " librian add [<folder>] Add a folder (default: the current folder) as a docs folder, without the menu",
9285
9431
  " librian serve --catalog Serve every docs folder of the catalog (what AI services run)",
9286
9432
  " librian serve --root <folder> [--exclude <name>]... [--max-bytes <n>]",
9287
9433
  " Serve one folder with unprefixed tools",
@@ -9289,7 +9435,7 @@ var USAGE = [
9289
9435
  "",
9290
9436
  " --catalog Read the docs folders from <LIBRIAN_HOME>/catalog.json (default ~/.librian)",
9291
9437
  " --root <folder> Markdown docs folder to serve",
9292
- " --exclude <name> Skip a folder name (e.g. evidence) or a root-relative path (e.g. ks/archive); repeatable",
9438
+ " --exclude <name> Skip a folder name (e.g. evidence) or a root-relative path (e.g. guide/archive); repeatable",
9293
9439
  ` --max-bytes <n> Response size limit in UTF-8 bytes (default ${DEFAULT_MAX_BYTES}, minimum ${MIN_MAX_BYTES})`,
9294
9440
  " --help Show this help",
9295
9441
  " --version Show the version"
@@ -9357,6 +9503,28 @@ function parseServe(argv) {
9357
9503
  }
9358
9504
  return { kind: "serve-root", root, exclude, maxBytes: maxBytes ?? DEFAULT_MAX_BYTES };
9359
9505
  }
9506
+ function parseAdd(argv) {
9507
+ let folder = null;
9508
+ for (const token of argv) {
9509
+ if (token == "--help") {
9510
+ return { kind: "help" };
9511
+ }
9512
+ if (token == "--version") {
9513
+ return { kind: "version" };
9514
+ }
9515
+ if (token.startsWith("-") == true) {
9516
+ return { kind: "error", message: `Unknown argument "${token}".` };
9517
+ }
9518
+ if (token == "") {
9519
+ return { kind: "error", message: "The folder must not be empty." };
9520
+ }
9521
+ if (folder != null) {
9522
+ return { kind: "error", message: "librian add takes one folder." };
9523
+ }
9524
+ folder = token;
9525
+ }
9526
+ return { kind: "add", folder };
9527
+ }
9360
9528
  function parseArguments(argv) {
9361
9529
  if (argv.length == 0) {
9362
9530
  return { kind: "menu" };
@@ -9368,6 +9536,9 @@ function parseArguments(argv) {
9368
9536
  if (command == "--version") {
9369
9537
  return { kind: "version" };
9370
9538
  }
9539
+ if (command == "add") {
9540
+ return parseAdd(argv.slice(1));
9541
+ }
9371
9542
  if (command == "serve") {
9372
9543
  return parseServe(argv.slice(1));
9373
9544
  }
@@ -13562,7 +13733,7 @@ __export(core_exports2, {
13562
13733
  memoizer: () => memoizer,
13563
13734
  mergeValues: () => mergeValues2,
13564
13735
  meta: () => meta,
13565
- parse: () => parse,
13736
+ parse: () => parse2,
13566
13737
  parseAsync: () => parseAsync,
13567
13738
  parseURLObject: () => parseURLObject,
13568
13739
  prettifyError: () => prettifyError,
@@ -14782,7 +14953,7 @@ var _parse = (_Err) => {
14782
14953
  };
14783
14954
  return fn;
14784
14955
  };
14785
- var parse = /* @__PURE__ */ _parse($ZodRealError);
14956
+ var parse2 = /* @__PURE__ */ _parse($ZodRealError);
14786
14957
  var _parseAsync = (_Err) => {
14787
14958
  const fn = async (schema, value, _ctx, params) => {
14788
14959
  const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
@@ -14870,18 +15041,18 @@ var validateAsync = async (schema, value, _ctx) => {
14870
15041
  return result.issues.length === 0;
14871
15042
  };
14872
15043
  var _encode = (_Err) => {
14873
- const parse3 = _parse(_Err);
15044
+ const parse4 = _parse(_Err);
14874
15045
  const fn = (schema, value, _ctx, _params) => {
14875
15046
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
14876
- return parse3(schema, value, ctx, finalizeParams(fn, _params));
15047
+ return parse4(schema, value, ctx, finalizeParams(fn, _params));
14877
15048
  };
14878
15049
  return fn;
14879
15050
  };
14880
15051
  var encode = /* @__PURE__ */ _encode($ZodRealError);
14881
15052
  var _decode = (_Err) => {
14882
- const parse3 = _parse(_Err);
15053
+ const parse4 = _parse(_Err);
14883
15054
  const fn = (schema, value, _ctx, _params) => {
14884
- return parse3(schema, value, _ctx, finalizeParams(fn, _params));
15055
+ return parse4(schema, value, _ctx, finalizeParams(fn, _params));
14885
15056
  };
14886
15057
  return fn;
14887
15058
  };
@@ -16376,7 +16547,7 @@ function capSize(inst, size) {
16376
16547
  if (!caps.length)
16377
16548
  return;
16378
16549
  const cap = Math.min(...caps.map((c) => c[0]));
16379
- const parse3 = inst._zod.parse;
16550
+ const parse4 = inst._zod.parse;
16380
16551
  inst._zod.parse = (payload, ctx) => {
16381
16552
  const n = size(payload.value);
16382
16553
  if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
@@ -16390,7 +16561,7 @@ function capSize(inst, size) {
16390
16561
  }
16391
16562
  }
16392
16563
  }
16393
- return parse3(payload, ctx);
16564
+ return parse4(payload, ctx);
16394
16565
  };
16395
16566
  }
16396
16567
  var arraySize = (input2) => Array.isArray(input2) ? input2.length : -1;
@@ -17935,10 +18106,10 @@ var $ZodFunction = /* @__PURE__ */ $constructor("$ZodFunction", (inst, def) => {
17935
18106
  throw new Error("implement() must be called with a function");
17936
18107
  }
17937
18108
  return Object.defineProperty(function(...args) {
17938
- const parsedArgs = inst._def.input ? parse(inst._def.input, args) : args;
18109
+ const parsedArgs = inst._def.input ? parse2(inst._def.input, args) : args;
17939
18110
  const result = Reflect.apply(func, this, parsedArgs);
17940
18111
  if (inst._def.output) {
17941
- return parse(inst._def.output, result);
18112
+ return parse2(inst._def.output, result);
17942
18113
  }
17943
18114
  return result;
17944
18115
  }, "_zod", { value: inst._zod, enumerable: false });
@@ -18079,7 +18250,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
18079
18250
  var NONE = 0;
18080
18251
  var ASSUMED = 1;
18081
18252
  var PROVEN = 2;
18082
- function isRecursive(inst, stack, resolve7) {
18253
+ function isRecursive(inst, stack, resolve6) {
18083
18254
  const cached2 = recursive.get(inst);
18084
18255
  if (cached2 !== void 0)
18085
18256
  return cached2 ? PROVEN : NONE;
@@ -18089,7 +18260,7 @@ function isRecursive(inst, stack, resolve7) {
18089
18260
  let result = NONE;
18090
18261
  const check2 = (child) => {
18091
18262
  if (result !== PROVEN && child?._zod) {
18092
- const answer = isRecursive(child, stack, resolve7);
18263
+ const answer = isRecursive(child, stack, resolve6);
18093
18264
  if (answer > result)
18094
18265
  result = answer;
18095
18266
  }
@@ -18100,7 +18271,7 @@ function isRecursive(inst, stack, resolve7) {
18100
18271
  const desc = Object.getOwnPropertyDescriptor(sh, key);
18101
18272
  if (spread && !desc.enumerable)
18102
18273
  continue;
18103
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve7) : NONE;
18274
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
18104
18275
  if (child > answer)
18105
18276
  answer = child;
18106
18277
  }
@@ -18164,7 +18335,7 @@ function isRecursive(inst, stack, resolve7) {
18164
18335
  break;
18165
18336
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
18166
18337
  case "lazy": {
18167
- const inner = def._cachedInner ?? (resolve7 ? inst._zod.innerType : void 0);
18338
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
18168
18339
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
18169
18340
  break;
18170
18341
  }
@@ -29998,7 +30169,7 @@ var ZodMiniType = /* @__PURE__ */ $constructor("ZodMiniType", (inst, def) => {
29998
30169
  own(this, "with", value);
29999
30170
  },
30000
30171
  parse(data, params) {
30001
- return parse(this, data, params, { callee: this.parse });
30172
+ return parse2(this, data, params, { callee: this.parse });
30002
30173
  },
30003
30174
  parseAsync(data, params) {
30004
30175
  return parseAsync(this, data, params, { callee: this.parseAsync });
@@ -30561,7 +30732,7 @@ __export(external_exports, {
30561
30732
  optional: () => optional,
30562
30733
  output: () => output,
30563
30734
  overwrite: () => _overwrite,
30564
- parse: () => parse2,
30735
+ parse: () => parse3,
30565
30736
  parseAsync: () => parseAsync2,
30566
30737
  partialRecord: () => partialRecord,
30567
30738
  pipe: () => pipe,
@@ -30889,7 +31060,7 @@ var ZodRealError = /* @__PURE__ */ $constructor("ZodError", initializer2, void 0
30889
31060
  });
30890
31061
 
30891
31062
  // node_modules/zod/v4/classic/parse.js
30892
- var parse2 = /* @__PURE__ */ _parse(ZodRealError);
31063
+ var parse3 = /* @__PURE__ */ _parse(ZodRealError);
30893
31064
  var parseAsync2 = /* @__PURE__ */ _parseAsync(ZodRealError);
30894
31065
  var safeParse3 = /* @__PURE__ */ _safeParse(ZodRealError);
30895
31066
  var safeParseAsync3 = /* @__PURE__ */ _safeParseAsync(ZodRealError);
@@ -31026,7 +31197,7 @@ var ZodType2 = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
31026
31197
  util_exports.own(this, "~standard", value);
31027
31198
  },
31028
31199
  parse: function _parse2(data, params) {
31029
- return parse2(this, data, params, { callee: _parse2 });
31200
+ return parse3(this, data, params, { callee: _parse2 });
31030
31201
  },
31031
31202
  parseAsync: async function _parseAsync2(data, params) {
31032
31203
  return await parseAsync2(this, data, params, { callee: _parseAsync2 });
@@ -36602,7 +36773,7 @@ var Protocol = class {
36602
36773
  return;
36603
36774
  }
36604
36775
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
36605
- await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
36776
+ await new Promise((resolve6) => setTimeout(resolve6, pollInterval));
36606
36777
  options?.signal?.throwIfAborted();
36607
36778
  }
36608
36779
  } catch (error62) {
@@ -36619,7 +36790,7 @@ var Protocol = class {
36619
36790
  */
36620
36791
  request(request, resultSchema, options) {
36621
36792
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
36622
- return new Promise((resolve7, reject) => {
36793
+ return new Promise((resolve6, reject) => {
36623
36794
  const earlyReject = (error62) => {
36624
36795
  reject(error62);
36625
36796
  };
@@ -36697,7 +36868,7 @@ var Protocol = class {
36697
36868
  if (!parseResult.success) {
36698
36869
  reject(parseResult.error);
36699
36870
  } else {
36700
- resolve7(parseResult.data);
36871
+ resolve6(parseResult.data);
36701
36872
  }
36702
36873
  } catch (error62) {
36703
36874
  reject(error62);
@@ -36959,12 +37130,12 @@ var Protocol = class {
36959
37130
  }
36960
37131
  } catch {
36961
37132
  }
36962
- return new Promise((resolve7, reject) => {
37133
+ return new Promise((resolve6, reject) => {
36963
37134
  if (signal.aborted) {
36964
37135
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
36965
37136
  return;
36966
37137
  }
36967
- const timeoutId = setTimeout(resolve7, interval);
37138
+ const timeoutId = setTimeout(resolve6, interval);
36968
37139
  signal.addEventListener("abort", () => {
36969
37140
  clearTimeout(timeoutId);
36970
37141
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -38104,7 +38275,7 @@ var McpServer = class {
38104
38275
  let task = createTaskResult.task;
38105
38276
  const pollInterval = task.pollInterval ?? 5e3;
38106
38277
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
38107
- await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
38278
+ await new Promise((resolve6) => setTimeout(resolve6, pollInterval));
38108
38279
  const updatedTask = await extra.taskStore.getTask(taskId);
38109
38280
  if (!updatedTask) {
38110
38281
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -38768,12 +38939,12 @@ var StdioServerTransport = class {
38768
38939
  this.onclose?.();
38769
38940
  }
38770
38941
  send(message) {
38771
- return new Promise((resolve7) => {
38942
+ return new Promise((resolve6) => {
38772
38943
  const json2 = serializeMessage(message);
38773
38944
  if (this._stdout.write(json2)) {
38774
- resolve7();
38945
+ resolve6();
38775
38946
  } else {
38776
- this._stdout.once("drain", resolve7);
38947
+ this._stdout.once("drain", resolve6);
38777
38948
  }
38778
38949
  });
38779
38950
  }
@@ -38873,7 +39044,7 @@ function normalizePrefix(prefix) {
38873
39044
  }
38874
39045
  function runList(library, args, maxBytes) {
38875
39046
  if (args.prefix != null && typeof args.prefix != "string") {
38876
- return errorResult('prefix must be a string: the start of a path such as "ks/ci".');
39047
+ return errorResult('prefix must be a string: the start of a path such as "guide/api".');
38877
39048
  }
38878
39049
  let prefix = "";
38879
39050
  if (typeof args.prefix == "string") {
@@ -39337,6 +39508,8 @@ var ELLIPSIS2 = "\u2026";
39337
39508
  var ELLIPSIS_BYTES = Buffer.byteLength(ELLIPSIS2);
39338
39509
  var QUERY_ECHO_MAX_CHARS = 80;
39339
39510
  var WORD_CHARACTERS = /[a-z0-9_]/;
39511
+ var BM25_K1 = 1.2;
39512
+ var BM25_B = 0.75;
39340
39513
  function normalize(text2) {
39341
39514
  return text2.normalize("NFC").toLowerCase();
39342
39515
  }
@@ -39434,14 +39607,18 @@ function matchBlock(document, start, end, chainText, label, bodyStart, terms) {
39434
39607
  }
39435
39608
  hits += counts[termIndex];
39436
39609
  }
39437
- return { document, start, end, label, bytes: document.bytesOf(start, end), hits, titleHits, bodyStart };
39610
+ return { document, start, end, label, bytes: document.bytesOf(start, end), termCounts: counts, hits, titleHits, score: 0, bodyStart };
39438
39611
  }
39439
39612
  function collectMatches(documents, terms) {
39440
39613
  const matches = [];
39614
+ let scannedBytes = 0;
39615
+ let scannedBlocks = 0;
39441
39616
  for (const document of documents) {
39442
39617
  const headings = document.headings;
39443
39618
  const introEnd = headings.length == 0 ? document.lines.length : headings[0].line - 1;
39444
39619
  if (introEnd >= 1 && isBlank(document, 1, introEnd) == false) {
39620
+ scannedBytes += document.bytesOf(1, introEnd);
39621
+ scannedBlocks += 1;
39445
39622
  const introMatch = matchBlock(document, 1, introEnd, "", INTRO_TITLE, 1, terms);
39446
39623
  if (introMatch != null) {
39447
39624
  matches.push(introMatch);
@@ -39449,6 +39626,8 @@ function collectMatches(documents, terms) {
39449
39626
  }
39450
39627
  for (let index = 0; index < headings.length; index += 1) {
39451
39628
  const heading = headings[index];
39629
+ scannedBytes += document.bytesOf(heading.line, heading.blockEnd);
39630
+ scannedBlocks += 1;
39452
39631
  const chainText = chainIndices(document, index).map((position) => document.headings[position].title).join("\n");
39453
39632
  const label = headingChain(document, index);
39454
39633
  const match = matchBlock(document, heading.line, heading.blockEnd, chainText, label, heading.line + 1, terms);
@@ -39457,14 +39636,22 @@ function collectMatches(documents, terms) {
39457
39636
  }
39458
39637
  }
39459
39638
  }
39460
- return matches;
39639
+ return { matches, averageBytes: scannedBlocks == 0 ? 1 : scannedBytes / scannedBlocks };
39640
+ }
39641
+ function scoreMatch(match, averageBytes) {
39642
+ const lengthFactor = 1 - BM25_B + BM25_B * (match.bytes / averageBytes);
39643
+ let score = 0;
39644
+ for (const termCount of match.termCounts) {
39645
+ score += termCount * (BM25_K1 + 1) / (termCount + BM25_K1 * lengthFactor);
39646
+ }
39647
+ return score;
39461
39648
  }
39462
39649
  function compareMatches(left, right) {
39463
39650
  if (left.titleHits != right.titleHits) {
39464
39651
  return right.titleHits - left.titleHits;
39465
39652
  }
39466
- if (left.hits != right.hits) {
39467
- return right.hits - left.hits;
39653
+ if (left.score != right.score) {
39654
+ return right.score - left.score;
39468
39655
  }
39469
39656
  const byPath = compareText(left.document.path, right.document.path);
39470
39657
  if (byPath != 0) {
@@ -39554,7 +39741,10 @@ function runSearch(library, args, maxBytes) {
39554
39741
  return errorResult("Query is empty. Give one or more words; wrap a phrase in double quotes.");
39555
39742
  }
39556
39743
  const documents = library.refresh().filter((document) => document.path.startsWith(prefix));
39557
- const matches = collectMatches(documents, terms);
39744
+ const { matches, averageBytes } = collectMatches(documents, terms);
39745
+ for (const match of matches) {
39746
+ match.score = scoreMatch(match, averageBytes);
39747
+ }
39558
39748
  matches.sort(compareMatches);
39559
39749
  const fileCount = new Set(matches.map((match) => match.document.path)).size;
39560
39750
  const echo = JSON.stringify(truncateChars(query.trim(), QUERY_ECHO_MAX_CHARS));
@@ -39630,9 +39820,9 @@ function describeTools(folder) {
39630
39820
  return [
39631
39821
  {
39632
39822
  name: nameOf("list"),
39633
- description: `${tag}List the markdown files under ${where} with size and document title, grouped by folder. Use prefix to open one folder: \`ks/ci\` matches \`ks/ci/\u2026\` and \`ks/ci-web-renewal.md\`, \`ks/ci/\` matches the folder only.`,
39823
+ 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.`,
39634
39824
  inputSchema: {
39635
- prefix: external_exports.string().optional().describe("Root-relative path prefix, e.g. `ks/ci/`. Omit to list everything.")
39825
+ prefix: external_exports.string().optional().describe("Root-relative path prefix, e.g. `guide/api/`. Omit to list everything.")
39636
39826
  },
39637
39827
  runner: runList
39638
39828
  },
@@ -39807,12 +39997,12 @@ function catalogReadyLine(version2, home, catalog, opened) {
39807
39997
 
39808
39998
  // src/modules/setup/runtime.ts
39809
39999
  import { existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync3, rmSync as rmSync2 } from "node:fs";
39810
- import { dirname, join as join4, resolve as resolve4 } from "node:path";
40000
+ import { dirname, join as join5, resolve as resolve4 } from "node:path";
39811
40001
 
39812
40002
  // src/modules/setup/commands.ts
39813
40003
  import { spawn } from "node:child_process";
39814
40004
  import { accessSync, constants, statSync as statSync4 } from "node:fs";
39815
- import { delimiter, join as join3 } from "node:path";
40005
+ import { delimiter, join as join4 } from "node:path";
39816
40006
  var DEFAULT_TIMEOUT_MS = 3e4;
39817
40007
  var WINDOWS_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
39818
40008
  function quotePart(part) {
@@ -39841,7 +40031,7 @@ function findOnPath(name, env = process.env, platform = process.platform) {
39841
40031
  }
39842
40032
  for (const directory of directories) {
39843
40033
  for (const candidate of candidates) {
39844
- const path = join3(directory, candidate);
40034
+ const path = join4(directory, candidate);
39845
40035
  try {
39846
40036
  if (statSync4(path).isFile() == false) {
39847
40037
  continue;
@@ -39950,17 +40140,17 @@ var PACKAGE_NAME = "librian";
39950
40140
  var INSTALL_TIMEOUT_MS = 18e4;
39951
40141
  var VIEW_TIMEOUT_MS = 3e4;
39952
40142
  function runtimeDirectory(home) {
39953
- return join4(home, "runtime");
40143
+ return join5(home, "runtime");
39954
40144
  }
39955
40145
  function packageDirectory(home) {
39956
- return join4(runtimeDirectory(home), "node_modules", PACKAGE_NAME);
40146
+ return join5(runtimeDirectory(home), "node_modules", PACKAGE_NAME);
39957
40147
  }
39958
40148
  function runtimeCliPath(home) {
39959
- return join4(packageDirectory(home), "dist", "cli.js");
40149
+ return join5(packageDirectory(home), "dist", "cli.js");
39960
40150
  }
39961
40151
  function readPackageVersion(directory) {
39962
40152
  try {
39963
- const parsed = JSON.parse(readFileSync3(join4(directory, "package.json"), "utf8"));
40153
+ const parsed = JSON.parse(readFileSync3(join5(directory, "package.json"), "utf8"));
39964
40154
  return typeof parsed.version == "string" ? parsed.version : null;
39965
40155
  } catch {
39966
40156
  return null;
@@ -39992,8 +40182,8 @@ function compareVersions(left, right) {
39992
40182
  function findNpmCli(nodePath) {
39993
40183
  const nodeDirectory = dirname(nodePath);
39994
40184
  const candidates = [
39995
- join4(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
39996
- join4(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
40185
+ join5(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
40186
+ join5(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
39997
40187
  ];
39998
40188
  for (const candidate of candidates) {
39999
40189
  if (existsSync2(candidate) == true) {
@@ -40039,24 +40229,6 @@ async function installRuntime(home, spec, toolchain) {
40039
40229
  }
40040
40230
  return { ok: true, message: status.state == "linked" ? `linked to ${status.linkedFrom}` : `${status.version ?? "?"} installed` };
40041
40231
  }
40042
- function checkCheckout(path) {
40043
- if (existsSync2(path) == false) {
40044
- return `${path} does not exist.`;
40045
- }
40046
- let name;
40047
- try {
40048
- name = JSON.parse(readFileSync3(join4(path, "package.json"), "utf8")).name;
40049
- } catch {
40050
- return `${path} has no readable package.json.`;
40051
- }
40052
- if (name != PACKAGE_NAME) {
40053
- return `${path} is not the ${PACKAGE_NAME} package.`;
40054
- }
40055
- if (existsSync2(join4(path, "dist", "cli.js")) == false) {
40056
- return `${path} has no dist/cli.js; run \`npm run build\` there first.`;
40057
- }
40058
- return null;
40059
- }
40060
40232
  async function latestPublishedVersion(toolchain) {
40061
40233
  const viewed = await runNpm(toolchain, ["view", PACKAGE_NAME, "version"], VIEW_TIMEOUT_MS);
40062
40234
  if (viewed.ok == false) {
@@ -40076,12 +40248,9 @@ function uninstallRuntime(home) {
40076
40248
  rmSync2(directory, { recursive: true, force: true });
40077
40249
  }
40078
40250
 
40079
- // src/modules/setup/runtime-screen.ts
40080
- import { resolve as resolve5 } from "node:path";
40081
-
40082
40251
  // src/modules/setup/services.ts
40083
40252
  import { homedir as homedir3 } from "node:os";
40084
- import { join as join5 } from "node:path";
40253
+ import { join as join6 } from "node:path";
40085
40254
 
40086
40255
  // src/modules/setup/probe.ts
40087
40256
  import { spawn as spawn2 } from "node:child_process";
@@ -40188,7 +40357,7 @@ function defaultServiceDependencies() {
40188
40357
  return { ...defaultToolchain(), probe: probeServer };
40189
40358
  }
40190
40359
  function isDefaultHome(home) {
40191
- return normalizeForCompare(home) == normalizeForCompare(join5(homedir3(), ".librian"));
40360
+ return normalizeForCompare(home) == normalizeForCompare(join6(homedir3(), ".librian"));
40192
40361
  }
40193
40362
  function expectedRegistration(home, toolchain) {
40194
40363
  const environment = {};
@@ -40403,7 +40572,6 @@ function buildRows2(status, latest, checked) {
40403
40572
  rows.push({ value: "check", key: "check", label: "Check for updates", hint: checked == true ? "up to date" : void 0 });
40404
40573
  }
40405
40574
  }
40406
- rows.push({ value: "link", key: "link", label: "Use a local checkout", hint: "link a built librian folder" });
40407
40575
  if (status.state != "missing") {
40408
40576
  rows.push({ value: "uninstall", key: "uninstall", label: "Uninstall" });
40409
40577
  }
@@ -40497,25 +40665,6 @@ async function openRuntimeScreen(home, dependencies = defaultServiceDependencies
40497
40665
  checked = false;
40498
40666
  latest = null;
40499
40667
  await checkRegisteredServer(home, dependencies);
40500
- } else if (choice == "link") {
40501
- let answer;
40502
- try {
40503
- answer = await text("Folder of the built librian checkout", { initial: status.linkedFrom, transient: true });
40504
- } catch (error62) {
40505
- if (error62 instanceof PromptBackError == false) {
40506
- throw error62;
40507
- }
40508
- continue;
40509
- }
40510
- const checkout = resolve5(answer.trim().replace(/^"(.*)"$/, "$1"));
40511
- const problem = checkCheckout(checkout);
40512
- if (problem != null) {
40513
- writeLine(styled(problem, "red"));
40514
- continue;
40515
- }
40516
- writeLine(styled("Linking the checkout\u2026", "dim"));
40517
- printResult(await installRuntime(home, checkout, dependencies), "link");
40518
- await checkRegisteredServer(home, dependencies);
40519
40668
  } else if (choice == "uninstall") {
40520
40669
  await uninstall(home, dependencies);
40521
40670
  }
@@ -40592,7 +40741,7 @@ async function openServicesScreen(home, dependencies = defaultServiceDependencie
40592
40741
  const needsRuntime = runtimeStatus(home).state == "missing";
40593
40742
  const registersSomething = planServiceChanges(statuses, wanted, home, dependencies).some((change) => change.action == "register");
40594
40743
  const blockedApply = needsRuntime == true && registersSomething == true ? "install the runtime first" : null;
40595
- const choice = await select("AI services", buildRows3(statuses, wanted, blockedApply), { initial: cursor, transient: true });
40744
+ const choice = await select("Register", buildRows3(statuses, wanted, blockedApply), { initial: cursor, transient: true });
40596
40745
  cursor = choice;
40597
40746
  if (choice == "back") {
40598
40747
  return;
@@ -40641,7 +40790,7 @@ async function openServicesScreen(home, dependencies = defaultServiceDependencie
40641
40790
  }
40642
40791
 
40643
40792
  // src/cli.ts
40644
- var EXIT_USAGE = 2;
40793
+ var EXIT_USAGE2 = 2;
40645
40794
  async function main() {
40646
40795
  const parsed = parseArguments(process.argv.slice(2));
40647
40796
  if (parsed.kind == "help") {
@@ -40657,7 +40806,16 @@ async function main() {
40657
40806
  if (parsed.kind == "error") {
40658
40807
  writeStderrLine(`librian: ${parsed.message}`);
40659
40808
  writeStderrLine(USAGE);
40660
- process.exitCode = EXIT_USAGE;
40809
+ process.exitCode = EXIT_USAGE2;
40810
+ return;
40811
+ }
40812
+ if (parsed.kind == "add") {
40813
+ process.exitCode = await runAddCommand({
40814
+ home: librianHome(),
40815
+ folder: parsed.folder,
40816
+ cwd: process.cwd(),
40817
+ writeError: (line) => writeStderrLine(`librian: ${line}`)
40818
+ });
40661
40819
  return;
40662
40820
  }
40663
40821
  if (parsed.kind == "menu") {
@@ -40693,7 +40851,7 @@ async function main() {
40693
40851
  writeStderrLine(catalogReadyLine(package_default.version, home, catalog, opened));
40694
40852
  return;
40695
40853
  }
40696
- const root = resolve6(parsed.root);
40854
+ const root = resolve5(parsed.root);
40697
40855
  let isFolder3 = false;
40698
40856
  try {
40699
40857
  isFolder3 = statSync5(root).isDirectory();
@@ -40702,7 +40860,7 @@ async function main() {
40702
40860
  }
40703
40861
  if (isFolder3 == false) {
40704
40862
  writeStderrLine(`librian: root "${root}" does not exist or is not a folder.`);
40705
- process.exitCode = EXIT_USAGE;
40863
+ process.exitCode = EXIT_USAGE2;
40706
40864
  return;
40707
40865
  }
40708
40866
  const library = openLibrary({ root, exclude: parsed.exclude });