A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Switch from Anthropic SDK to claude -p with in-process MCP

The engine was calling the Anthropic API directly and implementing its
own agentic loop (tool call → execute → feed result → repeat). This
replaces all of that with `claude -p` driven as a subprocess — Claude
Code handles the agentic loop internally, and our game tools are exposed
via an in-process MCP server over HTTP SSE on localhost.

The MCP server runs in a background thread sharing state (CampaignLog,
etc.) with the engine, so tool calls like mark_time update the game
clock immediately without any cross-process coordination. Auth uses
STORIED_CLAUDE_CODE_OAUTH_TOKEN mapped to CLAUDE_CODE_OAUTH_TOKEN on
the subprocess.

Also bumps all models to Opus 4.6, with --effort medium for gameplay
and --effort high for seeding/planning. Drops dollar cost tracking
since we don't have visibility into that through claude -p (token
counts are still reported).

Follows the pattern from docketeer's claude -p backend, adapted for
storied's simpler needs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+1388 -917
+1 -1
pyproject.toml
··· 10 10 ] 11 11 12 12 dependencies = [ 13 - "anthropic>=0.40", 14 13 "argcomplete>=3.0", 15 14 "httpx>=0.27", 15 + "mcp>=1.9", 16 16 "pymupdf>=1.24", 17 17 "pymupdf4llm>=0.0.17", 18 18 "pyyaml>=6.0",
+178
src/storied/claude.py
··· 1 + """Subprocess driver for claude -p (Claude Code in pipe mode). 2 + 3 + Shared logic for launching claude -p, formatting NDJSON input, and parsing 4 + NDJSON stream-json output events. 5 + """ 6 + 7 + import json 8 + import os 9 + import shutil 10 + from dataclasses import dataclass, field 11 + 12 + 13 + # -- Stream event types ------------------------------------------------------- 14 + 15 + 16 + @dataclass 17 + class TextDelta: 18 + text: str 19 + 20 + 21 + @dataclass 22 + class ToolStart: 23 + name: str 24 + 25 + 26 + @dataclass 27 + class ToolInputDelta: 28 + json_fragment: str 29 + 30 + 31 + @dataclass 32 + class ToolStop: 33 + pass 34 + 35 + 36 + @dataclass 37 + class Result: 38 + session_id: str | None = None 39 + usage: dict = field(default_factory=dict) 40 + duration_ms: int | None = None 41 + 42 + 43 + type StreamEvent = TextDelta | ToolStart | ToolInputDelta | ToolStop | Result 44 + 45 + 46 + # -- Builders ------------------------------------------------------------------ 47 + 48 + 49 + def build_mcp_config(url: str) -> str: 50 + """Build inline JSON for --mcp-config pointing to an HTTP MCP server.""" 51 + config = { 52 + "mcpServers": { 53 + "storied": { 54 + "type": "sse", 55 + "url": url, 56 + } 57 + } 58 + } 59 + return json.dumps(config) 60 + 61 + 62 + def build_claude_args( 63 + model: str, 64 + system_prompt: str, 65 + mcp_config: str, 66 + *, 67 + effort: str = "medium", 68 + session_id: str | None = None, 69 + resume_session_id: str | None = None, 70 + persist_session: bool = True, 71 + ) -> list[str]: 72 + """Build the argument list for claude -p.""" 73 + claude_path = shutil.which("claude") 74 + if not claude_path: 75 + raise FileNotFoundError( 76 + "Claude Code CLI not found on PATH. " 77 + "Install it with: npm install -g @anthropic-ai/claude-code" 78 + ) 79 + 80 + args = [ 81 + claude_path, 82 + "--tools", "", 83 + "-p", 84 + "--input-format", "stream-json", 85 + "--output-format", "stream-json", 86 + "--include-partial-messages", 87 + "--verbose", 88 + "--dangerously-skip-permissions", 89 + "--disable-slash-commands", 90 + "--strict-mcp-config", 91 + "--mcp-config", mcp_config, 92 + "--effort", effort, 93 + ] 94 + 95 + if not persist_session: 96 + args.append("--no-session-persistence") 97 + 98 + if resume_session_id: 99 + args.extend(["--resume", resume_session_id]) 100 + else: 101 + if session_id: 102 + args.extend(["--session-id", session_id]) 103 + args.extend(["--system-prompt", system_prompt, "--model", model]) 104 + 105 + return args 106 + 107 + 108 + def build_env() -> dict[str, str]: 109 + """Build the environment for the claude subprocess.""" 110 + env = {**os.environ} 111 + oauth_token = os.environ.get("STORIED_CLAUDE_CODE_OAUTH_TOKEN", "") 112 + if oauth_token: 113 + env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token 114 + return env 115 + 116 + 117 + def format_user_message(text: str) -> bytes: 118 + """Format a user message as NDJSON for claude -p stdin.""" 119 + envelope = { 120 + "type": "user", 121 + "message": { 122 + "role": "user", 123 + "content": [{"type": "text", "text": text}], 124 + }, 125 + } 126 + return json.dumps(envelope).encode() + b"\n" 127 + 128 + 129 + # -- Event parsing ------------------------------------------------------------- 130 + 131 + 132 + def parse_event(line: str) -> StreamEvent | None: 133 + """Parse a single NDJSON line into a typed StreamEvent.""" 134 + line = line.strip() 135 + if not line: 136 + return None 137 + 138 + try: 139 + event = json.loads(line) 140 + except json.JSONDecodeError: 141 + return None 142 + 143 + etype = event.get("type") 144 + 145 + if etype == "stream_event": 146 + return _parse_stream_event(event) 147 + elif etype == "result": 148 + return Result( 149 + session_id=event.get("session_id"), 150 + usage=event.get("usage", {}), 151 + duration_ms=event.get("duration_ms"), 152 + ) 153 + 154 + return None 155 + 156 + 157 + def _parse_stream_event(event: dict) -> StreamEvent | None: 158 + """Parse the inner stream_event envelope.""" 159 + inner = event.get("event", {}) 160 + inner_type = inner.get("type") 161 + 162 + if inner_type == "content_block_start": 163 + block = inner.get("content_block", {}) 164 + if block.get("type") == "tool_use": 165 + return ToolStart(name=block.get("name", "")) 166 + 167 + elif inner_type == "content_block_delta": 168 + delta = inner.get("delta", {}) 169 + delta_type = delta.get("type") 170 + if delta_type == "text_delta": 171 + return TextDelta(text=delta.get("text", "")) 172 + elif delta_type == "input_json_delta": 173 + return ToolInputDelta(json_fragment=delta.get("partial_json", "")) 174 + 175 + elif inner_type == "content_block_stop": 176 + return ToolStop() 177 + 178 + return None
+30 -133
src/storied/cli.py
··· 8 8 9 9 import argcomplete 10 10 11 - # Pricing per million tokens (USD) 12 - MODEL_PRICING: dict[str, dict[str, float]] = { 13 - "claude-haiku-4-5-20251001": { 14 - "input": 1.00, 15 - "output": 5.00, 16 - "cache_read": 0.10, 17 - "cache_write": 1.25, 18 - }, 19 - "claude-sonnet-4-5-20250929": { 20 - "input": 3.00, 21 - "output": 15.00, 22 - "cache_read": 0.30, 23 - "cache_write": 3.75, 24 - }, 25 - "claude-opus-4-5-20251101": { 26 - "input": 5.00, 27 - "output": 25.00, 28 - "cache_read": 0.50, 29 - "cache_write": 6.25, 30 - }, 31 - } 32 - DEFAULT_PRICING = MODEL_PRICING["claude-sonnet-4-5-20250929"] 33 - 34 11 # Slash commands available during play 35 12 SLASH_COMMANDS = { 36 13 "/help": "Show this help message", 37 14 "/save": "Save session state (without quitting)", 38 - "/context": "Show token usage and costs", 15 + "/context": "Show token usage", 39 16 "/dm": "Say something out-of-character to the DM (e.g. /dm less combat please)", 40 17 "/note": "Add a note to your character sheet (e.g. /note remember the prayer words)", 41 18 } ··· 284 261 on_progress=on_seed_progress, 285 262 ) 286 263 287 - pricing = MODEL_PRICING.get("claude-opus-4-5-20251101", DEFAULT_PRICING) 288 - cost = ( 289 - result.input_tokens * pricing["input"] / 1_000_000 290 - + result.output_tokens * pricing["output"] / 1_000_000 291 - ) 292 264 console.print( 293 265 f"[dim] Done — {result.tool_calls} tool calls, " 294 - f"{result.elapsed:.1f}s, ${cost:.2f}[/dim]" 266 + f"{result.elapsed:.1f}s[/dim]" 295 267 ) 296 268 console.print() 297 269 ··· 370 342 stats = engine.get_context_stats() 371 343 console.print() 372 344 373 - # Header with model, time, and usage 374 - pct = (stats["estimated_total"] / stats["model_limit"]) * 100 375 - total_k = stats["estimated_total"] / 1000 345 + # Header with model, time, and context estimate 346 + context_k = stats["context_total"] / 1000 376 347 limit_k = stats["model_limit"] / 1000 377 348 game_time = engine.get_current_time() 378 349 console.print( 379 350 f"[bold]Context Usage[/bold] [dim]({game_time})[/dim] " 380 - f"[dim]{engine.model} · {total_k:.1f}k/{limit_k:.0f}k tokens ({pct:.1f}%)[/dim]" 351 + f"[dim]{engine.model} · ~{context_k:.1f}k/{limit_k:.0f}k system tokens[/dim]" 381 352 ) 382 353 383 354 # Build visual bar (30 chars wide) 384 355 bar_width = 30 385 - 386 - # Calculate proportions 387 356 limit = stats["model_limit"] 388 - parts = [ 357 + bar_parts = [ 389 358 ("DM Instructions", stats["system_prompt"], "bright_blue"), 390 359 ("Character", stats["context_parts"].get("Character", 0), "green"), 391 360 ("Log", stats["context_parts"].get("Log", 0), "bright_cyan"), 392 361 ("Session", stats["context_parts"].get("Session", 0), "yellow"), 393 362 ] 394 - # Add location if present 395 363 if "Location" in stats["context_parts"]: 396 - parts.append(("Location", stats["context_parts"]["Location"], "cyan")) 397 - # Add NPCs 364 + bar_parts.append(("Location", stats["context_parts"]["Location"], "cyan")) 398 365 for key, val in stats["context_parts"].items(): 399 - if key.startswith("NPC:"): 400 - parts.append((key, val, "magenta")) 401 - parts.append(("Messages", stats["messages"], "bright_magenta")) 366 + if key.startswith("Entity:") or key.startswith("Linked:"): 367 + bar_parts.append((key.split(":", 1)[1], val, "magenta")) 402 368 403 - # Build the bar 404 369 bar = "" 405 370 legend_items = [] 406 - for name, tokens, color in parts: 371 + for name, tokens, color in bar_parts: 407 372 if tokens > 0: 408 373 width = max(1, int((tokens / limit) * bar_width)) 409 374 bar += f"[{color}]{'█' * width}[/{color}]" 410 375 pct_part = (tokens / limit) * 100 411 376 legend_items.append((name, tokens, pct_part, color)) 412 377 413 - # Fill remaining with dim blocks 414 - used_width = sum(max(1, int((t / limit) * bar_width)) for _, t, _ in parts if t > 0) 378 + used_width = sum(max(1, int((t / limit) * bar_width)) for _, t, _ in bar_parts if t > 0) 415 379 remaining_width = bar_width - used_width 416 380 if remaining_width > 0: 417 381 bar += f"[dim]{'░' * remaining_width}[/dim]" 418 382 419 383 console.print(bar) 420 384 421 - # Legend 422 385 for name, tokens, pct_part, color in legend_items: 423 386 tokens_str = f"{tokens:,}" if tokens < 1000 else f"{tokens/1000:.1f}k" 424 387 console.print(f" [{color}]█[/{color}] {name}: [dim]{tokens_str} ({pct_part:.1f}%)[/dim]") 425 388 426 - # Free space 427 - free_pct = (stats["estimated_remaining"] / limit) * 100 428 - free_k = stats["estimated_remaining"] / 1000 429 - console.print(f" [dim]░[/dim] Free: [dim]{free_k:.1f}k ({free_pct:.1f}%)[/dim]") 430 - 431 - # Session totals if we have them 389 + # Session totals 432 390 if stats["total_input"] > 0: 433 391 console.print() 434 392 console.print("[dim]Session totals:[/dim]") 435 - cache_read = stats.get("total_cache_read", 0) 436 - cache_create = stats.get("total_cache_creation", 0) 437 - total_input = stats["total_input"] 438 - 439 - # Calculate cache hit rate 440 - total_processed = cache_read + cache_create + total_input 441 - total_output = stats["total_output"] 442 - if total_processed > 0: 443 - hit_rate = (cache_read / total_processed) * 100 444 - 445 - # Get pricing for current model 446 - pricing = MODEL_PRICING.get(engine.model, DEFAULT_PRICING) 447 - 448 - # Calculate costs with and without caching 449 - cost_without = ( 450 - total_processed * pricing["input"] / 1_000_000 + 451 - total_output * pricing["output"] / 1_000_000 452 - ) 453 - cost_with = ( 454 - cache_read * pricing["cache_read"] / 1_000_000 + 455 - cache_create * pricing["cache_write"] / 1_000_000 + 456 - total_input * pricing["input"] / 1_000_000 + 457 - total_output * pricing["output"] / 1_000_000 458 - ) 459 - savings_pct = ((cost_without - cost_with) / cost_without) * 100 if cost_without > 0 else 0 460 - 461 - console.print( 462 - f" [dim]Tokens: {total_processed:,} in " 463 - f"({cache_read:,} cached, {cache_create:,} written, {total_input:,} new) · " 464 - f"{total_output:,} out[/dim]" 465 - ) 466 - console.print( 467 - f" [dim]Cache hit rate: [green]{hit_rate:.1f}%[/green] · " 468 - f"Savings: [green]{savings_pct:.0f}%[/green] · " 469 - f"Cost: [green]${cost_with:.3f}[/green] (${cost_without:.3f} without cache)[/dim]" 470 - ) 471 - else: 472 - console.print( 473 - f" [dim]Input: {total_input:,} · " 474 - f"Output: {stats['total_output']:,}[/dim]" 475 - ) 393 + console.print( 394 + f" [dim]Tokens: {stats['total_input']:,} in · " 395 + f"{stats['total_output']:,} out[/dim]" 396 + ) 476 397 477 398 console.print() 478 399 continue ··· 529 450 console.print() 530 451 531 452 # Show debug token info if enabled 532 - if args.debug: 533 - usage = engine.last_usage 534 - uncached = usage.get("input_tokens", 0) 535 - cache_write = usage.get("cache_creation_input_tokens", 0) 536 - cache_read = usage.get("cache_read_input_tokens", 0) 537 - output = usage.get("output_tokens", 0) 538 - 539 - # Format the debug line 540 - if cache_write > 0: 541 - in_part = f"in: {uncached:,} (writing {cache_write:,} to cache)" 542 - elif cache_read > 0: 543 - in_part = f"in: {uncached:,} +{cache_read:,} cached" 544 - else: 545 - in_part = f"in: {uncached:,}" 546 - 547 - total_in = engine.total_input_tokens + engine.total_cache_read_tokens 548 - total_out = engine.total_output_tokens 453 + if args.debug and engine._last_result: 454 + r = engine._last_result 455 + in_tokens = r.usage.get("input_tokens", 0) 456 + out_tokens = r.usage.get("output_tokens", 0) 549 457 console.print( 550 - f"[dim]{in_part} | out: {output:,} | " 551 - f"total: {total_in:,} in / {total_out:,} out[/dim]" 458 + f"[dim]in: {in_tokens:,} | out: {out_tokens:,} | " 459 + f"total: {engine._total_input_tokens:,} in / " 460 + f"{engine._total_output_tokens:,} out[/dim]" 552 461 ) 553 462 554 463 # Check if session ended (player quit gracefully) ··· 559 468 break 560 469 561 470 # Check if character was just created 562 - if creation_mode and load_character(player_id) is not None: 471 + if creation_mode and engine.character_created: 563 472 console.print() 564 473 console.print(Panel.fit( 565 474 "[bold green]Character created![/bold green]\n" ··· 633 542 if result.dry_run: 634 543 return 0 635 544 636 - # Real run — show summary with cost 637 - pricing = MODEL_PRICING.get(args.model, DEFAULT_PRICING) 638 - cost = ( 639 - result.input_tokens * pricing["input"] / 1_000_000 640 - + result.output_tokens * pricing["output"] / 1_000_000 641 - ) 642 - 643 545 print( 644 546 f"Done — {result.tool_calls} tool calls, " 645 547 f"{result.input_tokens:,} in / {result.output_tokens:,} out, " 646 - f"{result.elapsed:.1f}s, ${cost:.2f}", 548 + f"{result.elapsed:.1f}s", 647 549 flush=True, 648 550 ) 649 551 ··· 677 579 print("No character found. Create a character first with `storied play`.") 678 580 return 1 679 581 680 - pricing = MODEL_PRICING.get(args.model, DEFAULT_PRICING) 681 - cost = ( 682 - result.input_tokens * pricing["input"] / 1_000_000 683 - + result.output_tokens * pricing["output"] / 1_000_000 684 - ) 685 582 print( 686 583 f"Done — {result.tool_calls} tool calls, " 687 584 f"{result.input_tokens:,} in / {result.output_tokens:,} out, " 688 - f"{result.elapsed:.1f}s, ${cost:.2f}", 585 + f"{result.elapsed:.1f}s", 689 586 flush=True, 690 587 ) 691 588 return 0 ··· 801 698 ) 802 699 plan_parser.add_argument( 803 700 "--model", "-m", 804 - default="claude-opus-4-5-20251101", 805 - help="Model to use for planning (default: claude-opus-4-5-20251101)", 701 + default="claude-opus-4-6", 702 + help="Model to use for planning (default: claude-opus-4-6)", 806 703 ) 807 704 plan_parser.add_argument( 808 705 "--threshold", ··· 837 734 ) 838 735 seed_parser.add_argument( 839 736 "--model", "-m", 840 - default="claude-opus-4-5-20251101", 841 - help="Model to use for seeding (default: claude-opus-4-5-20251101)", 737 + default="claude-opus-4-6", 738 + help="Model to use for seeding (default: claude-opus-4-6)", 842 739 ) 843 740 seed_parser.add_argument( 844 741 "--force", "-f",
+176 -341
src/storied/engine.py
··· 1 - """DM Engine - the agentic loop for running 5e sessions.""" 1 + """DM Engine - drives claude -p for running 5e sessions.""" 2 2 3 - import copy 4 3 import json 5 - import os 4 + import re 5 + import subprocess 6 6 from collections.abc import Iterator 7 - from datetime import datetime, timezone 7 + from datetime import UTC, datetime 8 8 from pathlib import Path 9 + from threading import Thread 9 10 10 - import anthropic 11 - 12 - 13 - def _truncate(value: object, max_len: int = 60, max_items: int = 3) -> str: 14 - """Truncate a value for debug display.""" 15 - if isinstance(value, str): 16 - if len(value) <= max_len: 17 - return repr(value) 18 - return repr(value[:max_len]) + f"...+{len(value) - max_len} chars" 19 - 20 - if isinstance(value, list): 21 - if len(value) <= max_items: 22 - return "[" + ", ".join(_truncate(v, 30, 2) for v in value) + "]" 23 - items = [_truncate(v, 30, 2) for v in value[:max_items]] 24 - return "[" + ", ".join(items) + f", ...+{len(value) - max_items} more]" 25 - 26 - if isinstance(value, dict): 27 - if len(value) <= max_items: 28 - pairs = [f"{k!r}: {_truncate(v, 30, 2)}" for k, v in value.items()] 29 - return "{" + ", ".join(pairs) + "}" 30 - pairs = [f"{k!r}: {_truncate(v, 30, 2)}" for k, v in list(value.items())[:max_items]] 31 - return "{" + ", ".join(pairs) + f", ...+{len(value) - max_items} more" + "}" 32 - 33 - s = repr(value) 34 - if len(s) <= max_len: 35 - return s 36 - return s[:max_len] + f"...+{len(s) - max_len} chars" 11 + import yaml 37 12 38 13 from storied.character import format_character_context, load_character 14 + from storied.claude import ( 15 + Result, 16 + TextDelta, 17 + ToolInputDelta, 18 + ToolStart, 19 + ToolStop, 20 + build_claude_args, 21 + build_env, 22 + build_mcp_config, 23 + format_user_message, 24 + parse_event, 25 + ) 26 + from storied.mcp_server import start_server as start_mcp_server 39 27 from storied.content import ContentResolver 40 28 from storied.log import CampaignLog 41 29 from storied.session import ( 42 - ENTITY_TYPES, 43 30 extract_wiki_links, 44 31 format_session_context, 45 32 load_entity_content, 46 33 load_session, 47 - name_to_slug, 48 - resolve_wiki_link, 49 34 ) 50 - from storied.tools import TOOL_DEFINITIONS, execute_tool 51 35 52 36 53 37 def load_prompt(name: str, prompts_path: Path | None = None) -> str: ··· 58 42 return path.read_text() 59 43 60 44 45 + def _drain_stderr(stderr, lines: list[str]) -> None: 46 + """Read all lines from stderr into the list.""" 47 + for raw in stderr: 48 + lines.append(raw.decode(errors="replace")) 49 + 50 + 51 + def _tool_notification(name: str) -> str: 52 + """Build a friendly tool notification string from an MCP tool name. 53 + 54 + MCP tool names are prefixed as mcp__storied__<name> by Claude Code. 55 + """ 56 + short = name.rsplit("__", 1)[-1] if "__" in name else name 57 + 58 + labels = { 59 + "roll": "Rolling", 60 + "recall": "Recalling", 61 + "update_character": "Updating character sheet", 62 + "create_character": "Creating character", 63 + "set_scene": "Setting scene", 64 + "establish": "Establishing", 65 + "mark": "Recording", 66 + "note_discovery": "Noting discovery", 67 + "mark_time": "Logging time", 68 + "end_session": "Saving session", 69 + } 70 + label = labels.get(short, short) 71 + return f"\n[{label}...]\n" 72 + 73 + 61 74 class DMEngine: 62 - """The Dungeon Master engine - Claude with tools for running 5e sessions.""" 75 + """The Dungeon Master engine - Claude Code subprocess for running 5e sessions.""" 63 76 64 77 def __init__( 65 78 self, 66 79 world_id: str = "default", 67 80 player_id: str = "default", 68 81 base_path: Path | None = None, 69 - model: str = "claude-haiku-4-5-20251001", 82 + model: str = "claude-opus-4-6", 70 83 prompt_name: str = "dm-system", 71 84 transcript_path: Path | None = None, 72 85 ): 73 - """Initialize the DM engine. 74 - 75 - Args: 76 - world_id: World ID for world-specific content (default: "default") 77 - player_id: Player identifier for character loading (default: "default") 78 - base_path: Base path for content resolution (defaults to cwd) 79 - model: Claude model to use 80 - prompt_name: System prompt to use (default: "dm-system", or "character-creation") 81 - transcript_path: Optional path to write full debug transcript (JSONL format) 82 - """ 83 - self.client = anthropic.Anthropic( 84 - api_key=os.environ.get("STORIED_ANTHROPIC_API_KEY"), 85 - ) 86 86 self.model = model 87 87 self.world_id = world_id 88 88 self.player_id = player_id 89 89 self.base_path = base_path or Path.cwd() 90 - self.messages: list[dict] = [] 91 90 92 - # Token tracking (includes cache metrics) 93 - self.last_usage: dict = { 94 - "input_tokens": 0, 95 - "output_tokens": 0, 96 - "cache_creation_input_tokens": 0, 97 - "cache_read_input_tokens": 0, 98 - } 99 - self.total_input_tokens: int = 0 100 - self.total_output_tokens: int = 0 101 - self.total_cache_creation_tokens: int = 0 102 - self.total_cache_read_tokens: int = 0 91 + # Session management 92 + self._session_id: str | None = None 103 93 104 - # Session end flag (set when end_session tool is called) 94 + # Usage tracking from result events 95 + self._last_result: Result | None = None 96 + self._total_input_tokens: int = 0 97 + self._total_output_tokens: int = 0 98 + 99 + # Session end / character creation flags 105 100 self.session_ended: bool = False 101 + self.character_created: bool = False 106 102 107 103 # Debug mode for verbose tool output 108 104 self.debug: bool = False 109 105 110 - # Full transcript for debugging (JSONL format) 106 + # Transcript logging 111 107 self._transcript_path = transcript_path 112 108 if transcript_path: 113 109 transcript_path.parent.mkdir(parents=True, exist_ok=True) 114 110 115 - # Campaign log for time tracking (world-scoped) 111 + # Campaign log for time tracking (world-scoped, shared with MCP server) 116 112 self._campaign_log = CampaignLog(self.world_id, self.base_path) 117 113 114 + # Start in-process MCP server (shares CampaignLog with engine) 115 + self._mcp = start_mcp_server( 116 + world_id=self.world_id, 117 + player_id=self.player_id, 118 + base_path=self.base_path, 119 + tool_set="dm", 120 + campaign_log=self._campaign_log, 121 + ) 122 + 118 123 # Build system prompt with full context 119 124 self._prompt_name = prompt_name 120 125 self._base_prompt = load_prompt(prompt_name) 121 126 self._context_parts: dict[str, str] = {} 122 - context = self._build_context() 123 - 124 - # System prompt as content blocks with cache breakpoints 125 - self._system_blocks: list[dict] = [ 126 - { 127 - "type": "text", 128 - "text": self._base_prompt, 129 - "cache_control": {"type": "ephemeral"}, 130 - }, 131 - ] 132 - if context: 133 - self._system_blocks.append( 134 - { 135 - "type": "text", 136 - "text": context, 137 - "cache_control": {"type": "ephemeral"}, 138 - } 139 - ) 140 - 141 - # Tools with cache control on last tool 142 - self._cached_tools = self._get_tools_with_cache() 143 127 144 128 def _log_transcript(self, event_type: str, data: dict) -> None: 145 - """Append an event to the debug transcript. 146 - 147 - Args: 148 - event_type: Type of event (player_input, assistant_response, tool_call, tool_result) 149 - data: Event-specific data 150 - """ 129 + """Append an event to the debug transcript.""" 151 130 if not self._transcript_path: 152 131 return 153 132 154 133 entry = { 155 - "timestamp": datetime.now(timezone.utc).isoformat(), 134 + "timestamp": datetime.now(UTC).isoformat(), 156 135 "type": event_type, 157 136 **data, 158 137 } ··· 206 185 # Current location 207 186 location_slug = session.get("location") 208 187 if location_slug: 209 - # Try new format (display name) first, then old format (slug) 210 188 location_content = self._find_entity(location_slug) 211 189 if not location_content: 212 - location_content = self._load_world_content("locations", location_slug) 190 + location_content = self._load_world_content( 191 + "locations", location_slug 192 + ) 213 193 if location_content: 214 194 loc_context = self._format_entity("Location", location_content) 215 195 self._context_parts["Location"] = loc_context 216 196 parts.append(loc_context) 217 197 loaded_names.add(location_slug) 218 - # Collect wikilinks for one-hop loading 219 - linked_names.update(extract_wiki_links(location_content.get("body", ""))) 198 + linked_names.update( 199 + extract_wiki_links(location_content.get("body", "")) 200 + ) 220 201 221 202 # Present entities from wiki links in session 222 203 present_text = session.get("body", "") ··· 230 211 self._context_parts[f"Entity:{name}"] = entity_context 231 212 parts.append(entity_context) 232 213 loaded_names.add(name) 233 - # Collect wikilinks for one-hop loading 234 214 linked_names.update(extract_wiki_links(entity.get("body", ""))) 235 215 236 216 # One-hop: load linked entities (but not their links) ··· 276 256 277 257 def _parse_knowledge_file(self, file_path: Path) -> dict | None: 278 258 """Parse a player knowledge file with YAML frontmatter.""" 279 - import re 280 - 281 - import yaml 282 - 283 259 content = file_path.read_text() 284 260 if not content.startswith("---"): 285 261 return {"body": content} 286 262 287 - # Parse frontmatter 288 263 match = re.search(r"\n---\s*\n", content[3:]) 289 264 if not match: 290 265 return {"body": content} ··· 329 304 return f"## {entity_type}: {name}\n\n{body}" 330 305 331 306 @staticmethod 332 - def _get_tools_with_cache() -> list[dict]: 333 - """Get tool definitions with cache_control on the last tool.""" 334 - tools = copy.deepcopy(TOOL_DEFINITIONS) 335 - if tools: 336 - tools[-1]["cache_control"] = {"type": "ephemeral"} 337 - return tools 338 - 339 - def _prepare_messages_for_cache(self) -> list[dict]: 340 - """Prepare messages with cache_control on the last message.""" 341 - if not self.messages: 342 - return [] 343 - 344 - messages = copy.deepcopy(self.messages) 345 - last = messages[-1] 346 - 347 - # Ensure content is a list of blocks 348 - if isinstance(last["content"], str): 349 - last["content"] = [{"type": "text", "text": last["content"]}] 350 - 351 - # Add cache_control to last block 352 - if last["content"]: 353 - last["content"][-1]["cache_control"] = {"type": "ephemeral"} 354 - 355 - return messages 356 - 357 - @staticmethod 358 307 def _estimate_tokens(text: str) -> int: 359 308 """Rough token estimate (~4 chars per token).""" 360 309 return len(text) // 4 361 310 362 311 def get_context_stats(self) -> dict: 363 - """Get breakdown of context window usage. 364 - 365 - Returns dict with: 366 - - model_limit: context window size for model 367 - - system_prompt: base DM instructions tokens 368 - - context_parts: dict of component -> estimated tokens 369 - - messages: conversation history tokens (estimated) 370 - - last_input: actual input tokens from last API call 371 - - last_output: actual output tokens from last API call 372 - - total_input: cumulative input tokens this session 373 - - total_output: cumulative output tokens this session 374 - """ 375 - # Model context limits (approximate) 376 - model_limits = { 377 - "claude-haiku-4-5-20251001": 200_000, 378 - "claude-sonnet-4-5-20250514": 200_000, 379 - "claude-opus-4-5-20251101": 200_000, 380 - } 381 - model_limit = model_limits.get(self.model, 200_000) 312 + """Get breakdown of context window usage.""" 313 + model_limit = 200_000 382 314 383 - # Estimate system prompt components 384 315 base_prompt_tokens = self._estimate_tokens(self._base_prompt) 385 316 386 317 context_breakdown = {} 387 318 for name, content in self._context_parts.items(): 388 319 context_breakdown[name] = self._estimate_tokens(content) 389 320 390 - # Estimate messages 391 - import json 392 - messages_str = json.dumps(self.messages) 393 - messages_tokens = self._estimate_tokens(messages_str) 394 - 395 - # Total estimated context 396 321 context_total = base_prompt_tokens + sum(context_breakdown.values()) 397 - estimated_total = context_total + messages_tokens 322 + 323 + # Usage from last result event 324 + last_input = 0 325 + last_output = 0 326 + if self._last_result: 327 + last_input = self._last_result.usage.get("input_tokens", 0) 328 + last_output = self._last_result.usage.get("output_tokens", 0) 398 329 399 330 return { 400 331 "model_limit": model_limit, 401 332 "system_prompt": base_prompt_tokens, 402 333 "context_parts": context_breakdown, 403 334 "context_total": context_total, 404 - "messages": messages_tokens, 405 - "estimated_total": estimated_total, 406 - "estimated_remaining": model_limit - estimated_total, 407 - "last_input": self.last_usage.get("input_tokens", 0), 408 - "last_output": self.last_usage.get("output_tokens", 0), 409 - "last_cache_creation": self.last_usage.get("cache_creation_input_tokens", 0), 410 - "last_cache_read": self.last_usage.get("cache_read_input_tokens", 0), 411 - "total_input": self.total_input_tokens, 412 - "total_output": self.total_output_tokens, 413 - "total_cache_creation": self.total_cache_creation_tokens, 414 - "total_cache_read": self.total_cache_read_tokens, 335 + "last_input": last_input, 336 + "last_output": last_output, 337 + "total_input": self._total_input_tokens, 338 + "total_output": self._total_output_tokens, 415 339 } 416 340 417 341 def process_action(self, player_input: str) -> str: 418 - """Process player input and return DM narrative. 419 - 420 - This is a non-streaming version that returns the complete response. 421 - """ 342 + """Process player input and return DM narrative (non-streaming).""" 422 343 chunks = list(self.stream_action(player_input)) 423 344 return "".join(chunks) 424 345 425 346 def stream_action(self, player_input: str) -> Iterator[str]: 426 347 """Stream DM response for real-time output. 427 348 428 - Handles the full agentic loop: 429 - 1. Add player input to conversation 430 - 2. Call Claude with tools (streaming) 431 - 3. If Claude uses a tool, execute it and continue 432 - 4. Yield text chunks as they arrive 433 - 5. Repeat until Claude produces a final response 349 + Launches a claude -p subprocess, sends the user message, and yields 350 + text chunks as they arrive. Claude Code handles the agentic loop 351 + (tool calls via MCP) internally. 434 352 """ 435 - # Add player message to conversation 436 - self.messages.append({"role": "user", "content": player_input}) 437 353 self._log_transcript("player_input", {"content": player_input}) 438 354 439 - while True: 440 - # Stream from Claude 441 - assistant_content: list[dict] = [] 442 - tool_uses: list[dict] = [] 443 - current_tool: dict | None = None 355 + # Build system prompt (base + context) 356 + system_prompt = self._base_prompt 357 + context = self._build_context() 358 + if context: 359 + system_prompt += "\n\n---\n\n" + context 444 360 445 - with self.client.messages.stream( 446 - model=self.model, 447 - max_tokens=4096, 448 - system=self._system_blocks, 449 - tools=self._cached_tools, 450 - messages=self._prepare_messages_for_cache(), 451 - ) as stream: 452 - for event in stream: 453 - if event.type == "content_block_start": 454 - if event.content_block.type == "text": 455 - pass # Text will come in deltas 456 - elif event.content_block.type == "tool_use": 457 - current_tool = { 458 - "id": event.content_block.id, 459 - "name": event.content_block.name, 460 - "input_json": "", 461 - } 361 + mcp_config = build_mcp_config(self._mcp.url) 462 362 463 - elif event.type == "content_block_delta": 464 - if event.delta.type == "text_delta": 465 - yield event.delta.text 466 - elif event.delta.type == "input_json_delta": 467 - if current_tool: 468 - current_tool["input_json"] += event.delta.partial_json 363 + args = build_claude_args( 364 + model=self.model, 365 + system_prompt=system_prompt, 366 + mcp_config=mcp_config, 367 + effort="medium", 368 + resume_session_id=self._session_id, 369 + ) 469 370 470 - elif event.type == "content_block_stop": 471 - if current_tool: 472 - # Parse the accumulated JSON 473 - import json 371 + env = build_env() 474 372 475 - tool_input = json.loads(current_tool["input_json"]) 476 - tool_uses.append( 477 - { 478 - "id": current_tool["id"], 479 - "name": current_tool["name"], 480 - "input": tool_input, 481 - } 482 - ) 483 - current_tool = None 484 - 485 - # Get the final message for conversation history 486 - final_message = stream.get_final_message() 487 - 488 - # Track token usage (including cache metrics) 489 - if final_message.usage: 490 - cache_creation = getattr( 491 - final_message.usage, "cache_creation_input_tokens", 0 492 - ) or 0 493 - cache_read = getattr( 494 - final_message.usage, "cache_read_input_tokens", 0 495 - ) or 0 496 - self.last_usage = { 497 - "input_tokens": final_message.usage.input_tokens, 498 - "output_tokens": final_message.usage.output_tokens, 499 - "cache_creation_input_tokens": cache_creation, 500 - "cache_read_input_tokens": cache_read, 501 - } 502 - self.total_input_tokens += final_message.usage.input_tokens 503 - self.total_output_tokens += final_message.usage.output_tokens 504 - self.total_cache_creation_tokens += cache_creation 505 - self.total_cache_read_tokens += cache_read 373 + proc = subprocess.Popen( 374 + args, 375 + stdin=subprocess.PIPE, 376 + stdout=subprocess.PIPE, 377 + stderr=subprocess.PIPE, 378 + env=env, 379 + cwd=str(self.base_path), 380 + ) 506 381 507 - # Build assistant content for conversation history 508 - for block in final_message.content: 509 - if block.type == "text" and block.text: 510 - assistant_content.append({"type": "text", "text": block.text}) 511 - elif block.type == "tool_use": 512 - assistant_content.append( 513 - { 514 - "type": "tool_use", 515 - "id": block.id, 516 - "name": block.name, 517 - "input": block.input, 518 - } 519 - ) 382 + # Write user message and close stdin 383 + assert proc.stdin is not None 384 + proc.stdin.write(format_user_message(player_input)) 385 + proc.stdin.close() 520 386 521 - # Add assistant response to conversation (skip if empty) 522 - if assistant_content: 523 - self.messages.append({"role": "assistant", "content": assistant_content}) 524 - self._log_transcript("assistant_response", {"content": assistant_content}) 387 + # Drain stderr in background to prevent deadlock 388 + stderr_lines: list[str] = [] 389 + assert proc.stderr is not None 390 + stderr_thread = Thread( 391 + target=_drain_stderr, args=(proc.stderr, stderr_lines), daemon=True 392 + ) 393 + stderr_thread.start() 525 394 526 - # If there were tool uses, execute them and continue the loop 527 - if tool_uses: 528 - tool_results = [] 529 - for tool_use in tool_uses: 530 - tool_name = tool_use["name"] 531 - tool_input = tool_use["input"] 395 + # Parse NDJSON stream from stdout 396 + current_tool_json = "" 397 + assert proc.stdout is not None 532 398 533 - # Debug mode: show full tool call with truncated params 534 - if self.debug: 535 - params = ", ".join( 536 - f"{k}={_truncate(v)}" for k, v in tool_input.items() 537 - ) 538 - yield f"\n[→ {tool_name}({params})]\n" 539 - else: 540 - # Normal mode: show friendly messages 541 - if tool_name == "roll": 542 - reason = tool_input.get("reason", "") 543 - notation = tool_input.get("notation", "?") 544 - if reason: 545 - yield f"\n[{reason}: {notation}...]\n" 546 - else: 547 - yield f"\n[Rolling {notation}...]\n" 548 - elif tool_name == "recall": 549 - yield f"\n[Recalling: {tool_input.get('query', '?')}...]\n" 550 - elif tool_name == "update_character": 551 - yield "\n[Updating character sheet...]\n" 552 - elif tool_name == "create_character": 553 - name = tool_input.get("name", "character") 554 - yield f"\n[Creating {name}...]\n" 555 - elif tool_name == "set_scene": 556 - yield "\n[Setting scene...]\n" 557 - elif tool_name == "establish": 558 - name = tool_input.get("name", "?") 559 - yield f"\n[Establishing: {name}...]\n" 560 - elif tool_name == "note_discovery": 561 - entity = tool_input.get("entity", "?") 562 - yield f"\n[Noting: player learned about {entity}...]\n" 563 - elif tool_name == "mark_time": 564 - event = tool_input.get("event", "?") 565 - duration = tool_input.get("duration", "?") 566 - yield f"\n[{event} ({duration})]...\n" 567 - elif tool_name == "end_session": 568 - yield "\n[Saving session...]...\n" 399 + for raw_line in proc.stdout: 400 + line = raw_line.decode(errors="replace") 401 + self._log_transcript("stream_event", {"raw": line.strip()}) 569 402 570 - self._log_transcript( 571 - "tool_call", {"name": tool_name, "input": tool_input} 572 - ) 403 + event = parse_event(line) 404 + if event is None: 405 + continue 573 406 574 - result = execute_tool( 575 - tool_name, 576 - tool_input, 577 - world_id=self.world_id, 578 - player_id=self.player_id, 579 - base_path=self.base_path, 580 - campaign_log=self._campaign_log, 581 - ) 407 + match event: 408 + case TextDelta(text=text): 409 + yield text 582 410 583 - self._log_transcript( 584 - "tool_result", {"name": tool_name, "result": result} 585 - ) 411 + case ToolStart(name=name): 412 + short = name.rsplit("__", 1)[-1] if "__" in name else name 413 + if short == "end_session": 414 + self.session_ended = True 415 + if short == "create_character": 416 + self.character_created = True 586 417 587 - # Debug mode: show result 588 418 if self.debug: 589 - yield f"[⇐ {_truncate(result, max_len=100)}]\n\n" 590 - 591 - # Check if session ended 592 - if result == "SESSION_ENDED": 593 - self.session_ended = True 594 - result = "Session saved. Farewell!" 419 + yield f"\n[→ {short}(...)]\n" 420 + else: 421 + yield _tool_notification(name) 422 + current_tool_json = "" 595 423 596 - # Show dice roll results immediately (non-debug mode) 597 - if tool_name == "roll" and not self.debug: 598 - yield f"{result}\n" 424 + case ToolInputDelta(json_fragment=fragment): 425 + current_tool_json += fragment 599 426 600 - tool_results.append( 601 - { 602 - "type": "tool_result", 603 - "tool_use_id": tool_use["id"], 604 - "content": result or "Done", 605 - } 606 - ) 427 + case ToolStop(): 428 + if self.debug and current_tool_json: 429 + truncated = current_tool_json[:200] 430 + if len(current_tool_json) > 200: 431 + truncated += f"...+{len(current_tool_json) - 200}" 432 + yield f"[input: {truncated}]\n" 433 + current_tool_json = "" 607 434 608 - # Add tool results to conversation 609 - self.messages.append({"role": "user", "content": tool_results}) 435 + case Result() as r: 436 + self._session_id = r.session_id 437 + self._last_result = r 438 + self._total_input_tokens += r.usage.get("input_tokens", 0) 439 + self._total_output_tokens += r.usage.get("output_tokens", 0) 440 + self._log_transcript("result", { 441 + "session_id": r.session_id, 442 + "usage": r.usage, 443 + }) 610 444 611 - # Continue the loop to get Claude's response to the tool results 612 - continue 445 + proc.wait() 446 + stderr_thread.join() 613 447 614 - # No tool uses - we're done 615 - if final_message.stop_reason == "end_turn": 616 - break 448 + if proc.returncode and proc.returncode != 0: 449 + stderr_text = "".join(stderr_lines).strip() 450 + if stderr_text: 451 + yield f"\n[Error: {stderr_text[:200]}]\n" 617 452 618 453 def reset(self) -> None: 619 - """Reset the conversation history.""" 620 - self.messages = [] 454 + """Reset the conversation state.""" 455 + self._session_id = None 621 456 622 457 def get_current_time(self) -> str: 623 458 """Get the current game time as a display string."""
+146
src/storied/mcp_server.py
··· 1 + """In-process MCP server exposing storied game tools to Claude Code. 2 + 3 + start_server() launches an HTTP MCP server on localhost in a background 4 + thread. The engine passes its URL to claude -p via --mcp-config. Tools 5 + share state (CampaignLog, etc.) with the engine process. 6 + """ 7 + 8 + import socket 9 + import threading 10 + import time 11 + from pathlib import Path 12 + 13 + import anyio 14 + 15 + import uvicorn 16 + from mcp.server.lowlevel import Server 17 + from mcp.server.sse import SseServerTransport 18 + from mcp.types import TextContent, Tool 19 + 20 + from storied.log import CampaignLog 21 + from storied.tools import ( 22 + PLANNER_TOOL_DEFINITIONS, 23 + SEEDER_TOOL_DEFINITIONS, 24 + TOOL_DEFINITIONS, 25 + execute_tool, 26 + planner_execute_tool, 27 + seeder_execute_tool, 28 + ) 29 + 30 + TOOL_SETS: dict[str, list[dict]] = { 31 + "dm": TOOL_DEFINITIONS, 32 + "planner": PLANNER_TOOL_DEFINITIONS, 33 + "seeder": SEEDER_TOOL_DEFINITIONS, 34 + } 35 + 36 + EXECUTORS = { 37 + "dm": execute_tool, 38 + "planner": planner_execute_tool, 39 + "seeder": seeder_execute_tool, 40 + } 41 + 42 + 43 + def _to_mcp_tool(defn: dict) -> Tool: 44 + """Convert an Anthropic-format tool definition to an MCP Tool.""" 45 + return Tool( 46 + name=defn["name"], 47 + description=defn.get("description", ""), 48 + inputSchema=defn["input_schema"], 49 + ) 50 + 51 + 52 + def _find_free_port() -> int: 53 + """Find a free TCP port on localhost.""" 54 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 55 + s.bind(("127.0.0.1", 0)) 56 + return s.getsockname()[1] 57 + 58 + 59 + class MCPServerHandle: 60 + """Handle to a running in-process MCP server.""" 61 + 62 + def __init__(self, port: int, thread: threading.Thread): 63 + self.port = port 64 + self.url = f"http://127.0.0.1:{port}/sse" 65 + self._thread = thread 66 + 67 + def stop(self) -> None: 68 + """Stop the server (best-effort).""" 69 + if self._thread.is_alive(): 70 + self._thread.join(timeout=2) 71 + 72 + 73 + def start_server( 74 + world_id: str, 75 + player_id: str, 76 + base_path: Path, 77 + tool_set: str = "dm", 78 + campaign_log: CampaignLog | None = None, 79 + ) -> MCPServerHandle: 80 + """Start an in-process MCP HTTP server on a free localhost port. 81 + 82 + Returns an MCPServerHandle with the URL to pass to --mcp-config. 83 + The server runs in a daemon thread and shares state with the caller. 84 + """ 85 + if campaign_log is None: 86 + campaign_log = CampaignLog(world_id, base_path) 87 + 88 + definitions = TOOL_SETS.get(tool_set, TOOL_DEFINITIONS) 89 + executor = EXECUTORS.get(tool_set, execute_tool) 90 + mcp_tools = [_to_mcp_tool(d) for d in definitions] 91 + 92 + mcp = Server("storied") 93 + 94 + @mcp.list_tools() 95 + async def list_tools() -> list[Tool]: 96 + return mcp_tools 97 + 98 + @mcp.call_tool() 99 + async def call_tool(name: str, arguments: dict) -> list[TextContent]: 100 + result = executor( 101 + name, 102 + arguments, 103 + world_id=world_id, 104 + player_id=player_id, 105 + base_path=base_path, 106 + campaign_log=campaign_log, 107 + ) 108 + return [TextContent(type="text", text=str(result))] 109 + 110 + sse = SseServerTransport("/messages/") 111 + 112 + async def app(scope, receive, send): 113 + if scope["type"] != "http": 114 + return 115 + path = scope.get("path", "") 116 + if path == "/sse": 117 + try: 118 + async with sse.connect_sse(scope, receive, send) as streams: 119 + await mcp.run( 120 + streams[0], 121 + streams[1], 122 + mcp.create_initialization_options(), 123 + ) 124 + except (anyio.ClosedResourceError, anyio.BrokenResourceError): 125 + pass 126 + elif path.startswith("/messages/"): 127 + await sse.handle_post_message(scope, receive, send) 128 + 129 + port = _find_free_port() 130 + config = uvicorn.Config( 131 + app, host="127.0.0.1", port=port, log_level="warning", 132 + ) 133 + server = uvicorn.Server(config) 134 + 135 + thread = threading.Thread(target=server.run, daemon=True) 136 + thread.start() 137 + 138 + # Wait for the server to be ready 139 + for _ in range(50): 140 + try: 141 + with socket.create_connection(("127.0.0.1", port), timeout=0.1): 142 + break 143 + except OSError: 144 + time.sleep(0.1) 145 + 146 + return MCPServerHandle(port, thread)
+126 -187
src/storied/planner.py
··· 1 1 """World planner — enriches thin entities near the player's current position.""" 2 2 3 - import os 3 + import subprocess 4 4 import time 5 5 from collections.abc import Callable 6 6 from dataclasses import dataclass, field 7 7 from pathlib import Path 8 + from threading import Thread 8 9 9 - import anthropic 10 - 10 + from storied.character import format_character_context, load_character 11 + from storied.claude import ( 12 + Result, 13 + ToolStart, 14 + build_claude_args, 15 + build_env, 16 + build_mcp_config, 17 + format_user_message, 18 + parse_event, 19 + ) 11 20 from storied.engine import load_prompt 12 21 from storied.log import CampaignLog 22 + from storied.mcp_server import start_server as start_mcp_server 13 23 from storied.session import ( 14 24 extract_wiki_links, 15 - load_entity_content, 16 25 load_session, 17 26 resolve_wiki_link, 18 27 ) 19 - from storied.character import format_character_context, load_character 20 - from storied.tools import ( 21 - PLANNER_TOOL_DEFINITIONS, 22 - SEEDER_TOOL_DEFINITIONS, 23 - _load_entity, 24 - planner_execute_tool, 25 - seeder_execute_tool, 26 - ) 28 + from storied.tools import _load_entity 27 29 28 30 MAX_TURNS = 20 29 31 ··· 173 175 dry_run: bool = False 174 176 175 177 178 + def _drain_stderr(stderr, lines: list[str]) -> None: 179 + """Read all lines from stderr into the list.""" 180 + for raw in stderr: 181 + lines.append(raw.decode(errors="replace")) 182 + 183 + 184 + def _run_claude_collect( 185 + system_prompt: str, 186 + user_message: str, 187 + model: str, 188 + mcp_config: str, 189 + base_path: Path, 190 + on_progress: Callable[[str], None] | None = None, 191 + ) -> Result | None: 192 + """Run claude -p, collect tool events for progress, return the Result.""" 193 + args = build_claude_args( 194 + model=model, 195 + system_prompt=system_prompt, 196 + mcp_config=mcp_config, 197 + effort="high", 198 + persist_session=False, 199 + ) 200 + 201 + proc = subprocess.Popen( 202 + args, 203 + stdin=subprocess.PIPE, 204 + stdout=subprocess.PIPE, 205 + stderr=subprocess.PIPE, 206 + env=build_env(), 207 + cwd=str(base_path), 208 + ) 209 + 210 + assert proc.stdin is not None 211 + proc.stdin.write(format_user_message(user_message)) 212 + proc.stdin.close() 213 + 214 + stderr_lines: list[str] = [] 215 + assert proc.stderr is not None 216 + stderr_thread = Thread( 217 + target=_drain_stderr, args=(proc.stderr, stderr_lines), daemon=True 218 + ) 219 + stderr_thread.start() 220 + 221 + result: Result | None = None 222 + tool_calls = 0 223 + assert proc.stdout is not None 224 + 225 + for raw_line in proc.stdout: 226 + line = raw_line.decode(errors="replace") 227 + event = parse_event(line) 228 + if event is None: 229 + continue 230 + 231 + match event: 232 + case ToolStart(name=name): 233 + tool_calls += 1 234 + short = name.rsplit("__", 1)[-1] if "__" in name else name 235 + if on_progress: 236 + on_progress(f" [{short}]") 237 + 238 + case Result() as r: 239 + result = r 240 + 241 + proc.wait(timeout=600) 242 + stderr_thread.join() 243 + 244 + if result: 245 + result.usage["tool_calls"] = tool_calls 246 + 247 + return result 248 + 249 + 176 250 def plan_world( 177 251 world_id: str = "default", 178 252 player_id: str = "default", 179 253 base_path: Path | None = None, 180 - model: str = "claude-opus-4-5-20251101", 254 + model: str = "claude-opus-4-6", 181 255 threshold: float = 0.7, 182 256 max_entities: int = 8, 183 257 dry_run: bool = False, 184 258 on_progress: Callable[[str], None] | None = None, 185 259 ) -> PlanResult: 186 - """Enrich thin entities near the player's current position. 187 - 188 - 1. Load session state 189 - 2. Find nearby entities via wikilinks 190 - 3. Score each for richness 191 - 4. Filter below threshold, take top N thinnest 192 - 5. If not dry_run, run the model to enrich them 193 - 194 - on_progress is called with status messages as work happens. 195 - """ 260 + """Enrich thin entities near the player's current position.""" 196 261 if base_path is None: 197 262 base_path = Path.cwd() 198 263 ··· 218 283 for name, path in nearby: 219 284 score = entity_richness(path) 220 285 label = "rich" if score >= threshold else "thin" if score < 0.3 else "moderate" 221 - progress(f" {name}: {score:.1f} ({label}{', skipping' if score >= threshold else ''})") 286 + skip = ", skipping" if score >= threshold else "" 287 + progress(f" {name}: {score:.1f} ({label}{skip})") 222 288 if score < threshold: 223 289 scored.append((name, path, score)) 224 290 ··· 235 301 if dry_run or not candidates: 236 302 return result 237 303 238 - # Build context and run the agentic loop 304 + # Build context and run claude -p 239 305 candidate_pairs = [(name, path) for name, path, _ in candidates] 240 306 context = build_planning_context(world_id, player_id, base_path, candidate_pairs) 241 307 system_prompt = load_prompt("planner-system") 242 308 243 309 campaign_log = CampaignLog(world_id, base_path) 244 - 245 - client = anthropic.Anthropic( 246 - api_key=os.environ.get("STORIED_ANTHROPIC_API_KEY"), 310 + mcp = start_mcp_server( 311 + world_id, player_id, base_path, "planner", campaign_log, 247 312 ) 248 - 249 - messages: list[dict] = [ 250 - {"role": "user", "content": context}, 251 - ] 252 - 253 - total_input = 0 254 - total_output = 0 255 - total_tool_calls = 0 313 + mcp_config = build_mcp_config(mcp.url) 256 314 257 315 progress(f"Planning with {model}...") 258 316 259 - for turn in range(MAX_TURNS): 260 - if turn > 0: 261 - progress(" ...") 262 - response = client.messages.create( 263 - model=model, 264 - max_tokens=4096, 265 - system=system_prompt, 266 - tools=PLANNER_TOOL_DEFINITIONS, 267 - messages=messages, 268 - ) 317 + claude_result = _run_claude_collect( 318 + system_prompt=system_prompt, 319 + user_message=context, 320 + model=model, 321 + mcp_config=mcp_config, 322 + base_path=base_path, 323 + on_progress=on_progress, 324 + ) 269 325 270 - total_input += response.usage.input_tokens 271 - total_output += response.usage.output_tokens 326 + if claude_result: 327 + result.tool_calls = claude_result.usage.get("tool_calls", 0) 328 + result.input_tokens = claude_result.usage.get("input_tokens", 0) 329 + result.output_tokens = claude_result.usage.get("output_tokens", 0) 272 330 273 - # Build assistant message for conversation history 274 - assistant_content = [] 275 - tool_uses = [] 276 - for block in response.content: 277 - if block.type == "text": 278 - assistant_content.append({ 279 - "type": "text", 280 - "text": block.text, 281 - }) 282 - elif block.type == "tool_use": 283 - assistant_content.append({ 284 - "type": "tool_use", 285 - "id": block.id, 286 - "name": block.name, 287 - "input": block.input, 288 - }) 289 - tool_uses.append(block) 290 - 291 - if assistant_content: 292 - messages.append({"role": "assistant", "content": assistant_content}) 293 - 294 - if not tool_uses: 295 - break 296 - 297 - # Execute tools 298 - tool_results = [] 299 - for tool_use in tool_uses: 300 - total_tool_calls += 1 301 - 302 - tool_result = planner_execute_tool( 303 - tool_use.name, 304 - tool_use.input, 305 - world_id=world_id, 306 - base_path=base_path, 307 - campaign_log=campaign_log, 308 - ) 309 - 310 - progress(f" [{tool_use.name}] {tool_result}") 311 - 312 - tool_results.append({ 313 - "type": "tool_result", 314 - "tool_use_id": tool_use.id, 315 - "content": tool_result or "Done", 316 - }) 317 - 318 - messages.append({"role": "user", "content": tool_results}) 319 - 320 - if response.stop_reason == "end_turn": 321 - break 322 - 323 - result.tool_calls = total_tool_calls 324 - result.input_tokens = total_input 325 - result.output_tokens = total_output 326 331 result.elapsed = time.monotonic() - start_time 327 - 328 332 return result 329 333 330 334 ··· 342 346 world_id: str = "default", 343 347 player_id: str = "default", 344 348 base_path: Path | None = None, 345 - model: str = "claude-opus-4-5-20251101", 349 + model: str = "claude-opus-4-6", 346 350 on_progress: Callable[[str], None] | None = None, 347 351 ) -> SeedResult: 348 - """Build the initial world from a character sheet. 349 - 350 - Loads the character, sends it to a capable model with the world-seed 351 - prompt, and executes establish/set_scene/mark_time tools to populate 352 - the world before the first session. 353 - """ 352 + """Build the initial world from a character sheet.""" 354 353 if base_path is None: 355 354 base_path = Path.cwd() 356 355 ··· 367 366 368 367 # Build context from the character sheet 369 368 context = format_character_context(character) 370 - 371 369 system_prompt = load_prompt("world-seed") 370 + 372 371 campaign_log = CampaignLog(world_id, base_path) 373 - 374 - client = anthropic.Anthropic( 375 - api_key=os.environ.get("STORIED_ANTHROPIC_API_KEY"), 372 + mcp = start_mcp_server( 373 + world_id, player_id, base_path, "seeder", campaign_log, 376 374 ) 377 - 378 - messages: list[dict] = [ 379 - {"role": "user", "content": context}, 380 - ] 381 - 382 - total_input = 0 383 - total_output = 0 384 - total_tool_calls = 0 375 + mcp_config = build_mcp_config(mcp.url) 385 376 386 377 progress(f"Seeding with {model}...") 387 378 388 - for turn in range(MAX_TURNS): 389 - response = client.messages.create( 390 - model=model, 391 - max_tokens=4096, 392 - system=system_prompt, 393 - tools=SEEDER_TOOL_DEFINITIONS, 394 - messages=messages, 395 - ) 396 - 397 - total_input += response.usage.input_tokens 398 - total_output += response.usage.output_tokens 399 - 400 - # Build assistant message for conversation history 401 - assistant_content = [] 402 - tool_uses = [] 403 - for block in response.content: 404 - if block.type == "text": 405 - assistant_content.append({ 406 - "type": "text", 407 - "text": block.text, 408 - }) 409 - elif block.type == "tool_use": 410 - assistant_content.append({ 411 - "type": "tool_use", 412 - "id": block.id, 413 - "name": block.name, 414 - "input": block.input, 415 - }) 416 - tool_uses.append(block) 417 - 418 - if assistant_content: 419 - messages.append({"role": "assistant", "content": assistant_content}) 420 - 421 - if not tool_uses: 422 - break 423 - 424 - # Execute tools 425 - tool_results = [] 426 - for tool_use in tool_uses: 427 - total_tool_calls += 1 428 - 429 - tool_result = seeder_execute_tool( 430 - tool_use.name, 431 - tool_use.input, 432 - world_id=world_id, 433 - player_id=player_id, 434 - base_path=base_path, 435 - campaign_log=campaign_log, 436 - ) 379 + claude_result = _run_claude_collect( 380 + system_prompt=system_prompt, 381 + user_message=context, 382 + model=model, 383 + mcp_config=mcp_config, 384 + base_path=base_path, 385 + on_progress=on_progress, 386 + ) 437 387 438 - progress(f"[{tool_use.name}] {tool_result}") 388 + seed_result = SeedResult(elapsed=time.monotonic() - start_time) 439 389 440 - tool_results.append({ 441 - "type": "tool_result", 442 - "tool_use_id": tool_use.id, 443 - "content": tool_result or "Done", 444 - }) 390 + if claude_result: 391 + seed_result.tool_calls = claude_result.usage.get("tool_calls", 0) 392 + seed_result.input_tokens = claude_result.usage.get("input_tokens", 0) 393 + seed_result.output_tokens = claude_result.usage.get("output_tokens", 0) 445 394 446 - messages.append({"role": "user", "content": tool_results}) 447 - 448 - if response.stop_reason == "end_turn": 449 - break 450 - 451 - return SeedResult( 452 - tool_calls=total_tool_calls, 453 - input_tokens=total_input, 454 - output_tokens=total_output, 455 - elapsed=time.monotonic() - start_time, 456 - ) 395 + return seed_result
+172
tests/test_claude.py
··· 1 + """Tests for the claude -p subprocess driver.""" 2 + 3 + import json 4 + 5 + import pytest 6 + 7 + from storied.claude import ( 8 + Result, 9 + TextDelta, 10 + ToolInputDelta, 11 + ToolStart, 12 + ToolStop, 13 + build_claude_args, 14 + build_mcp_config, 15 + format_user_message, 16 + parse_event, 17 + ) 18 + 19 + 20 + class TestBuildMcpConfig: 21 + def test_produces_valid_json(self): 22 + config = build_mcp_config("http://127.0.0.1:9999/sse") 23 + parsed = json.loads(config) 24 + assert "mcpServers" in parsed 25 + assert "storied" in parsed["mcpServers"] 26 + 27 + def test_url_set(self): 28 + config = json.loads( 29 + build_mcp_config("http://127.0.0.1:8080/sse") 30 + ) 31 + server = config["mcpServers"]["storied"] 32 + assert server["url"] == "http://127.0.0.1:8080/sse" 33 + assert server["type"] == "sse" 34 + 35 + 36 + class TestBuildClaudeArgs: 37 + def test_new_session_includes_system_prompt(self, monkeypatch): 38 + monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/claude") 39 + args = build_claude_args("sonnet", "You are a DM", "{}") 40 + assert "--system-prompt" in args 41 + assert "You are a DM" in args 42 + assert "--model" in args 43 + assert "sonnet" in args 44 + 45 + def test_resume_session_skips_system_prompt(self, monkeypatch): 46 + monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/claude") 47 + args = build_claude_args( 48 + "sonnet", "ignored", "{}", 49 + resume_session_id="abc-123", 50 + ) 51 + assert "--resume" in args 52 + assert "abc-123" in args 53 + assert "--system-prompt" not in args 54 + 55 + def test_no_session_persistence_flag(self, monkeypatch): 56 + monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/claude") 57 + args = build_claude_args("sonnet", "sys", "{}", persist_session=False) 58 + assert "--no-session-persistence" in args 59 + 60 + def test_claude_not_found_raises(self, monkeypatch): 61 + monkeypatch.setattr("shutil.which", lambda _: None) 62 + with pytest.raises(FileNotFoundError, match="Claude Code CLI not found"): 63 + build_claude_args("sonnet", "sys", "{}") 64 + 65 + def test_tools_disabled(self, monkeypatch): 66 + monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/claude") 67 + args = build_claude_args("sonnet", "sys", "{}") 68 + idx = args.index("--tools") 69 + assert args[idx + 1] == "" 70 + 71 + def test_slash_commands_disabled(self, monkeypatch): 72 + monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/claude") 73 + args = build_claude_args("sonnet", "sys", "{}") 74 + assert "--disable-slash-commands" in args 75 + 76 + 77 + class TestFormatUserMessage: 78 + def test_produces_ndjson(self): 79 + msg = format_user_message("Hello world") 80 + parsed = json.loads(msg) 81 + assert parsed["type"] == "user" 82 + assert parsed["message"]["role"] == "user" 83 + content = parsed["message"]["content"] 84 + assert len(content) == 1 85 + assert content[0]["type"] == "text" 86 + assert content[0]["text"] == "Hello world" 87 + 88 + def test_ends_with_newline(self): 89 + msg = format_user_message("test") 90 + assert msg.endswith(b"\n") 91 + 92 + 93 + class TestParseEvent: 94 + def test_text_delta(self): 95 + line = json.dumps({ 96 + "type": "stream_event", 97 + "event": { 98 + "type": "content_block_delta", 99 + "index": 0, 100 + "delta": {"type": "text_delta", "text": "Hello"}, 101 + }, 102 + }) 103 + event = parse_event(line) 104 + assert isinstance(event, TextDelta) 105 + assert event.text == "Hello" 106 + 107 + def test_tool_start(self): 108 + line = json.dumps({ 109 + "type": "stream_event", 110 + "event": { 111 + "type": "content_block_start", 112 + "index": 1, 113 + "content_block": {"type": "tool_use", "id": "t1", "name": "mcp__storied__roll"}, 114 + }, 115 + }) 116 + event = parse_event(line) 117 + assert isinstance(event, ToolStart) 118 + assert event.name == "mcp__storied__roll" 119 + 120 + def test_tool_input_delta(self): 121 + line = json.dumps({ 122 + "type": "stream_event", 123 + "event": { 124 + "type": "content_block_delta", 125 + "index": 1, 126 + "delta": {"type": "input_json_delta", "partial_json": '{"notation"'}, 127 + }, 128 + }) 129 + event = parse_event(line) 130 + assert isinstance(event, ToolInputDelta) 131 + assert event.json_fragment == '{"notation"' 132 + 133 + def test_tool_stop(self): 134 + line = json.dumps({ 135 + "type": "stream_event", 136 + "event": {"type": "content_block_stop", "index": 1}, 137 + }) 138 + event = parse_event(line) 139 + assert isinstance(event, ToolStop) 140 + 141 + def test_result(self): 142 + line = json.dumps({ 143 + "type": "result", 144 + "session_id": "sess-1", 145 + "usage": {"input_tokens": 100, "output_tokens": 50}, 146 + "duration_ms": 1234, 147 + }) 148 + event = parse_event(line) 149 + assert isinstance(event, Result) 150 + assert event.session_id == "sess-1" 151 + assert event.usage["input_tokens"] == 100 152 + 153 + def test_empty_line(self): 154 + assert parse_event("") is None 155 + assert parse_event(" ") is None 156 + 157 + def test_invalid_json(self): 158 + assert parse_event("not json") is None 159 + 160 + def test_unknown_event_type(self): 161 + assert parse_event(json.dumps({"type": "unknown"})) is None 162 + 163 + def test_text_block_start_ignored(self): 164 + line = json.dumps({ 165 + "type": "stream_event", 166 + "event": { 167 + "type": "content_block_start", 168 + "index": 0, 169 + "content_block": {"type": "text"}, 170 + }, 171 + }) 172 + assert parse_event(line) is None
+33
tests/test_mcp_server.py
··· 1 + """Tests for the MCP server tool dispatch.""" 2 + 3 + import pytest 4 + 5 + from storied.mcp_server import _to_mcp_tool 6 + from storied.tools import TOOL_DEFINITIONS 7 + 8 + 9 + class TestToMcpTool: 10 + def test_converts_name(self): 11 + defn = TOOL_DEFINITIONS[0] # roll 12 + tool = _to_mcp_tool(defn) 13 + assert tool.name == "roll" 14 + 15 + def test_converts_description(self): 16 + defn = TOOL_DEFINITIONS[0] 17 + tool = _to_mcp_tool(defn) 18 + assert tool.description is not None 19 + assert len(tool.description) > 0 20 + 21 + def test_converts_input_schema(self): 22 + defn = TOOL_DEFINITIONS[0] 23 + tool = _to_mcp_tool(defn) 24 + assert tool.inputSchema["type"] == "object" 25 + assert "notation" in tool.inputSchema["properties"] 26 + 27 + @pytest.mark.parametrize( 28 + "defn", TOOL_DEFINITIONS, ids=[d["name"] for d in TOOL_DEFINITIONS] 29 + ) 30 + def test_all_tools_convert(self, defn: dict): 31 + tool = _to_mcp_tool(defn) 32 + assert tool.name == defn["name"] 33 + assert tool.inputSchema is not None
+46 -56
tests/test_planner.py
··· 1 1 """Tests for the world planner — entity richness scoring, discovery, and context.""" 2 2 3 + import json 3 4 from pathlib import Path 4 5 from unittest.mock import MagicMock, patch 5 6 ··· 369 370 ) 370 371 assert len(result.candidates) == 0 371 372 372 - @patch("storied.planner.anthropic.Anthropic") 373 - def test_plan_world_calls_api(self, mock_anthropic_cls: MagicMock, populated_world: Path): 373 + @patch("storied.planner.subprocess.Popen") 374 + def test_plan_world_calls_claude(self, mock_popen: MagicMock, populated_world: Path): 374 375 save_session( 375 376 "default", 376 377 { ··· 380 381 populated_world, 381 382 ) 382 383 383 - # Set up mock to return an end_turn response with no tool use 384 - mock_client = MagicMock() 385 - mock_anthropic_cls.return_value = mock_client 386 - mock_response = MagicMock() 387 - mock_response.stop_reason = "end_turn" 388 - mock_response.content = [MagicMock(type="text", text="Done enriching.")] 389 - mock_response.usage.input_tokens = 1000 390 - mock_response.usage.output_tokens = 200 391 - mock_response.usage.cache_creation_input_tokens = 0 392 - mock_response.usage.cache_read_input_tokens = 0 393 - mock_client.messages.create.return_value = mock_response 384 + # Mock subprocess returning a result event 385 + result_line = json.dumps({ 386 + "type": "result", 387 + "session_id": "sess-1", 388 + "usage": {"input_tokens": 1000, "output_tokens": 200, "tool_calls": 0}, 389 + "duration_ms": 5000, 390 + }) 391 + mock_proc = MagicMock() 392 + mock_proc.stdin = MagicMock() 393 + mock_proc.stdout = iter([result_line.encode() + b"\n"]) 394 + mock_proc.stderr = iter([]) 395 + mock_proc.wait.return_value = 0 396 + mock_proc.returncode = 0 397 + mock_popen.return_value = mock_proc 394 398 395 399 result = plan_world( 396 400 world_id="test-world", 397 401 player_id="default", 398 402 base_path=populated_world, 399 - model="claude-opus-4-5-20251101", 403 + model="claude-opus-4-6", 400 404 ) 401 405 402 406 assert result.dry_run is False 403 407 assert result.input_tokens == 1000 404 408 assert result.output_tokens == 200 405 - mock_client.messages.create.assert_called_once() 409 + mock_popen.assert_called_once() 406 410 407 - @patch("storied.planner.anthropic.Anthropic") 408 - def test_plan_world_executes_tools(self, mock_anthropic_cls: MagicMock, populated_world: Path): 411 + @patch("storied.planner.subprocess.Popen") 412 + def test_plan_world_counts_tool_calls(self, mock_popen: MagicMock, populated_world: Path): 409 413 save_session( 410 414 "default", 411 415 { ··· 415 419 populated_world, 416 420 ) 417 421 418 - mock_client = MagicMock() 419 - mock_anthropic_cls.return_value = mock_client 420 - 421 - # First response: tool use (establish) 422 - tool_block = MagicMock() 423 - tool_block.type = "tool_use" 424 - tool_block.id = "tool_1" 425 - tool_block.name = "establish" 426 - tool_block.input = { 427 - "entity_type": "npcs", 428 - "name": "Thin NPC", 429 - "knows": ["Has a secret past"], 430 - "wants": ["Find a warm meal"], 431 - } 432 - first_response = MagicMock() 433 - first_response.stop_reason = "tool_use" 434 - first_response.content = [tool_block] 435 - first_response.usage.input_tokens = 800 436 - first_response.usage.output_tokens = 100 437 - first_response.usage.cache_creation_input_tokens = 0 438 - first_response.usage.cache_read_input_tokens = 0 439 - 440 - # Second response: end_turn 441 - second_response = MagicMock() 442 - second_response.stop_reason = "end_turn" 443 - second_response.content = [MagicMock(type="text", text="Done.")] 444 - second_response.usage.input_tokens = 900 445 - second_response.usage.output_tokens = 50 446 - second_response.usage.cache_creation_input_tokens = 0 447 - second_response.usage.cache_read_input_tokens = 0 422 + # Stream with tool_use events followed by result 423 + lines = [ 424 + json.dumps({ 425 + "type": "stream_event", 426 + "event": { 427 + "type": "content_block_start", 428 + "index": 1, 429 + "content_block": {"type": "tool_use", "id": "t1", "name": "mcp__storied__establish"}, 430 + }, 431 + }), 432 + json.dumps({ 433 + "type": "result", 434 + "session_id": "sess-2", 435 + "usage": {"input_tokens": 800, "output_tokens": 100}, 436 + "duration_ms": 3000, 437 + }), 438 + ] 448 439 449 - mock_client.messages.create.side_effect = [first_response, second_response] 440 + mock_proc = MagicMock() 441 + mock_proc.stdin = MagicMock() 442 + mock_proc.stdout = iter([l.encode() + b"\n" for l in lines]) 443 + mock_proc.stderr = iter([]) 444 + mock_proc.wait.return_value = 0 445 + mock_proc.returncode = 0 446 + mock_popen.return_value = mock_proc 450 447 451 448 result = plan_world( 452 449 world_id="test-world", 453 450 player_id="default", 454 451 base_path=populated_world, 455 - model="claude-opus-4-5-20251101", 452 + model="claude-opus-4-6", 456 453 ) 457 454 458 455 assert result.tool_calls == 1 459 - assert result.input_tokens == 1700 460 - assert result.output_tokens == 150 461 - 462 - # Verify the entity was actually enriched 463 - content = (populated_world / "worlds/test-world/npcs/Thin NPC.md").read_text() 464 - assert "Has a secret past" in content 465 - assert "Find a warm meal" in content
+97 -99
tests/test_seeder.py
··· 1 1 """Tests for world seeding — empty world detection and initial worldbuilding.""" 2 2 3 + import json 3 4 from pathlib import Path 4 5 from unittest.mock import MagicMock, patch 5 6 ··· 96 97 class TestSeedWorld: 97 98 """Tests for the seed_world orchestrator.""" 98 99 99 - @patch("storied.planner.anthropic.Anthropic") 100 - def test_seed_world_builds_context_from_character( 101 - self, mock_anthropic_cls: MagicMock, character_world: Path 100 + @patch("storied.planner.subprocess.Popen") 101 + def test_seed_world_calls_claude( 102 + self, mock_popen: MagicMock, character_world: Path 102 103 ): 103 - mock_client = MagicMock() 104 - mock_anthropic_cls.return_value = mock_client 105 - mock_response = MagicMock() 106 - mock_response.stop_reason = "end_turn" 107 - mock_response.content = [MagicMock(type="text", text="Done.")] 108 - mock_response.usage.input_tokens = 500 109 - mock_response.usage.output_tokens = 100 110 - mock_client.messages.create.return_value = mock_response 104 + result_line = json.dumps({ 105 + "type": "result", 106 + "session_id": "sess-1", 107 + "usage": {"input_tokens": 500, "output_tokens": 100}, 108 + "duration_ms": 3000, 109 + }) 110 + mock_proc = MagicMock() 111 + mock_proc.stdin = MagicMock() 112 + mock_proc.stdout = iter([result_line.encode() + b"\n"]) 113 + mock_proc.stderr = iter([]) 114 + mock_proc.wait.return_value = 0 115 + mock_proc.returncode = 0 116 + mock_popen.return_value = mock_proc 111 117 112 118 seed_world( 113 119 world_id="default", ··· 115 121 base_path=character_world, 116 122 ) 117 123 118 - call_kwargs = mock_client.messages.create.call_args 119 - messages = call_kwargs.kwargs["messages"] 120 - user_content = messages[0]["content"] 121 - assert "Kael Stormborn" in user_content 122 - assert "Fighter" in user_content 124 + # Verify the subprocess was called with claude args 125 + mock_popen.assert_called_once() 126 + call_args = mock_popen.call_args 127 + args_list = call_args[0][0] if call_args[0] else call_args[1].get("args", []) 128 + # Should include --system-prompt and --mcp-config 129 + assert any("--system-prompt" in str(a) for a in args_list) 123 130 124 - @patch("storied.planner.anthropic.Anthropic") 125 - def test_seed_world_executes_tools( 126 - self, mock_anthropic_cls: MagicMock, character_world: Path 131 + @patch("storied.planner.subprocess.Popen") 132 + def test_seed_world_counts_tool_calls( 133 + self, mock_popen: MagicMock, character_world: Path 127 134 ): 128 - mock_client = MagicMock() 129 - mock_anthropic_cls.return_value = mock_client 130 - 131 - # First response: tool use (establish + set_scene) 132 - establish_block = MagicMock() 133 - establish_block.type = "tool_use" 134 - establish_block.id = "tool_1" 135 - establish_block.name = "establish" 136 - establish_block.input = { 137 - "entity_type": "locations", 138 - "name": "Millhaven", 139 - "description": "A farming village.", 140 - } 141 - scene_block = MagicMock() 142 - scene_block.type = "tool_use" 143 - scene_block.id = "tool_2" 144 - scene_block.name = "set_scene" 145 - scene_block.input = { 146 - "situation": "Kael walks west at dawn.", 147 - "location": "Millhaven", 148 - } 149 - first_response = MagicMock() 150 - first_response.stop_reason = "tool_use" 151 - first_response.content = [establish_block, scene_block] 152 - first_response.usage.input_tokens = 800 153 - first_response.usage.output_tokens = 150 154 - 155 - # Second response: end_turn 156 - second_response = MagicMock() 157 - second_response.stop_reason = "end_turn" 158 - second_response.content = [MagicMock(type="text", text="Done.")] 159 - second_response.usage.input_tokens = 900 160 - second_response.usage.output_tokens = 50 135 + lines = [ 136 + json.dumps({ 137 + "type": "stream_event", 138 + "event": { 139 + "type": "content_block_start", 140 + "index": 0, 141 + "content_block": {"type": "tool_use", "id": "t1", "name": "mcp__storied__establish"}, 142 + }, 143 + }), 144 + json.dumps({ 145 + "type": "stream_event", 146 + "event": { 147 + "type": "content_block_start", 148 + "index": 1, 149 + "content_block": {"type": "tool_use", "id": "t2", "name": "mcp__storied__set_scene"}, 150 + }, 151 + }), 152 + json.dumps({ 153 + "type": "result", 154 + "session_id": "sess-2", 155 + "usage": {"input_tokens": 800, "output_tokens": 150}, 156 + "duration_ms": 5000, 157 + }), 158 + ] 161 159 162 - mock_client.messages.create.side_effect = [first_response, second_response] 160 + mock_proc = MagicMock() 161 + mock_proc.stdin = MagicMock() 162 + mock_proc.stdout = iter([l.encode() + b"\n" for l in lines]) 163 + mock_proc.stderr = iter([]) 164 + mock_proc.wait.return_value = 0 165 + mock_proc.returncode = 0 166 + mock_popen.return_value = mock_proc 163 167 164 168 result = seed_world( 165 169 world_id="default", ··· 169 173 170 174 assert result.tool_calls == 2 171 175 172 - # Verify establish created the entity 173 - entity = character_world / "worlds/default/locations/Millhaven.md" 174 - assert entity.exists() 175 - assert "farming village" in entity.read_text() 176 - 177 - # Verify set_scene created the session 178 - session = character_world / "players/default/session.md" 179 - assert session.exists() 180 - assert "Kael walks west" in session.read_text() 181 - 182 - @patch("storied.planner.anthropic.Anthropic") 176 + @patch("storied.planner.subprocess.Popen") 183 177 def test_seed_world_returns_result( 184 - self, mock_anthropic_cls: MagicMock, character_world: Path 178 + self, mock_popen: MagicMock, character_world: Path 185 179 ): 186 - mock_client = MagicMock() 187 - mock_anthropic_cls.return_value = mock_client 188 - mock_response = MagicMock() 189 - mock_response.stop_reason = "end_turn" 190 - mock_response.content = [MagicMock(type="text", text="Done.")] 191 - mock_response.usage.input_tokens = 500 192 - mock_response.usage.output_tokens = 100 193 - mock_client.messages.create.return_value = mock_response 180 + result_line = json.dumps({ 181 + "type": "result", 182 + "session_id": "sess-1", 183 + "usage": {"input_tokens": 500, "output_tokens": 100}, 184 + "duration_ms": 3000, 185 + }) 186 + mock_proc = MagicMock() 187 + mock_proc.stdin = MagicMock() 188 + mock_proc.stdout = iter([result_line.encode() + b"\n"]) 189 + mock_proc.stderr = iter([]) 190 + mock_proc.wait.return_value = 0 191 + mock_proc.returncode = 0 192 + mock_popen.return_value = mock_proc 194 193 195 194 result = seed_world( 196 195 world_id="default", ··· 203 202 assert result.output_tokens == 100 204 203 assert result.elapsed > 0 205 204 206 - @patch("storied.planner.anthropic.Anthropic") 205 + @patch("storied.planner.subprocess.Popen") 207 206 def test_seed_world_reports_progress( 208 - self, mock_anthropic_cls: MagicMock, character_world: Path 207 + self, mock_popen: MagicMock, character_world: Path 209 208 ): 210 - mock_client = MagicMock() 211 - mock_anthropic_cls.return_value = mock_client 209 + lines = [ 210 + json.dumps({ 211 + "type": "stream_event", 212 + "event": { 213 + "type": "content_block_start", 214 + "index": 0, 215 + "content_block": {"type": "tool_use", "id": "t1", "name": "mcp__storied__establish"}, 216 + }, 217 + }), 218 + json.dumps({ 219 + "type": "result", 220 + "session_id": "sess-1", 221 + "usage": {"input_tokens": 800, "output_tokens": 100}, 222 + "duration_ms": 3000, 223 + }), 224 + ] 212 225 213 - establish_block = MagicMock() 214 - establish_block.type = "tool_use" 215 - establish_block.id = "tool_1" 216 - establish_block.name = "establish" 217 - establish_block.input = { 218 - "entity_type": "npcs", 219 - "name": "Elara", 220 - "description": "A healer.", 221 - } 222 - first_response = MagicMock() 223 - first_response.stop_reason = "tool_use" 224 - first_response.content = [establish_block] 225 - first_response.usage.input_tokens = 800 226 - first_response.usage.output_tokens = 100 227 - 228 - second_response = MagicMock() 229 - second_response.stop_reason = "end_turn" 230 - second_response.content = [MagicMock(type="text", text="Done.")] 231 - second_response.usage.input_tokens = 900 232 - second_response.usage.output_tokens = 50 233 - 234 - mock_client.messages.create.side_effect = [first_response, second_response] 226 + mock_proc = MagicMock() 227 + mock_proc.stdin = MagicMock() 228 + mock_proc.stdout = iter([l.encode() + b"\n" for l in lines]) 229 + mock_proc.stderr = iter([]) 230 + mock_proc.wait.return_value = 0 231 + mock_proc.returncode = 0 232 + mock_popen.return_value = mock_proc 235 233 236 234 progress_messages: list[str] = [] 237 235 seed_world(
+383 -100
uv.lock
··· 12 12 ] 13 13 14 14 [[package]] 15 - name = "anthropic" 16 - version = "0.75.0" 17 - source = { registry = "https://pypi.org/simple" } 18 - dependencies = [ 19 - { name = "anyio" }, 20 - { name = "distro" }, 21 - { name = "docstring-parser" }, 22 - { name = "httpx" }, 23 - { name = "jiter" }, 24 - { name = "pydantic" }, 25 - { name = "sniffio" }, 26 - { name = "typing-extensions" }, 27 - ] 28 - sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } 29 - wheels = [ 30 - { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, 31 - ] 32 - 33 - [[package]] 34 15 name = "anyio" 35 16 version = "4.12.0" 36 17 source = { registry = "https://pypi.org/simple" } ··· 53 34 ] 54 35 55 36 [[package]] 37 + name = "attrs" 38 + version = "26.1.0" 39 + source = { registry = "https://pypi.org/simple" } 40 + sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } 41 + wheels = [ 42 + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, 43 + ] 44 + 45 + [[package]] 56 46 name = "certifi" 57 47 version = "2025.11.12" 58 48 source = { registry = "https://pypi.org/simple" } ··· 62 52 ] 63 53 64 54 [[package]] 55 + name = "cffi" 56 + version = "2.0.0" 57 + source = { registry = "https://pypi.org/simple" } 58 + dependencies = [ 59 + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, 60 + ] 61 + sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } 62 + wheels = [ 63 + { 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, upload-time = "2025-09-08T23:22:44.795Z" }, 64 + { 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, upload-time = "2025-09-08T23:22:45.938Z" }, 65 + { 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, upload-time = "2025-09-08T23:22:47.349Z" }, 66 + { 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, upload-time = "2025-09-08T23:22:48.677Z" }, 67 + { 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, upload-time = "2025-09-08T23:22:50.06Z" }, 68 + { 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, upload-time = "2025-09-08T23:22:51.364Z" }, 69 + { 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, upload-time = "2025-09-08T23:22:52.902Z" }, 70 + { 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, upload-time = "2025-09-08T23:22:54.518Z" }, 71 + { 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, upload-time = "2025-09-08T23:22:55.867Z" }, 72 + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, 73 + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, 74 + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, 75 + { 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, upload-time = "2025-09-08T23:23:00.879Z" }, 76 + { 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, upload-time = "2025-09-08T23:23:02.231Z" }, 77 + { 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, upload-time = "2025-09-08T23:23:03.472Z" }, 78 + { 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, upload-time = "2025-09-08T23:23:04.792Z" }, 79 + { 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, upload-time = "2025-09-08T23:23:06.127Z" }, 80 + { 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, upload-time = "2025-09-08T23:23:07.753Z" }, 81 + { 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, upload-time = "2025-09-08T23:23:09.648Z" }, 82 + { 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, upload-time = "2025-09-08T23:23:10.928Z" }, 83 + { 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, upload-time = "2025-09-08T23:23:12.42Z" }, 84 + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, 85 + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, 86 + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, 87 + { 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, upload-time = "2025-09-08T23:23:18.087Z" }, 88 + { 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, upload-time = "2025-09-08T23:23:19.622Z" }, 89 + { 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, upload-time = "2025-09-08T23:23:20.853Z" }, 90 + { 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, upload-time = "2025-09-08T23:23:22.08Z" }, 91 + { 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, upload-time = "2025-09-08T23:23:23.314Z" }, 92 + { 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, upload-time = "2025-09-08T23:23:24.541Z" }, 93 + { 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, upload-time = "2025-09-08T23:23:26.143Z" }, 94 + { 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, upload-time = "2025-09-08T23:23:27.873Z" }, 95 + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, 96 + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, 97 + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, 98 + { 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, upload-time = "2025-09-08T23:23:29.347Z" }, 99 + { 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, upload-time = "2025-09-08T23:23:30.63Z" }, 100 + { 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, upload-time = "2025-09-08T23:23:31.91Z" }, 101 + { 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, upload-time = "2025-09-08T23:23:33.214Z" }, 102 + { 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, upload-time = "2025-09-08T23:23:34.495Z" }, 103 + { 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, upload-time = "2025-09-08T23:23:36.096Z" }, 104 + { 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, upload-time = "2025-09-08T23:23:37.328Z" }, 105 + { 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, upload-time = "2025-09-08T23:23:38.945Z" }, 106 + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, 107 + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, 108 + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, 109 + ] 110 + 111 + [[package]] 112 + name = "click" 113 + version = "8.3.1" 114 + source = { registry = "https://pypi.org/simple" } 115 + dependencies = [ 116 + { name = "colorama", marker = "sys_platform == 'win32'" }, 117 + ] 118 + sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } 119 + wheels = [ 120 + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, 121 + ] 122 + 123 + [[package]] 65 124 name = "colorama" 66 125 version = "0.4.6" 67 126 source = { registry = "https://pypi.org/simple" } ··· 145 204 ] 146 205 147 206 [[package]] 148 - name = "distro" 149 - version = "1.9.0" 207 + name = "cryptography" 208 + version = "46.0.5" 150 209 source = { registry = "https://pypi.org/simple" } 151 - sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } 152 - wheels = [ 153 - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, 210 + dependencies = [ 211 + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, 154 212 ] 155 - 156 - [[package]] 157 - name = "docstring-parser" 158 - version = "0.17.0" 159 - source = { registry = "https://pypi.org/simple" } 160 - sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } 213 + sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } 161 214 wheels = [ 162 - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, 215 + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, 216 + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, 217 + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, 218 + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, 219 + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, 220 + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, 221 + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, 222 + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, 223 + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, 224 + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, 225 + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, 226 + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, 227 + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, 228 + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, 229 + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, 230 + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, 231 + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, 232 + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, 233 + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, 234 + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, 235 + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, 236 + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, 237 + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, 238 + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, 239 + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, 240 + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, 241 + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, 242 + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, 243 + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, 244 + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, 245 + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, 246 + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, 247 + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, 248 + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, 249 + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, 250 + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, 251 + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, 252 + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, 253 + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, 254 + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, 255 + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, 256 + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, 163 257 ] 164 258 165 259 [[package]] ··· 200 294 ] 201 295 202 296 [[package]] 297 + name = "httpx-sse" 298 + version = "0.4.3" 299 + source = { registry = "https://pypi.org/simple" } 300 + sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } 301 + wheels = [ 302 + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, 303 + ] 304 + 305 + [[package]] 203 306 name = "idna" 204 307 version = "3.11" 205 308 source = { registry = "https://pypi.org/simple" } ··· 218 321 ] 219 322 220 323 [[package]] 221 - name = "jiter" 222 - version = "0.12.0" 324 + name = "jsonschema" 325 + version = "4.26.0" 326 + source = { registry = "https://pypi.org/simple" } 327 + dependencies = [ 328 + { name = "attrs" }, 329 + { name = "jsonschema-specifications" }, 330 + { name = "referencing" }, 331 + { name = "rpds-py" }, 332 + ] 333 + sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } 334 + wheels = [ 335 + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, 336 + ] 337 + 338 + [[package]] 339 + name = "jsonschema-specifications" 340 + version = "2025.9.1" 223 341 source = { registry = "https://pypi.org/simple" } 224 - sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } 342 + dependencies = [ 343 + { name = "referencing" }, 344 + ] 345 + sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } 225 346 wheels = [ 226 - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, 227 - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, 228 - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, 229 - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, 230 - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, 231 - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, 232 - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, 233 - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, 234 - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, 235 - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, 236 - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, 237 - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, 238 - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, 239 - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, 240 - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, 241 - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, 242 - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, 243 - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, 244 - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, 245 - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, 246 - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, 247 - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, 248 - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, 249 - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, 250 - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, 251 - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, 252 - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, 253 - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, 254 - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, 255 - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, 256 - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, 257 - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, 258 - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, 259 - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, 260 - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, 261 - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, 262 - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, 263 - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, 264 - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, 265 - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, 266 - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, 267 - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, 268 - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, 269 - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, 270 - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, 271 - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, 272 - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, 273 - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, 274 - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, 275 - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, 276 - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, 277 - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, 278 - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, 279 - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, 280 - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, 281 - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, 282 - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, 283 - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, 284 - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, 285 - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, 347 + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, 286 348 ] 287 349 288 350 [[package]] ··· 350 412 ] 351 413 352 414 [[package]] 415 + name = "mcp" 416 + version = "1.26.0" 417 + source = { registry = "https://pypi.org/simple" } 418 + dependencies = [ 419 + { name = "anyio" }, 420 + { name = "httpx" }, 421 + { name = "httpx-sse" }, 422 + { name = "jsonschema" }, 423 + { name = "pydantic" }, 424 + { name = "pydantic-settings" }, 425 + { name = "pyjwt", extra = ["crypto"] }, 426 + { name = "python-multipart" }, 427 + { name = "pywin32", marker = "sys_platform == 'win32'" }, 428 + { name = "sse-starlette" }, 429 + { name = "starlette" }, 430 + { name = "typing-extensions" }, 431 + { name = "typing-inspection" }, 432 + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, 433 + ] 434 + sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } 435 + wheels = [ 436 + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, 437 + ] 438 + 439 + [[package]] 353 440 name = "mdurl" 354 441 version = "0.1.2" 355 442 source = { registry = "https://pypi.org/simple" } ··· 428 515 ] 429 516 430 517 [[package]] 518 + name = "pycparser" 519 + version = "3.0" 520 + source = { registry = "https://pypi.org/simple" } 521 + sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } 522 + wheels = [ 523 + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, 524 + ] 525 + 526 + [[package]] 431 527 name = "pydantic" 432 528 version = "2.12.5" 433 529 source = { registry = "https://pypi.org/simple" } ··· 514 610 ] 515 611 516 612 [[package]] 613 + name = "pydantic-settings" 614 + version = "2.13.1" 615 + source = { registry = "https://pypi.org/simple" } 616 + dependencies = [ 617 + { name = "pydantic" }, 618 + { name = "python-dotenv" }, 619 + { name = "typing-inspection" }, 620 + ] 621 + sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } 622 + wheels = [ 623 + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, 624 + ] 625 + 626 + [[package]] 517 627 name = "pygments" 518 628 version = "2.19.2" 519 629 source = { registry = "https://pypi.org/simple" } ··· 523 633 ] 524 634 525 635 [[package]] 636 + name = "pyjwt" 637 + version = "2.12.1" 638 + source = { registry = "https://pypi.org/simple" } 639 + sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } 640 + wheels = [ 641 + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, 642 + ] 643 + 644 + [package.optional-dependencies] 645 + crypto = [ 646 + { name = "cryptography" }, 647 + ] 648 + 649 + [[package]] 526 650 name = "pymupdf" 527 651 version = "1.26.6" 528 652 source = { registry = "https://pypi.org/simple" } ··· 580 704 ] 581 705 582 706 [[package]] 707 + name = "python-dotenv" 708 + version = "1.2.2" 709 + source = { registry = "https://pypi.org/simple" } 710 + sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } 711 + wheels = [ 712 + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, 713 + ] 714 + 715 + [[package]] 716 + name = "python-multipart" 717 + version = "0.0.22" 718 + source = { registry = "https://pypi.org/simple" } 719 + sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } 720 + wheels = [ 721 + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, 722 + ] 723 + 724 + [[package]] 725 + name = "pywin32" 726 + version = "311" 727 + source = { registry = "https://pypi.org/simple" } 728 + wheels = [ 729 + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, 730 + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, 731 + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, 732 + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, 733 + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, 734 + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, 735 + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, 736 + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, 737 + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, 738 + ] 739 + 740 + [[package]] 583 741 name = "pyyaml" 584 742 version = "6.0.3" 585 743 source = { registry = "https://pypi.org/simple" } ··· 626 784 ] 627 785 628 786 [[package]] 787 + name = "referencing" 788 + version = "0.37.0" 789 + source = { registry = "https://pypi.org/simple" } 790 + dependencies = [ 791 + { name = "attrs" }, 792 + { name = "rpds-py" }, 793 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 794 + ] 795 + sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } 796 + wheels = [ 797 + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, 798 + ] 799 + 800 + [[package]] 629 801 name = "rich" 630 802 version = "14.2.0" 631 803 source = { registry = "https://pypi.org/simple" } ··· 639 811 ] 640 812 641 813 [[package]] 814 + name = "rpds-py" 815 + version = "0.30.0" 816 + source = { registry = "https://pypi.org/simple" } 817 + sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } 818 + wheels = [ 819 + { 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, upload-time = "2025-11-30T20:22:17.93Z" }, 820 + { 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, upload-time = "2025-11-30T20:22:19.297Z" }, 821 + { 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, upload-time = "2025-11-30T20:22:21.661Z" }, 822 + { 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, upload-time = "2025-11-30T20:22:23.408Z" }, 823 + { 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, upload-time = "2025-11-30T20:22:25.16Z" }, 824 + { 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, upload-time = "2025-11-30T20:22:26.505Z" }, 825 + { 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, upload-time = "2025-11-30T20:22:27.934Z" }, 826 + { 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, upload-time = "2025-11-30T20:22:29.341Z" }, 827 + { 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, upload-time = "2025-11-30T20:22:31.469Z" }, 828 + { 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, upload-time = "2025-11-30T20:22:32.997Z" }, 829 + { 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, upload-time = "2025-11-30T20:22:34.419Z" }, 830 + { 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, upload-time = "2025-11-30T20:22:35.903Z" }, 831 + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, 832 + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, 833 + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, 834 + { 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, upload-time = "2025-11-30T20:22:41.812Z" }, 835 + { 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, upload-time = "2025-11-30T20:22:43.479Z" }, 836 + { 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, upload-time = "2025-11-30T20:22:44.819Z" }, 837 + { 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, upload-time = "2025-11-30T20:22:46.103Z" }, 838 + { 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, upload-time = "2025-11-30T20:22:47.458Z" }, 839 + { 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, upload-time = "2025-11-30T20:22:48.872Z" }, 840 + { 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, upload-time = "2025-11-30T20:22:50.196Z" }, 841 + { 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, upload-time = "2025-11-30T20:22:51.87Z" }, 842 + { 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, upload-time = "2025-11-30T20:22:53.341Z" }, 843 + { 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, upload-time = "2025-11-30T20:22:54.778Z" }, 844 + { 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, upload-time = "2025-11-30T20:22:56.212Z" }, 845 + { 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, upload-time = "2025-11-30T20:22:58.2Z" }, 846 + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, 847 + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, 848 + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, 849 + { 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, upload-time = "2025-11-30T20:23:04.878Z" }, 850 + { 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, upload-time = "2025-11-30T20:23:06.342Z" }, 851 + { 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, upload-time = "2025-11-30T20:23:07.825Z" }, 852 + { 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, upload-time = "2025-11-30T20:23:09.228Z" }, 853 + { 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, upload-time = "2025-11-30T20:23:11.186Z" }, 854 + { 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, upload-time = "2025-11-30T20:23:12.864Z" }, 855 + { 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, upload-time = "2025-11-30T20:23:14.638Z" }, 856 + { 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, upload-time = "2025-11-30T20:23:16.105Z" }, 857 + { 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, upload-time = "2025-11-30T20:23:17.539Z" }, 858 + { 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, upload-time = "2025-11-30T20:23:19.029Z" }, 859 + { 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, upload-time = "2025-11-30T20:23:20.885Z" }, 860 + { 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, upload-time = "2025-11-30T20:23:22.488Z" }, 861 + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, 862 + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, 863 + { 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, upload-time = "2025-11-30T20:23:27.316Z" }, 864 + { 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, upload-time = "2025-11-30T20:23:29.151Z" }, 865 + { 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, upload-time = "2025-11-30T20:23:30.785Z" }, 866 + { 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, upload-time = "2025-11-30T20:23:32.209Z" }, 867 + { 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, upload-time = "2025-11-30T20:23:33.742Z" }, 868 + { 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, upload-time = "2025-11-30T20:23:35.253Z" }, 869 + { 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, upload-time = "2025-11-30T20:23:36.842Z" }, 870 + { 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, upload-time = "2025-11-30T20:23:38.343Z" }, 871 + { 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, upload-time = "2025-11-30T20:23:40.263Z" }, 872 + { 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, upload-time = "2025-11-30T20:23:42.186Z" }, 873 + { 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, upload-time = "2025-11-30T20:23:44.086Z" }, 874 + { 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, upload-time = "2025-11-30T20:23:46.004Z" }, 875 + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, 876 + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, 877 + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, 878 + { 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, upload-time = "2025-11-30T20:23:52.494Z" }, 879 + { 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, upload-time = "2025-11-30T20:23:54.036Z" }, 880 + { 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, upload-time = "2025-11-30T20:23:55.556Z" }, 881 + { 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, upload-time = "2025-11-30T20:23:57.033Z" }, 882 + { 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, upload-time = "2025-11-30T20:23:58.637Z" }, 883 + { 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, upload-time = "2025-11-30T20:24:00.2Z" }, 884 + { 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, upload-time = "2025-11-30T20:24:01.759Z" }, 885 + { 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, upload-time = "2025-11-30T20:24:03.687Z" }, 886 + { 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, upload-time = "2025-11-30T20:24:05.232Z" }, 887 + { 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, upload-time = "2025-11-30T20:24:06.878Z" }, 888 + { 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, upload-time = "2025-11-30T20:24:08.445Z" }, 889 + { 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, upload-time = "2025-11-30T20:24:10.956Z" }, 890 + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, 891 + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, 892 + ] 893 + 894 + [[package]] 642 895 name = "ruff" 643 896 version = "0.14.10" 644 897 source = { registry = "https://pypi.org/simple" } ··· 665 918 ] 666 919 667 920 [[package]] 668 - name = "sniffio" 669 - version = "1.3.1" 921 + name = "sse-starlette" 922 + version = "3.3.3" 670 923 source = { registry = "https://pypi.org/simple" } 671 - sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } 924 + dependencies = [ 925 + { name = "anyio" }, 926 + { name = "starlette" }, 927 + ] 928 + sdist = { url = "https://files.pythonhosted.org/packages/14/2f/9223c24f568bb7a0c03d751e609844dce0968f13b39a3f73fbb3a96cd27a/sse_starlette-3.3.3.tar.gz", hash = "sha256:72a95d7575fd5129bd0ae15275ac6432bb35ac542fdebb82889c24bb9f3f4049", size = 32420, upload-time = "2026-03-17T20:05:55.529Z" } 672 929 wheels = [ 673 - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, 930 + { url = "https://files.pythonhosted.org/packages/78/e2/b8cff57a67dddf9a464d7e943218e031617fb3ddc133aeeb0602ff5f6c85/sse_starlette-3.3.3-py3-none-any.whl", hash = "sha256:c5abb5082a1cc1c6294d89c5290c46b5f67808cfdb612b7ec27e8ba061c22e8d", size = 14329, upload-time = "2026-03-17T20:05:54.35Z" }, 931 + ] 932 + 933 + [[package]] 934 + name = "starlette" 935 + version = "0.52.1" 936 + source = { registry = "https://pypi.org/simple" } 937 + dependencies = [ 938 + { name = "anyio" }, 939 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 940 + ] 941 + sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } 942 + wheels = [ 943 + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, 674 944 ] 675 945 676 946 [[package]] ··· 678 948 version = "0.1.0" 679 949 source = { editable = "." } 680 950 dependencies = [ 681 - { name = "anthropic" }, 682 951 { name = "argcomplete" }, 683 952 { name = "httpx" }, 953 + { name = "mcp" }, 684 954 { name = "pymupdf" }, 685 955 { name = "pymupdf4llm" }, 686 956 { name = "pyyaml" }, ··· 697 967 698 968 [package.metadata] 699 969 requires-dist = [ 700 - { name = "anthropic", specifier = ">=0.40" }, 701 970 { name = "argcomplete", specifier = ">=3.0" }, 702 971 { name = "httpx", specifier = ">=0.27" }, 972 + { name = "mcp", specifier = ">=1.9" }, 703 973 { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, 704 974 { name = "pymupdf", specifier = ">=1.24" }, 705 975 { name = "pymupdf4llm", specifier = ">=0.0.17" }, ··· 740 1010 wheels = [ 741 1011 { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, 742 1012 ] 1013 + 1014 + [[package]] 1015 + name = "uvicorn" 1016 + version = "0.42.0" 1017 + source = { registry = "https://pypi.org/simple" } 1018 + dependencies = [ 1019 + { name = "click" }, 1020 + { name = "h11" }, 1021 + ] 1022 + sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } 1023 + wheels = [ 1024 + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, 1025 + ]