Claude Code Memory Compiler
This commit is contained in:
commit
f83d38d787
15 changed files with 2819 additions and 0 deletions
40
.claude/settings.json
Normal file
40
.claude/settings.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "uv run python hooks/session-start.py",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "uv run python hooks/pre-compact.py",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"matcher": "",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "uv run python hooks/session-end.py",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
|
||||
# Runtime state (regenerated by scripts)
|
||||
scripts/state.json
|
||||
scripts/last-flush.json
|
||||
scripts/flush.log
|
||||
scripts/compile.log
|
||||
scripts/session-flush-*
|
||||
scripts/flush-context-*
|
||||
|
||||
# Generated content (built up per-user by hooks and scripts)
|
||||
daily/
|
||||
knowledge/
|
||||
|
||||
# Reports (regenerated by lint)
|
||||
reports/
|
||||
|
||||
# uv lock (regenerated by uv sync)
|
||||
scripts/uv.lock
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
518
AGENTS.md
Normal file
518
AGENTS.md
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
# AGENTS.md - Personal Knowledge Base Schema
|
||||
|
||||
> Adapted from [Andrej Karpathy's LLM Knowledge Base](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) architecture.
|
||||
> Instead of ingesting external articles, this system compiles knowledge from your own AI conversations.
|
||||
|
||||
## The Compiler Analogy
|
||||
|
||||
```
|
||||
daily/ = source code (your conversations - the raw material)
|
||||
LLM = compiler (extracts and organizes knowledge)
|
||||
knowledge/ = executable (structured, queryable knowledge base)
|
||||
lint = test suite (health checks for consistency)
|
||||
queries = runtime (using the knowledge)
|
||||
```
|
||||
|
||||
You don't manually organize your knowledge. You have conversations, and the LLM handles the synthesis, cross-referencing, and maintenance.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Layer 1: `daily/` - Conversation Logs (Immutable Source)
|
||||
|
||||
Daily logs capture what happened in your AI coding sessions. These are the "raw sources" - append-only, never edited after the fact.
|
||||
|
||||
```
|
||||
daily/
|
||||
├── 2026-04-01.md
|
||||
├── 2026-04-02.md
|
||||
├── ...
|
||||
```
|
||||
|
||||
Each file follows this format:
|
||||
|
||||
```markdown
|
||||
# Daily Log: YYYY-MM-DD
|
||||
|
||||
## Sessions
|
||||
|
||||
### Session (HH:MM) - Brief Title
|
||||
|
||||
**Context:** What the user was working on.
|
||||
|
||||
**Key Exchanges:**
|
||||
- User asked about X, assistant explained Y
|
||||
- Decided to use Z approach because...
|
||||
- Discovered that W doesn't work when...
|
||||
|
||||
**Decisions Made:**
|
||||
- Chose library X over Y because...
|
||||
- Architecture: went with pattern Z
|
||||
|
||||
**Lessons Learned:**
|
||||
- Always do X before Y to avoid...
|
||||
- The gotcha with Z is that...
|
||||
|
||||
**Action Items:**
|
||||
- [ ] Follow up on X
|
||||
- [ ] Refactor Y when time permits
|
||||
```
|
||||
|
||||
### Layer 2: `knowledge/` - Compiled Knowledge (LLM-Owned)
|
||||
|
||||
The LLM owns this directory entirely. Humans read it but rarely edit it directly.
|
||||
|
||||
```
|
||||
knowledge/
|
||||
├── index.md # Master catalog - every article with one-line summary
|
||||
├── log.md # Append-only chronological build log
|
||||
├── concepts/ # Atomic knowledge articles
|
||||
├── connections/ # Cross-cutting insights linking 2+ concepts
|
||||
└── qa/ # Filed query answers (compounding knowledge)
|
||||
```
|
||||
|
||||
### Layer 3: This File (AGENTS.md)
|
||||
|
||||
The schema that tells the LLM how to compile and maintain the knowledge base. This is the "compiler specification."
|
||||
|
||||
---
|
||||
|
||||
## Structural Files
|
||||
|
||||
### `knowledge/index.md` - Master Catalog
|
||||
|
||||
A table listing every knowledge article. This is the primary retrieval mechanism - the LLM reads this FIRST when answering any query, then selects relevant articles to read in full.
|
||||
|
||||
Format:
|
||||
|
||||
```markdown
|
||||
# Knowledge Base Index
|
||||
|
||||
| Article | Summary | Compiled From | Updated |
|
||||
|---------|---------|---------------|---------|
|
||||
| [[concepts/supabase-auth]] | Row-level security patterns and JWT gotchas | daily/2026-04-02.md | 2026-04-02 |
|
||||
| [[connections/auth-and-webhooks]] | Token verification patterns shared across Supabase auth and Stripe webhooks | daily/2026-04-02.md, daily/2026-04-04.md | 2026-04-04 |
|
||||
```
|
||||
|
||||
### `knowledge/log.md` - Build Log
|
||||
|
||||
Append-only chronological record of every compile, query, and lint operation.
|
||||
|
||||
Format:
|
||||
|
||||
```markdown
|
||||
# Build Log
|
||||
|
||||
## [2026-04-01T14:30:00] compile | Daily Log 2026-04-01
|
||||
- Source: daily/2026-04-01.md
|
||||
- Articles created: [[concepts/nextjs-project-structure]], [[concepts/tailwind-setup]]
|
||||
- Articles updated: (none)
|
||||
|
||||
## [2026-04-02T09:00:00] query | "How do I handle auth redirects?"
|
||||
- Consulted: [[concepts/supabase-auth]], [[concepts/nextjs-middleware]]
|
||||
- Filed to: [[qa/auth-redirect-handling]]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Article Formats
|
||||
|
||||
### Concept Articles (`knowledge/concepts/`)
|
||||
|
||||
One article per atomic piece of knowledge. These are facts, patterns, decisions, preferences, and lessons extracted from your conversations.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Concept Name"
|
||||
aliases: [alternate-name, abbreviation]
|
||||
tags: [domain, topic]
|
||||
sources:
|
||||
- "daily/2026-04-01.md"
|
||||
- "daily/2026-04-03.md"
|
||||
created: 2026-04-01
|
||||
updated: 2026-04-03
|
||||
---
|
||||
|
||||
# Concept Name
|
||||
|
||||
[2-4 sentence core explanation]
|
||||
|
||||
## Key Points
|
||||
|
||||
- [Bullet points, each self-contained]
|
||||
|
||||
## Details
|
||||
|
||||
[Deeper explanation, encyclopedia-style paragraphs]
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[concepts/related-concept]] - How it connects
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-04-01.md]] - Initial discovery during project setup
|
||||
- [[daily/2026-04-03.md]] - Updated after debugging session
|
||||
```
|
||||
|
||||
### Connection Articles (`knowledge/connections/`)
|
||||
|
||||
Cross-cutting synthesis linking 2+ concepts. Created when a conversation reveals a non-obvious relationship.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Connection: X and Y"
|
||||
connects:
|
||||
- "concepts/concept-x"
|
||||
- "concepts/concept-y"
|
||||
sources:
|
||||
- "daily/2026-04-04.md"
|
||||
created: 2026-04-04
|
||||
updated: 2026-04-04
|
||||
---
|
||||
|
||||
# Connection: X and Y
|
||||
|
||||
## The Connection
|
||||
|
||||
[What links these concepts]
|
||||
|
||||
## Key Insight
|
||||
|
||||
[The non-obvious relationship discovered]
|
||||
|
||||
## Evidence
|
||||
|
||||
[Specific examples from conversations]
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[concepts/concept-x]]
|
||||
- [[concepts/concept-y]]
|
||||
```
|
||||
|
||||
### Q&A Articles (`knowledge/qa/`)
|
||||
|
||||
Filed answers from queries. Every complex question answered by the system can be permanently stored, making future queries smarter.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Q: Original Question"
|
||||
question: "The exact question asked"
|
||||
consulted:
|
||||
- "concepts/article-1"
|
||||
- "concepts/article-2"
|
||||
filed: 2026-04-05
|
||||
---
|
||||
|
||||
# Q: Original Question
|
||||
|
||||
## Answer
|
||||
|
||||
[The synthesized answer with [[wikilinks]] to sources]
|
||||
|
||||
## Sources Consulted
|
||||
|
||||
- [[concepts/article-1]] - Relevant because...
|
||||
- [[concepts/article-2]] - Provided context on...
|
||||
|
||||
## Follow-Up Questions
|
||||
|
||||
- What about edge case X?
|
||||
- How does this change if Y?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Operations
|
||||
|
||||
### 1. Compile (daily/ -> knowledge/)
|
||||
|
||||
When processing a daily log:
|
||||
|
||||
1. Read the daily log file
|
||||
2. Read `knowledge/index.md` to understand current knowledge state
|
||||
3. Read existing articles that may need updating
|
||||
4. For each piece of knowledge found in the log:
|
||||
- If an existing concept article covers this topic: UPDATE it with new information, add the daily log as a source
|
||||
- If it's a new topic: CREATE a new `concepts/` article
|
||||
5. If the log reveals a non-obvious connection between 2+ existing concepts: CREATE a `connections/` article
|
||||
6. UPDATE `knowledge/index.md` with new/modified entries
|
||||
7. APPEND to `knowledge/log.md`
|
||||
|
||||
**Important guidelines:**
|
||||
- A single daily log may touch 3-10 knowledge articles
|
||||
- Prefer updating existing articles over creating near-duplicates
|
||||
- Use Obsidian-style `[[wikilinks]]` with full relative paths from knowledge/
|
||||
- Write in encyclopedia style - factual, concise, self-contained
|
||||
- Every article must have YAML frontmatter
|
||||
- Every article must link back to its source daily logs
|
||||
|
||||
### 2. Query (Ask the Knowledge Base)
|
||||
|
||||
1. Read `knowledge/index.md` (the master catalog)
|
||||
2. Based on the question, identify 3-10 relevant articles from the index
|
||||
3. Read those articles in full
|
||||
4. Synthesize an answer with `[[wikilink]]` citations
|
||||
5. If `--file-back` is specified: create a `knowledge/qa/` article and update index.md and log.md
|
||||
|
||||
**Why this works without RAG:** At personal knowledge base scale (50-500 articles), the LLM reading a structured index outperforms cosine similarity. The LLM understands what the question is really asking and selects pages accordingly. Embeddings find similar words; the LLM finds relevant concepts.
|
||||
|
||||
### 3. Lint (Health Checks)
|
||||
|
||||
Seven checks, run periodically:
|
||||
|
||||
1. **Broken links** - `[[wikilinks]]` pointing to non-existent articles
|
||||
2. **Orphan pages** - Articles with zero inbound links from other articles
|
||||
3. **Orphan sources** - Daily logs that haven't been compiled yet
|
||||
4. **Stale articles** - Source daily log changed since article was last compiled
|
||||
5. **Contradictions** - Conflicting claims across articles (requires LLM judgment)
|
||||
6. **Missing backlinks** - A links to B but B doesn't link back to A
|
||||
7. **Sparse articles** - Below 200 words, likely incomplete
|
||||
|
||||
Output: a markdown report with severity levels (error, warning, suggestion).
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Wikilinks:** Use Obsidian-style `[[path/to/article]]` without `.md` extension
|
||||
- **Writing style:** Encyclopedia-style, factual, third-person where appropriate
|
||||
- **Dates:** ISO 8601 (YYYY-MM-DD for dates, full ISO for timestamps in log.md)
|
||||
- **File naming:** lowercase, hyphens for spaces (e.g., `supabase-row-level-security.md`)
|
||||
- **Frontmatter:** Every article must have YAML frontmatter with at minimum: title, sources, created, updated
|
||||
- **Sources:** Always link back to the daily log(s) that contributed to an article
|
||||
|
||||
---
|
||||
|
||||
## Full Project Structure
|
||||
|
||||
```
|
||||
llm-personal-kb/
|
||||
|-- .claude/
|
||||
| |-- settings.json # Hook configuration (auto-activates in Claude Code)
|
||||
|-- .gitignore # Excludes runtime state, temp files, caches
|
||||
|-- AGENTS.md # This file - schema + full technical reference
|
||||
|-- README.md # Concise overview + quick start
|
||||
|-- pyproject.toml # Dependencies (at root so hooks can find it)
|
||||
|-- daily/ # "Source code" - conversation logs (immutable)
|
||||
|-- knowledge/ # "Executable" - compiled knowledge (LLM-owned)
|
||||
| |-- index.md # Master catalog - THE retrieval mechanism
|
||||
| |-- log.md # Append-only build log
|
||||
| |-- concepts/ # Atomic knowledge articles
|
||||
| |-- connections/ # Cross-cutting insights linking 2+ concepts
|
||||
| |-- qa/ # Filed query answers (compounding knowledge)
|
||||
|-- scripts/ # CLI tools
|
||||
| |-- compile.py # Compile daily logs -> knowledge articles
|
||||
| |-- query.py # Ask questions (index-guided, no RAG)
|
||||
| |-- lint.py # 7 health checks
|
||||
| |-- flush.py # Extract memories from conversations (background)
|
||||
| |-- config.py # Path constants
|
||||
| |-- utils.py # Shared helpers
|
||||
|-- hooks/ # Claude Code hooks
|
||||
| |-- session-start.py # Injects knowledge into every session
|
||||
| |-- session-end.py # Extracts conversation -> daily log
|
||||
| |-- pre-compact.py # Safety net: captures context before compaction
|
||||
|-- reports/ # Lint reports (gitignored)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Hook System (Automatic Capture)
|
||||
|
||||
Hooks are configured in `.claude/settings.json` and fire automatically when you use Claude Code in this project.
|
||||
|
||||
### `.claude/settings.json` Format
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/session-start.py", "timeout": 15 }] }],
|
||||
"PreCompact": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/pre-compact.py", "timeout": 10 }] }],
|
||||
"SessionEnd": [{ "matcher": "", "hooks": [{ "type": "command", "command": "uv run python hooks/session-end.py", "timeout": 10 }] }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Commands use simple relative paths from the project root. Empty `matcher` catches all events.
|
||||
|
||||
### Hook Details
|
||||
|
||||
**`session-start.py`** (SessionStart)
|
||||
- Pure local I/O, no API calls, runs in under 1 second
|
||||
- Reads `knowledge/index.md` and the most recent daily log
|
||||
- Outputs JSON to stdout: `{"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": "..."}}`
|
||||
- Claude sees the knowledge base index at the start of every session
|
||||
- Max context: 20,000 characters
|
||||
|
||||
**`session-end.py`** (SessionEnd)
|
||||
- Reads hook input from stdin (JSON with `session_id`, `transcript_path`, `cwd`)
|
||||
- Copies the raw JSONL transcript to a temp file (no parsing in the hook - keeps it fast)
|
||||
- Spawns `flush.py` as a fully detached background process
|
||||
- Recursion guard: exits immediately if `CLAUDE_INVOKED_BY` env var is set
|
||||
|
||||
**`pre-compact.py`** (PreCompact)
|
||||
- Same architecture as session-end.py
|
||||
- Fires before Claude Code auto-compacts the context window
|
||||
- Guards against empty `transcript_path` (known Claude Code bug #13668)
|
||||
- Critical for long sessions: captures context before summarization discards it
|
||||
|
||||
**Why both PreCompact and SessionEnd?** Long-running sessions may trigger multiple auto-compactions before you close the session. Without PreCompact, intermediate context is lost to summarization before SessionEnd ever fires.
|
||||
|
||||
### Background Flush Process (`flush.py`)
|
||||
|
||||
Spawned by both hooks as a fully detached background process:
|
||||
- **Windows:** `CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS` flags
|
||||
- **Mac/Linux:** `start_new_session=True`
|
||||
|
||||
This ensures flush.py survives after Claude Code's hook process exits.
|
||||
|
||||
**What flush.py does:**
|
||||
1. Sets `CLAUDE_INVOKED_BY=memory_flush` env var (prevents recursive hook firing)
|
||||
2. Reads the pre-extracted conversation context from the temp `.md` file
|
||||
3. Skips if context is empty or if same session was flushed within 60 seconds (deduplication)
|
||||
4. Calls Claude Agent SDK (`query()` with `allowed_tools=[]`, `max_turns=2`)
|
||||
5. Claude decides what's worth saving - returns structured bullet points or `FLUSH_OK`
|
||||
6. Appends result to `daily/YYYY-MM-DD.md`
|
||||
7. Cleans up temp context file
|
||||
8. **End-of-day auto-compilation:** If it's past 6 PM local time (`COMPILE_AFTER_HOUR = 18`) and today's daily log has changed since its last compilation (hash comparison against `state.json`), spawns `compile.py` as another detached background process. This means compilation happens automatically once a day without needing a cron job or manual trigger.
|
||||
|
||||
### JSONL Transcript Format
|
||||
|
||||
Claude Code stores conversations as `.jsonl` files. Messages are nested under a `message` key:
|
||||
|
||||
```python
|
||||
entry = json.loads(line)
|
||||
msg = entry.get("message", {})
|
||||
role = msg.get("role", "") # "user" or "assistant"
|
||||
content = msg.get("content", "") # string or list of content blocks
|
||||
```
|
||||
|
||||
Content can be a string or a list of blocks (`{"type": "text", "text": "..."}` dicts).
|
||||
|
||||
---
|
||||
|
||||
## Script Details
|
||||
|
||||
### compile.py - The Compiler
|
||||
|
||||
Uses the Claude Agent SDK's async streaming `query()`:
|
||||
|
||||
```python
|
||||
async for message in query(
|
||||
prompt=compile_prompt,
|
||||
options=ClaudeAgentOptions(
|
||||
cwd=str(ROOT_DIR),
|
||||
system_prompt={"type": "preset", "preset": "claude_code"},
|
||||
allowed_tools=["Read", "Write", "Edit", "Glob", "Grep"],
|
||||
permission_mode="acceptEdits",
|
||||
max_turns=30,
|
||||
),
|
||||
):
|
||||
```
|
||||
|
||||
- Builds a prompt with: AGENTS.md schema, current index, all existing articles, and the daily log
|
||||
- Claude reads the daily log, decides what concepts to extract, and writes files directly
|
||||
- `permission_mode="acceptEdits"` auto-approves all file operations
|
||||
- Incremental: tracks SHA-256 hashes of daily logs in `state.json`, skips unchanged files
|
||||
- Cost: ~$0.45-0.65 per daily log (increases as KB grows)
|
||||
|
||||
**CLI:**
|
||||
```bash
|
||||
uv run python scripts/compile.py # compile new/changed only
|
||||
uv run python scripts/compile.py --all # force recompile everything
|
||||
uv run python scripts/compile.py --file daily/2026-04-01.md
|
||||
uv run python scripts/compile.py --dry-run
|
||||
```
|
||||
|
||||
### query.py - Index-Guided Retrieval
|
||||
|
||||
Loads the entire knowledge base into context (index + all articles). No RAG.
|
||||
|
||||
At personal KB scale (50-500 articles), the LLM reading a structured index outperforms vector similarity. The LLM understands what you're really asking; cosine similarity just finds similar words.
|
||||
|
||||
**CLI:**
|
||||
```bash
|
||||
uv run python scripts/query.py "What auth patterns do I use?"
|
||||
uv run python scripts/query.py "What's my error handling strategy?" --file-back
|
||||
```
|
||||
|
||||
With `--file-back`, creates a Q&A article in `knowledge/qa/` and updates the index and log. This is the compounding loop - every question makes the KB smarter.
|
||||
|
||||
### lint.py - Health Checks
|
||||
|
||||
Seven checks:
|
||||
|
||||
| Check | Type | Catches |
|
||||
|-------|------|---------|
|
||||
| Broken links | Structural | `[[wikilinks]]` to non-existent articles |
|
||||
| Orphan pages | Structural | Articles with zero inbound links |
|
||||
| Orphan sources | Structural | Daily logs not yet compiled |
|
||||
| Stale articles | Structural | Source logs changed since compilation |
|
||||
| Missing backlinks | Structural | A links to B but B doesn't link back |
|
||||
| Sparse articles | Structural | Under 200 words |
|
||||
| Contradictions | LLM | Conflicting claims across articles |
|
||||
|
||||
**CLI:**
|
||||
```bash
|
||||
uv run python scripts/lint.py # all checks
|
||||
uv run python scripts/lint.py --structural-only # skip LLM check (free)
|
||||
```
|
||||
|
||||
Reports saved to `reports/lint-YYYY-MM-DD.md`.
|
||||
|
||||
---
|
||||
|
||||
## State Tracking
|
||||
|
||||
`scripts/state.json` tracks:
|
||||
- `ingested` - map of daily log filenames to SHA-256 hashes, compilation timestamps, and costs
|
||||
- `query_count` - total queries run
|
||||
- `last_lint` - timestamp of most recent lint
|
||||
- `total_cost` - cumulative API cost
|
||||
|
||||
`scripts/last-flush.json` tracks flush deduplication (session_id + timestamp).
|
||||
|
||||
Both are gitignored and regenerated automatically.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
`pyproject.toml` (at project root):
|
||||
- `claude-agent-sdk>=0.1.29` - Claude Agent SDK for LLM calls with tool use
|
||||
- `python-dotenv>=1.0.0` - Environment variable management
|
||||
- `tzdata>=2024.1` - Timezone data
|
||||
- Python 3.12+, managed by [uv](https://docs.astral.sh/uv/)
|
||||
|
||||
No API key needed - uses Claude Code's built-in credentials at `~/.claude/.credentials.json`.
|
||||
|
||||
---
|
||||
|
||||
## Costs
|
||||
|
||||
| Operation | Cost |
|
||||
|-----------|------|
|
||||
| Compile one daily log | $0.45-0.65 |
|
||||
| Query (no file-back) | ~$0.15-0.25 |
|
||||
| Query (with file-back) | ~$0.25-0.40 |
|
||||
| Full lint (with contradictions) | ~$0.15-0.25 |
|
||||
| Structural lint only | $0.00 |
|
||||
| Memory flush (per session) | ~$0.02-0.05 |
|
||||
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
### Additional Article Types
|
||||
|
||||
Add directories like `people/`, `projects/`, `tools/` to `knowledge/`. Define the article format in this file (AGENTS.md) and update `utils.py`'s `list_wiki_articles()` to include them.
|
||||
|
||||
### Obsidian Integration
|
||||
|
||||
The knowledge base is pure markdown with `[[wikilinks]]` - works natively in Obsidian. Point a vault at `knowledge/` for graph view, backlinks, and search.
|
||||
|
||||
### Scaling Beyond Index-Guided Retrieval
|
||||
|
||||
At ~2,000+ articles / ~2M+ tokens, the index becomes too large for the context window. At that point, add hybrid RAG (keyword + semantic search) as a retrieval layer before the LLM. See Karpathy's recommendation of `qmd` by Tobi Lutke for search at scale.
|
||||
52
README.md
Normal file
52
README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# LLM Personal Knowledge Base
|
||||
|
||||
**Your AI conversations compile themselves into a searchable knowledge base.**
|
||||
|
||||
Adapted from [Karpathy's LLM Knowledge Base](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) architecture, but instead of clipping web articles, the raw data is your own conversations with Claude Code. When a session ends (or auto-compacts mid-session), Claude Code hooks capture the conversation transcript and spawn a background process that uses the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk) to extract the important stuff - decisions, lessons learned, patterns, gotchas - and appends it to a daily log. You then compile those daily logs into structured, cross-referenced knowledge articles organized by concept. Retrieval uses a simple index file instead of RAG - no vector database, no embeddings, just markdown.
|
||||
|
||||
Anthropic has clarified that personal use of the Claude Agent SDK is covered under your existing Claude subscription (Max, Team, or Enterprise) - no separate API credits needed. Unlike OpenClaw, which requires API billing for its memory flush, this runs on your subscription.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Tell your AI coding agent:
|
||||
|
||||
> "Clone https://github.com/coleam00/llm-personal-kb into this project. Set up the Claude Code hooks so my conversations automatically get captured into daily logs, compiled into a knowledge base, and injected back into future sessions. Read the AGENTS.md for the full technical reference on how everything works."
|
||||
|
||||
The agent will:
|
||||
1. Clone the repo and run `uv sync` to install dependencies
|
||||
2. Copy `.claude/settings.json` into your project (or merge the hooks into your existing settings)
|
||||
3. The hooks activate automatically next time you open Claude Code
|
||||
|
||||
From there, your conversations start accumulating. After 6 PM local time, the next session flush automatically triggers compilation of that day's logs into knowledge articles. You can also run `uv run python scripts/compile.py` manually at any time.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Conversation -> SessionEnd/PreCompact hooks -> flush.py extracts knowledge
|
||||
-> daily/YYYY-MM-DD.md -> compile.py -> knowledge/concepts/, connections/, qa/
|
||||
-> SessionStart hook injects index into next session -> cycle repeats
|
||||
```
|
||||
|
||||
- **Hooks** capture conversations automatically (session end + pre-compaction safety net)
|
||||
- **flush.py** calls the Claude Agent SDK to decide what's worth saving, and after 6 PM triggers end-of-day compilation automatically
|
||||
- **compile.py** turns daily logs into organized concept articles with cross-references (triggered automatically or run manually)
|
||||
- **query.py** answers questions using index-guided retrieval (no RAG needed at personal scale)
|
||||
- **lint.py** runs 7 health checks (broken links, orphans, contradictions, staleness)
|
||||
|
||||
## Key Commands
|
||||
|
||||
```bash
|
||||
uv run python scripts/compile.py # compile new daily logs
|
||||
uv run python scripts/query.py "question" # ask the knowledge base
|
||||
uv run python scripts/query.py "question" --file-back # ask + save answer back
|
||||
uv run python scripts/lint.py # run health checks
|
||||
uv run python scripts/lint.py --structural-only # free structural checks only
|
||||
```
|
||||
|
||||
## Why No RAG?
|
||||
|
||||
Karpathy's insight: at personal scale (50-500 articles), the LLM reading a structured `index.md` outperforms vector similarity. The LLM understands what you're really asking; cosine similarity just finds similar words. RAG becomes necessary at ~2,000+ articles when the index exceeds the context window.
|
||||
|
||||
## Technical Reference
|
||||
|
||||
See **[AGENTS.md](AGENTS.md)** for the complete technical reference: article formats, hook architecture, script internals, cross-platform details, costs, and customization options. AGENTS.md is designed to give an AI agent everything it needs to understand, modify, or rebuild the system.
|
||||
170
hooks/pre-compact.py
Normal file
170
hooks/pre-compact.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
"""
|
||||
PreCompact hook - captures conversation transcript before auto-compaction.
|
||||
|
||||
When Claude Code's context window fills up, it auto-compacts (summarizes and
|
||||
discards detail). This hook fires BEFORE that happens, extracting conversation
|
||||
context and spawning flush.py to extract knowledge that would otherwise
|
||||
be lost to summarization.
|
||||
|
||||
The hook itself does NO API calls - only local file I/O for speed (<10s).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Recursion guard
|
||||
if os.environ.get("CLAUDE_INVOKED_BY"):
|
||||
sys.exit(0)
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPTS_DIR = ROOT / "scripts"
|
||||
STATE_DIR = SCRIPTS_DIR
|
||||
|
||||
logging.basicConfig(
|
||||
filename=str(SCRIPTS_DIR / "flush.log"),
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s [pre-compact] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
MAX_TURNS = 30
|
||||
MAX_CONTEXT_CHARS = 15_000
|
||||
MIN_TURNS_TO_FLUSH = 5
|
||||
|
||||
|
||||
def extract_conversation_context(transcript_path: Path) -> tuple[str, int]:
|
||||
"""Read JSONL transcript and extract last ~N conversation turns as markdown."""
|
||||
turns: list[str] = []
|
||||
|
||||
with open(transcript_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
msg = entry.get("message", {})
|
||||
if isinstance(msg, dict):
|
||||
role = msg.get("role", "")
|
||||
content = msg.get("content", "")
|
||||
else:
|
||||
role = entry.get("role", "")
|
||||
content = entry.get("content", "")
|
||||
|
||||
if role not in ("user", "assistant"):
|
||||
continue
|
||||
|
||||
if isinstance(content, list):
|
||||
text_parts = []
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text_parts.append(block.get("text", ""))
|
||||
elif isinstance(block, str):
|
||||
text_parts.append(block)
|
||||
content = "\n".join(text_parts)
|
||||
|
||||
if isinstance(content, str) and content.strip():
|
||||
label = "User" if role == "user" else "Assistant"
|
||||
turns.append(f"**{label}:** {content.strip()}\n")
|
||||
|
||||
recent = turns[-MAX_TURNS:]
|
||||
context = "\n".join(recent)
|
||||
|
||||
if len(context) > MAX_CONTEXT_CHARS:
|
||||
context = context[-MAX_CONTEXT_CHARS:]
|
||||
boundary = context.find("\n**")
|
||||
if boundary > 0:
|
||||
context = context[boundary + 1 :]
|
||||
|
||||
return context, len(recent)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Read hook input from stdin
|
||||
try:
|
||||
raw_input = sys.stdin.read()
|
||||
try:
|
||||
hook_input: dict = json.loads(raw_input)
|
||||
except json.JSONDecodeError:
|
||||
fixed_input = re.sub(r'(?<!\\)\\(?!["\\])', r'\\\\', raw_input)
|
||||
hook_input = json.loads(fixed_input)
|
||||
except (json.JSONDecodeError, ValueError, EOFError) as e:
|
||||
logging.error("Failed to parse stdin: %s", e)
|
||||
return
|
||||
|
||||
session_id = hook_input.get("session_id", "unknown")
|
||||
transcript_path_str = hook_input.get("transcript_path", "")
|
||||
|
||||
logging.info("PreCompact fired: session=%s", session_id)
|
||||
|
||||
# transcript_path can be empty (known Claude Code bug #13668)
|
||||
if not transcript_path_str or not isinstance(transcript_path_str, str):
|
||||
logging.info("SKIP: no transcript path")
|
||||
return
|
||||
|
||||
transcript_path = Path(transcript_path_str)
|
||||
if not transcript_path.exists():
|
||||
logging.info("SKIP: transcript missing: %s", transcript_path_str)
|
||||
return
|
||||
|
||||
# Extract conversation context in the hook
|
||||
try:
|
||||
context, turn_count = extract_conversation_context(transcript_path)
|
||||
except Exception as e:
|
||||
logging.error("Context extraction failed: %s", e)
|
||||
return
|
||||
|
||||
if not context.strip():
|
||||
logging.info("SKIP: empty context")
|
||||
return
|
||||
|
||||
if turn_count < MIN_TURNS_TO_FLUSH:
|
||||
logging.info("SKIP: only %d turns (min %d)", turn_count, MIN_TURNS_TO_FLUSH)
|
||||
return
|
||||
|
||||
# Write context to a temp file for the background process
|
||||
timestamp = datetime.now(timezone.utc).astimezone().strftime("%Y%m%d-%H%M%S")
|
||||
context_file = STATE_DIR / f"flush-context-{session_id}-{timestamp}.md"
|
||||
context_file.write_text(context, encoding="utf-8")
|
||||
|
||||
# Spawn flush.py as a background process
|
||||
flush_script = SCRIPTS_DIR / "flush.py"
|
||||
|
||||
cmd = [
|
||||
"uv",
|
||||
"run",
|
||||
"--directory",
|
||||
str(ROOT),
|
||||
"python",
|
||||
str(flush_script),
|
||||
str(context_file),
|
||||
session_id,
|
||||
]
|
||||
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
||||
|
||||
try:
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=creation_flags,
|
||||
)
|
||||
logging.info("Spawned flush.py for session %s (%d turns, %d chars)", session_id, turn_count, len(context))
|
||||
except Exception as e:
|
||||
logging.error("Failed to spawn flush.py: %s", e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
174
hooks/session-end.py
Normal file
174
hooks/session-end.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""
|
||||
SessionEnd hook - captures conversation transcript for memory extraction.
|
||||
|
||||
When a Claude Code session ends, this hook reads the transcript path from
|
||||
stdin, extracts conversation context, and spawns flush.py as a background
|
||||
process to extract knowledge into the daily log.
|
||||
|
||||
The hook itself does NO API calls - only local file I/O for speed (<10s).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Recursion guard: if we were spawned by flush.py (which calls Agent SDK,
|
||||
# which runs Claude Code, which would fire this hook again), exit immediately.
|
||||
if os.environ.get("CLAUDE_INVOKED_BY"):
|
||||
sys.exit(0)
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DAILY_DIR = ROOT / "daily"
|
||||
SCRIPTS_DIR = ROOT / "scripts"
|
||||
STATE_DIR = SCRIPTS_DIR
|
||||
|
||||
logging.basicConfig(
|
||||
filename=str(SCRIPTS_DIR / "flush.log"),
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s [hook] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
MAX_TURNS = 30
|
||||
MAX_CONTEXT_CHARS = 15_000
|
||||
MIN_TURNS_TO_FLUSH = 1
|
||||
|
||||
|
||||
def extract_conversation_context(transcript_path: Path) -> tuple[str, int]:
|
||||
"""Read JSONL transcript and extract last ~N conversation turns as markdown."""
|
||||
turns: list[str] = []
|
||||
|
||||
with open(transcript_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
msg = entry.get("message", {})
|
||||
if isinstance(msg, dict):
|
||||
role = msg.get("role", "")
|
||||
content = msg.get("content", "")
|
||||
else:
|
||||
role = entry.get("role", "")
|
||||
content = entry.get("content", "")
|
||||
|
||||
if role not in ("user", "assistant"):
|
||||
continue
|
||||
|
||||
if isinstance(content, list):
|
||||
text_parts = []
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
text_parts.append(block.get("text", ""))
|
||||
elif isinstance(block, str):
|
||||
text_parts.append(block)
|
||||
content = "\n".join(text_parts)
|
||||
|
||||
if isinstance(content, str) and content.strip():
|
||||
label = "User" if role == "user" else "Assistant"
|
||||
turns.append(f"**{label}:** {content.strip()}\n")
|
||||
|
||||
recent = turns[-MAX_TURNS:]
|
||||
context = "\n".join(recent)
|
||||
|
||||
if len(context) > MAX_CONTEXT_CHARS:
|
||||
context = context[-MAX_CONTEXT_CHARS:]
|
||||
boundary = context.find("\n**")
|
||||
if boundary > 0:
|
||||
context = context[boundary + 1 :]
|
||||
|
||||
return context, len(recent)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Read hook input from stdin
|
||||
# Claude Code on Windows may pass paths with unescaped backslashes
|
||||
try:
|
||||
raw_input = sys.stdin.read()
|
||||
try:
|
||||
hook_input: dict = json.loads(raw_input)
|
||||
except json.JSONDecodeError:
|
||||
fixed_input = re.sub(r'(?<!\\)\\(?!["\\])', r'\\\\', raw_input)
|
||||
hook_input = json.loads(fixed_input)
|
||||
except (json.JSONDecodeError, ValueError, EOFError) as e:
|
||||
logging.error("Failed to parse stdin: %s", e)
|
||||
return
|
||||
|
||||
session_id = hook_input.get("session_id", "unknown")
|
||||
source = hook_input.get("source", "unknown")
|
||||
transcript_path_str = hook_input.get("transcript_path", "")
|
||||
|
||||
logging.info("SessionEnd fired: session=%s source=%s", session_id, source)
|
||||
|
||||
if not transcript_path_str or not isinstance(transcript_path_str, str):
|
||||
logging.info("SKIP: no transcript path")
|
||||
return
|
||||
|
||||
transcript_path = Path(transcript_path_str)
|
||||
if not transcript_path.exists():
|
||||
logging.info("SKIP: transcript missing: %s", transcript_path_str)
|
||||
return
|
||||
|
||||
# Extract conversation context in the hook (fast, no API calls)
|
||||
try:
|
||||
context, turn_count = extract_conversation_context(transcript_path)
|
||||
except Exception as e:
|
||||
logging.error("Context extraction failed: %s", e)
|
||||
return
|
||||
|
||||
if not context.strip():
|
||||
logging.info("SKIP: empty context")
|
||||
return
|
||||
|
||||
if turn_count < MIN_TURNS_TO_FLUSH:
|
||||
logging.info("SKIP: only %d turns (min %d)", turn_count, MIN_TURNS_TO_FLUSH)
|
||||
return
|
||||
|
||||
# Write context to a temp file for the background process
|
||||
timestamp = datetime.now(timezone.utc).astimezone().strftime("%Y%m%d-%H%M%S")
|
||||
context_file = STATE_DIR / f"session-flush-{session_id}-{timestamp}.md"
|
||||
context_file.write_text(context, encoding="utf-8")
|
||||
|
||||
# Spawn flush.py as a background process
|
||||
flush_script = SCRIPTS_DIR / "flush.py"
|
||||
|
||||
cmd = [
|
||||
"uv",
|
||||
"run",
|
||||
"--directory",
|
||||
str(ROOT),
|
||||
"python",
|
||||
str(flush_script),
|
||||
str(context_file),
|
||||
session_id,
|
||||
]
|
||||
|
||||
# On Windows, use CREATE_NO_WINDOW to avoid flash console window.
|
||||
# Do NOT use DETACHED_PROCESS — it breaks the Agent SDK's subprocess I/O.
|
||||
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
||||
|
||||
try:
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=creation_flags,
|
||||
)
|
||||
logging.info("Spawned flush.py for session %s (%d turns, %d chars)", session_id, turn_count, len(context))
|
||||
except Exception as e:
|
||||
logging.error("Failed to spawn flush.py: %s", e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
92
hooks/session-start.py
Normal file
92
hooks/session-start.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""
|
||||
SessionStart hook - injects knowledge base context into every conversation.
|
||||
|
||||
This is the "context injection" layer. When Claude Code starts a session,
|
||||
this hook reads the knowledge base index and recent daily log, then injects
|
||||
them as additional context so Claude always "remembers" what it has learned.
|
||||
|
||||
Configure in .claude/settings.json:
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"matcher": "",
|
||||
"command": "uv run python hooks/session-start.py"
|
||||
}]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Paths relative to project root
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
KNOWLEDGE_DIR = ROOT / "knowledge"
|
||||
DAILY_DIR = ROOT / "daily"
|
||||
INDEX_FILE = KNOWLEDGE_DIR / "index.md"
|
||||
|
||||
MAX_CONTEXT_CHARS = 20_000
|
||||
MAX_LOG_LINES = 30
|
||||
|
||||
|
||||
def get_recent_log() -> str:
|
||||
"""Read the most recent daily log (today or yesterday)."""
|
||||
today = datetime.now(timezone.utc).astimezone()
|
||||
|
||||
for offset in range(2):
|
||||
date = today - timedelta(days=offset)
|
||||
log_path = DAILY_DIR / f"{date.strftime('%Y-%m-%d')}.md"
|
||||
if log_path.exists():
|
||||
lines = log_path.read_text(encoding="utf-8").splitlines()
|
||||
# Return last N lines to keep context small
|
||||
recent = lines[-MAX_LOG_LINES:] if len(lines) > MAX_LOG_LINES else lines
|
||||
return "\n".join(recent)
|
||||
|
||||
return "(no recent daily log)"
|
||||
|
||||
|
||||
def build_context() -> str:
|
||||
"""Assemble the context to inject into the conversation."""
|
||||
parts = []
|
||||
|
||||
# Today's date
|
||||
today = datetime.now(timezone.utc).astimezone()
|
||||
parts.append(f"## Today\n{today.strftime('%A, %B %d, %Y')}")
|
||||
|
||||
# Knowledge base index (the core retrieval mechanism)
|
||||
if INDEX_FILE.exists():
|
||||
index_content = INDEX_FILE.read_text(encoding="utf-8")
|
||||
parts.append(f"## Knowledge Base Index\n\n{index_content}")
|
||||
else:
|
||||
parts.append("## Knowledge Base Index\n\n(empty - no articles compiled yet)")
|
||||
|
||||
# Recent daily log
|
||||
recent_log = get_recent_log()
|
||||
parts.append(f"## Recent Daily Log\n\n{recent_log}")
|
||||
|
||||
context = "\n\n---\n\n".join(parts)
|
||||
|
||||
# Truncate if too long
|
||||
if len(context) > MAX_CONTEXT_CHARS:
|
||||
context = context[:MAX_CONTEXT_CHARS] + "\n\n...(truncated)"
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def main():
|
||||
context = build_context()
|
||||
|
||||
output = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": context,
|
||||
}
|
||||
}
|
||||
|
||||
print(json.dumps(output))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[project]
|
||||
name = "llm-personal-kb"
|
||||
version = "0.1.0"
|
||||
description = "Personal knowledge base compiled from AI conversations - inspired by Karpathy's LLM KB architecture"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"claude-agent-sdk>=0.1.29",
|
||||
"python-dotenv>=1.0.0",
|
||||
"tzdata>=2024.1",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
224
scripts/compile.py
Normal file
224
scripts/compile.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
"""
|
||||
Compile daily conversation logs into structured knowledge articles.
|
||||
|
||||
This is the "LLM compiler" - it reads daily logs (source code) and produces
|
||||
organized knowledge articles (the executable).
|
||||
|
||||
Usage:
|
||||
uv run python compile.py # compile new/changed logs only
|
||||
uv run python compile.py --all # force recompile everything
|
||||
uv run python compile.py --file daily/2026-04-01.md # compile a specific log
|
||||
uv run python compile.py --dry-run # show what would be compiled
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from config import AGENTS_FILE, CONCEPTS_DIR, CONNECTIONS_DIR, DAILY_DIR, KNOWLEDGE_DIR, now_iso
|
||||
from utils import (
|
||||
file_hash,
|
||||
list_raw_files,
|
||||
list_wiki_articles,
|
||||
load_state,
|
||||
read_wiki_index,
|
||||
save_state,
|
||||
)
|
||||
|
||||
# ── Paths for the LLM to use ──────────────────────────────────────────
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
async def compile_daily_log(log_path: Path, state: dict) -> float:
|
||||
"""Compile a single daily log into knowledge articles.
|
||||
|
||||
Returns the API cost of the compilation.
|
||||
"""
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
query,
|
||||
)
|
||||
|
||||
log_content = log_path.read_text(encoding="utf-8")
|
||||
schema = AGENTS_FILE.read_text(encoding="utf-8")
|
||||
wiki_index = read_wiki_index()
|
||||
|
||||
# Read existing articles for context
|
||||
existing_articles_context = ""
|
||||
existing = {}
|
||||
for article_path in list_wiki_articles():
|
||||
rel = article_path.relative_to(KNOWLEDGE_DIR)
|
||||
existing[str(rel)] = article_path.read_text(encoding="utf-8")
|
||||
|
||||
if existing:
|
||||
parts = []
|
||||
for rel_path, content in existing.items():
|
||||
parts.append(f"### {rel_path}\n```markdown\n{content}\n```")
|
||||
existing_articles_context = "\n\n".join(parts)
|
||||
|
||||
timestamp = now_iso()
|
||||
|
||||
prompt = f"""You are a knowledge compiler. Your job is to read a daily conversation log
|
||||
and extract knowledge into structured wiki articles.
|
||||
|
||||
## Schema (AGENTS.md)
|
||||
|
||||
{schema}
|
||||
|
||||
## Current Wiki Index
|
||||
|
||||
{wiki_index}
|
||||
|
||||
## Existing Wiki Articles
|
||||
|
||||
{existing_articles_context if existing_articles_context else "(No existing articles yet)"}
|
||||
|
||||
## Daily Log to Compile
|
||||
|
||||
**File:** {log_path.name}
|
||||
|
||||
{log_content}
|
||||
|
||||
## Your Task
|
||||
|
||||
Read the daily log above and compile it into wiki articles following the schema exactly.
|
||||
|
||||
### Rules:
|
||||
|
||||
1. **Extract key concepts** - Identify 3-7 distinct concepts worth their own article
|
||||
2. **Create concept articles** in `knowledge/concepts/` - One .md file per concept
|
||||
- Use the exact article format from AGENTS.md (YAML frontmatter + sections)
|
||||
- Include `sources:` in frontmatter pointing to the daily log file
|
||||
- Use `[[concepts/slug]]` wikilinks to link to related concepts
|
||||
- Write in encyclopedia style - neutral, comprehensive
|
||||
3. **Create connection articles** in `knowledge/connections/` if this log reveals non-obvious
|
||||
relationships between 2+ existing concepts
|
||||
4. **Update existing articles** if this log adds new information to concepts already in the wiki
|
||||
- Read the existing article, add the new information, add the source to frontmatter
|
||||
5. **Update knowledge/index.md** - Add new entries to the table
|
||||
- Each entry: `| [[path/slug]] | One-line summary | source-file | {timestamp[:10]} |`
|
||||
6. **Append to knowledge/log.md** - Add a timestamped entry:
|
||||
```
|
||||
## [{timestamp}] compile | {log_path.name}
|
||||
- Source: daily/{log_path.name}
|
||||
- Articles created: [[concepts/x]], [[concepts/y]]
|
||||
- Articles updated: [[concepts/z]] (if any)
|
||||
```
|
||||
|
||||
### File paths:
|
||||
- Write concept articles to: {CONCEPTS_DIR}
|
||||
- Write connection articles to: {CONNECTIONS_DIR}
|
||||
- Update index at: {KNOWLEDGE_DIR / 'index.md'}
|
||||
- Append log at: {KNOWLEDGE_DIR / 'log.md'}
|
||||
|
||||
### Quality standards:
|
||||
- Every article must have complete YAML frontmatter
|
||||
- Every article must link to at least 2 other articles via [[wikilinks]]
|
||||
- Key Points section should have 3-5 bullet points
|
||||
- Details section should have 2+ paragraphs
|
||||
- Related Concepts section should have 2+ entries
|
||||
- Sources section should cite the daily log with specific claims extracted
|
||||
"""
|
||||
|
||||
cost = 0.0
|
||||
|
||||
try:
|
||||
async for message in query(
|
||||
prompt=prompt,
|
||||
options=ClaudeAgentOptions(
|
||||
cwd=str(ROOT_DIR),
|
||||
system_prompt={"type": "preset", "preset": "claude_code"},
|
||||
allowed_tools=["Read", "Write", "Edit", "Glob", "Grep"],
|
||||
permission_mode="acceptEdits",
|
||||
max_turns=30,
|
||||
),
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
pass # compilation output - LLM writes files directly
|
||||
elif isinstance(message, ResultMessage):
|
||||
cost = message.total_cost_usd or 0.0
|
||||
print(f" Cost: ${cost:.4f}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
return 0.0
|
||||
|
||||
# Update state
|
||||
rel_path = log_path.name
|
||||
state.setdefault("ingested", {})[rel_path] = {
|
||||
"hash": file_hash(log_path),
|
||||
"compiled_at": now_iso(),
|
||||
"cost_usd": cost,
|
||||
}
|
||||
state["total_cost"] = state.get("total_cost", 0.0) + cost
|
||||
save_state(state)
|
||||
|
||||
return cost
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compile daily logs into knowledge articles")
|
||||
parser.add_argument("--all", action="store_true", help="Force recompile all logs")
|
||||
parser.add_argument("--file", type=str, help="Compile a specific daily log file")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show what would be compiled")
|
||||
args = parser.parse_args()
|
||||
|
||||
state = load_state()
|
||||
|
||||
# Determine which files to compile
|
||||
if args.file:
|
||||
target = Path(args.file)
|
||||
if not target.is_absolute():
|
||||
target = DAILY_DIR / target.name
|
||||
if not target.exists():
|
||||
# Try resolving relative to project root
|
||||
target = ROOT_DIR / args.file
|
||||
if not target.exists():
|
||||
print(f"Error: {args.file} not found")
|
||||
sys.exit(1)
|
||||
to_compile = [target]
|
||||
else:
|
||||
all_logs = list_raw_files()
|
||||
if args.all:
|
||||
to_compile = all_logs
|
||||
else:
|
||||
to_compile = []
|
||||
for log_path in all_logs:
|
||||
rel = log_path.name
|
||||
prev = state.get("ingested", {}).get(rel, {})
|
||||
if not prev or prev.get("hash") != file_hash(log_path):
|
||||
to_compile.append(log_path)
|
||||
|
||||
if not to_compile:
|
||||
print("Nothing to compile - all daily logs are up to date.")
|
||||
return
|
||||
|
||||
print(f"{'[DRY RUN] ' if args.dry_run else ''}Files to compile ({len(to_compile)}):")
|
||||
for f in to_compile:
|
||||
print(f" - {f.name}")
|
||||
|
||||
if args.dry_run:
|
||||
return
|
||||
|
||||
# Compile each file sequentially
|
||||
total_cost = 0.0
|
||||
for i, log_path in enumerate(to_compile, 1):
|
||||
print(f"\n[{i}/{len(to_compile)}] Compiling {log_path.name}...")
|
||||
cost = asyncio.run(compile_daily_log(log_path, state))
|
||||
total_cost += cost
|
||||
print(f" Done.")
|
||||
|
||||
articles = list_wiki_articles()
|
||||
print(f"\nCompilation complete. Total cost: ${total_cost:.2f}")
|
||||
print(f"Knowledge base: {len(articles)} articles")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
33
scripts/config.py
Normal file
33
scripts/config.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Path constants and configuration for the personal knowledge base."""
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
DAILY_DIR = ROOT_DIR / "daily"
|
||||
KNOWLEDGE_DIR = ROOT_DIR / "knowledge"
|
||||
CONCEPTS_DIR = KNOWLEDGE_DIR / "concepts"
|
||||
CONNECTIONS_DIR = KNOWLEDGE_DIR / "connections"
|
||||
QA_DIR = KNOWLEDGE_DIR / "qa"
|
||||
REPORTS_DIR = ROOT_DIR / "reports"
|
||||
SCRIPTS_DIR = ROOT_DIR / "scripts"
|
||||
HOOKS_DIR = ROOT_DIR / "hooks"
|
||||
AGENTS_FILE = ROOT_DIR / "AGENTS.md"
|
||||
|
||||
INDEX_FILE = KNOWLEDGE_DIR / "index.md"
|
||||
LOG_FILE = KNOWLEDGE_DIR / "log.md"
|
||||
STATE_FILE = SCRIPTS_DIR / "state.json"
|
||||
|
||||
# ── Timezone ───────────────────────────────────────────────────────────
|
||||
TIMEZONE = "America/Chicago"
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
"""Current time in ISO 8601 format."""
|
||||
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def today_iso() -> str:
|
||||
"""Current date in ISO 8601 format."""
|
||||
return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d")
|
||||
255
scripts/flush.py
Normal file
255
scripts/flush.py
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
"""
|
||||
Memory flush agent - extracts important knowledge from conversation context.
|
||||
|
||||
Spawned by session-end.py or pre-compact.py as a background process. Reads
|
||||
pre-extracted conversation context from a .md file, uses the Claude Agent SDK
|
||||
to decide what's worth saving, and appends the result to today's daily log.
|
||||
|
||||
Usage:
|
||||
uv run python flush.py <context_file.md> <session_id>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Recursion prevention: set this BEFORE any imports that might trigger Claude
|
||||
import os
|
||||
os.environ["CLAUDE_INVOKED_BY"] = "memory_flush"
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DAILY_DIR = ROOT / "daily"
|
||||
SCRIPTS_DIR = ROOT / "scripts"
|
||||
STATE_FILE = SCRIPTS_DIR / "last-flush.json"
|
||||
LOG_FILE = SCRIPTS_DIR / "flush.log"
|
||||
|
||||
# Set up file-based logging so we can verify the background process ran.
|
||||
# The parent process sends stdout/stderr to DEVNULL (to avoid the inherited
|
||||
# file handle bug on Windows), so this is our only observability channel.
|
||||
logging.basicConfig(
|
||||
filename=str(LOG_FILE),
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def load_flush_state() -> dict:
|
||||
if STATE_FILE.exists():
|
||||
try:
|
||||
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def save_flush_state(state: dict) -> None:
|
||||
STATE_FILE.write_text(json.dumps(state), encoding="utf-8")
|
||||
|
||||
|
||||
def append_to_daily_log(content: str, section: str = "Session") -> None:
|
||||
"""Append content to today's daily log."""
|
||||
today = datetime.now(timezone.utc).astimezone()
|
||||
log_path = DAILY_DIR / f"{today.strftime('%Y-%m-%d')}.md"
|
||||
|
||||
if not log_path.exists():
|
||||
DAILY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_path.write_text(
|
||||
f"# Daily Log: {today.strftime('%Y-%m-%d')}\n\n## Sessions\n\n## Memory Maintenance\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
time_str = today.strftime("%H:%M")
|
||||
entry = f"### {section} ({time_str})\n\n{content}\n\n"
|
||||
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
f.write(entry)
|
||||
|
||||
|
||||
async def run_flush(context: str) -> str:
|
||||
"""Use Claude Agent SDK to extract important knowledge from conversation context."""
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
query,
|
||||
)
|
||||
|
||||
prompt = f"""Review the conversation context below and respond with a concise summary
|
||||
of important items that should be preserved in the daily log.
|
||||
Do NOT use any tools — just return plain text.
|
||||
|
||||
Format your response as a structured daily log entry with these sections:
|
||||
|
||||
**Context:** [One line about what the user was working on]
|
||||
|
||||
**Key Exchanges:**
|
||||
- [Important Q&A or discussions]
|
||||
|
||||
**Decisions Made:**
|
||||
- [Any decisions with rationale]
|
||||
|
||||
**Lessons Learned:**
|
||||
- [Gotchas, patterns, or insights discovered]
|
||||
|
||||
**Action Items:**
|
||||
- [Follow-ups or TODOs mentioned]
|
||||
|
||||
Skip anything that is:
|
||||
- Routine tool calls or file reads
|
||||
- Content that's trivial or obvious
|
||||
- Trivial back-and-forth or clarification exchanges
|
||||
|
||||
Only include sections that have actual content. If nothing is worth saving,
|
||||
respond with exactly: FLUSH_OK
|
||||
|
||||
## Conversation Context
|
||||
|
||||
{context}"""
|
||||
|
||||
response = ""
|
||||
|
||||
try:
|
||||
async for message in query(
|
||||
prompt=prompt,
|
||||
options=ClaudeAgentOptions(
|
||||
cwd=str(ROOT),
|
||||
allowed_tools=[],
|
||||
max_turns=2,
|
||||
),
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
response += block.text
|
||||
elif isinstance(message, ResultMessage):
|
||||
pass
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logging.error("Agent SDK error: %s\n%s", e, traceback.format_exc())
|
||||
response = f"FLUSH_ERROR: {type(e).__name__}: {e}"
|
||||
|
||||
return response
|
||||
|
||||
|
||||
COMPILE_AFTER_HOUR = 18 # 6 PM local time
|
||||
|
||||
|
||||
def maybe_trigger_compilation() -> None:
|
||||
"""If it's past the compile hour and today's log hasn't been compiled, run compile.py."""
|
||||
import subprocess as _sp
|
||||
|
||||
now = datetime.now(timezone.utc).astimezone()
|
||||
if now.hour < COMPILE_AFTER_HOUR:
|
||||
return
|
||||
|
||||
# Check if today's log has already been compiled
|
||||
today_log = f"{now.strftime('%Y-%m-%d')}.md"
|
||||
compile_state_file = SCRIPTS_DIR / "state.json"
|
||||
if compile_state_file.exists():
|
||||
try:
|
||||
compile_state = json.loads(compile_state_file.read_text(encoding="utf-8"))
|
||||
ingested = compile_state.get("ingested", {})
|
||||
if today_log in ingested:
|
||||
# Already compiled today - check if the log has changed since
|
||||
from hashlib import sha256
|
||||
log_path = DAILY_DIR / today_log
|
||||
if log_path.exists():
|
||||
current_hash = sha256(log_path.read_bytes()).hexdigest()[:16]
|
||||
if ingested[today_log].get("hash") == current_hash:
|
||||
return # log unchanged since last compile
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
compile_script = SCRIPTS_DIR / "compile.py"
|
||||
if not compile_script.exists():
|
||||
return
|
||||
|
||||
logging.info("End-of-day compilation triggered (after %d:00)", COMPILE_AFTER_HOUR)
|
||||
|
||||
cmd = ["uv", "run", "--directory", str(ROOT), "python", str(compile_script)]
|
||||
|
||||
kwargs: dict = {}
|
||||
if sys.platform == "win32":
|
||||
kwargs["creationflags"] = _sp.CREATE_NEW_PROCESS_GROUP | _sp.DETACHED_PROCESS
|
||||
else:
|
||||
kwargs["start_new_session"] = True
|
||||
|
||||
try:
|
||||
log_handle = open(str(SCRIPTS_DIR / "compile.log"), "a")
|
||||
_sp.Popen(cmd, stdout=log_handle, stderr=_sp.STDOUT, cwd=str(ROOT), **kwargs)
|
||||
except Exception as e:
|
||||
logging.error("Failed to spawn compile.py: %s", e)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
logging.error("Usage: %s <context_file.md> <session_id>", sys.argv[0])
|
||||
sys.exit(1)
|
||||
|
||||
context_file = Path(sys.argv[1])
|
||||
session_id = sys.argv[2]
|
||||
|
||||
logging.info("flush.py started for session %s, context: %s", session_id, context_file)
|
||||
|
||||
if not context_file.exists():
|
||||
logging.error("Context file not found: %s", context_file)
|
||||
return
|
||||
|
||||
# Deduplication: skip if same session was flushed within 60 seconds
|
||||
state = load_flush_state()
|
||||
if (
|
||||
state.get("session_id") == session_id
|
||||
and time.time() - state.get("timestamp", 0) < 60
|
||||
):
|
||||
logging.info("Skipping duplicate flush for session %s", session_id)
|
||||
context_file.unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
# Read pre-extracted context
|
||||
context = context_file.read_text(encoding="utf-8").strip()
|
||||
if not context:
|
||||
logging.info("Context file is empty, skipping")
|
||||
context_file.unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
logging.info("Flushing session %s: %d chars", session_id, len(context))
|
||||
|
||||
# Run the LLM extraction
|
||||
response = asyncio.run(run_flush(context))
|
||||
|
||||
# Append to daily log
|
||||
if "FLUSH_OK" in response:
|
||||
logging.info("Result: FLUSH_OK")
|
||||
append_to_daily_log(
|
||||
"FLUSH_OK - Nothing worth saving from this session", "Memory Flush"
|
||||
)
|
||||
elif "FLUSH_ERROR" in response:
|
||||
logging.error("Result: %s", response)
|
||||
append_to_daily_log(response, "Memory Flush")
|
||||
else:
|
||||
logging.info("Result: saved to daily log (%d chars)", len(response))
|
||||
append_to_daily_log(response, "Session")
|
||||
|
||||
# Update dedup state
|
||||
save_flush_state({"session_id": session_id, "timestamp": time.time()})
|
||||
|
||||
# Clean up context file
|
||||
context_file.unlink(missing_ok=True)
|
||||
|
||||
# End-of-day auto-compilation: if it's past the compile hour and today's
|
||||
# log hasn't been compiled yet, trigger compile.py in the background.
|
||||
maybe_trigger_compilation()
|
||||
|
||||
logging.info("Flush complete for session %s", session_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
312
scripts/lint.py
Normal file
312
scripts/lint.py
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
"""
|
||||
Lint the knowledge base for structural and semantic health.
|
||||
|
||||
Runs 7 checks: broken links, orphan pages, orphan sources, stale articles,
|
||||
contradictions (LLM), missing backlinks, and sparse articles.
|
||||
|
||||
Usage:
|
||||
uv run python lint.py # all checks
|
||||
uv run python lint.py --structural-only # skip LLM checks (faster, cheaper)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from config import KNOWLEDGE_DIR, REPORTS_DIR, now_iso, today_iso
|
||||
from utils import (
|
||||
count_inbound_links,
|
||||
extract_wikilinks,
|
||||
file_hash,
|
||||
get_article_word_count,
|
||||
list_raw_files,
|
||||
list_wiki_articles,
|
||||
load_state,
|
||||
read_all_wiki_content,
|
||||
save_state,
|
||||
wiki_article_exists,
|
||||
)
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def check_broken_links() -> list[dict]:
|
||||
"""Check for [[wikilinks]] that point to non-existent articles."""
|
||||
issues = []
|
||||
for article in list_wiki_articles():
|
||||
content = article.read_text(encoding="utf-8")
|
||||
rel = article.relative_to(KNOWLEDGE_DIR)
|
||||
for link in extract_wikilinks(content):
|
||||
if link.startswith("daily/"):
|
||||
continue # daily log references are valid
|
||||
if not wiki_article_exists(link):
|
||||
issues.append({
|
||||
"severity": "error",
|
||||
"check": "broken_link",
|
||||
"file": str(rel),
|
||||
"detail": f"Broken link: [[{link}]] - target does not exist",
|
||||
})
|
||||
return issues
|
||||
|
||||
|
||||
def check_orphan_pages() -> list[dict]:
|
||||
"""Check for articles with zero inbound links."""
|
||||
issues = []
|
||||
for article in list_wiki_articles():
|
||||
rel = article.relative_to(KNOWLEDGE_DIR)
|
||||
link_target = str(rel).replace(".md", "").replace("\\", "/")
|
||||
inbound = count_inbound_links(link_target)
|
||||
if inbound == 0:
|
||||
issues.append({
|
||||
"severity": "warning",
|
||||
"check": "orphan_page",
|
||||
"file": str(rel),
|
||||
"detail": f"Orphan page: no other articles link to [[{link_target}]]",
|
||||
})
|
||||
return issues
|
||||
|
||||
|
||||
def check_orphan_sources() -> list[dict]:
|
||||
"""Check for daily logs that haven't been compiled yet."""
|
||||
state = load_state()
|
||||
ingested = state.get("ingested", {})
|
||||
issues = []
|
||||
for log_path in list_raw_files():
|
||||
if log_path.name not in ingested:
|
||||
issues.append({
|
||||
"severity": "warning",
|
||||
"check": "orphan_source",
|
||||
"file": f"daily/{log_path.name}",
|
||||
"detail": f"Uncompiled daily log: {log_path.name} has not been ingested",
|
||||
})
|
||||
return issues
|
||||
|
||||
|
||||
def check_stale_articles() -> list[dict]:
|
||||
"""Check if source daily logs have changed since compilation."""
|
||||
state = load_state()
|
||||
ingested = state.get("ingested", {})
|
||||
issues = []
|
||||
for log_path in list_raw_files():
|
||||
rel = log_path.name
|
||||
if rel in ingested:
|
||||
stored_hash = ingested[rel].get("hash", "")
|
||||
current_hash = file_hash(log_path)
|
||||
if stored_hash != current_hash:
|
||||
issues.append({
|
||||
"severity": "warning",
|
||||
"check": "stale_article",
|
||||
"file": f"daily/{rel}",
|
||||
"detail": f"Stale: {rel} has changed since last compilation",
|
||||
})
|
||||
return issues
|
||||
|
||||
|
||||
def check_missing_backlinks() -> list[dict]:
|
||||
"""Check for asymmetric links: A links to B but B doesn't link to A."""
|
||||
issues = []
|
||||
for article in list_wiki_articles():
|
||||
content = article.read_text(encoding="utf-8")
|
||||
rel = article.relative_to(KNOWLEDGE_DIR)
|
||||
source_link = str(rel).replace(".md", "").replace("\\", "/")
|
||||
|
||||
for link in extract_wikilinks(content):
|
||||
if link.startswith("daily/"):
|
||||
continue
|
||||
target_path = KNOWLEDGE_DIR / f"{link}.md"
|
||||
if target_path.exists():
|
||||
target_content = target_path.read_text(encoding="utf-8")
|
||||
if f"[[{source_link}]]" not in target_content:
|
||||
issues.append({
|
||||
"severity": "suggestion",
|
||||
"check": "missing_backlink",
|
||||
"file": str(rel),
|
||||
"detail": f"[[{source_link}]] links to [[{link}]] but not vice versa",
|
||||
"auto_fixable": True,
|
||||
})
|
||||
return issues
|
||||
|
||||
|
||||
def check_sparse_articles() -> list[dict]:
|
||||
"""Check for articles with fewer than 200 words."""
|
||||
issues = []
|
||||
for article in list_wiki_articles():
|
||||
word_count = get_article_word_count(article)
|
||||
if word_count < 200:
|
||||
rel = article.relative_to(KNOWLEDGE_DIR)
|
||||
issues.append({
|
||||
"severity": "suggestion",
|
||||
"check": "sparse_article",
|
||||
"file": str(rel),
|
||||
"detail": f"Sparse article: {word_count} words (minimum recommended: 200)",
|
||||
})
|
||||
return issues
|
||||
|
||||
|
||||
async def check_contradictions() -> list[dict]:
|
||||
"""Use LLM to detect contradictions across articles."""
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
query,
|
||||
)
|
||||
|
||||
wiki_content = read_all_wiki_content()
|
||||
|
||||
prompt = f"""Review this knowledge base for contradictions, inconsistencies, or
|
||||
conflicting claims across articles.
|
||||
|
||||
## Knowledge Base
|
||||
|
||||
{wiki_content}
|
||||
|
||||
## Instructions
|
||||
|
||||
Look for:
|
||||
- Direct contradictions (article A says X, article B says not-X)
|
||||
- Inconsistent recommendations (different articles recommend conflicting approaches)
|
||||
- Outdated information that conflicts with newer entries
|
||||
|
||||
For each issue found, output EXACTLY one line in this format:
|
||||
CONTRADICTION: [file1] vs [file2] - description of the conflict
|
||||
INCONSISTENCY: [file] - description of the inconsistency
|
||||
|
||||
If no issues found, output exactly: NO_ISSUES
|
||||
|
||||
Do NOT output anything else - no preamble, no explanation, just the formatted lines."""
|
||||
|
||||
response = ""
|
||||
try:
|
||||
async for message in query(
|
||||
prompt=prompt,
|
||||
options=ClaudeAgentOptions(
|
||||
cwd=str(ROOT_DIR),
|
||||
allowed_tools=[],
|
||||
max_turns=2,
|
||||
),
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
response += block.text
|
||||
except Exception as e:
|
||||
return [{"severity": "error", "check": "contradiction", "file": "(system)", "detail": f"LLM check failed: {e}"}]
|
||||
|
||||
issues = []
|
||||
if "NO_ISSUES" not in response:
|
||||
for line in response.strip().split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("CONTRADICTION:") or line.startswith("INCONSISTENCY:"):
|
||||
issues.append({
|
||||
"severity": "warning",
|
||||
"check": "contradiction",
|
||||
"file": "(cross-article)",
|
||||
"detail": line,
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def generate_report(all_issues: list[dict]) -> str:
|
||||
"""Generate a markdown lint report."""
|
||||
errors = [i for i in all_issues if i["severity"] == "error"]
|
||||
warnings = [i for i in all_issues if i["severity"] == "warning"]
|
||||
suggestions = [i for i in all_issues if i["severity"] == "suggestion"]
|
||||
|
||||
lines = [
|
||||
f"# Lint Report - {today_iso()}",
|
||||
"",
|
||||
f"**Total issues:** {len(all_issues)}",
|
||||
f"- Errors: {len(errors)}",
|
||||
f"- Warnings: {len(warnings)}",
|
||||
f"- Suggestions: {len(suggestions)}",
|
||||
"",
|
||||
]
|
||||
|
||||
for severity, issues, marker in [
|
||||
("Errors", errors, "x"),
|
||||
("Warnings", warnings, "!"),
|
||||
("Suggestions", suggestions, "?"),
|
||||
]:
|
||||
if issues:
|
||||
lines.append(f"## {severity}")
|
||||
lines.append("")
|
||||
for issue in issues:
|
||||
fixable = " (auto-fixable)" if issue.get("auto_fixable") else ""
|
||||
lines.append(f"- **[{marker}]** `{issue['file']}` - {issue['detail']}{fixable}")
|
||||
lines.append("")
|
||||
|
||||
if not all_issues:
|
||||
lines.append("All checks passed. Knowledge base is healthy.")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Lint the knowledge base")
|
||||
parser.add_argument(
|
||||
"--structural-only",
|
||||
action="store_true",
|
||||
help="Skip LLM-based checks (contradictions) - faster and free",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("Running knowledge base lint checks...")
|
||||
all_issues: list[dict] = []
|
||||
|
||||
# Structural checks (free, instant)
|
||||
checks = [
|
||||
("Broken links", check_broken_links),
|
||||
("Orphan pages", check_orphan_pages),
|
||||
("Orphan sources", check_orphan_sources),
|
||||
("Stale articles", check_stale_articles),
|
||||
("Missing backlinks", check_missing_backlinks),
|
||||
("Sparse articles", check_sparse_articles),
|
||||
]
|
||||
|
||||
for name, check_fn in checks:
|
||||
print(f" Checking: {name}...")
|
||||
issues = check_fn()
|
||||
all_issues.extend(issues)
|
||||
print(f" Found {len(issues)} issue(s)")
|
||||
|
||||
# LLM check (costs money)
|
||||
if not args.structural_only:
|
||||
print(" Checking: Contradictions (LLM)...")
|
||||
issues = asyncio.run(check_contradictions())
|
||||
all_issues.extend(issues)
|
||||
print(f" Found {len(issues)} issue(s)")
|
||||
else:
|
||||
print(" Skipping: Contradictions (--structural-only)")
|
||||
|
||||
# Generate and save report
|
||||
report = generate_report(all_issues)
|
||||
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
report_path = REPORTS_DIR / f"lint-{today_iso()}.md"
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
print(f"\nReport saved to: {report_path}")
|
||||
|
||||
# Update state
|
||||
state = load_state()
|
||||
state["last_lint"] = now_iso()
|
||||
save_state(state)
|
||||
|
||||
# Summary
|
||||
errors = sum(1 for i in all_issues if i["severity"] == "error")
|
||||
warnings = sum(1 for i in all_issues if i["severity"] == "warning")
|
||||
suggestions = sum(1 for i in all_issues if i["severity"] == "suggestion")
|
||||
print(f"\nResults: {errors} errors, {warnings} warnings, {suggestions} suggestions")
|
||||
|
||||
if errors > 0:
|
||||
print("\nErrors found - knowledge base needs attention!")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
138
scripts/query.py
Normal file
138
scripts/query.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
Query the knowledge base using index-guided retrieval (no RAG).
|
||||
|
||||
The LLM reads the index, picks relevant articles, and synthesizes an answer.
|
||||
No vector database, no embeddings, no chunking - just structured markdown
|
||||
and an index the LLM can reason over.
|
||||
|
||||
Usage:
|
||||
uv run python query.py "How should I handle auth redirects?"
|
||||
uv run python query.py "What patterns do I use for API design?" --file-back
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from config import KNOWLEDGE_DIR, QA_DIR, now_iso
|
||||
from utils import load_state, read_all_wiki_content, save_state
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
async def run_query(question: str, file_back: bool = False) -> str:
|
||||
"""Query the knowledge base and optionally file the answer back."""
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeAgentOptions,
|
||||
ResultMessage,
|
||||
TextBlock,
|
||||
query,
|
||||
)
|
||||
|
||||
wiki_content = read_all_wiki_content()
|
||||
|
||||
tools = ["Read", "Glob", "Grep"]
|
||||
if file_back:
|
||||
tools.extend(["Write", "Edit"])
|
||||
|
||||
file_back_instructions = ""
|
||||
if file_back:
|
||||
timestamp = now_iso()
|
||||
file_back_instructions = f"""
|
||||
|
||||
## File Back Instructions
|
||||
|
||||
After answering, do the following:
|
||||
1. Create a Q&A article at {QA_DIR}/ with the filename being a slugified version
|
||||
of the question (e.g., knowledge/qa/how-to-handle-auth-redirects.md)
|
||||
2. Use the Q&A article format from the schema (frontmatter with title, question,
|
||||
consulted articles, filed date)
|
||||
3. Update {KNOWLEDGE_DIR / 'index.md'} with a new row for this Q&A article
|
||||
4. Append to {KNOWLEDGE_DIR / 'log.md'}:
|
||||
## [{timestamp}] query (filed) | question summary
|
||||
- Question: {question}
|
||||
- Consulted: [[list of articles read]]
|
||||
- Filed to: [[qa/article-name]]
|
||||
"""
|
||||
|
||||
prompt = f"""You are a knowledge base query engine. Answer the user's question by
|
||||
consulting the knowledge base below.
|
||||
|
||||
## How to Answer
|
||||
|
||||
1. Read the INDEX section first - it lists every article with a one-line summary
|
||||
2. Identify 3-10 articles that are relevant to the question
|
||||
3. Read those articles carefully (they're included below)
|
||||
4. Synthesize a clear, thorough answer
|
||||
5. Cite your sources using [[wikilinks]] (e.g., [[concepts/supabase-auth]])
|
||||
6. If the knowledge base doesn't contain relevant information, say so honestly
|
||||
|
||||
## Knowledge Base
|
||||
|
||||
{wiki_content}
|
||||
|
||||
## Question
|
||||
|
||||
{question}
|
||||
{file_back_instructions}"""
|
||||
|
||||
answer = ""
|
||||
cost = 0.0
|
||||
|
||||
try:
|
||||
async for message in query(
|
||||
prompt=prompt,
|
||||
options=ClaudeAgentOptions(
|
||||
cwd=str(ROOT_DIR),
|
||||
system_prompt={"type": "preset", "preset": "claude_code"},
|
||||
allowed_tools=tools,
|
||||
permission_mode="acceptEdits",
|
||||
max_turns=15,
|
||||
),
|
||||
):
|
||||
if isinstance(message, AssistantMessage):
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
answer += block.text
|
||||
elif isinstance(message, ResultMessage):
|
||||
cost = message.total_cost_usd or 0.0
|
||||
except Exception as e:
|
||||
answer = f"Error querying knowledge base: {e}"
|
||||
|
||||
# Update state
|
||||
state = load_state()
|
||||
state["query_count"] = state.get("query_count", 0) + 1
|
||||
state["total_cost"] = state.get("total_cost", 0.0) + cost
|
||||
save_state(state)
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Query the personal knowledge base")
|
||||
parser.add_argument("question", help="The question to ask")
|
||||
parser.add_argument(
|
||||
"--file-back",
|
||||
action="store_true",
|
||||
help="File the answer back into the knowledge base as a Q&A article",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Question: {args.question}")
|
||||
print(f"File back: {'yes' if args.file_back else 'no'}")
|
||||
print("-" * 60)
|
||||
|
||||
answer = asyncio.run(run_query(args.question, file_back=args.file_back))
|
||||
print(answer)
|
||||
|
||||
if args.file_back:
|
||||
print("\n" + "-" * 60)
|
||||
qa_count = len(list(QA_DIR.glob("*.md"))) if QA_DIR.exists() else 0
|
||||
print(f"Answer filed to knowledge/qa/ ({qa_count} Q&A articles total)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
133
scripts/utils.py
Normal file
133
scripts/utils.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""Shared utilities for the personal knowledge base."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from config import (
|
||||
CONCEPTS_DIR,
|
||||
CONNECTIONS_DIR,
|
||||
DAILY_DIR,
|
||||
INDEX_FILE,
|
||||
KNOWLEDGE_DIR,
|
||||
LOG_FILE,
|
||||
QA_DIR,
|
||||
STATE_FILE,
|
||||
)
|
||||
|
||||
|
||||
# ── State management ──────────────────────────────────────────────────
|
||||
|
||||
def load_state() -> dict:
|
||||
"""Load persistent state from state.json."""
|
||||
if STATE_FILE.exists():
|
||||
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
||||
return {"ingested": {}, "query_count": 0, "last_lint": None, "total_cost": 0.0}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
"""Save state to state.json."""
|
||||
STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
# ── File hashing ──────────────────────────────────────────────────────
|
||||
|
||||
def file_hash(path: Path) -> str:
|
||||
"""SHA-256 hash of a file (first 16 hex chars)."""
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
|
||||
|
||||
|
||||
# ── Slug / naming ─────────────────────────────────────────────────────
|
||||
|
||||
def slugify(text: str) -> str:
|
||||
"""Convert text to a filename-safe slug."""
|
||||
text = text.lower().strip()
|
||||
text = re.sub(r"[^\w\s-]", "", text)
|
||||
text = re.sub(r"[\s_]+", "-", text)
|
||||
text = re.sub(r"-+", "-", text)
|
||||
return text.strip("-")
|
||||
|
||||
|
||||
# ── Wikilink helpers ──────────────────────────────────────────────────
|
||||
|
||||
def extract_wikilinks(content: str) -> list[str]:
|
||||
"""Extract all [[wikilinks]] from markdown content."""
|
||||
return re.findall(r"\[\[([^\]]+)\]\]", content)
|
||||
|
||||
|
||||
def wiki_article_exists(link: str) -> bool:
|
||||
"""Check if a wikilinked article exists on disk."""
|
||||
path = KNOWLEDGE_DIR / f"{link}.md"
|
||||
return path.exists()
|
||||
|
||||
|
||||
# ── Wiki content helpers ──────────────────────────────────────────────
|
||||
|
||||
def read_wiki_index() -> str:
|
||||
"""Read the knowledge base index file."""
|
||||
if INDEX_FILE.exists():
|
||||
return INDEX_FILE.read_text(encoding="utf-8")
|
||||
return "# Knowledge Base Index\n\n| Article | Summary | Compiled From | Updated |\n|---------|---------|---------------|---------|"
|
||||
|
||||
|
||||
def read_all_wiki_content() -> str:
|
||||
"""Read index + all wiki articles into a single string for context."""
|
||||
parts = [f"## INDEX\n\n{read_wiki_index()}"]
|
||||
|
||||
for subdir in [CONCEPTS_DIR, CONNECTIONS_DIR, QA_DIR]:
|
||||
if not subdir.exists():
|
||||
continue
|
||||
for md_file in sorted(subdir.glob("*.md")):
|
||||
rel = md_file.relative_to(KNOWLEDGE_DIR)
|
||||
content = md_file.read_text(encoding="utf-8")
|
||||
parts.append(f"## {rel}\n\n{content}")
|
||||
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
|
||||
def list_wiki_articles() -> list[Path]:
|
||||
"""List all wiki article files."""
|
||||
articles = []
|
||||
for subdir in [CONCEPTS_DIR, CONNECTIONS_DIR, QA_DIR]:
|
||||
if subdir.exists():
|
||||
articles.extend(sorted(subdir.glob("*.md")))
|
||||
return articles
|
||||
|
||||
|
||||
def list_raw_files() -> list[Path]:
|
||||
"""List all daily log files."""
|
||||
if not DAILY_DIR.exists():
|
||||
return []
|
||||
return sorted(DAILY_DIR.glob("*.md"))
|
||||
|
||||
|
||||
# ── Index helpers ─────────────────────────────────────────────────────
|
||||
|
||||
def count_inbound_links(target: str, exclude_file: Path | None = None) -> int:
|
||||
"""Count how many wiki articles link to a given target."""
|
||||
count = 0
|
||||
for article in list_wiki_articles():
|
||||
if article == exclude_file:
|
||||
continue
|
||||
content = article.read_text(encoding="utf-8")
|
||||
if f"[[{target}]]" in content:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def get_article_word_count(path: Path) -> int:
|
||||
"""Count words in an article, excluding YAML frontmatter."""
|
||||
content = path.read_text(encoding="utf-8")
|
||||
# Strip frontmatter
|
||||
if content.startswith("---"):
|
||||
end = content.find("---", 3)
|
||||
if end != -1:
|
||||
content = content[end + 3:]
|
||||
return len(content.split())
|
||||
|
||||
|
||||
def build_index_entry(rel_path: str, summary: str, sources: str, updated: str) -> str:
|
||||
"""Build a single index table row."""
|
||||
link = rel_path.replace(".md", "")
|
||||
return f"| [[{link}]] | {summary} | {sources} | {updated} |"
|
||||
636
uv.lock
generated
Normal file
636
uv.lock
generated
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "26.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.2.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "claude-agent-sdk"
|
||||
version = "0.1.56"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "mcp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/c1/c7afb1a08cecef0644693ccd9975651e45cf23a50272b94b6eca2c1a7dc8/claude_agent_sdk-0.1.56.tar.gz", hash = "sha256:a95bc14e59f9d6c8e7fa2e6581008a3f24f10e1b57302719823f62cfb5beccdc", size = 121659 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/73/4e3d13c4d43614de35a113c87ec96b3db605baa23f9f5c4a38536837e18e/claude_agent_sdk-0.1.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f5a7c87617101e6bb0f23408104ac6f40f9b5adec91dcfe5b8de5f65a7df73a", size = 58585662 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/6d/78347c2efa1526f1f6e7edecabe636575f622bcaa7921965457f95dd12dc/claude_agent_sdk-0.1.56-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:824f4a10340f46dd26fee8e74aeed4fc64fec95084e327ab1ebb6058b349e1c3", size = 60419564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/c1/708262318926c8393d494a5dcaafd9bc7d6ba547c0a5fad4eff5f9aa0ecd/claude_agent_sdk-0.1.56-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:ff60dedc06b62b52e5937a9a2c4b0ec4ad0dd6764c20be656d01aeb8b11fba1d", size = 71893844 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/4f/24918a596b0d61c3a691af2a9ee52b8c54f1769ce2c5fef1d64350056e53/claude_agent_sdk-0.1.56-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:fe866b2119f69e99d9637acc27b588670c610fed1c4a096287874db5744d029b", size = 72030943 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d8/5ded242e55f0b5f295d4ee2cbe5ae3bca914eb0a2a291f81e38b68d3ef58/claude_agent_sdk-0.1.56-py3-none-win_amd64.whl", hash = "sha256:5934e082e1ccf975d65cd7412f2eaf2c5ffa6b9019a2ca2a9fb228310df7ddc8", size = 74141451 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "jsonschema-specifications" },
|
||||
{ name = "referencing" },
|
||||
{ name = "rpds-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-specifications"
|
||||
version = "2025.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "referencing" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "llm-personal-kb"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "claude-agent-sdk" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "tzdata" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "claude-agent-sdk", specifier = ">=0.1.29" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||
{ name = "tzdata", specifier = ">=2024.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.27.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.37.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "rpds-py" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2026.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.44.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425 },
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue