项目文件夹

文件
Ali Khokhar 71a78a0c5a Move runtime packages under src namespace (#1029)
## 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 -->
2026-07-09 01:19:05 -07:00

362 行
12 KiB
Python

from free_claude_code.messaging.rendering.telegram_markdown import (
escape_md_v2,
escape_md_v2_code,
mdv2_bold,
mdv2_code_inline,
render_markdown_to_mdv2,
)
from free_claude_code.messaging.transcript import RenderCtx, TranscriptBuffer
from free_claude_code.messaging.transcript.renderer import render_segments
from free_claude_code.messaging.transcript.segments import Segment, SubagentSegment
from free_claude_code.messaging.transcript.subagents import SubagentState
def _ctx() -> RenderCtx:
return RenderCtx(
bold=mdv2_bold,
code_inline=mdv2_code_inline,
escape_code=escape_md_v2_code,
escape_text=escape_md_v2,
render_markdown=render_markdown_to_mdv2,
thinking_tail_max=1000,
tool_input_tail_max=1200,
tool_output_tail_max=1600,
text_tail_max=2000,
)
def test_transcript_order_thinking_tool_text():
t = TranscriptBuffer()
t.apply({"type": "thinking_chunk", "text": "think1"})
t.apply({"type": "tool_use", "id": "tool_1", "name": "ls", "input": {"path": "."}})
t.apply({"type": "text_chunk", "text": "done"})
out = t.render(_ctx(), limit_chars=3900, status=None)
assert out.find("think1") < out.find("Tool call:") < out.find("done")
def test_transcript_can_hide_tool_results():
t = TranscriptBuffer(show_tool_results=False)
t.apply({"type": "tool_use", "id": "tool_1", "name": "ls", "input": {"path": "."}})
t.apply({"type": "tool_result", "tool_use_id": "tool_1", "content": "secret"})
out = t.render(_ctx(), limit_chars=3900, status=None)
assert "Tool call:" in out
assert "Tool result:" not in out
assert "secret" not in out
def test_transcript_subagent_suppresses_thinking_and_text_inside():
t = TranscriptBuffer()
# Enter subagent context (Task tool call).
t.apply(
{
"type": "tool_use",
"id": "task_1",
"name": "Task",
"input": {"description": "Fix bug"},
}
)
# These should be suppressed while inside subagent context.
t.apply({"type": "thinking_delta", "index": -1, "text": "secret"})
t.apply({"type": "text_chunk", "text": "visible?"})
# Tool activity should still show.
t.apply({"type": "tool_use", "id": "tool_2", "name": "ls", "input": {"path": "."}})
t.apply({"type": "tool_result", "tool_use_id": "tool_2", "content": "x"})
# Close subagent context (Task tool result).
t.apply({"type": "tool_result", "tool_use_id": "task_1", "content": "done"})
# Now text should show again.
t.apply({"type": "text_chunk", "text": "after"})
out = t.render(_ctx(), limit_chars=3900, status=None)
assert "Subagent:" in out
assert "secret" not in out
assert "visible?" not in out
# Only the current tool call should be shown (not the full history).
assert out.count("Tool call:") == 1
assert "\n 🛠" in out or out.startswith(" 🛠") or " 🛠" in out
assert "Tools used:" in out
assert "Tool calls:" in out
assert "after" in out
def test_transcript_subagent_closes_on_whitespace_tool_ids():
t = TranscriptBuffer()
# Provider emitted a Task tool_use id with leading whitespace.
t.apply(
{
"type": "tool_use",
"id": " functions.Task:0",
"name": "Task",
"input": {"description": "Outer"},
}
)
# Task completes, but tool_result references a trimmed id (or vice versa).
t.apply(
{"type": "tool_result", "tool_use_id": "functions.Task:0", "content": "done"}
)
# Next Task should be top-level, not nested under the previous subagent.
t.apply(
{
"type": "tool_use",
"id": "functions.Task:1",
"name": "Task",
"input": {"description": "Next"},
}
)
out = t.render(_ctx(), limit_chars=3900, status=None)
assert out.count("Subagent:") == 2
# If nesting is incorrect, the second subagent line will be indented under the first.
assert "\n 🤖 *Subagent:* `Next`" not in out
def test_transcript_subagent_closes_on_task_result_id_suffix_match():
t = TranscriptBuffer()
t.apply(
{
"type": "tool_use",
"id": "task_1",
"name": "Task",
"input": {"description": "Outer"},
}
)
t.apply({"type": "tool_result", "tool_use_id": "task_1_result", "content": "done"})
t.apply(
{
"type": "tool_use",
"id": "task_2",
"name": "Task",
"input": {"description": "Next"},
}
)
out = t.render(_ctx(), limit_chars=3900, status=None)
assert out.count("Subagent:") == 2
assert "\n 🤖 *Subagent:* `Next`" not in out
def test_transcript_unmatched_non_task_tool_result_does_not_pop_subagent():
state = SubagentState()
state.push("task_1", SubagentSegment("Outer"))
assert not state.close_for_tool_result("totally_unrelated", tool_name=None)
assert state.open_ids == ("task_1",)
def test_transcript_sequential_tasks_mismatched_results_no_depth_drift():
t = TranscriptBuffer()
t.apply(
{
"type": "tool_use",
"id": "task_1",
"name": "Task",
"input": {"description": "A"},
}
)
t.apply({"type": "tool_result", "tool_use_id": "task_1_result", "content": "done"})
t.apply(
{
"type": "tool_use",
"id": "task_2",
"name": "Task",
"input": {"description": "B"},
}
)
t.apply({"type": "tool_result", "tool_use_id": "task_2_result", "content": "done"})
t.apply(
{
"type": "tool_use",
"id": "task_3",
"name": "Task",
"input": {"description": "C"},
}
)
t.apply({"type": "text_chunk", "text": "still hidden inside task three"})
out = t.render(_ctx(), limit_chars=3900, status=None)
assert "🤖 *Subagent:* `A`\n 🤖 *Subagent:* `B`" not in out
assert "\n 🤖 *Subagent:* `C`" not in out
assert "still hidden inside task three" not in out
def test_transcript_synthetic_task_start_closes_on_functions_task_result_id():
t = TranscriptBuffer()
t.apply(
{
"type": "tool_use_start",
"index": 0,
"id": "",
"name": "Task",
"input": {"description": "Outer"},
}
)
t.apply({"type": "tool_result", "tool_use_id": "functions.Task:0", "content": "x"})
t.apply(
{
"type": "tool_use_start",
"index": 1,
"id": "",
"name": "Task",
"input": {"description": "Next"},
}
)
out = t.render(_ctx(), limit_chars=3900, status=None)
assert out.count("Subagent:") == 2
assert "\n 🤖 *Subagent:* `Next`" not in out
def test_transcript_synthetic_task_not_closed_by_unknown_non_task_result_id():
t = TranscriptBuffer()
t.apply(
{
"type": "tool_use_start",
"index": 0,
"id": "",
"name": "Task",
"input": {"description": "Outer"},
}
)
t.apply({"type": "tool_result", "tool_use_id": "call_deadbeef", "content": "x"})
t.apply({"type": "text_chunk", "text": "hidden while synthetic task is open"})
out = t.render(_ctx(), limit_chars=3900, status=None)
assert "hidden while synthetic task is open" not in out
def test_transcript_overlapping_tasks_are_flat_not_nested():
t = TranscriptBuffer()
t.apply(
{
"type": "tool_use",
"id": "task_a",
"name": "Task",
"input": {"description": "A"},
}
)
t.apply(
{
"type": "tool_use",
"id": "task_b",
"name": "Task",
"input": {"description": "B"},
}
)
t.apply({"type": "tool_result", "tool_use_id": "task_b", "content": "done"})
t.apply({"type": "tool_result", "tool_use_id": "task_a", "content": "done"})
out = t.render(_ctx(), limit_chars=3900, status=None)
assert "🤖 *Subagent:* `A`" in out
assert "🤖 *Subagent:* `B`" in out
assert out.find("🤖 *Subagent:* `A`") < out.find("🤖 *Subagent:* `B`")
assert "\n 🤖 *Subagent:* `B`" not in out
def test_transcript_truncates_by_dropping_oldest_segments():
t = TranscriptBuffer()
# Create many segments by opening/closing distinct text blocks.
for i in range(60):
t.apply({"type": "text_start", "index": i})
t.apply(
{"type": "text_delta", "index": i, "text": f"segment_{i} " + ("x" * 120)}
)
t.apply({"type": "block_stop", "index": i})
out = t.render(_ctx(), limit_chars=600, status="status")
assert escape_md_v2("... (truncated)") in out
# We keep the tail and drop the oldest segments when truncating.
assert escape_md_v2("segment_59") in out
assert escape_md_v2("segment_0") not in out
def test_transcript_render_many_segments_completes_quickly():
"""Render with 200+ segments exercises O(n) truncation (deque popleft)."""
t = TranscriptBuffer()
for i in range(200):
t.apply({"type": "text_start", "index": i})
t.apply({"type": "text_delta", "index": i, "text": f"seg_{i} " + ("y" * 80)})
t.apply({"type": "block_stop", "index": i})
out = t.render(_ctx(), limit_chars=500, status="ok")
assert escape_md_v2("... (truncated)") in out
assert "199" in out # last segment (MarkdownV2 escapes underscores)
assert "seg_0 " not in out # oldest segment dropped
def test_transcript_reused_index_closes_previous_open_block():
t = TranscriptBuffer()
# Open a text block at index 0, but never close it.
t.apply({"type": "text_start", "index": 0})
t.apply({"type": "text_delta", "index": 0, "text": "alpha visible"})
# Provider reuses index 0 for a new tool block without a stop.
t.apply(
{"type": "tool_use_start", "index": 0, "id": "t1", "name": "ls", "input": {}}
)
t.apply({"type": "text_delta", "index": 0, "text": "omega visible"})
out = t.render(_ctx(), limit_chars=3900, status=None)
assert out.find("alpha") < out.find("Tool call:") < out.find("omega")
def test_transcript_render_segment_exception_skipped():
"""When a segment's render() raises, that segment is skipped and rest is rendered."""
class StaticSegment(Segment):
def __init__(self, text: str) -> None:
super().__init__(kind="static")
self._text = text
def render(self, ctx: RenderCtx) -> str:
return self._text
class BrokenSegment(Segment):
def __init__(self) -> None:
super().__init__(kind="broken")
def render(self, ctx: RenderCtx) -> str:
raise ValueError("render failed")
out = render_segments(
[StaticSegment("before"), BrokenSegment(), StaticSegment("after")],
_ctx(),
limit_chars=3900,
status=None,
)
assert "before" in out
assert "after" in out
def test_transcript_render_status_only_exceeds_limit():
"""When all segments dropped, status-only output; long status returned as-is."""
t = TranscriptBuffer()
t.apply({"type": "text_chunk", "text": "x" * 5000})
long_status = "A" * 500
msg = t.render(_ctx(), limit_chars=100, status=long_status)
assert "... (truncated)" in msg or long_status in msg
def test_transcript_truncation_preserves_last_segment_tail():
"""When all segments exceed limit, preserve tail of last segment (not just marker+status)."""
t = TranscriptBuffer()
t.apply({"type": "thinking_chunk", "text": "Thinking..."})
t.apply(
{"type": "text_chunk", "text": "The actual output content here" + "x" * 500}
)
msg = t.render(_ctx(), limit_chars=100, status="✅ *Complete*")
# Must include actual content (tail of last segment), not only "... (truncated)\n✅ *Complete*"
assert escape_md_v2("... (truncated)") in msg
assert "✅ *Complete*" in msg
assert "actual output" in msg or "content" in msg or "x" in msg