unique-sdk 2026.26.0.dev8__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.
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/PKG-INFO +1 -1
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/pyproject.toml +1 -1
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_content.py +158 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/cli.py +70 -12
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/files.py +136 -10
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/shell.py +110 -26
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +21 -3
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/file_io.py +7 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/README.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_api_requestor.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_api_resource.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_api_version.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_error.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_http_client.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_list_object.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_object_classes.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_request_options.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_unique_object.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_unique_ql.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_unique_response.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_util.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_version.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/_webhook.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_acronyms.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_agentic_table.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_analytics_order.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_benchmarking.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_briefing.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_chat_completion.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_elicitation.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_embedding.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_event.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_folder.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_group.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_integrated.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_llm_models.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_mcp.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_assessment.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_execution.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_log.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_tool.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_module.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_scheduled_task.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_search_string.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_short_term_memory.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_space.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_user.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_web_search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/__main__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/cite_file.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/elicitation.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/folders.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/mcp.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/navigation.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/read.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/subagent.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/web_search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/web_search_config.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/config.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/formatting.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/state.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/analytics_order_run.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/benchmarking_run.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/chat_history.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/chat_in_space.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/sources.py +0 -0
- {unique_sdk-2026.26.0.dev8 → 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.
|
|
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>
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_content.py
RENAMED
|
@@ -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,
|
|
@@ -20,7 +20,14 @@ from unique_sdk.cli.commands.elicitation import (
|
|
|
20
20
|
cmd_elicit_respond,
|
|
21
21
|
cmd_elicit_wait,
|
|
22
22
|
)
|
|
23
|
-
from unique_sdk.cli.commands.files import
|
|
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
|
+
)
|
|
24
31
|
from unique_sdk.cli.commands.folders import cmd_mkdir, cmd_mvdir, cmd_rmdir
|
|
25
32
|
from unique_sdk.cli.commands.mcp import cmd_mcp
|
|
26
33
|
from unique_sdk.cli.commands.navigation import cmd_cd, cmd_ls, cmd_pwd
|
|
@@ -91,6 +98,7 @@ Path formats accepted by all commands:
|
|
|
91
98
|
\b
|
|
92
99
|
File identifiers:
|
|
93
100
|
report.pdf File name (matched in current directory)
|
|
101
|
+
/Reports/report.pdf File path (absolute or relative)
|
|
94
102
|
cont_abc123 Content ID (used directly)
|
|
95
103
|
|
|
96
104
|
\b
|
|
@@ -99,8 +107,10 @@ Examples:
|
|
|
99
107
|
unique-cli ls List root folders
|
|
100
108
|
unique-cli ls /Reports List a specific folder
|
|
101
109
|
unique-cli search "revenue" -l 50 Search with custom limit
|
|
102
|
-
unique-cli upload ./file.pdf Upload to current folder
|
|
110
|
+
unique-cli upload ./file.pdf Upload versioned to current folder
|
|
103
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
|
|
104
114
|
unique-cli elicit ask "Which?" Ask the user a question synchronously
|
|
105
115
|
unique-cli subagent Legal "Review" Invoke a connected space/subagent
|
|
106
116
|
unique-cli web-search search "x" Search the web via the public API
|
|
@@ -261,12 +271,13 @@ def mvdir(ctx: click.Context, old_name: str, new_name: str) -> None:
|
|
|
261
271
|
@click.argument("destination", required=False, default=None)
|
|
262
272
|
@click.pass_context
|
|
263
273
|
def upload(ctx: click.Context, local_path: str, destination: str | None) -> None:
|
|
264
|
-
"""Upload a local file (works like Linux cp).
|
|
274
|
+
"""Upload a local file with versioning enabled (works like Linux cp).
|
|
265
275
|
|
|
266
276
|
\b
|
|
267
|
-
Uploads LOCAL_PATH to the Unique platform
|
|
268
|
-
the target in cp -- it can be a
|
|
269
|
-
a combination of both. MIME type is
|
|
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.
|
|
270
281
|
|
|
271
282
|
\b
|
|
272
283
|
Destination formats:
|
|
@@ -288,6 +299,48 @@ def upload(ctx: click.Context, local_path: str, destination: str | None) -> None
|
|
|
288
299
|
click.echo(cmd_upload(LazyState.get(ctx), local_path, destination))
|
|
289
300
|
|
|
290
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
|
+
|
|
291
344
|
@main.command()
|
|
292
345
|
@click.argument("name_or_id")
|
|
293
346
|
@click.argument("local_dest", required=False, default=None)
|
|
@@ -296,8 +349,8 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
|
|
|
296
349
|
"""Download a file to your local machine.
|
|
297
350
|
|
|
298
351
|
\b
|
|
299
|
-
NAME_OR_ID is a file name
|
|
300
|
-
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.
|
|
301
354
|
|
|
302
355
|
\b
|
|
303
356
|
LOCAL_DEST is an optional path (directory or file) to save to.
|
|
@@ -306,6 +359,7 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
|
|
|
306
359
|
\b
|
|
307
360
|
Examples:
|
|
308
361
|
unique-cli download annual.pdf
|
|
362
|
+
unique-cli download /Reports/Q1/annual.pdf
|
|
309
363
|
unique-cli download annual.pdf ./downloads/
|
|
310
364
|
unique-cli download cont_abc123 ~/Desktop/
|
|
311
365
|
"""
|
|
@@ -331,10 +385,12 @@ def cite(
|
|
|
331
385
|
\b
|
|
332
386
|
Registers [filesourceN] markers for pages you referenced in your answer.
|
|
333
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.
|
|
334
389
|
|
|
335
390
|
\b
|
|
336
391
|
Examples:
|
|
337
392
|
unique-cli cite report.pdf --pages 3,5,7
|
|
393
|
+
unique-cli cite /Reports/Q1/report.pdf --pages 3,5,7
|
|
338
394
|
unique-cli cite cont_abc123 --pages 1-4
|
|
339
395
|
"""
|
|
340
396
|
click.echo(cmd_cite_file(LazyState.get(ctx), name_or_id, pages))
|
|
@@ -442,15 +498,16 @@ def dynamic_frontend_list(ctx: click.Context, output_json: bool) -> None:
|
|
|
442
498
|
@click.argument("name_or_id")
|
|
443
499
|
@click.pass_context
|
|
444
500
|
def rm(ctx: click.Context, name_or_id: str) -> None:
|
|
445
|
-
"""Delete a file by name or content ID.
|
|
501
|
+
"""Delete a file by path, name, or content ID.
|
|
446
502
|
|
|
447
503
|
\b
|
|
448
|
-
NAME_OR_ID is a file name
|
|
449
|
-
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_...).
|
|
450
506
|
|
|
451
507
|
\b
|
|
452
508
|
Examples:
|
|
453
509
|
unique-cli rm report.pdf
|
|
510
|
+
unique-cli rm /Reports/Q1/report.pdf
|
|
454
511
|
unique-cli rm cont_abc123
|
|
455
512
|
"""
|
|
456
513
|
click.echo(cmd_rm(LazyState.get(ctx), name_or_id))
|
|
@@ -465,11 +522,12 @@ def mv(ctx: click.Context, old_name: str, new_name: str) -> None:
|
|
|
465
522
|
|
|
466
523
|
\b
|
|
467
524
|
Changes the file's display title without changing its content ID
|
|
468
|
-
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.
|
|
469
526
|
|
|
470
527
|
\b
|
|
471
528
|
Examples:
|
|
472
529
|
unique-cli mv annual.pdf annual-2025.pdf
|
|
530
|
+
unique-cli mv /Reports/Q1/annual.pdf annual-2025.pdf
|
|
473
531
|
unique-cli mv cont_abc123 "New Title.pdf"
|
|
474
532
|
"""
|
|
475
533
|
click.echo(cmd_mv_file(LazyState.get(ctx), old_name, new_name))
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import mimetypes
|
|
6
6
|
import shutil
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
@@ -13,32 +14,113 @@ from unique_sdk.cli.state import ShellState
|
|
|
13
14
|
from unique_sdk.utils.file_io import download_content, upload_file
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def _normalize_unique_file_path(cwd: str, path: str) -> str:
|
|
18
|
+
"""Normalize a Unique file path without allowing traversal above root."""
|
|
19
|
+
raw_parts = (
|
|
20
|
+
path.split("/") if path.startswith("/") else [*cwd.split("/"), *path.split("/")]
|
|
21
|
+
)
|
|
22
|
+
parts: list[str] = []
|
|
23
|
+
for part in raw_parts:
|
|
24
|
+
if part in ("", "."):
|
|
25
|
+
continue
|
|
26
|
+
if part == "..":
|
|
27
|
+
if not parts:
|
|
28
|
+
raise ValueError(f"File path escapes root: {path}")
|
|
29
|
+
parts.pop()
|
|
30
|
+
continue
|
|
31
|
+
parts.append(part)
|
|
32
|
+
return "/" + "/".join(parts)
|
|
33
|
+
|
|
34
|
+
|
|
16
35
|
def _resolve_content_id(state: ShellState, name_or_id: str) -> tuple[str, str]:
|
|
17
36
|
"""Resolve a file name or content ID to (content_id, display_name).
|
|
18
37
|
|
|
19
|
-
Accepts
|
|
38
|
+
Accepts a content ID (cont_...), a file name in the current folder,
|
|
39
|
+
or an absolute/relative Unique file path.
|
|
20
40
|
"""
|
|
21
41
|
if name_or_id.startswith("cont_"):
|
|
42
|
+
if not state.is_content_within_workspace(name_or_id):
|
|
43
|
+
raise ValueError("permission denied (outside workspace scope)")
|
|
22
44
|
return name_or_id, name_or_id
|
|
23
45
|
|
|
46
|
+
lookup_name = name_or_id
|
|
24
47
|
scope_id = state.scope_id
|
|
48
|
+
if "/" in name_or_id:
|
|
49
|
+
unique_path = _normalize_unique_file_path(state.cwd, name_or_id)
|
|
50
|
+
folder_path, lookup_name = unique_path.rsplit("/", 1)
|
|
51
|
+
if not lookup_name:
|
|
52
|
+
raise ValueError(f"File path must include a file name: {name_or_id}")
|
|
53
|
+
if not folder_path:
|
|
54
|
+
folder_path = "/"
|
|
55
|
+
if not state.is_folder_target_within_workspace(folder_path):
|
|
56
|
+
raise ValueError("permission denied (outside workspace scope)")
|
|
57
|
+
|
|
58
|
+
info = unique_sdk.Folder.get_info(
|
|
59
|
+
user_id=state.config.user_id,
|
|
60
|
+
company_id=state.config.company_id,
|
|
61
|
+
folderPath=folder_path,
|
|
62
|
+
)
|
|
63
|
+
scope_id = info.get("id")
|
|
64
|
+
if not scope_id:
|
|
65
|
+
raise ValueError(f"folder not found: {folder_path}")
|
|
66
|
+
elif not state.is_within_workspace():
|
|
67
|
+
raise ValueError("permission denied (outside workspace scope)")
|
|
68
|
+
|
|
25
69
|
params: dict[str, Any] = {}
|
|
26
70
|
if scope_id:
|
|
27
71
|
params["parentId"] = scope_id
|
|
28
72
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
take = 100
|
|
74
|
+
skip = 0
|
|
75
|
+
while True:
|
|
76
|
+
result = unique_sdk.Content.get_infos(
|
|
77
|
+
user_id=state.config.user_id,
|
|
78
|
+
company_id=state.config.company_id,
|
|
79
|
+
skip=skip,
|
|
80
|
+
take=take,
|
|
81
|
+
**params,
|
|
82
|
+
)
|
|
83
|
+
content_infos = result.get("contentInfos", [])
|
|
84
|
+
if not content_infos:
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
for info in content_infos:
|
|
88
|
+
title = info.get("title") or ""
|
|
89
|
+
key = info.get("key") or ""
|
|
90
|
+
if lookup_name in {title, key}:
|
|
91
|
+
return info["id"], title or key
|
|
92
|
+
|
|
93
|
+
skip += len(content_infos)
|
|
38
94
|
|
|
39
95
|
raise ValueError(f"File not found: {name_or_id}")
|
|
40
96
|
|
|
41
97
|
|
|
98
|
+
def _format_version_value(value: Any) -> str:
|
|
99
|
+
if value is None:
|
|
100
|
+
return ""
|
|
101
|
+
return str(value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_content_versions(versions: Sequence[Mapping[str, Any]]) -> str:
|
|
105
|
+
if not versions:
|
|
106
|
+
return "No versions found."
|
|
107
|
+
|
|
108
|
+
lines = ["VERSION VERSION_ID ARCHIVED_AT REASON TITLE"]
|
|
109
|
+
for version in versions:
|
|
110
|
+
lines.append(
|
|
111
|
+
" ".join(
|
|
112
|
+
[
|
|
113
|
+
_format_version_value(version.get("versionNumber")),
|
|
114
|
+
_format_version_value(version.get("id")),
|
|
115
|
+
_format_version_value(version.get("archivedAt")),
|
|
116
|
+
_format_version_value(version.get("reason")),
|
|
117
|
+
_format_version_value(version.get("title") or version.get("key")),
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
42
124
|
def _resolve_upload_destination(
|
|
43
125
|
state: ShellState,
|
|
44
126
|
local_filename: str,
|
|
@@ -144,6 +226,7 @@ def cmd_upload(
|
|
|
144
226
|
displayed_filename=display_name,
|
|
145
227
|
mime_type=mime_type,
|
|
146
228
|
scope_or_unique_path=scope_id,
|
|
229
|
+
versioning_enabled=True,
|
|
147
230
|
)
|
|
148
231
|
|
|
149
232
|
content_id = result.id if hasattr(result, "id") else "?"
|
|
@@ -158,6 +241,49 @@ def cmd_upload(
|
|
|
158
241
|
return f"upload: {e}"
|
|
159
242
|
|
|
160
243
|
|
|
244
|
+
def cmd_versions(
|
|
245
|
+
state: ShellState,
|
|
246
|
+
name_or_id: str,
|
|
247
|
+
skip: int | None = None,
|
|
248
|
+
take: int | None = None,
|
|
249
|
+
) -> str:
|
|
250
|
+
"""List archived versions for a file by name or content ID."""
|
|
251
|
+
try:
|
|
252
|
+
content_id, display_name = _resolve_content_id(state, name_or_id)
|
|
253
|
+
params: dict[str, Any] = {"contentId": content_id}
|
|
254
|
+
if skip is not None:
|
|
255
|
+
params["skip"] = skip
|
|
256
|
+
if take is not None:
|
|
257
|
+
params["take"] = take
|
|
258
|
+
|
|
259
|
+
result = unique_sdk.Content.versions(
|
|
260
|
+
user_id=state.config.user_id,
|
|
261
|
+
company_id=state.config.company_id,
|
|
262
|
+
**params,
|
|
263
|
+
)
|
|
264
|
+
data = result.get("data", [])
|
|
265
|
+
return f"Versions for {display_name} ({content_id}):\n{_format_content_versions(data)}"
|
|
266
|
+
except (ValueError, unique_sdk.APIError) as e:
|
|
267
|
+
return f"versions: {e}"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_restore_version(state: ShellState, content_version_id: str) -> str:
|
|
271
|
+
"""Restore a file from an archived content version ID."""
|
|
272
|
+
if not state.is_within_workspace():
|
|
273
|
+
return "restore-version: permission denied (outside workspace scope)"
|
|
274
|
+
try:
|
|
275
|
+
result = unique_sdk.Content.restore_version(
|
|
276
|
+
user_id=state.config.user_id,
|
|
277
|
+
company_id=state.config.company_id,
|
|
278
|
+
contentVersionId=content_version_id,
|
|
279
|
+
)
|
|
280
|
+
title = result.get("title") or result.get("key") or result.get("id", "?")
|
|
281
|
+
content_id = result.get("id", "?")
|
|
282
|
+
return f"Restored: {title} ({content_id}) from version {content_version_id}"
|
|
283
|
+
except (ValueError, unique_sdk.APIError) as e:
|
|
284
|
+
return f"restore-version: {e}"
|
|
285
|
+
|
|
286
|
+
|
|
161
287
|
def cmd_download(
|
|
162
288
|
state: ShellState,
|
|
163
289
|
name_or_id: str,
|
|
@@ -16,7 +16,14 @@ from unique_sdk.cli.commands.elicitation import (
|
|
|
16
16
|
cmd_elicit_respond,
|
|
17
17
|
cmd_elicit_wait,
|
|
18
18
|
)
|
|
19
|
-
from unique_sdk.cli.commands.files import
|
|
19
|
+
from unique_sdk.cli.commands.files import (
|
|
20
|
+
cmd_download,
|
|
21
|
+
cmd_mv_file,
|
|
22
|
+
cmd_restore_version,
|
|
23
|
+
cmd_rm,
|
|
24
|
+
cmd_upload,
|
|
25
|
+
cmd_versions,
|
|
26
|
+
)
|
|
20
27
|
from unique_sdk.cli.commands.folders import cmd_mkdir, cmd_mvdir, cmd_rmdir
|
|
21
28
|
from unique_sdk.cli.commands.mcp import cmd_mcp
|
|
22
29
|
from unique_sdk.cli.commands.navigation import cmd_cd, cmd_ls, cmd_pwd
|
|
@@ -35,7 +42,7 @@ OVERVIEW_HELP = textwrap.dedent("""\
|
|
|
35
42
|
|
|
36
43
|
Navigate the knowledge base like a Linux filesystem. Folders are
|
|
37
44
|
identified by name, path, or scope ID. Files are identified by
|
|
38
|
-
name or content ID.
|
|
45
|
+
name, path, or content ID.
|
|
39
46
|
|
|
40
47
|
Navigation:
|
|
41
48
|
pwd Print current working directory
|
|
@@ -48,11 +55,13 @@ OVERVIEW_HELP = textwrap.dedent("""\
|
|
|
48
55
|
mvdir <old> <new> Rename a folder
|
|
49
56
|
|
|
50
57
|
File operations:
|
|
51
|
-
upload <local> [name] Upload a local file
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
upload <local> [name] Upload a local file with versioning
|
|
59
|
+
versions <name|path|id> List archived file versions
|
|
60
|
+
restore-version <ver_id> Restore a file from a version
|
|
61
|
+
download <name|path|id> [dest] Download a file to local machine
|
|
62
|
+
rm <name|path|id> Delete a file
|
|
63
|
+
mv <old|path|id> <new> Rename a file
|
|
64
|
+
cite <name|path|id> [--pages] Declare page citations for a file
|
|
56
65
|
|
|
57
66
|
Search:
|
|
58
67
|
search <query> [options] Combined search (vector + full-text)
|
|
@@ -292,13 +301,14 @@ class UniqueShell(cmd.Cmd):
|
|
|
292
301
|
# -- File operations --
|
|
293
302
|
|
|
294
303
|
def do_upload(self, arg: str) -> None:
|
|
295
|
-
"""Upload a local file (works like Linux cp).
|
|
304
|
+
"""Upload a local file with versioning enabled (works like Linux cp).
|
|
296
305
|
|
|
297
306
|
Usage: upload <local_path> [destination]
|
|
298
307
|
|
|
299
|
-
Uploads a file from your local machine
|
|
300
|
-
works like cp -- it can be a
|
|
301
|
-
MIME type is auto-detected from
|
|
308
|
+
Uploads a file from your local machine with immutable versioning
|
|
309
|
+
enabled. The destination argument works like cp -- it can be a
|
|
310
|
+
folder, a new filename, or both. MIME type is auto-detected from
|
|
311
|
+
the file extension.
|
|
302
312
|
|
|
303
313
|
Destination formats:
|
|
304
314
|
(omitted) Upload to current dir, keep original name
|
|
@@ -330,17 +340,78 @@ class UniqueShell(cmd.Cmd):
|
|
|
330
340
|
destination = parts[1] if len(parts) > 1 else None
|
|
331
341
|
self._print(cmd_upload(self.state, local_path, destination))
|
|
332
342
|
|
|
343
|
+
def do_versions(self, arg: str) -> None:
|
|
344
|
+
"""List archived versions for a file.
|
|
345
|
+
|
|
346
|
+
Usage: versions <name|path|content_id> [--skip N] [--take N]
|
|
347
|
+
|
|
348
|
+
Lists immutable versions for a file identified by its Unique
|
|
349
|
+
path, name (matched in the current directory), or content ID
|
|
350
|
+
(cont_...). Use the VERSION_ID column with restore-version.
|
|
351
|
+
|
|
352
|
+
Examples:
|
|
353
|
+
/Reports> versions annual.pdf
|
|
354
|
+
/Reports> versions /Reports/Q1/annual.pdf
|
|
355
|
+
/Reports> versions cont_mno345 --take 10
|
|
356
|
+
"""
|
|
357
|
+
parts = shlex.split(arg)
|
|
358
|
+
if not parts:
|
|
359
|
+
self._print("Usage: versions <name|path|content_id> [--skip N] [--take N]")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
name_or_id = parts[0]
|
|
363
|
+
skip: int | None = None
|
|
364
|
+
take: int | None = None
|
|
365
|
+
i = 1
|
|
366
|
+
while i < len(parts):
|
|
367
|
+
if parts[i] == "--skip" and i + 1 < len(parts):
|
|
368
|
+
try:
|
|
369
|
+
skip = int(parts[i + 1])
|
|
370
|
+
except ValueError:
|
|
371
|
+
self._print(f"Invalid --skip: {parts[i + 1]}")
|
|
372
|
+
return
|
|
373
|
+
i += 2
|
|
374
|
+
elif parts[i] == "--take" and i + 1 < len(parts):
|
|
375
|
+
try:
|
|
376
|
+
take = int(parts[i + 1])
|
|
377
|
+
except ValueError:
|
|
378
|
+
self._print(f"Invalid --take: {parts[i + 1]}")
|
|
379
|
+
return
|
|
380
|
+
i += 2
|
|
381
|
+
else:
|
|
382
|
+
self._print(f"Unknown option: {parts[i]}")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
self._print(cmd_versions(self.state, name_or_id, skip=skip, take=take))
|
|
386
|
+
|
|
387
|
+
def do_restore_version(self, arg: str) -> None:
|
|
388
|
+
"""Restore a file from a content version ID.
|
|
389
|
+
|
|
390
|
+
Usage: restore-version <content_version_id>
|
|
391
|
+
|
|
392
|
+
The content version ID is shown by the versions command.
|
|
393
|
+
|
|
394
|
+
Example:
|
|
395
|
+
/Reports> restore-version cver_abc123
|
|
396
|
+
Restored: annual.pdf (cont_mno345) from version cver_abc123
|
|
397
|
+
"""
|
|
398
|
+
content_version_id = arg.strip()
|
|
399
|
+
if not content_version_id:
|
|
400
|
+
self._print("Usage: restore-version <content_version_id>")
|
|
401
|
+
return
|
|
402
|
+
self._print(cmd_restore_version(self.state, content_version_id))
|
|
403
|
+
|
|
333
404
|
def do_download(self, arg: str) -> None:
|
|
334
405
|
"""Download a file from the platform to your local machine.
|
|
335
406
|
|
|
336
|
-
Usage: download <name|content_id> [local_path]
|
|
407
|
+
Usage: download <name|path|content_id> [local_path]
|
|
337
408
|
|
|
338
|
-
Downloads a file identified by its name (matched in
|
|
339
|
-
directory) or content ID (cont_...). Saves to the
|
|
340
|
-
path, or the current working directory if omitted.
|
|
409
|
+
Downloads a file identified by its Unique path, name (matched in
|
|
410
|
+
the current directory), or content ID (cont_...). Saves to the
|
|
411
|
+
specified local path, or the current working directory if omitted.
|
|
341
412
|
|
|
342
413
|
Arguments:
|
|
343
|
-
name_or_id File name or content ID (cont_...)
|
|
414
|
+
name_or_id File path, file name, or content ID (cont_...)
|
|
344
415
|
local_path Optional local directory or file path
|
|
345
416
|
|
|
346
417
|
Examples:
|
|
@@ -350,12 +421,15 @@ class UniqueShell(cmd.Cmd):
|
|
|
350
421
|
/Reports> download annual.pdf ./downloads/
|
|
351
422
|
Downloaded: annual.pdf -> ./downloads/annual.pdf
|
|
352
423
|
|
|
424
|
+
/Reports> download /Reports/Q1/annual.pdf ./downloads/
|
|
425
|
+
Downloaded: annual.pdf -> ./downloads/annual.pdf
|
|
426
|
+
|
|
353
427
|
/Reports> download cont_mno345 ~/Desktop/
|
|
354
428
|
Downloaded: cont_mno345 -> ~/Desktop/cont_mno345
|
|
355
429
|
"""
|
|
356
430
|
parts = shlex.split(arg)
|
|
357
431
|
if not parts:
|
|
358
|
-
self._print("Usage: download <name|content_id> [local_path]")
|
|
432
|
+
self._print("Usage: download <name|path|content_id> [local_path]")
|
|
359
433
|
return
|
|
360
434
|
name_or_id = parts[0]
|
|
361
435
|
local_dest = parts[1] if len(parts) > 1 else None
|
|
@@ -364,17 +438,18 @@ class UniqueShell(cmd.Cmd):
|
|
|
364
438
|
def do_cite(self, arg: str) -> None:
|
|
365
439
|
"""Declare page citations for a file.
|
|
366
440
|
|
|
367
|
-
Usage: cite <name|content_id> [--pages RANGE]
|
|
441
|
+
Usage: cite <name|path|content_id> [--pages RANGE]
|
|
368
442
|
|
|
369
443
|
Examples:
|
|
370
444
|
/Reports> cite report.pdf --pages 3,5,7
|
|
445
|
+
/Reports> cite /Reports/Q1/report.pdf --pages 3,5,7
|
|
371
446
|
/Reports> cite cont_abc123 --pages 1-4
|
|
372
447
|
"""
|
|
373
448
|
from unique_sdk.cli.commands.cite_file import cmd_cite_file
|
|
374
449
|
|
|
375
450
|
parts = shlex.split(arg)
|
|
376
451
|
if not parts:
|
|
377
|
-
self._print("Usage: cite <name|content_id> [--pages RANGE]")
|
|
452
|
+
self._print("Usage: cite <name|path|content_id> [--pages RANGE]")
|
|
378
453
|
return
|
|
379
454
|
pages: str | None = None
|
|
380
455
|
positional: list[str] = []
|
|
@@ -391,7 +466,7 @@ class UniqueShell(cmd.Cmd):
|
|
|
391
466
|
positional.append(token)
|
|
392
467
|
index += 1
|
|
393
468
|
if not positional:
|
|
394
|
-
self._print("Usage: cite <name|content_id> [--pages RANGE]")
|
|
469
|
+
self._print("Usage: cite <name|path|content_id> [--pages RANGE]")
|
|
395
470
|
return
|
|
396
471
|
self._print(cmd_cite_file(self.state, positional[0], pages))
|
|
397
472
|
|
|
@@ -420,28 +495,31 @@ class UniqueShell(cmd.Cmd):
|
|
|
420
495
|
def do_rm(self, arg: str) -> None:
|
|
421
496
|
"""Delete a file.
|
|
422
497
|
|
|
423
|
-
Usage: rm <name|content_id>
|
|
498
|
+
Usage: rm <name|path|content_id>
|
|
424
499
|
|
|
425
|
-
Permanently deletes a file by its name (matched in
|
|
426
|
-
directory) or content ID.
|
|
500
|
+
Permanently deletes a file by its Unique path, name (matched in
|
|
501
|
+
the current directory), or content ID.
|
|
427
502
|
|
|
428
503
|
Examples:
|
|
429
504
|
/Reports> rm annual.pdf
|
|
430
505
|
Deleted: annual.pdf (cont_mno345)
|
|
431
506
|
|
|
507
|
+
/Reports> rm /Reports/Q1/annual.pdf
|
|
508
|
+
Deleted: annual.pdf (cont_mno345)
|
|
509
|
+
|
|
432
510
|
/Reports> rm cont_xyz789
|
|
433
511
|
Deleted: cont_xyz789 (cont_xyz789)
|
|
434
512
|
"""
|
|
435
513
|
name_or_id = arg.strip()
|
|
436
514
|
if not name_or_id:
|
|
437
|
-
self._print("Usage: rm <name|content_id>")
|
|
515
|
+
self._print("Usage: rm <name|path|content_id>")
|
|
438
516
|
return
|
|
439
517
|
self._print(cmd_rm(self.state, name_or_id))
|
|
440
518
|
|
|
441
519
|
def do_mv(self, arg: str) -> None:
|
|
442
520
|
"""Rename a file.
|
|
443
521
|
|
|
444
|
-
Usage: mv <old_name|content_id> <new_name>
|
|
522
|
+
Usage: mv <old_name|path|content_id> <new_name>
|
|
445
523
|
|
|
446
524
|
Changes the file's display title. The content ID and location
|
|
447
525
|
remain the same.
|
|
@@ -450,12 +528,15 @@ class UniqueShell(cmd.Cmd):
|
|
|
450
528
|
/Reports> mv annual.pdf annual-2025.pdf
|
|
451
529
|
Renamed: annual.pdf -> annual-2025.pdf
|
|
452
530
|
|
|
531
|
+
/Reports> mv /Reports/Q1/annual.pdf annual-2025.pdf
|
|
532
|
+
Renamed: annual.pdf -> annual-2025.pdf
|
|
533
|
+
|
|
453
534
|
/Reports> mv cont_abc123 "New Title.pdf"
|
|
454
535
|
Renamed: cont_abc123 -> New Title.pdf
|
|
455
536
|
"""
|
|
456
537
|
parts = shlex.split(arg)
|
|
457
538
|
if len(parts) != 2:
|
|
458
|
-
self._print("Usage: mv <old_name|content_id> <new_name>")
|
|
539
|
+
self._print("Usage: mv <old_name|path|content_id> <new_name>")
|
|
459
540
|
return
|
|
460
541
|
self._print(cmd_mv_file(self.state, parts[0], parts[1]))
|
|
461
542
|
|
|
@@ -1103,6 +1184,9 @@ class UniqueShell(cmd.Cmd):
|
|
|
1103
1184
|
return False
|
|
1104
1185
|
|
|
1105
1186
|
def default(self, line: str) -> None:
|
|
1187
|
+
if line.startswith("restore-version"):
|
|
1188
|
+
self.do_restore_version(line[len("restore-version") :].strip())
|
|
1189
|
+
return
|
|
1106
1190
|
self._print(
|
|
1107
1191
|
f"Unknown command: {line.split()[0]}. Type 'help' for available commands."
|
|
1108
1192
|
)
|
|
@@ -3,7 +3,7 @@ name: unique-cli-file-management
|
|
|
3
3
|
description: >-
|
|
4
4
|
Manage files and folders on the Unique AI Platform using the unique-cli
|
|
5
5
|
command-line tool. Use when the user asks to upload, download, delete,
|
|
6
|
-
rename, list, find, look for, or organize files and folders on Unique,
|
|
6
|
+
rename, list, find, restore versions, list versions, look for, or organize files and folders on Unique,
|
|
7
7
|
or when working with scope IDs (scope_*) or content IDs (cont_*).
|
|
8
8
|
IMPORTANT: When a user says they are "looking for a file" or wants to
|
|
9
9
|
"find a file", they typically mean locating it within the Unique AI
|
|
@@ -50,11 +50,16 @@ unique-cli rmdir scope_abc123 -r
|
|
|
50
50
|
# Rename a folder
|
|
51
51
|
unique-cli mvdir Q1 "Q1-2025"
|
|
52
52
|
|
|
53
|
-
# Upload a file (to current scope -- cd first or specify destination)
|
|
53
|
+
# Upload a file with versioning enabled (to current scope -- cd first or specify destination)
|
|
54
54
|
unique-cli upload ./report.pdf
|
|
55
55
|
unique-cli upload ./report.pdf /Reports/Q1/
|
|
56
56
|
unique-cli upload ./data.csv scope_abc123
|
|
57
57
|
|
|
58
|
+
# List and restore file versions
|
|
59
|
+
unique-cli versions /Reports/Q1/report.pdf
|
|
60
|
+
unique-cli versions cont_abc123 --take 10
|
|
61
|
+
unique-cli restore-version cver_abc123
|
|
62
|
+
|
|
58
63
|
# Download a file
|
|
59
64
|
unique-cli download report.pdf ./local/
|
|
60
65
|
unique-cli download cont_abc123 ~/Desktop/
|
|
@@ -81,10 +86,11 @@ unique-cli mv report.pdf "Annual Report 2025.pdf"
|
|
|
81
86
|
| `..` | `..` | Parent directory |
|
|
82
87
|
| `/` | `/` | Root |
|
|
83
88
|
| Content ID | `cont_abc123` | File directly by ID |
|
|
89
|
+
| File path | `/Reports/Q1/report.pdf` | File in a folder |
|
|
84
90
|
|
|
85
91
|
## Upload Destination Resolution
|
|
86
92
|
|
|
87
|
-
The `upload` destination works like Linux `cp`:
|
|
93
|
+
The `upload` command always enables immutable content versioning. It does not expose an unversioned upload mode. Its destination works like Linux `cp`:
|
|
88
94
|
|
|
89
95
|
| Destination | Behavior |
|
|
90
96
|
|-------------|----------|
|
|
@@ -116,6 +122,18 @@ unique-cli download "annual.pdf" ./downloads/
|
|
|
116
122
|
unique-cli download cont_abc123 ./downloads/
|
|
117
123
|
```
|
|
118
124
|
|
|
125
|
+
### Restore a previous file version
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# List versions for a file path, file name in the current folder, or content ID.
|
|
129
|
+
unique-cli versions /Reports/Q1/annual.pdf
|
|
130
|
+
unique-cli versions "annual.pdf"
|
|
131
|
+
unique-cli versions cont_abc123 --take 20
|
|
132
|
+
|
|
133
|
+
# Restore using the VERSION_ID shown by `versions`.
|
|
134
|
+
unique-cli restore-version cver_abc123
|
|
135
|
+
```
|
|
136
|
+
|
|
119
137
|
### Create folder hierarchy and upload
|
|
120
138
|
|
|
121
139
|
```bash
|
|
@@ -106,6 +106,7 @@ def upload_file(
|
|
|
106
106
|
ingestion_config: Content.IngestionConfig | None = None,
|
|
107
107
|
metadata: dict[str, Any] | None = None,
|
|
108
108
|
preview_pdf_path: str | None = None,
|
|
109
|
+
versioning_enabled: bool | None = None,
|
|
109
110
|
):
|
|
110
111
|
"""Upload *path_to_file* as a Unique :class:`Content`.
|
|
111
112
|
|
|
@@ -148,6 +149,9 @@ def upload_file(
|
|
|
148
149
|
responsibility — there is no override kwarg, by design,
|
|
149
150
|
so all callers land on the same ``${content.id}_pdfPreview``
|
|
150
151
|
convention as the ingestion worker.
|
|
152
|
+
versioning_enabled: When ``True``, ask the platform to archive
|
|
153
|
+
previous blobs for the same content so they can be listed
|
|
154
|
+
and restored through the content version endpoints.
|
|
151
155
|
"""
|
|
152
156
|
if not chat_id and not scope_or_unique_path:
|
|
153
157
|
raise ValueError("chat_id or scope_or_unique_path must be provided")
|
|
@@ -177,6 +181,7 @@ def upload_file(
|
|
|
177
181
|
},
|
|
178
182
|
scopeId=scope_or_unique_path,
|
|
179
183
|
chatId=chat_id,
|
|
184
|
+
versioningEnabled=versioning_enabled,
|
|
180
185
|
)
|
|
181
186
|
|
|
182
187
|
# Step 2 — PUT the original bytes to the SAS URL minted by Step 1.
|
|
@@ -229,6 +234,7 @@ def upload_file(
|
|
|
229
234
|
},
|
|
230
235
|
fileUrl=createdContent.readUrl,
|
|
231
236
|
chatId=chat_id,
|
|
237
|
+
versioningEnabled=versioning_enabled,
|
|
232
238
|
**preview_kwargs,
|
|
233
239
|
)
|
|
234
240
|
else:
|
|
@@ -246,6 +252,7 @@ def upload_file(
|
|
|
246
252
|
},
|
|
247
253
|
fileUrl=createdContent.readUrl,
|
|
248
254
|
scopeId=scope_or_unique_path,
|
|
255
|
+
versioningEnabled=versioning_enabled,
|
|
249
256
|
**preview_kwargs,
|
|
250
257
|
)
|
|
251
258
|
|
|
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.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/__init__.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_acronyms.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_agentic_table.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_analytics_order.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_benchmarking.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_briefing.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_chat_completion.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_elicitation.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_embedding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_integrated.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_llm_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_log.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_message_tool.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_scheduled_task.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/api_resources/_search_string.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/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.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/cite_file.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/dynamic_frontend.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/elicitation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/navigation.py
RENAMED
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/scheduled_tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/cli/commands/web_search.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/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
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/analytics_order_run.py
RENAMED
|
File without changes
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev9}/unique_sdk/utils/benchmarking_run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|