cited-cli 0.3.14__tar.gz → 0.3.15__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.
- {cited_cli-0.3.14 → cited_cli-0.3.15}/PKG-INFO +4 -4
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/pyproject.toml +1 -1
- cited_cli-0.3.15/packages/core/src/cited_core/__init__.py +1 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/api/client.py +9 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/pyproject.toml +2 -2
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tool_changelog.yaml +51 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/_helpers.py +167 -91
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/tests/test_tools.py +72 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/pyproject.toml +4 -4
- cited_cli-0.3.14/packages/core/src/cited_core/__init__.py +0 -1
- {cited_cli-0.3.14 → cited_cli-0.3.15}/.github/workflows/changelog-guard.yml +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/.github/workflows/ci.yml +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/.github/workflows/release.yml +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/.gitignore +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/CLAUDE.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/DEMO.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/FUTURE.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/LICENSE +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/README.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/TEST_RUNBOOK.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/.claude-plugin/marketplace.json +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/.claude-plugin/plugin.json +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/.mcp.json +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/CLAUDE_DESKTOP_SETUP.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/agents/geo-analyst.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/commands/audit.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/commands/recommend.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/commands/status.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/settings.json +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/skills/business-setup/SKILL.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/skills/explore/SKILL.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/skills/geo-audit/SKILL.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/cited-plugins/skills/geo-recommend/SKILL.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/api/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/api/endpoints.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/auth/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/auth/oauth_server.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/auth/store.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/config/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/config/constants.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/config/manager.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/core/src/cited_core/errors.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/README.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/SKILL.md +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/__main__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/auth_provider.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/context.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/plan_gating.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/remote.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/server.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tool_manager.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/action_plan.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/agent.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/analytics.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/audit.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/auth.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/billing.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/business.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/changelog.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/hq.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/job.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/recommend.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/src/cited_mcp/tools/solution.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/tests/conftest.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/tests/test_auth_provider.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/packages/mcp/tests/test_plan_gating.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/scripts/dev-install.sh +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/scripts/mcp_test_harness.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/__main__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/api/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/api/client.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/api/endpoints.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/app.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/auth/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/auth/oauth_server.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/auth/store.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/agent.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/analytics.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/audit.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/auth.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/business.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/config_cmd.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/hq.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/job.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/mcp_cmd.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/named_audit.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/recommend.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/commands/solution.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/config/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/config/constants.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/config/manager.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/context.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/server.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/_helpers.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/audit.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/auth.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/business.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/job.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/recommend.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/mcp/tools/solution.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/output/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/output/formatter.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/output/progress.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/output/tables.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/utils/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/utils/errors.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/src/cited_cli/utils/interactive.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/__init__.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/conftest.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_api_client.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_auth.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_config.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_feature_parity.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_live_pipeline.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_mcp_server.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_mcp_tools.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_output.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_pipeline.py +0 -0
- {cited_cli-0.3.14 → cited_cli-0.3.15}/tests/test_version.py +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cited-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.15
|
|
4
4
|
Summary: CLI for the Cited GEO platform
|
|
5
5
|
Author-email: Cited <support@youcited.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
|
-
Requires-Dist: cited-core>=0.3.
|
|
9
|
+
Requires-Dist: cited-core>=0.3.15
|
|
10
10
|
Requires-Dist: rich>=13.0.0
|
|
11
11
|
Requires-Dist: typer>=0.15.0
|
|
12
12
|
Provides-Extra: dev
|
|
13
|
-
Requires-Dist: cited-mcp>=0.3.
|
|
13
|
+
Requires-Dist: cited-mcp>=0.3.15; extra == 'dev'
|
|
14
14
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
15
15
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
16
|
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
17
17
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
18
18
|
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
|
|
19
19
|
Provides-Extra: mcp
|
|
20
|
-
Requires-Dist: cited-mcp>=0.3.
|
|
20
|
+
Requires-Dist: cited-mcp>=0.3.15; extra == 'mcp'
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
# Cited CLI
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.15"
|
|
@@ -40,6 +40,15 @@ class CitedClient:
|
|
|
40
40
|
def close(self) -> None:
|
|
41
41
|
self._client.close()
|
|
42
42
|
|
|
43
|
+
def set_request_id(self, request_id: str | None) -> None:
|
|
44
|
+
"""Stamp X-Request-ID on subsequent outgoing requests so the cited
|
|
45
|
+
backend can echo it in its access logs and produce end-to-end traces.
|
|
46
|
+
Pass None to clear."""
|
|
47
|
+
if request_id:
|
|
48
|
+
self._client.headers["X-Request-ID"] = request_id
|
|
49
|
+
else:
|
|
50
|
+
self._client.headers.pop("X-Request-ID", None)
|
|
51
|
+
|
|
43
52
|
def __enter__(self) -> CitedClient:
|
|
44
53
|
return self
|
|
45
54
|
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cited-mcp"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.15"
|
|
8
8
|
description = "MCP server for the Cited GEO platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
11
11
|
requires-python = ">=3.11"
|
|
12
12
|
authors = [{ name = "Cited", email = "support@youcited.com" }]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"cited-core>=0.3.
|
|
14
|
+
"cited-core>=0.3.15",
|
|
15
15
|
"mcp>=1.27.0",
|
|
16
16
|
"PyJWT>=2.8.0",
|
|
17
17
|
"PyYAML>=6.0",
|
|
@@ -21,6 +21,57 @@
|
|
|
21
21
|
# this file.
|
|
22
22
|
|
|
23
23
|
versions:
|
|
24
|
+
- version: "0.3.15"
|
|
25
|
+
released: "2026-05-05"
|
|
26
|
+
# Fingerprint unchanged from 0.3.14 image runtime — pure observability +
|
|
27
|
+
# logging-enrichment release; no tool surface change. (The 0.3.14 entry
|
|
28
|
+
# below pins defa3022aedb, which was the host-venv compute at release
|
|
29
|
+
# prep — differing from the deployed image's 1c4fed943fff. Future
|
|
30
|
+
# entries should pin runtime fingerprints; this collision is benign
|
|
31
|
+
# because there's nothing in the surface for clients to act on.)
|
|
32
|
+
fingerprint: "1c4fed943fff"
|
|
33
|
+
tools_added: []
|
|
34
|
+
tools_changed: []
|
|
35
|
+
tools_removed: []
|
|
36
|
+
behavior_changed:
|
|
37
|
+
- area: structured_logging
|
|
38
|
+
change_summary: |
|
|
39
|
+
Every ``tool_call`` log line now carries:
|
|
40
|
+
- ``server_version`` and ``tools_fingerprint`` (so log entries
|
|
41
|
+
self-identify which build emitted them)
|
|
42
|
+
- ``deployment_id`` from ``CITED_DEPLOYMENT_ID`` env var
|
|
43
|
+
(format ``<env>-<short-sha>-<unix-ts>``, set by the deploy
|
|
44
|
+
script — uniquely identifies a single deploy attempt)
|
|
45
|
+
- ``client_id`` extracted from the OAuth JWT (lets us tell
|
|
46
|
+
Claude Desktop traffic from Claude.ai traffic)
|
|
47
|
+
- ``resources`` map with ID-shaped kwargs (``business_id``,
|
|
48
|
+
``audit_id``, ``job_id``, ``source_id``, etc.) — enough to
|
|
49
|
+
correlate user activity with backend records, never raw
|
|
50
|
+
content like buyer profiles or audit descriptions.
|
|
51
|
+
On failures the line now also carries ``error_type`` and
|
|
52
|
+
``status_code`` pulled from the typed-error response, so the
|
|
53
|
+
dashboard can categorise failures without grepping messages.
|
|
54
|
+
- area: end_to_end_tracing
|
|
55
|
+
change_summary: |
|
|
56
|
+
The MCP now stamps ``X-Request-ID`` on every outgoing call to
|
|
57
|
+
the cited backend (matching the ``request_id`` already in the
|
|
58
|
+
tool_call log line). The backend can echo this to its own
|
|
59
|
+
access logs to produce end-to-end traces — when a customer
|
|
60
|
+
reports "buyer_fit_query 500 with request_id abc123" the
|
|
61
|
+
owner can find both MCP-side and backend-side log entries
|
|
62
|
+
for the same request.
|
|
63
|
+
- area: deploy_pipeline
|
|
64
|
+
change_summary: |
|
|
65
|
+
Companion ``deploy-mcp.sh`` improvements (cited-mcp-infra
|
|
66
|
+
repo, commit 5542667): split log groups (prod writes to
|
|
67
|
+
``/ecs/cited-mcp-prod`` instead of mixing into the dev
|
|
68
|
+
group), real stability check that fails the deploy when ECS
|
|
69
|
+
silently rolls back to a prior task definition (the failure
|
|
70
|
+
mode that hid today's IAM-blocked prod deploy), and
|
|
71
|
+
deployment_id stamping. CloudWatch dashboard ``cited-mcp``
|
|
72
|
+
+ saved Insights queries (``cited-mcp/usage/per-user`` etc.)
|
|
73
|
+
+ failure-burst alarms also added.
|
|
74
|
+
|
|
24
75
|
- version: "0.3.14"
|
|
25
76
|
released: "2026-05-05"
|
|
26
77
|
fingerprint: "defa3022aedb"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import functools
|
|
4
5
|
import hashlib
|
|
5
6
|
import json
|
|
@@ -52,9 +53,7 @@ def _truncate_response(data: Any, max_bytes: int = _MAX_RESPONSE_BYTES) -> Any:
|
|
|
52
53
|
|
|
53
54
|
if isinstance(data, dict):
|
|
54
55
|
# Find list-valued fields and iteratively shrink the longest
|
|
55
|
-
list_fields = {
|
|
56
|
-
k: v for k, v in data.items() if isinstance(v, list) and len(v) > 0
|
|
57
|
-
}
|
|
56
|
+
list_fields = {k: v for k, v in data.items() if isinstance(v, list) and len(v) > 0}
|
|
58
57
|
truncated_fields: list[str] = []
|
|
59
58
|
attempts = 0
|
|
60
59
|
while (
|
|
@@ -165,9 +164,7 @@ def _auth_check(cited_ctx: CitedContext) -> dict[str, Any] | None:
|
|
|
165
164
|
return None
|
|
166
165
|
|
|
167
166
|
|
|
168
|
-
def _resolve_business_id(
|
|
169
|
-
cited_ctx: CitedContext, business_id: str | None
|
|
170
|
-
) -> str | None:
|
|
167
|
+
def _resolve_business_id(cited_ctx: CitedContext, business_id: str | None) -> str | None:
|
|
171
168
|
"""Return *business_id* if provided, else the session default."""
|
|
172
169
|
return business_id or cited_ctx.default_business_id
|
|
173
170
|
|
|
@@ -241,14 +238,107 @@ def _extract_user(token: str | None) -> str:
|
|
|
241
238
|
if not token:
|
|
242
239
|
return "anonymous"
|
|
243
240
|
try:
|
|
244
|
-
payload = pyjwt.decode(
|
|
245
|
-
token, options={"verify_signature": False}
|
|
246
|
-
)
|
|
241
|
+
payload = pyjwt.decode(token, options={"verify_signature": False})
|
|
247
242
|
return str(payload.get("email") or payload.get("sub") or "unknown")
|
|
248
243
|
except Exception:
|
|
249
244
|
return "unknown"
|
|
250
245
|
|
|
251
246
|
|
|
247
|
+
def _extract_client_id(token: str | None) -> str | None:
|
|
248
|
+
"""Decode OAuth client_id from a JWT (for connector-mix analytics)."""
|
|
249
|
+
if not token:
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
payload = pyjwt.decode(token, options={"verify_signature": False})
|
|
253
|
+
cid = payload.get("client_id") or payload.get("aud") or payload.get("azp")
|
|
254
|
+
if isinstance(cid, list):
|
|
255
|
+
cid = cid[0] if cid else None
|
|
256
|
+
return str(cid) if cid else None
|
|
257
|
+
except Exception:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Build identity — stamped into every tool_call line for log correlation.
|
|
263
|
+
# Computed once at module import: cheap, deterministic, survives the request.
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
_BUILD_IDENTITY: dict[str, str] = {}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get_build_identity() -> dict[str, str]:
|
|
270
|
+
"""Return cached server_version, tools_fingerprint, deployment_id.
|
|
271
|
+
|
|
272
|
+
Lazy + memoised because compute_tools_fingerprint walks the live server's
|
|
273
|
+
tool registry — fine to do once but expensive per request.
|
|
274
|
+
"""
|
|
275
|
+
if _BUILD_IDENTITY:
|
|
276
|
+
return _BUILD_IDENTITY
|
|
277
|
+
try:
|
|
278
|
+
from cited_mcp import __version__ as ver
|
|
279
|
+
from cited_mcp.server import compute_tools_fingerprint
|
|
280
|
+
from cited_mcp.server import mcp as live_mcp
|
|
281
|
+
|
|
282
|
+
_BUILD_IDENTITY["server_version"] = ver
|
|
283
|
+
_BUILD_IDENTITY["tools_fingerprint"] = compute_tools_fingerprint(live_mcp)
|
|
284
|
+
except Exception:
|
|
285
|
+
# Don't crash logging if the import fails — emit blanks instead.
|
|
286
|
+
_BUILD_IDENTITY["server_version"] = "unknown"
|
|
287
|
+
_BUILD_IDENTITY["tools_fingerprint"] = "unknown"
|
|
288
|
+
_BUILD_IDENTITY["deployment_id"] = os.environ.get("CITED_DEPLOYMENT_ID", "local")
|
|
289
|
+
return _BUILD_IDENTITY
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
# Resource ID extraction for tool_call logs.
|
|
294
|
+
#
|
|
295
|
+
# We log obvious-shaped IDs (business_id, audit_id, job_id, source_id, etc.)
|
|
296
|
+
# so customer-support and abuse-investigation flows can correlate user
|
|
297
|
+
# activity with platform records. We deliberately do NOT log free-text
|
|
298
|
+
# content (audit descriptions, buyer profiles, etc.) — those can contain
|
|
299
|
+
# sensitive material.
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
# Tool kwargs that name a resource we want to log. Match against suffixes:
|
|
303
|
+
# *_id and *_type are the conventions across the surface.
|
|
304
|
+
_RESOURCE_KEYS = {
|
|
305
|
+
"business_id",
|
|
306
|
+
"audit_id",
|
|
307
|
+
"baseline_id",
|
|
308
|
+
"job_id",
|
|
309
|
+
"recommendation_job_id",
|
|
310
|
+
"audit_job_id",
|
|
311
|
+
"named_audit_id",
|
|
312
|
+
"template_id",
|
|
313
|
+
"solution_id",
|
|
314
|
+
"recommendation_id",
|
|
315
|
+
"source_id",
|
|
316
|
+
"source_type",
|
|
317
|
+
"action_id",
|
|
318
|
+
"check_id",
|
|
319
|
+
"question_id",
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Maximum length of a logged ID value — avoids the case where a tool author
|
|
323
|
+
# accidentally maps a long string into one of these key names.
|
|
324
|
+
_MAX_RESOURCE_VALUE_LEN = 80
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _extract_resource_ids(kwargs: dict[str, Any]) -> dict[str, str]:
|
|
328
|
+
"""Pull ID-shaped kwargs into a flat dict, redacting anything else."""
|
|
329
|
+
out: dict[str, str] = {}
|
|
330
|
+
for key, val in kwargs.items():
|
|
331
|
+
if key not in _RESOURCE_KEYS:
|
|
332
|
+
continue
|
|
333
|
+
if val is None:
|
|
334
|
+
continue
|
|
335
|
+
s = str(val)
|
|
336
|
+
if len(s) > _MAX_RESOURCE_VALUE_LEN:
|
|
337
|
+
continue
|
|
338
|
+
out[key] = s
|
|
339
|
+
return out
|
|
340
|
+
|
|
341
|
+
|
|
252
342
|
# ---------------------------------------------------------------------------
|
|
253
343
|
# Per-user tier cache (avoids /auth/me call on every tool invocation)
|
|
254
344
|
# ---------------------------------------------------------------------------
|
|
@@ -294,31 +384,52 @@ def log_tool_call(
|
|
|
294
384
|
token = _resolve_token(ctx)
|
|
295
385
|
rl_key = _rate_limit_key(token)
|
|
296
386
|
user = _extract_user(token)
|
|
387
|
+
client_id = _extract_client_id(token)
|
|
297
388
|
request_id = uuid.uuid4().hex[:12]
|
|
298
389
|
start = time.monotonic()
|
|
390
|
+
build = _get_build_identity()
|
|
391
|
+
resources = _extract_resource_ids(kwargs)
|
|
392
|
+
# Header-propagation context — set inside the try block below so that
|
|
393
|
+
# _get_ctx failures still flow through structured error handling.
|
|
394
|
+
cited_ctx_for_header: CitedContext | None = None
|
|
395
|
+
|
|
396
|
+
def _log(event: str, **extra: Any) -> None:
|
|
397
|
+
payload: dict[str, Any] = {
|
|
398
|
+
"event": event,
|
|
399
|
+
"request_id": request_id,
|
|
400
|
+
"tool": tool_name,
|
|
401
|
+
"user": user,
|
|
402
|
+
"client_id": client_id,
|
|
403
|
+
"server_version": build["server_version"],
|
|
404
|
+
"tools_fingerprint": build["tools_fingerprint"],
|
|
405
|
+
"deployment_id": build["deployment_id"],
|
|
406
|
+
}
|
|
407
|
+
if resources:
|
|
408
|
+
payload["resources"] = resources
|
|
409
|
+
payload.update(extra)
|
|
410
|
+
logger.info(json.dumps(payload, default=str))
|
|
299
411
|
|
|
300
412
|
# Rate limit check
|
|
301
413
|
if rl_err := _check_rate_limit(rl_key):
|
|
302
|
-
|
|
303
|
-
json.dumps(
|
|
304
|
-
{
|
|
305
|
-
"event": "rate_limited",
|
|
306
|
-
"request_id": request_id,
|
|
307
|
-
"tool": tool_name,
|
|
308
|
-
"user": user,
|
|
309
|
-
}
|
|
310
|
-
)
|
|
311
|
-
)
|
|
414
|
+
_log("rate_limited")
|
|
312
415
|
rl_err["_request_id"] = request_id
|
|
313
416
|
return rl_err
|
|
314
417
|
|
|
315
418
|
try:
|
|
419
|
+
# Propagate request_id to the cited backend for end-to-end traces.
|
|
420
|
+
cited_ctx_for_header = _get_ctx(ctx)
|
|
421
|
+
cited_ctx_for_header.client.set_request_id(request_id)
|
|
422
|
+
|
|
316
423
|
# Plan gating check (skip for auth tools — must always be accessible)
|
|
317
424
|
from cited_mcp.plan_gating import is_tool_allowed, upgrade_message
|
|
318
425
|
|
|
319
426
|
_AUTH_TOOLS = {
|
|
320
|
-
"ping",
|
|
321
|
-
"
|
|
427
|
+
"ping",
|
|
428
|
+
"check_auth_status",
|
|
429
|
+
"login",
|
|
430
|
+
"logout",
|
|
431
|
+
"get_pricing",
|
|
432
|
+
"upgrade_plan",
|
|
322
433
|
}
|
|
323
434
|
if tool_name not in _AUTH_TOOLS and token:
|
|
324
435
|
cited_ctx = _get_ctx(ctx)
|
|
@@ -327,39 +438,31 @@ def log_tool_call(
|
|
|
327
438
|
if not is_tool_allowed(tool_name, user_tier):
|
|
328
439
|
gate_err = upgrade_message(tool_name, user_tier)
|
|
329
440
|
gate_err["_request_id"] = request_id
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
"request_id": request_id,
|
|
335
|
-
"tool": tool_name,
|
|
336
|
-
"user": user,
|
|
337
|
-
"current_tier": user_tier,
|
|
338
|
-
"required_tier": gate_err[
|
|
339
|
-
"required_tier"
|
|
340
|
-
],
|
|
341
|
-
}
|
|
342
|
-
)
|
|
441
|
+
_log(
|
|
442
|
+
"plan_gated",
|
|
443
|
+
current_tier=user_tier,
|
|
444
|
+
required_tier=gate_err["required_tier"],
|
|
343
445
|
)
|
|
344
446
|
return gate_err
|
|
345
447
|
|
|
346
448
|
result = await func(ctx, *args, **kwargs)
|
|
347
449
|
duration_ms = int((time.monotonic() - start) * 1000)
|
|
348
|
-
is_error = isinstance(result, dict) and result.get(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)
|
|
362
|
-
|
|
450
|
+
is_error = isinstance(result, dict) and result.get("error") is True
|
|
451
|
+
extra: dict[str, Any] = {
|
|
452
|
+
"duration_ms": duration_ms,
|
|
453
|
+
"success": not is_error,
|
|
454
|
+
}
|
|
455
|
+
if is_error and isinstance(result, dict):
|
|
456
|
+
# Pull through the typed error fields the tools already emit
|
|
457
|
+
# so failures are categorisable in the dashboard without
|
|
458
|
+
# re-grepping raw error messages.
|
|
459
|
+
if et := result.get("error_type"):
|
|
460
|
+
extra["error_type"] = et
|
|
461
|
+
if sc := result.get("status_code"):
|
|
462
|
+
extra["status_code"] = sc
|
|
463
|
+
if pr := result.get("payment_required"):
|
|
464
|
+
extra["payment_required"] = pr
|
|
465
|
+
_log("tool_call", **extra)
|
|
363
466
|
# Inject request_id into response
|
|
364
467
|
if isinstance(result, dict):
|
|
365
468
|
result["_request_id"] = request_id
|
|
@@ -372,19 +475,7 @@ def log_tool_call(
|
|
|
372
475
|
|
|
373
476
|
except httpx.TimeoutException:
|
|
374
477
|
duration_ms = int((time.monotonic() - start) * 1000)
|
|
375
|
-
|
|
376
|
-
json.dumps(
|
|
377
|
-
{
|
|
378
|
-
"event": "tool_call",
|
|
379
|
-
"request_id": request_id,
|
|
380
|
-
"tool": tool_name,
|
|
381
|
-
"user": user,
|
|
382
|
-
"duration_ms": duration_ms,
|
|
383
|
-
"success": False,
|
|
384
|
-
"error_type": "timeout",
|
|
385
|
-
}
|
|
386
|
-
)
|
|
387
|
-
)
|
|
478
|
+
_log("tool_call", duration_ms=duration_ms, success=False, error_type="timeout")
|
|
388
479
|
return {
|
|
389
480
|
"error": True,
|
|
390
481
|
"error_type": "upstream_timeout",
|
|
@@ -398,19 +489,7 @@ def log_tool_call(
|
|
|
398
489
|
|
|
399
490
|
except httpx.ConnectError:
|
|
400
491
|
duration_ms = int((time.monotonic() - start) * 1000)
|
|
401
|
-
|
|
402
|
-
json.dumps(
|
|
403
|
-
{
|
|
404
|
-
"event": "tool_call",
|
|
405
|
-
"request_id": request_id,
|
|
406
|
-
"tool": tool_name,
|
|
407
|
-
"user": user,
|
|
408
|
-
"duration_ms": duration_ms,
|
|
409
|
-
"success": False,
|
|
410
|
-
"error_type": "connection_error",
|
|
411
|
-
}
|
|
412
|
-
)
|
|
413
|
-
)
|
|
492
|
+
_log("tool_call", duration_ms=duration_ms, success=False, error_type="connection_error")
|
|
414
493
|
return {
|
|
415
494
|
"error": True,
|
|
416
495
|
"error_type": "connection_error",
|
|
@@ -424,29 +503,27 @@ def log_tool_call(
|
|
|
424
503
|
|
|
425
504
|
except Exception as exc:
|
|
426
505
|
duration_ms = int((time.monotonic() - start) * 1000)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
"tool": tool_name,
|
|
433
|
-
"user": user,
|
|
434
|
-
"duration_ms": duration_ms,
|
|
435
|
-
"success": False,
|
|
436
|
-
"error_type": type(exc).__name__,
|
|
437
|
-
}
|
|
438
|
-
)
|
|
506
|
+
_log(
|
|
507
|
+
"tool_call",
|
|
508
|
+
duration_ms=duration_ms,
|
|
509
|
+
success=False,
|
|
510
|
+
error_type=type(exc).__name__,
|
|
439
511
|
)
|
|
440
512
|
return {
|
|
441
513
|
"error": True,
|
|
442
514
|
"error_type": type(exc).__name__,
|
|
443
515
|
"message": (
|
|
444
|
-
"An unexpected error occurred. "
|
|
445
|
-
"This is usually transient — please retry."
|
|
516
|
+
"An unexpected error occurred. This is usually transient — please retry."
|
|
446
517
|
),
|
|
447
518
|
"retriable": True,
|
|
448
519
|
"_request_id": request_id,
|
|
449
520
|
}
|
|
521
|
+
finally:
|
|
522
|
+
# Clear the request_id header so the next caller's tool_call
|
|
523
|
+
# doesn't accidentally inherit this one's correlation id.
|
|
524
|
+
if cited_ctx_for_header is not None:
|
|
525
|
+
with contextlib.suppress(Exception):
|
|
526
|
+
cited_ctx_for_header.client.set_request_id(None)
|
|
450
527
|
|
|
451
528
|
return wrapper
|
|
452
529
|
|
|
@@ -461,8 +538,7 @@ def _api_error_response(e: CitedAPIError) -> dict[str, Any]:
|
|
|
461
538
|
|
|
462
539
|
for code, substring, hint in _ERROR_HINTS:
|
|
463
540
|
if e.status_code == code and (
|
|
464
|
-
substring is None
|
|
465
|
-
or (e.message and substring.lower() in e.message.lower())
|
|
541
|
+
substring is None or (e.message and substring.lower() in e.message.lower())
|
|
466
542
|
):
|
|
467
543
|
response["hint"] = hint
|
|
468
544
|
break
|
|
@@ -1556,6 +1556,78 @@ class TestDecoratorErrorHandling:
|
|
|
1556
1556
|
assert "_request_id" in result
|
|
1557
1557
|
|
|
1558
1558
|
|
|
1559
|
+
class TestEnrichedLogging:
|
|
1560
|
+
"""Verify tool_call lines carry server_version, fingerprint, deployment_id,
|
|
1561
|
+
resource IDs, and propagate X-Request-ID to the cited backend."""
|
|
1562
|
+
|
|
1563
|
+
def test_tool_call_log_includes_build_identity_and_resources(self, ctx, caplog):
|
|
1564
|
+
"""Successful tool_call log must carry server_version, tools_fingerprint,
|
|
1565
|
+
deployment_id, AND extracted resource IDs from kwargs."""
|
|
1566
|
+
import json
|
|
1567
|
+
import logging
|
|
1568
|
+
|
|
1569
|
+
from cited_mcp.tools._helpers import _BUILD_IDENTITY
|
|
1570
|
+
|
|
1571
|
+
client = ctx.request_context.lifespan_context.client
|
|
1572
|
+
client.get.return_value = {"id": "biz-abc"}
|
|
1573
|
+
|
|
1574
|
+
with caplog.at_level(logging.INFO, logger="cited_mcp.usage"):
|
|
1575
|
+
run(get_business(ctx, business_id="biz-abc"))
|
|
1576
|
+
|
|
1577
|
+
tool_call_lines = [
|
|
1578
|
+
json.loads(r.message)
|
|
1579
|
+
for r in caplog.records
|
|
1580
|
+
if r.name == "cited_mcp.usage" and '"event": "tool_call"' in r.message
|
|
1581
|
+
]
|
|
1582
|
+
assert tool_call_lines, "no tool_call log line emitted"
|
|
1583
|
+
rec = tool_call_lines[-1]
|
|
1584
|
+
# build identity is stamped
|
|
1585
|
+
assert rec["server_version"] == _BUILD_IDENTITY["server_version"]
|
|
1586
|
+
assert rec["tools_fingerprint"] == _BUILD_IDENTITY["tools_fingerprint"]
|
|
1587
|
+
assert "deployment_id" in rec
|
|
1588
|
+
# the business_id kwarg made it into resources
|
|
1589
|
+
assert rec.get("resources", {}).get("business_id") == "biz-abc"
|
|
1590
|
+
|
|
1591
|
+
def test_tool_call_propagates_request_id_to_client(self, ctx):
|
|
1592
|
+
"""The decorator must call set_request_id on the CitedClient so the
|
|
1593
|
+
backend gets X-Request-ID for end-to-end correlation."""
|
|
1594
|
+
client = ctx.request_context.lifespan_context.client
|
|
1595
|
+
client.get.return_value = {"id": "biz-abc"}
|
|
1596
|
+
|
|
1597
|
+
run(get_business(ctx, business_id="biz-abc"))
|
|
1598
|
+
|
|
1599
|
+
# set_request_id should have been called twice: once with the
|
|
1600
|
+
# request_id, once with None to clear in finally.
|
|
1601
|
+
assert client.set_request_id.call_count >= 2
|
|
1602
|
+
# First call carries a non-None 12-char hex request_id
|
|
1603
|
+
first = client.set_request_id.call_args_list[0].args[0]
|
|
1604
|
+
assert isinstance(first, str) and len(first) == 12
|
|
1605
|
+
# Last call clears it
|
|
1606
|
+
assert client.set_request_id.call_args_list[-1].args[0] is None
|
|
1607
|
+
|
|
1608
|
+
def test_tool_call_log_carries_typed_error_on_failure(self, ctx, caplog):
|
|
1609
|
+
"""When a tool returns {error: True, error_type: ..., status_code: ...}
|
|
1610
|
+
the tool_call log line should include those fields."""
|
|
1611
|
+
import json
|
|
1612
|
+
import logging
|
|
1613
|
+
|
|
1614
|
+
client = ctx.request_context.lifespan_context.client
|
|
1615
|
+
client.get.side_effect = CitedAPIError(404, "Business not found", "not_found")
|
|
1616
|
+
|
|
1617
|
+
with caplog.at_level(logging.INFO, logger="cited_mcp.usage"):
|
|
1618
|
+
run(get_business(ctx, business_id="missing"))
|
|
1619
|
+
|
|
1620
|
+
tool_call_lines = [
|
|
1621
|
+
json.loads(r.message)
|
|
1622
|
+
for r in caplog.records
|
|
1623
|
+
if r.name == "cited_mcp.usage" and '"event": "tool_call"' in r.message
|
|
1624
|
+
]
|
|
1625
|
+
assert tool_call_lines
|
|
1626
|
+
rec = tool_call_lines[-1]
|
|
1627
|
+
assert rec["success"] is False
|
|
1628
|
+
assert rec.get("status_code") == 404
|
|
1629
|
+
|
|
1630
|
+
|
|
1559
1631
|
class TestTransportErrors:
|
|
1560
1632
|
def test_timeout_returns_error(self, ctx):
|
|
1561
1633
|
"""httpx.TimeoutException should return a structured error, not crash."""
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cited-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.15"
|
|
8
8
|
description = "CLI for the Cited GEO platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
11
11
|
requires-python = ">=3.11"
|
|
12
12
|
authors = [{ name = "Cited", email = "support@youcited.com" }]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"cited-core>=0.3.
|
|
14
|
+
"cited-core>=0.3.15",
|
|
15
15
|
"typer>=0.15.0",
|
|
16
16
|
"rich>=13.0.0",
|
|
17
17
|
]
|
|
@@ -20,13 +20,13 @@ dependencies = [
|
|
|
20
20
|
cited = "cited_cli.app:main"
|
|
21
21
|
|
|
22
22
|
[project.optional-dependencies]
|
|
23
|
-
mcp = ["cited-mcp>=0.3.
|
|
23
|
+
mcp = ["cited-mcp>=0.3.15"]
|
|
24
24
|
dev = [
|
|
25
25
|
"pytest>=8.0",
|
|
26
26
|
"respx>=0.21",
|
|
27
27
|
"ruff>=0.4",
|
|
28
28
|
"mypy>=1.10",
|
|
29
|
-
"cited-mcp>=0.3.
|
|
29
|
+
"cited-mcp>=0.3.15",
|
|
30
30
|
"types-PyYAML>=6.0",
|
|
31
31
|
]
|
|
32
32
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.14"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|