librian 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.1.1] - 2026-11-08
8
+
9
+ ### Added
10
+
11
+ - **`librian add [<folder>]`.** Adds a docs folder without opening the menu:
12
+ run it inside the docs folder, or pass the path. It counts the markdown
13
+ files, asks `Add it as "akms_docs"? (Y/n)` and adds the entry. It refuses
14
+ your home folder and a drive root (exit code 2), says `Already added as …`
15
+ (exit code 0) for a folder that is already there, and adds nothing when the
16
+ input ends.
17
+ - **The folder question starts with the current folder.** Start `librian`
18
+ inside a docs folder and Enter is enough to add it. In your home folder or a
19
+ drive root the question starts empty.
20
+
21
+ ### Changed
22
+
23
+ - **Add asks for the folder only.** The Add form with its Name, Exclude and
24
+ Max bytes rows is gone: the folder is checked (a missing folder is explained
25
+ and asked again; a folder already added takes you to its entry; a folder
26
+ without markdown files defaults to No), then one question adds it with the
27
+ default name. Name, Exclude and Max bytes are changed with **Edit** on the
28
+ entry, which still opens the form.
29
+ - **Counting files is bounded.** Add, `librian add`, Check and the file counts
30
+ in the main menu stop at 5,000 files or after 2 seconds and show
31
+ `5000+ files`, so choosing a huge folder does not hang.
32
+ - **The menu's AI services screen is now called Register.** Its rows and
33
+ actions are the same: switch Claude Code or Codex on or off, then Apply.
34
+
35
+ ### Removed
36
+
37
+ - **Runtime → Use a local checkout.** It was for running an unpublished build.
38
+ Install a built checkout with
39
+ `npm install --prefix ~/.librian/runtime <path>` instead; the menu still
40
+ shows it as `linked to <path>` and **Install from npm** switches back.
41
+
7
42
  ## [0.1.0] - 2026-11-08
8
43
 
9
44
  First release.
package/README.md CHANGED
@@ -22,11 +22,11 @@ npx -y librian@latest # or: npm i -g librian, then run `librian`
22
22
  This opens the librian menu. Then:
23
23
 
24
24
  1. **Runtime → Install**: installs librian into `~/.librian/runtime`.
25
- 2. **Add docs folder**: give it a name and the path of a docs folder (see below). Repeat for more folders.
26
- 3. **AI services**: switch on Claude Code and/or Codex, then **Apply**. librian shows the commands it will run and asks before running them.
25
+ 2. **Add docs folder**: type the path of a docs folder. If you started `librian` inside it, the question is already filled in with the current folder, so Enter is enough. Repeat for more folders. Or skip the menu: run `librian add` inside the docs folder (see below).
26
+ 3. **Register**: switch on Claude Code and/or Codex, then **Apply**. librian shows the commands it will run and asks before running them.
27
27
  4. Open a new session in Claude Code or Codex. Sessions that are already open keep what they started with.
28
28
 
29
- To update, open `librian` → **Runtime → Check for updates**. To undo a registration, switch the service off in **AI services** and **Apply**.
29
+ To update, open `librian` → **Runtime → Check for updates**. To undo a registration, switch the service off in **Register** and **Apply**.
30
30
 
31
31
  ## The menu
32
32
 
@@ -37,6 +37,18 @@ Run `librian` in a terminal. Without a terminal (a pipe or a script) it prints t
37
37
  The main screen lists your docs folders; each one is an entry with a name and a path, like a connection in a database tool.
38
38
  Choose an entry to **Edit**, **Check** (file count, total size, largest files) or **Remove** it. Removing an entry never touches the folder itself.
39
39
 
40
+ **Add docs folder** asks for one thing, the folder, and then adds it:
41
+
42
+ ```
43
+ ? Docs folder › D:\workspace\projects\akms\akms.docs
44
+ 107 markdown files · 4.1M
45
+ ? Add it as "akms_docs"? (Y/n)
46
+ ✔ Added akms_docs. Open a new session to use akms_docs__read and the other tools.
47
+ ```
48
+
49
+ A folder that does not exist is refused with the reason and asked again with what you typed. A folder that is already added says `Already added as "akms".` and takes you to that entry. A folder without markdown files gets a warning and the question defaults to No. Esc saves nothing. The entry gets the default name, no Exclude and the default Max bytes; change them with **Edit** on the entry.
50
+ Counting stops at 5,000 files or after 2 seconds and shows `5000+ files`, so picking a huge folder by mistake does not hang the menu.
51
+
40
52
  An entry has:
41
53
 
42
54
  | Field | Meaning |
@@ -50,7 +62,18 @@ Symbolic links and junctions are not followed. librian only reads your docs; it
50
62
 
51
63
  Adding, editing or removing an entry takes effect in new sessions only.
52
64
 
