@ncukondo/reference-manager 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,8 +94,7 @@ function createChoices(results, terminalWidth) {
94
94
  index,
95
95
  item: result.reference
96
96
  }),
97
- message: formattedText,
98
- value: result.reference.id
97
+ message: formattedText
99
98
  };
100
99
  });
101
100
  }
@@ -115,6 +114,30 @@ function parseSelectedValues(values) {
115
114
  function getTerminalWidth() {
116
115
  return process.stdout.columns ?? 80;
117
116
  }
117
+ function collectEnabledIds(choices) {
118
+ const enabledIds = /* @__PURE__ */ new Set();
119
+ for (const choice of choices) {
120
+ if (choice.enabled) {
121
+ try {
122
+ const data = JSON.parse(choice.name);
123
+ enabledIds.add(data.item.id);
124
+ } catch {
125
+ }
126
+ }
127
+ }
128
+ return enabledIds;
129
+ }
130
+ function restoreEnabledState(choices, enabledIds) {
131
+ for (const choice of choices) {
132
+ try {
133
+ const data = JSON.parse(choice.name);
134
+ if (enabledIds.has(data.item.id)) {
135
+ choice.enabled = true;
136
+ }
137
+ } catch {
138
+ }
139
+ }
140
+ }
118
141
  async function runSearchPrompt(allReferences, searchFn, config, initialQuery = "") {
119
142
  const enquirer = await import("enquirer");
120
143
  const AutoComplete = enquirer.default.AutoComplete;
@@ -139,23 +162,29 @@ async function runSearchPrompt(allReferences, searchFn, config, initialQuery = "
139
162
  return choices;
140
163
  }
141
164
  lastQuery = input;
142
- if (!input.trim()) {
143
- const defaultResults = allReferences.slice(0, config.limit).map((ref) => ({
165
+ const enabledIds = collectEnabledIds(choices);
166
+ const newChoices = input.trim() ? createChoices(searchFn(input).slice(0, config.limit), terminalWidth) : createChoices(
167
+ allReferences.slice(0, config.limit).map((ref) => ({
144
168
  reference: ref,
145
169
  overallStrength: "exact",
146
170
  tokenMatches: [],
147
171
  score: 0
148
- }));
149
- return createChoices(defaultResults, terminalWidth);
150
- }
151
- const results = searchFn(input).slice(0, config.limit);
152
- return createChoices(results, terminalWidth);
172
+ })),
173
+ terminalWidth
174
+ );
175
+ restoreEnabledState(newChoices, enabledIds);
176
+ return newChoices;
153
177
  }
154
178
  };
