@ncukondo/search-hub 0.20.1 → 0.21.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.
package/README.md CHANGED
@@ -44,14 +44,14 @@ This creates config and data directories in platform-specific locations:
44
44
 
45
45
  2. Create a query file:
46
46
  ```bash
47
- search-hub query init -o query.yaml
47
+ search-hub query init "my review"
48
48
  ```
49
49
 
50
- This generates a YAML template with JSON Schema support for editor autocompletion. Edit it to define your search:
50
+ This creates `queries/my-review.yaml` with JSON Schema support for editor autocompletion. Edit it to define your search:
51
51
 
52
52
  ```yaml
53
53
  # yaml-language-server: $schema=./query.schema.json
54
- name: my_review
54
+ name: "my review"
55
55
  description: "Literature search for scoping review"
56
56
 
57
57
  query:
@@ -70,14 +70,14 @@ filters:
70
70
 
71
71
  3. Validate the query:
72
72
  ```bash
73
- search-hub query validate query.yaml
73
+ search-hub query validate my-review
74
74
  ```
75
75
 
76
- This checks structure, validates controlled vocabulary terms (MeSH, ERIC descriptors, Emtree) against external APIs, and suggests corrections for typos.
76
+ This checks structure, validates controlled vocabulary terms (MeSH, ERIC descriptors, Emtree) against external APIs, and suggests corrections for typos. The query name is automatically resolved to `queries/my-review.yaml`.
77
77
 
78
78
  4. Run search:
79
79
  ```bash
80
- search-hub search query.yaml
80
+ search-hub search my-review
81
81
  ```
82
82
 
83
83
  5. Export results:
@@ -91,31 +91,33 @@ Developing an effective search query is iterative. Start broad, then refine base
91
91
 
92
92
  ### Workflow
93
93
 
94
- 1. **Start with a broad query** - Get an initial set of results:
94
+ 1. **Create a query** - Start with a template:
95
95
  ```bash
96
- search-hub search query-v1.yaml --max-results 100
96
+ search-hub query init "my review"
97
+ # Creates queries/my-review.yaml
97
98
  ```
98
99
 
99
- 2. **Review initial results** - Check titles to assess quality:
100
+ 2. **Check hit counts** - Preview before downloading:
100
101
  ```bash
101
- search-hub results <session-v1> --limit 50
102
- search-hub results <session-v1> -q "title:diabetes year:2023-2025"
102
+ search-hub search my-review --count-only
103
103
  ```
104
104
 
105
- 3. **Check coverage** - Verify known relevant articles are captured:
105
+ 3. **Run the search** - When counts look good:
106
106
  ```bash
107
- search-hub check <session-v1> --file known-articles.txt
107
+ search-hub search my-review
108
108
  ```
109
109
 
110
- 4. **Refine the query** - Copy and modify your query file:
110
+ 4. **Review results** - Check titles to assess quality:
111
111
  ```bash
112
- cp query-v1.yaml query-v2.yaml
113
- # Edit query-v2.yaml to add/remove terms, adjust filters
112
+ search-hub results <session-id> --limit 50
113
+ search-hub results <session-id> -q "title:diabetes year:2023-2025"
114
114
  ```
115
115
 
116
- 5. **Run the refined search**:
116
+ 5. **Refine and re-run** - Edit the query file, then iterate:
117
117
  ```bash
118
- search-hub search query-v2.yaml --max-results 100
118
+ $EDITOR queries/my-review.yaml
119
+ search-hub search my-review --count-only # Re-check counts
120
+ search-hub search my-review # Execute full search
119
121
  ```
120
122
 
121
123
  6. **Compare results with diff** - See what changed:
@@ -128,22 +130,22 @@ Developing an effective search query is iterative. Start broad, then refine base
128
130
 
129
131
  - **Use `--count-only` first**: Check hit counts before downloading full results.
130
132
  ```bash
131
- search-hub search query.yaml --count-only
133
+ search-hub search my-review --count-only
132
134
  ```
133
135
 
134
136
  - **Use `--preview`** to see hit counts with sample titles:
135
137
  ```bash
136
- search-hub search query.yaml --preview
138
+ search-hub search my-review --preview
137
139
  ```
138
140
 
139
141
  - **Use `--dry-run`** to preview translations: See exactly what query each database will receive.
140
142
  ```bash
