oy-cli 0.3.1__tar.gz → 0.3.2__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.
- {oy_cli-0.3.1 → oy_cli-0.3.2}/PKG-INFO +5 -4
- {oy_cli-0.3.1 → oy_cli-0.3.2}/README.md +3 -2
- {oy_cli-0.3.1 → oy_cli-0.3.2}/oy_cli.egg-info/PKG-INFO +5 -4
- {oy_cli-0.3.1 → oy_cli-0.3.2}/oy_cli.egg-info/requires.txt +1 -1
- {oy_cli-0.3.1 → oy_cli-0.3.2}/oy_cli.py +2 -30
- {oy_cli-0.3.1 → oy_cli-0.3.2}/pyproject.toml +2 -2
- {oy_cli-0.3.1 → oy_cli-0.3.2}/shim.py +84 -66
- {oy_cli-0.3.1 → oy_cli-0.3.2}/LICENSE +0 -0
- {oy_cli-0.3.1 → oy_cli-0.3.2}/oy_cli.egg-info/SOURCES.txt +0 -0
- {oy_cli-0.3.1 → oy_cli-0.3.2}/oy_cli.egg-info/dependency_links.txt +0 -0
- {oy_cli-0.3.1 → oy_cli-0.3.2}/oy_cli.egg-info/entry_points.txt +0 -0
- {oy_cli-0.3.1 → oy_cli-0.3.2}/oy_cli.egg-info/top_level.txt +0 -0
- {oy_cli-0.3.1 → oy_cli-0.3.2}/setup.cfg +0 -0
- {oy_cli-0.3.1 → oy_cli-0.3.2}/tests/test_oy_cli.py +0 -0
- {oy_cli-0.3.1 → oy_cli-0.3.2}/tests/test_shim.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oy-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Tiny local coding CLI with a small tool surface
|
|
5
5
|
Author: oy-cli contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -10,9 +10,9 @@ Project-URL: Issues, https://github.com/wagov-dtt/oy-cli/issues
|
|
|
10
10
|
Requires-Python: >=3.14
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
+
Requires-Dist: boto3>=1.35.0
|
|
13
14
|
Requires-Dist: defopt>=7.0.0
|
|
14
15
|
Requires-Dist: httpx>=0.28.1
|
|
15
|
-
Requires-Dist: httpx-aws-auth>=4.1.1
|
|
16
16
|
Requires-Dist: markdownify>=1.2.0
|
|
17
17
|
Requires-Dist: openai>=1.68.0
|
|
18
18
|
Requires-Dist: rich>=14.3.0
|
|
@@ -23,6 +23,8 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
# oy-cli
|
|
25
25
|
|
|
26
|
+
[](https://pypi.org/project/oy-cli/)
|
|
27
|
+
|
|
26
28
|
**Tiny AI coding assistant for your shell.** Reads files, runs commands, makes precise edits, and stays intentionally small.
|
|
27
29
|
|
|
28
30
|
```bash
|
|
@@ -55,7 +57,6 @@ oy chat # Interactive multi-turn session
|
|
|
55
57
|
oy audit # Security audit against OWASP ASVS/MASVS
|
|
56
58
|
oy model # Show current default model
|
|
57
59
|
oy model <filter> # Pick/save a model
|
|
58
|
-
oy model --token # Print Bedrock OpenAI env exports
|
|
59
60
|
oy --help # Show all commands
|
|
60
61
|
```
|
|
61
62
|
|
|
@@ -228,7 +229,7 @@ Recommended:
|
|
|
228
229
|
- avoid exposing long-lived secrets in the environment
|
|
229
230
|
- review generated changes before shipping
|
|
230
231
|
|
|
231
|
-
**Automatic protections:** workspace-bound file access, structured edits through `apply`, sensitive header redaction in `httpx`, and
|
|
232
|
+
**Automatic protections:** workspace-bound file access, structured edits through `apply`, sensitive header redaction in `httpx`, and native boto3 credential resolution for Bedrock.
|
|
232
233
|
|
|
233
234
|
## Links
|
|
234
235
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# oy-cli
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/oy-cli/)
|
|
4
|
+
|
|
3
5
|
**Tiny AI coding assistant for your shell.** Reads files, runs commands, makes precise edits, and stays intentionally small.
|
|
4
6
|
|
|
5
7
|
```bash
|
|
@@ -32,7 +34,6 @@ oy chat # Interactive multi-turn session
|
|
|
32
34
|
oy audit # Security audit against OWASP ASVS/MASVS
|
|
33
35
|
oy model # Show current default model
|
|
34
36
|
oy model <filter> # Pick/save a model
|
|
35
|
-
oy model --token # Print Bedrock OpenAI env exports
|
|
36
37
|
oy --help # Show all commands
|
|
37
38
|
```
|
|
38
39
|
|
|
@@ -205,7 +206,7 @@ Recommended:
|
|
|
205
206
|
- avoid exposing long-lived secrets in the environment
|
|
206
207
|
- review generated changes before shipping
|
|
207
208
|
|
|
208
|
-
**Automatic protections:** workspace-bound file access, structured edits through `apply`, sensitive header redaction in `httpx`, and
|
|
209
|
+
**Automatic protections:** workspace-bound file access, structured edits through `apply`, sensitive header redaction in `httpx`, and native boto3 credential resolution for Bedrock.
|
|
209
210
|
|
|
210
211
|
## Links
|
|
211
212
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oy-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Tiny local coding CLI with a small tool surface
|
|
5
5
|
Author: oy-cli contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -10,9 +10,9 @@ Project-URL: Issues, https://github.com/wagov-dtt/oy-cli/issues
|
|
|
10
10
|
Requires-Python: >=3.14
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
+
Requires-Dist: boto3>=1.35.0
|
|
13
14
|
Requires-Dist: defopt>=7.0.0
|
|
14
15
|
Requires-Dist: httpx>=0.28.1
|
|
15
|
-
Requires-Dist: httpx-aws-auth>=4.1.1
|
|
16
16
|
Requires-Dist: markdownify>=1.2.0
|
|
17
17
|
Requires-Dist: openai>=1.68.0
|
|
18
18
|
Requires-Dist: rich>=14.3.0
|
|
@@ -23,6 +23,8 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
# oy-cli
|
|
25
25
|
|
|
26
|
+
[](https://pypi.org/project/oy-cli/)
|
|
27
|
+
|
|
26
28
|
**Tiny AI coding assistant for your shell.** Reads files, runs commands, makes precise edits, and stays intentionally small.
|
|
27
29
|
|
|
28
30
|
```bash
|
|
@@ -55,7 +57,6 @@ oy chat # Interactive multi-turn session
|
|
|
55
57
|
oy audit # Security audit against OWASP ASVS/MASVS
|
|
56
58
|
oy model # Show current default model
|
|
57
59
|
oy model <filter> # Pick/save a model
|
|
58
|
-
oy model --token # Print Bedrock OpenAI env exports
|
|
59
60
|
oy --help # Show all commands
|
|
60
61
|
```
|
|
61
62
|
|
|
@@ -228,7 +229,7 @@ Recommended:
|
|
|
228
229
|
- avoid exposing long-lived secrets in the environment
|
|
229
230
|
- review generated changes before shipping
|
|
230
231
|
|
|
231
|
-
**Automatic protections:** workspace-bound file access, structured edits through `apply`, sensitive header redaction in `httpx`, and
|
|
232
|
+
**Automatic protections:** workspace-bound file access, structured edits through `apply`, sensitive header redaction in `httpx`, and native boto3 credential resolution for Bedrock.
|
|
232
233
|
|
|
233
234
|
## Links
|
|
234
235
|
|
|
@@ -5,7 +5,6 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
import readline
|
|
7
7
|
import re
|
|
8
|
-
import shlex
|
|
9
8
|
import sys
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Any, Callable, Literal, TypeAlias, cast
|
|
@@ -28,7 +27,6 @@ from shim import (
|
|
|
28
27
|
UserMessage,
|
|
29
28
|
which,
|
|
30
29
|
CompletionClient,
|
|
31
|
-
bedrock_base_url,
|
|
32
30
|
default_region,
|
|
33
31
|
detect_available_shims,
|
|
34
32
|
ensure_api_env as ensure_shim_api_env,
|
|
@@ -36,7 +34,6 @@ from shim import (
|
|
|
36
34
|
join_model_spec,
|
|
37
35
|
list_model_ids as list_shim_model_ids,
|
|
38
36
|
list_models_for_shim,
|
|
39
|
-
make_bedrock_token,
|
|
40
37
|
require_api_env as require_shim_api_env,
|
|
41
38
|
resolve_shim as resolve_model_shim,
|
|
42
39
|
split_model_spec,
|
|
@@ -1719,36 +1716,11 @@ def resolve_model_choice(model_id=None):
|
|
|
1719
1716
|
query = Prompt.ask("Model or filter", console=STDERR).strip()
|
|
1720
1717
|
|
|
1721
1718
|
|
|
1722
|
-
def model(query: str | None = None
|
|
1723
|
-
"""Show or set the default model
|
|
1719
|
+
def model(query: str | None = None):
|
|
1720
|
+
"""Show or set the default model.
|
|
1724
1721
|
|
|
1725
1722
|
:param query: Exact model ID to save, or a filter string when running in a TTY.
|
|
1726
|
-
:param region: AWS region to use with --token.
|
|
1727
|
-
:param token: Print Bedrock-backed OpenAI credentials instead of model selection.
|
|
1728
1723
|
"""
|
|
1729
|
-
if token:
|
|
1730
|
-
chosen = default_region() if region is None else region
|
|
1731
|
-
_print(
|
|
1732
|
-
"status",
|
|
1733
|
-
f"Generating Bedrock credentials for {_fmt('inline', chosen)}.",
|
|
1734
|
-
err=True,
|
|
1735
|
-
)
|
|
1736
|
-
value = make_bedrock_token(chosen, cwd=Path.cwd())
|
|
1737
|
-
_print(
|
|
1738
|
-
value="## Bedrock Credentials\n\n"
|
|
1739
|
-
+ "Paste this into another shell if you want to reuse the current Bedrock session.\n\n"
|
|
1740
|
-
+ _fmt(
|
|
1741
|
-
"block",
|
|
1742
|
-
"\n".join(
|
|
1743
|
-
[
|
|
1744
|
-
f"export OPENAI_BASE_URL={shlex.quote(bedrock_base_url(chosen))}",
|
|
1745
|
-
f"export OPENAI_API_KEY={shlex.quote(value)}",
|
|
1746
|
-
]
|
|
1747
|
-
),
|
|
1748
|
-
"bash",
|
|
1749
|
-
)
|
|
1750
|
-
)
|
|
1751
|
-
return 0
|
|
1752
1724
|
current = _model(None)
|
|
1753
1725
|
if query is None and not sys.stdin.isatty():
|
|
1754
1726
|
shim = resolve_active_shim(current)
|
|
@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oy-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Tiny local coding CLI with a small tool surface"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
11
11
|
license = "Apache-2.0"
|
|
12
12
|
authors = [{ name = "oy-cli contributors" }]
|
|
13
13
|
dependencies = [
|
|
14
|
+
"boto3>=1.35.0",
|
|
14
15
|
"defopt>=7.0.0",
|
|
15
16
|
"httpx>=0.28.1",
|
|
16
|
-
"httpx-aws-auth>=4.1.1",
|
|
17
17
|
"markdownify>=1.2.0",
|
|
18
18
|
"openai>=1.68.0",
|
|
19
19
|
"rich>=14.3.0",
|
|
@@ -17,7 +17,6 @@ from pathlib import Path
|
|
|
17
17
|
from typing import Any, Awaitable, Callable, TypeAlias
|
|
18
18
|
from urllib.parse import quote
|
|
19
19
|
import httpx
|
|
20
|
-
import httpx_aws_auth
|
|
21
20
|
import msgspec
|
|
22
21
|
from openai import (
|
|
23
22
|
APIConnectionError,
|
|
@@ -2004,84 +2003,84 @@ def _bedrock_output_blocks(data: dict[str, Any]) -> list[ContentBlock]:
|
|
|
2004
2003
|
)
|
|
2005
2004
|
|
|
2006
2005
|
|
|
2007
|
-
def
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
*_fetch_json_ids(
|
|
2011
|
-
client,
|
|
2012
|
-
f"{base_url}/foundation-models",
|
|
2013
|
-
items_key="modelSummaries",
|
|
2014
|
-
id_key="modelId",
|
|
2015
|
-
item_filter=lambda item: (
|
|
2016
|
-
"TEXT" in item.get("outputModalities", [])
|
|
2017
|
-
and item["modelId"].startswith(("global.", "us."))
|
|
2018
|
-
),
|
|
2019
|
-
),
|
|
2020
|
-
*_fetch_json_ids(
|
|
2021
|
-
client,
|
|
2022
|
-
f"{base_url}/inference-profiles",
|
|
2023
|
-
items_key="inferenceProfileSummaries",
|
|
2024
|
-
id_key="inferenceProfileId",
|
|
2025
|
-
item_filter=lambda item: item["inferenceProfileId"].startswith(
|
|
2026
|
-
("global.", "us.")
|
|
2027
|
-
),
|
|
2028
|
-
),
|
|
2029
|
-
]
|
|
2006
|
+
def _boto3_bedrock_model_ids(region: str) -> list[str]:
|
|
2007
|
+
"""List available Bedrock model IDs using boto3."""
|
|
2008
|
+
import boto3
|
|
2030
2009
|
|
|
2010
|
+
client = boto3.client("bedrock", region_name=region)
|
|
2011
|
+
ids: list[str] = []
|
|
2012
|
+
# Foundation models
|
|
2013
|
+
try:
|
|
2014
|
+
resp = client.list_foundation_models()
|
|
2015
|
+
for summary in resp.get("modelSummaries", []):
|
|
2016
|
+
model_id = summary.get("modelId", "")
|
|
2017
|
+
if (
|
|
2018
|
+
"TEXT" in summary.get("outputModalities", [])
|
|
2019
|
+
and model_id.startswith(("global.", "us."))
|
|
2020
|
+
):
|
|
2021
|
+
ids.append(model_id)
|
|
2022
|
+
except Exception:
|
|
2023
|
+
pass
|
|
2024
|
+
# Inference profiles
|
|
2025
|
+
try:
|
|
2026
|
+
resp = client.list_inference_profiles()
|
|
2027
|
+
for summary in resp.get("inferenceProfileSummaries", []):
|
|
2028
|
+
profile_id = summary.get("inferenceProfileId", "")
|
|
2029
|
+
if profile_id.startswith(("global.", "us.")):
|
|
2030
|
+
ids.append(profile_id)
|
|
2031
|
+
except Exception:
|
|
2032
|
+
pass
|
|
2033
|
+
return ids
|
|
2031
2034
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
auth = httpx_aws_auth.AwsSigV4Auth(
|
|
2042
|
-
credentials=aws_credentials, service="bedrock", region=region
|
|
2035
|
+
|
|
2036
|
+
def _bedrock_converse_client(region: str) -> CompletionClient:
|
|
2037
|
+
import asyncio
|
|
2038
|
+
import boto3
|
|
2039
|
+
from botocore.config import Config as BotoConfig
|
|
2040
|
+
|
|
2041
|
+
config = BotoConfig(
|
|
2042
|
+
read_timeout=float(os.environ.get("OY_BEDROCK_READ_TIMEOUT", "120")),
|
|
2043
|
+
retries={"max_attempts": DEFAULT_RETRY_MAX_ATTEMPTS, "mode": "adaptive"},
|
|
2043
2044
|
)
|
|
2045
|
+
rt_client = boto3.client("bedrock-runtime", region_name=region, config=config)
|
|
2044
2046
|
|
|
2045
|
-
def
|
|
2047
|
+
async def chat_completion(
|
|
2046
2048
|
model: str,
|
|
2047
2049
|
messages: list[ChatMessage],
|
|
2048
|
-
tools: list[ToolSpec] | None,
|
|
2049
|
-
tool_choice: str,
|
|
2050
|
-
|
|
2050
|
+
tools: list[ToolSpec] | None = None,
|
|
2051
|
+
tool_choice: str = "auto",
|
|
2052
|
+
on_retry=None,
|
|
2053
|
+
) -> AssistantMessage:
|
|
2054
|
+
_ = on_retry
|
|
2051
2055
|
bedrock_messages, system_prompts = _encode_provider_messages(
|
|
2052
2056
|
messages, BEDROCK_CODEC
|
|
2053
2057
|
)
|
|
2054
|
-
|
|
2058
|
+
kwargs: dict[str, Any] = {
|
|
2059
|
+
"modelId": model,
|
|
2055
2060
|
"messages": bedrock_messages,
|
|
2056
2061
|
"inferenceConfig": {
|
|
2057
|
-
"maxTokens": int(
|
|
2062
|
+
"maxTokens": int(
|
|
2063
|
+
os.environ.get("OY_BEDROCK_MAX_OUTPUT_TOKENS", "4096")
|
|
2064
|
+
)
|
|
2058
2065
|
},
|
|
2059
2066
|
}
|
|
2060
2067
|
if system_prompts:
|
|
2061
|
-
|
|
2068
|
+
kwargs["system"] = system_prompts
|
|
2062
2069
|
if tools and (tool_config := _bedrock_tools(tools, tool_choice)):
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
make_client=lambda: async_http_client(auth=auth),
|
|
2072
|
-
build_request=make_request,
|
|
2073
|
-
decode=lambda data: _assistant_from_blocks(_bedrock_output_blocks(data)),
|
|
2074
|
-
error=lambda resp: _http_error_message("Bedrock", resp),
|
|
2075
|
-
timeout=BEDROCK_TIMEOUT,
|
|
2076
|
-
bedrock=True,
|
|
2077
|
-
)
|
|
2070
|
+
kwargs["toolConfig"] = tool_config
|
|
2071
|
+
# boto3 is synchronous; run in executor to avoid blocking the event loop
|
|
2072
|
+
loop = asyncio.get_running_loop()
|
|
2073
|
+
try:
|
|
2074
|
+
data = await loop.run_in_executor(None, lambda: rt_client.converse(**kwargs))
|
|
2075
|
+
except Exception as exc:
|
|
2076
|
+
raise RuntimeError(f"Bedrock converse error: {exc}") from exc
|
|
2077
|
+
return _assistant_from_blocks(_bedrock_output_blocks(data))
|
|
2078
2078
|
|
|
2079
2079
|
def list_models() -> list[str]:
|
|
2080
|
-
|
|
2081
|
-
return sorted(unique_strings(_bedrock_model_ids(client, region)))
|
|
2080
|
+
return sorted(unique_strings(_boto3_bedrock_model_ids(region)))
|
|
2082
2081
|
|
|
2083
|
-
return
|
|
2084
|
-
|
|
2082
|
+
return CompletionClient(
|
|
2083
|
+
chat_completion=chat_completion,
|
|
2085
2084
|
list_models=list_models,
|
|
2086
2085
|
)
|
|
2087
2086
|
|
|
@@ -2366,6 +2365,25 @@ def _require_aws_env(cwd: Path | None = None) -> None:
|
|
|
2366
2365
|
load_aws_credentials(cwd, allow_login=False)
|
|
2367
2366
|
|
|
2368
2367
|
|
|
2368
|
+
def _require_boto3_aws_env(cwd: Path | None = None) -> None:
|
|
2369
|
+
"""Validate that boto3 can resolve AWS credentials."""
|
|
2370
|
+
_ = cwd
|
|
2371
|
+
import boto3
|
|
2372
|
+
from botocore.exceptions import NoCredentialsError, PartialCredentialsError
|
|
2373
|
+
|
|
2374
|
+
session = boto3.Session()
|
|
2375
|
+
credentials = session.get_credentials()
|
|
2376
|
+
if credentials is None:
|
|
2377
|
+
raise RuntimeError(
|
|
2378
|
+
"No AWS credentials found. Configure via environment variables, "
|
|
2379
|
+
"~/.aws/credentials, IAM role, or AWS SSO."
|
|
2380
|
+
)
|
|
2381
|
+
try:
|
|
2382
|
+
credentials.get_frozen_credentials()
|
|
2383
|
+
except (NoCredentialsError, PartialCredentialsError) as exc:
|
|
2384
|
+
raise RuntimeError(f"AWS credentials incomplete: {exc}") from exc
|
|
2385
|
+
|
|
2386
|
+
|
|
2369
2387
|
def _build_openai_client(
|
|
2370
2388
|
region: str | None = None, cwd: Path | None = None, *, max_retries: int = 3
|
|
2371
2389
|
) -> CompletionClient:
|
|
@@ -2402,7 +2420,8 @@ def _build_gemini_client(
|
|
|
2402
2420
|
def _build_bedrock_client(
|
|
2403
2421
|
region: str | None = None, cwd: Path | None = None
|
|
2404
2422
|
) -> CompletionClient:
|
|
2405
|
-
|
|
2423
|
+
_ = cwd
|
|
2424
|
+
return _bedrock_converse_client(current_region(region))
|
|
2406
2425
|
|
|
2407
2426
|
|
|
2408
2427
|
def _build_mantle_client(
|
|
@@ -2455,9 +2474,8 @@ def _list_claude_models(
|
|
|
2455
2474
|
def _list_bedrock_models(
|
|
2456
2475
|
region: str | None = None, cwd: Path | None = None
|
|
2457
2476
|
) -> list[str]:
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
).list_models()
|
|
2477
|
+
_ = cwd
|
|
2478
|
+
return _bedrock_converse_client(current_region(region)).list_models()
|
|
2461
2479
|
|
|
2462
2480
|
|
|
2463
2481
|
def _list_mantle_models(
|
|
@@ -2487,7 +2505,7 @@ SHIM_SPECS: dict[str, ShimSpec] = {
|
|
|
2487
2505
|
),
|
|
2488
2506
|
SHIM_BEDROCK: ShimSpec(
|
|
2489
2507
|
name=SHIM_BEDROCK,
|
|
2490
|
-
ensure_env=
|
|
2508
|
+
ensure_env=_require_boto3_aws_env,
|
|
2491
2509
|
build_client=_build_bedrock_client,
|
|
2492
2510
|
list_models=_list_bedrock_models,
|
|
2493
2511
|
),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|