unique-sdk 2026.46.0.dev4__tar.gz → 2026.46.0.dev6__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.46.0.dev4 → unique_sdk-2026.46.0.dev6}/PKG-INFO +1 -1
  2. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/pyproject.toml +1 -1
  3. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/cli.py +20 -7
  4. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/navigation.py +4 -8
  5. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/search.py +10 -0
  6. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/shell.py +10 -8
  7. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +10 -0
  8. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/state.py +12 -4
  9. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/README.md +0 -0
  10. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/__init__.py +0 -0
  11. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_api_requestor.py +0 -0
  12. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_api_resource.py +0 -0
  13. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_api_version.py +0 -0
  14. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_error.py +0 -0
  15. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_http_client.py +0 -0
  16. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_list_object.py +0 -0
  17. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_object_classes.py +0 -0
  18. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_request_options.py +0 -0
  19. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_unique_object.py +0 -0
  20. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_unique_ql.py +0 -0
  21. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_unique_response.py +0 -0
  22. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_util.py +0 -0
  23. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_version.py +0 -0
  24. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/_webhook.py +0 -0
  25. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/__init__.py +0 -0
  26. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_acronyms.py +0 -0
  27. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_agentic_table.py +0 -0
  28. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_analytics_order.py +0 -0
  29. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_benchmarking.py +0 -0
  30. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_briefing.py +0 -0
  31. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_chat_completion.py +0 -0
  32. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_content.py +0 -0
  33. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_context_memory.py +0 -0
  34. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_elicitation.py +0 -0
  35. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_embedding.py +0 -0
  36. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_event.py +0 -0
  37. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_folder.py +0 -0
  38. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_group.py +0 -0
  39. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_integrated.py +0 -0
  40. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_llm_models.py +0 -0
  41. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_mcp.py +0 -0
  42. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_message.py +0 -0
  43. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_message_assessment.py +0 -0
  44. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_message_execution.py +0 -0
  45. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_message_log.py +0 -0
  46. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_message_tool.py +0 -0
  47. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_module.py +0 -0
  48. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  49. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_search.py +0 -0
  50. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_search_string.py +0 -0
  51. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  52. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_space.py +0 -0
  53. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_user.py +0 -0
  54. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/api_resources/_web_search.py +0 -0
  55. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/__init__.py +0 -0
  56. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/__main__.py +0 -0
  57. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/__init__.py +0 -0
  58. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  59. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/agentic_table.py +0 -0
  60. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
  61. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/browser.py +0 -0
  62. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/cite_file.py +0 -0
  63. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/elicitation.py +0 -0
  64. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/files.py +0 -0
  65. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/folders.py +0 -0
  66. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/mcp.py +0 -0
  67. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/read.py +0 -0
  68. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  69. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/subagent.py +0 -0
  70. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/web_search.py +0 -0
  71. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/commands/web_search_config.py +0 -0
  72. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/config.py +0 -0
  73. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/formatting.py +0 -0
  74. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/identity.py +0 -0
  75. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/metadata_filter.py +0 -0
  76. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/paths.py +0 -0
  77. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
  78. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  79. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  80. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  81. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  82. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  83. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  84. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  85. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/cli/workspace.py +0 -0
  86. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/utils/analytics_order_run.py +0 -0
  87. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/utils/benchmarking_run.py +0 -0
  88. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/utils/chat_history.py +0 -0
  89. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/utils/chat_in_space.py +0 -0
  90. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/utils/file_io.py +0 -0
  91. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/unique_sdk/utils/sources.py +0 -0
  92. {unique_sdk-2026.46.0.dev4 → unique_sdk-2026.46.0.dev6}/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.46.0.dev4
3
+ Version: 2026.46.0.dev6
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.46.0.dev4"
3
+ version = "2026.46.0.dev6"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -545,7 +545,7 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
545
545
 
546
546
 
547
547
  @main.command(name="cite")
548
- @click.argument("name_or_id")
548
+ @click.argument("names_or_ids", nargs=-1, required=True)
549
549
  @click.option(
550
550
  "--pages",
551
551
  "-p",
@@ -569,19 +569,22 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
569
569
  @click.pass_context
570
570
  def cite(
571
571
  ctx: click.Context,
572
- name_or_id: str,
572
+ names_or_ids: tuple[str, ...],
573
573
  pages: str | None,
574
574
  read_method: str,
575
575
  ) -> None:
576
- """Declare page citations for a file.
576
+ """Declare page citations for one or more files.
577
577
 
578
578
  \b
579
579
  Registers [filesourceN] markers for pages you referenced in your answer.
580
580
  Does NOT read or extract the file — use your own tools for that.
581
- NAME_OR_ID can be a file path, current-directory file name, or content ID.
581
+ Each NAME_OR_ID can be a file path, current-directory file name, or
582
+ content ID. Pass several to cite them in one call.
582
583
  --read-method is mandatory: it records how you read the page(s).
583
584
  --pages is optional; omit it to cite the whole file (e.g. non-paginated
584
- formats).
585
+ formats). With several files, --pages and --read-method apply to each.
586
+ A file that fails does not stop the others; the exit code is non-zero
587
+ when any file failed.
585
588
 