141
- search-hub search query.yaml --dry-run
143
+ search-hub search my-review --dry-run
142
144
  ```
143
145
 
144
146
  - **Compare removed articles carefully**: When narrowing a search, `--show removed` reveals what you're excluding. If important papers are removed, your refinement may be too aggressive.
145
147
 
146
- - **Keep query versions**: Save each iteration (v1, v2, v3) to track your development process and maintain reproducibility.
148
+ - **Track iterations**: Use `query assess` and `query log` to record and review your refinement history.
147
149
 
148
150
  ## Fulltext Retrieval
149
151
 
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqGD;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiDzE"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAqGD;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAmDzE"}
@@ -99,6 +99,7 @@ async function init(options = {}) {
99
99
  } = options;
100
100
  const configPath = join(configDir, "config.toml");
101
101
  const sessionsDir = join(dataDir, "sessions");
102
+ const queriesDir = join(dataDir, "queries");
102
103
  const result = {
103
104
  success: false,
104
105
  configPath,
@@ -118,6 +119,7 @@ async function init(options = {}) {
118
119
  }
119
120
  await mkdir(configDir, { recursive: true });
120
121
  await mkdir(sessionsDir, { recursive: true });
122
+ await mkdir(queriesDir, { recursive: true });
121
123
  const defaultConfig = getDefaultConfig();
122
124
  defaultConfig.session.directory = sessionsDir;
123
125
  const configContent = generateConfigContent(defaultConfig);
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sources":["../../../src/cli/commands/init.ts"],"sourcesContent":["import { mkdir, writeFile, access, constants } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { stringify as stringifyToml } from '@iarna/toml';\nimport { getDefaultConfig } from '../../config/index.js';\nimport { getConfigDir, getDataDir } from '../../config/paths.js';\nimport type { Config } from '../../config/index.js';\n\n/**\n * Options for the init command.\n */\nexport interface InitOptions {\n /** Config directory (defaults to platform-specific via getConfigDir()) */\n configDir?: string;\n /** Data directory (defaults to platform-specific via getDataDir()) */\n dataDir?: string;\n /** Force overwrite if directory already exists */\n force?: boolean;\n}\n\n/**\n * Result of the init command.\n */\nexport interface InitResult {\n /** Whether initialization was successful */\n success: boolean;\n /** Path to the created config file */\n configPath: string;\n /** Path to the sessions directory */\n sessionsDir: string;\n /** Path to the config directory */\n configDir: string;\n /** Path to the data directory */\n dataDir: string;\n /** Whether files already existed (only when success=false) */\n alreadyExists?: boolean;\n /** Whether existing files were overwritten (only when force=true) */\n overwritten?: boolean;\n /** Message describing the result */\n message?: string;\n}\n\n/**\n * Check if a file or directory exists.\n */\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Convert Config to TOML-compatible object.\n * Removes undefined values and converts to the expected format.\n */\nfunction configToToml(config: Config): Record<string, unknown> {\n return {\n session: {\n directory: config.session.directory,\n },\n log: {\n level: config.log.level,\n },\n output: {\n color: config.output.color,\n progress_bar: config.output.progress_bar,\n },\n providers: {\n pubmed: {\n enabled: config.providers.pubmed.enabled,\n api_key: config.providers.pubmed.api_key ?? '',\n email: config.providers.pubmed.email ?? '',\n rate_limit: config.providers.pubmed.rate_limit,\n timeout: config.providers.pubmed.timeout,\n retries: config.providers.pubmed.retries,\n max_results: config.providers.pubmed.max_results,\n },\n eric: {\n enabled: config.providers.eric.enabled,\n rate_limit: config.providers.eric.rate_limit,\n timeout: config.providers.eric.timeout,\n retries: config.providers.eric.retries,\n max_results: config.providers.eric.max_results,\n },\n arxiv: {\n enabled: config.providers.arxiv.enabled,\n rate_limit: config.providers.arxiv.rate_limit,\n timeout: config.providers.arxiv.timeout,\n retries: config.providers.arxiv.retries,\n max_results: config.providers.arxiv.max_results,\n },\n scopus: {\n enabled: config.providers.scopus.enabled,\n api_key: config.providers.scopus.api_key ?? '',\n inst_token: config.providers.scopus.inst_token ?? '',\n rate_limit: config.providers.scopus.rate_limit,\n timeout: config.providers.scopus.timeout,\n retries: config.providers.scopus.retries,\n max_results: config.providers.scopus.max_results,\n },\n wos: {\n enabled: config.providers.wos.enabled,\n api_key: config.providers.wos.api_key ?? '',\n rate_limit: config.providers.wos.rate_limit,\n timeout: config.providers.wos.timeout,\n retries: config.providers.wos.retries,\n max_results: config.providers.wos.max_results,\n },\n embase: {\n enabled: config.providers.embase.enabled,\n rate_limit: config.providers.embase.rate_limit,\n timeout: config.providers.embase.timeout,\n retries: config.providers.embase.retries,\n max_results: config.providers.embase.max_results,\n },\n },\n integration: {\n reference_manager: {\n enabled: config.integration.reference_manager.enabled,\n command: config.integration.reference_manager.command,\n auto_register: config.integration.reference_manager.auto_register,\n },\n },\n };\n}\n\n/**\n * Generate TOML config file content with comments.\n */\nfunction generateConfigContent(config: Config): string {\n const tomlObj = configToToml(config);\n const header = `# search-hub configuration file\n# See: https://github.com/search-hub/search-hub for documentation\n\n`;\n return header + stringifyToml(tomlObj as Parameters<typeof stringifyToml>[0]);\n}\n\n/**\n * Initialize the search-hub configuration directory.\n *\n * Creates:\n * - Config directory with config.toml\n * - Data directory with sessions/ subdirectory\n *\n * On Linux (XDG):\n * - ~/.config/search-hub/config.toml\n * - ~/.local/share/search-hub/sessions/\n */\nexport async function init(options: InitOptions = {}): Promise<InitResult> {\n const {\n configDir = getConfigDir(),\n dataDir = getDataDir(),\n force = false,\n } = options;\n\n const configPath = join(configDir, 'config.toml');\n const sessionsDir = join(dataDir, 'sessions');\n\n const result: InitResult = {\n success: false,\n configPath,\n sessionsDir,\n configDir,\n dataDir,\n };\n\n // Check if config directory already exists\n if (await exists(configDir)) {\n if (!force) {\n return {\n ...result,\n alreadyExists: true,\n message: `Configuration directory already exists at ${configDir}. Use --force to overwrite.`,\n };\n }\n result.overwritten = true;\n }\n\n // Create directories\n await mkdir(configDir, { recursive: true });\n await mkdir(sessionsDir, { recursive: true });\n\n // Generate and write config file\n // Use the default sessions directory for the saved config\n const defaultConfig = getDefaultConfig();\n // Set session.directory to the actual sessions path for the config file\n defaultConfig.session.directory = sessionsDir;\n const configContent = generateConfigContent(defaultConfig);\n await writeFile(configPath, configContent, 'utf-8');\n\n return {\n ...result,\n success: true,\n message: result.overwritten\n ? `Configuration overwritten at ${configDir}`\n : `Configuration created at ${configDir}`,\n };\n}\n"],"names":["stringifyToml"],"mappings":";;;;;;;AA4CA,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,UAAM,OAAO,MAAM,UAAU,IAAI;AACjC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,SAAS,aAAa,QAAyC;AAC7D,SAAO;AAAA,IACL,SAAS;AAAA,MACP,WAAW,OAAO,QAAQ;AAAA,IAAA;AAAA,IAE5B,KAAK;AAAA,MACH,OAAO,OAAO,IAAI;AAAA,IAAA;AAAA,IAEpB,QAAQ;AAAA,MACN,OAAO,OAAO,OAAO;AAAA,MACrB,cAAc,OAAO,OAAO;AAAA,IAAA;AAAA,IAE9B,WAAW;AAAA,MACT,QAAQ;AAAA,QACN,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,SAAS,OAAO,UAAU,OAAO,WAAW;AAAA,QAC5C,OAAO,OAAO,UAAU,OAAO,SAAS;AAAA,QACxC,YAAY,OAAO,UAAU,OAAO;AAAA,QACpC,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,aAAa,OAAO,UAAU,OAAO;AAAA,MAAA;AAAA,MAEvC,MAAM;AAAA,QACJ,SAAS,OAAO,UAAU,KAAK;AAAA,QAC/B,YAAY,OAAO,UAAU,KAAK;AAAA,QAClC,SAAS,OAAO,UAAU,KAAK;AAAA,QAC/B,SAAS,OAAO,UAAU,KAAK;AAAA,QAC/B,aAAa,OAAO,UAAU,KAAK;AAAA,MAAA;AAAA,MAErC,OAAO;AAAA,QACL,SAAS,OAAO,UAAU,MAAM;AAAA,QAChC,YAAY,OAAO,UAAU,MAAM;AAAA,QACnC,SAAS,OAAO,UAAU,MAAM;AAAA,QAChC,SAAS,OAAO,UAAU,MAAM;AAAA,QAChC,aAAa,OAAO,UAAU,MAAM;AAAA,MAAA;AAAA,MAEtC,QAAQ;AAAA,QACN,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,SAAS,OAAO,UAAU,OAAO,WAAW;AAAA,QAC5C,YAAY,OAAO,UAAU,OAAO,cAAc;AAAA,QAClD,YAAY,OAAO,UAAU,OAAO;AAAA,QACpC,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,aAAa,OAAO,UAAU,OAAO;AAAA,MAAA;AAAA,MAEvC,KAAK;AAAA,QACH,SAAS,OAAO,UAAU,IAAI;AAAA,QAC9B,SAAS,OAAO,UAAU,IAAI,WAAW;AAAA,QACzC,YAAY,OAAO,UAAU,IAAI;AAAA,QACjC,SAAS,OAAO,UAAU,IAAI;AAAA,QAC9B,SAAS,OAAO,UAAU,IAAI;AAAA,QAC9B,aAAa,OAAO,UAAU,IAAI;AAAA,MAAA;AAAA,MAEpC,QAAQ;AAAA,QACN,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,YAAY,OAAO,UAAU,OAAO;AAAA,QACpC,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,SAAS,OAAO,UAAU,OAAO;AAAA,QACjC,aAAa,OAAO,UAAU,OAAO;AAAA,MAAA;AAAA,IACvC;AAAA,IAEF,aAAa;AAAA,MACX,mBAAmB;AAAA,QACjB,SAAS,OAAO,YAAY,kBAAkB;AAAA,QAC9C,SAAS,OAAO,YAAY,kBAAkB;AAAA,QAC9C,eAAe,OAAO,YAAY,kBAAkB;AAAA,MAAA;AAAA,IACtD;AAAA,EACF;AAEJ;AAKA,SAAS,sBAAsB,QAAwB;AACrD,QAAM,UAAU,aAAa,MAAM;AACnC,QAAM,SAAS;AAAA;AAAA;AAAA;AAIf,SAAO,SAASA,UAAc,OAA8C;AAC9E;AAaA,eAAsB,KAAK,UAAuB,IAAyB;AACzE,QAAM;AAAA,IACJ,YAAY,aAAA;AAAA,IACZ,UAAU,WAAA;AAAA,IACV,QAAQ;AAAA,EAAA,IACN;AAEJ,QAAM,aAAa,KAAK,WAAW,aAAa;AAChD,QAAM,cAAc,KAAK,SAAS,UAAU;AAE5C,QAAM,SAAqB;AAAA,IACzB,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAIF,MAAI,MAAM,OAAO,SAAS,GAAG;AAC3B,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,QACL,GAAG;AAAA,QACH,eAAe;AAAA,QACf,SAAS,6CAA6C,SAAS;AAAA,MAAA;AAAA,IAEnE;AACA,WAAO,cAAc;AAAA,EACvB;AAGA,QAAM,MAAM,WAAW,EAAE,WAAW,MAAM;AAC1C,QAAM,MAAM,aAAa,EAAE,WAAW,MAAM;AAI5C,QAAM,gBAAgB,iBAAA;AAEtB,gBAAc,QAAQ,YAAY;AAClC,QAAM,gBAAgB,sBAAsB,aAAa;AACzD,QAAM,UAAU,YAAY,eAAe,OAAO;AAElD,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS;AAAA,IACT,SAAS,OAAO,cACZ,gCAAgC,SAAS,KACzC,4BAA4B,SAAS;AAAA,EAAA;AAE7C;"}
1
+ {"version":3,"file":"init.js","sources":["../../../src/cli/commands/init.ts"],"sourcesContent":["import { mkdir, writeFile, access, constants } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { stringify as stringifyToml } from '@iarna/toml';\nimport { getDefaultConfig } from '../../config/index.js';\nimport { getConfigDir, getDataDir } from '../../config/paths.js';\nimport type { Config } from '../../config/index.js';\n\n/**\n * Options for the init command.\n */\nexport interface InitOptions {\n /** Config directory (defaults to platform-specific via getConfigDir()) */\n configDir?: string;\n /** Data directory (defaults to platform-specific via getDataDir()) */\n dataDir?: string;\n /** Force overwrite if directory already exists */\n force?: boolean;\n}\n\n/**\n * Result of the init command.\n */\nexport interface InitResult {\n /** Whether initialization was successful */\n success: boolean;\n /** Path to the created config file */\n configPath: string;\n /** Path to the sessions directory */\n sessionsDir: string;\n /** Path to the config directory */\n configDir: string;\n /** Path to the data directory */\n dataDir: string;\n /** Whether files already existed (only when success=false) */\n alreadyExists?: boolean;\n /** Whether existing files were overwritten (only when force=true) */\n overwritten?: boolean;\n /** Message describing the result */\n message?: string;\n}\n\n/**\n * Check if a file or directory exists.\n */\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Convert Config to TOML-compatible object.\n * Removes undefined values and converts to the expected format.\n */\nfunction configToToml(config: Config): Record<string, unknown> {\n return {\n session: {\n directory: config.session.directory,\n },\n log: {\n level: config.log.level,\n },\n output: {\n color: config.output.color,\n progress_bar: config.output.progress_bar,\n },\n providers: {\n pubmed: {\n enabled: config.providers.pubmed.enabled,\n api_key: config.providers.pubmed.api_key ?? '',\n email: config.providers.pubmed.email ?? '',\n rate_limit: config.providers.pubmed.rate_limit,\n timeout: config.providers.pubmed.timeout,\n retries: config.providers.pubmed.retries,\n max_results: config.providers.pubmed.max_results,\n },\n eric: {\n enabled: config.providers.eric.enabled,\n rate_limit: config.providers.eric.rate_limit,\n timeout: config.providers.eric.timeout,\n retries: config.providers.eric.retries,\n max_results: config.providers.eric.max_results,\n },\n arxiv: {\n enabled: config.providers.arxiv.enabled,\n rate_limit: config.providers.arxiv.rate_limit,\n timeout: config.providers.arxiv.timeout,\n retries: config.providers.arxiv.retries,\n max_results: config.providers.arxiv.max_results,\n },\n scopus: {\n enabled: config.providers.scopus.enabled,\n api_key: config.providers.scopus.api_key ?? '',\n inst_token: config.providers.scopus.inst_token ?? '',\n rate_limit: config.providers.scopus.rate_limit,\n timeout: config.providers.scopus.timeout,\n retries: config.providers.scopus.retries,\n max_results: config.providers.scopus.max_results,\n },\n wos: {\n enabled: config.providers.wos.enabled,\n api_key: config.providers.wos.api_key ?? '',\n rate_limit: config.providers.wos.rate_limit,\n timeout: config.providers.wos.timeout,\n retries: config.providers.wos.retries,\n max_results: config.providers.wos.max_results,\n },\n embase: {\n enabled: config.providers.embase.enabled,\n rate_limit: config.providers.embase.rate_limit,\n timeout: config.providers.embase.timeout,\n retries: config.providers.embase.retries,\n max_results: config.providers.embase.max_results,\n },\n },\n integration: {\n reference_manager: {\n enabled: config.integration.reference_manager.enabled,\n command: config.integration.reference_manager.command,\n auto_register: config.integration.reference_manager.auto_register,\n },\n },\n };\n}\n\n/**\n * Generate TOML config file content with comments.\n */\nfunction generateConfigContent(config: Config): string {\n const tomlObj = configToToml(config);\n const header = `# search-hub configuration file\n# See: https://github.com/search-hub/search-hub for documentation\n\n`;\n return header + stringifyToml(tomlObj as Parameters<typeof stringifyToml>[0]);\n}\n\n/**\n * Initialize the search-hub configuration directory.\n *\n * Creates:\n * - Config directory with config.toml\n * - Data directory with sessions/ subdirectory\n *\n * On Linux (XDG):\n * - ~/.config/search-hub/config.toml\n * - ~/.local/share/search-hub/sessions/\n */\nexport async function init(options: InitOptions = {}): Promise<InitResult> {\n const {\n configDir = getConfigDir(),\n dataDir = getDataDir(),\n force = false,\n } = options;\n\n const configPath = join(configDir, 'config.toml');\n const sessionsDir = join(dataDir, 'sessions');\n const queriesDir = join(dataDir, 'queries');\n\n const result: InitResult = {\n success: false,\n configPath,\n sessionsDir,\n configDir,\n dataDir,\n };\n\n // Check if config directory already exists\n if (await exists(configDir)) {\n if (!force) {\n return {\n ...result,\n alreadyExists: true,\n message: `Configuration directory already exists at ${configDir}. Use --force to overwrite.`,\n };\n }\n result.overwritten = true;\n }\n\n // Create directories\n await mkdir(configDir, { recursive: true });\n await mkdir(sessionsDir, { recursive: true });\n await mkdir(queriesDir, { recursive: true });\n\n // Generate and write config file\n // Use the default sessions directory for the saved config\n const defaultConfig = getDefaultConfig();\n // Set session.directory to the actual sessions path for the config file\n defaultConfig.session.directory = sessionsDir;\n const configContent = generateConfigContent(defaultConfig);\n await writeFile(configPath, configContent, 'utf-8');\n\n return {\n ...result,\n success: true,\n message: result.overwritten\n ? `Configuration overwritten at ${configDir}`\n : `Configuration created at ${configDir}`,\n };\n}\n"],"names":["stringifyToml"],"mappings":"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"}
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Sanitize a title string into a safe filename (without extension).
3
+ */
4
+ export declare function sanitizeForFilename(title: string): string;
1
5
  /**
2
6
  * The YAML template string with comments preserved.
3
7
  * This is a raw string (not generated by a YAML library) so comments are kept.
@@ -6,22 +10,29 @@ export declare const QUERY_SCHEMA_FILENAME = "query.schema.json";
6
10
  /**
7
11
  * Generate the query template YAML string.
8
12
  *
13
+ * @param title - Optional title to set as the query name
9
14
  * @returns The YAML template string with comments
10
15
  */
