@ncukondo/reference-manager 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +64 -16
  2. package/bin/reference-manager.js +0 -0
  3. package/dist/chunks/{action-menu-CVSizwXm.js → action-menu-DwCcc6Gt.js} +3 -3
  4. package/dist/chunks/{action-menu-CVSizwXm.js.map → action-menu-DwCcc6Gt.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-CXoDLO8W.js → index-B4RmLBI1.js} +1698 -504
  8. package/dist/chunks/index-B4RmLBI1.js.map +1 -0
  9. package/dist/chunks/index-DEd6F5Rr.js +10 -0
  10. package/dist/chunks/index-DEd6F5Rr.js.map +1 -0
  11. package/dist/chunks/{index-DapYyqAC.js → index-DHgeuWGP.js} +112 -35
  12. package/dist/chunks/index-DHgeuWGP.js.map +1 -0
  13. package/dist/chunks/{loader-C1EpnyPm.js → loader-DStZe-OB.js} +82 -32
  14. package/dist/chunks/loader-DStZe-OB.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-CYo0O7MZ.js → style-select-BNQHC79W.js} +2 -2
  18. package/dist/chunks/{style-select-CYo0O7MZ.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/cite.d.ts.map +1 -1
  25. package/dist/cli/commands/config.d.ts.map +1 -1
  26. package/dist/cli/commands/edit.d.ts.map +1 -1
  27. package/dist/cli/commands/export.d.ts +1 -1
  28. package/dist/cli/commands/fulltext.d.ts +2 -2
  29. package/dist/cli/commands/fulltext.d.ts.map +1 -1
  30. package/dist/cli/commands/list.d.ts +2 -1
  31. package/dist/cli/commands/list.d.ts.map +1 -1
  32. package/dist/cli/commands/remove.d.ts.map +1 -1
  33. package/dist/cli/commands/search.d.ts +3 -2
  34. package/dist/cli/commands/search.d.ts.map +1 -1
  35. package/dist/cli/commands/server.d.ts.map +1 -1
  36. package/dist/cli/commands/update.d.ts.map +1 -1
  37. package/dist/cli/completion.d.ts.map +1 -1
  38. package/dist/cli/helpers.d.ts +36 -0
  39. package/dist/cli/helpers.d.ts.map +1 -1
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/server-client.d.ts +37 -1
  42. package/dist/cli/server-client.d.ts.map +1 -1
  43. package/dist/cli.js +2 -2
  44. package/dist/config/defaults.d.ts +7 -1
  45. package/dist/config/defaults.d.ts.map +1 -1
  46. package/dist/config/key-parser.d.ts +1 -1
  47. package/dist/config/loader.d.ts.map +1 -1
  48. package/dist/config/schema.d.ts +22 -8
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/features/attachments/directory-manager.d.ts +40 -0
  51. package/dist/features/attachments/directory-manager.d.ts.map +1 -0
  52. package/dist/features/attachments/directory.d.ts +36 -0
  53. package/dist/features/attachments/directory.d.ts.map +1 -0
  54. package/dist/features/attachments/filename.d.ts +30 -0
  55. package/dist/features/attachments/filename.d.ts.map +1 -0
  56. package/dist/features/attachments/types.d.ts +38 -0
  57. package/dist/features/attachments/types.d.ts.map +1 -0
  58. package/dist/features/fulltext/manager.d.ts +1 -1
  59. package/dist/features/fulltext/manager.d.ts.map +1 -1
  60. package/dist/features/interactive/tty.d.ts +2 -2
  61. package/dist/features/operations/attachments/add.d.ts +42 -0
  62. package/dist/features/operations/attachments/add.d.ts.map +1 -0
  63. package/dist/features/operations/attachments/detach.d.ts +38 -0
  64. package/dist/features/operations/attachments/detach.d.ts.map +1 -0
  65. package/dist/features/operations/attachments/get.d.ts +35 -0
  66. package/dist/features/operations/attachments/get.d.ts.map +1 -0
  67. package/dist/features/operations/attachments/index.d.ts +16 -0
  68. package/dist/features/operations/attachments/index.d.ts.map +1 -0
  69. package/dist/features/operations/attachments/list.d.ts +32 -0
  70. package/dist/features/operations/attachments/list.d.ts.map +1 -0
  71. package/dist/features/operations/attachments/open.d.ts +39 -0
  72. package/dist/features/operations/attachments/open.d.ts.map +1 -0
  73. package/dist/features/operations/attachments/sync.d.ts +50 -0
  74. package/dist/features/operations/attachments/sync.d.ts.map +1 -0
  75. package/dist/features/operations/fulltext/attach.d.ts +8 -2
  76. package/dist/features/operations/fulltext/attach.d.ts.map +1 -1
  77. package/dist/features/operations/fulltext/detach.d.ts +9 -3
  78. package/dist/features/operations/fulltext/detach.d.ts.map +1 -1
  79. package/dist/features/operations/fulltext/get.d.ts +8 -2
  80. package/dist/features/operations/fulltext/get.d.ts.map +1 -1
  81. package/dist/features/operations/fulltext/open.d.ts +8 -2
  82. package/dist/features/operations/fulltext/open.d.ts.map +1 -1
  83. package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts +39 -0
  84. package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts.map +1 -0
  85. package/dist/features/operations/fulltext-adapter/index.d.ts +7 -0
  86. package/dist/features/operations/fulltext-adapter/index.d.ts.map +1 -0
  87. package/dist/features/operations/index.d.ts +1 -0
  88. package/dist/features/operations/index.d.ts.map +1 -1
  89. package/dist/features/operations/library-operations.d.ts +43 -0
  90. package/dist/features/operations/library-operations.d.ts.map +1 -1
  91. package/dist/features/operations/operations-library.d.ts +7 -0
  92. package/dist/features/operations/operations-library.d.ts.map +1 -1
  93. package/dist/features/operations/remove.d.ts +1 -0
  94. package/dist/features/operations/remove.d.ts.map +1 -1
  95. package/dist/index.js +15 -15
  96. package/dist/index.js.map +1 -1
  97. package/dist/server.js +3 -3
  98. package/dist/utils/opener.d.ts +6 -1
  99. package/dist/utils/opener.d.ts.map +1 -1
  100. package/dist/utils/path.d.ts +28 -0
  101. package/dist/utils/path.d.ts.map +1 -0
  102. package/package.json +3 -1
  103. package/dist/chunks/index-CXoDLO8W.js.map +0 -1
  104. package/dist/chunks/index-DapYyqAC.js.map +0 -1
  105. package/dist/chunks/loader-C1EpnyPm.js.map +0 -1
  106. package/dist/chunks/tty-CDBIQraQ.js.map +0 -1
@@ -1,23 +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
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, dirname, 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-DStZe-OB.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, e as getDefaultCurrentDirConfigFilename, h as getDefaultUserConfigPath, o as openWithSystemApp } from "./loader-C1EpnyPm.js";
12
- import { mkdir, unlink, rename, copyFile, rm, readFile } from "node:fs/promises";
13
14
  import { parse as parse$2, stringify as stringify$2 } from "@iarna/toml";
14
- import { u as updateReference, B as BUILTIN_STYLES, g as getFulltextAttachmentTypes, s as startServerWithFileWatcher } from "./index-DapYyqAC.js";
15
15
  import "@citation-js/core";
16
16
  import "@citation-js/plugin-csl";
17
17
  import { ZodOptional as ZodOptional$2, z } from "zod";
18
18
  import { serve } from "@hono/node-server";
19
19
  const name = "@ncukondo/reference-manager";
20
- const version$1 = "0.14.1";
20
+ const version$1 = "0.15.1";
21
21
  const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
