Enhance user configuration management and logging

- Introduced user configuration command to set API key.
- Updated README and documentation for user config and logging paths.
- Refactored logging to support user-specific log files.
- Added tests for user configuration and logging behavior.
This commit is contained in:
2026-06-27 10:21:00 +08:00
parent 0e98ce57d4
commit 4685a0165d
10 changed files with 434 additions and 25 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
.pytest_cache/
.test-tmp/
pytest-cache-files-*/
dist/
__pycache__/
*.py[cod]
*$py.class

View File

@@ -9,6 +9,7 @@ AI Nexus currently documents AWS Bedrock Converse API as the workaround while An
```powershell
cd nexus-claude-api
uv sync --extra dev
uv run nexus-claude-api config set --api-key "your-nexus-api-key"
uv run nexus-claude-api start --port 4141 --claude-code
```
@@ -32,11 +33,17 @@ The printed command ends with `claude --model <configured-model>` so the current
Credential lookup order:
1. `--api-key`
2. local `nexus-claude-api.local.json`
2. user config `~/.config/nexus-claude-api/config.json`
3. `NEXUS_API_KEY`
4. `AWS_BEARER_TOKEN_BEDROCK`
For local hardcoded configuration, create `nexus-claude-api.local.json`:
To write user config:
```powershell
nexus-claude-api config set --api-key "your-nexus-api-key"
```
This creates `~/.config/nexus-claude-api/config.json`:
```json
{
@@ -44,7 +51,15 @@ For local hardcoded configuration, create `nexus-claude-api.local.json`:
}
```
This file is ignored by git.
Logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api.log` by default.
For repository-local development only, pass `--dev` and create `nexus-claude-api.local.json` in the current directory:
```powershell
uv run nexus-claude-api start --dev --port 4141 --claude-code
```
In development mode, config is read from `nexus-claude-api.local.json` and logs are written to `logs/nexus-claude-api.log` in the current directory. The local config file is ignored by git.
The service binds to `127.0.0.1` by default and does not persist API keys.
@@ -53,3 +68,4 @@ The service binds to `127.0.0.1` by default and does not persist API keys.
- [PRD](docs/PRD.md)
- [Requirements Design](docs/REQUIREMENTS_DESIGN.md)
- [AI Nexus Claude Documentation](docs/AI_NEXUS_CLAUDE.md)
- [Python Internal Distribution Guide](docs/PACKAGING_DISTRIBUTION.md)

View File

@@ -0,0 +1,187 @@
# Python Internal Distribution Guide
This document describes how to distribute `nexus-claude-api` internally without publishing to PyPI.
## Recommended Options
Use one of these two approaches:
- **Wheel distribution**: build a `.whl` file and send it to colleagues. This is best for stable internal releases.
- **Private Git installation**: let colleagues install directly from the company Git repository. This is best for teammates who can access the source code or need frequent updates.
Both options require:
- Python `>=3.11`
- Access to PyPI or the company Python package mirror for runtime dependencies
- A personal AI Nexus API key
Do not share `nexus-claude-api.local.json`, `.env`, logs, or any personal API key.
## Option 1: Build and Share a Wheel
A wheel is the standard Python install package format. It is not an `.exe`; colleagues install it with `pip`, then run the installed `nexus-claude-api` command.
### Publisher Steps
1. Confirm the package version in `pyproject.toml`:
```toml
[project]
name = "nexus-claude-api"
version = "0.1.0"
```
Update the version for every released package. Avoid sending different builds with the same version.
2. Run the supported checks:
```powershell
uv sync --extra dev
uv run pytest
uv run nexus-claude-api start --dry-run
```
3. Build the package:
```powershell
uv build
```
This creates files under `dist/`, usually:
```text
dist/nexus_claude_api-0.1.0-py3-none-any.whl
dist/nexus_claude_api-0.1.0.tar.gz
```
4. Test the wheel in a clean environment:
```powershell
py -3.11 -m venv C:\Temp\nexus-claude-api-test
C:\Temp\nexus-claude-api-test\Scripts\python.exe -m pip install .\dist\nexus_claude_api-0.1.0-py3-none-any.whl
C:\Temp\nexus-claude-api-test\Scripts\nexus-claude-api.exe start --dry-run
```
5. Send only the `.whl` file to colleagues through an approved internal channel.
### Colleague Installation
Install the wheel:
```powershell
py -3.11 -m pip install .\nexus_claude_api-0.1.0-py3-none-any.whl
```
Configure the personal Nexus key:
```powershell
nexus-claude-api config set --api-key "your-nexus-api-key"
```
Start the local proxy and print the Claude Code helper:
```powershell
nexus-claude-api start --port 4141 --claude-code
```
If PowerShell cannot find the installed script, use the module form:
```powershell
py -3.11 -m nexus_claude_api start --port 4141 --claude-code
```
### Upgrade
Send a new wheel with a new version, then ask colleagues to run:
```powershell
py -3.11 -m pip install --upgrade .\nexus_claude_api-0.1.1-py3-none-any.whl
```
## Option 2: Install from Private Git
Private Git installation avoids manual wheel sharing. It still uses the Python package metadata from `pyproject.toml`, so the installed command is the same.
### Publisher Steps
1. Push the code to the company Git repository.
2. Update the version in `pyproject.toml` for each released version.
3. Tag stable versions:
```powershell
git tag v0.1.0
git push origin v0.1.0
```
For the next release:
```powershell
git tag v0.1.1
git push origin v0.1.1
```
4. Share an install command that points to a tag:
```powershell
py -3.11 -m pip install "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.0"
```
Prefer tags or exact commit SHAs. Avoid asking normal users to install from `main`, because repeated installs can produce different code over time.
### Colleague Installation
Install from the company Git repository:
```powershell
py -3.11 -m pip install "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.0"
```
Configure the personal Nexus key:
```powershell
nexus-claude-api config set --api-key "your-nexus-api-key"
```
Start the local proxy:
```powershell
nexus-claude-api start --port 4141 --claude-code
```
### Upgrade
Upgrade to a newer tag:
```powershell
py -3.11 -m pip install --upgrade "git+https://company-git.example.com/group/nexus-claude-api.git@v0.1.1"
```
### Development Clone
For colleagues who will modify or test the source code:
```powershell
git clone https://company-git.example.com/group/nexus-claude-api.git
cd nexus-claude-api
uv sync --extra dev
uv run nexus-claude-api start --dry-run
```
## Choosing Between the Two
- Use **wheel distribution** for normal internal users who only need a stable tool.
- Use **private Git installation** for colleagues who understand the repository, need fast updates, or help develop the project.
- Keep version numbers and Git tags aligned. For example, `pyproject.toml` version `0.1.0` should correspond to tag `v0.1.0`.
## Release Checklist
Before sharing a release:
- Run `uv run pytest`.
- Run `uv run nexus-claude-api start --dry-run`.
- Test wheel installation in a clean Python environment.
- If using Git installation, test install from the exact tag.
- Confirm no personal credentials are included.
- Confirm `nexus-claude-api.local.json`, `.env`, logs, caches, and `dist/` artifacts are not committed accidentally.

View File

@@ -53,7 +53,8 @@ The proxy defaults to Opus because this deployment is intended for users whose N
## User Stories
- As a developer, I can store my Nexus key in ignored local config or set `NEXUS_API_KEY`, then run `nexus-claude-api start --claude-code` to get a Claude Code launch command.
- As a developer, I can store my Nexus key in user config with `nexus-claude-api config set --api-key <key>` or set `NEXUS_API_KEY`, then run `nexus-claude-api start --claude-code` to get a Claude Code launch command.
- As a contributor working from the repository, I can pass `--dev` to use current-directory development config and logs.
- As a Claude Code user, I can run coding workflows through local `http://127.0.0.1:4141`.
- As a Claude Code user, I can receive streaming model output.
- As a Claude Code user, I can use tool calls and tool results.
@@ -63,7 +64,12 @@ The proxy defaults to Opus because this deployment is intended for users whose N
## Acceptance Criteria
- `uv run nexus-claude-api start --port 4141 --claude-code` starts a local server.
- `nexus-claude-api config set --api-key <key>` writes user config to `~/.config/nexus-claude-api/config.json`.
- The server binds to `127.0.0.1` by default.
- Default startup reads credentials from user config before environment variables.
- `--dev` startup reads current-directory `nexus-claude-api.local.json` instead of user config.
- Default logs are written to `~/.config/nexus-claude-api/logs/nexus-claude-api.log`.
- `--dev` logs are written to current-directory `logs/nexus-claude-api.log`.
- Missing Nexus credentials fail fast with a clear error.
- `GET /health` returns healthy status.
- `GET /v1/models` returns the supported Claude models.
@@ -76,8 +82,10 @@ The proxy defaults to Opus because this deployment is intended for users whose N
## Security Requirements
- Do not persist API keys automatically.
- If the user chooses hardcoded local configuration, keep it in ignored `nexus-claude-api.local.json`.
- Do not persist API keys automatically during startup.
- Persist API keys only when the user explicitly runs `nexus-claude-api config set --api-key <key>` or manually writes a config file.
- Store normal user configuration in `~/.config/nexus-claude-api/config.json`.
- Use ignored current-directory `nexus-claude-api.local.json` only for explicit `--dev` repository-local development mode.
- Do not print or log API keys.
- Redact authorization headers in debug logs.
- Bind locally by default.

