unique-sdk 2026.44.0.dev4__tar.gz → 2026.44.0.dev6__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.
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/PKG-INFO +1 -1
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/pyproject.toml +1 -1
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/cli.py +28 -13
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/files.py +168 -38
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/web_search.py +174 -20
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +4 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +52 -2
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/README.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_api_requestor.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_api_resource.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_api_version.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_error.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_http_client.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_list_object.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_object_classes.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_request_options.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_unique_object.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_unique_ql.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_unique_response.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_util.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_version.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/_webhook.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_acronyms.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_agentic_table.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_analytics_order.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_benchmarking.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_briefing.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_chat_completion.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_content.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_context_memory.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_elicitation.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_embedding.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_event.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_folder.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_group.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_integrated.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_llm_models.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_mcp.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message_assessment.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message_execution.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message_log.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message_tool.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_module.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_scheduled_task.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_search.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_search_string.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_short_term_memory.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_space.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_user.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_web_search.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/__main__.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/agentic_table.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/browser.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/cite_file.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/elicitation.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/folders.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/mcp.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/navigation.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/read.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/search.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/subagent.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/web_search_config.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/config.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/formatting.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/identity.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/metadata_filter.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/paths.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/shell.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/state.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/workspace.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/analytics_order_run.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/benchmarking_run.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/chat_history.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/chat_in_space.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/file_io.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/sources.py +0 -0
- {unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/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.
|
|
3
|
+
Version: 2026.44.0.dev6
|
|
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>
|
|
@@ -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("
|
|
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
|
-
|
|
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
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
|
|
@@ -38,6 +38,12 @@ _SUPPORTED_UPLOAD_MIME_TYPES = {
|
|
|
38
38
|
".md": "text/markdown",
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
# Bounds so one name resolve can never page through the whole knowledge base.
|
|
42
|
+
_MAX_RESOLVE_MATCH_CANDIDATES = 100
|
|
43
|
+
_MAX_RESOLVE_INFOS_PAGES = 10
|
|
44
|
+
_RESOLVE_INFOS_PAGE_SIZE = 100
|
|
45
|
+
_MAX_RESOLVE_INFOS_ENTRIES = _MAX_RESOLVE_INFOS_PAGES * _RESOLVE_INFOS_PAGE_SIZE
|
|
46
|
+
|
|
41
47
|
|
|
42
48
|
def _normalize_unique_file_path(cwd: str, path: str) -> str:
|
|
43
49
|
"""Normalize a Unique file path without allowing traversal above root."""
|
|
@@ -79,6 +85,7 @@ def _resolve_content_id(
|
|
|
79
85
|
return name_or_id, name_or_id
|
|
80
86
|
|
|
81
87
|
lookup_name = name_or_id
|
|
88
|
+
folder_path = state.cwd
|
|
82
89
|
scope_id = state.scope_id
|
|
83
90
|
if "/" in name_or_id:
|
|
84
91
|
unique_path = _normalize_unique_file_path(state.cwd, name_or_id)
|
|
@@ -124,16 +131,10 @@ def _resolve_content_id(
|
|
|
124
131
|
if not scope_id:
|
|
125
132
|
raise ValueError(f"folder not found: {folder_path}")
|
|
126
133
|
elif scope_id is None:
|
|
127
|
-
#
|
|
128
|
-
# Content.get_infos scan across the whole knowledge base.
|
|
134
|
+
# Resolving a bare name at the root would page the whole knowledge base.
|
|
129
135
|
if state.workspace_metadata_filter is not None:
|
|
130
|
-
# With a per-message filter active
|
|
131
|
-
#
|
|
132
|
-
# runs — and the deny-vs-not-found distinction is an existence/title
|
|
133
|
-
# oracle across the task boundary. That is broader than the
|
|
134
|
-
# ls/read-at-root gating intent, so require a bounded context
|
|
135
|
-
# instead. The cont_ fast-path, an explicit in-scope path, or a
|
|
136
|
-
# ``cd`` into an in-scope folder all still work. See UN-21780.
|
|
136
|
+
# With a per-message filter active that is also a cross-scope
|
|
137
|
+
# existence/title oracle. See UN-21780.
|
|
137
138
|
raise ValueError(
|
|
138
139
|
f"permission denied: {name_or_id} can't be resolved by name at "
|
|
139
140
|
f"the knowledge-base root while a task scope is active "
|
|
@@ -141,51 +142,180 @@ def _resolve_content_id(
|
|
|
141
142
|
"by content id, or 'cd' into an in-scope folder."
|
|
142
143
|
)
|
|
143
144
|
if not state.is_within_workspace():
|
|
144
|
-
# No per-message filter: defer to the static scopeIds boundary.
|
|
145
145
|
raise ValueError("permission denied (outside workspace scope)")
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"{name_or_id} can't be resolved by name at the knowledge-base "
|
|
148
|
+
"root. Use 'unique-cli search', pass a content id (cont_...), "
|
|
149
|
+
"or 'cd' into a folder first."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return _resolve_content_id_in_folder(
|
|
153
|
+
state=state,
|
|
154
|
+
name_or_id=name_or_id,
|
|
155
|
+
lookup_name=lookup_name,
|
|
156
|
+
folder_path=folder_path,
|
|
157
|
+
scope_id=scope_id,
|
|
158
|
+
allow_chat_files=allow_chat_files,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _resolve_content_id_in_folder(
|
|
163
|
+
*,
|
|
164
|
+
state: ShellState,
|
|
165
|
+
name_or_id: str,
|
|
166
|
+
lookup_name: str,
|
|
167
|
+
folder_path: str,
|
|
168
|
+
scope_id: str,
|
|
169
|
+
allow_chat_files: bool,
|
|
170
|
+
) -> tuple[str, str]:
|
|
171
|
+
"""Resolve ``lookup_name`` inside one folder without paging the whole KB."""
|
|
172
|
+
match = _pick_match(
|
|
173
|
+
hits=_hits_by_file_path(
|
|
174
|
+
state=state,
|
|
175
|
+
folder_path=folder_path,
|
|
176
|
+
lookup_name=lookup_name,
|
|
177
|
+
scope_id=scope_id,
|
|
178
|
+
),
|
|
179
|
+
lookup_name=lookup_name,
|
|
180
|
+
)
|
|
181
|
+
capped = False
|
|
182
|
+
if match is None:
|
|
183
|
+
hits, capped = _hits_by_search(
|
|
184
|
+
state=state, lookup_name=lookup_name, scope_id=scope_id
|
|
185
|
+
)
|
|
186
|
+
match = _pick_match(hits=hits, lookup_name=lookup_name)
|
|
187
|
+
if match is None:
|
|
188
|
+
if capped:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"File not found in the first {_MAX_RESOLVE_INFOS_ENTRIES} "
|
|
191
|
+
f"entries of this folder: {name_or_id}. Use 'unique-cli "
|
|
192
|
+
"search' or pass a content id (cont_...)."
|
|
193
|
+
)
|
|
194
|
+
raise ValueError(f"File not found: {name_or_id}")
|
|
195
|
+
|
|
196
|
+
resolved_id, display_name = match
|
|
197
|
+
# Gate the *resolved* id too: resolving by file name or path must not
|
|
198
|
+
# bypass the per-message metadata-filter scope that the cont_ fast-path
|
|
199
|
+
# above enforces, and must honour the same read-only chat-file
|
|
200
|
+
# exemption (so rm/mv by name can't reach an out-of-scope attachment
|
|
201
|
+
# either). See UN-21780.
|
|
202
|
+
if not state.is_content_within_workspace(
|
|
203
|
+
resolved_id, allow_chat_files=allow_chat_files
|
|
204
|
+
):
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"permission denied: {name_or_id} is outside your "
|
|
207
|
+
f"task scope ({state.scope_denial_hint()}). Use "
|
|
208
|
+
"'unique-cli search' or 'ls' within that scope "
|
|
209
|
+
"instead."
|
|
210
|
+
)
|
|
211
|
+
return resolved_id, display_name
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _hits_by_file_path(
|
|
215
|
+
*, state: ShellState, folder_path: str, lookup_name: str, scope_id: str
|
|
216
|
+
) -> Sequence[Any]:
|
|
217
|
+
# One indexed lookup that returns no chunks and also finds files whose
|
|
218
|
+
# ingestion failed.
|
|
219
|
+
try:
|
|
220
|
+
result = unique_sdk.Content.get_info(
|
|
221
|
+
user_id=state.config.user_id,
|
|
222
|
+
company_id=state.config.company_id,
|
|
223
|
+
filePath=f"{folder_path.rstrip('/')}/{lookup_name}",
|
|
224
|
+
)
|
|
225
|
+
except unique_sdk.UniqueError:
|
|
226
|
+
return []
|
|
227
|
+
# A server that ignores filePath returns unrelated content.
|
|
228
|
+
return [
|
|
229
|
+
info
|
|
230
|
+
for info in result.get("contentInfo", [])
|
|
231
|
+
if info.get("ownerId") == scope_id
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _hits_by_search(
|
|
236
|
+
*, state: ShellState, lookup_name: str, scope_id: str
|
|
237
|
+
) -> tuple[Sequence[Any], bool]:
|
|
238
|
+
"""Return the hits, and whether the get_infos fallback hit its page cap."""
|
|
239
|
+
try:
|
|
240
|
+
hits = unique_sdk.Content.search(
|
|
241
|
+
user_id=state.config.user_id,
|
|
242
|
+
company_id=state.config.company_id,
|
|
243
|
+
where={
|
|
244
|
+
"AND": [
|
|
245
|
+
{"ownerId": {"equals": scope_id}},
|
|
246
|
+
{
|
|
247
|
+
"OR": [
|
|
248
|
+
{"title": {"equals": lookup_name}},
|
|
249
|
+
{"key": {"equals": lookup_name}},
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
includeFailedContent=True,
|
|
255
|
+
)
|
|
256
|
+
except unique_sdk.UniqueError:
|
|
257
|
+
return _resolve_content_id_via_infos_fallback(
|
|
258
|
+
state=state,
|
|
259
|
+
lookup_name=lookup_name,
|
|
260
|
+
scope_id=scope_id,
|
|
261
|
+
)
|
|
262
|
+
return hits, False
|
|
263
|
+
|
|
146
264
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
265
|
+
def _pick_match(*, hits: Sequence[Any], lookup_name: str) -> tuple[str, str] | None:
|
|
266
|
+
for content in hits[:_MAX_RESOLVE_MATCH_CANDIDATES]:
|
|
267
|
+
info = _as_content_fields(content)
|
|
268
|
+
resolved_id = info.get("id")
|
|
269
|
+
if not isinstance(resolved_id, str) or not resolved_id:
|
|
270
|
+
continue
|
|
271
|
+
title = info.get("title") or ""
|
|
272
|
+
key = info.get("key") or ""
|
|
273
|
+
if lookup_name in {title, key}:
|
|
274
|
+
return resolved_id, title or key
|
|
275
|
+
return None
|
|
150
276
|
|
|
151
|
-
|
|
277
|
+
|
|
278
|
+
def _as_content_fields(content: Any) -> Mapping[str, Any]:
|
|
279
|
+
if isinstance(content, Mapping):
|
|
280
|
+
return content
|
|
281
|
+
return {
|
|
282
|
+
"id": getattr(content, "id", None),
|
|
283
|
+
"title": getattr(content, "title", None),
|
|
284
|
+
"key": getattr(content, "key", None),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _resolve_content_id_via_infos_fallback(
|
|
289
|
+
*,
|
|
290
|
+
state: ShellState,
|
|
291
|
+
lookup_name: str,
|
|
292
|
+
scope_id: str,
|
|
293
|
+
) -> tuple[list[Mapping[str, Any]], bool]:
|
|
294
|
+
"""Folder-scoped get_infos fallback with a hard page cap.
|
|
295
|
+
|
|
296
|
+
Returns the match (if any), and whether the cap stopped the scan early.
|
|
297
|
+
"""
|
|
152
298
|
skip = 0
|
|
153
|
-
|
|
299
|
+
for _ in range(_MAX_RESOLVE_INFOS_PAGES):
|
|
154
300
|
result = unique_sdk.Content.get_infos(
|
|
155
301
|
user_id=state.config.user_id,
|
|
156
302
|
company_id=state.config.company_id,
|
|
303
|
+
parentId=scope_id,
|
|
157
304
|
skip=skip,
|
|
158
|
-
take=
|
|
159
|
-
**params,
|
|
305
|
+
take=_RESOLVE_INFOS_PAGE_SIZE,
|
|
160
306
|
)
|
|
161
307
|
content_infos = result.get("contentInfos", [])
|
|
162
308
|
if not content_infos:
|
|
163
|
-
|
|
164
|
-
|
|
309
|
+
return [], False
|
|
165
310
|
for info in content_infos:
|
|
166
311
|
title = info.get("title") or ""
|
|
167
312
|
key = info.get("key") or ""
|
|
168
313
|
if lookup_name in {title, key}:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
# the cont_ fast-path above enforces, and must honour the same
|
|
173
|
-
# read-only chat-file exemption (so rm/mv by name can't reach
|
|
174
|
-
# an out-of-scope attachment either). See UN-21780.
|
|
175
|
-
if not state.is_content_within_workspace(
|
|
176
|
-
resolved_id, allow_chat_files=allow_chat_files
|
|
177
|
-
):
|
|
178
|
-
raise ValueError(
|
|
179
|
-
f"permission denied: {name_or_id} is outside your "
|
|
180
|
-
f"task scope ({state.scope_denial_hint()}). Use "
|
|
181
|
-
"'unique-cli search' or 'ls' within that scope "
|
|
182
|
-
"instead."
|
|
183
|
-
)
|
|
184
|
-
return resolved_id, title or key
|
|
185
|
-
|
|
314
|
+
return [info], False
|
|
315
|
+
if len(content_infos) < _RESOLVE_INFOS_PAGE_SIZE:
|
|
316
|
+
return [], False
|
|
186
317
|
skip += len(content_infos)
|
|
187
|
-
|
|
188
|
-
raise ValueError(f"File not found: {name_or_id}")
|
|
318
|
+
return [], True
|
|
189
319
|
|
|
190
320
|
|
|
191
321
|
def _detect_upload_mime_type(path: Path) -> str:
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/web_search.py
RENAMED
|
@@ -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
|
-
|
|
446
|
+
options: dict[str, Any] = {}
|
|
406
447
|
if fetch_size is not None:
|
|
407
|
-
|
|
448
|
+
options["fetchSize"] = fetch_size
|
|
408
449
|
if include_content:
|
|
409
|
-
|
|
450
|
+
options["includeContent"] = True
|
|
410
451
|
if engine_override is not None:
|
|
411
|
-
|
|
452
|
+
options["searchEngineConfig"] = engine_override
|
|
412
453
|
if crawler_override is not None:
|
|
413
|
-
|
|
454
|
+
options["crawlerConfig"] = crawler_override
|
|
414
455
|
if chat_id:
|
|
415
|
-
|
|
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
|
-
|
|
470
|
+
query=query,
|
|
471
|
+
**options,
|
|
422
472
|
)
|
|
423
|
-
except (ValueError, unique_sdk.
|
|
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
|
|
432
|
-
return
|
|
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(
|
|
@@ -99,6 +99,10 @@ unique-cli mv report.pdf "Annual Report 2025.pdf"
|
|
|
99
99
|
| Content ID | `cont_abc123` | File directly by ID |
|
|
100
100
|
| File path | `/Reports/Q1/report.pdf` | File in a folder |
|
|
101
101
|
|
|
102
|
+
A bare file name such as `report.pdf` resolves in the current folder. At the
|
|
103
|
+
root (`/`) a bare file name is an error: pass a full path, a content ID, or `cd`
|
|
104
|
+
into the folder first.
|
|
105
|
+
|
|
102
106
|
## Upload Destination Resolution
|
|
103
107
|
|
|
104
108
|
The `upload` command always enables immutable content versioning. It does not expose an unversioned upload mode. Its destination works like Linux `cp`:
|
|
@@ -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).
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/__init__.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_acronyms.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_agentic_table.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_analytics_order.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_benchmarking.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_briefing.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_chat_completion.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_content.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_context_memory.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_elicitation.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_embedding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_integrated.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_llm_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message_log.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_message_tool.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_scheduled_task.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_search_string.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/api_resources/_web_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/agentic_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/cite_file.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/elicitation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/navigation.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/scheduled_tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/cli/commands/web_search_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/analytics_order_run.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev4 → unique_sdk-2026.44.0.dev6}/unique_sdk/utils/benchmarking_run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|