@ncukondo/reference-manager 0.6.0 → 0.7.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 (39) hide show
  1. package/README.md +36 -0
  2. package/dist/chunks/action-menu-CTtINmWd.js +118 -0
  3. package/dist/chunks/action-menu-CTtINmWd.js.map +1 -0
  4. package/dist/chunks/{file-watcher-Cwfnnw92.js → file-watcher-D7oyc-9z.js} +14 -2
  5. package/dist/chunks/{file-watcher-Cwfnnw92.js.map → file-watcher-D7oyc-9z.js.map} +1 -1
  6. package/dist/chunks/{index-CQO8hLYm.js → index-_7NEUoS7.js} +13 -9
  7. package/dist/chunks/index-_7NEUoS7.js.map +1 -0
  8. package/dist/chunks/{loader-B_ZLxCQW.js → loader-BItrdVWG.js} +59 -7
  9. package/dist/chunks/loader-BItrdVWG.js.map +1 -0
  10. package/dist/chunks/search-prompt-D67WyKrY.js +179 -0
  11. package/dist/chunks/search-prompt-D67WyKrY.js.map +1 -0
  12. package/dist/chunks/tty-CDBIQraQ.js +17 -0
  13. package/dist/chunks/tty-CDBIQraQ.js.map +1 -0
  14. package/dist/cli/commands/search.d.ts +19 -0
  15. package/dist/cli/commands/search.d.ts.map +1 -1
  16. package/dist/cli/index.d.ts.map +1 -1
  17. package/dist/cli.js +64 -10
  18. package/dist/cli.js.map +1 -1
  19. package/dist/config/defaults.d.ts.map +1 -1
  20. package/dist/config/loader.d.ts.map +1 -1
  21. package/dist/config/schema.d.ts +24 -1
  22. package/dist/config/schema.d.ts.map +1 -1
  23. package/dist/features/import/importer.d.ts.map +1 -1
  24. package/dist/features/interactive/action-menu.d.ts +56 -0
  25. package/dist/features/interactive/action-menu.d.ts.map +1 -0
  26. package/dist/features/interactive/debounce.d.ts +22 -0
  27. package/dist/features/interactive/debounce.d.ts.map +1 -0
  28. package/dist/features/interactive/format.d.ts +52 -0
  29. package/dist/features/interactive/format.d.ts.map +1 -0
  30. package/dist/features/interactive/search-prompt.d.ts +47 -0
  31. package/dist/features/interactive/search-prompt.d.ts.map +1 -0
  32. package/dist/features/interactive/tty.d.ts +20 -0
  33. package/dist/features/interactive/tty.d.ts.map +1 -0
  34. package/dist/features/operations/add.d.ts.map +1 -1
  35. package/dist/index.js +3 -3
  36. package/dist/server.js +2 -2
  37. package/package.json +2 -1
  38. package/dist/chunks/index-CQO8hLYm.js.map +0 -1
  39. package/dist/chunks/loader-B_ZLxCQW.js.map +0 -1