View File

@@ -54,16 +54,41 @@ Primary command:
uv run nexus-claude-api start --port 4141 --claude-code
```
User configuration command:
```powershell
nexus-claude-api config set --api-key <key>
```
Options:
- `--host`: default `127.0.0.1`
- `--port`, `-p`: default `4141`
- `--endpoint-url`: default `https://genai-nexus.api.corpinter.net`
- `--api-key`: optional; fallback to ignored local config, `NEXUS_API_KEY`, then `AWS_BEARER_TOKEN_BEDROCK`
- `--api-key`: optional; overrides config files and environment variables
- `--model`: default `claude-opus-4.6`
- `--small-model`: default `claude-opus-4.6`
- `--claude-code`: print Claude Code launch command
- `--verbose`, `-v`: debug logging without secrets
- `--dev`: use current-directory development config and logs
- `--dry-run`: validate config and print helper output without starting the server
Credential lookup order:
1. `--api-key`
2. Current-mode config file
3. `NEXUS_API_KEY`
4. `AWS_BEARER_TOKEN_BEDROCK`
Config file paths:
- Default mode: `~/.config/nexus-claude-api/config.json`
- Development mode with `--dev`: current-directory `nexus-claude-api.local.json`
Log file paths:
- Default mode: `~/.config/nexus-claude-api/logs/nexus-claude-api.log`
- Development mode with `--dev`: current-directory `logs/nexus-claude-api.log`
When `--claude-code` is used, print a PowerShell command that sets:
@@ -89,7 +114,7 @@ Expose:
`ANTHROPIC_AUTH_TOKEN` is printed as `dummy` because Claude Code expects an Anthropic auth token variable to exist. This local proxy does not validate that inbound token by default. It is not the Nexus key.
Inbound authentication headers are accepted for compatibility but not validated by default because the service is local. Outbound Nexus authentication uses `--api-key`, ignored local `nexus-claude-api.local.json`, `NEXUS_API_KEY`, or `AWS_BEARER_TOKEN_BEDROCK`.
Inbound authentication headers are accepted for compatibility but not validated by default because the service is local. Outbound Nexus authentication uses `--api-key`, the current-mode config file, `NEXUS_API_KEY`, or `AWS_BEARER_TOKEN_BEDROCK`.
## Model Mapping
@@ -182,7 +207,7 @@ Return Anthropic-compatible errors:
Status mapping:
- invalid request: `400`
- missing local Nexus credential: startup failure
- missing Nexus credential: startup failure
- Nexus auth failure: `401` or `403`
- Nexus throttling: `429`
- Nexus network/timeout: `502` or `504`
@@ -215,3 +240,14 @@ CLI tests:
- `nexus-claude-api --help`
- Claude Code command generation.
- Missing API key validation.
- User config writing with `nexus-claude-api config set --api-key <key>`.
- Default-mode user config lookup from `~/.config/nexus-claude-api/config.json`.
- Default mode ignores current-directory `nexus-claude-api.local.json`.
- Development mode uses current-directory `nexus-claude-api.local.json`.
- `--api-key` overrides config files and config files override environment variables.
Logging tests:
- Default-mode log path resolves to `~/.config/nexus-claude-api/logs/nexus-claude-api.log`.
- Development-mode log path resolves to current-directory `logs/nexus-claude-api.log`.
- Verbose logging enables debug details without logging secrets.