22
22
  const packageJson = {
23
23
  name,
@@ -261,6 +261,598 @@ function getExitCode(result) {
261
261
  }
262
262
  return 0;
263
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
+ }
264
856
  class OperationsLibrary {
265
857
  constructor(library, citationConfig) {
266
858
  this.library = library;
@@ -287,15 +879,15 @@ class OperationsLibrary {
287
879
  }
288
880
  // High-level operations
289
881
  async search(options) {
290
- const { searchReferences } = await import("./index-DapYyqAC.js").then((n) => n.d);
882
+ const { searchReferences } = await import("./index-DHgeuWGP.js").then((n) => n.n);
291
883
  return searchReferences(this.library, options);
292
884
  }
293
885
  async list(options) {
294
- const { listReferences } = await import("./index-DapYyqAC.js").then((n) => n.l);
886
+ const { listReferences } = await import("./index-DHgeuWGP.js").then((n) => n.m);
295
887
  return listReferences(this.library, options ?? {});
296
888
  }
297
889
  async cite(options) {
298
- const { citeReferences } = await import("./index-DapYyqAC.js").then((n) => n.b);
890
+ const { citeReferences } = await import("./index-DHgeuWGP.js").then((n) => n.l);
299
891
  const defaultStyle = options.defaultStyle ?? this.citationConfig?.defaultStyle;
300
892
  const cslDirectory = options.cslDirectory ?? this.citationConfig?.cslDirectory;
301
893
  const mergedOptions = {
@@ -306,9 +898,34 @@ class OperationsLibrary {
306
898
  return citeReferences(this.library, mergedOptions);
307
899
  }
308
900
  async import(inputs, options) {
309
- const { addReferences } = await import("./index-DapYyqAC.js").then((n) => n.a);
901
+ const { addReferences } = await import("./index-DHgeuWGP.js").then((n) => n.k);
310
902
  return addReferences(inputs, this.library, options ?? {});
311
903
  }
904
+ // Attachment operations
905
+ async attachAdd(options) {
906
+ const { addAttachment: addAttachment2 } = await import("./index-DEd6F5Rr.js");
907
+ return addAttachment2(this.library, options);
908
+ }
909
+ async attachList(options) {
910
+ const { listAttachments: listAttachments2 } = await import("./index-DEd6F5Rr.js");
911
+ return listAttachments2(this.library, options);
912
+ }
913
+ async attachGet(options) {
914
+ const { getAttachment: getAttachment2 } = await import("./index-DEd6F5Rr.js");
915
+ return getAttachment2(this.library, options);
916
+ }
917
+ async attachDetach(options) {
918
+ const { detachAttachment: detachAttachment2 } = await import("./index-DEd6F5Rr.js");
919
+ return detachAttachment2(this.library, options);
920
+ }
921
+ async attachSync(options) {
922
+ const { syncAttachments: syncAttachments2 } = await import("./index-DEd6F5Rr.js");
923
+ return syncAttachments2(this.library, options);
924
+ }
925
+ async attachOpen(options) {
926
+ const { openAttachment: openAttachment2 } = await import("./index-DEd6F5Rr.js");
927
+ return openAttachment2(this.library, options);
928
+ }
312
929
  }
313
930
  class ServerClient {
314
931
  constructor(baseUrl) {
@@ -495,20 +1112,125 @@ class ServerClient {
495
1112
  }
496
1113
  return await response.json();
497
1114
  }
498
- }
499
- async function getServerConnection(libraryPath, config2) {
500
- const portfilePath = getPortfilePath();
501
- const portfileData = await readPortfile(portfilePath);
502
- if (!portfileData) {
503
- if (config2.server.autoStart) {
504
- await startServerDaemon$1(libraryPath);
505
- await waitForPortfile(5e3);
506
- 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());
507
1132
  }
508
- return null;
1133
+ return await response.json();
509
1134
  }
510
- if (!isProcessRunning(portfileData.pid)) {
511
- 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);
512
1234
  return null;
513
1235
  }
514
1236
  if (!portfileData.library || portfileData.library !== libraryPath) {
@@ -615,47 +1337,566 @@ async function readIdentifiersFromStdin() {
615
1337
  if (!content) {
616
1338
  return [];
617
1339
  }
618
- return content.split(/[\s\n]+/).filter((id2) => id2.length > 0);
619
- }
620
- async function readIdentifierFromStdin() {
621
- const content = await readStdinContent();
622
- const firstLine = content.split("\n")[0]?.trim();
623
- return firstLine || void 0;
1340
+ return content.split(/[\s\n]+/).filter((id2) => id2.length > 0);
1341
+ }
1342
+ async function readIdentifierFromStdin() {
1343
+ const content = await readStdinContent();
1344
+ const firstLine = content.split("\n")[0]?.trim();
1345
+ return firstLine || void 0;
1346
+ }
1347
+ async function readConfirmation(prompt) {
1348
+ if (!isTTY()) {
1349
+ return true;
1350
+ }
1351
+ const enquirer = await import("enquirer");
1352
+ const Confirm = enquirer.default.Confirm;
1353
+ const confirmPrompt = new Confirm({
1354
+ name: "confirm",
1355
+ message: prompt,
1356
+ initial: false
1357
+ });
1358
+ try {
1359
+ return await confirmPrompt.run();
1360
+ } catch {
1361
+ return false;
1362
+ }
1363
+ }
1364
+ async function readStdinContent() {
1365
+ const chunks = [];
1366
+ for await (const chunk of stdin) {
1367
+ chunks.push(chunk);
1368
+ }
1369
+ return Buffer.concat(chunks).toString("utf-8").trim();
1370
+ }
1371
+ async function readStdinBuffer() {
1372
+ const chunks = [];
1373
+ for await (const chunk of stdin) {
1374
+ chunks.push(chunk);
1375
+ }
1376
+ return Buffer.concat(chunks);
1377
+ }
1378
+ const ExitCode = {
1379
+ /** Success */
1380
+ SUCCESS: 0,
1381
+ /** General error (e.g., not found, validation failed) */
1382
+ ERROR: 1,
1383
+ /** Internal/unexpected error */
1384
+ INTERNAL_ERROR: 4,
1385
+ /** Interrupted by SIGINT */
1386
+ SIGINT: 130
1387
+ };
1388
+ function setExitCode(code2) {
1389
+ process.exitCode = code2;
1390
+ }
1391
+ function exitWithError(message, code2 = ExitCode.ERROR) {
1392
+ process.stderr.write(`Error: ${message}
1393
+ `);
1394
+ setExitCode(code2);
1395
+ }
1396
+ async function executeAttachOpen(options, context) {
1397
+ const operationOptions = {
1398
+ identifier: options.identifier,
1399
+ attachmentsDirectory: options.attachmentsDirectory,
1400
+ ...options.filename !== void 0 && { filename: options.filename },
1401
+ ...options.role !== void 0 && { role: options.role },
1402
+ ...options.print !== void 0 && { print: options.print },
1403
+ ...options.idType !== void 0 && { idType: options.idType }
1404
+ };
1405
+ return openAttachment(context.library, operationOptions);
1406
+ }
1407
+ async function executeAttachAdd(options, context) {
1408
+ const operationOptions = {
1409
+ identifier: options.identifier,
1410
+ filePath: options.filePath,
1411
+ role: options.role,
1412
+ attachmentsDirectory: options.attachmentsDirectory,
1413
+ ...options.label !== void 0 && { label: options.label },
1414
+ ...options.move !== void 0 && { move: options.move },
1415
+ ...options.force !== void 0 && { force: options.force },
1416
+ ...options.idType !== void 0 && { idType: options.idType }
1417
+ };
1418
+ return addAttachment(context.library, operationOptions);
1419
+ }
1420
+ async function executeAttachList(options, context) {
1421
+ const operationOptions = {
1422
+ identifier: options.identifier,
1423
+ ...options.role !== void 0 && { role: options.role },
1424
+ ...options.idType !== void 0 && { idType: options.idType }
1425
+ };
1426
+ return listAttachments(context.library, operationOptions);
1427
+ }
1428
+ async function executeAttachGet(options, context) {
1429
+ const operationOptions = {
1430
+ identifier: options.identifier,
1431
+ attachmentsDirectory: options.attachmentsDirectory,
1432
+ ...options.filename !== void 0 && { filename: options.filename },
1433
+ ...options.role !== void 0 && { role: options.role },
1434
+ ...options.stdout !== void 0 && { stdout: options.stdout },
1435
+ ...options.idType !== void 0 && { idType: options.idType }
1436
+ };
1437
+ return getAttachment(context.library, operationOptions);
1438
+ }
1439
+ async function executeAttachDetach(options, context) {
1440
+ const operationOptions = {
1441
+ identifier: options.identifier,
1442
+ attachmentsDirectory: options.attachmentsDirectory,
1443
+ ...options.filename !== void 0 && { filename: options.filename },
1444
+ ...options.role !== void 0 && { role: options.role },
1445
+ ...options.all !== void 0 && { all: options.all },
1446
+ ...options.removeFiles !== void 0 && { removeFiles: options.removeFiles },
1447
+ ...options.idType !== void 0 && { idType: options.idType }
1448
+ };
1449
+ return detachAttachment(context.library, operationOptions);
1450
+ }
1451
+ async function executeAttachSync(options, context) {
1452
+ const operationOptions = {
1453
+ identifier: options.identifier,
1454
+ attachmentsDirectory: options.attachmentsDirectory,
1455
+ ...options.yes !== void 0 && { yes: options.yes },
1456
+ ...options.fix !== void 0 && { fix: options.fix },
1457
+ ...options.idType !== void 0 && { idType: options.idType }
1458
+ };
1459
+ return syncAttachments(context.library, operationOptions);
1460
+ }
1461
+ function formatAttachOpenOutput(result) {
1462
+ if (!result.success) {
1463
+ return `Error: ${result.error}`;
1464
+ }
1465
+ if (result.directoryCreated) {
1466
+ return `Created and opened: ${result.path}`;
1467
+ }
1468
+ return `Opened: ${result.path}`;
1469
+ }
1470
+ function formatAttachAddOutput(result) {
1471
+ if (result.requiresConfirmation) {
1472
+ return `File already exists: ${result.existingFile}
1473
+ Use --force to overwrite.`;
1474
+ }
1475
+ if (!result.success) {
1476
+ return `Error: ${result.error}`;
1477
+ }
1478
+ if (result.overwritten) {
1479
+ return `Added (overwritten): ${result.filename}`;
1480
+ }
1481
+ return `Added: ${result.filename}`;
1482
+ }
1483
+ function formatAttachListOutput(result, identifier) {
1484
+ if (!result.success) {
1485
+ return `Error: ${result.error}`;
1486
+ }
1487
+ if (result.files.length === 0) {
1488
+ return `No attachments for reference: ${identifier}`;
1489
+ }
1490
+ const grouped = /* @__PURE__ */ new Map();
1491
+ for (const file of result.files) {
1492
+ const existing = grouped.get(file.role) ?? [];
1493
+ existing.push(file);
1494
+ grouped.set(file.role, existing);
1495
+ }
1496
+ const lines = [];
1497
+ lines.push(`Attachments for ${identifier} (${result.directory}/):`);
1498
+ lines.push("");
1499
+ for (const [role, files] of grouped) {
1500
+ lines.push(`${role}:`);
1501
+ for (const file of files) {
1502
+ if (file.label) {
1503
+ lines.push(` ${file.filename} - "${file.label}"`);
1504
+ } else {
1505
+ lines.push(` ${file.filename}`);
1506
+ }
1507
+ }
1508
+ lines.push("");
1509
+ }
1510
+ return lines.join("\n").trimEnd();
1511
+ }
1512
+ function formatAttachDetachOutput(result) {
1513
+ if (!result.success) {
1514
+ return `Error: ${result.error}`;
1515
+ }
1516
+ const lines = [];
1517
+ for (const filename of result.detached) {
1518
+ if (result.deleted.includes(filename)) {
1519
+ lines.push(`Detached and deleted: ${filename}`);
1520
+ } else {
1521
+ lines.push(`Detached: ${filename}`);
1522
+ }
1523
+ }
1524
+ if (result.directoryDeleted) {
1525
+ lines.push("Directory removed.");
1526
+ }
1527
+ return lines.join("\n");
1528
+ }
1529
+ function pluralize(count, singular) {
1530
+ return count > 1 ? `${singular}s` : singular;
1531
+ }
1532
+ function formatNewFilesSection(result, lines) {
1533
+ const count = result.newFiles.length;
1534
+ if (count === 0) return;
1535
+ const verb = result.applied ? "Added" : "Found";
1536
+ const suffix = result.applied ? "" : " new";
1537
+ lines.push(`${verb} ${count}${suffix} ${pluralize(count, "file")}:`);
1538
+ for (const file of result.newFiles) {
1539
+ const labelPart = file.label ? `, label: "${file.label}"` : "";
1540
+ lines.push(` ${file.filename} → role: ${file.role}${labelPart}`);
1541
+ }
1542
+ lines.push("");
1543
+ }
1544
+ function formatMissingFilesSection(result, lines) {
1545
+ const count = result.missingFiles.length;
1546
+ if (count === 0) return;
1547
+ const header = result.applied ? `Removed ${count} missing ${pluralize(count, "file")} from metadata:` : `Missing ${count} ${pluralize(count, "file")} (in metadata but not on disk):`;
1548
+ lines.push(header);
1549
+ for (const filename of result.missingFiles) {
1550
+ lines.push(` ${filename}`);
1551
+ }
1552
+ lines.push("");
1553
+ }
1554
+ function formatAttachSyncOutput(result) {
1555
+ if (!result.success) {
1556
+ return `Error: ${result.error}`;
1557
+ }
1558
+ const hasNewFiles = result.newFiles.length > 0;
1559
+ const hasMissingFiles = result.missingFiles.length > 0;
1560
+ if (!hasNewFiles && !hasMissingFiles) {
1561
+ return "Already in sync.";
1562
+ }
1563
+ const lines = [];
1564
+ formatNewFilesSection(result, lines);
1565
+ formatMissingFilesSection(result, lines);
1566
+ if (result.applied) {
1567
+ lines.push("Changes applied.");
1568
+ } else {
1569
+ lines.push("");
1570
+ lines.push("(dry-run: no changes made)");
1571
+ if (hasNewFiles) {
1572
+ lines.push("Run with --yes to add new files");
1573
+ }
1574
+ if (hasMissingFiles) {
1575
+ lines.push("Run with --fix to remove missing files from metadata");
1576
+ }
1577
+ }
1578
+ return lines.join("\n").trimEnd();
1579
+ }
1580
+ function getAttachExitCode(result) {
1581
+ return result.success ? 0 : 1;
1582
+ }
1583
+ async function executeInteractiveSelect$1(context, config2) {
1584
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
1585
+ const allReferences = await context.library.getAll();
1586
+ const identifiers = await selectReferencesOrExit(
1587
+ allReferences,
1588
+ { multiSelect: false },
1589
+ config2.cli.tui
1590
+ );
1591
+ return identifiers[0];
1592
+ }
1593
+ async function resolveIdentifier(identifierArg, context, config2) {
1594
+ if (identifierArg) {
1595
+ return identifierArg;
1596
+ }
1597
+ if (isTTY()) {
1598
+ return executeInteractiveSelect$1(context, config2);
1599
+ }
1600
+ const stdinId = await readIdentifierFromStdin();
1601
+ if (!stdinId) {
1602
+ process.stderr.write(
1603
+ "Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
1604
+ );
1605
+ setExitCode(ExitCode.ERROR);
1606
+ return "";
1607
+ }
1608
+ return stdinId;
1609
+ }
1610
+ function displayNamingConvention(identifier, dirPath) {
1611
+ process.stderr.write(`
1612
+ Opening attachments directory for ${identifier}...
1613
+
1614
+ `);
1615
+ process.stderr.write("File naming convention:\n");
1616
+ process.stderr.write(" fulltext.pdf / fulltext.md - Paper body\n");
1617
+ process.stderr.write(" supplement-{label}.ext - Supplementary materials\n");
1618
+ process.stderr.write(" notes-{label}.ext - Your notes\n");
1619
+ process.stderr.write(" draft-{label}.ext - Draft versions\n");
1620
+ process.stderr.write(" {custom}-{label}.ext - Custom role\n\n");
1621
+ process.stderr.write(`Directory: ${dirPath}/
1622
+
1623
+ `);
1624
+ }
1625
+ async function waitForEnter() {
1626
+ return new Promise((resolve2) => {
1627
+ process.stderr.write("Press Enter when done editing...");
1628
+ process.stdin.setRawMode(true);
1629
+ process.stdin.resume();
1630
+ process.stdin.once("data", () => {
1631
+ process.stdin.setRawMode(false);
1632
+ process.stdin.pause();
1633
+ process.stderr.write("\n\n");
1634
+ resolve2();
1635
+ });
1636
+ });
1637
+ }
1638
+ function displayInteractiveSyncResult(result, identifier) {
1639
+ if (result.newFiles.length === 0) {
1640
+ process.stderr.write("No new files detected.\n");
1641
+ return;
1642
+ }
1643
+ process.stderr.write("Scanning directory...\n\n");
1644
+ process.stderr.write(
1645
+ `Found ${result.newFiles.length} new file${result.newFiles.length > 1 ? "s" : ""}:
1646
+ `
1647
+ );
1648
+ for (const file of result.newFiles) {
1649
+ const labelPart = file.label ? `, label: "${file.label}"` : "";
1650
+ process.stderr.write(` ✓ ${file.filename} → role: ${file.role}${labelPart}
1651
+ `);
1652
+ }
1653
+ process.stderr.write(`
1654
+ Updated metadata for ${identifier}.
1655
+ `);
1656
+ }
1657
+ async function runInteractiveMode(identifier, dirPath, attachmentsDirectory, idType, context) {
1658
+ displayNamingConvention(identifier, dirPath);
1659
+ await waitForEnter();
1660
+ const syncResult = await executeAttachSync(
1661
+ {
1662
+ identifier,
1663
+ attachmentsDirectory,
1664
+ yes: true,
1665
+ ...idType && { idType }
1666
+ },
1667
+ context
1668
+ );
1669
+ if (syncResult.success) {
1670
+ displayInteractiveSyncResult(syncResult, identifier);
1671
+ } else {
1672
+ process.stderr.write(`Sync error: ${syncResult.error}
1673
+ `);
1674
+ }
1675
+ }
1676
+ function buildOpenOptions(identifier, filenameArg, options, attachmentsDirectory) {
1677
+ return {
1678
+ identifier,
1679
+ attachmentsDirectory,
1680
+ ...filenameArg && { filename: filenameArg },
1681
+ ...options.print && { print: options.print },
1682
+ ...options.role && { role: options.role },
1683
+ ...options.uuid && { idType: "uuid" }
1684
+ };
1685
+ }
1686
+ async function handleAttachOpenAction(identifierArg, filenameArg, options, globalOpts) {
1687
+ try {
1688
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1689
+ const context = await createExecutionContext(config2, Library.load);
1690
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1691
+ const isDirectoryMode = !filenameArg && !options.role;
1692
+ const shouldUseInteractive = isTTY() && isDirectoryMode && !options.print && !options.noSync;
1693
+ const openOptions = buildOpenOptions(
1694
+ identifier,
1695
+ filenameArg,
1696
+ options,
1697
+ config2.attachments.directory
1698
+ );
1699
+ const result = await executeAttachOpen(openOptions, context);
1700
+ if (!result.success) {
1701
+ process.stderr.write(`Error: ${result.error}
1702
+ `);
1703
+ setExitCode(ExitCode.ERROR);
1704
+ return;
1705
+ }
1706
+ if (options.print) {
1707
+ process.stdout.write(`${result.path}
1708
+ `);
1709
+ setExitCode(ExitCode.SUCCESS);
1710
+ }
1711
+ if (shouldUseInteractive) {
1712
+ await runInteractiveMode(
1713
+ identifier,
1714
+ result.path ?? "",
1715
+ config2.attachments.directory,
1716
+ options.uuid ? "uuid" : void 0,
1717
+ context
1718
+ );
1719
+ } else {
1720
+ process.stderr.write(`${formatAttachOpenOutput(result)}
1721
+ `);
1722
+ }
1723
+ setExitCode(ExitCode.SUCCESS);
1724
+ } catch (error) {
1725
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1726
+ `);
1727
+ setExitCode(ExitCode.INTERNAL_ERROR);
1728
+ }
1729
+ }
1730
+ async function handleAttachAddAction(identifierArg, filePathArg, options, globalOpts) {
1731
+ try {
1732
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1733
+ const context = await createExecutionContext(config2, Library.load);
1734
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1735
+ const addOptions = {
1736
+ identifier,
1737
+ filePath: filePathArg,
1738
+ role: options.role,
1739
+ attachmentsDirectory: config2.attachments.directory,
1740
+ ...options.label && { label: options.label },
1741
+ ...options.move && { move: options.move },
1742
+ ...options.force && { force: options.force },
1743
+ ...options.uuid && { idType: "uuid" }
1744
+ };
1745
+ const result = await executeAttachAdd(addOptions, context);
1746
+ const output = formatAttachAddOutput(result);
1747
+ process.stderr.write(`${output}
1748
+ `);
1749
+ setExitCode(getAttachExitCode(result));
1750
+ } catch (error) {
1751
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1752
+ `);
1753
+ setExitCode(ExitCode.INTERNAL_ERROR);
1754
+ }
1755
+ }
1756
+ async function handleAttachListAction(identifierArg, options, globalOpts) {
1757
+ try {
1758
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1759
+ const context = await createExecutionContext(config2, Library.load);
1760
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1761
+ const listOptions = {
1762
+ identifier,
1763
+ attachmentsDirectory: config2.attachments.directory,
1764
+ ...options.role && { role: options.role },
1765
+ ...options.uuid && { idType: "uuid" }
1766
+ };
1767
+ const result = await executeAttachList(listOptions, context);
1768
+ const output = formatAttachListOutput(result, identifier);
1769
+ process.stdout.write(`${output}
1770
+ `);
1771
+ setExitCode(getAttachExitCode(result));
1772
+ } catch (error) {
1773
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1774
+ `);
1775
+ setExitCode(ExitCode.INTERNAL_ERROR);
1776
+ }
1777
+ }
1778
+ async function handleAttachGetAction(identifierArg, filenameArg, options, globalOpts) {
1779
+ try {
1780
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1781
+ const context = await createExecutionContext(config2, Library.load);
1782
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1783
+ const getOptions = {
1784
+ identifier,
1785
+ attachmentsDirectory: config2.attachments.directory,
1786
+ ...filenameArg && { filename: filenameArg },
1787
+ ...options.role && { role: options.role },
1788
+ ...options.stdout && { stdout: options.stdout },
1789
+ ...options.uuid && { idType: "uuid" }
1790
+ };
1791
+ const result = await executeAttachGet(getOptions, context);
1792
+ if (result.success && result.content && options.stdout) {
1793
+ process.stdout.write(result.content);
1794
+ } else if (result.success) {
1795
+ process.stdout.write(`${result.path}
1796
+ `);
1797
+ } else {
1798
+ process.stderr.write(`Error: ${result.error}
1799
+ `);
1800
+ }
1801
+ setExitCode(getAttachExitCode(result));
1802
+ } catch (error) {
1803
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1804
+ `);
1805
+ setExitCode(ExitCode.INTERNAL_ERROR);
1806
+ }
624
1807
  }
625
- async function readConfirmation(prompt) {
626
- if (!isTTY()) {
627
- return true;
628
- }
629
- const enquirer = await import("enquirer");
630
- const Confirm = enquirer.default.Confirm;
631
- const confirmPrompt = new Confirm({
632
- name: "confirm",
633
- message: prompt,
634
- initial: false
635
- });
1808
+ async function handleAttachDetachAction(identifierArg, filenameArg, options, globalOpts) {
636
1809
  try {
637
- return await confirmPrompt.run();
638
- } catch {
639
- return false;
1810
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1811
+ const context = await createExecutionContext(config2, Library.load);
1812
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1813
+ const detachOptions = {
1814
+ identifier,
1815
+ attachmentsDirectory: config2.attachments.directory,
1816
+ ...filenameArg && { filename: filenameArg },
1817
+ ...options.role && { role: options.role },
1818
+ ...options.all && { all: options.all },
1819
+ ...options.removeFiles && { removeFiles: options.removeFiles },
1820
+ ...options.uuid && { idType: "uuid" }
1821
+ };
1822
+ const result = await executeAttachDetach(detachOptions, context);
1823
+ const output = formatAttachDetachOutput(result);
1824
+ process.stderr.write(`${output}
1825
+ `);
1826
+ setExitCode(getAttachExitCode(result));
1827
+ } catch (error) {
1828
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1829
+ `);
1830
+ setExitCode(ExitCode.INTERNAL_ERROR);
640
1831
  }
641
1832
  }
642
- async function readStdinContent() {
643
- const chunks = [];
644
- for await (const chunk of stdin) {
645
- chunks.push(chunk);
1833
+ async function runInteractiveSyncMode(identifier, attachmentsDirectory, idType, context) {
1834
+ const dryRunOptions = {
1835
+ identifier,
1836
+ attachmentsDirectory,
1837
+ ...idType && { idType }
1838
+ };
1839
+ const dryRunResult = await executeAttachSync(dryRunOptions, context);
1840
+ const hasNewFiles = dryRunResult.newFiles.length > 0;
1841
+ const hasMissingFiles = dryRunResult.missingFiles.length > 0;
1842
+ if (!dryRunResult.success || !hasNewFiles && !hasMissingFiles) {
1843
+ process.stderr.write(`${formatAttachSyncOutput(dryRunResult)}
1844
+ `);
1845
+ return;
646
1846
  }
647
- return Buffer.concat(chunks).toString("utf-8").trim();
1847
+ process.stderr.write(`${formatAttachSyncOutput(dryRunResult)}
1848
+
1849
+ `);
1850
+ const shouldApplyNew = hasNewFiles && await readConfirmation("Add new files to metadata?");
1851
+ const shouldApplyFix = hasMissingFiles && shouldApplyNew && await readConfirmation("Remove missing files?");
1852
+ if (!shouldApplyNew && !shouldApplyFix) {
1853
+ process.stderr.write("No changes applied.\n");
1854
+ return;
1855
+ }
1856
+ const applyOptions = {
1857
+ identifier,
1858
+ attachmentsDirectory,
1859
+ ...shouldApplyNew && { yes: true },
1860
+ ...shouldApplyFix && { fix: true },
1861
+ ...idType && { idType }
1862
+ };
1863
+ const result = await executeAttachSync(applyOptions, context);
1864
+ process.stderr.write(`${formatAttachSyncOutput(result)}
1865
+ `);
648
1866
  }
649
- async function readStdinBuffer() {
650
- const chunks = [];
651
- for await (const chunk of stdin) {
652
- chunks.push(chunk);
1867
+ async function handleAttachSyncAction(identifierArg, options, globalOpts) {
1868
+ try {
1869
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1870
+ const context = await createExecutionContext(config2, Library.load);
1871
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1872
+ const attachmentsDirectory = config2.attachments.directory;
1873
+ const idType = options.uuid ? "uuid" : void 0;
1874
+ const shouldUseInteractive = isTTY() && !options.yes && !options.fix;
1875
+ if (shouldUseInteractive) {
1876
+ await runInteractiveSyncMode(identifier, attachmentsDirectory, idType, context);
1877
+ setExitCode(ExitCode.SUCCESS);
1878
+ return;
1879
+ }
1880
+ const syncOptions = {
1881
+ identifier,
1882
+ attachmentsDirectory,
1883
+ ...options.yes && { yes: true },
1884
+ ...options.fix && { fix: true },
1885
+ ...idType && { idType }
1886
+ };
1887
+ const result = await executeAttachSync(syncOptions, context);
1888
+ process.stderr.write(`${formatAttachSyncOutput(result)}
1889
+ `);
1890
+ setExitCode(getAttachExitCode(result));
1891
+ } catch (error) {
1892
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1893
+ `);
1894
+ setExitCode(ExitCode.INTERNAL_ERROR);
653
1895
  }
654
- return Buffer.concat(chunks);
655
1896
  }
656
1897
  async function validateOptions$2(options) {
657
- if (options.format && !["text", "html", "rtf"].includes(options.format)) {
658
- throw new Error(`Invalid format '${options.format}'. Must be one of: text, html, rtf`);
1898
+ if (options.output && !["text", "html", "rtf"].includes(options.output)) {
1899
+ throw new Error(`Invalid output format '${options.output}'. Must be one of: text, html, rtf`);
659
1900
  }
660
1901
  if (options.cslFile) {
661
1902
  const fs2 = await import("node:fs");
@@ -671,7 +1912,7 @@ function buildCiteOptions(options) {
671
1912
  ...options.style !== void 0 && { style: options.style },
672
1913
  ...options.cslFile !== void 0 && { cslFile: options.cslFile },
673
1914
  ...options.locale !== void 0 && { locale: options.locale },
674
- ...options.format !== void 0 && { format: options.format },
1915
+ ...options.output !== void 0 && { format: options.output },
675
1916
  ...options.inText !== void 0 && { inText: options.inText }
676
1917
  };
677
1918
  }
@@ -709,13 +1950,13 @@ function getCiteExitCode(result) {
709
1950
  return 0;
710
1951
  }
711
1952
  async function executeInteractiveCite(options, context, config2) {
712
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
713
- const { runStyleSelect } = await import("./style-select-CYo0O7MZ.js");
1953
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
1954
+ const { runStyleSelect } = await import("./style-select-BNQHC79W.js");
714
1955
  const allReferences = await context.library.getAll();
715
1956
  const identifiers = await selectReferencesOrExit(
716
1957
  allReferences,
717
1958
  { multiSelect: true },
718
- config2.cli.interactive
1959
+ config2.cli.tui
719
1960
  );
720
1961
  let style = options.style;
721
1962
  if (!style && !options.cslFile) {
@@ -724,7 +1965,8 @@ async function executeInteractiveCite(options, context, config2) {
724
1965
  defaultStyle: config2.citation.defaultStyle
725
1966
  });
726
1967
  if (styleResult.cancelled) {
727
- process.exit(0);
1968
+ setExitCode(ExitCode.SUCCESS);
1969
+ return { results: [] };
728
1970
  }
729
1971
  style = styleResult.style;
730
1972
  }
@@ -744,7 +1986,8 @@ async function handleCiteAction(identifiers, options, globalOpts) {
744
1986
  process.stderr.write(
745
1987
  "Error: No identifiers provided. Provide IDs, pipe them via stdin, or run interactively in a TTY.\n"
746
1988
  );
747
- process.exit(1);
1989
+ setExitCode(ExitCode.ERROR);
1990
+ return;
748
1991
  }
749
1992
  result = await executeCite({ ...options, identifiers: stdinIds }, context);
750
1993
  }
@@ -761,16 +2004,16 @@ async function handleCiteAction(identifiers, options, globalOpts) {
761
2004
  process.stderr.write(`${errors2}
762
2005
  `);
763
2006
  }
764
- process.exit(getCiteExitCode(result));
2007
+ setExitCode(getCiteExitCode(result));
765
2008
  } catch (error) {
766
2009
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
767
2010
  `);
768
- process.exit(4);
2011
+ setExitCode(ExitCode.INTERNAL_ERROR);
769
2012
  }
770
2013
  }
771
2014
  const ENV_OVERRIDE_MAP = {
772
2015
  REFERENCE_MANAGER_LIBRARY: "library",
773
- REFERENCE_MANAGER_FULLTEXT_DIR: "fulltext.directory",
2016
+ REFERENCE_MANAGER_ATTACHMENTS_DIR: "attachments.directory",
774
2017
  REFERENCE_MANAGER_CLI_DEFAULT_LIMIT: "cli.default_limit",
775
2018
  REFERENCE_MANAGER_MCP_DEFAULT_LIMIT: "mcp.default_limit",
776
2019
  PUBMED_EMAIL: "pubmed.email",
@@ -852,14 +2095,14 @@ const CONFIG_KEY_REGISTRY = [
852
2095
  description: "Default sort order",
853
2096
  enumValues: ["asc", "desc"]
854
2097
  },
855
- // cli.interactive section
2098
+ // cli.tui section
856
2099
  {
857
- key: "cli.interactive.limit",
2100
+ key: "cli.tui.limit",
858
2101
  type: "integer",
859
- description: "Result limit in interactive mode"
2102
+ description: "Result limit in TUI mode"
860
2103
  },
861
2104
  {
862
- key: "cli.interactive.debounce_ms",
2105
+ key: "cli.tui.debounce_ms",
863
2106
  type: "integer",
864
2107
  description: "Search debounce delay (ms)"
865
2108
  },
@@ -1064,7 +2307,7 @@ function createConfigTemplate() {
1064
2307
  # default_sort = "updated" # created, updated, published, author, title
1065
2308
  # default_order = "desc" # asc, desc
1066
2309
 
1067
- [cli.interactive]
2310
+ [cli.tui]
1068
2311
  # limit = 20
1069
2312
  # debounce_ms = 200
1070
2313
 
@@ -1307,9 +2550,9 @@ function toSnakeCaseConfig(config2) {
1307
2550
  default_limit: config2.cli.defaultLimit,
1308
2551
  default_sort: config2.cli.defaultSort,
1309
2552
  default_order: config2.cli.defaultOrder,
1310
- interactive: {
1311
- limit: config2.cli.interactive.limit,
1312
- debounce_ms: config2.cli.interactive.debounceMs
2553
+ tui: {
2554
+ limit: config2.cli.tui.limit,
2555
+ debounce_ms: config2.cli.tui.debounceMs
1313
2556
  },
1314
2557
  edit: {
1315
2558
  default_format: config2.cli.edit.defaultFormat
@@ -1466,21 +2709,21 @@ function resolveEditor(platform) {
1466
2709
  }
1467
2710
  function registerConfigCommand(program) {
1468
2711
  const configCmd = program.command("config").description("Manage configuration settings");
1469
- configCmd.command("show").description("Display effective configuration").option("--json", "Output in JSON format").option("--section <name>", "Show only a specific section").option("--sources", "Include source information for each value").action(async (options) => {
2712
+ 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) => {
1470
2713
  try {
1471
2714
  const config2 = loadConfig();
1472
2715
  const output = showConfig(config2, {
1473
- json: options.json,
2716
+ json: options.output === "json",
1474
2717
  section: options.section,
1475
2718
  sources: options.sources
1476
2719
  });
1477
2720
  process.stdout.write(`${output}
1478
2721
  `);
1479
- process.exit(0);
2722
+ setExitCode(ExitCode.SUCCESS);
1480
2723
  } catch (error) {
1481
2724
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1482
2725
  `);
1483
- process.exit(1);
2726
+ setExitCode(ExitCode.ERROR);
1484
2727
  }