@@ -0,0 +1,179 @@
1
+ function formatSingleAuthor(author) {
2
+ if (author.literal) {
3
+ return author.literal;
4
+ }
5
+ if (author.family) {
6
+ if (author.given) {
7
+ const initial = author.given.charAt(0).toUpperCase();
8
+ return `${author.family}, ${initial}.`;
9
+ }
10
+ return author.family;
11
+ }
12
+ return "";
13
+ }
14
+ function formatAuthors(authors) {
15
+ if (!authors || authors.length === 0) {
16
+ return "";
17
+ }
18
+ if (authors.length > 3) {
19
+ const first = authors[0];
20
+ if (!first) {
21
+ return "";
22
+ }
23
+ return `${formatSingleAuthor(first)}, et al.`;
24
+ }
25
+ const formatted = authors.map(formatSingleAuthor);
26
+ if (formatted.length === 1) {
27
+ return formatted[0] ?? "";
28
+ }
29
+ const allButLast = formatted.slice(0, -1).join(", ");
30
+ const last = formatted[formatted.length - 1] ?? "";
31
+ return `${allButLast}, & ${last}`;
32
+ }
33
+ function formatTitle(title, maxWidth) {
34
+ if (!title) {
35
+ return "";
36
+ }
37
+ if (title.length <= maxWidth) {
38
+ return title;
39
+ }
40
+ return `${title.slice(0, maxWidth - 3)}...`;
41
+ }
42
+ function formatIdentifiers(item) {
43
+ const identifiers = [];
44
+ if (item.DOI) {
45
+ identifiers.push(`DOI: ${item.DOI}`);
46
+ }
47
+ if (item.PMID) {
48
+ identifiers.push(`PMID: ${item.PMID}`);
49
+ }
50
+ if (item.PMCID) {
51
+ identifiers.push(`PMCID: ${item.PMCID}`);
52
+ }
53
+ if (item.ISBN) {
54
+ identifiers.push(`ISBN: ${item.ISBN}`);
55
+ }
56
+ return identifiers.join(" | ");
57
+ }
58
+ function extractYear(item) {
59
+ const dateParts = item.issued?.["date-parts"];
60
+ if (!dateParts || dateParts.length === 0) {
61
+ return void 0;
62
+ }
63
+ const firstDatePart = dateParts[0];
64
+ if (!firstDatePart || firstDatePart.length === 0) {
65
+ return void 0;
66
+ }
67
+ return firstDatePart[0];
68
+ }
69
+ function formatSearchResult(item, index, terminalWidth) {
70
+ const lines = [];
71
+ const authors = formatAuthors(item.author);
72
+ const year = extractYear(item);
73
+ const yearPart = year !== void 0 ? ` (${year})` : "";
74
+ const line1 = `[${index}] ${authors}${yearPart}`;
75
+ lines.push(line1);
76
+ const indent = " ";
77
+ const titleMaxWidth = terminalWidth - indent.length;
78
+ const title = formatTitle(item.title, titleMaxWidth);
79
+ if (title) {
80
+ lines.push(`${indent}${title}`);
81
+ }
82
+ const identifiers = formatIdentifiers(item);
83
+ if (identifiers) {
84
+ lines.push(`${indent}${identifiers}`);
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+ function createChoices(results, terminalWidth) {
89
+ return results.map((result, index) => {
90
+ const displayIndex = index + 1;
91
+ const formattedText = formatSearchResult(result.reference, displayIndex, terminalWidth);
92
+ return {
93
+ name: JSON.stringify({
94
+ index,
95
+ item: result.reference
96
+ }),
97
+ message: formattedText,
98
+ value: result.reference.id
99
+ };
100
+ });
101
+ }
102
+ function parseSelectedValues(values) {
103
+ const valueArray = Array.isArray(values) ? values : [values];
104
+ const items = [];
105
+ for (const value of valueArray) {
106
+ if (!value) continue;
107
+ try {
108
+ const data = JSON.parse(value);
109
+ items.push(data.item);
110
+ } catch {
111
+ }
112
+ }
113
+ return items;
114
+ }
115
+ function getTerminalWidth() {
116
+ return process.stdout.columns ?? 80;
117
+ }
118
+ async function runSearchPrompt(allReferences, searchFn, config, initialQuery = "") {
119
+ const enquirer = await import("enquirer");
120
+ const AutoComplete = enquirer.default.AutoComplete;
121
+ const terminalWidth = getTerminalWidth();
122
+ const initialResults = initialQuery ? searchFn(initialQuery).slice(0, config.limit) : allReferences.slice(0, config.limit).map((ref) => ({
123
+ reference: ref,
124
+ overallStrength: "exact",
125
+ tokenMatches: [],
126
+ score: 0
127
+ }));
128
+ const initialChoices = createChoices(initialResults, terminalWidth);
129
+ let lastQuery = initialQuery;
130
+ const promptOptions = {
131
+ name: "references",
132
+ message: "Search references",
133
+ initial: initialQuery,
134
+ choices: initialChoices,
135
+ multiple: true,
136
+ limit: config.limit,
137
+ suggest: (input, choices) => {
138
+ if (input === lastQuery) {
139
+ return choices;
140
+ }
141
+ lastQuery = input;
142
+ if (!input.trim()) {
143
+ const defaultResults = allReferences.slice(0, config.limit).map((ref) => ({
144
+ reference: ref,
145
+ overallStrength: "exact",
146
+ tokenMatches: [],
147
+ score: 0
148
+ }));
149
+ return createChoices(defaultResults, terminalWidth);
150
+ }
151
+ const results = searchFn(input).slice(0, config.limit);
152
+ return createChoices(results, terminalWidth);
153
+ }
154
+ };
155
+ try {
156
+ const prompt = new AutoComplete(promptOptions);
157
+ const result = await prompt.run();
158
+ const selected = parseSelectedValues(result);
159
+ return {
160
+ selected,
161
+ cancelled: false
162
+ };
163
+ } catch (error) {
164
+ if (error === "" || error instanceof Error && error.message === "") {
165
+ return {
166
+ selected: [],
167
+ cancelled: true
168
+ };
169
+ }
170
+ throw error;
171
+ }
172
+ }
173
+ export {
174
+ createChoices,
175
+ getTerminalWidth,
176
+ parseSelectedValues,
177
+ runSearchPrompt
178
+ };
179
+ //# sourceMappingURL=search-prompt-D67WyKrY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-prompt-D67WyKrY.js","sources":["../../src/features/interactive/format.ts","../../src/features/interactive/search-prompt.ts"],"sourcesContent":["/**\n * Display format functions for interactive search\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\n\n/**\n * CSL name type (author structure)\n */\ntype CslName = NonNullable<CslItem[\"author\"]>[number];\n\n/**\n * Format a single author name\n * - Personal: \"Smith, J.\" (family + initial of given)\n * - Institutional: \"World Health Organization\" (literal)\n */\nfunction formatSingleAuthor(author: CslName): string {\n if (author.literal) {\n return author.literal;\n }\n if (author.family) {\n if (author.given) {\n const initial = author.given.charAt(0).toUpperCase();\n return `${author.family}, ${initial}.`;\n }\n return author.family;\n }\n return \"\";\n}\n\n/**\n * Format author list for display\n * - Single author: \"Smith, J.\"\n * - Two authors: \"Smith, J., & Doe, A.\"\n * - Three authors: \"Smith, J., Doe, A., & Johnson, B.\"\n * - More than three: \"Smith, J., et al.\"\n *\n * @param authors - Array of CSL author objects\n * @returns Formatted author string\n */\nexport function formatAuthors(authors: CslName[] | undefined): string {\n if (!authors || authors.length === 0) {\n return \"\";\n }\n\n if (authors.length > 3) {\n const first = authors[0];\n if (!first) {\n return \"\";\n }\n return `${formatSingleAuthor(first)}, et al.`;\n }\n\n const formatted = authors.map(formatSingleAuthor);\n if (formatted.length === 1) {\n return formatted[0] ?? \"\";\n }\n\n // Join all but last with \", \" and append \"& \" + last\n const allButLast = formatted.slice(0, -1).join(\", \");\n const last = formatted[formatted.length - 1] ?? \"\";\n return `${allButLast}, & ${last}`;\n}\n\n/**\n * Truncate title to fit terminal width\n *\n * @param title - Title string\n * @param maxWidth - Maximum display width\n * @returns Truncated title with ellipsis if needed\n */\nexport function formatTitle(title: string | undefined, maxWidth: number): string {\n if (!title) {\n return \"\";\n }\n\n if (title.length <= maxWidth) {\n return title;\n }\n\n // Truncate and add ellipsis, keeping total length at maxWidth\n return `${title.slice(0, maxWidth - 3)}...`;\n}\n\n/**\n * Format identifiers (DOI, PMID, PMCID, ISBN) for display\n *\n * @param item - CSL item\n * @returns Formatted identifier string (e.g., \"DOI: 10.1000/example | PMID: 12345678\")\n */\nexport function formatIdentifiers(item: CslItem): string {\n const identifiers: string[] = [];\n\n if (item.DOI) {\n identifiers.push(`DOI: ${item.DOI}`);\n }\n if (item.PMID) {\n identifiers.push(`PMID: ${item.PMID}`);\n }\n if (item.PMCID) {\n identifiers.push(`PMCID: ${item.PMCID}`);\n }\n if (item.ISBN) {\n identifiers.push(`ISBN: ${item.ISBN}`);\n }\n\n return identifiers.join(\" | \");\n}\n\n/**\n * Extract year from CSL item\n */\nfunction extractYear(item: CslItem): number | undefined {\n const dateParts = item.issued?.[\"date-parts\"];\n if (!dateParts || dateParts.length === 0) {\n return undefined;\n }\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) {\n return undefined;\n }\n return firstDatePart[0];\n}\n\n/**\n * Compose a complete search result line\n *\n * Format:\n * ```\n * [1] Smith, J., & Doe, A. (2020)\n * Machine learning in medicine: A comprehensive review\n * DOI: 10.1000/example | PMID: 12345678\n * ```\n *\n * @param item - CSL item\n * @param index - Display index (1-based)\n * @param terminalWidth - Terminal width for title truncation\n * @returns Multi-line formatted string\n */\nexport function formatSearchResult(item: CslItem, index: number, terminalWidth: number): string {\n const lines: string[] = [];\n\n // Line 1: [index] Authors (year)\n const authors = formatAuthors(item.author);\n const year = extractYear(item);\n const yearPart = year !== undefined ? ` (${year})` : \"\";\n const line1 = `[${index}] ${authors}${yearPart}`;\n lines.push(line1);\n\n // Line 2: Title (indented, truncated)\n const indent = \" \";\n const titleMaxWidth = terminalWidth - indent.length;\n const title = formatTitle(item.title, titleMaxWidth);\n if (title) {\n lines.push(`${indent}${title}`);\n }\n\n // Line 3: Identifiers (indented)\n const identifiers = formatIdentifiers(item);\n if (identifiers) {\n lines.push(`${indent}${identifiers}`);\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Interactive search prompt using Enquirer's AutoComplete\n *\n * Provides real-time incremental search with multiple selection support.\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport type { SearchResult } from \"../search/types.js\";\nimport type { AutoCompleteChoice } from \"./enquirer.js\";\nimport { formatSearchResult } from \"./format.js\";\n\n/**\n * Configuration for the search prompt\n */\nexport interface SearchPromptConfig {\n /** Maximum number of results to display */\n limit: number;\n /** Debounce delay in milliseconds */\n debounceMs: number;\n}\n\n/**\n * Search function type for filtering references\n */\nexport type SearchFunction = (query: string) => SearchResult[];\n\n/**\n * Result from the search prompt\n */\nexport interface SearchPromptResult {\n /** Selected references */\n selected: CslItem[];\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Maps internal choice value to CslItem\n */\ninterface ChoiceData {\n index: number;\n item: CslItem;\n}\n\n/**\n * Creates choices from search results\n */\nexport function createChoices(\n results: SearchResult[],\n terminalWidth: number\n): AutoCompleteChoice[] {\n return results.map((result, index) => {\n const displayIndex = index + 1;\n const formattedText = formatSearchResult(result.reference, displayIndex, terminalWidth);\n\n // Enquirer returns the 'name' property on selection, not 'value'\n // So we store the JSON data in 'name' and use 'message' for display\n return {\n name: JSON.stringify({\n index,\n item: result.reference,\n } satisfies ChoiceData),\n message: formattedText,\n value: result.reference.id,\n };\n });\n}\n\n/**\n * Parses selected values back to CslItems\n */\nexport function parseSelectedValues(values: string | string[]): CslItem[] {\n const valueArray = Array.isArray(values) ? values : [values];\n const items: CslItem[] = [];\n\n for (const value of valueArray) {\n if (!value) continue;\n try {\n const data = JSON.parse(value) as ChoiceData;\n items.push(data.item);\n } catch {\n // If parsing fails, the value might be just the id (name)\n // In this case, we can't recover the full item\n // This shouldn't happen in normal operation\n }\n }\n\n return items;\n}\n\n/**\n * Gets terminal width, falling back to 80 if not available\n */\nexport function getTerminalWidth(): number {\n return process.stdout.columns ?? 80;\n}\n\n/**\n * Creates and runs an interactive search prompt\n */\nexport async function runSearchPrompt(\n allReferences: CslItem[],\n searchFn: SearchFunction,\n config: SearchPromptConfig,\n initialQuery = \"\"\n): Promise<SearchPromptResult> {\n // Dynamic import to allow mocking in tests\n // enquirer is a CommonJS module, so we must use default import\n const enquirer = await import(\"enquirer\");\n const AutoComplete = (enquirer.default as unknown as Record<string, unknown>)\n .AutoComplete as new (\n options: Record<string, unknown>\n ) => {\n run(): Promise<string | string[]>;\n };\n\n const terminalWidth = getTerminalWidth();\n\n // Create initial choices from all references (limited)\n const initialResults: SearchResult[] = initialQuery\n ? searchFn(initialQuery).slice(0, config.limit)\n : allReferences.slice(0, config.limit).map((ref) => ({\n reference: ref,\n overallStrength: \"exact\" as const,\n tokenMatches: [],\n score: 0,\n }));\n\n const initialChoices = createChoices(initialResults, terminalWidth);\n\n // Track last search query to avoid redundant searches\n let lastQuery = initialQuery;\n\n const promptOptions = {\n name: \"references\",\n message: \"Search references\",\n initial: initialQuery,\n choices: initialChoices,\n multiple: true,\n limit: config.limit,\n suggest: (input: string, choices: AutoCompleteChoice[]) => {\n // If input hasn't changed, return current choices\n if (input === lastQuery) {\n return choices;\n }\n lastQuery = input;\n\n // If input is empty, show all references (limited)\n if (!input.trim()) {\n const defaultResults: SearchResult[] = allReferences.slice(0, config.limit).map((ref) => ({\n reference: ref,\n overallStrength: \"exact\" as const,\n tokenMatches: [],\n score: 0,\n }));\n return createChoices(defaultResults, terminalWidth);\n }\n\n // Search and create new choices\n const results = searchFn(input).slice(0, config.limit);\n return createChoices(results, terminalWidth);\n },\n };\n\n try {\n const prompt = new AutoComplete(promptOptions);\n const result = await prompt.run();\n\n // Handle result\n const selected = parseSelectedValues(result);\n\n return {\n selected,\n cancelled: false,\n };\n } catch (error) {\n // Enquirer throws an empty string when cancelled\n if (error === \"\" || (error instanceof Error && error.message === \"\")) {\n return {\n selected: [],\n cancelled: true,\n };\n }\n throw error;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ class TTYError extends Error {
2
+ exitCode = 1;
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "TTYError";
6
+ }
7
+ }
8
+ function checkTTY() {
9
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
10
+ throw new TTYError("Interactive mode requires a TTY");
11
+ }
12
+ }
13
+ export {
14
+ TTYError,
15
+ checkTTY
16
+ };
17
+ //# sourceMappingURL=tty-CDBIQraQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tty-CDBIQraQ.js","sources":["../../src/features/interactive/tty.ts"],"sourcesContent":["/**\n * TTY detection for interactive mode\n */\n\n/**\n * Error thrown when TTY is required but not available\n */\nexport class TTYError extends Error {\n readonly exitCode = 1;\n\n constructor(message: string) {\n super(message);\n this.name = \"TTYError\";\n }\n}\n\n/**\n * Check if the current environment is a TTY\n *\n * Throws TTYError if stdin or stdout is not a TTY,\n * which means interactive mode cannot be used.\n *\n * @throws {TTYError} If not running in a TTY\n */\nexport function checkTTY(): void {\n if (!process.stdin.isTTY || !process.stdout.isTTY) {\n throw new TTYError(\"Interactive mode requires a TTY\");\n }\n}\n"],"names":[],"mappings":"AAOO,MAAM,iBAAiB,MAAM;AAAA,EACzB,WAAW;AAAA,EAEpB,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAUO,SAAS,WAAiB;AAC/B,MAAI,CAAC,QAAQ,MAAM,SAAS,CAAC,QAAQ,OAAO,OAAO;AACjD,UAAM,IAAI,SAAS,iCAAiC;AAAA,EACtD;AACF;"}
@@ -1,3 +1,4 @@
1
+ import type { Config } from "../../config/schema.js";
1
2
  import type { SearchResult } from "../../features/operations/search.js";