View File

@@ -5,6 +5,7 @@ import sys
import uvicorn
import nexus_claude_api.config as config
from nexus_claude_api.config import (
DEFAULT_ENDPOINT_URL,
DEFAULT_HOST,
@@ -13,7 +14,7 @@ from nexus_claude_api.config import (
DEFAULT_SMALL_MODEL,
Settings,
)
from nexus_claude_api.logging_config import configure_logging
from nexus_claude_api.logging_config import configure_logging, resolve_log_file
from nexus_claude_api.server import create_app
from nexus_claude_api.shell import generate_claude_code_powershell
@@ -31,11 +32,21 @@ def build_parser() -> argparse.ArgumentParser:
start.add_argument("--small-model", default=DEFAULT_SMALL_MODEL)
start.add_argument("--claude-code", action="store_true")
start.add_argument("--verbose", "-v", action="store_true")
start.add_argument(
"--dev",
action="store_true",
help="Use local development config and logs in the current directory.",
)
start.add_argument(
"--dry-run",
action="store_true",
help="Validate config and print helper output without starting the server.",
)
config = subparsers.add_parser("config", help="Manage user configuration")
config_subparsers = config.add_subparsers(dest="config_command")
config_set = config_subparsers.add_parser("set", help="Write user configuration")
config_set.add_argument("--api-key", required=True)
return parser
@@ -44,10 +55,21 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)
if args.command == "start":
return run_start(args)
if args.command == "config":
return run_config(args, parser)
parser.print_help()
return 0
def run_config(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
if args.config_command == "set":
config_path = config.write_user_config({"api_key": args.api_key})
print(f"Wrote user config to {config_path}")
return 0
parser.parse_args(["config", "--help"])
return 0
def run_start(args: argparse.Namespace) -> int:
settings = Settings.from_values(
host=args.host,
@@ -57,12 +79,15 @@ def run_start(args: argparse.Namespace) -> int:
model=args.model,
small_model=args.small_model,
verbose=args.verbose,
dev=args.dev,
)
if not settings.api_key:
print(
"Missing Nexus API key. Add nexus-claude-api.local.json, set "
"NEXUS_API_KEY or AWS_BEARER_TOKEN_BEDROCK, or pass --api-key.",
"Missing Nexus API key. Run "
"`nexus-claude-api config set --api-key <key>`, set NEXUS_API_KEY "
"or AWS_BEARER_TOKEN_BEDROCK, pass --api-key, or use --dev with "
"nexus-claude-api.local.json.",
file=sys.stderr,
)
return 2
@@ -74,7 +99,10 @@ def run_start(args: argparse.Namespace) -> int:
if args.dry_run:
return 0
log_file = configure_logging(verbose=settings.verbose)
log_file = configure_logging(
verbose=settings.verbose,
log_file=resolve_log_file(dev=args.dev),
)
print(f"Writing detailed logs to {log_file}")
app = create_app(settings)
uvicorn.run(

View File

@@ -13,6 +13,8 @@ DEFAULT_OPUS_MODEL = "claude-opus-4.6"
DEFAULT_MODEL = DEFAULT_OPUS_MODEL
DEFAULT_SMALL_MODEL = DEFAULT_OPUS_MODEL
LOCAL_CONFIG_FILENAME = "nexus-claude-api.local.json"
USER_CONFIG_DIR = Path.home() / ".config" / "nexus-claude-api"
USER_CONFIG_FILE = USER_CONFIG_DIR / "config.json"
MODEL_ID_MAP = {
@@ -48,12 +50,13 @@ class Settings:
small_model: str = DEFAULT_SMALL_MODEL,
verbose: bool = False,
require_api_key: bool = True,
dev: bool = False,
) -> "Settings":
local_config = load_local_config()
config = load_config(dev=dev)
resolved_api_key = (
api_key
or local_config.get("api_key")
or local_config.get("nexus_api_key")
or config.get("api_key")
or config.get("nexus_api_key")
or os.environ.get("NEXUS_API_KEY")
or os.environ.get("AWS_BEARER_TOKEN_BEDROCK")
)
@@ -78,6 +81,30 @@ def resolve_backend_model(model: str) -> str:
def load_local_config(path: str | Path = LOCAL_CONFIG_FILENAME) -> dict[str, str]:
return load_config_file(path)
def load_user_config(path: str | Path | None = None) -> dict[str, str]:
return load_config_file(path or USER_CONFIG_FILE)
def load_config(*, dev: bool = False) -> dict[str, str]:
if dev:
return load_local_config()
return load_user_config()
def write_user_config(config: dict[str, str], path: str | Path | None = None) -> Path:
config_path = Path(path or USER_CONFIG_FILE)
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(
json.dumps(config, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
return config_path
def load_config_file(path: str | Path) -> dict[str, str]:
config_path = Path(path)
if not config_path.exists():
return {}

View File

@@ -3,9 +3,13 @@ from __future__ import annotations
import logging
from pathlib import Path
import nexus_claude_api.config as config
LOG_DIR = Path("logs")
LOG_FILE = LOG_DIR / "nexus-claude-api.log"
LOCAL_LOG_DIR = Path("logs")
LOCAL_LOG_FILE = LOCAL_LOG_DIR / "nexus-claude-api.log"
USER_LOG_DIR = config.USER_CONFIG_DIR / "logs"
USER_LOG_FILE = USER_LOG_DIR / "nexus-claude-api.log"
LOG_FILE = USER_LOG_FILE
LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s: %(message)s"
@@ -24,3 +28,9 @@ def configure_logging(*, verbose: bool = False, log_file: Path = LOG_FILE) -> Pa
app_logger.propagate = False
return log_file
def resolve_log_file(*, dev: bool = False) -> Path:
if dev:
return LOCAL_LOG_FILE
return config.USER_CONFIG_DIR / "logs" / "nexus-claude-api.log"

View File

@@ -1,10 +1,16 @@
from __future__ import annotations
import json
import shutil
from pathlib import Path
from nexus_claude_api.cli import main
from nexus_claude_api.config import Settings, load_local_config
from nexus_claude_api.config import (
Settings,
load_local_config,
load_user_config,
write_user_config,
)
from nexus_claude_api.shell import generate_claude_code_powershell
@@ -41,6 +47,8 @@ def test_claude_code_command_uses_custom_model() -> None:
def test_missing_api_key_fails(monkeypatch) -> None:
tmp_path = _workspace_tmp("missing-key")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
monkeypatch.delenv("NEXUS_API_KEY", raising=False)
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
monkeypatch.chdir(tmp_path)
@@ -54,8 +62,43 @@ def test_missing_api_key_fails(monkeypatch) -> None:
assert exit_code == 2
def test_local_config_api_key(monkeypatch) -> None:
tmp_path = _workspace_tmp("local-config")
def test_user_config_api_key(monkeypatch) -> None:
tmp_path = _workspace_tmp("user-config")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
write_user_config({"api_key": "user-test-key"})
settings = Settings.from_values(require_api_key=False)
assert settings.api_key == "user-test-key"
assert load_user_config()["api_key"] == "user-test-key"
shutil.rmtree(tmp_path, ignore_errors=True)
def test_dev_local_config_api_key(monkeypatch) -> None:
tmp_path = _workspace_tmp("dev-local-config")
monkeypatch.chdir(tmp_path)
(tmp_path / "nexus-claude-api.local.json").write_text(
'{"api_key": "local-test-key"}',
encoding="utf-8",
)
try:
settings = Settings.from_values(require_api_key=False, dev=True)
assert settings.api_key == "local-test-key"
assert load_local_config()["api_key"] == "local-test-key"
finally:
monkeypatch.chdir(Path(__file__).parents[1])
shutil.rmtree(tmp_path, ignore_errors=True)
def test_default_mode_ignores_local_config(monkeypatch) -> None:
tmp_path = _workspace_tmp("default-ignores-local")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
monkeypatch.delenv("NEXUS_API_KEY", raising=False)
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
monkeypatch.chdir(tmp_path)
(tmp_path / "nexus-claude-api.local.json").write_text(
'{"api_key": "local-test-key"}',
@@ -65,13 +108,51 @@ def test_local_config_api_key(monkeypatch) -> None:
try:
settings = Settings.from_values(require_api_key=False)
assert settings.api_key == "local-test-key"
assert load_local_config()["api_key"] == "local-test-key"
assert settings.api_key is None
finally:
monkeypatch.chdir(Path(__file__).parents[1])
shutil.rmtree(tmp_path, ignore_errors=True)
def test_api_key_precedence_config_before_environment(monkeypatch) -> None:
tmp_path = _workspace_tmp("config-precedence")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
monkeypatch.setenv("NEXUS_API_KEY", "env-key")
write_user_config({"api_key": "config-key"})
settings = Settings.from_values(require_api_key=False)
assert settings.api_key == "config-key"
shutil.rmtree(tmp_path, ignore_errors=True)
def test_api_key_argument_overrides_config(monkeypatch) -> None:
tmp_path = _workspace_tmp("arg-precedence")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
write_user_config({"api_key": "config-key"})
settings = Settings.from_values(api_key="arg-key", require_api_key=False)
assert settings.api_key == "arg-key"
shutil.rmtree(tmp_path, ignore_errors=True)
def test_config_set_writes_user_config(monkeypatch) -> None:
tmp_path = _workspace_tmp("config-set")
user_config = tmp_path / ".config" / "nexus-claude-api" / "config.json"
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_FILE", user_config)
exit_code = main(["config", "set", "--api-key", "written-key"])
assert exit_code == 0
assert json.loads(user_config.read_text(encoding="utf-8")) == {
"api_key": "written-key"
}
shutil.rmtree(tmp_path, ignore_errors=True)
def _workspace_tmp(name: str) -> Path:
path = Path(__file__).parents[1] / ".test-tmp" / name
shutil.rmtree(path, ignore_errors=True)

View File

@@ -1,8 +1,9 @@
from __future__ import annotations
import logging
from pathlib import Path
from nexus_claude_api.logging_config import configure_logging
from nexus_claude_api.logging_config import configure_logging, resolve_log_file
def test_configure_logging_writes_app_logs_to_file_without_console(
@@ -55,3 +56,17 @@ def test_configure_logging_verbose_writes_debug_app_logs_to_file(
app_logger.handlers.extend(original_handlers)
app_logger.setLevel(original_level)
app_logger.propagate = original_propagate
def test_resolve_log_file_uses_user_config_dir_by_default(
tmp_path,
monkeypatch,
) -> None:
user_config_dir = tmp_path / ".config" / "nexus-claude-api"
monkeypatch.setattr("nexus_claude_api.config.USER_CONFIG_DIR", user_config_dir)
assert resolve_log_file() == user_config_dir / "logs" / "nexus-claude-api.log"
def test_resolve_log_file_uses_local_logs_in_dev_mode() -> None:
assert resolve_log_file(dev=True) == Path("logs") / "nexus-claude-api.log"