11
- export declare function generateQueryTemplate(): string;
16
+ export declare function generateQueryTemplate(title?: string): string;
17
+ export declare const QUERIES_DIR = "queries";
18
+ export interface WriteQueryTemplateOptions {
19
+ title: string;
20
+ output?: string | undefined;
21
+ stdout?: boolean | undefined;
22
+ force?: boolean | undefined;
23
+ cwd?: string | undefined;
24
+ }
12
25
  /**
13
26
  * Write the query template to a file or return it as a message.
14
27
  *
15
- * @param options - Output options
16
- * @param options.output - File path to write to (if omitted, returns template in message)
17
- * @param options.force - Whether to overwrite existing files
18
- * @returns Result with success status and message
28
+ * Output priority:
29
+ * 1. --stdout return template as message (no file)
30
+ * 2. -o <path> → write to that path
31
+ * 3. default write to queries/<sanitized-title>.yaml
19
32
  */
20
- export declare function writeQueryTemplate(options: {
21
- output?: string;
22
- force?: boolean;
23
- }): Promise<{
33
+ export declare function writeQueryTemplate(options: WriteQueryTemplateOptions): Promise<{
24
34
  success: boolean;
25
35
  message: string;
36
+ outputPath?: string;
26
37
  }>;
27
38
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AA2DzD;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAkCjD"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/init.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AA2DzD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAI5D;AAED,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwChJ"}
@@ -1,6 +1,13 @@
1
- import { access, writeFile } from "node:fs/promises";
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
2
  import { join, dirname } from "node:path";
