unique-sdk 2026.28.0.dev12__tar.gz → 2026.28.0.dev14__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 (86) hide show
  1. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/PKG-INFO +1 -1
  2. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/pyproject.toml +1 -1
  3. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/cli.py +41 -0
  4. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/files.py +6 -6
  5. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/folders.py +3 -3
  6. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/mcp.py +59 -6
  7. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/navigation.py +5 -5
  8. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/read.py +1 -1
  9. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/search.py +70 -1
  10. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +40 -1
  11. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/state.py +62 -0
  12. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/README.md +0 -0
  13. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/__init__.py +0 -0
  14. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_api_requestor.py +0 -0
  15. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_api_resource.py +0 -0
  16. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_api_version.py +0 -0
  17. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_error.py +0 -0
  18. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_http_client.py +0 -0
  19. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_list_object.py +0 -0
  20. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_object_classes.py +0 -0
  21. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_request_options.py +0 -0
  22. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_unique_object.py +0 -0
  23. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_unique_ql.py +0 -0
  24. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_unique_response.py +0 -0
  25. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_util.py +0 -0
  26. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_version.py +0 -0
  27. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/_webhook.py +0 -0
  28. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/__init__.py +0 -0
  29. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_acronyms.py +0 -0
  30. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_agentic_table.py +0 -0
  31. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_analytics_order.py +0 -0
  32. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_benchmarking.py +0 -0
  33. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_briefing.py +0 -0
  34. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_chat_completion.py +0 -0
  35. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_content.py +0 -0
  36. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
  37. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_elicitation.py +0 -0
  38. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_embedding.py +0 -0
  39. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_event.py +0 -0
  40. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_folder.py +0 -0
  41. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_group.py +0 -0
  42. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_integrated.py +0 -0
  43. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_llm_models.py +0 -0
  44. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_mcp.py +0 -0
  45. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_message.py +0 -0
  46. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_message_assessment.py +0 -0
  47. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_message_execution.py +0 -0
  48. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_message_log.py +0 -0
  49. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_message_tool.py +0 -0
  50. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_module.py +0 -0
  51. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  52. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_search.py +0 -0
  53. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_search_string.py +0 -0
  54. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  55. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_space.py +0 -0
  56. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_user.py +0 -0
  57. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/api_resources/_web_search.py +0 -0
  58. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/__init__.py +0 -0
  59. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/__main__.py +0 -0
  60. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/__init__.py +0 -0
  61. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  62. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/cite_file.py +0 -0
  63. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
  64. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/elicitation.py +0 -0
  65. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  66. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/subagent.py +0 -0
  67. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/web_search.py +0 -0
  68. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/commands/web_search_config.py +0 -0
  69. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/config.py +0 -0
  70. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/formatting.py +0 -0
  71. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/metadata_filter.py +0 -0
  72. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/shell.py +0 -0
  73. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-dynamic-frontend/SKILL.md +0 -0
  74. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  75. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  76. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  77. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  78. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  79. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  80. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/utils/analytics_order_run.py +0 -0
  81. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/utils/benchmarking_run.py +0 -0
  82. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/utils/chat_history.py +0 -0
  83. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/utils/chat_in_space.py +0 -0
  84. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/utils/file_io.py +0 -0
  85. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/unique_sdk/utils/sources.py +0 -0
  86. {unique_sdk-2026.28.0.dev12 → unique_sdk-2026.28.0.dev14}/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.28.0.dev12
3
+ Version: 2026.28.0.dev14
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.28.0.dev12"
3
+ version = "2026.28.0.dev14"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -53,10 +53,14 @@ from unique_sdk.cli.commands.scheduled_tasks import (
53
53
  )
54
54
  from unique_sdk.cli.commands.search import (
55
55
  cmd_search,
56
+ cmd_uploaded_search,
56
57
  )
57
58
  from unique_sdk.cli.commands.search import (
58
59
  is_error_output as _is_search_error_output,
59
60
  )
61
+ from unique_sdk.cli.commands.search import (
62
+ is_uploaded_search_error_output as _is_uploaded_search_error_output,
63
+ )
60
64
  from unique_sdk.cli.commands.subagent import cmd_subagent
