unique-sdk 2026.24.0.dev8__tar.gz → 2026.24.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 (81) hide show
  1. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/PKG-INFO +1 -1
  2. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/pyproject.toml +2 -1
  3. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_util.py +1 -1
  4. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/cli.py +29 -0
  5. unique_sdk-2026.24.0.dev9/unique_sdk/cli/commands/cite_file.py +150 -0
  6. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/shell.py +35 -0
  7. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +27 -0
  8. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/README.md +0 -0
  9. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/__init__.py +0 -0
  10. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_api_requestor.py +0 -0
  11. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_api_resource.py +0 -0
  12. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_api_version.py +0 -0
  13. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_error.py +0 -0
  14. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_http_client.py +0 -0
  15. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_list_object.py +0 -0
  16. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_object_classes.py +0 -0
  17. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_request_options.py +0 -0
  18. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_unique_object.py +0 -0
  19. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_unique_ql.py +0 -0
  20. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_unique_response.py +0 -0
  21. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_version.py +0 -0
  22. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/_webhook.py +0 -0
  23. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/__init__.py +0 -0
  24. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_acronyms.py +0 -0
  25. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_agentic_table.py +0 -0
  26. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_analytics_order.py +0 -0
  27. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_benchmarking.py +0 -0
  28. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_briefing.py +0 -0
  29. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_chat_completion.py +0 -0
  30. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_content.py +0 -0
  31. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_elicitation.py +0 -0
  32. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_embedding.py +0 -0
  33. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_event.py +0 -0
  34. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_folder.py +0 -0
  35. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_group.py +0 -0
  36. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_integrated.py +0 -0
  37. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_llm_models.py +0 -0
  38. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_mcp.py +0 -0
  39. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_message.py +0 -0
  40. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_message_assessment.py +0 -0
  41. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_message_execution.py +0 -0
  42. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_message_log.py +0 -0
  43. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_message_tool.py +0 -0
  44. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_module.py +0 -0
  45. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  46. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_search.py +0 -0
  47. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_search_string.py +0 -0
  48. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  49. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_space.py +0 -0
  50. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_user.py +0 -0
  51. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/api_resources/_web_search.py +0 -0
  52. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/__init__.py +0 -0
  53. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/__main__.py +0 -0
  54. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/__init__.py +0 -0
  55. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  56. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/elicitation.py +0 -0
  57. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/files.py +0 -0
  58. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/folders.py +0 -0
  59. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/mcp.py +0 -0
  60. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/navigation.py +0 -0
  61. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  62. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/search.py +0 -0
  63. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/subagent.py +0 -0
  64. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/web_search.py +0 -0
  65. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/commands/web_search_config.py +0 -0
  66. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/config.py +0 -0
  67. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/formatting.py +0 -0
  68. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  69. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  70. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  71. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  72. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  73. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  74. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/cli/state.py +0 -0
  75. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/utils/analytics_order_run.py +0 -0
  76. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/utils/benchmarking_run.py +0 -0
  77. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/utils/chat_history.py +0 -0
  78. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/utils/chat_in_space.py +0 -0
  79. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/utils/file_io.py +0 -0
  80. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.0.dev9}/unique_sdk/utils/sources.py +0 -0
  81. {unique_sdk-2026.24.0.dev8 → unique_sdk-2026.24.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.24.0.dev8
3
+ Version: 2026.24.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.24.0.dev8"
3
+ version = "2026.24.0.dev9"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -77,6 +77,7 @@ reportUnusedCallResult = "none"
77
77
  reportUnannotatedClassAttribute = "none"
78
78
  reportImplicitOverride = "none"
79
79
  reportImplicitStringConcatenation = "none"
80
+ reportPrivateUsage = "none"
80
81
  reportUnusedParameter = "none"
81
82
  reportImportCycles = "none"
82
83
 
@@ -187,7 +187,7 @@ def convert_to_unique_object(
187
187
  or (getattr(obj, "object") == "search_result")
188
188
  )
189
189
  ):
