alishahryar1--free-claude-code
71a78a0c5a
## Problem Runtime modules were published as generic top-level packages like `api`, `cli`, and `providers`. That shape is fragile for PyPI packaging and weakens explicit ownership boundaries. ## Changes | Before | After | | --- | --- | | Runtime code lived in root-level packages. | Runtime code lives under `src/free_claude_code/`. | | Console scripts targeted top-level modules. | Console scripts target namespaced modules. | | Tests and smoke helpers imported old package roots. | Tests and smoke helpers import `free_claude_code.*`. | | Packaging listed six root packages. | Packaging builds the single namespaced package. | | Contracts allowed old root package directories. | Contracts require the src namespace and reject old root imports. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR moves the runtime packages into the `src/free_claude_code` namespace. The main changes are: - Console scripts now point to `free_claude_code.*` entrypoints. - Runtime imports, tests, and smoke helpers now use the namespaced package. - Packaging now builds the single `src/free_claude_code` package. - Contract tests now reject old top-level runtime package roots and imports. </details> <h3>Confidence Score: 5/5</h3> This PR is safe to merge with minimal risk. The changes are a broad but mostly mechanical namespace and package-layout migration with updated packaging, tests, and contract coverage. No files require special attention. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - Reviewed the primary contract validation by examining the namespace validation log, which documents the exact commands executed, the working directory, exit codes, pytest output, wheel build output, install output, and import/entrypoint resolution. - Verified the wheel listing by inspecting the wheel listing artifact, confirming the available wheel filenames for the namespace validation. - Ran and inspected the isolated import/entrypoint validation harness saved as package-installed-import-check.py to validate import resolution and entrypoint exposure. - Captured and noted the wheel filename record in package-wheel-filename.txt to enable traceability of the observed artifact. <a href="https://app.greptile.com/trex/runs/13810533/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | pyproject.toml | Updates packaging to build the single `src/free_claude_code` package and retargets console scripts to namespaced modules. | | src/free_claude_code/config/env_template.py | Loads `.env.example` from packaged resources with a source-checkout fallback after the runtime package move. | | src/free_claude_code/cli/entrypoints.py | Updates CLI entrypoint imports to `free_claude_code.*` and continues to use the shared env template loader. | | src/free_claude_code/api/routes.py | Retargets API route dependencies and handlers to the namespaced package without changing route behavior. | | src/free_claude_code/api/app.py | Updates app factory imports to the namespaced package while preserving middleware, routers, and exception handling. | | src/free_claude_code/providers/runtime/factory.py | Updates lazy provider factory imports to `free_claude_code.providers.*` under the new package layout. | | tests/contracts/test_import_boundaries.py | Adds contract coverage requiring runtime packages to live under `src/free_claude_code` and rejecting old top-level imports. | | smoke/lib/child_process.py | Updates smoke child-process helpers to import CLI entrypoints from the namespaced package. | | README.md | Updates the project layout and extension guidance to refer to `src/free_claude_code` and importable `free_claude_code.*` modules. | | uv.lock | Reflects the package version bump associated with the runtime packaging move. | </details> <details open><summary><h3>Sequence Diagram</h3></summary> <a href="#gh-light-mode-only"> ```mermaid %%{init: {'theme': 'neutral'}}%% sequenceDiagram participant User as User / CLI participant Script as Console script participant Pkg as free_claude_code package participant API as free_claude_code.api participant Runtime as free_claude_code.providers.runtime participant Provider as Provider adapter User->>Script: run fcc-server / free-claude-code Script->>Pkg: load free_claude_code.cli.entrypoints:serve Pkg->>API: create FastAPI app and routes API->>Runtime: resolve configured provider Runtime->>Provider: instantiate namespaced adapter Provider-->>Runtime: stream/model responses Runtime-->>API: provider result API-->>User: Anthropic/OpenAI-compatible response ``` </a> <a href="#gh-dark-mode-only"> ```mermaid %%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant User as User / CLI participant Script as Console script participant Pkg as free_claude_code package participant API as free_claude_code.api participant Runtime as free_claude_code.providers.runtime participant Provider as Provider adapter User->>Script: run fcc-server / free-claude-code Script->>Pkg: load free_claude_code.cli.entrypoints:serve Pkg->>API: create FastAPI app and routes API->>Runtime: resolve configured provider Runtime->>Provider: instantiate namespaced adapter Provider-->>Runtime: stream/model responses Runtime-->>API: provider result API-->>User: Anthropic/OpenAI-compatible response ``` </a> </details> <sub>Reviews (2): Last reviewed commit: ["Fix documented package import paths"](https://github.com/alishahryar1/free-claude-code/commit/bfa9f2704c45f3684da39657d5e13f3814e5d450) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=42950471)</sub> <!-- /greptile_comment -->
198 行
5.8 KiB
Python
198 行
5.8 KiB
Python
from free_claude_code.messaging.event_parser import parse_cli_event
|
|
|
|
|
|
def test_parse_cli_event_assistant_content():
|
|
event = {
|
|
"type": "assistant",
|
|
"message": {
|
|
"content": [
|
|
{"type": "thinking", "thinking": "Internal thought"},
|
|
{"type": "text", "text": "Hello user"},
|
|
]
|
|
},
|
|
}
|
|
results = parse_cli_event(event)
|
|
assert len(results) == 2
|
|
assert results[0] == {"type": "thinking_chunk", "text": "Internal thought"}
|
|
assert results[1] == {"type": "text_chunk", "text": "Hello user"}
|
|
|
|
|
|
def test_parse_cli_event_assistant_tools():
|
|
event = {
|
|
"type": "assistant",
|
|
"message": {
|
|
"content": [{"type": "tool_use", "name": "ls", "input": {"path": "."}}]
|
|
},
|
|
}
|
|
results = parse_cli_event(event)
|
|
assert len(results) == 1
|
|
assert results[0]["type"] == "tool_use"
|
|
assert results[0]["name"] == "ls"
|
|
assert results[0]["input"] == {"path": "."}
|
|
|
|
|
|
def test_parse_cli_event_assistant_subagent():
|
|
event = {
|
|
"type": "assistant",
|
|
"message": {
|
|
"content": [
|
|
{
|
|
"type": "tool_use",
|
|
"name": "Task",
|
|
"input": {"description": "Fix bug"},
|
|
}
|
|
]
|
|
},
|
|
}
|
|
results = parse_cli_event(event)
|
|
assert len(results) == 1
|
|
assert results[0]["type"] == "tool_use"
|
|
assert results[0]["name"] == "Task"
|
|
assert results[0]["input"] == {"description": "Fix bug"}
|
|
|
|
|
|
def test_parse_cli_event_content_block_delta():
|
|
# Text delta
|
|
event_text = {
|
|
"type": "content_block_delta",
|
|
"index": 0,
|
|
"delta": {"type": "text_delta", "text": " more"},
|
|
}
|
|
results_text = parse_cli_event(event_text)
|
|
assert results_text == [{"type": "text_delta", "index": 0, "text": " more"}]
|
|
|
|
# Thinking delta
|
|
event_think = {
|
|
"type": "content_block_delta",
|
|
"index": 1,
|
|
"delta": {"type": "thinking_delta", "thinking": " more thought"},
|
|
}
|
|
results_think = parse_cli_event(event_think)
|
|
assert results_think == [
|
|
{"type": "thinking_delta", "index": 1, "text": " more thought"}
|
|
]
|
|
|
|
|
|
def test_parse_cli_event_content_block_start():
|
|
event = {
|
|
"type": "content_block_start",
|
|
"index": 2,
|
|
"content_block": {
|
|
"type": "tool_use",
|
|
"name": "Task",
|
|
"input": {"description": "deploy"},
|
|
},
|
|
}
|
|
results = parse_cli_event(event)
|
|
assert results == [
|
|
{
|
|
"type": "tool_use_start",
|
|
"index": 2,
|
|
"id": "",
|
|
"name": "Task",
|
|
"input": {"description": "deploy"},
|
|
}
|
|
]
|
|
|
|
|
|
def test_parse_cli_event_error():
|
|
event = {"type": "error", "error": {"message": "something failed"}}
|
|
results = parse_cli_event(event)
|
|
assert results == [{"type": "error", "message": "something failed"}]
|
|
|
|
|
|
def test_parse_cli_event_user_tool_result():
|
|
event = {
|
|
"type": "user",
|
|
"message": {
|
|
"content": [
|
|
{
|
|
"type": "tool_result",
|
|
"tool_use_id": "tool_1",
|
|
"content": "ok",
|
|
"is_error": False,
|
|
}
|
|
]
|
|
},
|
|
}
|
|
results = parse_cli_event(event)
|
|
assert results == [
|
|
{
|
|
"type": "tool_result",
|
|
"tool_use_id": "tool_1",
|
|
"content": "ok",
|
|
"is_error": False,
|
|
}
|
|
]
|
|
|
|
|
|
def test_parse_cli_event_exit_success():
|
|
event = {"type": "exit", "code": 0}
|
|
results = parse_cli_event(event)
|
|
assert results == [{"type": "complete", "status": "success"}]
|
|
|
|
|
|
def test_parse_cli_event_exit_failure():
|
|
event = {"type": "exit", "code": 1, "stderr": "fatal error"}
|
|
results = parse_cli_event(event)
|
|
assert results == [
|
|
{
|
|
"type": "error",
|
|
"message": "fatal error",
|
|
"source": "exit",
|
|
"exit_code": 1,
|
|
}
|
|
]
|
|
|
|
|
|
def test_parse_cli_event_invalid_input():
|
|
assert parse_cli_event(None) == []
|
|
assert parse_cli_event("not a dict") == []
|
|
assert parse_cli_event({"type": "unknown"}) == []
|
|
|
|
|
|
def test_parse_cli_event_system_ignored():
|
|
assert parse_cli_event({"type": "system", "foo": "bar"}) == []
|
|
|
|
|
|
def test_parse_cli_event_result_with_content_directly():
|
|
event = {"type": "result", "content": [{"type": "text", "text": "hi"}]}
|
|
assert parse_cli_event(event) == [{"type": "text_chunk", "text": "hi"}]
|
|
|
|
|
|
def test_parse_cli_event_result_with_result_content_directly():
|
|
event = {"type": "result", "result": {"content": [{"type": "text", "text": "hi"}]}}
|
|
assert parse_cli_event(event) == [{"type": "text_chunk", "text": "hi"}]
|
|
|
|
|
|
def test_parse_cli_event_content_block_unknown_type_skipped():
|
|
"""Content block with unknown type is skipped; known blocks still parsed."""
|
|
event = {
|
|
"type": "assistant",
|
|
"message": {
|
|
"content": [
|
|
{"type": "text", "text": "visible"},
|
|
{"type": "unknown", "data": "ignored"},
|
|
{"type": "thinking", "thinking": "thought"},
|
|
]
|
|
},
|
|
}
|
|
results = parse_cli_event(event)
|
|
assert len(results) == 2
|
|
assert results[0] == {"type": "text_chunk", "text": "visible"}
|
|
assert results[1] == {"type": "thinking_chunk", "text": "thought"}
|
|
|
|
|
|
def test_parse_cli_event_error_non_dict():
|
|
"""Error event with error as string (not dict) is handled."""
|
|
event = {"type": "error", "error": "plain string error"}
|
|
results = parse_cli_event(event)
|
|
assert results == [{"type": "error", "message": "plain string error"}]
|
|
|
|
|
|
def test_parse_cli_event_exit_code_none():
|
|
"""Exit event with no code defaults to success."""
|
|
event = {"type": "exit"}
|
|
results = parse_cli_event(event)
|
|
assert results == [{"type": "complete", "status": "success"}]
|