53
- ### AI services
65
+ #### `librian add`
66
+
67
+ ```
68
+ cd path/to/your/docs
69
+ librian add # adds the current folder
70
+ librian add path/to/docs # or a folder you name
71
+ ```
72
+
73
+ The same flow without the menu: it prints the file count, asks `Add it as "…"? (Y/n)` and adds the entry. Without a terminal it reads the answer from a line of input, and if the input ends it adds nothing and exits 0.
74
+ It refuses your home folder and a drive root (exit code 2): run it inside the docs folder or pass the path. A folder that is already added exits 0. When no AI service is registered yet, the last line points you to **Register**.
75
+
76
+ ### Register
54
77
 
55
78
  Registers librian as one MCP server named `librian` in **Claude Code** and **Codex**, for all your projects (user scope).
56
79
  It runs `claude mcp add --scope user …` and `codex mcp add …` for you, and `… mcp remove …` to unregister.
@@ -148,7 +171,7 @@ librian --help | --version
148
171
 
149
172
  ## Contributing
150
173
 
151
- To run a checkout of this repository instead of the published package, build it (`npm run build`) and use **Runtime → Use a local checkout** in the menu.
174
+ To run a checkout of this repository instead of the published package, build it (`npm run build`) and install it into the runtime: `npm install --prefix ~/.librian/runtime <path to the checkout>`. The menu then shows the runtime as `linked to <path>`, and **Runtime → Install from npm** switches back.
152
175
 
153
176
  ## License
154
177
 
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.1",
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);
7755
+ }
7756
+ let name = basename(root);
7757
+ if (name == "") {
7758
+ name = root;
7739
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",
@@ -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
  }
