unique-sdk 2026.26.0.dev7__tar.gz → 2026.26.0.dev9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/PKG-INFO +1 -1
  2. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/pyproject.toml +1 -1
  3. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/__init__.py +3 -0
  4. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_content.py +158 -0
  5. unique_sdk-2026.26.0.dev9/unique_sdk/api_resources/_dynamic_frontend.py +81 -0
  6. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/cli.py +149 -12
  7. unique_sdk-2026.26.0.dev9/unique_sdk/cli/commands/dynamic_frontend.py +116 -0
  8. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/files.py +136 -10
  9. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/shell.py +110 -26
  10. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +21 -3
  11. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/file_io.py +7 -0
  12. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/README.md +0 -0
  13. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_api_requestor.py +0 -0
  14. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_api_resource.py +0 -0
  15. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_api_version.py +0 -0
  16. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_error.py +0 -0
  17. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_http_client.py +0 -0
  18. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_list_object.py +0 -0
  19. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_object_classes.py +0 -0
  20. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_request_options.py +0 -0
  21. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_unique_object.py +0 -0
  22. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_unique_ql.py +0 -0
  23. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_unique_response.py +0 -0
  24. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_util.py +0 -0
  25. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_version.py +0 -0
  26. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/_webhook.py +0 -0
  27. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/__init__.py +0 -0
  28. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_acronyms.py +0 -0
  29. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_agentic_table.py +0 -0
  30. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_analytics_order.py +0 -0
  31. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_benchmarking.py +0 -0
  32. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_briefing.py +0 -0
  33. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_chat_completion.py +0 -0
  34. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_elicitation.py +0 -0
  35. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_embedding.py +0 -0
  36. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_event.py +0 -0
  37. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_folder.py +0 -0
  38. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_group.py +0 -0
  39. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_integrated.py +0 -0
  40. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_llm_models.py +0 -0
  41. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_mcp.py +0 -0
  42. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message.py +0 -0
  43. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_assessment.py +0 -0
  44. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_execution.py +0 -0
  45. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_log.py +0 -0
  46. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_tool.py +0 -0
  47. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_module.py +0 -0
  48. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  49. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_search.py +0 -0
  50. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_search_string.py +0 -0
  51. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  52. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_space.py +0 -0
  53. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_user.py +0 -0
  54. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_web_search.py +0 -0
  55. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/__init__.py +0 -0
  56. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/__main__.py +0 -0
  57. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/__init__.py +0 -0
  58. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  59. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/cite_file.py +0 -0
  60. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/elicitation.py +0 -0
  61. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/folders.py +0 -0
  62. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/mcp.py +0 -0
  63. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/navigation.py +0 -0
  64. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/read.py +0 -0
  65. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  66. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/search.py +0 -0
  67. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/subagent.py +0 -0
  68. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/web_search.py +0 -0
  69. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/web_search_config.py +0 -0
  70. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/config.py +0 -0
  71. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/formatting.py +0 -0
  72. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  73. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  74. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  75. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  76. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  77. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  78. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/state.py +0 -0
  79. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/analytics_order_run.py +0 -0
  80. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/benchmarking_run.py +0 -0
  81. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/chat_history.py +0 -0
  82. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/chat_in_space.py +0 -0
  83. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/sources.py +0 -0
  84. {unique_sdk-2026.26.0.dev7 → unique_sdk-2026.26.0.dev9}/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.26.0.dev7
3
+ Version: 2026.26.0.dev9
4
4
  Summary:
5
5
  Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
6
6
  Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_sdk"
3
- version = "2026.26.0.dev7"
3
+ version = "2026.26.0.dev9"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -77,6 +77,9 @@ from unique_sdk.api_resources._message import Message as Message
77
77
  from unique_sdk.api_resources._integrated import Integrated as Integrated
78
78
  from unique_sdk.api_resources._search import Search as Search
79
79
  from unique_sdk.api_resources._content import Content as Content
80
+ from unique_sdk.api_resources._dynamic_frontend import (
81
+ DynamicFrontend as DynamicFrontend,
82
+ )
80
83
  from unique_sdk.api_resources._search_string import SearchString as SearchString