2
3
  import { type SearchSortField, type SortOrder } from "../../features/pagination/index.js";
3
4
  import type { ExecutionContext } from "../execution-context.js";
@@ -14,6 +15,7 @@ export interface SearchCommandOptions {
14
15
  order?: SortOrder;
15
16
  limit?: number;
16
17
  offset?: number;
18
+ interactive?: boolean;
17
19
  }
18
20
  /**
19
21
  * Result from search command execution.
@@ -36,4 +38,21 @@ export declare function executeSearch(options: SearchCommandOptions, context: Ex
36
38
  * @returns Formatted output string
37
39
  */
38
40
  export declare function formatSearchOutput(result: SearchCommandResult, isJson?: boolean): string;
41
+ /**
42
+ * Result from interactive search command execution.
43
+ */
44
+ export interface InteractiveSearchResult {
45
+ output: string;
46
+ cancelled: boolean;
47
+ }
48
+ /**
49
+ * Execute interactive search command.
50
+ * Uses runSearchPrompt and runActionMenu from the interactive module.
51
+ *
52
+ * @param options - Search command options with interactive flag
53
+ * @param context - Execution context
54
+ * @param config - Application configuration
55
+ * @returns Interactive search result containing output and cancelled flag
56
+ */
57
+ export declare function executeInteractiveSearch(options: SearchCommandOptions, context: ExecutionContext, config: Config): Promise<InteractiveSearchResult>;
39
58
  //# sourceMappingURL=search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACtF,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,SAAS,EAIf,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAgBhE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC;AAwD/C;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,mBAAmB,CAAC,CAa9B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,UAAQ,GAAG,MAAM,CA2BtF"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACtF,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,SAAS,EAIf,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAgBhE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC;AAwD/C;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,mBAAmB,CAAC,CAa9B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,mBAAmB,EAAE,MAAM,UAAQ,GAAG,MAAM,CA2BtF;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAkBD;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,uBAAuB,CAAC,CA+ClC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqDpC;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA+BvC;AA+wBD;;GAEG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBxD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0DpC;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA+BvC;AAgyBD;;GAEG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBxD"}
