@ncukondo/reference-manager 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) 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-DvwR6nMj.js} +3 -3
  4. package/dist/chunks/{action-menu-CVSizwXm.js.map → action-menu-DvwR6nMj.js.map} +1 -1
  5. package/dist/chunks/{file-watcher-D2Y-SlcE.js → file-watcher-B_WpVHSV.js} +18 -18
  6. package/dist/chunks/{file-watcher-D2Y-SlcE.js.map → file-watcher-B_WpVHSV.js.map} +1 -1
  7. package/dist/chunks/index-B_WCu-ZQ.js +10 -0
  8. package/dist/chunks/index-B_WCu-ZQ.js.map +1 -0
  9. package/dist/chunks/{index-CXoDLO8W.js → index-Bv5IgsL-.js} +1522 -409
  10. package/dist/chunks/index-Bv5IgsL-.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-4FFB4igw.js} +66 -27
  14. package/dist/chunks/loader-4FFB4igw.js.map +1 -0
  15. package/dist/chunks/{reference-select-DSVwE9iu.js → reference-select-B9w9CLa1.js} +3 -3
  16. package/dist/chunks/{reference-select-DSVwE9iu.js.map → reference-select-B9w9CLa1.js.map} +1 -1
  17. package/dist/chunks/{style-select-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/export.d.ts +1 -1
  25. package/dist/cli/commands/fulltext.d.ts +2 -2
  26. package/dist/cli/commands/fulltext.d.ts.map +1 -1
  27. package/dist/cli/commands/list.d.ts +2 -1
  28. package/dist/cli/commands/list.d.ts.map +1 -1
  29. package/dist/cli/commands/search.d.ts +3 -2
  30. package/dist/cli/commands/search.d.ts.map +1 -1
  31. package/dist/cli/completion.d.ts.map +1 -1
  32. package/dist/cli/index.d.ts.map +1 -1
  33. package/dist/cli/server-client.d.ts +37 -1
  34. package/dist/cli/server-client.d.ts.map +1 -1
  35. package/dist/cli.js +2 -2
  36. package/dist/config/defaults.d.ts +7 -1
  37. package/dist/config/defaults.d.ts.map +1 -1
  38. package/dist/config/key-parser.d.ts +1 -1
  39. package/dist/config/loader.d.ts.map +1 -1
  40. package/dist/config/schema.d.ts +22 -8
  41. package/dist/config/schema.d.ts.map +1 -1
  42. package/dist/features/attachments/directory-manager.d.ts +40 -0
  43. package/dist/features/attachments/directory-manager.d.ts.map +1 -0
  44. package/dist/features/attachments/directory.d.ts +36 -0
  45. package/dist/features/attachments/directory.d.ts.map +1 -0
  46. package/dist/features/attachments/filename.d.ts +30 -0
  47. package/dist/features/attachments/filename.d.ts.map +1 -0
  48. package/dist/features/attachments/types.d.ts +38 -0
  49. package/dist/features/attachments/types.d.ts.map +1 -0
  50. package/dist/features/fulltext/manager.d.ts +1 -1
  51. package/dist/features/fulltext/manager.d.ts.map +1 -1
  52. package/dist/features/interactive/tty.d.ts +2 -2
  53. package/dist/features/operations/attachments/add.d.ts +42 -0
  54. package/dist/features/operations/attachments/add.d.ts.map +1 -0
  55. package/dist/features/operations/attachments/detach.d.ts +38 -0
  56. package/dist/features/operations/attachments/detach.d.ts.map +1 -0
  57. package/dist/features/operations/attachments/get.d.ts +35 -0
  58. package/dist/features/operations/attachments/get.d.ts.map +1 -0
  59. package/dist/features/operations/attachments/index.d.ts +16 -0
  60. package/dist/features/operations/attachments/index.d.ts.map +1 -0
  61. package/dist/features/operations/attachments/list.d.ts +32 -0
  62. package/dist/features/operations/attachments/list.d.ts.map +1 -0
  63. package/dist/features/operations/attachments/open.d.ts +39 -0
  64. package/dist/features/operations/attachments/open.d.ts.map +1 -0
  65. package/dist/features/operations/attachments/sync.d.ts +50 -0
  66. package/dist/features/operations/attachments/sync.d.ts.map +1 -0
  67. package/dist/features/operations/fulltext/attach.d.ts +8 -2
  68. package/dist/features/operations/fulltext/attach.d.ts.map +1 -1
  69. package/dist/features/operations/fulltext/detach.d.ts +9 -3
  70. package/dist/features/operations/fulltext/detach.d.ts.map +1 -1
  71. package/dist/features/operations/fulltext/get.d.ts +8 -2
  72. package/dist/features/operations/fulltext/get.d.ts.map +1 -1
  73. package/dist/features/operations/fulltext/open.d.ts +8 -2
  74. package/dist/features/operations/fulltext/open.d.ts.map +1 -1
  75. package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts +39 -0
  76. package/dist/features/operations/fulltext-adapter/fulltext-adapter.d.ts.map +1 -0
  77. package/dist/features/operations/fulltext-adapter/index.d.ts +7 -0
  78. package/dist/features/operations/fulltext-adapter/index.d.ts.map +1 -0
  79. package/dist/features/operations/index.d.ts +1 -0
  80. package/dist/features/operations/index.d.ts.map +1 -1
  81. package/dist/features/operations/library-operations.d.ts +43 -0
  82. package/dist/features/operations/library-operations.d.ts.map +1 -1
  83. package/dist/features/operations/operations-library.d.ts +7 -0
  84. package/dist/features/operations/operations-library.d.ts.map +1 -1
  85. package/dist/features/operations/remove.d.ts +1 -0
  86. package/dist/features/operations/remove.d.ts.map +1 -1
  87. package/dist/index.js +15 -15
  88. package/dist/index.js.map +1 -1
  89. package/dist/server.js +3 -3
  90. package/dist/utils/opener.d.ts +6 -1
  91. package/dist/utils/opener.d.ts.map +1 -1
  92. package/dist/utils/path.d.ts +28 -0
  93. package/dist/utils/path.d.ts.map +1 -0
  94. package/package.json +2 -1
  95. package/dist/chunks/index-CXoDLO8W.js.map +0 -1
  96. package/dist/chunks/index-DapYyqAC.js.map +0 -1
  97. package/dist/chunks/loader-C1EpnyPm.js.map +0 -1
  98. 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-4FFB4igw.js";
9
12
  import { spawn, spawnSync } from "node:child_process";
10
13
  import process$1, { stdin, stdout } from "node:process";
11
- import { l as loadConfig, 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.0";
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-B_WCu-ZQ.js");
907
+ return addAttachment2(this.library, options);
908
+ }
909
+ async attachList(options) {
910
+ const { listAttachments: listAttachments2 } = await import("./index-B_WCu-ZQ.js");
911
+ return listAttachments2(this.library, options);
912
+ }
913
+ async attachGet(options) {
914
+ const { getAttachment: getAttachment2 } = await import("./index-B_WCu-ZQ.js");
915
+ return getAttachment2(this.library, options);
916
+ }
917
+ async attachDetach(options) {
918
+ const { detachAttachment: detachAttachment2 } = await import("./index-B_WCu-ZQ.js");
919
+ return detachAttachment2(this.library, options);
920
+ }
921
+ async attachSync(options) {
922
+ const { syncAttachments: syncAttachments2 } = await import("./index-B_WCu-ZQ.js");
923
+ return syncAttachments2(this.library, options);
924
+ }
925
+ async attachOpen(options) {
926
+ const { openAttachment: openAttachment2 } = await import("./index-B_WCu-ZQ.js");
927
+ return openAttachment2(this.library, options);
928
+ }
312
929
  }
313
930
  class ServerClient {
314
931
  constructor(baseUrl) {
@@ -495,6 +1112,111 @@ class ServerClient {
495
1112
  }
496
1113
  return await response.json();
497
1114
  }
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());
1132
+ }
1133
+ return await response.json();
1134
+ }
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
+ }
498
1220
  }