61
65
  from unique_sdk.cli.commands.subagent import (
62
66
  is_error_output as _is_subagent_error_output,
@@ -739,6 +743,43 @@ def search(
739
743
  ctx.exit(1)
740
744
 
741
745
 
746
+ @main.command(name="uploaded-search")
747
+ @click.argument("query")
748
+ @click.option(
749
+ "--limit",
750
+ "-l",
751
+ default=200,
752
+ show_default=True,
753
+ help="Maximum number of results to return.",
754
+ )
755
+ @click.pass_context
756
+ def uploaded_search(
757
+ ctx: click.Context,
758
+ query: str,
759
+ limit: int,
760
+ ) -> None:
761
+ """Search the documents uploaded for this task (not the knowledge base).
762
+
763
+ \b
764
+ QUERY is the search text. This searches only the files attached to this
765
+ row/task (e.g. an Agentic Table row's uploaded documents), which are NOT
766
+ part of the knowledge-base folder scope and therefore never appear in
767
+ `unique-cli search`. Results are ranked the same way and cite as
768
+ `[sourceN]`, with numbering continuous across `search` and
769
+ `uploaded-search` within a turn.
770
+
771
+ \b
772
+ Examples:
773
+ unique-cli uploaded-search "target asset classes"
774
+ unique-cli uploaded-search "fee structure" --limit 50
775
+ """
776
+ state = LazyState.get(ctx)
777
+ output = cmd_uploaded_search(state, query, limit=limit)
778
+ click.echo(output)
779
+ if _is_uploaded_search_error_output(output):
780
+ ctx.exit(1)
781
+
782
+
742
783
  @main.command()
743
784
  @click.argument("payload", required=False, default=None)
744
785
  @click.option(
@@ -358,7 +358,7 @@ def cmd_upload(
358
358
  ).get("folderPath", scope_id)
359
359
  return f"Uploaded: {display_name} ({content_id}) to {folder_path}"
360
360
 
361
- except (ValueError, unique_sdk.APIError, OSError) as e:
361
+ except (ValueError, unique_sdk.UniqueError, OSError) as e:
362
362
  return f"upload: {e}"
363
363
 
364
364
 
@@ -384,7 +384,7 @@ def cmd_versions(
384
384
  )
385
385
  data = result.get("data", [])
386
386
  return f"Versions for {display_name} ({content_id}):\n{_format_content_versions(data)}"
387
- except (ValueError, unique_sdk.APIError) as e:
387
+ except (ValueError, unique_sdk.UniqueError) as e:
388
388
  return f"versions: {e}"
389
389
 
390
390
 
@@ -414,7 +414,7 @@ def cmd_restore_version(state: ShellState, content_version_id: str) -> str:
414
414
  title = result.get("title") or result.get("key") or result.get("id", "?")
415
415
  content_id = result.get("id", "?")
416
416
  return f"Restored: {title} ({content_id}) from version {content_version_id}"
417
- except (ValueError, unique_sdk.APIError) as e:
417
+ except (ValueError, unique_sdk.UniqueError) as e:
418
418
  return f"restore-version: {e}"
419
419
 
420
420
 
@@ -449,7 +449,7 @@ def cmd_download(
449
449
  shutil.move(str(downloaded_path), str(final_dest))
450
450
  return f"Downloaded: {display_name} -> {final_dest}"
451
451
 
452
- except (ValueError, unique_sdk.APIError, OSError) as e:
452
+ except (ValueError, unique_sdk.UniqueError, OSError) as e:
453
453
  return f"download: {e}"
454
454
 
455
455
 
@@ -468,7 +468,7 @@ def cmd_rm(state: ShellState, name_or_id: str) -> str:
468
468
  contentId=content_id,
469
469
  )
470
470
  return f"Deleted: {display_name} ({content_id})"
471
- except (ValueError, unique_sdk.APIError) as e:
471
+ except (ValueError, unique_sdk.UniqueError) as e:
472
472
  return f"rm: {e}"
473
473
 
474
474
 
@@ -488,7 +488,7 @@ def cmd_mv_file(state: ShellState, old_name: str, new_name: str) -> str:
488
488
  title=new_name,
489
489
  )
490
490
  return f"Renamed: {display_name} -> {result.get('title', new_name)}\n{format_content_info(result)}"
491
- except (ValueError, unique_sdk.APIError) as e:
491
+ except (ValueError, unique_sdk.UniqueError) as e:
492
492
  return f"mv: {e}"
493
493
 
494
494
 
@@ -70,7 +70,7 @@ def cmd_mkdir(state: ShellState, name: str) -> str:
70
70
  last = created[-1]
71
71
  return f"Created: {full_path} ({last['id']})"
72
72
  return f"Created: {full_path}"
73
- except (ValueError, unique_sdk.APIError) as e:
73
+ except (ValueError, unique_sdk.UniqueError) as e:
74
74
  return f"mkdir: {e}"
75
75
 
76
76
 
@@ -108,7 +108,7 @@ def cmd_rmdir(state: ShellState, target: str, recursive: bool = False) -> str:
108
108
  recursive=recursive,
109
109
  )
110
110
  return f"Deleted folder: {target}"
111
- except (ValueError, unique_sdk.APIError) as e:
111
+ except (ValueError, unique_sdk.UniqueError) as e:
112
112
  return f"rmdir: {e}"
113
113
 
114
114
 
@@ -145,5 +145,5 @@ def cmd_mvdir(state: ShellState, old_name: str, new_name: str) -> str:
145
145
  name=new_name,
146
146
  )