155
179
  try {
156
180
  const prompt = new AutoComplete(promptOptions);
157
181
  const result = await prompt.run();
158
- const selected = parseSelectedValues(result);
182
+ const promptAny = prompt;
183
+ let valuesToParse = result;
184
+ if (Array.isArray(result) && result.length === 0 && promptAny.focused?.enabled && promptAny.focused?.name) {
185
+ valuesToParse = [promptAny.focused.name];
186
+ }
187
+ const selected = parseSelectedValues(valuesToParse);
159
188
  return {
160
189
  selected,
161
190
  cancelled: false
@@ -176,4 +205,4 @@ export {
176
205
  parseSelectedValues,
177
206
  runSearchPrompt
178
207
  };
179
- //# sourceMappingURL=search-prompt-D67WyKrY.js.map
208
+ //# sourceMappingURL=search-prompt-RtHDJFgL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-prompt-RtHDJFgL.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 when 'value' is not defined\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 };\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 * Collects enabled (selected) item IDs from choices\n */\nfunction collectEnabledIds(choices: AutoCompleteChoice[]): Set<string> {\n const enabledIds = new Set<string>();\n for (const choice of choices) {\n if ((choice as { enabled?: boolean }).enabled) {\n try {\n const data = JSON.parse(choice.name) as ChoiceData;\n enabledIds.add(data.item.id);\n } catch {\n // Ignore parse errors\n }\n }\n }\n return enabledIds;\n}\n\n/**\n * Restores enabled state for choices matching the given IDs\n */\nfunction restoreEnabledState(choices: AutoCompleteChoice[], enabledIds: Set<string>): void {\n for (const choice of choices) {\n try {\n const data = JSON.parse(choice.name) as ChoiceData;\n if (enabledIds.has(data.item.id)) {\n (choice as { enabled?: boolean }).enabled = true;\n }\n } catch {\n // Ignore parse errors\n }\n }\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 // Collect enabled (selected) item IDs from current choices\n const enabledIds = collectEnabledIds(choices);\n\n // Create new choices based on input\n const newChoices = input.trim()\n ? createChoices(searchFn(input).slice(0, config.limit), terminalWidth)\n : createChoices(\n allReferences.slice(0, config.limit).map((ref) => ({\n reference: ref,\n overallStrength: \"exact\" as const,\n tokenMatches: [],\n score: 0,\n })),\n terminalWidth\n );\n\n // Restore enabled state for matching items\n restoreEnabledState(newChoices, enabledIds);\n\n return newChoices;\n },\n };\n\n try {\n const prompt = new AutoComplete(promptOptions);\n const result = await prompt.run();\n\n // Workaround for Enquirer bug: focused item with enabled=true may not be in result\n const promptAny = prompt as unknown as {\n focused?: { name: string; enabled?: boolean };\n selected?: Array<{ name: string }>;\n };\n\n let valuesToParse: string | string[] = result;\n\n // If result is empty but focused item is enabled, use focused item\n if (\n Array.isArray(result) &&\n result.length === 0 &&\n promptAny.focused?.enabled &&\n promptAny.focused?.name\n ) {\n valuesToParse = [promptAny.focused.name];\n }\n\n // Handle result\n const selected = parseSelectedValues(valuesToParse);\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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"}
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ 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.7.0";
16
+ const version$1 = "0.7.1";
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,
@@ -21627,7 +21627,7 @@ function validateInteractiveOptions(options) {
21627
21627
  async function executeInteractiveSearch(options, context, config2) {
21628
21628
  validateInteractiveOptions(options);
21629
21629
  const { checkTTY } = await import("./chunks/tty-CDBIQraQ.js");
21630
- const { runSearchPrompt } = await import("./chunks/search-prompt-D67WyKrY.js");
21630
+ const { runSearchPrompt } = await import("./chunks/search-prompt-RtHDJFgL.js");
21631
21631
  const { runActionMenu } = await import("./chunks/action-menu-CTtINmWd.js");
21632
21632
  const { search } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.y);
21633
21633
  const { tokenize } = await import("./chunks/file-watcher-D7oyc-9z.js").then((n) => n.x);
@@ -1 +1 @@
1
- {"version":3,"file":"search-prompt.d.ts","sourceRoot":"","sources":["../../../src/features/interactive/search-prompt.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;CACpB;AAUD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,MAAM,GACpB,kBAAkB,EAAE,CAgBtB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,EAAE,CAiBxE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,OAAO,EAAE,EACxB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,kBAAkB,EAC1B,YAAY,SAAK,GAChB,OAAO,CAAC,kBAAkB,CAAC,CAgF7B"}
1
+ {"version":3,"file":"search-prompt.d.ts","sourceRoot":"","sources":["../../../src/features/interactive/search-prompt.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAGxD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAE/D;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;CACpB;AAUD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,YAAY,EAAE,EACvB,aAAa,EAAE,MAAM,GACpB,kBAAkB,EAAE,CAetB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,EAAE,CAiBxE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAoCD;;GAEG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,OAAO,EAAE,EACxB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,kBAAkB,EAC1B,YAAY,SAAK,GAChB,OAAO,CAAC,kBAAkB,CAAC,CAwG7B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncukondo/reference-manager",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "A local reference management tool using CSL-JSON as the single source of truth",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1 +0,0 @@
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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"}