@@ -39807,12 +39978,12 @@ function catalogReadyLine(version2, home, catalog, opened) {
39807
39978
 
39808
39979
  // src/modules/setup/runtime.ts
39809
39980
  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";
39981
+ import { dirname, join as join5, resolve as resolve4 } from "node:path";
39811
39982
 
39812
39983
  // src/modules/setup/commands.ts
39813
39984
  import { spawn } from "node:child_process";
39814
39985
  import { accessSync, constants, statSync as statSync4 } from "node:fs";
39815
- import { delimiter, join as join3 } from "node:path";
39986
+ import { delimiter, join as join4 } from "node:path";
39816
39987
  var DEFAULT_TIMEOUT_MS = 3e4;
39817
39988
  var WINDOWS_SCRIPT_PATTERN = /\.(cmd|bat)$/i;
39818
39989
  function quotePart(part) {
@@ -39841,7 +40012,7 @@ function findOnPath(name, env = process.env, platform = process.platform) {
39841
40012
  }
39842
40013
  for (const directory of directories) {
39843
40014
  for (const candidate of candidates) {
39844
- const path = join3(directory, candidate);
40015
+ const path = join4(directory, candidate);
39845
40016
  try {
39846
40017
  if (statSync4(path).isFile() == false) {
39847
40018
  continue;
@@ -39950,17 +40121,17 @@ var PACKAGE_NAME = "librian";
39950
40121
  var INSTALL_TIMEOUT_MS = 18e4;
39951
40122
  var VIEW_TIMEOUT_MS = 3e4;
39952
40123
  function runtimeDirectory(home) {
39953
- return join4(home, "runtime");
40124
+ return join5(home, "runtime");
39954
40125
  }
39955
40126
  function packageDirectory(home) {
39956
- return join4(runtimeDirectory(home), "node_modules", PACKAGE_NAME);
40127
+ return join5(runtimeDirectory(home), "node_modules", PACKAGE_NAME);
39957
40128
  }
39958
40129
  function runtimeCliPath(home) {
39959
- return join4(packageDirectory(home), "dist", "cli.js");
40130
+ return join5(packageDirectory(home), "dist", "cli.js");
39960
40131
  }
39961
40132
  function readPackageVersion(directory) {
39962
40133
  try {
39963
- const parsed = JSON.parse(readFileSync3(join4(directory, "package.json"), "utf8"));
40134
+ const parsed = JSON.parse(readFileSync3(join5(directory, "package.json"), "utf8"));
39964
40135
  return typeof parsed.version == "string" ? parsed.version : null;
39965
40136
  } catch {
39966
40137
  return null;
@@ -39992,8 +40163,8 @@ function compareVersions(left, right) {
39992
40163
  function findNpmCli(nodePath) {
39993
40164
  const nodeDirectory = dirname(nodePath);
39994
40165
  const candidates = [
39995
- join4(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
39996
- join4(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
40166
+ join5(nodeDirectory, "node_modules", "npm", "bin", "npm-cli.js"),
40167
+ join5(nodeDirectory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
39997
40168
  ];
39998
40169
  for (const candidate of candidates) {
39999
40170
  if (existsSync2(candidate) == true) {
@@ -40039,24 +40210,6 @@ async function installRuntime(home, spec, toolchain) {
40039
40210
  }
40040
40211
  return { ok: true, message: status.state == "linked" ? `linked to ${status.linkedFrom}` : `${status.version ?? "?"} installed` };
40041
40212
  }
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
40213
  async function latestPublishedVersion(toolchain) {
40061
40214
  const viewed = await runNpm(toolchain, ["view", PACKAGE_NAME, "version"], VIEW_TIMEOUT_MS);
40062
40215
  if (viewed.ok == false) {
@@ -40076,12 +40229,9 @@ function uninstallRuntime(home) {
40076
40229
  rmSync2(directory, { recursive: true, force: true });
40077
40230
  }
40078
40231
 
40079
- // src/modules/setup/runtime-screen.ts
40080
- import { resolve as resolve5 } from "node:path";
40081
-
40082
40232
  // src/modules/setup/services.ts
40083
40233
  import { homedir as homedir3 } from "node:os";
40084
- import { join as join5 } from "node:path";
40234
+ import { join as join6 } from "node:path";
40085
40235
 
40086
40236
  // src/modules/setup/probe.ts
40087
40237
  import { spawn as spawn2 } from "node:child_process";
@@ -40188,7 +40338,7 @@ function defaultServiceDependencies() {
40188
40338
  return { ...defaultToolchain(), probe: probeServer };
40189
40339
  }
40190
40340
  function isDefaultHome(home) {
40191
- return normalizeForCompare(home) == normalizeForCompare(join5(homedir3(), ".librian"));
40341
+ return normalizeForCompare(home) == normalizeForCompare(join6(homedir3(), ".librian"));
40192
40342
  }
40193
40343
  function expectedRegistration(home, toolchain) {
40194
40344
  const environment = {};
@@ -40403,7 +40553,6 @@ function buildRows2(status, latest, checked) {
40403
40553
  rows.push({ value: "check", key: "check", label: "Check for updates", hint: checked == true ? "up to date" : void 0 });
40404
40554
  }
40405
40555
  }
40406
- rows.push({ value: "link", key: "link", label: "Use a local checkout", hint: "link a built librian folder" });
40407
40556
  if (status.state != "missing") {
40408
40557
  rows.push({ value: "uninstall", key: "uninstall", label: "Uninstall" });
40409
40558
  }
@@ -40497,25 +40646,6 @@ async function openRuntimeScreen(home, dependencies = defaultServiceDependencies
40497
40646
  checked = false;
40498
40647
  latest = null;
40499
40648
  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
40649
  } else if (choice == "uninstall") {
40520
40650
  await uninstall(home, dependencies);
40521
40651
  }
@@ -40592,7 +40722,7 @@ async function openServicesScreen(home, dependencies = defaultServiceDependencie
40592
40722
  const needsRuntime = runtimeStatus(home).state == "missing";
40593
40723
  const registersSomething = planServiceChanges(statuses, wanted, home, dependencies).some((change) => change.action == "register");
40594
40724
  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 });
40725
+ const choice = await select("Register", buildRows3(statuses, wanted, blockedApply), { initial: cursor, transient: true });
40596
40726
  cursor = choice;
40597
40727
  if (choice == "back") {
40598
40728
  return;
@@ -40641,7 +40771,7 @@ async function openServicesScreen(home, dependencies = defaultServiceDependencie
40641
40771
  }
40642
40772
 
40643
40773
  // src/cli.ts
40644
- var EXIT_USAGE = 2;
40774
+ var EXIT_USAGE2 = 2;
40645
40775
  async function main() {
40646
40776
  const parsed = parseArguments(process.argv.slice(2));
40647
40777
  if (parsed.kind == "help") {
@@ -40657,7 +40787,16 @@ async function main() {
40657
40787
  if (parsed.kind == "error") {
40658
40788
  writeStderrLine(`librian: ${parsed.message}`);
40659
40789
  writeStderrLine(USAGE);
40660
- process.exitCode = EXIT_USAGE;
40790
+ process.exitCode = EXIT_USAGE2;
40791
+ return;
40792
+ }
40793
+ if (parsed.kind == "add") {
40794
+ process.exitCode = await runAddCommand({
40795
+ home: librianHome(),
40796
+ folder: parsed.folder,
40797
+ cwd: process.cwd(),
40798
+ writeError: (line) => writeStderrLine(`librian: ${line}`)
40799
+ });
40661
40800
  return;
40662
40801
  }
40663
40802
  if (parsed.kind == "menu") {
@@ -40693,7 +40832,7 @@ async function main() {
40693
40832
  writeStderrLine(catalogReadyLine(package_default.version, home, catalog, opened));
40694
40833
  return;
40695
40834
  }
40696
- const root = resolve6(parsed.root);
40835
+ const root = resolve5(parsed.root);
40697
40836
  let isFolder3 = false;
40698
40837
  try {
40699
40838
  isFolder3 = statSync5(root).isDirectory();
@@ -40702,7 +40841,7 @@ async function main() {
40702
40841
  }
40703
40842
  if (isFolder3 == false) {
40704
40843
  writeStderrLine(`librian: root "${root}" does not exist or is not a folder.`);
40705
- process.exitCode = EXIT_USAGE;
40844
+ process.exitCode = EXIT_USAGE2;
40706
40845
  return;
40707
40846
  }
40708
40847
  const library = openLibrary({ root, exclude: parsed.exclude });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librian",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Find and read sections of a markdown docs folder over MCP, with outlines and line ranges that stay current as the docs change",
5
5
  "repository": {
6
6
  "type": "git",