Compare commits
7 Commits
de0fad496c
...
baf8837b5e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baf8837b5e | ||
|
|
5b78f7cf80 | ||
|
|
5bdc1bee18 | ||
|
|
4efc3d267c | ||
|
|
18ab3cabd5 | ||
|
|
33e8704458 | ||
|
|
a3256d15f4 |
@ -5,3 +5,6 @@ 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"
|
||||
85
AGENTS.md
85
AGENTS.md
@ -1,73 +1,42 @@
|
||||
# AGENTS.md
|
||||
|
||||
你是 `cc-slim`,一个只能基于当前仓库与用户输入行动的本地极简代理。
|
||||
这是当前 workspace 的项目级规则文件。
|
||||
|
||||
## 行动边界
|
||||
## 工作区规则
|
||||
|
||||
- 你只能依据以下信息行动:
|
||||
- 当前仓库中的文件
|
||||
- 当前用户输入
|
||||
- 工具返回的结果
|
||||
- 不要假设任何尚未看到的文件、命令、接口、配置或能力存在。
|
||||
- 信息缺失时,采用最小默认策略,并在最终回答里简短说明该默认策略。
|
||||
- 当前 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. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
|
||||
## 模式与权限
|
||||
|
||||
当前 harness 是极简实现,优先最小动作,不做不必要的重复试错。
|
||||
- 支持 `/mode build` 和 `/mode plan` 两种模式,`plan` 为只读规划模式。
|
||||
- 写操作和 Bash 默认需要确认,可通过 `--auto-approve` 或 `/permissions auto-on` 跳过。
|
||||
- 工作区外访问和 `plan` 模式限制属于硬性边界,不通过审批放行。
|
||||
|
||||
行动时必须以运行时注入的环境信息为准,特别是平台、shell、工作目录和可用工具列表。
|
||||
## Memory 与验证
|
||||
|
||||
当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。
|
||||
- `AGENTS.md` 是项目规则文件,不用于保存长期项目知识或用户偏好。
|
||||
- 当前项目支持结构化 memory:使用 `/remember` 保存长期知识,使用 `/memory` 查看。
|
||||
- `/dream` 的职责是从当前 session 提炼长期知识并更新 memory,不属于项目规则文件本身。
|
||||
- session 是原始对话历史,不直接拼进 system prompt;memory 才作为长期补充进入 prompt。
|
||||
- 默认语言:中文优先。
|
||||
- 默认验证:优先做最小可验证检查,不夸大成功状态。
|
||||
|
||||
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`。
|
||||
- 默认只应在当前 workspace 内读写和执行与项目相关的操作。
|
||||
- 修改已有文件内容时,优先使用 `Edit` 工具。
|
||||
- 创建新文件时,优先使用 `Write` 工具。
|
||||
- `Bash`:用于执行必须通过 shell 完成的最小命令。
|
||||
- 只有在确实需要复杂 shell 特性时才使用 Bash。
|
||||
- 不要用 Bash 拼接文件内容。
|
||||
- 修改已有文件内容时,优先使用 `Edit`。
|
||||
- 创建新文件时,优先使用 `Write`。
|
||||
- 只有在确实需要复杂 shell 特性时才使用 `Bash`。
|
||||
- 不要用 `Bash` 拼接文件内容。
|
||||
- Windows 环境下优先使用兼容写法,不默认使用 `cat <<EOF`、`ls -la` 等 Unix 风格写法。
|
||||
- 同一写入或创建目标,最多尝试 2 种不同 Bash 方案。
|
||||
- 如果两次 Bash 方案失败,应立即收敛:
|
||||
- 先检查路径、文件状态、shell 兼容性。
|
||||
- 如仍不确定,则询问用户,而不是继续盲试。
|
||||
- 不能虚构工具输出。
|
||||
- 不能在未验证前声称文件存在、命令成功或修改已生效。
|
||||
|
||||
## 回答规则
|
||||
|
||||
- 对不确定性保持诚实。
|
||||
- 需要时引用具体文件或命令。
|
||||
- 输出保持简短直接,不夸大成功状态。
|
||||
- 若受阻,只询问当前缺失的关键信息。
|
||||
|
||||
224
ARCHITECTURE.md
Normal file
224
ARCHITECTURE.md
Normal file
@ -0,0 +1,224 @@
|
||||
# 架构说明
|
||||
|
||||
本文档解释当前代码中真实存在的 `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
302
README.md
@ -0,0 +1,302 @@
|
||||
# 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
|
||||
311
README.zh.md
Normal file
311
README.zh.md
Normal file
@ -0,0 +1,311 @@
|
||||
# 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 分层顺序
|
||||
@ -4,10 +4,11 @@
|
||||
|
||||
## 推荐流程
|
||||
|
||||
1. 先读取最接近问题的文件。
|
||||
2. 路径不明确时,用 `Glob` 定位。
|
||||
3. 必须执行命令时,用 `Bash`。
|
||||
4. 只汇报与用户目标直接相关的信息。
|
||||
1. 先使用当前上下文中已经明确给出的信息。
|
||||
2. 如仍不确定,再读取最接近问题的文件。
|
||||
3. 路径不明确时,用 `Glob` 定位。
|
||||
4. 只有确实需要执行命令时,才用 `Bash`。
|
||||
5. 只汇报与用户目标直接相关的信息。
|
||||
|
||||
## 实用启发
|
||||
|
||||
@ -24,10 +25,14 @@
|
||||
- 修改已有文件时,优先使用 `Edit`,而不是 `Bash` 或 `Write`
|
||||
- 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash`
|
||||
- 会话与 memory 管理优先使用 slash command,而不是自然语言或 `Bash` 探查对应文件
|
||||
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存
|
||||
- 优先通过 `/help` 查看当前命令,而不是猜测命令格式
|
||||
- 长期有效的项目知识或用户偏好,优先使用 `/remember` 保存
|
||||
- 项目规则和工作方式仍应写在 `AGENTS.md`
|
||||
- 当前 session 中出现了值得长期保留的信息时,可使用 `/dream` 进行整理
|
||||
- `/remember` 适合手动记一条,`/dream` 适合系统性整理当前 session
|
||||
- 高风险操作会触发确认,优先先读再改,减少无意义审批
|
||||
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
|
||||
- 先在 workspace 内定位和操作,避免无关路径探索
|
||||
- workspace 是当前默认操作边界,先在 workspace 内定位和操作,避免无关路径探索
|
||||
- Windows 下先验证 shell 兼容性,再选择命令写法
|
||||
|
||||
## 沟通风格
|
||||
|
||||
@ -10,6 +10,7 @@ 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",
|
||||
|
||||
@ -42,45 +42,55 @@ def handle_command(
|
||||
args = parsed["args"]
|
||||
|
||||
if name == "/help":
|
||||
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 - 关闭自动批准")
|
||||
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")
|
||||
return agent
|
||||
|
||||
if name in {"/clear", "/new"}:
|
||||
session_meta = store.create_session(config.model)
|
||||
console.print(f"已创建新 session: {session_meta.get('session_id')}")
|
||||
console.print(f"已创建新会话: {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"当前模式: {mode.mode}")
|
||||
console.print(f"模式状态\n 当前模式: {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("[red]error:[/red] 仅支持 /mode build 或 /mode plan")
|
||||
console.print("命令错误: 仅支持 /mode build 或 /mode plan")
|
||||
return agent
|
||||
|
||||
if name == "/history":
|
||||
@ -89,45 +99,57 @@ def handle_command(
|
||||
|
||||
if name == "/resume":
|
||||
if not args:
|
||||
console.print("[red]error:[/red] 缺少 resume 目标")
|
||||
console.print("命令错误: 缺少 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: {session_meta.get('session_id')}")
|
||||
console.print(f"已恢复会话: {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。")
|
||||
console.print(content or "当前项目还没有已保存的 memory。")
|
||||
return agent
|
||||
|
||||
if name == "/dream":
|
||||
message = agent.dream(memory)
|
||||
console.print(message)
|
||||
return agent
|
||||
|
||||
if name == "/permissions":
|
||||
if args == "auto-on":
|
||||
permissions.set_auto_approve(True)
|
||||
console.print("已开启 auto_approve")
|
||||
console.print("权限状态\n auto_approve: 开启")
|
||||
return agent
|
||||
if args == "auto-off":
|
||||
permissions.set_auto_approve(False)
|
||||
console.print("已关闭 auto_approve")
|
||||
console.print("权限状态\n auto_approve: 关闭")
|
||||
return agent
|
||||
|
||||
status = permissions.status()
|
||||
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"自动允许: {', '.join(status['auto_allowed_tools'])}")
|
||||
console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}")
|
||||
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" 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}")
|
||||
return agent
|
||||
|
||||
if name == "/remember":
|
||||
if not args:
|
||||
console.print("[red]error:[/red] 缺少需要保存的 memory 内容")
|
||||
console.print("命令错误: 缺少需要保存的 memory 内容")
|
||||
return agent
|
||||
path = memory.append(args)
|
||||
console.print(f"已写入 memory: {path}")
|
||||
return agent
|
||||
|
||||
console.print(f"[red]error:[/red] 不支持的命令: {name},输入 /help 查看帮助")
|
||||
console.print(f"命令错误: 不支持的命令 {name},输入 /help 查看帮助")
|
||||
return agent
|
||||
|
||||
@ -11,7 +11,7 @@ from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
|
||||
from anthropic import Anthropic
|
||||
from openai import OpenAI
|
||||
from langfuse.openai import OpenAI
|
||||
|
||||
from cc_slim.memory import MemoryStore
|
||||
from cc_slim.permissions import PermissionChecker
|
||||
@ -25,11 +25,14 @@ 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(workspace: Path, cli: dict[str, Any]) -> Config:
|
||||
file_cfg = _load_file_config(workspace / ".cc-slim.toml")
|
||||
def resolve_config(config_root: Path, cli: dict[str, Any]) -> Config:
|
||||
file_cfg = _load_file_config(config_root / ".cc-slim.toml")
|
||||
provider = _pick(cli.get("provider"), os.getenv("CC_SLIM_PROVIDER"), file_cfg.get("provider"), "openai")
|
||||
model = _pick(
|
||||
cli.get("model"),
|
||||
@ -45,6 +48,21 @@ def resolve_config(workspace: 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:
|
||||
@ -55,6 +73,9 @@ def resolve_config(workspace: 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),
|
||||
)
|
||||
|
||||
@ -73,6 +94,7 @@ 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
|
||||
@ -90,6 +112,15 @@ 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 "已完成 dream,memory 已更新"
|
||||
|
||||
def stream_reply(self, user_input: str) -> Iterator[dict[str, Any]]:
|
||||
user_message = {"role": "user", "content": user_input}
|
||||
self.history.append(user_message)
|
||||
@ -144,6 +175,7 @@ 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
|
||||
@ -157,23 +189,92 @@ 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))
|
||||
|
||||
agents = workspace / "AGENTS.md"
|
||||
if agents.exists():
|
||||
parts.append(agents.read_text(encoding="utf-8"))
|
||||
workspace_agents = self._load_workspace_agents(workspace)
|
||||
if workspace_agents:
|
||||
parts.append(workspace_agents)
|
||||
|
||||
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"))
|
||||
parts.extend(self._load_workspace_skills(workspace))
|
||||
|
||||
memory = MemoryStore(workspace).read()
|
||||
if memory:
|
||||
parts.append(f"# Memory\n\n{memory}")
|
||||
memory_prompt = self._load_memory_prompt(workspace)
|
||||
if memory_prompt:
|
||||
parts.append(memory_prompt)
|
||||
|
||||
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()
|
||||
@ -196,6 +297,14 @@ 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,
|
||||
@ -289,9 +398,9 @@ class Agent:
|
||||
def _check_tool_permission(self, name: str, payload: dict[str, Any]) -> str | None:
|
||||
if not self.permission_checker:
|
||||
return None
|
||||
if not self.permission_checker.is_allowed(name, payload):
|
||||
if self.permission_checker.is_hard_blocked(name, payload):
|
||||
return self.permission_checker.denial_reason(name, payload)
|
||||
if self.permission_checker.requires_confirmation(name):
|
||||
if self.permission_checker.requires_confirmation(name, payload):
|
||||
if not self.confirm_tool:
|
||||
return self.permission_checker.denial_reason(name, payload)
|
||||
if not self.confirm_tool(name, payload):
|
||||
|
||||
@ -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("Tool blocked in plan mode:")
|
||||
or output.startswith("Permission denied for tool:")
|
||||
or output.startswith("Bash command appears to target paths outside the workspace")
|
||||
output.startswith("当前处于 plan 模式,禁止执行工具:")
|
||||
or output.startswith("当前未批准工具执行:")
|
||||
or output.startswith("当前命令疑似指向工作区外路径,已拒绝执行 Bash")
|
||||
):
|
||||
console.print(f"[red]{output}[/red]")
|
||||
else:
|
||||
@ -106,24 +106,25 @@ 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"),
|
||||
cwd: Path = typer.Option(Path("."), help="工作区根目录"),
|
||||
workspace: Path = typer.Option(Path("."), "--workspace", 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:
|
||||
root = cwd.resolve()
|
||||
store = SessionStore(root)
|
||||
memory = MemoryStore(root)
|
||||
program_root = Path(__file__).resolve().parents[2]
|
||||
workspace_root = workspace.resolve()
|
||||
store = SessionStore(workspace_root)
|
||||
memory = MemoryStore(workspace_root)
|
||||
mode = ModeState()
|
||||
permissions = PermissionChecker(root, auto_approve=auto_approve)
|
||||
permissions = PermissionChecker(workspace_root, auto_approve=auto_approve)
|
||||
permissions.set_mode(mode.mode)
|
||||
if history:
|
||||
render_history(store)
|
||||
return
|
||||
|
||||
config = resolve_config(
|
||||
root,
|
||||
program_root,
|
||||
{
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
@ -142,16 +143,16 @@ def run(
|
||||
else:
|
||||
session_meta = store.create_session(config.model)
|
||||
|
||||
agent = build_agent(root, config, store, permissions, session_meta, restored_history)
|
||||
agent = build_agent(workspace_root, config, store, permissions, session_meta, restored_history)
|
||||
|
||||
if prompt:
|
||||
render_stream(agent, prompt)
|
||||
return
|
||||
|
||||
console.print("[bold cyan]cc-slim[/bold cyan] REPL,输入 exit 或 quit 退出。")
|
||||
console.print("[bold cyan]cc-slim[/bold cyan] REPL,输入 /help 查看命令,输入 exit 或 quit 退出。")
|
||||
while True:
|
||||
try:
|
||||
user_input = typer.prompt(">")
|
||||
user_input = typer.prompt(f"[{mode.mode}] >")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
console.print()
|
||||
break
|
||||
@ -166,7 +167,7 @@ def run(
|
||||
agent = handle_command(
|
||||
user_input,
|
||||
console=console,
|
||||
root=root,
|
||||
root=workspace_root,
|
||||
config=config,
|
||||
store=store,
|
||||
memory=memory,
|
||||
|
||||
@ -5,28 +5,115 @@ 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 append(self, text: str) -> Path:
|
||||
path = self.path()
|
||||
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:
|
||||
def ensure_structure(self) -> None:
|
||||
path = self.path()
|
||||
if not path.exists():
|
||||
return ""
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
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]
|
||||
|
||||
@ -13,23 +13,28 @@ class PermissionChecker:
|
||||
self.auto_allowed_tools = ["Read", "Glob", "Grep"]
|
||||
self.confirm_required_tools = ["Write", "Edit", "Bash"]
|
||||
|
||||
def is_allowed(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
|
||||
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 False
|
||||
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
|
||||
return False
|
||||
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)
|
||||
|
||||
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"Tool blocked in plan mode: {tool_name}"
|
||||
return f"当前处于 plan 模式,禁止执行工具: {tool_name}"
|
||||
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
|
||||
return "Bash command appears to target paths outside the workspace"
|
||||
return f"Permission denied for tool: {tool_name}"
|
||||
return "当前命令疑似指向工作区外路径,已拒绝执行 Bash"
|
||||
return f"当前未批准工具执行: {tool_name}"
|
||||
|
||||
def requires_confirmation(self, tool_name: str) -> bool:
|
||||
def requires_confirmation(self, tool_name: str, payload: dict[str, object] | None = None) -> 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:
|
||||
|
||||
70
src/cc_slim/system.md
Normal file
70
src/cc_slim/system.md
Normal file
@ -0,0 +1,70 @@
|
||||
# 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. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
|
||||
|
||||
## 输出风格
|
||||
|
||||
- 保持简洁、直接、可验证。
|
||||
- 优先给出事实,而不是泛泛解释。
|
||||
- 对不确定性保持诚实。
|
||||
@ -159,12 +159,20 @@ def grep_tool(workspace: Path, data: dict[str, Any]) -> str:
|
||||
return f"文件不存在: {target.relative_to(workspace)}"
|
||||
|
||||
results: list[str] = []
|
||||
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)}"
|
||||
if target.is_file():
|
||||
files = [target]
|
||||
else:
|
||||
files = target.rglob("*")
|
||||
|
||||
found_file = False
|
||||
|
||||
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
|
||||
@ -175,6 +183,8 @@ 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)
|
||||
|
||||
140
tests/test_dream.py
Normal file
140
tests/test_dream.py
Normal file
@ -0,0 +1,140 @@
|
||||
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 "已完成 dream,memory 已更新"
|
||||
|
||||
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 "已完成 dream,memory 已更新" 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 == "已完成 dream,memory 已更新"
|
||||
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
|
||||
159
tests/test_memory.py
Normal file
159
tests/test_memory.py
Normal file
@ -0,0 +1,159 @@
|
||||
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
332
uv.lock
@ -52,12 +52,22 @@ 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" },
|
||||
@ -72,6 +82,7 @@ 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" },
|
||||
@ -92,6 +103,95 @@ 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"
|
||||
@ -131,6 +231,18 @@ 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"
|
||||
@ -177,6 +289,18 @@ 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"
|
||||
@ -271,6 +395,25 @@ 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"
|
||||
@ -311,6 +454,88 @@ 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"
|
||||
@ -329,6 +554,21 @@ 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"
|
||||
@ -466,6 +706,21 @@ 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"
|
||||
@ -569,3 +824,80 @@ 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" },
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user