@ncukondo/reference-manager 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +157 -13
  2. package/bin/reference-manager.js +0 -0
  3. package/dist/chunks/{action-menu-DNlpGiwS.js → action-menu-DvwR6nMj.js} +3 -3
  4. package/dist/chunks/{action-menu-DNlpGiwS.js.map → action-menu-DvwR6nMj.js.map} +1 -1
  5. package/dist/chunks/{file-watcher-D2Y-SlcE.js → file-watcher-B_WpVHSV.js} +18 -18
  6. package/dist/chunks/{file-watcher-D2Y-SlcE.js.map → file-watcher-B_WpVHSV.js.map} +1 -1
  7. package/dist/chunks/index-B_WCu-ZQ.js +10 -0
  8. package/dist/chunks/index-B_WCu-ZQ.js.map +1 -0
  9. package/dist/chunks/{index-UpzsmbyY.js → index-Bv5IgsL-.js} +2453 -497
  10. package/dist/chunks/index-Bv5IgsL-.js.map +1 -0
  11. package/dist/chunks/{index-4KSTJ3rp.js → index-DHgeuWGP.js} +122 -41
  12. package/dist/chunks/index-DHgeuWGP.js.map +1 -0
  13. package/dist/chunks/{loader-C1EpnyPm.js → loader-4FFB4igw.js} +66 -27
  14. package/dist/chunks/loader-4FFB4igw.js.map +1 -0
  15. package/dist/chunks/{reference-select-DSVwE9iu.js → reference-select-B9w9CLa1.js} +3 -3
  16. package/dist/chunks/{reference-select-DSVwE9iu.js.map → reference-select-B9w9CLa1.js.map} +1 -1
  17. package/dist/chunks/{style-select-CHjDTyq2.js → style-select-BNQHC79W.js} +2 -2
  18. package/dist/chunks/{style-select-CHjDTyq2.js.map → style-select-BNQHC79W.js.map} +1 -1
  19. package/dist/chunks/{tty-CDBIQraQ.js → tty-BMyaEOhX.js} +2 -2
  20. package/dist/chunks/tty-BMyaEOhX.js.map +1 -0
  21. package/dist/cli/commands/attach.d.ts +204 -0
  22. package/dist/cli/commands/attach.d.ts.map +1 -0
  23. package/dist/cli/commands/cite.d.ts +1 -1
  24. package/dist/cli/commands/config.d.ts +9 -0
  25. package/dist/cli/commands/config.d.ts.map +1 -0
  26. package/dist/cli/commands/export.d.ts +1 -1
  27. package/dist/cli/commands/fulltext.d.ts +2 -2
  28. package/dist/cli/commands/fulltext.d.ts.map +1 -1
  29. package/dist/cli/commands/list.d.ts +2 -1
  30. package/dist/cli/commands/list.d.ts.map +1 -1
  31. package/dist/cli/commands/search.d.ts +3 -2
  32. package/dist/cli/commands/search.d.ts.map +1 -1
  33. package/dist/cli/completion.d.ts +8 -0
  34. package/dist/cli/completion.d.ts.map +1 -1
  35. package/dist/cli/index.d.ts.map +1 -1
  36. package/dist/cli/server-client.d.ts +37 -1
  37. package/dist/cli/server-client.d.ts.map +1 -1
  38. package/dist/cli.js +2 -2
  39. package/dist/config/defaults.d.ts +7 -1
  40. package/dist/config/defaults.d.ts.map +1 -1
  41. package/dist/config/env-override.d.ts +36 -0
  42. package/dist/config/env-override.d.ts.map +1 -0
  43. package/dist/config/key-parser.d.ts +46 -0
  44. package/dist/config/key-parser.d.ts.map +1 -0
  45. package/dist/config/loader.d.ts.map +1 -1
  46. package/dist/config/schema.d.ts +22 -8
  47. package/dist/config/schema.d.ts.map +1 -1
  48. package/dist/config/toml-writer.d.ts +25 -0
  49. package/dist/config/toml-writer.d.ts.map +1 -0
  50. package/dist/config/value-validator.d.ts +21 -0
  51. package/dist/config/value-validator.d.ts.map +1 -0
  52. package/dist/features/attachments/directory-manager.d.ts +40 -0
  53. package/dist/features/attachments/directory-manager.d.ts.map +1 -0
  54. package/dist/features/attachments/directory.d.ts +36 -0
  55. package/dist/features/attachments/directory.d.ts.map +1 -0
  56. package/dist/features/attachments/filename.d.ts +30 -0
  57. package/dist/features/attachments/filename.d.ts.map +1 -0
  58. package/dist/features/attachments/types.d.ts +38 -0
  59. package/dist/features/attachments/types.d.ts.map +1 -0
  60. package/dist/features/config/edit.d.ts +38 -0
  61. package/dist/features/config/edit.d.ts.map +1 -0
  62. package/dist/features/config/get.d.ts +35 -0
  63. package/dist/features/config/get.d.ts.map +1 -0
  64. package/dist/features/config/list-keys.d.ts +16 -0
  65. package/dist/features/config/list-keys.d.ts.map +1 -0
  66. package/dist/features/config/path.d.ts +29 -0
  67. package/dist/features/config/path.d.ts.map +1 -0
  68. package/dist/features/config/set.d.ts +27 -0
  69. package/dist/features/config/set.d.ts.map +1 -0
  70. package/dist/features/config/show.d.ts +20 -0
  71. package/dist/features/config/show.d.ts.map +1 -0
  72. package/dist/features/config/unset.d.ts +17 -0
  73. package/dist/features/config/unset.d.ts.map +1 -0
  74. package/dist/features/config/write-target.d.ts +30 -0
  75. package/dist/features/config/write-target.d.ts.map +1 -0
  76. package/dist/features/fulltext/manager.d.ts +1 -1
  77. package/dist/features/fulltext/manager.d.ts.map +1 -1
  78. package/dist/features/import/importer.d.ts.map +1 -1
  79. package/dist/features/interactive/tty.d.ts +2 -2
  80. package/dist/features/operations/attachments/add.d.ts +42 -0
  81. package/dist/features/operations/attachments/add.d.ts.map +1 -0
  82. package/dist/features/operations/attachments/detach.d.ts +38 -0
  83. package/dist/features/operations/attachments/detach.d.ts.map +1 -0
  84. package/dist/features/operations/attachments/get.d.ts +35 -0
  85. package/dist/features/operations/attachments/get.d.ts.map +1 -0
  86. package/dist/features/operations/attachments/index.d.ts +16 -0
  87. package/dist/features/operations/attachments/index.d.ts.map +1 -0
  88. package/dist/features/operations/attachments/list.d.ts +32 -0
  89. package/dist/features/operations/attachments/list.d.ts.map +1 -0
  90. package/dist/features/operations/attachments/open.d.ts +39 -0
  91. package/dist/features/operations/attachments/open.d.ts.map +1 -0
  92. package/dist/features/operations/attachments/sync.d.ts +50 -0
  93. package/dist/features/operations/attachments/sync.d.ts.map +1 -0
  94. package/dist/features/operations/fulltext/attach.d.ts +8 -2
  95. package/dist/features/operations/fulltext/attach.d.ts.map +1 -1
  96. package/dist/features/operations/fulltext/detach.d.ts +9 -3
  97. package/dist/features/operations/fulltext/detach.d.ts.map +1 -1
  98. package/dist/features/operations/fulltext/get.d.ts +8 -2
  99. package/dist/features/operations/fulltext/get.d.ts.map +1 -1
  100. package/dist/features/operations/fulltext/open.d.ts +8 -2
  101. package/dist/features/operations/fulltext/open.d.ts.map +1 -1
  102. package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts +39 -0
  103. package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts.map +1 -0
  104. package/dist/features/operations/fulltext-adapter/index.d.ts +7 -0
  105. package/dist/features/operations/fulltext-adapter/index.d.ts.map +1 -0
  106. package/dist/features/operations/index.d.ts +1 -0
  107. package/dist/features/operations/index.d.ts.map +1 -1
  108. package/dist/features/operations/library-operations.d.ts +43 -0
  109. package/dist/features/operations/library-operations.d.ts.map +1 -1
  110. package/dist/features/operations/operations-library.d.ts +7 -0
  111. package/dist/features/operations/operations-library.d.ts.map +1 -1
  112. package/dist/features/operations/remove.d.ts +1 -0
  113. package/dist/features/operations/remove.d.ts.map +1 -1
  114. package/dist/index.js +15 -15
  115. package/dist/index.js.map +1 -1
  116. package/dist/server.js +3 -3
  117. package/dist/utils/opener.d.ts +6 -1
  118. package/dist/utils/opener.d.ts.map +1 -1
  119. package/dist/utils/path.d.ts +28 -0
  120. package/dist/utils/path.d.ts.map +1 -0
  121. package/package.json +2 -1
  122. package/dist/chunks/index-4KSTJ3rp.js.map +0 -1
  123. package/dist/chunks/index-UpzsmbyY.js.map +0 -1
  124. package/dist/chunks/loader-C1EpnyPm.js.map +0 -1
  125. package/dist/chunks/tty-CDBIQraQ.js.map +0 -1
@@ -1,22 +1,23 @@
1
1
  import { Command } from "commander";
2
- import { L as Library, p as pickDefined, q as sortOrderSchema, r as paginationOptionsSchema, F as FileWatcher, u as sortFieldSchema, v as searchSortFieldSchema } from "./file-watcher-D2Y-SlcE.js";
2
+ import { L as Library, p as pickDefined, a as sortOrderSchema, v as paginationOptionsSchema, F as FileWatcher, b as sortFieldSchema, u as searchSortFieldSchema } from "./file-watcher-B_WpVHSV.js";
3
3
  import * as fs from "node:fs";
4
- import { promises, readFileSync, existsSync, mkdtempSync, writeFileSync } from "node:fs";
4
+ import { promises, readFileSync, existsSync, writeFileSync, mkdirSync, mkdtempSync } from "node:fs";
5
5
  import * as os from "node:os";
6
6
  import { tmpdir } from "node:os";
7
7
  import * as path from "node:path";
8
- import { join, extname } from "node:path";
8
+ import path__default, { extname, join, basename, dirname } from "node:path";
9
+ import fs__default, { stat, rename, copyFile, readFile, unlink, readdir, mkdir, rm } from "node:fs/promises";
10
+ import { g as getExtension, i as isValidFulltextFiles, a as isReservedRole, F as FULLTEXT_ROLE, b as formatToExtension, c as findFulltextFiles, d as findFulltextFile, e as extensionToFormat, B as BUILTIN_STYLES, h as getFulltextAttachmentTypes, s as startServerWithFileWatcher } from "./index-DHgeuWGP.js";
11
+ import { o as openWithSystemApp, l as loadConfig, e as getDefaultCurrentDirConfigFilename, h as getDefaultUserConfigPath } from "./loader-4FFB4igw.js";
9
12
  import { spawn, spawnSync } from "node:child_process";
10
13
  import process$1, { stdin, stdout } from "node:process";
11
- import { l as loadConfig, o as openWithSystemApp } from "./loader-C1EpnyPm.js";
12
- import { mkdir, unlink, rename, copyFile, rm, readFile } from "node:fs/promises";
13
- import { u as updateReference, B as BUILTIN_STYLES, g as getFulltextAttachmentTypes, s as startServerWithFileWatcher } from "./index-4KSTJ3rp.js";
14
+ import { parse as parse$2, stringify as stringify$2 } from "@iarna/toml";
14
15
  import "@citation-js/core";
15
16
  import "@citation-js/plugin-csl";
16
17
  import { ZodOptional as ZodOptional$2, z } from "zod";
17
18
  import { serve } from "@hono/node-server";
18
19
  const name = "@ncukondo/reference-manager";
19
- const version$1 = "0.14.0";
20
+ const version$1 = "0.15.0";
20
21
  const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
21
22
  const packageJson = {
22
23
  name,
@@ -260,6 +261,598 @@ function getExitCode(result) {
260
261
  }
261
262
  return 0;
262
263
  }
