@ncukondo/search-hub 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +14 -7
  2. package/dist/cli/commands/config.d.ts +53 -0
  3. package/dist/cli/commands/config.d.ts.map +1 -1
  4. package/dist/cli/commands/config.js +62 -0
  5. package/dist/cli/commands/config.js.map +1 -1
  6. package/dist/cli/commands/fulltext/index.js +1 -0
  7. package/dist/cli/commands/fulltext/index.js.map +1 -1
  8. package/dist/cli/commands/init.d.ts +12 -17
  9. package/dist/cli/commands/init.d.ts.map +1 -1
  10. package/dist/cli/commands/init.js +116 -76
  11. package/dist/cli/commands/init.js.map +1 -1
  12. package/dist/cli/commands/query/init.d.ts +0 -1
  13. package/dist/cli/commands/query/init.d.ts.map +1 -1
  14. package/dist/cli/commands/query/init.js +2 -3
  15. package/dist/cli/commands/query/init.js.map +1 -1
  16. package/dist/cli/commands/query/resolve.d.ts.map +1 -1
  17. package/dist/cli/commands/query/resolve.js +5 -2
  18. package/dist/cli/commands/query/resolve.js.map +1 -1
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +183 -23
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/suggestions/rules.js +1 -1
  23. package/dist/cli/suggestions/rules.js.map +1 -1
  24. package/dist/config/index.d.ts +1 -0
  25. package/dist/config/index.d.ts.map +1 -1
  26. package/dist/config/loader.d.ts +4 -2
  27. package/dist/config/loader.d.ts.map +1 -1
  28. package/dist/config/loader.js +7 -6
  29. package/dist/config/loader.js.map +1 -1
  30. package/dist/config/paths.d.ts +21 -0
  31. package/dist/config/paths.d.ts.map +1 -1
  32. package/dist/config/paths.js +28 -5
  33. package/dist/config/paths.js.map +1 -1
  34. package/dist/index.js +6 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/package.json.js +1 -1
  37. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { generateReviewNextSteps, computeBatchContinuation } from "../commands/review/next-steps.js";