81
84
  from unique_sdk.api_resources._short_term_memory import (
82
85
  ShortTermMemory as ShortTermMemory,
@@ -170,6 +170,50 @@ class Content(APIResource["Content"]):
170
170
  # a SAS URL the caller PUTs the PDF bytes to. ``file_io.upload_file``
171
171
  # exposes this as ``preview_pdf_path`` for a one-call flow.
172
172
  previewPdfFileName: NotRequired[str | None]
173
+ # When true, the platform archives previous blobs for this
174
+ # content and makes them restorable through the version APIs.
175
+ versioningEnabled: NotRequired[bool | None]
176
+
177
+ class VersionsParams(RequestOptions):
178
+ contentId: str
179
+ skip: NotRequired[int | None]
180
+ take: NotRequired[int | None]
181
+
182
+ class ContentVersion(TypedDict):
183
+ id: str
184
+ contentId: str
185
+ versionNumber: int
186
+ reason: str
187
+ blobObjectKey: str
188
+ key: str
189
+ title: str | None
190
+ description: str | None
191
+ url: str | None
192
+ byteSize: int
193
+ mimeType: str
194
+ ownerType: str
195
+ ownerId: str
196
+ contentHash: str | None
197
+ archivedAt: str
198
+ archivedBy: str | None
199
+ originalCreatedAt: str
200
+ originalCreatedBy: str | None
201
+ createdBy: str | None
202
+
203
+ class PaginatedContentVersions(TypedDict):
204
+ data: list["Content.ContentVersion"]
205
+ object: str
206
+
207
+ class VersionDownloadUrlParams(RequestOptions):
208
+ contentVersionId: str
209
+
210
+ class ContentVersionDownloadUrl(TypedDict):
211
+ id: str
212
+ object: str
213
+ url: str
214
+
215
+ class RestoreVersionParams(RequestOptions):
216
+ contentVersionId: str
173
217
 
174
218
  class UpdateParams(RequestOptions):
175
219
  contentId: NotRequired[str]
@@ -467,6 +511,120 @@ class Content(APIResource["Content"]):
467
511
  ),
468
512
  )
469
513
 
514
+ @classmethod
515
+ def versions(
516
+ cls,
517
+ user_id: str,
518
+ company_id: str,
519
+ **params: Unpack["Content.VersionsParams"],
520
+ ) -> "Content.PaginatedContentVersions":
521
+ content_id = params.pop("contentId")
522
+ return cast(
523
+ Content.PaginatedContentVersions,
524
+ cls._static_request(
525
+ "get",
526
+ f"/content/{content_id}/versions",
527
+ user_id,
528
+ company_id,
529
+ params=params,
530
+ ),
531
+ )
532
+
533
+ @classmethod
534
+ async def versions_async(
535
+ cls,
536
+ user_id: str,
537
+ company_id: str,
538
+ **params: Unpack["Content.VersionsParams"],
539
+ ) -> "Content.PaginatedContentVersions":
540
+ content_id = params.pop("contentId")
541
+ return cast(
542
+ Content.PaginatedContentVersions,
543
+ await cls._static_request_async(
544
+ "get",
545
+ f"/content/{content_id}/versions",
546
+ user_id,
547
+ company_id,
548
+ params=params,
549
+ ),
550
+ )
551
+
552
+ @classmethod
553
+ def version_download_url(
554
+ cls,
555
+ user_id: str,
556
+ company_id: str,
557
+ **params: Unpack["Content.VersionDownloadUrlParams"],
558
+ ) -> "Content.ContentVersionDownloadUrl":
559
+ content_version_id = params.pop("contentVersionId")
560
+ return cast(
561
+ Content.ContentVersionDownloadUrl,
562
+ cls._static_request(
563
+ "get",
564
+ f"/content/versions/{content_version_id}/download-url",
565
+ user_id,
566
+ company_id,
567
+ params=params,
568
+ ),
569
+ )
570
+
571
+ @classmethod
572
+ async def version_download_url_async(
573
+ cls,
574
+ user_id: str,
575
+ company_id: str,
576
+ **params: Unpack["Content.VersionDownloadUrlParams"],
577
+ ) -> "Content.ContentVersionDownloadUrl":
578
+ content_version_id = params.pop("contentVersionId")
579
+ return cast(
580
+ Content.ContentVersionDownloadUrl,
581
+ await cls._static_request_async(
582
+ "get",
583
+ f"/content/versions/{content_version_id}/download-url",
584
+ user_id,
585
+ company_id,
586
+ params=params,
587
+ ),
588
+ )
589
+
590
+ @classmethod
591
+ def restore_version(
592
+ cls,
593
+ user_id: str,
594
+ company_id: str,
595
+ **params: Unpack["Content.RestoreVersionParams"],
596
+ ) -> "Content.ContentInfo":
597
+ content_version_id = params.pop("contentVersionId")
598
+ return cast(
599
+ Content.ContentInfo,
600
+ cls._static_request(
601
+ "post",
602
+ f"/content/versions/{content_version_id}/restore",
603
+ user_id,
604
+ company_id,
605
+ params=params,
606
+ ),
607
+ )
608
+
609
+ @classmethod
610
+ async def restore_version_async(
611
+ cls,
612
+ user_id: str,
613
+ company_id: str,
614
+ **params: Unpack["Content.RestoreVersionParams"],
615
+ ) -> "Content.ContentInfo":
616
+ content_version_id = params.pop("contentVersionId")
617
+ return cast(
618
+ Content.ContentInfo,
619
+ await cls._static_request_async(
620
+ "post",
621
+ f"/content/versions/{content_version_id}/restore",
622
+ user_id,
623
+ company_id,
624
+ params=params,
625
+ ),
626
+ )
627
+
470
628
  @classmethod