3
3
  import { generateQueryJSONSchema } from "../../../query/json-schema.js";
4
+ function sanitizeForFilename(title) {
5
+ const result = title.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
6
+ if (!result) {
7
+ throw new Error("Title produces an empty filename after sanitization");
8
+ }
9
+ return result;
10
+ }
4
11
  const QUERY_SCHEMA_FILENAME = "query.schema.json";
5
12
  const QUERY_TEMPLATE = `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}
6
13
  name: my_search
@@ -57,36 +64,44 @@ query:
57
64
  # exclude:
58
65
  # - "Letter"
59
66
  `;
60
- function generateQueryTemplate() {
61
- return QUERY_TEMPLATE;
67
+ function generateQueryTemplate(title) {
68
+ if (!title) return QUERY_TEMPLATE;
69
+ const escaped = title.replace(/"/g, '\\"');
70
+ return QUERY_TEMPLATE.replace("name: my_search", `name: "${escaped}"`);
62
71
  }
72
+ const QUERIES_DIR = "queries";
63
73
  async function writeQueryTemplate(options) {
64
- const template = generateQueryTemplate();
65
- if (!options.output) {
74
+ const template = generateQueryTemplate(options.title);
75
+ if (options.stdout) {
66
76
  return { success: true, message: template };
67
77
  }
78
+ const outputPath = options.output ?? join(options.cwd ?? process.cwd(), QUERIES_DIR, `${sanitizeForFilename(options.title)}.yaml`);
68
79
  if (!options.force) {
69
80
  try {
70
- await access(options.output);
81
+ await access(outputPath);
71
82
  return {
72
83
  success: false,
73
- message: `File already exists: ${options.output}. Use --force to overwrite.`
84
+ message: `File already exists: ${outputPath}. Use --force to overwrite.`
74
85
  };
75
86
  } catch {
76
87
  }
77
88
  }
78
- await writeFile(options.output, template, "utf-8");
79
- const schemaPath = join(dirname(options.output), QUERY_SCHEMA_FILENAME);
89
+ await mkdir(dirname(outputPath), { recursive: true });
90
+ await writeFile(outputPath, template, "utf-8");
91
+ const schemaPath = join(dirname(outputPath), QUERY_SCHEMA_FILENAME);
80
92
  const jsonSchema = generateQueryJSONSchema();
81
93
  await writeFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + "\n", "utf-8");
82
94
  return {
83
95
  success: true,
84
- message: `Template written to ${options.output}`
96
+ message: `Created: ${outputPath}`,
97
+ outputPath
85
98
  };
86
99
  }
87
100
  export {
101
+ QUERIES_DIR,
88
102
  QUERY_SCHEMA_FILENAME,
89
103
  generateQueryTemplate,
104
+ sanitizeForFilename,
90
105
  writeQueryTemplate
91
106
  };
92
107
  //# sourceMappingURL=init.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - id: concept-1 # Unique block identifier (for provider replacements)\\n\" +\n \" field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" # eric: # ERIC Descriptors (optional, ERIC only)\\n\" +\n \" # - \\\"ERIC Descriptor\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - id: concept-2\\n\" +\n \" # field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# providers: # Optional: per-database block replacements & filter additions\\n\" +\n \"# pubmed:\\n\" +\n \"# replaces:\\n\" +\n \"# concept-1: # Replace block by id\\n\" +\n \"# field: keyword\\n\" +\n \"# terms:\\n\" +\n \"# mesh:\\n\" +\n \"# - \\\"MeSH Heading\\\"\\n\" +\n \"# operator: OR\\n\" +\n \"# adds:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(): string {\n return QUERY_TEMPLATE;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * @param options - Output options\n * @param options.output - File path to write to (if omitted, returns template in message)\n * @param options.force - Whether to overwrite existing files\n * @returns Result with success status and message\n */\nexport async function writeQueryTemplate(options: {\n output?: string;\n force?: boolean;\n}): Promise<{ success: boolean; message: string }> {\n const template = generateQueryTemplate();\n\n if (!options.output) {\n // No output file specified, return template as message\n return { success: true, message: template };\n }\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(options.output);\n // File exists and force is not set\n return {\n success: false,\n message: `File already exists: ${options.output}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Write to file\n await fsWriteFile(options.output, template, \"utf-8\");\n\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(options.output), QUERY_SCHEMA_FILENAME);\n const jsonSchema = generateQueryJSONSchema();\n await fsWriteFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + \"\\n\", \"utf-8\");\n\n return {\n success: true,\n message: `Template written to ${options.output}`,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;AAaO,MAAM,wBAAwB;AAGrC,MAAM,iBACJ,qCAAqC,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4DrD,SAAS,wBAAgC;AAC9C,SAAO;AACT;AAUA,eAAsB,mBAAmB,SAGU;AACjD,QAAM,WAAW,sBAAA;AAEjB,MAAI,CAAC,QAAQ,QAAQ;AAEnB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,QAAQ,MAAM;AAE3B,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,QAAQ,MAAM;AAAA,MAAA;AAAA,IAEnD,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAMA,UAAY,QAAQ,QAAQ,UAAU,OAAO;AAGnD,QAAM,aAAa,KAAK,QAAQ,QAAQ,MAAM,GAAG,qBAAqB;AACtE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,uBAAuB,QAAQ,MAAM;AAAA,EAAA;AAElD;"}
1
+ {"version":3,"file":"init.js","sources":["../../../../src/cli/commands/query/init.ts"],"sourcesContent":["/**\n * Query init command implementation.\n *\n * Generates a skeleton YAML query file with helpful comments.\n */\nimport { writeFile as fsWriteFile, access, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { generateQueryJSONSchema } from \"../../../query/json-schema.js\";\n\n/**\n * Sanitize a title string into a safe filename (without extension).\n */\nexport function sanitizeForFilename(title: string): string {\n const result = title\n .trim()\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9_-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n if (!result) {\n throw new Error('Title produces an empty filename after sanitization');\n }\n return result;\n}\n\n/**\n * The YAML template string with comments preserved.\n * This is a raw string (not generated by a YAML library) so comments are kept.\n */\nexport const QUERY_SCHEMA_FILENAME = \"query.schema.json\";\n\n// prettier-ignore\nconst QUERY_TEMPLATE =\n `# yaml-language-server: $schema=./${QUERY_SCHEMA_FILENAME}\\n` +\n \"name: my_search\\n\" +\n \"description: \\\"\\\"\\n\" +\n \"\\n\" +\n \"query:\\n\" +\n \" - id: concept-1 # Unique block identifier (for provider replacements)\\n\" +\n \" field: title_abstract # title, abstract, title_abstract, author, keyword, all\\n\" +\n \" terms:\\n\" +\n \" keywords:\\n\" +\n \" - \\\"search term 1\\\"\\n\" +\n \" - \\\"search term 2\\\"\\n\" +\n \" # mesh: # PubMed MeSH terms (optional)\\n\" +\n \" # - \\\"MeSH Heading\\\"\\n\" +\n \" # eric: # ERIC Descriptors (optional, ERIC only)\\n\" +\n \" # - \\\"ERIC Descriptor\\\"\\n\" +\n \" exclude: [] # Terms to exclude (NOT operator)\\n\" +\n \" # Tip: Use exclude to filter out false matches from short keywords/acronyms\\n\" +\n \" # exclude:\\n\" +\n \" # - \\\"unwanted term\\\"\\n\" +\n \" # - \\\"irrelevant topic\\\"\\n\" +\n \" operator: OR # How to combine terms within this block\\n\" +\n \"\\n\" +\n \" # Add more blocks — blocks are AND'd together\\n\" +\n \" # - id: concept-2\\n\" +\n \" # field: title_abstract\\n\" +\n \" # terms:\\n\" +\n \" # keywords:\\n\" +\n \" # - \\\"another term\\\"\\n\" +\n \" # operator: OR\\n\" +\n \"\\n\" +\n \"# filters: # Optional: apply to all databases\\n\" +\n \"# year_from: 2020\\n\" +\n \"# year_to: 2026\\n\" +\n \"# language:\\n\" +\n \"# - en\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Review\\\"\\n\" +\n \"# - \\\"Comment\\\"\\n\" +\n \"\\n\" +\n \"# providers: # Optional: per-database block replacements & filter additions\\n\" +\n \"# pubmed:\\n\" +\n \"# replaces:\\n\" +\n \"# concept-1: # Replace block by id\\n\" +\n \"# field: keyword\\n\" +\n \"# terms:\\n\" +\n \"# mesh:\\n\" +\n \"# - \\\"MeSH Heading\\\"\\n\" +\n \"# operator: OR\\n\" +\n \"# adds:\\n\" +\n \"# filters:\\n\" +\n \"# publication_types:\\n\" +\n \"# exclude:\\n\" +\n \"# - \\\"Letter\\\"\\n\";\n\n/**\n * Generate the query template YAML string.\n *\n * @param title - Optional title to set as the query name\n * @returns The YAML template string with comments\n */\nexport function generateQueryTemplate(title?: string): string {\n if (!title) return QUERY_TEMPLATE;\n const escaped = title.replace(/\"/g, '\\\\\"');\n return QUERY_TEMPLATE.replace('name: my_search', `name: \"${escaped}\"`);\n}\n\nexport const QUERIES_DIR = \"queries\";\n\nexport interface WriteQueryTemplateOptions {\n title: string;\n output?: string | undefined;\n stdout?: boolean | undefined;\n force?: boolean | undefined;\n cwd?: string | undefined;\n}\n\n/**\n * Write the query template to a file or return it as a message.\n *\n * Output priority:\n * 1. --stdout return template as message (no file)\n * 2. -o <path> → write to that path\n * 3. default write to queries/<sanitized-title>.yaml\n */\nexport async function writeQueryTemplate(options: WriteQueryTemplateOptions): Promise<{ success: boolean; message: string; outputPath?: string }> {\n const template = generateQueryTemplate(options.title);\n\n if (options.stdout) {\n return { success: true, message: template };\n }\n\n // Determine output path\n const outputPath = options.output\n ?? join(options.cwd ?? process.cwd(), QUERIES_DIR, `${sanitizeForFilename(options.title)}.yaml`);\n\n // Check if file exists (unless force is set)\n if (!options.force) {\n try {\n await access(outputPath);\n return {\n success: false,\n message: `File already exists: ${outputPath}. Use --force to overwrite.`,\n };\n } catch {\n // File does not exist, proceed\n }\n }\n\n // Ensure parent directory exists\n await mkdir(dirname(outputPath), { recursive: true });\n\n // Write template\n await fsWriteFile(outputPath, template, \"utf-8\");\n\n // Generate JSON Schema file alongside output\n const schemaPath = join(dirname(outputPath), QUERY_SCHEMA_FILENAME);\n const jsonSchema = generateQueryJSONSchema();\n await fsWriteFile(schemaPath, JSON.stringify(jsonSchema, null, 2) + \"\\n\", \"utf-8\");\n\n return {\n success: true,\n message: `Created: ${outputPath}`,\n outputPath,\n };\n}\n"],"names":["fsWriteFile"],"mappings":";;;AAYO,SAAS,oBAAoB,OAAuB;AACzD,QAAM,SAAS,MACZ,KAAA,EACA,cACA,QAAQ,QAAQ,GAAG,EACnB,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE;AACvB,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;AAMO,MAAM,wBAAwB;AAGrC,MAAM,iBACJ,qCAAqC,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DrD,SAAS,sBAAsB,OAAwB;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,MAAM,QAAQ,MAAM,KAAK;AACzC,SAAO,eAAe,QAAQ,mBAAmB,UAAU,OAAO,GAAG;AACvE;AAEO,MAAM,cAAc;AAkB3B,eAAsB,mBAAmB,SAAyG;AAChJ,QAAM,WAAW,sBAAsB,QAAQ,KAAK;AAEpD,MAAI,QAAQ,QAAQ;AAClB,WAAO,EAAE,SAAS,MAAM,SAAS,SAAA;AAAA,EACnC;AAGA,QAAM,aAAa,QAAQ,UACtB,KAAK,QAAQ,OAAO,QAAQ,IAAA,GAAO,aAAa,GAAG,oBAAoB,QAAQ,KAAK,CAAC,OAAO;AAGjG,MAAI,CAAC,QAAQ,OAAO;AAClB,QAAI;AACF,YAAM,OAAO,UAAU;AACvB,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,wBAAwB,UAAU;AAAA,MAAA;AAAA,IAE/C,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,QAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,MAAM;AAGpD,QAAMA,UAAY,YAAY,UAAU,OAAO;AAG/C,QAAM,aAAa,KAAK,QAAQ,UAAU,GAAG,qBAAqB;AAClE,QAAM,aAAa,wBAAA;AACnB,QAAMA,UAAY,YAAY,KAAK,UAAU,YAAY,MAAM,CAAC,IAAI,MAAM,OAAO;AAEjF,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,YAAY,UAAU;AAAA,IAC/B;AAAA,EAAA;AAEJ;"}
@@ -0,0 +1,5 @@
1
+ export declare class NotAFileError extends Error {
2
+ constructor(path: string);
3
+ }
4
+ export declare function resolveQueryFile(arg: string): Promise<string>;
5
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/query/resolve.ts"],"names":[],"mappings":"AAYA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,IAAI,EAAE,MAAM;CAIzB;AAiBD,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0CnE"}
@@ -0,0 +1,59 @@
1
+ import { stat } from "node:fs/promises";
2
+ class NotAFileError extends Error {
3
+ constructor(path) {
4
+ super(`Path is not a file: ${path}`);
5
+ this.name = "NotAFileError";
6
+ }
7
+ }
8
+ async function isFile(path) {
9
+ try {
10
+ const s = await stat(path);
11
+ if (!s.isFile()) {
12
+ throw new NotAFileError(path);
13
+ }
14
+ return true;
15
+ } catch (error) {
16
+ if (error instanceof NotAFileError) {
17
+ throw error;
18
+ }
19
+ return false;
20
+ }
21
+ }
22
+ async function resolveQueryFile(arg) {
23
+ if (await isFile(arg)) {
24
+ return arg;
25
+ }
26
+ const candidates = [];
27
+ if (!arg.endsWith(".yaml") && !arg.endsWith(".yml")) {
28
+ const withExt = `${arg}.yaml`;
29
+ candidates.push(withExt);
30
+ if (await isFile(withExt)) {
31
+ return withExt;
32
+ }
33
+ }
34
+ const basename = arg.endsWith(".yaml") || arg.endsWith(".yml") ? arg : `${arg}.yaml`;
35
+ const inQueries = `queries/${basename}`;
36
+ candidates.push(inQueries);
37
+ if (await isFile(inQueries)) {
38
+ return inQueries;
39
+ }
40
+ if (!arg.endsWith(".yaml") && !arg.endsWith(".yml")) {
41
+ const inQueriesYml = `queries/${arg}.yml`;
42
+ candidates.push(inQueriesYml);
43
+ if (await isFile(inQueriesYml)) {
44
+ return inQueriesYml;
45
+ }
46
+ }
47
+ const tried = [`./${arg}`, ...candidates.map((c) => `./${c}`)];
48
+ throw new Error(
49
+ `Query file not found: "${arg}"
50
+ Tried:
51
+ ` + tried.map((p) => ` ${p}`).join("\n") + `
52
+ Create a new query: search-hub query init "${arg}"`
53
+ );
54
+ }
55
+ export {
56
+ NotAFileError,
57
+ resolveQueryFile
58
+ };
59
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sources":["../../../../src/cli/commands/query/resolve.ts"],"sourcesContent":["/**\n * Smart query file resolution.\n *\n * Resolution order:\n * 1. Exact path exists → use it\n * 2. <arg>.yaml exists → use it\n * 3. queries/<arg>.yaml exists → use it\n * 4. queries/<arg>.yml exists → use it\n * 5. Error with tried paths\n */\nimport { stat } from 'node:fs/promises';\n\nexport class NotAFileError extends Error {\n constructor(path: string) {\n super(`Path is not a file: ${path}`);\n this.name = 'NotAFileError';\n }\n}\n\nasync function isFile(path: string): Promise<boolean> {\n try {\n const s = await stat(path);\n if (!s.isFile()) {\n throw new NotAFileError(path);\n }\n return true;\n } catch (error) {\n if (error instanceof NotAFileError) {\n throw error;\n }\n return false;\n }\n}\n\nexport async function resolveQueryFile(arg: string): Promise<string> {\n // 1. Exact path\n if (await isFile(arg)) {\n return arg;\n }\n\n const candidates: string[] = [];\n\n // 2. arg + .yaml (skip if already ends with .yaml)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const withExt = `${arg}.yaml`;\n candidates.push(withExt);\n if (await isFile(withExt)) {\n return withExt;\n }\n }\n\n // 3. queries/<arg>.yaml\n const basename = arg.endsWith('.yaml') || arg.endsWith('.yml') ? arg : `${arg}.yaml`;\n const inQueries = `queries/${basename}`;\n candidates.push(inQueries);\n if (await isFile(inQueries)) {\n return inQueries;\n }\n\n // 4. queries/<arg>.yml (skip if arg already has extension)\n if (!arg.endsWith('.yaml') && !arg.endsWith('.yml')) {\n const inQueriesYml = `queries/${arg}.yml`;\n candidates.push(inQueriesYml);\n if (await isFile(inQueriesYml)) {\n return inQueriesYml;\n }\n }\n\n // 5. Error\n const tried = [`./${arg}`, ...candidates.map(c => `./${c}`)];\n throw new Error(\n `Query file not found: \"${arg}\"\\n` +\n ` Tried:\\n` +\n tried.map(p => ` ${p}`).join('\\n') + '\\n' +\n ` Create a new query: search-hub query init \"${arg}\"`\n );\n}\n"],"names":[],"mappings":";AAYO,MAAM,sBAAsB,MAAM;AAAA,EACvC,YAAY,MAAc;AACxB,UAAM,uBAAuB,IAAI,EAAE;AACnC,SAAK,OAAO;AAAA,EACd;AACF;AAEA,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,UAAM,IAAI,MAAM,KAAK,IAAI;AACzB,QAAI,CAAC,EAAE,UAAU;AACf,YAAM,IAAI,cAAc,IAAI;AAAA,IAC9B;AACA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,iBAAiB,eAAe;AAClC,YAAM;AAAA,IACR;AACA,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,iBAAiB,KAA8B;AAEnE,MAAI,MAAM,OAAO,GAAG,GAAG;AACrB,WAAO;AAAA,EACT;AAEA,QAAM,aAAuB,CAAA;AAG7B,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,UAAU,GAAG,GAAG;AACtB,eAAW,KAAK,OAAO;AACvB,QAAI,MAAM,OAAO,OAAO,GAAG;AACzB,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,WAAW,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,MAAM,IAAI,MAAM,GAAG,GAAG;AAC7E,QAAM,YAAY,WAAW,QAAQ;AACrC,aAAW,KAAK,SAAS;AACzB,MAAI,MAAM,OAAO,SAAS,GAAG;AAC3B,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,IAAI,SAAS,OAAO,KAAK,CAAC,IAAI,SAAS,MAAM,GAAG;AACnD,UAAM,eAAe,WAAW,GAAG;AACnC,eAAW,KAAK,YAAY;AAC5B,QAAI,MAAM,OAAO,YAAY,GAAG;AAC9B,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,QAAQ,CAAC,KAAK,GAAG,IAAI,GAAG,WAAW,IAAI,CAAA,MAAK,KAAK,CAAC,EAAE,CAAC;AAC3D,QAAM,IAAI;AAAA,IACR,0BAA0B,GAAG;AAAA;AAAA,IAE7B,MAAM,IAAI,CAAA,MAAK,OAAO,CAAC,EAAE,EAAE,KAAK,IAAI,IAAI;AAAA,+CACQ,GAAG;AAAA,EAAA;AAEvD;"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAyM5C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAiuFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAyM5C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CA+vFvC;AAED;;GAEG;AACH,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
package/dist/cli/index.js CHANGED
@@ -17,7 +17,8 @@ import { createEricCountValidator, createEmtreeCountValidator } from "../query/v
17
17
  import { createProviderInstance, executePreview, executeCountOnly, executeSearch } from "./commands/search-executor.js";
18
18
  import { translateQueryCommand, formatTranslateResult } from "./commands/query/translate.js";
19
19
  import { inspectQueryCommand, formatInspectOutput } from "./commands/query/inspect.js";
20
- import { writeQueryTemplate, generateQueryTemplate } from "./commands/query/init.js";
20
+ import { writeQueryTemplate } from "./commands/query/init.js";
21
+ import { resolveQueryFile } from "./commands/query/resolve.js";
21
22
  import { getSessionDetails, computeDeduplicationStats, formatSessionDetails, listSessionsForDisplay, formatSessionList } from "./commands/status.js";
22
23
  import { parseSearchOptions, validateSearchInput, formatShortKeywordWarning, formatDryRunOutput, formatPreviewOutput, formatCountOnlyOutput } from "./commands/search.js";
23
24
  import { parseResumeOptions, validateResumeInput, getResumableProvidersForCommand } from "./commands/resume.js";
@@ -77,10 +78,10 @@ Workflow:
77
78
  Iterate: search → results -q → check → diff Query refinement
78
79
 
79
80
  Quick Start:
80
- $ search-hub query init -o search.yaml # Create query template
81
- $ search-hub search search.yaml --count-only # Check hit counts
82
- $ search-hub search search.yaml # Execute search
83
- $ search-hub results <session> # Review titles`);
81
+ $ search-hub query init "my search" # Create query template
82
+ $ search-hub search my-search --count-only # Check hit counts
83
+ $ search-hub search my-search # Execute search
84
+ $ search-hub results <session> # Review titles`);
84
85
  program.command("init").description("Initialize configuration directory").option("-f, --force", "overwrite existing configuration", false).addHelpText("after", `
85
86
  Examples:
86
87
  $ search-hub init # Initialize with default settings
@@ -190,9 +191,10 @@ Use "search-hub query init" to generate a template.`);
190
191
  Examples:
191
192
  $ search-hub query validate ./diabetes-ai.yaml
192
193
  $ search-hub query validate ./diabetes-ai.yaml --no-vocab # Skip MeSH check
193
- $ search-hub query validate ./diabetes-ai.yaml --no-cache # Ignore cache`).action(async (file, opts) => {
194
+ $ search-hub query validate ./diabetes-ai.yaml --no-cache # Ignore cache`).action(async (fileArg, opts) => {
194
195
  const globalOpts = program.opts();
195
196
  try {
197
+ const file = await resolveQueryFile(fileArg);
196
198
  const noVocab = opts.vocab === false;
197
199
  const noCache = opts.cache === false;
198
200
  let cache;
@@ -279,9 +281,10 @@ Examples:
279
281
  queryCommand.command("translate").description("Show translated queries for each database").argument("<file>", "path to query YAML file").option("--db <provider>", "show translation for specific provider only").addHelpText("after", `
280
282
  Examples:
281
283
  $ search-hub query translate ./diabetes-ai.yaml # All databases
282
- $ search-hub query translate ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (file, options) => {
284
+ $ search-hub query translate ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (fileArg, options) => {
283
285
  const globalOpts = program.opts();
284
286
  try {
287
+ const file = await resolveQueryFile(fileArg);
285
288
  const translateOptions = options.db ? { providers: [options.db] } : {};
286
289
  const result = await translateQueryCommand(file, translateOptions);
287
290
  if (!globalOpts.quiet) {
@@ -301,9 +304,10 @@ Examples:
301
304
  queryCommand.command("inspect").description("Show how a query resolves per provider (block replacements and added filters)").argument("<file>", "path to query YAML file").option("--db <provider>", "show resolution for specific provider only").addHelpText("after", `
302
305
  Examples:
303
306
  $ search-hub query inspect ./diabetes-ai.yaml # All databases
304
- $ search-hub query inspect ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (file, options) => {
307
+ $ search-hub query inspect ./diabetes-ai.yaml --db pubmed # PubMed only`).action(async (fileArg, options) => {
305
308
  const globalOpts = program.opts();
306
309
  try {
310
+ const file = await resolveQueryFile(fileArg);
307
311
  const inspectOptions = options.db ? { providers: [options.db] } : {};
308
312
  const result = await inspectQueryCommand(file, inspectOptions);
309
313
  if (!result.success) {
@@ -328,24 +332,35 @@ Examples:
328
332
  process.exitCode = EXIT_CODES.QUERY_ERROR;
329
333
  }
330
334
  });
331
- queryCommand.command("init").description("Generate a template query YAML file").option("-o, --output <path>", "write to file (default: stdout)").option("--force", "overwrite existing file", false).action(async (options) => {
335
+ queryCommand.command("init").description("Generate a template query YAML file").argument("<title>", "query title (used for name field and filename)").option("-o, --output <path>", "write to specific file path").option("--stdout", "output to stdout instead of file").option("--force", "overwrite existing file", false).addHelpText("after", `
336
+ Examples:
337
+ $ search-hub query init "WBA pain mechanisms" # → queries/wba-pain-mechanisms.yaml
338
+ $ search-hub query init "WBA pain" -o ./custom-path.yaml # Custom output path
339
+ $ search-hub query init "WBA pain" --stdout # Print to stdout`).action(async (title, options) => {
332
340
  const globalOpts = program.opts();
333
341
  try {
334
- if (options.output) {
335
- const result = await writeQueryTemplate(options);
336
- if (!globalOpts.quiet) {
337
- if (result.success) {
338
- console.log(result.message);
339
- } else {
340
- console.error(result.message);
342
+ const result = await writeQueryTemplate({
343
+ title,
344
+ output: options.output,
345
+ stdout: options.stdout,
346
+ force: options.force
347
+ });
348
+ if (!globalOpts.quiet) {
349
+ if (result.success) {
350
+ console.log(result.message);
351
+ if (result.outputPath) {
352
+ const suggestion = formatSuggestion(getSuggestion({
353
+ command: "query init",
354
+ outputFile: result.outputPath
355
+ }));
356
+ if (suggestion) console.log("\n" + suggestion);
357
+ console.log("\nIterate: edit the same file and re-run step 3. Counts are logged automatically.");
341
358
  }
359
+ } else {
360
+ console.error(result.message);
342
361
  }
343
- process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR;
344
- } else {
345
- const template = generateQueryTemplate();
346
- console.log(template);
347
- process.exitCode = EXIT_CODES.SUCCESS;
348
362
  }
363
+ process.exitCode = result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERAL_ERROR;
349
364
  } catch (error) {
350
365
  if (!globalOpts.quiet) {
351
366
  console.error("Error:", error instanceof Error ? error.message : error);
@@ -356,9 +371,10 @@ Examples:
356
371
  queryCommand.command("assess").description("Record an assessment of the current query iteration").argument("<file>", "path to query YAML file").option("--verdict <verdict>", "assessment verdict (e.g., reject, good, refine)").option("--precision <precision>", "estimated precision (e.g., ~60%)").option("--comment <comment>", "free-text comment").addHelpText("after", `
357
372
  Examples:
358
373
  $ search-hub query assess query.yaml --verdict reject --comment "Too broad"
359
- $ search-hub query assess query.yaml --verdict good --precision "~60%"`).action(async (file, options) => {
374
+ $ search-hub query assess query.yaml --verdict good --precision "~60%"`).action(async (fileArg, options) => {
360
375
  const globalOpts = program.opts();
361
376
  try {
377
+ const file = await resolveQueryFile(fileArg);
362
378
  const result = await executeQueryAssess(file, options);
363
379
  if (result.success) {
364
380
  if (!globalOpts.quiet) {
@@ -386,9 +402,10 @@ Examples:
386
402
  queryCommand.command("log").description("View the query iteration history").argument("<file>", "path to query YAML file").option("--json", "output as JSON").addHelpText("after", `
387
403
  Examples:
388
404
  $ search-hub query log query.yaml
389
- $ search-hub query log query.yaml --json`).action(async (file, options) => {
405
+ $ search-hub query log query.yaml --json`).action(async (fileArg, options) => {
390
406
  const globalOpts = program.opts();
391
407
  try {
408
+ const file = await resolveQueryFile(fileArg);
392
409
  const entries = await readLogEntries(file);
393
410
  if (!globalOpts.quiet) {
394
411
  console.log(formatLogOutput(entries, { json: options?.json }));
@@ -468,7 +485,8 @@ Query features (use "query init" to see full template):
468
485
  async (queryFile, options) => {
469
486
  const globalOpts = program.opts();
470
487
  try {
471
- const searchOpts = parseSearchOptions(queryFile, {
488
+ const resolvedQueryFile = queryFile ? await resolveQueryFile(queryFile) : void 0;
489
+ const searchOpts = parseSearchOptions(resolvedQueryFile, {
472
490
  db: options?.db,
473
491
  query: options?.query,
474
492
  name: options?.name,
@@ -683,11 +701,14 @@ Warning: Some providers failed:
683
701
  if (result.sessionId) {
684
702
  const sessions = await listSessions(sessionsDir);
685
703
  const suggestionCmd = searchOpts.directQuery ? "search --query" : "search";
704
+ const currentSession = sessions.find((s) => s.id === result.sessionId);
705
+ const previousSession = currentSession ? sessions.filter((s) => s.name === currentSession.name && s.id !== result.sessionId).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0] : void 0;
686
706
  const suggestion = formatSuggestion(getSuggestion({
687
707
  command: suggestionCmd,
688
708
  sessionId: result.sessionId,
689
709
  sessionStatus: result.sessionStatus,
690
- sessionCount: sessions.length
710
+ sessionCount: sessions.length,
711
+ previousSessionId: previousSession?.id
691
712
  }));
692
713
  if (suggestion) console.log(suggestion);
693
714
  }