1485
2728
  });
1486
2729
  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) => {
@@ -1498,14 +2741,14 @@ function registerConfigCommand(program) {
1498
2741
  const { formatValue: formatValue2 } = await Promise.resolve().then(() => get);
1499
2742
  process.stdout.write(`${formatValue2(result.value)}
1500
2743
  `);
1501
- process.exit(0);
2744
+ setExitCode(ExitCode.SUCCESS);
1502
2745
  } else {
1503
- process.exit(1);
2746
+ setExitCode(ExitCode.ERROR);
1504
2747
  }
1505
2748
  } catch (error) {
1506
2749
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1507
2750
  `);
1508
- process.exit(1);
2751
+ setExitCode(ExitCode.ERROR);
1509
2752
  }
1510
2753
  });
1511
2754
  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) => {
@@ -1523,17 +2766,18 @@ function registerConfigCommand(program) {
1523
2766
  if (!result.success) {
1524
2767
  process.stderr.write(`Error: ${result.error}
1525
2768
  `);
1526
- process.exit(1);
2769
+ setExitCode(ExitCode.ERROR);
2770
+ return;
1527
2771
  }
1528
2772
  if (result.warning) {
1529
2773
  process.stderr.write(`${result.warning}
1530
2774
  `);
1531
2775
  }
1532
- process.exit(0);
2776
+ setExitCode(ExitCode.SUCCESS);
1533
2777
  } catch (error) {
1534
2778
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1535
2779
  `);