package/dist/cli.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { Command } from "commander";
2
2
  import { ZodOptional as ZodOptional$2, z } from "zod";
3
- import { p as pickDefined, q as sortOrderSchema, r as paginationOptionsSchema, L as Library, F as FileWatcher, u as sortFieldSchema, v as searchSortFieldSchema } from "./chunks/file-watcher-Cwfnnw92.js";
3
+ import { p as pickDefined, q as sortOrderSchema, r as paginationOptionsSchema, L as Library, F as FileWatcher, u as sortFieldSchema, v as searchSortFieldSchema } from "./chunks/file-watcher-D7oyc-9z.js";
4
4
  import { promises, existsSync, mkdtempSync, writeFileSync, readFileSync } 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
8
  import { join, extname } from "node:path";
9
9
  import { mkdir, unlink, rename, copyFile, rm, readFile } from "node:fs/promises";
10
- import { u as updateReference, B as BUILTIN_STYLES, s as startServerWithFileWatcher } from "./chunks/index-CQO8hLYm.js";
10
+ import { u as updateReference, B as BUILTIN_STYLES, s as startServerWithFileWatcher } from "./chunks/index-_7NEUoS7.js";
11
11
  import process$1, { stdin, stdout } from "node:process";
12
- import { l as loadConfig } from "./chunks/loader-B_ZLxCQW.js";
12
+ import { l as loadConfig } from "./chunks/loader-BItrdVWG.js";
13
13
  import { spawn } from "node:child_process";