499
1221
  async function getServerConnection(libraryPath, config2) {
500
1222
  const portfilePath = getPortfilePath();
@@ -622,40 +1344,494 @@ async function readIdentifierFromStdin() {
622
1344
  const firstLine = content.split("\n")[0]?.trim();
623
1345
  return firstLine || void 0;
624
1346
  }
625
- async function readConfirmation(prompt) {
626
- if (!isTTY()) {
627
- return true;
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
+ async function executeAttachOpen(options, context) {
1379
+ const operationOptions = {
1380
+ identifier: options.identifier,
1381
+ attachmentsDirectory: options.attachmentsDirectory,
1382
+ ...options.filename !== void 0 && { filename: options.filename },
1383
+ ...options.role !== void 0 && { role: options.role },
1384
+ ...options.print !== void 0 && { print: options.print },
1385
+ ...options.idType !== void 0 && { idType: options.idType }
1386
+ };
1387
+ return openAttachment(context.library, operationOptions);
1388
+ }
1389
+ async function executeAttachAdd(options, context) {
1390
+ const operationOptions = {
1391
+ identifier: options.identifier,
1392
+ filePath: options.filePath,
1393
+ role: options.role,
1394
+ attachmentsDirectory: options.attachmentsDirectory,
1395
+ ...options.label !== void 0 && { label: options.label },
1396
+ ...options.move !== void 0 && { move: options.move },
1397
+ ...options.force !== void 0 && { force: options.force },
1398
+ ...options.idType !== void 0 && { idType: options.idType }
1399
+ };
1400
+ return addAttachment(context.library, operationOptions);
1401
+ }
1402
+ async function executeAttachList(options, context) {
1403
+ const operationOptions = {
1404
+ identifier: options.identifier,
1405
+ ...options.role !== void 0 && { role: options.role },
1406
+ ...options.idType !== void 0 && { idType: options.idType }
1407
+ };
1408
+ return listAttachments(context.library, operationOptions);
1409
+ }
1410
+ async function executeAttachGet(options, context) {
1411
+ const operationOptions = {
1412
+ identifier: options.identifier,
1413
+ attachmentsDirectory: options.attachmentsDirectory,
1414
+ ...options.filename !== void 0 && { filename: options.filename },
1415
+ ...options.role !== void 0 && { role: options.role },
1416
+ ...options.stdout !== void 0 && { stdout: options.stdout },
1417
+ ...options.idType !== void 0 && { idType: options.idType }
1418
+ };
1419
+ return getAttachment(context.library, operationOptions);
1420
+ }
1421
+ async function executeAttachDetach(options, context) {
1422
+ const operationOptions = {
1423
+ identifier: options.identifier,
1424
+ attachmentsDirectory: options.attachmentsDirectory,
1425
+ ...options.filename !== void 0 && { filename: options.filename },
1426
+ ...options.role !== void 0 && { role: options.role },
1427
+ ...options.all !== void 0 && { all: options.all },
1428
+ ...options.removeFiles !== void 0 && { removeFiles: options.removeFiles },
1429
+ ...options.idType !== void 0 && { idType: options.idType }
1430
+ };
1431
+ return detachAttachment(context.library, operationOptions);
1432
+ }
1433
+ async function executeAttachSync(options, context) {
1434
+ const operationOptions = {
1435
+ identifier: options.identifier,
1436
+ attachmentsDirectory: options.attachmentsDirectory,
1437
+ ...options.yes !== void 0 && { yes: options.yes },
1438
+ ...options.fix !== void 0 && { fix: options.fix },
1439
+ ...options.idType !== void 0 && { idType: options.idType }
1440
+ };
1441
+ return syncAttachments(context.library, operationOptions);
1442
+ }
1443
+ function formatAttachOpenOutput(result) {
1444
+ if (!result.success) {
1445
+ return `Error: ${result.error}`;
1446
+ }
1447
+ if (result.directoryCreated) {
1448
+ return `Created and opened: ${result.path}`;
1449
+ }
1450
+ return `Opened: ${result.path}`;
1451
+ }
1452
+ function formatAttachAddOutput(result) {
1453
+ if (result.requiresConfirmation) {
1454
+ return `File already exists: ${result.existingFile}
1455
+ Use --force to overwrite.`;
1456
+ }
1457
+ if (!result.success) {
1458
+ return `Error: ${result.error}`;
1459
+ }
1460
+ if (result.overwritten) {
1461
+ return `Added (overwritten): ${result.filename}`;
1462
+ }
1463
+ return `Added: ${result.filename}`;
1464
+ }
1465
+ function formatAttachListOutput(result, identifier) {
1466
+ if (!result.success) {
1467
+ return `Error: ${result.error}`;
1468
+ }
1469
+ if (result.files.length === 0) {
1470
+ return `No attachments for reference: ${identifier}`;
1471
+ }
1472
+ const grouped = /* @__PURE__ */ new Map();
1473
+ for (const file of result.files) {
1474
+ const existing = grouped.get(file.role) ?? [];
1475
+ existing.push(file);
1476
+ grouped.set(file.role, existing);
1477
+ }
1478
+ const lines = [];
1479
+ lines.push(`Attachments for ${identifier} (${result.directory}/):`);
1480
+ lines.push("");
1481
+ for (const [role, files] of grouped) {
1482
+ lines.push(`${role}:`);
1483
+ for (const file of files) {
1484
+ if (file.label) {
1485
+ lines.push(` ${file.filename} - "${file.label}"`);
1486
+ } else {
1487
+ lines.push(` ${file.filename}`);
1488
+ }
1489
+ }
1490
+ lines.push("");
1491
+ }
1492
+ return lines.join("\n").trimEnd();
1493
+ }
1494
+ function formatAttachDetachOutput(result) {
1495
+ if (!result.success) {
1496
+ return `Error: ${result.error}`;
1497
+ }
1498
+ const lines = [];
1499
+ for (const filename of result.detached) {
1500
+ if (result.deleted.includes(filename)) {
1501
+ lines.push(`Detached and deleted: ${filename}`);
1502
+ } else {
1503
+ lines.push(`Detached: ${filename}`);
1504
+ }
1505
+ }
1506
+ if (result.directoryDeleted) {
1507
+ lines.push("Directory removed.");
1508
+ }
1509
+ return lines.join("\n");
1510
+ }
1511
+ function pluralize(count, singular) {
1512
+ return count > 1 ? `${singular}s` : singular;
1513
+ }
1514
+ function formatNewFilesSection(result, lines) {
1515
+ const count = result.newFiles.length;
1516
+ if (count === 0) return;
1517
+ const verb = result.applied ? "Added" : "Found";
1518
+ const suffix = result.applied ? "" : " new";
1519
+ lines.push(`${verb} ${count}${suffix} ${pluralize(count, "file")}:`);
1520
+ for (const file of result.newFiles) {
1521
+ const labelPart = file.label ? `, label: "${file.label}"` : "";
1522
+ lines.push(` ${file.filename} → role: ${file.role}${labelPart}`);
1523
+ }
1524
+ lines.push("");
1525
+ }
1526
+ function formatMissingFilesSection(result, lines) {
1527
+ const count = result.missingFiles.length;
1528
+ if (count === 0) return;
1529
+ const header = result.applied ? `Removed ${count} missing ${pluralize(count, "file")} from metadata:` : `Missing ${count} ${pluralize(count, "file")} (in metadata but not on disk):`;
1530
+ lines.push(header);
1531
+ for (const filename of result.missingFiles) {
1532
+ lines.push(` ${filename}`);
1533
+ }
1534
+ lines.push("");
1535
+ }
1536
+ function formatAttachSyncOutput(result) {
1537
+ if (!result.success) {
1538
+ return `Error: ${result.error}`;
1539
+ }
1540
+ const hasNewFiles = result.newFiles.length > 0;
1541
+ const hasMissingFiles = result.missingFiles.length > 0;
1542
+ if (!hasNewFiles && !hasMissingFiles) {
1543
+ return "Already in sync.";
1544
+ }
1545
+ const lines = [];
1546
+ formatNewFilesSection(result, lines);
1547
+ formatMissingFilesSection(result, lines);
1548
+ if (!result.applied) {
1549
+ if (hasNewFiles) {
1550
+ lines.push("Run with --yes to add new files");
1551
+ }
1552
+ if (hasMissingFiles) {
1553
+ lines.push("Run with --fix to remove missing files");
1554
+ }
1555
+ }
1556
+ return lines.join("\n").trimEnd();
1557
+ }
1558
+ function getAttachExitCode(result) {
1559
+ return result.success ? 0 : 1;
1560
+ }
1561
+ async function executeInteractiveSelect$1(context, config2) {
1562
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
1563
+ const allReferences = await context.library.getAll();
1564
+ const identifiers = await selectReferencesOrExit(
1565
+ allReferences,
1566
+ { multiSelect: false },
1567
+ config2.cli.tui
1568
+ );
1569
+ return identifiers[0];
1570
+ }
1571
+ async function resolveIdentifier(identifierArg, context, config2) {
1572
+ if (identifierArg) {
1573
+ return identifierArg;
1574
+ }
1575
+ if (isTTY()) {
1576
+ return executeInteractiveSelect$1(context, config2);
1577
+ }
1578
+ const stdinId = await readIdentifierFromStdin();
1579
+ if (!stdinId) {
1580
+ process.stderr.write(
1581
+ "Error: No identifier provided. Provide an ID, pipe one via stdin, or run interactively in a TTY.\n"
1582
+ );
1583
+ process.exit(1);
1584
+ }
1585
+ return stdinId;
1586
+ }
1587
+ function displayNamingConvention(identifier, dirPath) {
1588
+ process.stderr.write(`
1589
+ Opening attachments directory for ${identifier}...
1590
+
1591
+ `);
1592
+ process.stderr.write("File naming convention:\n");
1593
+ process.stderr.write(" fulltext.pdf / fulltext.md - Paper body\n");
1594
+ process.stderr.write(" supplement-{label}.ext - Supplementary materials\n");
1595
+ process.stderr.write(" notes-{label}.ext - Your notes\n");
1596
+ process.stderr.write(" draft-{label}.ext - Draft versions\n");
1597
+ process.stderr.write(" {custom}-{label}.ext - Custom role\n\n");
1598
+ process.stderr.write(`Directory: ${dirPath}/
1599
+
1600
+ `);
1601
+ }
1602
+ async function waitForEnter() {
1603
+ return new Promise((resolve2) => {
1604
+ process.stderr.write("Press Enter when done editing...");
1605
+ process.stdin.setRawMode(true);
1606
+ process.stdin.resume();
1607
+ process.stdin.once("data", () => {
1608
+ process.stdin.setRawMode(false);
1609
+ process.stdin.pause();
1610
+ process.stderr.write("\n\n");
1611
+ resolve2();
1612
+ });
1613
+ });
1614
+ }
1615
+ function displayInteractiveSyncResult(result, identifier) {
1616
+ if (result.newFiles.length === 0) {
1617
+ process.stderr.write("No new files detected.\n");
1618
+ return;
1619
+ }
1620
+ process.stderr.write("Scanning directory...\n\n");
1621
+ process.stderr.write(
1622
+ `Found ${result.newFiles.length} new file${result.newFiles.length > 1 ? "s" : ""}:
1623
+ `
1624
+ );
1625
+ for (const file of result.newFiles) {
1626
+ const labelPart = file.label ? `, label: "${file.label}"` : "";
1627
+ process.stderr.write(` ✓ ${file.filename} → role: ${file.role}${labelPart}
1628
+ `);
1629
+ }
1630
+ process.stderr.write(`
1631
+ Updated metadata for ${identifier}.
1632
+ `);
1633
+ }
1634
+ async function runInteractiveMode(identifier, dirPath, attachmentsDirectory, idType, context) {
1635
+ displayNamingConvention(identifier, dirPath);
1636
+ await waitForEnter();
1637
+ const syncResult = await executeAttachSync(
1638
+ {
1639
+ identifier,
1640
+ attachmentsDirectory,
1641
+ yes: true,
1642
+ ...idType && { idType }
1643
+ },
1644
+ context
1645
+ );
1646
+ if (syncResult.success) {
1647
+ displayInteractiveSyncResult(syncResult, identifier);
1648
+ } else {
1649
+ process.stderr.write(`Sync error: ${syncResult.error}
1650
+ `);
1651
+ }
1652
+ }
1653
+ function buildOpenOptions(identifier, filenameArg, options, attachmentsDirectory) {
1654
+ return {
1655
+ identifier,
1656
+ attachmentsDirectory,
1657
+ ...filenameArg && { filename: filenameArg },
1658
+ ...options.print && { print: options.print },
1659
+ ...options.role && { role: options.role },
1660
+ ...options.uuid && { idType: "uuid" }
1661
+ };
1662
+ }
1663
+ async function handleAttachOpenAction(identifierArg, filenameArg, options, globalOpts) {
1664
+ try {
1665
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1666
+ const context = await createExecutionContext(config2, Library.load);
1667
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1668
+ const isDirectoryMode = !filenameArg && !options.role;
1669
+ const shouldUseInteractive = isTTY() && isDirectoryMode && !options.print && !options.noSync;
1670
+ const openOptions = buildOpenOptions(
1671
+ identifier,
1672
+ filenameArg,
1673
+ options,
1674
+ config2.attachments.directory
1675
+ );
1676
+ const result = await executeAttachOpen(openOptions, context);
1677
+ if (!result.success) {
1678
+ process.stderr.write(`Error: ${result.error}
1679
+ `);
1680
+ process.exit(1);
1681
+ }
1682
+ if (options.print) {
1683
+ process.stdout.write(`${result.path}
1684
+ `);
1685
+ process.exit(0);
1686
+ }
1687
+ if (shouldUseInteractive) {
1688
+ await runInteractiveMode(
1689
+ identifier,
1690
+ result.path ?? "",
1691
+ config2.attachments.directory,
1692
+ options.uuid ? "uuid" : void 0,
1693
+ context
1694
+ );
1695
+ } else {
1696
+ process.stderr.write(`${formatAttachOpenOutput(result)}
1697
+ `);
1698
+ }
1699
+ process.exit(0);
1700
+ } catch (error) {
1701
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1702
+ `);
1703
+ process.exit(4);
1704
+ }
1705
+ }
1706
+ async function handleAttachAddAction(identifierArg, filePathArg, options, globalOpts) {
1707
+ try {
1708
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1709
+ const context = await createExecutionContext(config2, Library.load);
1710
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1711
+ const addOptions = {
1712
+ identifier,
1713
+ filePath: filePathArg,
1714
+ role: options.role,
1715
+ attachmentsDirectory: config2.attachments.directory,
1716
+ ...options.label && { label: options.label },
1717
+ ...options.move && { move: options.move },
1718
+ ...options.force && { force: options.force },
1719
+ ...options.uuid && { idType: "uuid" }
1720
+ };
1721
+ const result = await executeAttachAdd(addOptions, context);
1722
+ const output = formatAttachAddOutput(result);
1723
+ process.stderr.write(`${output}
1724
+ `);
1725
+ process.exit(getAttachExitCode(result));
1726
+ } catch (error) {
1727
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1728
+ `);
1729
+ process.exit(4);
1730
+ }
1731
+ }
1732
+ async function handleAttachListAction(identifierArg, options, globalOpts) {
1733
+ try {
1734
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1735
+ const context = await createExecutionContext(config2, Library.load);
1736
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1737
+ const listOptions = {
1738
+ identifier,
1739
+ attachmentsDirectory: config2.attachments.directory,
1740
+ ...options.role && { role: options.role },
1741
+ ...options.uuid && { idType: "uuid" }
1742
+ };
1743
+ const result = await executeAttachList(listOptions, context);
1744
+ const output = formatAttachListOutput(result, identifier);
1745
+ process.stdout.write(`${output}
1746
+ `);
1747
+ process.exit(getAttachExitCode(result));
1748
+ } catch (error) {
1749
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1750
+ `);
1751
+ process.exit(4);
628
1752
  }
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
- });
1753
+ }
1754
+ async function handleAttachGetAction(identifierArg, filenameArg, options, globalOpts) {
636
1755
  try {
637
- return await confirmPrompt.run();
638
- } catch {
639
- return false;
1756
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1757
+ const context = await createExecutionContext(config2, Library.load);
1758
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1759
+ const getOptions = {
1760
+ identifier,
1761
+ attachmentsDirectory: config2.attachments.directory,
1762
+ ...filenameArg && { filename: filenameArg },
1763
+ ...options.role && { role: options.role },
1764
+ ...options.stdout && { stdout: options.stdout },
1765
+ ...options.uuid && { idType: "uuid" }
1766
+ };
1767
+ const result = await executeAttachGet(getOptions, context);
1768
+ if (result.success && result.content && options.stdout) {
1769
+ process.stdout.write(result.content);
1770
+ } else if (result.success) {
1771
+ process.stdout.write(`${result.path}
1772
+ `);
1773
+ } else {
1774
+ process.stderr.write(`Error: ${result.error}
1775
+ `);
1776
+ }
1777
+ process.exit(getAttachExitCode(result));
1778
+ } catch (error) {
1779
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1780
+ `);
1781
+ process.exit(4);
640
1782
  }
641
1783
  }
642
- async function readStdinContent() {
643
- const chunks = [];
644
- for await (const chunk of stdin) {
645
- chunks.push(chunk);
1784
+ async function handleAttachDetachAction(identifierArg, filenameArg, options, globalOpts) {
1785
+ try {
1786
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1787
+ const context = await createExecutionContext(config2, Library.load);
1788
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1789
+ const detachOptions = {
1790
+ identifier,
1791
+ attachmentsDirectory: config2.attachments.directory,
1792
+ ...filenameArg && { filename: filenameArg },
1793
+ ...options.role && { role: options.role },
1794
+ ...options.all && { all: options.all },
1795
+ ...options.removeFiles && { removeFiles: options.removeFiles },
1796
+ ...options.uuid && { idType: "uuid" }
1797
+ };
1798
+ const result = await executeAttachDetach(detachOptions, context);
1799
+ const output = formatAttachDetachOutput(result);
1800
+ process.stderr.write(`${output}
1801
+ `);
1802
+ process.exit(getAttachExitCode(result));
1803
+ } catch (error) {
1804
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1805
+ `);
1806
+ process.exit(4);
646
1807
  }
647
- return Buffer.concat(chunks).toString("utf-8").trim();
648
1808
  }
649
- async function readStdinBuffer() {
650
- const chunks = [];
651
- for await (const chunk of stdin) {
652
- chunks.push(chunk);
1809
+ async function handleAttachSyncAction(identifierArg, options, globalOpts) {
1810
+ try {
1811
+ const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
1812
+ const context = await createExecutionContext(config2, Library.load);
1813
+ const identifier = await resolveIdentifier(identifierArg, context, config2);
1814
+ const syncOptions = {
1815
+ identifier,
1816
+ attachmentsDirectory: config2.attachments.directory,
1817
+ ...options.yes && { yes: options.yes },
1818
+ ...options.fix && { fix: options.fix },
1819
+ ...options.uuid && { idType: "uuid" }
1820
+ };
1821
+ const result = await executeAttachSync(syncOptions, context);
1822
+ const output = formatAttachSyncOutput(result);
1823
+ process.stderr.write(`${output}
1824
+ `);
1825
+ process.exit(getAttachExitCode(result));
1826
+ } catch (error) {
1827
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
1828
+ `);
1829
+ process.exit(4);
653
1830
  }
654
- return Buffer.concat(chunks);
655
1831
  }
656
1832
  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`);
1833
+ if (options.output && !["text", "html", "rtf"].includes(options.output)) {
1834
+ throw new Error(`Invalid output format '${options.output}'. Must be one of: text, html, rtf`);
659
1835
  }
660
1836
  if (options.cslFile) {
661
1837
  const fs2 = await import("node:fs");
@@ -671,7 +1847,7 @@ function buildCiteOptions(options) {
671
1847
  ...options.style !== void 0 && { style: options.style },
672
1848
  ...options.cslFile !== void 0 && { cslFile: options.cslFile },
673
1849
  ...options.locale !== void 0 && { locale: options.locale },
674
- ...options.format !== void 0 && { format: options.format },
1850
+ ...options.output !== void 0 && { format: options.output },
675
1851
  ...options.inText !== void 0 && { inText: options.inText }
676
1852
  };
677
1853
  }
@@ -709,13 +1885,13 @@ function getCiteExitCode(result) {
709
1885
  return 0;
710
1886
  }
711
1887
  async function executeInteractiveCite(options, context, config2) {
712
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
713
- const { runStyleSelect } = await import("./style-select-CYo0O7MZ.js");
1888
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
1889
+ const { runStyleSelect } = await import("./style-select-BNQHC79W.js");
714
1890
  const allReferences = await context.library.getAll();
715
1891
  const identifiers = await selectReferencesOrExit(
716
1892
  allReferences,
717
1893
  { multiSelect: true },
718
- config2.cli.interactive
1894
+ config2.cli.tui
719
1895
  );
720
1896
  let style = options.style;
721
1897
  if (!style && !options.cslFile) {
@@ -770,7 +1946,7 @@ async function handleCiteAction(identifiers, options, globalOpts) {
770
1946
  }
771
1947
  const ENV_OVERRIDE_MAP = {
772
1948
  REFERENCE_MANAGER_LIBRARY: "library",
773
- REFERENCE_MANAGER_FULLTEXT_DIR: "fulltext.directory",
1949
+ REFERENCE_MANAGER_ATTACHMENTS_DIR: "attachments.directory",
774
1950
  REFERENCE_MANAGER_CLI_DEFAULT_LIMIT: "cli.default_limit",
775
1951
  REFERENCE_MANAGER_MCP_DEFAULT_LIMIT: "mcp.default_limit",
776
1952
  PUBMED_EMAIL: "pubmed.email",
@@ -852,14 +2028,14 @@ const CONFIG_KEY_REGISTRY = [
852
2028
  description: "Default sort order",
853
2029
  enumValues: ["asc", "desc"]
854
2030
  },
855
- // cli.interactive section
2031
+ // cli.tui section
856
2032
  {
857
- key: "cli.interactive.limit",
2033
+ key: "cli.tui.limit",
858
2034
  type: "integer",
859
- description: "Result limit in interactive mode"
2035
+ description: "Result limit in TUI mode"
860
2036
  },
861
2037
  {
862
- key: "cli.interactive.debounce_ms",
2038
+ key: "cli.tui.debounce_ms",
863
2039
  type: "integer",
864
2040
  description: "Search debounce delay (ms)"
865
2041
  },
@@ -1064,7 +2240,7 @@ function createConfigTemplate() {
1064
2240
  # default_sort = "updated" # created, updated, published, author, title
1065
2241
  # default_order = "desc" # asc, desc
1066
2242
 
1067
- [cli.interactive]
2243
+ [cli.tui]
1068
2244
  # limit = 20
1069
2245
  # debounce_ms = 200
1070
2246
 
@@ -1307,9 +2483,9 @@ function toSnakeCaseConfig(config2) {
1307
2483
  default_limit: config2.cli.defaultLimit,
1308
2484
  default_sort: config2.cli.defaultSort,
1309
2485
  default_order: config2.cli.defaultOrder,
1310
- interactive: {
1311
- limit: config2.cli.interactive.limit,
1312
- debounce_ms: config2.cli.interactive.debounceMs
2486
+ tui: {
2487
+ limit: config2.cli.tui.limit,
2488
+ debounce_ms: config2.cli.tui.debounceMs
1313
2489
  },
1314
2490
  edit: {
1315
2491
  default_format: config2.cli.edit.defaultFormat
@@ -1466,11 +2642,11 @@ function resolveEditor(platform) {
1466
2642
  }
1467
2643
  function registerConfigCommand(program) {
1468
2644
  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) => {
2645
+ configCmd.command("show").description("Display effective configuration").option("-o, --output <format>", "Output format: text|json").option("--section <name>", "Show only a specific section").option("--sources", "Include source information for each value").action(async (options) => {
1470
2646
  try {
1471
2647
  const config2 = loadConfig();
1472
2648
  const output = showConfig(config2, {
1473
- json: options.json,
2649
+ json: options.output === "json",
1474
2650
  section: options.section,
1475
2651
  sources: options.sources
1476
2652
  });
@@ -1557,7 +2733,7 @@ function registerConfigCommand(program) {
1557
2733
  process.exit(1);
1558
2734
  }
1559
2735
  });
1560
- configCmd.command("list-keys").description("List all available configuration keys").option("--section <name>", "List keys only in a specific section").action(async (options) => {
2736
+ configCmd.command("keys").description("List all available configuration keys").option("--section <name>", "List keys only in a specific section").action(async (options) => {
1561
2737
  try {
1562
2738
  const output = listConfigKeys({ section: options.section });
1563
2739
  if (output) {
@@ -4542,12 +5718,12 @@ function formatEditOutput(result) {
4542
5718
  return lines.join("\n");
4543
5719
  }
4544
5720
  async function executeInteractiveEdit(options, context, config2) {
4545
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
5721
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
4546
5722
  const allReferences = await context.library.getAll();
4547
5723
  const identifiers = await selectReferencesOrExit(
4548
5724
  allReferences,
4549
5725
  { multiSelect: true },
4550
- config2.cli.interactive
5726
+ config2.cli.tui
4551
5727
  );
4552
5728
  const format2 = options.format ?? config2.cli.edit.defaultFormat;
4553
5729
  return executeEditCommand(
@@ -7778,7 +8954,7 @@ async function executeExport(options, context) {
7778
8954
  return { items: items2, notFound };
7779
8955
  }
7780
8956
  function formatExportOutput(result, options) {
7781
- const format2 = options.format ?? "json";
8957
+ const format2 = options.output ?? "json";
7782
8958
  const singleIdRequest = (options.ids?.length ?? 0) === 1 && !options.all && !options.search;
7783
8959
  const data = result.items.length === 1 && singleIdRequest ? result.items[0] : result.items;
7784
8960
  if (format2 === "json") {
@@ -7795,183 +8971,6 @@ function formatExportOutput(result, options) {
7795
8971
  function getExportExitCode(result) {
7796
8972
  return result.notFound.length > 0 ? 1 : 0;
7797
8973
  }
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
8974
  function detectType(filePath) {
7976
8975
  const ext = extname(filePath).toLowerCase();
7977
8976
  if (ext === ".pdf") return "pdf";
@@ -7996,8 +8995,8 @@ function resolveFileType(explicitType, filePath, stdinContent) {
7996
8995
  function prepareStdinSource(stdinContent, fileType) {
7997
8996
  try {
7998
8997
  const tempDir = mkdtempSync(join(tmpdir(), "refmgr-"));
7999
- const ext = fileType === "pdf" ? ".pdf" : ".md";
8000
- const sourcePath = join(tempDir, `stdin${ext}`);
8998
+ const ext = formatToExtension(fileType);
8999
+ const sourcePath = join(tempDir, `stdin.${ext}`);
8001
9000
  writeFileSync(sourcePath, stdinContent);
8002
9001
  return { sourcePath, tempDir };
8003
9002
  } catch (error) {
@@ -8012,13 +9011,6 @@ async function cleanupTempDir(tempDir) {
8012
9011
  });
8013
9012
  }
8014
9013
  }
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
9014
  function prepareSourcePath(filePath, stdinContent, fileType) {
8023
9015
  if (stdinContent) {
8024
9016
  return prepareStdinSource(stdinContent, fileType);
@@ -8028,12 +9020,26 @@ function prepareSourcePath(filePath, stdinContent, fileType) {
8028
9020
  }
8029
9021
  return { sourcePath: filePath };
8030
9022
  }
8031
- async function performAttach(manager, item, sourcePath, fileType, move, force) {
8032
- const attachOptions = {
8033
- ...move !== void 0 && { move },
8034
- ...force !== void 0 && { force }
9023
+ function convertResult(result, fileType) {
9024
+ if (result.success) {
9025
+ return {
9026
+ success: true,
9027
+ filename: result.filename,
9028
+ type: fileType,
9029
+ overwritten: result.overwritten
9030
+ };
9031
+ }
9032
+ if (result.requiresConfirmation) {
9033
+ return {
9034
+ success: false,
9035
+ existingFile: result.existingFile,
9036
+ requiresConfirmation: true
9037
+ };
9038
+ }
9039
+ return {
9040
+ success: false,
9041
+ error: result.error
8035
9042
  };
8036
- return manager.attachFile(item, sourcePath, fileType, attachOptions);
8037
9043
  }
8038
9044
  async function fulltextAttach(library, options) {
8039
9045
  const {
@@ -8046,71 +9052,78 @@ async function fulltextAttach(library, options) {
8046
9052
  fulltextDirectory,
8047
9053
  stdinContent
8048
9054
  } = options;
8049
- const item = await library.find(identifier, { idType });
8050
- if (!item) {
8051
- return { success: false, error: `Reference '${identifier}' not found` };
8052
- }
8053
9055
  const fileTypeResult = resolveFileType(explicitType, filePath, stdinContent);
8054
9056
  if (typeof fileTypeResult === "object" && "error" in fileTypeResult) {
9057
+ const item = await library.find(identifier, { idType });
9058
+ if (!item) {
9059
+ return { success: false, error: `Reference '${identifier}' not found` };
9060
+ }
8055
9061
  return { success: false, error: fileTypeResult.error };
8056
9062
  }
8057
9063
  const fileType = fileTypeResult;
8058
9064
  const sourceResult = prepareSourcePath(filePath, stdinContent, fileType);
8059
9065
  if ("error" in sourceResult) {
9066
+ const item = await library.find(identifier, { idType });
9067
+ if (!item) {
9068
+ return { success: false, error: `Reference '${identifier}' not found` };
9069
+ }
8060
9070
  return { success: false, error: sourceResult.error };
8061
9071
  }
8062
9072
  const { sourcePath, tempDir } = sourceResult;
8063
- const manager = new FulltextManager(fulltextDirectory);
8064
9073
  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, {
9074
+ const result = await addAttachment(library, {
8072
9075
  identifier,
8073
- updates: {
8074
- custom: { fulltext: newFulltext }
8075
- },
8076
- idType
9076
+ filePath: sourcePath,
9077
+ role: FULLTEXT_ROLE,
9078
+ move: move ?? false,
9079
+ force: force ?? false,
9080
+ idType,
9081
+ attachmentsDirectory: fulltextDirectory
8077
9082
  });
8078
9083
  await cleanupTempDir(tempDir);
8079
- return {
8080
- success: true,
8081
- filename: result.filename,
8082
- type: fileType,
8083
- overwritten: result.overwritten
8084
- };
9084
+ return convertResult(result, fileType);
8085
9085
  } catch (error) {
8086
9086
  await cleanupTempDir(tempDir);
8087
- if (error instanceof FulltextIOError) {
8088
- return { success: false, error: error.message };
8089
- }
8090
9087
  throw error;
8091
9088
  }
8092
9089
  }
8093
- async function getFileContent(manager, item, type2, identifier) {
8094
- const filePath = manager.getFilePath(item, type2);
8095
- if (!filePath) {
9090
+ function buildFilePath$1(attachmentsDirectory, directory, filename) {
9091
+ return normalizePathForOutput(join(attachmentsDirectory, directory, filename));
9092
+ }
9093
+ async function getFileContent(filePath) {
9094
+ const content = await readFile(filePath);
9095
+ return { success: true, content };
9096
+ }
9097
+ async function handleStdoutMode(attachments, type2, identifier, fulltextDirectory) {
9098
+ const file = findFulltextFile(attachments, type2);
9099
+ if (!file || !attachments?.directory) {
8096
9100
  return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
8097
9101
  }
9102
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
8098
9103
  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
- };
9104
+ return await getFileContent(filePath);
9105
+ } catch {
9106
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
8106
9107
  }
8107
9108
  }
8108
- function getFilePaths(manager, item, types2, identifier) {
9109
+ function getSingleTypePath(attachments, type2, identifier, fulltextDirectory) {
9110
+ const file = findFulltextFile(attachments, type2);
9111
+ if (!file || !attachments?.directory) {
9112
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
9113
+ }
9114
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
9115
+ const paths = {};
9116
+ paths[type2] = filePath;
9117
+ return { success: true, paths };
9118
+ }
9119
+ function getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier) {
8109
9120
  const paths = {};
8110
- for (const t of types2) {
8111
- const filePath = manager.getFilePath(item, t);
8112
- if (filePath) {
8113
- paths[t] = filePath;
9121
+ for (const file of fulltextFiles) {
9122
+ const ext = file.filename.split(".").pop() || "";
9123
+ const format2 = extensionToFormat(ext);
9124
+ if (format2) {
9125
+ const filePath = buildFilePath$1(fulltextDirectory, attachments.directory, file.filename);
9126
+ paths[format2] = filePath;
8114
9127
  }
8115
9128
  }
8116
9129
  if (Object.keys(paths).length === 0) {
@@ -8124,92 +9137,98 @@ async function fulltextGet(library, options) {
8124
9137
  if (!item) {
8125
9138
  return { success: false, error: `Reference '${identifier}' not found` };
8126
9139
  }
8127
- const manager = new FulltextManager(fulltextDirectory);
9140
+ const attachments = item.custom?.attachments;
8128
9141
  if (stdout2 && type2) {
8129
- return getFileContent(manager, item, type2, identifier);
9142
+ return handleStdoutMode(attachments, type2, identifier, fulltextDirectory);
9143
+ }
9144
+ const fulltextFiles = findFulltextFiles(attachments);
9145
+ if (fulltextFiles.length === 0) {
9146
+ return { success: false, error: `No fulltext attached to '${identifier}'` };
9147
+ }
9148
+ if (type2) {
9149
+ return getSingleTypePath(attachments, type2, identifier, fulltextDirectory);
8130
9150
  }
8131
- const attachedTypes = type2 ? [type2] : manager.getAttachedTypes(item);
8132
- if (attachedTypes.length === 0) {
9151
+ if (!attachments) {
8133
9152
  return { success: false, error: `No fulltext attached to '${identifier}'` };
8134
9153
  }
8135
- return getFilePaths(manager, item, attachedTypes, identifier);
9154
+ return getAllFulltextPaths(attachments, fulltextFiles, fulltextDirectory, identifier);
8136
9155
  }
8137
- async function performDetachOperations(manager, item, typesToDetach, deleteFile) {
9156
+ function getFilesToDetach(attachments, type2) {
9157
+ if (type2) {
9158
+ const file = findFulltextFile(attachments, type2);
9159
+ return file ? [file] : [];
9160
+ }
9161
+ return findFulltextFiles(attachments);
9162
+ }
9163
+ async function detachFiles(library, files, identifier, removeFiles, idType, fulltextDirectory) {
8138
9164
  const detached = [];
8139
9165
  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);
9166
+ for (const file of files) {
9167
+ const result = await detachAttachment(library, {
9168
+ identifier,
9169
+ filename: file.filename,
9170
+ removeFiles: removeFiles ?? false,
9171
+ idType,
9172
+ attachmentsDirectory: fulltextDirectory
9173
+ });
9174
+ if (result.success) {
9175
+ const ext = file.filename.split(".").pop() || "";
9176
+ const format2 = extensionToFormat(ext);
9177
+ if (format2) {
9178
+ detached.push(format2);
9179
+ if (result.deleted.length > 0) {
9180
+ deleted.push(format2);
9181
+ }
9182
+ }
8146
9183
  }
8147
9184
  }
8148
9185
  return { detached, deleted };
8149
9186
  }
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;
9187
+ function buildResult(detached, deleted, identifier) {
9188
+ if (detached.length === 0) {
9189
+ return { success: false, error: `Failed to detach fulltext from '${identifier}'` };
8157
9190
  }
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 };
9191
+ const result = { success: true, detached };
9192
+ if (deleted.length > 0) {
9193
+ result.deleted = deleted;
8163
9194
  }
8164
- throw error;
9195
+ return result;
8165
9196
  }
8166
9197
  async function fulltextDetach(library, options) {
8167
- const { identifier, type: type2, delete: deleteFile, idType = "id", fulltextDirectory } = options;
9198
+ const { identifier, type: type2, removeFiles, idType = "id", fulltextDirectory } = options;
8168
9199
  const item = await library.find(identifier, { idType });
8169
9200
  if (!item) {
8170
9201
  return { success: false, error: `Reference '${identifier}' not found` };
8171
9202
  }
8172
- const manager = new FulltextManager(fulltextDirectory);
8173
- const typesToDetach = type2 ? [type2] : manager.getAttachedTypes(item);
8174
- if (typesToDetach.length === 0) {
9203
+ const attachments = item.custom?.attachments;
9204
+ const fulltextFiles = findFulltextFiles(attachments);
9205
+ if (fulltextFiles.length === 0) {
8175
9206
  return { success: false, error: `No fulltext attached to '${identifier}'` };
8176
9207
  }
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);
9208
+ const filesToDetach = getFilesToDetach(attachments, type2);
9209
+ if (filesToDetach.length === 0) {
9210
+ return { success: false, error: `No ${type2} fulltext attached to '${identifier}'` };
8199
9211
  }
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";
9212
+ const { detached, deleted } = await detachFiles(
9213
+ library,
9214
+ filesToDetach,
9215
+ identifier,
9216
+ removeFiles,
9217
+ idType,
9218
+ fulltextDirectory
9219
+ );
9220
+ return buildResult(detached, deleted, identifier);
9221
+ }
9222
+ function buildFilePath(attachmentsDirectory, directory, filename) {
9223
+ return join(attachmentsDirectory, directory, filename);
9224
+ }
9225
+ function determineTypeToOpen(attachments) {
9226
+ const files = findFulltextFiles(attachments);
9227
+ if (files.length === 0) return void 0;
9228
+ const pdfFile = files.find((f) => f.filename.endsWith(".pdf"));
9229
+ if (pdfFile) return "pdf";
9230
+ const mdFile = files.find((f) => f.filename.endsWith(".md"));
9231
+ if (mdFile) return "markdown";
8213
9232
  return void 0;
8214
9233
  }
8215
9234
  async function fulltextOpen(library, options) {
@@ -8218,14 +9237,16 @@ async function fulltextOpen(library, options) {
8218
9237
  if (!item) {
8219
9238
  return { success: false, error: `Reference not found: ${identifier}` };
8220
9239
  }
8221
- const typeToOpen = type2 ?? determineTypeToOpen(item);
9240
+ const attachments = item.custom?.attachments;
9241
+ const typeToOpen = type2 ?? determineTypeToOpen(attachments);
8222
9242
  if (!typeToOpen) {
8223
9243
  return { success: false, error: `No fulltext attached to reference: ${identifier}` };
8224
9244
  }
8225
- const filePath = getFulltextPath(item, typeToOpen, fulltextDirectory);
8226
- if (!filePath) {
9245
+ const file = findFulltextFile(attachments, typeToOpen);
9246
+ if (!file || !attachments?.directory) {
8227
9247
  return { success: false, error: `No ${typeToOpen} attached to reference: ${identifier}` };
8228
9248
  }
9249
+ const filePath = buildFilePath(fulltextDirectory, attachments.directory, file.filename);
8229
9250
  if (!existsSync(filePath)) {
8230
9251
  return {
8231
9252
  success: false,
@@ -8273,7 +9294,7 @@ async function executeFulltextDetach(options, context) {
8273
9294
  const operationOptions = {
8274
9295
  identifier: options.identifier,
8275
9296
  type: options.type,
8276
- delete: options.delete,
9297
+ removeFiles: options.removeFiles,
8277
9298
  idType: options.idType,
8278
9299
  fulltextDirectory: options.fulltextDirectory
8279
9300
  };
@@ -8344,12 +9365,12 @@ function getFulltextExitCode(result) {
8344
9365
  return result.success ? 0 : 1;
8345
9366
  }
8346
9367
  async function executeInteractiveSelect(context, config2) {
8347
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
9368
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
8348
9369
  const allReferences = await context.library.getAll();
8349
9370
  const identifiers = await selectReferencesOrExit(
8350
9371
  allReferences,
8351
9372
  { multiSelect: false },
8352
- config2.cli.interactive
9373
+ config2.cli.tui
8353
9374
  );
8354
9375
  return identifiers[0];
8355
9376
  }
@@ -8388,7 +9409,7 @@ async function handleFulltextAttachAction(identifierArg, filePathArg, options, g
8388
9409
  const stdinContent = !filePath && type2 ? await readStdinBuffer() : void 0;
8389
9410
  const attachOptions = {
8390
9411
  identifier,
8391
- fulltextDirectory: config2.fulltext.directory,
9412
+ fulltextDirectory: config2.attachments.directory,
8392
9413
  ...filePath && { filePath },
8393
9414
  ...type2 && { type: type2 },
8394
9415
  ...options.move && { move: options.move },
@@ -8442,7 +9463,7 @@ async function handleFulltextGetAction(identifierArg, options, globalOpts) {
8442
9463
  }
8443
9464
  const getOptions = {
8444
9465
  identifier,
8445
- fulltextDirectory: config2.fulltext.directory,
9466
+ fulltextDirectory: config2.attachments.directory,
8446
9467
  ...options.pdf && { type: "pdf" },
8447
9468
  ...options.markdown && { type: "markdown" },
8448
9469
  ...options.stdout && { stdout: options.stdout },
@@ -8478,10 +9499,10 @@ async function handleFulltextDetachAction(identifierArg, options, globalOpts) {
8478
9499
  }
8479
9500
  const detachOptions = {
8480
9501
  identifier,
8481
- fulltextDirectory: config2.fulltext.directory,
9502
+ fulltextDirectory: config2.attachments.directory,
8482
9503
  ...options.pdf && { type: "pdf" },
8483
9504
  ...options.markdown && { type: "markdown" },
8484
- ...options.delete && { delete: options.delete },
9505
+ ...options.removeFiles && { removeFiles: options.removeFiles },
8485
9506
  ...options.force && { force: options.force },
8486
9507
  ...options.uuid && { idType: "uuid" }
8487
9508
  };
@@ -8517,7 +9538,7 @@ async function handleFulltextOpenAction(identifierArg, options, globalOpts) {
8517
9538
  }
8518
9539
  const openOptions = {
8519
9540
  identifier,
8520
- fulltextDirectory: config2.fulltext.directory,
9541
+ fulltextDirectory: config2.attachments.directory,
8521
9542
  ...options.pdf && { type: "pdf" },
8522
9543
  ...options.markdown && { type: "markdown" },
8523
9544
  ...options.uuid && { idType: "uuid" }
@@ -8624,21 +9645,28 @@ const VALID_LIST_SORT_FIELDS = /* @__PURE__ */ new Set([
8624
9645
  "pub"
8625
9646
  ]);
8626
9647
  function getOutputFormat$1(options) {
9648
+ if (options.output) {
9649
+ if (options.output === "ids") return "ids-only";
9650
+ return options.output;
9651
+ }
8627
9652
  if (options.json) return "json";
8628
9653
  if (options.idsOnly) return "ids-only";
8629
- if (options.uuid) return "uuid";
9654
+ if (options.uuidOnly) return "uuid";
8630
9655
  if (options.bibtex) return "bibtex";
8631
9656
  return "pretty";
8632
9657
  }
8633
9658
  function validateOptions$1(options) {
8634
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
9659
+ const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
8635
9660
  Boolean
8636
9661
  );
8637
9662
  if (outputOptions.length > 1) {
8638
9663
  throw new Error(
8639
- "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
9664
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
8640
9665
  );
8641
9666
  }
9667
+ if (options.output && outputOptions.length > 0) {
9668
+ throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
9669
+ }
8642
9670
  if (options.sort !== void 0) {
8643
9671
  const sortStr = String(options.sort);
8644
9672
  if (!VALID_LIST_SORT_FIELDS.has(sortStr)) {
@@ -29050,7 +30078,7 @@ function registerFulltextAttachTool(server, getLibraryOperations, getConfig) {
29050
30078
  filePath: args.path,
29051
30079
  force: true,
29052
30080
  // MCP tools don't support interactive confirmation
29053
- fulltextDirectory: config2.fulltext.directory
30081
+ fulltextDirectory: config2.attachments.directory
29054
30082
  });
29055
30083
  if (!result.success) {
29056
30084
  return {
@@ -29083,7 +30111,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
29083
30111
  const config2 = getConfig();
29084
30112
  const pathResult = await fulltextGet(libraryOps, {
29085
30113
  identifier: args.id,
29086
- fulltextDirectory: config2.fulltext.directory
30114
+ fulltextDirectory: config2.attachments.directory
29087
30115
  });
29088
30116
  if (!pathResult.success) {
29089
30117
  return {
@@ -29097,7 +30125,7 @@ function registerFulltextGetTool(server, getLibraryOperations, getConfig) {
29097
30125
  identifier: args.id,
29098
30126
  type: "markdown",
29099
30127
  stdout: true,
29100
- fulltextDirectory: config2.fulltext.directory
30128
+ fulltextDirectory: config2.attachments.directory
29101
30129
  });
29102
30130
  if (contentResult.success && contentResult.content) {
29103
30131
  responses.push({
@@ -29136,7 +30164,7 @@ function registerFulltextDetachTool(server, getLibraryOperations, getConfig) {
29136
30164
  const config2 = getConfig();
29137
30165
  const result = await fulltextDetach(libraryOps, {
29138
30166
  identifier: args.id,
29139
- fulltextDirectory: config2.fulltext.directory
30167
+ fulltextDirectory: config2.attachments.directory
29140
30168
  });
29141
30169
  if (!result.success) {
29142
30170
  return {
@@ -29342,7 +30370,7 @@ async function mcpStart(options) {
29342
30370
  async function executeRemove(options, context) {
29343
30371
  const { identifier, idType = "id", fulltextDirectory, deleteFulltext = false } = options;
29344
30372
  if (context.mode === "local" && deleteFulltext && fulltextDirectory) {
29345
- const { removeReference } = await import("./index-DapYyqAC.js").then((n) => n.r);
30373
+ const { removeReference } = await import("./index-DHgeuWGP.js").then((n) => n.r);
29346
30374
  return removeReference(context.library, {
29347
30375
  identifier,
29348
30376
  idType,
@@ -29396,12 +30424,12 @@ Continue?`;
29396
30424
  return readConfirmation(confirmMsg);
29397
30425
  }
29398
30426
  async function executeInteractiveRemove(context, config2) {
29399
- const { selectReferenceItemsOrExit } = await import("./reference-select-DSVwE9iu.js");
30427
+ const { selectReferenceItemsOrExit } = await import("./reference-select-B9w9CLa1.js");
29400
30428
  const allReferences = await context.library.getAll();
29401
30429
  const selectedItems = await selectReferenceItemsOrExit(
29402
30430
  allReferences,
29403
30431
  { multiSelect: false },
29404
- config2.cli.interactive
30432
+ config2.cli.tui
29405
30433
  );
29406
30434
  const selectedItem = selectedItems[0];
29407
30435
  return { identifier: selectedItem.id, item: selectedItem };
@@ -29483,7 +30511,7 @@ async function handleRemoveAction(identifierArg, options, globalOpts) {
29483
30511
  const removeOptions = {
29484
30512
  identifier,
29485
30513
  idType: useUuid ? "uuid" : "id",
29486
- fulltextDirectory: config2.fulltext.directory,
30514
+ fulltextDirectory: config2.attachments.directory,
29487
30515
  deleteFulltext: force && hasFulltext
29488
30516
  };
29489
30517
  const result = await executeRemove(removeOptions, context);
@@ -29506,21 +30534,28 @@ const VALID_SEARCH_SORT_FIELDS = /* @__PURE__ */ new Set([
29506
30534
  "rel"
29507
30535
  ]);
29508
30536
  function getOutputFormat(options) {
30537
+ if (options.output) {
30538
+ if (options.output === "ids") return "ids-only";
30539
+ return options.output;
30540
+ }
29509
30541
  if (options.json) return "json";
29510
30542
  if (options.idsOnly) return "ids-only";
29511
- if (options.uuid) return "uuid";
30543
+ if (options.uuidOnly) return "uuid";
29512
30544
  if (options.bibtex) return "bibtex";
29513
30545
  return "pretty";
29514
30546
  }
29515
30547
  function validateOptions(options) {
29516
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
30548
+ const outputOptions = [options.json, options.idsOnly, options.uuidOnly, options.bibtex].filter(
29517
30549
  Boolean
29518
30550
  );
29519
30551
  if (outputOptions.length > 1) {
29520
30552
  throw new Error(
29521
- "Multiple output formats specified. Only one of --json, --ids-only, --uuid, --bibtex can be used."
30553
+ "Multiple output formats specified. Only one of --json, --ids-only, --uuid-only, --bibtex can be used."
29522
30554
  );
29523
30555
  }
30556
+ if (options.output && outputOptions.length > 0) {
30557
+ throw new Error("Cannot combine --output with convenience flags (--json, --ids-only, etc.)");
30558
+ }
29524
30559
  if (options.sort !== void 0) {
29525
30560
  const sortStr = String(options.sort);
29526
30561
  if (!VALID_SEARCH_SORT_FIELDS.has(sortStr)) {
@@ -29576,35 +30611,39 @@ function formatSearchOutput(result, options) {
29576
30611
  return lines.join("\n");
29577
30612
  }
29578
30613
  function validateInteractiveOptions(options) {
29579
- const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
29580
- Boolean
29581
- );
30614
+ const outputOptions = [
30615
+ options.output,
30616
+ options.json,
30617
+ options.idsOnly,
30618
+ options.uuidOnly,
30619
+ options.bibtex
30620
+ ].filter(Boolean);
29582
30621
  if (outputOptions.length > 0) {
29583
30622
  throw new Error(
29584
- "Interactive mode cannot be combined with output format options (--json, --ids-only, --uuid, --bibtex)"
30623
+ "TUI mode cannot be combined with output format options (--output, --json, --ids-only, --uuid-only, --bibtex)"
29585
30624
  );
29586
30625
  }
29587
30626
  }
29588
30627
  async function executeInteractiveSearch(options, context, config2) {
29589
30628
  validateInteractiveOptions(options);
29590
- const { checkTTY } = await import("./tty-CDBIQraQ.js");
30629
+ const { checkTTY } = await import("./tty-BMyaEOhX.js");
29591
30630
  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);
30631
+ const { runActionMenu } = await import("./action-menu-DvwR6nMj.js");
30632
+ const { search } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.y);
30633
+ const { tokenize } = await import("./file-watcher-B_WpVHSV.js").then((n) => n.x);
29595
30634
  checkTTY();
29596
30635
  const allReferences = await context.library.getAll();
29597
30636
  const searchFn = (query) => {
29598
30637
  const { tokens } = tokenize(query);
29599
30638
  return search(allReferences, tokens);
29600
30639
  };
29601
- const interactiveConfig = config2.cli.interactive;
30640
+ const tuiConfig = config2.cli.tui;
29602
30641
  const searchResult = await runSearchPrompt(
29603
30642
  allReferences,
29604
30643
  searchFn,
29605
30644
  {
29606
- limit: interactiveConfig.limit,
29607
- debounceMs: interactiveConfig.debounceMs
30645
+ limit: tuiConfig.limit,
30646
+ debounceMs: tuiConfig.debounceMs
29608
30647
  },
29609
30648
  options.query || ""
29610
30649
  );
@@ -29897,12 +30936,12 @@ function formatUpdateOutput(result, identifier) {
29897
30936
  return parts.join("\n");
29898
30937
  }
29899
30938
  async function executeInteractiveUpdate(context, config2) {
29900
- const { selectReferencesOrExit } = await import("./reference-select-DSVwE9iu.js");
30939
+ const { selectReferencesOrExit } = await import("./reference-select-B9w9CLa1.js");
29901
30940
  const allReferences = await context.library.getAll();
29902
30941
  const identifiers = await selectReferencesOrExit(
29903
30942
  allReferences,
29904
30943
  { multiSelect: false },
29905
- config2.cli.interactive
30944
+ config2.cli.tui
29906
30945
  );
29907
30946
  return identifiers[0];
29908
30947
  }
@@ -30007,8 +31046,13 @@ function collectSetOption(value, previous) {
30007
31046
  }
30008
31047
  const SEARCH_SORT_FIELDS = searchSortFieldSchema.options;
30009
31048
  const SORT_ORDERS = sortOrderSchema.options;
30010
- const CITATION_FORMATS = ["text", "html", "rtf"];
31049
+ const CITATION_OUTPUT_FORMATS = ["text", "html", "rtf"];
31050
+ const EXPORT_OUTPUT_FORMATS = ["json", "yaml", "bibtex"];
31051
+ const LIST_OUTPUT_FORMATS = ["pretty", "json", "bibtex", "ids", "uuid"];
31052
+ const MUTATION_OUTPUT_FORMATS = ["json", "text"];
31053
+ const CONFIG_OUTPUT_FORMATS = ["text", "json"];
30011
31054
  const LOG_LEVELS = ["silent", "info", "debug"];
31055
+ const ADD_INPUT_FORMATS = ["json", "bibtex", "ris", "pmid", "doi", "isbn", "auto"];
30012
31056
  const CONFIG_SECTIONS = [
30013
31057
  "backup",
30014
31058
  "citation",
@@ -30019,17 +31063,44 @@ const CONFIG_SECTIONS = [
30019
31063
  "server",
30020
31064
  "watch"
30021
31065
  ];
31066
+ const ATTACHMENT_ROLES = ["fulltext", "supplement", "notes", "draft"];
30022
31067
  const OPTION_VALUES = {
30023
31068
  "--sort": SEARCH_SORT_FIELDS,
30024
31069
  // search includes 'relevance'
30025
31070
  "--order": SORT_ORDERS,
30026
- "--format": CITATION_FORMATS,
30027
31071
  "--style": BUILTIN_STYLES,
30028
31072
  "--log-level": LOG_LEVELS,
30029
- "--section": CONFIG_SECTIONS
31073
+ "--section": CONFIG_SECTIONS,
31074
+ "--input": ADD_INPUT_FORMATS,
31075
+ "-i": ADD_INPUT_FORMATS,
31076
+ "--role": ATTACHMENT_ROLES,
31077
+ "-r": ATTACHMENT_ROLES
31078
+ };
31079
+ const OUTPUT_VALUES_BY_COMMAND = {
31080
+ cite: CITATION_OUTPUT_FORMATS,
31081
+ export: EXPORT_OUTPUT_FORMATS,
31082
+ list: LIST_OUTPUT_FORMATS,
31083
+ search: LIST_OUTPUT_FORMATS,
31084
+ add: MUTATION_OUTPUT_FORMATS,
31085
+ remove: MUTATION_OUTPUT_FORMATS,
31086
+ update: MUTATION_OUTPUT_FORMATS
31087
+ // config show uses CONFIG_OUTPUT_FORMATS, handled specially
30030
31088
  };
31089
+ function getOptionValuesForCommand(option, command, subcommand) {
31090
+ if (option === "--output" || option === "-o") {
31091
+ if (command === "config" && subcommand === "show") {
31092
+ return CONFIG_OUTPUT_FORMATS;
31093
+ }
31094
+ if (command && OUTPUT_VALUES_BY_COMMAND[command]) {
31095
+ return OUTPUT_VALUES_BY_COMMAND[command];
31096
+ }
31097
+ return CITATION_OUTPUT_FORMATS;
31098
+ }
31099
+ return OPTION_VALUES[option];
31100
+ }
30031
31101
  const ID_COMPLETION_COMMANDS = /* @__PURE__ */ new Set(["cite", "remove", "update"]);
30032
31102
  const ID_COMPLETION_FULLTEXT_SUBCOMMANDS = /* @__PURE__ */ new Set(["attach", "get", "detach", "open"]);
31103
+ const ID_COMPLETION_ATTACH_SUBCOMMANDS = /* @__PURE__ */ new Set(["open", "add", "list", "get", "detach", "sync"]);
30033
31104
  function toCompletionItems(values) {
30034
31105
  return values.map((name2) => ({ name: name2 }));
30035
31106
  }
@@ -30063,6 +31134,13 @@ function extractGlobalOptions(program) {
30063
31134
  function findSubcommand(program, name2) {
30064
31135
  return program.commands.find((cmd) => cmd.name() === name2);
30065
31136
  }
31137
+ function parseCommandContext(env) {
31138
+ const words = env.line.trim().split(/\s+/);
31139
+ const args = words.slice(1);
31140
+ const command = args[0];
31141
+ const subcommand = args.length >= 2 ? args[1] : void 0;
31142
+ return { command, subcommand };
31143
+ }
30066
31144
  function getCompletions(env, program) {
30067
31145
  const { line, prev, last } = env;
30068
31146
  const words = line.trim().split(/\s+/);
@@ -30074,7 +31152,8 @@ function getCompletions(env, program) {
30074
31152
  }
30075
31153
  const firstArg = args[0] ?? "";
30076
31154
  if (prev?.startsWith("-")) {
30077
- const optionValues = OPTION_VALUES[prev];
31155
+ const { command, subcommand } = parseCommandContext(env);
31156
+ const optionValues = getOptionValuesForCommand(prev, command, subcommand);
30078
31157
  if (optionValues) {
30079
31158
  return toCompletionItems(optionValues);
30080
31159
  }
@@ -30114,6 +31193,13 @@ function needsIdCompletion(env) {
30114
31193
  }
30115
31194
  return { needs: false };
30116
31195
  }
31196
+ if (command === "attach" && args.length >= 2) {
31197
+ const subcommand = args[1] ?? "";
31198
+ if (ID_COMPLETION_ATTACH_SUBCOMMANDS.has(subcommand)) {
31199
+ return { needs: true, command, subcommand };
31200
+ }
31201
+ return { needs: false };
31202
+ }
30117
31203
  if (ID_COMPLETION_COMMANDS.has(command)) {
30118
31204
  return { needs: true, command };
30119
31205
  }
@@ -30253,6 +31339,7 @@ function createProgram() {
30253
31339
  registerCiteCommand(program);
30254
31340
  registerServerCommand(program);
30255
31341
  registerFulltextCommand(program);
31342
+ registerAttachCommand(program);
30256
31343
  registerMcpCommand(program);
30257
31344
  registerConfigCommand(program);
30258
31345
  registerCompletionCommand(program);
@@ -30277,7 +31364,7 @@ async function handleListAction(options, program) {
30277
31364
  }
30278
31365
  }
30279
31366
  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) => {
31367
+ program.command("list").description("List all references in the library").option("-o, --output <format>", "Output format: pretty|json|bibtex|ids|uuid").option("--json", "Alias for --output json").option("--bibtex", "Alias for --output bibtex").option("--ids-only", "Alias for --output ids").option("--uuid-only", "Alias for --output uuid").option("--sort <field>", "Sort by field: created|updated|published|author|title").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (options) => {
30281
31368
  await handleListAction(options, program);
30282
31369
  });
30283
31370
  }
@@ -30306,7 +31393,7 @@ async function handleExportAction(ids, options, program) {
30306
31393
  }
30307
31394
  }
30308
31395
  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) => {
31396
+ program.command("export [ids...]").description("Export raw CSL-JSON for external tool integration").option("--uuid", "Interpret identifiers as UUIDs").option("--all", "Export all references").option("--search <query>", "Export references matching search query").option("-o, --output <format>", "Output format: json (default), yaml, bibtex").action(async (ids, options) => {
30310
31397
  await handleExportAction(ids, options, program);
30311
31398
  });
30312
31399
  }
@@ -30315,7 +31402,7 @@ async function handleSearchAction(query, options, program) {
30315
31402
  const globalOpts = program.opts();
30316
31403
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
30317
31404
  const context = await createExecutionContext(config2, Library.load);
30318
- if (options.interactive) {
31405
+ if (options.tui) {
30319
31406
  const result2 = await executeInteractiveSearch({ ...options, query }, context, config2);
30320
31407
  if (result2.output) {
30321
31408
  process.stdout.write(`${result2.output}
@@ -30337,9 +31424,9 @@ async function handleSearchAction(query, options, program) {
30337
31424
  }
30338
31425
  }
30339
31426
  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");
31427
+ program.command("search").description("Search references").argument("[query]", "Search query (required unless using --tui)").option("-t, --tui", "Enable TUI (interactive) search mode").option("-o, --output <format>", "Output format: pretty|json|bibtex|ids|uuid").option("--json", "Alias for --output json").option("--bibtex", "Alias for --output bibtex").option("--ids-only", "Alias for --output ids").option("--uuid-only", "Alias for --output uuid").option("--sort <field>", "Sort by field: created|updated|published|author|title|relevance").option("--order <order>", "Sort order: asc|desc").option("-n, --limit <n>", "Maximum number of results", Number.parseInt).option("--offset <n>", "Number of results to skip", Number.parseInt).action(async (query, options) => {
31428
+ if (!options.tui && !query) {
31429
+ process.stderr.write("Error: Search query is required unless using --tui\n");
30343
31430
  process.exit(1);
30344
31431
  }
30345
31432
  await handleSearchAction(query ?? "", options, program);
@@ -30350,8 +31437,8 @@ function buildAddOptions(inputs, options, config2, stdinContent) {
30350
31437
  inputs,
30351
31438
  force: options.force ?? false
30352
31439
  };
30353
- if (options.format !== void 0) {
30354
- addOptions.format = options.format;
31440
+ if (options.input !== void 0) {
31441
+ addOptions.format = options.input;
30355
31442
  }
30356
31443
  if (options.verbose !== void 0) {
30357
31444
  addOptions.verbose = options.verbose;
@@ -30419,11 +31506,7 @@ async function handleAddAction(inputs, options, program) {
30419
31506
  }
30420
31507
  }
30421
31508
  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) => {
31509
+ program.command("add").description("Add new reference(s) to the library").argument("[input...]", "File paths or identifiers (PMID/DOI/ISBN), or use stdin").option("-f, --force", "Skip duplicate detection").option("-i, --input <format>", "Input format: json|bibtex|ris|pmid|doi|isbn|auto", "auto").option("--verbose", "Show detailed error information").option("-o, --output <format>", "Output format: json|text", "text").option("--full", "Include full CSL-JSON data in JSON output").action(async (inputs, options) => {
30427
31510
  await handleAddAction(inputs, options, program);
30428
31511
  });
30429
31512
  }
@@ -30441,7 +31524,7 @@ function registerEditCommand(program) {
30441
31524
  program.command("edit").description("Edit references interactively using an external editor").argument(
30442
31525
  "[identifier...]",
30443
31526
  "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) => {
31527
+ ).option("--uuid", "Interpret identifiers as UUIDs").option("--format <format>", "Edit format: yaml (default), json").option("--editor <editor>", "Editor command (overrides $VISUAL/$EDITOR)").action(async (identifiers, options) => {
30445
31528
  await handleEditAction(identifiers, options, program.opts());
30446
31529
  });
30447
31530
  }
@@ -30449,7 +31532,7 @@ function registerCiteCommand(program) {
30449
31532
  program.command("cite").description("Generate formatted citations for references").argument(
30450
31533
  "[id-or-uuid...]",
30451
31534
  "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) => {
31535
+ ).option("--uuid", "Treat arguments as UUIDs instead of IDs").option("--style <style>", "CSL style name").option("--csl-file <path>", "Path to custom CSL file").option("--locale <locale>", "Locale code (e.g., en-US, ja-JP)").option("-o, --output <format>", "Output format: text|html|rtf").option("--in-text", "Generate in-text citations instead of bibliography entries").action(async (identifiers, options) => {
30453
31536
  await handleCiteAction(identifiers, options, program.opts());
30454
31537
  });
30455
31538
  }
@@ -30554,6 +31637,30 @@ function registerMcpCommand(program) {
30554
31637
  }
30555
31638
  });
30556
31639
  }
31640
+ function registerAttachCommand(program) {
31641
+ const attachCmd = program.command("attach").description("Manage file attachments for references");
31642
+ attachCmd.command("open").description("Open attachments directory or specific file").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to open").option("--role <role>", "Open file by role").option("--print", "Output path instead of opening").option("--no-sync", "Skip interactive sync prompt").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
31643
+ await handleAttachOpenAction(identifier, filename, options, program.opts());
31644
+ });
31645
+ attachCmd.command("add").description("Add a file attachment to a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("<file-path>", "Path to the file to attach").requiredOption(
31646
+ "--role <role>",
31647
+ "Role for the file (fulltext, supplement, notes, draft, or custom)"
31648
+ ).option("--label <label>", "Human-readable label").option("--move", "Move file instead of copy").option("-f, --force", "Overwrite existing attachment").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filePath, options) => {
31649
+ await handleAttachAddAction(identifier, filePath, options, program.opts());
31650
+ });
31651
+ attachCmd.command("list").description("List attachments for a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--role <role>", "Filter by role").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
31652
+ await handleAttachListAction(identifier, options, program.opts());
31653
+ });
31654
+ attachCmd.command("get").description("Get attachment file path or content").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to get").option("--role <role>", "Get file by role").option("--stdout", "Output file content to stdout").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
31655
+ await handleAttachGetAction(identifier, filename, options, program.opts());
31656
+ });
31657
+ attachCmd.command("detach").description("Detach file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[filename]", "Specific file to detach").option("--role <role>", "Detach files by role").option("--all", "Detach all files of specified role").option("--remove-files", "Also delete files from disk").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filename, options) => {
31658
+ await handleAttachDetachAction(identifier, filename, options, program.opts());
31659
+ });
31660
+ attachCmd.command("sync").description("Synchronize metadata with files on disk").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--yes", "Apply changes (add new files)").option("--fix", "Remove missing files from metadata").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
31661
+ await handleAttachSyncAction(identifier, options, program.opts());
31662
+ });
31663
+ }
30557
31664
  function registerFulltextCommand(program) {
30558
31665
  const fulltextCmd = program.command("fulltext").description("Manage full-text files attached to references");
30559
31666
  fulltextCmd.command("attach").description("Attach a full-text file to a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").argument("[file-path]", "Path to the file to attach").option("--pdf [path]", "Attach as PDF (path optional if provided as argument)").option("--markdown [path]", "Attach as Markdown (path optional if provided as argument)").option("--move", "Move file instead of copy").option("-f, --force", "Overwrite existing attachment").option("--uuid", "Interpret identifier as UUID").action(async (identifier, filePath, options) => {
@@ -30562,7 +31669,7 @@ function registerFulltextCommand(program) {
30562
31669
  fulltextCmd.command("get").description("Get full-text file path or content").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Get PDF file only").option("--markdown", "Get Markdown file only").option("--stdout", "Output file content to stdout").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
30563
31670
  await handleFulltextGetAction(identifier, options, program.opts());
30564
31671
  });
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) => {
31672
+ fulltextCmd.command("detach").description("Detach full-text file from a reference").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Detach PDF only").option("--markdown", "Detach Markdown only").option("--remove-files", "Also delete the file from disk").option("-f, --force", "Skip confirmation for file removal").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
30566
31673
  await handleFulltextDetachAction(identifier, options, program.opts());
30567
31674
  });
30568
31675
  fulltextCmd.command("open").description("Open full-text file with system default application").argument("[identifier]", "Citation key or UUID (interactive selection if omitted)").option("--pdf", "Open PDF file").option("--markdown", "Open Markdown file").option("--uuid", "Interpret identifier as UUID").action(async (identifier, options) => {
@@ -30584,8 +31691,14 @@ async function main(argv) {
30584
31691
  await program.parseAsync(argv);
30585
31692
  }
30586
31693
  export {
31694
+ addAttachment as a,
30587
31695
  createProgram as c,
31696
+ detachAttachment as d,
30588
31697
  formatBibtex as f,
30589
- main as m
31698
+ getAttachment as g,
31699
+ listAttachments as l,
31700
+ main as m,
31701
+ openAttachment as o,
31702
+ syncAttachments as s
30590
31703
  };
30591
- //# sourceMappingURL=index-CXoDLO8W.js.map
31704
+ //# sourceMappingURL=index-Bv5IgsL-.js.map