unique-sdk 2026.44.0.dev3__tar.gz → 2026.44.0.dev4__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.dev4}/PKG-INFO +1 -1
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/pyproject.toml +1 -1
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/cli.py +13 -5
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/files.py +29 -2
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/folders.py +64 -5
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/navigation.py +90 -6
- unique_sdk-2026.44.0.dev4/unique_sdk/cli/paths.py +42 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/shell.py +7 -3
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/state.py +16 -5
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/README.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_api_requestor.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_api_resource.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_api_version.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_error.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_http_client.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_list_object.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_object_classes.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_request_options.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_unique_object.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_unique_ql.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_unique_response.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_util.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_version.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/_webhook.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_acronyms.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_agentic_table.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_analytics_order.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_benchmarking.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_briefing.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_chat_completion.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_content.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_context_memory.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_elicitation.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_embedding.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_event.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_folder.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_group.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_integrated.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_llm_models.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_mcp.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_message.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_message_assessment.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_message_execution.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_message_log.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_message_tool.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_module.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_scheduled_task.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_search.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_search_string.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_short_term_memory.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_space.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_user.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_web_search.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/__main__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/__init__.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/agentic_table.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/browser.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/cite_file.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/elicitation.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/mcp.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/read.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/search.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/subagent.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/web_search.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/commands/web_search_config.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/config.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/formatting.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/identity.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/metadata_filter.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/cli/workspace.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/utils/analytics_order_run.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/utils/benchmarking_run.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/utils/chat_history.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/utils/chat_in_space.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/utils/file_io.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/utils/sources.py +0 -0
- {unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/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.dev4
|
|
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,
|
|
@@ -322,23 +328,25 @@ def ls(ctx: click.Context, target: str | None) -> None:
|
|
|
322
328
|
unique-cli ls scope_abc List by scope ID
|
|
323
329
|
"""
|
|
324
330
|
output = cmd_ls(LazyState.get(ctx), target)
|
|
325
|
-
emit(output)
|
|
331
|
+
emit(output, is_error=_is_ls_error_output)
|
|
326
332
|
|
|
327
333
|
|
|
328
334
|
@main.command()
|
|
329
335
|
@click.argument("name")
|
|
330
336
|
@click.pass_context
|
|
331
337
|
def mkdir(ctx: click.Context, name: str) -> None:
|
|
332
|
-
"""Create a
|
|
338
|
+
"""Create a folder by relative name or absolute path.
|
|
333
339
|
|
|
334
340
|
\b
|
|
335
|
-
NAME is
|
|
336
|
-
(
|
|
341
|
+
A relative NAME is created under the current directory. An absolute
|
|
342
|
+
path (starting with /) is created from the root, including under
|
|
343
|
+
/home-<userId>. Nested paths create the full hierarchy.
|
|
337
344
|
|
|
338
345
|
\b
|
|
339
346
|
Examples:
|
|
340
347
|
unique-cli mkdir Q2
|
|
341
348
|
unique-cli mkdir "2025/Q1"
|
|
349
|
+
unique-cli mkdir /home-user_1/second-brain/clients/X
|
|
342
350
|
"""
|
|
343
351
|
output = cmd_mkdir(LazyState.get(ctx), name)
|
|
344
352
|
emit(output, is_error=_is_mkdir_error_output)
|
|
@@ -421,7 +429,7 @@ def upload(ctx: click.Context, local_path: str, destination: str | None) -> None
|
|
|
421
429
|
unique-cli upload ./report.pdf /Archive/2025/
|
|
422
430
|
"""
|
|
423
431
|
output = cmd_upload(LazyState.get(ctx), local_path, destination)
|
|
424
|
-
emit(output)
|
|
432
|
+
emit(output, is_error=_is_upload_error_output)
|
|
425
433
|
|
|
426
434
|
|
|
427
435
|
@main.command()
|
|
@@ -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.dev4}/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.
|
|
@@ -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:
|
|
@@ -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.dev4}/unique_sdk/api_resources/__init__.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_acronyms.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_agentic_table.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_analytics_order.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_benchmarking.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_briefing.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_chat_completion.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_content.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_context_memory.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/api_resources/_elicitation.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/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.dev4}/unique_sdk/api_resources/_integrated.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/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.dev4}/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.dev4}/unique_sdk/api_resources/_message_log.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/unique_sdk/cli/commands/cite_file.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/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.dev4}/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.dev4}/unique_sdk/cli/commands/web_search.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/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
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/unique_sdk/utils/analytics_order_run.py
RENAMED
|
File without changes
|
{unique_sdk-2026.44.0.dev3 → unique_sdk-2026.44.0.dev4}/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
|