586
589
  \b
587
590
  Examples:
@@ -589,9 +592,19 @@ def cite(
589
592
  unique-cli cite /Reports/Q1/report.pdf --pages 3,5,7 --read-method vision
590
593
  unique-cli cite cont_abc123 --pages 1-4 --read-method indexed
591
594
  unique-cli cite notes.docx --read-method text
595
+ unique-cli cite a.xlsx b.xlsx c.csv --read-method text
592
596
  """
593
- output = cmd_cite_file(LazyState.get(ctx), name_or_id, pages, read_method)
594
- emit(output, is_error=_is_cite_error_output)
597
+ state = LazyState.get(ctx)
598
+ any_failed = False
599
+ for name_or_id in names_or_ids:
600
+ output = cmd_cite_file(state, name_or_id, pages, read_method)
601
+ if _is_cite_error_output(output):
602
+ any_failed = True
603
+ click.echo(output, err=True)
604
+ else:
605
+ click.echo(output)
606
+ if any_failed:
607
+ raise SystemExit(1)
595
608
 
596
609
 
597
610
  @main.command(name="read")
@@ -184,14 +184,10 @@ def cmd_ls(
184
184
  else:
185
185
  resolved_path, scope_id = state.cwd, state.scope_id
186
186
 
187
- # A non-root target must lie inside the per-message scope: without
188
- # this, `ls <path>` would enumerate out-of-scope folders/files that
189
- # read/cite correctly deny. See UN-21780.
190
- if (
191
- scope_id is not None
192
- and state.workspace_metadata_filter is not None
193
- and not state.folder_allowed_by_metadata_filter(scope_id)
194
- ):
187
+ # A non-root target must lie inside the task scope: without this,
188
+ # `ls <path>` would enumerate out-of-scope folders/files that
189
+ # read/cite correctly deny. See UN-21780 and UN-28458.
190
+ if scope_id is not None and not state.folder_navigable(scope_id):
195
191
  return (
196
192
  f"ls: permission denied: target is outside your task scope "
197
193
  f"({state.scope_denial_hint()})."
@@ -214,6 +214,16 @@ def cmd_search(
214
214
  folder_scope_id: str | None = None
215
215
  if folder:
216
216
  folder_scope_id = _resolve_folder_to_scope_id(state, folder)
217
+ # Without a per-message filter, --folder replaces the static
218
+ # scopeIds, so it must lie inside them. See UN-28458.
219
+ if (
220
+ not state.workspace_metadata_filter
221
+ and not state.is_folder_target_within_workspace(folder_scope_id)
222
+ ):
223
+ return (
224
+ f"{SEARCH_ERROR_PREFIX} permission denied: {folder} is "
225
+ f"outside your task scope ({state.scope_denial_hint()})."
226
+ )
217
227
  elif state.scope_id and not state.workspace_metadata_filter:
218
228
  # The incidental cwd narrows search only when there is no
219
229
  # per-message filter. With a filter active, only an explicit
@@ -67,7 +67,7 @@ OVERVIEW_HELP = textwrap.dedent("""\
67
67
  download <name|path|id> [dest] Download a file to local machine
68
68
  rm <name|path|id> Delete a file
69
69
  mv <old|path|id> <new> Rename a file
70
- cite <name|path|id> [--pages] --read-method METHOD Declare page citations
70
+ cite <name|path|id> [...] [--pages] --read-method METHOD Declare page citations (one or more files)
71
71
 
72
72
  Search:
73
73
  search <query> [options] Combined search (vector + full-text)
@@ -498,27 +498,28 @@ class UniqueShell(cmd.Cmd):
498
498
  self._print(cmd_download(self.state, name_or_id, local_dest))
499
499
 
500
500
  def do_cite(self, arg: str) -> None:
501
- """Declare page citations for a file.
501
+ """Declare page citations for one or more files.
502
502
 
503
- Usage: cite <name|path|content_id> [--pages RANGE] --read-method METHOD
503
+ Usage: cite <name|path|content_id> [...] [--pages RANGE] --read-method METHOD
504
504
 
505
505
  --read-method is mandatory (one of: text, vision, indexed). --pages is
506
- optional; omit it to cite the whole file.
506
+ optional; omit it to cite the whole file. With several files, --pages
507
+ and --read-method apply to each; a file that fails does not stop the
508
+ others.
507
509
 
508
510
  Examples:
509
511
  /Reports> cite report.pdf --pages 3,5,7 --read-method text
510
512
  /Reports> cite /Reports/Q1/report.pdf --pages 3,5,7 --read-method vision
511
513
  /Reports> cite cont_abc123 --pages 1-4 --read-method indexed
512
514
  /Reports> cite notes.docx --read-method text
515
+ /Reports> cite a.xlsx b.xlsx c.csv --read-method text
513
516
  """
514
517
  from unique_sdk.cli.commands.cite_file import (
515
518
  READ_METHODS,
516
519
  cmd_cite_file,
517
520
  )
518
521
 
519
- usage = (
520
- "Usage: cite <name|path|content_id> [--pages RANGE] --read-method METHOD"
521
- )
522
+ usage = "Usage: cite <name|path|content_id> [...] [--pages RANGE] --read-method METHOD"
522
523
  parts = shlex.split(arg)
523
524
  if not parts:
524
525
  self._print(usage)
@@ -555,7 +556,8 @@ class UniqueShell(cmd.Cmd):
555
556
  f"cite: --read-method is required (one of: {', '.join(READ_METHODS)})"
556
557
  )
557
558
  return
558
- self._print(cmd_cite_file(self.state, positional[0], pages, read_method))
559
+ for name_or_id in positional:
560
+ self._print(cmd_cite_file(self.state, name_or_id, pages, read_method))
559
561
 
560
562
  def _parse_int(self, raw: str, flag: str) -> tuple[int | None, bool]:
561
563
  """Parse an int option value, returning (value, ok). Prints on failure."""
@@ -83,6 +83,8 @@ unique-cli cite report.pdf --pages 3,5,7 --read-method text
83
83
  unique-cli cite cont_abc123 --pages 1-4 --read-method vision
84
84
  # Non-paginated files (Excel, CSV, txt): omit --pages to cite the whole file
85
85
  unique-cli cite data.xlsx --read-method text
86
+ # Several files in one call (--pages and --read-method apply to each)
87
+ unique-cli cite a.xlsx b.xlsx c.csv --read-method text
86
88
 
87
89
  # Delete a file
88
90
  unique-cli rm report.pdf
@@ -222,11 +224,19 @@ answer, declare citations. `cite` works on **any** file type, not just PDFs.
222
224
  unique-cli cite report.pdf --pages 3,5 --read-method text
223
225
  unique-cli cite cont_abc123 --pages 1-4 --read-method vision
224
226
  unique-cli cite data.xlsx --read-method text # non-paginated: omit --pages
227
+ unique-cli cite a.xlsx b.xlsx c.csv --read-method text # several files, one call
225
228
  ```
226
229
 
227
230
  This registers `[filesourceN]` markers. Use them inline in your answer.
228
231
  The platform converts `[filesourceN]` into footnotes and clickable reference chips.
229
232
 
233
+ **Cite several files in ONE call.** Each `unique-cli cite` process takes seconds to
234
+ start, so pass every file with the same `--read-method` (and the same `--pages`, if
235
+ any) to a single call. A file that fails does not stop the others, and the command
236
+ exits non-zero when any file failed — read stderr for the failing file. You may chain
237
+ the call at the end of your last file-reading command (`... && unique-cli cite ...`)
238
+ instead of spending a separate tool call on it.
239
+
230
240
  **Which page number to cite.** `cite` expects **physical page positions** (1-based) — the same positions ingestion assigns and that `unique-cli read` prints as `[p.N]` / `[p.N-M]` prefixes. NEVER cite a printed page number from a header/footer; those often differ from the physical position.
231
241
 
232
242
  **Preferred path — you read the file with `unique-cli read` (no download needed):**
@@ -468,6 +468,16 @@ class ShellState:
468
468
  return True
469
469
  return self._metadata_filter.allows_folder_scope(scope_id)
470
470
 
471
+ def folder_navigable(self, scope_id: str) -> bool:
472
+ """True if ``ls``/``cd`` may open *scope_id* under the active scope.
473
+
474
+ The per-message filter decides when present; otherwise a non-empty
475
+ ``scopeIds`` list admits only those folders and their descendants.
476
+ """
477
+ if self._metadata_filter is not None:
478
+ return self.folder_allowed_by_metadata_filter(scope_id)
479
+ return self.is_folder_target_within_workspace(scope_id)
480
+
471
481
  def folder_path_allowed_by_metadata_filter(self, folder_path: str) -> bool:
472
482
  """True if an absolute folder *path* lies within a navigable scope.
473
483
 
@@ -564,10 +574,8 @@ class ShellState:
564
574
  return f"{self._path}> "
565
575
 
566
576
  def _require_navigable_folder(self, scope_id: str) -> None:
567
- """Refuse a folder the per-message filter excludes. See UN-28458."""
568
- if self._metadata_filter is None:
569
- return
570
- if self.folder_allowed_by_metadata_filter(scope_id):
577
+ """Refuse a folder outside the active scope. See UN-28458."""
578
+ if self.folder_navigable(scope_id):
571
579
  return
572
580
  raise ValueError(
573
581
  "permission denied: target is outside your task scope "