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