14
14
  import { serve } from "@hono/node-server";
15
15
  const name = "@ncukondo/reference-manager";
16
- const version$1 = "0.6.0";
16
+ const version$1 = "0.7.0";
17
17
  const description$1 = "A local reference management tool using CSL-JSON as the single source of truth";
18
18
  const packageJson = {
19
19
  name,
@@ -20959,19 +20959,19 @@ class OperationsLibrary {
20959
20959
  }
20960
20960
  // High-level operations
20961
20961
  async search(options) {
20962
- const { searchReferences } = await import("./chunks/index-CQO8hLYm.js").then((n) => n.d);
20962
+ const { searchReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.e);
20963
20963
  return searchReferences(this.library, options);
20964
20964
  }
20965
20965
  async list(options) {
20966
- const { listReferences } = await import("./chunks/index-CQO8hLYm.js").then((n) => n.l);
20966
+ const { listReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.l);
20967
20967
  return listReferences(this.library, options ?? {});
20968
20968
  }
20969
20969
  async cite(options) {
20970
- const { citeReferences } = await import("./chunks/index-CQO8hLYm.js").then((n) => n.b);
20970
+ const { citeReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.d);
20971
20971
  return citeReferences(this.library, options);
20972
20972
  }
20973
20973
  async import(inputs, options) {
20974
- const { addReferences } = await import("./chunks/index-CQO8hLYm.js").then((n) => n.a);
20974
+ const { addReferences } = await import("./chunks/index-_7NEUoS7.js").then((n) => n.b);
20975
20975
  return addReferences(inputs, this.library, options ?? {});
20976
20976
  }
20977
20977
  }