190
- obj._retrieve_params = params # pyright: ignore[reportPrivateUsage]
190
+ obj._retrieve_params = params
191
191
 
192
192
  return obj
193
193
  else:
@@ -7,6 +7,7 @@ from typing import Any
7
7
  import click
8
8
 
9
9
  from unique_sdk.cli import __version__
10
+ from unique_sdk.cli.commands.cite_file import cmd_cite_file
10
11
  from unique_sdk.cli.commands.elicitation import (
11
12
  cmd_elicit_ask,
12
13
  cmd_elicit_create,
@@ -300,6 +301,34 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
300
301
  click.echo(cmd_download(LazyState.get(ctx), name_or_id, local_dest))
301
302
 
302
303
 
304
+ @main.command(name="cite")
305
+ @click.argument("name_or_id")
306
+ @click.option(
307
+ "--pages",
308
+ "-p",
309
+ default=None,
310
+ help="Page numbers to cite: '3-7' or '1,3,5'. Omit for whole-file.",
311
+ )
312
+ @click.pass_context
313
+ def cite(
314
+ ctx: click.Context,
315
+ name_or_id: str,
316
+ pages: str | None,
317
+ ) -> None:
318
+ """Declare page citations for a file.
319
+
320
+ \b
321
+ Registers [filesourceN] markers for pages you referenced in your answer.
322
+ Does NOT read or extract the file — use your own tools for that.
323
+
324
+ \b
325
+ Examples:
326
+ unique-cli cite report.pdf --pages 3,5,7
327
+ unique-cli cite cont_abc123 --pages 1-4
328
+ """
329
+ click.echo(cmd_cite_file(LazyState.get(ctx), name_or_id, pages))
330
+
331
+
303
332
  @main.command()
304
333
  @click.argument("name_or_id")
305
334
  @click.pass_context
@@ -0,0 +1,150 @@
1
+ """Cite command: declare file page citations without extracting text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from unique_sdk.cli.commands._citation_manifest import (
9
+ UnsafeRefsLogPathError,
10
+ _append_turn_refs_manifest_entry,
11
+ _locked_turn_refs_manifest,
12
+ _read_turn_refs_manifest,
13
+ )
14
+ from unique_sdk.cli.commands.files import _resolve_content_id
15
+ from unique_sdk.cli.state import ShellState
16
+
17
+ CITE_ERROR_PREFIX = "cite:"
18
+
19
+ _FILE_REFS_LOG_RELATIVE_PATH = Path(".unique") / "file-refs.jsonl"
20
+ _FILE_REFS_LOCK_FILENAME = "file-refs.lock"
21
+ _CHAT_FILES_MANIFEST = Path(".unique") / "chat-files.json"
22
+
23
+
24
+ _MAX_PAGES_PER_CALL = 500
25
+
26
+
27
+ def _parse_pages(pages: str | None) -> list[int]:
28
+ """Parse '3-7' or '1,3,5' into a list of 1-based page numbers.
29
+
30
+ Returns [0] (whole-file) when pages is None or empty.
31
+ Returns [] on invalid input (triggers error in caller).
32
+ """
33
+ if not pages or not pages.strip():
34
+ return [0]
35
+ selected: list[int] = []
36
+ for part in pages.split(","):
37
+ part = part.strip()
38
+ if not part:
39
+ continue
40
+ if "-" in part:
41
+ bounds = part.split("-", 1)
42
+ start_s, end_s = bounds[0].strip(), bounds[1].strip()
43
+ if not start_s.isdecimal() or not end_s.isdecimal():
44
+ return []
45
+ start, end = int(start_s), int(end_s)
46
+ if start < 1 or end < start or (end - start + 1) > _MAX_PAGES_PER_CALL:
47
+ return []
48
+ selected.extend(range(start, end + 1))
49
+ else:
50
+ if not part.isdecimal() or int(part) < 1:
51
+ return []
52
+ selected.append(int(part))
53
+ if not selected:
54
+ return []
55
+ if len(set(selected)) > _MAX_PAGES_PER_CALL:
56
+ return []
57
+ return sorted(set(selected))
58
+
59
+
60
+ def _resolve_content_id_with_manifest(
61
+ state: ShellState, name_or_id: str
62
+ ) -> tuple[str, str]:
63
+ """Resolve a filename/content_id checking the chat-files manifest first.
64
+
65
+ Resolution order:
66
+ 1. If name_or_id starts with "cont_", return it directly.
67
+ 2. Check .unique/chat-files.json for a matching filename (exact or basename).
68
+ 3. Fall back to KB resolution via _resolve_content_id.
69
+ """
70
+ if name_or_id.startswith("cont_"):
71
+ return name_or_id, name_or_id
72
+
73
+ manifest_path = Path.cwd() / _CHAT_FILES_MANIFEST
74
+ if manifest_path.is_file():
75
+ try:
76
+ manifest: dict[str, str] = json.loads(
77
+ manifest_path.read_text(encoding="utf-8")
78
+ )
79
+ except (json.JSONDecodeError, OSError):
80
+ manifest = {}
81
+
82
+ basename = Path(name_or_id).name
83
+ content_id = manifest.get(name_or_id) or manifest.get(basename)
84
+ if content_id:
85
+ return content_id, basename
86
+
87
+ return _resolve_content_id(state, name_or_id)
88
+
89
+
90
+ def cmd_cite_file(
91
+ state: ShellState,
92
+ name_or_id: str,
93
+ pages: str | None,
94
+ ) -> str:
95
+ """Declare citations for a file's pages.
96
+
97
+ Writes entries to .unique/file-refs.jsonl and returns [filesourceN]
98
+ markers for the agent to use inline.
99
+ """
100
+ try:
101
+ content_id, filename = _resolve_content_id_with_manifest(state, name_or_id)
102
+ except Exception as exc:
103
+ return f"{CITE_ERROR_PREFIX} {exc}"
104
+
105
+ page_list = _parse_pages(pages)
106
+ if not page_list:
107
+ return f"{CITE_ERROR_PREFIX} invalid --pages value"
108
+
109
+ refs_log_path = Path.cwd() / _FILE_REFS_LOG_RELATIVE_PATH
110
+
111
+ try:
112
+ with _locked_turn_refs_manifest(
113
+ refs_log_path, lock_filename=_FILE_REFS_LOCK_FILENAME
114
+ ):
115
+ existing = _read_turn_refs_manifest(refs_log_path)
116
+
117
+ existing_keys: dict[tuple[str, int], int] = {}
118
+ for entry in existing:
119
+ key = (entry.get("contentId", ""), entry.get("page", 0))
120
+ existing_keys[key] = entry.get("sourceNumber", 0)
121
+
122
+ next_source_number = max(existing_keys.values()) + 1 if existing_keys else 1
123
+
124
+ output_lines: list[str] = []
125
+ for page in page_list:
126
+ key = (content_id, page)
127
+ if key in existing_keys:
128
+ sn = existing_keys[key]
129
+ output_lines.append(
130
+ f"[filesource{sn}] -> {filename} page {page} (already declared)"
131
+ )
132
+ continue
133
+
134
+ entry = {
135
+ "sourceNumber": next_source_number,
136
+ "contentId": content_id,
137
+ "filename": filename,
138
+ "page": page,
139
+ }
140
+ _append_turn_refs_manifest_entry(refs_log_path, entry)
141
+ output_lines.append(
142
+ f"[filesource{next_source_number}] -> {filename} page {page}"
143
+ )
144
+ existing_keys[key] = next_source_number
145
+ next_source_number += 1
146
+
147
+ except UnsafeRefsLogPathError as exc:
148
+ return f"{CITE_ERROR_PREFIX} {exc}"
149
+
150
+ return "\n".join(output_lines)
@@ -52,6 +52,7 @@ OVERVIEW_HELP = textwrap.dedent("""\
52
52
  download <name|id> [dest] Download a file to local machine
53
53
  rm <name|id> Delete a file
54
54
  mv <old> <new> Rename a file
55
+ cite <name|id> [--pages] Declare page citations for a file
55
56
 
56
57
  Search:
57
58
  search <query> [options] Combined search (vector + full-text)
@@ -359,6 +360,40 @@ class UniqueShell(cmd.Cmd):
359
360
  local_dest = parts[1] if len(parts) > 1 else None
360
361
  self._print(cmd_download(self.state, name_or_id, local_dest))
361
362
 
363
+ def do_cite(self, arg: str) -> None:
364
+ """Declare page citations for a file.
365
+
366
+ Usage: cite <name|content_id> [--pages RANGE]
367
+
368
+ Examples:
369
+ /Reports> cite report.pdf --pages 3,5,7
370
+ /Reports> cite cont_abc123 --pages 1-4
371
+ """
372
+ from unique_sdk.cli.commands.cite_file import cmd_cite_file
373
+
374
+ parts = shlex.split(arg)
375
+ if not parts:
376
+ self._print("Usage: cite <name|content_id> [--pages RANGE]")
377
+ return
378
+ pages: str | None = None
379
+ positional: list[str] = []
380
+ index = 0
381
+ while index < len(parts):
382
+ token = parts[index]
383
+ if token in ("--pages", "-p"):
384
+ if index + 1 >= len(parts):
385
+ self._print("cite: --pages requires a value")
386
+ return
387
+ pages = parts[index + 1]
388
+ index += 2
389
+ else:
390
+ positional.append(token)
391
+ index += 1
392
+ if not positional:
393
+ self._print("Usage: cite <name|content_id> [--pages RANGE]")
394
+ return
395
+ self._print(cmd_cite_file(self.state, positional[0], pages))
396
+
362
397
  def do_rm(self, arg: str) -> None:
363
398
  """Delete a file.
364
399
 
@@ -59,6 +59,10 @@ unique-cli upload ./data.csv scope_abc123
59
59
  unique-cli download report.pdf ./local/
60
60
  unique-cli download cont_abc123 ~/Desktop/
61
61
 
62
+ # Declare page citations after reading a file
63
+ unique-cli cite report.pdf --pages 3,5,7
64
+ unique-cli cite cont_abc123 --pages 1-4
65
+
62
66
  # Delete a file
63
67
  unique-cli rm report.pdf
64
68
  unique-cli rm cont_abc123
@@ -119,6 +123,29 @@ unique-cli mkdir "2025/Q1/Financials"
119
123
  unique-cli upload ./budget.xlsx /2025/Q1/Financials/
120
124
  ```
121
125
 
126
+ ## Citing File Pages
127
+
128
+ After reading **any** file and using its content in your answer, declare citations:
129
+
130
+ ```bash
131
+ unique-cli cite report.pdf --pages 3,5
132
+ unique-cli cite cont_abc123 --pages 1-4
133
+ ```
134
+
135
+ This registers `[filesourceN]` markers. Use them inline in your answer.
136
+ The platform converts `[filesourceN]` into footnotes and clickable reference chips.
137
+
138
+ **MANDATORY 3-step verification before EVERY `unique-cli cite` call — NO EXCEPTIONS:**
139
+
140
+ 1. `pdfinfo file.pdf | grep Pages` — get total physical page count.
141
+ 2. For **each** page you intend to cite, run `pdftotext -f N -l N file.pdf -` and confirm the content you are referencing is actually on that physical page. Do NOT skip this. Do NOT assume page numbers.
142
+ 3. Only after step 2 confirms a match, call `unique-cli cite` with the verified physical page numbers.
143
+
144
+ Page numbers are **physical PDF positions** (1-based). NEVER use printed page numbers from headers/footers — they often differ from physical positions.
145
+
146
+ - Numbers are **per-turn only**; do not reuse from prior turns.
147
+ - Do NOT use `cite` for content from `unique-cli search` or `unique-cli web-search`.
148
+
122
149
  ## Error Handling
123
150
 
124
151
  - If env vars are missing, the CLI exits with a clear error listing the missing variables.