2
2
  const queryInitRule = (ctx) => {
3
3
  if (ctx.command !== "query init") return null;
4
- const file = ctx.outputFile ?? "queries/query.yaml";
4
+ const file = ctx.outputFile ?? ".search-hub/queries/query.yaml";
5
5
  return {
6
6
  next: [
7
7
  { command: `$EDITOR ${file}`, description: "Edit your query" },
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","sources":["../../../src/cli/suggestions/rules.ts"],"sourcesContent":["import type { SuggestionContext, SuggestionResult, SuggestionRule } from './types.js';\nimport { computeBatchContinuation, generateReviewNextSteps } from '../commands/review/next-steps.js';\n\n// Phase 1: Query Preparation rules\n\nconst queryInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query init') return null;\n const file = ctx.outputFile ?? 'queries/query.yaml';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit your query' },\n { command: `search-hub query validate ${file}`, description: 'Validate query' },\n { command: `search-hub search ${file} --count-only`, description: 'Check hit counts' },\n ],\n seeAlso: [],\n };\n};\n\nconst queryValidateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query validate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n\n if (ctx.validationSuccess === false) {\n const next = [{ command: `$EDITOR ${file}`, description: 'Fix errors and re-validate' }];\n\n if (ctx.hasSchemaLink === false) {\n return {\n next,\n or: {\n label: 'Or create a new query from the template',\n items: [{ command: 'search-hub query init \"<title>\"', description: '' }],\n },\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n }\n\n const next = [\n { command: `search-hub search ${file} --dry-run`, description: 'Check DB translations' },\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n ];\n\n if (ctx.hasSchemaLink === false) {\n return {\n tip: 'Tip: Start from a template to get $schema support and usage examples:\\n'\n + ' search-hub query init \"<title>\"',\n next,\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n};\n\nconst queryTranslateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query translate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 2: Search Execution rules\n\n/**\n * Build search completion suggestions based on session status.\n * Shared by search, search --query, and resume commands.\n */\nfunction searchCompletionSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed': {\n const seeAlso: SuggestionResult['seeAlso'] = [];\n if (ctx.previousSessionId) {\n seeAlso.push({\n command: `search-hub diff ${ctx.previousSessionId} ${sid}`,\n description: 'Compare with previous',\n });\n } else if (ctx.sessionCount !== undefined && ctx.sessionCount > 1) {\n seeAlso.push({\n command: `search-hub diff <other-session> ${sid}`,\n description: 'Compare with another query version',\n });\n }\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso,\n };\n }\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Retry failed databases' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [\n { command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' },\n { command: `search-hub status ${sid}`, description: 'View error details' },\n ],\n seeAlso: [],\n };\n default:\n return null;\n }\n}\n\nconst searchDryRunRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --dry-run') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchPreviewRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --preview') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchCountOnlyRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --count-only') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit query to refine' },\n { command: `search-hub search ${file} --count-only`, description: 'Re-check counts' },\n { command: `search-hub query assess ${file} --verdict refine`, description: 'Record assessment' },\n ],\n seeAlso: [\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n };\n};\n\nconst searchDirectQueryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --query') return null;\n const base = searchCompletionSuggestion(ctx);\n if (base === null) return null;\n return {\n next: base.next,\n seeAlso: [\n ...base.seeAlso,\n { command: 'search-hub query init \"<title>\"', description: 'Save as YAML for reproducibility' },\n ],\n };\n};\n\nconst searchRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search') return null;\n return searchCompletionSuggestion(ctx);\n};\n\nconst resumeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'resume') return null;\n return searchCompletionSuggestion(ctx);\n};\n\n// Phase 3: Result Analysis rules\n\nconst statusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'status') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed':\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso: [],\n };\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Resume search' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [{ command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' }],\n seeAlso: [],\n };\n default:\n return null;\n }\n};\n\n/**\n * Suggestion for results/summary commands - conditional based on reviews.yaml existence.\n */\nfunction resultReviewSuggestion(ctx: SuggestionContext): SuggestionResult {\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === true) {\n return {\n next: [{ command: `search-hub review status --session ${sid}`, description: 'Check review progress' }],\n seeAlso: [],\n };\n }\n return {\n next: [{ command: `search-hub review init --session ${sid}`, description: 'Start systematic review' }],\n seeAlso: [],\n };\n}\n\nconst resultsRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'results') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst summaryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'summary') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst diffRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'diff') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const seeAlso: SuggestionResult['seeAlso'] = [];\n\n // Suggest merge when both sessions have unique articles\n if (ctx.diffAddedCount !== undefined && ctx.diffAddedCount > 0 &&\n ctx.diffRemovedCount !== undefined && ctx.diffRemovedCount > 0) {\n const sid1 = ctx.diffSession1Id ?? '<session-id-1>';\n seeAlso.push({\n command: `search-hub merge ${sid1} ${sid}`,\n description: 'Combine results from both sessions',\n });\n }\n\n seeAlso.push({ command: `search-hub results ${sid}`, description: 'View detailed results' });\n\n return { next: [], seeAlso };\n};\n\nconst mergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'merge') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View merged results' },\n { command: `search-hub summary ${sid}`, description: 'View merge statistics' },\n ],\n seeAlso: [],\n };\n};\n\nconst relatedRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'related') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View related articles' },\n { command: `search-hub review init ${sid}`, description: 'Screen related articles' },\n { command: `search-hub export ${sid}`, description: 'Export results' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 4: Review Workflow rules\n\nconst reviewInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review init') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review extract --session ${sid} --basis title --name title-screening`,\n description: 'Start title screening',\n },\n ],\n seeAlso: [],\n };\n};\n\nconst reviewStatusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review status') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewListRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review list') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review extract --session ${sid} --name <name>`,\n description: 'Extract subset for review',\n },\n ],\n };\n};\n\nconst reviewExtractRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review extract') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const name = ctx.extractName ?? '<name>';\n const result: SuggestionResult = {\n next: [\n {\n command: `search-hub review merge --session ${sid} --name ${name}`,\n description: 'Merge review results',\n },\n ],\n seeAlso: [],\n };\n\n // Batch continuation: suggest next batch if --limit was used with remaining articles\n if (\n ctx.extractLimit !== undefined &&\n ctx.extractedCount !== undefined &&\n ctx.totalMatching !== undefined\n ) {\n const batch = computeBatchContinuation({\n sessionId: sid,\n extractName: name !== '<name>' ? name : undefined,\n extractedCount: ctx.extractedCount,\n totalMatching: ctx.totalMatching,\n limit: ctx.extractLimit,\n offset: ctx.extractOffset,\n });\n if (batch) result.seeAlso.push(batch);\n }\n\n return result;\n};\n\nconst reviewMergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review merge') return null;\n const rs = ctx.reviewStatus;\n if (!rs) {\n // Fallback: suggest status check when reviewStatus not available\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review status --session ${sid}`,\n description: 'Check progress',\n },\n ],\n seeAlso: [],\n };\n }\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewFinalizeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review finalize') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewExportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub register ${sid} --reviewed`,\n description: 'Register with reference-manager',\n },\n ],\n };\n};\n\n// Phase 5: Registration & Export rules\n\nconst exportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start review workflow',\n },\n ],\n };\n }\n return { next: [], seeAlso: [] };\n};\n\nconst registerRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'register') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start systematic review',\n },\n ],\n };\n }\n // Terminal state: no suggestions\n return null;\n};\n\nconst queryAssessRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query assess') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query log ${file}`, description: 'View iteration history' },\n ],\n seeAlso: [\n { command: `$EDITOR ${file}`, description: 'Edit query and re-run count' },\n ],\n };\n};\n\nconst notesRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'notes add' && ctx.command !== 'notes assess') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [{ command: `search-hub notes list ${sid}`, description: 'View notes' }],\n };\n};\n\n/**\n * All suggestion rules in evaluation order.\n */\nconst rules: SuggestionRule[] = [\n // Phase 1\n queryInitRule,\n queryValidateRule,\n queryTranslateRule,\n // Phase 2\n searchDryRunRule,\n searchPreviewRule,\n searchCountOnlyRule,\n searchDirectQueryRule,\n searchRule,\n resumeRule,\n // Phase 3\n statusRule,\n resultsRule,\n summaryRule,\n diffRule,\n mergeRule,\n relatedRule,\n // Phase 4\n reviewInitRule,\n reviewStatusRule,\n reviewListRule,\n reviewExtractRule,\n reviewMergeRule,\n reviewFinalizeRule,\n reviewExportRule,\n // Query iteration\n queryAssessRule,\n // Phase 5\n exportRule,\n registerRule,\n notesRule,\n];\n\n/**\n * Evaluate suggestion rules for the given context.\n * Returns the first matching rule's result, or null if no rules match.\n */\nexport function getSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n for (const rule of rules) {\n const result = rule(ctx);\n if (result !== null) return result;\n }\n return null;\n}\n"],"names":["next"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","sources":["../../../src/cli/suggestions/rules.ts"],"sourcesContent":["import type { SuggestionContext, SuggestionResult, SuggestionRule } from './types.js';\nimport { computeBatchContinuation, generateReviewNextSteps } from '../commands/review/next-steps.js';\n\n// Phase 1: Query Preparation rules\n\nconst queryInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query init') return null;\n const file = ctx.outputFile ?? '.search-hub/queries/query.yaml';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit your query' },\n { command: `search-hub query validate ${file}`, description: 'Validate query' },\n { command: `search-hub search ${file} --count-only`, description: 'Check hit counts' },\n ],\n seeAlso: [],\n };\n};\n\nconst queryValidateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query validate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n\n if (ctx.validationSuccess === false) {\n const next = [{ command: `$EDITOR ${file}`, description: 'Fix errors and re-validate' }];\n\n if (ctx.hasSchemaLink === false) {\n return {\n next,\n or: {\n label: 'Or create a new query from the template',\n items: [{ command: 'search-hub query init \"<title>\"', description: '' }],\n },\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n }\n\n const next = [\n { command: `search-hub search ${file} --dry-run`, description: 'Check DB translations' },\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n ];\n\n if (ctx.hasSchemaLink === false) {\n return {\n tip: 'Tip: Start from a template to get $schema support and usage examples:\\n'\n + ' search-hub query init \"<title>\"',\n next,\n seeAlso: [],\n };\n }\n\n return { next, seeAlso: [] };\n};\n\nconst queryTranslateRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query translate') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 2: Search Execution rules\n\n/**\n * Build search completion suggestions based on session status.\n * Shared by search, search --query, and resume commands.\n */\nfunction searchCompletionSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed': {\n const seeAlso: SuggestionResult['seeAlso'] = [];\n if (ctx.previousSessionId) {\n seeAlso.push({\n command: `search-hub diff ${ctx.previousSessionId} ${sid}`,\n description: 'Compare with previous',\n });\n } else if (ctx.sessionCount !== undefined && ctx.sessionCount > 1) {\n seeAlso.push({\n command: `search-hub diff <other-session> ${sid}`,\n description: 'Compare with another query version',\n });\n }\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso,\n };\n }\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Retry failed databases' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [\n { command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' },\n { command: `search-hub status ${sid}`, description: 'View error details' },\n ],\n seeAlso: [],\n };\n default:\n return null;\n }\n}\n\nconst searchDryRunRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --dry-run') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub search ${file} --preview`, description: 'Preview hit counts + sample titles' },\n { command: `search-hub search ${file}`, description: 'Execute search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchPreviewRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --preview') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query assess ${file} --verdict <verdict>`, description: 'Record assessment' },\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n seeAlso: [],\n };\n};\n\nconst searchCountOnlyRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --count-only') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `$EDITOR ${file}`, description: 'Edit query to refine' },\n { command: `search-hub search ${file} --count-only`, description: 'Re-check counts' },\n { command: `search-hub query assess ${file} --verdict refine`, description: 'Record assessment' },\n ],\n seeAlso: [\n { command: `search-hub search ${file}`, description: 'Execute full search' },\n ],\n };\n};\n\nconst searchDirectQueryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search --query') return null;\n const base = searchCompletionSuggestion(ctx);\n if (base === null) return null;\n return {\n next: base.next,\n seeAlso: [\n ...base.seeAlso,\n { command: 'search-hub query init \"<title>\"', description: 'Save as YAML for reproducibility' },\n ],\n };\n};\n\nconst searchRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'search') return null;\n return searchCompletionSuggestion(ctx);\n};\n\nconst resumeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'resume') return null;\n return searchCompletionSuggestion(ctx);\n};\n\n// Phase 3: Result Analysis rules\n\nconst statusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'status') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n\n switch (ctx.sessionStatus) {\n case 'completed':\n return {\n next: [{ command: `search-hub results ${sid}`, description: 'View results' }],\n seeAlso: [],\n };\n case 'partial':\n return {\n next: [{ command: `search-hub resume ${sid}`, description: 'Resume search' }],\n seeAlso: [],\n };\n case 'failed':\n return {\n next: [{ command: `search-hub resume ${sid} --retry-failed`, description: 'Retry all databases' }],\n seeAlso: [],\n };\n default:\n return null;\n }\n};\n\n/**\n * Suggestion for results/summary commands - conditional based on reviews.yaml existence.\n */\nfunction resultReviewSuggestion(ctx: SuggestionContext): SuggestionResult {\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === true) {\n return {\n next: [{ command: `search-hub review status --session ${sid}`, description: 'Check review progress' }],\n seeAlso: [],\n };\n }\n return {\n next: [{ command: `search-hub review init --session ${sid}`, description: 'Start systematic review' }],\n seeAlso: [],\n };\n}\n\nconst resultsRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'results') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst summaryRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'summary') return null;\n return resultReviewSuggestion(ctx);\n};\n\nconst diffRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'diff') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const seeAlso: SuggestionResult['seeAlso'] = [];\n\n // Suggest merge when both sessions have unique articles\n if (ctx.diffAddedCount !== undefined && ctx.diffAddedCount > 0 &&\n ctx.diffRemovedCount !== undefined && ctx.diffRemovedCount > 0) {\n const sid1 = ctx.diffSession1Id ?? '<session-id-1>';\n seeAlso.push({\n command: `search-hub merge ${sid1} ${sid}`,\n description: 'Combine results from both sessions',\n });\n }\n\n seeAlso.push({ command: `search-hub results ${sid}`, description: 'View detailed results' });\n\n return { next: [], seeAlso };\n};\n\nconst mergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'merge') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View merged results' },\n { command: `search-hub summary ${sid}`, description: 'View merge statistics' },\n ],\n seeAlso: [],\n };\n};\n\nconst relatedRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'related') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n { command: `search-hub results ${sid}`, description: 'View related articles' },\n { command: `search-hub review init ${sid}`, description: 'Screen related articles' },\n { command: `search-hub export ${sid}`, description: 'Export results' },\n ],\n seeAlso: [],\n };\n};\n\n// Phase 4: Review Workflow rules\n\nconst reviewInitRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review init') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review extract --session ${sid} --basis title --name title-screening`,\n description: 'Start title screening',\n },\n ],\n seeAlso: [],\n };\n};\n\nconst reviewStatusRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review status') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewListRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review list') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review extract --session ${sid} --name <name>`,\n description: 'Extract subset for review',\n },\n ],\n };\n};\n\nconst reviewExtractRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review extract') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n const name = ctx.extractName ?? '<name>';\n const result: SuggestionResult = {\n next: [\n {\n command: `search-hub review merge --session ${sid} --name ${name}`,\n description: 'Merge review results',\n },\n ],\n seeAlso: [],\n };\n\n // Batch continuation: suggest next batch if --limit was used with remaining articles\n if (\n ctx.extractLimit !== undefined &&\n ctx.extractedCount !== undefined &&\n ctx.totalMatching !== undefined\n ) {\n const batch = computeBatchContinuation({\n sessionId: sid,\n extractName: name !== '<name>' ? name : undefined,\n extractedCount: ctx.extractedCount,\n totalMatching: ctx.totalMatching,\n limit: ctx.extractLimit,\n offset: ctx.extractOffset,\n });\n if (batch) result.seeAlso.push(batch);\n }\n\n return result;\n};\n\nconst reviewMergeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review merge') return null;\n const rs = ctx.reviewStatus;\n if (!rs) {\n // Fallback: suggest status check when reviewStatus not available\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [\n {\n command: `search-hub review status --session ${sid}`,\n description: 'Check progress',\n },\n ],\n seeAlso: [],\n };\n }\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewFinalizeRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review finalize') return null;\n const rs = ctx.reviewStatus;\n if (!rs) return null;\n\n return generateReviewNextSteps({\n sessionId: ctx.sessionId ?? '<session-id>',\n statusResult: rs,\n ...(rs.mode && { mode: rs.mode }),\n });\n};\n\nconst reviewExportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'review export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub register ${sid} --reviewed`,\n description: 'Register with reference-manager',\n },\n ],\n };\n};\n\n// Phase 5: Registration & Export rules\n\nconst exportRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'export') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start review workflow',\n },\n ],\n };\n }\n return { next: [], seeAlso: [] };\n};\n\nconst registerRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'register') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n if (ctx.hasReviews === false) {\n return {\n next: [],\n seeAlso: [\n {\n command: `search-hub review init --session ${sid}`,\n description: 'Start systematic review',\n },\n ],\n };\n }\n // Terminal state: no suggestions\n return null;\n};\n\nconst queryAssessRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'query assess') return null;\n const file = ctx.queryFile ?? '<query-file>';\n return {\n next: [\n { command: `search-hub query log ${file}`, description: 'View iteration history' },\n ],\n seeAlso: [\n { command: `$EDITOR ${file}`, description: 'Edit query and re-run count' },\n ],\n };\n};\n\nconst notesRule: SuggestionRule = (ctx) => {\n if (ctx.command !== 'notes add' && ctx.command !== 'notes assess') return null;\n const sid = ctx.sessionId ?? '<session-id>';\n return {\n next: [],\n seeAlso: [{ command: `search-hub notes list ${sid}`, description: 'View notes' }],\n };\n};\n\n/**\n * All suggestion rules in evaluation order.\n */\nconst rules: SuggestionRule[] = [\n // Phase 1\n queryInitRule,\n queryValidateRule,\n queryTranslateRule,\n // Phase 2\n searchDryRunRule,\n searchPreviewRule,\n searchCountOnlyRule,\n searchDirectQueryRule,\n searchRule,\n resumeRule,\n // Phase 3\n statusRule,\n resultsRule,\n summaryRule,\n diffRule,\n mergeRule,\n relatedRule,\n // Phase 4\n reviewInitRule,\n reviewStatusRule,\n reviewListRule,\n reviewExtractRule,\n reviewMergeRule,\n reviewFinalizeRule,\n reviewExportRule,\n // Query iteration\n queryAssessRule,\n // Phase 5\n exportRule,\n registerRule,\n notesRule,\n];\n\n/**\n * Evaluate suggestion rules for the given context.\n * Returns the first matching rule's result, or null if no rules match.\n */\nexport function getSuggestion(ctx: SuggestionContext): SuggestionResult | null {\n for (const rule of rules) {\n const result = rule(ctx);\n if (result !== null) return result;\n }\n return null;\n}\n"],"names":["next"],"mappings":"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"}
@@ -6,4 +6,5 @@
6
6
  export { loadConfig, saveConfig, type LoadConfigOptions, type SaveConfigOptions } from './loader';
7
7
  export { ConfigSchema, type Config, type ProviderConfig } from './schema';
8
8
  export { getDefaultConfig, DEFAULT_CONFIG } from './defaults';
9
+ export { getProjectDir, getLocalConfigPath, getLocalSessionsDir, getQueriesDir, isInsideProject, } from './paths';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,eAAe,GAChB,MAAM,SAAS,CAAC"}
@@ -7,8 +7,10 @@ export type RawConfig = Partial<Config>;
7
7
  export interface LoadConfigOptions {
8
8
  /** Path to global config file (default: platform-specific via getDefaultConfigPath()) */
9
9
  globalConfigPath?: string;
10
- /** Path to local config file (default: ./search-hub.config.toml) */
10
+ /** Path to local config file (default: .search-hub/config.toml via getLocalConfigPath()) */
11
11
  localConfigPath?: string;
12
+ /** Project directory for .search-hub/ resolution (default: cwd) */
13
+ projectDir?: string;
12
14
  /**
13
15
  * Explicit config file path specified via CLI --config option.
14
16
  * Takes priority over global and local config files (applied after env vars).
@@ -30,7 +32,7 @@ export declare function loadTomlFile(path: string): Promise<RawConfig>;
30
32
  * 1. CLI options (cliOptions)
31
33
  * 2. Explicit --config file (explicitConfigPath)
32
34
  * 3. Environment variables
33
- * 4. Local config (./search-hub.config.toml)
35
+ * 4. Local config (.search-hub/config.toml)
34
36
  * 5. Global config (platform-specific, e.g. ~/.config/search-hub/config.toml on Linux)
35
37
  * 6. Default values
36
38
  */
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAIxD,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrE,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAyBnE;AAOD;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4CjF;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AASD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAoBf"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAIxD,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrE,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAyBnE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDjF;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AASD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -1,10 +1,10 @@
1
- import { mkdir, writeFile, readFile } from "node:fs/promises";
1
+ import { readFile, mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
- import { stringify, parse } from "@iarna/toml";
3
+ import { parse, stringify } from "@iarna/toml";
4
4
  import { ConfigSchema } from "./schema.js";
5
5
  import { getDefaultConfig } from "./defaults.js";
6
6
  import { applyEnvVars } from "./env.js";
7
- import { getDefaultConfigPath, getDefaultSessionsDir } from "./paths.js";
7
+ import { getDefaultConfigPath, getLocalConfigPath, isInsideProject, getLocalSessionsDir, getDefaultSessionsDir } from "./paths.js";
8
8
  import { deepMerge } from "../utils/deep-merge.js";
9
9
  import { expandPath } from "../utils/path.js";
10
10
  async function loadTomlFile(path) {
@@ -28,11 +28,11 @@ async function loadTomlFile(path) {
28
28
  );
29
29
  }
30
30
  }
31
- const DEFAULT_LOCAL_CONFIG_PATH = "./search-hub.config.toml";
32
31
  async function loadConfig(options = {}) {
33
32
  const {
34
33
  globalConfigPath = getDefaultConfigPath(),
35
- localConfigPath = DEFAULT_LOCAL_CONFIG_PATH,
34
+ localConfigPath = getLocalConfigPath(),
35
+ projectDir,
36
36
  explicitConfigPath,
37
37
  cliOptions
38
38
  } = options;
@@ -53,7 +53,8 @@ async function loadConfig(options = {}) {
53
53
  }
54
54
  config = ConfigSchema.parse(config);
55
55
  if (!config.session.directory) {
56
- config.session.directory = getDefaultSessionsDir();
56
+ const inProject = projectDir ? await isInsideProject(projectDir) : false;
57
+ config.session.directory = inProject ? getLocalSessionsDir(projectDir) : getDefaultSessionsDir();
57
58
  }
58
59
  return config;
59
60
  }
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sources":["../../src/config/loader.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { parse as parseToml, stringify as stringifyToml } from '@iarna/toml';\nimport { ConfigSchema, type Config } from './schema.js';\nimport { getDefaultConfig } from './defaults.js';\nimport { applyEnvVars } from './env.js';\nimport { getDefaultConfigPath, getDefaultSessionsDir } from './paths.js';\nimport { deepMerge, type DeepPartial } from '../utils/deep-merge.js';\nimport { expandPath } from '../utils/path.js';\n\nexport type RawConfig = Partial<Config>;\n\n/**\n * Options for loadConfig function.\n */\nexport interface LoadConfigOptions {\n /** Path to global config file (default: platform-specific via getDefaultConfigPath()) */\n globalConfigPath?: string;\n /** Path to local config file (default: ./search-hub.config.toml) */\n localConfigPath?: string;\n /**\n * Explicit config file path specified via CLI --config option.\n * Takes priority over global and local config files (applied after env vars).\n */\n explicitConfigPath?: string;\n /** CLI options to apply (highest priority) */\n cliOptions?: DeepPartial<Config>;\n}\n\n/**\n * Load and parse a TOML config file.\n * Returns empty object if file doesn't exist.\n * Throws with clear message if TOML is invalid.\n */\nexport async function loadTomlFile(path: string): Promise<RawConfig> {\n let content: string;\n\n try {\n content = await readFile(path, 'utf-8');\n } catch (error) {\n // File doesn't exist or can't be read\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return {};\n }\n throw error;\n }\n\n // Empty file\n if (!content.trim()) {\n return {};\n }\n\n try {\n return parseToml(content) as RawConfig;\n } catch (error) {\n throw new Error(\n `Invalid TOML in ${path}: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n}\n\n/**\n * Default local config file path.\n */\nconst DEFAULT_LOCAL_CONFIG_PATH = './search-hub.config.toml';\n\n/**\n * Load configuration from all sources and merge them.\n *\n * Priority (highest to lowest):\n * 1. CLI options (cliOptions)\n * 2. Explicit --config file (explicitConfigPath)\n * 3. Environment variables\n * 4. Local config (./search-hub.config.toml)\n * 5. Global config (platform-specific, e.g. ~/.config/search-hub/config.toml on Linux)\n * 6. Default values\n */\nexport async function loadConfig(options: LoadConfigOptions = {}): Promise<Config> {\n const {\n globalConfigPath = getDefaultConfigPath(),\n localConfigPath = DEFAULT_LOCAL_CONFIG_PATH,\n explicitConfigPath,\n cliOptions,\n } = options;\n\n // 1. Start with defaults\n let config = getDefaultConfig();\n\n // 2. Load and merge global config (lowest file priority)\n const expandedGlobalPath = expandPath(globalConfigPath);\n const globalConfig = await loadTomlFile(expandedGlobalPath);\n config = deepMerge(config, globalConfig as DeepPartial<Config>);\n\n // 3. Load and merge local config (overrides global)\n const localConfig = await loadTomlFile(localConfigPath);\n config = deepMerge(config, localConfig as DeepPartial<Config>);\n\n // 4. Apply environment variables (overrides local)\n config = applyEnvVars(config);\n\n // 5. Apply explicit --config file (overrides env vars, local, and global)\n if (explicitConfigPath) {\n const expandedExplicitPath = expandPath(explicitConfigPath);\n const explicitConfig = await loadTomlFile(expandedExplicitPath);\n config = deepMerge(config, explicitConfig as DeepPartial<Config>);\n }\n\n // 6. Apply CLI options (highest priority)\n if (cliOptions) {\n config = deepMerge(config, cliOptions);\n }\n\n // 7. Validate\n config = ConfigSchema.parse(config);\n\n // 8. Resolve empty session.directory to platform default\n if (!config.session.directory) {\n config.session.directory = getDefaultSessionsDir();\n }\n\n return config;\n}\n\n/**\n * Options for saveConfig function.\n */\nexport interface SaveConfigOptions {\n /** Path to save config file (default: platform-specific via getDefaultConfigPath()) */\n path?: string;\n /** Create directory if it doesn't exist (default: true) */\n createDir?: boolean;\n}\n\n// Re-define JsonMap type to match @iarna/toml's expected input\n// This is necessary because the library's JsonMap type is not exported\ntype TomlValue = boolean | number | string | Date | TomlMap | TomlValue[];\ninterface TomlMap {\n [key: string]: TomlValue;\n}\n\n/**\n * Save configuration to a TOML file.\n *\n * @param config - Configuration object to save\n * @param options - Save options\n * @throws Error if config is invalid or file write fails\n */\nexport async function saveConfig(\n config: Config,\n options: SaveConfigOptions = {}\n): Promise<void> {\n const {\n path = getDefaultConfigPath(),\n createDir = true,\n } = options;\n\n // Validate config before saving\n ConfigSchema.parse(config);\n\n // Expand path and ensure directory exists\n const expandedPath = expandPath(path);\n if (createDir) {\n await mkdir(dirname(expandedPath), { recursive: true });\n }\n\n // Convert to TOML and write\n // Config structure is compatible with TOML's JsonMap type\n // The cast is safe because Config only contains TOML-compatible types\n const tomlContent = stringifyToml(config as TomlMap);\n await writeFile(expandedPath, tomlContent, 'utf-8');\n}\n"],"names":["parseToml","stringifyToml"],"mappings":";;;;;;;;;AAkCA,eAAsB,aAAa,MAAkC;AACnE,MAAI;AAEJ,MAAI;AACF,cAAU,MAAM,SAAS,MAAM,OAAO;AAAA,EACxC,SAAS,OAAO;AAEd,QAAK,MAAgC,SAAS,UAAU;AACtD,aAAO,CAAA;AAAA,IACT;AACA,UAAM;AAAA,EACR;AAGA,MAAI,CAAC,QAAQ,QAAQ;AACnB,WAAO,CAAA;AAAA,EACT;AAEA,MAAI;AACF,WAAOA,MAAU,OAAO;AAAA,EAC1B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,mBAAmB,IAAI,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,IAAA;AAAA,EAEtF;AACF;AAKA,MAAM,4BAA4B;AAalC,eAAsB,WAAW,UAA6B,IAAqB;AACjF,QAAM;AAAA,IACJ,mBAAmB,qBAAA;AAAA,IACnB,kBAAkB;AAAA,IAClB;AAAA,IACA;AAAA,EAAA,IACE;AAGJ,MAAI,SAAS,iBAAA;AAGb,QAAM,qBAAqB,WAAW,gBAAgB;AACtD,QAAM,eAAe,MAAM,aAAa,kBAAkB;AAC1D,WAAS,UAAU,QAAQ,YAAmC;AAG9D,QAAM,cAAc,MAAM,aAAa,eAAe;AACtD,WAAS,UAAU,QAAQ,WAAkC;AAG7D,WAAS,aAAa,MAAM;AAG5B,MAAI,oBAAoB;AACtB,UAAM,uBAAuB,WAAW,kBAAkB;AAC1D,UAAM,iBAAiB,MAAM,aAAa,oBAAoB;AAC9D,aAAS,UAAU,QAAQ,cAAqC;AAAA,EAClE;AAGA,MAAI,YAAY;AACd,aAAS,UAAU,QAAQ,UAAU;AAAA,EACvC;AAGA,WAAS,aAAa,MAAM,MAAM;AAGlC,MAAI,CAAC,OAAO,QAAQ,WAAW;AAC7B,WAAO,QAAQ,YAAY,sBAAA;AAAA,EAC7B;AAEA,SAAO;AACT;AA0BA,eAAsB,WACpB,QACA,UAA6B,IACd;AACf,QAAM;AAAA,IACJ,OAAO,qBAAA;AAAA,IACP,YAAY;AAAA,EAAA,IACV;AAGJ,eAAa,MAAM,MAAM;AAGzB,QAAM,eAAe,WAAW,IAAI;AACpC,MAAI,WAAW;AACb,UAAM,MAAM,QAAQ,YAAY,GAAG,EAAE,WAAW,MAAM;AAAA,EACxD;AAKA,QAAM,cAAcC,UAAc,MAAiB;AACnD,QAAM,UAAU,cAAc,aAAa,OAAO;AACpD;"}
1
+ {"version":3,"file":"loader.js","sources":["../../src/config/loader.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport { parse as parseToml, stringify as stringifyToml } from '@iarna/toml';\nimport { ConfigSchema, type Config } from './schema.js';\nimport { getDefaultConfig } from './defaults.js';\nimport { applyEnvVars } from './env.js';\nimport { getDefaultConfigPath, getDefaultSessionsDir, getLocalConfigPath, getLocalSessionsDir, isInsideProject } from './paths.js';\nimport { deepMerge, type DeepPartial } from '../utils/deep-merge.js';\nimport { expandPath } from '../utils/path.js';\n\nexport type RawConfig = Partial<Config>;\n\n/**\n * Options for loadConfig function.\n */\nexport interface LoadConfigOptions {\n /** Path to global config file (default: platform-specific via getDefaultConfigPath()) */\n globalConfigPath?: string;\n /** Path to local config file (default: .search-hub/config.toml via getLocalConfigPath()) */\n localConfigPath?: string;\n /** Project directory for .search-hub/ resolution (default: cwd) */\n projectDir?: string;\n /**\n * Explicit config file path specified via CLI --config option.\n * Takes priority over global and local config files (applied after env vars).\n */\n explicitConfigPath?: string;\n /** CLI options to apply (highest priority) */\n cliOptions?: DeepPartial<Config>;\n}\n\n/**\n * Load and parse a TOML config file.\n * Returns empty object if file doesn't exist.\n * Throws with clear message if TOML is invalid.\n */\nexport async function loadTomlFile(path: string): Promise<RawConfig> {\n let content: string;\n\n try {\n content = await readFile(path, 'utf-8');\n } catch (error) {\n // File doesn't exist or can't be read\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return {};\n }\n throw error;\n }\n\n // Empty file\n if (!content.trim()) {\n return {};\n }\n\n try {\n return parseToml(content) as RawConfig;\n } catch (error) {\n throw new Error(\n `Invalid TOML in ${path}: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n}\n\n/**\n * Load configuration from all sources and merge them.\n *\n * Priority (highest to lowest):\n * 1. CLI options (cliOptions)\n * 2. Explicit --config file (explicitConfigPath)\n * 3. Environment variables\n * 4. Local config (.search-hub/config.toml)\n * 5. Global config (platform-specific, e.g. ~/.config/search-hub/config.toml on Linux)\n * 6. Default values\n */\nexport async function loadConfig(options: LoadConfigOptions = {}): Promise<Config> {\n const {\n globalConfigPath = getDefaultConfigPath(),\n localConfigPath = getLocalConfigPath(),\n projectDir,\n explicitConfigPath,\n cliOptions,\n } = options;\n\n // 1. Start with defaults\n let config = getDefaultConfig();\n\n // 2. Load and merge global config (lowest file priority)\n const expandedGlobalPath = expandPath(globalConfigPath);\n const globalConfig = await loadTomlFile(expandedGlobalPath);\n config = deepMerge(config, globalConfig as DeepPartial<Config>);\n\n // 3. Load and merge local config (overrides global)\n const localConfig = await loadTomlFile(localConfigPath);\n config = deepMerge(config, localConfig as DeepPartial<Config>);\n\n // 4. Apply environment variables (overrides local)\n config = applyEnvVars(config);\n\n // 5. Apply explicit --config file (overrides env vars, local, and global)\n if (explicitConfigPath) {\n const expandedExplicitPath = expandPath(explicitConfigPath);\n const explicitConfig = await loadTomlFile(expandedExplicitPath);\n config = deepMerge(config, explicitConfig as DeepPartial<Config>);\n }\n\n // 6. Apply CLI options (highest priority)\n if (cliOptions) {\n config = deepMerge(config, cliOptions);\n }\n\n // 7. Validate\n config = ConfigSchema.parse(config);\n\n // 8. Resolve empty session.directory based on project context\n if (!config.session.directory) {\n const inProject = projectDir\n ? await isInsideProject(projectDir)\n : false;\n config.session.directory = inProject\n ? getLocalSessionsDir(projectDir)\n : getDefaultSessionsDir();\n }\n\n return config;\n}\n\n/**\n * Options for saveConfig function.\n */\nexport interface SaveConfigOptions {\n /** Path to save config file (default: platform-specific via getDefaultConfigPath()) */\n path?: string;\n /** Create directory if it doesn't exist (default: true) */\n createDir?: boolean;\n}\n\n// Re-define JsonMap type to match @iarna/toml's expected input\n// This is necessary because the library's JsonMap type is not exported\ntype TomlValue = boolean | number | string | Date | TomlMap | TomlValue[];\ninterface TomlMap {\n [key: string]: TomlValue;\n}\n\n/**\n * Save configuration to a TOML file.\n *\n * @param config - Configuration object to save\n * @param options - Save options\n * @throws Error if config is invalid or file write fails\n */\nexport async function saveConfig(\n config: Config,\n options: SaveConfigOptions = {}\n): Promise<void> {\n const {\n path = getDefaultConfigPath(),\n createDir = true,\n } = options;\n\n // Validate config before saving\n ConfigSchema.parse(config);\n\n // Expand path and ensure directory exists\n const expandedPath = expandPath(path);\n if (createDir) {\n await mkdir(dirname(expandedPath), { recursive: true });\n }\n\n // Convert to TOML and write\n // Config structure is compatible with TOML's JsonMap type\n // The cast is safe because Config only contains TOML-compatible types\n const tomlContent = stringifyToml(config as TomlMap);\n await writeFile(expandedPath, tomlContent, 'utf-8');\n}\n"],"names":["parseToml","stringifyToml"],"mappings":";;;;;;;;;AAoCA,eAAsB,aAAa,MAAkC;AACnE,MAAI;AAEJ,MAAI;AACF,cAAU,MAAM,SAAS,MAAM,OAAO;AAAA,EACxC,SAAS,OAAO;AAEd,QAAK,MAAgC,SAAS,UAAU;AACtD,aAAO,CAAA;AAAA,IACT;AACA,UAAM;AAAA,EACR;AAGA,MAAI,CAAC,QAAQ,QAAQ;AACnB,WAAO,CAAA;AAAA,EACT;AAEA,MAAI;AACF,WAAOA,MAAU,OAAO;AAAA,EAC1B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,mBAAmB,IAAI,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,IAAA;AAAA,EAEtF;AACF;AAaA,eAAsB,WAAW,UAA6B,IAAqB;AACjF,QAAM;AAAA,IACJ,mBAAmB,qBAAA;AAAA,IACnB,kBAAkB,mBAAA;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE;AAGJ,MAAI,SAAS,iBAAA;AAGb,QAAM,qBAAqB,WAAW,gBAAgB;AACtD,QAAM,eAAe,MAAM,aAAa,kBAAkB;AAC1D,WAAS,UAAU,QAAQ,YAAmC;AAG9D,QAAM,cAAc,MAAM,aAAa,eAAe;AACtD,WAAS,UAAU,QAAQ,WAAkC;AAG7D,WAAS,aAAa,MAAM;AAG5B,MAAI,oBAAoB;AACtB,UAAM,uBAAuB,WAAW,kBAAkB;AAC1D,UAAM,iBAAiB,MAAM,aAAa,oBAAoB;AAC9D,aAAS,UAAU,QAAQ,cAAqC;AAAA,EAClE;AAGA,MAAI,YAAY;AACd,aAAS,UAAU,QAAQ,UAAU;AAAA,EACvC;AAGA,WAAS,aAAa,MAAM,MAAM;AAGlC,MAAI,CAAC,OAAO,QAAQ,WAAW;AAC7B,UAAM,YAAY,aACd,MAAM,gBAAgB,UAAU,IAChC;AACJ,WAAO,QAAQ,YAAY,YACvB,oBAAoB,UAAU,IAC9B,sBAAA;AAAA,EACN;AAEA,SAAO;AACT;AA0BA,eAAsB,WACpB,QACA,UAA6B,IACd;AACf,QAAM;AAAA,IACJ,OAAO,qBAAA;AAAA,IACP,YAAY;AAAA,EAAA,IACV;AAGJ,eAAa,MAAM,MAAM;AAGzB,QAAM,eAAe,WAAW,IAAI;AACpC,MAAI,WAAW;AACb,UAAM,MAAM,QAAQ,YAAY,GAAG,EAAE,WAAW,MAAM;AAAA,EACxD;AAKA,QAAM,cAAcC,UAAc,MAAiB;AACnD,QAAM,UAAU,cAAc,aAAa,OAAO;AACpD;"}
@@ -20,4 +20,25 @@ export declare function getDefaultConfigPath(): string;
20
20
  * Get the default sessions directory.
21
21
  */
22
22
  export declare function getDefaultSessionsDir(): string;
23
+ /**
24
+ * Get the project directory path (.search-hub/) relative to a base directory.
25
+ * Defaults to cwd.
26
+ */
27
+ export declare function getProjectDir(baseDir?: string): string;
28
+ /**
29
+ * Get the local config file path (.search-hub/config.toml).
30
+ */
31
+ export declare function getLocalConfigPath(baseDir?: string): string;
32
+ /**
33
+ * Get the local sessions directory (.search-hub/sessions/).
34
+ */
35
+ export declare function getLocalSessionsDir(baseDir?: string): string;
36
+ /**
37
+ * Get the local queries directory (.search-hub/queries/).
38
+ */
39
+ export declare function getQueriesDir(baseDir?: string): string;
40
+ /**
41
+ * Check if the given directory (default: cwd) contains a .search-hub/ project directory.
42
+ */
43
+ export declare function isInsideProject(baseDir?: string): Promise<boolean>;
23
44
  //# sourceMappingURL=paths.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/config/paths.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/config/paths.ts"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAKD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOxE"}
@@ -1,22 +1,45 @@
1
1
  import envPaths from "env-paths";
2
2
  import { join } from "node:path";
3
+ import { stat } from "node:fs/promises";
3
4
  const paths = envPaths("search-hub", { suffix: "" });
4
5
  function getConfigDir() {
5
6
  return paths.config;
6
7
  }
7
- function getDataDir() {
8
- return paths.data;
9
- }
10
8
  function getDefaultConfigPath() {
11
9
  return join(paths.config, "config.toml");
12
10
  }
13
11
  function getDefaultSessionsDir() {
14
12
  return join(paths.data, "sessions");
15
13
  }
14
+ const PROJECT_DIR_NAME = ".search-hub";
15
+ function getProjectDir(baseDir) {
16
+ return join(baseDir ?? process.cwd(), PROJECT_DIR_NAME);
17
+ }
18
+ function getLocalConfigPath(baseDir) {
19
+ return join(getProjectDir(baseDir), "config.toml");
20
+ }
21
+ function getLocalSessionsDir(baseDir) {
22
+ return join(getProjectDir(baseDir), "sessions");
23
+ }
24
+ function getQueriesDir(baseDir) {
25
+ return join(getProjectDir(baseDir), "queries");
26
+ }
27
+ async function isInsideProject(baseDir) {
28
+ try {
29
+ const stats = await stat(getProjectDir(baseDir));
30
+ return stats.isDirectory();
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
16
35
  export {
17
36
  getConfigDir,
18
- getDataDir,
19
37
  getDefaultConfigPath,
20
- getDefaultSessionsDir
38
+ getDefaultSessionsDir,
39
+ getLocalConfigPath,
40
+ getLocalSessionsDir,
41
+ getProjectDir,
42
+ getQueriesDir,
43
+ isInsideProject
21
44
  };
22
45
  //# sourceMappingURL=paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sources":["../../src/config/paths.ts"],"sourcesContent":["/**\n * Platform-specific paths using XDG Base Directory spec on Linux,\n * ~/Library on macOS, and AppData on Windows.\n */\nimport envPaths from 'env-paths';\nimport { join } from 'node:path';\n\n// Use empty suffix to get clean 'search-hub' directory names\nconst paths = envPaths('search-hub', { suffix: '' });\n\n/**\n * Get the config directory for search-hub.\n * - Linux: ~/.config/search-hub\n * - macOS: ~/Library/Preferences/search-hub\n * - Windows: %APPDATA%\\search-hub\\Config\n */\nexport function getConfigDir(): string {\n return paths.config;\n}\n\n/**\n * Get the data directory for search-hub.\n * - Linux: ~/.local/share/search-hub\n * - macOS: ~/Library/Application Support/search-hub\n * - Windows: %LOCALAPPDATA%\\search-hub\\Data\n */\nexport function getDataDir(): string {\n return paths.data;\n}\n\n/**\n * Get the default config file path.\n */\nexport function getDefaultConfigPath(): string {\n return join(paths.config, 'config.toml');\n}\n\n/**\n * Get the default sessions directory.\n */\nexport function getDefaultSessionsDir(): string {\n return join(paths.data, 'sessions');\n}\n"],"names":[],"mappings":";;AAQA,MAAM,QAAQ,SAAS,cAAc,EAAE,QAAQ,IAAI;AAQ5C,SAAS,eAAuB;AACrC,SAAO,MAAM;AACf;AAQO,SAAS,aAAqB;AACnC,SAAO,MAAM;AACf;AAKO,SAAS,uBAA+B;AAC7C,SAAO,KAAK,MAAM,QAAQ,aAAa;AACzC;AAKO,SAAS,wBAAgC;AAC9C,SAAO,KAAK,MAAM,MAAM,UAAU;AACpC;"}
1
+ {"version":3,"file":"paths.js","sources":["../../src/config/paths.ts"],"sourcesContent":["/**\n * Platform-specific paths using XDG Base Directory spec on Linux,\n * ~/Library on macOS, and AppData on Windows.\n */\nimport envPaths from 'env-paths';\nimport { join } from 'node:path';\nimport { stat } from 'node:fs/promises';\n\n// Use empty suffix to get clean 'search-hub' directory names\nconst paths = envPaths('search-hub', { suffix: '' });\n\n/**\n * Get the config directory for search-hub.\n * - Linux: ~/.config/search-hub\n * - macOS: ~/Library/Preferences/search-hub\n * - Windows: %APPDATA%\\search-hub\\Config\n */\nexport function getConfigDir(): string {\n return paths.config;\n}\n\n/**\n * Get the data directory for search-hub.\n * - Linux: ~/.local/share/search-hub\n * - macOS: ~/Library/Application Support/search-hub\n * - Windows: %LOCALAPPDATA%\\search-hub\\Data\n */\nexport function getDataDir(): string {\n return paths.data;\n}\n\n/**\n * Get the default config file path.\n */\nexport function getDefaultConfigPath(): string {\n return join(paths.config, 'config.toml');\n}\n\n/**\n * Get the default sessions directory.\n */\nexport function getDefaultSessionsDir(): string {\n return join(paths.data, 'sessions');\n}\n\n/** Name of the project-local directory. */\nconst PROJECT_DIR_NAME = '.search-hub';\n\n/**\n * Get the project directory path (.search-hub/) relative to a base directory.\n * Defaults to cwd.\n */\nexport function getProjectDir(baseDir?: string): string {\n return join(baseDir ?? process.cwd(), PROJECT_DIR_NAME);\n}\n\n/**\n * Get the local config file path (.search-hub/config.toml).\n */\nexport function getLocalConfigPath(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'config.toml');\n}\n\n/**\n * Get the local sessions directory (.search-hub/sessions/).\n */\nexport function getLocalSessionsDir(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'sessions');\n}\n\n/**\n * Get the local queries directory (.search-hub/queries/).\n */\nexport function getQueriesDir(baseDir?: string): string {\n return join(getProjectDir(baseDir), 'queries');\n}\n\n/**\n * Check if the given directory (default: cwd) contains a .search-hub/ project directory.\n */\nexport async function isInsideProject(baseDir?: string): Promise<boolean> {\n try {\n const stats = await stat(getProjectDir(baseDir));\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n"],"names":[],"mappings":";;;AASA,MAAM,QAAQ,SAAS,cAAc,EAAE,QAAQ,IAAI;AAQ5C,SAAS,eAAuB;AACrC,SAAO,MAAM;AACf;AAeO,SAAS,uBAA+B;AAC7C,SAAO,KAAK,MAAM,QAAQ,aAAa;AACzC;AAKO,SAAS,wBAAgC;AAC9C,SAAO,KAAK,MAAM,MAAM,UAAU;AACpC;AAGA,MAAM,mBAAmB;AAMlB,SAAS,cAAc,SAA0B;AACtD,SAAO,KAAK,WAAW,QAAQ,IAAA,GAAO,gBAAgB;AACxD;AAKO,SAAS,mBAAmB,SAA0B;AAC3D,SAAO,KAAK,cAAc,OAAO,GAAG,aAAa;AACnD;AAKO,SAAS,oBAAoB,SAA0B;AAC5D,SAAO,KAAK,cAAc,OAAO,GAAG,UAAU;AAChD;AAKO,SAAS,cAAc,SAA0B;AACtD,SAAO,KAAK,cAAc,OAAO,GAAG,SAAS;AAC/C;AAKA,eAAsB,gBAAgB,SAAoC;AACxE,MAAI;AACF,UAAM,QAAQ,MAAM,KAAK,cAAc,OAAO,CAAC;AAC/C,WAAO,MAAM,YAAA;AAAA,EACf,QAAQ;AACN,WAAO;AAAA,EACT;AACF;"}
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { extractControlledVocabTerms, validateControlledVocab } from "./query/vo
6
6
  import { loadConfig, saveConfig } from "./config/loader.js";
7
7
  import { ConfigSchema } from "./config/schema.js";
8
8
  import { DEFAULT_CONFIG, getDefaultConfig } from "./config/defaults.js";
9
+ import { getLocalConfigPath, getLocalSessionsDir, getProjectDir, getQueriesDir, isInsideProject } from "./config/paths.js";
9
10
  import { createSession, generateSessionId, getResumableProviders, listSessions, loadSession, sanitizeName, saveSession, sessionExists, updateDatabaseStatus, updateSessionStatus } from "./session/manager.js";
10
11
  import { SessionLogger } from "./session/logger.js";
11
12
  import { createProviderError, isAuthError, isProviderError, isRateLimitError } from "./providers/base/types.js";
@@ -33,9 +34,14 @@ export {
33
34
  formatValidationErrors,
34
35
  generateSessionId,
35
36
  getDefaultConfig,
37
+ getLocalConfigPath,
38
+ getLocalSessionsDir,
39
+ getProjectDir,
40
+ getQueriesDir,
36
41
  getResumableProviders,
37
42
  globalRegistry,
38
43
  isAuthError,
44
+ isInsideProject,
39
45
  isProviderError,
40
46
  isRateLimitError,
41
47
  listSessions,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
@@ -1,4 +1,4 @@
1
- const version = "0.22.0";
1
+ const version = "0.23.1";
2
2
  const pkg = {
3
3
  version
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ncukondo/search-hub",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "A CLI tool for systematic literature searching across multiple academic databases",
5
5
  "type": "module",
6
6
  "engines": {