1536
- process.exit(1);
2780
+ setExitCode(ExitCode.ERROR);
1537
2781
  }
1538
2782
  });
1539
2783
  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) => {
@@ -1548,27 +2792,28 @@ function registerConfigCommand(program) {
1548
2792
  if (!result.success) {
1549
2793
  process.stderr.write(`Error: ${result.error}
1550
2794
  `);
1551
- process.exit(1);
2795
+ setExitCode(ExitCode.ERROR);
2796
+ return;
1552
2797
  }
1553
- process.exit(0);
2798
+ setExitCode(ExitCode.SUCCESS);
1554
2799
  } catch (error) {
1555
2800
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1556
2801
  `);
1557
- process.exit(1);
2802
+ setExitCode(ExitCode.ERROR);
1558
2803
  }
1559
2804
  });
1560
- configCmd.command("list-keys").description("List all available configuration keys").option("--section <name>", "List keys only in a specific section").action(async (options) => {
2805
+ configCmd.command("keys").description("List all available configuration keys").option("--section <name>", "List keys only in a specific section").action(async (options) => {
1561
2806
  try {
1562
2807
  const output = listConfigKeys({ section: options.section });
1563
2808
  if (output) {
1564
2809
  process.stdout.write(`${output}
1565
2810
  `);
1566
2811
  }
1567
- process.exit(0);
2812
+ setExitCode(ExitCode.SUCCESS);
1568
2813
  } catch (error) {
1569
2814
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1570
2815
  `);
1571
- process.exit(1);
2816
+ setExitCode(ExitCode.ERROR);
1572
2817
  }
1573
2818
  });
1574
2819
  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) => {
@@ -1576,18 +2821,19 @@ function registerConfigCommand(program) {
1576
2821
  const output = showConfigPaths({ user: options.user, local: options.local });
1577
2822
  process.stdout.write(`${output}
1578
2823
  `);
1579
- process.exit(0);
2824
+ setExitCode(ExitCode.SUCCESS);
1580
2825
  } catch (error) {
1581
2826
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1582
2827
  `);
1583
- process.exit(1);
2828
+ setExitCode(ExitCode.ERROR);
1584
2829
  }
1585
2830
  });
1586
2831
  configCmd.command("edit").description("Open configuration file in editor").option("--local", "Edit current directory config").action(async (options) => {
1587
2832
  try {
1588
2833
  if (!process.stdin.isTTY) {
1589
2834
  process.stderr.write("Error: config edit requires a terminal (TTY)\n");
1590
- process.exit(1);
2835
+ setExitCode(ExitCode.ERROR);
2836
+ return;
1591
2837
  }
1592
2838
  const target = getConfigEditTarget({ local: options.local });
1593
2839
  if (!target.exists) {
@@ -1597,11 +2843,11 @@ function registerConfigCommand(program) {
1597
2843
  }
1598
2844
  const editor = resolveEditor();
1599
2845
  const exitCode = openEditor(editor, target.path);
1600
- process.exit(exitCode);
2846
+ setExitCode(exitCode);
1601
2847
  } catch (error) {
1602
2848
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1603
2849
  `);
1604
- process.exit(1);
2850
+ setExitCode(ExitCode.ERROR);
1605
2851
  }
1606
2852
  });
1607
2853
  }
@@ -4542,12 +5788,12 @@ function formatEditOutput(result) {
4542
5788
  return lines.join("\n");
4543
5789
  }
