unique-sdk 2026.44.0.dev4__tar.gz → 2026.44.0.dev5__tar.gz

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 (92) hide show
  1. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/PKG-INFO +1 -1
  2. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/pyproject.toml +1 -1
  3. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/cli.py +28 -13
  4. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/web_search.py +174 -20
  5. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +52 -2
  6. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/README.md +0 -0
  7. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/__init__.py +0 -0
  8. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_api_requestor.py +0 -0
  9. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_api_resource.py +0 -0
  10. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_api_version.py +0 -0
  11. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_error.py +0 -0
  12. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_http_client.py +0 -0
  13. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_list_object.py +0 -0
  14. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_object_classes.py +0 -0
  15. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_request_options.py +0 -0
  16. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_unique_object.py +0 -0
  17. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_unique_ql.py +0 -0
  18. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_unique_response.py +0 -0
  19. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_util.py +0 -0
  20. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_version.py +0 -0
  21. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/_webhook.py +0 -0
  22. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/__init__.py +0 -0
  23. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_acronyms.py +0 -0
  24. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_agentic_table.py +0 -0
  25. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_analytics_order.py +0 -0
  26. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_benchmarking.py +0 -0
  27. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_briefing.py +0 -0
  28. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_chat_completion.py +0 -0
  29. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_content.py +0 -0
  30. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_context_memory.py +0 -0
  31. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_elicitation.py +0 -0
  32. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_embedding.py +0 -0
  33. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_event.py +0 -0
  34. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_folder.py +0 -0
  35. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_group.py +0 -0
  36. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_integrated.py +0 -0
  37. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_llm_models.py +0 -0
  38. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_mcp.py +0 -0
  39. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message.py +0 -0
  40. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_assessment.py +0 -0
  41. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_execution.py +0 -0
  42. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_log.py +0 -0
  43. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_tool.py +0 -0
  44. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_module.py +0 -0
  45. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  46. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_search.py +0 -0
  47. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_search_string.py +0 -0
  48. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  49. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_space.py +0 -0
  50. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_user.py +0 -0
  51. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_web_search.py +0 -0
  52. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/__init__.py +0 -0
  53. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/__main__.py +0 -0
  54. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/__init__.py +0 -0
  55. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  56. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/agentic_table.py +0 -0
  57. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
  58. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/browser.py +0 -0
  59. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/cite_file.py +0 -0
  60. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/elicitation.py +0 -0
  61. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/files.py +0 -0
  62. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/folders.py +0 -0
  63. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/mcp.py +0 -0
  64. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/navigation.py +0 -0
  65. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/read.py +0 -0
  66. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  67. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/search.py +0 -0
  68. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/subagent.py +0 -0
  69. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/web_search_config.py +0 -0
  70. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/config.py +0 -0
  71. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/formatting.py +0 -0
  72. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/identity.py +0 -0
  73. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/metadata_filter.py +0 -0
  74. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/paths.py +0 -0
  75. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/shell.py +0 -0
  76. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
  77. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  78. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  79. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  80. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  81. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  82. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  83. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  84. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/state.py +0 -0
  85. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/workspace.py +0 -0
  86. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/analytics_order_run.py +0 -0
  87. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/benchmarking_run.py +0 -0
  88. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/chat_history.py +0 -0
  89. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/chat_in_space.py +0 -0
  90. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/file_io.py +0 -0
  91. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/sources.py +0 -0
  92. {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-sdk
3
- Version: 2026.44.0.dev4
3
+ Version: 2026.44.0.dev5
4
4
  Summary:
5
5
  Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
6
6
  Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_sdk"
3
- version = "2026.44.0.dev4"
3
+ version = "2026.44.0.dev5"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -105,6 +105,7 @@ from unique_sdk.cli.commands.subagent import (
105
105
  from unique_sdk.cli.commands.web_search import (
106
106
  cmd_web_crawl,
107
107
  cmd_web_search,
108
+ cmd_web_search_many,
108
109
  )
109
110
  from unique_sdk.cli.commands.web_search import (
110
111
  is_error_output as _is_web_search_error_output,
@@ -2127,6 +2128,7 @@ Examples:
2127
2128
  unique-cli web-search search "quarterly earnings 2026"
2128
2129
  unique-cli web-search search "AI regulation" -n 10
2129
2130
  unique-cli web-search search "python tutorial" --json
2131
+ unique-cli web-search search "git history" "redis history" "docker history"
2130
2132
  unique-cli web-search search "sustainability" --include-content --json
2131
2133
  unique-cli web-search search "tax reform" \\
2132
2134
  --engine-config '{"searchEngineName":"Google","fetchSize":3}'
@@ -2135,7 +2137,7 @@ Examples:
2135
2137
 
2136
2138
 
2137
2139
  @web_search_group.command("search", help=_SEARCH_HELP)
2138
- @click.argument("query")
2140
+ @click.argument("queries", nargs=-1, required=True)
2139
2141
  @click.option(
2140
2142
  "--fetch-size",
2141
2143
  "-n",
@@ -2191,7 +2193,7 @@ Examples:
2191
2193
  @click.pass_context
2192
2194
  def web_search_search_cmd(
2193
2195
  ctx: click.Context,
2194
- query: str,
2196
+ queries: tuple[str, ...],
2195
2197
  fetch_size: int | None,
2196
2198
  include_content: bool,
2197
2199
  engine_config_raw: str | None,
@@ -2202,17 +2204,30 @@ def web_search_search_cmd(
2202
2204
  ) -> None:
2203
2205
  """Search the web via the Unique public API."""
2204
2206
  resolved_config = _resolve_web_search_config_path(ctx, config_path)
2205
- output = cmd_web_search(
2206
- LazyState.get(ctx),
2207
- query,
2208
- fetch_size=fetch_size,
2209
- include_content=include_content,
2210
- engine_config_raw=engine_config_raw,
2211
- crawler_config_raw=crawler_config_raw,
2212
- output_json=output_json,
2213
- config_path=resolved_config,
2214
- chat_id=_resolve_cli_chat_id(ctx, chat_id),
2215
- )
2207
+ if len(queries) == 1:
2208
+ output = cmd_web_search(
2209
+ LazyState.get(ctx),
2210
+ queries[0],
2211
+ fetch_size=fetch_size,
2212
+ include_content=include_content,
2213
+ engine_config_raw=engine_config_raw,
2214
+ crawler_config_raw=crawler_config_raw,
2215
+ output_json=output_json,
2216
+ config_path=resolved_config,
2217
+ chat_id=_resolve_cli_chat_id(ctx, chat_id),
2218
+ )
2219
+ else:
2220
+ output = cmd_web_search_many(
2221
+ LazyState.get(ctx),
2222
+ list(queries),
2223
+ fetch_size=fetch_size,
2224
+ include_content=include_content,
2225
+ engine_config_raw=engine_config_raw,
2226
+ crawler_config_raw=crawler_config_raw,
2227
+ output_json=output_json,
2228
+ config_path=resolved_config,
2229
+ chat_id=_resolve_cli_chat_id(ctx, chat_id),
2230
+ )
2216
2231
  _emit_web_search(ctx, output)
2217
2232
 
2218
2233
 
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import json
21
21
  import logging
22
+ from concurrent.futures import ThreadPoolExecutor
22
23
  from pathlib import Path
23
24
  from typing import Any, cast
24
25
 
@@ -40,6 +41,8 @@ from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
40
41
  _LOGGER = logging.getLogger(__name__)
41
42
 
42
43
  DEFAULT_PARALLEL = 10
44
+ MAX_SEARCH_QUERIES = 8
45
+ SEARCH_CONCURRENCY = 4
43
46
  _SNIPPET_PREVIEW_LIMIT = 200
44
47
  _CONTENT_PREVIEW_LIMIT = 500
45
48
  _WEB_REFS_LOG_RELATIVE_PATH = Path(".unique") / "web-refs.jsonl"
@@ -231,14 +234,18 @@ def _format_search_results(payload: dict[str, Any]) -> str:
231
234
  return "\n".join(lines).rstrip()
232
235
 
233
236
 
237
+ def _search_results_projection(payload: dict[str, Any]) -> dict[str, Any]:
238
+ """Stable projection — drops the SDK envelope ``object`` discriminator."""
239
+ return {
240
+ "engine": payload.get("engine"),
241
+ "query": payload.get("query"),
242
+ "results": payload.get("results", []),
243
+ }
244
+
245
+
234
246
  def _format_search_results_json(payload: dict[str, Any]) -> str:
235
- """Stable JSON projection — drops the SDK envelope ``object`` discriminator."""
236
247
  return json.dumps(
237
- {
238
- "engine": payload.get("engine"),
239
- "query": payload.get("query"),
240
- "results": payload.get("results", []),
241
- },
248
+ _search_results_projection(payload),
242
249
  indent=2,
243
250
  ensure_ascii=False,
244
251
  )
@@ -385,6 +392,40 @@ def cmd_web_search(
385
392
  chat_id: Optional chat id this call is made on behalf of. When
386
393
  set, the space's Web Search toggle is enforced server-side.
387
394
  """
395
+ options = _search_request_options(
396
+ fetch_size=fetch_size,
397
+ include_content=include_content,
398
+ engine_config_raw=engine_config_raw,
399
+ crawler_config_raw=crawler_config_raw,
400
+ config_path=config_path,
401
+ chat_id=chat_id,
402
+ )
403
+ if isinstance(options, str):
404
+ return options
405
+
406
+ payload = _fetch_search_payload(state, query, options)
407
+ if isinstance(payload, str):
408
+ return payload
409
+
410
+ try:
411
+ payload = _annotate_web_results_for_citations(payload)
412
+ except UnsafeRefsLogPathError as exc:
413
+ return f"{WEB_SEARCH_ERROR_PREFIX} {exc}"
414
+ if output_json:
415
+ return _format_search_results_json(payload)
416
+ return _format_search_results(payload)
417
+
418
+
419
+ def _search_request_options(
420
+ *,
421
+ fetch_size: int | None,
422
+ include_content: bool,
423
+ engine_config_raw: str | None,
424
+ crawler_config_raw: str | None,
425
+ config_path: str | None,
426
+ chat_id: str | None,
427
+ ) -> dict[str, Any] | str:
428
+ """Build the request fields shared by every query, or return an error string."""
388
429
  file_overrides = _load_file_overrides(config_path)
389
430
  if isinstance(file_overrides, str):
390
431
  return file_overrides
@@ -402,34 +443,147 @@ def cmd_web_search(
402
443
  if fetch_size is None:
403
444
  fetch_size = file_overrides.fetch_size
404
445
 
405
- params: dict[str, Any] = {"query": query}
446
+ options: dict[str, Any] = {}
406
447
  if fetch_size is not None:
407
- params["fetchSize"] = fetch_size
448
+ options["fetchSize"] = fetch_size
408
449
  if include_content:
409
- params["includeContent"] = True
450
+ options["includeContent"] = True
410
451
  if engine_override is not None:
411
- params["searchEngineConfig"] = engine_override
452
+ options["searchEngineConfig"] = engine_override
412
453
  if crawler_override is not None:
413
- params["crawlerConfig"] = crawler_override
454
+ options["crawlerConfig"] = crawler_override
414
455
  if chat_id:
415
- params["chatId"] = chat_id
456
+ options["chatId"] = chat_id
457
+ return options
416
458
 
459
+
460
+ def _fetch_search_payload(
461
+ state: ShellState,
462
+ query: str,
463
+ options: dict[str, Any],
464
+ ) -> dict[str, Any] | str:
465
+ """Run one search request; return the payload, or an error string."""
417
466
  try:
418
467
  resource = unique_sdk.WebSearch.search(
419
468
  user_id=state.config.user_id,
420
469
  company_id=state.config.company_id,
421
- **params,
470
+ query=query,
471
+ **options,
422
472
  )
423
- except (ValueError, unique_sdk.APIError) as exc:
473
+ except (ValueError, unique_sdk.UniqueError) as exc:
424
474
  return f"{WEB_SEARCH_ERROR_PREFIX} {exc}"
475
+ return _payload_from_resource(resource)
476
+
477
+
478
+ def cmd_web_search_many(
479
+ state: ShellState,
480
+ queries: list[str],
481
+ fetch_size: int | None = None,
482
+ include_content: bool = False,
483
+ engine_config_raw: str | None = None,
484
+ crawler_config_raw: str | None = None,
485
+ output_json: bool = False,
486
+ config_path: str | None = None,
487
+ chat_id: str | None = None,
488
+ ) -> str:
489
+ """Run several web searches in one call and return one block per query.
490
+
491
+ Requests run concurrently, at most ``SEARCH_CONCURRENCY`` at a time.
492
+ Citation numbers are assigned afterwards, one query at a time in the
493
+ order given, so ``[websourceN]`` is the same on every run. A URL found
494
+ by several queries keeps one number; the manifest gets one row per search.
495
+
496
+ A failed query shows its own error and does not drop the others. The
497
+ output is a ``web-search:`` error only when every query failed.
498
+ ``output_json`` returns a list with one entry per query: the usual
499
+ ``{engine, query, results}`` object, or ``{query, error}`` on failure.
500
+ """
501
+ if not queries:
502
+ return f"{WEB_SEARCH_ERROR_PREFIX} no queries provided."
503
+ if len(queries) > MAX_SEARCH_QUERIES:
504
+ return (
505
+ f"{WEB_SEARCH_ERROR_PREFIX} too many queries ({len(queries)}); "
506
+ f"the limit is {MAX_SEARCH_QUERIES} per call. "
507
+ "Split them across several calls."
508
+ )
509
+
510
+ options = _search_request_options(
511
+ fetch_size=fetch_size,
512
+ include_content=include_content,
513
+ engine_config_raw=engine_config_raw,
514
+ crawler_config_raw=crawler_config_raw,
515
+ config_path=config_path,
516
+ chat_id=chat_id,
517
+ )
518
+ if isinstance(options, str):
519
+ return options
520
+
521
+ with ThreadPoolExecutor(
522
+ max_workers=min(SEARCH_CONCURRENCY, len(queries))
523
+ ) as executor:
524
+ fetched = list(
525
+ executor.map(
526
+ lambda query: _fetch_search_payload(state, query, options), queries
527
+ )
528
+ )
529
+
530
+ outcomes: list[dict[str, Any] | str] = []
531
+ for item in fetched:
532
+ if isinstance(item, str):
533
+ outcomes.append(item)
534
+ continue
535
+ try:
536
+ outcomes.append(_annotate_web_results_for_citations(item))
537
+ except UnsafeRefsLogPathError as exc:
538
+ return f"{WEB_SEARCH_ERROR_PREFIX} {exc}"
539
+
540
+ failures = [
541
+ f"{query!r}: {outcome}"
542
+ for query, outcome in zip(queries, outcomes, strict=True)
543
+ if isinstance(outcome, str)
544
+ ]
545
+ if len(failures) == len(queries):
546
+ return (
547
+ f"{WEB_SEARCH_ERROR_PREFIX} all {len(queries)} queries failed.\n"
548
+ + "\n".join(failures)
549
+ )
425
550
 
426
- try:
427
- payload = _annotate_web_results_for_citations(_payload_from_resource(resource))
428
- except UnsafeRefsLogPathError as exc:
429
- return f"{WEB_SEARCH_ERROR_PREFIX} {exc}"
430
551
  if output_json:
431
- return _format_search_results_json(payload)
432
- return _format_search_results(payload)
552
+ return _format_many_search_results_json(queries, outcomes)
553
+ return _format_many_search_results(queries, outcomes)
554
+
555
+
556
+ def _format_many_search_results(
557
+ queries: list[str],
558
+ outcomes: list[dict[str, Any] | str],
559
+ ) -> str:
560
+ blocks: list[str] = []
561
+ failed = 0
562
+ for position, (query, outcome) in enumerate(
563
+ zip(queries, outcomes, strict=True), start=1
564
+ ):
565
+ header = f"=== query {position}/{len(queries)} ==="
566
+ if isinstance(outcome, str):
567
+ failed += 1
568
+ blocks.append(f"{header}\nquery: {query!r}\nERROR: {outcome}")
569
+ continue
570
+ blocks.append(f"{header}\n{_format_search_results(outcome)}")
571
+ if failed:
572
+ blocks.append(f"{failed} of {len(queries)} queries failed.")
573
+ return "\n\n".join(blocks)
574
+
575
+
576
+ def _format_many_search_results_json(
577
+ queries: list[str],
578
+ outcomes: list[dict[str, Any] | str],
579
+ ) -> str:
580
+ entries = [
581
+ {"query": query, "error": outcome}
582
+ if isinstance(outcome, str)
583
+ else _search_results_projection(outcome)
584
+ for query, outcome in zip(queries, outcomes, strict=True)
585
+ ]
586
+ return json.dumps(entries, indent=2, ensure_ascii=False)
433
587
 
434
588
 
435
589
  def cmd_web_crawl(
@@ -58,6 +58,31 @@ unique-cli web-search search "python tutorial" --json
58
58
  unique-cli web-search search "sustainability reports" --include-content --json
59
59
  ```
60
60
 
61
+ ### Several searches: pass every query in ONE call
62
+
63
+ When you need more than one search (several topics, companies or
64
+ questions), pass all queries as separate arguments of a single `search`
65
+ command. Do **not** run one `unique-cli web-search search` call per query,
66
+ and do not start several in the background: the queries run concurrently
67
+ inside the one call and the answer comes back in one round.
68
+
69
+ ```bash
70
+ unique-cli web-search search "Git first release" "Redis first release" "Docker first release" --json
71
+ ```
72
+
73
+ - Up to 8 queries per call (4 run at the same time). More than 8 is an
74
+ error: split them across calls.
75
+ - Options such as `-n`, `--include-content` and `--json` apply to every
76
+ query. With `--include-content`, prefer fewer queries per call: the output
77
+ grows with every query.
78
+ - Output has one block per query, in the order you gave them, each with
79
+ its query. `--json` returns a list with one entry per query.
80
+ - `[websourceN]` numbers follow the query order. A URL found by two
81
+ queries has one number.
82
+ - A query that fails shows its own error and the other queries still
83
+ return. The command exits `1` only when every query failed.
84
+ - A single query behaves exactly as before.
85
+
61
86
  ### Per-call engine override
62
87
 
63
88
  Force a specific engine (or any engine-specific knob) without changing server config:
@@ -141,7 +166,7 @@ discriminator:
141
166
 
142
167
  ## Command Reference
143
168
 
144
- ### `unique-cli web-search search <query>`
169
+ ### `unique-cli web-search search <query> [<query>...]`
145
170
 
146
171
  | Option | Short | Default | Description |
147
172
  |--------|-------|---------|-------------|
@@ -217,6 +242,28 @@ Found 3 result(s):
217
242
  > | unique-cli web-search crawl --stdin
218
243
  > ```
219
244
 
245
+ ### `search` with several queries (text)
246
+
247
+ ```
248
+ === query 1/2 ===
249
+ engine: Google query: 'Git first release'
250
+ Found 1 result(s):
251
+
252
+ 1. Git history [websource1]
253
+ https://example.com/git
254
+ ...
255
+
256
+ === query 2/2 ===
257
+ query: 'Redis first release'
258
+ ERROR: web-search: upstream failed
259
+
260
+ 1 of 2 queries failed.
261
+ ```
262
+
263
+ With `--json` the output is a list. Each entry is the usual
264
+ `{engine, query, results}` object, or `{"query": "...", "error": "..."}`
265
+ for a query that failed.
266
+
220
267
  ### `crawl` (text)
221
268
 
222
269
  ```
@@ -261,7 +308,10 @@ In one-shot mode, the CLI exits **`0`** on success and **`1`** on
261
308
  failures (config-file errors, invalid JSON overrides, API errors,
262
309
  missing URLs for `crawl`, and citation-manifest I/O failures — e.g.
263
310
  when `.unique/web-refs.jsonl` cannot be written or its parent
264
- directory is unsafe). Error messages are prefixed with
311
+ directory is unsafe). With several queries in one `search` call, a failed
312
+ query alone does not change the exit code: it exits `1` only when every
313
+ query failed, or when the call is invalid (more than 8 queries, bad
314
+ override JSON). Error messages are prefixed with
265
315
  `web-search:` or `web-crawl:` so they're easy to grep.
266
316
 
267
317
  ```bash