264
+ function normalizePathForOutput(p) {
265
+ return p.replace(/\\/g, "/");
266
+ }
267
+ function extractUuidPrefix(uuid2) {
268
+ const normalized = uuid2.replace(/-/g, "");
269
+ return normalized.slice(0, 8);
270
+ }
271
+ function generateDirectoryName(ref2) {
272
+ const uuid2 = ref2.custom?.uuid;
273
+ if (!uuid2) {
274
+ throw new Error("Reference must have custom.uuid");
275
+ }
276
+ const uuidPrefix = extractUuidPrefix(uuid2);
277
+ const pmid = ref2.PMID?.trim();
278
+ if (pmid) {
279
+ return `${ref2.id}-PMID${pmid}-${uuidPrefix}`;
280
+ }
281
+ return `${ref2.id}-${uuidPrefix}`;
282
+ }
283
+ function getDirectoryPath(ref2, baseDir) {
284
+ const existingDir = ref2.custom?.attachments?.directory;
285
+ if (existingDir) {
286
+ return normalizePathForOutput(path__default.join(baseDir, existingDir));
287
+ }
288
+ const dirName = generateDirectoryName(ref2);
289
+ return normalizePathForOutput(path__default.join(baseDir, dirName));
290
+ }
291
+ async function ensureDirectory(ref2, baseDir) {
292
+ const dirPath = getDirectoryPath(ref2, baseDir);
293
+ try {
294
+ await fs__default.mkdir(dirPath, { recursive: true });
295
+ } catch (error) {
296
+ if (error.code !== "EEXIST") {
297
+ throw error;
298
+ }
299
+ }
300
+ return dirPath;
301
+ }
302
+ async function deleteDirectoryIfEmpty(dirPath) {
303
+ try {
304
+ const files = await fs__default.readdir(dirPath);
305
+ if (files.length === 0) {
306
+ await fs__default.rmdir(dirPath);
307
+ }
308
+ } catch (error) {
309
+ if (error.code !== "ENOENT") {
310
+ throw error;
311
+ }
312
+ }
313
+ }
314
+ function slugifyLabel(label) {
315
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
316
+ }
317
+ function generateFilename(role, ext, label) {
318
+ if (label) {
319
+ const slug = slugifyLabel(label);
320
+ return `${role}-${slug}.${ext}`;
321
+ }
322
+ return `${role}.${ext}`;
323
+ }
324
+ function parseFilename(filename) {
325
+ if (!filename) {
326
+ return null;
327
+ }
328
+ const ext = path__default.extname(filename);
329
+ const extWithoutDot = ext.startsWith(".") ? ext.slice(1) : ext;
330
+ const basename2 = ext ? filename.slice(0, -ext.length) : filename;
331
+ const firstHyphenIndex = basename2.indexOf("-");
332
+ if (firstHyphenIndex === -1) {
333
+ return {
334
+ role: basename2,
335
+ ext: extWithoutDot
336
+ };
337
+ }
338
+ const role = basename2.slice(0, firstHyphenIndex);
339
+ const label = basename2.slice(firstHyphenIndex + 1);
340
+ if (label) {
341
+ return {
342
+ role,
343
+ ext: extWithoutDot,
344
+ label
345
+ };
346
+ }
347
+ return {
348
+ role,
349
+ ext: extWithoutDot
350
+ };
351
+ }
352
+ async function checkSourceFile(filePath) {
353
+ try {
354
+ await stat(filePath);
355
+ return null;
356
+ } catch {
357
+ return `Source file not found: ${filePath}`;
358
+ }
359
+ }
360
+ function validateFulltextConstraint(existingFiles, newFile) {
361
+ if (newFile.role !== "fulltext") {
362
+ return null;
363
+ }
364
+ const newExt = getExtension(newFile.filename);
365
+ const existingFulltexts = existingFiles.filter((f) => f.role === "fulltext");
366
+ for (const existing of existingFulltexts) {
367
+ const existingExt = getExtension(existing.filename);
368
+ if (existingExt === newExt) {
369
+ return `A fulltext ${newExt.toUpperCase()} already exists. Use --force to overwrite.`;
370
+ }
371
+ }
372
+ const testFiles = [...existingFiles, newFile];
373
+ if (!isValidFulltextFiles(testFiles)) {
374
+ return "fulltext role allows max 2 files (1 PDF + 1 Markdown)";
375
+ }
376
+ return null;
377
+ }
378
+ function findExistingFile(files, filename) {
379
+ return files.find((f) => f.filename === filename);
380
+ }
381
+ async function copyOrMoveFile(sourcePath, destPath, move) {
382
+ try {
383
+ if (move) {
384
+ await rename(sourcePath, destPath);
385
+ } else {
386
+ await copyFile(sourcePath, destPath);
387
+ }
388
+ return null;
389
+ } catch (error) {
390
+ return `Failed to ${move ? "move" : "copy"} file: ${error instanceof Error ? error.message : String(error)}`;
391
+ }
392
+ }
393
+ async function updateAttachmentMetadata$1(library, item, updatedAttachments) {
394
+ await library.update(item.id, {
395
+ custom: {
396
+ ...item.custom,
397
+ attachments: updatedAttachments
398
+ }
399
+ });
400
+ }
401
+ function buildUpdatedFiles$1(existingFiles, newFile, existingFile) {
402
+ if (existingFile) {
403
+ return existingFiles.map((f) => f.filename === newFile.filename ? newFile : f);
404
+ }
405
+ return [...existingFiles, newFile];
406
+ }
407
+ async function addAttachment(library, options) {
408
+ const {
409
+ identifier,
410
+ filePath,
411
+ role,
412
+ label,
413
+ move = false,
414
+ force = false,
415
+ idType = "id",
416
+ attachmentsDirectory
417
+ } = options;
418
+ const item = await library.find(identifier, { idType });
419
+ if (!item) {
420
+ return { success: false, error: `Reference '${identifier}' not found` };
421
+ }
422
+ const uuid2 = item.custom?.uuid;
423
+ if (!uuid2) {
424
+ return { success: false, error: "Reference has no UUID. Cannot create attachment directory." };
425
+ }
426
+ const sourceError = await checkSourceFile(filePath);
427
+ if (sourceError) {
428
+ return { success: false, error: sourceError };
429
+ }
430
+ const ext = extname(filePath).slice(1).toLowerCase();
431
+ const filename = generateFilename(role, ext, label);
432
+ const existingAttachments = item.custom?.attachments;
433
+ const existingFiles = existingAttachments?.files ?? [];
434
+ const newFile = {
435
+ filename,
436
+ role,
437
+ ...label && { label }
438
+ };
439
+ const existingFile = findExistingFile(existingFiles, filename);
440
+ if (!existingFile || !force) {
441
+ const constraintError = validateFulltextConstraint(existingFiles, newFile);
442
+ if (constraintError) {
443
+ return { success: false, error: constraintError };
444
+ }
445
+ }
446
+ if (existingFile && !force) {
447
+ return {
448
+ success: false,
449
+ existingFile: filename,
450
+ requiresConfirmation: true
451
+ };
452
+ }
453
+ const ref2 = item;
454
+ const dirPath = await ensureDirectory(ref2, attachmentsDirectory);
455
+ const dirName = existingAttachments?.directory ?? generateDirectoryName(ref2);
456
+ const destPath = join(dirPath, filename);
457
+ const copyError = await copyOrMoveFile(filePath, destPath, move);
458
+ if (copyError) {
459
+ return { success: false, error: copyError };
460
+ }
461
+ const updatedFiles = buildUpdatedFiles$1(existingFiles, newFile, existingFile);
462
+ const updatedAttachments = {
463
+ directory: dirName,
464
+ files: updatedFiles
465
+ };
466
+ await updateAttachmentMetadata$1(library, item, updatedAttachments);
467
+ await library.save();
468
+ return {
469
+ success: true,
470
+ filename,
471
+ directory: dirName,
472
+ overwritten: !!existingFile
473
+ };
474
+ }
475
+ async function listAttachments(library, options) {
476
+ const { identifier, idType = "id", role } = options;
477
+ const item = await library.find(identifier, { idType });
478
+ if (!item) {
479
+ return { success: false, files: [], error: `Reference '${identifier}' not found` };
480
+ }
481
+ const attachments = item.custom?.attachments;
482
+ if (!attachments || attachments.files.length === 0) {
483
+ return { success: false, files: [], error: `No attachments for reference '${identifier}'` };
484
+ }
485
+ let files = attachments.files;
486
+ if (role) {
487
+ files = files.filter((f) => f.role === role);
488
+ if (files.length === 0) {
489
+ return {
490
+ success: false,
491
+ files: [],
492
+ error: `No ${role} attachments for reference '${identifier}'`
493
+ };
494
+ }
495
+ }
496
+ return {
497
+ success: true,
498
+ directory: attachments.directory,
499
+ files
500
+ };
501
+ }
502
+ function findAttachment(files, filename, role) {
503
+ if (filename) {
504
+ return files.find((f) => f.filename === filename);
505
+ }
506
+ if (role) {
507
+ return files.find((f) => f.role === role);
508
+ }
509
+ return void 0;
510
+ }
511
+ async function getAttachment(library, options) {
512
+ const {
513
+ identifier,
514
+ filename,
515
+ role,
516
+ idType = "id",
517
+ stdout: stdout2 = false,
518
+ attachmentsDirectory
519
+ } = options;
520
+ const item = await library.find(identifier, { idType });
521
+ if (!item) {
522
+ return { success: false, error: `Reference '${identifier}' not found` };
523
+ }
524
+ const attachments = item.custom?.attachments;
525
+ if (!attachments || attachments.files.length === 0) {
526
+ return { success: false, error: `No attachments for reference '${identifier}'` };
527
+ }
528
+ const attachment = findAttachment(attachments.files, filename, role);
529
+ if (!attachment) {
530
+ if (filename) {
531
+ return { success: false, error: `Attachment '${filename}' not found` };
532
+ }
533
+ if (role) {
534
+ return { success: false, error: `No ${role} attachment found` };
535
+ }
536
+ return { success: false, error: "No filename or role specified" };
537
+ }
538
+ const filePath = join(attachmentsDirectory, attachments.directory, attachment.filename);
539
+ const normalizedPath = normalizePathForOutput(filePath);
540
+ if (stdout2) {
541
+ try {
542
+ const content = await readFile(filePath);
543
+ return { success: true, path: normalizedPath, content };
544
+ } catch {
545
+ return {
546
+ success: false,
547
+ error: `File not found on disk: ${normalizedPath}`
548
+ };
549
+ }
550
+ }
551
+ return { success: true, path: normalizedPath };
552
+ }
553
+ function findFilesToDetach(files, filename, role, all) {
554
+ if (filename) {
555
+ const file = files.find((f) => f.filename === filename);
556
+ return file ? [file] : [];
557
+ }
558
+ if (role && all) {
559
+ return files.filter((f) => f.role === role);
560
+ }
561
+ return [];
562
+ }
563
+ async function deleteFiles(dirPath, filenames) {
564
+ const deleted = [];
565
+ for (const filename of filenames) {
566
+ try {
567
+ await unlink(join(dirPath, filename));
568
+ deleted.push(filename);
569
+ } catch {
570
+ }
571
+ }
572
+ return deleted;
573
+ }
574
+ async function updateMetadata(library, item, attachments, remainingFiles) {
575
+ const updatedAttachments = remainingFiles.length > 0 ? { directory: attachments.directory, files: remainingFiles } : void 0;
576
+ await library.update(item.id, {
577
+ custom: {
578
+ ...item.custom,
579
+ attachments: updatedAttachments
580
+ }
581
+ });
582
+ }
583
+ async function tryDeleteEmptyDirectory(dirPath) {
584
+ try {
585
+ await deleteDirectoryIfEmpty(dirPath);
586
+ try {
587
+ await stat(dirPath);
588
+ return false;
589
+ } catch {
590
+ return true;
591
+ }
592
+ } catch {
593
+ return false;
594
+ }
595
+ }
596
+ function errorResult$1(error) {
597
+ return { success: false, detached: [], deleted: [], error };
598
+ }
599
+ async function detachAttachment(library, options) {
600
+ const {
601
+ identifier,
602
+ filename,
603
+ role,
604
+ all = false,
605
+ removeFiles = false,
606
+ idType = "id",
607
+ attachmentsDirectory
608
+ } = options;
609
+ if (!filename && !role) {
610
+ return errorResult$1("Either filename or role must be specified");
611
+ }
612
+ const item = await library.find(identifier, { idType });
613
+ if (!item) {
614
+ return errorResult$1(`Reference '${identifier}' not found`);
615
+ }
616
+ const attachments = item.custom?.attachments;
617
+ if (!attachments || attachments.files.length === 0) {
618
+ return errorResult$1(`No attachments for reference '${identifier}'`);
619
+ }
620
+ const filesToDetach = findFilesToDetach(attachments.files, filename, role, all);
621
+ if (filesToDetach.length === 0) {
622
+ if (filename) {
623
+ return errorResult$1(`Attachment '${filename}' not found`);
624
+ }
625
+ return errorResult$1(`No ${role} attachments found`);
626
+ }
627
+ const detachedFilenames = filesToDetach.map((f) => f.filename);
628
+ const dirPath = join(attachmentsDirectory, attachments.directory);
629
+ const deletedFiles = removeFiles ? await deleteFiles(dirPath, detachedFilenames) : [];
630
+ const remainingFiles = attachments.files.filter((f) => !detachedFilenames.includes(f.filename));
631
+ await updateMetadata(library, item, attachments, remainingFiles);
632
+ await library.save();
633
+ const directoryDeleted = removeFiles && remainingFiles.length === 0 ? await tryDeleteEmptyDirectory(dirPath) : false;
634
+ return {
635
+ success: true,
636
+ detached: detachedFilenames,
637
+ deleted: deletedFiles,
638
+ directoryDeleted
639
+ };
640
+ }
641
+ function errorResult(error) {
642
+ return { success: false, newFiles: [], missingFiles: [], applied: false, error };
643
+ }
644
+ function inferFromFilename(filename) {
645
+ const parsed = parseFilename(filename);
646
+ if (!parsed) {
647
+ return { filename, role: "other", label: filename };
648
+ }
649
+ const { role, label } = parsed;
650
+ if (isReservedRole(role)) {
651
+ return label ? { filename, role, label } : { filename, role };
652
+ }
653
+ const basename2 = label ? `${role}-${label}` : role;
654
+ return { filename, role: "other", label: basename2 };
655
+ }
656
+ async function getFilesOnDisk(dirPath) {
657
+ try {
658
+ const entries = await readdir(dirPath);
659
+ const files = [];
660
+ for (const entry of entries) {
661
+ const entryPath = join(dirPath, entry);
662
+ const stats = await stat(entryPath);
663
+ if (stats.isFile()) {
664
+ files.push(entry);
665
+ }
666
+ }
667
+ return files;
668
+ } catch {
669
+ return [];
670
+ }
671
+ }
672
+ async function directoryExists(dirPath) {
673
+ try {
674
+ const stats = await stat(dirPath);
675
+ return stats.isDirectory();
676
+ } catch {
677
+ return false;
678
+ }
679
+ }
680
+ function findNewFiles(diskFiles, metadataFilenames) {
681
+ return diskFiles.filter((f) => !metadataFilenames.has(f)).map(inferFromFilename);
682
+ }
683
+ function findMissingFiles(metadataFiles, diskFilenames) {
684
+ return metadataFiles.filter((f) => !diskFilenames.has(f.filename)).map((f) => f.filename);
685
+ }
686
+ function buildUpdatedFiles(metadataFiles, newFiles, missingFiles, shouldApplyNew, shouldApplyFix) {
687
+ let updatedFiles = [...metadataFiles];
688
+ if (shouldApplyNew) {
689
+ for (const newFile of newFiles) {
690
+ const attachmentFile = {
691
+ filename: newFile.filename,
692
+ role: newFile.role,
693
+ ...newFile.label && { label: newFile.label }
694
+ };
695
+ updatedFiles.push(attachmentFile);
696
+ }
697
+ }
698
+ if (shouldApplyFix) {
699
+ const missingSet = new Set(missingFiles);
700
+ updatedFiles = updatedFiles.filter((f) => !missingSet.has(f.filename));
701
+ }
702
+ return updatedFiles;
703
+ }
704
+ async function updateAttachmentMetadata(library, item, attachments, updatedFiles) {
705
+ await library.update(item.id, {
706
+ custom: {
707
+ ...item.custom,
708
+ attachments: {
709
+ ...attachments,
710
+ files: updatedFiles
711
+ }
712
+ }
713
+ });
714
+ }
715
+ async function syncAttachments(library, options) {
716
+ const { identifier, yes = false, fix = false, idType = "id", attachmentsDirectory } = options;
717
+ const item = await library.find(identifier, { idType });
718
+ if (!item) {
719
+ return errorResult(`Reference '${identifier}' not found`);
720
+ }
721
+ const attachments = item.custom?.attachments;
722
+ if (!attachments?.directory) {
723
+ return errorResult(`No attachments for reference: ${identifier}`);
724
+ }
725
+ const dirPath = join(attachmentsDirectory, attachments.directory);
726
+ if (!await directoryExists(dirPath)) {
727
+ return errorResult(`Attachments directory does not exist: ${attachments.directory}`);
728
+ }
729
+ const metadataFiles = attachments.files || [];
730
+ const metadataFilenames = new Set(metadataFiles.map((f) => f.filename));
731
+ const diskFiles = await getFilesOnDisk(dirPath);
732
+ const diskFilenames = new Set(diskFiles);
733
+ const newFiles = findNewFiles(diskFiles, metadataFilenames);
734
+ const missingFiles = findMissingFiles(metadataFiles, diskFilenames);
735
+ const shouldApplyNew = yes && newFiles.length > 0;
736
+ const shouldApplyFix = fix && missingFiles.length > 0;
737
+ const shouldApply = shouldApplyNew || shouldApplyFix;
738
+ if (shouldApply) {
739
+ const updatedFiles = buildUpdatedFiles(
740
+ metadataFiles,
741
+ newFiles,
742
+ missingFiles,
743
+ shouldApplyNew,
744
+ shouldApplyFix
745
+ );
746
+ await updateAttachmentMetadata(library, item, attachments, updatedFiles);
747
+ await library.save();
748
+ }
749
+ return { success: true, newFiles, missingFiles, applied: shouldApply };
750
+ }
751
+ async function pathExists(path2) {
752
+ try {
753
+ await stat(path2);
754
+ return true;
755
+ } catch {
756
+ return false;
757
+ }
758
+ }
759
+ function findFileByRole(attachments, role) {
760
+ if (!attachments?.files) {
761
+ return null;
762
+ }
763
+ const file = attachments.files.find((f) => f.role === role);
764
+ return file?.filename ?? null;
765
+ }
766
+ async function resolveDirectory(ref2, attachmentsDirectory) {
767
+ const attachments = ref2.custom?.attachments;
768
+ let dirPath;
769
+ let directoryCreated = false;
770
+ if (attachments?.directory) {
771
+ dirPath = join(attachmentsDirectory, attachments.directory);
772
+ } else {
773
+ dirPath = await ensureDirectory(ref2, attachmentsDirectory);
774
+ directoryCreated = true;
775
+ }
776
+ if (!await pathExists(dirPath)) {
777
+ dirPath = await ensureDirectory(ref2, attachmentsDirectory);
778
+ directoryCreated = true;
779
+ }
780
+ return { dirPath, directoryCreated };
781
+ }
782
+ async function updateDirectoryMetadata(library, ref2, dirPath) {
783
+ const dirName = basename(dirPath);
784
+ const item = ref2;
785
+ await library.update(ref2.id, {
786
+ custom: {
787
+ ...item.custom,
788
+ attachments: {
789
+ directory: dirName,
790
+ files: []
791
+ }
792
+ }
793
+ });
794
+ await library.save();
795
+ }
796
+ async function resolveTargetPath(dirPath, filename, role, attachments) {
797
+ if (filename) {
798
+ const targetPath = join(dirPath, filename);
799
+ if (!await pathExists(targetPath)) {
800
+ return { error: `Attachment file not found: ${filename}` };
801
+ }
802
+ return { path: targetPath };
803
+ }
804
+ if (role) {
805
+ const foundFilename = findFileByRole(attachments, role);
806
+ if (!foundFilename) {
807
+ return { error: `No ${role} attachment found` };
808
+ }
809
+ const targetPath = join(dirPath, foundFilename);
810
+ if (!await pathExists(targetPath)) {
811
+ return { error: `Attachment file not found: ${foundFilename}` };
812
+ }
813
+ return { path: targetPath };
814
+ }
815
+ return { path: dirPath };
816
+ }
817
+ async function openAttachment(library, options) {
818
+ const {
819
+ identifier,
820
+ filename,
821
+ role,
822
+ print = false,
823
+ idType = "id",
824
+ attachmentsDirectory
825
+ } = options;
826
+ const item = await library.find(identifier, { idType });
827
+ if (!item) {
828
+ return { success: false, error: `Reference '${identifier}' not found` };
829
+ }
830
+ const ref2 = item;
831
+ if (!ref2.custom?.uuid) {
832
+ return {
833
+ success: false,
834
+ error: "Reference has no UUID. Cannot determine attachment directory."
835
+ };
836
+ }
837
+ const attachments = ref2.custom?.attachments;
838
+ const { dirPath, directoryCreated } = await resolveDirectory(ref2, attachmentsDirectory);
839
+ if (directoryCreated && !attachments?.directory) {
840
+ await updateDirectoryMetadata(library, ref2, dirPath);
841
+ }
842
+ const targetResult = await resolveTargetPath(dirPath, filename, role, attachments);
843
+ if ("error" in targetResult) {
844
+ return { success: false, error: targetResult.error };
845
+ }
846
+ if (!print) {
847
+ await openWithSystemApp(targetResult.path);
848
+ }
849
+ return {
850
+ success: true,
851
+ // Normalize for output (forward slashes for cross-platform consistency)
852
+ path: normalizePathForOutput(targetResult.path),
853
+ directoryCreated
854
+ };
855
+ }
263
856
  class OperationsLibrary {
264
857
  constructor(library, citationConfig) {
265
858
  this.library = library;
@@ -286,15 +879,15 @@ class OperationsLibrary {
286
879
  }
287
880
  // High-level operations
288
881
  async search(options) {
289
- const { searchReferences } = await import("./index-4KSTJ3rp.js").then((n) => n.d);
882
+ const { searchReferences } = await import("./index-DHgeuWGP.js").then((n) => n.n);
290
883
  return searchReferences(this.library, options);
291
884
  }
292
885
  async list(options) {
293
- const { listReferences } = await import("./index-4KSTJ3rp.js").then((n) => n.l);
886
+ const { listReferences } = await import("./index-DHgeuWGP.js").then((n) => n.m);
294
887
  return listReferences(this.library, options ?? {});
295
888
  }
296
889
  async cite(options) {
297
- const { citeReferences } = await import("./index-4KSTJ3rp.js").then((n) => n.b);
890
+ const { citeReferences } = await import("./index-DHgeuWGP.js").then((n) => n.l);
298
891
  const defaultStyle = options.defaultStyle ?? this.citationConfig?.defaultStyle;
299
892
  const cslDirectory = options.cslDirectory ?? this.citationConfig?.cslDirectory;
300
893
  const mergedOptions = {
@@ -305,9 +898,34 @@ class OperationsLibrary {
305
898
  return citeReferences(this.library, mergedOptions);
306
899
  }
307
900
  async import(inputs, options) {
308
- const { addReferences } = await import("./index-4KSTJ3rp.js").then((n) => n.a);
901
+ const { addReferences } = await import("./index-DHgeuWGP.js").then((n) => n.k);
309
902
  return addReferences(inputs, this.library, options ?? {});
310
903
  }
904
+ // Attachment operations
905
+ async attachAdd(options) {
906
+ const { addAttachment: addAttachment2 } = await import("./index-B_WCu-ZQ.js");
907
+ return addAttachment2(this.library, options);
908
+ }
909
+ async attachList(options) {
910
+ const { listAttachments: listAttachments2 } = await import("./index-B_WCu-ZQ.js");
911
+ return listAttachments2(this.library, options);
912
+ }
913
+ async attachGet(options) {
914
+ const { getAttachment: getAttachment2 } = await import("./index-B_WCu-ZQ.js");
915
+ return getAttachment2(this.library, options);
916
+ }
917
+ async attachDetach(options) {
918
+ const { detachAttachment: detachAttachment2 } = await import("./index-B_WCu-ZQ.js");
919
+ return detachAttachment2(this.library, options);
920
+ }
921
+ async attachSync(options) {
922
+ const { syncAttachments: syncAttachments2 } = await import("./index-B_WCu-ZQ.js");
923
+ return syncAttachments2(this.library, options);
924
+ }
925
+ async attachOpen(options) {
926
+ const { openAttachment: openAttachment2 } = await import("./index-B_WCu-ZQ.js");
927
+ return openAttachment2(this.library, options);
928
+ }
311
929
  }
312
930
  class ServerClient {
313
931
  constructor(baseUrl) {
@@ -494,20 +1112,125 @@ class ServerClient {
494
1112
  }
495
1113
  return await response.json();
496
1114
  }
497
- }
498
- async function getServerConnection(libraryPath, config2) {
499
- const portfilePath = getPortfilePath();
500
- const portfileData = await readPortfile(portfilePath);
501
- if (!portfileData) {
502
- if (config2.server.autoStart) {
503
- await startServerDaemon$1(libraryPath);
504
- await waitForPortfile(5e3);
505
- return await getServerConnection(libraryPath, config2);
1115
+ // ─────────────────────────────────────────────────────────────────────────
1116
+ // Attachment operations
1117
+ // ─────────────────────────────────────────────────────────────────────────
1118
+ /**
1119
+ * Add attachment to a reference.
1120
+ * @param options - Add attachment options
1121
+ * @returns Result of the add operation
1122
+ */
1123
+ async attachAdd(options) {
1124
+ const url = `${this.baseUrl}/api/attachments/add`;
1125
+ const response = await fetch(url, {
1126
+ method: "POST",
1127
+ headers: { "Content-Type": "application/json" },
1128
+ body: JSON.stringify(options)
1129
+ });
1130
+ if (!response.ok) {
1131
+ throw new Error(await response.text());
506
1132
  }
507
- return null;
1133
+ return await response.json();
508
1134
  }
509
- if (!isProcessRunning(portfileData.pid)) {
510
- await removePortfile(portfilePath);
1135
+ /**
1136
+ * List attachments for a reference.
1137
+ * @param options - List attachments options
1138
+ * @returns List of attachments
1139
+ */
1140
+ async attachList(options) {
1141
+ const url = `${this.baseUrl}/api/attachments/list`;
1142
+ const response = await fetch(url, {
1143
+ method: "POST",
1144
+ headers: { "Content-Type": "application/json" },
1145
+ body: JSON.stringify(options)
1146
+ });
1147
+ if (!response.ok) {
1148
+ throw new Error(await response.text());
1149
+ }
1150
+ return await response.json();
1151
+ }
1152
+ /**
1153
+ * Get attachment file path or content.
1154
+ * @param options - Get attachment options
1155
+ * @returns Attachment file path or content
1156
+ */
1157
+ async attachGet(options) {
1158
+ const url = `${this.baseUrl}/api/attachments/get`;
1159
+ const response = await fetch(url, {
1160
+ method: "POST",
1161
+ headers: { "Content-Type": "application/json" },
1162
+ body: JSON.stringify(options)
1163
+ });
1164
+ if (!response.ok) {
1165
+ throw new Error(await response.text());
1166
+ }
1167
+ return await response.json();
1168
+ }
1169
+ /**
1170
+ * Detach attachment from a reference.
1171
+ * @param options - Detach attachment options
1172
+ * @returns Result of the detach operation
1173
+ */
1174
+ async attachDetach(options) {
1175
+ const url = `${this.baseUrl}/api/attachments/detach`;
1176
+ const response = await fetch(url, {
1177
+ method: "POST",
1178
+ headers: { "Content-Type": "application/json" },
1179
+ body: JSON.stringify(options)
1180
+ });
1181
+ if (!response.ok) {
1182
+ throw new Error(await response.text());
1183
+ }
1184
+ return await response.json();
1185
+ }
1186
+ /**
1187
+ * Sync attachments with files on disk.
1188
+ * @param options - Sync attachment options
1189
+ * @returns Sync result
1190
+ */
1191
+ async attachSync(options) {
1192
+ const url = `${this.baseUrl}/api/attachments/sync`;
1193
+ const response = await fetch(url, {
1194
+ method: "POST",
1195
+ headers: { "Content-Type": "application/json" },
1196
+ body: JSON.stringify(options)
1197
+ });
1198
+ if (!response.ok) {
1199
+ throw new Error(await response.text());
1200
+ }
1201
+ return await response.json();
1202
+ }
1203
+ /**
1204
+ * Open attachment directory or file.
1205
+ * @param options - Open attachment options
1206
+ * @returns Result of the open operation
1207
+ */
1208
+ async attachOpen(options) {
1209
+ const url = `${this.baseUrl}/api/attachments/open`;
1210
+ const response = await fetch(url, {
1211
+ method: "POST",
1212
+ headers: { "Content-Type": "application/json" },
1213
+ body: JSON.stringify(options)
1214
+ });
1215
+ if (!response.ok) {
1216
+ throw new Error(await response.text());
1217
+ }
1218
+ return await response.json();
1219
+ }
1220
+ }
1221
+ async function getServerConnection(libraryPath, config2) {
1222
+ const portfilePath = getPortfilePath();
1223
+ const portfileData = await readPortfile(portfilePath);
1224
+ if (!portfileData) {
1225
+ if (config2.server.autoStart) {
1226
+ await startServerDaemon$1(libraryPath);
1227
+ await waitForPortfile(5e3);
1228
+ return await getServerConnection(libraryPath, config2);
1229
+ }
1230
+ return null;
1231
+ }
1232
+ if (!isProcessRunning(portfileData.pid)) {
1233
+ await removePortfile(portfilePath);
511
1234
  return null;
512
1235
  }
513
1236
  if (!portfileData.library || portfileData.library !== libraryPath) {
@@ -652,145 +1375,1411 @@ async function readStdinBuffer() {
652
1375
  }
653
1376
  return Buffer.concat(chunks);
654
1377
  }
655
- async function validateOptions$2(options) {
656
- if (options.format && !["text", "html", "rtf"].includes(options.format)) {
657
- throw new Error(`Invalid format '${options.format}'. Must be one of: text, html, rtf`);
658
- }
659
- if (options.cslFile) {
660
- const fs2 = await import("node:fs");
661
- if (!fs2.existsSync(options.cslFile)) {
662
- throw new Error(`CSL file '${options.cslFile}' not found`);
663
- }
664
- }
1378
+ async function executeAttachOpen(options, context) {
1379
+ const operationOptions = {
1380
+ identifier: options.identifier,
1381
+ attachmentsDirectory: options.attachmentsDirectory,
1382
+ ...options.filename !== void 0 && { filename: options.filename },
1383
+ ...options.role !== void 0 && { role: options.role },
1384
+ ...options.print !== void 0 && { print: options.print },
1385
+ ...options.idType !== void 0 && { idType: options.idType }
1386
+ };
1387
+ return openAttachment(context.library, operationOptions);
665
1388
  }
666
- function buildCiteOptions(options) {
667
- return {
668
- identifiers: options.identifiers,
669
- ...options.uuid && { idType: "uuid" },
670
- ...options.style !== void 0 && { style: options.style },
671
- ...options.cslFile !== void 0 && { cslFile: options.cslFile },
672
- ...options.locale !== void 0 && { locale: options.locale },
673
- ...options.format !== void 0 && { format: options.format },
674
- ...options.inText !== void 0 && { inText: options.inText }
1389
+ async function executeAttachAdd(options, context) {
1390
+ const operationOptions = {
1391
+ identifier: options.identifier,
1392
+ filePath: options.filePath,
1393
+ role: options.role,
1394
+ attachmentsDirectory: options.attachmentsDirectory,
1395
+ ...options.label !== void 0 && { label: options.label },
1396
+ ...options.move !== void 0 && { move: options.move },
1397
+ ...options.force !== void 0 && { force: options.force },
1398
+ ...options.idType !== void 0 && { idType: options.idType }
675
1399
  };
1400
+ return addAttachment(context.library, operationOptions);
676
1401
  }
677
- async function executeCite(options, context) {
678
- await validateOptions$2(options);
679
- return context.library.cite(buildCiteOptions(options));
1402
+ async function executeAttachList(options, context) {
1403
+ const operationOptions = {
1404
+ identifier: options.identifier,
1405
+ ...options.role !== void 0 && { role: options.role },
1406
+ ...options.idType !== void 0 && { idType: options.idType }
1407
+ };
1408
+ return listAttachments(context.library, operationOptions);
680
1409
  }
681
- function formatCiteOutput(result) {
1410
+ async function executeAttachGet(options, context) {
1411
+ const operationOptions = {
1412
+ identifier: options.identifier,
1413
+ attachmentsDirectory: options.attachmentsDirectory,
1414
+ ...options.filename !== void 0 && { filename: options.filename },
1415
+ ...options.role !== void 0 && { role: options.role },
1416
+ ...options.stdout !== void 0 && { stdout: options.stdout },
1417
+ ...options.idType !== void 0 && { idType: options.idType }
1418
+ };
1419
+ return getAttachment(context.library, operationOptions);
1420
+ }
1421
+ async function executeAttachDetach(options, context) {
1422
+ const operationOptions = {
1423
+ identifier: options.identifier,
1424
+ attachmentsDirectory: options.attachmentsDirectory,
1425
+ ...options.filename !== void 0 && { filename: options.filename },
1426
+ ...options.role !== void 0 && { role: options.role },
1427
+ ...options.all !== void 0 && { all: options.all },
1428
+ ...options.removeFiles !== void 0 && { removeFiles: options.removeFiles },
1429
+ ...options.idType !== void 0 && { idType: options.idType }
1430
+ };
1431
+ return detachAttachment(context.library, operationOptions);
1432
+ }
1433
+ async function executeAttachSync(options, context) {
1434
+ const operationOptions = {
1435
+ identifier: options.identifier,
1436
+ attachmentsDirectory: options.attachmentsDirectory,
1437
+ ...options.yes !== void 0 && { yes: options.yes },
1438
+ ...options.fix !== void 0 && { fix: options.fix },
1439
+ ...options.idType !== void 0 && { idType: options.idType }
1440
+ };
1441
+ return syncAttachments(context.library, operationOptions);
1442
+ }
1443
+ function formatAttachOpenOutput(result) {
1444
+ if (!result.success) {
1445
+ return `Error: ${result.error}`;
1446
+ }
1447
+ if (result.directoryCreated) {
1448
+ return `Created and opened: ${result.path}`;
1449
+ }
1450
+ return `Opened: ${result.path}`;
1451
+ }
1452
+ function formatAttachAddOutput(result) {
1453
+ if (result.requiresConfirmation) {
1454
+ return `File already exists: ${result.existingFile}
1455
+ Use --force to overwrite.`;
1456
+ }
1457
+ if (!result.success) {
1458
+ return `Error: ${result.error}`;
1459
+ }
1460
+ if (result.overwritten) {
1461
+ return `Added (overwritten): ${result.filename}`;
1462
+ }
1463
+ return `Added: ${result.filename}`;
1464
+ }
1465
+ function formatAttachListOutput(result, identifier) {
1466
+ if (!result.success) {
1467
+ return `Error: ${result.error}`;
1468
+ }
1469
+ if (result.files.length === 0) {
1470
+ return `No attachments for reference: ${identifier}`;
1471
+ }
1472
+ const grouped = /* @__PURE__ */ new Map();
1473
+ for (const file of result.files) {
1474
+ const existing = grouped.get(file.role) ?? [];
1475
+ existing.push(file);
1476
+ grouped.set(file.role, existing);
1477
+ }
682
1478
  const lines = [];
683
- for (const r of result.results) {
684
- if (r.success) {
685
- lines.push(r.citation);
1479
+ lines.push(`Attachments for ${identifier} (${result.directory}/):`);
1480
+ lines.push("");
1481
+ for (const [role, files] of grouped) {
1482
+ lines.push(`${role}:`);
1483
+ for (const file of files) {
1484
+ if (file.label) {
1485
+ lines.push(` ${file.filename} - "${file.label}"`);
1486
+ } else {
1487
+ lines.push(` ${file.filename}`);
1488
+ }
686
1489
  }
1490
+ lines.push("");
687
1491
  }
688
- return lines.join("\n");
1492
+ return lines.join("\n").trimEnd();
689
1493
  }
690
- function formatCiteErrors(result) {
1494
+ function formatAttachDetachOutput(result) {
1495
+ if (!result.success) {
1496
+ return `Error: ${result.error}`;
1497
+ }
691
1498
  const lines = [];
692
- for (const r of result.results) {
693
- if (!r.success) {
694
- lines.push(`Error for '${r.identifier}': ${r.error}`);
1499
+ for (const filename of result.detached) {
1500
+ if (result.deleted.includes(filename)) {
1501
+ lines.push(`Detached and deleted: ${filename}`);
1502
+ } else {
1503
+ lines.push(`Detached: ${filename}`);
695
1504
  }
696
1505
  }
1506
+ if (result.directoryDeleted) {
1507
+ lines.push("Directory removed.");
1508
+ }
697
1509
  return lines.join("\n");
698
1510
  }
699
- function getCiteExitCode(result) {
700
- const hasSuccess = result.results.some((r) => r.success);
701
- const hasError = result.results.some((r) => !r.success);
702
- if (hasSuccess) {
703
- return 0;
1511
+ function pluralize(count, singular) {
1512
+ return count > 1 ? `${singular}s` : singular;
1513
+ }
1514
+ function formatNewFilesSection(result, lines) {
1515
+ const count = result.newFiles.length;
1516
+ if (count === 0) return;
1517
+ const verb = result.applied ? "Added" : "Found";
1518
+ const suffix = result.applied ? "" : " new";
1519
+ lines.push(`${verb} ${count}${suffix} ${pluralize(count, "file")}:`);
1520
+ for (const file of result.newFiles) {
1521
+ const labelPart = file.label ? `, label: "${file.label}"` : "";
1522
+ lines.push(` ${file.filename} → role: ${file.role}${labelPart}`);
1523
+ }
1524
+ lines.push("");
1525
+ }
1526
+ function formatMissingFilesSection(result, lines) {
1527
+ const count = result.missingFiles.length;
1528
+ if (count === 0) return;
1529
+ const header = result.applied ? `Removed ${count} missing ${pluralize(count, "file")} from metadata:` : `Missing ${count} ${pluralize(count, "file")} (in metadata but not on disk):`;
1530
+ lines.push(header);
1531
+ for (const filename of result.missingFiles) {
1532
+ lines.push(` ${filename}`);
704
1533
  }
705
- if (hasError) {
706
- return 1;
1534
+ lines.push("");
1535
+ }
1536
+ function formatAttachSyncOutput(result) {
1537
+ if (!result.success) {
1538
+ return `Error: ${result.error}`;
707
1539
  }
708
- return 0;
1540
+ const hasNewFiles = result.newFiles.length > 0;
1541
+ const hasMissingFiles = result.missingFiles.length > 0;
1542
+ if (!hasNewFiles && !hasMissingFiles) {
1543
+ return "Already in sync.";
1544
+ }
1545
+ const lines = [];
1546
+ formatNewFilesSection(result, lines);
1547
+ formatMissingFilesSection(result, lines);
1548
+ if (!result.applied) {
1549
+ if (hasNewFiles) {
1550
+ lines.push("Run with --yes to add new files");
1551
+ }
1552
+ if (hasMissingFiles) {
1553
+ lines.push("Run with --fix to remove missing files");
1554
+ }
1555
+ }
1556
+ return lines.join("\n").trimEnd();
709
1557
  }
710
- async function executeInteractiveCite(options, context, config2) {
711
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
712
- const { runStyleSelect } = await import("./style-select-CHjDTyq2.js");
1558
+ function getAttachExitCode(result) {
1559
+ return result.success ? 0 : 1;
1560
+ }
1561
+ async function executeInteractiveSelect$1(context, config2) {
1562
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
713
1563
  const allReferences = await context.library.getAll();
714
1564
  const identifiers = await selectReferencesOrExit(
715
1565
  allReferences,
716
- { multiSelect: true },
717
- config2.cli.interactive
1566
+ { multiSelect: false },
1567
+ config2.cli.tui
718
1568
  );
719
- let style = options.style;
720
- if (!style && !options.cslFile) {
721
- const styleResult = await runStyleSelect({
722
- cslDirectory: config2.citation.cslDirectory,
723
- defaultStyle: config2.citation.defaultStyle
1569
+ return identifiers[0];
1570
+ }
1571
+ async function resolveIdentifier(identifierArg, context, config2) {
1572
+ if (identifierArg) {
1573
+ return identifierArg;
1574
+ }
1575
+ if (isTTY()) {
1576
+ return executeInteractiveSelect$1(context, config2);
1577
+ }
1578
+ const stdinId = await readIdentifierFromStdin();
1579
+ if (!stdinId) {
1580
+ process.stderr.write(
1581
+ "Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
1582
+ );
1583
+ process.exit(1);
1584
+ }
1585
+ return stdinId;
1586
+ }
1587
+ function displayNamingConvention(identifier, dirPath) {
1588
+ process.stderr.write(`
1589
+ Opening attachments directory for ${identifier}...
1590
+
1591
+ `);
1592
+ process.stderr.write("File naming convention:\n");
1593
+ process.stderr.write(" fulltext.pdf / fulltext.md - Paper body\n");
1594
+ process.stderr.write(" supplement-{label}.ext - Supplementary materials\n");
1595
+ process.stderr.write(" notes-{label}.ext - Your notes\n");
1596
+ process.stderr.write(" draft-{label}.ext - Draft versions\n");
1597
+ process.stderr.write(" {custom}-{label}.ext - Custom role\n\n");
1598
+ process.stderr.write(`Directory: ${dirPath}/
1599
+
1600
+ `);
1601
+ }
1602
+ async function waitForEnter() {
1603
+ return new Promise((resolve2) => {
1604
+ process.stderr.write("Press Enter when done editing...");
1605
+ process.stdin.setRawMode(true);
1606
+ process.stdin.resume();
1607
+ process.stdin.once("data", () => {
1608
+ process.stdin.setRawMode(false);
1609
+ process.stdin.pause();
1610
+ process.stderr.write("\n\n");
1611
+ resolve2();
724
1612
  });
725
- if (styleResult.cancelled) {
1613
+ });
1614
+ }
1615
+ function displayInteractiveSyncResult(result, identifier) {
1616
+ if (result.newFiles.length === 0) {
1617
+ process.stderr.write("No new files detected.\n");
1618
+ return;
1619
+ }
1620
+ process.stderr.write("Scanning directory...\n\n");
1621
+ process.stderr.write(
1622
+ `Found ${result.newFiles.length} new file${result.newFiles.length > 1 ? "s" : ""}:
1623
+ `
1624
+ );
1625
+ for (const file of result.newFiles) {
1626
+ const labelPart = file.label ? `, label: "${file.label}"` : "";
1627
+ process.stderr.write(` ✓ ${file.filename} → role: ${file.role}${labelPart}
1628
+ `);
1629
+ }
1630
+ process.stderr.write(`
1631
+ Updated metadata for ${identifier}.
1632
+ `);
1633
+ }
1634
+ async function runInteractiveMode(identifier, dirPath, attachmentsDirectory, idType, context) {
1635
+ displayNamingConvention(identifier, dirPath);
1636
+ await waitForEnter();
1637
+ const syncResult = await executeAttachSync(
1638
+ {
1639
+ identifier,
1640
+ attachmentsDirectory,
1641
+ yes: true,
1642
+ ...idType && { idType }
1643
+ },
1644
+ context
1645
+ );
1646
+ if (syncResult.success) {
1647
+ displayInteractiveSyncResult(syncResult, identifier);
1648
+ } else {
1649
+ process.stderr.write(`Sync error: ${syncResult.error}
1650
+ `);
1651
+ }
1652
+ }
1653
+ function buildOpenOptions(identifier, filenameArg, options, attachmentsDirectory) {
1654
+ return {
1655
+ identifier,
1656
+ attachmentsDirectory,
1657
+ ...filenameArg && { filename: filenameArg },
1658
+ ...options.print && { print: options.print },
1659
+ ...options.role && { role: options.role },
1660
+ ...options.uuid && { idType: "uuid" }
1661
+ };
1662
+ }
1663
+ async function handleAttachOpenAction(identifierArg, filenameArg, options, globalOpts) {
1664
+ try {
1665
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1666
+ const context = await createExecutionContext(config2, Library.load);
1667
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1668
+ const isDirectoryMode = !filenameArg && !options.role;
1669
+ const shouldUseInteractive = isTTY() && isDirectoryMode && !options.print && !options.noSync;
1670
+ const openOptions = buildOpenOptions(
1671
+ identifier,
1672
+ filenameArg,
1673
+ options,
1674
+ config2.attachments.directory
1675
+ );
1676
+ const result = await executeAttachOpen(openOptions, context);
1677
+ if (!result.success) {
1678
+ process.stderr.write(`Error: ${result.error}
1679
+ `);
1680
+ process.exit(1);
1681
+ }
1682
+ if (options.print) {
1683
+ process.stdout.write(`${result.path}
1684
+ `);
726
1685
  process.exit(0);
727
1686
  }
728
- style = styleResult.style;
1687
+ if (shouldUseInteractive) {
1688
+ await runInteractiveMode(
1689
+ identifier,
1690
+ result.path ?? "",
1691
+ config2.attachments.directory,
1692
+ options.uuid ? "uuid" : void 0,
1693
+ context
1694
+ );
1695
+ } else {
1696
+ process.stderr.write(`${formatAttachOpenOutput(result)}
1697
+ `);
1698
+ }
1699
+ process.exit(0);
1700
+ } catch (error) {
1701
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1702
+ `);
1703
+ process.exit(4);
729
1704
  }
730
- return executeCite({ ...options, ...style && { style }, identifiers }, context);
731
1705
  }
732
- async function handleCiteAction(identifiers, options, globalOpts) {
1706
+ async function handleAttachAddAction(identifierArg, filePathArg, options, globalOpts) {
733
1707
  try {
734
1708
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
735
1709
  const context = await createExecutionContext(config2, Library.load);
736
- let result;
737
- if (identifiers.length === 0) {
738
- if (isTTY()) {
739
- result = await executeInteractiveCite(options, context, config2);
740
- } else {
741
- const stdinIds = await readIdentifiersFromStdin();
742
- if (stdinIds.length === 0) {
743
- process.stderr.write(
744
- "Error: No identifiers provided. Provide IDs, pipe them via stdin, or run interactively in a TTY.\n"
745
- );
746
- process.exit(1);
747
- }
748
- result = await executeCite({ ...options, identifiers: stdinIds }, context);
749
- }
750
- } else {
751
- result = await executeCite({ ...options, identifiers }, context);
1710
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1711
+ const addOptions = {
1712
+ identifier,
1713
+ filePath: filePathArg,
1714
+ role: options.role,
1715
+ attachmentsDirectory: config2.attachments.directory,
1716
+ ...options.label && { label: options.label },
1717
+ ...options.move && { move: options.move },
1718
+ ...options.force && { force: options.force },
1719
+ ...options.uuid && { idType: "uuid" }
1720
+ };
1721
+ const result = await executeAttachAdd(addOptions, context);
1722
+ const output = formatAttachAddOutput(result);
1723
+ process.stderr.write(`${output}
1724
+ `);
1725
+ process.exit(getAttachExitCode(result));
1726
+ } catch (error) {
1727
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1728
+ `);
1729
+ process.exit(4);
1730
+ }
1731
+ }
1732
+ async function handleAttachListAction(identifierArg, options, globalOpts) {
1733
+ try {
1734
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1735
+ const context = await createExecutionContext(config2, Library.load);
1736
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1737
+ const listOptions = {
1738
+ identifier,
1739
+ attachmentsDirectory: config2.attachments.directory,
1740
+ ...options.role && { role: options.role },
1741
+ ...options.uuid && { idType: "uuid" }
1742
+ };
1743
+ const result = await executeAttachList(listOptions, context);
1744
+ const output = formatAttachListOutput(result, identifier);
1745
+ process.stdout.write(`${output}
1746
+ `);
1747
+ process.exit(getAttachExitCode(result));
1748
+ } catch (error) {
1749
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1750
+ `);
1751
+ process.exit(4);
1752
+ }
1753
+ }
1754
+ async function handleAttachGetAction(identifierArg, filenameArg, options, globalOpts) {
1755
+ try {
1756
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1757
+ const context = await createExecutionContext(config2, Library.load);
1758
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1759
+ const getOptions = {
1760
+ identifier,
1761
+ attachmentsDirectory: config2.attachments.directory,
1762
+ ...filenameArg && { filename: filenameArg },
1763
+ ...options.role && { role: options.role },
1764
+ ...options.stdout && { stdout: options.stdout },
1765
+ ...options.uuid && { idType: "uuid" }
1766
+ };
1767
+ const result = await executeAttachGet(getOptions, context);
1768
+ if (result.success && result.content && options.stdout) {
1769
+ process.stdout.write(result.content);
1770
+ } else if (result.success) {
1771
+ process.stdout.write(`${result.path}
1772
+ `);
1773
+ } else {
1774
+ process.stderr.write(`Error: ${result.error}
1775
+ `);
1776
+ }
1777
+ process.exit(getAttachExitCode(result));
1778
+ } catch (error) {
1779
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1780
+ `);
1781
+ process.exit(4);
1782
+ }
1783
+ }
1784
+ async function handleAttachDetachAction(identifierArg, filenameArg, options, globalOpts) {
1785
+ try {
1786
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1787
+ const context = await createExecutionContext(config2, Library.load);
1788
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1789
+ const detachOptions = {
1790
+ identifier,
1791
+ attachmentsDirectory: config2.attachments.directory,
1792
+ ...filenameArg && { filename: filenameArg },
1793
+ ...options.role && { role: options.role },
1794
+ ...options.all && { all: options.all },
1795
+ ...options.removeFiles && { removeFiles: options.removeFiles },
1796
+ ...options.uuid && { idType: "uuid" }
1797
+ };
1798
+ const result = await executeAttachDetach(detachOptions, context);
1799
+ const output = formatAttachDetachOutput(result);
1800
+ process.stderr.write(`${output}
1801
+ `);
1802
+ process.exit(getAttachExitCode(result));
1803
+ } catch (error) {
1804
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1805
+ `);
1806
+ process.exit(4);
1807
+ }
1808
+ }
1809
+ async function handleAttachSyncAction(identifierArg, options, globalOpts) {
1810
+ try {
1811
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1812
+ const context = await createExecutionContext(config2, Library.load);
1813
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1814
+ const syncOptions = {
1815
+ identifier,
1816
+ attachmentsDirectory: config2.attachments.directory,
1817
+ ...options.yes && { yes: options.yes },
1818
+ ...options.fix && { fix: options.fix },
1819
+ ...options.uuid && { idType: "uuid" }
1820
+ };
1821
+ const result = await executeAttachSync(syncOptions, context);
1822
+ const output = formatAttachSyncOutput(result);
1823
+ process.stderr.write(`${output}
1824
+ `);
1825
+ process.exit(getAttachExitCode(result));
1826
+ } catch (error) {
1827
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1828
+ `);
1829
+ process.exit(4);
1830
+ }
1831
+ }
1832
+ async function validateOptions$2(options) {
1833
+ if (options.output && !["text", "html", "rtf"].includes(options.output)) {
1834
+ throw new Error(`Invalid output format '${options.output}'. Must be one of: text, html, rtf`);
1835
+ }
1836
+ if (options.cslFile) {
1837
+ const fs2 = await import("node:fs");
1838
+ if (!fs2.existsSync(options.cslFile)) {
1839
+ throw new Error(`CSL file '${options.cslFile}' not found`);
1840
+ }
1841
+ }
1842
+ }
1843
+ function buildCiteOptions(options) {
1844
+ return {
1845
+ identifiers: options.identifiers,
1846
+ ...options.uuid && { idType: "uuid" },
1847
+ ...options.style !== void 0 && { style: options.style },
1848
+ ...options.cslFile !== void 0 && { cslFile: options.cslFile },
1849
+ ...options.locale !== void 0 && { locale: options.locale },
1850
+ ...options.output !== void 0 && { format: options.output },
1851
+ ...options.inText !== void 0 && { inText: options.inText }
1852
+ };
1853
+ }
1854
+ async function executeCite(options, context) {
1855
+ await validateOptions$2(options);
1856
+ return context.library.cite(buildCiteOptions(options));
1857
+ }
1858
+ function formatCiteOutput(result) {
1859
+ const lines = [];
1860
+ for (const r of result.results) {
1861
+ if (r.success) {
1862
+ lines.push(r.citation);
1863
+ }
1864
+ }
1865
+ return lines.join("\n");
1866
+ }
1867
+ function formatCiteErrors(result) {
1868
+ const lines = [];
1869
+ for (const r of result.results) {
1870
+ if (!r.success) {
1871
+ lines.push(`Error for '${r.identifier}': ${r.error}`);
1872
+ }
1873
+ }
1874
+ return lines.join("\n");
1875
+ }
1876
+ function getCiteExitCode(result) {
1877
+ const hasSuccess = result.results.some((r) => r.success);
1878
+ const hasError = result.results.some((r) => !r.success);
1879
+ if (hasSuccess) {
1880
+ return 0;
1881
+ }
1882
+ if (hasError) {
1883
+ return 1;
1884
+ }
1885
+ return 0;
1886
+ }
1887
+ async function executeInteractiveCite(options, context, config2) {
1888
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
1889
+ const { runStyleSelect } = await import("./style-select-BNQHC79W.js");
1890
+ const allReferences = await context.library.getAll();
1891
+ const identifiers = await selectReferencesOrExit(
1892
+ allReferences,
1893
+ { multiSelect: true },
1894
+ config2.cli.tui
1895
+ );
1896
+ let style = options.style;
1897
+ if (!style && !options.cslFile) {
1898
+ const styleResult = await runStyleSelect({
1899
+ cslDirectory: config2.citation.cslDirectory,
1900
+ defaultStyle: config2.citation.defaultStyle
1901
+ });
1902
+ if (styleResult.cancelled) {
1903
+ process.exit(0);
1904
+ }
1905
+ style = styleResult.style;
1906
+ }
1907
+ return executeCite({ ...options, ...style && { style }, identifiers }, context);
1908
+ }
1909
+ async function handleCiteAction(identifiers, options, globalOpts) {
1910
+ try {
1911
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1912
+ const context = await createExecutionContext(config2, Library.load);
1913
+ let result;
1914
+ if (identifiers.length === 0) {
1915
+ if (isTTY()) {
1916
+ result = await executeInteractiveCite(options, context, config2);
1917
+ } else {
1918
+ const stdinIds = await readIdentifiersFromStdin();
1919
+ if (stdinIds.length === 0) {
1920
+ process.stderr.write(
1921
+ "Error: No identifiers provided. Provide IDs, pipe them via stdin, or run interactively in a TTY.\n"
1922
+ );
1923
+ process.exit(1);
1924
+ }
1925
+ result = await executeCite({ ...options, identifiers: stdinIds }, context);
1926
+ }
1927
+ } else {
1928
+ result = await executeCite({ ...options, identifiers }, context);
1929
+ }
1930
+ const output = formatCiteOutput(result);
1931
+ if (output) {
1932
+ process.stdout.write(`${output}
1933
+ `);
1934
+ }
1935
+ const errors2 = formatCiteErrors(result);
1936
+ if (errors2) {
1937
+ process.stderr.write(`${errors2}
1938
+ `);
1939
+ }
1940
+ process.exit(getCiteExitCode(result));
1941
+ } catch (error) {
1942
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1943
+ `);
1944
+ process.exit(4);
1945
+ }
1946
+ }
1947
+ const ENV_OVERRIDE_MAP = {
1948
+ REFERENCE_MANAGER_LIBRARY: "library",
1949
+ REFERENCE_MANAGER_ATTACHMENTS_DIR: "attachments.directory",
1950
+ REFERENCE_MANAGER_CLI_DEFAULT_LIMIT: "cli.default_limit",
1951
+ REFERENCE_MANAGER_MCP_DEFAULT_LIMIT: "mcp.default_limit",
1952
+ PUBMED_EMAIL: "pubmed.email",
1953
+ PUBMED_API_KEY: "pubmed.api_key"
1954
+ };
1955
+ const KEY_TO_ENV_MAP = Object.fromEntries(
1956
+ Object.entries(ENV_OVERRIDE_MAP).map(([envVar, configKey]) => [configKey, envVar])
1957
+ );
1958
+ function getEnvOverrideInfo(configKey) {
1959
+ const envVar = KEY_TO_ENV_MAP[configKey];
1960
+ if (!envVar) {
1961
+ return null;
1962
+ }
1963
+ const value = process.env[envVar];
1964
+ return {
1965
+ envVar,
1966
+ value: value ?? null
1967
+ };
1968
+ }
1969
+ const CONFIG_KEY_REGISTRY = [
1970
+ // Top-level keys
1971
+ { key: "library", type: "string", description: "Path to library file" },
1972
+ {
1973
+ key: "log_level",
1974
+ type: "enum",
1975
+ description: "Log level",
1976
+ enumValues: ["silent", "info", "debug"]
1977
+ },
1978
+ // backup section
1979
+ { key: "backup.max_generations", type: "integer", description: "Maximum backup generations" },
1980
+ { key: "backup.max_age_days", type: "integer", description: "Maximum backup age in days" },
1981
+ { key: "backup.directory", type: "string", description: "Backup directory path" },
1982
+ // watch section
1983
+ { key: "watch.debounce_ms", type: "integer", description: "File watch debounce delay (ms)" },
1984
+ { key: "watch.poll_interval_ms", type: "integer", description: "File watch poll interval (ms)" },
1985
+ {
1986
+ key: "watch.retry_interval_ms",
1987
+ type: "integer",
1988
+ description: "File watch retry interval (ms)"
1989
+ },
1990
+ { key: "watch.max_retries", type: "integer", description: "Maximum file watch retries" },
1991
+ // server section
1992
+ { key: "server.auto_start", type: "boolean", description: "Auto-start server on CLI commands" },
1993
+ {
1994
+ key: "server.auto_stop_minutes",
1995
+ type: "integer",
1996
+ description: "Auto-stop server after idle minutes (0 = never)"
1997
+ },
1998
+ // citation section
1999
+ { key: "citation.default_style", type: "string", description: "Default citation style" },
2000
+ { key: "citation.csl_directory", type: "string[]", description: "CSL style file directories" },
2001
+ { key: "citation.default_locale", type: "string", description: "Default locale for citations" },
2002
+ {
2003
+ key: "citation.default_format",
2004
+ type: "enum",
2005
+ description: "Default format",
2006
+ enumValues: ["text", "html", "rtf"]
2007
+ },
2008
+ // pubmed section
2009
+ { key: "pubmed.email", type: "string", description: "Email for PubMed API", optional: true },
2010
+ { key: "pubmed.api_key", type: "string", description: "API key for PubMed", optional: true },
2011
+ // fulltext section
2012
+ { key: "fulltext.directory", type: "string", description: "Fulltext storage directory" },
2013
+ // cli section
2014
+ {
2015
+ key: "cli.default_limit",
2016
+ type: "integer",
2017
+ description: "Default result limit (0 = unlimited)"
2018
+ },
2019
+ {
2020
+ key: "cli.default_sort",
2021
+ type: "enum",
2022
+ description: "Default sort field",
2023
+ enumValues: ["created", "updated", "published", "author", "title"]
2024
+ },
2025
+ {
2026
+ key: "cli.default_order",
2027
+ type: "enum",
2028
+ description: "Default sort order",
2029
+ enumValues: ["asc", "desc"]
2030
+ },
2031
+ // cli.tui section
2032
+ {
2033
+ key: "cli.tui.limit",
2034
+ type: "integer",
2035
+ description: "Result limit in TUI mode"
2036
+ },
2037
+ {
2038
+ key: "cli.tui.debounce_ms",
2039
+ type: "integer",
2040
+ description: "Search debounce delay (ms)"
2041
+ },
2042
+ // cli.edit section
2043
+ {
2044
+ key: "cli.edit.default_format",
2045
+ type: "enum",
2046
+ description: "Default edit format",
2047
+ enumValues: ["yaml", "json"]
2048
+ },
2049
+ // mcp section
2050
+ { key: "mcp.default_limit", type: "integer", description: "Default result limit for MCP" }
2051
+ ];
2052
+ const KEY_MAP = new Map(CONFIG_KEY_REGISTRY.map((info) => [info.key, info]));
2053
+ let allKeysCache = null;
2054
+ function parseConfigKey(key) {
2055
+ return key.split(".");
2056
+ }
2057
+ function isValidConfigKey(key) {
2058
+ if (!key) {
2059
+ return false;
2060
+ }
2061
+ return KEY_MAP.has(key);
2062
+ }
2063
+ function getConfigKeyInfo(key) {
2064
+ return KEY_MAP.get(key) ?? null;
2065
+ }
2066
+ function getAllConfigKeys(section) {
2067
+ if (!allKeysCache) {
2068
+ allKeysCache = CONFIG_KEY_REGISTRY.map((info) => info.key).sort();
2069
+ }
2070
+ if (!section) {
2071
+ return allKeysCache;
2072
+ }
2073
+ const prefix = `${section}.`;
2074
+ return allKeysCache.filter((key) => key.startsWith(prefix));
2075
+ }
2076
+ function toInternalPath(key) {
2077
+ const segments = parseConfigKey(key);
2078
+ return segments.map((segment, index) => {
2079
+ if (index === 0) {
2080
+ if (segment === "log_level") {
2081
+ return "logLevel";
2082
+ }
2083
+ return segment;
2084
+ }
2085
+ return snakeToCamel(segment);
2086
+ });
2087
+ }
2088
+ function snakeToCamel(str2) {
2089
+ return str2.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
2090
+ }
2091
+ function validateConfigValue(key, value) {
2092
+ const keyInfo = getConfigKeyInfo(key);
2093
+ if (!keyInfo) {
2094
+ return { valid: false, error: `Unknown configuration key: '${key}'` };
2095
+ }
2096
+ return validateByType(keyInfo, value);
2097
+ }
2098
+ function validateByType(keyInfo, value) {
2099
+ switch (keyInfo.type) {
2100
+ case "string":
2101
+ return validateString(keyInfo, value);
2102
+ case "integer":
2103
+ return validateInteger(keyInfo, value);
2104
+ case "boolean":
2105
+ return validateBoolean(value);
2106
+ case "enum":
2107
+ return validateEnum(keyInfo, value);
2108
+ case "string[]":
2109
+ return validateStringArray(value);
2110
+ default:
2111
+ return { valid: false, error: `Unknown type: ${keyInfo.type}` };
2112
+ }
2113
+ }
2114
+ function validateString(keyInfo, value) {
2115
+ if (typeof value !== "string") {
2116
+ return { valid: false, error: `Expected string, received ${typeof value}` };
2117
+ }
2118
+ if (value === "" && !keyInfo.optional) {
2119
+ return { valid: false, error: "Value cannot be empty" };
2120
+ }
2121
+ return { valid: true, value };
2122
+ }
2123
+ function validateInteger(keyInfo, value) {
2124
+ if (typeof value !== "number") {
2125
+ return { valid: false, error: `Expected number, received ${typeof value}` };
2126
+ }
2127
+ if (!Number.isInteger(value)) {
2128
+ return { valid: false, error: "Value must be an integer" };
2129
+ }
2130
+ const requiresPositive = keyInfo.key.startsWith("backup.") && keyInfo.key !== "backup.directory";
2131
+ if (requiresPositive && value <= 0) {
2132
+ return { valid: false, error: "Value must be a positive integer" };
2133
+ }
2134
+ if (!requiresPositive && value < 0) {
2135
+ return { valid: false, error: "Value must be a non-negative integer" };
2136
+ }
2137
+ return { valid: true, value };
2138
+ }
2139
+ function validateBoolean(value) {
2140
+ if (typeof value !== "boolean") {
2141
+ return { valid: false, error: `Expected boolean, received ${typeof value}` };
2142
+ }
2143
+ return { valid: true, value };
2144
+ }
2145
+ function validateEnum(keyInfo, value) {
2146
+ if (typeof value !== "string") {
2147
+ return { valid: false, error: `Expected string, received ${typeof value}` };
2148
+ }
2149
+ const allowedValues = keyInfo.enumValues ?? [];
2150
+ if (!allowedValues.includes(value)) {
2151
+ return {
2152
+ valid: false,
2153
+ error: `Invalid value '${value}'. Expected one of: ${allowedValues.join(", ")}`
2154
+ };
2155
+ }
2156
+ return { valid: true, value };
2157
+ }
2158
+ function validateStringArray(value) {
2159
+ if (!Array.isArray(value)) {
2160
+ return { valid: false, error: "Expected array" };
2161
+ }
2162
+ for (const item of value) {
2163
+ if (typeof item !== "string") {
2164
+ return { valid: false, error: "All array elements must be strings" };
2165
+ }
2166
+ }
2167
+ return { valid: true, value };
2168
+ }
2169
+ function parseValueForKey(key, stringValue) {
2170
+ const keyInfo = getConfigKeyInfo(key);
2171
+ if (!keyInfo) {
2172
+ return null;
2173
+ }
2174
+ switch (keyInfo.type) {
2175
+ case "string":
2176
+ case "enum":
2177
+ return stringValue;
2178
+ case "integer": {
2179
+ const num = Number(stringValue);
2180
+ if (Number.isNaN(num)) {
2181
+ return stringValue;
2182
+ }
2183
+ return num;
2184
+ }
2185
+ case "boolean":
2186
+ if (stringValue === "true") return true;
2187
+ if (stringValue === "false") return false;
2188
+ return stringValue;
2189
+ // Let validation catch the error
2190
+ case "string[]":
2191
+ return stringValue.split(",").map((s) => s.trim());
2192
+ default:
2193
+ return stringValue;
2194
+ }
2195
+ }
2196
+ function getConfigEditTarget(options, context) {
2197
+ const cwd = process.cwd();
2198
+ let path2;
2199
+ if (options.local) {
2200
+ path2 = join(cwd, getDefaultCurrentDirConfigFilename());
2201
+ } else {
2202
+ path2 = getDefaultUserConfigPath();
2203
+ }
2204
+ return {
2205
+ path: path2,
2206
+ exists: existsSync(path2)
2207
+ };
2208
+ }
2209
+ function createConfigTemplate() {
2210
+ return `# Reference Manager Configuration
2211
+ # Documentation: https://github.com/ncukondo/reference-manager#configuration
2212
+
2213
+ # library = "~/.local/share/reference-manager/library.json"
2214
+ # log_level = "info" # silent, info, debug
2215
+
2216
+ [backup]
2217
+ # max_generations = 50
2218
+ # max_age_days = 365
2219
+ # directory = "~/.cache/reference-manager/backups"
2220
+
2221
+ [server]
2222
+ # auto_start = false
2223
+ # auto_stop_minutes = 0
2224
+
2225
+ [citation]
2226
+ # default_style = "apa"
2227
+ # default_locale = "en-US"
2228
+ # default_format = "text" # text, html, rtf
2229
+ # csl_directory = ["~/.local/share/reference-manager/csl"]
2230
+
2231
+ [pubmed]
2232
+ # email = ""
2233
+ # api_key = ""
2234
+
2235
+ [fulltext]
2236
+ # directory = "~/.local/share/reference-manager/fulltext"
2237
+
2238
+ [cli]
2239
+ # default_limit = 0 # 0 = unlimited
2240
+ # default_sort = "updated" # created, updated, published, author, title
2241
+ # default_order = "desc" # asc, desc
2242
+
2243
+ [cli.tui]
2244
+ # limit = 20
2245
+ # debounce_ms = 200
2246
+
2247
+ [cli.edit]
2248
+ # default_format = "yaml" # yaml, json
2249
+
2250
+ [mcp]
2251
+ # default_limit = 20
2252
+ `;
2253
+ }
2254
+ function getConfigValue(config2, key, options) {
2255
+ if (!isValidConfigKey(key)) {
2256
+ return { found: false, error: `Unknown configuration key: '${key}'` };
2257
+ }
2258
+ if (options.envOverride !== void 0 && !options.configOnly) {
2259
+ return {
2260
+ found: true,
2261
+ value: options.envOverride,
2262
+ fromEnv: true
2263
+ };
2264
+ }
2265
+ const internalPath = toInternalPath(key);
2266
+ const value = getNestedValue(config2, internalPath);
2267
+ if (value === void 0 || value === null) {
2268
+ return { found: false, error: `Value for '${key}' is not set` };
2269
+ }
2270
+ return { found: true, value };
2271
+ }
2272
+ function getNestedValue(obj, path2) {
2273
+ let current = obj;
2274
+ for (const segment of path2) {
2275
+ if (current === null || current === void 0) {
2276
+ return void 0;
2277
+ }
2278
+ if (typeof current !== "object") {
2279
+ return void 0;
2280
+ }
2281
+ current = current[segment];
2282
+ }
2283
+ return current;
2284
+ }
2285
+ function formatValue(value) {
2286
+ if (Array.isArray(value)) {
2287
+ return value.join(",");
2288
+ }
2289
+ if (typeof value === "object" && value !== null) {
2290
+ return JSON.stringify(value);
2291
+ }
2292
+ return String(value);
2293
+ }
2294
+ const get = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
2295
+ __proto__: null,
2296
+ formatValue,
2297
+ getConfigValue
2298
+ }, Symbol.toStringTag, { value: "Module" }));
2299
+ function listConfigKeys(options) {
2300
+ const keys = getAllConfigKeys(options.section);
2301
+ if (keys.length === 0) {
2302
+ return "";
2303
+ }
2304
+ const entries = keys.map((key) => {
2305
+ const info = getConfigKeyInfo(key);
2306
+ if (!info) {
2307
+ return null;
2308
+ }
2309
+ return {
2310
+ key: info.key,
2311
+ type: info.type,
2312
+ description: info.description
2313
+ };
2314
+ }).filter((e) => e !== null);
2315
+ const maxKeyWidth = Math.max(...entries.map((e) => e.key.length));
2316
+ const maxTypeWidth = Math.max(...entries.map((e) => e.type.length));
2317
+ const lines = entries.map((entry) => {
2318
+ const keyPadded = entry.key.padEnd(maxKeyWidth);
2319
+ const typePadded = entry.type.padEnd(maxTypeWidth);
2320
+ return `${keyPadded} ${typePadded} ${entry.description}`;
2321
+ });
2322
+ return lines.join("\n");
2323
+ }
2324
+ function getExistenceStatus(path2) {
2325
+ return existsSync(path2) ? "exists" : "not found";
2326
+ }
2327
+ function showConfigPaths(options, context) {
2328
+ const cwd = process.cwd();
2329
+ const userConfigPath = getDefaultUserConfigPath();
2330
+ const localConfigPath = join(cwd, getDefaultCurrentDirConfigFilename());
2331
+ const envConfigPath = process.env.REFERENCE_MANAGER_CONFIG;
2332
+ if (options.user) {
2333
+ return userConfigPath;
2334
+ }
2335
+ if (options.local) {
2336
+ return localConfigPath;
2337
+ }
2338
+ const lines = [];
2339
+ lines.push(`User: ${userConfigPath} (${getExistenceStatus(userConfigPath)})`);
2340
+ lines.push(`Local: ${localConfigPath} (${getExistenceStatus(localConfigPath)})`);
2341
+ if (envConfigPath) {
2342
+ lines.push(
2343
+ `Env: ${envConfigPath} (${getExistenceStatus(envConfigPath)}) (REFERENCE_MANAGER_CONFIG)`
2344
+ );
2345
+ }
2346
+ return lines.join("\n");
2347
+ }
2348
+ function serializeToTOML$1(obj) {
2349
+ return stringify$2(obj);
2350
+ }
2351
+ function parseKeyPath(key) {
2352
+ return key.split(".");
2353
+ }
2354
+ function setNestedValue(obj, keyPath, value) {
2355
+ if (keyPath.length === 0) {
2356
+ return;
2357
+ }
2358
+ let current = obj;
2359
+ for (let i = 0; i < keyPath.length - 1; i++) {
2360
+ const segment = keyPath[i];
2361
+ if (!(segment in current) || typeof current[segment] !== "object") {
2362
+ current[segment] = {};
2363
+ }
2364
+ current = current[segment];
2365
+ }
2366
+ const finalKey = keyPath[keyPath.length - 1];
2367
+ current[finalKey] = value;
2368
+ }
2369
+ function removeNestedValue(obj, keyPath) {
2370
+ if (keyPath.length === 0) {
2371
+ return false;
2372
+ }
2373
+ const firstKey = keyPath[0];
2374
+ if (keyPath.length === 1) {
2375
+ if (firstKey in obj) {
2376
+ delete obj[firstKey];
2377
+ return true;
2378
+ }
2379
+ return false;
2380
+ }
2381
+ if (!(firstKey in obj) || typeof obj[firstKey] !== "object") {
2382
+ return false;
2383
+ }
2384
+ const nested = obj[firstKey];
2385
+ const removed = removeNestedValue(nested, keyPath.slice(1));
2386
+ if (removed && Object.keys(nested).length === 0) {
2387
+ delete obj[firstKey];
2388
+ }
2389
+ return removed;
2390
+ }
2391
+ function loadExistingTOML(filePath) {
2392
+ if (!existsSync(filePath)) {
2393
+ return {};
2394
+ }
2395
+ try {
2396
+ const content = readFileSync(filePath, "utf-8");
2397
+ return parse$2(content);
2398
+ } catch {
2399
+ return {};
2400
+ }
2401
+ }
2402
+ async function writeTOMLValue(filePath, key, value) {
2403
+ const dir = dirname(filePath);
2404
+ await mkdir(dir, { recursive: true });
2405
+ const obj = loadExistingTOML(filePath);
2406
+ const keyPath = parseKeyPath(key);
2407
+ setNestedValue(obj, keyPath, value);
2408
+ const content = serializeToTOML$1(obj);
2409
+ writeFileSync(filePath, content, "utf-8");
2410
+ }
2411
+ async function removeTOMLKey(filePath, key) {
2412
+ if (!existsSync(filePath)) {
2413
+ return;
2414
+ }
2415
+ const obj = loadExistingTOML(filePath);
2416
+ const keyPath = parseKeyPath(key);
2417
+ const removed = removeNestedValue(obj, keyPath);
2418
+ if (removed) {
2419
+ const content = serializeToTOML$1(obj);
2420
+ writeFileSync(filePath, content, "utf-8");
2421
+ }
2422
+ }
2423
+ async function setConfigValue(configPath, key, value, options = {}) {
2424
+ if (!isValidConfigKey(key)) {
2425
+ return { success: false, error: `Unknown configuration key: '${key}'` };
2426
+ }
2427
+ const validation2 = validateConfigValue(key, value);
2428
+ if (!validation2.valid) {
2429
+ return { success: false, error: validation2.error ?? "Validation failed" };
2430
+ }
2431
+ try {
2432
+ await writeTOMLValue(configPath, key, value);
2433
+ } catch (error) {
2434
+ return {
2435
+ success: false,
2436
+ error: `Failed to write config: ${error instanceof Error ? error.message : String(error)}`
2437
+ };
2438
+ }
2439
+ if (options.envOverrideInfo?.value !== void 0 && options.envOverrideInfo?.value !== null) {
2440
+ const warning = `Warning: '${key}' is overridden by environment variable ${options.envOverrideInfo.envVar}
2441
+ Environment value: ${options.envOverrideInfo.value}
2442
+ Config file value: ${value} (saved but inactive)
2443
+
2444
+ The environment variable takes precedence. To use the config file value,
2445
+ unset the environment variable: unset ${options.envOverrideInfo.envVar}`;
2446
+ return { success: true, warning };
2447
+ }
2448
+ return { success: true };
2449
+ }
2450
+ function toSnakeCaseConfig(config2) {
2451
+ const result = {};
2452
+ result.library = config2.library;
2453
+ result.log_level = config2.logLevel;
2454
+ result.backup = {
2455
+ max_generations: config2.backup.maxGenerations,
2456
+ max_age_days: config2.backup.maxAgeDays,
2457
+ directory: config2.backup.directory
2458
+ };
2459
+ result.watch = {
2460
+ debounce_ms: config2.watch.debounceMs,
2461
+ poll_interval_ms: config2.watch.pollIntervalMs,
2462
+ retry_interval_ms: config2.watch.retryIntervalMs,
2463
+ max_retries: config2.watch.maxRetries
2464
+ };
2465
+ result.server = {
2466
+ auto_start: config2.server.autoStart,
2467
+ auto_stop_minutes: config2.server.autoStopMinutes
2468
+ };
2469
+ result.citation = {
2470
+ default_style: config2.citation.defaultStyle,
2471
+ csl_directory: config2.citation.cslDirectory,
2472
+ default_locale: config2.citation.defaultLocale,
2473
+ default_format: config2.citation.defaultFormat
2474
+ };
2475
+ result.pubmed = {
2476
+ email: config2.pubmed.email,
2477
+ api_key: config2.pubmed.apiKey
2478
+ };
2479
+ result.fulltext = {
2480
+ directory: config2.fulltext.directory
2481
+ };
2482
+ result.cli = {
2483
+ default_limit: config2.cli.defaultLimit,
2484
+ default_sort: config2.cli.defaultSort,
2485
+ default_order: config2.cli.defaultOrder,
2486
+ tui: {
2487
+ limit: config2.cli.tui.limit,
2488
+ debounce_ms: config2.cli.tui.debounceMs
2489
+ },
2490
+ edit: {
2491
+ default_format: config2.cli.edit.defaultFormat
2492
+ }
2493
+ };
2494
+ result.mcp = {
2495
+ default_limit: config2.mcp.defaultLimit
2496
+ };
2497
+ return result;
2498
+ }
2499
+ function filterSection(config2, section) {
2500
+ const sectionValue = config2[section];
2501
+ if (sectionValue === void 0) {
2502
+ return {};
2503
+ }
2504
+ return { [section]: sectionValue };
2505
+ }
2506
+ function serializeToTOML(config2, withSources) {
2507
+ const lines = [];
2508
+ if (withSources) {
2509
+ lines.push("# Effective configuration");
2510
+ lines.push("# Source priority: CLI > current dir > env > user > default");
2511
+ lines.push("");
2512
+ }
2513
+ for (const [key, value] of Object.entries(config2)) {
2514
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2515
+ lines.push(formatTOMLValue(key, value));
2516
+ }
2517
+ }
2518
+ const hasTopLevel = Object.values(config2).some(
2519
+ (v) => typeof v !== "object" || v === null || Array.isArray(v)
2520
+ );
2521
+ if (hasTopLevel) {
2522
+ lines.push("");
2523
+ }
2524
+ for (const [key, value] of Object.entries(config2)) {
2525
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2526
+ serializeSection(lines, key, value);
2527
+ }
2528
+ }
2529
+ return lines.join("\n");
2530
+ }
2531
+ function serializeSection(lines, prefix, obj) {
2532
+ const scalarEntries = [];
2533
+ const objectEntries = [];
2534
+ for (const [key, value] of Object.entries(obj)) {
2535
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
2536
+ objectEntries.push([key, value]);
2537
+ } else {
2538
+ scalarEntries.push([key, value]);
2539
+ }
2540
+ }
2541
+ if (scalarEntries.length > 0) {
2542
+ lines.push(`[${prefix}]`);
2543
+ for (const [key, value] of scalarEntries) {
2544
+ lines.push(formatTOMLValue(key, value));
2545
+ }
2546
+ lines.push("");
2547
+ }
2548
+ for (const [key, value] of objectEntries) {
2549
+ serializeSection(lines, `${prefix}.${key}`, value);
2550
+ }
2551
+ }
2552
+ function formatTOMLValue(key, value) {
2553
+ if (typeof value === "string") {
2554
+ return `${key} = "${value}"`;
2555
+ }
2556
+ if (typeof value === "number" || typeof value === "boolean") {
2557
+ return `${key} = ${value}`;
2558
+ }
2559
+ if (Array.isArray(value)) {
2560
+ const items2 = value.map((v) => typeof v === "string" ? `"${v}"` : String(v));
2561
+ return `${key} = [ ${items2.join(", ")} ]`;
2562
+ }
2563
+ if (value === void 0 || value === null) {
2564
+ return `# ${key} = (not set)`;
2565
+ }
2566
+ return `${key} = ${JSON.stringify(value)}`;
2567
+ }
2568
+ function showConfig(config2, options) {
2569
+ let snakeCaseConfig = toSnakeCaseConfig(config2);
2570
+ if (options.section) {
2571
+ snakeCaseConfig = filterSection(snakeCaseConfig, options.section);
2572
+ }
2573
+ if (options.json) {
2574
+ return JSON.stringify(snakeCaseConfig, null, 2);
2575
+ }
2576
+ return serializeToTOML(snakeCaseConfig, options.sources ?? false);
2577
+ }
2578
+ async function unsetConfigValue(configPath, key) {
2579
+ if (!isValidConfigKey(key)) {
2580
+ return { success: false, error: `Unknown configuration key: '${key}'` };
2581
+ }
2582
+ try {
2583
+ await removeTOMLKey(configPath, key);
2584
+ } catch (error) {
2585
+ return {
2586
+ success: false,
2587
+ error: `Failed to update config: ${error instanceof Error ? error.message : String(error)}`
2588
+ };
2589
+ }
2590
+ return { success: true };
2591
+ }
2592
+ function resolveWriteTarget(options) {
2593
+ const { local, user, cwd, userConfigPath } = options;
2594
+ const localConfigPath = join(cwd, getDefaultCurrentDirConfigFilename());
2595
+ if (local) {
2596
+ return localConfigPath;
2597
+ }
2598
+ if (user) {
2599
+ return userConfigPath;
2600
+ }
2601
+ if (existsSync(localConfigPath)) {
2602
+ return localConfigPath;
2603
+ }
2604
+ return userConfigPath;
2605
+ }
2606
+ function createTempFile(content, format2) {
2607
+ const timestamp2 = Date.now();
2608
+ const extension = format2 === "yaml" ? ".yaml" : ".json";
2609
+ const fileName = `ref-edit-${timestamp2}${extension}`;
2610
+ const filePath = path.join(os.tmpdir(), fileName);
2611
+ fs.writeFileSync(filePath, content, "utf-8");
2612
+ return filePath;
2613
+ }
2614
+ function openEditor(editor, filePath) {
2615
+ const command = `${editor} "${filePath}"`;
2616
+ const result = spawnSync(command, {
2617
+ stdio: "inherit",
2618
+ shell: true
2619
+ });
2620
+ return result.status ?? 1;
2621
+ }
2622
+ function readTempFile(filePath) {
2623
+ return fs.readFileSync(filePath, "utf-8");
2624
+ }
2625
+ function deleteTempFile(filePath) {
2626
+ try {
2627
+ fs.unlinkSync(filePath);
2628
+ } catch {
2629
+ }
2630
+ }
2631
+ function resolveEditor(platform) {
2632
+ const visual = process.env.VISUAL;
2633
+ if (visual && visual.trim() !== "") {
2634
+ return visual;
2635
+ }
2636
+ const editor = process.env.EDITOR;
2637
+ if (editor && editor.trim() !== "") {
2638
+ return editor;
2639
+ }
2640
+ const currentPlatform = process.platform;
2641
+ return currentPlatform === "win32" ? "notepad" : "vi";
2642
+ }
2643
+ function registerConfigCommand(program) {
2644
+ const configCmd = program.command("config").description("Manage configuration settings");
2645
+ configCmd.command("show").description("Display effective configuration").option("-o, --output <format>", "Output format: text|json").option("--section <name>", "Show only a specific section").option("--sources", "Include source information for each value").action(async (options) => {
2646
+ try {
2647
+ const config2 = loadConfig();
2648
+ const output = showConfig(config2, {
2649
+ json: options.output === "json",
2650
+ section: options.section,
2651
+ sources: options.sources
2652
+ });
2653
+ process.stdout.write(`${output}
2654
+ `);
2655
+ process.exit(0);
2656
+ } catch (error) {
2657
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
2658
+ `);
2659
+ process.exit(1);
2660
+ }
2661
+ });
2662
+ configCmd.command("get <key>").description("Get a specific configuration value").option("--config-only", "Return only the config file value (ignore env vars)").action(async (key, options) => {
2663
+ try {
2664
+ const config2 = loadConfig();
2665
+ const envOverrideValue = options.configOnly ? null : getEnvOverrideInfo(key)?.value ?? null;
2666
+ const getOptions = {
2667
+ configOnly: options.configOnly
2668
+ };
2669
+ if (envOverrideValue !== null) {
2670
+ getOptions.envOverride = envOverrideValue;
2671
+ }
2672
+ const result = getConfigValue(config2, key, getOptions);
2673
+ if (result.found) {
2674
+ const { formatValue: formatValue2 } = await Promise.resolve().then(() => get);
2675
+ process.stdout.write(`${formatValue2(result.value)}
2676
+ `);
2677
+ process.exit(0);
2678
+ } else {
2679
+ process.exit(1);
2680
+ }
2681
+ } catch (error) {
2682
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
2683
+ `);
2684
+ process.exit(1);
2685
+ }
2686
+ });
2687
+ configCmd.command("set <key> <value>").description("Set a configuration value").option("--local", "Write to current directory config (create if not exists)").option("--user", "Write to user config (ignore local config even if exists)").action(async (key, value, options) => {
2688
+ try {
2689
+ const configPath = resolveWriteTarget({
2690
+ local: options.local,
2691
+ user: options.user,
2692
+ cwd: process.cwd(),
2693
+ userConfigPath: getDefaultUserConfigPath()
2694
+ });
2695
+ const parsedValue = parseValueForKey(key, value);
2696
+ const envOverrideInfo = getEnvOverrideInfo(key);
2697
+ const setOptions = envOverrideInfo ? { envOverrideInfo } : {};
2698
+ const result = await setConfigValue(configPath, key, parsedValue, setOptions);
2699
+ if (!result.success) {
2700
+ process.stderr.write(`Error: ${result.error}
2701
+ `);
2702
+ process.exit(1);
2703
+ }
2704
+ if (result.warning) {
2705
+ process.stderr.write(`${result.warning}
2706
+ `);
2707
+ }
2708
+ process.exit(0);
2709
+ } catch (error) {
2710
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
2711
+ `);
2712
+ process.exit(1);
2713
+ }
2714
+ });
2715
+ configCmd.command("unset <key>").description("Remove a configuration value (revert to default)").option("--local", "Remove from current directory config").option("--user", "Remove from user config (ignore local config even if exists)").action(async (key, options) => {
2716
+ try {
2717
+ const configPath = resolveWriteTarget({
2718
+ local: options.local,
2719
+ user: options.user,
2720
+ cwd: process.cwd(),
2721
+ userConfigPath: getDefaultUserConfigPath()
2722
+ });
2723
+ const result = await unsetConfigValue(configPath, key);
2724
+ if (!result.success) {
2725
+ process.stderr.write(`Error: ${result.error}
2726
+ `);
2727
+ process.exit(1);
2728
+ }
2729
+ process.exit(0);
2730
+ } catch (error) {
2731
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
2732
+ `);
2733
+ process.exit(1);
2734
+ }
2735
+ });
2736
+ configCmd.command("keys").description("List all available configuration keys").option("--section <name>", "List keys only in a specific section").action(async (options) => {
2737
+ try {
2738
+ const output = listConfigKeys({ section: options.section });
2739
+ if (output) {
2740
+ process.stdout.write(`${output}
2741
+ `);
2742
+ }
2743
+ process.exit(0);
2744
+ } catch (error) {
2745
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
2746
+ `);
2747
+ process.exit(1);
752
2748
  }
753
- const output = formatCiteOutput(result);
754
- if (output) {
2749
+ });
2750
+ configCmd.command("path").description("Show configuration file paths").option("--user", "Show only user config path").option("--local", "Show only local config path").action(async (options) => {
2751
+ try {
2752
+ const output = showConfigPaths({ user: options.user, local: options.local });
755
2753
  process.stdout.write(`${output}
756
2754
  `);
757
- }
758
- const errors2 = formatCiteErrors(result);
759
- if (errors2) {
760
- process.stderr.write(`${errors2}
2755
+ process.exit(0);
2756
+ } catch (error) {
2757
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
761
2758
  `);
2759
+ process.exit(1);
762
2760
  }
763
- process.exit(getCiteExitCode(result));
764
- } catch (error) {
765
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
2761
+ });
2762
+ configCmd.command("edit").description("Open configuration file in editor").option("--local", "Edit current directory config").action(async (options) => {
2763
+ try {
2764
+ if (!process.stdin.isTTY) {
2765
+ process.stderr.write("Error: config edit requires a terminal (TTY)\n");
2766
+ process.exit(1);
2767
+ }
2768
+ const target = getConfigEditTarget({ local: options.local });
2769
+ if (!target.exists) {
2770
+ const template = createConfigTemplate();
2771
+ mkdirSync(dirname(target.path), { recursive: true });
2772
+ writeFileSync(target.path, template, "utf-8");
2773
+ }
2774
+ const editor = resolveEditor();
2775
+ const exitCode = openEditor(editor, target.path);
2776
+ process.exit(exitCode);
2777
+ } catch (error) {
2778
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
766
2779
  `);
767
- process.exit(4);
768
- }
769
- }
770
- function createTempFile(content, format2) {
771
- const timestamp2 = Date.now();
772
- const extension = format2 === "yaml" ? ".yaml" : ".json";
773
- const fileName = `ref-edit-${timestamp2}${extension}`;
774
- const filePath = path.join(os.tmpdir(), fileName);
775
- fs.writeFileSync(filePath, content, "utf-8");
776
- return filePath;
777
- }
778
- function openEditor(editor, filePath) {
779
- const command = `${editor} "${filePath}"`;
780
- const result = spawnSync(command, {
781
- stdio: "inherit",
782
- shell: true
2780
+ process.exit(1);
2781
+ }
783
2782
  });
784
- return result.status ?? 1;
785
- }
786
- function readTempFile(filePath) {
787
- return fs.readFileSync(filePath, "utf-8");
788
- }
789
- function deleteTempFile(filePath) {
790
- try {
791
- fs.unlinkSync(filePath);
792
- } catch {
793
- }
794
2783
  }
795
2784
  function datePartsToIso(dateParts) {
796
2785
  if (!dateParts || dateParts.length === 0 || !dateParts[0]) {
@@ -3576,18 +5565,6 @@ ${yamlContent}`);
3576
5565
  }
3577
5566
  return sections.join("\n---\n\n");
3578
5567
  }
3579
- function resolveEditor(platform) {
3580
- const visual = process.env.VISUAL;
3581
- if (visual && visual.trim() !== "") {
3582
- return visual;
3583
- }
3584
- const editor = process.env.EDITOR;
3585
- if (editor && editor.trim() !== "") {
3586
- return editor;
3587
- }
3588
- const currentPlatform = process.platform;
3589
- return currentPlatform === "win32" ? "notepad" : "vi";
3590
- }
3591
5568
  function serialize(items2, format2) {
3592
5569
  return format2 === "yaml" ? serializeToYaml(items2) : serializeToJson(items2);
3593
5570
  }
@@ -3741,12 +5718,12 @@ function formatEditOutput(result) {
3741
5718
  return lines.join("\n");
3742
5719
  }
3743
5720
  async function executeInteractiveEdit(options, context, config2) {
3744
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
5721
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
3745
5722
  const allReferences = await context.library.getAll();
3746
5723
  const identifiers = await selectReferencesOrExit(
3747
5724
  allReferences,
3748
5725
  { multiSelect: true },
3749
- config2.cli.interactive
5726
+ config2.cli.tui
3750
5727
  );
3751
5728
  const format2 = options.format ?? config2.cli.edit.defaultFormat;
3752
5729
  return executeEditCommand(
@@ -6977,7 +8954,7 @@ async function executeExport(options, context) {
6977
8954
  return { items: items2, notFound };
6978
8955
  }
6979
8956
  function formatExportOutput(result, options) {
6980
- const format2 = options.format ?? "json";
8957
+ const format2 = options.output ?? "json";
6981
8958
  const singleIdRequest = (options.ids?.length ?? 0) === 1 && !options.all && !options.search;
6982
8959
  const data = result.items.length === 1 && singleIdRequest ? result.items[0] : result.items;
6983
8960
  if (format2 === "json") {
@@ -6994,183 +8971,6 @@ function formatExportOutput(result, options) {
6994
8971
  function getExportExitCode(result) {
6995
8972
  return result.notFound.length > 0 ? 1 : 0;
6996
8973
  }
6997
- const FULLTEXT_EXTENSIONS = {
6998
- pdf: ".pdf",
6999
- markdown: ".md"
7000
- };
7001
- function generateFulltextFilename(item, type2) {
7002
- const uuid2 = item.custom?.uuid;
7003
- if (!uuid2) {
7004
- throw new Error("Missing uuid in custom field");
7005
- }
7006
- const parts = [item.id];
7007
- if (item.PMID && item.PMID.length > 0) {
7008
- parts.push(`PMID${item.PMID}`);
7009
- }
7010
- parts.push(uuid2);
7011
- return parts.join("-") + FULLTEXT_EXTENSIONS[type2];
7012
- }
7013
- class FulltextIOError extends Error {
7014
- constructor(message, cause) {
7015
- super(message);
7016
- this.cause = cause;
7017
- this.name = "FulltextIOError";
7018
- }
7019
- }
7020
- class FulltextNotAttachedError extends Error {
7021
- constructor(itemId, type2) {
7022
- super(`No ${type2} attached to reference ${itemId}`);
7023
- this.itemId = itemId;
7024
- this.type = type2;
7025
- this.name = "FulltextNotAttachedError";
7026
- }
7027
- }
7028
- class FulltextManager {
7029
- constructor(fulltextDirectory) {
7030
- this.fulltextDirectory = fulltextDirectory;
7031
- }
7032
- /**
7033
- * Ensure the fulltext directory exists
7034
- */
7035
- async ensureDirectory() {
7036
- await mkdir(this.fulltextDirectory, { recursive: true });
7037
- }
7038
- /**
7039
- * Attach a file to a reference
7040
- */
7041
- async attachFile(item, sourcePath, type2, options) {
7042
- const { move = false, force = false } = options ?? {};
7043
- const newFilename = generateFulltextFilename(item, type2);
7044
- this.validateSourceFile(sourcePath);
7045
- const existingFilename = this.getExistingFilename(item, type2);
7046
- if (existingFilename && !force) {
7047
- return {
7048
- filename: newFilename,
7049
- existingFile: existingFilename,
7050
- overwritten: false
7051
- };
7052
- }
7053
- await this.ensureDirectory();
7054
- const deletedOldFile = await this.deleteOldFileIfNeeded(existingFilename, newFilename, force);
7055
- const destPath = join(this.fulltextDirectory, newFilename);
7056
- await this.copyOrMoveFile(sourcePath, destPath, move);
7057
- const result = {
7058
- filename: newFilename,
7059
- overwritten: existingFilename !== void 0
7060
- };
7061
- if (deletedOldFile) {
7062
- result.deletedOldFile = deletedOldFile;
7063
- }
7064
- return result;
7065
- }
7066
- /**
7067
- * Validate that source file exists
7068
- */
7069
- validateSourceFile(sourcePath) {
7070
- if (!existsSync(sourcePath)) {
7071
- throw new FulltextIOError(`Source file not found: ${sourcePath}`);
7072
- }
7073
- }
7074
- /**
7075
- * Delete old file if force mode and filename changed
7076
- * @returns Deleted filename or undefined
7077
- */
7078
- async deleteOldFileIfNeeded(existingFilename, newFilename, force) {
7079
- if (!force || !existingFilename || existingFilename === newFilename) {
7080
- return void 0;
7081
- }
7082
- const oldPath = join(this.fulltextDirectory, existingFilename);
7083
- try {
7084
- await unlink(oldPath);
7085
- } catch {
7086
- }
7087
- return existingFilename;
7088
- }
7089
- /**
7090
- * Copy or move file to destination
7091
- */
7092
- async copyOrMoveFile(sourcePath, destPath, move) {
7093
- try {
7094
- if (move) {
7095
- await rename(sourcePath, destPath);
7096
- } else {
7097
- await copyFile(sourcePath, destPath);
7098
- }
7099
- } catch (error) {
7100
- const operation = move ? "move" : "copy";
7101
- throw new FulltextIOError(
7102
- `Failed to ${operation} file to ${destPath}`,
7103
- error instanceof Error ? error : void 0
7104
- );
7105
- }
7106
- }
7107
- /**
7108
- * Get the full path for an attached file
7109
- * @returns Full path or null if not attached
7110
- */
7111
- getFilePath(item, type2) {
7112
- const filename = this.getExistingFilename(item, type2);
7113
- if (!filename) {
7114
- return null;
7115
- }
7116
- return join(this.fulltextDirectory, filename);
7117
- }
7118
- /**
7119
- * Detach a file from a reference
7120
- */
7121
- async detachFile(item, type2, options) {
7122
- const { delete: deleteFile = false } = options ?? {};
7123
- const filename = this.getExistingFilename(item, type2);
7124
- if (!filename) {
7125
- throw new FulltextNotAttachedError(item.id, type2);
7126
- }
7127
- if (deleteFile) {
7128
- const filePath = join(this.fulltextDirectory, filename);
7129
- try {
7130
- await unlink(filePath);
7131
- } catch {
7132
- }
7133
- }
7134
- return {
7135
- filename,
7136
- deleted: deleteFile
7137
- };
7138
- }
7139
- /**
7140
- * Get list of attached fulltext types
7141
- */
7142
- getAttachedTypes(item) {
7143
- const types2 = [];
7144
- const fulltext = item.custom?.fulltext;
7145
- if (fulltext?.pdf) {
7146
- types2.push("pdf");
7147
- }
7148
- if (fulltext?.markdown) {
7149
- types2.push("markdown");
7150
- }
7151
- return types2;
7152
- }
7153
- /**
7154
- * Check if item has attachment
7155
- * @param type Optional type to check; if omitted, checks for any attachment
7156
- */
7157
- hasAttachment(item, type2) {
7158
- if (type2) {
7159
- return this.getExistingFilename(item, type2) !== void 0;
7160
- }
7161
- return this.getAttachedTypes(item).length > 0;
7162
- }
7163
- /**
7164
- * Get existing filename from item metadata
7165
- */
7166
- getExistingFilename(item, type2) {
7167
- const fulltext = item.custom?.fulltext;
7168
- if (!fulltext) {
7169
- return void 0;
7170
- }
7171
- return fulltext[type2];
7172
- }
7173
- }
7174
8974
  function detectType(filePath) {
7175
8975
  const ext = extname(filePath).toLowerCase();
7176
8976
  if (ext === ".pdf") return "pdf";
@@ -7195,8 +8995,8 @@ function resolveFileType(explicitType, filePath, stdinContent) {
7195
8995
  function prepareStdinSource(stdinContent, fileType) {
7196
8996
  try {
7197
8997
  const tempDir = mkdtempSync(join(tmpdir(), "refmgr-"));
7198
- const ext = fileType === "pdf" ? ".pdf" : ".md";
7199
- const sourcePath = join(tempDir, `stdin${ext}`);
8998
+ const ext = formatToExtension(fileType);
8999
+ const sourcePath = join(tempDir, `stdin.${ext}`);
7200
9000
  writeFileSync(sourcePath, stdinContent);
7201
9001
  return { sourcePath, tempDir };
7202
9002
  } catch (error) {
@@ -7211,13 +9011,6 @@ async function cleanupTempDir(tempDir) {
7211
9011
  });
7212
9012
  }
7213
9013
  }
7214
- function buildNewFulltext(currentFulltext, fileType, filename) {
7215
- const newFulltext = {};
7216
- if (currentFulltext.pdf) newFulltext.pdf = currentFulltext.pdf;
7217
- if (currentFulltext.markdown) newFulltext.markdown = currentFulltext.markdown;
7218
- newFulltext[fileType] = filename;
7219
- return newFulltext;
7220
- }
7221
9014
  function prepareSourcePath(filePath, stdinContent, fileType) {
7222
9015
  if (stdinContent) {
7223
9016
  return prepareStdinSource(stdinContent, fileType);
@@ -7227,12 +9020,26 @@ function prepareSourcePath(filePath, stdinContent, fileType) {
7227
9020
  }
7228
9021
  return { sourcePath: filePath };
7229
9022
  }
7230
- async function performAttach(manager, item, sourcePath, fileType, move, force) {
7231
- const attachOptions = {
7232
- ...move !== void 0 && { move },
7233
- ...force !== void 0 && { force }
9023
+ function convertResult(result, fileType) {
9024
+ if (result.success) {
9025
+ return {
9026
+ success: true,
9027
+ filename: result.filename,
9028
+ type: fileType,
9029
+ overwritten: result.overwritten
9030
+ };
9031
+ }
9032
+ if (result.requiresConfirmation) {
9033
+ return {
9034
+ success: false,
9035
+ existingFile: result.existingFile,
9036
+ requiresConfirmation: true
9037
+ };
9038
+ }
9039
+ return {
9040
+ success: false,
9041
+ error: result.error
7234
9042
  };
7235
- return manager.attachFile(item, sourcePath, fileType, attachOptions);
7236
9043
  }
7237
9044
  async function fulltextAttach(library, options) {
7238
9045
  const {
@@ -7245,71 +9052,78 @@ async function fulltextAttach(library, options) {
7245
9052
  fulltextDirectory,
7246
9053
  stdinContent
7247
9054
  } = options;
7248
- const item = await library.find(identifier, { idType });
7249
- if (!item) {
7250
- return { success: false, error: `Reference '${identifier}' not found` };
7251
- }
7252
9055
  const fileTypeResult = resolveFileType(explicitType, filePath, stdinContent);
7253
9056
  if (typeof fileTypeResult === "object" && "error" in fileTypeResult) {
9057
+ const item = await library.find(identifier, { idType });
9058
+ if (!item) {
9059
+ return { success: false, error: `Reference '${identifier}' not found` };
9060
+ }
7254
9061
  return { success: false, error: fileTypeResult.error };
7255
9062
  }
7256
9063
  const fileType = fileTypeResult;
7257
9064
  const sourceResult = prepareSourcePath(filePath, stdinContent, fileType);
7258
9065
  if ("error" in sourceResult) {
9066
+ const item = await library.find(identifier, { idType });
9067
+ if (!item) {
9068
+ return { success: false, error: `Reference '${identifier}' not found` };
9069
+ }
7259
9070
  return { success: false, error: sourceResult.error };
7260
9071
  }
7261
9072
  const { sourcePath, tempDir } = sourceResult;
7262
- const manager = new FulltextManager(fulltextDirectory);
7263
9073
  try {
7264
- const result = await performAttach(manager, item, sourcePath, fileType, move, force);
7265
- if (result.existingFile && !result.overwritten) {
7266
- await cleanupTempDir(tempDir);
7267
- return { success: false, existingFile: result.existingFile, requiresConfirmation: true };
7268
- }
7269
- const newFulltext = buildNewFulltext(item.custom?.fulltext ?? {}, fileType, result.filename);
7270
- await updateReference(library, {
9074
+ const result = await addAttachment(library, {
7271
9075
  identifier,
7272
- updates: {
7273
- custom: { fulltext: newFulltext }
7274
- },
7275
- idType
9076
+ filePath: sourcePath,
9077
+ role: FULLTEXT_ROLE,
9078
+ move: move ?? false,
9079
+ force: force ?? false,
9080
+ idType,
9081
+ attachmentsDirectory: fulltextDirectory
7276
9082
  });
7277
9083
  await cleanupTempDir(tempDir);
7278
- return {
7279
- success: true,
7280
- filename: result.filename,
7281
- type: fileType,
7282
- overwritten: result.overwritten
7283
- };
9084
+ return convertResult(result, fileType);
7284
9085
  } catch (error) {
7285
9086
  await cleanupTempDir(tempDir);
7286
- if (error instanceof FulltextIOError) {
7287
- return { success: false, error: error.message };
7288
- }
7289
9087
  throw error;
7290
9088
  }
7291
9089
  }
7292
- async function getFileContent(manager, item, type2, identifier) {
7293
- const filePath = manager.getFilePath(item, type2);
7294
- if (!filePath) {
9090
+ function buildFilePath$1(attachmentsDirectory, directory, filename) {
9091
+ return normalizePathForOutput(join(attachmentsDirectory, directory, filename));
9092
+ }
9093
+ async function getFileContent(filePath) {
9094
+ const content = await readFile(filePath);
9095
+ return { success: true, content };
9096
+ }
9097
+ async function handleStdoutMode(attachments, type2, identifier, fulltextDirectory) {
9098
+ const file = findFulltextFile(attachments, type2);
9099
+ if (!file || !attachments?.directory) {
7295
9100
  return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
7296
9101
  }
9102
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
7297
9103
  try {
7298
- const content = await readFile(filePath);
7299
- return { success: true, content };
7300
- } catch (error) {
7301
- return {
7302
- success: false,
7303
- error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`
7304
- };
9104
+ return await getFileContent(filePath);
9105
+ } catch {
9106
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
9107
+ }
9108
+ }
9109
+ function getSingleTypePath(attachments, type2, identifier, fulltextDirectory) {
9110
+ const file = findFulltextFile(attachments, type2);
9111
+ if (!file || !attachments?.directory) {
9112
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
7305
9113
  }
9114
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
9115
+ const paths = {};
9116
+ paths[type2] = filePath;
9117
+ return { success: true, paths };
7306
9118
  }
7307
- function getFilePaths(manager, item, types2, identifier) {
9119
+ function getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier) {
7308
9120
  const paths = {};
7309
- for (const t of types2) {
7310
- const filePath = manager.getFilePath(item, t);
7311
- if (filePath) {
7312
- paths[t] = filePath;
9121
+ for (const file of fulltextFiles) {
9122
+ const ext = file.filename.split(".").pop() || "";
9123
+ const format2 = extensionToFormat(ext);
9124
+ if (format2) {
9125
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
9126
+ paths[format2] = filePath;
7313
9127
  }
7314
9128
  }
7315
9129
  if (Object.keys(paths).length === 0) {
@@ -7323,92 +9137,98 @@ async function fulltextGet(library, options) {
7323
9137
  if (!item) {
7324
9138
  return { success: false, error: `Reference '${identifier}' not found` };
7325
9139
  }
7326
- const manager = new FulltextManager(fulltextDirectory);
9140
+ const attachments = item.custom?.attachments;
7327
9141
  if (stdout2 && type2) {
7328
- return getFileContent(manager, item, type2, identifier);
9142
+ return handleStdoutMode(attachments, type2, identifier, fulltextDirectory);
9143
+ }
9144
+ const fulltextFiles = findFulltextFiles(attachments);
9145
+ if (fulltextFiles.length === 0) {
9146
+ return { success: false, error: `No fulltext attached to '${identifier}'` };
7329
9147
  }
7330
- const attachedTypes = type2 ? [type2] : manager.getAttachedTypes(item);
7331
- if (attachedTypes.length === 0) {
9148
+ if (type2) {
9149
+ return getSingleTypePath(attachments, type2, identifier, fulltextDirectory);
9150
+ }
9151
+ if (!attachments) {
7332
9152
  return { success: false, error: `No fulltext attached to '${identifier}'` };
7333
9153
  }
7334
- return getFilePaths(manager, item, attachedTypes, identifier);
9154
+ return getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier);
9155
+ }
9156
+ function getFilesToDetach(attachments, type2) {
9157
+ if (type2) {
9158
+ const file = findFulltextFile(attachments, type2);
9159
+ return file ? [file] : [];
9160
+ }
9161
+ return findFulltextFiles(attachments);
7335
9162
  }
7336
- async function performDetachOperations(manager, item, typesToDetach, deleteFile) {
9163
+ async function detachFiles(library, files, identifier, removeFiles, idType, fulltextDirectory) {
7337
9164
  const detached = [];
7338
9165
  const deleted = [];
7339
- for (const t of typesToDetach) {
7340
- const detachOptions = deleteFile ? { delete: deleteFile } : {};
7341
- const result = await manager.detachFile(item, t, detachOptions);
7342
- detached.push(t);
7343
- if (result.deleted) {
7344
- deleted.push(t);
9166
+ for (const file of files) {
9167
+ const result = await detachAttachment(library, {
9168
+ identifier,
9169
+ filename: file.filename,
9170
+ removeFiles: removeFiles ?? false,
9171
+ idType,
9172
+ attachmentsDirectory: fulltextDirectory
9173
+ });
9174
+ if (result.success) {
9175
+ const ext = file.filename.split(".").pop() || "";
9176
+ const format2 = extensionToFormat(ext);
9177
+ if (format2) {
9178
+ detached.push(format2);
9179
+ if (result.deleted.length > 0) {
9180
+ deleted.push(format2);
9181
+ }
9182
+ }
7345
9183
  }
7346
9184
  }
7347
9185
  return { detached, deleted };
7348
9186
  }
7349
- function buildRemainingFulltext(currentFulltext, detached) {
7350
- const newFulltext = {};
7351
- if (currentFulltext.pdf && !detached.includes("pdf")) {
7352
- newFulltext.pdf = currentFulltext.pdf;
9187
+ function buildResult(detached, deleted, identifier) {
9188
+ if (detached.length === 0) {
9189
+ return { success: false, error: `Failed to detach fulltext from '${identifier}'` };
7353
9190
  }
7354
- if (currentFulltext.markdown && !detached.includes("markdown")) {
7355
- newFulltext.markdown = currentFulltext.markdown;
9191
+ const result = { success: true, detached };
9192
+ if (deleted.length > 0) {
9193
+ result.deleted = deleted;
7356
9194
  }
7357
- return Object.keys(newFulltext).length > 0 ? newFulltext : void 0;
7358
- }
7359
- function handleDetachError(error) {
7360
- if (error instanceof FulltextNotAttachedError || error instanceof FulltextIOError) {
7361
- return { success: false, error: error.message };
7362
- }
7363
- throw error;
9195
+ return result;
7364
9196
  }
7365
9197
  async function fulltextDetach(library, options) {
7366
- const { identifier, type: type2, delete: deleteFile, idType = "id", fulltextDirectory } = options;
9198
+ const { identifier, type: type2, removeFiles, idType = "id", fulltextDirectory } = options;
7367
9199
  const item = await library.find(identifier, { idType });
7368
9200
  if (!item) {
7369
9201
  return { success: false, error: `Reference '${identifier}' not found` };
7370
9202
  }
7371
- const manager = new FulltextManager(fulltextDirectory);
7372
- const typesToDetach = type2 ? [type2] : manager.getAttachedTypes(item);
7373
- if (typesToDetach.length === 0) {
9203
+ const attachments = item.custom?.attachments;
9204
+ const fulltextFiles = findFulltextFiles(attachments);
9205
+ if (fulltextFiles.length === 0) {
7374
9206
  return { success: false, error: `No fulltext attached to '${identifier}'` };
7375
9207
  }
7376
- try {
7377
- const { detached, deleted } = await performDetachOperations(
7378
- manager,
7379
- item,
7380
- typesToDetach,
7381
- deleteFile
7382
- );
7383
- const updatedFulltext = buildRemainingFulltext(item.custom?.fulltext ?? {}, detached);
7384
- await updateReference(library, {
7385
- identifier,
7386
- updates: {
7387
- custom: { fulltext: updatedFulltext }
7388
- },
7389
- idType
7390
- });
7391
- const resultData = { success: true, detached };
7392
- if (deleted.length > 0) {
7393
- resultData.deleted = deleted;
7394
- }
7395
- return resultData;
7396
- } catch (error) {
7397
- return handleDetachError(error);
9208
+ const filesToDetach = getFilesToDetach(attachments, type2);
9209
+ if (filesToDetach.length === 0) {
9210
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
7398
9211
  }
7399
- }
7400
- function getFulltextPath(item, type2, fulltextDirectory) {
7401
- const fulltext = item.custom?.fulltext;
7402
- if (!fulltext) return void 0;
7403
- const filename = type2 === "pdf" ? fulltext.pdf : fulltext.markdown;
7404
- if (!filename) return void 0;
7405
- return join(fulltextDirectory, filename);
7406
- }
7407
- function determineTypeToOpen(item) {
7408
- const fulltext = item.custom?.fulltext;
7409
- if (!fulltext) return void 0;
7410
- if (fulltext.pdf) return "pdf";
7411
- if (fulltext.markdown) return "markdown";
9212
+ const { detached, deleted } = await detachFiles(
9213
+ library,
9214
+ filesToDetach,
9215
+ identifier,
9216
+ removeFiles,
9217
+ idType,
9218
+ fulltextDirectory
9219
+ );
9220
+ return buildResult(detached, deleted, identifier);
9221
+ }
9222
+ function buildFilePath(attachmentsDirectory, directory, filename) {
9223
+ return join(attachmentsDirectory, directory, filename);
9224
+ }
9225
+ function determineTypeToOpen(attachments) {
9226
+ const files = findFulltextFiles(attachments);
9227
+ if (files.length === 0) return void 0;
9228
+ const pdfFile = files.find((f) => f.filename.endsWith(".pdf"));
9229
+ if (pdfFile) return "pdf";
9230
+ const mdFile = files.find((f) => f.filename.endsWith(".md"));
9231
+ if (mdFile) return "markdown";
7412
9232
  return void 0;
7413
9233
  }
7414
9234
  async function fulltextOpen(library, options) {
@@ -7417,14 +9237,16 @@ async function fulltextOpen(library, options) {
7417
9237
  if (!item) {
7418
9238
  return { success: false, error: `Reference not found: ${identifier}` };
7419
9239
  }
7420
- const typeToOpen = type2 ?? determineTypeToOpen(item);
9240
+ const attachments = item.custom?.attachments;
9241
+ const typeToOpen = type2 ?? determineTypeToOpen(attachments);
7421
9242
  if (!typeToOpen) {
7422
9243
  return { success: false, error: `No fulltext attached to reference: ${identifier}` };
7423
9244
  }
7424
- const filePath = getFulltextPath(item, typeToOpen, fulltextDirectory);
7425
- if (!filePath) {
9245
+ const file = findFulltextFile(attachments, typeToOpen);
9246
+ if (!file || !attachments?.directory) {
7426
9247
  return { success: false, error: `No ${typeToOpen} attached to reference: ${identifier}` };
7427
9248
  }
9249
+ const filePath = buildFilePath(fulltextDirectory, attachments.directory, file.filename);
7428
9250
  if (!existsSync(filePath)) {
7429
9251
  return {
7430
9252
  success: false,
@@ -7472,7 +9294,7 @@ async function executeFulltextDetach(options, context) {
7472
9294
  const operationOptions = {
7473
9295
  identifier: options.identifier,
7474
9296
  type: options.type,
7475
- delete: options.delete,
9297
+ removeFiles: options.removeFiles,
7476
9298
  idType: options.idType,
7477
9299
  fulltextDirectory: options.fulltextDirectory
7478
9300
  };
@@ -7543,12 +9365,12 @@ function getFulltextExitCode(result) {
7543
9365
  return result.success ? 0 : 1;
7544
9366
  }
7545
9367
  async function executeInteractiveSelect(context, config2) {
7546
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
9368
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
7547
9369
  const allReferences = await context.library.getAll();
7548
9370
  const identifiers = await selectReferencesOrExit(
7549
9371
  allReferences,
7550
9372
  { multiSelect: false },
7551
- config2.cli.interactive
9373
+ config2.cli.tui
7552
9374
  );
7553
9375
  return identifiers[0];
7554
9376
  }
@@ -7587,7 +9409,7 @@ async function handleFulltextAttachAction(identifierArg, filePathArg, options, g
7587
9409
  const stdinContent = !filePath && type2 ? await readStdinBuffer() : void 0;
7588
9410
  const attachOptions = {
7589
9411
  identifier,
7590
- fulltextDirectory: config2.fulltext.directory,
9412
+ fulltextDirectory: config2.attachments.directory,
7591
9413
  ...filePath && { filePath },
7592
9414
  ...type2 && { type: type2 },
7593
9415
  ...options.move && { move: options.move },
@@ -7641,7 +9463,7 @@ async function handleFulltextGetAction(identifierArg, options, globalOpts) {
7641
9463
  }
7642
9464
  const getOptions = {
7643
9465
  identifier,
7644
- fulltextDirectory: config2.fulltext.directory,
9466
+ fulltextDirectory: config2.attachments.directory,
7645
9467
  ...options.pdf && { type: "pdf" },
7646
9468
  ...options.markdown && { type: "markdown" },
7647
9469
  ...options.stdout && { stdout: options.stdout },
@@ -7677,10 +9499,10 @@ async function handleFulltextDetachAction(identifierArg, options, globalOpts) {
7677
9499
  }
7678
9500
  const detachOptions = {
7679
9501
  identifier,
7680
- fulltextDirectory: config2.fulltext.directory,
9502
+ fulltextDirectory: config2.attachments.directory,
7681
9503
  ...options.pdf && { type: "pdf" },
7682
9504
  ...options.markdown && { type: "markdown" },
7683
- ...options.delete && { delete: options.delete },
9505
+ ...options.removeFiles && { removeFiles: options.removeFiles },
7684
9506
  ...options.force && { force: options.force },
7685
9507
  ...options.uuid && { idType: "uuid" }
7686
9508
  };
@@ -7716,7 +9538,7 @@ async function handleFulltextOpenAction(identifierArg, options, globalOpts) {
7716
9538
  }
7717
9539
  const openOptions = {
7718
9540
  identifier,
7719
- fulltextDirectory: config2.fulltext.directory,
9541
+ fulltextDirectory: config2.attachments.directory,
7720
9542
  ...options.pdf && { type: "pdf" },
7721
9543
  ...options.markdown && { type: "markdown" },
7722
9544
  ...options.uuid && { idType: "uuid" }
@@ -7823,21 +9645,28 @@ const VALID_LIST_SORT_FIELDS = /* @__PURE__ */ new Set([
7823
9645
  "pub"
7824
9646
  ]);
7825
9647
  function getOutputFormat$1(options) {
9648
+ if (options.output) {
9649
+ if (options.output === "ids") return "ids-only";
9650
+ return options.output;
9651
+ }
7826
9652
  if (options.json) return "json";
7827
9653
  if (options.idsOnly) return "ids-only";
7828
- if (options.uuid) return "uuid";
9654
+ if (options.uuidOnly) return "uuid";
7829
9655
  if (options.bibtex) return "bibtex";
7830
9656
  return "pretty";
7831
9657
  }
7832
9658
  function validateOptions$1(options) {
7833
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
9659
+ const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
7834
9660
  Boolean
7835
9661
  );
7836
9662
  if (outputOptions.length > 1) {
7837
9663
  throw new Error(
7838
- "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
9664
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
7839
9665
  );
7840
9666
  }
9667
+ if (options.output && outputOptions.length > 0) {
9668
+ throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
9669
+ }
7841
9670
  if (options.sort !== void 0) {
7842
9671
  const sortStr = String(options.sort);
7843
9672
  if (!VALID_LIST_SORT_FIELDS.has(sortStr)) {
@@ -28249,7 +30078,7 @@ function registerFulltextAttachTool(server, getLibraryOperations, getConfig) {
28249
30078
  filePath: args.path,
28250
30079
  force: true,
28251
30080
  // MCP tools don't support interactive confirmation
28252
- fulltextDirectory: config2.fulltext.directory
30081
+ fulltextDirectory: config2.attachments.directory
28253
30082
  });
28254
30083
  if (!result.success) {
28255
30084
  return {
@@ -28282,7 +30111,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
28282
30111
  const config2 = getConfig();
28283
30112
  const pathResult = await fulltextGet(libraryOps, {
28284
30113
  identifier: args.id,
28285
- fulltextDirectory: config2.fulltext.directory
30114
+ fulltextDirectory: config2.attachments.directory
28286
30115
  });
28287
30116
  if (!pathResult.success) {
28288
30117
  return {
@@ -28296,7 +30125,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
28296
30125
  identifier: args.id,
28297
30126
  type: "markdown",
28298
30127
  stdout: true,
28299
- fulltextDirectory: config2.fulltext.directory
30128
+ fulltextDirectory: config2.attachments.directory
28300
30129
  });
28301
30130
  if (contentResult.success && contentResult.content) {
28302
30131
  responses.push({
@@ -28335,7 +30164,7 @@ function registerFulltextDetachTool(server, getLibraryOperations, getConfig) {
28335
30164
  const config2 = getConfig();
28336
30165
  const result = await fulltextDetach(libraryOps, {
28337
30166
  identifier: args.id,
28338
- fulltextDirectory: config2.fulltext.directory
30167
+ fulltextDirectory: config2.attachments.directory
28339
30168
  });
28340
30169
  if (!result.success) {
28341
30170
  return {
@@ -28541,7 +30370,7 @@ async function mcpStart(options) {
28541
30370
  async function executeRemove(options, context) {
28542
30371
  const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
28543
30372
  if (context.mode === "local" && deleteFulltext && fulltextDirectory) {
28544
- const { removeReference } = await import("./index-4KSTJ3rp.js").then((n) => n.r);
30373
+ const { removeReference } = await import("./index-DHgeuWGP.js").then((n) => n.r);
28545
30374
  return removeReference(context.library, {
28546
30375
  identifier,
28547
30376
  idType,
@@ -28595,12 +30424,12 @@ Continue?`;
28595
30424
  return readConfirmation(confirmMsg);
28596
30425
  }
28597
30426
  async function executeInteractiveRemove(context, config2) {
28598
- const { selectReferenceItemsOrExit } = await import("./reference-select-DSVwE9iu.js");
30427
+ const { selectReferenceItemsOrExit } = await import("./reference-select-B9w9CLa1.js");
28599
30428
  const allReferences = await context.library.getAll();
28600
30429
  const selectedItems = await selectReferenceItemsOrExit(
28601
30430
  allReferences,
28602
30431
  { multiSelect: false },
28603
- config2.cli.interactive
30432
+ config2.cli.tui
28604
30433
  );
28605
30434
  const selectedItem = selectedItems[0];
28606
30435
  return { identifier: selectedItem.id, item: selectedItem };
@@ -28682,7 +30511,7 @@ async function handleRemoveAction(identifierArg, options, globalOpts) {
28682
30511
  const removeOptions = {
28683
30512
  identifier,
28684
30513
  idType: useUuid ? "uuid" : "id",
28685
- fulltextDirectory: config2.fulltext.directory,
30514
+ fulltextDirectory: config2.attachments.directory,
28686
30515
  deleteFulltext: force && hasFulltext
28687
30516
  };
28688
30517
  const result = await executeRemove(removeOptions, context);
@@ -28705,21 +30534,28 @@ const VALID_SEARCH_SORT_FIELDS = /* @__PURE__ */ new Set([
28705
30534
  "rel"
28706
30535
  ]);
28707
30536
  function getOutputFormat(options) {
30537
+ if (options.output) {
30538
+ if (options.output === "ids") return "ids-only";
30539
+ return options.output;
30540
+ }
28708
30541
  if (options.json) return "json";
28709
30542
  if (options.idsOnly) return "ids-only";
28710
- if (options.uuid) return "uuid";
30543
+ if (options.uuidOnly) return "uuid";
28711
30544
  if (options.bibtex) return "bibtex";
28712
30545
  return "pretty";
28713
30546
  }
28714
30547
  function validateOptions(options) {
28715
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
30548
+ const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
28716
30549
  Boolean
28717
30550
  );
28718
30551
  if (outputOptions.length > 1) {
28719
30552
  throw new Error(
28720
- "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
30553
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
28721
30554
  );
28722
30555
  }
30556
+ if (options.output && outputOptions.length > 0) {
30557
+ throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
30558
+ }
28723
30559
  if (options.sort !== void 0) {
28724
30560
  const sortStr = String(options.sort);
28725
30561
  if (!VALID_SEARCH_SORT_FIELDS.has(sortStr)) {
@@ -28775,35 +30611,39 @@ function formatSearchOutput(result, options) {
28775
30611
  return lines.join("\n");
28776
30612
  }
28777
30613
  function validateInteractiveOptions(options) {
28778
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
28779
- Boolean
28780
- );
30614
+ const outputOptions = [
30615
+ options.output,
30616
+ options.json,
30617
+ options.idsOnly,
30618
+ options.uuidOnly,
30619
+ options.bibtex
30620
+ ].filter(Boolean);
28781
30621
  if (outputOptions.length > 0) {
28782
30622
  throw new Error(
28783
- "Interactive mode cannot be combined with output format options (--json, --ids-only, --uuid, --bibtex)"
30623
+ "TUI mode cannot be combined with output format options (--output, --json, --ids-only, --uuid-only, --bibtex)"
28784
30624
  );
28785
30625
  }
28786
30626
  }
28787
30627
  async function executeInteractiveSearch(options, context, config2) {
28788
30628
  validateInteractiveOptions(options);
28789
- const { checkTTY } = await import("./tty-CDBIQraQ.js");
30629
+ const { checkTTY } = await import("./tty-BMyaEOhX.js");
28790
30630
  const { runSearchPrompt } = await import("./search-prompt-BrWpOcij.js");
28791
- const { runActionMenu } = await import("./action-menu-DNlpGiwS.js");
28792
- const { search } = await import("./file-watcher-D2Y-SlcE.js").then((n) => n.y);
28793
- const { tokenize } = await import("./file-watcher-D2Y-SlcE.js").then((n) => n.x);
30631
+ const { runActionMenu } = await import("./action-menu-DvwR6nMj.js");
30632
+ const { search } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.y);
30633
+ const { tokenize } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.x);
28794
30634
  checkTTY();
28795
30635
  const allReferences = await context.library.getAll();
28796
30636
  const searchFn = (query) => {
28797
30637
  const { tokens } = tokenize(query);
28798
30638
  return search(allReferences, tokens);
28799
30639
  };
28800
- const interactiveConfig = config2.cli.interactive;
30640
+ const tuiConfig = config2.cli.tui;
28801
30641
  const searchResult = await runSearchPrompt(
28802
30642
  allReferences,
28803
30643
  searchFn,
28804
30644
  {
28805
- limit: interactiveConfig.limit,
28806
- debounceMs: interactiveConfig.debounceMs
30645
+ limit: tuiConfig.limit,
30646
+ debounceMs: tuiConfig.debounceMs
28807
30647
  },
28808
30648
  options.query || ""
28809
30649
  );
@@ -29096,12 +30936,12 @@ function formatUpdateOutput(result, identifier) {
29096
30936
  return parts.join("\n");
29097
30937
  }
29098
30938
  async function executeInteractiveUpdate(context, config2) {
29099
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
30939
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
29100
30940
  const allReferences = await context.library.getAll();
29101
30941
  const identifiers = await selectReferencesOrExit(
29102
30942
  allReferences,
29103
30943
  { multiSelect: false },
29104
- config2.cli.interactive
30944
+ config2.cli.tui
29105
30945
  );
29106
30946
  return identifiers[0];
29107
30947
  }
@@ -29206,18 +31046,61 @@ function collectSetOption(value, previous) {
29206
31046
  }
29207
31047
  const SEARCH_SORT_FIELDS = searchSortFieldSchema.options;
29208
31048
  const SORT_ORDERS = sortOrderSchema.options;
29209
- const CITATION_FORMATS = ["text", "html", "rtf"];
31049
+ const CITATION_OUTPUT_FORMATS = ["text", "html", "rtf"];
31050
+ const EXPORT_OUTPUT_FORMATS = ["json", "yaml", "bibtex"];
31051
+ const LIST_OUTPUT_FORMATS = ["pretty", "json", "bibtex", "ids", "uuid"];
31052
+ const MUTATION_OUTPUT_FORMATS = ["json", "text"];
31053
+ const CONFIG_OUTPUT_FORMATS = ["text", "json"];
29210
31054
  const LOG_LEVELS = ["silent", "info", "debug"];
31055
+ const ADD_INPUT_FORMATS = ["json", "bibtex", "ris", "pmid", "doi", "isbn", "auto"];
31056
+ const CONFIG_SECTIONS = [
31057
+ "backup",
31058
+ "citation",
31059
+ "cli",
31060
+ "fulltext",
31061
+ "mcp",
31062
+ "pubmed",
31063
+ "server",
31064
+ "watch"
31065
+ ];
31066
+ const ATTACHMENT_ROLES = ["fulltext", "supplement", "notes", "draft"];
29211
31067
  const OPTION_VALUES = {
29212
31068
  "--sort": SEARCH_SORT_FIELDS,
29213
31069
  // search includes 'relevance'
29214
31070
  "--order": SORT_ORDERS,
29215
- "--format": CITATION_FORMATS,
29216
31071
  "--style": BUILTIN_STYLES,
29217
- "--log-level": LOG_LEVELS
31072
+ "--log-level": LOG_LEVELS,
31073
+ "--section": CONFIG_SECTIONS,
31074
+ "--input": ADD_INPUT_FORMATS,
31075
+ "-i": ADD_INPUT_FORMATS,
31076
+ "--role": ATTACHMENT_ROLES,
31077
+ "-r": ATTACHMENT_ROLES
31078
+ };
31079
+ const OUTPUT_VALUES_BY_COMMAND = {
31080
+ cite: CITATION_OUTPUT_FORMATS,
31081
+ export: EXPORT_OUTPUT_FORMATS,
31082
+ list: LIST_OUTPUT_FORMATS,
31083
+ search: LIST_OUTPUT_FORMATS,
31084
+ add: MUTATION_OUTPUT_FORMATS,
31085
+ remove: MUTATION_OUTPUT_FORMATS,
31086
+ update: MUTATION_OUTPUT_FORMATS
31087
+ // config show uses CONFIG_OUTPUT_FORMATS, handled specially
29218
31088
  };
31089
+ function getOptionValuesForCommand(option, command, subcommand) {
31090
+ if (option === "--output" || option === "-o") {
31091
+ if (command === "config" && subcommand === "show") {
31092
+ return CONFIG_OUTPUT_FORMATS;
31093
+ }
31094
+ if (command && OUTPUT_VALUES_BY_COMMAND[command]) {
31095
+ return OUTPUT_VALUES_BY_COMMAND[command];
31096
+ }
31097
+ return CITATION_OUTPUT_FORMATS;
31098
+ }
31099
+ return OPTION_VALUES[option];
31100
+ }
29219
31101
  const ID_COMPLETION_COMMANDS = /* @__PURE__ */ new Set(["cite", "remove", "update"]);
29220
31102
  const ID_COMPLETION_FULLTEXT_SUBCOMMANDS = /* @__PURE__ */ new Set(["attach", "get", "detach", "open"]);
31103
+ const ID_COMPLETION_ATTACH_SUBCOMMANDS = /* @__PURE__ */ new Set(["open", "add", "list", "get", "detach", "sync"]);
29221
31104
  function toCompletionItems(values) {
29222
31105
  return values.map((name2) => ({ name: name2 }));
29223
31106
  }
@@ -29251,6 +31134,13 @@ function extractGlobalOptions(program) {
29251
31134
  function findSubcommand(program, name2) {
29252
31135
  return program.commands.find((cmd) => cmd.name() === name2);
29253
31136
  }
31137
+ function parseCommandContext(env) {
31138
+ const words = env.line.trim().split(/\s+/);
31139
+ const args = words.slice(1);
31140
+ const command = args[0];
31141
+ const subcommand = args.length >= 2 ? args[1] : void 0;
31142
+ return { command, subcommand };
31143
+ }
29254
31144
  function getCompletions(env, program) {
29255
31145
  const { line, prev, last } = env;
29256
31146
  const words = line.trim().split(/\s+/);
@@ -29262,7 +31152,8 @@ function getCompletions(env, program) {
29262
31152
  }
29263
31153
  const firstArg = args[0] ?? "";
29264
31154
  if (prev?.startsWith("-")) {
29265
- const optionValues = OPTION_VALUES[prev];
31155
+ const { command, subcommand } = parseCommandContext(env);
31156
+ const optionValues = getOptionValuesForCommand(prev, command, subcommand);
29266
31157
  if (optionValues) {
29267
31158
  return toCompletionItems(optionValues);
29268
31159
  }
@@ -29302,11 +31193,43 @@ function needsIdCompletion(env) {
29302
31193
  }
29303
31194
  return { needs: false };
29304
31195
  }
31196
+ if (command === "attach" && args.length >= 2) {
31197
+ const subcommand = args[1] ?? "";
31198
+ if (ID_COMPLETION_ATTACH_SUBCOMMANDS.has(subcommand)) {
31199
+ return { needs: true, command, subcommand };
31200
+ }
31201
+ return { needs: false };
31202
+ }
29305
31203
  if (ID_COMPLETION_COMMANDS.has(command)) {
29306
31204
  return { needs: true, command };
29307
31205
  }
29308
31206
  return { needs: false };
29309
31207
  }
31208
+ const CONFIG_KEY_SUBCOMMANDS = /* @__PURE__ */ new Set(["get", "set", "unset"]);
31209
+ function needsConfigKeyCompletion(env) {
31210
+ const { line, prev } = env;
31211
+ const words = line.trim().split(/\s+/);
31212
+ const args = words.slice(1);
31213
+ if (args.length < 2) {
31214
+ return false;
31215
+ }
31216
+ const command = args[0] ?? "";
31217
+ const subcommand = args[1] ?? "";
31218
+ if (prev?.startsWith("-")) {
31219
+ return false;
31220
+ }
31221
+ if (command === "config" && CONFIG_KEY_SUBCOMMANDS.has(subcommand)) {
31222
+ if (subcommand === "set" && args.length > 3) {
31223
+ return false;
31224
+ }
31225
+ return true;
31226
+ }
31227
+ return false;
31228
+ }
31229
+ function getConfigKeyCompletions(prefix) {
31230
+ const keys = getAllConfigKeys();
31231
+ return keys.filter((key) => key.toLowerCase().startsWith(prefix.toLowerCase())).map((key) => ({ name: key }));
31232
+ }
29310
31233
  const MAX_ID_COMPLETIONS = 100;
29311
31234
  async function getLibraryForCompletion() {
29312
31235
  try {
@@ -29372,6 +31295,11 @@ async function handleCompletion(program) {
29372
31295
  tabtab.log(completions2);
29373
31296
  return;
29374
31297
  }
31298
+ if (needsConfigKeyCompletion(env)) {
31299
+ const keyCompletions = getConfigKeyCompletions(env.last);
31300
+ tabtab.log(keyCompletions);
31301
+ return;
31302
+ }
29375
31303
  const idContext = needsIdCompletion(env);
29376
31304
  if (idContext.needs) {
29377
31305
  const library = await getLibraryForCompletion();
@@ -29411,7 +31339,9 @@ function createProgram() {
29411
31339
  registerCiteCommand(program);
29412
31340
  registerServerCommand(program);
29413
31341
  registerFulltextCommand(program);
31342
+ registerAttachCommand(program);
29414
31343
  registerMcpCommand(program);
31344
+ registerConfigCommand(program);
29415
31345
  registerCompletionCommand(program);
29416
31346
  return program;
29417
31347
  }
@@ -29434,7 +31364,7 @@ async function handleListAction(options, program) {
29434
31364
  }
29435
31365
  }
29436
31366
  function registerListCommand(program) {
29437
- program.command("list").description("List all references in the library").option("--json", "Output in JSON format").option("--ids-only", "Output only citation keys").option("--uuid", "Output only UUIDs").option("--bibtex", "Output in BibTeX format").option("--sort <field>", "Sort by field: created|updated|published|author|title").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (options) => {
31367
+ program.command("list").description("List all references in the library").option("-o, --output <format>", "Output format: pretty|json|bibtex|ids|uuid").option("--json", "Alias for --output json").option("--bibtex", "Alias for --output bibtex").option("--ids-only", "Alias for --output ids").option("--uuid-only", "Alias for --output uuid").option("--sort <field>", "Sort by field: created|updated|published|author|title").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (options) => {
29438
31368
  await handleListAction(options, program);
29439
31369
  });
29440
31370
  }
@@ -29463,7 +31393,7 @@ async function handleExportAction(ids, options, program) {
29463
31393
  }
29464
31394
  }
29465
31395
  function registerExportCommand(program) {
29466
- program.command("export [ids...]").description("Export raw CSL-JSON for external tool integration").option("--uuid", "Interpret identifiers as UUIDs").option("--all", "Export all references").option("--search <query>", "Export references matching search query").option("-f, --format <fmt>", "Output format: json (default), yaml, bibtex").action(async (ids, options) => {
31396
+ program.command("export [ids...]").description("Export raw CSL-JSON for external tool integration").option("--uuid", "Interpret identifiers as UUIDs").option("--all", "Export all references").option("--search <query>", "Export references matching search query").option("-o, --output <format>", "Output format: json (default), yaml, bibtex").action(async (ids, options) => {
29467
31397
  await handleExportAction(ids, options, program);
29468
31398
  });
29469
31399
  }
@@ -29472,7 +31402,7 @@ async function handleSearchAction(query, options, program) {
29472
31402
  const globalOpts = program.opts();
29473
31403
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
29474
31404
  const context = await createExecutionContext(config2, Library.load);
29475
- if (options.interactive) {
31405
+ if (options.tui) {
29476
31406
  const result2 = await executeInteractiveSearch({ ...options, query }, context, config2);
29477
31407
  if (result2.output) {
29478
31408
  process.stdout.write(`${result2.output}
@@ -29494,9 +31424,9 @@ async function handleSearchAction(query, options, program) {
29494
31424
  }
29495
31425
  }
29496
31426
  function registerSearchCommand(program) {
29497
- program.command("search").description("Search references").argument("[query]", "Search query (required unless using --interactive)").option("-i, --interactive", "Enable interactive search mode").option("--json", "Output in JSON format").option("--ids-only", "Output only citation keys").option("--uuid", "Output only UUIDs").option("--bibtex", "Output in BibTeX format").option("--sort <field>", "Sort by field: created|updated|published|author|title|relevance").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (query, options) => {
29498
- if (!options.interactive && !query) {
29499
- process.stderr.write("Error: Search query is required unless using --interactive\n");
31427
+ program.command("search").description("Search references").argument("[query]", "Search query (required unless using --tui)").option("-t, --tui", "Enable TUI (interactive) search mode").option("-o, --output <format>", "Output format: pretty|json|bibtex|ids|uuid").option("--json", "Alias for --output json").option("--bibtex", "Alias for --output bibtex").option("--ids-only", "Alias for --output ids").option("--uuid-only", "Alias for --output uuid").option("--sort <field>", "Sort by field: created|updated|published|author|title|relevance").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (query, options) => {
31428
+ if (!options.tui && !query) {
31429
+ process.stderr.write("Error: Search query is required unless using --tui\n");
29500
31430
  process.exit(1);
29501
31431
  }
29502
31432
  await handleSearchAction(query ?? "", options, program);
@@ -29507,8 +31437,8 @@ function buildAddOptions(inputs, options, config2, stdinContent) {
29507
31437
  inputs,
29508
31438
  force: options.force ?? false
29509
31439
  };
29510
- if (options.format !== void 0) {
29511
- addOptions.format = options.format;
31440
+ if (options.input !== void 0) {
31441
+ addOptions.format = options.input;
29512
31442
  }
29513
31443
  if (options.verbose !== void 0) {
29514
31444
  addOptions.verbose = options.verbose;
@@ -29576,11 +31506,7 @@ async function handleAddAction(inputs, options, program) {
29576
31506
  }
29577
31507
  }
29578
31508
  function registerAddCommand(program) {
29579
- program.command("add").description("Add new reference(s) to the library").argument("[input...]", "File paths or identifiers (PMID/DOI/ISBN), or use stdin").option("-f, --force", "Skip duplicate detection").option(
29580
- "--format <format>",
29581
- "Explicit input format: json|bibtex|ris|pmid|doi|isbn|auto",
29582
- "auto"
29583
- ).option("--verbose", "Show detailed error information").option("-o, --output <format>", "Output format: json|text", "text").option("--full", "Include full CSL-JSON data in JSON output").action(async (inputs, options) => {
31509
+ program.command("add").description("Add new reference(s) to the library").argument("[input...]", "File paths or identifiers (PMID/DOI/ISBN), or use stdin").option("-f, --force", "Skip duplicate detection").option("-i, --input <format>", "Input format: json|bibtex|ris|pmid|doi|isbn|auto", "auto").option("--verbose", "Show detailed error information").option("-o, --output <format>", "Output format: json|text", "text").option("--full", "Include full CSL-JSON data in JSON output").action(async (inputs, options) => {
29584
31510
  await handleAddAction(inputs, options, program);
29585
31511
  });
29586
31512
  }
@@ -29598,7 +31524,7 @@ function registerEditCommand(program) {
29598
31524
  program.command("edit").description("Edit references interactively using an external editor").argument(
29599
31525
  "[identifier...]",
29600
31526
  "Citation keys or UUIDs to edit (interactive selection if omitted)"
29601
- ).option("--uuid", "Interpret identifiers as UUIDs").option("-f, --format <format>", "Edit format: yaml (default), json").option("--editor <editor>", "Editor command (overrides $VISUAL/$EDITOR)").action(async (identifiers, options) => {
31527
+ ).option("--uuid", "Interpret identifiers as UUIDs").option("--format <format>", "Edit format: yaml (default), json").option("--editor <editor>", "Editor command (overrides $VISUAL/$EDITOR)").action(async (identifiers, options) => {
29602
31528
  await handleEditAction(identifiers, options, program.opts());
29603
31529
  });
29604
31530
  }
@@ -29606,7 +31532,7 @@ function registerCiteCommand(program) {
29606
31532
  program.command("cite").description("Generate formatted citations for references").argument(
29607
31533
  "[id-or-uuid...]",
29608
31534
  "Citation keys or UUIDs to cite (interactive selection if omitted)"
29609
- ).option("--uuid", "Treat arguments as UUIDs instead of IDs").option("--style <style>", "CSL style name").option("--csl-file <path>", "Path to custom CSL file").option("--locale <locale>", "Locale code (e.g., en-US, ja-JP)").option("--format <format>", "Output format: text|html|rtf").option("--in-text", "Generate in-text citations instead of bibliography entries").action(async (identifiers, options) => {
31535
+ ).option("--uuid", "Treat arguments as UUIDs instead of IDs").option("--style <style>", "CSL style name").option("--csl-file <path>", "Path to custom CSL file").option("--locale <locale>", "Locale code (e.g., en-US, ja-JP)").option("-o, --output <format>", "Output format: text|html|rtf").option("--in-text", "Generate in-text citations instead of bibliography entries").action(async (identifiers, options) => {
29610
31536
  await handleCiteAction(identifiers, options, program.opts());
29611
31537
  });
29612
31538
  }
@@ -29711,6 +31637,30 @@ function registerMcpCommand(program) {
29711
31637
  }
29712
31638
  });
29713
31639
  }
31640
+ function registerAttachCommand(program) {
31641
+ const attachCmd = program.command("attach").description("Manage file attachments for references");
31642
+ attachCmd.command("open").description("Open attachments directory or specific file").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to open").option("--role <role>", "Open file by role").option("--print", "Output path instead of opening").option("--no-sync", "Skip interactive sync prompt").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
31643
+ await handleAttachOpenAction(identifier, filename, options, program.opts());
31644
+ });
31645
+ attachCmd.command("add").description("Add a file attachment to a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("<file-path>", "Path to the file to attach").requiredOption(
31646
+ "--role <role>",
31647
+ "Role for the file (fulltext, supplement, notes, draft, or custom)"
31648
+ ).option("--label <label>", "Human-readable label").option("--move", "Move file instead of copy").option("-f, --force", "Overwrite existing attachment").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filePath, options) => {
31649
+ await handleAttachAddAction(identifier, filePath, options, program.opts());
31650
+ });
31651
+ attachCmd.command("list").description("List attachments for a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--role <role>", "Filter by role").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
31652
+ await handleAttachListAction(identifier, options, program.opts());
31653
+ });
31654
+ attachCmd.command("get").description("Get attachment file path or content").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to get").option("--role <role>", "Get file by role").option("--stdout", "Output file content to stdout").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
31655
+ await handleAttachGetAction(identifier, filename, options, program.opts());
31656
+ });
31657
+ attachCmd.command("detach").description("Detach file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to detach").option("--role <role>", "Detach files by role").option("--all", "Detach all files of specified role").option("--remove-files", "Also delete files from disk").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
31658
+ await handleAttachDetachAction(identifier, filename, options, program.opts());
31659
+ });
31660
+ attachCmd.command("sync").description("Synchronize metadata with files on disk").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--yes", "Apply changes (add new files)").option("--fix", "Remove missing files from metadata").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
31661
+ await handleAttachSyncAction(identifier, options, program.opts());
31662
+ });
31663
+ }
29714
31664
  function registerFulltextCommand(program) {
29715
31665
  const fulltextCmd = program.command("fulltext").description("Manage full-text files attached to references");
29716
31666
  fulltextCmd.command("attach").description("Attach a full-text file to a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[file-path]", "Path to the file to attach").option("--pdf [path]", "Attach as PDF (path optional if provided as argument)").option("--markdown [path]", "Attach as Markdown (path optional if provided as argument)").option("--move", "Move file instead of copy").option("-f, --force", "Overwrite existing attachment").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filePath, options) => {
@@ -29719,7 +31669,7 @@ function registerFulltextCommand(program) {
29719
31669
  fulltextCmd.command("get").description("Get full-text file path or content").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Get PDF file only").option("--markdown", "Get Markdown file only").option("--stdout", "Output file content to stdout").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
29720
31670
  await handleFulltextGetAction(identifier, options, program.opts());
29721
31671
  });
29722
- fulltextCmd.command("detach").description("Detach full-text file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Detach PDF only").option("--markdown", "Detach Markdown only").option("--delete", "Also delete the file from disk").option("-f, --force", "Skip confirmation for delete").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
31672
+ fulltextCmd.command("detach").description("Detach full-text file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Detach PDF only").option("--markdown", "Detach Markdown only").option("--remove-files", "Also delete the file from disk").option("-f, --force", "Skip confirmation for file removal").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
29723
31673
  await handleFulltextDetachAction(identifier, options, program.opts());
29724
31674
  });
29725
31675
  fulltextCmd.command("open").description("Open full-text file with system default application").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Open PDF file").option("--markdown", "Open Markdown file").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
@@ -29741,8 +31691,14 @@ async function main(argv) {
29741
31691
  await program.parseAsync(argv);
29742
31692
  }
29743
31693
  export {
31694
+ addAttachment as a,
29744
31695
  createProgram as c,
31696
+ detachAttachment as d,
29745
31697
  formatBibtex as f,
29746
- main as m
31698
+ getAttachment as g,
31699
+ listAttachments as l,
31700
+ main as m,
31701
+ openAttachment as o,
31702
+ syncAttachments as s
29747
31703
  };
29748
- //# sourceMappingURL=index-UpzsmbyY.js.map
31704
+ //# sourceMappingURL=index-Bv5IgsL-.js.map