@@ -21614,6 +21614,48 @@ function formatSearchOutput(result, isJson = false) {
21614
21614
  lines.push(...result.items);
21615
21615
  return lines.join("\n");
21616
21616
  }
21617
+ function validateInteractiveOptions(options) {
21618
+ const outputOptions = [options.json, options.idsOnly, options.uuid, options.bibtex].filter(
21619
+ Boolean
21620
+ );
21621
+ if (outputOptions.length > 0) {
21622
+ throw new Error(
21623
+ "Interactive mode cannot be combined with output format options (--json, --ids-only, --uuid, --bibtex)"
21624
+ );
21625
+ }
21626
+ }
21627
+ async function executeInteractiveSearch(options, context, config2) {
21628
+ validateInteractiveOptions(options);
21629
+ const { checkTTY } = await import("./chunks/tty-CDBIQraQ.js");
21630
+ const { runSearchPrompt } = await import("./chunks/search-prompt-D67WyKrY.js");
21631
+ const { runActionMenu } = await import("./chunks/action-menu-CTtINmWd.js");
21632
+ const { search } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.y);
21633
+ const { tokenize } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.x);
21634
+ checkTTY();
21635
+ const allReferences = await context.library.getAll();
21636
+ const searchFn = (query) => {
21637
+ const { tokens } = tokenize(query);
21638
+ return search(allReferences, tokens);
21639
+ };
21640
+ const interactiveConfig = config2.cli.interactive;
21641
+ const searchResult = await runSearchPrompt(
21642
+ allReferences,
21643
+ searchFn,
21644
+ {
21645
+ limit: interactiveConfig.limit,
21646
+ debounceMs: interactiveConfig.debounceMs
21647
+ },
21648
+ options.query || ""
21649
+ );
21650
+ if (searchResult.cancelled || searchResult.selected.length === 0) {
21651
+ return { output: "", cancelled: true };
21652
+ }
21653
+ const actionResult = await runActionMenu(searchResult.selected);
21654
+ return {
21655
+ output: actionResult.output,
21656
+ cancelled: actionResult.cancelled
21657
+ };
21658
+ }
21617
21659
  async function serverStart(options) {
21618
21660
  const existingStatus = await serverStatus(options.portfilePath);
21619
21661
  if (existingStatus !== null) {
@@ -22285,6 +22327,14 @@ async function handleSearchAction(query, options, program) {
22285
22327
  const globalOpts = program.opts();
22286
22328
  const config2 = await loadConfigWithOverrides({ ...globalOpts, ...options });
22287
22329
  const context = await createExecutionContext(config2, Library.load);
22330
+ if (options.interactive) {
22331
+ const result2 = await executeInteractiveSearch({ ...options, query }, context, config2);
22332
+ if (result2.output) {
22333
+ process.stdout.write(`${result2.output}
22334
+ `);
22335
+ }
22336
+ process.exit(result2.cancelled ? 0 : 0);
22337
+ }
22288
22338
  const result = await executeSearch({ ...options, query }, context);
22289
22339
  const output = formatSearchOutput(result, options.json ?? false);
22290
22340
  if (output) {
@@ -22299,8 +22349,12 @@ async function handleSearchAction(query, options, program) {
22299
22349
  }
22300
22350
  }
22301
22351
  function registerSearchCommand(program) {
22302
- program.command("search").description("Search references").argument("<query>", "Search query").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) => {
22303
- await handleSearchAction(query, options, program);
22352
+ 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) => {
22353
+ if (!options.interactive && !query) {
22354
+ process.stderr.write("Error: Search query is required unless using --interactive\n");
22355
+ process.exit(1);
22356
+ }
22357
+ await handleSearchAction(query ?? "", options, program);
22304
22358
  });
22305
22359
  }
22306
22360
  async function handleAddAction(inputs, options, program) {