147
147
  return f"Renamed folder -> {result.get('name', new_name)}\n{format_folder_info(result)}"
148
- except (ValueError, unique_sdk.APIError) as e:
148
+ except (ValueError, unique_sdk.UniqueError) as e:
149
149
  return f"mvdir: {e}"
@@ -337,16 +337,25 @@ def _citation_footer(annotated: list[tuple[int, dict[str, Any]]]) -> str:
337
337
 
338
338
 
339
339
  def _append_mcp_output_manifest(
340
- name: str, text: str, *, server_name: str | None = None
340
+ name: str,
341
+ text: str,
342
+ *,
343
+ server_name: str | None = None,
344
+ output_path: Path | None = None,
341
345
  ) -> None:
342
346
  """Best-effort append of one MCP tool result to the per-turn manifest.
343
347
 
344
348
  Never raises: a manifest failure must not change what the agent sees as
345
349
  the tool result. The groundedness check simply does not fire for this
346
350
  call when the write fails.
351
+
352
+ ``output_path`` is the full path of the ``mcp-output.jsonl`` manifest. It
353
+ defaults to ``Path.cwd() / .unique / mcp-output.jsonl`` so the CLI flow
354
+ (where cwd is the agent workspace) is unchanged; callers that run outside
355
+ the workspace cwd (e.g. the in-process tools-mode proxy) pass it explicitly.
347
356
  """
348
357
  try:
349
- refs_log_path = Path.cwd() / _MCP_OUTPUT_LOG_RELATIVE_PATH
358
+ refs_log_path = output_path or (Path.cwd() / _MCP_OUTPUT_LOG_RELATIVE_PATH)
350
359
  _append_turn_refs_manifest_entry(
351
360
  refs_log_path,
352
361
  {
@@ -359,6 +368,47 @@ def _append_mcp_output_manifest(
359
368
  _LOGGER.warning("mcp: failed to append output manifest: %s", exc)
360
369
 
361
370
 
371
+ def record_mcp_citations(
372
+ response: Any,
373
+ *,
374
+ tool_name: str,
375
+ server_name: str | None,
376
+ unique_dir: Path,
377
+ formatted_text: str,
378
+ ) -> str:
379
+ """Write both per-turn MCP manifests under ``unique_dir`` and return the
380
+ ``[mcpsourceN]`` citation footer for the agent.
381
+
382
+ Shared by the ``unique-cli mcp`` skills flow (``cmd_mcp``) and the
383
+ in-process tools-mode proxy in assistants-core, so both write identical
384
+ manifests and footers. ``unique_dir`` is the workspace ``.unique`` directory
385
+ (its filenames are joined directly here — do not pass the workspace root).
386
+
387
+ - ``response`` is the raw ``unique_sdk.MCP`` result, used for citation
388
+ extraction (titles from ``resource_link`` names / JSON bodies).
389
+ - ``formatted_text`` is the source text the model actually saw for this
390
+ tool result, recorded as the hallucination groundedness context.
391
+
392
+ Best-effort and never raises: the underlying manifest writers swallow their
393
+ own errors, and ``_annotate_mcp_results_for_citations`` owns the per-turn
394
+ file lock — this function must stay lock-free to avoid a same-process flock
395
+ self-deadlock.
396
+ """
397
+ _append_mcp_output_manifest(
398
+ tool_name,
399
+ formatted_text,
400
+ server_name=server_name,
401
+ output_path=unique_dir / _MCP_OUTPUT_LOG_RELATIVE_PATH.name,
402
+ )
403
+ annotated = _annotate_mcp_results_for_citations(
404
+ response,
405
+ tool_name=tool_name,
406
+ server_name=server_name,
407
+ refs_log_path=unique_dir / _MCP_REFS_LOG_RELATIVE_PATH.name,
408
+ )
409
+ return _citation_footer(annotated)
410
+
411
+
362
412
  def _read_payload(
363
413
  payload: str | None,
364
414
  file: str | None,
@@ -440,8 +490,11 @@ def cmd_mcp(
440
490
  formatted = f"mcp: formatter error ({fmt_exc}); raw response:\n{fallback}"
441
491
 
442
492
  server_name = _server_name_from_tool(name) or getattr(response, "mcpServerId", None)
443
- _append_mcp_output_manifest(name, formatted, server_name=server_name)
444
- annotated = _annotate_mcp_results_for_citations(
445
- response, tool_name=name, server_name=server_name
493
+ footer = record_mcp_citations(
494
+ response,
495
+ tool_name=name,
496
+ server_name=server_name,
497
+ unique_dir=Path.cwd() / ".unique",
498
+ formatted_text=formatted,
446
499
  )
447
- return formatted + _citation_footer(annotated)
500
+ return formatted + footer
@@ -19,7 +19,7 @@ def cmd_cd(state: ShellState, target: str) -> str:
19
19
  try:
20
20
  new_path = state.cd(target)
21
21
  return new_path
22
- except (ValueError, unique_sdk.APIError) as e:
22
+ except (ValueError, unique_sdk.UniqueError) as e:
23
23
  return f"cd: {e}"
24
24
 
25
25
 
@@ -65,7 +65,7 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
65
65
  scopeId=sid,
66
66
  )
67
67
  )
68
- except unique_sdk.APIError:
68
+ except unique_sdk.UniqueError:
69
69
  pass
70
70
  scoped_files: list[Any] = []
71
71
  for cid in content_ids:
@@ -86,7 +86,7 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
86
86
  items = info.get("contentInfo", [])
87
87
  if items:
88
88
  scoped_files.append(items[0])
89
- except unique_sdk.APIError:
89
+ except unique_sdk.UniqueError:
90
90
  pass
91
91
  output = format_ls(scoped_folders, scoped_files)
92
92
  summary = (
@@ -107,7 +107,7 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
107
107
  scopeId=ws_id,
108
108
  )
109
109
  folders.append(info)
110
- except unique_sdk.APIError:
110
+ except unique_sdk.UniqueError:
111
111
  pass
112
112
  output = format_ls(folders, [])
113
113
  summary = f"\n{len(folders)} folder(s), 0 file(s)"
@@ -161,5 +161,5 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
161
161
  summary = f"\n{total_folders} folder(s), {total_files} file(s)"
162
162
  return output + summary
163
163
 
164
- except (ValueError, unique_sdk.APIError) as e:
164
+ except (ValueError, unique_sdk.UniqueError) as e:
165
165
  return f"ls: {e}"
@@ -110,7 +110,7 @@ def cmd_read(
110
110
  company_id=state.config.company_id,
111
111
  where={"id": {"equals": cont_id}},
112
112
  )
113
- except unique_sdk.APIError as e:
113
+ except unique_sdk.UniqueError as e:
114
114
  return f"{READ_ERROR_PREFIX} {e}"
115
115
 
116
116
  if not results:
@@ -30,6 +30,7 @@ from unique_sdk.cli.state import ShellState
30
30
  DEFAULT_LIMIT = 200
31
31
 
32
32
  SEARCH_ERROR_PREFIX = "search:"
33
+ UPLOADED_SEARCH_ERROR_PREFIX = "uploaded-search:"
33
34
 
34
35
  _REFS_LOG_RELATIVE_PATH = Path(".unique") / "kb-search-refs.jsonl"
35
36
  _LOCK_FILENAME = "kb-search-refs.lock"
@@ -272,7 +273,7 @@ def cmd_search(
272
273
  **search_params,
273
274
  )
274
275
 
275
- except (ValueError, unique_sdk.APIError) as e:
276
+ except (ValueError, unique_sdk.UniqueError) as e:
276
277
  return f"{SEARCH_ERROR_PREFIX} {e}"
277
278
 
278
279
  log_path = refs_log_path or (Path.cwd() / _REFS_LOG_RELATIVE_PATH)
@@ -282,6 +283,69 @@ def cmd_search(
282
283
  return f"{SEARCH_ERROR_PREFIX} {exc}"
283
284
 
284
285
 
286
+ def cmd_uploaded_search(
287
+ state: ShellState,
288
+ query: str,
289
+ limit: int = DEFAULT_LIMIT,
290
+ *,
291
+ refs_log_path: Path | None = None,
292
+ ) -> str:
293
+ """Search this task's per-row **uploaded** documents.
294
+
295
+ Uploaded files (e.g. an Agentic Table row's attached documents) live
296
+ outside the knowledge-base folder scopes, so the scope-bound
297
+ ``unique-cli search`` cannot return them. This mirrors the platform's
298
+ UploadedSearch tool: a chat-scoped ``Search.create`` (``chatOnly=True``)
299
+ over the owning chat and the selected content ids, both written by the
300
+ Swappable Intelligence runner to ``.unique-uploaded.json``. See UN-21780.
301
+
302
+ Results share the same per-turn ``<sourceN>`` manifest as
303
+ ``unique-cli search`` (``_format_results_with_citations`` appends under the
304
+ same lock), so ``[sourceN]`` citation numbering stays continuous across
305
+ both commands within a turn.
306
+
307
+ Args:
308
+ state: Shell state carrying user/company credentials and the uploaded
309
+ document scope loaded from ``.unique-uploaded.json``.
310
+ query: Search string.
311
+ limit: Maximum number of results.
312
+ refs_log_path: Override the per-turn citation manifest path — for
313
+ tests; production callers leave this ``None``.
314
+ """
315
+ if not state.uploaded_search_available:
316
+ return (
317
+ f"{UPLOADED_SEARCH_ERROR_PREFIX} no uploaded documents are "
318
+ "available for this task. Use `unique-cli search` for the "
319
+ "knowledge base."
320
+ )
321
+
322
+ try:
323
+ search_params: dict[str, Any] = {
324
+ "searchString": query,
325
+ "searchType": "COMBINED",
326
+ "chatOnly": True,
327
+ "limit": limit,
328
+ }
329
+ if state.uploaded_search_chat_id:
330
+ search_params["chatId"] = state.uploaded_search_chat_id
331
+ if state.uploaded_search_content_ids:
332
+ search_params["contentIds"] = state.uploaded_search_content_ids
333
+
334
+ results = unique_sdk.Search.create(
335
+ user_id=state.config.user_id,
336
+ company_id=state.config.company_id,
337
+ **search_params,
338
+ )
339
+ except (ValueError, unique_sdk.UniqueError) as e:
340
+ return f"{UPLOADED_SEARCH_ERROR_PREFIX} {e}"
341
+
342
+ log_path = refs_log_path or (Path.cwd() / _REFS_LOG_RELATIVE_PATH)
343
+ try:
344
+ return _format_results_with_citations(results, refs_log_path=log_path)
345
+ except UnsafeRefsLogPathError as exc:
346
+ return f"{UPLOADED_SEARCH_ERROR_PREFIX} {exc}"
347
+
348
+
285
349
  def is_error_output(output: str) -> bool:
286
350
  """Return ``True`` when ``output`` is a CLI error message.
287
351
 
@@ -290,3 +354,8 @@ def is_error_output(output: str) -> bool:
290
354
  exit code without changing the existing string-returning contract.
291
355
  """
292
356
  return output.startswith(SEARCH_ERROR_PREFIX)
357
+
358
+
359
+ def is_uploaded_search_error_output(output: str) -> bool:
360
+ """Return ``True`` when ``output`` is an ``uploaded-search`` error message."""
361
+ return output.startswith(UPLOADED_SEARCH_ERROR_PREFIX)
@@ -7,7 +7,10 @@ description: >-
7
7
  platform. Use whenever the user asks to find, search, or query documents
8
8
  or content on Unique, including filtering by folder or metadata.
9
9
  Also covers `unique-cli read <cont_id>` for reading the full indexed text
10
- of a document when its content ID is already known.
10
+ of a document when its content ID is already known, and
11
+ `unique-cli uploaded-search "<query>"` for searching documents uploaded for
12
+ the current task/row, which are NOT in the knowledge-base scope and never
13
+ appear in `unique-cli search`.
11
14
  NOTE: This search uses combined vector + full-text indexing. Excel
12
15
  (.xlsx/.xls), CSV (.csv), and image files are NOT full-text indexed,
13
16
  so they will not appear in search results. To locate these file types,
@@ -64,6 +67,9 @@ When no `--folder` is given:
64
67
  - If the task defines a scope (a per-message scope filter): that scope is the
65
68
  search boundary, regardless of the current directory. `cd`/cwd does **not**
66
69
  narrow further — only an explicit `--folder` ANDs an additional constraint.
70
+ **You do not need `--folder` here** — just run `unique-cli search "<query>"`.
71
+ Passing a folder *path* that isn't navigable to your user errors with
72
+ `Folder ... not found`; only narrow within the scope via a `scope_` id.
67
73
  - Otherwise, in interactive mode: searches within the current directory
68
74
  - Otherwise, at root `/`: searches the entire knowledge base
69
75
 
@@ -89,10 +95,43 @@ unique-cli search "audit" -m department=Legal -m year=2025
89
95
  unique-cli search "regulatory" -f /Legal -m year=2025 -l 50
90
96
  ```
91
97
 
98
+ ## Searching Uploaded Documents (`uploaded-search`)
99
+
100
+ Documents **uploaded for the current task** (e.g. an Agentic Table row's
101
+ attached files) are **not** part of the knowledge-base folder scope. They will
102
+ **never** appear in `unique-cli search` results, no matter the folder or
103
+ metadata filters — uploaded files are scoped to the chat, not to a KB folder.
104
+ Use `uploaded-search` to retrieve them:
105
+
106
+ ```bash
107
+ # Search the documents uploaded for this row/task
108
+ unique-cli uploaded-search "target asset classes and investment strategy"
109
+
110
+ # Limit results
111
+ unique-cli uploaded-search "fee structure" --limit 50
112
+ ```
113
+
114
+ When to use which:
115
+
116
+ | You want to search… | Command |
117
+ |---------------------|---------|
118
+ | The knowledge base (folders the task scope grants) | `unique-cli search "<query>"` |
119
+ | Documents uploaded for **this** row/task | `unique-cli uploaded-search "<query>"` |
120
+
121
+ `uploaded-search` returns the same `<sourceN>...</sourceN>` blocks as `search`
122
+ and shares the **same per-turn citation manifest**, so `[sourceN]` numbering is
123
+ continuous across both commands within a turn — cite an uploaded-document fact
124
+ exactly the same way (`[sourceN]`). If no documents were uploaded for the task,
125
+ the command reports that and you should fall back to `unique-cli search`.
126
+
127
+ > **Note:** there is no `--folder`/`--metadata` for `uploaded-search` — the set
128
+ > of uploaded documents is fixed by what was attached to the task.
129
+
92
130
  ## Command Reference
93
131
 
94
132
  ```
95
133
  unique-cli search <query> [--folder <path|scope_id>] [--metadata <key=value>]... [--limit <N>]
134
+ unique-cli uploaded-search <query> [--limit <N>]
96
135
  ```
97
136
 
98
137
  | Option | Short | Default | Description |
@@ -12,6 +12,7 @@ from unique_sdk.cli.config import Config
12
12
  from unique_sdk.cli.metadata_filter import MetadataFilter
13
13
 
14
14
  _SEARCH_CONFIG_FILENAME = ".unique-search.json"
15
+ _UPLOADED_CONFIG_FILENAME = ".unique-uploaded.json"
15
16
  _CHAT_FILES_MANIFEST_PATH = Path(".unique") / "chat-files.json"
16
17
 
17
18
 
@@ -27,6 +28,38 @@ def _load_search_config() -> dict[str, Any]:
27
28
  return data if isinstance(data, dict) else {}
28
29
 
29
30
 
31
+ def _load_uploaded_config() -> dict[str, Any]:
32
+ """Load ``.unique-uploaded.json`` from the cwd, or ``{}`` when absent/invalid.
33
+
34
+ Written by the Swappable Intelligence runner when the task carries per-row
35
+ uploaded documents (an Agentic Table row's ``selectedUploadedFiles``). It
36
+ holds the chat that owns those uploads and their content ids so
37
+ ``unique-cli uploaded-search`` can reach them: uploaded files live outside
38
+ the knowledge-base folder scopes, so the scope-bound ``unique-cli search``
39
+ structurally cannot return them. See UN-21780.
40
+ """
41
+ config_path = Path.cwd() / _UPLOADED_CONFIG_FILENAME
42
+ if not config_path.is_file():
43
+ return {}
44
+ try:
45
+ data = json.loads(config_path.read_text(encoding="utf-8"))
46
+ except (json.JSONDecodeError, OSError):
47
+ return {}
48
+ return data if isinstance(data, dict) else {}
49
+
50
+
51
+ def _load_uploaded_content_ids(data: dict[str, Any]) -> list[str]:
52
+ content_ids = data.get("contentIds")
53
+ if isinstance(content_ids, list) and all(isinstance(c, str) for c in content_ids):
54
+ return content_ids
55
+ return []
56
+
57
+
58
+ def _load_uploaded_chat_id(data: dict[str, Any]) -> str | None:
59
+ chat_id = data.get("chatId")
60
+ return chat_id if isinstance(chat_id, str) and chat_id else None
61
+
62
+
30
63
  def _load_workspace_scope_ids(data: dict[str, Any]) -> list[str]:
31
64
  scope_ids = data.get("scopeIds")
32
65
  if isinstance(scope_ids, list) and all(isinstance(s, str) for s in scope_ids):
@@ -100,6 +133,35 @@ class ShellState:
100
133
  self._workspace_metadata_filter: dict[str, Any] | None = None
101
134
  self._metadata_filter: MetadataFilter | None = None
102
135
  self.workspace_metadata_filter = _load_workspace_metadata_filter(_search_config)
136
+ # Per-row uploaded documents reachable via ``unique-cli uploaded-search``.
137
+ # Loaded from ``.unique-uploaded.json`` (written by the runner). The chat
138
+ # id is the chat that *owns* the uploads — for a subagent this is the
139
+ # parent chat — and is required for the chat-scoped uploaded search.
140
+ # See UN-21780.
141
+ _uploaded_config = _load_uploaded_config()
142
+ self.uploaded_search_chat_id: str | None = _load_uploaded_chat_id(
143
+ _uploaded_config
144
+ )
145
+ self.uploaded_search_content_ids: list[str] = _load_uploaded_content_ids(
146
+ _uploaded_config
147
+ )
148
+
149
+ @property
150
+ def uploaded_search_available(self) -> bool:
151
+ """True when this task has per-row uploaded documents to search.
152
+
153
+ Requires *both* a non-empty selected content-id set and the owning
154
+ chat id. The owning chat id alone is not enough: ``cmd_uploaded_search``
155
+ would then issue a ``chatOnly`` ``Search.create`` with no ``contentIds``
156
+ filter, silently widening scope from the row's selected files to *every*
157
+ upload in the chat. That degenerate state arises when the runner wrote
158
+ a ``.unique-uploaded.json`` whose ``contentIds`` were absent or
159
+ malformed (e.g. mixed types, rejected wholesale by
160
+ ``_load_uploaded_content_ids``) while the ``chatId`` still parsed — so
161
+ we treat it as "no uploaded scope" rather than "all chat uploads".
162
+ See UN-21780.
163
+ """
164
+ return bool(self.uploaded_search_content_ids and self.uploaded_search_chat_id)
103
165
 
104
166
  @property
105
167
  def workspace_metadata_filter(self) -> dict[str, Any] | None: