unique-sdk 2026.44.0.dev3__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.
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/PKG-INFO +1 -1
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/pyproject.toml +1 -1
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/cli.py +41 -18
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/files.py +29 -2
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/folders.py +64 -5
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/navigation.py +90 -6
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/web_search.py +174 -20
- unique_sdk-2026.44.0.dev5/unique_sdk/cli/paths.py +42 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/shell.py +7 -3
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +52 -2
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/state.py +16 -5
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/README.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_api_requestor.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_api_resource.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_api_version.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_error.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_http_client.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_list_object.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_object_classes.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_request_options.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_unique_object.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_unique_ql.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_unique_response.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_util.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_version.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/_webhook.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_acronyms.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_agentic_table.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_analytics_order.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_benchmarking.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_briefing.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_chat_completion.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_content.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_context_memory.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_elicitation.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_embedding.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_event.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_folder.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_group.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_integrated.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_llm_models.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_mcp.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_assessment.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_execution.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_log.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_tool.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_module.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_scheduled_task.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_search.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_search_string.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_short_term_memory.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_space.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_user.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_web_search.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/__main__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/agentic_table.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/browser.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/cite_file.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/elicitation.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/mcp.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/read.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/search.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/subagent.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/web_search_config.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/config.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/formatting.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/identity.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/metadata_filter.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/workspace.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/analytics_order_run.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/benchmarking_run.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/chat_history.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/chat_in_space.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/file_io.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/sources.py +0 -0
- {unique_sdk-2026.44.0.dev3 → 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.
|
|
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>
|
|
@@ -58,6 +58,9 @@ from unique_sdk.cli.commands.files import (
|
|
|
58
58
|
from unique_sdk.cli.commands.files import (
|
|
59
59
|
is_permission_denied_output as _is_permission_denied_output,
|
|
60
60
|
)
|
|
61
|
+
from unique_sdk.cli.commands.files import (
|
|
62
|
+
is_upload_error_output as _is_upload_error_output,
|
|
63
|
+
)
|
|
61
64
|
from unique_sdk.cli.commands.folders import cmd_mkdir, cmd_mvdir, cmd_rmdir
|
|
62
65
|
from unique_sdk.cli.commands.folders import (
|
|
63
66
|
is_mkdir_error_output as _is_mkdir_error_output,
|
|
@@ -67,6 +70,9 @@ from unique_sdk.cli.commands.mcp import (
|
|
|
67
70
|
is_error_output as _is_mcp_error_output,
|
|
68
71
|
)
|
|
69
72
|
from unique_sdk.cli.commands.navigation import cmd_cd, cmd_ls, cmd_pwd
|
|
73
|
+
from unique_sdk.cli.commands.navigation import (
|
|
74
|
+
is_ls_error_output as _is_ls_error_output,
|
|
75
|
+
)
|
|
70
76
|
from unique_sdk.cli.commands.read import cmd_read
|
|
71
77
|
from unique_sdk.cli.commands.read import (
|
|
72
78
|
is_error_output as _is_read_error_output,
|
|
@@ -99,6 +105,7 @@ from unique_sdk.cli.commands.subagent import (
|
|
|
99
105
|
from unique_sdk.cli.commands.web_search import (
|
|
100
106
|
cmd_web_crawl,
|
|
101
107
|
cmd_web_search,
|
|
108
|
+
cmd_web_search_many,
|
|
102
109
|
)
|
|
103
110
|
from unique_sdk.cli.commands.web_search import (
|
|
104
111
|
is_error_output as _is_web_search_error_output,
|
|
@@ -322,23 +329,25 @@ def ls(ctx: click.Context, target: str | None) -> None:
|
|
|
322
329
|
unique-cli ls scope_abc List by scope ID
|
|
323
330
|
"""
|
|
324
331
|
output = cmd_ls(LazyState.get(ctx), target)
|
|
325
|
-
emit(output)
|
|
332
|
+
emit(output, is_error=_is_ls_error_output)
|
|
326
333
|
|
|
327
334
|
|
|
328
335
|
@main.command()
|
|
329
336
|
@click.argument("name")
|
|
330
337
|
@click.pass_context
|
|
331
338
|
def mkdir(ctx: click.Context, name: str) -> None:
|
|
332
|
-
"""Create a
|
|
339
|
+
"""Create a folder by relative name or absolute path.
|
|
333
340
|
|
|
334
341
|
\b
|
|
335
|
-
NAME is
|
|
336
|
-
(
|
|
342
|
+
A relative NAME is created under the current directory. An absolute
|
|
343
|
+
path (starting with /) is created from the root, including under
|
|
344
|
+
/home-<userId>. Nested paths create the full hierarchy.
|
|
337
345
|
|
|
338
346
|
\b
|
|
339
347
|
Examples:
|
|
340
348
|
unique-cli mkdir Q2
|
|
341
349
|
unique-cli mkdir "2025/Q1"
|
|
350
|
+
unique-cli mkdir /home-user_1/second-brain/clients/X
|
|
342
351
|
"""
|
|
343
352
|
output = cmd_mkdir(LazyState.get(ctx), name)
|
|
344
353
|
emit(output, is_error=_is_mkdir_error_output)
|
|
@@ -421,7 +430,7 @@ def upload(ctx: click.Context, local_path: str, destination: str | None) -> None
|
|
|
421
430
|
unique-cli upload ./report.pdf /Archive/2025/
|
|
422
431
|
"""
|
|
423
432
|
output = cmd_upload(LazyState.get(ctx), local_path, destination)
|
|
424
|
-
emit(output)
|
|
433
|
+
emit(output, is_error=_is_upload_error_output)
|
|
425
434
|
|
|
426
435
|
|
|
427
436
|
@main.command()
|
|
@@ -2119,6 +2128,7 @@ Examples:
|
|
|
2119
2128
|
unique-cli web-search search "quarterly earnings 2026"
|
|
2120
2129
|
unique-cli web-search search "AI regulation" -n 10
|
|
2121
2130
|
unique-cli web-search search "python tutorial" --json
|
|
2131
|
+
unique-cli web-search search "git history" "redis history" "docker history"
|
|
2122
2132
|
unique-cli web-search search "sustainability" --include-content --json
|
|
2123
2133
|
unique-cli web-search search "tax reform" \\
|
|
2124
2134
|
--engine-config '{"searchEngineName":"Google","fetchSize":3}'
|
|
@@ -2127,7 +2137,7 @@ Examples:
|
|
|
2127
2137
|
|
|
2128
2138
|
|
|
2129
2139
|
@web_search_group.command("search", help=_SEARCH_HELP)
|
|
2130
|
-
@click.argument("
|
|
2140
|
+
@click.argument("queries", nargs=-1, required=True)
|
|
2131
2141
|
@click.option(
|
|
2132
2142
|
"--fetch-size",
|
|
2133
2143
|
"-n",
|
|
@@ -2183,7 +2193,7 @@ Examples:
|
|
|
2183
2193
|
@click.pass_context
|
|
2184
2194
|
def web_search_search_cmd(
|
|
2185
2195
|
ctx: click.Context,
|
|
2186
|
-
|
|
2196
|
+
queries: tuple[str, ...],
|
|
2187
2197
|
fetch_size: int | None,
|
|
2188
2198
|
include_content: bool,
|
|
2189
2199
|
engine_config_raw: str | None,
|
|
@@ -2194,17 +2204,30 @@ def web_search_search_cmd(
|
|
|
2194
2204
|
) -> None:
|
|
2195
2205
|
"""Search the web via the Unique public API."""
|
|
2196
2206
|
resolved_config = _resolve_web_search_config_path(ctx, config_path)
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
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
|
+
)
|
|
2208
2231
|
_emit_web_search(ctx, output)
|
|
2209
2232
|
|
|
2210
2233
|
|
|
@@ -11,6 +11,7 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import unique_sdk
|
|
13
13
|
from unique_sdk.cli.formatting import format_content_info
|
|
14
|
+
from unique_sdk.cli.paths import canonical_folder_path, same_folder_path
|
|
14
15
|
from unique_sdk.cli.state import ShellState
|
|
15
16
|
from unique_sdk.utils.file_io import download_content, upload_file
|
|
16
17
|
|
|
@@ -280,6 +281,18 @@ def _resolve_upload_destination(
|
|
|
280
281
|
resolved_id = info.get("id")
|
|
281
282
|
if not resolved_id:
|
|
282
283
|
raise ValueError(f"folder not found: {folder_path}")
|
|
284
|
+
try:
|
|
285
|
+
actual = canonical_folder_path(
|
|
286
|
+
state.config.user_id,
|
|
287
|
+
state.config.company_id,
|
|
288
|
+
resolved_id,
|
|
289
|
+
)
|
|
290
|
+
except (ValueError, unique_sdk.UniqueError) as e:
|
|
291
|
+
raise ValueError(f"refused to upload to {folder_path}: {e}") from e
|
|
292
|
+
if not same_folder_path(actual, folder_path):
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"refused to upload to {folder_path}: resolved to {actual}"
|
|
295
|
+
)
|
|
283
296
|
return resolved_id, display_name
|
|
284
297
|
|
|
285
298
|
scope_id = state.scope_id
|
|
@@ -350,7 +363,12 @@ def cmd_upload(
|
|
|
350
363
|
versioning_enabled=True,
|
|
351
364
|
)
|
|
352
365
|
|
|
353
|
-
content_id =
|
|
366
|
+
content_id = getattr(result, "id", None)
|
|
367
|
+
if not content_id:
|
|
368
|
+
return (
|
|
369
|
+
f"upload: refused to upload {display_name} to {scope_id}: "
|
|
370
|
+
"no content id returned"
|
|
371
|
+
)
|
|
354
372
|
folder_path = unique_sdk.Folder.get_folder_path(
|
|
355
373
|
user_id=state.config.user_id,
|
|
356
374
|
company_id=state.config.company_id,
|
|
@@ -358,7 +376,7 @@ def cmd_upload(
|
|
|
358
376
|
).get("folderPath", scope_id)
|
|
359
377
|
return f"Uploaded: {display_name} ({content_id}) to {folder_path}"
|
|
360
378
|
|
|
361
|
-
except (ValueError, unique_sdk.UniqueError, OSError) as e:
|
|
379
|
+
except (ValueError, unique_sdk.UniqueError, OSError, RuntimeError) as e:
|
|
362
380
|
return f"upload: {e}"
|
|
363
381
|
|
|
364
382
|
|
|
@@ -492,6 +510,15 @@ def cmd_mv_file(state: ShellState, old_name: str, new_name: str) -> str:
|
|
|
492
510
|
return f"mv: {e}"
|
|
493
511
|
|
|
494
512
|
|
|
513
|
+
def is_upload_error_output(output: str) -> bool:
|
|
514
|
+
"""Return True when *output* is a ``cmd_upload`` error.
|
|
515
|
+
|
|
516
|
+
Success text is ``Uploaded: ...``. Local misses, missing destinations,
|
|
517
|
+
and API failures all start with ``upload:`` and must exit non-zero.
|
|
518
|
+
"""
|
|
519
|
+
return output.startswith("upload:")
|
|
520
|
+
|
|
521
|
+
|
|
495
522
|
def is_permission_denied_output(output: str) -> bool:
|
|
496
523
|
"""Return ``True`` when a file-op result is a permission/scope denial.
|
|
497
524
|
|
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
|
|
7
7
|
import unique_sdk
|
|
8
8
|
from unique_sdk.cli.formatting import format_folder_info
|
|
9
|
+
from unique_sdk.cli.paths import canonical_folder_path, same_folder_path
|
|
9
10
|
from unique_sdk.cli.state import ShellState
|
|
10
11
|
|
|
11
12
|
|
|
@@ -40,8 +41,60 @@ def _metadata_filter_denies_folder(state: ShellState, scope_id: str | None) -> b
|
|
|
40
41
|
return not state.folder_allowed_by_metadata_filter(scope_id)
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
def _existing_folder_id(state: ShellState, folder_path: str) -> str | None:
|
|
45
|
+
"""Return the scope id at *folder_path* when that exact folder exists."""
|
|
46
|
+
try:
|
|
47
|
+
info = unique_sdk.Folder.get_info(
|
|
48
|
+
user_id=state.config.user_id,
|
|
49
|
+
company_id=state.config.company_id,
|
|
50
|
+
folderPath=folder_path,
|
|
51
|
+
)
|
|
52
|
+
except (ValueError, unique_sdk.UniqueError):
|
|
53
|
+
return None
|
|
54
|
+
folder_id = info.get("id")
|
|
55
|
+
if not folder_id:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
actual = canonical_folder_path(
|
|
59
|
+
state.config.user_id,
|
|
60
|
+
state.config.company_id,
|
|
61
|
+
folder_id,
|
|
62
|
+
)
|
|
63
|
+
except (ValueError, unique_sdk.UniqueError):
|
|
64
|
+
return None
|
|
65
|
+
if not same_folder_path(actual, folder_path):
|
|
66
|
+
return None
|
|
67
|
+
return folder_id
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _created_folder_error(
|
|
71
|
+
state: ShellState,
|
|
72
|
+
scope_id: str | None,
|
|
73
|
+
folder_path: str,
|
|
74
|
+
) -> str | None:
|
|
75
|
+
"""Return an ``mkdir:`` error when *scope_id* is not exactly *folder_path*.
|
|
76
|
+
|
|
77
|
+
``create_paths`` can return the ``/home-<userId>`` id in place of the leaf.
|
|
78
|
+
A failed lookup is a refusal: printing ``Created:`` would claim the folder
|
|
79
|
+
exists.
|
|
80
|
+
"""
|
|
81
|
+
if not scope_id:
|
|
82
|
+
return f"mkdir: refused to create {folder_path}"
|
|
83
|
+
try:
|
|
84
|
+
actual = canonical_folder_path(
|
|
85
|
+
state.config.user_id,
|
|
86
|
+
state.config.company_id,
|
|
87
|
+
scope_id,
|
|
88
|
+
)
|
|
89
|
+
except (ValueError, unique_sdk.UniqueError) as e:
|
|
90
|
+
return f"mkdir: refused to create {folder_path}: {e}"
|
|
91
|
+
if not same_folder_path(actual, folder_path):
|
|
92
|
+
return f"mkdir: refused to create {folder_path}: resolved to {actual}"
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
43
96
|
def cmd_mkdir(state: ShellState, name: str) -> str:
|
|
44
|
-
"""Create a
|
|
97
|
+
"""Create a folder by relative name or absolute path."""
|
|
45
98
|
# A per-message metaDataFilter *replaces* the static scopeIds, so the
|
|
46
99
|
# static-scope check must not over-deny an in-filter destination; the
|
|
47
100
|
# normalized path is gated against the filter below. See UN-21780.
|
|
@@ -49,7 +102,7 @@ def cmd_mkdir(state: ShellState, name: str) -> str:
|
|
|
49
102
|
state.workspace_metadata_filter is None
|
|
50
103
|
and not state.is_folder_target_within_workspace(name)
|
|
51
104
|
):
|
|
52
|
-
return "mkdir: permission denied (outside workspace scope)"
|
|
105
|
+
return f"mkdir: permission denied: refused {name} (outside workspace scope)"
|
|
53
106
|
# Resolve the destination (collapsing any `..`) and gate the *path*, not
|
|
54
107
|
# just the current scope id: `mkdir ../../Other/X` would otherwise pass a
|
|
55
108
|
# cwd-only check and create structure outside the per-message task scope.
|
|
@@ -68,9 +121,15 @@ def cmd_mkdir(state: ShellState, name: str) -> str:
|
|
|
68
121
|
)
|
|
69
122
|
created = result.get("createdFolders", [])
|
|
70
123
|
if created:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
124
|
+
last_id = created[-1].get("id")
|
|
125
|
+
mismatch = _created_folder_error(state, last_id, full_path)
|
|
126
|
+
if mismatch:
|
|
127
|
+
return mismatch
|
|
128
|
+
return f"Created: {full_path} ({last_id})"
|
|
129
|
+
existing_id = _existing_folder_id(state, full_path)
|
|
130
|
+
if existing_id:
|
|
131
|
+
return f"Created: {full_path} ({existing_id})"
|
|
132
|
+
return f"mkdir: refused to create {full_path}"
|
|
74
133
|
except (ValueError, unique_sdk.UniqueError) as e:
|
|
75
134
|
return f"mkdir: {e}"
|
|
76
135
|
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/navigation.py
RENAMED
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
import unique_sdk
|
|
8
8
|
from unique_sdk.cli.formatting import format_ls
|
|
9
9
|
from unique_sdk.cli.metadata_filter import _collect_filter_targets
|
|
10
|
+
from unique_sdk.cli.paths import canonical_folder_path, same_folder_path
|
|
10
11
|
from unique_sdk.cli.state import ShellState
|
|
11
12
|
|
|
12
13
|
|
|
@@ -23,13 +24,73 @@ def cmd_cd(state: ShellState, target: str) -> str:
|
|
|
23
24
|
return f"cd: {e}"
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _canonical_listing_error(
|
|
28
|
+
state: ShellState,
|
|
29
|
+
label: str,
|
|
30
|
+
claimed_path: str,
|
|
31
|
+
scope_id: str,
|
|
32
|
+
) -> str | None:
|
|
33
|
+
"""Return an ``ls:`` error when *scope_id* is not exactly *claimed_path*.
|
|
34
|
+
|
|
35
|
+
``get_info`` can hand back an ancestor for ``/home-<userId>/...``. The
|
|
36
|
+
comparison runs before any rows are trusted, including when that ancestor
|
|
37
|
+
has children. A failed lookup is an error: printing ``(empty)`` would hide
|
|
38
|
+
it.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
actual = canonical_folder_path(
|
|
42
|
+
state.config.user_id,
|
|
43
|
+
state.config.company_id,
|
|
44
|
+
scope_id,
|
|
45
|
+
)
|
|
46
|
+
except (ValueError, unique_sdk.UniqueError) as e:
|
|
47
|
+
return f"ls: cannot list {label}: {e}"
|
|
48
|
+
if not same_folder_path(actual, claimed_path):
|
|
49
|
+
return (
|
|
50
|
+
f"ls: cannot list {label}: resolved to {actual} instead (not listable here)"
|
|
51
|
+
)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _counts_without_rows_error(
|
|
56
|
+
label: str,
|
|
57
|
+
total_folders: int,
|
|
58
|
+
total_files: int,
|
|
59
|
+
) -> str | None:
|
|
60
|
+
"""Return an ``ls:`` error when the API reports rows it did not return."""
|
|
61
|
+
if total_folders or total_files:
|
|
62
|
+
return (
|
|
63
|
+
f"ls: cannot list {label}: listing returned no entries "
|
|
64
|
+
f"({total_folders} folder(s), {total_files} file(s) reported)"
|
|
65
|
+
)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _with_unlistable_marker(output: str, unlistable: list[str]) -> str:
|
|
70
|
+
"""Append folders that could not be listed, or fail when none could."""
|
|
71
|
+
if not unlistable:
|
|
72
|
+
return output
|
|
73
|
+
if output.startswith("(empty)"):
|
|
74
|
+
return "ls: cannot list " + ", ".join(unlistable) + " (not listable)"
|
|
75
|
+
return output + "\nnot listable: " + ", ".join(unlistable)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_ls_error_output(output: str) -> bool:
|
|
79
|
+
"""Return True when *output* is a ``cmd_ls`` error.
|
|
80
|
+
|
|
81
|
+
Success listings start with a table, ``(empty)``, or a count. Failures
|
|
82
|
+
start with ``ls:`` and must exit non-zero from the one-shot CLI.
|
|
83
|
+
"""
|
|
84
|
+
return output.startswith("ls:")
|
|
85
|
+
|
|
86
|
+
|
|
26
87
|
def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
27
88
|
"""List folders and files at the given (or current) path."""
|
|
28
89
|
try:
|
|
29
90
|
if target is not None:
|
|
30
|
-
|
|
91
|
+
resolved_path, scope_id = state.resolve_path(target)
|
|
31
92
|
else:
|
|
32
|
-
scope_id = state.scope_id
|
|
93
|
+
resolved_path, scope_id = state.cwd, state.scope_id
|
|
33
94
|
|
|
34
95
|
# A non-root target must lie inside the per-message scope: without
|
|
35
96
|
# this, `ls <path>` would enumerate out-of-scope folders/files that
|
|
@@ -56,6 +117,7 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
56
117
|
# UN-21780.
|
|
57
118
|
folder_ids = state.navigable_folder_ids()
|
|
58
119
|
scoped_folders: list[Any] = []
|
|
120
|
+
unlistable_folders: list[str] = []
|
|
59
121
|
for sid in folder_ids:
|
|
60
122
|
try:
|
|
61
123
|
scoped_folders.append(
|
|
@@ -66,7 +128,7 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
66
128
|
)
|
|
67
129
|
)
|
|
68
130
|
except unique_sdk.UniqueError:
|
|
69
|
-
|
|
131
|
+
unlistable_folders.append(sid)
|
|
70
132
|
scoped_files: list[Any] = []
|
|
71
133
|
for cid in content_ids:
|
|
72
134
|
# A contentId mentioned in the filter is not necessarily in
|
|
@@ -93,12 +155,13 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
93
155
|
f"\n{len(scoped_folders)} folder(s), {len(scoped_files)} "
|
|
94
156
|
"file(s) in task scope"
|
|
95
157
|
)
|
|
96
|
-
return output + summary
|
|
158
|
+
return _with_unlistable_marker(output + summary, unlistable_folders)
|
|
97
159
|
|
|
98
160
|
# When at root with a workspace restriction, show only the allowed scope
|
|
99
161
|
# folders — the agent must not see the full company folder tree.
|
|
100
162
|
if scope_id is None and state.workspace_scope_ids:
|
|
101
163
|
folders: list[Any] = []
|
|
164
|
+
unlistable_folders = []
|
|
102
165
|
for ws_id in state.workspace_scope_ids:
|
|
103
166
|
try:
|
|
104
167
|
info = unique_sdk.Folder.get_info(
|
|
@@ -108,10 +171,21 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
108
171
|
)
|
|
109
172
|
folders.append(info)
|
|
110
173
|
except unique_sdk.UniqueError:
|
|
111
|
-
|
|
174
|
+
unlistable_folders.append(ws_id)
|
|
112
175
|
output = format_ls(folders, [])
|
|
113
176
|
summary = f"\n{len(folders)} folder(s), 0 file(s)"
|
|
114
|
-
return output + summary
|
|
177
|
+
return _with_unlistable_marker(output + summary, unlistable_folders)
|
|
178
|
+
|
|
179
|
+
if scope_id:
|
|
180
|
+
label = target if target is not None else resolved_path
|
|
181
|
+
path_error = _canonical_listing_error(
|
|
182
|
+
state,
|
|
183
|
+
label,
|
|
184
|
+
resolved_path,
|
|
185
|
+
scope_id,
|
|
186
|
+
)
|
|
187
|
+
if path_error:
|
|
188
|
+
return path_error
|
|
115
189
|
|
|
116
190
|
folder_params: dict[str, Any] = {}
|
|
117
191
|
content_params: dict[str, Any] = {}
|
|
@@ -136,6 +210,16 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
|
|
|
136
210
|
total_folders = folder_result.get("totalCount", len(folders))
|
|
137
211
|
total_files = content_result.get("totalCount", len(files))
|
|
138
212
|
|
|
213
|
+
if scope_id and not folders and not files:
|
|
214
|
+
label = target if target is not None else resolved_path
|
|
215
|
+
counts_error = _counts_without_rows_error(
|
|
216
|
+
label,
|
|
217
|
+
total_folders,
|
|
218
|
+
total_files,
|
|
219
|
+
)
|
|
220
|
+
if counts_error:
|
|
221
|
+
return counts_error
|
|
222
|
+
|
|
139
223
|
# With a per-message filter, listing inside an allowed folder must not
|
|
140
224
|
# reveal files the filter excludes (e.g. a combined folder + contentId
|
|
141
225
|
# allowlist): read/cite would deny them, so ls must hide them too.
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/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(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Folder-path comparisons shared by the CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import unique_sdk
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def same_folder_path(left: str, right: str) -> bool:
|
|
11
|
+
"""True when two folder paths name the same location.
|
|
12
|
+
|
|
13
|
+
A missing leading slash and a trailing slash do not make two paths differ.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def norm(value: str) -> str:
|
|
17
|
+
text = value.strip()
|
|
18
|
+
if text and not text.startswith("/"):
|
|
19
|
+
text = f"/{text}"
|
|
20
|
+
return os.path.normpath(text.rstrip("/")) or "/"
|
|
21
|
+
|
|
22
|
+
return norm(left) == norm(right)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def canonical_folder_path(user_id: str, company_id: str, scope_id: str) -> str:
|
|
26
|
+
"""Return the stored path for *scope_id*.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: The folder has no path.
|
|
30
|
+
unique_sdk.UniqueError: The path lookup failed.
|
|
31
|
+
"""
|
|
32
|
+
actual = (
|
|
33
|
+
unique_sdk.Folder.get_folder_path(
|
|
34
|
+
user_id=user_id,
|
|
35
|
+
company_id=company_id,
|
|
36
|
+
scope_id=scope_id,
|
|
37
|
+
).get("folderPath")
|
|
38
|
+
or ""
|
|
39
|
+
)
|
|
40
|
+
if not actual:
|
|
41
|
+
raise ValueError(f"folder {scope_id} has no path")
|
|
42
|
+
return actual
|
|
@@ -239,12 +239,13 @@ class UniqueShell(cmd.Cmd):
|
|
|
239
239
|
# -- Folder operations --
|
|
240
240
|
|
|
241
241
|
def do_mkdir(self, arg: str) -> None:
|
|
242
|
-
"""Create a
|
|
242
|
+
"""Create a folder by relative name or absolute path.
|
|
243
243
|
|
|
244
244
|
Usage: mkdir <name>
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
A relative name is created under the current directory. An absolute
|
|
247
|
+
path (starting with /) is created from the root, including under
|
|
248
|
+
/home-<userId>. Nested paths create the full hierarchy.
|
|
248
249
|
|
|
249
250
|
Examples:
|
|
250
251
|
/Reports> mkdir Q2
|
|
@@ -252,6 +253,9 @@ class UniqueShell(cmd.Cmd):
|
|
|
252
253
|
|
|
253
254
|
/Reports> mkdir "2025/Q1"
|
|
254
255
|
Created: /Reports/2025/Q1 (scope_xyz999)
|
|
256
|
+
|
|
257
|
+
/> mkdir /home-user_1/second-brain/clients/X
|
|
258
|
+
Created: /home-user_1/second-brain/clients/X (scope_home1)
|
|
255
259
|
"""
|
|
256
260
|
name = arg.strip()
|
|
257
261
|
if not name:
|
|
@@ -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
|
|
@@ -10,6 +10,7 @@ from typing import Any
|
|
|
10
10
|
import unique_sdk
|
|
11
11
|
from unique_sdk.cli.config import Config
|
|
12
12
|
from unique_sdk.cli.metadata_filter import MetadataFilter
|
|
13
|
+
from unique_sdk.cli.paths import canonical_folder_path, same_folder_path
|
|
13
14
|
from unique_sdk.cli.workspace import manifest_path as workspace_manifest_path
|
|
14
15
|
|
|
15
16
|
_SEARCH_CONFIG_FILENAME = ".unique-search.json"
|
|
@@ -561,6 +562,19 @@ class ShellState:
|
|
|
561
562
|
def prompt(self) -> str:
|
|
562
563
|
return f"{self._path}> "
|
|
563
564
|
|
|
565
|
+
def _enter_folder(self, scope_id: str, folder_path: str) -> str:
|
|
566
|
+
"""Store *folder_path* only when *scope_id* is that exact folder."""
|
|
567
|
+
actual = canonical_folder_path(
|
|
568
|
+
self.config.user_id,
|
|
569
|
+
self.config.company_id,
|
|
570
|
+
scope_id,
|
|
571
|
+
)
|
|
572
|
+
if not same_folder_path(actual, folder_path):
|
|
573
|
+
raise ValueError(f"cannot change to {folder_path}: resolved to {actual}")
|
|
574
|
+
self._scope_id = scope_id
|
|
575
|
+
self._path = folder_path
|
|
576
|
+
return self._path
|
|
577
|
+
|
|
564
578
|
def cd(self, target: str) -> str:
|
|
565
579
|
"""Change directory. Returns the new path.
|
|
566
580
|
|
|
@@ -590,8 +604,7 @@ class ShellState:
|
|
|
590
604
|
company_id=self.config.company_id,
|
|
591
605
|
folderPath=parent,
|
|
592
606
|
)
|
|
593
|
-
self.
|
|
594
|
-
self._scope_id = info["id"]
|
|
607
|
+
return self._enter_folder(info["id"], parent)
|
|
595
608
|
return self._path
|
|
596
609
|
|
|
597
610
|
if target.startswith("scope_"):
|
|
@@ -619,9 +632,7 @@ class ShellState:
|
|
|
619
632
|
company_id=self.config.company_id,
|
|
620
633
|
folderPath=resolved_path,
|
|
621
634
|
)
|
|
622
|
-
self.
|
|
623
|
-
self._path = resolved_path
|
|
624
|
-
return self._path
|
|
635
|
+
return self._enter_folder(info["id"], resolved_path)
|
|
625
636
|
|
|
626
637
|
def resolve_path(self, target: str | None) -> tuple[str, str | None]:
|
|
627
638
|
"""Resolve a target to (path, scope_id) without changing state.
|
|
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.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/__init__.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_acronyms.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_agentic_table.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_analytics_order.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_benchmarking.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_briefing.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_chat_completion.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_content.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_context_memory.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_elicitation.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_embedding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_integrated.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_llm_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_log.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_message_tool.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/api_resources/_scheduled_task.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/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.dev3 → unique_sdk-2026.44.0.dev5}/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.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/agentic_table.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/cite_file.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/elicitation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/cli/commands/scheduled_tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/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
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/unique_sdk/utils/analytics_order_run.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev5}/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
|