Compare commits

..

No commits in common. "baf8837b5e17b6bf79b3a68a611eae486e8d7d1e" and "de0fad496cc7706fe6668986667afaa38946c2bf" have entirely different histories.

17 changed files with 147 additions and 1897 deletions

View File

@ -5,6 +5,3 @@ api_key = "sk-YO39YO8cIfGekojKR5prka8YcitpGUxDUFeo5Fogp1msvrpe"
base_url = "https://api.118229.xyz/v1"
# 可选:最大工具循环次数
max_turns = 12
LANGFUSE_SECRET_KEY="sk-lf-48f1ba27-c53d-443a-ab78-6db7735fd33e"
LANGFUSE_PUBLIC_KEY="pk-lf-b9431acc-b0a6-494f-a9d3-ce8ac4ea33ba"
LANGFUSE_BASE_URL="http://10.109.59.251:3000"

View File

@ -1,42 +1,73 @@
# AGENTS.md
这是当前 workspace 的项目级规则文件
你是 `cc-slim`,一个只能基于当前仓库与用户输入行动的本地极简代理
## 工作区规则
## 行动边界
- 当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。
- `AGENTS.md`、`SKILLS/` 与代码文件都属于当前 workspace 输入,不属于程序内置 prompt。
- 行动时必须以运行时注入的环境信息为准特别是平台、shell、workspace 和可用工具列表。
- 对运行时已明确注入的信息,默认直接使用;除非用户明确要求验证,否则不要再次用 Bash 查询当前目录、平台或类似已知状态。
- 当前 harness 是极简实现,优先最小动作,不做不必要的重复试错。
- 你只能依据以下信息行动:
- 当前仓库中的文件
- 当前用户输入
- 工具返回的结果
- 不要假设任何尚未看到的文件、命令、接口、配置或能力存在。
- 信息缺失时,采用最小默认策略,并在最终回答里简短说明该默认策略。
## 交互规则
## 默认策略
- REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
- REPL 会显示当前模式提示符,例如 `build >``plan >`
- `/clear` 是清空当前上下文的主别名,等价于 `/new`
当用户请求处理仓库内任务时,按以下最小闭环执行:
## 模式与权限
1. 先判断运行时状态、已有上下文和当前用户输入是否已经足以决定下一步。
2. 只有在减少不确定性或执行动作确有必要时,才检查最相关的文件或调用工具。
3. 选择最小的下一步动作。
4. 使用工具后重新判断结果。
5. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
- 支持 `/mode build``/mode plan` 两种模式,`plan` 为只读规划模式。
- 写操作和 Bash 默认需要确认,可通过 `--auto-approve``/permissions auto-on` 跳过。
- 工作区外访问和 `plan` 模式限制属于硬性边界,不通过审批放行。
当前 harness 是极简实现,优先最小动作,不做不必要的重复试错。
## Memory 与验证
行动时必须以运行时注入的环境信息为准特别是平台、shell、工作目录和可用工具列表。
- `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。
- 当前项目支持结构化 memory使用 `/remember` 保存长期知识,使用 `/memory` 查看。
- `/dream` 的职责是从当前 session 提炼长期知识并更新 memory不属于项目规则文件本身。
- session 是原始对话历史,不直接拼进 system promptmemory 才作为长期补充进入 prompt。
- 默认语言:中文优先。
- 默认验证:优先做最小可验证检查,不夸大成功状态。
当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。
## 工具偏好
REPL 内的会话管理命令优先使用 slash command。
REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
当前项目支持最小 memory使用 `/remember` 保存长期信息,使用 `/memory` 查看。
写操作和 Bash 默认需要确认,可通过 `--auto-approve``/permissions auto-on` 跳过。
支持 `/mode build``/mode plan` 两种模式,`plan` 为只读规划模式。
未明确说明时,使用以下默认值:
- 工作目录:当前进程目录
- 文件编码:`utf-8`
- Shell 执行:按原样执行单条命令
- 输出风格:简洁、直接
- 路径不明确:先检查再操作
- 需求有歧义:采用仍能推进任务的最窄解释
## 工具使用规则
- `Read`:用于读取文件内容。
- 需要按文件名或路径模式查找时,优先使用 `Glob`
- 需要搜索文件内容时,优先使用 `Grep`
- 修改已有文件内容时,优先使用 `Edit`
- 创建新文件时,优先使用 `Write`
- 只有在确实需要复杂 shell 特性时才使用 `Bash`
- 不要用 `Bash` 拼接文件内容。
- 默认只应在当前 workspace 内读写和执行与项目相关的操作。
- 修改已有文件内容时,优先使用 `Edit` 工具。
- 创建新文件时,优先使用 `Write` 工具。
- `Bash`:用于执行必须通过 shell 完成的最小命令。
- 只有在确实需要复杂 shell 特性时才使用 Bash。
- 不要用 Bash 拼接文件内容。
- Windows 环境下优先使用兼容写法,不默认使用 `cat <<EOF`、`ls -la` 等 Unix 风格写法。
- 同一写入或创建目标,最多尝试 2 种不同 Bash 方案。
- 如果两次 Bash 方案失败,应立即收敛:
- 先检查路径、文件状态、shell 兼容性。
- 如仍不确定,则询问用户,而不是继续盲试。
- 不能虚构工具输出。
- 不能在未验证前声称文件存在、命令成功或修改已生效。
## 回答规则
- 对不确定性保持诚实。
- 需要时引用具体文件或命令。
- 输出保持简短直接,不夸大成功状态。
- 若受阻,只询问当前缺失的关键信息。

View File

@ -1,224 +0,0 @@
# 架构说明
本文档解释当前代码中真实存在的 `cc-slim` 架构。
## 当前已实现
### 1. 入口层
`src/cc_slim/main.py` 提供单一 Typer 命令入口,支持:
- 单次 prompt 执行
- REPL 模式
- 显式 `--workspace`
- history 列表与 session resume
- `--auto-approve` 启动模式
REPL 本身刻意保持很小。slash command 会在普通 agent loop 之前先被拦截。
当前代码里也明确区分两个 root
- program root
- `cc-slim` 自己读取 `.cc-slim.toml` 的位置
- workspace root
- 真正被工具、session、memory、权限与边界逻辑操作的项目目录
### 2. 命令层
`src/cc_slim/commands.py` 是一个很薄的 slash command 路由层。
它负责处理:
- session 命令
- memory 命令
- mode 命令
- permission 命令
它本身不负责模型推理逻辑,而是把动作委托给 store 或当前 `Agent`
### 3. 核心 Agent Loop
`src/cc_slim/engine.py` 包含最小 harness 闭环:
1. 把用户消息追加到内存 history
2. 调模型
3. 流式输出文本 chunk
4. 如果模型请求工具,先做权限检查
5. 执行工具
6. 回填工具结果
7. 循环,直到得到最终回答或达到最大轮数
当前对外有两个主要入口:
- `stream_reply()`
- 给 REPL 使用
- `reply()`
- 兼容包装层,把流式文本拼成完整字符串
### 4. Prompt 组装
system prompt 的层次是刻意分开的:
1. 内置 `system.md`
2. runtime summary
3. workspace `AGENTS.md`
4. workspace `SKILLS/*.md`
5. `Memory`
每一层只负责自己的职责,不混用。
### 5. 工具层
`src/cc_slim/tools.py` 提供一个刻意收窄的工具面:
- `Read`
- `Glob`
- `Grep`
- `Write`
- `Edit`
- `Bash`
设计重点是:工具都是简单的 string-in / string-out primitive。
重要边界:
- 文件类工具通过 `_safe_path()` 保持在 workspace 内
- `Edit``Write` 都是整文件覆盖,不支持局部 patch
- `Grep` 是普通文本搜索,不是 regex 引擎
- `Bash` 可用,但受 mode、permission 与 workspace boundary 控制
### 6. Session 层
`src/cc_slim/session.py` 保存原始对话历史。
Session 的职责是:
- 保存可恢复的消息历史
- 支持 `--history``--resume`
- 保存标题、时间戳等元信息
Session 刻意保持为“原始历史”,而不是长期知识。
### 7. Memory 层
`src/cc_slim/memory.py` 以 Markdown 形式保存结构化长期知识。
当前 sections
- `User Memory`
- `Project Memory`
- `Constraints`
- `Consolidated Facts`
- `Scratch Notes`
Memory 不是 transcript。它是一个能跨 session 持续带入 prompt 的长期知识层。
### 8. Dream 层
Dream v1 当前以手动命令形式存在:
- `/dream`
它只使用当前已加载的 session调用模型提炼长期有价值的信息把结果写回结构化 Memory并刷新内存中的 system prompt。
这很关键,因为 Dream 不是 summary。summary 压缩对话内容,而 Dream 要保留的是未来仍值得记住的知识。
### 9. Mode、Permission、Boundary
当前工具执行受三层控制:
- mode
- permission approval
- workspace boundary
优先级关系很重要:
1. 先做硬阻止
2. 再进入审批
3. 最后才执行工具
当前硬阻止包括:
- `plan` 模式下阻止 `Write`、`Edit`、`Bash`
- 明显指向 workspace 外路径的 `Bash`
只有通过硬阻止检查后,才会进入普通 y/n 审批流程。
## 为什么 Memory 不是 Session
这是整个项目最重要的设计点之一。
Session
- 原始对话和工具轨迹
- 可恢复
- 天然带噪声
- 适合做连续性上下文
Memory
- 结构化长期知识
- 不是“发生过”就保留,而是“值得以后继续记住”才保留
- 规模足够小,可以持续注入 prompt
如果把 Session 和 Memory 混成一个概念,整个 harness 会同时失去清晰性和可控性。
## 为什么 Dream 不是 Summary
Summary 回答的问题是:
- 这次对话发生了什么?
Dream 回答的问题是:
- 这次对话里有哪些内容以后仍值得记住?
所以当前 Dream v1
- 不做 multi-session 汇总
- 不改写 `AGENTS.md`
- 只更新结构化 memory sections
- 保留 `Scratch Notes`
它是一个很小的 consolidation pass而不是 transcript reducer。
## 为什么 AGENTS.md 不再承担系统人设
`system.md` 负责内置 harness 行为。
workspace `AGENTS.md` 负责项目 / 工作区规则。
这样可以把这几类东西拆开:
- 程序内置身份和行为
- 当前项目规则
- 长期用户 / 项目知识
如果不拆开prompt assembly 会越来越难理解,也更难继续演化。
## 当前明确未做的内容
当前架构没有实现:
- sandbox
- sub-agent orchestration
- 向量记忆检索
- 自动 dream 或定时 consolidation
- multi-session dream
- 复杂权限策略
- patch 级编辑工具
- skill execution isolation
这些都是刻意保留在范围之外的内容。
## 未来可选增强
如果项目以后继续扩展,合理的可选方向包括:
- 更稳健的 `Bash` 风险检测,而不是完整 shell 解析
- memory section 质量约束
- 针对选定 session 的 dream
- 更明确的配置来源 / 生效状态展示
- 更细一点的 streaming 边界测试
这些是可选增强,不是当前已经实现的能力。

302
README.md
View File

@ -1,302 +0,0 @@
# cc-slim
`cc-slim` is a deliberately small coding harness in the style of Claude Code / Codex.
It focuses on the core local loop only:
- REPL / CLI entry
- model-driven agentic loop
- streamed text output
- small tool surface
- session persistence
- structured project memory
- manual `/dream` consolidation
- mode / permission / workspace control
The project is intentionally not a full platform. It is built to be readable, easy to explain in an interview, and small enough to reason about end to end.
## What It Actually Implements
- Agentic loop with model -> tool call -> tool execution -> tool result -> final answer
- Streaming REPL output with tool call / tool result status lines
- Built-in tools:
- `Read`
- `Glob`
- `Grep`
- `Write`
- `Edit`
- `Bash`
- Slash command router for session, memory, mode, and permissions
- Session persistence with history / resume
- Structured Memory v2 stored as Markdown
- Manual Dream v1 to consolidate the current session into Memory
- Permission gate for `Write`, `Edit`, and `Bash`
- `build` / `plan` mode
- workspace boundary as the default operating scope
- explicit `--workspace` support
## What It Does Not Implement
- sandboxing
- sub-agents or coordinator routing
- vector database or Mem0 backend
- automatic background dream / consolidation
- multi-session dream
- complex skill execution framework
- fine-grained path allowlists / denylists
Those are intentional omissions, not missing TODOs.
## Installation
```bash
uv sync
```
Set credentials through environment variables or `.cc-slim.toml` in the `cc-slim` program directory.
`--workspace` only changes the target project. It does not change where `cc-slim` reads its own config.
## Quick Start
Run inside the target project directory:
```bash
uv run cc-slim
```
Run from elsewhere but target a workspace explicitly:
```bash
uv run cc-slim --workspace E:\04test
```
Single-shot prompt:
```bash
uv run cc-slim "what does this repo do?"
```
List history for the current workspace:
```bash
uv run cc-slim --history
```
Resume a prior session:
```bash
uv run cc-slim --resume 1
```
Skip confirmation for high-risk tools:
```bash
uv run cc-slim --auto-approve
```
## REPL Commands
Session:
- `/history`
- `/resume <id-or-index>`
- `/new`
- `/clear` as an alias for `/new`
Memory:
- `/memory`
- `/remember <text>`
- `/dream`
Mode:
- `/mode`
- `/mode build`
- `/mode plan`
- `/build`
- `/plan`
Permissions:
- `/permissions`
- `/permissions auto-on`
- `/permissions auto-off`
## Runtime Model
There are two roots in the current code:
- program root
- the `cc-slim` repository itself
- used for reading `.cc-slim.toml`
- workspace root
- the project being operated on
- used for `AGENTS.md`, `SKILLS`, `Session`, `Memory`, permissions, and boundary checks
The system prompt is assembled in this order:
1. `src/cc_slim/system.md`
2. runtime summary
3. workspace `AGENTS.md`
4. workspace `SKILLS/*.md`
5. structured `Memory`
This keeps responsibilities separate:
- `system.md`
- built-in harness behavior
- workspace `AGENTS.md`
- workspace / project rules
- `Memory`
- long-lived project knowledge and user preferences
- `Session`
- raw conversation history
- `Dream`
- manual consolidation from the current session into Memory
## Mermaid Architecture
```mermaid
flowchart TD
U[User] --> CLI[CLI / REPL in main.py]
CLI --> CMD[Slash command router in commands.py]
CLI --> LOOP[Agent loop in engine.py]
CMD --> SESS[SessionStore]
CMD --> MEM[MemoryStore]
CMD --> MODE[ModeState]
CMD --> PERM[PermissionChecker]
CMD --> DREAM[/dream -> Agent.dream/]
SYS[system.md] --> PROMPT[Prompt Assembly]
RT[Runtime Summary] --> PROMPT
AG[workspace AGENTS.md] --> PROMPT
SK[workspace SKILLS/*.md] --> PROMPT
MEM --> PROMPT
PROMPT --> LOOP
SESS --> LOOP
LOOP --> TOOLS[Read / Glob / Grep / Write / Edit / Bash]
PERM --> TOOLS
MODE --> PERM
BOUNDARY[Workspace Boundary] --> PERM
LOOP --> STREAM[Streaming text + tool events]
STREAM --> CLI
DREAM --> MEM
SESS --> CMD
```
## Tools
Current tool boundaries are intentionally narrow:
- `Read`
- reads a text file inside the workspace
- `Glob`
- matches paths inside the workspace
- `Grep`
- plain-text contains search inside the workspace
- not regex-based
- returns up to 20 matches
- `Write`
- create or overwrite a full file
- `Edit`
- overwrite an existing file with full new content
- `Bash`
- runs a shell command with the workspace as `cwd`
There is no partial-edit patch tool, no AST manipulation, and no shell sandbox.
## Modes, Permissions, and Boundary Control
`build` mode is the normal execution mode.
`plan` mode is read-only in practice:
- allowed by default:
- `Read`
- `Glob`
- `Grep`
- hard-blocked:
- `Write`
- `Edit`
- `Bash`
Permission behavior:
- `Read`, `Glob`, `Grep` are auto-allowed
- `Write`, `Edit`, `Bash` require confirmation unless `--auto-approve` or `/permissions auto-on` is enabled
- hard boundaries are checked before approval:
- `plan` mode restrictions
- obvious workspace-external `Bash` targets
## Session, Memory, Dream
Session storage:
- stored under `~/.config/cc-slim/sessions/<workspace>/`
- message history in `.jsonl`
- metadata in `.meta.json`
Memory storage:
- stored under `~/.config/cc-slim/memory/<workspace>/MEMORY.md`
- structured sections:
- `User Memory`
- `Project Memory`
- `Constraints`
- `Consolidated Facts`
- `Scratch Notes`
`/remember`:
- appends raw notes into `Scratch Notes`
`/dream`:
- uses only the currently loaded session
- asks the model to extract durable information
- updates structured Memory sections
- keeps `Scratch Notes` intact
This is consolidation, not chat summary.
## Why This Project Is Intentionally Small
This repository is meant to demonstrate the core harness mechanics without hiding them behind infrastructure.
It intentionally does not include:
- sandbox
- because the project is about control flow and boundary handling, not process isolation
- sub-agent / coordinator orchestration
- because a single readable loop is easier to explain and verify
- vector DB / Mem0 backend
- because Memory here is file-based and explicit by design
- automatic background dream
- because manual consolidation makes the memory boundary easier to reason about
- complex skill runtime
- because workspace `SKILLS/*.md` is enough to show prompt layering without building a plugin platform
The result is smaller than production systems, but much easier to inspect, demo, and discuss.
## Testing
Run the current test suite:
```bash
uv run pytest -q
```
Current tests cover:
- config resolution
- tool primitives
- structured memory behavior
- dream updates
- prompt assembly order

View File

@ -1,311 +0,0 @@
# cc-slim
`cc-slim` 是一个刻意保持极简的 coding harness风格接近 Claude Code / Codex。
它只聚焦本地 coding agent 的最小核心闭环:
- CLI / REPL 入口
- 模型驱动的 agentic loop
- 流式文本输出
- 小而清晰的工具面
- session 持久化
- 结构化长期记忆
- 手动 `/dream` consolidation
- mode / permission / workspace 控制面
这个项目不是一个“大而全的平台”。它的目标是:
- 可读
- 可讲解
- 适合写进简历
- 足够小,能从头到尾讲清楚
## 当前真实已实现的能力
- 模型 -> 工具调用 -> 工具执行 -> 结果回填 -> 最终回答 的 agentic loop
- 带工具状态提示的 streaming REPL
- 内置工具:
- `Read`
- `Glob`
- `Grep`
- `Write`
- `Edit`
- `Bash`
- 用于 session / memory / mode / permission 的 slash command 路由
- session 持久化、history、resume
- 基于 Markdown 的结构化 Memory v2
- 手动 `/dream`,把当前 session 提炼进 Memory
- `Write` / `Edit` / `Bash` 的 permission gate
- `build` / `plan` 两种模式
- 以 workspace 为默认边界
- 显式 `--workspace` 支持
## 当前明确没有实现的内容
- sandbox
- sub-agent / coordinator
- 向量数据库或 Mem0 backend
- 自动后台 dream / consolidation
- multi-session dream
- 复杂技能执行框架
- 细粒度路径白名单 / 黑名单
这些是刻意不做的取舍,不是“以后补完才算完整”。
## 安装
```bash
uv sync
```
凭证可以通过环境变量提供,也可以通过 `cc-slim` 程序目录下的 `.cc-slim.toml` 提供。
## 快速开始
在目标项目目录中直接运行:
```bash
uv run cc-slim
```
从其他目录运行,但显式指定工作区:
```bash
uv run cc-slim --workspace E:\04test
```
单次提问:
```bash
uv run cc-slim "what does this repo do?"
```
查看当前 workspace 的历史 session
```bash
uv run cc-slim --history
```
恢复历史 session
```bash
uv run cc-slim --resume 1
```
跳过高风险工具确认:
```bash
uv run cc-slim --auto-approve
```
`--workspace` 只影响操作目标项目,不影响 `cc-slim` 自身配置的读取位置。
## REPL 命令
会话命令:
- `/history`
- `/resume <id-or-index>`
- `/new`
- `/clear`,作为 `/new` 的别名
Memory 命令:
- `/memory`
- `/remember <text>`
- `/dream`
模式命令:
- `/mode`
- `/mode build`
- `/mode plan`
- `/build`
- `/plan`
权限命令:
- `/permissions`
- `/permissions auto-on`
- `/permissions auto-off`
## 运行时模型
当前代码里实际上区分了两个 root
- program root
- `cc-slim` 自己的程序仓库
- 用于读取 `.cc-slim.toml`
- workspace root
- 当前实际要操作的项目目录
- 用于 `AGENTS.md`、`SKILLS`、`Session`、`Memory`、权限与边界控制
system prompt 的组装顺序是:
1. `src/cc_slim/system.md`
2. runtime summary
3. workspace `AGENTS.md`
4. workspace `SKILLS/*.md`
5. 结构化 `Memory`
这几层职责分别是:
- `system.md`
- 程序内置的基础行为
- workspace `AGENTS.md`
- 当前项目 / 工作区规则
- `Memory`
- 长期项目知识与用户偏好
- `Session`
- 原始对话历史
- `Dream`
- 把当前 session 手动整理进 Memory
## Mermaid 架构图
```mermaid
flowchart TD
U[User] --> CLI[CLI / REPL in main.py]
CLI --> CMD[Slash command router in commands.py]
CLI --> LOOP[Agent loop in engine.py]
CMD --> SESS[SessionStore]
CMD --> MEM[MemoryStore]
CMD --> MODE[ModeState]
CMD --> PERM[PermissionChecker]
CMD --> DREAM[/dream -> Agent.dream/]
SYS[system.md] --> PROMPT[Prompt Assembly]
RT[Runtime Summary] --> PROMPT
AG[workspace AGENTS.md] --> PROMPT
SK[workspace SKILLS/*.md] --> PROMPT
MEM --> PROMPT
PROMPT --> LOOP
SESS --> LOOP
LOOP --> TOOLS[Read / Glob / Grep / Write / Edit / Bash]
PERM --> TOOLS
MODE --> PERM
BOUNDARY[Workspace Boundary] --> PERM
LOOP --> STREAM[Streaming text + tool events]
STREAM --> CLI
DREAM --> MEM
SESS --> CMD
```
## 工具层
当前工具边界是刻意收窄的:
- `Read`
- 读取 workspace 内文本文件
- `Glob`
- 在 workspace 内按路径模式查找文件
- `Grep`
- 在 workspace 内做普通文本包含搜索
- 不是正则搜索
- 最多返回 20 条结果
- `Write`
- 创建或覆盖完整文件
- `Edit`
- 用完整新内容覆盖已有文件
- `Bash`
- 以 workspace 为 `cwd` 执行 shell 命令
当前没有:
- patch 级局部编辑工具
- AST 编辑
- shell sandbox
## 模式、权限与边界控制
`build` 是正常执行模式。
`plan` 在当前实现中是只读模式:
- 默认允许:
- `Read`
- `Glob`
- `Grep`
- 硬性阻止:
- `Write`
- `Edit`
- `Bash`
权限行为:
- `Read`、`Glob`、`Grep` 自动允许
- `Write`、`Edit`、`Bash` 需要确认,除非使用 `--auto-approve``/permissions auto-on`
- 硬边界优先于审批:
- `plan` 模式限制
- 明显指向 workspace 外路径的 `Bash`
## Session、Memory、Dream
Session
- 存在 `~/.config/cc-slim/sessions/<workspace>/`
- 消息历史为 `.jsonl`
- 元信息为 `.meta.json`
Memory
- 存在 `~/.config/cc-slim/memory/<workspace>/MEMORY.md`
- 当前结构化 sections 为:
- `User Memory`
- `Project Memory`
- `Constraints`
- `Consolidated Facts`
- `Scratch Notes`
`/remember`
- 把原始笔记追加到 `Scratch Notes`
`/dream`
- 只使用当前已加载的 session
- 调模型提炼长期有效信息
- 回写结构化 Memory sections
- 保留 `Scratch Notes`
它是 consolidation不是聊天 summary。
## Why this project is intentionally small
这个仓库的目标是展示 harness 的核心机制,而不是用更多基础设施把问题“藏起来”。
因此它刻意没有做:
- sandbox
- 因为当前重点是控制流、权限和边界,而不是进程隔离
- sub-agent / coordinator
- 因为单 agent 闭环更容易讲清楚,也更容易验证
- vector DB / Mem0 backend
- 因为当前 Memory 刻意保持文件化、可读、可控
- 自动后台 dream
- 因为手动 consolidation 更容易清楚地区分 Session 与 Memory
- 复杂技能系统
- 因为 `SKILLS/*.md` 已足够展示 prompt layering而不需要引入插件平台
结果是它比生产系统小很多,但也更容易读、演示和讲解。
## 测试
运行测试:
```bash
uv run pytest -q
```
当前测试覆盖:
- 配置解析
- 工具原语
- 结构化 memory 行为
- dream 更新行为
- prompt 分层顺序

View File

@ -4,11 +4,10 @@
## 推荐流程
1. 先使用当前上下文中已经明确给出的信息。
2. 如仍不确定,再读取最接近问题的文件。
3. 路径不明确时,用 `Glob` 定位。
4. 只有确实需要执行命令时,才用 `Bash`
5. 只汇报与用户目标直接相关的信息。
1. 先读取最接近问题的文件。
2. 路径不明确时,用 `Glob` 定位。
3. 必须执行命令时,用 `Bash`
4. 只汇报与用户目标直接相关的信息。
## 实用启发
@ -25,14 +24,10 @@
- 修改已有文件时,优先使用 `Edit`,而不是 `Bash``Write`
- 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash`
- 会话与 memory 管理优先使用 slash command而不是自然语言或 `Bash` 探查对应文件
- 优先通过 `/help` 查看当前命令,而不是猜测命令格式
- 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存
- 项目规则和工作方式仍应写在 `AGENTS.md`
- 当前 session 中出现了值得长期保留的信息时,可使用 `/dream` 进行整理
- `/remember` 适合手动记一条,`/dream` 适合系统性整理当前 session
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存
- 高风险操作会触发确认,优先先读再改,减少无意义审批
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
- workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索
- 先在 workspace 内定位和操作,避免无关路径探索
- Windows 下先验证 shell 兼容性,再选择命令写法
## 沟通风格

View File

@ -10,7 +10,6 @@ readme = "AGENTS.md"
requires-python = ">=3.11"
dependencies = [
"anthropic>=0.93.0",
"langfuse>=2.60.0",
"openai>=1.76.0",
"rich>=14.3.3",
"typer>=0.24.1",

View File

@ -42,55 +42,45 @@ def handle_command(
args = parsed["args"]
if name == "/help":
console.print("会话命令")
console.print(" /history 查看当前工作目录的历史会话")
console.print(" /resume <id-or-index> 恢复指定会话")
console.print(" /new 创建新会话")
console.print(" /clear /new 的别名,用于清空当前上下文")
console.print("")
console.print("Memory 命令")
console.print(" /memory 查看当前项目 memory")
console.print(" /remember <text> 追加保存长期信息")
console.print(" /dream 从当前 session 整理长期 memory")
console.print("")
console.print("模式命令")
console.print(" /mode 查看当前模式")
console.print(" /mode build 切换到 build 模式")
console.print(" /mode plan 切换到 plan 模式")
console.print(" /build /mode build 的简写")
console.print(" /plan /mode plan 的简写")
console.print("")
console.print("权限命令")
console.print(" /permissions 查看当前权限状态")
console.print(" /permissions auto-on 开启自动批准")
console.print(" /permissions auto-off 关闭自动批准")
console.print("")
console.print("退出方式")
console.print(" exit / quit 退出 REPL")
console.print("/help - 显示命令帮助")
console.print("/clear - 清空当前上下文并创建新 session")
console.print("/history - 查看历史 session")
console.print("/resume <id-or-index> - 恢复指定 session")
console.print("/new - 创建全新 session")
console.print("/remember <text> - 保存长期 memory")
console.print("/memory - 查看当前项目 memory")
console.print("/mode - 查看当前模式")
console.print("/mode build - 切换到 build 模式")
console.print("/mode plan - 切换到 plan 模式")
console.print("/build - 切换到 build 模式")
console.print("/plan - 切换到 plan 模式")
console.print("/permissions - 查看当前权限状态")
console.print("/permissions auto-on - 开启自动批准")
console.print("/permissions auto-off - 关闭自动批准")
return agent
if name in {"/clear", "/new"}:
session_meta = store.create_session(config.model)
console.print(f"已创建新会话: {session_meta.get('session_id')}")
console.print(f"已创建新 session: {session_meta.get('session_id')}")
return build_agent(root, config, store, permissions, session_meta, [])
if name in {"/build", "/plan"}:
target_mode = "build" if name == "/build" else "plan"
mode.set_mode(target_mode)
permissions.set_mode(target_mode)
console.print(f"模式已切换为: {target_mode}")
console.print(f"当前模式: {target_mode}")
return agent
if name == "/mode":
if not args:
console.print(f"模式状态\n 当前模式: {mode.mode}")
console.print(f"当前模式: {mode.mode}")
return agent
if args in {"build", "plan"}:
mode.set_mode(args)
permissions.set_mode(args)
console.print(f"模式已切换为: {args}")
console.print(f"当前模式: {args}")
return agent
console.print("命令错误: 仅支持 /mode build 或 /mode plan")
console.print("[red]error:[/red] 仅支持 /mode build 或 /mode plan")
return agent
if name == "/history":
@ -99,57 +89,45 @@ def handle_command(
if name == "/resume":
if not args:
console.print("命令错误: 缺少 resume 目标")
console.print("[red]error:[/red] 缺少 resume 目标")
return agent
session_id = store.resolve_session_id(args)
session_meta = store.load_meta(session_id)
restored_history = store.load_messages(session_id)
console.print(f"已恢复会话: {session_meta.get('session_id')}")
console.print(f"已恢复 session: {session_meta.get('session_id')}")
return build_agent(root, config, store, permissions, session_meta, restored_history)
if name == "/memory":
content = memory.read()
console.print(content or "当前项目还没有已保存的 memory。")
return agent
if name == "/dream":
message = agent.dream(memory)
console.print(message)
console.print(content or "当前项目还没有 memory。")
return agent
if name == "/permissions":
if args == "auto-on":
permissions.set_auto_approve(True)
console.print("权限状态\n auto_approve: 开启")
console.print("已开启 auto_approve")
return agent
if args == "auto-off":
permissions.set_auto_approve(False)
console.print("权限状态\n auto_approve: 关闭")
console.print("已关闭 auto_approve")
return agent
status = permissions.status()
auto_allowed_items = list(status["auto_allowed_tools"] if isinstance(status["auto_allowed_tools"], list) else [])
confirm_required_items = list(
status["confirm_required_tools"] if isinstance(status["confirm_required_tools"], list) else []
)
auto_allowed = ", ".join(str(item) for item in auto_allowed_items)
confirm_required = ", ".join(str(item) for item in confirm_required_items)
console.print("权限状态")
console.print(f" auto_approve: {'开启' if status['auto_approve'] else '关闭'}")
console.print(f"auto_approve: {status['auto_approve']}")
console.print(f"mode: {status['mode']}")
console.print(f"workspace: {status['workspace']}")
console.print(f"boundary: {status['boundary_policy']}")
console.print(f" 自动允许: {auto_allowed}")
console.print(f" 需要确认: {confirm_required}")
console.print(f"自动允许: {', '.join(status['auto_allowed_tools'])}")
console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}")
return agent
if name == "/remember":
if not args:
console.print("命令错误: 缺少需要保存的 memory 内容")
console.print("[red]error:[/red] 缺少需要保存的 memory 内容")
return agent
path = memory.append(args)
console.print(f"已写入 memory: {path}")
return agent
console.print(f"命令错误: 不支持的命令 {name},输入 /help 查看帮助")
console.print(f"[red]error:[/red] 不支持的命令: {name},输入 /help 查看帮助")
return agent

View File

@ -11,7 +11,7 @@ from pathlib import Path
from typing import Any, Iterator
from anthropic import Anthropic
from langfuse.openai import OpenAI
from openai import OpenAI
from cc_slim.memory import MemoryStore
from cc_slim.permissions import PermissionChecker
@ -25,14 +25,11 @@ class Config:
model: str
api_key: str
base_url: str | None
langfuse_public_key: str | None = None
langfuse_secret_key: str | None = None
langfuse_base_url: str | None = None
max_turns: int = 12
def resolve_config(config_root: Path, cli: dict[str, Any]) -> Config:
file_cfg = _load_file_config(config_root / ".cc-slim.toml")
def resolve_config(workspace: Path, cli: dict[str, Any]) -> Config:
file_cfg = _load_file_config(workspace / ".cc-slim.toml")
provider = _pick(cli.get("provider"), os.getenv("CC_SLIM_PROVIDER"), file_cfg.get("provider"), "openai")
model = _pick(
cli.get("model"),
@ -48,21 +45,6 @@ def resolve_config(config_root: Path, cli: dict[str, Any]) -> Config:
"",
)
base_url = _pick(cli.get("base_url"), os.getenv("CC_SLIM_BASE_URL"), file_cfg.get("base_url"), None)
langfuse_public_key = _pick(
os.getenv("LANGFUSE_PUBLIC_KEY"),
file_cfg.get("LANGFUSE_PUBLIC_KEY"),
None,
)
langfuse_secret_key = _pick(
os.getenv("LANGFUSE_SECRET_KEY"),
file_cfg.get("LANGFUSE_SECRET_KEY"),
None,
)
langfuse_base_url = _pick(
os.getenv("LANGFUSE_BASE_URL"),
file_cfg.get("LANGFUSE_BASE_URL"),
None,
)
max_turns_raw = _pick(cli.get("max_turns"), os.getenv("CC_SLIM_MAX_TURNS"), file_cfg.get("max_turns"), 12)
if not api_key:
@ -73,9 +55,6 @@ def resolve_config(config_root: Path, cli: dict[str, Any]) -> Config:
model=str(model).strip(),
api_key=str(api_key).strip(),
base_url=str(base_url).strip() if base_url else None,
langfuse_public_key=str(langfuse_public_key).strip() if langfuse_public_key else None,
langfuse_secret_key=str(langfuse_secret_key).strip() if langfuse_secret_key else None,
langfuse_base_url=str(langfuse_base_url).strip() if langfuse_base_url else None,
max_turns=int(max_turns_raw),
)
@ -94,7 +73,6 @@ class Agent:
) -> None:
self.config = config
self.tools = {tool.name: tool for tool in tools}
self.workspace = workspace
self.history: list[dict[str, Any]] = list(history or [])
self.session_store = session_store
self.session_id = session_id
@ -112,15 +90,6 @@ class Agent:
raise RuntimeError(event["message"])
return "".join(parts).strip() or "(empty response)"
def dream(self, memory_store: MemoryStore) -> str:
if not self.history:
return "当前会话没有可整理的内容。"
dream_markdown = self._run_dream_model(memory_store.read(), self._serialize_history_for_dream())
memory_store.apply_dream(dream_markdown)
self.system_prompt = self._build_system_prompt(self.workspace)
return "已完成 dreammemory 已更新"
def stream_reply(self, user_input: str) -> Iterator[dict[str, Any]]:
user_message = {"role": "user", "content": user_input}
self.history.append(user_message)
@ -175,7 +144,6 @@ class Agent:
def _build_client(self) -> Any:
if self.config.provider == "openai":
self._configure_langfuse()
kwargs: dict[str, Any] = {"api_key": self.config.api_key}
if self.config.base_url:
kwargs["base_url"] = self.config.base_url
@ -189,92 +157,23 @@ class Agent:
def _build_system_prompt(self, workspace: Path) -> str:
parts: list[str] = []
parts.append(self._load_builtin_system_prompt())
parts.append(self._build_runtime_summary(workspace))
workspace_agents = self._load_workspace_agents(workspace)
if workspace_agents:
parts.append(workspace_agents)
agents = workspace / "AGENTS.md"
if agents.exists():
parts.append(agents.read_text(encoding="utf-8"))
parts.extend(self._load_workspace_skills(workspace))
skills_dir = workspace / "SKILLS"
if skills_dir.exists():
for path in sorted(skills_dir.glob("*.md"), key=lambda p: p.name):
parts.append(path.read_text(encoding="utf-8"))
memory_prompt = self._load_memory_prompt(workspace)
if memory_prompt:
parts.append(memory_prompt)
memory = MemoryStore(workspace).read()
if memory:
parts.append(f"# Memory\n\n{memory}")
return "\n\n".join(part.strip() for part in parts if part.strip())
def _load_builtin_system_prompt(self) -> str:
return (Path(__file__).resolve().parent / "system.md").read_text(encoding="utf-8")
def _load_workspace_agents(self, workspace: Path) -> str:
agents = workspace / "AGENTS.md"
if not agents.exists():
return ""
return agents.read_text(encoding="utf-8")
def _load_workspace_skills(self, workspace: Path) -> list[str]:
skills: list[str] = []
skills_dir = workspace / "SKILLS"
if not skills_dir.exists():
return skills
for path in sorted(skills_dir.glob("*.md"), key=lambda p: p.name):
skills.append(path.read_text(encoding="utf-8"))
return skills
def _load_memory_prompt(self, workspace: Path) -> str:
memory = MemoryStore(workspace).read()
if not memory:
return ""
return memory
def _serialize_history_for_dream(self) -> str:
lines: list[str] = []
for item in self.history:
role = item.get("role", "unknown")
if role == "assistant" and item.get("tool_calls"):
content = item.get("content", "")
else:
content = item.get("content", "")
if isinstance(content, list):
content = json.dumps(content, ensure_ascii=False)
lines.append(f"[{role}] {content}")
return "\n".join(lines)
def _dream_prompt(self, current_memory: str, session_text: str) -> str:
return (
"你不是在做聊天总结,而是在从当前 session 中提炼长期有价值的信息。\n"
"只保留未来仍值得记住的内容,忽略一次性任务细节、临时状态和短期噪声。\n"
"请结合当前 memory 与当前 session输出可直接写回 memory 的 Markdown。\n"
"只输出以下四个 sections不要输出代码块、解释或其他标题\n\n"
"## User Memory\n"
"## Project Memory\n"
"## Constraints\n"
"## Consolidated Facts\n\n"
f"当前 memory:\n{current_memory}\n\n"
f"当前 session:\n{session_text}"
)
def _run_dream_model(self, current_memory: str, session_text: str) -> str:
prompt = self._dream_prompt(current_memory, session_text)
if self.config.provider == "openai":
response = self.client.chat.completions.create(
model=self.config.model,
messages=[
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": prompt},
],
)
return response.choices[0].message.content or ""
response = self.client.messages.create(
model=self.config.model,
system=self.system_prompt,
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
return "\n".join(block.text for block in response.content if block.type == "text")
def _build_runtime_summary(self, workspace: Path) -> str:
tool_names = ", ".join(self.tools.keys()) or "(none)"
shell_name = self._detect_shell()
@ -297,14 +196,6 @@ class Agent:
return os.getenv("COMSPEC", "Windows shell (likely PowerShell or cmd.exe)")
return os.getenv("SHELL", "unknown shell")
def _configure_langfuse(self) -> None:
if self.config.langfuse_public_key:
os.environ["LANGFUSE_PUBLIC_KEY"] = self.config.langfuse_public_key
if self.config.langfuse_secret_key:
os.environ["LANGFUSE_SECRET_KEY"] = self.config.langfuse_secret_key
if self.config.langfuse_base_url:
os.environ["LANGFUSE_BASE_URL"] = self.config.langfuse_base_url
def _call_openai(self) -> dict[str, Any]:
response = self.client.chat.completions.create(
model=self.config.model,
@ -398,9 +289,9 @@ class Agent:
def _check_tool_permission(self, name: str, payload: dict[str, Any]) -> str | None:
if not self.permission_checker:
return None
if self.permission_checker.is_hard_blocked(name, payload):
if not self.permission_checker.is_allowed(name, payload):
return self.permission_checker.denial_reason(name, payload)
if self.permission_checker.requires_confirmation(name, payload):
if self.permission_checker.requires_confirmation(name):
if not self.confirm_tool:
return self.permission_checker.denial_reason(name, payload)
if not self.confirm_tool(name, payload):

View File

@ -33,9 +33,9 @@ def render_stream(agent: Agent, user_input: str) -> None:
elif event["type"] == "tool_result":
output = str(event.get("output", ""))
if (
output.startswith("当前处于 plan 模式,禁止执行工具:")
or output.startswith("当前未批准工具执行:")
or output.startswith("当前命令疑似指向工作区外路径,已拒绝执行 Bash")
output.startswith("Tool blocked in plan mode:")
or output.startswith("Permission denied for tool:")
or output.startswith("Bash command appears to target paths outside the workspace")
):
console.print(f"[red]{output}[/red]")
else:
@ -106,25 +106,24 @@ def run(
model: Optional[str] = typer.Option(None, help="模型名称"),
api_key: Optional[str] = typer.Option(None, help="API Key优先级最高"),
base_url: Optional[str] = typer.Option(None, help="可选的 API Base URL"),
workspace: Path = typer.Option(Path("."), "--workspace", help="要操作的工作区根目录"),
cwd: Path = typer.Option(Path("."), help="工作区根目录"),
max_turns: Optional[int] = typer.Option(None, help="最大工具循环轮数"),
history: bool = typer.Option(False, "--history", help="列出当前工作目录的历史 session"),
resume: Optional[str] = typer.Option(None, "--resume", help="按 session id 或序号恢复历史 session"),
auto_approve: bool = typer.Option(False, "--auto-approve", help="跳过高风险工具确认"),
) -> None:
program_root = Path(__file__).resolve().parents[2]
workspace_root = workspace.resolve()
store = SessionStore(workspace_root)
memory = MemoryStore(workspace_root)
root = cwd.resolve()
store = SessionStore(root)
memory = MemoryStore(root)
mode = ModeState()
permissions = PermissionChecker(workspace_root, auto_approve=auto_approve)
permissions = PermissionChecker(root, auto_approve=auto_approve)
permissions.set_mode(mode.mode)
if history:
render_history(store)
return
config = resolve_config(
program_root,
root,
{
"provider": provider,
"model": model,
@ -143,16 +142,16 @@ def run(
else:
session_meta = store.create_session(config.model)
agent = build_agent(workspace_root, config, store, permissions, session_meta, restored_history)
agent = build_agent(root, config, store, permissions, session_meta, restored_history)
if prompt:
render_stream(agent, prompt)
return
console.print("[bold cyan]cc-slim[/bold cyan] REPL输入 /help 查看命令,输入 exit 或 quit 退出。")
console.print("[bold cyan]cc-slim[/bold cyan] REPL输入 exit 或 quit 退出。")
while True:
try:
user_input = typer.prompt(f"[{mode.mode}] >")
user_input = typer.prompt(">")
except (EOFError, KeyboardInterrupt):
console.print()
break
@ -167,7 +166,7 @@ def run(
agent = handle_command(
user_input,
console=console,
root=workspace_root,
root=root,
config=config,
store=store,
memory=memory,

View File

@ -5,115 +5,28 @@ import re
from pathlib import Path
SECTION_ORDER = [
"User Memory",
"Project Memory",
"Constraints",
"Consolidated Facts",
"Scratch Notes",
]
class MemoryStore:
def __init__(self, cwd: Path) -> None:
self.cwd = cwd.resolve()
self.root = Path.home() / ".config" / "cc-slim" / "memory" / self._sanitize_cwd(self.cwd)
self.root.mkdir(parents=True, exist_ok=True)
self.ensure_structure()
def ensure_structure(self) -> None:
def append(self, text: str) -> Path:
path = self.path()
if not path.exists():
path.write_text(self._default_template(), encoding="utf-8")
return
existing = path.read_text(encoding="utf-8").strip()
if self._is_structured(existing):
return
migrated = self._default_template(existing)
path.write_text(migrated, encoding="utf-8")
def append(self, text: str) -> str:
sections = self.read_sections()
entry = self._format_scratch_entry(text)
scratch = sections["Scratch Notes"].strip()
sections["Scratch Notes"] = f"{scratch}\n\n{entry}".strip() if scratch else entry
self._write_sections(sections)
return "Scratch Notes"
existing = self.read()
content = f"{existing}\n\n{text.strip()}" if existing else text.strip()
path.write_text(content + "\n", encoding="utf-8")
return path
def read(self) -> str:
self.ensure_structure()
return self.path().read_text(encoding="utf-8").strip()
def read_sections(self) -> dict[str, str]:
self.ensure_structure()
return self._parse_sections(self.read())
def apply_dream(self, dream_markdown: str) -> None:
current = self.read_sections()
updated = self._parse_sections(dream_markdown)
for name in SECTION_ORDER:
if name == "Scratch Notes":
continue
if updated.get(name, "").strip():
current[name] = updated[name].strip()
self._write_sections(current)
path = self.path()
if not path.exists():
return ""
return path.read_text(encoding="utf-8").strip()
def path(self) -> Path:
return self.root / "MEMORY.md"
def _default_template(self, scratch_notes: str = "") -> str:
scratch = self._format_scratch_entry(scratch_notes) if scratch_notes.strip() else ""
sections = {name: "" for name in SECTION_ORDER}
sections["Scratch Notes"] = scratch
return self._render_sections(sections)
def _format_scratch_entry(self, text: str) -> str:
lines = [line.rstrip() for line in text.strip().splitlines() if line.strip()]
if not lines:
return ""
if len(lines) == 1:
return f"- {lines[0]}"
tail = "\n".join(f" {line}" for line in lines[1:])
return f"- {lines[0]}\n{tail}"
def _is_structured(self, text: str) -> bool:
required_sections = [
"# Memory",
"## User Memory",
"## Project Memory",
"## Constraints",
"## Consolidated Facts",
"## Scratch Notes",
]
return all(section in text for section in required_sections)
def _parse_sections(self, text: str) -> dict[str, str]:
sections = {name: "" for name in SECTION_ORDER}
current: str | None = None
for line in text.splitlines():
if line.startswith("## "):
name = line[3:].strip()
current = name if name in sections else None
continue
if line.startswith("# Memory"):
continue
if current is not None:
sections[current] = f"{sections[current]}\n{line}".strip()
return sections
def _render_sections(self, sections: dict[str, str]) -> str:
parts = ["# Memory", ""]
for name in SECTION_ORDER:
parts.append(f"## {name}")
parts.append(sections.get(name, "").strip())
parts.append("")
return "\n".join(parts).rstrip() + "\n"
def _write_sections(self, sections: dict[str, str]) -> None:
self.path().write_text(self._render_sections(sections), encoding="utf-8")
def _sanitize_cwd(self, cwd: Path) -> str:
text = re.sub(r"[^A-Za-z0-9._-]+", "_", str(cwd))
digest = hashlib.sha1(str(cwd).encode("utf-8")).hexdigest()[:8]

View File

@ -13,28 +13,23 @@ class PermissionChecker:
self.auto_allowed_tools = ["Read", "Glob", "Grep"]
self.confirm_required_tools = ["Write", "Edit", "Bash"]
def is_hard_blocked(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
if self.mode == "plan" and tool_name in self.confirm_required_tools:
return True
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return True
return False
def is_allowed(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
return not self.is_hard_blocked(tool_name, payload)
if self.mode == "plan" and tool_name in self.confirm_required_tools:
return False
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return False
return True
def denial_reason(self, tool_name: str, payload: dict[str, object] | None = None) -> str:
if self.mode == "plan" and tool_name in self.confirm_required_tools:
return f"当前处于 plan 模式,禁止执行工具: {tool_name}"
return f"Tool blocked in plan mode: {tool_name}"
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return "当前命令疑似指向工作区外路径,已拒绝执行 Bash"
return f"当前未批准工具执行: {tool_name}"
return "Bash command appears to target paths outside the workspace"
return f"Permission denied for tool: {tool_name}"
def requires_confirmation(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
def requires_confirmation(self, tool_name: str) -> bool:
if self.auto_approve:
return False
if self.is_hard_blocked(tool_name, payload):
return False
return tool_name in self.confirm_required_tools
def set_auto_approve(self, enabled: bool) -> None:

View File

@ -1,70 +0,0 @@
# cc-slim System
你是 `cc-slim`,一个本地极简代理。
## 基本原则
- 只能依据以下信息行动:
- 当前用户输入
- 当前仓库文件
- 工具返回结果
- 当前历史信息上下文
- 不要假设任何尚未看到的文件、命令、接口、配置或能力存在。
- 信息缺失时,采用最小默认策略,并在最终回答中简短说明。
## 工具原则
- 只有在减少不确定性或执行动作确有必要时才使用工具。
- 优先选择更小、更直接的动作。
- 对运行时摘要中已经明确给出的信息,优先直接使用当前上下文回答;不要为了重复确认已知信息而优先调用 Bash。
- 不能虚构工具输出。
- 不能在未验证前声称文件存在、命令成功或修改已生效。
## 工具使用严格规则(高优先级)
Bash 是高权限工具,必须谨慎使用。
### 禁止使用 Bash 的情况(默认规则)
在以下情况下,绝对禁止调用 Bash
- 用户是在询问信息(如:环境、配置、状态、文件内容总结)
- system prompt 或上下文中已经提供了答案
- 可以通过 Read / Grep / Glob 获取信息
- 只是为了“确认一下”或“更保险”而运行命令
- 没有明确需要执行系统命令的需求
在这些情况下,必须直接回答,不能调用 Bash。
---
### 允许使用 Bash 的情况(必须同时满足)
只有在以下条件同时满足时,才可以调用 Bash
1. 用户明确要求执行命令(如:运行程序、构建、安装、测试)
2. 或者任务本质是“执行动作”,而不是“获取信息”
3. 且没有更安全/更小的工具可以替代
---
### 工具优先级(必须遵守)
获取信息时,必须按以下优先级选择工具:
1. 已有上下文(优先直接回答)
2. Read读取单个文件
3. Grep / Glob搜索文件
4. Bash最后手段默认禁止
---
## 行动策略
1. 先判断当前上下文是否已经足够决定下一步。
2. 如需减少不确定性,读取最相关的文件或调用最小工具。
3. 执行最小下一步动作。
4. 根据结果重新判断。
5. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
## 输出风格
- 保持简洁、直接、可验证。
- 优先给出事实,而不是泛泛解释。
- 对不确定性保持诚实。

View File

@ -159,20 +159,12 @@ def grep_tool(workspace: Path, data: dict[str, Any]) -> str:
return f"文件不存在: {target.relative_to(workspace)}"
results: list[str] = []
if target.is_file():
files = [target]
else:
files = target.rglob("*")
found_file = False
files = [target] if target.is_file() else [path for path in target.rglob("*") if path.is_file()]
if target.is_dir() and not files:
return f"目录为空: {target.relative_to(workspace)}"
for file_path in files:
if not file_path.is_file():
continue
found_file = True
try:
if file_path.stat().st_size > 1024 * 1024:
continue
text = file_path.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
@ -183,8 +175,6 @@ def grep_tool(workspace: Path, data: dict[str, Any]) -> str:
if len(results) >= 20:
return "\n".join(results)
if target.is_dir() and not found_file:
return f"目录为空: {target.relative_to(workspace)}"
if not results:
return "未找到匹配内容"
return "\n".join(results)

View File

@ -1,140 +0,0 @@
from __future__ import annotations
import io
import sys
from pathlib import Path
import pytest
from rich.console import Console
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cc_slim.commands import handle_command
from cc_slim.engine import Agent, Config
from cc_slim.memory import MemoryStore
from cc_slim.mode import ModeState
from cc_slim.permissions import PermissionChecker
from cc_slim.session import SessionStore
from cc_slim.tools import build_default_tools
def make_workspace(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, MemoryStore]:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr("cc_slim.memory.Path.home", lambda: home)
monkeypatch.setattr("cc_slim.session.Path.home", lambda: home)
workspace = tmp_path / "workspace"
workspace.mkdir()
(workspace / "AGENTS.md").write_text("# Workspace Rules\nworkspace-agents\n", encoding="utf-8")
skills = workspace / "SKILLS"
skills.mkdir()
(skills / "a.md").write_text("skill-a\n", encoding="utf-8")
memory = MemoryStore(workspace)
return workspace, memory
def make_agent(monkeypatch: pytest.MonkeyPatch, workspace: Path) -> Agent:
monkeypatch.setattr(Agent, "_build_client", lambda self: object())
config = Config(provider="openai", model="test-model", api_key="key", base_url=None)
permissions = PermissionChecker(workspace)
return Agent(config=config, tools=build_default_tools(workspace), workspace=workspace, permission_checker=permissions)
def test_dream_command_is_routed(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
output = io.StringIO()
console = Console(file=output, force_terminal=False, color_system=None)
class DummyAgent:
def __init__(self) -> None:
self.called = False
def dream(self, store: MemoryStore) -> str:
self.called = True
assert store is memory
return "已完成 dreammemory 已更新"
agent = DummyAgent()
result = handle_command(
"/dream",
console=console,
root=workspace,
config=type("ConfigObj", (), {"model": "test-model"})(),
store=SessionStore(workspace),
memory=memory,
mode=ModeState(),
permissions=PermissionChecker(workspace),
agent=agent, # type: ignore[arg-type]
build_agent=lambda *args, **kwargs: agent,
render_history=lambda store: None,
)
assert agent.called is True
assert result is agent
assert "已完成 dreammemory 已更新" in output.getvalue()
def test_agent_dream_updates_memory_and_refreshes_prompt(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
agent = make_agent(monkeypatch, workspace)
agent.history = [{"role": "user", "content": "请记住这个项目运行在 Windows 上"}]
original_prompt = agent.system_prompt
monkeypatch.setattr(
agent,
"_run_dream_model",
lambda current_memory, session_text: """## Constraints
- 项目默认运行在 Windows
## Consolidated Facts
- 需要优先考虑 PowerShell 兼容性
""",
)
result = agent.dream(memory)
sections = memory.read_sections()
assert result == "已完成 dreammemory 已更新"
assert sections["Constraints"] == "- 项目默认运行在 Windows 上"
assert sections["Consolidated Facts"] == "- 需要优先考虑 PowerShell 兼容性"
assert agent.system_prompt != original_prompt
assert "项目默认运行在 Windows 上" in agent.system_prompt
def test_dream_does_not_modify_workspace_agents(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
agent = make_agent(monkeypatch, workspace)
agent.history = [{"role": "user", "content": "记住这个项目使用 uv run"}]
agents_before = (workspace / "AGENTS.md").read_text(encoding="utf-8")
monkeypatch.setattr(
agent,
"_run_dream_model",
lambda current_memory, session_text: """## Project Memory
- 使用 uv run 执行命令
""",
)
agent.dream(memory)
assert (workspace / "AGENTS.md").read_text(encoding="utf-8") == agents_before
def test_system_prompt_layers_remain_in_order(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
workspace, memory = make_workspace(monkeypatch, tmp_path)
memory.apply_dream(
"""## Project Memory
- project-memory-marker
"""
)
agent = make_agent(monkeypatch, workspace)
prompt = agent.system_prompt
system_index = prompt.index("# cc-slim System")
runtime_index = prompt.index("## 运行环境")
agents_index = prompt.index("workspace-agents")
skills_index = prompt.index("skill-a")
memory_index = prompt.index("# Memory")
assert system_index < runtime_index < agents_index < skills_index < memory_index

View File

@ -1,159 +0,0 @@
from __future__ import annotations
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cc_slim.memory import MemoryStore
def make_store(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[MemoryStore, Path]:
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr("cc_slim.memory.Path.home", lambda: home)
workspace = tmp_path / "workspace"
workspace.mkdir()
return MemoryStore(workspace), workspace
def test_memory_initializes_structured_template(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
content = store.read()
assert content.startswith("# Memory")
assert "## User Memory" in content
assert "## Project Memory" in content
assert "## Constraints" in content
assert "## Consolidated Facts" in content
assert "## Scratch Notes" in content
def test_memory_migrates_v1_text_into_scratch_notes(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, workspace = make_store(monkeypatch, tmp_path)
store.path().write_text("old note\nsecond line\n", encoding="utf-8")
migrated = MemoryStore(workspace)
sections = migrated.read_sections()
assert sections["Scratch Notes"] == "- old note\n second line"
def test_read_sections_parses_structured_memory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
store.path().write_text(
"""# Memory
## User Memory
- prefers concise output
## Project Memory
- use uv run
## Constraints
- windows first
## Consolidated Facts
- workspace is repo root
## Scratch Notes
- temporary note
""",
encoding="utf-8",
)
sections = store.read_sections()
assert sections["User Memory"] == "- prefers concise output"
assert sections["Project Memory"] == "- use uv run"
assert sections["Constraints"] == "- windows first"
assert sections["Consolidated Facts"] == "- workspace is repo root"
assert sections["Scratch Notes"] == "- temporary note"
def test_apply_dream_updates_structured_sections_only(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
store.path().write_text(
"""# Memory
## User Memory
- old user
## Project Memory
- old project
## Constraints
- old constraint
## Consolidated Facts
- old fact
## Scratch Notes
- keep me
""",
encoding="utf-8",
)
store.apply_dream(
"""## User Memory
- new user
## Project Memory
- new project
## Constraints
- new constraint
## Consolidated Facts
- new fact
"""
)
sections = store.read_sections()
assert sections["User Memory"] == "- new user"
assert sections["Project Memory"] == "- new project"
assert sections["Constraints"] == "- new constraint"
assert sections["Consolidated Facts"] == "- new fact"
assert sections["Scratch Notes"] == "- keep me"
def test_apply_dream_keeps_unmentioned_sections(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
store, _ = make_store(monkeypatch, tmp_path)
store.path().write_text(
"""# Memory
## User Memory
- old user
## Project Memory
- old project
## Constraints
- old constraint
## Consolidated Facts
- old fact
## Scratch Notes
- keep me
""",
encoding="utf-8",
)
store.apply_dream(
"""## Project Memory
- updated project
## Consolidated Facts
- updated fact
"""
)
sections = store.read_sections()
assert sections["User Memory"] == "- old user"
assert sections["Project Memory"] == "- updated project"
assert sections["Constraints"] == "- old constraint"
assert sections["Consolidated Facts"] == "- updated fact"
assert sections["Scratch Notes"] == "- keep me"

332
uv.lock
View File

@ -52,22 +52,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload_time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
name = "backoff"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload_time = "2022-10-05T19:19:32.061Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload_time = "2022-10-05T19:19:30.546Z" },
]
[[package]]
name = "cc-slim"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },
{ name = "langfuse" },
{ name = "openai" },
{ name = "rich" },
{ name = "typer" },
@ -82,7 +72,6 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "anthropic", specifier = ">=0.93.0" },
{ name = "langfuse", specifier = ">=2.60.0" },
{ name = "openai", specifier = ">=1.76.0" },
{ name = "rich", specifier = ">=14.3.3" },
{ name = "typer", specifier = ">=0.24.1" },
@ -103,95 +92,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload_time = "2026-02-25T02:54:15.766Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload_time = "2026-04-02T09:28:39.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload_time = "2026-04-02T09:26:02.191Z" },
{ url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload_time = "2026-04-02T09:26:03.583Z" },
{ url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload_time = "2026-04-02T09:26:04.738Z" },
{ url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload_time = "2026-04-02T09:26:06.36Z" },
{ url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload_time = "2026-04-02T09:26:08.347Z" },
{ url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload_time = "2026-04-02T09:26:09.823Z" },
{ url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload_time = "2026-04-02T09:26:10.953Z" },
{ url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload_time = "2026-04-02T09:26:12.142Z" },
{ url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload_time = "2026-04-02T09:26:13.711Z" },
{ url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload_time = "2026-04-02T09:26:14.941Z" },
{ url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload_time = "2026-04-02T09:26:16.478Z" },
{ url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload_time = "2026-04-02T09:26:17.751Z" },
{ url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload_time = "2026-04-02T09:26:18.981Z" },
{ url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload_time = "2026-04-02T09:26:20.295Z" },
{ url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload_time = "2026-04-02T09:26:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload_time = "2026-04-02T09:26:22.901Z" },
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload_time = "2026-04-02T09:26:24.331Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload_time = "2026-04-02T09:26:25.568Z" },
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload_time = "2026-04-02T09:26:26.865Z" },
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload_time = "2026-04-02T09:26:28.044Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload_time = "2026-04-02T09:26:29.239Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload_time = "2026-04-02T09:26:30.5Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload_time = "2026-04-02T09:26:31.709Z" },
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload_time = "2026-04-02T09:26:33.282Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload_time = "2026-04-02T09:26:34.845Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload_time = "2026-04-02T09:26:36.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload_time = "2026-04-02T09:26:37.672Z" },
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload_time = "2026-04-02T09:26:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload_time = "2026-04-02T09:26:40.17Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload_time = "2026-04-02T09:26:41.416Z" },
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload_time = "2026-04-02T09:26:42.554Z" },
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload_time = "2026-04-02T09:26:44.075Z" },
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload_time = "2026-04-02T09:26:45.198Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload_time = "2026-04-02T09:26:46.824Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload_time = "2026-04-02T09:26:48.397Z" },
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload_time = "2026-04-02T09:26:49.684Z" },
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload_time = "2026-04-02T09:26:50.915Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload_time = "2026-04-02T09:26:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload_time = "2026-04-02T09:26:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload_time = "2026-04-02T09:26:54.975Z" },
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload_time = "2026-04-02T09:26:56.303Z" },
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload_time = "2026-04-02T09:26:57.554Z" },
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload_time = "2026-04-02T09:26:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload_time = "2026-04-02T09:27:00.437Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload_time = "2026-04-02T09:27:02.021Z" },
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload_time = "2026-04-02T09:27:03.192Z" },
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload_time = "2026-04-02T09:27:04.454Z" },
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload_time = "2026-04-02T09:27:05.971Z" },
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload_time = "2026-04-02T09:27:07.194Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload_time = "2026-04-02T09:27:08.749Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload_time = "2026-04-02T09:27:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload_time = "2026-04-02T09:27:11.175Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload_time = "2026-04-02T09:27:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload_time = "2026-04-02T09:27:13.721Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload_time = "2026-04-02T09:27:15.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload_time = "2026-04-02T09:27:16.834Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload_time = "2026-04-02T09:27:18.229Z" },
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload_time = "2026-04-02T09:27:19.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload_time = "2026-04-02T09:27:20.79Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload_time = "2026-04-02T09:27:22.063Z" },
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload_time = "2026-04-02T09:27:23.486Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload_time = "2026-04-02T09:27:25.146Z" },
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload_time = "2026-04-02T09:27:26.642Z" },
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload_time = "2026-04-02T09:27:28.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload_time = "2026-04-02T09:27:29.474Z" },
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload_time = "2026-04-02T09:27:30.793Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload_time = "2026-04-02T09:27:32.44Z" },
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload_time = "2026-04-02T09:27:34.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload_time = "2026-04-02T09:27:35.369Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload_time = "2026-04-02T09:27:36.661Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload_time = "2026-04-02T09:27:38.019Z" },
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload_time = "2026-04-02T09:27:39.37Z" },
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload_time = "2026-04-02T09:27:40.722Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload_time = "2026-04-02T09:27:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload_time = "2026-04-02T09:27:43.924Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload_time = "2026-04-02T09:27:45.348Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload_time = "2026-04-02T09:27:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload_time = "2026-04-02T09:27:48.053Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload_time = "2026-04-02T09:27:49.795Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload_time = "2026-04-02T09:27:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload_time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
name = "click"
version = "8.3.2"
@ -231,18 +131,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload_time = "2025-07-21T07:35:00.684Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.74.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload_time = "2026-04-02T21:23:26.679Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload_time = "2026-04-02T21:22:49.108Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@ -289,18 +177,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload_time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "importlib-metadata"
version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload_time = "2025-12-21T10:00:19.278Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload_time = "2025-12-21T10:00:18.329Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
@ -395,25 +271,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload_time = "2026-02-02T12:37:55.055Z" },
]
[[package]]
name = "langfuse"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff" },
{ name = "httpx" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/9c/b912a00ffae92ff9955cdd9b74fb839be58f631d4329ae2a8a0376f697f2/langfuse-4.2.0.tar.gz", hash = "sha256:d0bd26d5065cf6a59d7d1093b08d8910e2458dc3da7ed8ccec160db114c18342", size = 275582, upload_time = "2026-04-10T11:55:25.21Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/0a/b84e3e68a690ccfe6d64953c572772c685fcb0915b7f2ee3a87c22e388ab/langfuse-4.2.0-py3-none-any.whl", hash = "sha256:bfd760bf10fd0228f297f6369436620f76d16b589de46393d65706b27e4e4082", size = 475449, upload_time = "2026-04-10T11:55:23.624Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
@ -454,88 +311,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload_time = "2026-04-08T21:01:39.217Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload_time = "2026-04-09T14:38:34.544Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload_time = "2026-04-09T14:38:11.833Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/28/e8eca94966fe9a1465f6094dc5ddc5398473682180279c94020bc23b4906/opentelemetry_exporter_otlp_proto_common-1.41.0.tar.gz", hash = "sha256:966bbce537e9edb166154779a7c4f8ab6b8654a03a28024aeaf1a3eacb07d6ee", size = 20411, upload_time = "2026-04-09T14:38:36.572Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/c4/78b9bf2d9c1d5e494f44932988d9d91c51a66b9a7b48adf99b62f7c65318/opentelemetry_exporter_otlp_proto_common-1.41.0-py3-none-any.whl", hash = "sha256:7a99177bf61f85f4f9ed2072f54d676364719c066f6d11f515acc6c745c7acf0", size = 18366, upload_time = "2026-04-09T14:38:15.135Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/63/d9f43cd75f3fabb7e01148c89cfa9491fc18f6580a6764c554ff7c953c46/opentelemetry_exporter_otlp_proto_http-1.41.0.tar.gz", hash = "sha256:dcd6e0686f56277db4eecbadd5262124e8f2cc739cadbc3fae3d08a12c976cf5", size = 24139, upload_time = "2026-04-09T14:38:38.128Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b5/a214cd907eedc17699d1c2d602288ae17cb775526df04db3a3b3585329d2/opentelemetry_exporter_otlp_proto_http-1.41.0-py3-none-any.whl", hash = "sha256:a9c4ee69cce9c3f4d7ee736ad1b44e3c9654002c0816900abbafd9f3cf289751", size = 22673, upload_time = "2026-04-09T14:38:18.349Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/d9/08e3dc6156878713e8c811682bc76151f5fe1a3cb7f3abda3966fd56e71e/opentelemetry_proto-1.41.0.tar.gz", hash = "sha256:95d2e576f9fb1800473a3e4cfcca054295d06bdb869fda4dc9f4f779dc68f7b6", size = 45669, upload_time = "2026-04-09T14:38:45.978Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/8c/65ef7a9383a363864772022e822b5d5c6988e6f9dabeebb9278f5b86ebc3/opentelemetry_proto-1.41.0-py3-none-any.whl", hash = "sha256:b970ab537309f9eed296be482c3e7cca05d8aca8165346e929f658dbe153b247", size = 72074, upload_time = "2026-04-09T14:38:29.38Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/0e/a586df1186f9f56b5a0879d52653effc40357b8e88fc50fe300038c3c08b/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd", size = 230181, upload_time = "2026-04-09T14:38:47.225Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/13/a7825118208cb32e6a4edcd0a99f925cbef81e77b3b0aedfd9125583c543/opentelemetry_sdk-1.41.0-py3-none-any.whl", hash = "sha256:a596f5687964a3e0d7f8edfdcf5b79cbca9c93c7025ebf5fb00f398a9443b0bd", size = 180214, upload_time = "2026-04-09T14:38:30.657Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.62b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload_time = "2026-04-09T14:38:48.274Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload_time = "2026-04-09T14:38:32.394Z" },
]
[[package]]
name = "packaging"
version = "26.0"
@ -554,21 +329,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload_time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "protobuf"
version = "6.33.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload_time = "2026-03-18T19:05:00.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload_time = "2026-03-18T19:04:48.373Z" },
{ url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload_time = "2026-03-18T19:04:50.381Z" },
{ url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload_time = "2026-03-18T19:04:51.866Z" },
{ url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload_time = "2026-03-18T19:04:53.096Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload_time = "2026-03-18T19:04:54.616Z" },
{ url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload_time = "2026-03-18T19:04:55.768Z" },
{ url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload_time = "2026-03-18T19:04:59.826Z" },
]
[[package]]
name = "pydantic"
version = "2.12.5"
@ -706,21 +466,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload_time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
name = "requests"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload_time = "2026-03-30T16:09:15.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload_time = "2026-03-30T16:09:13.83Z" },
]
[[package]]
name = "rich"
version = "14.3.3"
@ -824,80 +569,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload_time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload_time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload_time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "wrapt"
version = "1.17.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload_time = "2025-08-12T05:53:21.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload_time = "2025-08-12T05:51:45.79Z" },
{ url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload_time = "2025-08-12T05:51:34.629Z" },
{ url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload_time = "2025-08-12T05:51:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload_time = "2025-08-12T05:52:32.134Z" },
{ url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload_time = "2025-08-12T05:52:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload_time = "2025-08-12T05:52:12.626Z" },
{ url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload_time = "2025-08-12T05:52:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload_time = "2025-08-12T05:53:03.936Z" },
{ url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload_time = "2025-08-12T05:53:02.885Z" },
{ url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload_time = "2025-08-12T05:52:53.368Z" },
{ url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload_time = "2025-08-12T05:51:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload_time = "2025-08-12T05:51:35.906Z" },
{ url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload_time = "2025-08-12T05:51:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload_time = "2025-08-12T05:52:34.784Z" },
{ url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload_time = "2025-08-12T05:52:13.599Z" },
{ url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload_time = "2025-08-12T05:52:14.56Z" },
{ url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload_time = "2025-08-12T05:52:36.165Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload_time = "2025-08-12T05:53:07.123Z" },
{ url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload_time = "2025-08-12T05:53:05.436Z" },
{ url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload_time = "2025-08-12T05:52:54.367Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload_time = "2025-08-12T05:51:48.627Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload_time = "2025-08-12T05:51:37.156Z" },
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload_time = "2025-08-12T05:51:58.425Z" },
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload_time = "2025-08-12T05:52:37.53Z" },
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload_time = "2025-08-12T05:52:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload_time = "2025-08-12T05:52:17.914Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload_time = "2025-08-12T05:52:39.243Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload_time = "2025-08-12T05:53:10.074Z" },
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload_time = "2025-08-12T05:53:08.695Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload_time = "2025-08-12T05:52:55.34Z" },
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload_time = "2025-08-12T05:51:49.864Z" },
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload_time = "2025-08-12T05:51:38.935Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload_time = "2025-08-12T05:51:59.365Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload_time = "2025-08-12T05:52:40.965Z" },
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload_time = "2025-08-12T05:52:20.326Z" },
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload_time = "2025-08-12T05:52:21.581Z" },
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload_time = "2025-08-12T05:52:43.043Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload_time = "2025-08-12T05:53:12.605Z" },
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload_time = "2025-08-12T05:53:11.106Z" },
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload_time = "2025-08-12T05:52:56.531Z" },
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload_time = "2025-08-12T05:51:51.109Z" },
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload_time = "2025-08-12T05:51:39.912Z" },
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload_time = "2025-08-12T05:52:00.693Z" },
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload_time = "2025-08-12T05:52:44.521Z" },
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload_time = "2025-08-12T05:52:22.618Z" },
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload_time = "2025-08-12T05:52:24.057Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload_time = "2025-08-12T05:52:45.976Z" },
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload_time = "2025-08-12T05:53:15.214Z" },
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload_time = "2025-08-12T05:53:14.178Z" },
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload_time = "2025-08-12T05:52:57.784Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload_time = "2025-08-12T05:53:20.674Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload_time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload_time = "2025-06-08T17:06:38.034Z" },
]