4544
5790
  async function executeInteractiveEdit(options, context, config2) {
4545
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
5791
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
4546
5792
  const allReferences = await context.library.getAll();
4547
5793
  const identifiers = await selectReferencesOrExit(
4548
5794
  allReferences,
4549
5795
  { multiSelect: true },
4550
- config2.cli.interactive
5796
+ config2.cli.tui
4551
5797
  );
4552
5798
  const format2 = options.format ?? config2.cli.edit.defaultFormat;
4553
5799
  return executeEditCommand(
@@ -4572,7 +5818,7 @@ async function handleEditAction(identifiers, options, globalOpts) {
4572
5818
  const output2 = formatEditOutput(result2);
4573
5819
  process.stderr.write(`${output2}
4574
5820
  `);
4575
- process.exit(result2.success ? 0 : 1);
5821
+ setExitCode(result2.success ? ExitCode.SUCCESS : ExitCode.ERROR);
4576
5822
  return;
4577
5823
  } else {
4578
5824
  const stdinIds = await readIdentifiersFromStdin();
@@ -4580,13 +5826,15 @@ async function handleEditAction(identifiers, options, globalOpts) {
4580
5826
  process.stderr.write(
4581
5827
  "Error: No identifiers provided. Provide IDs, pipe them via stdin, or run interactively in a TTY.\n"
4582
5828
  );
4583
- process.exit(1);
5829
+ setExitCode(ExitCode.ERROR);
5830
+ return;
4584
5831
  }
4585
5832
  resolvedIdentifiers = stdinIds;
4586
5833
  }
4587
5834
  if (!isTTY()) {
4588
5835
  process.stderr.write("Error: Edit command requires a TTY to open the editor.\n");
4589
- process.exit(1);
5836
+ setExitCode(ExitCode.ERROR);
5837
+ return;
4590
5838
  }
4591
5839
  const format2 = options.format ?? config2.cli.edit.defaultFormat;
4592
5840
  const result = await executeEditCommand(
@@ -4601,11 +5849,11 @@ async function handleEditAction(identifiers, options, globalOpts) {
4601
5849
  const output = formatEditOutput(result);
4602
5850
  process.stderr.write(`${output}
4603
5851
  `);
4604
- process.exit(result.success ? 0 : 1);
5852
+ setExitCode(result.success ? ExitCode.SUCCESS : ExitCode.ERROR);
4605
5853
  } catch (error) {
4606
5854
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
4607
5855
  `);
4608
- process.exit(4);
5856
+ setExitCode(ExitCode.INTERNAL_ERROR);
4609
5857
  }
4610
5858
  }
4611
5859
  const ALIAS = Symbol.for("yaml.alias");
@@ -7778,7 +9026,7 @@ async function executeExport(options, context) {
7778
9026
  return { items: items2, notFound };
7779
9027
  }
7780
9028
  function formatExportOutput(result, options) {
7781
- const format2 = options.format ?? "json";
9029
+ const format2 = options.output ?? "json";
7782
9030
  const singleIdRequest = (options.ids?.length ?? 0) === 1 && !options.all && !options.search;
7783
9031
  const data = result.items.length === 1 && singleIdRequest ? result.items[0] : result.items;
7784
9032
  if (format2 === "json") {
@@ -7795,183 +9043,6 @@ function formatExportOutput(result, options) {
7795
9043
  function getExportExitCode(result) {
7796
9044
  return result.notFound.length > 0 ? 1 : 0;
7797
9045
  }
7798
- const FULLTEXT_EXTENSIONS = {
7799
- pdf: ".pdf",
7800
- markdown: ".md"
7801
- };
7802
- function generateFulltextFilename(item, type2) {
7803
- const uuid2 = item.custom?.uuid;
7804
- if (!uuid2) {
7805
- throw new Error("Missing uuid in custom field");
7806
- }
7807
- const parts = [item.id];
7808
- if (item.PMID && item.PMID.length > 0) {
7809
- parts.push(`PMID${item.PMID}`);
7810
- }
7811
- parts.push(uuid2);
7812
- return parts.join("-") + FULLTEXT_EXTENSIONS[type2];
7813
- }
7814
- class FulltextIOError extends Error {
7815
- constructor(message, cause) {
7816
- super(message);
7817
- this.cause = cause;
7818
- this.name = "FulltextIOError";
7819
- }
7820
- }
7821
- class FulltextNotAttachedError extends Error {
7822
- constructor(itemId, type2) {
7823
- super(`No ${type2} attached to reference ${itemId}`);
7824
- this.itemId = itemId;
7825
- this.type = type2;
7826
- this.name = "FulltextNotAttachedError";
7827
- }
7828
- }
7829
- class FulltextManager {
7830
- constructor(fulltextDirectory) {
7831
- this.fulltextDirectory = fulltextDirectory;
7832
- }
7833
- /**
7834
- * Ensure the fulltext directory exists
7835
- */
7836
- async ensureDirectory() {
7837
- await mkdir(this.fulltextDirectory, { recursive: true });
7838
- }
7839
- /**
7840
- * Attach a file to a reference
7841
- */
7842
- async attachFile(item, sourcePath, type2, options) {
7843
- const { move = false, force = false } = options ?? {};
7844
- const newFilename = generateFulltextFilename(item, type2);
7845
- this.validateSourceFile(sourcePath);
7846
- const existingFilename = this.getExistingFilename(item, type2);
7847
- if (existingFilename && !force) {
7848
- return {
7849
- filename: newFilename,
7850
- existingFile: existingFilename,
7851
- overwritten: false
7852
- };
7853
- }
7854
- await this.ensureDirectory();
7855
- const deletedOldFile = await this.deleteOldFileIfNeeded(existingFilename, newFilename, force);
7856
- const destPath = join(this.fulltextDirectory, newFilename);
7857
- await this.copyOrMoveFile(sourcePath, destPath, move);
7858
- const result = {
7859
- filename: newFilename,
7860
- overwritten: existingFilename !== void 0
7861
- };
7862
- if (deletedOldFile) {
7863
- result.deletedOldFile = deletedOldFile;
7864
- }
7865
- return result;
7866
- }
7867
- /**
7868
- * Validate that source file exists
7869
- */
7870
- validateSourceFile(sourcePath) {
7871
- if (!existsSync(sourcePath)) {
7872
- throw new FulltextIOError(`Source file not found: ${sourcePath}`);
7873
- }
7874
- }
7875
- /**
7876
- * Delete old file if force mode and filename changed
7877
- * @returns Deleted filename or undefined
7878
- */
7879
- async deleteOldFileIfNeeded(existingFilename, newFilename, force) {
7880
- if (!force || !existingFilename || existingFilename === newFilename) {
7881
- return void 0;
7882
- }
7883
- const oldPath = join(this.fulltextDirectory, existingFilename);
7884
- try {
7885
- await unlink(oldPath);
7886
- } catch {
7887
- }
7888
- return existingFilename;
7889
- }
7890
- /**
7891
- * Copy or move file to destination
7892
- */
7893
- async copyOrMoveFile(sourcePath, destPath, move) {
7894
- try {
7895
- if (move) {
7896
- await rename(sourcePath, destPath);
7897
- } else {
7898
- await copyFile(sourcePath, destPath);
7899
- }
7900
- } catch (error) {
7901
- const operation = move ? "move" : "copy";
7902
- throw new FulltextIOError(
7903
- `Failed to ${operation} file to ${destPath}`,
7904
- error instanceof Error ? error : void 0
7905
- );
7906
- }
7907
- }
7908
- /**
7909
- * Get the full path for an attached file
7910
- * @returns Full path or null if not attached
7911
- */
7912
- getFilePath(item, type2) {
7913
- const filename = this.getExistingFilename(item, type2);
7914
- if (!filename) {
7915
- return null;
7916
- }
7917
- return join(this.fulltextDirectory, filename);
7918
- }
7919
- /**
7920
- * Detach a file from a reference
7921
- */
7922
- async detachFile(item, type2, options) {
7923
- const { delete: deleteFile = false } = options ?? {};
7924
- const filename = this.getExistingFilename(item, type2);
7925
- if (!filename) {
7926
- throw new FulltextNotAttachedError(item.id, type2);
7927
- }
7928
- if (deleteFile) {
7929
- const filePath = join(this.fulltextDirectory, filename);
7930
- try {
7931
- await unlink(filePath);
7932
- } catch {
7933
- }
7934
- }
7935
- return {
7936
- filename,
7937
- deleted: deleteFile
7938
- };
7939
- }
7940
- /**
7941
- * Get list of attached fulltext types
7942
- */
7943
- getAttachedTypes(item) {
7944
- const types2 = [];
7945
- const fulltext = item.custom?.fulltext;
7946
- if (fulltext?.pdf) {
7947
- types2.push("pdf");
7948
- }
7949
- if (fulltext?.markdown) {
7950
- types2.push("markdown");
7951
- }
7952
- return types2;
7953
- }
7954
- /**
7955
- * Check if item has attachment
7956
- * @param type Optional type to check; if omitted, checks for any attachment
7957
- */
7958
- hasAttachment(item, type2) {
7959
- if (type2) {
7960
- return this.getExistingFilename(item, type2) !== void 0;
7961
- }
7962
- return this.getAttachedTypes(item).length > 0;
7963
- }
7964
- /**
7965
- * Get existing filename from item metadata
7966
- */
7967
- getExistingFilename(item, type2) {
7968
- const fulltext = item.custom?.fulltext;
7969
- if (!fulltext) {
7970
- return void 0;
7971
- }
7972
- return fulltext[type2];
7973
- }
7974
- }
7975
9046
  function detectType(filePath) {
7976
9047
  const ext = extname(filePath).toLowerCase();
7977
9048
  if (ext === ".pdf") return "pdf";
@@ -7996,8 +9067,8 @@ function resolveFileType(explicitType, filePath, stdinContent) {
7996
9067
  function prepareStdinSource(stdinContent, fileType) {
7997
9068
  try {
7998
9069
  const tempDir = mkdtempSync(join(tmpdir(), "refmgr-"));
7999
- const ext = fileType === "pdf" ? ".pdf" : ".md";
8000
- const sourcePath = join(tempDir, `stdin${ext}`);
9070
+ const ext = formatToExtension(fileType);
9071
+ const sourcePath = join(tempDir, `stdin.${ext}`);
8001
9072
  writeFileSync(sourcePath, stdinContent);
8002
9073
  return { sourcePath, tempDir };
8003
9074
  } catch (error) {
@@ -8012,13 +9083,6 @@ async function cleanupTempDir(tempDir) {
8012
9083
  });
8013
9084
  }
8014
9085
  }
8015
- function buildNewFulltext(currentFulltext, fileType, filename) {
8016
- const newFulltext = {};
8017
- if (currentFulltext.pdf) newFulltext.pdf = currentFulltext.pdf;
8018
- if (currentFulltext.markdown) newFulltext.markdown = currentFulltext.markdown;
8019
- newFulltext[fileType] = filename;
8020
- return newFulltext;
8021
- }
8022
9086
  function prepareSourcePath(filePath, stdinContent, fileType) {
8023
9087
  if (stdinContent) {
8024
9088
  return prepareStdinSource(stdinContent, fileType);
@@ -8028,12 +9092,26 @@ function prepareSourcePath(filePath, stdinContent, fileType) {
8028
9092
  }
8029
9093
  return { sourcePath: filePath };
8030
9094
  }
8031
- async function performAttach(manager, item, sourcePath, fileType, move, force) {
8032
- const attachOptions = {
8033
- ...move !== void 0 && { move },
8034
- ...force !== void 0 && { force }
9095
+ function convertResult(result, fileType) {
9096
+ if (result.success) {
9097
+ return {
9098
+ success: true,
9099
+ filename: result.filename,
9100
+ type: fileType,
9101
+ overwritten: result.overwritten
9102
+ };
9103
+ }
9104
+ if (result.requiresConfirmation) {
9105
+ return {
9106
+ success: false,
9107
+ existingFile: result.existingFile,
9108
+ requiresConfirmation: true
9109
+ };
9110
+ }
9111
+ return {
9112
+ success: false,
9113
+ error: result.error
8035
9114
  };
8036
- return manager.attachFile(item, sourcePath, fileType, attachOptions);
8037
9115
  }
8038
9116
  async function fulltextAttach(library, options) {
8039
9117
  const {
@@ -8046,71 +9124,78 @@ async function fulltextAttach(library, options) {
8046
9124
  fulltextDirectory,
8047
9125
  stdinContent
8048
9126
  } = options;
8049
- const item = await library.find(identifier, { idType });
8050
- if (!item) {
8051
- return { success: false, error: `Reference '${identifier}' not found` };
8052
- }
8053
9127
  const fileTypeResult = resolveFileType(explicitType, filePath, stdinContent);
8054
9128
  if (typeof fileTypeResult === "object" && "error" in fileTypeResult) {
9129
+ const item = await library.find(identifier, { idType });
9130
+ if (!item) {
9131
+ return { success: false, error: `Reference '${identifier}' not found` };
9132
+ }
8055
9133
  return { success: false, error: fileTypeResult.error };
8056
9134
  }
8057
9135
  const fileType = fileTypeResult;
8058
9136
  const sourceResult = prepareSourcePath(filePath, stdinContent, fileType);
8059
9137
  if ("error" in sourceResult) {
9138
+ const item = await library.find(identifier, { idType });
9139
+ if (!item) {
9140
+ return { success: false, error: `Reference '${identifier}' not found` };
9141
+ }
8060
9142
  return { success: false, error: sourceResult.error };
8061
9143
  }
8062
9144
  const { sourcePath, tempDir } = sourceResult;
8063
- const manager = new FulltextManager(fulltextDirectory);
8064
9145
  try {
8065
- const result = await performAttach(manager, item, sourcePath, fileType, move, force);
8066
- if (result.existingFile && !result.overwritten) {
8067
- await cleanupTempDir(tempDir);
8068
- return { success: false, existingFile: result.existingFile, requiresConfirmation: true };
8069
- }
8070
- const newFulltext = buildNewFulltext(item.custom?.fulltext ?? {}, fileType, result.filename);
8071
- await updateReference(library, {
9146
+ const result = await addAttachment(library, {
8072
9147
  identifier,
8073
- updates: {
8074
- custom: { fulltext: newFulltext }
8075
- },
8076
- idType
9148
+ filePath: sourcePath,
9149
+ role: FULLTEXT_ROLE,
9150
+ move: move ?? false,
9151
+ force: force ?? false,
9152
+ idType,
9153
+ attachmentsDirectory: fulltextDirectory
8077
9154
  });
8078
9155
  await cleanupTempDir(tempDir);
8079
- return {
8080
- success: true,
8081
- filename: result.filename,
8082
- type: fileType,
8083
- overwritten: result.overwritten
8084
- };
9156
+ return convertResult(result, fileType);
8085
9157
  } catch (error) {
8086
9158
  await cleanupTempDir(tempDir);
8087
- if (error instanceof FulltextIOError) {
8088
- return { success: false, error: error.message };
8089
- }
8090
9159
  throw error;
8091
9160
  }
8092
9161
  }
8093
- async function getFileContent(manager, item, type2, identifier) {
8094
- const filePath = manager.getFilePath(item, type2);
8095
- if (!filePath) {
9162
+ function buildFilePath$1(attachmentsDirectory, directory, filename) {
9163
+ return normalizePathForOutput(join(attachmentsDirectory, directory, filename));
9164
+ }
9165
+ async function getFileContent(filePath) {
9166
+ const content = await readFile(filePath);
9167
+ return { success: true, content };
9168
+ }
9169
+ async function handleStdoutMode(attachments, type2, identifier, fulltextDirectory) {
9170
+ const file = findFulltextFile(attachments, type2);
9171
+ if (!file || !attachments?.directory) {
8096
9172
  return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
8097
9173
  }
9174
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
8098
9175
  try {
8099
- const content = await readFile(filePath);
8100
- return { success: true, content };
8101
- } catch (error) {
8102
- return {
8103
- success: false,
8104
- error: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`
8105
- };
9176
+ return await getFileContent(filePath);
9177
+ } catch {
9178
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
8106
9179
  }
8107
9180
  }
8108
- function getFilePaths(manager, item, types2, identifier) {
9181
+ function getSingleTypePath(attachments, type2, identifier, fulltextDirectory) {
9182
+ const file = findFulltextFile(attachments, type2);
9183
+ if (!file || !attachments?.directory) {
9184
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
9185
+ }
9186
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
9187
+ const paths = {};
9188
+ paths[type2] = filePath;
9189
+ return { success: true, paths };
9190
+ }
9191
+ function getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier) {
8109
9192
  const paths = {};
8110
- for (const t of types2) {
8111
- const filePath = manager.getFilePath(item, t);
8112
- if (filePath) {
8113
- paths[t] = filePath;
9193
+ for (const file of fulltextFiles) {
9194
+ const ext = file.filename.split(".").pop() || "";
9195
+ const format2 = extensionToFormat(ext);
9196
+ if (format2) {
9197
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
9198
+ paths[format2] = filePath;
8114
9199
  }
8115
9200
  }
8116
9201
  if (Object.keys(paths).length === 0) {
@@ -8124,92 +9209,98 @@ async function fulltextGet(library, options) {
8124
9209
  if (!item) {
8125
9210
  return { success: false, error: `Reference '${identifier}' not found` };
8126
9211
  }
8127
- const manager = new FulltextManager(fulltextDirectory);
9212
+ const attachments = item.custom?.attachments;
8128
9213
  if (stdout2 && type2) {
8129
- return getFileContent(manager, item, type2, identifier);
9214
+ return handleStdoutMode(attachments, type2, identifier, fulltextDirectory);
9215
+ }
9216
+ const fulltextFiles = findFulltextFiles(attachments);
9217
+ if (fulltextFiles.length === 0) {
9218
+ return { success: false, error: `No fulltext attached to '${identifier}'` };
9219
+ }
9220
+ if (type2) {
9221
+ return getSingleTypePath(attachments, type2, identifier, fulltextDirectory);
8130
9222
  }
8131
- const attachedTypes = type2 ? [type2] : manager.getAttachedTypes(item);
8132
- if (attachedTypes.length === 0) {
9223
+ if (!attachments) {
8133
9224
  return { success: false, error: `No fulltext attached to '${identifier}'` };
8134
9225
  }
8135
- return getFilePaths(manager, item, attachedTypes, identifier);
9226
+ return getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier);
8136
9227
  }
8137
- async function performDetachOperations(manager, item, typesToDetach, deleteFile) {
9228
+ function getFilesToDetach(attachments, type2) {
9229
+ if (type2) {
9230
+ const file = findFulltextFile(attachments, type2);
9231
+ return file ? [file] : [];
9232
+ }
9233
+ return findFulltextFiles(attachments);
9234
+ }
9235
+ async function detachFiles(library, files, identifier, removeFiles, idType, fulltextDirectory) {
8138
9236
  const detached = [];
8139
9237
  const deleted = [];
8140
- for (const t of typesToDetach) {
8141
- const detachOptions = deleteFile ? { delete: deleteFile } : {};
8142
- const result = await manager.detachFile(item, t, detachOptions);
8143
- detached.push(t);
8144
- if (result.deleted) {
8145
- deleted.push(t);
9238
+ for (const file of files) {
9239
+ const result = await detachAttachment(library, {
9240
+ identifier,
9241
+ filename: file.filename,
9242
+ removeFiles: removeFiles ?? false,
9243
+ idType,
9244
+ attachmentsDirectory: fulltextDirectory
9245
+ });
9246
+ if (result.success) {
9247
+ const ext = file.filename.split(".").pop() || "";
9248
+ const format2 = extensionToFormat(ext);
9249
+ if (format2) {
9250
+ detached.push(format2);
9251
+ if (result.deleted.length > 0) {
9252
+ deleted.push(format2);
9253
+ }
9254
+ }
8146
9255
  }
8147
9256
  }
8148
9257
  return { detached, deleted };
8149
9258
  }
8150
- function buildRemainingFulltext(currentFulltext, detached) {
8151
- const newFulltext = {};
8152
- if (currentFulltext.pdf && !detached.includes("pdf")) {
8153
- newFulltext.pdf = currentFulltext.pdf;
8154
- }
8155
- if (currentFulltext.markdown && !detached.includes("markdown")) {
8156
- newFulltext.markdown = currentFulltext.markdown;
9259
+ function buildResult(detached, deleted, identifier) {
9260
+ if (detached.length === 0) {
9261
+ return { success: false, error: `Failed to detach fulltext from '${identifier}'` };
8157
9262
  }
8158
- return Object.keys(newFulltext).length > 0 ? newFulltext : void 0;
8159
- }
8160
- function handleDetachError(error) {
8161
- if (error instanceof FulltextNotAttachedError || error instanceof FulltextIOError) {
8162
- return { success: false, error: error.message };
9263
+ const result = { success: true, detached };
9264
+ if (deleted.length > 0) {
9265
+ result.deleted = deleted;
8163
9266
  }
8164
- throw error;
9267
+ return result;
8165
9268
  }
8166
9269
  async function fulltextDetach(library, options) {
8167
- const { identifier, type: type2, delete: deleteFile, idType = "id", fulltextDirectory } = options;
9270
+ const { identifier, type: type2, removeFiles, idType = "id", fulltextDirectory } = options;
8168
9271
  const item = await library.find(identifier, { idType });
8169
9272
  if (!item) {
8170
9273
  return { success: false, error: `Reference '${identifier}' not found` };
8171
9274
  }
8172
- const manager = new FulltextManager(fulltextDirectory);
8173
- const typesToDetach = type2 ? [type2] : manager.getAttachedTypes(item);
8174
- if (typesToDetach.length === 0) {
9275
+ const attachments = item.custom?.attachments;
9276
+ const fulltextFiles = findFulltextFiles(attachments);
9277
+ if (fulltextFiles.length === 0) {
8175
9278
  return { success: false, error: `No fulltext attached to '${identifier}'` };
8176
9279
  }
8177
- try {
8178
- const { detached, deleted } = await performDetachOperations(
8179
- manager,
8180
- item,
8181
- typesToDetach,
8182
- deleteFile
8183
- );
8184
- const updatedFulltext = buildRemainingFulltext(item.custom?.fulltext ?? {}, detached);
8185
- await updateReference(library, {
8186
- identifier,
8187
- updates: {
8188
- custom: { fulltext: updatedFulltext }
8189
- },
8190
- idType
8191
- });
8192
- const resultData = { success: true, detached };
8193
- if (deleted.length > 0) {
8194
- resultData.deleted = deleted;
8195
- }
8196
- return resultData;
8197
- } catch (error) {
8198
- return handleDetachError(error);
9280
+ const filesToDetach = getFilesToDetach(attachments, type2);
9281
+ if (filesToDetach.length === 0) {
9282
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
8199
9283
  }
8200
- }
8201
- function getFulltextPath(item, type2, fulltextDirectory) {
8202
- const fulltext = item.custom?.fulltext;
8203
- if (!fulltext) return void 0;
8204
- const filename = type2 === "pdf" ? fulltext.pdf : fulltext.markdown;
8205
- if (!filename) return void 0;
8206
- return join(fulltextDirectory, filename);
8207
- }
8208
- function determineTypeToOpen(item) {
8209
- const fulltext = item.custom?.fulltext;
8210
- if (!fulltext) return void 0;
8211
- if (fulltext.pdf) return "pdf";
8212
- if (fulltext.markdown) return "markdown";
9284
+ const { detached, deleted } = await detachFiles(
9285
+ library,
9286
+ filesToDetach,
9287
+ identifier,
9288
+ removeFiles,
9289
+ idType,
9290
+ fulltextDirectory
9291
+ );
9292
+ return buildResult(detached, deleted, identifier);
9293
+ }
9294
+ function buildFilePath(attachmentsDirectory, directory, filename) {
9295
+ return join(attachmentsDirectory, directory, filename);
9296
+ }
9297
+ function determineTypeToOpen(attachments) {
9298
+ const files = findFulltextFiles(attachments);
9299
+ if (files.length === 0) return void 0;
9300
+ const pdfFile = files.find((f) => f.filename.endsWith(".pdf"));
9301
+ if (pdfFile) return "pdf";
9302
+ const mdFile = files.find((f) => f.filename.endsWith(".md"));
9303
+ if (mdFile) return "markdown";
8213
9304
  return void 0;
8214
9305
  }
8215
9306
  async function fulltextOpen(library, options) {
@@ -8218,14 +9309,16 @@ async function fulltextOpen(library, options) {
8218
9309
  if (!item) {
8219
9310
  return { success: false, error: `Reference not found: ${identifier}` };
8220
9311
  }
8221
- const typeToOpen = type2 ?? determineTypeToOpen(item);
9312
+ const attachments = item.custom?.attachments;
9313
+ const typeToOpen = type2 ?? determineTypeToOpen(attachments);
8222
9314
  if (!typeToOpen) {
8223
9315
  return { success: false, error: `No fulltext attached to reference: ${identifier}` };
8224
9316
  }
8225
- const filePath = getFulltextPath(item, typeToOpen, fulltextDirectory);
8226
- if (!filePath) {
9317
+ const file = findFulltextFile(attachments, typeToOpen);
9318
+ if (!file || !attachments?.directory) {
8227
9319
  return { success: false, error: `No ${typeToOpen} attached to reference: ${identifier}` };
8228
9320
  }
9321
+ const filePath = buildFilePath(fulltextDirectory, attachments.directory, file.filename);
8229
9322
  if (!existsSync(filePath)) {
8230
9323
  return {
8231
9324
  success: false,
@@ -8273,7 +9366,7 @@ async function executeFulltextDetach(options, context) {
8273
9366
  const operationOptions = {
8274
9367
  identifier: options.identifier,
8275
9368
  type: options.type,
8276
- delete: options.delete,
9369
+ removeFiles: options.removeFiles,
8277
9370
  idType: options.idType,
8278
9371
  fulltextDirectory: options.fulltextDirectory
8279
9372
  };
@@ -8344,12 +9437,12 @@ function getFulltextExitCode(result) {
8344
9437
  return result.success ? 0 : 1;
8345
9438
  }
8346
9439
  async function executeInteractiveSelect(context, config2) {
8347
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
9440
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
8348
9441
  const allReferences = await context.library.getAll();
8349
9442
  const identifiers = await selectReferencesOrExit(
8350
9443
  allReferences,
8351
9444
  { multiSelect: false },
8352
- config2.cli.interactive
9445
+ config2.cli.tui
8353
9446
  );
8354
9447
  return identifiers[0];
8355
9448
  }
@@ -8380,7 +9473,8 @@ async function handleFulltextAttachAction(identifierArg, filePathArg, options, g
8380
9473
  process.stderr.write(
8381
9474
  "Error: No identifier provided. Provide an ID or run interactively in a TTY.\n"
8382
9475
  );
8383
- process.exit(1);
9476
+ setExitCode(ExitCode.ERROR);
9477
+ return;
8384
9478
  }
8385
9479
  identifier = await executeInteractiveSelect(context, config2);
8386
9480
  }
@@ -8388,7 +9482,7 @@ async function handleFulltextAttachAction(identifierArg, filePathArg, options, g
8388
9482
  const stdinContent = !filePath && type2 ? await readStdinBuffer() : void 0;
8389
9483
  const attachOptions = {
8390
9484
  identifier,
8391
- fulltextDirectory: config2.fulltext.directory,
9485
+ fulltextDirectory: config2.attachments.directory,
8392
9486
  ...filePath && { filePath },
8393
9487
  ...type2 && { type: type2 },
8394
9488
  ...options.move && { move: options.move },
@@ -8400,11 +9494,11 @@ async function handleFulltextAttachAction(identifierArg, filePathArg, options, g
8400
9494
  const output = formatFulltextAttachOutput(result);
8401
9495
  process.stderr.write(`${output}
8402
9496
  `);
8403
- process.exit(getFulltextExitCode(result));
9497
+ setExitCode(getFulltextExitCode(result));
8404
9498
  } catch (error) {
8405
9499
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
8406
9500
  `);
8407
- process.exit(4);
9501
+ setExitCode(ExitCode.INTERNAL_ERROR);
8408
9502
  }
8409
9503
  }
8410
9504
  function outputFulltextGetResult(result, useStdout) {
@@ -8436,13 +9530,14 @@ async function handleFulltextGetAction(identifierArg, options, globalOpts) {
8436
9530
  process.stderr.write(
8437
9531
  "Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
8438
9532
  );
8439
- process.exit(1);
9533
+ setExitCode(ExitCode.ERROR);
9534
+ return;
8440
9535
  }
8441
9536
  identifier = stdinId;
8442
9537
  }
8443
9538
  const getOptions = {
8444
9539
  identifier,
8445
- fulltextDirectory: config2.fulltext.directory,
9540
+ fulltextDirectory: config2.attachments.directory,
8446
9541
  ...options.pdf && { type: "pdf" },
8447
9542
  ...options.markdown && { type: "markdown" },
8448
9543
  ...options.stdout && { stdout: options.stdout },
@@ -8450,11 +9545,11 @@ async function handleFulltextGetAction(identifierArg, options, globalOpts) {
8450
9545
  };
8451
9546
  const result = await executeFulltextGet(getOptions, context);
8452
9547
  outputFulltextGetResult(result, Boolean(options.stdout));
8453
- process.exit(getFulltextExitCode(result));
9548
+ setExitCode(getFulltextExitCode(result));
8454
9549
  } catch (error) {
8455
9550
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
8456
9551
  `);
8457
- process.exit(4);
9552
+ setExitCode(ExitCode.INTERNAL_ERROR);
8458
9553
  }
8459
9554
  }
8460
9555
  async function handleFulltextDetachAction(identifierArg, options, globalOpts) {
@@ -8472,16 +9567,17 @@ async function handleFulltextDetachAction(identifierArg, options, globalOpts) {
8472
9567
  process.stderr.write(
8473
9568
  "Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
8474
9569
  );
8475
- process.exit(1);
9570
+ setExitCode(ExitCode.ERROR);
9571
+ return;
8476
9572
  }
8477
9573
  identifier = stdinId;
8478
9574
  }
8479
9575
  const detachOptions = {
8480
9576
  identifier,
8481
- fulltextDirectory: config2.fulltext.directory,
9577
+ fulltextDirectory: config2.attachments.directory,
8482
9578
  ...options.pdf && { type: "pdf" },
8483
9579
  ...options.markdown && { type: "markdown" },
8484
- ...options.delete && { delete: options.delete },
9580
+ ...options.removeFiles && { removeFiles: options.removeFiles },
8485
9581
  ...options.force && { force: options.force },
8486
9582
  ...options.uuid && { idType: "uuid" }
8487
9583
  };
@@ -8489,11 +9585,11 @@ async function handleFulltextDetachAction(identifierArg, options, globalOpts) {
8489
9585
  const output = formatFulltextDetachOutput(result);
8490
9586
  process.stderr.write(`${output}
8491
9587
  `);
8492
- process.exit(getFulltextExitCode(result));
9588
+ setExitCode(getFulltextExitCode(result));
8493
9589
  } catch (error) {
8494
9590
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
8495
9591
  `);
8496
- process.exit(4);
9592
+ setExitCode(ExitCode.INTERNAL_ERROR);
8497
9593
  }
8498
9594
  }
8499
9595
  async function handleFulltextOpenAction(identifierArg, options, globalOpts) {
@@ -8511,13 +9607,14 @@ async function handleFulltextOpenAction(identifierArg, options, globalOpts) {
8511
9607
  process.stderr.write(
8512
9608
  "Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
8513
9609
  );
8514
- process.exit(1);
9610
+ setExitCode(ExitCode.ERROR);
9611
+ return;
8515
9612
  }
8516
9613
  identifier = stdinId;
8517
9614
  }
8518
9615
  const openOptions = {
8519
9616
  identifier,
8520
- fulltextDirectory: config2.fulltext.directory,
9617
+ fulltextDirectory: config2.attachments.directory,
8521
9618
  ...options.pdf && { type: "pdf" },
8522
9619
  ...options.markdown && { type: "markdown" },
8523
9620
  ...options.uuid && { idType: "uuid" }
@@ -8526,11 +9623,11 @@ async function handleFulltextOpenAction(identifierArg, options, globalOpts) {
8526
9623
  const output = formatFulltextOpenOutput(result);
8527
9624
  process.stderr.write(`${output}
8528
9625
  `);
8529
- process.exit(getFulltextExitCode(result));
9626
+ setExitCode(getFulltextExitCode(result));
8530
9627
  } catch (error) {
8531
9628
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
8532
9629
  `);
8533
- process.exit(4);
9630
+ setExitCode(ExitCode.INTERNAL_ERROR);
8534
9631
  }
8535
9632
  }
8536
9633
  function formatAuthor(author) {
@@ -8624,21 +9721,28 @@ const VALID_LIST_SORT_FIELDS = /* @__PURE__ */ new Set([
8624
9721
  "pub"
8625
9722
  ]);
8626
9723
  function getOutputFormat$1(options) {
9724
+ if (options.output) {
9725
+ if (options.output === "ids") return "ids-only";
9726
+ return options.output;
9727
+ }
8627
9728
  if (options.json) return "json";
8628
9729
  if (options.idsOnly) return "ids-only";
8629
- if (options.uuid) return "uuid";
9730
+ if (options.uuidOnly) return "uuid";
8630
9731
  if (options.bibtex) return "bibtex";
8631
9732
  return "pretty";
8632
9733
  }
8633
9734
  function validateOptions$1(options) {
8634
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
9735
+ const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
8635
9736
  Boolean
8636
9737
  );
8637
9738
  if (outputOptions.length > 1) {
8638
9739
  throw new Error(
8639
- "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
9740
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
8640
9741
  );
8641
9742
  }
9743
+ if (options.output && outputOptions.length > 0) {
9744
+ throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
9745
+ }
8642
9746
  if (options.sort !== void 0) {
8643
9747
  const sortStr = String(options.sort);
8644
9748
  if (!VALID_LIST_SORT_FIELDS.has(sortStr)) {
@@ -29050,7 +30154,7 @@ function registerFulltextAttachTool(server, getLibraryOperations, getConfig) {
29050
30154
  filePath: args.path,
29051
30155
  force: true,
29052
30156
  // MCP tools don't support interactive confirmation
29053
- fulltextDirectory: config2.fulltext.directory
30157
+ fulltextDirectory: config2.attachments.directory
29054
30158
  });
29055
30159
  if (!result.success) {
29056
30160
  return {
@@ -29083,7 +30187,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
29083
30187
  const config2 = getConfig();
29084
30188
  const pathResult = await fulltextGet(libraryOps, {
29085
30189
  identifier: args.id,
29086
- fulltextDirectory: config2.fulltext.directory
30190
+ fulltextDirectory: config2.attachments.directory
29087
30191
  });
29088
30192
  if (!pathResult.success) {
29089
30193
  return {
@@ -29097,7 +30201,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
29097
30201
  identifier: args.id,
29098
30202
  type: "markdown",
29099
30203
  stdout: true,
29100
- fulltextDirectory: config2.fulltext.directory
30204
+ fulltextDirectory: config2.attachments.directory
29101
30205
  });
29102
30206
  if (contentResult.success && contentResult.content) {
29103
30207
  responses.push({
@@ -29136,7 +30240,7 @@ function registerFulltextDetachTool(server, getLibraryOperations, getConfig) {
29136
30240
  const config2 = getConfig();
29137
30241
  const result = await fulltextDetach(libraryOps, {
29138
30242
  identifier: args.id,
29139
- fulltextDirectory: config2.fulltext.directory
30243
+ fulltextDirectory: config2.attachments.directory
29140
30244
  });
29141
30245
  if (!result.success) {
29142
30246
  return {
@@ -29342,7 +30446,7 @@ async function mcpStart(options) {
29342
30446
  async function executeRemove(options, context) {
29343
30447
  const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
29344
30448
  if (context.mode === "local" && deleteFulltext && fulltextDirectory) {
29345
- const { removeReference } = await import("./index-DapYyqAC.js").then((n) => n.r);
30449
+ const { removeReference } = await import("./index-DHgeuWGP.js").then((n) => n.r);
29346
30450
  return removeReference(context.library, {
29347
30451
  identifier,
29348
30452
  idType,
@@ -29396,12 +30500,12 @@ Continue?`;
29396
30500
  return readConfirmation(confirmMsg);
29397
30501
  }
29398
30502
  async function executeInteractiveRemove(context, config2) {
29399
- const { selectReferenceItemsOrExit } = await import("./reference-select-DSVwE9iu.js");
30503
+ const { selectReferenceItemsOrExit } = await import("./reference-select-B9w9CLa1.js");
29400
30504
  const allReferences = await context.library.getAll();
29401
30505
  const selectedItems = await selectReferenceItemsOrExit(
29402
30506
  allReferences,
29403
30507
  { multiSelect: false },
29404
- config2.cli.interactive
30508
+ config2.cli.tui
29405
30509
  );
29406
30510
  const selectedItem = selectedItems[0];
29407
30511
  return { identifier: selectedItem.id, item: selectedItem };
@@ -29452,7 +30556,7 @@ function handleRemoveError(error, identifierArg, outputFormat) {
29452
30556
  `);
29453
30557
  }
29454
30558
  const isUserError = message.includes("not found") || message.includes("No identifier");
29455
- process.exit(isUserError ? 1 : 4);
30559
+ setExitCode(isUserError ? ExitCode.ERROR : ExitCode.INTERNAL_ERROR);
29456
30560
  }
29457
30561
  async function handleRemoveAction(identifierArg, options, globalOpts) {
29458
30562
  const { formatRemoveJsonOutput: formatRemoveJsonOutput2 } = await Promise.resolve().then(() => jsonOutput);
@@ -29473,22 +30577,24 @@ async function handleRemoveAction(identifierArg, options, globalOpts) {
29473
30577
  if (hasFulltext && !isTTY() && !force) {
29474
30578
  process.stderr.write(`Error: ${formatFulltextWarning(fulltextTypes)}
29475
30579
  `);
29476
- process.exit(1);
30580
+ setExitCode(ExitCode.ERROR);
30581
+ return;
29477
30582
  }
29478
30583
  const confirmed = await confirmRemoveIfNeeded(refToRemove, hasFulltext, force);
29479
30584
  if (!confirmed) {
29480
30585
  process.stderr.write("Cancelled.\n");
29481
- process.exit(2);
30586
+ setExitCode(2);
30587
+ return;
29482
30588
  }
29483
30589
  const removeOptions = {
29484
30590
  identifier,
29485
30591
  idType: useUuid ? "uuid" : "id",
29486
- fulltextDirectory: config2.fulltext.directory,
30592
+ fulltextDirectory: config2.attachments.directory,
29487
30593
  deleteFulltext: force && hasFulltext
29488
30594
  };
29489
30595
  const result = await executeRemove(removeOptions, context);
29490
30596
  outputResult(result, identifier, outputFormat, options.full, formatRemoveJsonOutput2);
29491
- process.exit(result.removed ? 0 : 1);
30597
+ setExitCode(result.removed ? ExitCode.SUCCESS : ExitCode.ERROR);
29492
30598
  } catch (error) {
29493
30599
  handleRemoveError(error, identifierArg, outputFormat);
29494
30600
  }
@@ -29506,21 +30612,28 @@ const VALID_SEARCH_SORT_FIELDS = /* @__PURE__ */ new Set([
29506
30612
  "rel"
29507
30613
  ]);
29508
30614
  function getOutputFormat(options) {
30615
+ if (options.output) {
30616
+ if (options.output === "ids") return "ids-only";
30617
+ return options.output;
30618
+ }
29509
30619
  if (options.json) return "json";
29510
30620
  if (options.idsOnly) return "ids-only";
29511
- if (options.uuid) return "uuid";
30621
+ if (options.uuidOnly) return "uuid";
29512
30622
  if (options.bibtex) return "bibtex";
29513
30623
  return "pretty";
29514
30624
  }
29515
30625
  function validateOptions(options) {
29516
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
30626
+ const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
29517
30627
  Boolean
29518
30628
  );
29519
30629
  if (outputOptions.length > 1) {
29520
30630
  throw new Error(
29521
- "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
30631
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
29522
30632
  );
29523
30633
  }
30634
+ if (options.output && outputOptions.length > 0) {
30635
+ throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
30636
+ }
29524
30637
  if (options.sort !== void 0) {
29525
30638
  const sortStr = String(options.sort);
29526
30639
  if (!VALID_SEARCH_SORT_FIELDS.has(sortStr)) {
@@ -29576,35 +30689,39 @@ function formatSearchOutput(result, options) {
29576
30689
  return lines.join("\n");
29577
30690
  }
29578
30691
  function validateInteractiveOptions(options) {
29579
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
29580
- Boolean
29581
- );
30692
+ const outputOptions = [
30693
+ options.output,
30694
+ options.json,
30695
+ options.idsOnly,
30696
+ options.uuidOnly,
30697
+ options.bibtex
30698
+ ].filter(Boolean);
29582
30699
  if (outputOptions.length > 0) {
29583
30700
  throw new Error(
29584
- "Interactive mode cannot be combined with output format options (--json, --ids-only, --uuid, --bibtex)"
30701
+ "TUI mode cannot be combined with output format options (--output, --json, --ids-only, --uuid-only, --bibtex)"
29585
30702
  );
29586
30703
  }
29587
30704
  }
29588
30705
  async function executeInteractiveSearch(options, context, config2) {
29589
30706
  validateInteractiveOptions(options);
29590
- const { checkTTY } = await import("./tty-CDBIQraQ.js");
30707
+ const { checkTTY } = await import("./tty-BMyaEOhX.js");
29591
30708
  const { runSearchPrompt } = await import("./search-prompt-BrWpOcij.js");
29592
- const { runActionMenu } = await import("./action-menu-CVSizwXm.js");
29593
- const { search } = await import("./file-watcher-D2Y-SlcE.js").then((n) => n.y);
29594
- const { tokenize } = await import("./file-watcher-D2Y-SlcE.js").then((n) => n.x);
30709
+ const { runActionMenu } = await import("./action-menu-DwCcc6Gt.js");
30710
+ const { search } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.y);
30711
+ const { tokenize } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.x);
29595
30712
  checkTTY();
29596
30713
  const allReferences = await context.library.getAll();
29597
30714
  const searchFn = (query) => {
29598
30715
  const { tokens } = tokenize(query);
29599
30716
  return search(allReferences, tokens);
29600
30717
  };
29601
- const interactiveConfig = config2.cli.interactive;
30718
+ const tuiConfig = config2.cli.tui;
29602
30719
  const searchResult = await runSearchPrompt(
29603
30720
  allReferences,
29604
30721
  searchFn,
29605
30722
  {
29606
- limit: interactiveConfig.limit,
29607
- debounceMs: interactiveConfig.debounceMs
30723
+ limit: tuiConfig.limit,
30724
+ debounceMs: tuiConfig.debounceMs
29608
30725
  },
29609
30726
  options.query || ""
29610
30727
  );
@@ -29667,7 +30784,7 @@ async function startServerForeground(options) {
29667
30784
  server.close();
29668
30785
  await dispose();
29669
30786
  await removePortfile(options.portfilePath);
29670
- process.exit(0);
30787
+ setExitCode(ExitCode.SUCCESS);
29671
30788
  };
29672
30789
  process.on("SIGINT", cleanup);
29673
30790
  process.on("SIGTERM", cleanup);
@@ -29897,12 +31014,12 @@ function formatUpdateOutput(result, identifier) {
29897
31014
  return parts.join("\n");
29898
31015
  }
29899
31016
  async function executeInteractiveUpdate(context, config2) {
29900
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
31017
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
29901
31018
  const allReferences = await context.library.getAll();
29902
31019
  const identifiers = await selectReferencesOrExit(
29903
31020
  allReferences,
29904
31021
  { multiSelect: false },
29905
- config2.cli.interactive
31022
+ config2.cli.tui
29906
31023
  );
29907
31024
  return identifiers[0];
29908
31025
  }
@@ -29919,14 +31036,16 @@ async function resolveUpdateIdentifier(identifierArg, hasSetOptions, context, co
29919
31036
  process.stderr.write(
29920
31037
  "Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
29921
31038
  );
29922
- process.exit(1);
31039
+ setExitCode(ExitCode.ERROR);
31040
+ return "";
29923
31041
  }
29924
31042
  return stdinId;
29925
31043
  }
29926
31044
  process.stderr.write(
29927
31045
  "Error: No identifier provided. When using stdin for JSON input, identifier must be provided as argument.\n"
29928
31046
  );
29929
- process.exit(1);
31047
+ setExitCode(ExitCode.ERROR);
31048
+ return "";
29930
31049
  }
29931
31050
  function parseUpdateInput(setOptions, file) {
29932
31051
  if (setOptions && setOptions.length > 0 && file) {
@@ -29947,23 +31066,25 @@ function handleUpdateError(error) {
29947
31066
  if (message.includes("Parse error")) {
29948
31067
  process.stderr.write(`Error: ${message}
29949
31068
  `);
29950
- process.exit(3);
31069
+ setExitCode(3);
29951
31070
  }
29952
31071
  if (message.includes("not found") || message.includes("validation")) {
29953
31072
  process.stderr.write(`Error: ${message}
29954
31073
  `);
29955
- process.exit(1);
31074
+ setExitCode(ExitCode.ERROR);
29956
31075
  }
29957
31076
  process.stderr.write(`Error: ${message}
29958
31077
  `);
29959
- process.exit(4);
31078
+ setExitCode(ExitCode.INTERNAL_ERROR);
29960
31079
  }
29961
31080
  function handleUpdateErrorWithFormat(error, identifier, outputFormat) {
29962
31081
  const message = error instanceof Error ? error.message : String(error);
29963
31082
  if (outputFormat === "json") {
29964
31083
  process.stdout.write(`${JSON.stringify({ success: false, id: identifier, error: message })}
29965
31084
  `);
29966
- process.exit(message.includes("not found") || message.includes("validation") ? 1 : 4);
31085
+ setExitCode(
31086
+ message.includes("not found") || message.includes("validation") ? ExitCode.ERROR : ExitCode.INTERNAL_ERROR
31087
+ );
29967
31088
  }
29968
31089
  handleUpdateError(error);
29969
31090
  }
@@ -29997,7 +31118,7 @@ async function handleUpdateAction(identifierArg, file, options, globalOpts) {
29997
31118
  process.stderr.write(`${output}
29998
31119
  `);
29999
31120
  }
30000
- process.exit(result.updated ? 0 : 1);
31121
+ setExitCode(result.updated ? ExitCode.SUCCESS : ExitCode.ERROR);
30001
31122
  } catch (error) {
30002
31123
  handleUpdateErrorWithFormat(error, identifierArg ?? "", outputFormat);
30003
31124
  }
@@ -30007,8 +31128,13 @@ function collectSetOption(value, previous) {
30007
31128
  }
30008
31129
  const SEARCH_SORT_FIELDS = searchSortFieldSchema.options;
30009
31130
  const SORT_ORDERS = sortOrderSchema.options;
30010
- const CITATION_FORMATS = ["text", "html", "rtf"];
31131
+ const CITATION_OUTPUT_FORMATS = ["text", "html", "rtf"];
31132
+ const EXPORT_OUTPUT_FORMATS = ["json", "yaml", "bibtex"];
31133
+ const LIST_OUTPUT_FORMATS = ["pretty", "json", "bibtex", "ids", "uuid"];
31134
+ const MUTATION_OUTPUT_FORMATS = ["json", "text"];
31135
+ const CONFIG_OUTPUT_FORMATS = ["text", "json"];
30011
31136
  const LOG_LEVELS = ["silent", "info", "debug"];
31137
+ const ADD_INPUT_FORMATS = ["json", "bibtex", "ris", "pmid", "doi", "isbn", "auto"];
30012
31138
  const CONFIG_SECTIONS = [
30013
31139
  "backup",
30014
31140
  "citation",
@@ -30019,17 +31145,44 @@ const CONFIG_SECTIONS = [
30019
31145
  "server",
30020
31146
  "watch"
30021
31147
  ];
31148
+ const ATTACHMENT_ROLES = ["fulltext", "supplement", "notes", "draft"];
30022
31149
  const OPTION_VALUES = {
30023
31150
  "--sort": SEARCH_SORT_FIELDS,
30024
31151
  // search includes 'relevance'
30025
31152
  "--order": SORT_ORDERS,
30026
- "--format": CITATION_FORMATS,
30027
31153
  "--style": BUILTIN_STYLES,
30028
31154
  "--log-level": LOG_LEVELS,
30029
- "--section": CONFIG_SECTIONS
31155
+ "--section": CONFIG_SECTIONS,
31156
+ "--input": ADD_INPUT_FORMATS,
31157
+ "-i": ADD_INPUT_FORMATS,
31158
+ "--role": ATTACHMENT_ROLES,
31159
+ "-r": ATTACHMENT_ROLES
30030
31160
  };
31161
+ const OUTPUT_VALUES_BY_COMMAND = {
31162
+ cite: CITATION_OUTPUT_FORMATS,
31163
+ export: EXPORT_OUTPUT_FORMATS,
31164
+ list: LIST_OUTPUT_FORMATS,
31165
+ search: LIST_OUTPUT_FORMATS,
31166
+ add: MUTATION_OUTPUT_FORMATS,
31167
+ remove: MUTATION_OUTPUT_FORMATS,
31168
+ update: MUTATION_OUTPUT_FORMATS
31169
+ // config show uses CONFIG_OUTPUT_FORMATS, handled specially
31170
+ };
31171
+ function getOptionValuesForCommand(option, command, subcommand) {
31172
+ if (option === "--output" || option === "-o") {
31173
+ if (command === "config" && subcommand === "show") {
31174
+ return CONFIG_OUTPUT_FORMATS;
31175
+ }
31176
+ if (command && OUTPUT_VALUES_BY_COMMAND[command]) {
31177
+ return OUTPUT_VALUES_BY_COMMAND[command];
31178
+ }
31179
+ return CITATION_OUTPUT_FORMATS;
31180
+ }
31181
+ return OPTION_VALUES[option];
31182
+ }
30031
31183
  const ID_COMPLETION_COMMANDS = /* @__PURE__ */ new Set(["cite", "remove", "update"]);
30032
31184
  const ID_COMPLETION_FULLTEXT_SUBCOMMANDS = /* @__PURE__ */ new Set(["attach", "get", "detach", "open"]);
31185
+ const ID_COMPLETION_ATTACH_SUBCOMMANDS = /* @__PURE__ */ new Set(["open", "add", "list", "get", "detach", "sync"]);
30033
31186
  function toCompletionItems(values) {
30034
31187
  return values.map((name2) => ({ name: name2 }));
30035
31188
  }
@@ -30063,6 +31216,13 @@ function extractGlobalOptions(program) {
30063
31216
  function findSubcommand(program, name2) {
30064
31217
  return program.commands.find((cmd) => cmd.name() === name2);
30065
31218
  }
31219
+ function parseCommandContext(env) {
31220
+ const words = env.line.trim().split(/\s+/);
31221
+ const args = words.slice(1);
31222
+ const command = args[0];
31223
+ const subcommand = args.length >= 2 ? args[1] : void 0;
31224
+ return { command, subcommand };
31225
+ }
30066
31226
  function getCompletions(env, program) {
30067
31227
  const { line, prev, last } = env;
30068
31228
  const words = line.trim().split(/\s+/);
@@ -30074,7 +31234,8 @@ function getCompletions(env, program) {
30074
31234
  }
30075
31235
  const firstArg = args[0] ?? "";
30076
31236
  if (prev?.startsWith("-")) {
30077
- const optionValues = OPTION_VALUES[prev];
31237
+ const { command, subcommand } = parseCommandContext(env);
31238
+ const optionValues = getOptionValuesForCommand(prev, command, subcommand);
30078
31239
  if (optionValues) {
30079
31240
  return toCompletionItems(optionValues);
30080
31241
  }
@@ -30114,6 +31275,13 @@ function needsIdCompletion(env) {
30114
31275
  }
30115
31276
  return { needs: false };
30116
31277
  }
31278
+ if (command === "attach" && args.length >= 2) {
31279
+ const subcommand = args[1] ?? "";
31280
+ if (ID_COMPLETION_ATTACH_SUBCOMMANDS.has(subcommand)) {
31281
+ return { needs: true, command, subcommand };
31282
+ }
31283
+ return { needs: false };
31284
+ }
30117
31285
  if (ID_COMPLETION_COMMANDS.has(command)) {
30118
31286
  return { needs: true, command };
30119
31287
  }
@@ -30235,7 +31403,7 @@ function registerCompletionCommand(program) {
30235
31403
  } else {
30236
31404
  console.error(`Unknown action: ${action}`);
30237
31405
  console.error("Usage: ref completion [install|uninstall]");
30238
- process.exit(1);
31406
+ setExitCode(ExitCode.ERROR);
30239
31407
  }
30240
31408
  });
30241
31409
  }
@@ -30253,6 +31421,7 @@ function createProgram() {
30253
31421
  registerCiteCommand(program);
30254
31422
  registerServerCommand(program);
30255
31423
  registerFulltextCommand(program);
31424
+ registerAttachCommand(program);
30256
31425
  registerMcpCommand(program);
30257
31426
  registerConfigCommand(program);
30258
31427
  registerCompletionCommand(program);
@@ -30269,15 +31438,13 @@ async function handleListAction(options, program) {
30269
31438
  process.stdout.write(`${output}
30270
31439
  `);
30271
31440
  }
30272
- process.exit(0);
31441
+ setExitCode(ExitCode.SUCCESS);
30273
31442
  } catch (error) {
30274
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
30275
- `);
30276
- process.exit(4);
31443
+ exitWithError(error instanceof Error ? error.message : String(error), ExitCode.INTERNAL_ERROR);
30277
31444
  }
30278
31445
  }
30279
31446
  function registerListCommand(program) {
30280
- 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) => {
31447
+ 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) => {
30281
31448
  await handleListAction(options, program);
30282
31449
  });
30283
31450
  }
@@ -30298,15 +31465,15 @@ async function handleExportAction(ids, options, program) {
30298
31465
  `);
30299
31466
  }
30300
31467
  }
30301
- process.exit(getExportExitCode(result));
31468
+ setExitCode(getExportExitCode(result));
30302
31469
  } catch (error) {
30303
31470
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
30304
31471
  `);
30305
- process.exit(4);
31472
+ setExitCode(ExitCode.INTERNAL_ERROR);
30306
31473
  }
30307
31474
  }
30308
31475
  function registerExportCommand(program) {
30309
- 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) => {
31476
+ 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) => {
30310
31477
  await handleExportAction(ids, options, program);
30311
31478
  });
30312
31479
  }
@@ -30315,13 +31482,13 @@ async function handleSearchAction(query, options, program) {
30315
31482
  const globalOpts = program.opts();
30316
31483
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
30317
31484
  const context = await createExecutionContext(config2, Library.load);
30318
- if (options.interactive) {
31485
+ if (options.tui) {
30319
31486
  const result2 = await executeInteractiveSearch({ ...options, query }, context, config2);
30320
31487
  if (result2.output) {
30321
31488
  process.stdout.write(`${result2.output}
30322
31489
  `);
30323
31490
  }
30324
- process.exit(result2.cancelled ? 0 : 0);
31491
+ setExitCode(ExitCode.SUCCESS);
30325
31492
  }
30326
31493
  const result = await executeSearch({ ...options, query }, context);
30327
31494
  const output = formatSearchOutput(result, { ...options, query });
@@ -30329,18 +31496,19 @@ async function handleSearchAction(query, options, program) {
30329
31496
  process.stdout.write(`${output}
30330
31497
  `);
30331
31498
  }
30332
- process.exit(0);
31499
+ setExitCode(ExitCode.SUCCESS);
30333
31500
  } catch (error) {
30334
31501
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
30335
31502
  `);
30336
- process.exit(4);
31503
+ setExitCode(ExitCode.INTERNAL_ERROR);
30337
31504
  }
30338
31505
  }
30339
31506
  function registerSearchCommand(program) {
30340
- 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) => {
30341
- if (!options.interactive && !query) {
30342
- process.stderr.write("Error: Search query is required unless using --interactive\n");
30343
- process.exit(1);
31507
+ 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) => {
31508
+ if (!options.tui && !query) {
31509
+ process.stderr.write("Error: Search query is required unless using --tui\n");
31510
+ setExitCode(ExitCode.ERROR);
31511
+ return;
30344
31512
  }
30345
31513
  await handleSearchAction(query ?? "", options, program);
30346
31514
  });
@@ -30350,8 +31518,8 @@ function buildAddOptions(inputs, options, config2, stdinContent) {
30350
31518
  inputs,
30351
31519
  force: options.force ?? false
30352
31520
  };
30353
- if (options.format !== void 0) {
30354
- addOptions.format = options.format;
31521
+ if (options.input !== void 0) {
31522
+ addOptions.format = options.input;
30355
31523
  }
30356
31524
  if (options.verbose !== void 0) {
30357
31525
  addOptions.verbose = options.verbose;
@@ -30405,7 +31573,7 @@ async function handleAddAction(inputs, options, program) {
30405
31573
  process.stderr.write(`${output}
30406
31574
  `);
30407
31575
  }
30408
- process.exit(getExitCode(result));
31576
+ setExitCode(getExitCode(result));
30409
31577
  } catch (error) {
30410
31578
  const message = error instanceof Error ? error.message : String(error);
30411
31579
  if (outputFormat === "json") {
@@ -30415,15 +31583,11 @@ async function handleAddAction(inputs, options, program) {
30415
31583
  process.stderr.write(`Error: ${message}
30416
31584
  `);
30417
31585
  }
30418
- process.exit(1);
31586
+ setExitCode(ExitCode.ERROR);
30419
31587
  }
30420
31588
  }
30421
31589
  function registerAddCommand(program) {
30422
- 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(
30423
- "--format <format>",
30424
- "Explicit input format: json|bibtex|ris|pmid|doi|isbn|auto",
30425
- "auto"
30426
- ).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) => {
31590
+ 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) => {
30427
31591
  await handleAddAction(inputs, options, program);
30428
31592
  });
30429
31593
  }
@@ -30441,7 +31605,7 @@ function registerEditCommand(program) {
30441
31605
  program.command("edit").description("Edit references interactively using an external editor").argument(
30442
31606
  "[identifier...]",
30443
31607
  "Citation keys or UUIDs to edit (interactive selection if omitted)"
30444
- ).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) => {
31608
+ ).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) => {
30445
31609
  await handleEditAction(identifiers, options, program.opts());
30446
31610
  });
30447
31611
  }
@@ -30449,7 +31613,7 @@ function registerCiteCommand(program) {
30449
31613
  program.command("cite").description("Generate formatted citations for references").argument(
30450
31614
  "[id-or-uuid...]",
30451
31615
  "Citation keys or UUIDs to cite (interactive selection if omitted)"
30452
- ).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) => {
31616
+ ).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) => {
30453
31617
  await handleCiteAction(identifiers, options, program.opts());
30454
31618
  });
30455
31619
  }
@@ -30469,18 +31633,18 @@ function registerServerCommand(program) {
30469
31633
  };
30470
31634
  await serverStart(startOptions);
30471
31635
  if (options.daemon) {
30472
- process.exit(0);
31636
+ setExitCode(ExitCode.SUCCESS);
30473
31637
  }
30474
31638
  } catch (error) {
30475
31639
  const message = error instanceof Error ? error.message : String(error);
30476
31640
  if (message.includes("already running") || message.includes("conflict")) {
30477
31641
  process.stderr.write(`Error: ${message}
30478
31642
  `);
30479
- process.exit(1);
31643
+ setExitCode(ExitCode.ERROR);
30480
31644
  }
30481
31645
  process.stderr.write(`Error: ${message}
30482
31646
  `);
30483
- process.exit(4);
31647
+ setExitCode(ExitCode.INTERNAL_ERROR);
30484
31648
  }
30485
31649
  });
30486
31650
  serverCmd.command("stop").description("Stop running server").action(async () => {
@@ -30488,17 +31652,17 @@ function registerServerCommand(program) {
30488
31652
  const portfilePath = getPortfilePath();
30489
31653
  await serverStop(portfilePath);
30490
31654
  process.stderr.write("Server stopped.\n");
30491
- process.exit(0);
31655
+ setExitCode(ExitCode.SUCCESS);
30492
31656
  } catch (error) {
30493
31657
  const message = error instanceof Error ? error.message : String(error);
30494
31658
  if (message.includes("not running")) {
30495
31659
  process.stderr.write(`Error: ${message}
30496
31660
  `);
30497
- process.exit(1);
31661
+ setExitCode(ExitCode.ERROR);
30498
31662
  }
30499
31663
  process.stderr.write(`Error: ${message}
30500
31664
  `);
30501
- process.exit(4);
31665
+ setExitCode(ExitCode.INTERNAL_ERROR);
30502
31666
  }
30503
31667
  });
30504
31668
  serverCmd.command("status").description("Check server status").action(async () => {
@@ -30513,15 +31677,15 @@ PID: ${status.pid}
30513
31677
  Library: ${status.library}
30514
31678
  `
30515
31679
  );
30516
- process.exit(0);
31680
+ setExitCode(ExitCode.SUCCESS);
30517
31681
  } else {
30518
31682
  process.stdout.write("Server not running\n");
30519
- process.exit(1);
31683
+ setExitCode(ExitCode.ERROR);
30520
31684
  }
30521
31685
  } catch (error) {
30522
31686
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
30523
31687
  `);
30524
- process.exit(4);
31688
+ setExitCode(ExitCode.INTERNAL_ERROR);
30525
31689
  }
30526
31690
  });
30527
31691
  }
@@ -30550,10 +31714,34 @@ function registerMcpCommand(program) {
30550
31714
  } catch (error) {
30551
31715
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
30552
31716
  `);
30553
- process.exit(1);
31717
+ setExitCode(ExitCode.ERROR);
30554
31718
  }
30555
31719
  });
30556
31720
  }
31721
+ function registerAttachCommand(program) {
31722
+ const attachCmd = program.command("attach").description("Manage file attachments for references");
31723
+ 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) => {
31724
+ await handleAttachOpenAction(identifier, filename, options, program.opts());
31725
+ });
31726
+ 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(
31727
+ "--role <role>",
31728
+ "Role for the file (fulltext, supplement, notes, draft, or custom)"
31729
+ ).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) => {
31730
+ await handleAttachAddAction(identifier, filePath, options, program.opts());
31731
+ });
31732
+ 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) => {
31733
+ await handleAttachListAction(identifier, options, program.opts());
31734
+ });
31735
+ 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) => {
31736
+ await handleAttachGetAction(identifier, filename, options, program.opts());
31737
+ });
31738
+ 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) => {
31739
+ await handleAttachDetachAction(identifier, filename, options, program.opts());
31740
+ });
31741
+ 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) => {
31742
+ await handleAttachSyncAction(identifier, options, program.opts());
31743
+ });
31744
+ }
30557
31745
  function registerFulltextCommand(program) {
30558
31746
  const fulltextCmd = program.command("fulltext").description("Manage full-text files attached to references");
30559
31747
  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) => {
@@ -30562,7 +31750,7 @@ function registerFulltextCommand(program) {
30562
31750
  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) => {
30563
31751
  await handleFulltextGetAction(identifier, options, program.opts());
30564
31752
  });
30565
- 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) => {
31753
+ 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) => {
30566
31754
  await handleFulltextDetachAction(identifier, options, program.opts());
30567
31755
  });
30568
31756
  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) => {
@@ -30576,16 +31764,22 @@ async function main(argv) {
30576
31764
  return;
30577
31765
  }
30578
31766
  process.on("SIGINT", () => {
30579
- process.exit(130);
31767
+ setExitCode(ExitCode.SIGINT);
30580
31768
  });
30581
31769
  process.on("SIGTERM", () => {
30582
- process.exit(0);
31770
+ setExitCode(ExitCode.SUCCESS);
30583
31771
  });
30584
31772
  await program.parseAsync(argv);
30585
31773
  }
30586
31774
  export {
31775
+ addAttachment as a,
30587
31776
  createProgram as c,
31777
+ detachAttachment as d,
30588
31778
  formatBibtex as f,
30589
- main as m
31779
+ getAttachment as g,
31780
+ listAttachments as l,
31781
+ main as m,
31782
+ openAttachment as o,
31783
+ syncAttachments as s
30590
31784
  };
30591
- //# sourceMappingURL=index-CXoDLO8W.js.map
31785
+ //# sourceMappingURL=index-B4RmLBI1.js.map