471
629
  def ingest_magic_table_sheets(
472
630
  cls,
@@ -0,0 +1,81 @@
1
+ from typing import Literal, cast
2
+
3
+ from typing_extensions import NotRequired, Unpack
4
+
5
+ from unique_sdk._api_resource import APIResource
6
+ from unique_sdk._request_options import RequestOptions
7
+ from unique_sdk._util import classproperty
8
+
9
+
10
+ class DynamicFrontend(APIResource["DynamicFrontend"]):
11
+ @classproperty
12
+ def OBJECT_NAME(cls) -> Literal["dynamic-frontend"]:
13
+ return "dynamic-frontend"
14
+
15
+ id: str
16
+ spaceId: str
17
+ name: str
18
+ contentId: str
19
+ status: dict[str, object] | None
20
+
21
+ class CreateParams(RequestOptions):
22
+ name: str
23
+ contentId: str
24
+
25
+ class UpdateParams(RequestOptions):
26
+ contentId: str
27
+ name: NotRequired[str | None]
28
+
29
+ @classmethod
30
+ def create(
31
+ cls,
32
+ user_id: str,
33
+ company_id: str,
34
+ **params: Unpack["DynamicFrontend.CreateParams"],
35
+ ) -> "DynamicFrontend":
36
+ return cast(
37
+ DynamicFrontend,
38
+ cls._static_request(
39
+ "post",
40
+ "/dynamic-frontend",
41
+ user_id,
42
+ company_id,
43
+ params=params,
44
+ ),
45
+ )
46
+
47
+ @classmethod
48
+ def modify(
49
+ cls,
50
+ space_id: str,
51
+ user_id: str,
52
+ company_id: str,
53
+ **params: Unpack["DynamicFrontend.UpdateParams"],
54
+ ) -> "DynamicFrontend":
55
+ return cast(
56
+ DynamicFrontend,
57
+ cls._static_request(
58
+ "patch",
59
+ f"/dynamic-frontend/{space_id}",
60
+ user_id,
61
+ company_id,
62
+ params=params,
63
+ ),
64
+ )
65
+
66
+ @classmethod
67
+ def list(
68
+ cls,
69
+ user_id: str,
70
+ company_id: str,
71
+ ) -> list["DynamicFrontend"]:
72
+ response = cls._static_request(
73
+ "get",
74
+ "/dynamic-frontend",
75
+ user_id,
76
+ company_id,
77
+ )
78
+ if isinstance(response, list):
79
+ return cast(list[DynamicFrontend], response)
80
+ data = response.get("data", []) if isinstance(response, dict) else []
81
+ return cast(list[DynamicFrontend], data)
@@ -8,6 +8,10 @@ import click
8
8
 
9
9
  from unique_sdk.cli import __version__
10
10
  from unique_sdk.cli.commands.cite_file import cmd_cite_file
11
+ from unique_sdk.cli.commands.dynamic_frontend import (
12
+ cmd_dynamic_frontend_deploy,
13
+ cmd_dynamic_frontend_list,
14
+ )
11
15
  from unique_sdk.cli.commands.elicitation import (
12
16
  cmd_elicit_ask,
13
17
  cmd_elicit_create,
@@ -16,7 +20,14 @@ from unique_sdk.cli.commands.elicitation import (
16
20
  cmd_elicit_respond,
17
21
  cmd_elicit_wait,
18
22
  )
19
- from unique_sdk.cli.commands.files import cmd_download, cmd_mv_file, cmd_rm, cmd_upload
23
+ from unique_sdk.cli.commands.files import (
24
+ cmd_download,
25
+ cmd_mv_file,
26
+ cmd_restore_version,
27
+ cmd_rm,
28
+ cmd_upload,
29
+ cmd_versions,
30
+ )
20
31
  from unique_sdk.cli.commands.folders import cmd_mkdir, cmd_mvdir, cmd_rmdir
21
32
  from unique_sdk.cli.commands.mcp import cmd_mcp
22
33
  from unique_sdk.cli.commands.navigation import cmd_cd, cmd_ls, cmd_pwd
@@ -53,6 +64,8 @@ from unique_sdk.cli.config import load_config
53
64
  from unique_sdk.cli.shell import UniqueShell
54
65
  from unique_sdk.cli.state import ShellState
55
66
 
67
+ _DYNAMIC_FRONTEND_ERROR_PREFIX = "dynamic-frontend "
68
+
56
69
  MAIN_HELP = """\
57
70
  Unique CLI -- Linux-like file explorer for the Unique AI Platform.
58
71
 
@@ -85,6 +98,7 @@ Path formats accepted by all commands:
85
98
  \b
86
99
  File identifiers:
87
100
  report.pdf File name (matched in current directory)
101
+ /Reports/report.pdf File path (absolute or relative)
88
102
  cont_abc123 Content ID (used directly)
89
103
 
90
104
  \b
@@ -93,12 +107,15 @@ Examples:
93
107
  unique-cli ls List root folders
94
108
  unique-cli ls /Reports List a specific folder
95
109
  unique-cli search "revenue" -l 50 Search with custom limit
96
- unique-cli upload ./file.pdf Upload to current folder
110
+ unique-cli upload ./file.pdf Upload versioned to current folder
97
111
  unique-cli download cont_abc123 Download by content ID
112
+ unique-cli versions cont_abc123 List archived file versions
113
+ unique-cli restore-version cver_1 Restore a file from a version
98
114
  unique-cli elicit ask "Which?" Ask the user a question synchronously
99
115
  unique-cli subagent Legal "Review" Invoke a connected space/subagent
100
116
  unique-cli web-search search "x" Search the web via the public API
101
117
  unique-cli web-search crawl URL Crawl a URL via the public API
118
+ unique-cli dynamic-frontend list List manageable Dynamic Frontend spaces
102
119
  """
103
120
 
104
121
 
@@ -254,12 +271,13 @@ def mvdir(ctx: click.Context, old_name: str, new_name: str) -> None:
254
271
  @click.argument("destination", required=False, default=None)
255
272
  @click.pass_context
256
273
  def upload(ctx: click.Context, local_path: str, destination: str | None) -> None:
257
- """Upload a local file (works like Linux cp).
274
+ """Upload a local file with versioning enabled (works like Linux cp).
258
275
 
259
276
  \b
260
- Uploads LOCAL_PATH to the Unique platform. DESTINATION works like
261
- the target in cp -- it can be a folder path, a new filename, or
262
- a combination of both. MIME type is auto-detected.
277
+ Uploads LOCAL_PATH to the Unique platform with immutable versioning
278
+ enabled. DESTINATION works like the target in cp -- it can be a
279
+ folder path, a new filename, or a combination of both. MIME type is
280
+ auto-detected.
263
281
 
264
282
  \b
265
283
  Destination formats:
@@ -281,6 +299,48 @@ def upload(ctx: click.Context, local_path: str, destination: str | None) -> None
281
299
  click.echo(cmd_upload(LazyState.get(ctx), local_path, destination))
282
300
 
283
301
 
302
+ @main.command()
303
+ @click.argument("name_or_id")
304
+ @click.option("--skip", type=int, default=None, help="Number of versions to skip.")
305
+ @click.option("--take", type=int, default=None, help="Number of versions to return.")
306
+ @click.pass_context
307
+ def versions(
308
+ ctx: click.Context,
309
+ name_or_id: str,
310
+ skip: int | None,
311
+ take: int | None,
312
+ ) -> None:
313
+ """List archived versions for a file.
314
+
315
+ \b
316
+ NAME_OR_ID is a file path, a file name matched in the current
317
+ directory, or a content ID (cont_...) which is resolved directly.
318
+
319
+ \b
320
+ Examples:
321
+ unique-cli versions report.pdf
322
+ unique-cli versions /Reports/Q1/report.pdf
323
+ unique-cli versions cont_abc123 --take 10
324
+ """
325
+ click.echo(cmd_versions(LazyState.get(ctx), name_or_id, skip=skip, take=take))
326
+
327
+
328
+ @main.command(name="restore-version")
329
+ @click.argument("content_version_id")
330
+ @click.pass_context
331
+ def restore_version(ctx: click.Context, content_version_id: str) -> None:
332
+ """Restore a file from a content version ID.
333
+
334
+ \b
335
+ CONTENT_VERSION_ID is returned by `unique-cli versions`.
336
+
337
+ \b
338
+ Examples:
339
+ unique-cli restore-version cver_abc123
340
+ """
341
+ click.echo(cmd_restore_version(LazyState.get(ctx), content_version_id))
342
+
343
+
284
344
  @main.command()
285
345
  @click.argument("name_or_id")
286
346
  @click.argument("local_dest", required=False, default=None)
@@ -289,8 +349,8 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
289
349
  """Download a file to your local machine.
290
350
 
291
351
  \b
292
- NAME_OR_ID is a file name (matched in the current directory) or
293
- a content ID (cont_...) which is resolved directly.
352
+ NAME_OR_ID is a file path, a file name matched in the current
353
+ directory, or a content ID (cont_...) which is resolved directly.
294
354
 
295
355
  \b
296
356
  LOCAL_DEST is an optional path (directory or file) to save to.
@@ -299,6 +359,7 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
299
359
  \b
300
360
  Examples:
301
361
  unique-cli download annual.pdf
362
+ unique-cli download /Reports/Q1/annual.pdf
302
363
  unique-cli download annual.pdf ./downloads/
303
364
  unique-cli download cont_abc123 ~/Desktop/
304
365
  """
@@ -324,10 +385,12 @@ def cite(
324
385
  \b
325
386
  Registers [filesourceN] markers for pages you referenced in your answer.
326
387
  Does NOT read or extract the file — use your own tools for that.
388
+ NAME_OR_ID can be a file path, current-directory file name, or content ID.
327
389
 
328
390
  \b
329
391
  Examples:
330
392
  unique-cli cite report.pdf --pages 3,5,7
393
+ unique-cli cite /Reports/Q1/report.pdf --pages 3,5,7
331
394
  unique-cli cite cont_abc123 --pages 1-4
332
395
  """
333
396
  click.echo(cmd_cite_file(LazyState.get(ctx), name_or_id, pages))
@@ -359,19 +422,92 @@ def read_cmd(ctx: click.Context, cont_id: str) -> None:
359
422
  click.echo(output)
360
423
 
361
424
 
425
+ @main.group(name="dynamic-frontend")
426
+ def dynamic_frontend() -> None:
427
+ """Deploy and list Dynamic Frontend spaces."""
428
+
429
+
430
+ @dynamic_frontend.command(name="deploy")
431
+ @click.option(
432
+ "--file",
433
+ "file_path",
434
+ default=None,
435
+ type=click.Path(exists=True),
436
+ help="Path to an upload-ready Dynamic Frontend ZIP bundle.",
437
+ )
438
+ @click.option(
439
+ "--content-id",
440
+ default=None,
441
+ help="Existing Knowledge Base content id for the ZIP bundle.",
442
+ )
443
+ @click.option(
444
+ "--name",
445
+ default=None,
446
+ help="Space display name. Required when creating; optional rename when updating.",
447
+ )
448
+ @click.option(
449
+ "--space-id", default=None, help="Existing Dynamic Frontend space id to update."
450
+ )
451
+ @click.option(
452
+ "--json", "output_json", is_flag=True, default=False, help="Print raw JSON."
453
+ )
454
+ @click.pass_context
455
+ def dynamic_frontend_deploy(
456
+ ctx: click.Context,
457
+ file_path: str | None,
458
+ content_id: str | None,
459
+ name: str | None,
460
+ space_id: str | None,
461
+ output_json: bool,
462
+ ) -> None:
463
+ """Create or update a Dynamic Frontend space.
464
+
465
+ \b
466
+ Examples:
467
+ unique-cli dynamic-frontend deploy --file ./app.zip --name "Revenue Dashboard"
468
+ unique-cli dynamic-frontend deploy --content-id content_123 --name "Revenue Dashboard"
469
+ unique-cli dynamic-frontend deploy --space-id assistant_123 --file ./app.zip
470
+ """
471
+ output = cmd_dynamic_frontend_deploy(
472
+ LazyState.get(ctx),
473
+ file=file_path,
474
+ content_id=content_id,
475
+ name=name,
476
+ space_id=space_id,
477
+ output_json=output_json,
478
+ )
479
+ click.echo(output)
480
+ if output.startswith(_DYNAMIC_FRONTEND_ERROR_PREFIX):
481
+ ctx.exit(1)
482
+
483
+
484
+ @dynamic_frontend.command(name="list")
485
+ @click.option(
486
+ "--json", "output_json", is_flag=True, default=False, help="Print raw JSON."
487
+ )
488
+ @click.pass_context
489
+ def dynamic_frontend_list(ctx: click.Context, output_json: bool) -> None:
490
+ """List Dynamic Frontend spaces the current user can manage."""
491
+ output = cmd_dynamic_frontend_list(LazyState.get(ctx), output_json=output_json)
492
+ click.echo(output)
493
+ if output.startswith(_DYNAMIC_FRONTEND_ERROR_PREFIX):
494
+ ctx.exit(1)
495
+
496
+
362
497
  @main.command()
363
498
  @click.argument("name_or_id")
364
499
  @click.pass_context
365
500
  def rm(ctx: click.Context, name_or_id: str) -> None:
366
- """Delete a file by name or content ID.
501
+ """Delete a file by path, name, or content ID.
367
502
 
368
503
  \b
369
- NAME_OR_ID is a file name (matched in the current directory) or
370
- a content ID (cont_...).
504
+ NAME_OR_ID is a file path, a file name matched in the current
505
+ directory, or a content ID (cont_...).
371
506
 
372
507
  \b
373
508
  Examples:
374
509
  unique-cli rm report.pdf
510
+ unique-cli rm /Reports/Q1/report.pdf
375
511
  unique-cli rm cont_abc123
376
512
  """
377
513
  click.echo(cmd_rm(LazyState.get(ctx), name_or_id))
@@ -386,11 +522,12 @@ def mv(ctx: click.Context, old_name: str, new_name: str) -> None:
386
522
 
387
523
  \b
388
524
  Changes the file's display title without changing its content ID
389
- or location. OLD_NAME can be a file name or content ID.
525
+ or location. OLD_NAME can be a file path, file name, or content ID.
390
526
 
391
527
  \b
392
528
  Examples:
393
529
  unique-cli mv annual.pdf annual-2025.pdf
530
+ unique-cli mv /Reports/Q1/annual.pdf annual-2025.pdf
394
531
  unique-cli mv cont_abc123 "New Title.pdf"
395
532
  """
396
533
  click.echo(cmd_mv_file(LazyState.get(ctx), old_name, new_name))
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import unique_sdk
7
+ from unique_sdk.cli.state import ShellState
8
+ from unique_sdk.utils.file_io import upload_file
9
+
10
+
11
+ def _upload_bundle(state: ShellState, file_path: str) -> str:
12
+ path = Path(file_path).expanduser().resolve()
13
+ if not path.is_file():
14
+ raise ValueError(f"local file not found: {file_path}")
15
+
16
+ if not state.scope_id:
17
+ raise ValueError("cannot upload bundle to root. cd into a folder first.")
18
+
19
+ result = upload_file(
20
+ userId=state.config.user_id,
21
+ companyId=state.config.company_id,
22
+ path_to_file=str(path),
23
+ displayed_filename=path.name,
24
+ mime_type="application/zip",
25
+ scope_or_unique_path=state.scope_id,
26
+ )
27
+ content_id = getattr(result, "id", None)
28
+ if not isinstance(content_id, str) or not content_id:
29
+ raise ValueError("upload did not return a content id")
30
+ return content_id
31
+
32
+
33
+ def _format_space(space: object) -> str:
34
+ space_id = getattr(space, "spaceId", None) or getattr(space, "id", "")
35
+ name = getattr(space, "name", "")
36
+ content_id = getattr(space, "contentId", "")
37
+ status = getattr(space, "status", None)
38
+ phase = ""
39
+ url = ""
40
+ if isinstance(status, dict):
41
+ phase = str(status.get("phase") or "")
42
+ url = str(status.get("url") or "")
43
+ return "\t".join(
44
+ part for part in [str(space_id), str(name), str(content_id), phase, url] if part
45
+ )
46
+
47
+
48
+ def cmd_dynamic_frontend_deploy(
49
+ state: ShellState,
50
+ *,
51
+ file: str | None = None,
52
+ content_id: str | None = None,
53
+ name: str | None = None,
54
+ space_id: str | None = None,
55
+ output_json: bool = False,
56
+ ) -> str:
57
+ try:
58
+ if not file and not content_id:
59
+ return "dynamic-frontend deploy: provide either --file or --content-id."
60
+ if file and content_id:
61
+ return "dynamic-frontend deploy: --file and --content-id are mutually exclusive."
62
+ if not space_id and not name:
63
+ return "dynamic-frontend deploy: provide --name when creating a Dynamic Frontend space."
64
+
65
+ resolved_content_id = _upload_bundle(state, file) if file else content_id
66
+ if not resolved_content_id:
67
+ return "dynamic-frontend deploy: expected a content id after resolving deploy input."
68
+
69
+ if space_id:
70
+ update_params: unique_sdk.DynamicFrontend.UpdateParams = {
71
+ "contentId": resolved_content_id,
72
+ }
73
+ if name is not None:
74
+ update_params["name"] = name
75
+ space = unique_sdk.DynamicFrontend.modify(
76
+ space_id,
77
+ user_id=state.config.user_id,
78
+ company_id=state.config.company_id,
79
+ **update_params,
80
+ )
81
+ action = "Updated"
82
+ else:
83
+ space = unique_sdk.DynamicFrontend.create(
84
+ user_id=state.config.user_id,
85
+ company_id=state.config.company_id,
86
+ name=name or "",
87
+ contentId=resolved_content_id,
88
+ )
89
+ action = "Created"
90
+
91
+ if output_json:
92
+ return json.dumps(dict(space), indent=2, default=str)
93
+
94
+ space_id_value = getattr(space, "spaceId", None) or getattr(space, "id", "")
95
+ name_value = getattr(space, "name", name or "")
96
+ return (
97
+ f'{action} Dynamic Frontend space "{name_value}" ({space_id_value})\n'
98
+ f"Content: {resolved_content_id}"
99
+ )
100
+ except (ValueError, unique_sdk.APIError, OSError) as e:
101
+ return f"dynamic-frontend deploy: {e}"
102
+
103
+
104
+ def cmd_dynamic_frontend_list(state: ShellState, *, output_json: bool = False) -> str:
105
+ try:
106
+ spaces = unique_sdk.DynamicFrontend.list(
107
+ user_id=state.config.user_id,
108
+ company_id=state.config.company_id,
109
+ )
110
+ if output_json:
111
+ return json.dumps(spaces, indent=2, default=str)
112
+ if not spaces:
113
+ return "No manageable Dynamic Frontend spaces found."
114
+ return "\n".join(_format_space(space) for space in spaces)
115
+ except unique_sdk.APIError as e:
116
+ return f"dynamic-frontend list: {e}"