From 1419eadd29e9282fddd6c760159e29a1f41ac27e Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:16:23 +0000 Subject: [PATCH 01/53] refactor(integrations): remove TS-only openclaw plugin (replaced by openclaw-smart) --- reflexio/integrations/openclaw/TESTING.md | 517 ---- .../integrations/openclaw/hook/handler.js | 473 ---- .../integrations/openclaw/package-lock.json | 2156 ----------------- reflexio/integrations/openclaw/package.json | 18 - .../openclaw/plugin/hook/handler.ts | 241 -- .../openclaw/plugin/hook/setup.ts | 140 -- .../integrations/openclaw/plugin/index.ts | 130 - .../openclaw/plugin/lib/publish.ts | 113 - .../openclaw/plugin/lib/search.ts | 52 - .../openclaw/plugin/lib/server.ts | 103 - .../openclaw/plugin/lib/sqlite-buffer.ts | 156 -- .../openclaw/plugin/lib/user-id.ts | 134 - .../openclaw/plugin/openclaw.plugin.json | 41 - .../integrations/openclaw/plugin/package.json | 17 - .../openclaw/plugin/rules/reflexio.md | 24 - .../openclaw/plugin/skills/reflexio/SKILL.md | 48 - .../integrations/openclaw/publish_clawhub.sh | 278 --- .../integrations/openclaw/references/HOOK.md | 164 -- .../integrations/openclaw/scripts/install.sh | 36 - .../openclaw/scripts/uninstall.sh | 35 - .../openclaw/tests/publish.test.ts | 27 - .../openclaw/tests/search.test.ts | 31 - .../openclaw/tests/server.test.ts | 42 - .../integrations/openclaw/tests/setup.test.ts | 49 - .../openclaw/tests/sqlite-buffer.test.ts | 91 - .../openclaw/tests/user-id.test.ts | 50 - reflexio/integrations/openclaw/tsconfig.json | 16 - .../integrations/openclaw/types/openclaw.d.ts | 230 -- .../integrations/openclaw/vitest.config.ts | 13 - 29 files changed, 5425 deletions(-) delete mode 100644 reflexio/integrations/openclaw/TESTING.md delete mode 100644 reflexio/integrations/openclaw/hook/handler.js delete mode 100644 reflexio/integrations/openclaw/package-lock.json delete mode 100644 reflexio/integrations/openclaw/package.json delete mode 100644 reflexio/integrations/openclaw/plugin/hook/handler.ts delete mode 100644 reflexio/integrations/openclaw/plugin/hook/setup.ts delete mode 100644 reflexio/integrations/openclaw/plugin/index.ts delete mode 100644 reflexio/integrations/openclaw/plugin/lib/publish.ts delete mode 100644 reflexio/integrations/openclaw/plugin/lib/search.ts delete mode 100644 reflexio/integrations/openclaw/plugin/lib/server.ts delete mode 100644 reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts delete mode 100644 reflexio/integrations/openclaw/plugin/lib/user-id.ts delete mode 100644 reflexio/integrations/openclaw/plugin/openclaw.plugin.json delete mode 100644 reflexio/integrations/openclaw/plugin/package.json delete mode 100644 reflexio/integrations/openclaw/plugin/rules/reflexio.md delete mode 100644 reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md delete mode 100755 reflexio/integrations/openclaw/publish_clawhub.sh delete mode 100644 reflexio/integrations/openclaw/references/HOOK.md delete mode 100755 reflexio/integrations/openclaw/scripts/install.sh delete mode 100755 reflexio/integrations/openclaw/scripts/uninstall.sh delete mode 100644 reflexio/integrations/openclaw/tests/publish.test.ts delete mode 100644 reflexio/integrations/openclaw/tests/search.test.ts delete mode 100644 reflexio/integrations/openclaw/tests/server.test.ts delete mode 100644 reflexio/integrations/openclaw/tests/setup.test.ts delete mode 100644 reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts delete mode 100644 reflexio/integrations/openclaw/tests/user-id.test.ts delete mode 100644 reflexio/integrations/openclaw/tsconfig.json delete mode 100644 reflexio/integrations/openclaw/types/openclaw.d.ts delete mode 100644 reflexio/integrations/openclaw/vitest.config.ts diff --git a/reflexio/integrations/openclaw/TESTING.md b/reflexio/integrations/openclaw/TESTING.md deleted file mode 100644 index 8919a082..00000000 --- a/reflexio/integrations/openclaw/TESTING.md +++ /dev/null @@ -1,517 +0,0 @@ -# Manual Testing Guide — Reflexio × OpenClaw Federated Plugin - -Step-by-step guide for manually testing the plugin end-to-end. Each phase builds on the previous one. This guide is self-contained — you should not need to reference other documentation. - -## Prerequisites - -- OpenClaw installed and running: `openclaw --version` -- Reflexio OpenClaw plugin installed: `./scripts/install.sh` or `clawhub plugin install reflexio-federated` -- An LLM API key for the Reflexio server (e.g., `export OPENAI_API_KEY=sk-...`) -- A terminal where you can see OpenClaw's stderr output (agent logs) - -### How to view agent logs - -OpenClaw outputs hook logs to stderr. To see Reflexio hook messages during a session: - -```bash -# Option A: Run openclaw in a terminal and watch stderr -openclaw chat 2>reflexio-hook.log -# In another terminal: tail -f reflexio-hook.log - -# Option B: Run with stderr visible -openclaw chat 2>&1 | tee session.log -``` - -Look for lines starting with `[reflexio]` — these are from the Reflexio hooks. - ---- - -## Phase 1: Install & Verify - -### 1.1 Install the plugin (if not already installed) - -```bash -# Option A: From source -cd /path/to/reflexio/integrations/openclaw -./scripts/install.sh - -# Option B: Via ClawHub -clawhub plugin install reflexio-federated -``` - -### 1.2 Verify plugin is loaded - -```bash -openclaw plugins list -# Expected: reflexio-federated (loaded) - -openclaw plugins inspect reflexio-federated -# Expected: Status: loaded -``` - -If the plugin shows as "disabled" or failed, run: - -```bash -openclaw plugins enable reflexio-federated -openclaw gateway restart -``` - -### 1.3 Verify Reflexio server (optional) - -The plugin automatically starts the local Reflexio server on first agent session (see Phase 2). You can optionally verify it manually: - -```bash -reflexio status check -``` - -If the server isn't running yet, that's fine — the plugin will start it automatically during first-use setup. - ---- - -## Phase 2: First-Session Auto-Setup & Server Auto-Start - -This phase verifies the plugin's first-use setup (CLI detection, LLM provider prompt, server startup) and that the search hook works correctly with no playbooks yet. - -### 2.0 Ensure the server is NOT running (optional) - -If you want to test auto-start, stop the server if it's running: - -```bash -reflexio services stop 2>/dev/null -# Verify it's stopped: -reflexio status check -# Expected: connection error or "Server is not running" -``` - -If the server is already running, that's fine — Phase 2.1 will still verify the plugin works with an existing server. - -### 2.1 Start a conversation — the plugin should perform first-use setup - -```bash -openclaw chat -``` - -Send a simple, self-contained task that doesn't require a project: - -```text -Write a Python function that takes a list of numbers and returns the mean and median. -``` - -**What to check:** -- The agent responds normally with working code (no errors, no mention of Reflexio) -- On first session, you may see a one-time LLM provider setup prompt (if reflexio-ai CLI needs initialization) -- In the plugin logs (stderr), you should see: - - `[reflexio] agent:bootstrap hook fired` — the bootstrap handler ran - - `[reflexio] Server running` or `[reflexio] Starting server` — server health check - - Potentially `[reflexio] Per-message search` — search attempted for first message -- The response should NOT be delayed more than ~5 seconds by the plugin (timeout limit) -- If setup runs, look for confirmation messages about LLM provider and storage configuration - -### 2.2 Verify the server is running - -After the first message, check: - -```bash -reflexio status check -# Expected: Server is running -``` - -The plugin starts the server in the background during `agent:bootstrap` if needed. Subsequent messages in this session (and all future sessions) will find the server ready. - -### 2.3 Send a second message to verify search is working - -In the same session: - -```text -Now write a version that also returns the standard deviation. -``` - -**What to check:** -- In plugin logs: search should be attempted (look for `[reflexio]` lines with search-related messages) -- Search results may be empty (no playbooks yet on cold start) — that's expected -- No search timeout errors in logs (timeout is 5 seconds by default) - -### 2.4 Verify the agent doesn't mention Reflexio - -The agent's behavioral rule says "never mention Reflexio to the user." Confirm the agent response contains no references to Reflexio, playbooks, or search results. - ---- - -## Phase 3: Capture & Publish - -This phase creates a correction scenario and verifies the system captures it. - -### 3.1 Create a correction scenario - -In the same session (or a new one via `openclaw chat`), send these messages in order. The goal is to get the agent to do something one way, then correct it: - -**Message 1** — give a task with an implicit choice: -```text -Write a shell script that installs project dependencies and starts the dev server. -``` - -Wait for the agent to respond. It will likely use `npm install` or a similar default. - -**Message 2** — correct the agent's choice: -```text -No, don't use npm. In this project we always use pnpm. Please rewrite using pnpm instead. -``` - -Wait for the agent to apply the correction. - -**Message 3** — continue the task to provide more context: -```text -Also add a health check that curls localhost:3000/health before starting the main process. -``` - -**What to check:** -- The agent applies the correction (uses pnpm in the rewrite) -- In plugin logs: look for publish-related messages — the plugin should detect the correction -- If you don't see a publish during the session, that's OK — the session-end hook will capture the full conversation and publish it then - -### 3.2 End the session - -Exit the session: -```text -/stop -``` -(or press Ctrl+C, depending on your OpenClaw configuration) - -**What to check in plugin logs:** -- `[reflexio] command:stop hook fired` — the plugin detected session end -- `[reflexio] Queued N interactions for publish` — buffered turns are being published -- `[reflexio] Published via reflexio_publish CLI` — the publish command executed successfully - -### 3.3 Verify playbooks were extracted - -Wait ~30 seconds for server-side LLM extraction, then check: - -```bash -reflexio user-playbooks list --limit 10 -``` - -**Expected:** At least one playbook containing "pnpm" (e.g., content like "use pnpm instead of npm"). - -If no playbooks appear, the batch interval may not have been met (requires 5+ interactions). Use the manual extraction command instead: - -```bash -# If no playbooks were extracted automatically, this is expected for short sessions. -# Phase 5 covers manual extraction as a workaround. -``` - -Also check profiles: -```bash -reflexio user-profiles list --limit 10 -``` - -You may see a profile entry about project conventions (e.g., "uses pnpm"). - ---- - -## Phase 4: Retrieval (Warm Start) - -This phase verifies the agent applies corrections from previous sessions. - -### 4.1 Start a new session and trigger a related task - -```bash -openclaw chat -``` - -Send a task related to the correction from Phase 3: - -```text -Add the 'lodash' package to this project's dependencies. -``` - -**What to check:** -- In plugin logs: `[reflexio]` lines showing search was executed before the response -- The agent should use `pnpm add lodash` (not `npm install lodash`) — applying the correction from Phase 3 **without being told again** -- If the agent still uses npm, the playbook may not have been extracted yet. Check `reflexio user-playbooks list` and retry after extraction completes. - -### 4.2 Send an unrelated task - -In the same session: - -```text -Explain how Python's garbage collector works. -``` - -**What to check:** -- The search returns different (or no) playbooks — the pnpm correction should NOT appear for a Python question -- The agent responds normally - ---- - -## Phase 5: Manual Commands - -### 5.1 Test `/reflexio-extract` - -Start a new session and have a conversation with at least one correction or learning: - -```bash -openclaw chat -``` - -Send a few messages: -```text -Write a function to validate email addresses using regex. -``` -Then after the response: -```text -That regex is too permissive. Use a stricter pattern that requires a TLD of at least 2 characters. -``` - -Now use the `reflexio_publish` tool to flush immediately: - -Ask the agent to use the `reflexio_publish` tool to publish the conversation: - -```text -Please publish our conversation using the reflexio_publish tool so I can test the publish mechanism. -``` - -**What to check:** -- The agent calls the `reflexio_publish` tool -- Tool output confirms the conversation was published (e.g., "Published 2 interactions to Reflexio") -- Verify extraction worked: - ```bash - reflexio user-playbooks list --limit 10 - ``` - You should see a new playbook about email validation regex. - -### 5.2 Test manual aggregation - -After accumulating playbooks from Phases 3-5.1, manually trigger aggregation: - -```bash -reflexio agent-playbooks aggregate --agent-version openclaw-agent --wait -``` - -**What to check:** -- Command completes without errors -- Reports how many agent playbooks were created or updated -- Verify: - ```bash - reflexio agent-playbooks list --agent-version openclaw-agent - ``` - You should see agent playbooks with `PENDING` status. - ---- - -## Phase 6: Multi-User (Multiple Agent Instances) - -This phase tests that different OpenClaw agents get isolated user playbooks but share agent playbooks. - -### 6.1 Set up a second agent instance - -If you don't already have multiple agents, create one. OpenClaw stores agent definitions in `~/.openclaw/openclaw.json`. Add a second agent: - -```bash -openclaw agents add --name test-reviewer -``` - -This creates a new agent with its own workspace at `~/.openclaw/workspace-test-reviewer/`. - -Verify both agents exist: -```bash -openclaw agents list -# Should show at least two agents (e.g., "main" and "test-reviewer") -``` - -> **Note:** Use whatever agent names you already have. The test just needs two distinct agents. Replace `main` and `test-reviewer` in the commands below with your actual agent names. - -### 6.2 Create different corrections on each agent - -**Agent 1** (your default agent): -```bash -openclaw chat -``` - -Have this conversation: -```text -Write a function to format a date as a string. -``` -Then: -```text -Always use ISO 8601 format (YYYY-MM-DD) for dates, never locale-specific formats. -``` -Exit: `/stop` - -**Agent 2** (your second agent): -```bash -openclaw chat --agent test-reviewer -``` - -Have this conversation: -```text -Write a function to log errors. -``` -Then: -```text -Always include the stack trace when logging errors, not just the message. -``` -Exit: `/stop` - -### 6.3 Verify user playbook isolation - -Wait ~30 seconds for extraction, then check. Replace `main` and `test-reviewer` with your actual agent names: - -```bash -# Check what each agent sees (use your actual agent names) -reflexio user-playbooks list --user-id main -reflexio user-playbooks list --user-id test-reviewer -``` - -**Expected:** -- Agent 1's playbooks include the date formatting correction, NOT the error logging one -- Agent 2's playbooks include the error logging correction, NOT the date formatting one - -> **Note:** If playbooks weren't extracted (batch interval not met), manually extract from each agent's session using `/reflexio-extract`, or seed playbooks directly: -> ```bash -> reflexio user-playbooks add --user-id main --content "Always use ISO 8601 (YYYY-MM-DD) for date formatting" --trigger "formatting dates" -> reflexio user-playbooks add --user-id test-reviewer --content "Always include stack traces when logging errors" --trigger "logging errors" -> ``` - -### 6.4 Aggregate and verify shared playbooks - -```bash -reflexio agent-playbooks aggregate --agent-version openclaw-agent --wait -reflexio agent-playbooks list --agent-version openclaw-agent -``` - -**Expected:** -- Agent playbooks contain corrections from **both** agents (date formatting + error logging) -- Search from either agent returns the shared playbooks: - ```bash - reflexio search "format a date" --user-id main - reflexio search "format a date" --user-id test-reviewer - ``` - Both should return the date formatting agent playbook. - -### 6.5 Clean up test agent (optional) - -If you created a test agent just for this phase: -```bash -openclaw agents remove --name test-reviewer -``` - ---- - -## Phase 7: Graceful Degradation - -### 7.1 Stop the server and verify agent still works - -```bash -reflexio services stop -``` - -Start a new OpenClaw session: -```bash -openclaw chat -``` - -Send a task: -```text -Explain the difference between TCP and UDP. -``` - -**What to check:** -- The agent responds normally (no crashes, no errors visible to the user) -- In hook logs: - - `[reflexio] Server not running — starting in background` — auto-start triggered at bootstrap - - `[reflexio] Per-message search failed` — first search may fail while server starts (expected) -- The hook buffers the turn to local SQLite (`~/.reflexio/sessions.db`) — it will be published when the server is ready - -### 7.2 Verify the server auto-recovered - -Wait ~10 seconds after the first message, then check: - -```bash -reflexio status check -# Expected: Server is running (auto-started by the hook) -``` - -Send a second message in the same session: -```text -Now explain when you'd use one over the other. -``` - -**What to check:** -- Search should succeed now (server is running) -- No "Search failed" errors in hook logs for this message - -### 7.3 Verify buffered turns are retried on next session - -Exit the current session: `/stop` - -Start a new session (this triggers the `agent:bootstrap` event): -```bash -openclaw chat -``` - -**What to check in hook logs:** -- `[reflexio] Retrying N unpublished session(s)` — the bootstrap handler retries buffered turns from the previous session where the server wasn't ready - -Exit the session: `/stop` - ---- - -## Phase 8: Uninstall - -### 8.1 Uninstall the plugin - -```bash -cd /path/to/reflexio/integrations/openclaw -./scripts/uninstall.sh -``` - -Or if using ClawHub: - -```bash -clawhub plugin uninstall reflexio-federated -``` - -### 8.2 Verify plugin is removed - -```bash -openclaw plugins list -# Should NOT show reflexio-federated - -openclaw plugins inspect reflexio-federated 2>&1 | grep -i "not found" -# Expected: plugin not found or similar error -``` - -### 8.3 Verify agent works without plugin - -```bash -openclaw chat -``` - -Send any message and confirm the agent works normally with no Reflexio-related errors or log lines. - -### 8.4 Optional: Delete user data - -To remove stored conversations and playbooks: - -```bash -cd /path/to/reflexio/integrations/openclaw -./scripts/uninstall.sh --purge -``` - -This deletes `~/.reflexio/` entirely. If you only ran `uninstall.sh` without `--purge`, data is preserved in case you want to re-enable the plugin later. - ---- - -## Troubleshooting - -| Symptom | Cause | Fix | -|---------|-------|-----| -| Plugin doesn't load | Plugin not installed or gateway not restarted | Run `./scripts/install.sh` or `clawhub plugin install reflexio-federated`; verify with `openclaw plugins list` | -| Agent doesn't follow past corrections | Search plugin timeout or no playbooks yet | Check `reflexio user-playbooks list`; verify server is running with `reflexio status check` | -| `[reflexio] Search failed` in every message | Server not running | `reflexio services start --only backend &` or restart the agent | -| Playbooks not extracted after session | Batch interval not met (need 5+ interactions) | Use `reflexio_publish` tool to manually flush, or seed playbooks with `reflexio user-playbooks add` | -| Agent mentions Reflexio to user | Agent behavioral rule not applied | Check if plugin skills loaded correctly; restart agent | -| First-use setup never ran | LLM provider already configured | Run `reflexio setup openclaw` manually if you need to reconfigure | -| Aggregation never runs | Flag file stuck | `rm ~/.reflexio/logs/.aggregation-running` | -| Can't start second agent | Agent not configured in OpenClaw | `openclaw agents add --name ` then `openclaw agents list` to verify | -| Search returns corrections from wrong agent | User playbooks aren't scoped by agent | Verify user_id resolution with `reflexio search "" --user-id ` | diff --git a/reflexio/integrations/openclaw/hook/handler.js b/reflexio/integrations/openclaw/hook/handler.js deleted file mode 100644 index 7acaf5eb..00000000 --- a/reflexio/integrations/openclaw/hook/handler.js +++ /dev/null @@ -1,473 +0,0 @@ -// --------------------------------------------------------------------------- -// Security contract — localhost only, HTTP only. -// -// This hook is a localhost-only integration. It buffers OpenClaw conversations -// to a local SQLite database and talks to the Reflexio backend HTTP server on -// the same machine via native fetch(). It does not spawn subprocesses, does -// not read configuration from the filesystem, and does not consult any -// environment variables. The destination is a hardcoded loopback URL. -// -// Traffic: only HTTP requests to http://127.0.0.1:8081/api/* and -// http://127.0.0.1:8081/health. No other hosts are contacted. -// -// Bootstrap: the Reflexio server must be running on port 8081 before the -// hook is useful. If it is not, every fetch() attempt fails gracefully with -// a logged error and the hook returns — the agent continues without -// cross-session memory that session. Starting the server is the user's -// responsibility; the skill's First-Use Setup runs `reflexio services start` -// once at install time. -// -// Writes are confined to ~/.reflexio/sessions.db (SQLite buffer). -// --------------------------------------------------------------------------- - -const { randomUUID } = require("node:crypto"); -const { mkdirSync } = require("node:fs"); -const { homedir } = require("node:os"); -const { dirname, join } = require("node:path"); - -const Database = require("better-sqlite3"); - -// Hardcoded loopback destination — all traffic goes here, nowhere else. -const LOCAL_SERVER_URL = "http://127.0.0.1:8081"; -// Hardcoded agent label; stored alongside extracted playbooks so they are -// scoped to this integration build. -const AGENT_VERSION = "openclaw-agent"; - -// --------------------------------------------------------------------------- -// HTTP helper — single fetch() path for every API call -// --------------------------------------------------------------------------- - -async function apiPost(path, body, timeoutMs = 10_000) { - const ctrl = new AbortController(); - const timer = setTimeout(() => ctrl.abort(), timeoutMs); - try { - const res = await fetch(`${LOCAL_SERVER_URL}${path}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - signal: ctrl.signal, - }); - if (!res.ok) { - throw new Error(`HTTP ${res.status} ${res.statusText}`); - } - return await res.json(); - } finally { - clearTimeout(timer); - } -} - -// Format unified search results (profiles + playbooks) as a markdown block -// the agent can read directly. Kept in sync with the CLI's text output -// shape so the injected bootstrap file looks familiar. -function formatSearchResults(data) { - const lines = []; - const profiles = data?.profiles ?? []; - const userPlaybooks = data?.user_playbooks ?? []; - const agentPlaybooks = data?.agent_playbooks ?? []; - - if (profiles.length > 0) { - lines.push("## User Profiles"); - for (const p of profiles) { - const content = p.profile_content ?? p.content ?? ""; - if (content.trim()) lines.push(`- ${content.trim()}`); - } - lines.push(""); - } - if (userPlaybooks.length > 0) { - lines.push("## User Playbooks (from this agent's history)"); - for (const pb of userPlaybooks) { - const summary = - pb.content ?? pb.instruction ?? pb.trigger ?? JSON.stringify(pb); - lines.push(`- ${summary}`); - } - lines.push(""); - } - if (agentPlaybooks.length > 0) { - lines.push("## Agent Playbooks (shared across instances)"); - for (const pb of agentPlaybooks) { - const summary = - pb.content ?? pb.instruction ?? pb.trigger ?? JSON.stringify(pb); - lines.push(`- ${summary}`); - } - lines.push(""); - } - return lines.join("\n").trim(); -} - -// --------------------------------------------------------------------------- -// SQLite session store — persistent, crash-safe conversation buffer. -// DB lives in ~/.reflexio/sessions.db. -// --------------------------------------------------------------------------- - -const DB_PATH = join(homedir(), ".reflexio", "sessions.db"); -const MAX_CONTENT_LENGTH = 10_000; -const MAX_INTERACTIONS = 200; -const BATCH_SIZE = 10; // Publish every N complete exchanges mid-session -const MAX_RETRIES = 3; // Give up retrying after this many failures - -let _db = null; - -function getDb() { - if (_db) return _db; - mkdirSync(dirname(DB_PATH), { recursive: true, mode: 0o700 }); - _db = new Database(DB_PATH); - _db.pragma("journal_mode = WAL"); - // Use prepare().run() for DDL; better-sqlite3 accepts DDL statements - // through prepared statements the same as DML. - _db.prepare( - "CREATE TABLE IF NOT EXISTS turns (" + - "id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "session_id TEXT NOT NULL, " + - "role TEXT NOT NULL, " + - "content TEXT NOT NULL, " + - "timestamp TEXT NOT NULL, " + - "published INTEGER DEFAULT 0, " + - "retry_count INTEGER DEFAULT 0" + - ")", - ).run(); - _db.prepare( - "CREATE INDEX IF NOT EXISTS idx_session_published ON turns(session_id, published)", - ).run(); - // Add retry_count column if missing (migration for existing DBs) - try { - _db.prepare("ALTER TABLE turns ADD COLUMN retry_count INTEGER DEFAULT 0").run(); - } catch { - // Column already exists -- ignore - } - // Clean up old published turns (keep 7 days) - _db.prepare( - "DELETE FROM turns WHERE published = 1 AND timestamp < datetime('now', '-7 days')", - ).run(); - // Graceful close on process exit is intentionally omitted: - // better-sqlite3 flushes WAL on its own and we avoid referencing the - // runtime's process object here. - return _db; -} - -// --------------------------------------------------------------------------- -// Smart truncation — preserves head + tail with a marker in between -// --------------------------------------------------------------------------- - -function smartTruncate(content, maxLength = MAX_CONTENT_LENGTH) { - if (!content || content.length <= maxLength) return content || ""; - const headLen = Math.floor(maxLength * 0.8); - const tailLen = Math.max(0, maxLength - headLen - 80); - const truncated = content.length - headLen - tailLen; - const marker = `\n\n[...truncated ${truncated} chars...]\n\n`; - if (tailLen === 0) return content.slice(0, headLen) + marker; - return content.slice(0, headLen) + marker + content.slice(-tailLen); -} - -// --------------------------------------------------------------------------- -// Session ID resolution -// --------------------------------------------------------------------------- - -let _fallbackSessionId = null; - -function getSessionId(event) { - const key = event.context?.sessionKey; - if (key) return key; - if (!_fallbackSessionId) { - _fallbackSessionId = `anon-${randomUUID()}`; - console.error( - `[reflexio] No sessionKey; using fallback: ${_fallbackSessionId}`, - ); - } - return _fallbackSessionId; -} - -// --------------------------------------------------------------------------- -// User ID resolution — multi-agent instance support -// -// Derived entirely from the OpenClaw session key, which encodes the -// per-agent identifier as a prefix of the form "agent::". -// When the session key doesn't match that shape, everything falls back to -// the single label "openclaw". -// --------------------------------------------------------------------------- - -function resolveUserId(event) { - const sessionKey = event.context?.sessionKey ?? ""; - const sessionMatch = sessionKey.match(/^agent:([^:]+):/); - if (sessionMatch) return sessionMatch[1]; - return "openclaw"; -} - -// --------------------------------------------------------------------------- -// Shared publish logic — POSTs buffered turns to /api/publish_interaction -// --------------------------------------------------------------------------- - -async function publishSession(db, sessionId, userId, agentVersion) { - const turns = db - .prepare( - "SELECT id, role, content FROM turns WHERE session_id = ? AND published = 0 AND retry_count < ? ORDER BY id LIMIT ?", - ) - .all(sessionId, MAX_RETRIES, MAX_INTERACTIONS); - - if (turns.length === 0) return; - - // Mark selected turns as in-flight (published = 2) synchronously to prevent - // concurrent publishSession calls from picking up the same rows. - const maxId = turns[turns.length - 1].id; - db.prepare( - "UPDATE turns SET published = 2 WHERE session_id = ? AND published = 0 AND retry_count < ? AND id <= ?", - ).run(sessionId, MAX_RETRIES, maxId); - - const body = { - user_id: userId, - source: "openclaw", - agent_version: agentVersion, - session_id: sessionId, - interaction_data_list: turns.map((t) => ({ - role: t.role, - content: t.content, - })), - skip_aggregation: false, - force_extraction: false, - }; - - try { - await apiPost("/api/publish_interaction", body, 15_000); - db.prepare( - "UPDATE turns SET published = 1 WHERE session_id = ? AND published = 2", - ).run(sessionId); - console.error( - `[reflexio] Published ${turns.length} interactions (session ${sessionId})`, - ); - } catch (err) { - console.error( - `[reflexio] Publish failed: ${err?.message ?? err}, incrementing retry count`, - ); - try { - db.prepare( - "UPDATE turns SET published = 0, retry_count = retry_count + 1 WHERE session_id = ? AND published = 2", - ).run(sessionId); - } catch (e) { - console.error( - `[reflexio] Failed to update retry count: ${e.message}`, - ); - } - } -} - -/** - * Main hook dispatcher for Reflexio-OpenClaw integration. - * - * Events handled: - * agent:bootstrap - Inject user profile + retry unpublished sessions - * message:received - Search Reflexio before agent responds - * message:sent - Buffer turn to SQLite + incremental publish - * command:stop - Flush remaining unpublished turns to Reflexio - */ -async function reflexioHook(event) { - // Skip sub-agent sessions to avoid recursion (guards all event types) - const sessionKey = event.context?.sessionKey ?? ""; - if (sessionKey.includes(":subagent:")) return; - - const eventKey = `${event.type}:${event.action}`; - - switch (eventKey) { - case "agent:bootstrap": - return handleBootstrap(event); - case "message:received": - return handleSearchBeforeResponse(event); - case "message:sent": - return handleMessageSent(event); - case "command:stop": - return handleSessionEnd(event); - } -} - -// --------------------------------------------------------------------------- -// Bootstrap: inject user profile + retry unpublished sessions -// -// Precondition: the Reflexio backend is already running on LOCAL_SERVER_URL. -// The hook does not start it — that's the user's responsibility, handled -// once at install time by the skill's First-Use Setup. If the server is -// unreachable, every API call fails quickly and the handler returns. -// --------------------------------------------------------------------------- - -async function handleBootstrap(event) { - const workspaceDir = event.context?.workspaceDir; - if (!workspaceDir) return; - - console.error(`[reflexio] bootstrap hook fired, workspace=${workspaceDir}`); - - const userId = resolveUserId(event); - const currentSessionId = getSessionId(event); - - // --- Inject user profile via unified search --- - try { - const data = await apiPost( - "/api/search", - { - query: "communication style, expertise, and preferences", - user_id: userId, - top_k: 3, - }, - 10_000, - ); - - const profiles = Array.isArray(data?.profiles) ? data.profiles : []; - if (profiles.length > 0) { - const profileLines = profiles - .map((p) => `- ${(p.profile_content ?? p.content ?? "").trim()}`) - .filter((line) => line.length > 2); - - if (profileLines.length > 0 && Array.isArray(event.context.bootstrapFiles)) { - const bootstrapContent = [ - "## About This User (from Reflexio)", - "", - ...profileLines, - "", - 'Use `reflexio search ""` before starting work to get task-specific behavioral corrections.', - ].join("\n"); - - event.context.bootstrapFiles.push({ - name: "REFLEXIO_USER_PROFILE.md", - path: "REFLEXIO_USER_PROFILE.md", - content: bootstrapContent, - source: "hook:reflexio-context", - }); - console.error( - `[reflexio] Injected user profile (${bootstrapContent.length} chars)`, - ); - } - } - } catch (err) { - console.error( - `[reflexio] Bootstrap profile fetch failed: ${err?.message ?? err}`, - ); - } - - // --- Retry unpublished turns from previous sessions --- - try { - const db = getDb(); - const oldSessions = db - .prepare( - "SELECT DISTINCT session_id FROM turns WHERE published = 0 AND retry_count < ? AND session_id != ? LIMIT 5", - ) - .all(MAX_RETRIES, currentSessionId); - - if (oldSessions.length > 0) { - console.error( - `[reflexio] Retrying ${oldSessions.length} unpublished session(s)`, - ); - for (const { session_id } of oldSessions) { - // Await sequentially so we don't hammer the server with - // concurrent publishes for stale sessions. - // eslint-disable-next-line no-await-in-loop - await publishSession(db, session_id, userId, AGENT_VERSION); - } - } - } catch (err) { - console.error(`[reflexio] Retry failed: ${err?.message ?? err}`); - } -} - -// --------------------------------------------------------------------------- -// Message received: search Reflexio before the agent responds -// --------------------------------------------------------------------------- - -const TRIVIAL_RESPONSE_RE = /^(yes|no|ok|sure|thanks|y|n)$/i; - -async function handleSearchBeforeResponse(event) { - let prompt = event.context?.userMessage; - if (!prompt || prompt.length < 5) return; - if (TRIVIAL_RESPONSE_RE.test(prompt.trim())) return; - prompt = prompt.slice(0, 4096); - - try { - const userId = resolveUserId(event); - const data = await apiPost( - "/api/search", - { - query: prompt, - user_id: userId, - top_k: 5, - agent_version: AGENT_VERSION, - }, - 5_000, - ); - - const formatted = formatSearchResults(data); - if (formatted && Array.isArray(event.context?.bootstrapFiles)) { - event.context.bootstrapFiles.push({ - name: "REFLEXIO_CONTEXT.md", - path: "REFLEXIO_CONTEXT.md", - content: formatted, - source: "hook:reflexio-context", - }); - console.error( - `[reflexio] Injected search context for message (${formatted.length} chars)`, - ); - } - } catch (err) { - console.error( - `[reflexio] Per-message search failed: ${err?.message ?? err}`, - ); - // Server may be down. The skill's First-Use Setup is responsible for - // starting it; the hook does not launch processes. - } -} - -// --------------------------------------------------------------------------- -// Message sent: buffer turn + incremental publish every BATCH_SIZE exchanges -// --------------------------------------------------------------------------- - -function handleMessageSent(event) { - const userMessage = event.context?.userMessage; - const agentResponse = event.context?.agentResponse; - const sessionId = getSessionId(event); - - if (!userMessage && !agentResponse) return; - - try { - const db = getDb(); - const now = new Date().toISOString(); - - const insertTurn = db.transaction((sid, user, agent, ts) => { - const stmt = db.prepare( - "INSERT INTO turns (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)", - ); - if (user) stmt.run(sid, "user", smartTruncate(user), ts); - if (agent) stmt.run(sid, "assistant", smartTruncate(agent), ts); - }); - insertTurn(sessionId, userMessage, agentResponse, now); - - // Incremental publish: every BATCH_SIZE complete exchanges - const { count } = db - .prepare( - "SELECT COUNT(*) as count FROM turns WHERE session_id = ? AND published = 0", - ) - .get(sessionId); - - if (count >= BATCH_SIZE * 2) { - const userId = resolveUserId(event); - void publishSession(db, sessionId, userId, AGENT_VERSION).catch((e) => - console.error(`[reflexio] Incremental publish failed: ${e?.message ?? e}`), - ); - } - } catch (err) { - console.error(`[reflexio] Failed to buffer turn: ${err.message}`); - } -} - -// --------------------------------------------------------------------------- -// Session end: flush remaining unpublished turns -// --------------------------------------------------------------------------- - -async function handleSessionEnd(event) { - const sessionId = getSessionId(event); - const userId = resolveUserId(event); - - try { - const db = getDb(); - await publishSession(db, sessionId, userId, AGENT_VERSION); - } catch (err) { - console.error(`[reflexio] Session flush failed: ${err?.message ?? err}`); - } -} - -// OpenClaw expects a CommonJS default export. -module.exports = reflexioHook; -module.exports.default = reflexioHook; diff --git a/reflexio/integrations/openclaw/package-lock.json b/reflexio/integrations/openclaw/package-lock.json deleted file mode 100644 index 102b563e..00000000 --- a/reflexio/integrations/openclaw/package-lock.json +++ /dev/null @@ -1,2156 +0,0 @@ -{ - "name": "openclaw-federated-dev", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "openclaw-federated-dev", - "version": "0.1.0", - "devDependencies": { - "@types/better-sqlite3": "^7.0.0", - "@types/node": "^20.0.0", - "better-sqlite3": "^11.0.0", - "typescript": "^5.0.0", - "vitest": "^3.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/reflexio/integrations/openclaw/package.json b/reflexio/integrations/openclaw/package.json deleted file mode 100644 index 88056256..00000000 --- a/reflexio/integrations/openclaw/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "openclaw-federated-dev", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "test": "vitest run", - "test:watch": "vitest", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "@types/better-sqlite3": "^7.0.0", - "@types/node": "^20.0.0", - "better-sqlite3": "^11.0.0", - "typescript": "^5.0.0", - "vitest": "^3.0.0" - } -} diff --git a/reflexio/integrations/openclaw/plugin/hook/handler.ts b/reflexio/integrations/openclaw/plugin/hook/handler.ts deleted file mode 100644 index d6a85a8e..00000000 --- a/reflexio/integrations/openclaw/plugin/hook/handler.ts +++ /dev/null @@ -1,241 +0,0 @@ -// Core hook logic: search injection, message buffering, session flush. -// -// Decoupled from the Plugin SDK so each handler can be tested independently. -// The SDK wiring in index.ts calls these functions with injected deps. -import * as os from "node:os"; -import * as path from "node:path"; -import type Database from "better-sqlite3"; -import { - openDb, - insertTurn, - countUnpublished, - cleanupOldTurns, - getOldUnpublishedSessions, -} from "../lib/sqlite-buffer.ts"; -import { publishSession } from "../lib/publish.ts"; -import { shouldSkipSearch, runSearch, isConnectionError } from "../lib/search.ts"; -import { ensureServerRunning, type CommandRunner } from "../lib/server.ts"; -import { resolveUserId, resolveAgentVersion } from "../lib/user-id.ts"; -import { runSetupCheck, type SetupGuidance } from "./setup.ts"; - -type Logger = { - info?: (msg: string) => void; - warn?: (msg: string) => void; - error?: (msg: string) => void; -}; - -export interface PluginConfig { - publish: { batch_size: number; max_retries: number; max_content_length: number }; - search: { timeout_ms: number; top_k: number; min_prompt_length: number }; - server: { health_check_timeout_ms: number; stale_flag_ms: number }; -} - -export const DEFAULT_CONFIG: PluginConfig = { - publish: { batch_size: 10, max_retries: 3, max_content_length: 10_000 }, - search: { timeout_ms: 5_000, top_k: 5, min_prompt_length: 5 }, - server: { health_check_timeout_ms: 3_000, stale_flag_ms: 120_000 }, -}; - -const DB_PATH = path.join(os.homedir(), ".reflexio", "sessions.db"); -const MAX_INTERACTIONS = 200; -const MAX_OLD_SESSIONS = 5; - -let _db: Database.Database | null = null; - -function getDb(): Database.Database { - if (!_db) { - _db = openDb(DB_PATH); - cleanupOldTurns(_db, 7); - process.on("exit", () => { - if (_db) _db.close(); - }); - } - return _db; -} - -/** - * Handle before_prompt_build: auto-setup, search injection, retry old sessions. - * Returns prependSystemContext string if search results found. - */ -export async function handleBeforePromptBuild( - sessionKey: string, - agentId: string, - prompt: string | undefined, - runner: CommandRunner, - config: PluginConfig = DEFAULT_CONFIG, - log?: Logger, -): Promise<{ prependSystemContext?: string; setupGuidance?: SetupGuidance }> { - // 1. Auto-setup check - let guidance: SetupGuidance | null = null; - try { - guidance = await runSetupCheck(agentId); - } catch (err) { - log?.error?.(`[reflexio] Setup check failed: ${err}`); - } - if (guidance) { - return { - prependSystemContext: `# Reflexio Setup Required\n\n${guidance.message}`, - setupGuidance: guidance, - }; - } - - // 2. Ensure server is running - try { - await ensureServerRunning(runner, { - staleFlagMs: config.server.stale_flag_ms, - log, - }); - } catch (err) { - log?.error?.(`[reflexio] Server check failed: ${err}`); - } - - // 3. Retry unpublished turns from old sessions - try { - const db = getDb(); - const agentVersion = resolveAgentVersion(); - const oldSessions = getOldUnpublishedSessions( - db, - sessionKey, - config.publish.max_retries, - MAX_OLD_SESSIONS, - ); - for (const sid of oldSessions) { - const userId = resolveUserId(sid); - publishSession( - db, - sid, - userId, - agentVersion, - config.publish.max_retries, - MAX_INTERACTIONS, - log, - ); - } - } catch (err) { - log?.error?.(`[reflexio] Old session retry failed: ${err}`); - } - - // 4. Search injection - if (!prompt || shouldSkipSearch(prompt, config.search.min_prompt_length)) { - return {}; - } - - try { - const userId = resolveUserId(sessionKey); - const context = await runSearch( - prompt, - userId, - config.search.top_k, - config.search.timeout_ms, - runner, - ); - if (context) { - return { prependSystemContext: context }; - } - } catch (err) { - const errMsg = String(err); - log?.error?.(`[reflexio] Search failed: ${errMsg}`); - if (isConnectionError(errMsg)) { - try { - await ensureServerRunning(runner, { staleFlagMs: config.server.stale_flag_ms, log }); - } catch {} - } - } - - return {}; -} - -/** - * Handle message_sent: buffer turn to SQLite, trigger incremental publish. - */ -export function handleMessageSent( - sessionKey: string, - userMessage: string | undefined, - agentResponse: string | undefined, - runner: CommandRunner, - config: PluginConfig = DEFAULT_CONFIG, - log?: Logger, -): void { - if (!userMessage && !agentResponse) return; - - try { - const db = getDb(); - const maxLen = config.publish.max_content_length; - if (userMessage) insertTurn(db, sessionKey, "user", userMessage, maxLen); - if (agentResponse) insertTurn(db, sessionKey, "assistant", agentResponse, maxLen); - - // Incremental publish at batch threshold - const count = countUnpublished(db, sessionKey); - if (count >= config.publish.batch_size * 2) { - const userId = resolveUserId(sessionKey); - const agentVersion = resolveAgentVersion(); - publishSession( - db, - sessionKey, - userId, - agentVersion, - config.publish.max_retries, - MAX_INTERACTIONS, - log, - ); - } - } catch (err) { - log?.error?.(`[reflexio] Failed to buffer turn: ${err}`); - } -} - -/** - * Handle session end / compaction / reset: flush all unpublished turns. - */ -export function handleSessionFlush( - sessionKey: string, - log?: Logger, - config: PluginConfig = DEFAULT_CONFIG, -): void { - try { - const db = getDb(); - const userId = resolveUserId(sessionKey); - const agentVersion = resolveAgentVersion(); - publishSession( - db, - sessionKey, - userId, - agentVersion, - config.publish.max_retries, - MAX_INTERACTIONS, - log, - ); - } catch (err) { - log?.error?.(`[reflexio] Session flush failed: ${err}`); - } -} - -/** - * Handle reflexio_publish tool: immediate flush of current session. - */ -export function handleToolPublish( - sessionKey: string, - log?: Logger, - config: PluginConfig = DEFAULT_CONFIG, -): string { - try { - const db = getDb(); - const count = countUnpublished(db, sessionKey); - if (count === 0) return "No unpublished turns to flush."; - - const userId = resolveUserId(sessionKey); - const agentVersion = resolveAgentVersion(); - publishSession( - db, - sessionKey, - userId, - agentVersion, - config.publish.max_retries, - MAX_INTERACTIONS, - log, - ); - return `Flushing ${count} turns to Reflexio server.`; - } catch (err) { - return `Publish failed: ${err}`; - } -} diff --git a/reflexio/integrations/openclaw/plugin/hook/setup.ts b/reflexio/integrations/openclaw/plugin/hook/setup.ts deleted file mode 100644 index 275f80c2..00000000 --- a/reflexio/integrations/openclaw/plugin/hook/setup.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Runtime auto-setup: check reflexio CLI, check config, check server. -// -// Runs on before_prompt_build. Gated by per-agent marker file. -// Each step that fails returns a guidance message for the agent to present -// to the user with options — the user picks, Openclaw executes. -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { execFile, execFileSync } from "node:child_process"; - -export interface SetupGuidance { - /** Message for the agent to present to the user. */ - message: string; - /** Whether the plugin can function without resolving this. */ - blocking: boolean; -} - -const REFLEXIO_DIR = path.join(os.homedir(), ".reflexio"); - -/** Sanitize agentId for safe use in filesystem paths. */ -function sanitizeAgentId(agentId: string): string { - return agentId.replace(/[^a-zA-Z0-9_-]/g, "_"); -} - -/** Check if setup has been completed for a given agent. */ -export function isSetupComplete(reflexioDir: string, agentId: string): boolean { - const safeId = sanitizeAgentId(agentId); - const marker = path.join(reflexioDir, `.setup_complete_${safeId}`); - return fs.existsSync(marker); -} - -/** Create the setup-complete marker for an agent. */ -export function markSetupComplete(reflexioDir: string, agentId: string): void { - const safeId = sanitizeAgentId(agentId); - fs.mkdirSync(reflexioDir, { recursive: true }); - fs.writeFileSync( - path.join(reflexioDir, `.setup_complete_${safeId}`), - new Date().toISOString(), - ); -} - -/** Check if `reflexio` CLI is available on PATH. */ -export function checkReflexioInstalled(): Promise { - return new Promise((resolve) => { - execFile( - "reflexio", - ["--version"], - { timeout: 5_000 }, - (err) => { resolve(err === null); }, - ); - }); -} - -/** Detect which Python package installer is available. */ -export function detectInstaller(): "pipx" | "pip" | null { - try { - execFileSync("pipx", ["--version"], { timeout: 3_000, stdio: ["ignore", "pipe", "pipe"] }); - return "pipx"; - } catch { - // pipx not found - } - try { - execFileSync("pip", ["--version"], { timeout: 3_000, stdio: ["ignore", "pipe", "pipe"] }); - return "pip"; - } catch { - // pip not found - } - return null; -} - -/** Check if Reflexio has been configured (~/.reflexio/.env with REFLEXIO_URL). */ -export function checkReflexioConfigured(reflexioDir: string = REFLEXIO_DIR): boolean { - try { - const envPath = path.join(reflexioDir, ".env"); - const content = fs.readFileSync(envPath, "utf-8"); - return content.split("\n").some((line) => { - const trimmed = line.trim(); - if (trimmed.startsWith("#")) return false; - const match = trimmed.match(/^REFLEXIO_URL\s*=\s*"?([^"\s]+)/); - return match !== null && match[1].length > 0; - }); - } catch { - return false; - } -} - -/** - * Run the full setup check chain. Returns guidance for the agent if setup - * is incomplete, or null if everything is ready. - */ -export async function runSetupCheck( - agentId: string, - reflexioDir: string = REFLEXIO_DIR, -): Promise { - // Gate: already set up? - if (isSetupComplete(reflexioDir, agentId)) return null; - - // Step 1: Is reflexio installed? - const installed = await checkReflexioInstalled(); - if (!installed) { - const installer = detectInstaller(); - if (installer === "pipx") { - return { - message: - "Reflexio CLI is required but not found. " + - "I can install it via pipx. OK if I run `pipx install reflexio-ai`?", - blocking: true, - }; - } - if (installer === "pip") { - return { - message: - "Reflexio CLI is required but not found. " + - "I can install it via pip. OK if I run `pip install reflexio-ai`? " + - "(If this fails on macOS, I'll guide you through pipx instead.)", - blocking: true, - }; - } - return { - message: - "Reflexio CLI is required but not found, and neither pipx nor pip is available. " + - "Please install pipx first (https://pipx.pypa.io/), then I can install Reflexio for you.", - blocking: true, - }; - } - - // Step 2: Is Reflexio configured? - if (!checkReflexioConfigured(reflexioDir)) { - return { - message: - "Reflexio needs initial configuration (storage backend, LLM provider). " + - "OK if I run `reflexio setup init` to start the setup wizard?", - blocking: true, - }; - } - - // All checks passed — create marker - markSetupComplete(reflexioDir, agentId); - return null; -} diff --git a/reflexio/integrations/openclaw/plugin/index.ts b/reflexio/integrations/openclaw/plugin/index.ts deleted file mode 100644 index c6589c54..00000000 --- a/reflexio/integrations/openclaw/plugin/index.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Reflexio Federated — Openclaw plugin entry. -// -// Registers lifecycle hooks and one tool against the Openclaw Plugin API: -// - before_prompt_build: auto-setup, search injection, retry old sessions -// - message_sent: buffer turn to SQLite, incremental publish -// - before_compaction: flush unpublished turns before transcript is lost -// - before_reset: flush unpublished turns before transcript is wiped -// - session_end: final flush of all remaining turns -// - reflexio_publish: agent-invoked immediate flush -// -// All core logic lives in ./hook/handler.ts — this file is only SDK wiring. -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; - -import { - handleBeforePromptBuild, - handleMessageSent, - handleSessionFlush, - handleToolPublish, - DEFAULT_CONFIG, - type PluginConfig, -} from "./hook/handler.ts"; - -// Track the most recently active session key, updated by before_prompt_build and message_sent. -// -// LIMITATION: This is a process-global variable. The Openclaw Plugin SDK does not pass session -// context into the tool `execute` callback, so `reflexio_publish` can only target the most -// recently active session. If multiple sessions run concurrently in the same process, this value -// will reflect whichever session fired last — meaning `reflexio_publish` may flush the wrong -// session's turns. This plugin is designed for single-session use. Concurrent multi-session -// support would require the Plugin SDK to expose session context in the tool execute callback. -let _activeSessionKey = ""; - -export default definePluginEntry({ - id: "reflexio-federated", - name: "Reflexio Federated", - description: - "Cross-session memory via Reflexio server. Publishes conversations for extraction, injects relevant profiles and playbooks before each response.", - register(api) { - const log = api.logger; - const runner = api.runtime.system.runCommandWithTimeout; - - // Merge user config with defaults - const rawConfig = (api.pluginConfig ?? {}) as Record; - const config: PluginConfig = { - publish: { ...DEFAULT_CONFIG.publish, ...((rawConfig.publish as Record) ?? {}) }, - search: { ...DEFAULT_CONFIG.search, ...((rawConfig.search as Record) ?? {}) }, - server: { ...DEFAULT_CONFIG.server, ...((rawConfig.server as Record) ?? {}) }, - }; - - // before_prompt_build: setup, search, retry - api.on("before_prompt_build", async (_event: unknown, ctx: unknown) => { - const context = ctx as { agentId?: string; sessionKey?: string }; - const sessionKey = context.sessionKey ?? ""; - const agentId = context.agentId ?? "main"; - if (sessionKey) _activeSessionKey = sessionKey; - - // Extract prompt from event (best-effort) - const eventObj = _event as { prompt?: string; messages?: { role?: string; content?: unknown }[] }; - let prompt = eventObj.prompt; - if (!prompt && Array.isArray(eventObj.messages)) { - const lastUser = [...eventObj.messages].reverse().find((m) => m.role === "user"); - if (lastUser) { - prompt = typeof lastUser.content === "string" - ? lastUser.content - : JSON.stringify(lastUser.content); - } - } - - const result = await handleBeforePromptBuild( - sessionKey, - agentId, - prompt, - runner, - config, - log, - ); - - if (result.prependSystemContext) { - return { prependSystemContext: result.prependSystemContext }; - } - }); - - // message_sent: buffer turn to SQLite - api.on("message_sent", (_event: unknown, ctx: unknown) => { - const context = ctx as { sessionKey?: string }; - const event = _event as { userMessage?: string; agentResponse?: string; content?: string; role?: string }; - const sessionKey = context.sessionKey ?? ""; - if (sessionKey) _activeSessionKey = sessionKey; - handleMessageSent( - sessionKey, - event.userMessage, - event.agentResponse ?? (event.role === "assistant" ? (typeof event.content === "string" ? event.content : undefined) : undefined), - runner, - config, - log, - ); - }); - - // before_compaction: flush before transcript is compacted - api.on("before_compaction", async (_event, ctx) => { - handleSessionFlush(ctx.sessionKey ?? "", log, config); - }); - - // before_reset: flush before transcript is wiped - api.on("before_reset", async (_event, ctx) => { - handleSessionFlush(ctx.sessionKey ?? "", log, config); - }); - - // session_end: final flush - api.on("session_end", async (event, ctx) => { - handleSessionFlush(ctx.sessionKey ?? event.sessionKey ?? "", log, config); - }); - - // reflexio_publish tool: agent-invoked immediate flush - api.registerTool({ - name: "reflexio_publish", - description: - "Immediately publish all buffered conversation turns to the Reflexio server. " + - "Use after user corrections, key milestones, or high-signal moments when you " + - "don't want to wait for the automatic session-end publish.", - parameters: { type: "object", properties: {} }, - optional: true, - async execute(_id: string, _params: Record) { - // Use the module-level active session key tracked by before_prompt_build and message_sent. - const result = handleToolPublish(_activeSessionKey, log, config); - return { content: [{ type: "text" as const, text: result }] }; - }, - }); - }, -}); diff --git a/reflexio/integrations/openclaw/plugin/lib/publish.ts b/reflexio/integrations/openclaw/plugin/lib/publish.ts deleted file mode 100644 index bdfb1d40..00000000 --- a/reflexio/integrations/openclaw/plugin/lib/publish.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Build payload and spawn `reflexio interactions publish`. -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { spawn } from "node:child_process"; -import type Database from "better-sqlite3"; -import type { Turn } from "./sqlite-buffer.ts"; -import { - getUnpublished, - markInFlight, - markPublished, - markFailed, -} from "./sqlite-buffer.ts"; - -type Logger = { - info?: (msg: string) => void; - error?: (msg: string) => void; -}; - -const PAYLOAD_DIR = path.join(os.homedir(), ".reflexio", "tmp"); - -/** Build a JSON payload string from turns. */ -export function buildPayload( - turns: Turn[], - userId: string, - agentVersion: string, - sessionId: string, -): string { - return JSON.stringify({ - user_id: userId, - source: "openclaw", - agent_version: agentVersion, - session_id: sessionId, - interactions: turns.map((t) => ({ role: t.role, content: t.content })), - }); -} - -/** Sanitize a string for use in a filename. */ -function sanitizeFilename(name: string): string { - return name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^-+/, "").slice(0, 100) || "unnamed"; -} - -/** - * Publish unpublished turns for a session. Fire-and-forget spawn of CLI. - * Marks turns in-flight before spawning, then published/failed on exit. - */ -export function publishSession( - db: Database.Database, - sessionId: string, - userId: string, - agentVersion: string, - maxRetries: number, - maxInteractions: number, - log?: Logger, -): void { - const turns = getUnpublished(db, sessionId, maxRetries, maxInteractions); - if (turns.length === 0) return; - - const maxId = turns[turns.length - 1].id; - markInFlight(db, sessionId, maxId, maxRetries); - - const payload = buildPayload(turns, userId, agentVersion, sessionId); - - fs.mkdirSync(PAYLOAD_DIR, { recursive: true, mode: 0o700 }); - const payloadFile = path.join( - PAYLOAD_DIR, - `publish-${sanitizeFilename(sessionId)}-${Date.now()}.json`, - ); - fs.writeFileSync(payloadFile, payload, { mode: 0o600 }); - - const child = spawn( - "reflexio", - ["interactions", "publish", "--file", payloadFile], - { stdio: ["ignore", "ignore", "ignore"], detached: true }, - ); - - child.on("error", (err) => { - log?.error?.(`[reflexio] Failed to spawn reflexio binary: ${err.message}`); - try { - markFailed(db, sessionId, maxId); - } catch (dbErr) { - log?.error?.(`[reflexio] Failed to update retry count after spawn error: ${dbErr}`); - } - try { - fs.unlinkSync(payloadFile); - } catch {} - }); - - child.on("close", (code) => { - if (code === 0) { - try { - markPublished(db, sessionId, maxId); - } catch (err) { - log?.error?.(`[reflexio] Failed to mark turns published: ${err}`); - } - } else { - log?.error?.(`[reflexio] Publish failed (exit ${code}), incrementing retry`); - try { - markFailed(db, sessionId, maxId); - } catch (err) { - log?.error?.(`[reflexio] Failed to update retry count: ${err}`); - } - } - try { - fs.unlinkSync(payloadFile); - } catch {} - }); - child.unref(); - - log?.info?.( - `[reflexio] Queued ${turns.length} turns for publish (session ${sessionId})`, - ); -} diff --git a/reflexio/integrations/openclaw/plugin/lib/search.ts b/reflexio/integrations/openclaw/plugin/lib/search.ts deleted file mode 100644 index 70ba1faf..00000000 --- a/reflexio/integrations/openclaw/plugin/lib/search.ts +++ /dev/null @@ -1,52 +0,0 @@ -// Search Reflexio and format results for context injection. -import type { CommandRunner } from "./server.ts"; - -const TRIVIAL_RE = /^(yes|no|ok|okay|sure|thanks|thank you|yep|nope|right|correct|got it|done|good|great|fine|lgtm|y|n|k|ty|thx|ack|np)$/i; - -/** Decide whether to skip search for a given message. */ -export function shouldSkipSearch(prompt: string, minLength: number): boolean { - if (!prompt || prompt.length < minLength) return true; - return TRIVIAL_RE.test(prompt.trim()); -} - -/** Format raw search output for context injection. Returns null if empty/no results. */ -export function formatSearchContext(raw: string): string | null { - const trimmed = raw.trim(); - if (!trimmed) return null; - if (trimmed.includes("Found 0 profiles, 0 playbooks")) return null; - return trimmed; -} - -/** - * Run `reflexio search` and return formatted context string. - * Returns null if no results. - * Throws an Error (with stderr as message) on non-zero exit code so callers can - * distinguish connection/binary errors from empty-results. - */ -export async function runSearch( - prompt: string, - userId: string, - topK: number, - timeoutMs: number, - runner: CommandRunner, -): Promise { - const result = await runner( - ["reflexio", "search", prompt.slice(0, 4096), "--user-id", userId, "--top-k", String(topK)], - { timeoutMs }, - ); - if (result.code !== 0) { - throw new Error(result.stderr || `reflexio search exited with code ${result.code}`); - } - return formatSearchContext(result.stdout); -} - -/** - * Check if a search failure looks like a connection error. - */ -export function isConnectionError(errMsg: string): boolean { - return ( - errMsg.includes("Cannot reach server") || - errMsg.includes("Connection refused") || - errMsg.includes("ECONNREFUSED") - ); -} diff --git a/reflexio/integrations/openclaw/plugin/lib/server.ts b/reflexio/integrations/openclaw/plugin/lib/server.ts deleted file mode 100644 index 9404da78..00000000 --- a/reflexio/integrations/openclaw/plugin/lib/server.ts +++ /dev/null @@ -1,103 +0,0 @@ -// Server management: URL resolution, health check, auto-start. -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { spawn } from "node:child_process"; - -export type CommandRunner = ( - argv: string[], - opts: { timeoutMs: number; input?: string }, -) => Promise<{ stdout: string; stderr: string; code: number | null }>; - -type Logger = { - info?: (msg: string) => void; - warn?: (msg: string) => void; - error?: (msg: string) => void; -}; - -const LOGS_DIR = path.join(os.homedir(), ".reflexio", "logs"); -const STARTING_FLAG = path.join(LOGS_DIR, ".server-starting"); -const DEFAULT_URL = "http://127.0.0.1:8081"; -const DEFAULT_STALE_FLAG_MS = 2 * 60 * 1000; - -/** Resolve Reflexio server URL: env var > ~/.reflexio/.env > default. */ -export function resolveServerUrl(): string { - if (process.env.REFLEXIO_URL) return process.env.REFLEXIO_URL; - try { - const envPath = path.join(os.homedir(), ".reflexio", ".env"); - const content = fs.readFileSync(envPath, "utf-8"); - const match = content.match(/^REFLEXIO_URL="?([^"\n]+)/m); - if (match) return match[1]; - } catch { - // .env file missing - } - return DEFAULT_URL; -} - -/** Check if a URL points to a local server. */ -export function isLocalServer(url: string): boolean { - try { - const parsed = new URL(url); - return parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost"; - } catch { - return url.includes("127.0.0.1") || url.includes("localhost"); - } -} - -/** - * Ensure the local Reflexio server is running. - * Remote servers are never auto-started. Uses a flag file to prevent concurrent starts. - */ -export async function ensureServerRunning( - runner: CommandRunner, - opts: { staleFlagMs?: number; log?: Logger } = {}, -): Promise { - const serverUrl = resolveServerUrl(); - if (!isLocalServer(serverUrl)) return; - - const staleFlagMs = opts.staleFlagMs ?? DEFAULT_STALE_FLAG_MS; - - // Check flag file — if recent, another start is in progress - try { - const stat = fs.statSync(STARTING_FLAG); - if (Date.now() - stat.mtimeMs < staleFlagMs) { - opts.log?.info?.("[reflexio] Server start already in progress, skipping"); - return; - } - fs.unlinkSync(STARTING_FLAG); - } catch { - // Flag doesn't exist — proceed - } - - // Health check - try { - const result = await runner( - ["curl", "-sf", "--max-time", "2", `${serverUrl}/health`], - { timeoutMs: 3_000 }, - ); - if (result.code === 0) return; // Server healthy - } catch { - // Server not running - } - - // Start server in background - fs.mkdirSync(LOGS_DIR, { recursive: true, mode: 0o700 }); - fs.writeFileSync(STARTING_FLAG, String(Date.now()), { mode: 0o600 }); - - const logPath = path.join(LOGS_DIR, "server.log"); - const logFd = fs.openSync(logPath, "a"); - const child = spawn( - "reflexio", - ["services", "start", "--only", "backend"], - { detached: true, stdio: ["ignore", logFd, logFd] }, - ); - child.unref(); - fs.closeSync(logFd); - - // Remove stale flag after 30 seconds - setTimeout(() => { - try { fs.unlinkSync(STARTING_FLAG); } catch { /* already removed */ } - }, 30_000).unref(); - - opts.log?.info?.("[reflexio] Server not running — starting in background"); -} diff --git a/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts b/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts deleted file mode 100644 index 5cf9d2d3..00000000 --- a/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +++ /dev/null @@ -1,156 +0,0 @@ -// SQLite session buffer — persistent, crash-safe conversation store. -import Database from "better-sqlite3"; -import * as fs from "node:fs"; -import * as path from "node:path"; - -const MAX_CONTENT_LENGTH = 10_000; - -export interface Turn { - id: number; - session_id: string; - role: string; - content: string; - timestamp: string; - published: number; - retry_count: number; -} - -/** Smart truncation: preserve head + tail with marker, guaranteed <= maxLength. */ -export function smartTruncate( - content: string, - maxLength: number = MAX_CONTENT_LENGTH, -): string { - if (!content || content.length <= maxLength) return content || ""; - // Use a fixed-width marker estimate to break the circular dependency - // between marker length and truncated count. - const markerTemplate = "\n\n[...truncated NNNNNNN chars...]\n\n"; // ~38 chars - const budget = maxLength - markerTemplate.length; - if (budget <= 0) return content.slice(0, maxLength); - const headLen = Math.floor(budget * 0.8); - const tailLen = budget - headLen; - const truncated = content.length - headLen - tailLen; - const marker = `\n\n[...truncated ${truncated} chars...]\n\n`; - const result = - tailLen <= 0 - ? content.slice(0, headLen) + marker - : content.slice(0, headLen) + marker + content.slice(-tailLen); - // Final safety clamp for edge cases (very large truncated counts exceeding reserved marker width) - return result.length <= maxLength ? result : result.slice(0, maxLength); -} - -/** Open (or create) the SQLite database and ensure schema exists. */ -export function openDb(dbPath: string): Database.Database { - fs.mkdirSync(path.dirname(dbPath), { recursive: true, mode: 0o700 }); - const db = new Database(dbPath); - db.pragma("journal_mode = WAL"); - db.exec(` - CREATE TABLE IF NOT EXISTS turns ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - role TEXT NOT NULL, - content TEXT NOT NULL, - timestamp TEXT NOT NULL, - published INTEGER DEFAULT 0, - retry_count INTEGER DEFAULT 0 - ); - CREATE INDEX IF NOT EXISTS idx_session_published - ON turns(session_id, published); - `); - try { - db.exec("ALTER TABLE turns ADD COLUMN retry_count INTEGER DEFAULT 0"); - } catch { - // Column already exists - } - return db; -} - -/** Insert a single turn into the buffer. */ -export function insertTurn( - db: Database.Database, - sessionId: string, - role: string, - content: string, - maxContentLength: number = MAX_CONTENT_LENGTH, -): void { - const truncated = smartTruncate(content, maxContentLength); - const now = new Date().toISOString(); - db.prepare( - "INSERT INTO turns (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)", - ).run(sessionId, role, truncated, now); -} - -/** Get unpublished turns for a session, respecting retry limit. */ -export function getUnpublished( - db: Database.Database, - sessionId: string, - maxRetries: number, - maxInteractions: number, -): Turn[] { - return db - .prepare( - "SELECT * FROM turns WHERE session_id = ? AND published = 0 AND retry_count < ? ORDER BY id LIMIT ?", - ) - .all(sessionId, maxRetries, maxInteractions) as Turn[]; -} - -/** Get distinct session IDs with unpublished turns (excluding current session). */ -export function getOldUnpublishedSessions( - db: Database.Database, - currentSessionId: string, - maxRetries: number, - limit: number, -): string[] { - const rows = db - .prepare( - "SELECT session_id FROM turns WHERE published = 0 AND retry_count < ? AND session_id != ? GROUP BY session_id ORDER BY MIN(id) LIMIT ?", - ) - .all(maxRetries, currentSessionId, limit) as { session_id: string }[]; - return rows.map((r) => r.session_id); -} - -/** Count unpublished turns for a session. */ -export function countUnpublished( - db: Database.Database, - sessionId: string, -): number { - const row = db - .prepare( - "SELECT COUNT(*) as count FROM turns WHERE session_id = ? AND published = 0", - ) - .get(sessionId) as { count: number }; - return row.count; -} - -/** Mark turns as in-flight (published=2) to prevent concurrent publish. */ -export function markInFlight( - db: Database.Database, - sessionId: string, - maxId: number, - maxRetries: number, -): void { - db.prepare( - "UPDATE turns SET published = 2 WHERE session_id = ? AND published = 0 AND retry_count < ? AND id <= ?", - ).run(sessionId, maxRetries, maxId); -} - -/** Mark in-flight turns as successfully published (scoped to batch). */ -export function markPublished(db: Database.Database, sessionId: string, maxId: number): void { - db.prepare( - "UPDATE turns SET published = 1 WHERE session_id = ? AND published = 2 AND id <= ?", - ).run(sessionId, maxId); -} - -/** Reset in-flight turns back to unpublished and increment retry count (scoped to batch). */ -export function markFailed(db: Database.Database, sessionId: string, maxId: number): void { - db.prepare( - "UPDATE turns SET published = 0, retry_count = retry_count + 1 WHERE session_id = ? AND published = 2 AND id <= ?", - ).run(sessionId, maxId); -} - -/** Delete published turns older than the given number of days. */ -export function cleanupOldTurns(db: Database.Database, days: number = 7): void { - const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); - db.prepare( - "DELETE FROM turns WHERE published = 1 AND timestamp < ?", - ).run(cutoff); -} diff --git a/reflexio/integrations/openclaw/plugin/lib/user-id.ts b/reflexio/integrations/openclaw/plugin/lib/user-id.ts deleted file mode 100644 index 53d40df9..00000000 --- a/reflexio/integrations/openclaw/plugin/lib/user-id.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Multi-user identity resolution. -// -// Each Openclaw agent instance maps to a distinct Reflexio user. -// Resolution chain: env var > session key agentId > openclaw.json > fallback. -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; - -let _openclawConfig: Record | null = null; - -/** Strip JSON5 line and block comments while respecting quoted strings. */ -export function stripJsonComments(raw: string): string { - const result: string[] = []; - let inBlockComment = false; - for (const line of raw.split("\n")) { - let out = ""; - let inString = false; - let escape = false; - let i = 0; - while (i < line.length) { - const ch = line[i]; - if (inBlockComment) { - // Look for end of block comment - if (ch === "*" && line[i + 1] === "/") { - inBlockComment = false; - i += 2; - } else { - i++; - } - continue; - } - if (escape) { - escape = false; - out += ch; - i++; - continue; - } - if (inString) { - if (ch === "\\") { - escape = true; - out += ch; - i++; - continue; - } - if (ch === '"') { - inString = false; - out += ch; - i++; - continue; - } - out += ch; - i++; - continue; - } - // Outside string - if (ch === '"') { - inString = true; - out += ch; - i++; - continue; - } - if (ch === "/" && line[i + 1] === "/") { - // Line comment — rest of line is comment - break; - } - if (ch === "/" && line[i + 1] === "*") { - // Block comment start - inBlockComment = true; - i += 2; - continue; - } - out += ch; - i++; - } - result.push(out); - } - return result.join("\n"); -} - -/** Read and cache ~/.openclaw/openclaw.json (JSON5 — strip comments before parsing). */ -function loadOpenclawConfig(): Record { - if (_openclawConfig !== null) return _openclawConfig; - try { - const configPath = path.join(os.homedir(), ".openclaw", "openclaw.json"); - const raw = fs.readFileSync(configPath, "utf-8"); - const stripped = stripJsonComments(raw); - _openclawConfig = JSON.parse(stripped); - } catch { - _openclawConfig = {}; - } - return _openclawConfig!; -} - -/** - * Resolve the Reflexio user ID. - * - * @param sessionKey - The Openclaw session key from hook context. - * @returns The resolved user ID string. - */ -export function resolveUserId(sessionKey: string): string { - // 1. Explicit env override - if (process.env.REFLEXIO_USER_ID) return process.env.REFLEXIO_USER_ID; - - // 2. Extract agentId from session key (format: agent::) - const sessionMatch = sessionKey.match(/^agent:([^:]+):/); - if (sessionMatch) return sessionMatch[1]; - - // 3. Read openclaw.json - const config = loadOpenclawConfig(); - const agents = config.agents as Record | undefined; - if (agents) { - if (typeof agents.defaults === "string") return agents.defaults; - if (Array.isArray(agents.list) && agents.list.length > 0) { - const first = agents.list[0]; - if (first && typeof first === "object" && (first as Record).name) { - return (first as Record).name as string; - } - if (typeof first === "string") return first; - } - } - - // 4. Fallback - return "openclaw"; -} - -/** Resolve the agent version label. */ -export function resolveAgentVersion(): string { - return process.env.REFLEXIO_AGENT_VERSION || "openclaw-agent"; -} - -/** Reset cached state — for testing only. */ -export function _resetCache(): void { - _openclawConfig = null; -} diff --git a/reflexio/integrations/openclaw/plugin/openclaw.plugin.json b/reflexio/integrations/openclaw/plugin/openclaw.plugin.json deleted file mode 100644 index cfa84d55..00000000 --- a/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "id": "reflexio-federated", - "name": "Reflexio Federated", - "description": "Cross-session memory via Reflexio server. Publishes conversations for extraction, injects relevant profiles and playbooks before each response.", - "skills": ["./skills"], - "configSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "publish": { - "type": "object", - "additionalProperties": false, - "properties": { - "batch_size": { "type": "integer", "default": 10 }, - "max_retries": { "type": "integer", "default": 3 }, - "max_content_length": { "type": "integer", "default": 10000 } - }, - "default": { "batch_size": 10, "max_retries": 3, "max_content_length": 10000 } - }, - "search": { - "type": "object", - "additionalProperties": false, - "properties": { - "timeout_ms": { "type": "integer", "default": 5000 }, - "top_k": { "type": "integer", "default": 5 }, - "min_prompt_length": { "type": "integer", "default": 5 } - }, - "default": { "timeout_ms": 5000, "top_k": 5, "min_prompt_length": 5 } - }, - "server": { - "type": "object", - "additionalProperties": false, - "properties": { - "health_check_timeout_ms": { "type": "integer", "default": 3000 }, - "stale_flag_ms": { "type": "integer", "default": 120000 } - }, - "default": { "health_check_timeout_ms": 3000, "stale_flag_ms": 120000 } - } - } - } -} diff --git a/reflexio/integrations/openclaw/plugin/package.json b/reflexio/integrations/openclaw/plugin/package.json deleted file mode 100644 index 41afd003..00000000 --- a/reflexio/integrations/openclaw/plugin/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@reflexio/openclaw-federated", - "version": "0.1.0", - "description": "Cross-session memory via Reflexio server for Openclaw agents.", - "private": true, - "type": "module", - "openclaw": { - "extensions": ["./index.ts"], - "compat": { - "pluginApi": ">=2026.3.24", - "minGatewayVersion": "2026.3.24" - } - }, - "dependencies": { - "better-sqlite3": "^11.0.0" - } -} diff --git a/reflexio/integrations/openclaw/plugin/rules/reflexio.md b/reflexio/integrations/openclaw/plugin/rules/reflexio.md deleted file mode 100644 index 97e97a4c..00000000 --- a/reflexio/integrations/openclaw/plugin/rules/reflexio.md +++ /dev/null @@ -1,24 +0,0 @@ -# Reflexio: Cross-Session Memory - -This agent uses Reflexio to remember corrections and preferences across sessions. - -## How It Works - -A `before_prompt_build` hook automatically runs `reflexio search` on every message you receive. When relevant corrections or preferences exist, they are injected as context before your response. - -## What You MUST Do - -1. **When you see injected Reflexio context** — read it carefully and follow every instruction. Behavior Corrections tell you what to do differently. User Preferences tell you how the user works. Agent Playbooks are shared corrections from all instances. - -2. **If no Reflexio context appears for a substantive task request** — run a manual search. Skip this for short or trivial messages (greetings, one-word replies, yes/no responses): - ``` - reflexio search "" - ``` - -3. **After user corrections** — once confirmed, call the `reflexio_publish` tool to flush the conversation to Reflexio immediately. - -4. **Never mention Reflexio to the user** unless they ask about it. It should be invisible. - -5. **If Reflexio is unavailable, proceed normally** — it enhances but never blocks your work. Never degrade the core agent experience because of Reflexio issues. - -6. **Never ask the user to manage the Reflexio server** — infrastructure is the plugin's responsibility. diff --git a/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md b/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md deleted file mode 100644 index 2ca4f504..00000000 --- a/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: reflexio -description: "Cross-session memory via Reflexio server. Search is automatic (hook-injected before every response). Use reflexio_publish tool to flush corrections and learnings immediately. Manual search fallback via exec if hook injection is empty." ---- - -# Reflexio: Cross-Session Memory - -Reflexio remembers corrections and preferences from past conversations. A hook automatically searches Reflexio before every response and injects relevant context — you don't need to do anything for retrieval. - -## What Reflexio Stores (on the server) - -**User Profiles** — facts about the user: expertise, preferences, environment, constraints. - -**User Playbooks** — behavioral corrections from past sessions: -- **trigger**: when does this rule apply? -- **instruction**: what to do instead -- **pitfall**: what to avoid -- **rationale**: why the correction matters - -**Agent Playbooks** — shared corrections aggregated from all agent instances. - -## Search (Automatic) - -A hook runs `reflexio search` on every user message and injects matching profiles and playbooks as context before your response. No action needed. - -**If no context appears for a task** — the hook may not have found results, or the server may be starting. Run a manual search: -```bash -reflexio search "" -``` - -If the command fails with a connection error and `REFLEXIO_URL` is unset or points to localhost, the plugin will start the server automatically. If it points to a remote server, report the issue to the user. - -## Publish (When to Use `reflexio_publish`) - -Conversations are automatically published to Reflexio at session end and periodically mid-session. For high-signal moments, call the `reflexio_publish` tool to flush immediately: - -- **User corrects you** and confirms the fix (explicit "good" / "perfect" or moves on) -- **You complete a key milestone** with non-obvious learnings -- **High-friction session** with multiple corrections - -The Reflexio server handles extraction (profiles, playbooks) from the published conversations. You don't need to structure the data — just publish, and the server does the rest. - -## Infrastructure - -- The plugin checks and starts the Reflexio server automatically -- **Never ask the user** to start, stop, or manage the server -- **Never mention Reflexio** to the user unless they ask -- If Reflexio is unavailable, proceed normally — it enhances but never blocks diff --git a/reflexio/integrations/openclaw/publish_clawhub.sh b/reflexio/integrations/openclaw/publish_clawhub.sh deleted file mode 100755 index 906d6d5c..00000000 --- a/reflexio/integrations/openclaw/publish_clawhub.sh +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env bash -# Publishes the reflexio openclaw integration as a single ClawHub skill. -# -# Two modes: -# 1) CLI publish — stages into a temp dir, generates top-level SKILL.md, -# runs `bun clawhub skill publish`. Requires bun + an authed session. -# 2) Stage-only — same staging + SKILL.md generation, but skips publish -# and prints the staged path so you can drop it onto the web form at -# https://clawhub.ai/publish-skill. -# -# Usage: -# ./publish_clawhub.sh [changelog] # CLI publish -# ./publish_clawhub.sh --stage-only # produce folder for web upload -# -# Prereqs: -# - CLI mode: `bun clawhub login` done, `bun` on PATH -# - Stage-only mode: just `rsync` and `python3` on PATH - -set -euo pipefail - -STAGE_ONLY=0 -if [[ "${1:-}" == "--stage-only" ]]; then - STAGE_ONLY=1 - VERSION="0.0.0-staged" - CHANGELOG="" -else - VERSION="${1:?usage: publish_clawhub.sh [changelog] | --stage-only}" - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]]; then - echo "error: version must be semver (got: $VERSION)" >&2 - exit 1 - fi - CHANGELOG="${2:-Release $VERSION}" -fi - -SRC="$(cd "$(dirname "$0")" && pwd)" -STAGE="$(mktemp -d -t reflexio-clawhub-XXXXXX)" -if (( STAGE_ONLY == 0 )); then - trap 'rm -rf "$STAGE"' EXIT -fi - -if (( STAGE_ONLY == 0 )); then - if ! command -v bun >/dev/null 2>&1; then - echo "error: bun not found on PATH (install from https://bun.sh)" >&2 - exit 1 - fi - - if ! bun clawhub whoami >/dev/null 2>&1; then - echo "error: not logged in to clawhub. run: bun clawhub login" >&2 - exit 1 - fi -fi - -echo "staging bundle → $STAGE" -# Prune everything that shouldn't be in the published bundle. The web -# uploader at clawhub.ai/publish-skill does NOT read .clawhubignore -# (that's a CLI-only feature), so we pre-filter here and the same stage -# dir works for both CLI publish and web drag-drop. -rsync -a \ - --exclude='node_modules/' \ - --exclude='package-lock.json' \ - --exclude='.DS_Store' \ - --exclude='eval/' \ - --exclude='TESTING.md' \ - --exclude='publish_clawhub.sh' \ - --exclude='.clawhubignore' \ - "$SRC"/ "$STAGE"/ - -echo "generating top-level SKILL.md from skill/SKILL.md" -python3 - "$STAGE" <<'PY' -import re -import sys -from pathlib import Path - -stage = Path(sys.argv[1]) -src_skill = stage / "skill" / "SKILL.md" -body = src_skill.read_text() - -m = re.match(r"^---\n(.*?)\n---\n(.*)$", body, re.DOTALL) -if not m: - sys.exit(f"error: {src_skill} has no YAML frontmatter") -orig_frontmatter, orig_body = m.group(1), m.group(2) - -# Pull the original description verbatim (multi-line safe). -desc_match = re.search( - r'^description:\s*"((?:[^"\\]|\\.)*)"', orig_frontmatter, re.MULTILINE -) -if not desc_match: - sys.exit("error: could not find description in skill/SKILL.md frontmatter") -description = desc_match.group(1) - -frontmatter = ( - "---\n" - "name: reflexio\n" - f'description: "{description}"\n' - "metadata:\n" - " openclaw:\n" - " homepage: https://github.com/reflexio-ai/reflexio/tree/main/reflexio/integrations/openclaw\n" - " emoji: 🧠\n" - " requires:\n" - " bins: [reflexio]\n" - "---\n" -) - -privacy = """ -## Privacy & Data Collection — Read This First - -**This skill causes the agent to automatically capture conversations and -forward them to a local Reflexio server for LLM-based extraction.** Read this -before enabling — there are two distinct network hops and you need to -understand both. - -### Credential requirement (not declared in skill metadata) - -The skill's registry metadata declares no required environment variables and -the hook reads none. **But the end-to-end system does require an LLM provider -API key**, and you WILL be asked for one during First-Use Setup. - -- Step 2 of First-Use Setup below runs `reflexio setup openclaw`, which opens - an **interactive wizard** prompting you to choose an LLM provider (OpenAI, - Anthropic, Gemini, DeepSeek, OpenRouter, and several others) and paste an - API key. -- The key is stored in `~/.reflexio/.env` and read by the local Reflexio - server during extraction. **The hook itself never reads the key** (the hook - has no environment variable access and no filesystem config reads — both - enforced in `handler.js`). -- If you want fully offline operation, point the wizard at a local LLM - (Ollama at `http://127.0.0.1:11434`, LM Studio, vLLM, etc.) instead of a - hosted provider — the wizard accepts any LiteLLM-compatible base URL. - -**Why the metadata doesn't declare it:** ClawHub's `metadata.openclaw.requires.env` -describes environment variables the hook's own code path reads. The hook is -deliberately stateless at the credential level, so listing anything there -would be inaccurate. The dependency is at the *backend server* level, one -hop away. This disclosure is here instead of in the metadata because prose -is the right place to explain the distinction. - -### Network hops — two of them - -**Hop 1: the hook → the local Reflexio server (always localhost).** -The hook is hard-pinned to `http://127.0.0.1:8081`. It communicates via native -`fetch()` with no configuration knobs; the destination is a hardcoded constant -in `handler.js`. It reads zero environment variables and zero configuration -files. This hop cannot leave your machine. - -**Hop 2: the local Reflexio server → an LLM provider (may leave your -machine).** The server uses an LLM provider (OpenAI, Anthropic, Gemini, -DeepSeek, etc.) to extract playbooks and profiles from captured conversations. -That provider is configured in `~/.reflexio/.env`. **If you configured an -external provider, excerpts of your conversations will be sent to that -provider** as part of extraction — trigger text, sample content, and enough -context for the extractor to produce a useful summary. The primary full -conversation text stays in your local SQLite database at `~/.reflexio/`, but -the extracted summaries and illustrative excerpts traverse whatever the LLM -provider's network path is. - -**If you want fully offline operation**, configure the local server to use a -local LLM (Ollama, LM Studio, vLLM, etc.) before enabling this skill. Do not -rely on the hook's localhost pinning as a privacy guarantee for the system as -a whole — that only bounds the hook, not the server behind it. - -**What is captured:** full user and assistant messages; every tool call, input, -and output (including failed tool calls and exact error strings); self-correction -text from the assistant's own output. None of this is scrubbed for PII, -credentials, file paths, or API outputs. If you work on sensitive tasks, -disable the hook before starting them, or tell the agent mid-task. - -**How to disable:** - -- Per-session, stop automatic capture: `openclaw hooks disable reflexio-context` -- Remove the integration entirely: `reflexio setup openclaw --uninstall` -- Search-only mode (keep retrieval, drop auto-publish): edit `hook/handler.js`, - remove the `command:stop` handler, and reinstall -- Sensitive task: tell the agent "don't log this" — the workspace rule instructs - it to stop all capture and search calls for the rest of the session - -**Transparency contract:** the agent is instructed to tell you Reflexio is -active on the first turn of each session, disclose what is captured AND -mention that the local server may forward excerpts to an LLM provider, and -honor session-level opt-outs. This is enforced by -`~/.openclaw/workspace/reflexio.md`. Edit that file if your deployment needs -different rules (require explicit approval before every publish, offline-only, -etc). - -Full data-handling details live in the **Privacy & Data Collection** section -further down in this SKILL.md. - ---- - -""" - -first_use = """ -## First-Use Setup - -Run these commands the first time this skill is invoked. They are idempotent — -skip if `reflexio` is already on PATH, the local Reflexio server is responding -at `http://127.0.0.1:8081/health`, and `openclaw hooks list` shows -`reflexio-context` as `ready`. **Only run them after reading the privacy -disclosure above, and be aware that step 2 is interactive — it will prompt -you to pick an LLM provider and paste an API key for the local server's -extraction pipeline.** - -```bash -# 1. Install the reflexio CLI (pipx preferred; pip as fallback). -command -v reflexio >/dev/null 2>&1 \\ - || pipx install reflexio-ai 2>/dev/null \\ - || pip install --user reflexio-ai - -# 2. INTERACTIVE: activates the hook, slash commands, and workspace rule in -# OpenClaw. Also prompts you to choose an LLM provider and paste an API key, -# which gets written to ~/.reflexio/.env. That key is what the local server -# uses to extract playbooks from captured conversations. Point at a local LLM -# (Ollama / LM Studio / vLLM) here if you want fully offline operation. -# Before running this, tell the user what will be asked and why. -reflexio setup openclaw - -# 3. Start the local Reflexio backend in the background. The hook talks to -# this server via HTTP on 127.0.0.1:8081 — it will NOT start the server for -# you. Tell the user you are doing this before running it. -curl -sf --max-time 2 http://127.0.0.1:8081/health >/dev/null 2>&1 \\ - || (nohup reflexio services start --only backend --no-reload \\ - >> ~/.reflexio/logs/server.log 2>&1 & \\ - sleep 5) -``` - -This installs: - -- `reflexio-context` hook — HTTP-only capture + per-message playbook - injection (communicates only with `http://127.0.0.1:8081`) -- `/reflexio-extract` slash command — publish session learnings mid-session -- `/reflexio-aggregate` slash command — consolidate user playbooks into - shared agent playbooks -- `~/.openclaw/workspace/reflexio.md` — always-active behavioral rule - (transparency + opt-out handling) - -If the local server is unreachable, the hook logs the error and the agent -continues the user's task without Reflexio context that session. - ---- - -""" - -(stage / "SKILL.md").write_text(frontmatter + privacy + first_use + orig_body) -print(f"wrote {stage / 'SKILL.md'}") -PY - -if (( STAGE_ONLY == 1 )); then - cat < - Tags: latest - Changelog: Initial release - -When done, delete the temp folder: rm -rf $STAGE -───────────────────────────────────────────────────────────── -EOF - exit 0 -fi - -echo "publishing to clawhub: slug=reflexio version=$VERSION" -( - cd "$STAGE" - bun clawhub skill publish . \ - --slug reflexio \ - --version "$VERSION" \ - --tags latest \ - --changelog "$CHANGELOG" -) - -echo "done. verify at https://clawhub.ai/skill/reflexio" diff --git a/reflexio/integrations/openclaw/references/HOOK.md b/reflexio/integrations/openclaw/references/HOOK.md deleted file mode 100644 index 542cf02b..00000000 --- a/reflexio/integrations/openclaw/references/HOOK.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -name: reflexio-federated-plugin -description: "Automatically capture conversations and inject cross-session context before every response" -metadata: - openclaw: - emoji: "🧠" - hooks: ["before_prompt_build", "message_sent", "before_compaction", "before_reset", "session_end"] - requires: - bins: ["reflexio"] - env: ["REFLEXIO_USER_ID", "REFLEXIO_AGENT_VERSION"] ---- - -# Reflexio Federated Plugin - -The plugin is the core component that enables automatic conversation capture and cross-session context injection. It is implemented against the **Openclaw Plugin SDK** (`openclaw/plugin-sdk`) and runs on every agent session, persisting data to a local SQLite buffer. - -## Plugin SDK Hooks - -The plugin registers the following hooks via `api.on(...)` in the Plugin SDK: - -### `before_prompt_build` (before each response) - -Fires before the agent builds its next response. This is the primary hook — it handles: - -1. **First-use setup check** — detects whether the `reflexio` CLI is installed and configured. If setup is needed, injects a setup instruction into the system context. -2. **Server auto-start** — checks if the local Reflexio server is healthy via a health check; if not, spawns `reflexio services start --only backend` in the background. -3. **Retry unpublished turns** — publishes any buffered turns from previous sessions that failed to reach the server. -4. **Search injection** — runs `reflexio search` on the user's message and, if results are found, prepends them as system context before the agent responds. Skips search for trivial/short inputs to reduce noise. Times out after `search.timeout_ms` (default: 5 000 ms) — never blocks the response. - -### `message_sent` (each turn) - -Fires after the agent sends a response. Buffers the (user message, agent response) pair into local SQLite at `~/.reflexio/sessions.db`. Lightweight local write — no network calls. If the buffer exceeds `publish.batch_size * 2` unpublished turns, triggers an incremental publish to the server. - -### `before_compaction` (before transcript compaction) - -Fires before Openclaw compacts the transcript. Flushes all unpublished buffered turns to the Reflexio server so no data is lost when the compaction discards history. - -### `before_reset` (before transcript wipe) - -Fires before Openclaw wipes the transcript (e.g., `/reset`). Same as `before_compaction` — flushes all remaining unpublished turns. - -### `session_end` (session end) - -Fires when the session ends. Performs a final flush of all remaining buffered turns to the Reflexio server. - -## Tool: `reflexio_publish` - -In addition to hooks, the plugin registers one agent-invocable tool: - -- **`reflexio_publish`** — immediately flushes all buffered conversation turns to the Reflexio server. Useful after user corrections or high-signal moments when automatic session-end flushing would be too late. - -> **Single-session limitation:** The `execute` callback in the Plugin SDK does not receive session context, so this tool always targets the most recently active session. Concurrent multi-session use is not supported. - -## Prerequisites - -1. **`reflexio` CLI on PATH** — `pipx install reflexio-ai` or `pip install --user reflexio-ai`. The plugin shells out to this CLI to start the backend server and publish conversations. - -2. **Node.js 18+** — Required to run the plugin. - -3. **An LLM provider API key in `~/.reflexio/.env`** — Required for the backend server to extract playbooks and profiles. The first-use setup wizard prompts you to configure this on the first agent session. - -## Configuration - -All settings are optional and defined in `plugin/openclaw.plugin.json` under the `configSchema`: - -### `publish` — Conversation capture and publish behavior - -```json -"publish": { - "batch_size": 10, // Number of turns to buffer before mid-session publish - "max_retries": 3, // Retry count for failed publish attempts - "max_content_length": 10000 // Max characters per turn (prevents oversized payloads) -} -``` - -### `search` — Context injection before each response - -```json -"search": { - "timeout_ms": 5000, // Max time to wait for search results - "top_k": 5, // Number of playbooks to return per search - "min_prompt_length": 5 // Skip search for inputs shorter than this -} -``` - -### `server` — Reflexio server health and startup - -```json -"server": { - "health_check_timeout_ms": 3000, // Time limit for health checks - "stale_flag_ms": 120000 // Flag file age before retry (2 minutes) -} -``` - -## Security and Privacy - -### What the plugin reads - -- **Environment variables**: `REFLEXIO_USER_ID` (overrides auto-derived user ID) and `REFLEXIO_AGENT_VERSION` (overrides agent version label). Both are optional. -- **`~/.openclaw/openclaw.json`**: read by the Openclaw host process to load the plugin; the plugin itself does not read this file directly. -- **`~/.reflexio/.env`**: read to resolve `REFLEXIO_URL` if the env var is not set in the process environment. - -### What the plugin shells out to - -The plugin spawns the `reflexio` CLI for: -- `reflexio services start --only backend` — starts the local server if not running -- `reflexio search ` — fetches context from the local server -- `reflexio interactions publish --file ` — publishes buffered turns - -All spawned processes use the binary on your `PATH`. If `reflexio` is not found, the `error` event on the child process is caught, the affected turns are marked for retry, and the plugin continues working without blocking the agent. - -### Network destinations - -The plugin communicates only with the Reflexio server at the URL resolved from `REFLEXIO_URL` / `~/.reflexio/.env` / default `http://127.0.0.1:8081`. Local-server detection uses exact hostname matching (`127.0.0.1` or `localhost`); only local servers are auto-started. - -**The local Reflexio server makes outbound LLM API calls** for profile/playbook extraction. The destination depends on your `~/.reflexio/.env` configuration (OpenAI, Anthropic, Gemini, or a local LLM). For a fully offline setup, configure a local LLM provider (Ollama, LM Studio, vLLM) during first-use setup. - -## Buffering and Retry - -The plugin uses SQLite to buffer conversations locally: - -- **Location**: `~/.reflexio/sessions.db` -- **Retention**: Buffered turns are kept until successfully published to the server -- **Retry logic**: On every `before_prompt_build`, the plugin retries any unpublished sessions from previous runs (up to `max_retries`) -- **Graceful degradation**: If the server is down, the agent works normally and buffers persist - -## Lifecycle Summary - -| Hook | Trigger | Actions | -|------|---------|---------| -| `before_prompt_build` | Before each agent response | Setup check, server auto-start, retry old sessions, search injection | -| `message_sent` | After each agent response | Buffer turn to SQLite, incremental publish if batch threshold exceeded | -| `before_compaction` | Before transcript compaction | Flush all unpublished turns | -| `before_reset` | Before transcript wipe | Flush all unpublished turns | -| `session_end` | Session ends | Final flush of all remaining turns | - -## Implementation - -The plugin is implemented in TypeScript in `plugin/`: - -- **`index.ts`** — SDK wiring: `definePluginEntry`, hook registration, tool registration -- **`hook/handler.ts`** — Core logic: `handleBeforePromptBuild`, `handleMessageSent`, `handleSessionFlush`, `handleToolPublish` -- **`hook/setup.ts`** — First-use setup: CLI detection, LLM provider prompt, server start - -Supporting libraries in `plugin/lib/`: - -- **`server.ts`** — URL resolution, health check, auto-start -- **`user-id.ts`** — User identity and agent version resolution from env vars / session key -- **`sqlite-buffer.ts`** — Turn buffering, retry management -- **`publish.ts`** — Payload construction, CLI spawn with error handling -- **`search.ts`** — Search invocation, result formatting, trivial-input filtering - -## Debugging - -All plugin log lines are prefixed with `[reflexio]` for easy filtering: - -```bash -# Option A: capture stderr to file and tail it -openclaw chat 2>reflexio-plugin.log -tail -f reflexio-plugin.log | grep "\[reflexio\]" - -# Option B: watch inline -openclaw chat 2>&1 | grep "\[reflexio\]" -``` diff --git a/reflexio/integrations/openclaw/scripts/install.sh b/reflexio/integrations/openclaw/scripts/install.sh deleted file mode 100755 index 483a4e51..00000000 --- a/reflexio/integrations/openclaw/scripts/install.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# reflexio-federated install.sh — plugin installation. -set -euo pipefail - -PLUGIN_DIR="$(cd "$(dirname "$0")/../plugin" && pwd)" -OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}" - -die() { echo "error: $*" >&2; exit 1; } -info() { echo "==> $*"; } - -# 1. Prereq checks -info "Checking prerequisites..." -command -v openclaw >/dev/null || die "openclaw CLI required but not found on PATH" -command -v node >/dev/null || die "node required but not found on PATH" - -# 2. Install the plugin -info "Installing plugin..." -openclaw plugins uninstall --force reflexio-federated 2>/dev/null || true -rm -rf "$OPENCLAW_HOME/extensions/reflexio-federated" -openclaw plugins install "$PLUGIN_DIR" -openclaw plugins enable reflexio-federated 2>/dev/null || true - -# 3. Restart gateway -info "Restarting openclaw gateway..." -openclaw gateway restart - -# 4. Verify -info "Verification:" -if openclaw plugins inspect reflexio-federated 2>/dev/null | grep -q "Status: loaded"; then - info " ✓ plugin registered and loaded" -else - die "Plugin did not reach 'loaded' status. Run 'openclaw plugins inspect reflexio-federated' to debug." -fi - -info "Installation complete." -info "Reflexio setup (CLI install, storage config, server start) happens automatically on first agent session." diff --git a/reflexio/integrations/openclaw/scripts/uninstall.sh b/reflexio/integrations/openclaw/scripts/uninstall.sh deleted file mode 100755 index 32686849..00000000 --- a/reflexio/integrations/openclaw/scripts/uninstall.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# reflexio-federated uninstall.sh — reverses install.sh. -# Leaves ~/.reflexio/ user data intact unless --purge is passed. -set -euo pipefail - -OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}" -PURGE_DATA="${1:-}" - -info() { echo "==> $*"; } - -info "Disabling plugin..." -openclaw plugins disable reflexio-federated 2>/dev/null || echo "(already disabled)" - -info "Uninstalling plugin..." -openclaw plugins uninstall --force reflexio-federated 2>/dev/null || echo "(already uninstalled)" -rm -rf "$OPENCLAW_HOME/extensions/reflexio-federated" - -info "Cleaning up state files..." -rm -f "$HOME/.reflexio/sessions.db" -rm -f "$HOME/.reflexio/logs/.server-starting" - -# Remove setup markers -find "$HOME/.reflexio/" -name ".setup_complete_*" -delete 2>/dev/null || true - -if [[ "$PURGE_DATA" == "--purge" ]]; then - info "Purging ~/.reflexio/ user data per --purge flag..." - rm -rf "$HOME/.reflexio" -else - info "User data at ~/.reflexio/ preserved. Use --purge to delete it too." -fi - -info "Restarting openclaw gateway..." -openclaw gateway restart || echo "warning: gateway restart failed — you may need to restart manually" - -info "Uninstall complete." diff --git a/reflexio/integrations/openclaw/tests/publish.test.ts b/reflexio/integrations/openclaw/tests/publish.test.ts deleted file mode 100644 index 41d9e9fc..00000000 --- a/reflexio/integrations/openclaw/tests/publish.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { buildPayload } from "../plugin/lib/publish.ts"; -import type { Turn } from "../plugin/lib/sqlite-buffer.ts"; - -describe("buildPayload", () => { - it("builds correct JSON structure", () => { - const turns: Turn[] = [ - { id: 1, session_id: "s1", role: "user", content: "hello", timestamp: "2026-01-01T00:00:00Z", published: 0, retry_count: 0 }, - { id: 2, session_id: "s1", role: "assistant", content: "hi", timestamp: "2026-01-01T00:00:01Z", published: 0, retry_count: 0 }, - ]; - const payload = buildPayload(turns, "user1", "openclaw-agent", "sess1"); - const parsed = JSON.parse(payload); - expect(parsed.user_id).toBe("user1"); - expect(parsed.source).toBe("openclaw"); - expect(parsed.agent_version).toBe("openclaw-agent"); - expect(parsed.session_id).toBe("sess1"); - expect(parsed.interactions).toHaveLength(2); - expect(parsed.interactions[0].role).toBe("user"); - expect(parsed.interactions[0].content).toBe("hello"); - }); - - it("returns empty interactions for empty turns", () => { - const payload = buildPayload([], "user1", "openclaw-agent", "sess1"); - const parsed = JSON.parse(payload); - expect(parsed.interactions).toHaveLength(0); - }); -}); diff --git a/reflexio/integrations/openclaw/tests/search.test.ts b/reflexio/integrations/openclaw/tests/search.test.ts deleted file mode 100644 index 8084a023..00000000 --- a/reflexio/integrations/openclaw/tests/search.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { shouldSkipSearch, formatSearchContext } from "../plugin/lib/search.ts"; - -describe("shouldSkipSearch", () => { - it("skips short messages", () => { - expect(shouldSkipSearch("hi", 5)).toBe(true); - }); - - it("skips trivial responses", () => { - expect(shouldSkipSearch("yes", 5)).toBe(true); - expect(shouldSkipSearch("ok", 5)).toBe(true); - expect(shouldSkipSearch("thanks", 5)).toBe(true); - expect(shouldSkipSearch("Sure", 5)).toBe(true); - }); - - it("does not skip real messages", () => { - expect(shouldSkipSearch("Write a Python function to sort a list", 5)).toBe(false); - }); -}); - -describe("formatSearchContext", () => { - it("returns null for empty results", () => { - expect(formatSearchContext("")).toBeNull(); - expect(formatSearchContext("Found 0 profiles, 0 playbooks")).toBeNull(); - }); - - it("returns trimmed content for real results", () => { - const result = formatSearchContext("## Playbooks\n- Use type hints"); - expect(result).toBe("## Playbooks\n- Use type hints"); - }); -}); diff --git a/reflexio/integrations/openclaw/tests/server.test.ts b/reflexio/integrations/openclaw/tests/server.test.ts deleted file mode 100644 index 65ed1959..00000000 --- a/reflexio/integrations/openclaw/tests/server.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { resolveServerUrl, isLocalServer } from "../plugin/lib/server.ts"; - -describe("resolveServerUrl", () => { - const originalEnv = process.env; - - beforeEach(() => { - process.env = { ...originalEnv }; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - it("returns REFLEXIO_URL env var when set", () => { - process.env.REFLEXIO_URL = "https://custom.server:9090"; - expect(resolveServerUrl()).toBe("https://custom.server:9090"); - }); - - it("returns default when no env var and no .env file", () => { - delete process.env.REFLEXIO_URL; - expect(resolveServerUrl()).toBe("http://127.0.0.1:8081"); - }); -}); - -describe("isLocalServer", () => { - it("returns true for localhost", () => { - expect(isLocalServer("http://localhost:8081")).toBe(true); - }); - - it("returns true for 127.0.0.1", () => { - expect(isLocalServer("http://127.0.0.1:8081")).toBe(true); - }); - - it("returns false for remote URL", () => { - expect(isLocalServer("https://reflexio.ai:8081")).toBe(false); - }); - - it("returns false for localhost.evil.com", () => { - expect(isLocalServer("https://localhost.evil.com")).toBe(false); - }); -}); diff --git a/reflexio/integrations/openclaw/tests/setup.test.ts b/reflexio/integrations/openclaw/tests/setup.test.ts deleted file mode 100644 index f36c3a5d..00000000 --- a/reflexio/integrations/openclaw/tests/setup.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { - isSetupComplete, - markSetupComplete, - checkReflexioInstalled, - checkReflexioConfigured, -} from "../plugin/hook/setup.ts"; - -describe("isSetupComplete", () => { - let tmpDir: string; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "reflexio-setup-test-")); - }); - - afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it("returns false when marker does not exist", () => { - expect(isSetupComplete(tmpDir, "main")).toBe(false); - }); - - it("returns true after markSetupComplete", () => { - markSetupComplete(tmpDir, "main"); - expect(isSetupComplete(tmpDir, "main")).toBe(true); - }); - - it("is per-agent", () => { - markSetupComplete(tmpDir, "main"); - expect(isSetupComplete(tmpDir, "work")).toBe(false); - }); -}); - -describe("checkReflexioInstalled", () => { - it("returns a boolean", async () => { - const result = await checkReflexioInstalled(); - expect(typeof result).toBe("boolean"); - }); -}); - -describe("checkReflexioConfigured", () => { - it("returns false for nonexistent directory", () => { - expect(checkReflexioConfigured("/tmp/nonexistent-reflexio-dir-" + Date.now())).toBe(false); - }); -}); diff --git a/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts b/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts deleted file mode 100644 index 48178ead..00000000 --- a/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { - openDb, - insertTurn, - getUnpublished, - markInFlight, - markPublished, - markFailed, - smartTruncate, - cleanupOldTurns, -} from "../plugin/lib/sqlite-buffer.ts"; -import type Database from "better-sqlite3"; - -describe("smartTruncate", () => { - it("returns content unchanged when under limit", () => { - expect(smartTruncate("hello", 100)).toBe("hello"); - }); - - it("truncates with head + marker + tail within maxLength", () => { - const content = "a".repeat(200); - const result = smartTruncate(content, 100); - expect(result.length).toBeLessThanOrEqual(100); - expect(result).toContain("[...truncated"); - }); - - it("returns empty string for empty input", () => { - expect(smartTruncate("", 100)).toBe(""); - }); -}); - -describe("sqlite-buffer", () => { - let db: Database.Database; - let tmpDir: string; - - beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "reflexio-test-")); - const dbPath = path.join(tmpDir, "sessions.db"); - db = openDb(dbPath); - }); - - afterEach(() => { - db.close(); - fs.rmSync(tmpDir, { recursive: true, force: true }); - }); - - it("inserts and retrieves turns", () => { - insertTurn(db, "sess1", "user", "hello"); - insertTurn(db, "sess1", "assistant", "hi there"); - const turns = getUnpublished(db, "sess1", 3, 100); - expect(turns).toHaveLength(2); - expect(turns[0].role).toBe("user"); - expect(turns[1].role).toBe("assistant"); - }); - - it("marks turns as in-flight then published", () => { - insertTurn(db, "sess1", "user", "hello"); - const turns = getUnpublished(db, "sess1", 3, 100); - const maxId = turns[turns.length - 1].id; - markInFlight(db, "sess1", maxId, 3); - expect(getUnpublished(db, "sess1", 3, 100)).toHaveLength(0); - markPublished(db, "sess1", maxId); - expect(getUnpublished(db, "sess1", 3, 100)).toHaveLength(0); - }); - - it("increments retry count on failure", () => { - insertTurn(db, "sess1", "user", "hello"); - const turns = getUnpublished(db, "sess1", 3, 100); - const id = turns[0].id; - markInFlight(db, "sess1", id, 3); - markFailed(db, "sess1", id); - const after = getUnpublished(db, "sess1", 3, 100); - expect(after).toHaveLength(1); - // After MAX_RETRIES failures, turn should be excluded - markInFlight(db, "sess1", id, 3); - markFailed(db, "sess1", id); - markInFlight(db, "sess1", id, 3); - markFailed(db, "sess1", id); - expect(getUnpublished(db, "sess1", 3, 100)).toHaveLength(0); - }); - - it("respects max interactions limit", () => { - for (let i = 0; i < 10; i++) { - insertTurn(db, "sess1", "user", `msg ${i}`); - } - const turns = getUnpublished(db, "sess1", 3, 5); - expect(turns).toHaveLength(5); - }); -}); diff --git a/reflexio/integrations/openclaw/tests/user-id.test.ts b/reflexio/integrations/openclaw/tests/user-id.test.ts deleted file mode 100644 index 319a502c..00000000 --- a/reflexio/integrations/openclaw/tests/user-id.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { resolveUserId, stripJsonComments, _resetCache } from "../plugin/lib/user-id.ts"; - -describe("stripJsonComments", () => { - it("strips line comments", () => { - expect(stripJsonComments('{ "key": "val" } // comment')).toBe('{ "key": "val" } '); - }); - - it("preserves URLs in quoted strings", () => { - expect(stripJsonComments('{ "url": "http://example.com" }')).toBe( - '{ "url": "http://example.com" }', - ); - }); - - it("strips comment after quoted string containing //", () => { - expect(stripJsonComments('{ "url": "http://x.com" } // note')).toBe( - '{ "url": "http://x.com" } ', - ); - }); -}); - -describe("resolveUserId", () => { - const originalEnv = process.env; - - beforeEach(() => { - process.env = { ...originalEnv }; - _resetCache(); - }); - - afterEach(() => { - process.env = originalEnv; - }); - - it("returns REFLEXIO_USER_ID env var if set", () => { - process.env.REFLEXIO_USER_ID = "custom-user"; - expect(resolveUserId("agent:main:abc123")).toBe("custom-user"); - }); - - it("extracts agentId from session key format agent::", () => { - expect(resolveUserId("agent:work:session789")).toBe("work"); - }); - - it("falls back to 'openclaw' when no session key or env", () => { - expect(resolveUserId("")).toBe("openclaw"); - }); - - it("falls back to 'openclaw' for non-agent session key format", () => { - expect(resolveUserId("random-session-key")).toBe("openclaw"); - }); -}); diff --git a/reflexio/integrations/openclaw/tsconfig.json b/reflexio/integrations/openclaw/tsconfig.json deleted file mode 100644 index 0d7bb9a6..00000000 --- a/reflexio/integrations/openclaw/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": true, - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "types": ["node"] - }, - "include": ["plugin/**/*.ts", "types/**/*.d.ts"], - "exclude": ["tests", "node_modules"] -} diff --git a/reflexio/integrations/openclaw/types/openclaw.d.ts b/reflexio/integrations/openclaw/types/openclaw.d.ts deleted file mode 100644 index 308213fd..00000000 --- a/reflexio/integrations/openclaw/types/openclaw.d.ts +++ /dev/null @@ -1,230 +0,0 @@ -// Narrow ambient shim for the Openclaw Plugin SDK surface that this plugin -// actually touches. The real types ship with the `openclaw` npm package, but -// we avoid depending on the full host at build time — plugins load at runtime -// via the host's own jiti instance. -// -// If you need richer type coverage, install `openclaw` as a devDependency and -// delete this file; the real .d.ts files will take over. -declare module "openclaw/plugin-sdk/plugin-entry" { - export type PluginHookName = - | "before_model_resolve" - | "before_prompt_build" - | "before_agent_start" - | "before_agent_reply" - | "llm_input" - | "llm_output" - | "agent_end" - | "before_compaction" - | "after_compaction" - | "before_reset" - | "inbound_claim" - | "message_received" - | "message_sending" - | "message_sent" - | "before_tool_call" - | "after_tool_call" - | "tool_result_persist" - | "before_message_write" - | "session_start" - | "session_end" - | "subagent_spawning" - | "subagent_delivery_target" - | "subagent_spawned" - | "subagent_ended" - | "gateway_start" - | "gateway_stop" - | "before_dispatch" - | "reply_dispatch" - | "before_install"; - - export type PluginLogger = { - info?: (msg: string) => void; - warn?: (msg: string) => void; - error?: (msg: string) => void; - debug?: (msg: string) => void; - }; - - export type PluginRuntime = { - subagent?: { - run: (params: { - sessionKey: string; - message: string; - provider?: string; - model?: string; - extraSystemPrompt?: string; - lane?: string; - deliver?: boolean; - idempotencyKey?: string; - }) => Promise<{ runId: string }>; - }; - system: { - runCommandWithTimeout: ( - argv: string[], - opts: { timeoutMs: number; input?: string }, - ) => Promise<{ stdout: string; stderr: string; code: number | null }>; - }; - config: { - loadConfig: () => unknown; - }; - }; - - export type PluginHookAgentContext = { - runId?: string; - agentId?: string; - sessionKey?: string; - sessionId?: string; - workspaceDir?: string; - }; - - export type PluginHookSessionContext = { - agentId?: string; - sessionId: string; - sessionKey?: string; - workspaceDir?: string; - }; - - export type PluginHookBeforeAgentStartEvent = { - prompt: string; - messages?: unknown[]; - }; - - export type PluginHookBeforeAgentStartResult = { - prependSystemContext?: string; - systemPrompt?: string; - prependContext?: string; - appendSystemContext?: string; - modelOverride?: string; - providerOverride?: string; - }; - - export type PluginHookBeforeCompactionEvent = { - messageCount: number; - compactingCount?: number; - tokenCount?: number; - messages?: unknown[]; - sessionFile?: string; - }; - - export type PluginHookBeforeResetEvent = { - sessionFile?: string; - messages?: unknown[]; - reason?: string; - }; - - export type PluginHookSessionEndEvent = { - sessionId: string; - sessionKey?: string; - messageCount: number; - durationMs?: number; - reason?: string; - sessionFile?: string; - }; - - export type OpenClawPluginApi = { - id: string; - name: string; - runtime: PluginRuntime; - logger: PluginLogger; - pluginConfig?: Record; - registerTool: (tool: { - name: string; - description: string; - parameters: Record; - optional?: boolean; - execute: (id: string, params: Record) => Promise<{ - content: { type: string; text: string }[]; - }>; - }) => void; - on: { - ( - hookName: "before_agent_start", - handler: ( - event: PluginHookBeforeAgentStartEvent, - ctx: PluginHookAgentContext, - ) => - | Promise - | PluginHookBeforeAgentStartResult - | void, - opts?: { priority?: number }, - ): void; - ( - hookName: "before_compaction", - handler: ( - event: PluginHookBeforeCompactionEvent, - ctx: PluginHookAgentContext, - ) => Promise | void, - opts?: { priority?: number }, - ): void; - ( - hookName: "before_reset", - handler: ( - event: PluginHookBeforeResetEvent, - ctx: PluginHookAgentContext, - ) => Promise | void, - opts?: { priority?: number }, - ): void; - ( - hookName: "session_end", - handler: ( - event: PluginHookSessionEndEvent, - ctx: PluginHookSessionContext, - ) => Promise | void, - opts?: { priority?: number }, - ): void; - ( - hookName: PluginHookName, - handler: (event: unknown, ctx: unknown) => unknown, - opts?: { priority?: number }, - ): void; - }; - }; - - export type OpenClawPluginDefinition = { - id: string; - name: string; - description?: string; - version?: string; - register: (api: OpenClawPluginApi) => void | Promise; - }; - - export function definePluginEntry( - def: OpenClawPluginDefinition, - ): OpenClawPluginDefinition; -} - -declare module "openclaw/plugin-sdk/agent-runtime" { - export interface PreparedModel { - model: unknown; - auth: unknown; - } - - export interface PreparedModelError { - error: string; - } - - export function prepareSimpleCompletionModelForAgent(opts: { - cfg: unknown; - agentId: string; - modelRef?: string; - }): Promise; - - export interface CompletionMessage { - role: string; - content: { type: string; text?: string }[]; - timestamp: number; - } - - export interface CompletionResult { - content: { type: string; text?: string }[]; - } - - export function completeWithPreparedSimpleCompletionModel(opts: { - model: unknown; - auth: unknown; - context: { - systemPrompt?: string; - messages: CompletionMessage[]; - }; - options?: { maxTokens?: number }; - }): Promise; -} diff --git a/reflexio/integrations/openclaw/vitest.config.ts b/reflexio/integrations/openclaw/vitest.config.ts deleted file mode 100644 index 07e7f1ac..00000000 --- a/reflexio/integrations/openclaw/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["tests/**/*.test.ts"], - env: { - // Point HOME to a nonexistent path so tests cannot accidentally read - // ~/.reflexio/.env or other real user config files. Tests that need - // specific home-dir content must set HOME themselves. - HOME: "/nonexistent-test-home", - }, - }, -}); From 3976c1198471d5ba1ea81e344366cb04f260ece8 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:17:22 +0000 Subject: [PATCH 02/53] feat(openclaw): scaffold openclaw-smart plugin directory tree --- .../integrations/openclaw/plugin/src/openclaw_smart/__init__.py | 0 .../openclaw/plugin/src/openclaw_smart/events/__init__.py | 0 reflexio/integrations/openclaw/plugin/tests/__init__.py | 0 .../integrations/openclaw/plugin/tests/integration/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/__init__.py create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/__init__.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/integration/__init__.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/__init__.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reflexio/integrations/openclaw/plugin/tests/__init__.py b/reflexio/integrations/openclaw/plugin/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py b/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b From 7646a83fc8d869854bbbb977e2762005ace112e7 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:18:48 +0000 Subject: [PATCH 03/53] feat(openclaw): add TS deps (package.json, tsconfig.json, types shim, .gitignore) --- .../integrations/openclaw/plugin/.gitignore | 8 + .../integrations/openclaw/plugin/package.json | 24 ++ .../openclaw/plugin/tsconfig.json | 16 ++ .../openclaw/plugin/types/openclaw.d.ts | 230 ++++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/.gitignore create mode 100644 reflexio/integrations/openclaw/plugin/package.json create mode 100644 reflexio/integrations/openclaw/plugin/tsconfig.json create mode 100644 reflexio/integrations/openclaw/plugin/types/openclaw.d.ts diff --git a/reflexio/integrations/openclaw/plugin/.gitignore b/reflexio/integrations/openclaw/plugin/.gitignore new file mode 100644 index 00000000..2a7ee983 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.tsbuildinfo +__pycache__/ +*.pyc +.uv-cache/ +.venv/ +package-lock.json diff --git a/reflexio/integrations/openclaw/plugin/package.json b/reflexio/integrations/openclaw/plugin/package.json new file mode 100644 index 00000000..a4c84164 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/package.json @@ -0,0 +1,24 @@ +{ + "name": "openclaw-smart", + "version": "0.1.0", + "description": "Self-improving openClaw plugin — learns from corrections across sessions via reflexio.", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "openclaw": { + "extensions": ["./index.ts"], + "compat": { + "pluginApi": ">=2026.3.24", + "minGatewayVersion": "2026.3.24" + } + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "vitest": "^3.0.0" + } +} diff --git a/reflexio/integrations/openclaw/plugin/tsconfig.json b/reflexio/integrations/openclaw/plugin/tsconfig.json new file mode 100644 index 00000000..4435eb0c --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["index.ts", "tests-ts/**/*.ts", "types/**/*.d.ts"], + "exclude": ["node_modules", "src", "tests", "scripts"] +} diff --git a/reflexio/integrations/openclaw/plugin/types/openclaw.d.ts b/reflexio/integrations/openclaw/plugin/types/openclaw.d.ts new file mode 100644 index 00000000..308213fd --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/types/openclaw.d.ts @@ -0,0 +1,230 @@ +// Narrow ambient shim for the Openclaw Plugin SDK surface that this plugin +// actually touches. The real types ship with the `openclaw` npm package, but +// we avoid depending on the full host at build time — plugins load at runtime +// via the host's own jiti instance. +// +// If you need richer type coverage, install `openclaw` as a devDependency and +// delete this file; the real .d.ts files will take over. +declare module "openclaw/plugin-sdk/plugin-entry" { + export type PluginHookName = + | "before_model_resolve" + | "before_prompt_build" + | "before_agent_start" + | "before_agent_reply" + | "llm_input" + | "llm_output" + | "agent_end" + | "before_compaction" + | "after_compaction" + | "before_reset" + | "inbound_claim" + | "message_received" + | "message_sending" + | "message_sent" + | "before_tool_call" + | "after_tool_call" + | "tool_result_persist" + | "before_message_write" + | "session_start" + | "session_end" + | "subagent_spawning" + | "subagent_delivery_target" + | "subagent_spawned" + | "subagent_ended" + | "gateway_start" + | "gateway_stop" + | "before_dispatch" + | "reply_dispatch" + | "before_install"; + + export type PluginLogger = { + info?: (msg: string) => void; + warn?: (msg: string) => void; + error?: (msg: string) => void; + debug?: (msg: string) => void; + }; + + export type PluginRuntime = { + subagent?: { + run: (params: { + sessionKey: string; + message: string; + provider?: string; + model?: string; + extraSystemPrompt?: string; + lane?: string; + deliver?: boolean; + idempotencyKey?: string; + }) => Promise<{ runId: string }>; + }; + system: { + runCommandWithTimeout: ( + argv: string[], + opts: { timeoutMs: number; input?: string }, + ) => Promise<{ stdout: string; stderr: string; code: number | null }>; + }; + config: { + loadConfig: () => unknown; + }; + }; + + export type PluginHookAgentContext = { + runId?: string; + agentId?: string; + sessionKey?: string; + sessionId?: string; + workspaceDir?: string; + }; + + export type PluginHookSessionContext = { + agentId?: string; + sessionId: string; + sessionKey?: string; + workspaceDir?: string; + }; + + export type PluginHookBeforeAgentStartEvent = { + prompt: string; + messages?: unknown[]; + }; + + export type PluginHookBeforeAgentStartResult = { + prependSystemContext?: string; + systemPrompt?: string; + prependContext?: string; + appendSystemContext?: string; + modelOverride?: string; + providerOverride?: string; + }; + + export type PluginHookBeforeCompactionEvent = { + messageCount: number; + compactingCount?: number; + tokenCount?: number; + messages?: unknown[]; + sessionFile?: string; + }; + + export type PluginHookBeforeResetEvent = { + sessionFile?: string; + messages?: unknown[]; + reason?: string; + }; + + export type PluginHookSessionEndEvent = { + sessionId: string; + sessionKey?: string; + messageCount: number; + durationMs?: number; + reason?: string; + sessionFile?: string; + }; + + export type OpenClawPluginApi = { + id: string; + name: string; + runtime: PluginRuntime; + logger: PluginLogger; + pluginConfig?: Record; + registerTool: (tool: { + name: string; + description: string; + parameters: Record; + optional?: boolean; + execute: (id: string, params: Record) => Promise<{ + content: { type: string; text: string }[]; + }>; + }) => void; + on: { + ( + hookName: "before_agent_start", + handler: ( + event: PluginHookBeforeAgentStartEvent, + ctx: PluginHookAgentContext, + ) => + | Promise + | PluginHookBeforeAgentStartResult + | void, + opts?: { priority?: number }, + ): void; + ( + hookName: "before_compaction", + handler: ( + event: PluginHookBeforeCompactionEvent, + ctx: PluginHookAgentContext, + ) => Promise | void, + opts?: { priority?: number }, + ): void; + ( + hookName: "before_reset", + handler: ( + event: PluginHookBeforeResetEvent, + ctx: PluginHookAgentContext, + ) => Promise | void, + opts?: { priority?: number }, + ): void; + ( + hookName: "session_end", + handler: ( + event: PluginHookSessionEndEvent, + ctx: PluginHookSessionContext, + ) => Promise | void, + opts?: { priority?: number }, + ): void; + ( + hookName: PluginHookName, + handler: (event: unknown, ctx: unknown) => unknown, + opts?: { priority?: number }, + ): void; + }; + }; + + export type OpenClawPluginDefinition = { + id: string; + name: string; + description?: string; + version?: string; + register: (api: OpenClawPluginApi) => void | Promise; + }; + + export function definePluginEntry( + def: OpenClawPluginDefinition, + ): OpenClawPluginDefinition; +} + +declare module "openclaw/plugin-sdk/agent-runtime" { + export interface PreparedModel { + model: unknown; + auth: unknown; + } + + export interface PreparedModelError { + error: string; + } + + export function prepareSimpleCompletionModelForAgent(opts: { + cfg: unknown; + agentId: string; + modelRef?: string; + }): Promise; + + export interface CompletionMessage { + role: string; + content: { type: string; text?: string }[]; + timestamp: number; + } + + export interface CompletionResult { + content: { type: string; text?: string }[]; + } + + export function completeWithPreparedSimpleCompletionModel(opts: { + model: unknown; + auth: unknown; + context: { + systemPrompt?: string; + messages: CompletionMessage[]; + }; + options?: { maxTokens?: number }; + }): Promise; +} From e9296581714cb3bb87b21e7a3804c3a45f53d547 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:20:05 +0000 Subject: [PATCH 04/53] feat(openclaw): add Python deps via uv (pyproject.toml, uv.lock, README stub) --- .../integrations/openclaw/plugin/README.md | 5 + .../openclaw/plugin/pyproject.toml | 37 + reflexio/integrations/openclaw/plugin/uv.lock | 3835 +++++++++++++++++ 3 files changed, 3877 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/README.md create mode 100644 reflexio/integrations/openclaw/plugin/pyproject.toml create mode 100644 reflexio/integrations/openclaw/plugin/uv.lock diff --git a/reflexio/integrations/openclaw/plugin/README.md b/reflexio/integrations/openclaw/plugin/README.md new file mode 100644 index 00000000..3aea5fca --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/README.md @@ -0,0 +1,5 @@ +# openclaw-smart + +openClaw plugin: cross-session memory via local reflexio backend. + +See ../README.md for usage. See `docs/superpowers/specs/2026-05-19-openclaw-smart-design.md` in the parent repo for design. diff --git a/reflexio/integrations/openclaw/plugin/pyproject.toml b/reflexio/integrations/openclaw/plugin/pyproject.toml new file mode 100644 index 00000000..e6873041 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "openclaw-smart" +version = "0.1.0" +description = "openClaw plugin: cross-session memory via local reflexio backend" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "reflexio-ai", + # Used by reflexio's local embedding provider (ONNXMiniLM_L6_V2). + "chromadb>=0.5", + "einops>=0.8.0", +] + +[project.scripts] +openclaw-smart-hook = "openclaw_smart.hook:main" +openclaw-smart = "openclaw_smart.cli:main" + +[tool.uv.sources] +# This plugin lives inside the reflexio repo; depend on the local reflexio +# package directly, not the PyPI release. Path is from plugin/ → up 4 levels +# to the open_source/reflexio submodule root. +reflexio-ai = { path = "../../../..", editable = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/openclaw_smart"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[dependency-groups] +dev = [ + "pytest>=9.0.3", +] diff --git a/reflexio/integrations/openclaw/plugin/uv.lock b/reflexio/integrations/openclaw/plugin/uv.lock new file mode 100644 index 00000000..1d83f145 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/uv.lock @@ -0,0 +1,3835 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.102.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/47/cb2a71f70431fb09af4db83e3ea89eb2dd8e0e348d27af53ed32e6c599dd/anthropic-0.102.0.tar.gz", hash = "sha256:96f747cad11886c4ae12d4080131b94eebd68b202bd2190fe27959031bb1fa9c", size = 763697, upload-time = "2026-05-13T18:12:41.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/75/0f6c603594876413bc858a00e7cc0d80a0cc14edf5c7b959a3ea6ec45e44/anthropic-0.102.0-py3-none-any.whl", hash = "sha256:ab96540bbd4b0f36564252d955a86f8abbe4f00944a24bc9931acc9b139bab6f", size = 763070, upload-time = "2026-05-13T18:12:43.474Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { 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" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, +] + +[[package]] +name = "braintrust" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chevron" }, + { name = "exceptiongroup" }, + { name = "gitpython" }, + { name = "jsonschema" }, + { name = "packaging" }, + { name = "python-slugify" }, + { name = "requests" }, + { name = "sseclient-py" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/be/c2c154fc70f6ba0ccc5d998e227f0c4ba72b8f539669592ab368f68009e2/braintrust-0.21.0.tar.gz", hash = "sha256:e0f272313cd9c5d977199ccd1faedd37ff79103f00d75d26238ffaa772439b1a", size = 615368, upload-time = "2026-05-14T17:19:43.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d6/26f2d9ebd9c495212c1c3791ef87d0e4227151ef88e3620bc7b9ae28fce8/braintrust-0.21.0-py3-none-any.whl", hash = "sha256:9885d29ce356fb0176b51423c93c7a9b0bacaf80b19f7c7b7485d7cb4c7b34ca", size = 718817, upload-time = "2026-05-14T17:19:41.443Z" }, +] + +[[package]] +name = "build" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, +] + +[[package]] +name = "cachetools" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/c1/67cfb86aa21144796ff51068326d467fbef8ee42f8d08a3a8a926106cf0c/cachetools-7.1.3.tar.gz", hash = "sha256:135cfe944bc3c1e805505f65dae0bef375a2f96261171ab66c79ef77d0bda39d", size = 45780, upload-time = "2026-05-18T18:21:03.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/52/8ff5c1a3b2e821ced9b2998fba3ee29aa4525c0bf51e5ee55dd6f61a4ed5/cachetools-7.1.3-py3-none-any.whl", hash = "sha256:9876787e2346e20584d5cca236cb5d49d04e7193de91646f230725b2e1e8b804", size = 16763, upload-time = "2026-05-18T18:21:02.386Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "chevron" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/1f/ca74b65b19798895d63a6e92874162f44233467c9e7c1ed8afd19016ebe9/chevron-0.14.0.tar.gz", hash = "sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf", size = 11440, upload-time = "2021-01-02T22:47:59.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/93/342cc62a70ab727e093ed98e02a725d85b746345f05d2b5e5034649f4ec8/chevron-0.14.0-py3-none-any.whl", hash = "sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443", size = 11595, upload-time = "2021-01-02T22:47:57.847Z" }, +] + +[[package]] +name = "chromadb" +version = "1.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "jsonschema" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/d1/5e33b26985f0c7046a0be1cee2158ada1748ee700d2545057fde1468d74d/chromadb-1.5.9.tar.gz", hash = "sha256:5c20e62a455c28bacac927f26116a73fd8e1799e0d908be8e8a4f02197a54731", size = 2595635, upload-time = "2026-05-05T05:54:51.713Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/5b/3cced915244f43ed14b53fe9f63a37f05f865064f4e4fe7d9448d3f2a352/chromadb-1.5.9-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60701011b5e6409647fa40d12c7c5a66b2b0bfcf33a52db2ad53a30a2abc4957", size = 22564540, upload-time = "2026-05-05T05:54:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/34/4c/adcef1f4e82a2ef69ccd3711d55fc289193d54c4c0ff7a0292a3631db46f/chromadb-1.5.9-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:814b9c95617377f6501e5757d63dfddb554a283a7739c87b9fa573850174e6f3", size = 21699698, upload-time = "2026-05-05T05:54:45.078Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/937bc4d2e6f8ab9664ec79931fbbd69efff47e513ec2924b071e4b0ff774/chromadb-1.5.9-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9192d111bd662241625867962333d99369a00769a50f8b2f58cb388731274d7e", size = 22680924, upload-time = "2026-05-05T05:54:36.25Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ec/0c42039e80b9acc534f67b73b7a42471948042859b3a64867b50a4a77fa3/chromadb-1.5.9-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc09b3df76e5a5cb386aed2715a2eea152e3949f9e1ba93c7119505377749929", size = 23316203, upload-time = "2026-05-05T05:54:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ce/0f7be6e5d0feafa2cda54b12e6542afeea7dea89d2d411e14da90f8abb96/chromadb-1.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:4fd0b560e56761b7f3cb4d5c6205fd5f20814484b4a3e4e9af9038c2b428fc6c", size = 23542454, upload-time = "2026-05-05T05:54:54.942Z" }, +] + +[[package]] +name = "click" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "duckduckgo-search" +version = "8.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "lxml" }, + { name = "primp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/ef/07791a05751e6cc9de1dd49fb12730259ee109b18e6d097e25e6c32d5617/duckduckgo_search-8.1.1.tar.gz", hash = "sha256:9da91c9eb26a17e016ea1da26235d40404b46b0565ea86d75a9f78cc9441f935", size = 22868, upload-time = "2025-07-06T15:30:59.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/72/c027b3b488b1010cf71670032fcf7e681d44b81829d484bb04e31a949a8d/duckduckgo_search-8.1.1-py3-none-any.whl", hash = "sha256:f48adbb06626ee05918f7e0cef3a45639e9939805c4fc179e68c48a12f1b5062", size = 18932, upload-time = "2025-07-06T15:30:58.339Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + +[[package]] +name = "ecdsa" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/ca/8de7744cb3bc966c85430ca2d0fcaeea872507c6a4cf6e007f7fe269ed9d/ecdsa-0.19.2.tar.gz", hash = "sha256:62635b0ac1ca2e027f82122b5b81cb706edc38cd91c63dda28e4f3455a2bf930", size = 202432, upload-time = "2026-03-26T09:58:17.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[[package]] +name = "gepa" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/62/10f5a8f24c075e3b64f952be73ba8e15f0055584bbcdf9ce48d754a36679/gepa-0.1.1.tar.gz", hash = "sha256:643fda01c23de4c9f01306e01305dd69facc29bcb34ad59e4cd07e6621d34aa1", size = 272251, upload-time = "2026-03-16T10:17:53.131Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/b7/8c72dedbb950d88a6f64588fcbc590d2a21e2b9f19b36aa6c5016c54ec75/gepa-0.1.1-py3-none-any.whl", hash = "sha256:71ead7c591eafcc727b83509cdc4182f20264800a6ddf8520d61419daeb47466", size = 244246, upload-time = "2026-03-16T10:17:51.922Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, +] + +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hdbscan" +version = "0.8.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/6c/152c4c7831563488d360b2da03671631bb95431a19297a92178f65514cd5/hdbscan-0.8.43.tar.gz", hash = "sha256:cfe6e7e68f153c92cef3d19a86d29c1075f33774b1e97d284ba79b157506c8bb", size = 7089058, upload-time = "2026-05-13T03:09:18.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/33/56167699cec17079831ed7ae6233d38b3540c410048453ea0767bc06f29e/hdbscan-0.8.43-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9084f82fcf0e66915141d82564f6cfaf8575fcdf500872678845bc98eac2dfc7", size = 2569190, upload-time = "2026-05-13T03:08:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/61/79/95d995a6100f74f439804c66534f9baaf888e62d7c957fbaf5ca67329195/hdbscan-0.8.43-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35d4b564dcaf0093c1df9607d72e07bc5bc80d033fc57d6c971f80cf59f750d3", size = 5808558, upload-time = "2026-05-13T03:09:32.562Z" }, + { url = "https://files.pythonhosted.org/packages/c5/85/927ba7f0a6e5cc71c1b6a818e63332dfee0c4c807f0aa57d2d3f6da9789c/hdbscan-0.8.43-cp312-cp312-win_amd64.whl", hash = "sha256:7ffb9e1dc706246d38f4ecf6a15d19160b5d5e06ec7a5e37d3c09b7415c376ee", size = 1937311, upload-time = "2026-05-13T03:08:58.868Z" }, + { url = "https://files.pythonhosted.org/packages/38/f1/125d300380cde6761d84808670fa73153dc41c7ef742c90bcf838fce4a39/hdbscan-0.8.43-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:97a8f43c0081c5b15094234b5b854f92bdea2b93b8152c38e2f5ec69203177ab", size = 2559588, upload-time = "2026-05-13T03:08:30.054Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/65a1f90f85f64888a55ad23353eed7f9fb41a26d84ea5774d5df8e6bad3c/hdbscan-0.8.43-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3831c2c799360be302ddc011b9385ab1c836bcd3a4bea10cd91b7e4c0335d32", size = 5793238, upload-time = "2026-05-13T03:09:30.225Z" }, + { url = "https://files.pythonhosted.org/packages/68/7a/13c7a3bb1c057c91ba68c19e3f4f87d59fde520f83dbdade88584f435cf1/hdbscan-0.8.43-cp313-cp313-win_amd64.whl", hash = "sha256:cbdf0d252c323fd76bc2fb656ed71ca5ed184494b9606d6dfb6f25fd7a2abbab", size = 1937269, upload-time = "2026-05-13T03:08:52.827Z" }, + { url = "https://files.pythonhosted.org/packages/38/61/e72e1ca6f2c06bd463600c372cfeccbbda270307fed1df4d9f4c46b16f83/hdbscan-0.8.43-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:484f49639239bf57c58e7290da52ce6ba0ee87bf1212462523c4d6c1e631e104", size = 2582755, upload-time = "2026-05-13T03:08:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/1e/57/ef6360710177243916be0aa611fa495d47f9e320a33f8d43c32bc0fef738/hdbscan-0.8.43-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ef9de8ddffe492c42e6bacbef0f88c9828f4408ff29572d1fa5a9f30eb7929a", size = 5766844, upload-time = "2026-05-13T03:09:35.75Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d2/366e699e1fcb77a80bfa779775ae9c6f80638bdda7bbbd2c29f3add28bcd/hdbscan-0.8.43-cp314-cp314-win_amd64.whl", hash = "sha256:a81473c09bd68aeecc7fdab3afb8f9f49cc6066843f5629bfe176271a2db3607", size = 1956593, upload-time = "2026-05-13T03:08:48.932Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "json-repair" +version = "0.59.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/7c/e95bb03068572146eba37e8175c760f470ea0a6097310e16bbf2bc6e6457/json_repair-0.59.10.tar.gz", hash = "sha256:2e4b85537c752d8a513ea28fdad891e5ede32c83de745366b97f648b8c34ede7", size = 49133, upload-time = "2026-05-14T06:41:51.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/87/49b20c6b81493d55c311f711ed87319d0fbad8bd0bbfbe36e52103af36bd/json_repair-0.59.10-py3-none-any.whl", hash = "sha256:5468fa3eaadcc9b4a5646776bc4176e2fe5f374b5848a15f468cce3b60e3db0e", size = 47742, upload-time = "2026-05-14T06:41:49.812Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { 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" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { 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" }, +] + +[[package]] +name = "kubernetes" +version = "35.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, +] + +[[package]] +name = "limits" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/69/826a5d1f45426c68d8f6539f8d275c0e4fcaa57f0c017ec3100986558a41/limits-5.8.0.tar.gz", hash = "sha256:c9e0d74aed837e8f6f50d1fcebcf5fd8130957287206bc3799adaee5092655da", size = 226104, upload-time = "2026-02-05T07:17:35.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/98/cb5ca20618d205a09d5bec7591fbc4130369c7e6308d9a676a28ff3ab22c/limits-5.8.0-py3-none-any.whl", hash = "sha256:ae1b008a43eb43073c3c579398bd4eb4c795de60952532dc24720ab45e1ac6b8", size = 60954, upload-time = "2026-02-05T07:17:34.425Z" }, +] + +[[package]] +name = "litellm" +version = "1.85.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/d5/3c9b560db2ffa9e498655d0dfd74f408bc5b32ede858b5731c2a5fa4c752/litellm-1.85.0.tar.gz", hash = "sha256:babdd569809af913d08a08a7eb55df1ed3e6a3960ee365c6cef4ad031c9bc72a", size = 15344387, upload-time = "2026-05-17T01:59:15.97Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/38/e6a4abb062e039d18d59538cc4e6fc370c2c10cd2bff4a2e546acb69dcb9/litellm-1.85.0-py3-none-any.whl", hash = "sha256:2bb449153610691faffd76f5b94a8c29e4b66fc5394156ebf54fd4fe92759b1a", size = 16978229, upload-time = "2026-05-17T01:59:11.902Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mmh3" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, + { url = "https://files.pythonhosted.org/packages/63/b4/65bc1fb2bb7f83e91c30865023b1847cf89a5f237165575e8c83aa536584/mmh3-5.2.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d771f085fcdf4035786adfb1d8db026df1eb4b41dac1c3d070d1e49512843227", size = 40794, upload-time = "2026-03-05T15:55:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/c4/86/7168b3d83be8eb553897b1fac9da8bbb06568e5cfe555ffc329ebb46f59d/mmh3-5.2.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7f196cd7910d71e9d9860da0ff7a77f64d22c1ad931f1dd18559a06e03109fc0", size = 41923, upload-time = "2026-03-05T15:55:10.924Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/b653ab611c9060ce8ff0ba25c0226757755725e789292f3ca138a58082cd/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b1f12bd684887a0a5d55e6363ca87056f361e45451105012d329b86ec19dbe0b", size = 39131, upload-time = "2026-03-05T15:55:11.961Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5a2e0d34ab4d33543f01121e832395ea510132ea8e52cdf63926d9d81754/mmh3-5.2.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d106493a60dcb4aef35a0fac85105e150a11cf8bc2b0d388f5a33272d756c966", size = 39825, upload-time = "2026-03-05T15:55:13.013Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/81699a8f39a3f8d368bec6443435c0c392df0d200ad915bf0d222b588e03/mmh3-5.2.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:44983e45310ee5b9f73397350251cdf6e63a466406a105f1d16cb5baa659270b", size = 40344, upload-time = "2026-03-05T15:55:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/71c8c775807606e8fd8acc5c69016e1caf3200d50b50b6dd4b40ce10b76c/mmh3-5.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:368625fb01666655985391dbad3860dc0ba7c0d6b9125819f3121ee7292b4ac8", size = 56291, upload-time = "2026-03-05T15:55:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/2c24517d4b2ce9e4917362d24f274d3d541346af764430249ddcc4cb3a08/mmh3-5.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:72d1cc63bcc91e14933f77d51b3df899d6a07d184ec515ea7f56bff659e124d7", size = 40575, upload-time = "2026-03-05T15:55:16.518Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/e4a360164365ac9f07a25f0f7928e3a66eb9ecc989384060747aa170e6aa/mmh3-5.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e8b4b5580280b9265af3e0409974fb79c64cf7523632d03fbf11df18f8b0181e", size = 40052, upload-time = "2026-03-05T15:55:17.735Z" }, + { url = "https://files.pythonhosted.org/packages/97/ca/120d92223a7546131bbbc31c9174168ee7a73b1366f5463ffe69d9e691fe/mmh3-5.2.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4cbbde66f1183db040daede83dd86c06d663c5bb2af6de1142b7c8c37923dd74", size = 97311, upload-time = "2026-03-05T15:55:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/b6/71/c1a60c1652b8813ef9de6d289784847355417ee0f2980bca002fe87f4ae5/mmh3-5.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ff038d52ef6aa0f309feeba00c5095c9118d0abf787e8e8454d6048db2037fc", size = 103279, upload-time = "2026-03-05T15:55:20.448Z" }, + { url = "https://files.pythonhosted.org/packages/48/29/ad97f4be1509cdcb28ae32c15593ce7c415db47ace37f8fad35b493faa9a/mmh3-5.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4130d0b9ce5fad6af07421b1aecc7e079519f70d6c05729ab871794eded8617", size = 106290, upload-time = "2026-03-05T15:55:21.6Z" }, + { url = "https://files.pythonhosted.org/packages/77/29/1f86d22e281bd8827ba373600a4a8b0c0eae5ca6aa55b9a8c26d2a34decc/mmh3-5.2.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e0bfe77d238308839699944164b96a2eeccaf55f2af400f54dc20669d8d5f2", size = 113116, upload-time = "2026-03-05T15:55:22.826Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7c/339971ea7ed4c12d98f421f13db3ea576a9114082ccb59d2d1a0f00ccac1/mmh3-5.2.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f963eafc0a77a6c0562397da004f5876a9bcf7265a7bcc3205e29636bc4a1312", size = 120740, upload-time = "2026-03-05T15:55:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/3c7c4bdb8e926bb3c972d1e2907d77960c1c4b250b41e8366cf20c6e4373/mmh3-5.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92883836caf50d5255be03d988d75bc93e3f86ba247b7ca137347c323f731deb", size = 99143, upload-time = "2026-03-05T15:55:25.456Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/33dd8706e732458c8375eae63c981292de07a406bad4ec03e5269654aa2c/mmh3-5.2.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57b52603e89355ff318025dd55158f6e71396c0f1f609d548e9ea9c94cc6ce0a", size = 98703, upload-time = "2026-03-05T15:55:26.723Z" }, + { url = "https://files.pythonhosted.org/packages/51/04/76bbce05df76cbc3d396f13b2ea5b1578ef02b6a5187e132c6c33f99d596/mmh3-5.2.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f40a95186a72fa0b67d15fef0f157bfcda00b4f59c8a07cbe5530d41ac35d105", size = 106484, upload-time = "2026-03-05T15:55:28.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8f/c6e204a2c70b719c1f62ffd9da27aef2dddcba875ea9c31ca0e87b975a46/mmh3-5.2.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:58370d05d033ee97224c81263af123dea3d931025030fd34b61227a768a8858a", size = 110012, upload-time = "2026-03-05T15:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/7181efd8e39db386c1ebc3e6b7d1f702a09d7c1197a6f2742ed6b5c16597/mmh3-5.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7be6dfb49e48fd0a7d91ff758a2b51336f1cd21f9d44b20f6801f072bd080cdd", size = 97508, upload-time = "2026-03-05T15:55:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/afa7ca2615fd85e1469474bb860e381443d0b868c083b62b41cb1d7ca32f/mmh3-5.2.1-cp314-cp314-win32.whl", hash = "sha256:54fe8518abe06a4c3852754bfd498b30cc58e667f376c513eac89a244ce781a4", size = 41387, upload-time = "2026-03-05T15:55:32.403Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/46d42a260ee1357db3d486e6c7a692e303c017968e14865e00efa10d09fc/mmh3-5.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:3f796b535008708846044c43302719c6956f39ca2d93f2edda5319e79a29efbb", size = 42101, upload-time = "2026-03-05T15:55:33.646Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7b/848a8378059d96501a41159fca90d6a99e89736b0afbe8e8edffeac8c74b/mmh3-5.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:cd471ede0d802dd936b6fab28188302b2d497f68436025857ca72cd3810423fe", size = 39836, upload-time = "2026-03-05T15:55:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/27/61/1dabea76c011ba8547c25d30c91c0ec22544487a8750997a27a0c9e1180b/mmh3-5.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5174a697ce042fa77c407e05efe41e03aa56dae9ec67388055820fb48cf4c3ba", size = 57727, upload-time = "2026-03-05T15:55:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/731185950d1cf2d5e28979cc8593016ba1619a295faba10dda664a4931b5/mmh3-5.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0a3984146e414684a6be2862d84fcb1035f4984851cb81b26d933bab6119bf00", size = 41308, upload-time = "2026-03-05T15:55:37.254Z" }, + { url = "https://files.pythonhosted.org/packages/76/aa/66c76801c24b8c9418b4edde9b5e57c75e72c94e29c48f707e3962534f18/mmh3-5.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bd6e7d363aa93bd3421b30b6af97064daf47bc96005bddba67c5ffbc6df426b8", size = 40758, upload-time = "2026-03-05T15:55:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/79a1f638a02f0ae389f706d13891e2fbf7d8c0a22ecde67ba828951bb60a/mmh3-5.2.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:113f78e7463a36dbbcea05bfe688efd7fa759d0f0c56e73c974d60dcfec3dfcc", size = 109670, upload-time = "2026-03-05T15:55:40.13Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/8cd0e187a288985bcfc79bf5144d1d712df9dee74365f59d26e3a1865be6/mmh3-5.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e8ec5f606e0809426d2440e0683509fb605a8820a21ebd120dcdba61b74ef7f", size = 117399, upload-time = "2026-03-05T15:55:42.076Z" }, + { url = "https://files.pythonhosted.org/packages/42/94/dfea6059bd5c5beda565f58a4096e43f4858fb6d2862806b8bbd12cbb284/mmh3-5.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22b0f9971ec4e07e8223f2beebe96a6cfc779d940b6f27d26604040dd74d3a44", size = 120386, upload-time = "2026-03-05T15:55:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/47/cb/f9c45e62aaa67220179f487772461d891bb582bb2f9783c944832c60efd9/mmh3-5.2.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85ffc9920ffc39c5eee1e3ac9100c913a0973996fbad5111f939bbda49204bb7", size = 125924, upload-time = "2026-03-05T15:55:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/fe54a4a7c11bc9f623dfc1707decd034245602b076dfc1dcc771a4163170/mmh3-5.2.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7aec798c2b01aaa65a55f1124f3405804184373abb318a3091325aece235f67c", size = 135280, upload-time = "2026-03-05T15:55:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/97/67/fe7e9e9c143daddd210cd22aef89cbc425d58ecf238d2b7d9eb0da974105/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55dbbd8ffbc40d1697d5e2d0375b08599dae8746b0b08dea05eee4ce81648fac", size = 110050, upload-time = "2026-03-05T15:55:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/6d4b09fcbef80794de447c9378e39eefc047156b290fa3dd2d5257ca8227/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c85c38a279ca9295a69b9b088a2e48aa49737bb1b34e6a9dc6297c110e8d912", size = 111158, upload-time = "2026-03-05T15:55:48.239Z" }, + { url = "https://files.pythonhosted.org/packages/81/a6/ca51c864bdb30524beb055a6d8826db3906af0834ec8c41d097a6e8573d5/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6290289fa5fb4c70fd7f72016e03633d60388185483ff3b162912c81205ae2cf", size = 116890, upload-time = "2026-03-05T15:55:49.405Z" }, + { url = "https://files.pythonhosted.org/packages/cc/04/5a1fe2e2ad843d03e89af25238cbc4f6840a8bb6c4329a98ab694c71deda/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4fc6cd65dc4d2fdb2625e288939a3566e36127a84811a4913f02f3d5931da52d", size = 123121, upload-time = "2026-03-05T15:55:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/3c820c6f4897afd25905270a9f2330a23f77a207ea7356f7aadace7273c0/mmh3-5.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:623f938f6a039536cc02b7582a07a080f13fdfd48f87e63201d92d7e34d09a18", size = 110187, upload-time = "2026-03-05T15:55:52.143Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/1d71cd143752361c0aebef16ad3f55926a6faf7b112d355745c1f8a25f7f/mmh3-5.2.1-cp314-cp314t-win32.whl", hash = "sha256:29bc3973676ae334412efdd367fcd11d036b7be3efc1ce2407ef8676dabfeb82", size = 41934, upload-time = "2026-03-05T15:55:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/63a2a88f31d93dea03947cccc2a076946857e799ea4f7acdecbf43b324aa/mmh3-5.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:28cfab66577000b9505a0d068c731aee7ca85cd26d4d63881fab17857e0fe1fb", size = 43036, upload-time = "2026-03-05T15:55:55.252Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/b1/d111b1df656761f980d9e298a60039a9cb66036b1d039e777537743d0ac3/onnxruntime-1.26.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05b028781b322ad74b57ce5b50aa5280bb1fe96ceec334628ade681e0b24c1ac", size = 18016624, upload-time = "2026-05-12T00:41:01.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/3f9d896a0385a36bd04345d6d0b802821a5782adde562e7e135f6bb71c73/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0", size = 16052692, upload-time = "2026-05-08T19:07:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/2a4e04f8dbeffad19bbcced4bcd4289bf478921518437404d6b92bdf213b/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6", size = 18185439, upload-time = "2026-05-08T19:07:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/44/fc/026d0a7162b9c2153dac292baea9e027c42304dc1d9dc6f8ff5b4cfbaedd/onnxruntime-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:a26374dc7fbcaae593601086b242120e13f2310558df0991da6dd8b8fac00414", size = 13026427, upload-time = "2026-05-08T19:08:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/1dcf88e45e4c69db5f7b106f2dacc3801ba98994e082ca03e1dfdf7bfe57/onnxruntime-1.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:54a8053410fd31fd66469bd754fcfe8a4df9f7eb44756b4b5479bf50c842d948", size = 12796647, upload-time = "2026-05-08T19:07:52.108Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/3e52249aa08fa301e217ecba07b5246a8338fa2b401e109326e3fc5be0f9/onnxruntime-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:61bec80655efa460591c2bc655392d57d2650ce85533a6b9b3b7a790d7ea7916", size = 13026751, upload-time = "2026-05-08T19:08:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/c1c8782b14af6797c303de132d6eef26a9fb80dfacd3750ce57911d11c6b/onnxruntime-1.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a6677545ff451e3539a02746d2f207d8c5baa4a0a818886bb9d6a6eb9511ee89", size = 12796807, upload-time = "2026-05-08T19:07:54.879Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, + { url = "https://files.pythonhosted.org/packages/40/89/17546c1c20f6bfc3ae41c22152378a26edfea918af3129e2139dcd7c99f3/onnxruntime-1.26.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:33a791f31432a3af1a96db5e54818b37aba5e5eefc2e6af5794c10a9118a9993", size = 18019724, upload-time = "2026-05-08T19:07:30.723Z" }, + { url = "https://files.pythonhosted.org/packages/bb/24/89457a35f6af29538a76647f2c18c3a28277e6c19234c847e7b4b7c19860/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e90c00732c4553618103149d93f688e8c3063017938f8983e21a71d9f3b6d22e", size = 16054821, upload-time = "2026-05-08T19:07:22.348Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/15b2e1815cf570d238e0135529f80d2dce64e8e8818a1489cae83823c5c6/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01498e80ba8988428d08c2d51b1338f89e3de2a93e6ffe555f79c68f26a5c06b", size = 18185815, upload-time = "2026-05-08T19:07:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/2e11055faf015e4b07f45b513fa49b391baf2e19d92d77d73ebee13c1004/onnxruntime-1.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:7ead61450d8405167c87dd3a31d8da1d576b490a57dab1aa8b82a7da6825f5aa", size = 13349887, upload-time = "2026-05-08T19:08:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/0f9d1a5718b1781c610c1e354765a3820597081754277a6a9a2b50705702/onnxruntime-1.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:31d71a53490e46910877d0902b5ad99c69a5955e5c7ea6c82863519410e1ba7c", size = 13140121, upload-time = "2026-05-08T19:07:57.804Z" }, + { url = "https://files.pythonhosted.org/packages/1c/42/3b8e635f067d06d9f45bede470b8d539d101a4166c272213158dfd08b6ce/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b6d258fb78fdfcf049795bcfaa74dcb90ae7baa277afd21e6fd28b83f2c496", size = 16057240, upload-time = "2026-05-08T19:07:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/f2be40a31b908d96b861ae0ce98582fa376c18a7f816b9d5eb4cd6aa0a4c/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4eefd386a45202aefb7a5132b94f32df9d506c9edcc7faf2fc60d65183f4b183", size = 18197382, upload-time = "2026-05-08T19:07:46.965Z" }, +] + +[[package]] +name = "openai" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/50/5901f01ef14e6c27788beb91e54fef5d6204fb5fb9e97402fc8a14de2e32/openai-2.37.0.tar.gz", hash = "sha256:f4bc562cc5f3a43d40d678105572d9d44765f6e0f50c125f63055419b72f4bd9", size = 754706, upload-time = "2026-05-15T22:30:35.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/4c/bce61680d0699a78a405fd9a67989b175ba020590428831aab2ab1d2be7c/openai-2.37.0-py3-none-any.whl", hash = "sha256:814633888b8f3b1ffd6615697c6e4ef93632d08b7c2e28c8c5ef3556e5a10107", size = 1303238, upload-time = "2026-05-15T22:30:32.767Z" }, +] + +[[package]] +name = "openclaw-smart" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "chromadb" }, + { name = "einops" }, + { name = "reflexio-ai" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "chromadb", specifier = ">=0.5" }, + { name = "einops", specifier = ">=0.8.0" }, + { name = "reflexio-ai", editable = "../../../../" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.3" }] + +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/9b/e4503060b8695579dbaad187dc8cef4554188de68748c88060599b77489e/opentelemetry_exporter_otlp_proto_grpc-1.41.1.tar.gz", hash = "sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b", size = 25755, upload-time = "2026-04-24T13:15:41.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c54f33c92443d087703e57e52e55f22f111373a5c4c4aa349ea60efe512e/opentelemetry_exporter_otlp_proto_grpc-1.41.1-py3-none-any.whl", hash = "sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94", size = 20297, upload-time = "2026-04-24T13:15:20.212Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "primp" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/ca/383bdf0df3dc87b60bf73c55da526ac743d42c5155a84a9014775b895e96/primp-1.2.3.tar.gz", hash = "sha256:a531b01f57cb59e3e7a3a2b526bb151b61dc7b2e15d2f6961615a553632e2889", size = 1342866, upload-time = "2026-04-20T08:34:09.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/b1/05bb7e00bd17a439aae7ed49b0c0834508e3ce50624dfa43c6dcb8b71bd0/primp-1.2.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e96d6ab40fba41039947dad0fcc42b0b56b67180883e526715720bb2d90f3bfc", size = 4360352, upload-time = "2026-04-20T08:33:52.785Z" }, + { url = "https://files.pythonhosted.org/packages/7e/db/8bb1e4b6bb715f606b53793831e7910aebeb40169e439138e9124686820a/primp-1.2.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:42f28679916ce080e643e7464786abeb659c8062c0f74bb411918c7f07e5b806", size = 4035926, upload-time = "2026-04-20T08:34:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/52b5e6d840be4d5a8f689d9ba82dd616c67e29e3e1d13baf6a4c9be3f4b3/primp-1.2.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643d47cf24962331ad2b049d6bb4329dce6b18a0914490dbf09541cb38596d39", size = 4309649, upload-time = "2026-04-20T08:34:01.369Z" }, + { url = "https://files.pythonhosted.org/packages/40/85/d98e20104429bb8393939396c2317ec6163a83d856b1fe555bf5f021e97a/primp-1.2.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:898a12b44af9aed20c10fd4b497314731e9f6dcab20f4aa64cf118f79df17fa0", size = 3910331, upload-time = "2026-04-20T08:33:37.294Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c7/53f11e2e2fe758830f6f208cef5bd3d0ecc6f538c0a2ca8e15a1fa502bd1/primp-1.2.3-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4bdf2b164c2908f7bdc845215dd21ebded1f2f43e9e9ae2d9a961ea56e5cb87", size = 4152054, upload-time = "2026-04-20T08:33:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5a/48e8f985daeeb58c7f1789bb6748d9aba250ea3541d787bcbcb1243abfd3/primp-1.2.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:036884d4c6c866c93a88e591e49f67ed160f6a9d98c779fc652cce63de62996a", size = 4443286, upload-time = "2026-04-20T08:34:07.782Z" }, + { url = "https://files.pythonhosted.org/packages/5e/81/42b32337bd8c7b7b8184a1fcd79fd51d4773427553a8d2036eb0a93a137d/primp-1.2.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc695c20f5c6e345abc3262bef3c246a571986db5cea73bdc41db6b166066c8", size = 4323757, upload-time = "2026-04-20T08:33:43.807Z" }, + { url = "https://files.pythonhosted.org/packages/37/43/9ee78c283cbca7fcd635c2a9bb5633aa6568b1925958017e1a979fffe56c/primp-1.2.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13255b0826c60681478c787fbe29cfc773caf6242390fee047dac0f23f6e8c11", size = 4525772, upload-time = "2026-04-20T08:33:45.427Z" }, + { url = "https://files.pythonhosted.org/packages/c5/41/f10164485d84145a1ea18c7966d1ee098d1790b2088b7f122570463b7d5c/primp-1.2.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9bc40f94c8e58444befaf7e78b8aadd96e94e32789dadbe4a03785db772aa4dc", size = 4471705, upload-time = "2026-04-20T08:33:54.317Z" }, + { url = "https://files.pythonhosted.org/packages/18/a2/5328d66dd8f63bacfc9ef0e1e5fde66b2bb43103108fe8f01dcb2d2f0e50/primp-1.2.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2e0e8e245113ab3c9fd4b36014b3a04869cf08f72e8e7f36c4f5ef46d26da090", size = 4148309, upload-time = "2026-04-20T08:33:31.609Z" }, + { url = "https://files.pythonhosted.org/packages/c9/86/4d000d3ee341e5f1e73d81fb2c84e985f23b343a1aa4234c40987ec6eae7/primp-1.2.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3bb50934b5e209e7da4876d3419ebc23d1425fe6f089c0cd95382d21bd8a39bd", size = 4276881, upload-time = "2026-04-20T08:33:34.414Z" }, + { url = "https://files.pythonhosted.org/packages/22/f5/50acc3e349d22044abf4ed7c2abaaba11a01b9ccb6a7d66a3fbc2275c590/primp-1.2.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b293f13078afee9d41b74c77d05a5eaeb57dfab5110648dd24bc3fb3c750a05c", size = 4775229, upload-time = "2026-04-20T08:34:10.241Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d6/2c482973211666fdf2c4365babf343a88ea30f753ef650121cebd4ad7ece/primp-1.2.3-cp310-abi3-win32.whl", hash = "sha256:c600dd83e9c978bf494aab072cd5bbfdd59b131f3afc353850c53373a86992e5", size = 3504032, upload-time = "2026-04-20T08:33:28.344Z" }, + { url = "https://files.pythonhosted.org/packages/70/89/3d2032a8f5e986fcc03bc86ac98705ce2bd35ed0e182d4949c159214da6a/primp-1.2.3-cp310-abi3-win_amd64.whl", hash = "sha256:3cbbe52a6eb51a4831d3dd35055f13b28ff5b9be2487c14ffe66922bf8028b49", size = 3872596, upload-time = "2026-04-20T08:33:46.927Z" }, + { url = "https://files.pythonhosted.org/packages/79/00/f726d54ff00213641069a66ee2fadf17f01f77a2f1ba92de229830056419/primp-1.2.3-cp310-abi3-win_arm64.whl", hash = "sha256:03c668481b2cf34552880f4b6ebabfa913fdaeb2921ce982e42c428f451b630c", size = 3875239, upload-time = "2026-04-20T08:33:32.981Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e6/1fe1bcaf7566d7e6c9b39748f0d6ded857c80cf3252acf6aa3c6b7b8dbb0/primp-1.2.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:0148fd5c0502bb88a26217b606ebb254de44a666dac52657ff97f727577bc7ce", size = 4348055, upload-time = "2026-04-20T08:33:48.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/46/cae79466969a31d885409ce716eb5a4af66f66d1121e63ee3ddd7d666b9e/primp-1.2.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13bfc39172a083cc5520d9045a99988d7f8502458be139d6b1926de4d57e30e9", size = 4034114, upload-time = "2026-04-20T08:33:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c3/e8b24ff42ef6bf71f8b4277fd6d83a139d9b9ad14b0ffb4c76a8e9225a15/primp-1.2.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:283e751671e78cbac9f1ff72e89225d74bcd9ea08886910160f485cd78a55f2b", size = 4303794, upload-time = "2026-04-20T08:34:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/72/b4/9c59197fee68abdc53683b65b6b0e5ea6e0f098cc2527af29edd05b22ed4/primp-1.2.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec5d94244f24c61b350791112c1687c97d144f400a7ea5d8476dbd2fad6de9af", size = 3908577, upload-time = "2026-04-20T08:33:59.567Z" }, + { url = "https://files.pythonhosted.org/packages/71/b2/25c7cfcfe33f2fa82ab8cc72f4082fc12c8f803ceb2624c787fa72c73e12/primp-1.2.3-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48d459dda86bfddce3a14b514694e0c8028f20cc461dd52e105aba400c622513", size = 4153501, upload-time = "2026-04-20T08:34:03.22Z" }, + { url = "https://files.pythonhosted.org/packages/04/9b/83e61b32b9049478f63c1956ff3244619691c3edf6e96b19254047756b36/primp-1.2.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0d8441f07c948a54ff21bfc4094cd2967b5fb6f8a9ef0c9562a1eb35da0127e", size = 4440289, upload-time = "2026-04-20T08:33:24.734Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/48c351f601f204c52d5a477d7a19efcd8b7deb6478c57195fbe8bf9c3632/primp-1.2.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:728b6f63552f1bc729e0490ee69a3a9fbd39698c1578b0e6313f3779de469a8c", size = 4306848, upload-time = "2026-04-20T08:33:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9a/683b2fbb8c2df3b3d2b351e86eec0a873479b95bb804e5d882939057366b/primp-1.2.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9952509dcd8aa8750f4cdfc86b24cdcec96f9342a6525003bc3446ba466d5512", size = 4521679, upload-time = "2026-04-20T08:33:38.699Z" }, + { url = "https://files.pythonhosted.org/packages/80/eb/bbbbc79cf4132f2beeb0a0f99f41837dd66c158d0439f59df75a2d592d0a/primp-1.2.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e74a6aa83aa65d02665ad5d0e53d7877ea8abce6f3d50ed92495f9c4c9d10e2e", size = 4469041, upload-time = "2026-04-20T08:33:51.161Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5c/208fe1c6e9d3a28c6eae530378ac9c60fe8ae389f68a4707ef7986d5b133/primp-1.2.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1dfc4dd5bf7e4f9a45f9c7ae1256f957cc7ca8d5344f82d335129a13e1d7ffea", size = 4144925, upload-time = "2026-04-20T08:33:35.94Z" }, + { url = "https://files.pythonhosted.org/packages/da/06/ce175b54edecf22b750da67bc095bea53f2d87d191c4461b30122c7ddd1e/primp-1.2.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:97c37df6fc7eb989f324b326e0547b23757a82f9d8b334075d4b0bd29e310723", size = 4276929, upload-time = "2026-04-20T08:33:49.521Z" }, + { url = "https://files.pythonhosted.org/packages/2e/12/a913ab53ad61fe51d44ef0f8fee6b63a897d2b9c8a4b43299d7f5decc003/primp-1.2.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ead9c8b660fa0ebb193317b85d61dd1bd840a3e97e882b33b896dc9e37ed6d63", size = 4772684, upload-time = "2026-04-20T08:33:42.456Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/2c3aafb2814703979bf5a216575f4478bcf6070bc6ec143056cf3b56c134/primp-1.2.3-cp314-cp314t-win32.whl", hash = "sha256:cb52db4d54105097773c0df4e2ef0ce7b42461d9d4d3ec28a0622ca2d22945bb", size = 3499126, upload-time = "2026-04-20T08:33:29.953Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/bad9c739a35f4cf69e2f39c01f664c9a5969e753c4cfcdc33e113cc984af/primp-1.2.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d85a44585df27038e18298f7e35335c4961926e7401edd1a1bf4f7f967b3f107", size = 3870129, upload-time = "2026-04-20T08:34:11.666Z" }, + { url = "https://files.pythonhosted.org/packages/67/78/882563dc554c5f710b4e47c58978586c2528703435800a7a84e81b339d90/primp-1.2.3-cp314-cp314t-win_arm64.whl", hash = "sha256:6513dcb55e6b511c1ae383a3a0e887929b09d8d6b2c70e2b188b425c51471a02", size = 3874087, upload-time = "2026-04-20T08:33:58.109Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" }, + { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" }, + { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" }, + { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" }, + { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" }, + { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, + { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, + { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pypika" +version = "0.51.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919, upload-time = "2026-02-04T11:27:48.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585, upload-time = "2026-02-04T11:27:46.251Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { 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" }, +] + +[[package]] +name = "python-jose" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "pyasn1" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "7.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { 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" }, +] + +[[package]] +name = "reflexio-ai" +version = "0.2.22" +source = { editable = "../../../../" } +dependencies = [ + { name = "aiohttp" }, + { name = "anthropic" }, + { name = "bcrypt" }, + { name = "braintrust" }, + { name = "cachetools" }, + { name = "chromadb" }, + { name = "colorlog" }, + { name = "duckduckgo-search" }, + { name = "einops" }, + { name = "fastapi" }, + { name = "gepa" }, + { name = "hdbscan" }, + { name = "httpx" }, + { name = "json-repair" }, + { name = "litellm" }, + { name = "nltk" }, + { name = "openai" }, + { name = "passlib" }, + { name = "pydantic", extra = ["email"] }, + { name = "python-dateutil" }, + { name = "python-dotenv" }, + { name = "python-jose" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "rich" }, + { name = "sentence-transformers" }, + { name = "slowapi" }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "typer" }, + { name = "uvicorn" }, + { name = "websocket-client" }, + { name = "xlsxwriter" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.11.12" }, + { name = "anthropic", specifier = ">=0.72.0" }, + { name = "bcrypt", specifier = ">=4.2.1" }, + { name = "braintrust", specifier = ">=0.12.0" }, + { name = "cachetools", specifier = ">=6.2.4" }, + { name = "chromadb", specifier = ">=1.5.8" }, + { name = "colorlog", specifier = ">=6.10.1" }, + { name = "datasets", marker = "extra == 'benchmark'", specifier = ">=4.8.4" }, + { name = "duckduckgo-search", specifier = ">=7.0.1" }, + { name = "einops", specifier = ">=0.8.0" }, + { name = "fastapi", specifier = ">=0.111.1" }, + { name = "fire", marker = "extra == 'benchmark'", specifier = ">=0.7.1" }, + { name = "fpdf2", marker = "extra == 'benchmark'", specifier = ">=2.8.7" }, + { name = "gepa", specifier = ">=0.1.1,<0.2" }, + { name = "hdbscan", specifier = ">=0.8.40" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "json-repair", specifier = ">=0.30.0" }, + { name = "langchain-core", marker = "extra == 'langchain'", specifier = ">=1.2.28" }, + { name = "litellm", specifier = ">=1.80.11" }, + { name = "markdown", marker = "extra == 'benchmark'", specifier = ">=3.10.2" }, + { name = "nltk", specifier = ">=3.9.3" }, + { name = "openai", specifier = ">=2.8.0" }, + { name = "openpyxl", marker = "extra == 'benchmark'", specifier = ">=3.1.5" }, + { name = "pandas", marker = "extra == 'notebooks'", specifier = ">=3.0.2" }, + { name = "passlib", specifier = ">=1.7.4" }, + { name = "pdf2image", marker = "extra == 'benchmark'", specifier = ">=1.17.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pydantic", extras = ["email"], specifier = ">=2.13.0" }, + { name = "pypdf", marker = "extra == 'benchmark'", specifier = ">=6.10.0" }, + { name = "python-dateutil", specifier = ">=2.8.0" }, + { name = "python-docx", marker = "extra == 'benchmark'", specifier = ">=1.2.0" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "python-jose", specifier = ">=3.3.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "redis", specifier = ">=6.2.0" }, + { name = "reportlab", marker = "extra == 'benchmark'", specifier = ">=4.4.10" }, + { name = "requests", specifier = ">=2.25.0" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "sentence-transformers", specifier = ">=3.0" }, + { name = "slowapi", specifier = ">=0.1.9" }, + { name = "sqlite-vec", marker = "extra == 'vec'", specifier = ">=0.1.6" }, + { name = "tenacity", specifier = ">=9.0.0" }, + { name = "tiktoken", specifier = ">=0.12.0" }, + { name = "typer", specifier = ">=0.15.0" }, + { name = "uvicorn", specifier = ">=0.34.0" }, + { name = "websocket-client", specifier = ">=1.8.0" }, + { name = "xlsxwriter", specifier = ">=3.2.2" }, +] +provides-extras = ["vec", "notebooks", "langchain", "benchmark"] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=24.10.0" }, + { name = "build", specifier = ">=1.0.0" }, + { name = "commitizen", specifier = ">=4.1.0" }, + { name = "jupyterlab", specifier = ">=4.4.3" }, + { name = "matplotlib", specifier = ">=3.10.8" }, + { name = "moto", specifier = ">=5.0.28" }, + { name = "mutmut", specifier = ">=3.2.0" }, + { name = "polars", specifier = ">=1.40.1" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pyright", specifier = ">=1.1.400" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.0" }, + { name = "pytest-cov", specifier = ">=6.0" }, + { name = "pytest-timeout", specifier = ">=2.3.1" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "python-semantic-release", specifier = ">=10.0.0" }, + { name = "ruff", specifier = ">=0.15.0" }, + { name = "syrupy", specifier = ">=5.0" }, + { name = "twine", specifier = ">=6.0.0" }, +] +docs = [ + { name = "griffe", specifier = ">=0.38.0" }, + { name = "mkdocs", specifier = ">=1.5.3" }, + { name = "mkdocs-material", specifier = ">=9.5.3" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.24.0" }, + { name = "mkdocstrings-python", specifier = ">=1.7.0" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, + { 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" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/27/16d127a61303e05847d878b23687f3371868c76e738557fa80b4373a8c2b/sentence_transformers-5.5.0.tar.gz", hash = "sha256:9cec675e68bfe09d07466d1f13ab06d1d79d60a0f45b154baf433bde6ae159cb", size = 444908, upload-time = "2026-05-12T14:05:42.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/20/18416624bcbae866ec0b111979766cebabe8e5ff7563ab953ecbaf3ff9e7/sentence_transformers-5.5.0-py3-none-any.whl", hash = "sha256:75313fdcc2397ec4b58297c25d6187fcca5a6b2aeb09570a72eff5a3223d8d58", size = 588665, upload-time = "2026-05-12T14:05:40.899Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "slowapi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "limits" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +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" } +wheels = [ + { 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" }, +] + +[[package]] +name = "sseclient-py" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/2e/59920f7d66b7f9932a3d83dd0ec53fab001be1e058bf582606fe414a5198/sseclient_py-1.9.0-py3-none-any.whl", hash = "sha256:340062b1587fc2880892811e2ab5b176d98ef3eee98b3672ff3a3ba1e8ed0f6f", size = 8351, upload-time = "2026-01-02T23:39:30.995Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c2/64b06cbb7830fb3cd9be13e1158b31a3f36b68e6a209105ee3c9d9480be0/torch-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b958caff4a14d3a3b0b2dfc6a378f64dda9728a9dad28c08a0db9ce4dafb549", size = 122988114, upload-time = "2026-05-13T14:54:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2f/bdbaaa267de519ef1b73054bf590d8c93c37a266c9a4e24a01bd38b6918f/torch-2.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:3fee918902090ade827643e758e98363278815de583c75d111fdd665ebffde9f", size = 122987706, upload-time = "2026-05-13T14:54:00.335Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" }, + { url = "https://files.pythonhosted.org/packages/67/dc/ac069f8d6e8be701535921141055293b0d4819d3d7f224a4612cf157c7f9/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", size = 88027282, upload-time = "2026-05-13T14:53:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/1c1eb00e34555b536dddf792676026a988d710ed36981aa00499b36b0620/torch-2.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:891c769072637c74e9a5a77a3bc782894696d8ffec83b938df8536dee7f0ba78", size = 426386961, upload-time = "2026-05-13T14:51:28.406Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d4/7e730dba0c7032a4154dc9056b76cf9625515e030e269cfbf8098fcfee7d/torch-2.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e2ad3eb85d39c3cab62dfa93ed5a73516e6a53c6713cb97d004004fe089f0f1f", size = 532272265, upload-time = "2026-05-13T14:51:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b4/92c80d1bbfee1c0036c06d1d2155a3065bd2423134c83bf8a47e65cd6b9b/torch-2.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:c66696857e987efb8bc1777a37357ec4f60ab5e8af6250b83d6034437fa2d8f3", size = 122987138, upload-time = "2026-05-13T14:51:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/2e12b37ce50a19a037d7bc62d652a5a8f27385a7b05859d6bc9204f20cfe/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", size = 88320100, upload-time = "2026-05-13T14:51:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/83c450ec7b0bb40a7b74611c1b5440f9260e33c54c90d556fd4a1f0fd955/torch-2.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a43ac605a5e13116c72b64c359644cce0229f213dde48d2ae0ae5eb5becf7feb", size = 426391871, upload-time = "2026-05-13T14:52:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/1a0b575d98d0afedd8f157d23fa3d2759421483660448e60d0a4b10b6daa/torch-2.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a7512adfdd7f6732e40de1c620831e3c75b39b98cef60b11d0c5f0a76473ec5", size = 532192241, upload-time = "2026-05-13T14:51:07.795Z" }, + { url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/e6/4134ea2fbea322cddc7ffc94a0d8ee47fe32ce8e876b320cd37d88edfc4d/transformers-5.8.1.tar.gz", hash = "sha256:4dd5b6de4105725104d84fd6abd74b305f4debfc251b38c648ee5dd087cf543b", size = 8532019, upload-time = "2026-05-13T03:21:57.234Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b1/8be7e7ef0b5200491312201918b6125ef9c9df9dd0f0240ccef9ac824e6b/transformers-5.8.1-py3-none-any.whl", hash = "sha256:5340fb95962162cdfdae5cc91d7f8fedd92ed75216c1154c5e1f590fcf56dd0e", size = 10632882, upload-time = "2026-05-13T03:21:52.876Z" }, +] + +[[package]] +name = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/82a802dac4689f2a2fb2e69302e6a138eecc3e175bbe976ba3cfc717683a/triton-3.7.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a44a8476d0d3571eac4e4d1048e1ff75aad81a09ff4602ccfc56c6dea1672e", size = 188507879, upload-time = "2026-05-07T19:05:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/8f/af/9904ec6d3c93d9b24e5ec360445bbdf758b7f00bfbeedb89cb0eb64eb8bb/triton-3.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b85e72968a9d8bba5ddb24e9b64aaabaf48affb042f2755cb7cfa92b7531ce", size = 201460637, upload-time = "2026-05-07T18:46:34.749Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/4835a8ea746b88727d8899f4e3ccce4f9cacb38abfc3bb0a638266c53111/triton-3.7.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a160de426fd99f92b0baf509045360afbd3bfaa0b4a5171dde800ec9f09684", size = 188608706, upload-time = "2026-05-07T19:05:39.218Z" }, + { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +] From 96cb4cdfe077df49bbab6c80e181b4c4287593b2 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:20:28 +0000 Subject: [PATCH 05/53] feat(openclaw): add plugin manifest with config schema --- .../openclaw/plugin/openclaw.plugin.json | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/openclaw.plugin.json diff --git a/reflexio/integrations/openclaw/plugin/openclaw.plugin.json b/reflexio/integrations/openclaw/plugin/openclaw.plugin.json new file mode 100644 index 00000000..fb44ce83 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/openclaw.plugin.json @@ -0,0 +1,48 @@ +{ + "id": "reflexio-openclaw-smart", + "name": "Reflexio openClaw Smart", + "description": "Cross-session memory and self-improvement for openClaw via local reflexio backend.", + "skills": ["./skills"], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "publish": { + "type": "object", + "additionalProperties": false, + "properties": { + "batch_size": { "type": "integer", "default": 10 }, + "max_retries": { "type": "integer", "default": 3 }, + "max_content_length": { "type": "integer", "default": 10000 } + } + }, + "search": { + "type": "object", + "additionalProperties": false, + "properties": { + "timeout_ms": { "type": "integer", "default": 5000 }, + "top_k": { "type": "integer", "default": 3 }, + "min_prompt_length": { "type": "integer", "default": 5 } + } + }, + "server": { + "type": "object", + "additionalProperties": false, + "properties": { + "health_check_timeout_ms": { "type": "integer", "default": 3000 }, + "stale_flag_ms": { "type": "integer", "default": 120000 } + } + }, + "extraction": { + "type": "object", + "additionalProperties": false, + "properties": { + "model": { + "type": "string", + "description": "openClaw model id passed as --model to `openclaw infer model run`. If omitted, the user's openClaw default is used." + } + } + } + } + } +} From 7427e06989ed71dd58ec0082e6ed20760fde448a Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:23:07 +0000 Subject: [PATCH 06/53] feat(server/llm): add openclaw_provider LiteLLM CustomLLM (23 tests) --- .../server/llm/providers/openclaw_provider.py | 257 ++++++++++++++++++ tests/server/llm/test_openclaw_provider.py | 221 +++++++++++++++ 2 files changed, 478 insertions(+) create mode 100644 reflexio/server/llm/providers/openclaw_provider.py create mode 100644 tests/server/llm/test_openclaw_provider.py diff --git a/reflexio/server/llm/providers/openclaw_provider.py b/reflexio/server/llm/providers/openclaw_provider.py new file mode 100644 index 00000000..fbe05f9c --- /dev/null +++ b/reflexio/server/llm/providers/openclaw_provider.py @@ -0,0 +1,257 @@ +"""openClaw CLI as a LiteLLM custom provider. + +Routes ``litellm.completion(model="openclaw/...", ...)`` through the +user's locally-installed ``openclaw`` CLI by spawning +``openclaw infer model run --prompt

--json [--model ]``. + +Activation is opt-in via ``OPENCLAW_SMART_USE_LOCAL_CLI=1``. Without it, +the provider does not register and reflexio falls back to its normal +provider priority. Every spawn sets ``OPENCLAW_SMART_INTERNAL=1`` so the +openclaw-smart plugin's hooks short-circuit when this provider invokes +the CLI (recursion guard). +""" + +from __future__ import annotations + +import json +import logging +import os +import shutil +import subprocess # noqa: S404 — subprocess is the integration point. +from typing import Any + +import litellm +from litellm.llms.custom_llm import CustomLLM +from litellm.types.utils import Choices, Message, ModelResponse, Usage + +_LOGGER = logging.getLogger(__name__) + +PROVIDER_KEY = "openclaw" +ENV_ENABLE = "OPENCLAW_SMART_USE_LOCAL_CLI" +ENV_CLI_PATH = "OPENCLAW_BIN" +ENV_DEFAULT_MODEL = "OPENCLAW_DEFAULT_MODEL" +ENV_TIMEOUT = "OPENCLAW_CLI_TIMEOUT" +_DEFAULT_TIMEOUT_SECONDS = 180 + +_TRUTHY = {"1", "true", "yes"} + +# Module-level state reset by tests via the _reset_module_state fixture. +_REGISTERED: bool = False +_HANDLER: "OpenClawLLM | None" = None + + +class OpenClawCLIError(RuntimeError): + """Raised when the openclaw CLI subprocess fails.""" + + +def _env_enabled() -> bool: + """Return True when OPENCLAW_SMART_USE_LOCAL_CLI is truthy. + + Returns: + bool: True if the opt-in env var is set to a truthy value, else False. + """ + return os.environ.get(ENV_ENABLE, "").lower() in _TRUTHY + + +def _resolve_cli_path() -> str | None: + """Resolve the openclaw CLI absolute path. + + Honors ``OPENCLAW_BIN`` env override, otherwise falls back to + ``shutil.which("openclaw")``. + + Returns: + str | None: Absolute path to the openclaw binary, or None if not found. + """ + override = os.environ.get(ENV_CLI_PATH) + if override and os.path.isfile(override) and os.access(override, os.X_OK): + return override + return shutil.which("openclaw") + + +def is_openclaw_available() -> bool: + """Return True when the openclaw provider is usable right now. + + Both the opt-in env var *and* a resolvable CLI path are required. + + Returns: + bool: True iff ``OPENCLAW_SMART_USE_LOCAL_CLI`` is truthy AND a + CLI binary is resolvable. + """ + return _env_enabled() and _resolve_cli_path() is not None + + +def _extract_text(payload: Any) -> str: + """Extract the model's text reply from openclaw's JSON output. + + The exact field name in ``openclaw infer model run --json`` output + couldn't be probed at design time (no model providers were + auth-configured). Scan common keys in priority order: ``text`` → + ``content`` → ``output`` → ``message.content``. The first end-to-end + test will surface the real key; we can pin it then if needed. + + Args: + payload (Any): Parsed JSON response from openclaw. + + Returns: + str: Completion text, or empty string if no known key matches. + """ + if not isinstance(payload, dict): + return "" + for key in ("text", "content", "output"): + value = payload.get(key) + if isinstance(value, str): + return value + msg = payload.get("message") + if isinstance(msg, dict) and isinstance(msg.get("content"), str): + return msg["content"] + return "" + + +def _call_cli(*, prompt: str, model: str | None, timeout_s: int) -> str: + """Invoke openclaw CLI for a one-shot completion. + + Args: + prompt (str): User prompt text passed via ``--prompt``. + model (str | None): Optional model id passed via ``--model``. + timeout_s (int): Subprocess timeout in seconds. + + Returns: + str: Completion text extracted from the CLI's JSON stdout. + + Raises: + OpenClawCLIError: If the CLI is not found, exits non-zero, returns + non-JSON stdout, or returns JSON with no recognized text field. + """ + cli = _resolve_cli_path() + if not cli: + raise OpenClawCLIError("openclaw CLI not found; set OPENCLAW_BIN") + + argv = [cli, "infer", "model", "run", "--prompt", prompt, "--json"] + if model: + argv += ["--model", model] + + env = os.environ.copy() + env["OPENCLAW_SMART_INTERNAL"] = "1" + + try: + proc = subprocess.run( # noqa: S603 — argv is fully constructed. + argv, + env=env, + capture_output=True, + text=True, + timeout=timeout_s, + check=False, + ) + except subprocess.TimeoutExpired as exc: + raise OpenClawCLIError(f"openclaw timed out after {timeout_s}s") from exc + + if proc.returncode != 0: + raise OpenClawCLIError(proc.stderr.strip() or f"openclaw exit {proc.returncode}") + + try: + payload = json.loads(proc.stdout) + except json.JSONDecodeError as exc: + raise OpenClawCLIError( + f"openclaw returned non-JSON: {proc.stdout[:200]}" + ) from exc + + text = _extract_text(payload) + if not text: + raise OpenClawCLIError( + f"openclaw JSON had no completion text: {list(payload)[:5]}" + ) + return text + + +class OpenClawLLM(CustomLLM): + """LiteLLM CustomLLM that routes completions through the openclaw CLI.""" + + def completion(self, *args: Any, **kwargs: Any) -> ModelResponse: + """Synchronous completion via ``openclaw infer model run``. + + The ``openclaw/`` prefix is stripped from the requested model id. + If no model is provided, ``OPENCLAW_DEFAULT_MODEL`` is used; if + that's also unset, the CLI's own default is used. + + Args: + *args: Unused (LiteLLM signature). + **kwargs: LiteLLM completion kwargs (``messages``, ``model``). + + Returns: + ModelResponse: Reply text in ``choices[0].message.content``. + """ + model_kwarg = str(kwargs.get("model", "")) + if "/" in model_kwarg: + model = model_kwarg.split("/", 1)[1] or None + else: + model = model_kwarg or None + if not model: + model = os.environ.get(ENV_DEFAULT_MODEL) or None + + messages = kwargs.get("messages", []) + prompt = _messages_to_prompt(messages) + timeout_s = int(os.environ.get(ENV_TIMEOUT, str(_DEFAULT_TIMEOUT_SECONDS))) + + text = _call_cli(prompt=prompt, model=model, timeout_s=timeout_s) + + return ModelResponse( + choices=[Choices(message=Message(content=text, role="assistant"))], + usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0), + model=model_kwarg or "openclaw", + ) + + +def _messages_to_prompt(messages: list[dict[str, Any]]) -> str: + """Flatten chat messages into a single prompt string. + + ``openclaw infer model run`` takes a single ``--prompt``; we serialize + the chat as ``role: content`` lines separated by blank lines. Content + blocks (lists) are flattened to text. + + Args: + messages (list[dict[str, Any]]): LiteLLM-style chat messages. + + Returns: + str: Newline-joined role:content lines. + """ + parts: list[str] = [] + for m in messages: + role = m.get("role", "user") + content = m.get("content", "") + if isinstance(content, list): + content = "".join( + c.get("text", "") for c in content if isinstance(c, dict) + ) + parts.append(f"{role}: {content}") + return "\n\n".join(parts) + + +def register_if_enabled() -> bool: + """Register the openclaw provider with LiteLLM if the opt-in env is set. + + Idempotent: safe to call multiple times. + + Returns: + bool: True if the provider is now registered, False if env is off or + the CLI is missing. + """ + global _REGISTERED, _HANDLER + if _REGISTERED: + return True + if not _env_enabled(): + return False + if not _resolve_cli_path(): + _LOGGER.warning( + "%s=1 but openclaw CLI not found; set %s", + ENV_ENABLE, + ENV_CLI_PATH, + ) + return False + _HANDLER = OpenClawLLM() + litellm.custom_provider_map = [ + *getattr(litellm, "custom_provider_map", []), + {"provider": PROVIDER_KEY, "custom_handler": _HANDLER}, + ] + _REGISTERED = True + _LOGGER.info("Registered openclaw LiteLLM provider") + return True diff --git a/tests/server/llm/test_openclaw_provider.py b/tests/server/llm/test_openclaw_provider.py new file mode 100644 index 00000000..2250be24 --- /dev/null +++ b/tests/server/llm/test_openclaw_provider.py @@ -0,0 +1,221 @@ +"""Tests for the openclaw LiteLLM custom provider.""" + +from __future__ import annotations + +import json +import subprocess +from unittest.mock import MagicMock, patch + +import pytest + +from reflexio.server.llm.providers import openclaw_provider as ocp + + +@pytest.fixture(autouse=True) +def _reset_module_state() -> None: + """Each test starts with fresh registration state.""" + ocp._REGISTERED = False + ocp._HANDLER = None + + +def _fake_completed_process( + stdout: str, stderr: str = "", returncode: int = 0 +) -> subprocess.CompletedProcess[str]: + return subprocess.CompletedProcess( + args=["openclaw"], returncode=returncode, stdout=stdout, stderr=stderr + ) + + +class TestEnvFlags: + def test_disabled_by_default(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(ocp.ENV_ENABLE, raising=False) + assert ocp._env_enabled() is False + + def test_enabled_with_truthy(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(ocp.ENV_ENABLE, "1") + assert ocp._env_enabled() is True + monkeypatch.setenv(ocp.ENV_ENABLE, "true") + assert ocp._env_enabled() is True + monkeypatch.setenv(ocp.ENV_ENABLE, "YES") + assert ocp._env_enabled() is True + + def test_disabled_with_falsy(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv(ocp.ENV_ENABLE, "0") + assert ocp._env_enabled() is False + monkeypatch.setenv(ocp.ENV_ENABLE, "") + assert ocp._env_enabled() is False + + +class TestResolveCliPath: + def test_env_override_honored( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh\nexit 0\n") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + assert ocp._resolve_cli_path() == str(fake) + + def test_falls_back_to_which(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(ocp.ENV_CLI_PATH, raising=False) + with patch("shutil.which", return_value="/usr/local/bin/openclaw"): + assert ocp._resolve_cli_path() == "/usr/local/bin/openclaw" + + def test_none_when_missing(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(ocp.ENV_CLI_PATH, raising=False) + with patch("shutil.which", return_value=None): + assert ocp._resolve_cli_path() is None + + +class TestExtractText: + def test_text_key(self) -> None: + assert ocp._extract_text({"text": "A"}) == "A" + + def test_content_key(self) -> None: + assert ocp._extract_text({"content": "B"}) == "B" + + def test_output_key(self) -> None: + assert ocp._extract_text({"output": "C"}) == "C" + + def test_message_content_nested(self) -> None: + assert ocp._extract_text({"message": {"content": "D"}}) == "D" + + def test_unknown_shape_returns_empty(self) -> None: + assert ocp._extract_text({"unknown": "X"}) == "" + + def test_non_dict_returns_empty(self) -> None: + assert ocp._extract_text("plain string") == "" + assert ocp._extract_text([]) == "" + assert ocp._extract_text(None) == "" + + +class TestCallCli: + def test_invokes_subprocess_with_prompt( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + + proc = _fake_completed_process(json.dumps({"text": "PONG"})) + with patch("subprocess.run", return_value=proc) as run: + result = ocp._call_cli(prompt="ping", model=None, timeout_s=10) + assert result == "PONG" + argv = run.call_args[0][0] + assert str(fake) in argv + assert "--prompt" in argv + assert "ping" in argv + assert "--json" in argv + assert "infer" in argv and "model" in argv and "run" in argv + + def test_propagates_recursion_guard( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + + proc = _fake_completed_process(json.dumps({"text": "ok"})) + with patch("subprocess.run", return_value=proc) as run: + ocp._call_cli(prompt="p", model=None, timeout_s=10) + env = run.call_args[1]["env"] + assert env.get("OPENCLAW_SMART_INTERNAL") == "1" + + def test_passes_model_override( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + + proc = _fake_completed_process(json.dumps({"text": "ok"})) + with patch("subprocess.run", return_value=proc) as run: + ocp._call_cli(prompt="p", model="anthropic/claude-sonnet-4-6", timeout_s=10) + argv = run.call_args[0][0] + assert "--model" in argv + assert "anthropic/claude-sonnet-4-6" in argv + + def test_raises_on_nonzero_exit( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + + proc = _fake_completed_process("", stderr="bad model", returncode=2) + with patch("subprocess.run", return_value=proc): + with pytest.raises(ocp.OpenClawCLIError, match="bad model"): + ocp._call_cli(prompt="p", model=None, timeout_s=10) + + def test_raises_on_missing_cli(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(ocp.ENV_CLI_PATH, raising=False) + with patch("shutil.which", return_value=None): + with pytest.raises(ocp.OpenClawCLIError, match="not found"): + ocp._call_cli(prompt="p", model=None, timeout_s=10) + + def test_raises_on_invalid_json( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + + proc = _fake_completed_process("not json") + with patch("subprocess.run", return_value=proc): + with pytest.raises(ocp.OpenClawCLIError, match="non-JSON"): + ocp._call_cli(prompt="p", model=None, timeout_s=10) + + def test_raises_on_empty_completion( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + + proc = _fake_completed_process(json.dumps({"unrelated": "x"})) + with patch("subprocess.run", return_value=proc): + with pytest.raises(ocp.OpenClawCLIError, match="no completion text"): + ocp._call_cli(prompt="p", model=None, timeout_s=10) + + +class TestRegisterIfEnabled: + def test_skips_when_disabled(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv(ocp.ENV_ENABLE, raising=False) + assert ocp.register_if_enabled() is False + + def test_skips_when_cli_missing( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv(ocp.ENV_ENABLE, "1") + monkeypatch.delenv(ocp.ENV_CLI_PATH, raising=False) + with patch("shutil.which", return_value=None): + assert ocp.register_if_enabled() is False + + def test_registers_when_enabled_and_cli_present( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_ENABLE, "1") + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + assert ocp.register_if_enabled() is True + assert ocp._REGISTERED is True + + def test_idempotent( + self, monkeypatch: pytest.MonkeyPatch, tmp_path + ) -> None: + fake = tmp_path / "openclaw" + fake.write_text("#!/bin/sh") + fake.chmod(0o755) + monkeypatch.setenv(ocp.ENV_ENABLE, "1") + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake)) + assert ocp.register_if_enabled() is True + # Second call must not fail + assert ocp.register_if_enabled() is True From a715a3d84ec6ee1f62a177735a297ee659d82cc2 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:23:57 +0000 Subject: [PATCH 07/53] feat(server/llm): register openclaw provider alongside claude_code at bootstrap --- reflexio/server/llm/litellm_client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/reflexio/server/llm/litellm_client.py b/reflexio/server/llm/litellm_client.py index 91d662a4..fb7d87c7 100644 --- a/reflexio/server/llm/litellm_client.py +++ b/reflexio/server/llm/litellm_client.py @@ -32,6 +32,9 @@ from reflexio.server.llm.providers.claude_code_provider import ( register_if_enabled as _register_claude_code, ) +from reflexio.server.llm.providers.openclaw_provider import ( + register_if_enabled as _register_openclaw, +) from reflexio.server.llm.providers.embedding_service_provider import ( EmbeddingUnavailableError, embedding_provider_mode, @@ -63,9 +66,10 @@ # Suppress LiteLLM's verbose logging litellm.suppress_debug_info = True -# Opt-in registration of claude-smart's local providers. All no-ops -# unless the matching env var is set. Safe to call at import. +# Opt-in registration of local CLI providers. All no-ops unless the +# matching env var is set. Safe to call at import. _register_claude_code() +_register_openclaw() _register_local_embedder() _register_nomic_embedder() From 7994b4db8181e6e61205b667c524ba3483068bfd Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:42:12 +0000 Subject: [PATCH 08/53] feat(openclaw): port ids.py with agent_id fallback Extracts _resolve_from_git() helper so resolve_project_id (cwd-fallback) and resolve_project_id_with_fallback (agent_id-fallback for openClaw, which can run outside a git repo) share the git toplevel check. --- .../openclaw/plugin/src/openclaw_smart/ids.py | 82 +++++++++++++++++++ .../openclaw/plugin/tests/test_ids.py | 41 ++++++++++ 2 files changed, 123 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_ids.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py new file mode 100644 index 00000000..3bfc7380 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py @@ -0,0 +1,82 @@ +"""Resolve stable identifiers for openClaw sessions. + +Two identifiers matter to reflexio: + +- ``session_id``: openClaw's per-session id (``sessionKey``), passed via the + TS shim. We forward it to reflexio's interaction ``session_id`` field so + individual turns remain attributable to their conversation, but it is no + longer the scope key for extracted preferences. +- ``project_id``: a stable, cross-session name for the project. We use this + as reflexio's ``user_id`` for preferences, so user preferences extracted + in one session are visible to every later session in the same repo. + ``agent_version`` is hardcoded to ``"openclaw"`` in the adapter so shared + skills roll up globally across all openclaw projects. +""" + +from __future__ import annotations + +import logging +import os +import subprocess # noqa: S404 — git invocation with a fixed flag set. +from pathlib import Path + +_LOGGER = logging.getLogger(__name__) + + +def _resolve_from_git(cwd: Path) -> str | None: + """Return the git toplevel basename for *cwd*, or ``None`` if not a repo.""" + try: + result = subprocess.run( # noqa: S603, S607 — fixed argv, cwd is a Path. + ["git", "rev-parse", "--show-toplevel"], + cwd=cwd, + capture_output=True, + text=True, + check=False, + timeout=5, + ) + if result.returncode == 0: + toplevel = result.stdout.strip() + if toplevel: + return Path(toplevel).name + except (FileNotFoundError, subprocess.TimeoutExpired) as exc: + _LOGGER.debug("git toplevel resolution failed: %s", exc) + return None + + +def resolve_project_id(cwd: str | os.PathLike[str] | None = None) -> str: + """Return a stable project identifier for the given working directory. + + Prefers the basename of the git toplevel (so worktrees, submodules, and + ``cd src/`` all still map to the same project). Falls back to the cwd + basename when the directory is not inside a git repo. + + Args: + cwd: Working directory to resolve. Defaults to ``os.getcwd()``. + + Returns: + str: A non-empty identifier. Never raises. + """ + base = Path(cwd) if cwd is not None else Path.cwd() + return _resolve_from_git(base) or base.name or "unknown-project" + + +def resolve_project_id_with_fallback( + cwd: str | os.PathLike[str] | None, + agent_id: str | None, +) -> str: + """Resolve project ID, falling back to ``agent_id`` when not in a git repo. + + openClaw can run from any directory (or none at all), so when there is no + git context we prefer the openClaw agent id over a fabricated cwd + basename, which would be brittle across sessions. + + Args: + cwd: Working directory of the openClaw session (``ctx.workspaceDir``). + agent_id: openClaw agent id from ``ctx.agentId``. + + Returns: + str: git repo basename if cwd is in a repo; else ``agent_id``; else + the literal ``"openclaw"``. + """ + base = Path(cwd) if cwd is not None else Path.cwd() + return _resolve_from_git(base) or agent_id or "openclaw" diff --git a/reflexio/integrations/openclaw/plugin/tests/test_ids.py b/reflexio/integrations/openclaw/plugin/tests/test_ids.py new file mode 100644 index 00000000..fd11241d --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_ids.py @@ -0,0 +1,41 @@ +"""Tests for openclaw_smart.ids.""" + +from __future__ import annotations + +from unittest.mock import patch + +from openclaw_smart import ids + + +def test_resolve_project_id_uses_git_toplevel(tmp_path): + with patch("openclaw_smart.ids._resolve_from_git", return_value="my-repo"): + assert ids.resolve_project_id(str(tmp_path)) == "my-repo" + + +def test_resolve_project_id_falls_back_to_cwd_basename(tmp_path): + with patch("openclaw_smart.ids._resolve_from_git", return_value=None): + assert ids.resolve_project_id(str(tmp_path)) == tmp_path.name + + +def test_resolve_project_id_with_fallback_uses_git(tmp_path): + with patch("openclaw_smart.ids._resolve_from_git", return_value="my-repo"): + assert ( + ids.resolve_project_id_with_fallback(str(tmp_path), "agent-x") == "my-repo" + ) + + +def test_resolve_project_id_with_fallback_uses_agent_id(tmp_path): + with patch("openclaw_smart.ids._resolve_from_git", return_value=None): + assert ( + ids.resolve_project_id_with_fallback(str(tmp_path), "agent-x") == "agent-x" + ) + + +def test_resolve_project_id_with_fallback_uses_literal(tmp_path): + with patch("openclaw_smart.ids._resolve_from_git", return_value=None): + assert ids.resolve_project_id_with_fallback(str(tmp_path), None) == "openclaw" + + +def test_resolve_project_id_with_fallback_empty_agent_id_uses_literal(tmp_path): + with patch("openclaw_smart.ids._resolve_from_git", return_value=None): + assert ids.resolve_project_id_with_fallback(str(tmp_path), "") == "openclaw" From f3183fdcf1c1392d53ea125652a96bde61b5204d Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:43:35 +0000 Subject: [PATCH 09/53] feat(openclaw): port internal_call.py recursion guard Drops claude-code/codex-specific signals (CLAUDE_CODE_ENTRYPOINT, Codex title/suggestion prompts) and uses the OPENCLAW_SMART_INTERNAL env marker plus a workspaceDir-inside-reflexio check as the two recursion-guard signals. --- .../src/openclaw_smart/internal_call.py | 66 +++++++++++++++++++ .../plugin/tests/test_internal_call.py | 52 +++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_internal_call.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py new file mode 100644 index 00000000..a6e385f4 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py @@ -0,0 +1,66 @@ +"""Detect hook invocations that should not be published to reflexio. + +The single concern for openclaw-smart is reflexio's own LLM provider. The +``openclaw`` LiteLLM provider (see +``reflexio.server.llm.providers.openclaw_provider``) shells out to the +``openclaw`` CLI to answer extractor prompts. That subprocess is a full +openClaw invocation, so it fires *our* hooks too — and without a guard, +the ``agent_end`` hook would publish the extractor's own system prompt +back into reflexio as a user interaction. Reflexio would then train on +its own internals. + +Detection signals, OR'd: + - Env var ``OPENCLAW_SMART_INTERNAL=1``, set by reflexio's provider + before spawning ``openclaw``. + - ``payload.cwd`` resolves inside the reflexio repository. Catches + direct interactive ``openclaw`` runs from inside the reflexio + checkout (manual debugging) that would otherwise pollute the corpus. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +INTERNAL_ENV = "OPENCLAW_SMART_INTERNAL" + +# Plugin layout: +# /reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +# parents[5] = , the directory we want to fence off. +# +# In install mode the in-repo layout is absent — the env marker is the +# primary signal and this path never matches. ``OPENCLAW_SMART_REFLEXIO_DIR`` +# lets callers (and tests) override the path without touching the module. +_THIS_DIR = Path(__file__).resolve().parent +_REFLEXIO_DIR = Path( + os.environ.get("OPENCLAW_SMART_REFLEXIO_DIR") or _THIS_DIR.parents[5] +) + + +def is_internal_invocation(payload: dict[str, Any]) -> bool: + """True if this hook fire originated from reflexio's own LLM provider. + + Args: + payload (dict[str, Any]): Parsed openClaw hook payload. Only ``cwd`` + (or ``workspaceDir``) is inspected. + + Returns: + bool: True when the env marker is set or ``cwd`` points inside the + reflexio repository. False otherwise, including when ``cwd`` is + missing or unresolvable. + """ + if os.environ.get(INTERNAL_ENV) == "1": + return True + cwd = payload.get("cwd") or payload.get("workspaceDir") + if not isinstance(cwd, str) or not cwd: + return False + try: + resolved = Path(cwd).resolve() + except OSError: + return False + try: + resolved.relative_to(_REFLEXIO_DIR) + except ValueError: + return False + return True diff --git a/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py b/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py new file mode 100644 index 00000000..d98a6f4e --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py @@ -0,0 +1,52 @@ +"""Tests for openclaw_smart.internal_call.""" + +from __future__ import annotations + +import pytest + +from openclaw_smart import internal_call + + +def test_guard_fires_when_env_set(monkeypatch): + monkeypatch.setenv("OPENCLAW_SMART_INTERNAL", "1") + assert internal_call.is_internal_invocation({}) is True + + +def test_guard_inactive_by_default(monkeypatch): + monkeypatch.delenv("OPENCLAW_SMART_INTERNAL", raising=False) + assert internal_call.is_internal_invocation({}) is False + + +def test_guard_fires_when_cwd_inside_reflexio(monkeypatch, tmp_path): + monkeypatch.delenv("OPENCLAW_SMART_INTERNAL", raising=False) + repo = tmp_path / "reflexio-repo" + inner = repo / "src" / "module" + inner.mkdir(parents=True) + monkeypatch.setattr(internal_call, "_REFLEXIO_DIR", repo.resolve()) + assert internal_call.is_internal_invocation({"cwd": str(inner)}) is True + + +def test_guard_inactive_when_cwd_outside_reflexio(monkeypatch, tmp_path): + monkeypatch.delenv("OPENCLAW_SMART_INTERNAL", raising=False) + repo = tmp_path / "reflexio-repo" + other = tmp_path / "other" + repo.mkdir() + other.mkdir() + monkeypatch.setattr(internal_call, "_REFLEXIO_DIR", repo.resolve()) + assert internal_call.is_internal_invocation({"cwd": str(other)}) is False + + +def test_guard_handles_workspace_dir_key(monkeypatch, tmp_path): + """openClaw payloads use ``workspaceDir`` rather than ``cwd``.""" + monkeypatch.delenv("OPENCLAW_SMART_INTERNAL", raising=False) + repo = tmp_path / "reflexio-repo" + inner = repo / "src" + inner.mkdir(parents=True) + monkeypatch.setattr(internal_call, "_REFLEXIO_DIR", repo.resolve()) + assert internal_call.is_internal_invocation({"workspaceDir": str(inner)}) is True + + +def test_guard_inactive_when_cwd_missing(monkeypatch): + monkeypatch.delenv("OPENCLAW_SMART_INTERNAL", raising=False) + assert internal_call.is_internal_invocation({"cwd": ""}) is False + assert internal_call.is_internal_invocation({"cwd": None}) is False From 2f1e4329a2b9109e5ceddb33208509521717e786 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:44:21 +0000 Subject: [PATCH 10/53] feat(openclaw): port runtime.py with HOST_OPENCLAW --- .../plugin/src/openclaw_smart/runtime.py | 47 +++++++++++++++++++ .../openclaw/plugin/tests/test_runtime.py | 42 +++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_runtime.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py new file mode 100644 index 00000000..46c727fa --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py @@ -0,0 +1,47 @@ +"""Host/runtime state shared by openclaw-smart entrypoints. + +openclaw-smart only runs in the openClaw context — there is no Claude Code +or Codex variant — but we keep ``set_host``/``host`` for symmetry with the +claude-smart API and to leave room for additional hosts in the future. The +reflexio ``agent_version`` is hardcoded so all openClaw projects roll up +into the same shared-learning bucket. +""" + +from __future__ import annotations + +import os + +HOST_ENV = "OPENCLAW_SMART_HOST" + +HOST_OPENCLAW = "openclaw" +VALID_HOSTS = frozenset({HOST_OPENCLAW}) + +_AGENT_VERSION = "openclaw" +_current_host: str | None = None + + +def set_host(value: str | None) -> str: + """Set the current host, returning the normalized value.""" + global _current_host + host_value = value if value in VALID_HOSTS else HOST_OPENCLAW + _current_host = host_value + os.environ[HOST_ENV] = host_value + return host_value + + +def host() -> str: + """Return the current host, defaulting to openClaw.""" + if _current_host is not None: + return _current_host + value = os.environ.get(HOST_ENV) + return value if value in VALID_HOSTS else HOST_OPENCLAW + + +def is_openclaw() -> bool: + """True when the current hook invocation came from openClaw.""" + return host() == HOST_OPENCLAW + + +def agent_version() -> str: + """Reflexio agent version used for cross-project openClaw learning.""" + return _AGENT_VERSION diff --git a/reflexio/integrations/openclaw/plugin/tests/test_runtime.py b/reflexio/integrations/openclaw/plugin/tests/test_runtime.py new file mode 100644 index 00000000..4ac66338 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_runtime.py @@ -0,0 +1,42 @@ +"""Tests for openclaw_smart.runtime.""" + +from __future__ import annotations + +import pytest + +from openclaw_smart import runtime + + +@pytest.fixture(autouse=True) +def _reset_host(monkeypatch): + """Ensure each test starts with the module-level host unset.""" + monkeypatch.setattr(runtime, "_current_host", None) + monkeypatch.delenv(runtime.HOST_ENV, raising=False) + yield + + +def test_default_host_is_openclaw(): + assert runtime.host() == runtime.HOST_OPENCLAW + assert runtime.is_openclaw() is True + + +def test_set_host_sets_module_state(): + runtime.set_host("openclaw") + assert runtime.host() == "openclaw" + assert runtime.is_openclaw() + + +def test_set_host_falls_back_for_unknown_value(): + runtime.set_host("definitely-not-real") + assert runtime.host() == runtime.HOST_OPENCLAW + + +def test_set_host_writes_env_var(): + runtime.set_host("openclaw") + import os + + assert os.environ.get(runtime.HOST_ENV) == "openclaw" + + +def test_agent_version_is_openclaw(): + assert runtime.agent_version() == "openclaw" From f75b31c5b54b6ba6ad69c304000c840ee6f74cdb Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:45:52 +0000 Subject: [PATCH 11/53] feat(openclaw): port state.py JSONL buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames CLAUDE_SMART_STATE_DIR→OPENCLAW_SMART_STATE_DIR and the default buffer path ~/.claude-smart/sessions/ → ~/.openclaw-smart/sessions/. cs-cite references renamed to oc-cite. --- .../plugin/src/openclaw_smart/state.py | 255 ++++++++++++++++++ .../openclaw/plugin/tests/test_state.py | 141 ++++++++++ 2 files changed, 396 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_state.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py new file mode 100644 index 00000000..ac99254d --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py @@ -0,0 +1,255 @@ +"""Per-session JSONL buffer for interactions awaiting publish to reflexio. + +Each openClaw session gets one file at +``~/.openclaw-smart/sessions/{session_id}.jsonl``. Lines are one of: + +- ``{"role": "User", ...}`` — a user turn (see InteractionData fields) +- ``{"role": "Assistant", ...}`` — a finalized assistant turn +- ``{"role": "Assistant_tool", ...}`` — a single tool invocation, attached + to the next assistant turn at ``agent_end`` time +- ``{"published_up_to": N}`` — high-water mark so ``agent_end`` / + ``session_end`` don't re-publish rows already sent to reflexio + +The buffer exists for offline resilience: when reflexio is unreachable, +``agent_end`` appends without publishing and the next successful hook drains. +""" + +from __future__ import annotations + +import json +import logging +import os +from pathlib import Path +from typing import Any, Iterable + +try: + import fcntl # POSIX only — Windows hooks fall back to append-without-lock. +except ImportError: # pragma: no cover — non-POSIX platforms + fcntl = None # type: ignore[assignment] + +_LOGGER = logging.getLogger(__name__) + +_ENV_STATE_DIR = "OPENCLAW_SMART_STATE_DIR" +_DEFAULT_STATE_DIR = Path.home() / ".openclaw-smart" / "sessions" + +_TOOL_DATA_FIELD_MAX_LEN = 256 + +_VALID_CITATION_KINDS = frozenset({"playbook", "profile"}) + + +def _truncate_tool_data_field(value: Any) -> Any: + """Truncate a single tool_data field value to ``_TOOL_DATA_FIELD_MAX_LEN``. + + Only *top-level string* values are shortened. Nested containers + (dicts, lists) and non-string scalars pass through unchanged, even if + the container holds overlong strings — extractor prompts built from + this payload are bounded upstream by reflexio, and truncating a mid- + structure string risks producing invalid JSON when the caller later + serializes. The cap keeps long fields (``Edit.old_string`` / + ``new_string`` diffs, multi-line ``Bash`` scripts) from inflating the + extractor's input; short fields like file paths, URLs, and typical + commands stay intact. + """ + if isinstance(value, str) and len(value) > _TOOL_DATA_FIELD_MAX_LEN: + return value[:_TOOL_DATA_FIELD_MAX_LEN] + return value + + +def state_dir() -> Path: + """Root directory for session JSONL files. Honours ``OPENCLAW_SMART_STATE_DIR``.""" + override = os.environ.get(_ENV_STATE_DIR) + return Path(override) if override else _DEFAULT_STATE_DIR + + +def session_path(session_id: str) -> Path: + """Return the JSONL path for a given session id.""" + return state_dir() / f"{session_id}.jsonl" + + +def injected_path(session_id: str) -> Path: + """Return the JSONL path for the per-session oc-cite registry.""" + return state_dir() / f"{session_id}.injected.jsonl" + + +def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None: + """Append citation-registry entries to the per-session injected-items file. + + Each entry maps a short ``id`` (4-hex-char) back to the skill or + preference it came from so the ``agent_end`` hook can resolve ids cited + by openClaw via ``oc-cite`` into human-readable titles for the + dashboard. Silently no-ops when ``entries`` is empty. + """ + records = list(entries) + if not records: + return + path = injected_path(session_id) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as fh: + if fcntl is not None: + try: + fcntl.flock(fh.fileno(), fcntl.LOCK_EX) + except OSError as exc: + _LOGGER.debug("flock failed on %s: %s", path, exc) + for rec in records: + fh.write(json.dumps(rec, ensure_ascii=False) + "\n") + + +def read_injected(session_id: str) -> dict[str, dict[str, Any]]: + """Return the per-session citation registry keyed by id. + + Later entries win when the same id was injected multiple times + (identical content produces the same hash-derived id, so the extra + record only refreshes metadata). + """ + path = injected_path(session_id) + if not path.exists(): + return {} + registry: dict[str, dict[str, Any]] = {} + with path.open("r", encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError as exc: + _LOGGER.warning("Skipping malformed injected line in %s: %s", path, exc) + continue + item_id = entry.get("id") + if isinstance(item_id, str) and item_id: + registry[item_id] = entry + return registry + + +def append(session_id: str, record: dict[str, Any]) -> None: + """Append one JSON record to the session buffer. Creates the dir if needed. + + Holds an exclusive ``flock`` on the buffer file across the write so + concurrent hooks (e.g. parallel ``after_tool_call`` fires) cannot + interleave JSON lines when a payload exceeds the buffered-writer's + flush size. + """ + path = session_path(session_id) + path.parent.mkdir(parents=True, exist_ok=True) + line = json.dumps(record, ensure_ascii=False) + "\n" + with path.open("a", encoding="utf-8") as fh: + if fcntl is not None: + try: + fcntl.flock(fh.fileno(), fcntl.LOCK_EX) + except OSError as exc: + _LOGGER.debug("flock failed on %s: %s", path, exc) + fh.write(line) + + +def read_all(session_id: str) -> list[dict[str, Any]]: + """Return every record in the buffer as a list of dicts. Missing file → [].""" + path = session_path(session_id) + if not path.exists(): + return [] + records: list[dict[str, Any]] = [] + with path.open("r", encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if not line: + continue + try: + records.append(json.loads(line)) + except json.JSONDecodeError as exc: + _LOGGER.warning("Skipping malformed buffer line in %s: %s", path, exc) + return records + + +def _to_wire_citations(cited_items: Any) -> list[dict[str, str]]: + """Map local ``cited_items`` to the wire ``Citation`` shape. + + Local entries (from ``events.agent_end._resolve_cited_items``) carry + ``{id, kind, title, real_id}``; reflexio's ``InteractionData.citations`` + wants ``{kind, real_id, tag, title}`` where ``tag`` is the rank id + (``s1-301``-style) we already keep under ``id``. Entries without a + ``real_id`` (unresolved injections) are dropped — the server can't + join them back to a stored row. + """ + if not isinstance(cited_items, list): + return [] + out: list[dict[str, str]] = [] + for item in cited_items: + if not isinstance(item, dict): + continue + real_id = item.get("real_id") + kind = item.get("kind") + if not isinstance(real_id, str) or not real_id: + continue + if kind not in _VALID_CITATION_KINDS: + continue + tag = item.get("id") + title = item.get("title") + out.append( + { + "kind": kind, + "real_id": real_id, + "tag": tag if isinstance(tag, str) else "", + "title": title if isinstance(title, str) else "", + } + ) + return out + + +def unpublished_slice( + records: Iterable[dict[str, Any]], +) -> tuple[int, list[dict[str, Any]]]: + """Split records into (last-published index, unpublished turn records). + + Walks the records in order, tracking the most recent ``published_up_to`` + marker and collecting turn records (anything with a ``role``) that come + after it. Tool records are folded into the closest following Assistant + turn's ``tools_used``. + + Returns: + tuple[int, list[dict]]: ``(published_up_to, interactions)``. The + integer is the watermark after which all turns are unpublished; + the list is formatted for ``InteractionData`` construction. + """ + published = 0 + pending_tools: list[dict[str, Any]] = [] + turns: list[dict[str, Any]] = [] + for idx, rec in enumerate(records): + if "published_up_to" in rec: + published = rec["published_up_to"] + pending_tools = [] + turns = [] + continue + if idx < published: + continue + role = rec.get("role") + if role == "Assistant_tool": + tool_input = rec.get("tool_input") or {} + tool_output = rec.get("tool_output") or "" + tool_entry: dict[str, Any] = { + "tool_name": rec.get("tool_name", ""), + "status": rec.get("status", "success"), + } + tool_data: dict[str, Any] = {} + if tool_input: + tool_data["input"] = { + k: _truncate_tool_data_field(v) for k, v in tool_input.items() + } + if tool_output: + tool_data["output"] = _truncate_tool_data_field(tool_output) + if tool_data: + tool_entry["tool_data"] = tool_data + pending_tools.append(tool_entry) + continue + if role in {"User", "Assistant"}: + turn = { + k: v for k, v in rec.items() if k not in {"role", "ts", "cited_items"} + } + turn["role"] = role + if role == "Assistant": + citations = _to_wire_citations(rec.get("cited_items")) + if citations: + turn["citations"] = citations + if pending_tools: + turn["tools_used"] = pending_tools + pending_tools = [] + turns.append(turn) + return published, turns diff --git a/reflexio/integrations/openclaw/plugin/tests/test_state.py b/reflexio/integrations/openclaw/plugin/tests/test_state.py new file mode 100644 index 00000000..f9bf3c87 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_state.py @@ -0,0 +1,141 @@ +"""Tests for openclaw_smart.state.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from openclaw_smart import state + + +@pytest.fixture(autouse=True) +def isolate_state_dir(monkeypatch, tmp_path): + """Each test gets its own state dir via OPENCLAW_SMART_STATE_DIR.""" + sessions = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(sessions)) + return sessions + + +def test_state_dir_honours_env(isolate_state_dir): + assert state.state_dir() == isolate_state_dir + + +def test_append_creates_file(isolate_state_dir): + state.append("sess1", {"role": "User", "content": "hi"}) + path = isolate_state_dir / "sess1.jsonl" + assert path.exists() + lines = path.read_text().strip().splitlines() + assert len(lines) == 1 + assert json.loads(lines[0])["content"] == "hi" + + +def test_read_all_returns_records(): + state.append("sess2", {"role": "User", "content": "a"}) + state.append("sess2", {"role": "Assistant", "content": "b"}) + records = state.read_all("sess2") + assert len(records) == 2 + assert records[0]["content"] == "a" + assert records[1]["content"] == "b" + + +def test_read_all_missing_file_returns_empty(): + assert state.read_all("never-existed") == [] + + +def test_read_all_skips_malformed_lines(isolate_state_dir): + isolate_state_dir.mkdir(parents=True, exist_ok=True) + path = isolate_state_dir / "broken.jsonl" + path.write_text( + '{"role": "User", "content": "good"}\nnot json\n{"role": "Assistant", "content": "also good"}\n' + ) + records = state.read_all("broken") + assert len(records) == 2 + assert records[0]["content"] == "good" + assert records[1]["content"] == "also good" + + +def test_unpublished_slice_returns_watermark_and_turns(): + state.append("s3", {"role": "User", "content": "1"}) + state.append("s3", {"role": "Assistant", "content": "2"}) + state.append("s3", {"published_up_to": 2}) + state.append("s3", {"role": "User", "content": "3"}) + state.append("s3", {"role": "Assistant", "content": "4"}) + records = state.read_all("s3") + watermark, turns = state.unpublished_slice(records) + assert watermark == 2 + contents = [t["content"] for t in turns] + assert contents == ["3", "4"] + + +def test_unpublished_slice_folds_tool_records(): + records = [ + {"role": "User", "content": "run ls"}, + { + "role": "Assistant_tool", + "tool_name": "Bash", + "tool_input": {"command": "ls"}, + "tool_output": "a.txt", + "status": "success", + }, + {"role": "Assistant", "content": "Done."}, + ] + _, turns = state.unpublished_slice(records) + assert len(turns) == 2 + assert turns[0]["role"] == "User" + assistant_turn = turns[1] + assert assistant_turn["role"] == "Assistant" + assert assistant_turn["tools_used"] == [ + { + "tool_name": "Bash", + "status": "success", + "tool_data": { + "input": {"command": "ls"}, + "output": "a.txt", + }, + } + ] + + +def test_unpublished_slice_truncates_long_tool_fields(): + long_value = "x" * 500 + records = [ + { + "role": "Assistant_tool", + "tool_name": "Edit", + "tool_input": {"new_string": long_value}, + "tool_output": "", + "status": "success", + }, + {"role": "Assistant", "content": "ok"}, + ] + _, turns = state.unpublished_slice(records) + truncated = turns[0]["tools_used"][0]["tool_data"]["input"]["new_string"] + assert len(truncated) == 256 + assert truncated == "x" * 256 + + +def test_append_injected_writes_registry(): + state.append_injected( + "s4", + [ + {"id": "s1-abcd", "kind": "skill", "title": "Test skill", "real_id": "abc"}, + {"id": "p1-efgh", "kind": "preference", "title": "Test pref", "real_id": "def"}, + ], + ) + registry = state.read_injected("s4") + assert set(registry) == {"s1-abcd", "p1-efgh"} + assert registry["s1-abcd"]["title"] == "Test skill" + + +def test_append_injected_noop_when_empty(): + state.append_injected("s5", []) + assert state.read_injected("s5") == {} + + +def test_read_injected_later_wins(): + state.append_injected("s6", [{"id": "s1-abcd", "title": "old"}]) + state.append_injected("s6", [{"id": "s1-abcd", "title": "new"}]) + registry = state.read_injected("s6") + assert registry["s1-abcd"]["title"] == "new" From 93e01835996143635991796b855044e6ea3181ea Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:46:47 +0000 Subject: [PATCH 12/53] feat(openclaw): port query_compose.py --- .../src/openclaw_smart/query_compose.py | 52 +++++++++++++++++++ .../plugin/tests/test_query_compose.py | 50 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_query_compose.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py new file mode 100644 index 00000000..075d6975 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py @@ -0,0 +1,52 @@ +"""Compose a reflexio search query from a before_tool_call payload. + +Deterministic — no LLM call — so the before_tool_call hook can stay inside +its latency budget. The output is fed to ``ReflexioClient.search(query=...)`` +(the unified ``/api/search`` endpoint, which fans out to user playbooks, +agent playbooks, and preferences server-side), which tokenizes via reflexio's +FTS5 sanitizer (OR-joined, stemmed) plus a vector-similarity leg. Short, +meaning-dense strings give the most selective hybrid ranking. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Mapping + +_MAX_SNIPPET_LEN = 400 + + +def from_tool_call(tool_name: str, tool_input: Mapping[str, Any]) -> str: + """Compose a search query from an openClaw before_tool_call payload. + + Args: + tool_name (str): Tool name as reported by openClaw (after + camelCase→snake_case translation, e.g. ``"Edit"``, ``"Bash"``). + tool_input (Mapping[str, Any]): The tool's input dict as delivered + by the hook payload (after camelCase translation). + + Returns: + str: A short query suitable for reflexio hybrid search, or ``""`` + when the tool is not one we compose for (caller should then + skip the search entirely). + """ + match tool_name: + case "Edit" | "Write" | "NotebookEdit": + return _from_file_edit(tool_input) + case "Bash": + return _from_bash(tool_input) + case _: + return "" + + +def _from_file_edit(tool_input: Mapping[str, Any]) -> str: + path = tool_input.get("file_path") or "" + snippet = tool_input.get("new_string") or tool_input.get("content") or "" + basename = Path(path).name if path else "" + return f"{basename} {snippet[:_MAX_SNIPPET_LEN]}".strip() + + +def _from_bash(tool_input: Mapping[str, Any]) -> str: + command = tool_input.get("command") or "" + first_line = command.splitlines()[0] if command else "" + return first_line[:_MAX_SNIPPET_LEN].strip() diff --git a/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py b/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py new file mode 100644 index 00000000..32c8b865 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py @@ -0,0 +1,50 @@ +"""Tests for openclaw_smart.query_compose.""" + +from __future__ import annotations + +from openclaw_smart import query_compose + + +def test_edit_tool_query_uses_basename_and_snippet(): + q = query_compose.from_tool_call( + "Edit", {"file_path": "src/auth.py", "new_string": "import oauth2"} + ) + assert "auth.py" in q + assert "import oauth2" in q + # Full directory path is dropped — only basename survives. + assert "src/" not in q + + +def test_write_tool_uses_content_fallback(): + q = query_compose.from_tool_call( + "Write", {"file_path": "notes.md", "content": "release plan"} + ) + assert "notes.md" in q + assert "release plan" in q + + +def test_bash_tool_query_uses_first_line(): + q = query_compose.from_tool_call( + "Bash", {"command": "pytest tests/\nexit 0"} + ) + assert "pytest tests/" in q + assert "exit 0" not in q + + +def test_empty_input_returns_empty_for_known_tool(): + assert query_compose.from_tool_call("Edit", {}) == "" + assert query_compose.from_tool_call("Bash", {}) == "" + + +def test_unknown_tool_returns_empty(): + assert query_compose.from_tool_call("UnknownTool", {"foo": "bar"}) == "" + + +def test_long_snippet_is_truncated(): + long_text = "a" * 1000 + q = query_compose.from_tool_call( + "Edit", {"file_path": "x.py", "new_string": long_text} + ) + # Basename + space + snippet truncated to 400 chars + assert "a" * 400 in q + assert len(q) <= len("x.py ") + 400 From d1de4dbe0cbdba0b13245df10fa432c807d4fd14 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:48:41 +0000 Subject: [PATCH 13/53] feat(openclaw): port stall_banner.py --- .../plugin/src/openclaw_smart/stall_banner.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py new file mode 100644 index 00000000..0a62e614 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py @@ -0,0 +1,61 @@ +"""Render the 1-line session_start banner for a credit/auth stall. + +The template branches on the stall reason; output goes through openClaw's +``prependContext`` so the model sees it once per stall event. +""" + +from __future__ import annotations + +from datetime import datetime +from typing import Literal + +StallReason = Literal["billing_error", "auth_error"] + +_DASHBOARD = "localhost:3001" + + +def render_banner(*, reason: str | None, reset_estimate: datetime | None) -> str: + """Format the session_start banner for the given stall reason. + + Args: + reason (str | None): ``"billing_error"`` or ``"auth_error"``. Other + values (including ``None``) yield an empty string so callers can + pass raw DB values safely. + reset_estimate (datetime | None): Best-effort credit reset time; + included in the billing-error banner when present. + + Returns: + str: A single-line banner, or ``""`` for unknown reasons. + """ + match reason: + case "billing_error": + if reset_estimate is None: + return ( + f"openclaw-smart: learning paused — Agent SDK credit " + f"exhausted. Details: {_DASHBOARD}" + ) + return ( + f"openclaw-smart: learning paused — Agent SDK credit " + f"exhausted (resets ~{_format_reset(reset_estimate)}). " + f"Details: {_DASHBOARD}" + ) + case "auth_error": + return ( + f"openclaw-smart: learning paused — please run /login. " + f"Details: {_DASHBOARD}" + ) + case _: + return "" + + +def _format_reset(value: datetime) -> str: + """Format a reset datetime as e.g. ``Jun 12 9:00`` for the banner. + + Args: + value (datetime): The reset time to format. + + Returns: + str: The banner-friendly representation, with the hour shown + without a leading zero (e.g. ``Jun 12 9:00``). + """ + return f"{value.strftime('%b %d')} {value.hour}:{value.minute:02d}" From a7d63bbb9ee7253dbe9454e4c5a04ef7178300f8 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:51:48 +0000 Subject: [PATCH 14/53] feat(openclaw): port reflexio_adapter.py with agent_version=openclaw Adapter wraps ReflexioClient with port 8081 default. agent_version comes from runtime.agent_version() which returns 'openclaw' so playbooks and searches roll up across all openClaw projects. --- .../src/openclaw_smart/reflexio_adapter.py | 335 ++++++++++++++++++ .../plugin/tests/test_reflexio_adapter.py | 192 ++++++++++ 2 files changed, 527 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py new file mode 100644 index 00000000..99c36203 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py @@ -0,0 +1,335 @@ +"""Thin wrapper over ``reflexio.ReflexioClient`` for openclaw-smart's read/write paths. + +Exists so hook handlers (a) don't import reflexio directly at module scope — +import failures shouldn't crash hooks — and (b) can be stubbed in tests. +""" + +from __future__ import annotations + +import logging +import os +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from typing import Any, Sequence + +from openclaw_smart import runtime + +_LOGGER = logging.getLogger(__name__) + +_ENV_URL = "REFLEXIO_URL" +_DEFAULT_URL = "http://localhost:8081/" +_SEARCH_MODE_HYBRID = "hybrid" # reflexio.models.config_schema.SearchMode.HYBRID +_UNIFIED_ENTITY_TYPES = ("profiles", "user_playbooks", "agent_playbooks") +_AGENT_PLAYBOOK_APPROVAL_STATUSES = ("pending", "approved") +_REJECTED_AGENT_PLAYBOOK_STATUS = "rejected" + + +@dataclass +class Adapter: + """Wraps the reflexio client and absorbs connection errors. + + All methods degrade to a neutral no-op return (empty list / False) on + connection failure so a missing or down reflexio server never crashes + an openClaw hook. + """ + + url: str = "" + + def __post_init__(self) -> None: + self.url = self.url or os.environ.get(_ENV_URL, _DEFAULT_URL) + self._client: Any | None = None + + # ----------------------------------------------------------------- + # Client lazy-initialization + # ----------------------------------------------------------------- + + def _get_client(self) -> Any | None: + """Return the ReflexioClient, or None if reflexio is unreachable/unimportable.""" + if self._client is not None: + return self._client + try: + from reflexio import ReflexioClient # type: ignore[import-not-found] + except ImportError as exc: + _LOGGER.debug("reflexio not importable: %s", exc) + return None + try: + self._client = ReflexioClient(url_endpoint=self.url) + except Exception as exc: # noqa: BLE001 — adapter must never raise. + _LOGGER.warning("Failed to construct ReflexioClient: %s", exc) + return None + return self._client + + # ----------------------------------------------------------------- + # Writes + # ----------------------------------------------------------------- + + def publish( + self, + *, + session_id: str, + project_id: str, + interactions: Sequence[dict[str, Any]], + force_extraction: bool = False, + skip_aggregation: bool = False, + ) -> bool: + """Publish buffered interactions to reflexio. Returns True on success.""" + if not interactions: + return True + client = self._get_client() + if client is None: + return False + try: + client.publish_interaction( + user_id=project_id, + interactions=list(interactions), + agent_version=runtime.agent_version(), + session_id=session_id, + wait_for_response=False, + force_extraction=force_extraction, + skip_aggregation=skip_aggregation, + ) + return True + except Exception as exc: # noqa: BLE001 + _LOGGER.warning("publish_interaction failed: %s", exc) + return False + + def apply_extraction_defaults(self, *, window_size: int, stride_size: int) -> bool: + """Push openclaw-smart's preferred extraction defaults to the reflexio server. + + Reads the current ``Config`` and only issues a ``set_config`` when the + server-side values differ, so steady state is a single cheap GET. + + Reflexio persists ``Config`` to disk, so once these values land they + survive backend restarts. The flip side: if an operator customizes + ``window_size``/``stride_size`` via the dashboard, this call will + overwrite those values back to the openclaw-smart defaults on the + next session_start. To change the defaults, edit the constants at + the call site in ``events/session_start.py``. + """ + client = self._get_client() + if client is None: + return False + try: + config = client.get_config() + if ( + getattr(config, "window_size", None) == window_size + and getattr(config, "stride_size", None) == stride_size + ): + return True + config.window_size = window_size + config.stride_size = stride_size + client.set_config(config) + return True + except Exception as exc: # noqa: BLE001 — adapter must never raise. + _LOGGER.warning("apply_extraction_defaults failed: %s", exc) + return False + + def apply_optimizer_defaults( + self, *, script_path: str, timeout_seconds: int = 300 + ) -> bool: + """Push openclaw-smart's shared skill optimizer defaults to reflexio. + + Idempotent compare-then-write: reads ``Config``, only issues a + ``set_config`` when the server-side values differ from the desired + dict below. Called unconditionally from session_start; the caller's + only escape hatch is ``OPENCLAW_SMART_ENABLE_OPTIMIZER=0``. + """ + client = self._get_client() + if client is None: + return False + try: + config = client.get_config() + opt = getattr(config, "playbook_optimizer_config", None) + if opt is None: + return False + + desired = { + "enabled": True, + "optimize_user_playbooks": False, + "optimize_agent_playbooks": True, + "auto_update_user_playbooks": True, + "min_commit_windows": 1, + "max_metric_calls": 15, + "assistant_script_path": script_path, + "assistant_script_args": [], + "webhook_url": None, + "webhook_timeout_seconds": timeout_seconds, + } + if all(getattr(opt, key, None) == value for key, value in desired.items()): + return True + for key, value in desired.items(): + setattr(opt, key, value) + client.set_config(config) + return True + except Exception as exc: # noqa: BLE001 — adapter must never raise. + _LOGGER.warning("apply_optimizer_defaults failed: %s", exc) + return False + + # ----------------------------------------------------------------- + # Stall-state reads/writes (used by session_start banner) + # ----------------------------------------------------------------- + + def fetch_stall_state(self) -> Any | None: + """Fetch the current learning-stall snapshot from reflexio.""" + client = self._get_client() + if client is None: + return None + try: + return client.get_stall_state() + except Exception as exc: # noqa: BLE001 + _LOGGER.debug("get_stall_state failed: %s", exc) + return None + + def mark_stall_notified(self) -> None: + """Idempotently flip ``notified_in_cc`` on the active stall row.""" + client = self._get_client() + if client is None: + return + try: + client.mark_stall_notified() + except Exception as exc: # noqa: BLE001 + _LOGGER.debug("mark_stall_notified failed: %s", exc) + + # ----------------------------------------------------------------- + # Broad reads (used by /show) + # ----------------------------------------------------------------- + + def fetch_user_playbooks(self, *, project_id: str, top_k: int = 10) -> list[Any]: + """Fetch CURRENT user playbooks for ``project_id``.""" + client = self._get_client() + if client is None: + return [] + try: + response = client.search_user_playbooks( + user_id=project_id, + status_filter=[None], # None => CURRENT in reflexio's filter API + top_k=top_k, + ) + except Exception as exc: # noqa: BLE001 + _LOGGER.debug("search_user_playbooks failed: %s", exc) + return [] + return _extract_items(response, "user_playbooks") + + def fetch_agent_playbooks(self, top_k: int = 10) -> list[Any]: + """Fetch CURRENT agent playbooks globally (shared across projects). + + Filter by ``agent_version`` so we only pull in playbooks produced by + openClaw sessions. + """ + client = self._get_client() + if client is None: + return [] + try: + response = client.search_agent_playbooks( + agent_version=runtime.agent_version(), + status_filter=[None], + top_k=top_k, + ) + except Exception as exc: # noqa: BLE001 + _LOGGER.debug("search_agent_playbooks failed: %s", exc) + return [] + return _filter_rejected_agent_playbooks( + _extract_items(response, "agent_playbooks") + ) + + def fetch_project_profiles(self, project_id: str, top_k: int = 20) -> list[Any]: + """Fetch preferences extracted for this project (across sessions).""" + client = self._get_client() + if client is None: + return [] + try: + response = client.search_user_profiles( + user_id=project_id, + query="", + top_k=top_k, + ) + except Exception as exc: # noqa: BLE001 + _LOGGER.debug("search_user_profiles failed: %s", exc) + return [] + return _extract_items(response, "user_profiles") + + # ----------------------------------------------------------------- + # Query-aware unified search (used by before_tool_call / before_prompt_build) + # ----------------------------------------------------------------- + + def search_all( + self, *, project_id: str, query: str, top_k: int = 5 + ) -> tuple[list[Any], list[Any], list[Any]]: + """Unified hybrid search → ``(user_playbooks, agent_playbooks, preferences)``.""" + client = self._get_client() + if client is None: + return [], [], [] + try: + response = client.search( + query=query, + user_id=project_id, + agent_version=runtime.agent_version(), + entity_types=list(_UNIFIED_ENTITY_TYPES), + agent_playbook_status_filter=list(_AGENT_PLAYBOOK_APPROVAL_STATUSES), + enable_agent_answer=False, + top_k=top_k, + search_mode=_SEARCH_MODE_HYBRID, + ) + except Exception as exc: # noqa: BLE001 + _LOGGER.debug("unified search failed: %s", exc) + return [], [], [] + return ( + _extract_items(response, "user_playbooks"), + _filter_rejected_agent_playbooks( + _extract_items(response, "agent_playbooks") + ), + _extract_items(response, "profiles"), + ) + + # ----------------------------------------------------------------- + # Broad fetch for explicit audit views (no query → can't use unified /api/search) + # ----------------------------------------------------------------- + + def fetch_all( + self, + *, + project_id: str, + user_playbook_top_k: int = 10, + agent_playbook_top_k: int = 10, + profile_top_k: int = 20, + ) -> tuple[list[Any], list[Any], list[Any]]: + """Parallel broad fetch for /show → ``(user_playbooks, agent_playbooks, preferences)``.""" + with ThreadPoolExecutor(max_workers=3) as pool: + up_future = pool.submit( + self.fetch_user_playbooks, + project_id=project_id, + top_k=user_playbook_top_k, + ) + ap_future = pool.submit(self.fetch_agent_playbooks, agent_playbook_top_k) + pr_future = pool.submit( + self.fetch_project_profiles, project_id, profile_top_k + ) + return up_future.result(), ap_future.result(), pr_future.result() + + +def _extract_items(response: Any, field: str) -> list[Any]: + """Pull a list field from a reflexio response object or dict, tolerating shape drift.""" + if response is None: + return [] + if isinstance(response, dict): + value = response.get(field) + else: + value = getattr(response, field, None) + return list(value) if value else [] + + +def _filter_rejected_agent_playbooks(items: list[Any]) -> list[Any]: + """Drop rejected shared skills defensively, even if an older backend ignores filters.""" + return [ + item + for item in items + if _agent_playbook_status(item) != _REJECTED_AGENT_PLAYBOOK_STATUS + ] + + +def _agent_playbook_status(item: Any) -> str: + if isinstance(item, dict): + value = item.get("playbook_status") + else: + value = getattr(item, "playbook_status", None) + return str(value or "").lower() diff --git a/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py b/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py new file mode 100644 index 00000000..f85a01ff --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py @@ -0,0 +1,192 @@ +"""Tests for openclaw_smart.reflexio_adapter.""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from openclaw_smart.reflexio_adapter import Adapter + + +def test_default_url_is_8081(): + adapter = Adapter() + assert adapter.url == "http://localhost:8081/" + + +def test_env_var_overrides_url(monkeypatch): + monkeypatch.setenv("REFLEXIO_URL", "http://example.com:9000/") + adapter = Adapter() + assert adapter.url == "http://example.com:9000/" + + +def test_explicit_url_wins_over_env(monkeypatch): + monkeypatch.setenv("REFLEXIO_URL", "http://example.com:9000/") + adapter = Adapter(url="http://other:7000/") + assert adapter.url == "http://other:7000/" + + +def test_get_client_returns_none_on_construction_failure(): + adapter = Adapter() + with patch( + "openclaw_smart.reflexio_adapter.ReflexioClient", + side_effect=ConnectionError, + create=True, + ): + # ReflexioClient is imported lazily inside _get_client; we patch on + # the imported module to mimic that path. + pass + with patch.dict( + "sys.modules", + {"reflexio": MagicMock(ReflexioClient=MagicMock(side_effect=ConnectionError))}, + ): + assert adapter._get_client() is None + + +def test_publish_returns_true_for_empty_interactions(): + adapter = Adapter() + assert ( + adapter.publish(session_id="s", project_id="p", interactions=[]) is True + ) + + +def test_publish_returns_false_when_client_none(): + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=None): + assert ( + adapter.publish( + session_id="s", + project_id="p", + interactions=[{"role": "User", "content": "x"}], + ) + is False + ) + + +def test_publish_passes_openclaw_agent_version(): + fake_client = MagicMock() + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + adapter.publish( + session_id="s1", + project_id="proj", + interactions=[{"role": "User", "content": "x"}], + ) + kwargs = fake_client.publish_interaction.call_args[1] + assert kwargs["agent_version"] == "openclaw" + assert kwargs["user_id"] == "proj" + assert kwargs["session_id"] == "s1" + assert kwargs["wait_for_response"] is False + + +def test_publish_forwards_force_extraction(): + fake_client = MagicMock() + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + adapter.publish( + session_id="s1", + project_id="p", + interactions=[{"role": "User"}], + force_extraction=True, + skip_aggregation=True, + ) + kwargs = fake_client.publish_interaction.call_args[1] + assert kwargs["force_extraction"] is True + assert kwargs["skip_aggregation"] is True + + +def test_publish_returns_false_on_exception(): + fake_client = MagicMock() + fake_client.publish_interaction.side_effect = RuntimeError("boom") + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + assert ( + adapter.publish( + session_id="s", + project_id="p", + interactions=[{"role": "User"}], + ) + is False + ) + + +def test_search_all_degrades_to_empty(): + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=None): + u, a, p = adapter.search_all(project_id="p", query="q", top_k=3) + assert u == [] and a == [] and p == [] + + +def test_search_all_passes_agent_version(): + fake_client = MagicMock() + fake_client.search.return_value = MagicMock( + user_playbooks=[], agent_playbooks=[], profiles=[] + ) + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + adapter.search_all(project_id="p", query="q", top_k=5) + kwargs = fake_client.search.call_args[1] + assert kwargs["agent_version"] == "openclaw" + assert kwargs["user_id"] == "p" + + +def test_fetch_user_playbooks_degrades_to_empty(): + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=None): + assert adapter.fetch_user_playbooks(project_id="p") == [] + + +def test_fetch_agent_playbooks_filters_rejected(): + fake_client = MagicMock() + fake_client.search_agent_playbooks.return_value = MagicMock( + agent_playbooks=[ + {"id": "a", "playbook_status": "approved"}, + {"id": "b", "playbook_status": "rejected"}, + {"id": "c", "playbook_status": "pending"}, + ] + ) + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + result = adapter.fetch_agent_playbooks(top_k=5) + ids = [item["id"] for item in result] + assert "b" not in ids + assert "a" in ids and "c" in ids + + +def test_apply_extraction_defaults_skips_when_already_matching(): + fake_client = MagicMock() + config = MagicMock(window_size=10, stride_size=5) + fake_client.get_config.return_value = config + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + assert ( + adapter.apply_extraction_defaults(window_size=10, stride_size=5) is True + ) + fake_client.set_config.assert_not_called() + + +def test_apply_extraction_defaults_writes_when_different(): + fake_client = MagicMock() + config = MagicMock(window_size=99, stride_size=99) + fake_client.get_config.return_value = config + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + adapter.apply_extraction_defaults(window_size=10, stride_size=5) + assert config.window_size == 10 + assert config.stride_size == 5 + fake_client.set_config.assert_called_once_with(config) + + +def test_fetch_stall_state_returns_none_when_client_none(): + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=None): + assert adapter.fetch_stall_state() is None + + +def test_mark_stall_notified_swallows_errors(): + fake_client = MagicMock() + fake_client.mark_stall_notified.side_effect = RuntimeError + adapter = Adapter() + with patch.object(adapter, "_get_client", return_value=fake_client): + # Must not raise + adapter.mark_stall_notified() From 9abeca0a85efa69efb550936c45c37ba2f4eb4ba Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:54:41 +0000 Subject: [PATCH 15/53] feat(openclaw): port oc_cite.py with [oc:] marker prefix Citation tags use [oc:s1-ab12] / [oc:p1-cd34] format. CITATION_INSTRUCTION references 'openclaw-smart learning applied' marker line. Bin script will live at plugin/bin/oc-cite (Phase 7). --- .../plugin/src/openclaw_smart/oc_cite.py | 196 ++++++++++++++++++ .../openclaw/plugin/tests/test_oc_cite.py | 88 ++++++++ 2 files changed, 284 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py new file mode 100644 index 00000000..d8602658 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py @@ -0,0 +1,196 @@ +"""Support helpers for openclaw-smart citation tracking. + +Context injected by before_prompt_build / before_tool_call tags each skill +and preference bullet with a rank-based id fingerprinted by the underlying +real id (``[oc:s1-1a2b]`` for the first skill whose ``user_playbook_id`` +starts with ``1a2b``, ``[oc:p2-c3d4]`` for the second preference). The +injected instruction asks the assistant to end impactful replies with a +marker like:: + + ✨ 1 openclaw-smart learning applied [oc:s1-1a2b] + +The agent_end hook later scans the assistant text for those markers and +resolves the ids against a per-session registry persisted at +``~/.openclaw-smart/sessions/.injected.jsonl``. Legacy +``oc-cite`` Bash tool calls are still accepted as a fallback for older +instructions. + +Why rank + fingerprint: rank alone resets at every injection, so a later +injection's ``s1`` would silently overwrite an earlier entry in the +append-only registry — if openClaw cited ``s1`` across a turn boundary, +the resolver would pick the wrong skill. Appending the first four +alphanumeric chars of the real id makes the id stable across injections +in the common case (distinct real ids → distinct fingerprints), so +cross-injection collisions become rare. + +This module holds: + +- ``rank_id``: ``p{n}-{fp}`` / ``s{n}-{fp}`` tag for a given (kind, rank, + real_id) tuple. Fingerprint is omitted when no real id is available. + ``p`` is preference, ``s`` is skill. +- ``CITATION_CMD_RE``: regex matching a valid legacy ``oc-cite`` command line. +- ``ensure_installed``: idempotent copy of ``plugin/bin/oc-cite`` to + ``~/.openclaw-smart/bin/oc-cite`` with the executable bit set. +- ``CITATION_INSTRUCTION``: the trailer text appended to injected context + so the assistant knows when and how to emit the citation marker. +""" + +from __future__ import annotations + +import logging +import re +import shutil +import stat as stat_ +from pathlib import Path +from typing import Any + +_LOGGER = logging.getLogger(__name__) + +# plugin/src/openclaw_smart/oc_cite.py -> parents[2] is plugin/ +_THIS_DIR = Path(__file__).resolve().parent +_PLUGIN_ROOT = _THIS_DIR.parents[2] +_SOURCE_SCRIPT = _PLUGIN_ROOT / "bin" / "oc-cite" +_INSTALL_DIR = Path.home() / ".openclaw-smart" / "bin" +INSTALL_PATH = _INSTALL_DIR / "oc-cite" + +_FINGERPRINT_LEN = 4 + +# Match a bare `oc-cite ` invocation. Ids are rank tokens of the form +# `p` (preference) or `s` (skill) with an optional `-` +# fingerprint (1-4 alphanumeric chars), optionally `oc:`-prefixed (since +# bullets render as `[oc:p1-ab12]` and the model often copies the tag +# verbatim). The `(?i:...)` inline flags make the prefix, kind letter, +# and fingerprint case-insensitive so `OC:P1-AB12` is accepted. Tokens +# may be comma- and/or whitespace-separated. Chained commands (&&, |, ;) +# and extra trailing tokens remain rejected by the anchored `\s*$` +# terminator so accidental mentions don't register as citations. +_ID_TOKEN = r"(?i:oc:)?(?i:[ps])\d+(?:-(?i:[a-z0-9]){1,4})?" +_ID_SEP = r"[,\s]+" +CITATION_CMD_RE = re.compile( + rf"^\s*(?:[^\s]*/)?oc-cite\s+({_ID_TOKEN}(?:{_ID_SEP}{_ID_TOKEN})*)\s*$" +) +_CLEAN_ID_RE = re.compile(r"^(?i:oc:)?((?i:[ps])\d+(?:-(?i:[a-z0-9]){1,4})?)$") +_SPLIT_RE = re.compile(_ID_SEP) +_TEXT_CITATION_LINE_RE = re.compile( + r"(?im)^\s*✨\s+\d+\s+openclaw-smart learning(?:s)? applied\s+" + r"\[oc:(?P[^\]]+)\]\s*$" +) + +CITATION_INSTRUCTION = ( + "_First, fully answer the user — citation does not change what or how " + "you reply. Then, as a final step, consider whether to cite: if — and " + "only if — an injected `[oc:…]` item materially changed your reply " + "(different wording, action, or conclusion than you would have produced " + "without it), append exactly one final citation line after your answer. " + "Do not call `oc-cite` or any other tool for citations. Ids come verbatim " + "from the `[oc:…]` tags — keep the leading `p` (preference) or `s` " + "(skill) and the `-` suffix. Use this exact format for one id: " + "`✨ 1 openclaw-smart learning applied [oc:s1-ab12]`. Use this exact " + "format for multiple ids: `✨ 2 openclaw-smart learnings applied " + "[oc:s1-ab12,p2-cd34]`, where the number is the count of ids in the " + "brackets. Never emit a standalone wrapper like `✨s1-ab12✨` or " + "`✨abc123✨`; those are not openclaw-smart citations and cannot be " + "resolved. Default is to skip. If an item is merely on-topic, confirms " + "what you already planned, or your reply would read the same without it, " + "do not cite — end the turn normally with your reply. When unsure, skip. " + "Do not add any other text, tool calls, or role markers after the final " + "citation line._" +) + + +def _fingerprint(real_id: Any) -> str: + """Return the first ``_FINGERPRINT_LEN`` alphanumeric chars of ``real_id``.""" + if real_id is None: + return "" + return "".join(c for c in str(real_id).lower() if c.isalnum())[:_FINGERPRINT_LEN] + + +def rank_id(kind: str, rank: int, real_id: Any = None) -> str: + """Return the citation id for a skill or preference item. + + Format is ``{letter}{rank}-{fingerprint}`` where ``letter`` is ``p`` for + preferences and ``s`` for skills, ``rank`` is the 1-based position within + the current retrieval batch, and ``fingerprint`` is up to 4 alphanumeric + chars derived from ``real_id``. The fingerprint is omitted when no real + id is available (falling back to the rank form ``s1`` / ``p1``). + + Raises: + ValueError: If ``kind`` is not ``"profile"`` or ``"playbook"``. + """ + if kind == "profile": + prefix = "p" + elif kind == "playbook": + prefix = "s" + else: + raise ValueError(f"unknown citation kind: {kind!r}") + fp = _fingerprint(real_id) + return f"{prefix}{rank}-{fp}" if fp else f"{prefix}{rank}" + + +def parse_citation_command(command: str) -> list[str]: + """Extract citation ids from an ``oc-cite`` Bash command string. + + Returns an empty list when the command does not match the expected shape + (chained commands, extra arguments, or anything other than a bare + ``oc-cite `` invocation are rejected to avoid false positives from + accidental mentions). + """ + match = CITATION_CMD_RE.match(command or "") + if not match: + return [] + return _parse_id_tokens(match.group(1)) + + +def parse_text_citations(text: str) -> list[str]: + """Extract text-only citation ids from a final learning marker line. + + The parser intentionally only accepts lines containing the visual + ``openclaw-smart learning(s) applied`` marker, so ordinary references to + injected ``[oc:...]`` ids inside an answer do not count as citations. + When multiple matching lines exist, the last one wins because the + instruction requires the citation marker to be final. + """ + matches = list(_TEXT_CITATION_LINE_RE.finditer(text or "")) + if not matches: + return [] + return _parse_id_tokens(matches[-1].group("ids")) + + +def _parse_id_tokens(raw_ids: str) -> list[str]: + ids: list[str] = [] + for tok in _SPLIT_RE.split(raw_ids.strip()): + if clean := _CLEAN_ID_RE.match(tok): + ids.append(clean.group(1).lower()) + return ids + + +def ensure_installed() -> Path: + """Idempotently install ``oc-cite`` into ``~/.openclaw-smart/bin/``. + + Called from every before_tool_call / before_prompt_build inject, so we + short-circuit when the target file already exists with the executable + bit set — the steady-state path is one ``stat`` syscall instead of + mkdir + copy + stat + chmod. Keying on filesystem state (rather than a + module-level boolean) keeps test isolation working when tests + monkeypatch ``INSTALL_PATH`` to a fresh tmpdir. + + Never raises — filesystem errors are logged at DEBUG and the caller + proceeds with injection regardless (the citation feature degrades to + silent if the script is unreachable). + """ + try: + if ( + INSTALL_PATH.is_file() + and INSTALL_PATH.stat().st_mode & stat_.S_IXUSR + and _SOURCE_SCRIPT.is_file() + and INSTALL_PATH.read_bytes() == _SOURCE_SCRIPT.read_bytes() + ): + return INSTALL_PATH + _INSTALL_DIR.mkdir(parents=True, exist_ok=True) + if _SOURCE_SCRIPT.is_file(): + shutil.copy2(_SOURCE_SCRIPT, INSTALL_PATH) + mode = INSTALL_PATH.stat().st_mode + INSTALL_PATH.chmod(mode | stat_.S_IXUSR | stat_.S_IXGRP | stat_.S_IXOTH) + except OSError as exc: + _LOGGER.debug("oc-cite install failed: %s", exc) + return INSTALL_PATH diff --git a/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py b/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py new file mode 100644 index 00000000..780d4e26 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py @@ -0,0 +1,88 @@ +"""Tests for openclaw_smart.oc_cite.""" + +from __future__ import annotations + +from openclaw_smart import oc_cite + + +def test_rank_id_skill_with_fingerprint(): + tag = oc_cite.rank_id("playbook", 1, "abc12345") + assert tag == "s1-abc1" + + +def test_rank_id_preference_with_fingerprint(): + tag = oc_cite.rank_id("profile", 2, "DEAD-beef-1234") + assert tag == "p2-dead" + + +def test_rank_id_without_real_id_omits_fingerprint(): + assert oc_cite.rank_id("playbook", 3) == "s3" + assert oc_cite.rank_id("profile", 4) == "p4" + + +def test_rank_id_with_non_alnum_real_id(): + assert oc_cite.rank_id("playbook", 1, "---") == "s1" + + +def test_rank_id_rejects_unknown_kind(): + import pytest + + with pytest.raises(ValueError): + oc_cite.rank_id("something-else", 1) + + +def test_parse_citation_command_extracts_ids(): + cmd = "oc-cite s1-ab12 p2-cd34" + assert oc_cite.parse_citation_command(cmd) == ["s1-ab12", "p2-cd34"] + + +def test_parse_citation_command_accepts_prefix(): + assert oc_cite.parse_citation_command("oc-cite oc:s1-ab12") == ["s1-ab12"] + + +def test_parse_citation_command_rejects_chained_commands(): + assert oc_cite.parse_citation_command("oc-cite s1 && echo x") == [] + + +def test_parse_citation_command_rejects_path_with_args(): + # Path-prefixed binary is fine; extra trailing junk is not. + assert oc_cite.parse_citation_command("/usr/bin/oc-cite s1-ab12") == ["s1-ab12"] + assert oc_cite.parse_citation_command("oc-cite s1 garbage extra") == [] + + +def test_parse_text_citations_single(): + text = "Answer body...\n✨ 1 openclaw-smart learning applied [oc:s1-ab12]" + assert oc_cite.parse_text_citations(text) == ["s1-ab12"] + + +def test_parse_text_citations_multi(): + text = "Done.\n✨ 2 openclaw-smart learnings applied [oc:s1-ab12,p2-cd34]" + assert oc_cite.parse_text_citations(text) == ["s1-ab12", "p2-cd34"] + + +def test_parse_text_citations_no_marker_returns_empty(): + text = "Body that mentions [oc:s1-ab12] but no marker line." + assert oc_cite.parse_text_citations(text) == [] + + +def test_parse_text_citations_last_wins(): + text = ( + "✨ 1 openclaw-smart learning applied [oc:s1-aaaa]\n" + "more text\n" + "✨ 1 openclaw-smart learning applied [oc:s2-bbbb]" + ) + assert oc_cite.parse_text_citations(text) == ["s2-bbbb"] + + +def test_citation_instruction_uses_oc_prefix(): + assert "[oc:" in oc_cite.CITATION_INSTRUCTION + assert "[cs:" not in oc_cite.CITATION_INSTRUCTION + assert "openclaw-smart" in oc_cite.CITATION_INSTRUCTION + + +def test_ensure_installed_returns_install_path(monkeypatch, tmp_path): + monkeypatch.setattr(oc_cite, "_INSTALL_DIR", tmp_path / "bin") + monkeypatch.setattr(oc_cite, "INSTALL_PATH", tmp_path / "bin" / "oc-cite") + # Source script likely doesn't exist yet — should still return target path. + result = oc_cite.ensure_installed() + assert result == tmp_path / "bin" / "oc-cite" From 6a426bd57957809292bbfc54f447bb865ae880db Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:55:55 +0000 Subject: [PATCH 16/53] feat(openclaw): port context_format.py with [oc:] marker prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bullets render as '- [oc:s1-abc1] content' / '- [oc:p1-xyz7] content'. Project header reads 'openclaw-smart — project `name`'. --- .../src/openclaw_smart/context_format.py | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py new file mode 100644 index 00000000..2d3a2411 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py @@ -0,0 +1,223 @@ +"""Render reflexio preferences + skills as markdown for display or injection.""" + +from __future__ import annotations + +from typing import Any, Iterable + +from openclaw_smart import oc_cite + + +def _first_nonempty(*values: Any) -> str: + """Return the first truthy string value, or an empty string.""" + for v in values: + if isinstance(v, str) and v.strip(): + return v.strip() + return "" + + +def render( + *, + project_id: str, + user_playbooks: Iterable[Any], + agent_playbooks: Iterable[Any], + profiles: Iterable[Any], +) -> str: + """Render skills + preferences as full audit markdown. + + Empty sections are omitted. When all sections are empty, returns "". + """ + markdown, _ = render_with_registry( + project_id=project_id, + user_playbooks=user_playbooks, + agent_playbooks=agent_playbooks, + profiles=profiles, + ) + return markdown + + +def render_with_registry( + *, + project_id: str, + user_playbooks: Iterable[Any], + agent_playbooks: Iterable[Any], + profiles: Iterable[Any], +) -> tuple[str, list[dict[str, Any]]]: + """Variant of ``render`` that also returns the citation registry. + + Every skill and preference bullet is tagged with a short ``[oc:ID]`` + prefix. The registry maps those ids back to ``{id, kind, title, + content}`` entries so ``events.agent_end`` can resolve citations into + human-readable titles for the dashboard. + + Agent playbooks (cross-project, distilled) are listed before user + playbooks (this project's lessons) under one ``### Project-specific + skills`` heading. The model doesn't need to reason about the split. + """ + playbook_lines, playbook_entries = _format_combined_playbooks( + agent_playbooks=agent_playbooks, user_playbooks=user_playbooks + ) + profile_lines, profile_entries = _format_profiles(profiles) + if not playbook_lines and not profile_lines: + return "", [] + + sections: list[str] = [f"## openclaw-smart — project `{project_id}`"] + if playbook_lines: + sections.append("### Project-specific skills") + sections.extend(playbook_lines) + if profile_lines: + sections.append("### Project preferences") + sections.extend(profile_lines) + sections.append(oc_cite.CITATION_INSTRUCTION) + return "\n".join(sections) + "\n", playbook_entries + profile_entries + + +def render_inline( + *, + project_id: str, + user_playbooks: Iterable[Any], + agent_playbooks: Iterable[Any], + profiles: Iterable[Any], +) -> str: + """Render skills + preferences for mid-session injection. + + Same bullet format as ``render`` but with no top-level project header. + This block is injected just-in-time alongside an in-flight user prompt or + tool call, so the caller already has project context. + """ + markdown, _ = render_inline_with_registry( + project_id=project_id, + user_playbooks=user_playbooks, + agent_playbooks=agent_playbooks, + profiles=profiles, + ) + return markdown + + +def render_inline_with_registry( + *, + project_id: str, + user_playbooks: Iterable[Any], + agent_playbooks: Iterable[Any], + profiles: Iterable[Any], +) -> tuple[str, list[dict[str, Any]]]: + """Variant of ``render_inline`` that also returns the citation registry.""" + del project_id # kept for symmetry with ``render_with_registry``. + playbook_lines, playbook_entries = _format_combined_playbooks( + agent_playbooks=agent_playbooks, user_playbooks=user_playbooks + ) + profile_lines, profile_entries = _format_profiles(profiles) + if not playbook_lines and not profile_lines: + return "", [] + sections: list[str] = [] + if playbook_lines: + sections.append("### Relevant project-specific skills") + sections.extend(playbook_lines) + if profile_lines: + sections.append("### Relevant project preferences") + sections.extend(profile_lines) + sections.append(oc_cite.CITATION_INSTRUCTION) + return "\n".join(sections) + "\n", playbook_entries + profile_entries + + +def _format_combined_playbooks( + *, + agent_playbooks: Iterable[Any], + user_playbooks: Iterable[Any], +) -> tuple[list[str], list[dict[str, Any]]]: + """Render agent playbooks first, then user playbooks, with one shared rank counter.""" + lines: list[str] = [] + entries: list[dict[str, Any]] = [] + rank = 0 + for pb in agent_playbooks: + rank = _append_playbook_bullet( + pb, "agent_playbook_id", "agent_playbook", rank, lines, entries + ) + for pb in user_playbooks: + rank = _append_playbook_bullet( + pb, "user_playbook_id", "user_playbook", rank, lines, entries + ) + return lines, entries + + +def _append_playbook_bullet( + pb: Any, + id_field: str, + source_kind: str, + rank: int, + lines: list[str], + entries: list[dict[str, Any]], +) -> int: + content = _first_nonempty(_field(pb, "content")) + if not content: + return rank + rank += 1 + trigger = _first_nonempty(_field(pb, "trigger")) + rationale = _first_nonempty(_field(pb, "rationale")) + real_id = _field(pb, id_field) + item_id = oc_cite.rank_id("playbook", rank, real_id) + title = _title_from_content(content) + bullet = f"- [oc:{item_id}] {content}" + if trigger: + bullet += f" _(when: {trigger})_" + if rationale: + bullet += f" — *why:* {rationale}" + lines.append(bullet) + entries.append( + { + "id": item_id, + "kind": "playbook", + "title": title, + "content": content, + "real_id": str(real_id) if real_id is not None else None, + "source_kind": source_kind, + } + ) + return rank + + +def _format_profiles( + profiles: Iterable[Any], +) -> tuple[list[str], list[dict[str, Any]]]: + lines: list[str] = [] + entries: list[dict[str, Any]] = [] + rank = 0 + for p in profiles: + content = _first_nonempty(_field(p, "content")) + if not content: + continue + rank += 1 + real_id = _field(p, "profile_id") + item_id = oc_cite.rank_id("profile", rank, real_id) + title = _title_from_content(content) + lines.append(f"- [oc:{item_id}] {content}") + entries.append( + { + "id": item_id, + "kind": "profile", + "title": title, + "content": content, + "real_id": str(real_id) if real_id is not None else None, + } + ) + return lines, entries + + +def _title_from_content(content: str, limit: int = 80) -> str: + """Derive a compact human-readable title from a bullet's content.""" + text = content.strip() + if not text: + return "" + for terminator in (". ", "\n"): + idx = text.find(terminator) + if 0 < idx <= limit: + return text[:idx].rstrip() + if len(text) <= limit: + return text + return text[: limit - 1].rstrip() + "…" + + +def _field(obj: Any, name: str) -> Any: + """Read ``name`` from either an attribute or a dict key.""" + if isinstance(obj, dict): + return obj.get(name) + return getattr(obj, name, None) From f01fbfc51c5025952b769484ce79e8e197394480 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:57:37 +0000 Subject: [PATCH 17/53] feat(openclaw): port context_inject.py pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emits {"prependContext": markdown} envelope on stdout (per Phase 0 finding B2 — prependContext is per-turn, prependSystemContext is cached). TS shim translates this to the openClaw plugin-SDK return shape. Drops hook_event_name parameter; the TS shim knows the event. --- .../src/openclaw_smart/context_inject.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py new file mode 100644 index 00000000..fdea7c84 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py @@ -0,0 +1,79 @@ +"""Shared "search reflexio, render markdown, emit envelope" pipeline. + +before_tool_call and before_prompt_build both (a) run a query-aware reflexio +search, (b) render the hits with ``context_format.render_inline_with_registry``, +(c) persist the citation registry for the agent_end hook to resolve, and +(d) emit a ``prependContext`` envelope on stdout. The TS shim wraps stdout +and translates it to the openClaw plugin-SDK return shape. + +Per Phase 0 finding B2 we use ``prependContext`` (per-turn) rather than +``prependSystemContext`` (cached) so injected context refreshes every turn. +``before_tool_call`` is observe-only per finding Q1, so handlers there +should NOT call ``emit_context`` — they only append state. + +The caller remains responsible for handler-specific framing — see the two +call sites for the small policy differences. +""" + +from __future__ import annotations + +import json +import sys +import time + +from openclaw_smart import context_format, oc_cite, state +from openclaw_smart.reflexio_adapter import Adapter + + +def emit_context( + *, + session_id: str, + project_id: str, + query: str, + top_k: int, + adapter: Adapter | None = None, +) -> bool: + """Search reflexio, render hits, emit ``prependContext`` JSON on stdout. + + Args: + session_id (str): openClaw session id (``sessionKey``); used to scope + the per-session citation registry. + project_id (str): reflexio ``user_id`` for this repo. + query (str): Free-text query routed to reflexio's unified + ``/api/search`` endpoint, which fans out to user playbooks + (project-scoped), agent playbooks (global), and preferences + (project-scoped) server-side. + top_k (int): Cap on hits per collection. + adapter (Adapter | None): Injection seam for tests. A fresh + ``Adapter()`` is used when ``None``. + + Returns: + bool: ``True`` when markdown was emitted to stdout; ``False`` when + the search returned nothing to inject. + """ + user_playbooks, agent_playbooks, profiles = (adapter or Adapter()).search_all( + project_id=project_id, + query=query, + top_k=top_k, + ) + markdown, registry = context_format.render_inline_with_registry( + project_id=project_id, + user_playbooks=user_playbooks, + agent_playbooks=agent_playbooks, + profiles=profiles, + ) + if not markdown: + return False + + oc_cite.ensure_installed() + state.append_injected( + session_id, + (dict(entry, ts=int(time.time())) for entry in registry), + ) + + sys.stdout.write(json.dumps({"prependContext": markdown})) + sys.stdout.write("\n") + return True + + +__all__ = ["emit_context"] From 4802d694d58aaa100c385f343e42243d9c33b2fa Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 02:58:21 +0000 Subject: [PATCH 18/53] feat(openclaw): port publish.py --- .../plugin/src/openclaw_smart/publish.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py new file mode 100644 index 00000000..ee8e2c38 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py @@ -0,0 +1,72 @@ +"""Publish-to-reflexio orchestration used by agent_end, session_end, and the sync CLI. + +One helper — ``publish_unpublished`` — owns the read-buffer → slice → publish → +stamp-watermark sequence so the three call sites stay in sync. Returns a +``(status, interaction_count)`` tuple so callers can format appropriate +messaging without peeking at the adapter. +""" + +from __future__ import annotations + +from typing import Literal + +from openclaw_smart import state +from openclaw_smart.reflexio_adapter import Adapter + +PublishStatus = Literal["nothing", "ok", "failed"] + + +def publish_unpublished( + *, + session_id: str, + project_id: str, + force_extraction: bool, + skip_aggregation: bool, + adapter: Adapter | None = None, +) -> tuple[PublishStatus, int]: + """Drain the session buffer to reflexio and stamp the high-water mark. + + Args: + session_id (str): openClaw session id (``sessionKey``), attached to + each interaction. + project_id (str): Stable project name; used as reflexio's ``user_id`` + (preferences) so preferences accumulate at the project level + across sessions. ``agent_version`` is hardcoded to ``"openclaw"`` + in the adapter so skills roll up globally per agent rather than + per project. + force_extraction (bool): Whether to ask reflexio to run extraction + synchronously instead of queuing for the next sweep. + skip_aggregation (bool): When True, reflexio extracts preferences and + raw project-specific skill entries but skips the rollup into + shared skills. openclaw-smart passes False on every publish path + so ``user_playbooks`` roll up into ``agent_playbooks``; + aggregation additionally requires ``aggregation_config`` to be + set on reflexio's ``user_playbook_extractor_configs[0]`` and + ``optimize_agent_playbooks=true`` at the top level — otherwise + the rollup silently no-ops. + adapter (Adapter | None): Injection point for tests; a fresh + ``Adapter()`` is constructed when omitted. + + Returns: + tuple[PublishStatus, int]: ``("nothing", 0)`` if the buffer has no + unpublished turns, ``("ok", n)`` after a successful publish of + ``n`` interactions, or ``("failed", n)`` if reflexio rejected or + was unreachable. On ``"failed"`` the watermark is not advanced, + so the next hook retries the same batch. + """ + records = state.read_all(session_id) + _, interactions = state.unpublished_slice(records) + if not interactions: + return ("nothing", 0) + client = adapter if adapter is not None else Adapter() + ok = client.publish( + session_id=session_id, + project_id=project_id, + interactions=interactions, + force_extraction=force_extraction, + skip_aggregation=skip_aggregation, + ) + if ok: + state.append(session_id, {"published_up_to": len(records)}) + return ("ok", len(interactions)) + return ("failed", len(interactions)) From 0dfd9e4da05eb2bbe488c3bf56bf865f8f3f7705 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:00:20 +0000 Subject: [PATCH 19/53] feat(openclaw): port optimizer_assistant.py Shells out to 'openclaw infer model run --prompt ... --json'. Unlike claude/codex, openClaw's one-shot completion has no tool access, so the optimizer evaluates rules purely from prompt context. Sets OPENCLAW_SMART_INTERNAL=1 to prevent hook recursion. --- .../src/openclaw_smart/optimizer_assistant.py | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py new file mode 100644 index 00000000..a7e25ca3 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py @@ -0,0 +1,271 @@ +"""Local-script assistant backend for Reflexio playbook optimization. + +Reflexio's ``LocalScriptAssistant`` sends one JSON payload on stdin and expects +one JSON object on stdout. This module bridges that protocol to a guarded +``openclaw infer model run`` subprocess so candidate playbooks can be evaluated +against the local model without re-entering openclaw-smart hooks. + +Compared to claude-smart's variant, openClaw's ``infer model run`` is a one-shot +completion (not a full agent with tool access), so we cannot grant read-only +tool calls during evaluation. The optimizer therefore evaluates rules purely +based on the prompt context. ``OPENCLAW_SMART_INTERNAL=1`` is set on the +subprocess env to short-circuit our own hooks (recursion guard). +""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess # noqa: S404 — subprocess is the integration point. +import sys +from typing import Any + +_CLI_TIMEOUT_SECONDS = 300 +ENV_CLI_PATH = "OPENCLAW_BIN" +ENV_DEFAULT_MODEL = "OPENCLAW_DEFAULT_MODEL" +ENV_TIMEOUT = "OPENCLAW_OPTIMIZER_TIMEOUT" + + +class OptimizerAssistantError(Exception): + """Raised for any local assistant protocol or openClaw CLI failure.""" + + +def main() -> int: + """Console-script entrypoint for ``openclaw-smart-optimizer-assistant``. + + Returns: + int: Exit code (0 on success, 1 on any error). + """ + try: + payload = _read_payload() + messages = _validated_list(payload, "messages") + playbooks = _validated_list(payload, "playbooks") + prompt, system_prompt = _build_prompt(messages, playbooks) + content = _run_openclaw_cli(prompt=prompt, system_prompt=system_prompt) + except Exception as exc: # noqa: BLE001 — script errors become LocalScript failures. + sys.stderr.write(f"{type(exc).__name__}: {exc}\n") + return 1 + + json.dump({"content": content}, sys.stdout, ensure_ascii=False) + sys.stdout.write("\n") + return 0 + + +def _read_payload() -> dict[str, Any]: + """Read and parse a JSON payload from stdin. + + Returns: + dict[str, Any]: Parsed payload. + + Raises: + OptimizerAssistantError: If stdin is not valid JSON or not an object. + """ + raw = sys.stdin.read() + try: + payload = json.loads(raw) + except json.JSONDecodeError as exc: + raise OptimizerAssistantError("stdin must be a JSON object") from exc + if not isinstance(payload, dict): + raise OptimizerAssistantError("stdin must be a JSON object") + return payload + + +def _validated_list(payload: dict[str, Any], field: str) -> list[Any]: + """Return ``payload[field]`` as a list, raising if missing or wrong type.""" + value = payload.get(field) + if not isinstance(value, list): + raise OptimizerAssistantError(f"payload.{field} must be a list") + return value + + +def _build_prompt(messages: list[Any], playbooks: list[Any]) -> tuple[str, str]: + """Build ``(prompt, system_prompt)`` from the optimizer payload. + + Args: + messages (list[Any]): Conversation messages from the optimizer. + playbooks (list[Any]): Candidate playbook rules to evaluate. + + Returns: + tuple[str, str]: ``(prompt, system_prompt)`` for the CLI call. + + Raises: + OptimizerAssistantError: If messages contain no content. + """ + normalized = [_normalize_message(message) for message in messages] + normalized = [message for message in normalized if message["content"]] + if not normalized: + raise OptimizerAssistantError("payload.messages must contain content") + + final_message = normalized[-1] + prior_messages = normalized[:-1] + + system_sections = [_render_playbooks(playbooks)] + existing_system = [ + message["content"] for message in normalized if message["role"] == "system" + ] + if existing_system: + system_sections.append( + "## Existing system context\n" + "\n\n".join(existing_system) + ) + + prior_dialogue = [ + message + for message in prior_messages + if message["role"] in {"user", "assistant"} + ] + if prior_dialogue: + system_sections.append( + "## Conversation so far\n" + _render_transcript(prior_dialogue) + ) + + prompt = final_message["content"] + if final_message["role"] != "user": + prompt = _render_transcript([final_message]) + system_prompt = "\n\n".join(section for section in system_sections if section) + return prompt, system_prompt + + +def _normalize_message(message: Any) -> dict[str, str]: + """Coerce a raw message dict to ``{role, content}`` with safe defaults.""" + if not isinstance(message, dict): + raise OptimizerAssistantError("each message must be an object") + role = str(message.get("role") or "user").strip().lower() + if role not in {"user", "assistant", "system"}: + role = "user" + content = message.get("content") + if not isinstance(content, str): + raise OptimizerAssistantError("each message.content must be a string") + return {"role": role, "content": content.strip()} + + +def _render_playbooks(playbooks: list[Any]) -> str: + """Render the candidate playbook rules as a markdown section.""" + if not playbooks: + return "" + lines = ["## Candidate playbook rules"] + for index, playbook in enumerate(playbooks, start=1): + if not isinstance(playbook, dict): + raise OptimizerAssistantError("each playbook must be an object") + content = playbook.get("content") + if not isinstance(content, str) or not content.strip(): + raise OptimizerAssistantError("each playbook.content must be a string") + trigger = playbook.get("trigger") + suffix = "" + if isinstance(trigger, str) and trigger.strip(): + suffix = f" (when: {trigger.strip()})" + lines.append(f"{index}. {content.strip()}{suffix}") + return "\n".join(lines) + + +def _render_transcript(messages: list[dict[str, str]]) -> str: + """Render a list of normalized messages as a ``Role: content`` transcript.""" + labels = {"user": "User", "assistant": "Assistant", "system": "System"} + return "\n\n".join( + f"{labels.get(message['role'], 'User')}: {message['content']}" + for message in messages + ) + + +def _resolve_cli_path() -> str | None: + """Return the openclaw binary path, honoring ``OPENCLAW_BIN`` then PATH.""" + override = os.environ.get(ENV_CLI_PATH) + if override and os.path.isfile(override) and os.access(override, os.X_OK): + return override + return shutil.which("openclaw") + + +def _run_openclaw_cli(*, prompt: str, system_prompt: str) -> str: + """Invoke ``openclaw infer model run`` and return the completion text. + + Args: + prompt (str): User-facing portion of the prompt. + system_prompt (str): System context (playbook rules + transcript) + concatenated into the same prompt — openclaw does not have a + separate ``--system-prompt`` flag. + + Returns: + str: Completion text extracted from the CLI's JSON stdout. + + Raises: + OptimizerAssistantError: On missing CLI, timeout, non-zero exit, or + unparseable JSON output. + """ + cli = _resolve_cli_path() + if not cli: + raise OptimizerAssistantError("openclaw CLI not found; set OPENCLAW_BIN") + + full_prompt = _join_prompt(prompt=prompt, system_prompt=system_prompt) + argv = [cli, "infer", "model", "run", "--prompt", full_prompt, "--json"] + model = os.environ.get(ENV_DEFAULT_MODEL) + if model: + argv += ["--model", model] + + env = os.environ.copy() + env["OPENCLAW_SMART_INTERNAL"] = "1" + timeout_s = int(os.environ.get(ENV_TIMEOUT, str(_CLI_TIMEOUT_SECONDS))) + + try: + proc = subprocess.run( # noqa: S603 — argv is fully constructed. + argv, + env=env, + capture_output=True, + text=True, + timeout=timeout_s, + check=False, + ) + except subprocess.TimeoutExpired as exc: + raise OptimizerAssistantError( + f"openclaw CLI timed out after {timeout_s}s" + ) from exc + except FileNotFoundError as exc: + raise OptimizerAssistantError("openclaw CLI not found on PATH") from exc + + if proc.returncode != 0: + stderr = proc.stderr.strip() + raise OptimizerAssistantError( + f"openclaw CLI exited {proc.returncode}: {stderr[:500]}" + ) + + try: + data = json.loads(proc.stdout) + except json.JSONDecodeError as exc: + raise OptimizerAssistantError( + f"openclaw CLI returned non-JSON: {proc.stdout[:200]}" + ) from exc + if not isinstance(data, dict): + raise OptimizerAssistantError("openclaw CLI JSON output must be an object") + + content = _extract_content(data) + if not content: + raise OptimizerAssistantError( + f"openclaw CLI JSON had no completion text: {list(data)[:5]}" + ) + return content + + +def _join_prompt(*, prompt: str, system_prompt: str) -> str: + """Concatenate system context above the user prompt.""" + if not system_prompt: + return prompt + return f"{system_prompt}\n\n## Task\n{prompt}" + + +def _extract_content(payload: dict[str, Any]) -> str: + """Pull the completion text from openclaw's JSON output. + + The exact field name in ``openclaw infer model run --json`` output is not + pinned at design time; scan common keys in priority order. + """ + for key in ("text", "content", "output", "result", "response"): + value = payload.get(key) + if isinstance(value, str) and value: + return value + msg = payload.get("message") + if isinstance(msg, dict) and isinstance(msg.get("content"), str): + return msg["content"] + return "" + + +if __name__ == "__main__": + raise SystemExit(main()) From 9da209b08d1b974be04395f106438d372003f6cb Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:03:46 +0000 Subject: [PATCH 20/53] feat(openclaw/events): port session_start handler Reads sessionKey (or sessionId), pushes extraction (5/3) and optimizer defaults to reflexio, emits {"prependContext": banner} if a learning stall is active. Falls back silently when nothing to inject. Adds openclaw-smart-optimizer-assistant console script. --- .../openclaw/plugin/pyproject.toml | 1 + .../openclaw_smart/events/session_start.py | 130 ++++++++++++++++++ .../plugin/tests/test_events_session_start.py | 110 +++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py diff --git a/reflexio/integrations/openclaw/plugin/pyproject.toml b/reflexio/integrations/openclaw/plugin/pyproject.toml index e6873041..0681dbb5 100644 --- a/reflexio/integrations/openclaw/plugin/pyproject.toml +++ b/reflexio/integrations/openclaw/plugin/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ [project.scripts] openclaw-smart-hook = "openclaw_smart.hook:main" openclaw-smart = "openclaw_smart.cli:main" +openclaw-smart-optimizer-assistant = "openclaw_smart.optimizer_assistant:main" [tool.uv.sources] # This plugin lives inside the reflexio repo; depend on the local reflexio diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py new file mode 100644 index 00000000..d08b9eb4 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py @@ -0,0 +1,130 @@ +"""session_start hook — apply startup defaults without broad memory retrieval. + +For openClaw, the ``session_start`` event delivers ``{sessionId, sessionKey, +agentId, workspaceDir}``. The handler: + +1. Fetches the current stall state from reflexio and renders a 1-line banner + to ``prependContext`` if learning is paused. +2. Pushes openclaw-smart's preferred extraction defaults (smaller window / + stride than reflexio's out-of-box 10/5). +3. Pushes optimizer defaults so reflexio can evaluate candidate playbooks + via the local ``openclaw infer model run`` CLI. + +Output: when there is a banner, emits ``{"prependContext": ""}`` on +stdout for the TS shim to relay back to openClaw. Otherwise emits nothing. +""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path +from typing import Any + +from openclaw_smart.reflexio_adapter import Adapter +from openclaw_smart.stall_banner import render_banner + +# openclaw-smart's preferred extraction cadence — more frequent, smaller +# windows than reflexio's out-of-box 10/5. Applied idempotently to the +# reflexio server on every session_start via +# Adapter.apply_extraction_defaults. +_WINDOW_SIZE = 5 +_STRIDE_SIZE = 3 +# Optimizer is on by default. Set this env var to "0" to skip pushing the +# openclaw-smart optimizer defaults on session_start (kill switch). +_DISABLE_OPTIMIZER_ENV = "OPENCLAW_SMART_ENABLE_OPTIMIZER" +_OPTIMIZER_TIMEOUT_SECONDS = 300 + + +def _adapter() -> Adapter: + """Construct the reflexio adapter for this hook invocation. + + Indirected through a factory so tests can monkeypatch the adapter + construction without touching the ``Adapter`` class itself. + + Returns: + Adapter: A fresh adapter bound to the current process env. + """ + return Adapter() + + +def _stall_banner(adapter: Any) -> str: + """Return the prepend-able stall banner, or "" if no banner should fire. + + Reads ``adapter.fetch_stall_state()``; if it reports an active, + not-yet-notified stall, renders a one-line banner via + ``stall_banner.render_banner``. All exceptions are absorbed: this is + defense-in-depth — even though the hook dispatcher already wraps + ``handle`` in try/except, a stall-path bug must never block the + existing playbook/profile rendering. + + Args: + adapter (Any): The adapter to query. Duck-typed so tests can stub. + + Returns: + str: The banner text, or ``""`` when there is nothing to show. + """ + try: + state_obj = adapter.fetch_stall_state() + except Exception: # noqa: BLE001 — stall path must never crash the hook. + return "" + if state_obj is None: + return "" + if not getattr(state_obj, "stalled", False): + return "" + if getattr(state_obj, "notified_in_cc", False): + return "" + try: + return render_banner( + reason=getattr(state_obj, "reason", None), + reset_estimate=getattr(state_obj, "reset_estimate", None), + ) + except Exception: # noqa: BLE001 — render_banner bug must not block playbook injection. + return "" + + +def handle(payload: dict[str, Any]) -> None: + """Handle a session_start hook payload. + + Args: + payload (dict[str, Any]): openClaw event/ctx blob, expected to + contain ``sessionKey`` (or ``sessionId``). + """ + session_id = payload.get("sessionKey") or payload.get("sessionId") + if not session_id: + return + + adapter = _adapter() + + # Stall banner — emitted via prependContext, fires at most once per stall + # event (controlled server-side via mark_stall_notified). + banner = _stall_banner(adapter) + + adapter.apply_extraction_defaults( + window_size=_WINDOW_SIZE, + stride_size=_STRIDE_SIZE, + ) + if os.environ.get(_DISABLE_OPTIMIZER_ENV) != "0": + adapter.apply_optimizer_defaults( + script_path=_optimizer_assistant_path(), + timeout_seconds=_OPTIMIZER_TIMEOUT_SECONDS, + ) + + if not banner: + return + + sys.stdout.write(json.dumps({"prependContext": banner})) + sys.stdout.write("\n") + + try: + adapter.mark_stall_notified() + except Exception: # noqa: BLE001 — telemetry must not break session. + pass + + +def _optimizer_assistant_path() -> str: + """Return the absolute path to the openclaw-smart-optimizer-assistant binary.""" + executable = Path(sys.executable) + suffix = ".exe" if os.name == "nt" else "" + return str(executable.with_name(f"openclaw-smart-optimizer-assistant{suffix}")) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py b/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py new file mode 100644 index 00000000..096206b8 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py @@ -0,0 +1,110 @@ +"""Tests for openclaw_smart.events.session_start.""" + +from __future__ import annotations + +import json +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import pytest + +from openclaw_smart.events import session_start + + +@pytest.fixture +def fake_adapter(): + adapter = MagicMock() + adapter.fetch_stall_state.return_value = None + adapter.apply_extraction_defaults.return_value = True + adapter.apply_optimizer_defaults.return_value = True + return adapter + + +def test_handle_no_session_id_returns_silently(fake_adapter, capsys): + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({}) + assert capsys.readouterr().out == "" + fake_adapter.apply_extraction_defaults.assert_not_called() + + +def test_handle_applies_extraction_defaults(fake_adapter, capsys): + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({"sessionKey": "s1", "workspaceDir": "/tmp"}) + fake_adapter.apply_extraction_defaults.assert_called_once_with( + window_size=5, stride_size=3 + ) + + +def test_handle_applies_optimizer_defaults_by_default(fake_adapter, monkeypatch): + monkeypatch.delenv("OPENCLAW_SMART_ENABLE_OPTIMIZER", raising=False) + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({"sessionKey": "s1"}) + fake_adapter.apply_optimizer_defaults.assert_called_once() + kwargs = fake_adapter.apply_optimizer_defaults.call_args[1] + assert "openclaw-smart-optimizer-assistant" in kwargs["script_path"] + + +def test_handle_skips_optimizer_when_disabled(fake_adapter, monkeypatch): + monkeypatch.setenv("OPENCLAW_SMART_ENABLE_OPTIMIZER", "0") + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({"sessionKey": "s1"}) + fake_adapter.apply_optimizer_defaults.assert_not_called() + + +def test_handle_emits_banner_when_stall_active(fake_adapter, capsys): + fake_adapter.fetch_stall_state.return_value = SimpleNamespace( + stalled=True, + notified_in_cc=False, + reason="auth_error", + reset_estimate=None, + ) + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({"sessionKey": "s1"}) + out = capsys.readouterr().out.strip() + assert out, "expected stdout envelope when stall is active" + parsed = json.loads(out) + assert "prependContext" in parsed + assert "openclaw-smart" in parsed["prependContext"] + fake_adapter.mark_stall_notified.assert_called_once() + + +def test_handle_skips_banner_when_already_notified(fake_adapter, capsys): + fake_adapter.fetch_stall_state.return_value = SimpleNamespace( + stalled=True, + notified_in_cc=True, + reason="auth_error", + reset_estimate=None, + ) + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({"sessionKey": "s1"}) + assert capsys.readouterr().out == "" + fake_adapter.mark_stall_notified.assert_not_called() + + +def test_handle_skips_banner_when_not_stalled(fake_adapter, capsys): + fake_adapter.fetch_stall_state.return_value = SimpleNamespace( + stalled=False, + notified_in_cc=False, + reason=None, + reset_estimate=None, + ) + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({"sessionKey": "s1"}) + assert capsys.readouterr().out == "" + + +def test_handle_swallows_stall_state_exceptions(fake_adapter, capsys): + fake_adapter.fetch_stall_state.side_effect = RuntimeError("boom") + with patch.object(session_start, "_adapter", return_value=fake_adapter): + # Must not raise + session_start.handle({"sessionKey": "s1"}) + assert capsys.readouterr().out == "" + # Defaults still applied + fake_adapter.apply_extraction_defaults.assert_called_once() + + +def test_handle_falls_back_to_session_id_key(fake_adapter): + """If only ``sessionId`` is present (openClaw sometimes), it is used.""" + with patch.object(session_start, "_adapter", return_value=fake_adapter): + session_start.handle({"sessionId": "sess-x"}) + fake_adapter.apply_extraction_defaults.assert_called_once() From bca781725b4182613cee064b109e78ee60979b57 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:05:25 +0000 Subject: [PATCH 21/53] feat(openclaw/events): port before_prompt_build handler Reads sessionKey/sessionId + prompt + agentId/workspaceDir from payload, buffers User turn to JSONL, calls context_inject.emit_context which writes {"prependContext": markdown} to stdout. Per Phase 0 finding B2, uses prependContext (per-turn) not prependSystemContext (cached). --- .../events/before_prompt_build.py | 69 +++++++++++ .../tests/test_events_before_prompt_build.py | 114 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py new file mode 100644 index 00000000..084fe3dd --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py @@ -0,0 +1,69 @@ +"""before_prompt_build hook — buffer the user turn and inject matching context. + +Two responsibilities, in order: + +1. Buffer the prompt into the session JSONL (this is the sole source of + ``"User"`` role turns downstream — openClaw replays the rest of the + transcript via tool events, not before_prompt_build). +2. Use the prompt text as a search query against reflexio's preferences + + skills and emit the top hits as ``prependContext`` so the model sees + relevant rules before planning the response. + +The shared pipeline lives in ``context_inject.emit_context``. Retrieval is +best-effort: any failure from search (reflexio unreachable, HTTP timeout, +unexpected shape) is caught so the buffered-prompt behaviour is always +preserved. +""" + +from __future__ import annotations + +import logging +import time +from typing import Any + +from openclaw_smart import context_inject, ids, state + +_LOGGER = logging.getLogger(__name__) +_TOP_K = 3 + + +def handle(payload: dict[str, Any]) -> None: + """before_prompt_build dispatcher — buffers the prompt, then injects context. + + Args: + payload (dict[str, Any]): The openClaw event/ctx blob (merged). Expected + keys ``sessionKey`` (or ``sessionId``), ``prompt``, optionally + ``agentId`` and ``workspaceDir``. + + Returns: + None: Side effects only — appends to the session buffer and may + write a ``{"prependContext": ...}`` JSON document to stdout. + """ + session_id = payload.get("sessionKey") or payload.get("sessionId") + prompt = payload.get("prompt") or "" + if not session_id or not prompt: + return + + project_id = ids.resolve_project_id_with_fallback( + cwd=payload.get("workspaceDir"), + agent_id=payload.get("agentId"), + ) + state.append( + session_id, + { + "ts": int(time.time()), + "role": "User", + "content": prompt, + "user_id": project_id, + }, + ) + + try: + context_inject.emit_context( + session_id=session_id, + project_id=project_id, + query=prompt, + top_k=_TOP_K, + ) + except Exception as exc: # noqa: BLE001 — never break the user's turn + _LOGGER.debug("before_prompt_build context inject failed: %s", exc) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py b/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py new file mode 100644 index 00000000..5f051383 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py @@ -0,0 +1,114 @@ +"""Tests for openclaw_smart.events.before_prompt_build.""" + +from __future__ import annotations + +import json +from unittest.mock import patch + +import pytest + +from openclaw_smart import state +from openclaw_smart.events import before_prompt_build as bpb + + +@pytest.fixture(autouse=True) +def isolate_state_dir(monkeypatch, tmp_path): + sessions = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(sessions)) + return sessions + + +def test_handle_buffers_prompt(isolate_state_dir): + with patch( + "openclaw_smart.events.before_prompt_build.context_inject" + ) as ci, patch( + "openclaw_smart.events.before_prompt_build.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + ci.emit_context.return_value = False + bpb.handle( + { + "sessionKey": "s1", + "prompt": "implement OAuth", + "agentId": "agent-x", + "workspaceDir": "/tmp/x", + } + ) + + path = isolate_state_dir / "s1.jsonl" + assert path.exists() + lines = path.read_text().splitlines() + record = json.loads(lines[0]) + assert record["role"] == "User" + assert record["content"] == "implement OAuth" + assert record["user_id"] == "proj-x" + + +def test_handle_calls_emit_context_with_top_k(): + with patch( + "openclaw_smart.events.before_prompt_build.context_inject" + ) as ci, patch( + "openclaw_smart.events.before_prompt_build.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + bpb.handle( + { + "sessionKey": "s1", + "prompt": "implement OAuth", + "agentId": "agent-x", + "workspaceDir": "/tmp/x", + } + ) + ci.emit_context.assert_called_once() + kwargs = ci.emit_context.call_args[1] + assert kwargs["session_id"] == "s1" + assert kwargs["project_id"] == "proj-x" + assert kwargs["query"] == "implement OAuth" + assert kwargs["top_k"] == 3 + + +def test_handle_skips_when_no_session_id(isolate_state_dir): + with patch("openclaw_smart.events.before_prompt_build.context_inject") as ci: + bpb.handle({"prompt": "x", "agentId": "a"}) + ci.emit_context.assert_not_called() + assert not isolate_state_dir.exists() or not any(isolate_state_dir.iterdir()) + + +def test_handle_skips_when_no_prompt(isolate_state_dir): + with patch("openclaw_smart.events.before_prompt_build.context_inject") as ci: + bpb.handle({"sessionKey": "s1", "prompt": "", "agentId": "a"}) + ci.emit_context.assert_not_called() + + +def test_handle_swallows_emit_exceptions(isolate_state_dir): + with patch( + "openclaw_smart.events.before_prompt_build.context_inject" + ) as ci, patch( + "openclaw_smart.events.before_prompt_build.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + ci.emit_context.side_effect = ConnectionError("reflexio down") + # Must not raise + bpb.handle( + { + "sessionKey": "s1", + "prompt": "implement OAuth", + "agentId": "a", + "workspaceDir": "/tmp/x", + } + ) + # Prompt still buffered + path = isolate_state_dir / "s1.jsonl" + assert path.exists() + + +def test_handle_falls_back_to_session_id(): + with patch( + "openclaw_smart.events.before_prompt_build.context_inject" + ) as ci, patch( + "openclaw_smart.events.before_prompt_build.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + bpb.handle({"sessionId": "s2", "prompt": "hello", "agentId": "a"}) + ci.emit_context.assert_called_once() + assert ci.emit_context.call_args[1]["session_id"] == "s2" From 014de8476f67e6a5b54fa29f13e01a286ba278d9 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:06:50 +0000 Subject: [PATCH 22/53] feat(openclaw/events): port before_tool_call as observe-only stub Phase 0 finding Q1: openClaw's before_tool_call hook doesn't honor prependContext, so context injection would have no effect on the upcoming tool call. Handler is a silent no-op in v1; TS shim may choose not to register it at all. --- .../openclaw_smart/events/before_tool_call.py | 30 ++++++++++++++++ .../tests/test_events_before_tool_call.py | 35 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py new file mode 100644 index 00000000..06c96c41 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py @@ -0,0 +1,30 @@ +"""before_tool_call hook — observe-only stub. + +Phase 0 probe finding Q1: openClaw's ``before_tool_call`` hook does NOT +honor ``prependContext`` returned by plugins, so injecting context here +would have no effect on the upcoming tool call. (In contrast, Claude +Code's PreToolUse hook is the primary just-in-time injection point.) + +This handler is therefore a silent no-op in v1. The TS shim may choose +not to register it at all — having a stub here keeps the dispatcher +symmetric and leaves room for future observe-only telemetry. +""" + +from __future__ import annotations + +import logging +from typing import Any + +_LOGGER = logging.getLogger(__name__) + + +def handle(payload: dict[str, Any]) -> None: + """before_tool_call dispatcher — silent no-op. + + Args: + payload (dict[str, Any]): Unused. openClaw delivers + ``{toolName, params, sessionKey, agentId}`` here but we have no + actionable side effect to apply per Phase 0 finding Q1. + """ + del payload + _LOGGER.debug("before_tool_call invoked (observe-only stub)") diff --git a/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py b/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py new file mode 100644 index 00000000..25f5ab41 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py @@ -0,0 +1,35 @@ +"""Tests for openclaw_smart.events.before_tool_call (observe-only stub).""" + +from __future__ import annotations + +from openclaw_smart.events import before_tool_call as btc + + +def test_handle_is_silent(capsys): + btc.handle( + { + "sessionKey": "s1", + "tool_name": "Edit", + "tool_input": {"file_path": "x.py", "new_string": "..."}, + "agentId": "a", + } + ) + assert capsys.readouterr().out == "" + + +def test_handle_accepts_empty_payload(capsys): + btc.handle({}) + assert capsys.readouterr().out == "" + + +def test_handle_accepts_camelcase_payload(capsys): + """openClaw delivers camelCase fields; handler should ignore them silently.""" + btc.handle( + { + "sessionKey": "s1", + "toolName": "Bash", + "params": {"command": "ls"}, + "agentId": "a", + } + ) + assert capsys.readouterr().out == "" From 64caf92aba275f2e2515d90986b705613c0dfac0 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:08:27 +0000 Subject: [PATCH 23/53] feat(openclaw/events): port after_tool_call handler Translates openClaw's camelCase fields (toolName/params/result) to our snake_case schema (tool_name/tool_input/tool_output) via _extract_fields, which also accepts snake_case for resilience. Secret redaction and 4096-char output truncation preserved from claude-smart. --- .../openclaw_smart/events/after_tool_call.py | 152 ++++++++++++++++++ .../tests/test_events_after_tool_call.py | 143 ++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py new file mode 100644 index 00000000..2f7a8475 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py @@ -0,0 +1,152 @@ +"""after_tool_call hook — record the tool invocation and its outcome. + +openClaw delivers ``after_tool_call`` payloads with camelCase fields +(``toolName``, ``params``, ``result``). Per Phase 0 finding B1 the handler +translates these to snake_case before persisting so downstream consumers +(state buffer, reflexio API) see a stable shape. +""" + +from __future__ import annotations + +import re +import time +from typing import Any + +from openclaw_smart import state + +# Tool inputs are persisted locally and later published to reflexio, so we +# apply a conservative redaction pass at ingestion time. Chosen to avoid +# false positives over maximal coverage — the dashboard shows these +# verbatim, and users noticing a masked command is far less surprising +# than a masked ``LOG_LEVEL=INFO``. +_MAX_STR_LEN = 4096 +_SECRET_ASSIGNMENT = re.compile( + r"(?P[A-Z][A-Z0-9_]{2,})=(?P['\"]?)" + r"(?P[A-Za-z0-9+/=_\-]{20,})(?P=quote)" +) + + +def _looks_like_secret(value: str) -> bool: + """Heuristic: mixed-case letters plus digits suggest a high-entropy token.""" + has_lower = any(c.islower() for c in value) + has_upper = any(c.isupper() for c in value) + has_digit = any(c.isdigit() for c in value) + return has_lower and has_upper and has_digit + + +def _mask_secrets(text: str) -> str: + """Mask values that look like high-entropy secrets in ``KEY=value`` form.""" + + def sub(match: "re.Match[str]") -> str: + value = match.group("value") + if not _looks_like_secret(value): + return match.group(0) + key = match.group("key") + quote = match.group("quote") + return f"{key}={quote}{quote}" + + return _SECRET_ASSIGNMENT.sub(sub, text) + + +def _redact_string(value: str) -> str: + """Mask secrets in ``value`` then truncate to ``_MAX_STR_LEN`` chars.""" + masked = _mask_secrets(value) + if len(masked) > _MAX_STR_LEN: + return masked[:_MAX_STR_LEN] + "…(truncated)" + return masked + + +def _redact(tool_input: dict[str, Any]) -> dict[str, Any]: + """Redact obvious secrets and truncate oversized string values.""" + return { + k: _redact_string(v) if isinstance(v, str) else v + for k, v in tool_input.items() + } + + +def _derive_status(tool_response: Any) -> str: + """Classify the tool outcome as 'success' or 'error'. + + openClaw's ``after_tool_call`` payload places the tool result under + ``result``; a structured failure may use ``is_error``/``error`` keys, + while plain string results default to success. + """ + if isinstance(tool_response, dict): + if tool_response.get("is_error") or tool_response.get("error"): + return "error" + return "success" + + +_OUTPUT_TEXT_KEYS = ("stdout", "stderr", "output", "content", "text", "error") + + +def _flatten_tool_response_text(tool_response: Any) -> str: + """Flatten ``tool_response`` into a single string for buffering. + + Tool responses arrive in heterogeneous shapes — Bash sends a dict with + ``stdout``/``stderr``, Edit/Read send a string or a dict with + ``content``/``output``, and failures populate ``error``. Joining the + well-known string-valued keys preserves the parts most useful for + downstream learning (failure messages, command output) without + serializing entire structured payloads. + """ + if tool_response is None: + return "" + if isinstance(tool_response, str): + return tool_response + if isinstance(tool_response, dict): + parts = [ + tool_response[key] + for key in _OUTPUT_TEXT_KEYS + if isinstance(tool_response.get(key), str) and tool_response[key] + ] + if parts: + return "\n".join(parts) + for key in ("text", "content"): + value = tool_response.get(key) + if isinstance(value, str): + return value + return "" + for attr in ("text", "content"): + value = getattr(tool_response, attr, None) + if isinstance(value, str): + return value + return "" + + +def _extract_fields(payload: dict[str, Any]) -> dict[str, Any]: + """Map openClaw's camelCase keys onto our snake_case schema. + + Accepts either style so the handler is resilient to dispatcher changes — + if a future TS shim emits already-translated keys, the snake_case + branch wins. + """ + return { + "session_id": payload.get("sessionKey") or payload.get("sessionId"), + "tool_name": payload.get("tool_name") or payload.get("toolName") or "", + "tool_input": payload.get("tool_input") or payload.get("params") or {}, + "tool_response": payload.get("tool_response") + if "tool_response" in payload + else payload.get("result"), + } + + +def handle(payload: dict[str, Any]) -> None: + """Persist one tool invocation to the session JSONL buffer.""" + fields = _extract_fields(payload) + session_id = fields["session_id"] + tool_name = fields["tool_name"] + if not session_id or not tool_name: + return + + tool_response = fields["tool_response"] + output_text = _flatten_tool_response_text(tool_response) + record = { + "ts": int(time.time()), + "role": "Assistant_tool", + "tool_name": tool_name, + "tool_input": _redact(fields["tool_input"]), + "tool_output": _redact_string(output_text) if output_text else "", + "status": _derive_status(tool_response), + } + state.append(session_id, record) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py b/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py new file mode 100644 index 00000000..87a697e6 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py @@ -0,0 +1,143 @@ +"""Tests for openclaw_smart.events.after_tool_call.""" + +from __future__ import annotations + +import json + +import pytest + +from openclaw_smart.events import after_tool_call as atc + + +@pytest.fixture(autouse=True) +def isolate_state_dir(monkeypatch, tmp_path): + sessions = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(sessions)) + return sessions + + +def _read_records(sessions_dir, session_id="s1"): + path = sessions_dir / f"{session_id}.jsonl" + if not path.exists(): + return [] + return [json.loads(line) for line in path.read_text().splitlines() if line] + + +def test_handle_records_tool_invocation_camelcase(isolate_state_dir): + """openClaw delivers camelCase fields; handler translates to snake_case.""" + atc.handle( + { + "sessionKey": "s1", + "toolName": "Bash", + "params": {"command": "ls"}, + "result": "file1.txt", + "agentId": "a", + } + ) + records = _read_records(isolate_state_dir) + assert len(records) == 1 + rec = records[0] + assert rec["role"] == "Assistant_tool" + assert rec["tool_name"] == "Bash" + assert rec["tool_input"] == {"command": "ls"} + assert rec["tool_output"] == "file1.txt" + assert rec["status"] == "success" + + +def test_handle_accepts_snake_case_fallback(isolate_state_dir): + """Already-translated payloads work too.""" + atc.handle( + { + "sessionKey": "s2", + "tool_name": "Edit", + "tool_input": {"file_path": "x.py", "new_string": "hi"}, + "tool_response": "ok", + "status": "success", + } + ) + records = _read_records(isolate_state_dir, "s2") + assert records[0]["tool_name"] == "Edit" + assert records[0]["tool_input"]["file_path"] == "x.py" + + +def test_handle_redacts_high_entropy_secrets(isolate_state_dir): + secret_command = "API_KEY=AbCdEfGhIj1234567890QwErTyUiOp curl ..." + atc.handle( + { + "sessionKey": "s3", + "toolName": "Bash", + "params": {"command": secret_command}, + "result": "", + } + ) + records = _read_records(isolate_state_dir, "s3") + command = records[0]["tool_input"]["command"] + assert "AbCdEfGhIj1234567890QwErTyUiOp" not in command + assert " Date: Tue, 19 May 2026 03:10:52 +0000 Subject: [PATCH 24/53] feat(openclaw/events): port agent_end handler Reads final assistant text from payload.messages (per Phase 0 Q2), rather than scanning a transcript file. Resolves [oc:] citation markers from assistant text against the per-session injected registry, appends an Assistant record, then drains the publish buffer. Drops plan-decision and cs-cite Bash scans (claude-code-specific). --- .../src/openclaw_smart/events/agent_end.py | 189 +++++++++++++++ .../plugin/tests/test_events_agent_end.py | 220 ++++++++++++++++++ 2 files changed, 409 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py new file mode 100644 index 00000000..c0f62db3 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py @@ -0,0 +1,189 @@ +"""agent_end hook — finalize the assistant turn and publish to reflexio. + +This handler is the openClaw analogue of claude-smart's ``stop.py``, but +substantially simpler. Per Phase 0 finding Q2, openClaw delivers the final +messages inline as ``event.messages: unknown[]``, so we read assistant text +directly from the payload instead of polling a transcript JSONL on disk. + +Pipeline: +1. Extract final assistant text from ``payload.messages``. +2. Parse ``[oc:…]`` citation markers from that text and resolve them + against the per-session injected registry. +3. Append an ``Assistant`` record (always, even when the text is empty — + ``state.unpublished_slice`` folds any buffered ``Assistant_tool`` + records into the next ``Assistant`` turn's ``tools_used``). +4. Drain the buffer to reflexio via ``publish.publish_unpublished``. + +claude-smart's plan-mode decision scan and ``cs-cite`` Bash tool scan are +intentionally NOT ported — both rely on Claude-Code-specific transcript +shapes that openClaw does not emit. +""" + +from __future__ import annotations + +import logging +import time +from typing import Any + +from openclaw_smart import ids, oc_cite, publish, state + +_LOGGER = logging.getLogger(__name__) + + +def _extract_assistant_text(messages: Any) -> str: + """Return the final assistant turn's text from a messages list. + + Walks ``messages`` from the end, collecting every contiguous assistant + entry up to the most recent non-assistant boundary, then joins their + text content. Mirrors claude-smart's transcript walker but works on + inline message dicts. + + Args: + messages (Any): openClaw's ``event.messages`` blob, expected to be + a list of dicts with ``role`` and ``content`` fields. + + Returns: + str: Concatenated assistant text, or ``""`` when none is found. + """ + if not isinstance(messages, list): + return "" + collected: list[str] = [] + for entry in reversed(messages): + if not isinstance(entry, dict): + continue + role = _entry_role(entry) + if role != "assistant": + # Boundary — stop collecting; everything before this belongs to + # earlier turns and has already been (or will be) published. + break + content = _entry_content(entry) + text_parts = _extract_text_blocks(content) + if text_parts: + # Prepend because we walked in reverse order. + collected = text_parts + collected + return "\n\n".join(part for part in collected if part) + + +def _entry_role(entry: dict[str, Any]) -> str: + """Read the role from an openClaw or Claude-Code-shaped message dict.""" + role = entry.get("role") + if isinstance(role, str): + return role.lower() + message = entry.get("message") + if isinstance(message, dict): + nested = message.get("role") + if isinstance(nested, str): + return nested.lower() + return "" + + +def _entry_content(entry: dict[str, Any]) -> Any: + """Return the content payload from a message entry, accepting both shapes.""" + if "content" in entry: + return entry["content"] + message = entry.get("message") + if isinstance(message, dict): + return message.get("content") + return None + + +def _extract_text_blocks(content: Any) -> list[str]: + """Return assistant-visible text from a message content payload. + + Accepts: + - bare strings (``content: "hi"``) + - block lists (``content: [{type: "text", text: "hi"}, ...]``) + """ + if isinstance(content, str): + return [content] if content else [] + if not isinstance(content, list): + return [] + out: list[str] = [] + for block in content: + if not isinstance(block, dict): + continue + text = block.get("text") + if isinstance(text, str) and text: + out.append(text) + continue + inner = block.get("content") + if isinstance(inner, str) and inner: + out.append(inner) + return out + + +def _resolve_cited_items( + session_id: str, cited_ids: list[str] +) -> list[dict[str, Any]]: + """Map citation ids to ``{id, kind, title}`` entries via the session registry. + + Unknown ids (model hallucinations, or items injected in a newer session + than this hook can see) are dropped. Duplicate ids within one turn + collapse to a single entry — the user-facing badge row doesn't need + the multiplicity. + """ + if not cited_ids: + return [] + registry = state.read_injected(session_id) + seen: set[str] = set() + resolved: list[dict[str, Any]] = [] + for cid in cited_ids: + if cid in seen: + continue + entry = registry.get(cid) + if not entry: + continue + seen.add(cid) + item: dict[str, Any] = { + "id": entry.get("id", cid), + "kind": entry.get("kind", ""), + "title": entry.get("title", ""), + } + real_id = entry.get("real_id") + if real_id: + item["real_id"] = real_id + source_kind = entry.get("source_kind") + if isinstance(source_kind, str) and source_kind: + item["source_kind"] = source_kind + resolved.append(item) + return resolved + + +def handle(payload: dict[str, Any]) -> None: + """Finalize the current assistant turn and publish to reflexio. + + Args: + payload (dict[str, Any]): openClaw event/ctx blob, expected to contain + ``sessionKey`` (or ``sessionId``), ``messages``, and optionally + ``agentId`` / ``workspaceDir``. + """ + session_id = payload.get("sessionKey") or payload.get("sessionId") + if not session_id: + return + + project_id = ids.resolve_project_id_with_fallback( + cwd=payload.get("workspaceDir"), + agent_id=payload.get("agentId"), + ) + + assistant_text = _extract_assistant_text(payload.get("messages")) + cited_ids = oc_cite.parse_text_citations(assistant_text) + cited_items = _resolve_cited_items(session_id, cited_ids) + + now = int(time.time()) + record: dict[str, Any] = { + "ts": now, + "role": "Assistant", + "content": assistant_text, + "user_id": project_id, + } + if cited_items: + record["cited_items"] = cited_items + state.append(session_id, record) + + publish.publish_unpublished( + session_id=session_id, + project_id=project_id, + force_extraction=False, + skip_aggregation=False, + ) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py b/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py new file mode 100644 index 00000000..cd8c022a --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py @@ -0,0 +1,220 @@ +"""Tests for openclaw_smart.events.agent_end.""" + +from __future__ import annotations + +import json +from unittest.mock import MagicMock, patch + +import pytest + +from openclaw_smart import state +from openclaw_smart.events import agent_end + + +@pytest.fixture(autouse=True) +def isolate_state_dir(monkeypatch, tmp_path): + sessions = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(sessions)) + return sessions + + +def _read_records(sessions_dir, session_id="s1"): + path = sessions_dir / f"{session_id}.jsonl" + if not path.exists(): + return [] + return [json.loads(line) for line in path.read_text().splitlines() if line] + + +def test_extract_assistant_text_plain_string(): + text = agent_end._extract_assistant_text( + [ + {"role": "user", "content": "hi"}, + {"role": "assistant", "content": "hello"}, + ] + ) + assert text == "hello" + + +def test_extract_assistant_text_block_form(): + text = agent_end._extract_assistant_text( + [ + {"role": "user", "content": "hi"}, + { + "role": "assistant", + "content": [ + {"type": "text", "text": "first"}, + {"type": "text", "text": "second"}, + ], + }, + ] + ) + assert "first" in text and "second" in text + + +def test_extract_assistant_text_concatenates_trailing_assistant_turns(): + text = agent_end._extract_assistant_text( + [ + {"role": "user", "content": "q"}, + {"role": "assistant", "content": "step 1"}, + {"role": "assistant", "content": "step 2"}, + ] + ) + assert "step 1" in text + assert "step 2" in text + + +def test_extract_assistant_text_stops_at_user_boundary(): + text = agent_end._extract_assistant_text( + [ + {"role": "assistant", "content": "old turn"}, + {"role": "user", "content": "new q"}, + {"role": "assistant", "content": "new answer"}, + ] + ) + assert "new answer" in text + assert "old turn" not in text + + +def test_extract_assistant_text_handles_nested_message_shape(): + """Accept Claude-Code-style ``{message: {role, content}}`` shape too.""" + text = agent_end._extract_assistant_text( + [{"message": {"role": "assistant", "content": "hi"}}] + ) + assert text == "hi" + + +def test_extract_assistant_text_empty_for_no_messages(): + assert agent_end._extract_assistant_text([]) == "" + assert agent_end._extract_assistant_text(None) == "" + + +def test_handle_no_session_id_returns_silently(isolate_state_dir): + with patch("openclaw_smart.events.agent_end.publish") as pub: + agent_end.handle({}) + pub.publish_unpublished.assert_not_called() + + +def test_handle_appends_assistant_record(isolate_state_dir): + with patch("openclaw_smart.events.agent_end.publish") as pub, patch( + "openclaw_smart.events.agent_end.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + pub.publish_unpublished.return_value = ("ok", 1) + agent_end.handle( + { + "sessionKey": "s1", + "agentId": "a", + "messages": [ + {"role": "user", "content": "q"}, + {"role": "assistant", "content": "Done."}, + ], + } + ) + + records = _read_records(isolate_state_dir) + assert records[-1]["role"] == "Assistant" + assert records[-1]["content"] == "Done." + assert records[-1]["user_id"] == "proj-x" + + +def test_handle_publishes_unpublished_slice(isolate_state_dir): + with patch("openclaw_smart.events.agent_end.publish") as pub, patch( + "openclaw_smart.events.agent_end.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + pub.publish_unpublished.return_value = ("ok", 1) + agent_end.handle( + { + "sessionKey": "s1", + "messages": [{"role": "assistant", "content": "Done."}], + } + ) + pub.publish_unpublished.assert_called_once() + kwargs = pub.publish_unpublished.call_args[1] + assert kwargs["session_id"] == "s1" + assert kwargs["project_id"] == "proj-x" + assert kwargs["force_extraction"] is False + assert kwargs["skip_aggregation"] is False + + +def test_handle_resolves_citations(isolate_state_dir, monkeypatch): + # Seed the injected registry so cited tags resolve. + state.append_injected( + "s1", + [ + { + "id": "s1-ab12", + "kind": "playbook", + "title": "Use OAuth2", + "real_id": "abc", + } + ], + ) + + with patch("openclaw_smart.events.agent_end.publish") as pub, patch( + "openclaw_smart.events.agent_end.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + pub.publish_unpublished.return_value = ("ok", 1) + agent_end.handle( + { + "sessionKey": "s1", + "messages": [ + { + "role": "assistant", + "content": ( + "Implemented OAuth.\n" + "✨ 1 openclaw-smart learning applied [oc:s1-ab12]" + ), + } + ], + } + ) + + records = _read_records(isolate_state_dir) + assert "cited_items" in records[-1] + cited = records[-1]["cited_items"] + assert cited[0]["id"] == "s1-ab12" + assert cited[0]["title"] == "Use OAuth2" + assert cited[0]["real_id"] == "abc" + + +def test_handle_skips_unknown_citations(isolate_state_dir): + """Citations for ids not in the registry are dropped, not raised.""" + with patch("openclaw_smart.events.agent_end.publish") as pub, patch( + "openclaw_smart.events.agent_end.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + pub.publish_unpublished.return_value = ("ok", 1) + agent_end.handle( + { + "sessionKey": "s1", + "messages": [ + { + "role": "assistant", + "content": ( + "Done.\n" + "✨ 1 openclaw-smart learning applied [oc:s9-9999]" + ), + } + ], + } + ) + records = _read_records(isolate_state_dir) + assert "cited_items" not in records[-1] + + +def test_handle_falls_back_to_session_id(isolate_state_dir): + with patch("openclaw_smart.events.agent_end.publish") as pub, patch( + "openclaw_smart.events.agent_end.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + pub.publish_unpublished.return_value = ("ok", 1) + agent_end.handle( + { + "sessionId": "sess-y", + "messages": [{"role": "assistant", "content": "ok"}], + } + ) + kwargs = pub.publish_unpublished.call_args[1] + assert kwargs["session_id"] == "sess-y" From 51d0b45631dafb88243593e851f5715adf902a29 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:12:20 +0000 Subject: [PATCH 25/53] feat(openclaw/events): port session_end handler with force_extraction=True Diverges from claude-smart's session_end (force_extraction=False) so the final session's lessons are extracted synchronously and visible in the very next session. --- .../src/openclaw_smart/events/session_end.py | 36 ++++++++++++++++++ .../plugin/tests/test_events_session_end.py | 37 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py new file mode 100644 index 00000000..e5aed7e3 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py @@ -0,0 +1,36 @@ +"""session_end hook — flush any remaining interactions with forced extraction. + +Fires when an openClaw session terminates. We force extraction here (unlike +``agent_end``, which lets reflexio queue extraction for its next sweep) so +the user's distilled lessons from this session are usable in the very next +session, not after the next scheduled sweep. +""" + +from __future__ import annotations + +from typing import Any + +from openclaw_smart import ids, publish + + +def handle(payload: dict[str, Any]) -> None: + """Drain the buffer and trigger synchronous extraction. + + Args: + payload (dict[str, Any]): openClaw event/ctx blob, expected to + contain ``sessionKey`` (or ``sessionId``) and optionally + ``agentId`` / ``workspaceDir``. + """ + session_id = payload.get("sessionKey") or payload.get("sessionId") + if not session_id: + return + project_id = ids.resolve_project_id_with_fallback( + cwd=payload.get("workspaceDir"), + agent_id=payload.get("agentId"), + ) + publish.publish_unpublished( + session_id=session_id, + project_id=project_id, + force_extraction=True, + skip_aggregation=False, + ) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py b/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py new file mode 100644 index 00000000..404a2e60 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py @@ -0,0 +1,37 @@ +"""Tests for openclaw_smart.events.session_end.""" + +from __future__ import annotations + +from unittest.mock import patch + +from openclaw_smart.events import session_end + + +def test_handle_no_session_id_returns_silently(): + with patch("openclaw_smart.events.session_end.publish") as pub: + session_end.handle({}) + pub.publish_unpublished.assert_not_called() + + +def test_handle_force_extracts(): + with patch("openclaw_smart.events.session_end.publish") as pub, patch( + "openclaw_smart.events.session_end.ids.resolve_project_id_with_fallback", + return_value="proj-x", + ): + session_end.handle({"sessionKey": "s1", "agentId": "a"}) + pub.publish_unpublished.assert_called_once() + kwargs = pub.publish_unpublished.call_args[1] + assert kwargs["session_id"] == "s1" + assert kwargs["project_id"] == "proj-x" + assert kwargs["force_extraction"] is True + assert kwargs["skip_aggregation"] is False + + +def test_handle_falls_back_to_session_id_key(): + with patch("openclaw_smart.events.session_end.publish") as pub, patch( + "openclaw_smart.events.session_end.ids.resolve_project_id_with_fallback", + return_value="proj-y", + ): + session_end.handle({"sessionId": "s2"}) + kwargs = pub.publish_unpublished.call_args[1] + assert kwargs["session_id"] == "s2" From 67ce4c52a51c53cdd3223ee714891928723e0775 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:14:30 +0000 Subject: [PATCH 26/53] feat(openclaw): port hook.py dispatcher with openclaw event map Six events: session-start, before-prompt-build, before-tool-call, after-tool-call, agent-end, session-end. Dispatcher reads stdin JSON, routes to handler, and emits nothing on no-op paths (unknown event, recursion guard, handler exception) so the TS shim sees empty stdout as 'no return value'. --- .../plugin/src/openclaw_smart/hook.py | 130 ++++++++++++++++++ .../plugin/tests/test_hook_dispatch.py | 119 ++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py new file mode 100644 index 00000000..5a6a2ef9 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py @@ -0,0 +1,130 @@ +"""Dispatch table for openclaw-smart hook events. + +The TS shim spawns ``python -m openclaw_smart.hook `` (or the +``openclaw-smart-hook`` console script) once per hook invocation, piping +the openClaw event payload on stdin. This module reads the JSON, routes +to the matching handler, and makes sure no unhandled exception ever +propagates back to openClaw. + +Handlers emit their own ``{"prependContext": …}`` JSON to stdout when +they have context to inject; otherwise they emit nothing. The dispatcher +emits nothing on no-op paths (unknown event, recursion guard, handler +exception) so the TS shim can treat empty stdout as "no return value". +""" + +from __future__ import annotations + +import json +import logging +import sys +from typing import Any, Callable + +from openclaw_smart import runtime +from openclaw_smart.internal_call import is_internal_invocation + +_LOGGER = logging.getLogger(__name__) + + +def _load_handlers() -> dict[str, Callable[[dict[str, Any]], None]]: + """Import handler modules lazily to keep cold-start cost low.""" + from openclaw_smart.events import ( + after_tool_call, + agent_end, + before_prompt_build, + before_tool_call, + session_end, + session_start, + ) + + return { + "session-start": session_start.handle, + "before-prompt-build": before_prompt_build.handle, + "before-tool-call": before_tool_call.handle, + "after-tool-call": after_tool_call.handle, + "agent-end": agent_end.handle, + "session-end": session_end.handle, + } + + +_HANDLERS: dict[str, Callable[[dict[str, Any]], None]] | None = None + + +def _handlers() -> dict[str, Callable[[dict[str, Any]], None]]: + global _HANDLERS + if _HANDLERS is None: + _HANDLERS = _load_handlers() + return _HANDLERS + + +def _read_stdin_json() -> dict[str, Any]: + """Parse stdin as JSON. Returns ``{}`` on empty or malformed input.""" + try: + raw = sys.stdin.read() + except (OSError, ValueError) as exc: + _LOGGER.debug("stdin read failed: %s", exc) + return {} + if not raw.strip(): + return {} + try: + parsed = json.loads(raw) + except json.JSONDecodeError as exc: + _LOGGER.debug("stdin JSON decode failed: %s", exc) + return {} + return parsed if isinstance(parsed, dict) else {} + + +def _parse_args(argv: list[str]) -> str: + """Return the event name from ``argv``. + + Accepts either ``[event]`` or ``[host, event]`` for symmetry with + claude-smart, but the host arg (if present) must be ``"openclaw"`` and + is otherwise ignored — runtime.set_host is called either way. + """ + if not argv: + return "" + if len(argv) >= 2 and argv[0] in runtime.VALID_HOSTS: + return argv[1] + return argv[0] + + +def main(argv: list[str] | None = None) -> int: + """Entry point used by ``python -m openclaw_smart.hook`` and the console script. + + Args: + argv (list[str] | None): Command-line args (sans program name). + Defaults to ``sys.argv[1:]``. + + Returns: + int: Always 0 — failures are absorbed so the host never sees a + non-zero exit and decides to surface a plugin error. + """ + argv = argv if argv is not None else sys.argv[1:] + event = _parse_args(argv) + runtime.set_host(runtime.HOST_OPENCLAW) + if not event: + _LOGGER.warning("hook dispatcher called with no event name") + return 0 + + payload = _read_stdin_json() + + # Self-feedback guard: when this hook fires inside reflexio's own + # openclaw subprocess (the openclaw LLM provider), skip all handlers + # so we don't publish the extractor's system prompt back into reflexio. + # See openclaw_smart.internal_call for detection logic. + if is_internal_invocation(payload): + return 0 + + handler = _handlers().get(event) + if handler is None: + _LOGGER.warning("unknown hook event: %s", event) + return 0 + + try: + handler(payload) + except Exception as exc: # noqa: BLE001 — hooks must never crash the session. + _LOGGER.exception("hook handler %s raised: %s", event, exc) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py b/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py new file mode 100644 index 00000000..5ab1a8ac --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py @@ -0,0 +1,119 @@ +"""Tests for openclaw_smart.hook dispatch.""" + +from __future__ import annotations + +import io +import json +from unittest.mock import patch + +import pytest + +from openclaw_smart import hook + + +@pytest.fixture(autouse=True) +def _reset_handlers(): + """Reload handler dispatch dict for each test (state is module-global).""" + hook._HANDLERS = None + yield + hook._HANDLERS = None + + +def test_main_dispatches_to_handler(monkeypatch): + called = {} + + def fake_handler(payload): + called["payload"] = payload + + monkeypatch.setattr( + "sys.stdin", + io.StringIO(json.dumps({"sessionKey": "s1", "agentId": "a"})), + ) + monkeypatch.setattr(hook, "_HANDLERS", {"session-start": fake_handler}) + + rc = hook.main(["session-start"]) + assert rc == 0 + assert called["payload"] == {"sessionKey": "s1", "agentId": "a"} + + +def test_main_accepts_host_arg(monkeypatch): + called = {} + + def fake_handler(payload): + called["payload"] = payload + + monkeypatch.setattr("sys.stdin", io.StringIO('{"sessionKey":"s1"}')) + monkeypatch.setattr(hook, "_HANDLERS", {"session-start": fake_handler}) + + rc = hook.main(["openclaw", "session-start"]) + assert rc == 0 + assert called + + +def test_main_silent_when_recursion_guard_fires(monkeypatch, capsys): + monkeypatch.setenv("OPENCLAW_SMART_INTERNAL", "1") + monkeypatch.setattr("sys.stdin", io.StringIO("{}")) + called = {"count": 0} + + def boom(payload): + called["count"] += 1 + raise RuntimeError("should not be called") + + monkeypatch.setattr(hook, "_HANDLERS", {"session-start": boom}) + rc = hook.main(["session-start"]) + assert rc == 0 + assert called["count"] == 0 + assert capsys.readouterr().out == "" + + +def test_main_silent_on_handler_exception(monkeypatch, capsys): + monkeypatch.setattr("sys.stdin", io.StringIO("{}")) + + def boom(payload): + raise RuntimeError("x") + + monkeypatch.setattr(hook, "_HANDLERS", {"session-start": boom}) + rc = hook.main(["session-start"]) + assert rc == 0 + assert capsys.readouterr().out == "" + + +def test_main_silent_on_unknown_event(monkeypatch, capsys): + monkeypatch.setattr("sys.stdin", io.StringIO("{}")) + rc = hook.main(["no-such-event"]) + assert rc == 0 + assert capsys.readouterr().out == "" + + +def test_main_silent_with_no_event(monkeypatch, capsys): + monkeypatch.setattr("sys.stdin", io.StringIO("{}")) + rc = hook.main([]) + assert rc == 0 + assert capsys.readouterr().out == "" + + +def test_main_handles_malformed_stdin(monkeypatch): + called = {} + + def fake_handler(payload): + called["payload"] = payload + + monkeypatch.setattr("sys.stdin", io.StringIO("not json {{{")) + monkeypatch.setattr(hook, "_HANDLERS", {"session-start": fake_handler}) + rc = hook.main(["session-start"]) + assert rc == 0 + # Malformed → empty dict, handler still called + assert called["payload"] == {} + + +def test_load_handlers_includes_all_events(): + handlers = hook._load_handlers() + expected = { + "session-start", + "before-prompt-build", + "before-tool-call", + "after-tool-call", + "agent-end", + "session-end", + } + assert set(handlers) == expected From 5a59e0648a9d103c774890274979666b47082e36 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:16:55 +0000 Subject: [PATCH 27/53] feat(openclaw): port cli.py command handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements show, learn, restart, clear-all. Drops claude-smart's install/uninstall flows (Codex marketplace, etc.) — those go through reflexio setup openclaw (Phase 10) and the openClaw plugin system. --- .../openclaw/plugin/src/openclaw_smart/cli.py | 623 ++++++++++++++++++ .../openclaw/plugin/tests/test_cli.py | 154 +++++ 2 files changed, 777 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_cli.py diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py new file mode 100644 index 00000000..b606ff81 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py @@ -0,0 +1,623 @@ +"""User-facing CLI for openclaw-smart. + +Exposes the following subcommands: + +- ``show``: print current project-specific skills and project preferences + (as markdown). +- ``learn``: publish unpublished interactions and force reflexio extraction + now over the active session buffer. +- ``restart``: stop and restart the reflexio backend + dashboard services + so local edits take effect without restarting openClaw. +- ``clear-all``: delete all local reflexio data and openclaw-smart session + buffers. + +Installation/uninstallation is intentionally NOT exposed here — those flows +live in ``reflexio setup openclaw`` (parent reflexio CLI) and are managed by +the openClaw plugin system directly. +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import time +from dataclasses import dataclass +from pathlib import Path + +from openclaw_smart import context_format, ids, publish, state +from openclaw_smart.reflexio_adapter import Adapter + +_REFLEXIO_ENV_PATH = Path.home() / ".reflexio" / ".env" +_REFLEXIO_UNREACHABLE_MSG = ( + "Failed to reach reflexio. Check ~/.openclaw-smart/backend.log " + "or restart openClaw.\n" +) + +# plugin/src/openclaw_smart/cli.py -> parents[2] is plugin/ +_THIS_DIR = Path(__file__).resolve().parent +_PLUGIN_ROOT = _THIS_DIR.parents[2] +_SCRIPTS_DIR = _PLUGIN_ROOT / "scripts" +_DASHBOARD_DIR = _PLUGIN_ROOT / "dashboard" +_BACKEND_SCRIPT = _SCRIPTS_DIR / "backend-service.sh" +_DASHBOARD_SCRIPT = _SCRIPTS_DIR / "dashboard-service.sh" +_REFLEXIO_DIR = Path.home() / ".reflexio" +_DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data" +_REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / "config_self-host-org.json" +_LOCAL_STORAGE_ENV = "LOCAL_STORAGE_PATH" + + +def _latest_session_id() -> str | None: + """Most-recently-modified session JSONL in the state dir. None if none exist.""" + root = state.state_dir() + if not root.is_dir(): + return None + files = sorted(root.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True) + if not files: + return None + return files[0].stem + + +def cmd_show(args: argparse.Namespace) -> int: + """Print this project's skills + preferences, plus globally-shared skills. + + Args: + args (argparse.Namespace): Parsed CLI args. Honors ``args.project`` + (override project id for the project-scoped fetches). + + Returns: + int: 0 on success. + """ + project_id = args.project or ids.resolve_project_id() + adapter = Adapter() + user_playbooks, agent_playbooks, profiles = adapter.fetch_all( + project_id=project_id, + user_playbook_top_k=3, + agent_playbook_top_k=3, + profile_top_k=3, + ) + md = context_format.render( + project_id=project_id, + user_playbooks=user_playbooks, + agent_playbooks=agent_playbooks, + profiles=profiles, + ) + sys.stdout.write( + md or f"_No skills or preferences yet for project `{project_id}`._\n" + ) + return 0 + + +def cmd_learn(args: argparse.Namespace) -> int: + """Force reflexio extraction over the active session's interactions. + + Args: + args (argparse.Namespace): Parsed CLI args. Honors ``args.session`` + (defaults to most-recent), ``args.project`` (defaults to + ``ids.resolve_project_id()``), and ``args.note`` (free-form + text appended as a User turn before publish). + + Returns: + int: 0 on success or no-op, 1 if reflexio is unreachable. + """ + session_id = args.session or _latest_session_id() + if not session_id: + sys.stdout.write("No active openclaw-smart session buffer found.\n") + return 0 + project_id = args.project or ids.resolve_project_id() + + note = (args.note or "").strip() + if note: + state.append( + session_id, + { + "ts": int(time.time()), + "role": "User", + "content": note, + "user_id": project_id, + }, + ) + + status, count = publish.publish_unpublished( + session_id=session_id, + project_id=project_id, + force_extraction=True, + skip_aggregation=False, + ) + if status == "ok": + suffix = " (including your note)" if note else "" + sys.stdout.write( + f"Forced extraction on session `{session_id}` over {count} interactions{suffix}.\n" + ) + return 0 + if status == "nothing": + sys.stdout.write(f"No unpublished interactions on session `{session_id}`.\n") + return 0 + sys.stdout.write(_REFLEXIO_UNREACHABLE_MSG) + return 1 + + +def _run_service(script: Path, subcmd: str) -> int: + """Invoke a service script (``backend-service.sh`` / ``dashboard-service.sh``). + + Args: + script (Path): Absolute path to the service shell script. + subcmd (str): Subcommand to pass (``start``, ``stop``, ``status``). + + Returns: + int: The script's exit code, or 1 if the script is missing. + """ + if not script.exists(): + sys.stderr.write(f"error: {script} not found\n") + return 1 + try: + subprocess.run([str(script), subcmd], check=True) + return 0 + except subprocess.CalledProcessError as exc: + return exc.returncode or 1 + + +def _service_status(script: Path, wait_ready_s: float = 3.0) -> str: + """Return the one-line status string for a service script.""" + if not script.exists(): + return "script missing" + deadline = time.monotonic() + wait_ready_s + while True: + result = subprocess.run( + [str(script), "status"], capture_output=True, text=True, check=False + ) + status = result.stdout.strip() or "unknown" + if status != "not running" or time.monotonic() >= deadline: + return status + time.sleep(0.2) + + +def _is_running_status(status: str) -> bool: + return status.startswith("running on ") + + +def cmd_restart(args: argparse.Namespace) -> int: + """Restart the reflexio backend and openclaw-smart dashboard services. + + Args: + args (argparse.Namespace): Parsed CLI args. Honors + ``args.skip_backend``, ``args.skip_dashboard``, and + ``args.no_rebuild``. + + Returns: + int: 0 on success, non-zero if the dashboard rebuild fails or + either service's ``start`` subcommand exits non-zero. + """ + do_backend = not args.skip_backend + do_dashboard = not args.skip_dashboard + + if not (do_backend or do_dashboard): + sys.stdout.write("Nothing to restart (both services skipped).\n") + return 0 + + if do_backend: + sys.stdout.write("Stopping reflexio backend…\n") + _run_service(_BACKEND_SCRIPT, "stop") + if do_dashboard: + sys.stdout.write("Stopping dashboard…\n") + _run_service(_DASHBOARD_SCRIPT, "stop") + + if do_dashboard and not args.no_rebuild and _DASHBOARD_DIR.is_dir(): + rc = _rebuild_dashboard() + if rc != 0: + if do_backend: + _run_service(_BACKEND_SCRIPT, "start") + return rc + + start_rc = 0 + if do_backend: + sys.stdout.write("Starting reflexio backend…\n") + rc = _run_service(_BACKEND_SCRIPT, "start") + if rc != 0: + sys.stderr.write(f"error: reflexio backend failed to start (exit {rc})\n") + start_rc = rc + if do_dashboard: + sys.stdout.write("Starting dashboard…\n") + rc = _run_service(_DASHBOARD_SCRIPT, "start") + if rc != 0: + sys.stderr.write(f"error: dashboard failed to start (exit {rc})\n") + start_rc = start_rc or rc + + sys.stdout.write("\n") + if do_backend: + sys.stdout.write(f"reflexio backend: {_service_status(_BACKEND_SCRIPT)}\n") + if do_dashboard: + sys.stdout.write(f"dashboard: {_service_status(_DASHBOARD_SCRIPT)}\n") + return start_rc + + +def _rebuild_dashboard() -> int: + """Rebuild the dashboard Next.js bundle, installing deps if needed.""" + if not shutil.which("npm"): + sys.stderr.write("warning: npm not on PATH; serving previous build\n") + return 0 + next_bin = _DASHBOARD_DIR / "node_modules" / ".bin" / "next" + if not next_bin.exists(): + install_cmd = ( + ["npm", "ci", "--no-audit", "--no-fund"] + if (_DASHBOARD_DIR / "package-lock.json").exists() + else ["npm", "install", "--no-audit", "--no-fund"] + ) + sys.stdout.write( + f"Installing dashboard dependencies ({' '.join(install_cmd)})…\n" + ) + try: + subprocess.run(install_cmd, cwd=_DASHBOARD_DIR, check=True) + except subprocess.CalledProcessError as exc: + sys.stderr.write(f"error: dashboard install failed (exit {exc.returncode})\n") + return exc.returncode or 1 + sys.stdout.write("Rebuilding dashboard…\n") + try: + subprocess.run(["npm", "run", "build"], cwd=_DASHBOARD_DIR, check=True) + except subprocess.CalledProcessError as exc: + sys.stderr.write(f"error: dashboard build failed (exit {exc.returncode})\n") + return exc.returncode or 1 + return 0 + + +class _ClearAllError(RuntimeError): + """Raised when a destructive clear-all target is unsafe or unsupported.""" + + +@dataclass(frozen=True) +class _ClearAllTarget: + """One filesystem target removed by ``clear-all``.""" + + path: Path + kind: str + label: str + + +def _read_dotenv_value(env_path: Path, key: str) -> str | None: + """Read one simple KEY=VALUE binding from a dotenv file.""" + if not env_path.is_file(): + return None + try: + lines = env_path.read_text().splitlines() + except OSError: + return None + prefix = f"{key}=" + for raw_line in lines: + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if line.startswith("export "): + line = line[7:].lstrip() + if not line.startswith(prefix): + continue + value = line[len(prefix) :].strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + value = value[1:-1] + return value.strip() + return None + + +def _resolve_absolute_path(raw_path: str | Path, *, source: str) -> Path: + path = Path(raw_path).expanduser() + if not path.is_absolute(): + raise _ClearAllError(f"{source} must be an absolute path: {raw_path}") + return path + + +def _effective_storage_root() -> Path: + raw = os.environ.get(_LOCAL_STORAGE_ENV, "").strip() + if not raw: + raw = _read_dotenv_value(_REFLEXIO_ENV_PATH, _LOCAL_STORAGE_ENV) or "" + return _resolve_absolute_path( + raw or _DEFAULT_STORAGE_ROOT, source=_LOCAL_STORAGE_ENV + ) + + +def _load_reflexio_config() -> dict[str, object] | None: + if not _REFLEXIO_CONFIG_PATH.is_file(): + return None + try: + loaded = json.loads(_REFLEXIO_CONFIG_PATH.read_text()) + except (OSError, json.JSONDecodeError) as exc: + raise _ClearAllError( + f"could not read reflexio config {_REFLEXIO_CONFIG_PATH}: {exc}" + ) from exc + if not isinstance(loaded, dict): + raise _ClearAllError( + f"reflexio config {_REFLEXIO_CONFIG_PATH} is not a JSON object" + ) + return loaded + + +def _storage_config_kind(storage_config: dict[str, object]) -> str: + if storage_config.get("managed_by") == "platform": + return "remote" + explicit_type = str( + storage_config.get("type") or storage_config.get("storage_type") or "" + ).lower() + if explicit_type in {"supabase", "postgres"}: + return "remote" + if explicit_type == "disk": + return "disk" + if explicit_type == "sqlite": + return "sqlite" + if ( + "url" in storage_config + and "key" in storage_config + and "db_url" in storage_config + ): + return "remote" + if "db_url" in storage_config: + return "remote" + if "dir_path" in storage_config: + return "disk" + if "db_path" in storage_config or not storage_config: + return "sqlite" + raise _ClearAllError( + "unsupported reflexio storage_config shape; refusing to delete local data" + ) + + +def _dangerous_clear_all_paths() -> set[Path]: + home = Path.home().resolve(strict=False) + reflexio_dir = _resolve_absolute_path(_REFLEXIO_DIR, source="reflexio dir") + return { + Path("/").resolve(strict=False), + home, + reflexio_dir.resolve(strict=False), + _resolve_absolute_path( + reflexio_dir / "configs", source="reflexio configs" + ).resolve(strict=False), + _PLUGIN_ROOT.resolve(strict=False), + _PLUGIN_ROOT.parent.resolve(strict=False), + } + + +def _validate_deletion_target(path: Path) -> None: + if path.exists() and path.is_symlink(): + raise _ClearAllError(f"refusing to delete symlink target: {path}") + resolved = path.resolve(strict=False) + if resolved in _dangerous_clear_all_paths(): + raise _ClearAllError(f"refusing to delete dangerous path: {resolved}") + + +def _disk_org_targets(base_dir: Path) -> list[_ClearAllTarget]: + if base_dir.exists() and base_dir.is_symlink(): + raise _ClearAllError( + f"refusing to inspect symlink disk storage dir: {base_dir}" + ) + if not base_dir.exists(): + return [] + if not base_dir.is_dir(): + raise _ClearAllError( + f"configured disk storage path is not a directory: {base_dir}" + ) + return [ + _ClearAllTarget(child, "dir", "disk org data") + for child in sorted(base_dir.glob("disk_*")) + ] + + +def _resolve_clear_all_targets() -> list[_ClearAllTarget]: + targets = [ + _ClearAllTarget(_effective_storage_root(), "dir", "managed local storage root") + ] + + config = _load_reflexio_config() + storage_config = config.get("storage_config") if config else None + if storage_config is not None: + if not isinstance(storage_config, dict): + raise _ClearAllError("reflexio storage_config is not a JSON object") + kind = _storage_config_kind(storage_config) + if kind == "remote": + raise _ClearAllError( + "clear-all only resets local Reflexio storage; " + "remote Supabase/Postgres storage is not supported" + ) + if kind == "sqlite": + raw_db_path = storage_config.get("db_path") + if isinstance(raw_db_path, str) and raw_db_path.strip(): + db_path = _resolve_absolute_path( + raw_db_path.strip(), source="configured SQLite db_path" + ) + for suffix in ("", "-wal", "-shm", "-journal"): + targets.append( + _ClearAllTarget( + Path(f"{db_path}{suffix}"), + "file", + "configured SQLite data", + ) + ) + elif kind == "disk": + raw_dir_path = storage_config.get("dir_path") + if not isinstance(raw_dir_path, str) or not raw_dir_path.strip(): + raise _ClearAllError("configured disk storage is missing dir_path") + disk_base = _resolve_absolute_path( + raw_dir_path.strip(), source="configured disk dir_path" + ) + targets.extend(_disk_org_targets(disk_base)) + + deduped: list[_ClearAllTarget] = [] + seen: set[Path] = set() + for target in targets: + _validate_deletion_target(target.path) + resolved = target.path.resolve(strict=False) + if resolved in seen: + continue + deduped.append(_ClearAllTarget(resolved, target.kind, target.label)) + seen.add(resolved) + return deduped + + +def _remove_clear_all_target(target: _ClearAllTarget) -> bool: + """Remove a target. Returns True when something was actually removed.""" + path = target.path + if not path.exists(): + return False + if target.kind == "dir": + if not path.is_dir(): + raise _ClearAllError( + f"expected directory target but found non-directory: {path}" + ) + shutil.rmtree(path) + return True + if target.kind == "file": + if not path.is_file(): + raise _ClearAllError(f"expected file target but found non-file: {path}") + path.unlink() + return True + raise _ClearAllError(f"unknown clear-all target kind: {target.kind}") + + +def cmd_clear_all(args: argparse.Namespace) -> int: + """Delete all local reflexio data and openclaw-smart session buffers. + + Stops the managed backend first when it is running, wipes local Reflexio + data targets, removes local session JSONL buffers under ``state_dir()``, + then restarts the backend if it was running before. Requires ``--yes``. + + Args: + args (argparse.Namespace): Parsed CLI args. Honors ``args.yes`` + (skip the confirmation prompt). + + Returns: + int: 0 on success, 1 if reset is unsafe, unsupported, or fails. + """ + try: + targets = _resolve_clear_all_targets() + except _ClearAllError as exc: + sys.stderr.write(f"error: {exc}\n") + return 1 + + if not args.yes: + target_lines = "\n".join( + f" - {target.path} ({target.label})" for target in targets + ) + sys.stdout.write( + "This will permanently delete ALL local reflexio data at:\n" + f"{target_lines}\n" + f"and local session buffers under {state.state_dir()}.\n" + "If the reflexio backend is running, it will be stopped first " + "and restarted afterward.\n" + "Re-run with --yes to confirm.\n" + ) + return 1 + + was_running = _is_running_status(_service_status(_BACKEND_SCRIPT, wait_ready_s=0.0)) + if was_running: + sys.stdout.write("Stopping reflexio backend…\n") + stop_rc = _run_service(_BACKEND_SCRIPT, "stop") + if stop_rc != 0: + sys.stderr.write( + f"error: reflexio backend failed to stop (exit {stop_rc})\n" + ) + return stop_rc or 1 + + removed_targets = 0 + try: + for target in targets: + if _remove_clear_all_target(target): + removed_targets += 1 + except (OSError, _ClearAllError) as exc: + sys.stderr.write(f"error: could not remove reflexio data: {exc}\n") + return 1 + + removed_buffers = 0 + root = state.state_dir() + if root.is_dir(): + for buf in root.glob("*.jsonl"): + try: + buf.unlink() + removed_buffers += 1 + except OSError as exc: + sys.stderr.write(f"warning: could not remove {buf}: {exc}\n") + + start_rc = 0 + if was_running: + sys.stdout.write("Starting reflexio backend…\n") + start_rc = _run_service(_BACKEND_SCRIPT, "start") + + target_summary = ( + f"removed {removed_targets} data target(s)" + if removed_targets + else "nothing to wipe" + ) + sys.stdout.write( + f"Cleared reflexio: {target_summary}. " + f"Removed {removed_buffers} local session buffer(s).\n" + ) + return start_rc or 0 + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="openclaw-smart") + sub = parser.add_subparsers(dest="command", required=True) + + sh = sub.add_parser( + "show", + help="Show current project-specific skills and project preferences", + ) + sh.add_argument("--project", help="Override project id") + sh.set_defaults(func=cmd_show) + + ln = sub.add_parser( + "learn", + help="Force reflexio extraction over the active session now", + ) + ln.add_argument("--session", help="Session id (defaults to latest)") + ln.add_argument("--project", help="Override project id") + ln.add_argument( + "--note", + default=None, + help=( + "Free-form note to publish as a User turn before extraction " + "(e.g. an insight, preference, or workflow rule)" + ), + ) + ln.set_defaults(func=cmd_learn) + + ca = sub.add_parser( + "clear-all", + help="Delete all local reflexio data and restart the backend", + ) + ca.add_argument( + "--yes", + action="store_true", + help="Confirm the destructive clear without prompting", + ) + ca.set_defaults(func=cmd_clear_all) + + rs = sub.add_parser( + "restart", + help="Restart the reflexio backend and dashboard to pick up changes", + ) + rs.add_argument( + "--skip-backend", + action="store_true", + help="Do not stop/start the reflexio backend", + ) + rs.add_argument( + "--skip-dashboard", + action="store_true", + help="Do not stop/start the dashboard", + ) + rs.add_argument( + "--no-rebuild", + action="store_true", + help="Skip the `npm run build` step before restarting the dashboard", + ) + rs.set_defaults(func=cmd_restart) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_cli.py b/reflexio/integrations/openclaw/plugin/tests/test_cli.py new file mode 100644 index 00000000..e6f4a6b0 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_cli.py @@ -0,0 +1,154 @@ +"""Tests for openclaw_smart.cli.""" + +from __future__ import annotations + +from argparse import Namespace +from unittest.mock import MagicMock, patch + +import pytest + +from openclaw_smart import cli + + +@pytest.fixture(autouse=True) +def isolate_state_dir(monkeypatch, tmp_path): + sessions = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(sessions)) + return sessions + + +def test_cmd_show_fetches_all_entities(capsys): + with patch("openclaw_smart.cli.Adapter") as Ad, patch( + "openclaw_smart.cli.ids.resolve_project_id", return_value="proj-x" + ): + Ad.return_value.fetch_all.return_value = ([], [], []) + rc = cli.cmd_show(Namespace(project=None)) + assert rc == 0 + out = capsys.readouterr().out + assert "proj-x" in out + + +def test_cmd_show_renders_markdown(capsys): + with patch("openclaw_smart.cli.Adapter") as Ad, patch( + "openclaw_smart.cli.ids.resolve_project_id", return_value="proj-x" + ): + Ad.return_value.fetch_all.return_value = ( + [{"user_playbook_id": "abc", "content": "Test rule"}], + [], + [], + ) + cli.cmd_show(Namespace(project=None)) + out = capsys.readouterr().out + assert "Test rule" in out + assert "[oc:" in out + + +def test_cmd_show_honors_project_override(capsys): + with patch("openclaw_smart.cli.Adapter") as Ad, patch( + "openclaw_smart.cli.ids.resolve_project_id", return_value="ignored" + ): + Ad.return_value.fetch_all.return_value = ([], [], []) + cli.cmd_show(Namespace(project="explicit-proj")) + Ad.return_value.fetch_all.assert_called_once() + kwargs = Ad.return_value.fetch_all.call_args[1] + assert kwargs["project_id"] == "explicit-proj" + + +def test_cmd_learn_no_session_returns_zero(isolate_state_dir): + rc = cli.cmd_learn(Namespace(session=None, project=None, note=None)) + assert rc == 0 + + +def test_cmd_learn_force_extracts(isolate_state_dir): + # Seed a session JSONL so _latest_session_id picks it up. + from openclaw_smart import state + + state.append("sess-1", {"role": "User", "content": "x"}) + with patch("openclaw_smart.cli.publish") as pub, patch( + "openclaw_smart.cli.ids.resolve_project_id", return_value="proj-x" + ): + pub.publish_unpublished.return_value = ("ok", 1) + rc = cli.cmd_learn(Namespace(session=None, project=None, note=None)) + assert rc == 0 + kwargs = pub.publish_unpublished.call_args[1] + assert kwargs["force_extraction"] is True + assert kwargs["session_id"] == "sess-1" + + +def test_cmd_learn_appends_note(isolate_state_dir): + from openclaw_smart import state + + state.append("sess-2", {"role": "User", "content": "earlier"}) + with patch("openclaw_smart.cli.publish") as pub, patch( + "openclaw_smart.cli.ids.resolve_project_id", return_value="proj-x" + ): + pub.publish_unpublished.return_value = ("ok", 2) + cli.cmd_learn(Namespace(session="sess-2", project=None, note="key insight")) + records = state.read_all("sess-2") + contents = [r.get("content") for r in records] + assert "key insight" in contents + + +def test_cmd_learn_handles_unreachable_backend(isolate_state_dir, capsys): + from openclaw_smart import state + + state.append("sess-3", {"role": "User", "content": "x"}) + with patch("openclaw_smart.cli.publish") as pub, patch( + "openclaw_smart.cli.ids.resolve_project_id", return_value="proj-x" + ): + pub.publish_unpublished.return_value = ("failed", 0) + rc = cli.cmd_learn(Namespace(session=None, project=None, note=None)) + assert rc == 1 + assert "Failed to reach reflexio" in capsys.readouterr().out + + +def test_cmd_clear_all_requires_yes(capsys): + with patch( + "openclaw_smart.cli._resolve_clear_all_targets", return_value=[] + ): + rc = cli.cmd_clear_all(Namespace(yes=False)) + assert rc != 0 + assert "--yes" in capsys.readouterr().out + + +def test_cmd_clear_all_with_yes_proceeds(monkeypatch, tmp_path): + sessions = tmp_path / "sessions" + sessions.mkdir() + (sessions / "old.jsonl").write_text('{"role":"User"}\n') + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(sessions)) + + with patch( + "openclaw_smart.cli._resolve_clear_all_targets", return_value=[] + ), patch("openclaw_smart.cli._service_status", return_value="not running"), patch( + "openclaw_smart.cli._run_service", return_value=0 + ): + rc = cli.cmd_clear_all(Namespace(yes=True)) + assert rc == 0 + assert not (sessions / "old.jsonl").exists() + + +def test_build_parser_accepts_show(): + parser = cli._build_parser() + args = parser.parse_args(["show"]) + assert args.command == "show" + assert args.project is None + + +def test_build_parser_accepts_learn_with_note(): + parser = cli._build_parser() + args = parser.parse_args(["learn", "--note", "hello"]) + assert args.command == "learn" + assert args.note == "hello" + + +def test_build_parser_accepts_clear_all_yes(): + parser = cli._build_parser() + args = parser.parse_args(["clear-all", "--yes"]) + assert args.yes is True + + +def test_build_parser_accepts_restart_flags(): + parser = cli._build_parser() + args = parser.parse_args(["restart", "--skip-backend", "--no-rebuild"]) + assert args.skip_backend is True + assert args.no_rebuild is True From 3cb3cde63a37d0a35b5cc830c31da56dfe3d7ba1 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:34:02 +0000 Subject: [PATCH 28/53] feat(openclaw/scripts): port _lib.sh shared utilities --- .../openclaw/plugin/scripts/_lib.sh | 371 ++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/scripts/_lib.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/_lib.sh b/reflexio/integrations/openclaw/plugin/scripts/_lib.sh new file mode 100644 index 00000000..679ea01a --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/_lib.sh @@ -0,0 +1,371 @@ +# shellcheck shell=bash +# Shared helpers for openclaw-smart plugin scripts. Source, do not execute. + +# openClaw hooks run with a minimal non-interactive PATH that often omits +# nvm/asdf/brew shims where `npm`, `uv`, etc. live. Pull the user's login-shell +# PATH the same way claude-mem does so hook-spawned scripts find them without +# the user having to mutate their global PATH. Best-effort — failures silent. +openclaw_smart_source_login_path() { + local _SHELL_PATH + if [ -n "${SHELL:-}" ] && [ -x "$SHELL" ]; then + if _SHELL_PATH="$("$SHELL" -lc 'printf %s "$PATH"' 2>/dev/null)"; then + [ -n "$_SHELL_PATH" ] && export PATH="$_SHELL_PATH:$PATH" + fi + fi +} + +# Prepend the astral.sh installer's default bin directories to PATH so a +# freshly-installed `uv` is reachable before the user re-sources their +# shell rc. Prepend (not append) so the just-installed binary wins over +# any stale copy earlier in PATH. Literals only — no subshell, so safe +# under `set -u`. +openclaw_smart_prepend_astral_bins() { + export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" +} + +# Prepend openclaw-smart's private Node.js install, if present. The Setup +# hook installs Node here when the user does not already have a suitable +# node/npm on PATH, so later hook-spawned dashboard scripts can run without +# requiring nvm/brew/global npm to be visible from openClaw's shell. +openclaw_smart_prepend_node_bins() { + local _OC_NODE_ROOT + _OC_NODE_ROOT="$HOME/.openclaw-smart/node/current" + export PATH="$_OC_NODE_ROOT/bin:$_OC_NODE_ROOT:$PATH" +} + +openclaw_smart_is_internal_invocation_env() { + if [ "${OPENCLAW_SMART_INTERNAL:-}" = "1" ]; then + return 0 + fi + return 1 +} + +openclaw_smart_dashboard_unavailable_marker() { + printf '%s\n' "$HOME/.openclaw-smart/dashboard-unavailable" +} + +openclaw_smart_node_recovery_hint() { + cat <<'EOF' +Recovery: + 1. Restart openClaw to let openclaw-smart retry its private Node.js install. + 2. If the retry is blocked by your network or OS policy, install Node.js 20.9+ manually: +EOF + if openclaw_smart_is_windows; then + cat <<'EOF' + - winget install OpenJS.NodeJS.LTS + - or download the LTS installer from https://nodejs.org/ +EOF + elif [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then + cat <<'EOF' + - brew install node + - or download the LTS installer from https://nodejs.org/ +EOF + else + cat <<'EOF' + - use your distro package manager for nodejs/npm + - or download the LTS archive from https://nodejs.org/ +EOF + fi + cat <<'EOF' + 3. Run /openclaw-smart:restart, then /openclaw-smart:dashboard. +EOF +} + +openclaw_smart_write_dashboard_unavailable() { + local reason marker + reason="$1" + marker="$(openclaw_smart_dashboard_unavailable_marker)" + mkdir -p "$(dirname "$marker")" + { + printf 'openclaw-smart dashboard is unavailable: %s\n\n' "$reason" + printf 'The learning backend and hooks can still work; only the dashboard UI is affected.\n\n' + printf 'Private Node.js location: %s\n\n' "$HOME/.openclaw-smart/node/current" + openclaw_smart_node_recovery_hint + } > "$marker" +} + +openclaw_smart_clear_dashboard_unavailable() { + rm -f "$(openclaw_smart_dashboard_unavailable_marker)" +} + +# Return 0 (true) if running under a Windows-flavoured bash (Git Bash, +# MSYS, Cygwin). Used to gate POSIX-only primitives (setsid, process +# groups) and route around Windows-specific potholes (the python3 App +# Execution Alias stub at WindowsApps\python3.exe). +openclaw_smart_is_windows() { + case "$(uname -s 2>/dev/null)" in + MINGW*|MSYS*|CYGWIN*) return 0 ;; + *) return 1 ;; + esac +} + +# Print the absolute path of a working python interpreter, or nothing +# (and return non-zero) if none is usable. On Windows, `python3` is +# usually the Microsoft Store "App Execution Alias" stub at +# %LocalAppData%\Microsoft\WindowsApps\python3.exe — `command -v python3` +# returns truthy but invoking it just prints a "Python was not found" +# message and exits non-zero. We probe with `-V` to filter the stub out +# and prefer `python` (the real interpreter when one is installed). +openclaw_smart_resolve_python() { + if openclaw_smart_is_windows; then + for cand in python python3; do + if command -v "$cand" >/dev/null 2>&1 && "$cand" -V >/dev/null 2>&1; then + command -v "$cand" + return 0 + fi + done + return 1 + fi + for cand in python3 python; do + if command -v "$cand" >/dev/null 2>&1 && "$cand" -V >/dev/null 2>&1; then + command -v "$cand" + return 0 + fi + done + return 1 +} + +openclaw_smart_download() { + local url dest src _OC_PY + url="$1" + dest="$2" + mkdir -p "$(dirname "$dest")" + case "$url" in + file://*) + src="${url#file://}" + cp "$src" "$dest" + return $? + ;; + esac + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" -o "$dest" + return $? + fi + if command -v wget >/dev/null 2>&1; then + wget -q -O "$dest" "$url" + return $? + fi + if command -v powershell >/dev/null 2>&1; then + URL="$url" DEST="$dest" powershell -NoProfile -ExecutionPolicy Bypass -Command \ + '$ProgressPreference="SilentlyContinue"; Invoke-WebRequest -Uri $env:URL -OutFile $env:DEST' + return $? + fi + if command -v pwsh >/dev/null 2>&1; then + URL="$url" DEST="$dest" pwsh -NoProfile -Command \ + '$ProgressPreference="SilentlyContinue"; Invoke-WebRequest -Uri $env:URL -OutFile $env:DEST' + return $? + fi + _OC_PY=$(openclaw_smart_resolve_python || true) + if [ -n "$_OC_PY" ]; then + "$_OC_PY" - "$url" "$dest" <<'PY' +import sys +import urllib.request + +url, dest = sys.argv[1], sys.argv[2] +with urllib.request.urlopen(url, timeout=60) as response: + data = response.read() +with open(dest, "wb") as fh: + fh.write(data) +PY + return $? + fi + return 1 +} + +openclaw_smart_sha256_file() { + local path _OC_PY + path="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | awk '{print $1}' + return "${PIPESTATUS[0]}" + fi + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$path" | awk '{print $1}' + return "${PIPESTATUS[0]}" + fi + if command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 "$path" | awk '{print $NF}' + return "${PIPESTATUS[0]}" + fi + _OC_PY=$(openclaw_smart_resolve_python || true) + if [ -n "$_OC_PY" ]; then + "$_OC_PY" - "$path" <<'PY' +import hashlib +import sys + +h = hashlib.sha256() +with open(sys.argv[1], "rb") as fh: + for chunk in iter(lambda: fh.read(1024 * 1024), b""): + h.update(chunk) +print(h.hexdigest()) +PY + return $? + fi + return 1 +} + +# Return 0 if `node` exists and satisfies the minimum major/minor pair. +# Patch versions are intentionally ignored because our requirement is a +# floor, not an exact runtime pin. +openclaw_smart_node_satisfies() { + local min_major min_minor version major minor + min_major="$1" + min_minor="$2" + command -v node >/dev/null 2>&1 || return 1 + version=$(node -v 2>/dev/null | sed 's/^v//') || return 1 + major=$(printf '%s' "$version" | awk -F. '{print $1}') + minor=$(printf '%s' "$version" | awk -F. '{print $2}') + [ -n "$major" ] && [ -n "$minor" ] || return 1 + case "$major:$minor" in + *[!0-9:]*|:*|*:) return 1 ;; + esac + [ "$major" -gt "$min_major" ] && return 0 + [ "$major" -eq "$min_major" ] && [ "$minor" -ge "$min_minor" ] +} + +openclaw_smart_resolve_npm() { + local cand + for cand in npm npm.cmd; do + if command -v "$cand" >/dev/null 2>&1; then + command -v "$cand" + return 0 + fi + done + return 1 +} + +openclaw_smart_npm_available() { + local npm_bin + npm_bin=$(openclaw_smart_resolve_npm || true) + [ -n "$npm_bin" ] || return 1 + "$npm_bin" --version >/dev/null 2>&1 +} + +openclaw_smart_log_max_bytes() { + printf '%s\n' "10000000" +} + +openclaw_smart_trim_log_file() { + local file max_bytes size tmp + file="$1" + max_bytes="${2:-$(openclaw_smart_log_max_bytes)}" + case "$max_bytes" in + ''|*[!0-9]*) return 0 ;; + esac + [ -f "$file" ] || return 0 + size=$(wc -c < "$file" 2>/dev/null | tr -d '[:space:]') || return 0 + case "$size" in + ''|*[!0-9]*) return 0 ;; + esac + [ "$size" -le "$max_bytes" ] && return 0 + + tmp="${file}.trim.$$" + if tail -c "$max_bytes" "$file" > "$tmp" 2>/dev/null; then + # Rewrite the existing path instead of replacing it, so a process with + # this file already open in append mode keeps writing to the capped file. + cat "$tmp" > "$file" + fi + rm -f "$tmp" +} + +openclaw_smart_append_capped_log() { + local file max_bytes + file="$1" + max_bytes="${2:-$(openclaw_smart_log_max_bytes)}" + shift 2 + mkdir -p "$(dirname "$file")" + openclaw_smart_trim_log_file "$file" "$max_bytes" + printf '%s\n' "$*" >> "$file" + openclaw_smart_trim_log_file "$file" "$max_bytes" +} + +# Spawn a command fully detached from the current shell so a hook timeout +# (openClaw's install/SessionStart budget) cannot kill it mid-flight. +# POSIX: setsid → python3 os.setsid → nohup (in that order of strength). +# Windows: nohup alone — Git Bash has no setsid, no process groups, and +# `os.setsid()` is POSIX-only; nohup ignores SIGHUP which is enough to +# survive the parent console closing. The python3 fallback is gated on a +# real-interpreter probe (-V) so the Windows App Execution Alias stub +# doesn't get invoked. Caller is responsible for redirecting stdout/stderr; +# we do not impose a log destination here. Stdin is closed so the child +# cannot inherit a tty. Use `$!` after this call to capture the pid. +openclaw_smart_spawn_detached() { + if openclaw_smart_is_windows; then + nohup "$@" < /dev/null & + return 0 + fi + if command -v setsid >/dev/null 2>&1; then + setsid nohup "$@" < /dev/null & + elif _OC_PY=$(openclaw_smart_resolve_python) && [ -n "$_OC_PY" ]; then + "$_OC_PY" -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \ + "$@" < /dev/null & + else + nohup "$@" < /dev/null & + fi +} + +# Terminate a process and (on POSIX) its whole process group, escalating +# from TERM to KILL after a short grace period. On Windows there are no +# POSIX process groups, so we use `taskkill /T /F /PID` which walks the +# child-process tree via the Windows job-object/parent-pid relationships +# — the closest equivalent to a group kill. +# +# Windows-specific subtlety: in Git Bash / MSYS, `$!` for a backgrounded +# job returns the MSYS pid (an internal counter), NOT the native Windows +# pid that taskkill needs. `ps -W` (or `-o winpid=`) exposes the WINPID +# column for the translation. If the lookup fails we fall back to +# treating the input as a native pid, so callers can pass either an MSYS +# pid (recorded via $!) or a Windows pid (from tasklist) interchangeably. +# The `//T //F //PID` syntax escapes Git Bash's MSYS path-mangling of +# arguments that begin with `/`. +openclaw_smart_kill_tree() { + pid="$1" + [ -z "$pid" ] && return 0 + if openclaw_smart_is_windows; then + # Git Bash's `ps` is the procps fork, not BSD/Linux ps; it has no + # -o option but its default header is `PID PPID PGID WINPID TTY ...`, + # so column 4 of the data row is the Windows pid. awk extracts it + # without depending on -o support. + target="" + if command -v ps >/dev/null 2>&1; then + target=$(ps -p "$pid" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d ' \r\n' || true) + fi + [ -z "$target" ] && target="$pid" + if command -v taskkill >/dev/null 2>&1; then + taskkill //T //F //PID "$target" >/dev/null 2>&1 || true + else + kill -TERM "$pid" 2>/dev/null || true + sleep 0.5 + kill -KILL "$pid" 2>/dev/null || true + fi + return 0 + fi + current_pgid="" + if command -v ps >/dev/null 2>&1; then + current_pgid=$(ps -o pgid= -p "$$" 2>/dev/null | tr -d ' ') + fi + if [ -n "$current_pgid" ] && [ "$pid" = "$current_pgid" ]; then + return 0 + fi + if ! kill -TERM -- "-$pid" 2>/dev/null; then + kill -TERM "$pid" 2>/dev/null || true + sleep 0.5 + kill -KILL "$pid" 2>/dev/null || true + return 0 + fi + for _ in 1 2 3 4 5; do + kill -0 -- "-$pid" 2>/dev/null || return 0 + sleep 0.2 + done + kill -KILL -- "-$pid" 2>/dev/null || true +} + +# Return 0 (true) if $1 names a pid file whose pid is currently alive. +# Silent on missing/empty/stale files. +openclaw_smart_pid_alive_file() { + pid_file="$1" + [ -f "$pid_file" ] || return 1 + pid=$(cat "$pid_file" 2>/dev/null || echo "") + [ -n "$pid" ] || return 1 + kill -0 "$pid" 2>/dev/null +} From fe43e6e99b4bc2d2888bb7851d3876830663d43e Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:34:34 +0000 Subject: [PATCH 29/53] feat(openclaw/scripts): port ensure-plugin-root.sh --- .../plugin/scripts/ensure-plugin-root.sh | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100755 reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh b/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh new file mode 100755 index 00000000..48a1f1de --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Maintain ~/.reflexio/openclaw-plugin-root as a symlink to the active plugin +# install dir so slash commands can reference one short path regardless +# of whether the user is on the remote marketplace or the local-dev install. +# +# Usage: ensure-plugin-root.sh [--force] +# --force overwrite any existing link (used by setup scripts) +# default self-heal only if the link is missing or points at an +# invalid target +set -eu + +TARGET="${1:-}" +FORCE="${2:-}" + +if [ -z "$TARGET" ]; then + echo "[openclaw-smart] ensure-plugin-root: usage: $0 [--force]" >&2 + exit 1 +fi + +if [ ! -f "$TARGET/pyproject.toml" ]; then + echo "[openclaw-smart] ensure-plugin-root: $TARGET is not a plugin dir (no pyproject.toml)" >&2 + exit 1 +fi + +LINK="$HOME/.reflexio/openclaw-plugin-root" +mkdir -p "$(dirname "$LINK")" + +if [ "$FORCE" = "--force" ]; then + ln -sfn "$TARGET" "$LINK" + echo "[openclaw-smart] plugin-root → $TARGET (forced)" >&2 + exit 0 +fi + +# Opt-in: when OPENCLAW_SMART_PLUGIN_ROOT_FOLLOW_SESSION=1 (set in the +# environment or in ~/.reflexio/.env), always relink to $TARGET so the +# symlink tracks the currently loaded plugin. Off by default to preserve +# a pinned local-dev link across sessions that load the remote plugin. +FOLLOW="${OPENCLAW_SMART_PLUGIN_ROOT_FOLLOW_SESSION:-}" +if [ -z "$FOLLOW" ] && [ -f "$HOME/.reflexio/.env" ]; then + FOLLOW="$(grep -E '^OPENCLAW_SMART_PLUGIN_ROOT_FOLLOW_SESSION=' "$HOME/.reflexio/.env" \ + | tail -n1 | cut -d= -f2-)" + # Strip a single pair of surrounding double or single quotes, if present. + FOLLOW="${FOLLOW#\"}"; FOLLOW="${FOLLOW%\"}" + FOLLOW="${FOLLOW#\'}"; FOLLOW="${FOLLOW%\'}" +fi +if [ "$FOLLOW" = "1" ]; then + ln -sfn "$TARGET" "$LINK" + echo "[openclaw-smart] plugin-root → $TARGET (follow-session)" >&2 + exit 0 +fi + +# Cache-tracking: if the link currently resolves to a path under the +# managed openClaw plugin cache (~/.openclaw/plugins/cache/), always +# retarget it to $TARGET. Plugin updates leave old version directories +# behind, so a valid pyproject.toml at the stale target is not proof the +# link is fresh. Links pointing outside the cache (e.g., a user's +# local-dev checkout) are left alone here and handled by the self-heal +# below. +if [ -L "$LINK" ]; then + # Literal target string, not realpath: we compare against what was written by ln -s. + CURRENT="$(readlink "$LINK" 2>/dev/null || true)" + case "$CURRENT" in + "$HOME/.openclaw/plugins/cache/"*) + CURRENT_NORM="${CURRENT%/}" + TARGET_NORM="${TARGET%/}" + if [ "$CURRENT_NORM" != "$TARGET_NORM" ]; then + ln -sfn "$TARGET" "$LINK" + echo "[openclaw-smart] plugin-root → $TARGET (cache-tracking, was $CURRENT)" >&2 + fi + exit 0 + ;; + esac +fi + +# Self-heal path: only rewrite the link if it's missing or its target is +# gone/invalid. This preserves a valid local-dev symlink set earlier by +# a setup script, so SessionStart hooks on the local install don't +# clobber the user's repo-pointing link. +if [ -f "$LINK/pyproject.toml" ]; then + exit 0 +fi + +ln -sfn "$TARGET" "$LINK" +echo "[openclaw-smart] plugin-root → $TARGET" >&2 From 6b811631460c9e06b921d3df5d84b96057f63a72 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:35:35 +0000 Subject: [PATCH 30/53] feat(openclaw/scripts): port hook_entry.sh dispatch --- .../openclaw/plugin/scripts/hook_entry.sh | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100755 reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh b/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh new file mode 100755 index 00000000..d266f251 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Dispatch an openClaw hook event to the openclaw_smart Python package. +# OPENCLAW_PLUGIN_ROOT points at the plugin dir (dev: /.../openclaw/plugin; +# installed: ~/.openclaw/plugins/cache/.../openclaw-smart/), +# which is also the Python project root with pyproject.toml + uv.lock. +# We invoke via `uv run --project` so the pinned env from uv.lock is used. +# +# If a prior Setup recorded an install failure at +# ~/.openclaw-smart/install-failed, short-circuit with a user-visible +# message instead of trying to run uv and failing silently. +set -eu + +HOST="openclaw" +EVENT="${1:-}" +case "$EVENT" in + openclaw) + HOST="$EVENT" + EVENT="${2:-}" + ;; +esac +if [ -z "$EVENT" ]; then + echo '' + exit 0 +fi + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=_lib.sh +. "$HERE/_lib.sh" +if openclaw_smart_is_internal_invocation_env; then + echo '' + exit 0 +fi +# Pick up uv from the user's login-shell PATH (covers ~/.local/bin populated +# by the astral.sh installer) so a fresh install works before the user +# restarts their terminal. Matches the pattern used by smart-install.sh. +openclaw_smart_source_login_path +# Explicit fallback for the astral.sh installer's default paths, in case +# the user's login-shell rc hasn't yet been re-sourced to pick them up. +openclaw_smart_prepend_astral_bins + +PLUGIN_ROOT="$(cd "$HERE/.." && pwd)" + +FAILURE_MARKER="$HOME/.openclaw-smart/install-failed" +STATE_DIR="$HOME/.openclaw-smart" +if [ -f "$FAILURE_MARKER" ]; then + if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then + python3 - "$FAILURE_MARKER" <<'PY' +import json, pathlib, sys +msg = pathlib.Path(sys.argv[1]).read_text().strip() or "unknown error" +print(json.dumps({ + "prependContext": ( + f"> **openclaw-smart is not installed correctly:** {msg}\n" + "> Re-run the plugin's Setup (restart openClaw) " + "or fix the underlying issue and delete " + "`~/.openclaw-smart/install-failed` to retry." + ) +})) +PY + else + echo '' + fi + exit 0 +fi + +if ! command -v uv >/dev/null 2>&1; then + # Self-heal from skipped Setup/SessionStart bootstrap. SessionStart can + # afford to wait because it has the install budget; prompt/tool hooks start + # the same installer detached so normal work is not blocked by first-run + # dependency setup. + if [ "${OPENCLAW_SMART_BOOTSTRAPPING:-}" = "1" ]; then + echo '' + exit 0 + fi + if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then + mkdir -p "$STATE_DIR" + if [ "$EVENT" = "session-start" ]; then + OPENCLAW_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2 + openclaw_smart_prepend_astral_bins + openclaw_smart_prepend_node_bins + if command -v uv >/dev/null 2>&1; then + bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true + fi + else + openclaw_smart_spawn_detached env OPENCLAW_SMART_BOOTSTRAPPING=1 \ + bash "$PLUGIN_ROOT/scripts/smart-install.sh" \ + >>"$STATE_DIR/install.log" 2>&1 || true + fi + fi + if ! command -v uv >/dev/null 2>&1; then + echo '' + exit 0 + fi +fi + +# Stdin is the hook payload JSON — stream it through to the Python CLI. +exec uv run --project "$PLUGIN_ROOT" --quiet python -m openclaw_smart.hook "$HOST" "$EVENT" From 7e760aa52adca5a8d5d807bf799255b4bf548305 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:36:07 +0000 Subject: [PATCH 31/53] feat(openclaw/scripts): port cli.sh skill wrapper --- .../openclaw/plugin/scripts/cli.sh | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100755 reflexio/integrations/openclaw/plugin/scripts/cli.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/cli.sh b/reflexio/integrations/openclaw/plugin/scripts/cli.sh new file mode 100755 index 00000000..ea27d7af --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/cli.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Wrapper for slash commands that invoke the openclaw_smart CLI via uv. +# openClaw runs `!` bash directives in slash command .md files in a +# non-interactive, non-login shell that does NOT source ~/.zshrc or +# ~/.bash_profile. As a result, binaries installed by smart-install.sh +# at ~/.local/bin (e.g. uv from the astral.sh installer) are invisible +# to those directives until the user manually re-sources their shell rc. +# This wrapper bootstraps PATH the same way hook_entry.sh does so the +# slash commands work on a fresh install. +set -eu + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=_lib.sh +. "$HERE/_lib.sh" +openclaw_smart_source_login_path +openclaw_smart_prepend_astral_bins +openclaw_smart_prepend_node_bins + +PLUGIN_ROOT="$(cd "$HERE/.." && pwd)" + +# If the Setup hook recorded an install failure, surface that reason +# instead of falling through to a generic "uv not found" — mirrors the +# branch at hook_entry.sh so slash commands and hooks behave consistently +# on a broken install. +FAILURE_MARKER="$HOME/.openclaw-smart/install-failed" +if [ -f "$FAILURE_MARKER" ]; then + msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "")" + [ -n "$msg" ] || msg="unknown error" + echo "openclaw-smart is not installed correctly: $msg" >&2 + echo "Re-run the plugin's Setup (restart openClaw) or fix the underlying issue and delete $FAILURE_MARKER to retry." >&2 + exit 1 +fi + +if ! command -v uv >/dev/null 2>&1; then + # Self-heal: the Setup/SessionStart hook may have been skipped (trust + # prompt declined, plugin enabled mid-session, etc.) leaving the install + # half-done. Run smart-install.sh inline so the user does not have to + # restart openClaw just to recover. + # Guard against recursion: if smart-install.sh ever shells back through + # this wrapper (e.g. a future migration step) we must not loop forever. + if [ "${OPENCLAW_SMART_BOOTSTRAPPING:-}" = "1" ]; then + echo "openclaw-smart: bootstrap recursion detected; aborting." >&2 + exit 1 + fi + if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then + echo "openclaw-smart: 'uv' not found — bootstrapping dependencies (~1-3 min on first install)..." >&2 + OPENCLAW_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2 + openclaw_smart_prepend_astral_bins + openclaw_smart_prepend_node_bins + fi + if ! command -v uv >/dev/null 2>&1; then + if [ -f "$FAILURE_MARKER" ]; then + msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "unknown error")" + echo "openclaw-smart: install failed: $msg" >&2 + echo "Fix the underlying issue and delete $FAILURE_MARKER to retry." >&2 + else + echo "openclaw-smart: 'uv' not found on PATH after bootstrap attempt." >&2 + echo "Install it from https://docs.astral.sh/uv/ or rerun $PLUGIN_ROOT/scripts/smart-install.sh manually." >&2 + fi + exit 1 + fi +fi + +exec uv run --project "$PLUGIN_ROOT" --quiet python -m openclaw_smart.cli "$@" From 1d070e0837a54017d7a3faf9f3e6aafdff019c98 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:37:28 +0000 Subject: [PATCH 32/53] feat(openclaw/scripts): port backend-service.sh (port 8081) --- .../plugin/scripts/backend-service.sh | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100755 reflexio/integrations/openclaw/plugin/scripts/backend-service.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh b/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh new file mode 100755 index 00000000..e619e70c --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +# Auto-start the reflexio FastAPI backend (port 8081) if it's not already +# running. Detached spawn, returns immediately so the SessionStart hook +# doesn't block the session. +# +# Subcommands: +# start probe /health; if nothing we recognize is on the port, +# spawn `uv run reflexio services start --only backend +# --no-reload` detached. Polls /health briefly so first +# use after session start lands on a warm server, then +# returns empty stdout regardless. +# stop SIGTERM the recorded process group, escalating to +# SIGKILL after a short grace period. +# session-end no-op by default; only stops the backend if +# OPENCLAW_SMART_BACKEND_STOP_ON_END=1 (opt-in — the +# backend is intended to be long-lived across sessions). +# status print "running on http://localhost:PORT" or "not running". +set -eu + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=_lib.sh +. "$HERE/_lib.sh" +openclaw_smart_source_login_path +openclaw_smart_prepend_astral_bins + +CMD="${1:-start}" +PORT=8081 +EMBEDDING_PORT="${EMBEDDING_PORT:-8082}" +# Pass through to `reflexio services start/stop` so the spawned backend +# binds to PORT. +export BACKEND_PORT="$PORT" +export EMBEDDING_PORT + +# Default: route extraction through the active host CLI + ONNX embedder +# so openclaw-smart works without any LLM API key. Users can opt out by +# pre-exporting these to 0. +export OPENCLAW_SMART_USE_LOCAL_CLI="${OPENCLAW_SMART_USE_LOCAL_CLI:-1}" +export OPENCLAW_SMART_USE_LOCAL_EMBEDDING="${OPENCLAW_SMART_USE_LOCAL_EMBEDDING:-1}" +if [ "${OPENCLAW_SMART_USE_LOCAL_EMBEDDING:-}" = "1" ]; then + export REFLEXIO_EMBEDDING_PROVIDER="${REFLEXIO_EMBEDDING_PROVIDER:-local_service}" + export REFLEXIO_EMBEDDING_SERVICE_URL="${REFLEXIO_EMBEDDING_SERVICE_URL:-http://127.0.0.1:$EMBEDDING_PORT}" +fi +PLUGIN_ROOT="$(cd "$HERE/.." && pwd)" + +# Pin the openclaw CLI explicitly so the reflexio backend's openclaw_provider +# can find it from a hook context whose PATH lacks the user's normal CLI dir. +if [ -z "${OPENCLAW_SMART_CLI_PATH:-}" ]; then + if _oc_cli_path=$(command -v openclaw 2>/dev/null) && [ -n "$_oc_cli_path" ]; then + export OPENCLAW_SMART_CLI_PATH="$_oc_cli_path" + elif [ -x "$HOME/.local/bin/openclaw" ]; then + export OPENCLAW_SMART_CLI_PATH="$HOME/.local/bin/openclaw" + fi + unset _oc_cli_path +fi + +STATE_DIR="$HOME/.openclaw-smart" +PID_FILE="$STATE_DIR/backend.pid" +LOG_FILE="$STATE_DIR/backend.log" +LOG_MAX_BYTES="$(openclaw_smart_log_max_bytes)" +mkdir -p "$STATE_DIR" +openclaw_smart_trim_log_file "$LOG_FILE" "$LOG_MAX_BYTES" + +emit_ok() { echo ''; } + +emit_start_failure() { + reason="$1" + if py=$(openclaw_smart_resolve_python 2>/dev/null); then + "$py" - "$reason" <<'PY' +import json +import sys + +reason = sys.argv[1].strip() +message = ( + "> **openclaw-smart learning backend is not running.** " + "Interactions are being buffered locally, but learning will not publish " + "until the backend starts.\n" +) +if reason: + message += f">\n> Last startup error: `{reason}`\n" +message += ( + ">\n> Make sure the openclaw CLI is on PATH so the local model provider " + "can be reached. Then run `/openclaw-smart:restart`." +) +print(json.dumps({"prependContext": message})) +PY + else + emit_ok + fi +} + +# Tree-kill the recorded process. +kill_group() { + openclaw_smart_kill_tree "$1" +} + +# True if /health returns 200. Reflexio's /health is a plain GET with no +# marker header, so we can't distinguish our backend from someone else's +# reflexio on the same port — if you run two reflexio instances on $PORT +# you'll get collision regardless of what we do here. +backend_healthy() { + command -v curl >/dev/null 2>&1 || return 1 + curl -sf -o /dev/null "http://127.0.0.1:$PORT/health" 2>/dev/null +} + +# True only if the recorded PID is alive AND /health responds. A stale +# PID file from a crashed backend is not enough — we must see the port +# actually answer, so next hook retries cleanly. +is_our_backend_running() { + if [ -f "$PID_FILE" ]; then + pid=$(cat "$PID_FILE" 2>/dev/null || echo "") + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + backend_healthy && return 0 + fi + fi + # Recover from a missing PID file if a foreign-but-functional reflexio + # is already serving — no need to start a second one. + backend_healthy && return 0 + return 1 +} + +# True if *anything* is listening on the port (even non-HTTP). Used to +# avoid stomping on a foreign listener with a failed-to-start uvicorn. +port_occupied() { + (echo >"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null +} + +# Reap any reflexio/uvicorn listener still holding $PORT after the PID +# file kill. Filters by cmdline so we don't knock over an unrelated +# service a user has bound to $PORT — symmetric with start's refusal to +# stomp on a foreign listener. Silent on failure. +reap_port_listeners() { + command -v lsof >/dev/null 2>&1 || return 0 + candidates=$(lsof -ti:"$PORT" 2>/dev/null) || candidates="" + [ -z "$candidates" ] && return 0 + ours="" + for pid in $candidates; do + cmdline=$(ps -p "$pid" -o command= 2>/dev/null || true) + case "$cmdline" in + *reflexio*|*uvicorn*) ours="$ours $pid" ;; + esac + done + [ -z "$ours" ] && return 0 + # shellcheck disable=SC2086 + kill -TERM $ours 2>/dev/null || true + sleep 1 + remaining="" + for pid in $ours; do + kill -0 "$pid" 2>/dev/null && remaining="$remaining $pid" + done + [ -z "$remaining" ] && return 0 + # shellcheck disable=SC2086 + kill -KILL $remaining 2>/dev/null || true +} + +# Full shutdown: kill the recorded process group (if any) then sweep the +# port for surviving reflexio listeners. Used by both `stop` and the +# opt-in `session-end` path so a stale/missing PID file doesn't produce +# a silent no-op. +full_stop() { + if [ -f "$PID_FILE" ]; then + kill_group "$(cat "$PID_FILE" 2>/dev/null)" + rm -f "$PID_FILE" + fi + reap_port_listeners +} + +case "$CMD" in + start) + if openclaw_smart_is_internal_invocation_env; then + emit_ok; exit 0 + fi + # Opt-out: users who don't want the backend managed by the hook can + # set OPENCLAW_SMART_BACKEND_AUTOSTART=0. + if [ "${OPENCLAW_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then + emit_ok; exit 0 + fi + if is_our_backend_running; then emit_ok; exit 0; fi + if port_occupied; then + # Something answered the TCP probe but /health didn't — don't + # start a second uvicorn on top of it. + openclaw_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[openclaw-smart] backend: port $PORT held by another process; skipping" + emit_ok; exit 0 + fi + if ! command -v uv >/dev/null 2>&1; then + if [ "${OPENCLAW_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then + openclaw_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[openclaw-smart] backend: uv not on PATH; running installer" + OPENCLAW_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true + openclaw_smart_source_login_path + openclaw_smart_prepend_astral_bins + fi + if ! command -v uv >/dev/null 2>&1; then + openclaw_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[openclaw-smart] backend: uv not on PATH after installer; skipping" + emit_ok; exit 0 + fi + fi + # The reflexio project root lives four levels above the plugin dir: + # plugin/ -> integrations/openclaw -> reflexio/integrations -> reflexio -> open_source/reflexio + REFLEXIO_PROJECT="$(cd "$PLUGIN_ROOT/../../../.." && pwd)" + cd "$REFLEXIO_PROJECT" + + # Cap local interaction history to keep the SQLite store small for + # openclaw-smart users. Reflexio's library defaults are much higher + # (250k/50k) for server deployments; here we override only in the + # openclaw-smart plugin context. Users can still override via env. + export INTERACTION_CLEANUP_THRESHOLD="${INTERACTION_CLEANUP_THRESHOLD:-500}" + export INTERACTION_CLEANUP_DELETE_COUNT="${INTERACTION_CLEANUP_DELETE_COUNT:-200}" + + # backend-log-runner.sh owns stdout/stderr capture so process output + # cannot grow backend.log past its cap. + # + # --workers: reflexio defaults to 2 (zero-downtime worker recycling + # for server deployments). For a single-user openClaw plugin that's + # pure overhead: ~1.1 GB extra RSS, periodic 5–10 s spawn hiccups + # during worker rotation, and SQLite can't accept concurrent writers + # anyway. Default to 1 here; opt in to N via + # OPENCLAW_SMART_BACKEND_WORKERS for power users running concurrent + # openClaw sessions or wanting zero-downtime recycling. + workers="${OPENCLAW_SMART_BACKEND_WORKERS:-1}" + openclaw_smart_spawn_detached bash "$HERE/backend-log-runner.sh" \ + "$LOG_FILE" "$LOG_MAX_BYTES" -- \ + uv run --project "$REFLEXIO_PROJECT" --quiet \ + reflexio services start --only backend --no-reload --workers "$workers" + svc_pid=$! + # Record the spawned pid, not a pgid sampled with ps. On POSIX, + # setsid/python os.setsid make this pid the new process group leader; + # sampling immediately can race and capture the caller's pgid instead. + echo "$svc_pid" > "$PID_FILE" + + # Give uvicorn up to ~10s to answer /health. The very first boot + # after a fresh checkout may be slower (LiteLLM import, chromadb + # warmup). We always return ok; the backend catches up in background + # if it needs to. + for _ in 1 2 3 4 5 6 7 8 9 10; do + backend_healthy && break + sleep 1 + done + if ! backend_healthy; then + pid=$(cat "$PID_FILE" 2>/dev/null || echo "") + if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then + reason=$(tail -n 120 "$LOG_FILE" 2>/dev/null | grep -E "No LLM provider available|No generation-capable LLM provider available|CLI not found|skipping provider registration|Application startup failed" | tail -n 1 | sed 's/^[[:space:]]*//') + emit_start_failure "$reason" + exit 0 + fi + fi + emit_ok + ;; + stop) + full_stop + emit_ok + ;; + session-end) + # Default: leave the backend running so learning keeps flowing + # between sessions. Opt in to teardown with + # OPENCLAW_SMART_BACKEND_STOP_ON_END=1. + if [ "${OPENCLAW_SMART_BACKEND_STOP_ON_END:-0}" = "1" ]; then + full_stop + fi + emit_ok + ;; + status) + if is_our_backend_running; then echo "running on http://localhost:$PORT"; else echo "not running"; fi + ;; + *) + emit_ok + ;; +esac From 9820d4d87e02b42950edb9a4765982cf54140cd7 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:37:51 +0000 Subject: [PATCH 33/53] feat(openclaw/scripts): port backend-log-runner.sh --- .../plugin/scripts/backend-log-runner.sh | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh b/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh new file mode 100755 index 00000000..52175697 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Run the backend command and stream its stdout/stderr into a capped log file. +set -eu + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=_lib.sh +. "$HERE/_lib.sh" + +if [ "$#" -lt 4 ] || [ "$3" != "--" ]; then + exit 2 +fi + +LOG_FILE="$1" +MAX_BYTES="$2" +shift 3 + +mkdir -p "$(dirname "$LOG_FILE")" +openclaw_smart_trim_log_file "$LOG_FILE" "$MAX_BYTES" + +STATUS_FILE="${TMPDIR:-/tmp}/openclaw-smart-backend-log-runner.$$.status" +rm -f "$STATUS_FILE" + +( + set +e + "$@" 2>&1 + printf '%s\n' "$?" > "$STATUS_FILE" +) | while IFS= read -r line || [ -n "$line" ]; do + openclaw_smart_append_capped_log "$LOG_FILE" "$MAX_BYTES" "$line" +done + +status=$(cat "$STATUS_FILE" 2>/dev/null || printf '1') +rm -f "$STATUS_FILE" +exit "$status" From c4a7ece504bfec182855173ec4c0b26b1245b849 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:38:12 +0000 Subject: [PATCH 34/53] feat(openclaw/scripts): port dashboard-open.sh (shared dashboard with claude-smart) --- .../openclaw/plugin/scripts/dashboard-open.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh b/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh new file mode 100755 index 00000000..69634d71 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Open the reflexio dashboard in the user's browser. The dashboard itself +# (Next.js app) is provided by the claude-smart sibling plugin and listens +# on http://localhost:3001 by default — both plugins target the same URL. +# We only need the "open browser" hop here. +set -eu + +URL="${REFLEXIO_DASHBOARD_URL:-http://localhost:3001}" +if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$URL" >/dev/null 2>&1 || true +elif command -v open >/dev/null 2>&1; then + open "$URL" >/dev/null 2>&1 || true +else + echo "Open this URL in your browser: $URL" +fi From 815693ddd92a0fedc21a0d81b90e9a51e0add7d2 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:40:09 +0000 Subject: [PATCH 35/53] feat(openclaw/scripts): port smart-install.sh first-run setup --- .../openclaw/plugin/scripts/smart-install.sh | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100755 reflexio/integrations/openclaw/plugin/scripts/smart-install.sh diff --git a/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh b/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh new file mode 100755 index 00000000..cf06c67f --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash +# Run once on plugin install. Syncs the Python env and flips on the +# openclaw LiteLLM provider in reflexio's .env so extraction works with no +# external API key. +# +# On failure, writes the reason to ~/.openclaw-smart/install-failed so +# hook_entry.sh can short-circuit and surface a user-visible message +# instead of silently no-op'ing every session. +# +# Dashboard: openclaw-smart shares claude-smart's dashboard at +# http://localhost:3001. We do not build a dashboard here. +set -eu + +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=_lib.sh +. "$HERE/_lib.sh" +openclaw_smart_source_login_path +openclaw_smart_prepend_astral_bins + +PLUGIN_ROOT="$(cd "$HERE/.." && pwd)" + +MARKER_DIR="$HOME/.openclaw-smart" +FAILURE_MARKER="$MARKER_DIR/install-failed" +SUCCESS_MARKER="$MARKER_DIR/install-complete" +INSTALL_LOCK="$MARKER_DIR/install.lock" +INSTALL_REAP_LOCK="$MARKER_DIR/install.lock.reap" +mkdir -p "$MARKER_DIR" + +remove_stale_install_lock() { + local expected current + + expected="$1" + if ! mkdir "$INSTALL_REAP_LOCK" 2>/dev/null; then + sleep 1 + return 0 + fi + current="$(cat "$INSTALL_LOCK" 2>/dev/null || true)" + if [ "$current" = "$expected" ]; then + rm -f "$INSTALL_LOCK" + fi + rmdir "$INSTALL_REAP_LOCK" 2>/dev/null || true +} + +acquire_install_lock() { + local lock_pid + + if command -v flock >/dev/null 2>&1; then + exec 9>"$INSTALL_LOCK" + if ! flock 9; then + echo "[openclaw-smart] install lock failed; continuing without serialization" >&2 + echo '' + exit 0 + fi + return 0 + fi + + while ! ( set -C; printf '%s\n' "$$" > "$INSTALL_LOCK" ) 2>/dev/null; do + lock_pid="$(cat "$INSTALL_LOCK" 2>/dev/null || true)" + case "$lock_pid" in + ''|*[!0-9]*) + remove_stale_install_lock "$lock_pid" + ;; + *) + if kill -0 "$lock_pid" 2>/dev/null; then + sleep 1 + else + remove_stale_install_lock "$lock_pid" + fi + ;; + esac + done + trap '[ "$(cat "$INSTALL_LOCK" 2>/dev/null || true)" = "$$" ] && rm -f "$INSTALL_LOCK" || true' EXIT +} + +# Serialize concurrent installer runs (SessionStart hook + slash-command +# self-heal can both invoke this script). Wait for the active installer +# rather than returning early, otherwise callers can re-check uv before +# the first install has finished and report a false missing-dependency error. +acquire_install_lock + +rm -f "$FAILURE_MARKER" + +write_failure() { + local reason + reason="$1" + printf '%s\n' "$reason" > "$FAILURE_MARKER" + rm -f "$SUCCESS_MARKER" + echo "[openclaw-smart] install failed: $reason" >&2 + echo '' + exit 0 +} + +fingerprint_file() { + local path + path="$1" + if [ -f "$path" ]; then + cksum "$path" 2>/dev/null | awk '{print $1 ":" $2}' + else + printf 'missing\n' + fi +} + +install_fingerprint() { + printf 'plugin_root=%s\n' "$PLUGIN_ROOT" + printf 'smart_install=%s\n' "$(fingerprint_file "$HERE/smart-install.sh")" + printf 'pyproject=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/pyproject.toml")" + printf 'uv_lock=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/uv.lock")" + # Resolved python interpreter — catches a system upgrade (3.12.4 → 3.12.5) + # that would otherwise let install_complete return true against a venv + # built against a now-deleted interpreter. + if command -v uv >/dev/null 2>&1; then + printf 'python=%s\n' "$(uv python find 3.12 2>/dev/null || echo missing)" + else + printf 'python=no-uv\n' + fi +} + +install_complete() { + [ -f "$SUCCESS_MARKER" ] || return 1 + [ "$(cat "$SUCCESS_MARKER" 2>/dev/null || true)" = "$(install_fingerprint)" ] || return 1 + command -v uv >/dev/null 2>&1 || return 1 + [ -d "$PLUGIN_ROOT/.venv" ] || return 1 + [ -f "$HOME/.reflexio/.env" ] || return 1 + grep -q '^OPENCLAW_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1 + grep -q '^OPENCLAW_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1 + return 0 +} + +write_success_marker() { + install_fingerprint > "$SUCCESS_MARKER" +} + +preflight_supported_runtime_platform() { + local os_name machine darwin_major + os_name="$(uname -s 2>/dev/null || echo unknown)" + machine="$(uname -m 2>/dev/null || echo unknown)" + case "$os_name" in + Darwin*) + if [ "$machine" != "arm64" ]; then + write_failure "openclaw-smart currently supports Apple Silicon macOS 14+ only; Intel Mac is not supported because native ML wheels are unavailable." + fi + darwin_major="$(uname -r 2>/dev/null | awk -F. '{print $1}')" + case "$darwin_major" in + ''|*[!0-9]*) + write_failure "openclaw-smart could not determine the macOS version; Apple Silicon macOS 14+ is required." + ;; + esac + if [ "$darwin_major" -lt 23 ]; then + write_failure "openclaw-smart currently supports macOS 14+ on Apple Silicon; macOS 13 and older are not supported because native ML wheels are unavailable." + fi + ;; + MINGW*|MSYS*|CYGWIN*) + case "$machine" in + x86_64|amd64) : ;; + *) + write_failure "openclaw-smart currently supports Windows x64 only; Windows ARM is not supported because native ML wheels are unavailable." + ;; + esac + ;; + Linux*) + : # Existing Linux installs remain supported when package wheels are available. + ;; + *) + write_failure "openclaw-smart currently supports Apple Silicon macOS 14+, Windows x64, and Linux for vanilla installs." + ;; + esac +} + +preflight_supported_runtime_platform + +if install_complete; then + echo '' + exit 0 +fi + +if ! command -v uv >/dev/null 2>&1; then + echo "[openclaw-smart] uv not found — installing from astral.sh..." >&2 + # The astral.sh bash installer downloads a zip and unzips it. On + # Windows-flavoured bash (Git Bash / MSYS) the bundled `unzip` corrupts + # the Windows uv binary (bad CRC on the inflated uv.exe), leaving the + # install half-finished. Use the official PowerShell installer + # (install.ps1) on Windows, which writes uv.exe to ~/.local/bin + # natively — same destination the bash installer targets on POSIX, so + # openclaw_smart_prepend_astral_bins picks it up uniformly afterwards. + if openclaw_smart_is_windows; then + if ! command -v powershell >/dev/null 2>&1; then + write_failure "uv install needs PowerShell on Windows but powershell is not on PATH — install uv manually from https://docs.astral.sh/uv/" + fi + if ! powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://astral.sh/uv/install.ps1 | iex" >&2; then + write_failure "uv install via PowerShell failed — install manually from https://docs.astral.sh/uv/" + fi + else + UV_INSTALLER="$MARKER_DIR/uv-install.sh" + if ! openclaw_smart_download https://astral.sh/uv/install.sh "$UV_INSTALLER"; then + write_failure "uv installer download failed — install manually from https://docs.astral.sh/uv/" + fi + if ! sh "$UV_INSTALLER" >&2; then + write_failure "uv install failed — install manually from https://docs.astral.sh/uv/" + fi + fi + openclaw_smart_prepend_astral_bins + if ! command -v uv >/dev/null 2>&1; then + UV_FOUND="" + for candidate in "$HOME/.local/bin/uv" "$HOME/.local/bin/uv.exe" "$HOME/.cargo/bin/uv" "$HOME/bin/uv"; do + if [ -x "$candidate" ]; then + UV_FOUND="$candidate" + break + fi + done + if [ -n "$UV_FOUND" ]; then + write_failure "uv installed at $UV_FOUND — add its parent directory to PATH in your shell rc" + else + write_failure "uv install reported success but binary not found — install manually from https://docs.astral.sh/uv/" + fi + fi +fi + +cd "$PLUGIN_ROOT" +echo "[openclaw-smart] running uv sync..." >&2 +if ! uv sync --locked --python 3.12 --quiet >&2; then + write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose" +fi + +# Reflexio's CLI reads ~/.reflexio/.env (see reflexio/cli/env_loader.py); +# append our two opt-in flags there so `reflexio services start` picks +# them up regardless of which directory the user runs it from. Keep +# claude-smart's flags intact if it is also installed. +REFLEXIO_ENV="$HOME/.reflexio/.env" +mkdir -p "$(dirname "$REFLEXIO_ENV")" +touch "$REFLEXIO_ENV" +if ! grep -q '^OPENCLAW_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then + printf '\n# Route reflexio generation through the local openClaw CLI\nOPENCLAW_SMART_USE_LOCAL_CLI=1\n' >> "$REFLEXIO_ENV" + echo "[openclaw-smart] appended OPENCLAW_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2 +fi +if ! grep -q '^OPENCLAW_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then + printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nOPENCLAW_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV" + echo "[openclaw-smart] appended OPENCLAW_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2 +fi + +if ! command -v openclaw >/dev/null 2>&1; then + echo "[openclaw-smart] WARNING: 'openclaw' CLI not on PATH — reflexio extractors will have no LLM until it's installed" >&2 +fi + +# Point ~/.reflexio/openclaw-plugin-root at this install so slash commands +# can reference one stable short path regardless of which openClaw +# marketplace loaded us. +if ! bash "$HERE/ensure-plugin-root.sh" "$PLUGIN_ROOT"; then + echo "[openclaw-smart] WARNING: failed to set ~/.reflexio/openclaw-plugin-root symlink — slash commands may not resolve" >&2 +fi + +write_success_marker +echo "[openclaw-smart] install complete. Backend auto-starts on session start." >&2 +echo '' From 0297bcf0f4e09fa3291654f1ee35e9dd6c66ab93 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:42:28 +0000 Subject: [PATCH 36/53] feat(openclaw): TS shim + vitest tests for hook forwarding --- .../integrations/openclaw/plugin/index.ts | 96 +++++++++++++++++ .../tests-ts/__mocks__/plugin-entry-stub.ts | 6 ++ .../tests-ts/test_shim_dispatch.test.ts | 100 ++++++++++++++++++ .../openclaw/plugin/vitest.config.ts | 18 ++++ 4 files changed, 220 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/index.ts create mode 100644 reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts create mode 100644 reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts create mode 100644 reflexio/integrations/openclaw/plugin/vitest.config.ts diff --git a/reflexio/integrations/openclaw/plugin/index.ts b/reflexio/integrations/openclaw/plugin/index.ts new file mode 100644 index 00000000..243a9f2a --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/index.ts @@ -0,0 +1,96 @@ +// openclaw-smart — TS shim that forwards every openClaw hook to the +// Python openclaw_smart package via bash + uv. +// +// All logic lives in src/openclaw_smart/. This file only does SDK wiring. +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import * as path from "node:path"; + +type AnyHandler = (event: unknown, ctx: unknown) => unknown; + +const PLUGIN_ROOT = __dirname; +const HOOK_ENTRY = path.join(PLUGIN_ROOT, "scripts", "hook_entry.sh"); +const CLI_SCRIPT = path.join(PLUGIN_ROOT, "scripts", "cli.sh"); + +// Map openClaw hook name → bash event token + per-event timeout (ms). +const HOOKS: { name: string; token: string; timeoutMs: number }[] = [ + { name: "session_start", token: "session-start", timeoutMs: 30000 }, + { name: "before_prompt_build", token: "before-prompt-build", timeoutMs: 15000 }, + { name: "before_tool_call", token: "before-tool-call", timeoutMs: 10000 }, + { name: "after_tool_call", token: "after-tool-call", timeoutMs: 15000 }, + { name: "agent_end", token: "agent-end", timeoutMs: 30000 }, + { name: "session_end", token: "session-end", timeoutMs: 60000 }, +]; + +export default definePluginEntry({ + id: "reflexio-openclaw-smart", + name: "Reflexio openClaw Smart", + description: + "Cross-session memory via reflexio. Publishes conversations for extraction, " + + "injects relevant profiles and playbooks before each response.", + register(api) { + const log = api.logger; + const runner = api.runtime.system.runCommandWithTimeout; + const pluginConfig = api.pluginConfig ?? {}; + + // Track the most recent session key for the reflexio_publish tool callback. + let activeSessionKey = ""; + + for (const { name, token, timeoutMs } of HOOKS) { + const handler: AnyHandler = async (event, ctx) => { + const ctxObj = (ctx ?? {}) as { sessionKey?: string }; + if (ctxObj.sessionKey) activeSessionKey = ctxObj.sessionKey; + const payload = { event, ctx, plugin_config: pluginConfig }; + try { + const r = await runner( + ["bash", HOOK_ENTRY, "openclaw", token], + { timeoutMs, input: JSON.stringify(payload) }, + ); + if (r.code !== 0) { + log.debug?.(`hook ${name} exited ${r.code}: ${r.stderr?.slice(0, 200)}`); + return undefined; + } + const out = (r.stdout ?? "").trim(); + if (!out) return undefined; + return JSON.parse(out); + } catch (e) { + log.debug?.(`hook ${name} failed: ${(e as Error).message}`); + return undefined; + } + }; + // Cast: AnyHandler is the most general shape but the SDK's `on` overloads + // narrow specific events. We delegate all to the same shell entry point, + // so `as never` opts out of the specialized signatures. + api.on(name as never, handler as never); + } + + // Agent-invoked immediate publish. + api.registerTool({ + name: "reflexio_publish", + description: + "Immediately publish all buffered conversation turns to the Reflexio server. " + + "Use after user corrections or high-signal moments when you don't want to wait " + + "for the automatic session-end publish.", + parameters: { type: "object", properties: {} }, + optional: true, + async execute(_id, _params) { + try { + const r = await runner( + ["bash", CLI_SCRIPT, "learn", "--session", activeSessionKey], + { timeoutMs: 30000 }, + ); + const text = + r.code === 0 + ? r.stdout ?? "publish queued" + : `publish failed: ${r.stderr?.slice(0, 200)}`; + return { content: [{ type: "text" as const, text }] }; + } catch (e) { + return { + content: [ + { type: "text" as const, text: `publish error: ${(e as Error).message}` }, + ], + }; + } + }, + }); + }, +}); diff --git a/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts b/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts new file mode 100644 index 00000000..8fe9f0cb --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts @@ -0,0 +1,6 @@ +// Test-only stub for "openclaw/plugin-sdk/plugin-entry". The real module +// ships with the openclaw host runtime; under vitest we provide a +// passthrough so plugin definitions can be exercised in isolation. +export function definePluginEntry(def: T): T { + return def; +} diff --git a/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts b/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts new file mode 100644 index 00000000..4d0e380b --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi } from "vitest"; +import plugin from "../index.ts"; + +describe("openclaw-smart TS shim", () => { + it("registers all 6 hooks", async () => { + const onCalls: string[] = []; + const api = { + logger: {}, + runtime: { system: { runCommandWithTimeout: vi.fn() } }, + pluginConfig: {}, + registerTool: vi.fn(), + on: (name: string, _h: unknown) => { + onCalls.push(name); + }, + }; + await plugin.register(api as never); + expect(onCalls).toEqual([ + "session_start", + "before_prompt_build", + "before_tool_call", + "after_tool_call", + "agent_end", + "session_end", + ]); + }); + + it("registers the reflexio_publish tool", async () => { + const reg = vi.fn(); + const api = { + logger: {}, + runtime: { system: { runCommandWithTimeout: vi.fn() } }, + pluginConfig: {}, + registerTool: reg, + on: vi.fn(), + }; + await plugin.register(api as never); + expect(reg).toHaveBeenCalledWith( + expect.objectContaining({ name: "reflexio_publish" }), + ); + }); + + it("forwards a hook payload via bash and returns parsed JSON", async () => { + const run = vi.fn(async () => ({ + stdout: '{"prependContext":"hi"}', + stderr: "", + code: 0, + })); + const handlers: Record = {}; + const api = { + logger: {}, + runtime: { system: { runCommandWithTimeout: run } }, + pluginConfig: {}, + registerTool: vi.fn(), + on: (name: string, h: Function) => { + handlers[name] = h; + }, + }; + await plugin.register(api as never); + const result = await handlers["session_start"]({}, { sessionKey: "s1" }); + expect(result).toEqual({ prependContext: "hi" }); + expect(run).toHaveBeenCalledWith( + expect.arrayContaining(["bash"]), + expect.objectContaining({ input: expect.any(String) }), + ); + }); + + it("returns undefined when subprocess exits non-zero", async () => { + const run = vi.fn(async () => ({ stdout: "", stderr: "err", code: 1 })); + const handlers: Record = {}; + const api = { + logger: { debug: vi.fn() }, + runtime: { system: { runCommandWithTimeout: run } }, + pluginConfig: {}, + registerTool: vi.fn(), + on: (name: string, h: Function) => { + handlers[name] = h; + }, + }; + await plugin.register(api as never); + const result = await handlers["session_start"]({}, {}); + expect(result).toBeUndefined(); + }); + + it("returns undefined on empty stdout (no-op event)", async () => { + const run = vi.fn(async () => ({ stdout: "", stderr: "", code: 0 })); + const handlers: Record = {}; + const api = { + logger: { debug: vi.fn() }, + runtime: { system: { runCommandWithTimeout: run } }, + pluginConfig: {}, + registerTool: vi.fn(), + on: (name: string, h: Function) => { + handlers[name] = h; + }, + }; + await plugin.register(api as never); + const result = await handlers["before_tool_call"]({}, {}); + expect(result).toBeUndefined(); + }); +}); diff --git a/reflexio/integrations/openclaw/plugin/vitest.config.ts b/reflexio/integrations/openclaw/plugin/vitest.config.ts new file mode 100644 index 00000000..18450401 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/vitest.config.ts @@ -0,0 +1,18 @@ +import * as path from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests-ts/**/*.test.ts"], + }, + resolve: { + alias: { + // The real openclaw plugin SDK ships with the host. Under vitest we + // shim only the surface the plugin imports. + "openclaw/plugin-sdk/plugin-entry": path.resolve( + __dirname, + "tests-ts/__mocks__/plugin-entry-stub.ts", + ), + }, + }, +}); From 7f4086a5a37fd2e8c668cb9fb017fd095aa6fa4a Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:43:34 +0000 Subject: [PATCH 37/53] feat(openclaw/skills): add 6 SKILL.md files (reflexio, learn, show, dashboard, restart, clear-all) --- .../openclaw/plugin/skills/clear-all/SKILL.md | 8 ++++++++ .../openclaw/plugin/skills/dashboard/SKILL.md | 8 ++++++++ .../openclaw/plugin/skills/learn/SKILL.md | 10 ++++++++++ .../openclaw/plugin/skills/reflexio/SKILL.md | 19 +++++++++++++++++++ .../openclaw/plugin/skills/restart/SKILL.md | 6 ++++++ .../openclaw/plugin/skills/show/SKILL.md | 8 ++++++++ 6 files changed, 59 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md create mode 100644 reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md create mode 100644 reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md create mode 100644 reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md create mode 100644 reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md create mode 100644 reflexio/integrations/openclaw/plugin/skills/show/SKILL.md diff --git a/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md b/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md new file mode 100644 index 00000000..0a19e4cb --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md @@ -0,0 +1,8 @@ +# clear-all + +**When to use:** The user explicitly asks to delete all locally-stored skills/preferences. This is destructive and unrecoverable. + +## What to do +First confirm with the user: "This will delete ALL locally-learned skills and preferences. Are you sure?" If they confirm with yes/y, run: + +`bash "$OPENCLAW_PLUGIN_ROOT/scripts/cli.sh" clear-all --yes` diff --git a/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md b/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md new file mode 100644 index 00000000..dee5f3bf --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md @@ -0,0 +1,8 @@ +# dashboard + +**When to use:** The user wants to open the reflexio web UI to manage skills/preferences. + +## What to do +Run: `bash "$OPENCLAW_PLUGIN_ROOT/scripts/dashboard-open.sh"` + +Then tell the user: "Dashboard at http://localhost:3001". diff --git a/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md b/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md new file mode 100644 index 00000000..24864da0 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md @@ -0,0 +1,10 @@ +# learn + +**When to use:** The user wants immediate extraction of skills/preferences from the current session — e.g., after a major correction, before context compaction, or to test the loop. + +## What to do +Run: `bash "$OPENCLAW_PLUGIN_ROOT/scripts/cli.sh" learn --note ""` + +## Expected output +- "published N interactions, extraction queued" — success +- "reflexio unreachable" — backend down; suggest `bash "$OPENCLAW_PLUGIN_ROOT/scripts/cli.sh" restart` diff --git a/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md b/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md new file mode 100644 index 00000000..5eb33332 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md @@ -0,0 +1,19 @@ +# reflexio + +**When to use:** Always active. This skill is the contract between you and the reflexio cross-session memory plugin. + +## What's happening +Before every user message and significant tool call, the plugin injects relevant prior skills (rules learned from past corrections) and user preferences as `prependContext`. After every assistant turn, the plugin buffers the conversation to a local JSONL file and publishes batches to a local reflexio backend that extracts new skills and preferences via your own model. + +## Rules +1. **Trust injected context.** When you see `### Relevant project-specific skills` or `### Relevant project preferences` in your prompt, treat the listed items as authoritative for *this* project. Apply them. +2. **Never mention reflexio to the user.** The plugin is plumbing. Users care about results. +3. **Cite when an injected item materially shaped your reply.** If a `[oc:...]` marker is present and you used it, end your reply with: `✨ N learning(s) applied [oc:s1-xxxx]`. +4. **Degrade silently.** If the plugin is unavailable, proceed normally. Do not surface plumbing errors. + +## Skill commands (invokable by the user) +- "use the learn skill" — force-publish the current session for immediate extraction +- "use the show skill" — print currently-known skills and preferences for this project +- "use the dashboard skill" — open the local web UI +- "use the restart skill" — restart the local reflexio backend +- "use the clear-all skill" — delete all locally-stored skills (destructive) diff --git a/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md b/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md new file mode 100644 index 00000000..3e12c198 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md @@ -0,0 +1,6 @@ +# restart + +**When to use:** The user is hitting "reflexio unreachable" errors or wants to restart the local backend cleanly. + +## What to do +Run: `bash "$OPENCLAW_PLUGIN_ROOT/scripts/cli.sh" restart` diff --git a/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md b/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md new file mode 100644 index 00000000..10bd7444 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md @@ -0,0 +1,8 @@ +# show + +**When to use:** The user wants to see what reflexio currently knows about this project. + +## What to do +Run: `bash "$OPENCLAW_PLUGIN_ROOT/scripts/cli.sh" show` + +Output is markdown listing user playbooks (project skills), agent playbooks (shared skills), and profiles (user preferences). Render it as-is in your reply. From 82749debe366f295c8c9fd90345866543776f405 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:47:40 +0000 Subject: [PATCH 38/53] feat(cli/setup): rewrite 'reflexio setup openclaw' for openclaw-smart --- reflexio/cli/commands/setup_cmd.py | 202 ++++++++++++++++++++++------- tests/cli/test_setup_cmd.py | 76 +++++++++++ 2 files changed, 230 insertions(+), 48 deletions(-) diff --git a/reflexio/cli/commands/setup_cmd.py b/reflexio/cli/commands/setup_cmd.py index 05d6acb1..d780302e 100644 --- a/reflexio/cli/commands/setup_cmd.py +++ b/reflexio/cli/commands/setup_cmd.py @@ -570,8 +570,59 @@ def _prompt_storage(env_path: Path) -> str: raise typer.Exit(1) -def _install_openclaw_integration() -> bool: - """Install the Reflexio plugin into OpenClaw via the plugin system. +_OPENCLAW_PLUGIN_ID = "reflexio-openclaw-smart" + + +def _openclaw_plugin_dir() -> Path: + """Resolve the absolute path of the bundled openclaw-smart plugin dir.""" + import reflexio + + pkg_dir = Path(reflexio.__file__).parent + return pkg_dir / "integrations" / "openclaw" / "plugin" + + +def _write_openclaw_env(env_path: Path, openclaw_bin: str) -> None: + """Persist the openclaw CLI path and local-CLI opt-in to ``~/.reflexio/.env``.""" + _set_env_var(env_path, "OPENCLAW_BIN", openclaw_bin) + _set_env_var(env_path, "OPENCLAW_SMART_USE_LOCAL_CLI", "1") + + +def _remove_env_keys(env_path: Path, keys: tuple[str, ...]) -> None: + """Delete the given keys from a ``.env`` file. Silent on missing file/keys.""" + if not env_path.exists(): + return + lines = env_path.read_text().splitlines() + drop = {f"{k}=" for k in keys} + kept = [line for line in lines if not any(line.startswith(p) for p in drop)] + if len(kept) != len(lines): + env_path.write_text("\n".join(kept) + ("\n" if kept else "")) + + +def _run_smart_install(plugin_dir: Path) -> None: + """Run the plugin's first-run installer so uv/.venv land before first hook.""" + script = plugin_dir / "scripts" / "smart-install.sh" + if not script.exists(): + typer.echo(f"Warning: {script} missing, skipping first-run install") + return + proc = subprocess.run(["bash", str(script)], capture_output=True, text=True) + if proc.returncode != 0: + typer.echo( + "Warning: smart-install.sh exited " + f"{proc.returncode}; first session may take longer to bootstrap" + ) + if proc.stderr: + typer.echo(proc.stderr.strip()) + + +def _install_openclaw_integration(env_path: Path) -> bool: + """Install the openclaw-smart plugin into openClaw via the plugin system. + + Writes ``OPENCLAW_BIN`` and ``OPENCLAW_SMART_USE_LOCAL_CLI=1`` to + ``~/.reflexio/.env``, installs and enables the bundled plugin, runs the + first-run installer, and verifies the plugin is loaded. + + Args: + env_path (Path): Path to the ``~/.reflexio/.env`` file to update. Returns: bool: True if the plugin was verified as registered. @@ -579,30 +630,28 @@ def _install_openclaw_integration() -> bool: Raises: typer.Exit: If the openclaw CLI is not found on PATH. """ - if not shutil.which("openclaw"): + openclaw_bin = shutil.which("openclaw") + if not openclaw_bin: typer.echo("Error: openclaw CLI not found. Install from https://openclaw.ai") raise typer.Exit(1) - import reflexio - - pkg_dir = Path(reflexio.__file__).parent - plugin_dir = pkg_dir / "integrations" / "openclaw" / "plugin" - + plugin_dir = _openclaw_plugin_dir() if not plugin_dir.exists(): typer.echo(f"Error: plugin directory not found at {plugin_dir}") raise typer.Exit(1) + _write_openclaw_env(env_path, openclaw_bin) + # Clean install: remove any existing installation and stale extension dir subprocess.run( - ["openclaw", "plugins", "uninstall", "--force", "reflexio-federated"], + ["openclaw", "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID], check=False, capture_output=True, text=True, ) - stale_ext = Path.home() / ".openclaw" / "extensions" / "reflexio-federated" + stale_ext = Path.home() / ".openclaw" / "extensions" / _OPENCLAW_PLUGIN_ID shutil.rmtree(stale_ext, ignore_errors=True) - # Install plugin and restart gateway so inspect sees the new state try: subprocess.run( ["openclaw", "plugins", "install", str(plugin_dir)], @@ -611,13 +660,7 @@ def _install_openclaw_integration() -> bool: text=True, ) subprocess.run( - ["openclaw", "plugins", "enable", "reflexio-federated"], - check=True, - capture_output=True, - text=True, - ) - subprocess.run( - ["openclaw", "gateway", "restart"], + ["openclaw", "plugins", "enable", _OPENCLAW_PLUGIN_ID], check=True, capture_output=True, text=True, @@ -626,10 +669,21 @@ def _install_openclaw_integration() -> bool: typer.echo(f"Error: openclaw command failed: {exc.stderr or exc.stdout}") raise typer.Exit(1) from exc + # First-run: warm uv venv so the next hook isn't billed for cold install + _run_smart_install(plugin_dir) + + # Gateway restart is best-effort — older openclaw builds may not expose it + subprocess.run( + ["openclaw", "gateway", "restart"], + check=False, + capture_output=True, + text=True, + ) + # Verify — match exact "Status: loaded" to avoid false positives from # "not loaded" or "unloaded" result = subprocess.run( - ["openclaw", "plugins", "inspect", "reflexio-federated"], + ["openclaw", "plugins", "inspect", _OPENCLAW_PLUGIN_ID], capture_output=True, text=True, ) @@ -638,26 +692,33 @@ def _install_openclaw_integration() -> bool: return True typer.echo( - "Error: Plugin not loaded -- check 'openclaw plugins inspect reflexio-federated'" + f"Error: Plugin not loaded -- check 'openclaw plugins inspect {_OPENCLAW_PLUGIN_ID}'" ) return False -def _uninstall_openclaw() -> None: - """Remove the Reflexio integration from OpenClaw.""" +def _uninstall_openclaw(env_path: Path | None = None, purge: bool = False) -> None: + """Remove the openclaw-smart integration from openClaw. + + Args: + env_path (Path | None): Path to ``~/.reflexio/.env`` to strip the + ``OPENCLAW_BIN`` / ``OPENCLAW_SMART_USE_LOCAL_CLI`` keys from. ``None`` + skips the .env cleanup. + purge (bool): Also delete ``~/.openclaw-smart/`` state directory. + """ typer.confirm( - "This will remove the Reflexio integration from OpenClaw. Continue?", + "This will remove the Reflexio integration from openClaw. Continue?", abort=True, ) if shutil.which("openclaw"): subprocess.run( - ["openclaw", "plugins", "disable", "reflexio-federated"], + ["openclaw", "plugins", "disable", _OPENCLAW_PLUGIN_ID], check=False, capture_output=True, text=True, ) subprocess.run( - ["openclaw", "plugins", "uninstall", "--force", "reflexio-federated"], + ["openclaw", "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID], check=False, capture_output=True, text=True, @@ -665,6 +726,9 @@ def _uninstall_openclaw() -> None: else: typer.echo("Warning: openclaw CLI not found on PATH, skipping plugin removal") + if env_path is not None: + _remove_env_keys(env_path, ("OPENCLAW_BIN", "OPENCLAW_SMART_USE_LOCAL_CLI")) + # Remove setup markers from reflexio.cli.paths import reflexio_home @@ -674,7 +738,27 @@ def _uninstall_openclaw() -> None: marker.unlink(missing_ok=True) typer.echo(f"Removed setup marker: {marker}") - typer.echo("Reflexio integration fully removed from OpenClaw.") + if purge: + state_dir = Path.home() / ".openclaw-smart" + if state_dir.exists(): + shutil.rmtree(state_dir, ignore_errors=True) + typer.echo(f"Removed plugin state: {state_dir}") + + typer.echo("Reflexio integration fully removed from openClaw.") + + +def _repair_openclaw() -> None: + """Re-run the plugin's first-run installer and clear any failure marker.""" + plugin_dir = _openclaw_plugin_dir() + if not plugin_dir.exists(): + typer.echo(f"Error: plugin directory not found at {plugin_dir}") + raise typer.Exit(1) + failure_marker = Path.home() / ".openclaw-smart" / "install-failed" + if failure_marker.exists(): + failure_marker.unlink() + typer.echo(f"Cleared {failure_marker}") + _run_smart_install(plugin_dir) + typer.echo("Repair complete. Restart openClaw to pick up the refreshed env.") @app.command("openclaw") @@ -682,7 +766,27 @@ def openclaw( uninstall: Annotated[ bool, typer.Option( - "--uninstall", help="Remove the Reflexio integration from OpenClaw" + "--uninstall", help="Remove the Reflexio integration from openClaw" + ), + ] = False, + repair: Annotated[ + bool, + typer.Option( + "--repair", + help=( + "Re-run the plugin first-run installer and clear any failure " + "marker without reinstalling the plugin in openClaw." + ), + ), + ] = False, + purge: Annotated[ + bool, + typer.Option( + "--purge", + help=( + "With --uninstall: also delete ~/.openclaw-smart/ state " + "(buffered sessions, install markers, logs). Destructive." + ), ), ] = False, embedding: Annotated[ @@ -697,9 +801,21 @@ def openclaw( ), ] = "auto", ) -> None: - """Set up (or remove) the Reflexio integration for OpenClaw.""" + """Set up (or remove) the openclaw-smart integration for openClaw.""" + # Resolve the user-global .env path up front; all three flows write to it. + from reflexio.cli.env_loader import ensure_user_env_for_setup + + env_path = ensure_user_env_for_setup() + if env_path is None: + typer.echo("Error: could not locate or create a .env file") + raise typer.Exit(1) + + if repair: + _repair_openclaw() + return + if uninstall: - _uninstall_openclaw() + _uninstall_openclaw(env_path=env_path, purge=purge) return if embedding not in _VALID_EMBEDDING_FLAGS: @@ -709,26 +825,15 @@ def openclaw( ) raise typer.Exit(1) - # Step 1: Load .env path. Always target ~/.reflexio/.env — running setup - # from a worktree or project root that happens to contain its own .env - # would otherwise pollute that file via load_reflexio_env's CWD-first - # search. Setup writes are user-global, not project-local. - from reflexio.cli.env_loader import ensure_user_env_for_setup - - env_path = ensure_user_env_for_setup() - if env_path is None: - typer.echo("Error: could not locate or create a .env file") - raise typer.Exit(1) - - # Step 2: LLM provider + # Step 1: LLM provider display_name, model, _provider_key = _prompt_llm_provider(env_path) - # Step 3: Storage. Decided BEFORE the embedding step because Managed / + # Step 2: Storage. Decided BEFORE the embedding step because Managed / # Self-hosted modes own their own embedding config server-side, and a # local override would just shadow the operator's choice. storage_label = _prompt_storage(env_path) - # Step 3.5: Upfront embedding-provider step. Local is the default when + # Step 2.5: Upfront embedding-provider step. Local is the default when # chromadb is importable; the choice persists to org config so it # survives later cloud-key changes. Skipped for remote storage modes # for the reason above. @@ -737,12 +842,13 @@ def openclaw( if not is_remote: embedding_label = _choose_embedding_provider(env_path, embedding_flag=embedding) - # Step 4: Install OpenClaw integration + # Step 3: Install openclaw-smart integration typer.echo("") - hook_ok = _install_openclaw_integration() + hook_ok = _install_openclaw_integration(env_path) - # Step 5: Summary - hook_status = "reflexio-federated" if hook_ok else "reflexio-federated (unverified)" + hook_status = ( + _OPENCLAW_PLUGIN_ID if hook_ok else f"{_OPENCLAW_PLUGIN_ID} (unverified)" + ) typer.echo("") typer.echo("Setup complete!") @@ -753,7 +859,7 @@ def openclaw( typer.echo(f" Plugin: {hook_status}") typer.echo("") typer.echo("Next steps:") - typer.echo(" 1. Restart OpenClaw gateway: openclaw gateway restart") + typer.echo(" 1. Restart openClaw gateway: openclaw gateway restart") typer.echo( " 2. Start a conversation -- Reflexio will capture and learn automatically" ) diff --git a/tests/cli/test_setup_cmd.py b/tests/cli/test_setup_cmd.py index b609faf5..06841515 100644 --- a/tests/cli/test_setup_cmd.py +++ b/tests/cli/test_setup_cmd.py @@ -916,6 +916,82 @@ def test_openclaw_invalid_embedding_flag_exits(self) -> None: with pytest.raises(typer.Exit): openclaw(uninstall=False, embedding="opneai") + +class TestOpenclawSetup: + """Tests for the openclaw-smart install/uninstall/repair flows.""" + + def test_plugin_id_constant(self) -> None: + """The plugin id must match what openClaw and the TS shim register.""" + from reflexio.cli.commands.setup_cmd import _OPENCLAW_PLUGIN_ID + + assert _OPENCLAW_PLUGIN_ID == "reflexio-openclaw-smart" + + def test_write_openclaw_env_persists_keys(self, tmp_path: Path) -> None: + """``_write_openclaw_env`` upserts OPENCLAW_BIN + USE_LOCAL_CLI=1.""" + from reflexio.cli.commands.setup_cmd import _write_openclaw_env + + env = tmp_path / ".env" + _write_openclaw_env(env, "/usr/local/bin/openclaw") + body = env.read_text() + assert 'OPENCLAW_BIN="/usr/local/bin/openclaw"' in body + assert 'OPENCLAW_SMART_USE_LOCAL_CLI="1"' in body + + def test_remove_env_keys_strips_lines(self, tmp_path: Path) -> None: + """``_remove_env_keys`` drops the named keys, leaves others untouched.""" + from reflexio.cli.commands.setup_cmd import _remove_env_keys + + env = tmp_path / ".env" + env.write_text( + "OTHER=keep\n" + 'OPENCLAW_BIN="/usr/local/bin/openclaw"\n' + 'OPENCLAW_SMART_USE_LOCAL_CLI="1"\n' + "STILL=keep\n" + ) + _remove_env_keys(env, ("OPENCLAW_BIN", "OPENCLAW_SMART_USE_LOCAL_CLI")) + remaining = env.read_text().splitlines() + assert remaining == ["OTHER=keep", "STILL=keep"] + + def test_install_openclaw_uses_new_plugin_id( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """`_install_openclaw_integration` passes the new plugin id to subprocess.""" + from reflexio.cli.commands import setup_cmd + + env_path = tmp_path / ".env" + env_path.write_text("") + + monkeypatch.setattr(setup_cmd.shutil, "which", lambda _: "/usr/bin/openclaw") + plugin_dir = tmp_path / "plugin" + plugin_dir.mkdir() + monkeypatch.setattr(setup_cmd, "_openclaw_plugin_dir", lambda: plugin_dir) + monkeypatch.setattr(setup_cmd, "_run_smart_install", lambda _p: None) + + calls: list[list[str]] = [] + + class _Result: + def __init__(self, stdout: str = "Status: loaded\n") -> None: + self.stdout = stdout + self.stderr = "" + self.returncode = 0 + + def fake_run(argv, **_kw): # noqa: ANN001 + calls.append(list(argv)) + if argv[:3] == ["openclaw", "plugins", "inspect"]: + return _Result("Status: loaded\n") + return _Result("") + + monkeypatch.setattr(setup_cmd.subprocess, "run", fake_run) + + ok = setup_cmd._install_openclaw_integration(env_path) + + assert ok is True + flat_args = [arg for call in calls for arg in call] + assert "reflexio-openclaw-smart" in flat_args + assert "reflexio-federated" not in flat_args + body = env_path.read_text() + assert 'OPENCLAW_BIN="/usr/bin/openclaw"' in body + assert 'OPENCLAW_SMART_USE_LOCAL_CLI="1"' in body + def test_claude_code_invalid_embedding_flag_exits(self) -> None: """``setup claude-code --embedding=opneai`` must fail fast on the typo.""" from reflexio.cli.commands.setup_cmd import claude_code_setup From 61b6f243000dd56d9357abd0676db6b89741672a Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:49:31 +0000 Subject: [PATCH 39/53] test(openclaw): integration test for recursion guard --- .../openclaw/plugin/pyproject.toml | 4 + .../test_recursion_guard_integration.py | 86 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py diff --git a/reflexio/integrations/openclaw/plugin/pyproject.toml b/reflexio/integrations/openclaw/plugin/pyproject.toml index 0681dbb5..da6bf9b2 100644 --- a/reflexio/integrations/openclaw/plugin/pyproject.toml +++ b/reflexio/integrations/openclaw/plugin/pyproject.toml @@ -31,6 +31,10 @@ packages = ["src/openclaw_smart"] [tool.pytest.ini_options] testpaths = ["tests"] +markers = [ + "integration: tests that depend on shared state or a running reflexio backend", + "e2e: end-to-end tests that drive the full hook pipeline", +] [dependency-groups] dev = [ diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py new file mode 100644 index 00000000..f1c5ef81 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py @@ -0,0 +1,86 @@ +"""Integration: hook silently no-ops when OPENCLAW_SMART_INTERNAL=1. + +This guard prevents an infinite feedback loop where the reflexio backend's +own openclaw subprocess (the openclaw LLM provider) re-enters openclaw-smart +and republishes the extractor's system prompt back into reflexio. +""" + +from __future__ import annotations + +import io +import json +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.integration + + +@pytest.fixture +def isolated_state(monkeypatch, tmp_path: Path) -> Path: + """Force state.py to write under tmp_path so the test never touches ~/.openclaw-smart/.""" + state_dir = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(state_dir)) + return state_dir + + +def test_hook_short_circuits_when_internal_env_set( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + isolated_state: Path, +) -> None: + """With OPENCLAW_SMART_INTERNAL=1, every handler is bypassed and stdout is empty.""" + monkeypatch.setenv("OPENCLAW_SMART_INTERNAL", "1") + + from openclaw_smart import hook + + payload = { + "event": {"prompt": "anything"}, + "ctx": {"sessionKey": "internal-1", "agentId": "a"}, + } + monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload))) + + rc = hook.main(["openclaw", "before-prompt-build"]) + assert rc == 0 + + # No session JSONL should have been created — the handler bailed before + # ever touching state.append(). + assert not isolated_state.exists() or not list(isolated_state.glob("*.jsonl")) + + out = capsys.readouterr().out + # The TS shim treats empty stdout as "no return value" per the design. + assert out.strip() == "" + + +def test_hook_short_circuits_when_workspace_inside_reflexio( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + isolated_state: Path, + tmp_path: Path, +) -> None: + """A workspaceDir inside the reflexio install dir is also treated as internal. + + The reflexio backend cwd's into its own checkout when it spawns the openclaw + subprocess for the LLM provider. internal_call.is_internal_invocation honours + this so the recursion guard fires even if the env var got stripped. + """ + import reflexio + + reflexio_pkg_dir = Path(reflexio.__file__).resolve().parent + payload = { + "event": {"prompt": "anything"}, + "ctx": { + "sessionKey": "internal-2", + "agentId": "a", + "workspaceDir": str(reflexio_pkg_dir / "sub"), + }, + } + monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload))) + + from openclaw_smart import hook + + rc = hook.main(["openclaw", "before-prompt-build"]) + assert rc == 0 + out = capsys.readouterr().out + assert out.strip() == "" + assert not isolated_state.exists() or not list(isolated_state.glob("*.jsonl")) From 4dc0cfc65b94b37994a54cdd2d0a274eeb938ada Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:52:16 +0000 Subject: [PATCH 40/53] test(server/llm): integration test for openclaw_provider via LiteLLM --- .../llm/test_openclaw_provider_integration.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/server/llm/test_openclaw_provider_integration.py diff --git a/tests/server/llm/test_openclaw_provider_integration.py b/tests/server/llm/test_openclaw_provider_integration.py new file mode 100644 index 00000000..5200ce5f --- /dev/null +++ b/tests/server/llm/test_openclaw_provider_integration.py @@ -0,0 +1,103 @@ +"""Integration: LiteLLM dispatches to the openclaw provider end-to-end. + +Exercises ``register_if_enabled`` + ``litellm.completion`` together so a +regression in the registration glue (provider key, handler installation, +or kwargs routing) is caught even when each layer's unit tests pass. + +The ``subprocess.run`` call inside the provider is patched so the test +never spawns a real openclaw CLI. +""" + +from __future__ import annotations + +import json +from unittest.mock import patch + +import litellm +import pytest + +from reflexio.server.llm.providers import openclaw_provider as ocp + +pytestmark = pytest.mark.integration + + +def _fake_completed_process(stdout: str, *, stderr: str = "", returncode: int = 0): + """Build a CompletedProcess look-alike for ``patch('subprocess.run')``.""" + from subprocess import CompletedProcess + + return CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr) + + +@pytest.fixture(autouse=True) +def _reset_provider_state(monkeypatch: pytest.MonkeyPatch): + """Each test starts with the provider unregistered + a clean LiteLLM map.""" + monkeypatch.setattr(ocp, "_REGISTERED", False, raising=False) + monkeypatch.setattr(ocp, "_HANDLER", None, raising=False) + monkeypatch.setattr( + litellm, "custom_provider_map", [], raising=False + ) + yield + + +def test_litellm_dispatches_to_openclaw_provider( + monkeypatch: pytest.MonkeyPatch, tmp_path +) -> None: + """End-to-end: env opt-in → register → provider handler completes via fake CLI. + + Calls the registered handler directly rather than going through + ``litellm.completion`` because the root conftest installs a global + completion mock for unit/integration tests. The handler-side wiring is + exactly what LiteLLM would route to in production. + """ + fake_cli = tmp_path / "openclaw" + fake_cli.write_text("#!/bin/sh\nexit 0\n") + fake_cli.chmod(0o755) + monkeypatch.setenv(ocp.ENV_ENABLE, "1") + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(fake_cli)) + + assert ocp.register_if_enabled() is True + + # Registration installed our handler under the openclaw provider key. + entry = next( + item + for item in getattr(litellm, "custom_provider_map", []) + if item.get("provider") == ocp.PROVIDER_KEY + ) + handler = entry["custom_handler"] + assert isinstance(handler, ocp.OpenClawLLM) + + fake_proc = _fake_completed_process(json.dumps({"text": "PONG"})) + with patch("subprocess.run", return_value=fake_proc) as run: + resp = handler.completion( + model="openclaw/anthropic/claude-sonnet-4-6", + messages=[{"role": "user", "content": "ping"}], + ) + + assert resp.choices[0].message.content == "PONG" + + # Provider unwrapped the openclaw/ prefix and forwarded the inner model id. + argv = run.call_args[0][0] + assert "--model" in argv + assert "anthropic/claude-sonnet-4-6" in argv + # Recursion guard env is set on the spawned process. + spawn_env = run.call_args[1]["env"] + assert spawn_env.get("OPENCLAW_SMART_INTERNAL") == "1" + + +def test_register_noop_when_env_disabled(monkeypatch: pytest.MonkeyPatch) -> None: + """Without ``OPENCLAW_SMART_USE_LOCAL_CLI``, registration is skipped.""" + monkeypatch.delenv(ocp.ENV_ENABLE, raising=False) + assert ocp.register_if_enabled() is False + assert litellm.custom_provider_map == [] + + +def test_register_noop_when_cli_missing( + monkeypatch: pytest.MonkeyPatch, tmp_path +) -> None: + """Env opt-in without a resolvable CLI path is a logged no-op, not a crash.""" + monkeypatch.setenv(ocp.ENV_ENABLE, "1") + # Point to a path that doesn't exist *and* override PATH so the + # ``shutil.which`` fallback also misses. + monkeypatch.setenv(ocp.ENV_CLI_PATH, str(tmp_path / "does-not-exist")) + monkeypatch.setenv("PATH", str(tmp_path)) + assert ocp.register_if_enabled() is False From f45150fec9ce2b88aa4341c8729bde2590367a1c Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:54:57 +0000 Subject: [PATCH 41/53] test(openclaw): e2e session loop test + TS shim payload flattening --- .../integrations/openclaw/plugin/index.ts | 9 +- .../integration/test_e2e_session_loop.py | 182 ++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py diff --git a/reflexio/integrations/openclaw/plugin/index.ts b/reflexio/integrations/openclaw/plugin/index.ts index 243a9f2a..42c717b8 100644 --- a/reflexio/integrations/openclaw/plugin/index.ts +++ b/reflexio/integrations/openclaw/plugin/index.ts @@ -39,7 +39,14 @@ export default definePluginEntry({ const handler: AnyHandler = async (event, ctx) => { const ctxObj = (ctx ?? {}) as { sessionKey?: string }; if (ctxObj.sessionKey) activeSessionKey = ctxObj.sessionKey; - const payload = { event, ctx, plugin_config: pluginConfig }; + // Python handlers read a flat dict (e.g. payload.get("sessionKey"), + // payload.get("prompt")). Merge ctx first, event on top so any + // event-specific overrides win on key clash. + const payload = { + ...(ctxObj as Record), + ...((event ?? {}) as Record), + plugin_config: pluginConfig, + }; try { const r = await runner( ["bash", HOOK_ENTRY, "openclaw", token], diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py new file mode 100644 index 00000000..a8416fa2 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py @@ -0,0 +1,182 @@ +"""End-to-end: drive all 6 hooks with synthetic payloads, assert buffer state. + +Exercises the hook dispatcher → event-handler → state buffer pipeline in +order, the same sequence openClaw would trigger during a real session. The +reflexio adapter is patched to a no-op so the test never depends on a live +backend; failures inside ``publish`` would otherwise be tolerated silently +(per ``reflexio_adapter.Adapter.publish``) but explicit patching keeps the +test deterministic across CI runs. +""" + +from __future__ import annotations + +import io +import json +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +pytestmark = pytest.mark.e2e + + +@pytest.fixture +def isolated_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + """Force state.py to use a tmp directory so the test never touches ~/.openclaw-smart/.""" + state_dir = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(state_dir)) + return state_dir + + +@pytest.fixture +def stub_adapter(monkeypatch: pytest.MonkeyPatch) -> MagicMock: + """Replace every imported ``Adapter`` symbol with a MagicMock that returns success. + + Each consumer module does ``from openclaw_smart.reflexio_adapter import Adapter`` + at import time, so patching the source module alone leaves stale bindings in + consumers. We patch the consumer-side aliases as well so the test is + isolated regardless of import order. + """ + from openclaw_smart import ( # noqa: F401 — force-import the consumers + context_inject, + publish, + reflexio_adapter, + ) + from openclaw_smart.events import session_start + + fake = MagicMock() + fake.publish.return_value = True + fake.apply_extraction_defaults.return_value = True + fake.apply_optimizer_defaults.return_value = True + fake.fetch_stall_state.return_value = None + fake.search_all.return_value = ([], [], []) + + factory = lambda *a, **kw: fake # noqa: E731 — single-line lambda is clearer here + monkeypatch.setattr(reflexio_adapter, "Adapter", factory) + monkeypatch.setattr(publish, "Adapter", factory) + monkeypatch.setattr(context_inject, "Adapter", factory) + monkeypatch.setattr(session_start, "Adapter", factory) + return fake + + +def _drive(monkeypatch: pytest.MonkeyPatch, event: str, payload: dict) -> int: + from openclaw_smart import hook + + monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload))) + return hook.main(["openclaw", event]) + + +def test_full_session_loop_records_all_roles( + monkeypatch: pytest.MonkeyPatch, + isolated_state: Path, + stub_adapter: MagicMock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Walk the 6 hooks; assert User, Assistant_tool, Assistant records land.""" + from openclaw_smart import state + + session = "e2e-1" + project = "test-project" + + # session-start: pushes defaults + emits a stall banner (or empty stdout). + assert _drive(monkeypatch, "session-start", {"sessionKey": session, "agentId": project}) == 0 + capsys.readouterr() # drain + + # before-prompt-build: appends a "User" record. + assert ( + _drive( + monkeypatch, + "before-prompt-build", + { + "sessionKey": session, + "prompt": "implement OAuth flow", + "agentId": project, + }, + ) + == 0 + ) + capsys.readouterr() + + # before-tool-call: observe-only stub; no record, no stdout. + assert ( + _drive( + monkeypatch, + "before-tool-call", + { + "sessionKey": session, + "toolName": "Edit", + "params": {"file_path": "x.py"}, + "agentId": project, + }, + ) + == 0 + ) + assert capsys.readouterr().out.strip() == "" + + # after-tool-call: appends an "Assistant_tool" record (camelCase translated). + assert ( + _drive( + monkeypatch, + "after-tool-call", + { + "sessionKey": session, + "toolName": "Edit", + "params": {"file_path": "x.py", "new_string": "..."}, + "result": "wrote 10 lines", + "agentId": project, + }, + ) + == 0 + ) + capsys.readouterr() + + # agent-end: extracts the assistant text and appends an "Assistant" record. + assert ( + _drive( + monkeypatch, + "agent-end", + { + "sessionKey": session, + "agentId": project, + "messages": [ + {"role": "user", "content": "implement OAuth flow"}, + {"role": "assistant", "content": "Done."}, + ], + }, + ) + == 0 + ) + capsys.readouterr() + + # session-end: force-publish; should not crash. + assert _drive(monkeypatch, "session-end", {"sessionKey": session, "agentId": project}) == 0 + capsys.readouterr() + + records = state.read_all(session) + roles = [r.get("role") for r in records if "role" in r] + assert "User" in roles + assert "Assistant_tool" in roles + assert "Assistant" in roles + + # session_end publishes; we asserted publish was called via the stub. + assert stub_adapter.publish.called + + # The agent_end / session_end path writes a published_up_to watermark + # once a publish succeeds. The stub returns True, so we should see it. + assert any("published_up_to" in r for r in records), ( + "expected published_up_to watermark after session_end" + ) + + +def test_unknown_event_is_silent( + monkeypatch: pytest.MonkeyPatch, + isolated_state: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """An unrecognized hook name short-circuits with empty stdout, never crashes.""" + monkeypatch.setattr("sys.stdin", io.StringIO("{}")) + from openclaw_smart import hook + + rc = hook.main(["openclaw", "this-does-not-exist"]) + assert rc == 0 + assert capsys.readouterr().out.strip() == "" From b11eece9dfb7a405b8386fb1cd7a3aa16673814e Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:56:23 +0000 Subject: [PATCH 42/53] test(openclaw): integration test for publish-to-reflexio --- ...t_publish_to_local_reflexio_integration.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py new file mode 100644 index 00000000..161713b5 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py @@ -0,0 +1,112 @@ +"""Integration: publish buffered turns to a local SQLite-backed reflexio. + +Requires a reflexio backend reachable at ``REFLEXIO_URL`` (default +``http://localhost:8081/``). Skipped automatically when the backend is +unreachable so the suite is portable across machines. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +pytestmark = pytest.mark.integration + + +def _reflexio_url() -> str: + return os.environ.get("REFLEXIO_URL", "http://localhost:8081/") + + +def _backend_alive(url: str) -> bool: + """True iff ``GET health`` returns 200 within a short window.""" + import urllib.error + import urllib.request + + health = url.rstrip("/") + "/health" + try: + with urllib.request.urlopen(health, timeout=1.5) as resp: # nosec — local only + return resp.status == 200 + except (urllib.error.URLError, TimeoutError, OSError): + return False + + +@pytest.fixture +def require_backend() -> None: + if not _backend_alive(_reflexio_url()): + pytest.skip(f"reflexio backend not reachable at {_reflexio_url()}") + + +@pytest.fixture +def isolated_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + """Force state.py to write under tmp_path so the test doesn't leak fixtures.""" + state_dir = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(state_dir)) + return state_dir + + +def test_publish_lands_in_reflexio_storage( + require_backend: None, + monkeypatch: pytest.MonkeyPatch, + isolated_state: Path, + worker_id: str = "test", +) -> None: + """Append two turns to the JSONL buffer → publish → assert success status + count.""" + from openclaw_smart import state + from openclaw_smart.publish import publish_unpublished + + session_id = f"oc-int-{worker_id}" + project_id = f"oc-test-proj-{worker_id}" + + state.append(session_id, {"role": "User", "content": "implement auth"}) + state.append( + session_id, + {"role": "Assistant", "content": "Wrote login/logout endpoints."}, + ) + + status, count = publish_unpublished( + session_id=session_id, + project_id=project_id, + force_extraction=False, + skip_aggregation=False, + ) + + assert status == "ok", f"publish returned {status!r} — check backend logs" + assert count == 2 + + # The watermark is now stamped — a second publish with no new turns is a no-op. + status2, count2 = publish_unpublished( + session_id=session_id, + project_id=project_id, + force_extraction=False, + skip_aggregation=False, + ) + assert (status2, count2) == ("nothing", 0) + + +def test_publish_failed_when_backend_url_invalid( + monkeypatch: pytest.MonkeyPatch, isolated_state: Path +) -> None: + """Pointing the adapter at a dead port surfaces 'failed' without stamping.""" + from openclaw_smart import state + from openclaw_smart.publish import publish_unpublished + from openclaw_smart.reflexio_adapter import Adapter + + # Pick a port that's almost certainly closed. + bogus = Adapter(url="http://127.0.0.1:9/") + session_id = "oc-int-bogus" + state.append(session_id, {"role": "User", "content": "hi"}) + + status, count = publish_unpublished( + session_id=session_id, + project_id="proj-bogus", + force_extraction=False, + skip_aggregation=False, + adapter=bogus, + ) + assert status == "failed" + assert count == 1 + # No watermark stamped — a retry would re-publish the same record. + records = state.read_all(session_id) + assert not any("published_up_to" in r for r in records) From 42fe2572f840753cad80a202842bdbad6a81bfd8 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:57:49 +0000 Subject: [PATCH 43/53] test(openclaw): integration test for search-and-inject --- .../test_search_inject_integration.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py new file mode 100644 index 00000000..21936d1e --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py @@ -0,0 +1,144 @@ +"""Integration: search reflexio, render the hits, assert the prependContext envelope. + +Two flavours of coverage: + +* **live-backend path** — when reflexio is reachable at ``REFLEXIO_URL``, run + ``context_inject.emit_context`` end-to-end and assert the expected envelope + shape. Skipped automatically when the backend isn't running. +* **stub path** — replace ``Adapter.search_all`` with a fixed return value so + the rendering + envelope-emit logic is exercised regardless of environment. +""" + +from __future__ import annotations + +import json +import os +import sys +from io import StringIO +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +pytestmark = pytest.mark.integration + + +def _reflexio_url() -> str: + return os.environ.get("REFLEXIO_URL", "http://localhost:8081/") + + +def _backend_alive(url: str) -> bool: + import urllib.error + import urllib.request + + health = url.rstrip("/") + "/health" + try: + with urllib.request.urlopen(health, timeout=1.5) as resp: # nosec — local only + return resp.status == 200 + except (urllib.error.URLError, TimeoutError, OSError): + return False + + +@pytest.fixture +def isolated_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + state_dir = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(state_dir)) + return state_dir + + +def _parse_emitted(captured: StringIO) -> dict | None: + raw = captured.getvalue().strip() + if not raw: + return None + return json.loads(raw) + + +def test_emit_context_returns_false_when_search_empty( + monkeypatch: pytest.MonkeyPatch, isolated_state: Path +) -> None: + """No hits → no envelope, no JSONL side-effect.""" + from openclaw_smart import context_inject + + captured = StringIO() + monkeypatch.setattr(sys, "stdout", captured) + + fake_adapter = MagicMock() + fake_adapter.search_all.return_value = ([], [], []) + + emitted = context_inject.emit_context( + session_id="sess-empty", + project_id="proj-1", + query="anything", + top_k=3, + adapter=fake_adapter, + ) + assert emitted is False + assert _parse_emitted(captured) is None + assert not (isolated_state / "sess-empty.injected.jsonl").exists() + + +def test_emit_context_wraps_hits_in_prependContext_envelope( + monkeypatch: pytest.MonkeyPatch, isolated_state: Path +) -> None: + """A stubbed user playbook is rendered as markdown under ``prependContext``.""" + from openclaw_smart import context_inject + + captured = StringIO() + monkeypatch.setattr(sys, "stdout", captured) + + user_playbook = { + "content": "Always validate user input before persistence.", + "trigger": "any external string lands in the persistence layer", + "rationale": "blocks a whole class of injection bugs", + "user_playbook_id": "pb-1", + } + fake_adapter = MagicMock() + fake_adapter.search_all.return_value = ([user_playbook], [], []) + + emitted = context_inject.emit_context( + session_id="sess-hit", + project_id="proj-1", + query="how do I validate user input", + top_k=3, + adapter=fake_adapter, + ) + + assert emitted is True + payload = _parse_emitted(captured) + assert payload is not None + md = payload.get("prependContext") + assert md, f"expected prependContext markdown, got {payload!r}" + # Title text shows up inside the rendered markdown. + assert "validate user input" in md.lower() + # Citation registry was persisted for the agent_end hook to consume. + assert (isolated_state / "sess-hit.injected.jsonl").exists() + + +def test_emit_context_against_live_backend( + monkeypatch: pytest.MonkeyPatch, isolated_state: Path +) -> None: + """When a real backend is running, the full pipeline executes without error. + + No fixture data is seeded — the backend may legitimately return zero hits; + the assertion is that the call completes, not that there's data to inject. + """ + if not _backend_alive(_reflexio_url()): + pytest.skip(f"reflexio backend not reachable at {_reflexio_url()}") + + from openclaw_smart import context_inject + + captured = StringIO() + monkeypatch.setattr(sys, "stdout", captured) + + # No assertion on emitted bool — backend state is environment-dependent. + context_inject.emit_context( + session_id="sess-live", + project_id="proj-live", + query="ping", + top_k=3, + ) + # If markdown was emitted it should at least be valid JSON with the right key. + raw = captured.getvalue().strip() + if raw: + payload = json.loads(raw) + assert "prependContext" in payload From d7f932ea7c759e98603a6c08dc0f57db8024eb09 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 03:58:53 +0000 Subject: [PATCH 44/53] docs(openclaw): rewrite README for openclaw-smart architecture --- reflexio/integrations/openclaw/README.md | 390 +++++++---------------- 1 file changed, 123 insertions(+), 267 deletions(-) diff --git a/reflexio/integrations/openclaw/README.md b/reflexio/integrations/openclaw/README.md index 8f39adaf..a4eb8ed6 100644 --- a/reflexio/integrations/openclaw/README.md +++ b/reflexio/integrations/openclaw/README.md @@ -1,274 +1,130 @@ -# Reflexio OpenClaw Federated Plugin +# openclaw-smart -Connect [OpenClaw](https://openclaw.ai) agents to [Reflexio](https://github.com/reflexio-ai/reflexio) for automatic cross-session memory with multi-user support. Conversations are captured automatically to a local SQLite buffer, published to the Reflexio server on session end, and relevant profiles and playbooks are injected before every response via a hook. +openclaw-smart is the openClaw plugin counterpart of +[claude-smart](https://github.com/reflexio-ai/claude-smart): a thin TS shim +plus a Python (`openclaw_smart`) package that wires openClaw's plugin hooks +into a local [Reflexio](https://github.com/reflexio-ai/reflexio) backend +running at `http://localhost:8081/`. Conversations are buffered to a JSONL +session log, published for extraction (skills + preferences) via your own +LLM, and the top-matching items are injected back into every subsequent +turn as `prependContext`. The plugin degrades silently when the backend is +unreachable. -## Table of Contents - -- [How It Works](#how-it-works) -- [Multi-User Architecture](#multi-user-architecture) -- [Prerequisites](#prerequisites) -- [Installation](#installation) -- [First-Use Setup](#first-use-setup) -- [Configuration](#configuration) -- [Using `reflexio_publish` Tool](#using-reflexio_publish-tool) -- [Cross-Session Retrieval](#cross-session-retrieval) -- [File Structure](#file-structure) -- [Comparison with openclaw-embedded](#comparison-with-openclaw-embedded) -- [Uninstall](#uninstall) -- [Manual Testing](#manual-testing) - -## How It Works - -The plugin has three independent mechanisms: - -### 1. Capture (Automatic via Hook) - -``` -Each Turn (message:sent) - └── Buffer (user message, agent response) → local SQLite (~/.reflexio/sessions.db) - -Session End (command:stop) - └── Flush all buffered turns to Reflexio server via reflexio_publish CLI - └── Server automatically: - 1. Analyzes conversation via LLM pipeline - 2. Extracts playbooks: behavior instructions with context (trigger, rationale, blocking_issue) - 3. Extracts user profiles: preferences, expertise, communication patterns - 4. Stores everything with vector embeddings for semantic search -``` - -Correction detection happens **server-side via LLM** — the plugin does not detect corrections itself. - -### 2. Retrieve (Automatic via Hook Injection) - -``` -Per-message (message:received hook — automatic) - └── Hook injects search results from reflexio search before every agent response - └── Returns user playbooks + agent playbooks relevant to the current message - └── Semantic search matches message content against playbook triggers -``` - -Both user playbooks (corrections specific to this agent instance) and agent playbooks (shared corrections aggregated from all instances) are returned and injected as context. - -### 3. Aggregate (Automatic, optional manual trigger) - -``` -After each publish - └── Aggregation runs in background automatically - └── Clusters similar user playbooks across all agent instances - └── Deduplicates and consolidates into shared agent playbooks - └── New agent playbooks start as PENDING → can be reviewed → APPROVED/REJECTED - -Manual trigger - └── reflexio agent-playbooks aggregate (CLI) -``` - -Agent playbooks accumulate corrections from all agent instances, so every instance benefits from corrections made by any other instance. - -## Multi-User Architecture - -Each OpenClaw agent (identified by its `agentId`) is treated as a distinct Reflexio user. This enables per-agent learning isolation alongside cross-agent shared learning: - -``` -~/.openclaw/ -├── agents/ -│ ├── main/ → Reflexio user_id: "main" -│ ├── work/ → Reflexio user_id: "work" -│ └── ops/ → Reflexio user_id: "ops" -``` - -- **User playbooks**: per-agent corrections, isolated by `agentId`. Mistakes made by `main` are tracked separately from mistakes made by `work`. -- **Agent playbooks**: shared corrections aggregated from ALL agents. Once a correction is aggregated and approved, every instance sees it via `reflexio search`. -- **`user_id`** is derived from the OpenClaw session key prefix (`agent::...`), with fallback to `openclaw`. There is no override — the hook is deliberately locked to automatic identity resolution to eliminate env-var reads. - - -## Prerequisites - -- [OpenClaw](https://openclaw.ai) installed and running -- [Node.js](https://nodejs.org/) 18+ (for the plugin runtime) -- The `reflexio` CLI on PATH: `pipx install reflexio-ai` (or `pip install --user reflexio-ai`) -- An LLM API key for the Reflexio server (e.g., `OPENAI_API_KEY`) — required for playbook/profile extraction but read only by the server, never by the hook - -Supported LLM providers: OpenAI, Anthropic, Google Gemini, DeepSeek, OpenRouter, MiniMax, DashScope, xAI, Moonshot, ZAI, or any local LLM endpoint (Ollama, LM Studio, vLLM). - -## Installation - -### Option 1 — ClawHub (recommended) - -```bash -clawhub plugin install reflexio-federated -``` - -On first use, the plugin auto-installs the `reflexio-ai` CLI (via `pipx` or `pip`) and runs first-use setup. - -### Option 2 — From Source (if you have `reflexio-ai` and Node.js installed) +## Quick install ```bash -cd /path/to/reflexio/integrations/openclaw -./scripts/install.sh -``` - -This installs the plugin from source, registers it with OpenClaw, and restarts the gateway. - -### Option 3 — Manual (for development) - -```bash -# Build the plugin -cd /path/to/reflexio/integrations/openclaw/plugin -npm install -npm run typecheck -npm run build # if applicable - -# Install via openclaw -cd /path/to/reflexio/integrations/openclaw -./scripts/install.sh -``` - -## First-Use Setup - -On the first OpenClaw session after installation, the plugin automatically: - -1. Detects if the `reflexio` CLI is installed; if not, installs it via `pipx` -2. Creates `~/.reflexio/` configuration directory if it doesn't exist -3. Prompts for LLM provider selection (OpenAI, Anthropic, Gemini, etc.) and API key -4. Configures the local storage backend (SQLite for local use, Supabase optional) -5. Starts the Reflexio server in the background at `http://127.0.0.1:8081` -6. Runs first search to verify setup is complete - -**No manual steps required** — everything is automatic and transparent to the user. +reflexio setup openclaw +``` + +This walks you through: + +1. Picking an LLM provider and storage backend (SQLite by default). +2. Writing `OPENCLAW_BIN` + `OPENCLAW_SMART_USE_LOCAL_CLI=1` to + `~/.reflexio/.env` so the backend's `openclaw` LiteLLM provider knows + which CLI to spawn for extraction. +3. Installing the bundled plugin into openClaw + (`openclaw plugins install `). +4. Running the plugin's first-run installer (`scripts/smart-install.sh`) + so the next session doesn't pay a cold-install cost. +5. Calling `openclaw plugins inspect reflexio-openclaw-smart` to verify + the plugin loaded. + +`reflexio setup openclaw --uninstall [--purge]` reverses everything; +`--repair` re-runs only the first-run installer. + +## How it works + +* **session_start** — pushes openclaw-smart's preferred extraction window / + stride and the shared-skill optimizer defaults to the reflexio backend. + Emits a stall banner (`prependContext`) if learning has been idle. +* **before_prompt_build** — appends the user prompt to the session JSONL + buffer, then runs an `/api/search` query against reflexio scoped to this + project. Top hits are rendered as markdown and emitted under + `prependContext` (per-turn, not cached). +* **before_tool_call / after_tool_call** — `before_*` is an observe-only + stub (openClaw does not honor injection at that point). `after_*` + redacts secrets, truncates oversized strings, normalizes camelCase + payloads to snake_case, and appends an `Assistant_tool` record. +* **agent_end** — reads `payload["messages"]`, appends the latest + assistant turn, then publishes everything since the last + `published_up_to` watermark (no force-extraction). +* **session_end** — drains the buffer one last time with + `force_extraction=True` so the session's learnings are available before + the next openClaw run. + +The full design lives in +[`docs/superpowers/specs/2026-05-19-openclaw-smart-design.md`](../../../../docs/superpowers/specs/2026-05-19-openclaw-smart-design.md). + +## Skills + +Five user-invocable skills ship under `plugin/skills/`: + +| Skill | Purpose | +| ----------- | -------------------------------------------------------------------------- | +| `reflexio` | Always-on contract: trust injected context, cite with `[oc:…]`, stay quiet | +| `learn` | Force-publish the current session for immediate extraction | +| `show` | Dump currently-known skills + preferences for this project as markdown | +| `dashboard` | Open the local reflexio web UI (`http://localhost:3001`, shared with claude-smart) | +| `restart` | Restart the local reflexio backend cleanly | +| `clear-all` | Delete all locally-stored skills + preferences (destructive, prompts) | ## Configuration -This plugin stores all configuration in `plugin/openclaw.plugin.json` and reads the following tunable settings: - -### `publish` — Conversation capture and publish behavior - -- `batch_size` (default: 10): Number of turns to buffer before mid-session publish. At session end, all remaining turns are flushed regardless. -- `max_retries` (default: 3): Number of times to retry failed publish attempts. -- `max_content_length` (default: 10000): Maximum character length per turn to avoid oversized payloads. - -### `search` — Context injection before each response - -- `timeout_ms` (default: 5000): Maximum time to wait for search results. If the server is slow, search times out and the agent proceeds without injected context (graceful degradation). -- `top_k` (default: 5): Number of playbooks to return per search. -- `min_prompt_length` (default: 5): Skip search for trivial inputs (< 5 chars, or yes/no/ok/sure/thanks). - -### `server` — Reflexio server health and startup - -- `health_check_timeout_ms` (default: 3000): Time limit for server health checks. Used to determine if a restart is needed. -- `stale_flag_ms` (default: 120000): Flag file age threshold. If a startup flag is older than 2 minutes, assume the previous startup attempt failed and try again. - -All settings are optional and have sensible defaults. To override, edit `plugin/openclaw.plugin.json` and restart the agent. - -## Using `reflexio_publish` Tool - -Conversations are automatically published at session end. For high-signal moments, use the `reflexio_publish` tool to flush immediately: - -- **User corrects you** and confirms the fix (explicit "good" / "perfect" or moves on) -- **You complete a key milestone** with non-obvious learnings -- **High-friction session** with multiple corrections - -The Reflexio server handles extraction (profiles, playbooks) from the published conversations. You don't need to structure the data — just publish, and the server does the rest. - -## Cross-Session Retrieval - -**Session 1 (cold start):** No playbooks exist yet. The agent works normally. At session end, the hook captures and publishes the full conversation. The Reflexio server's LLM pipeline analyzes it and extracts corrections or user preferences. - -**Session 2+:** Before each response, the hook runs `reflexio search` and injects matching playbooks and profiles as context. Over time: - -- Mistakes made once are not repeated (corrections match by trigger similarity) -- User preferences are remembered (profiles extracted automatically) -- The agent adapts its approach per-task based on accumulated playbooks -- Corrections from one agent instance propagate to all instances via aggregation - -## File Structure - -``` -openclaw/ -├── README.md ← This file -├── TESTING.md ← Manual testing guide -├── plugin/ ← Compiled plugin (OpenClaw extension) -│ ├── openclaw.plugin.json ← Plugin manifest and config schema -│ ├── package.json ← NPM metadata -│ ├── lib/ ← Core libraries -│ │ ├── user-id.ts ← Multi-user identity resolution -│ │ ├── server.ts ← Server URL, health check, auto-start -│ │ ├── sqlite-buffer.ts ← Turn buffering -│ │ ├── publish.ts ← Publish and CLI spawn -│ │ └── search.ts ← Search and result formatting -│ ├── hook/ ← Hook event handlers -│ │ ├── handler.ts ← Core hook logic (bootstrap, message events, command:stop) -│ │ └── setup.ts ← First-use setup (CLI install, storage config) -│ ├── skills/ ← Agent instruction and tooling -│ │ └── reflexio/ -│ │ └── SKILL.md ← Teaches agent when/how to search and publish -│ ├── rules/ ← Always-active behavioral constraints -│ │ └── reflexio.md ← Follow injected context, manual fallback, transparency -│ └── index.ts ← Plugin entry point (SDK registration) -├── hook/ ← Flat hook for manual installation -│ ├── handler.ts ← Same as plugin/hook/handler.ts -│ └── HOOK.md ← Hook metadata (see references/HOOK.md) -├── scripts/ ← Installation and uninstall -│ ├── install.sh ← Plugin installation -│ └── uninstall.sh ← Plugin removal (preserves user data) -└── _old/ ← Legacy documentation and code (to be removed) -``` - -## Comparison with openclaw-embedded - -| Aspect | openclaw (federated) | openclaw-embedded | -| -------------------- | ---------------------------------------- | --------------------------------------- | -| Architecture | Federated (server-based) | Standalone (no server) | -| Storage | Reflexio server + SQLite buffer | Local SQLite only | -| Multi-user support | Yes — per-agentId user isolation | Single user per instance | -| Cross-instance sync | Yes — via agent playbooks aggregation | No — isolated per instance | -| Prerequisites | Reflexio CLI, LLM API key, Node.js | SQLite (no server, no API key needed) | -| Best for | Teams, multiple agents, persistent core | Single agent, offline-first, lightweight | -| Deployment | Reflexio server + OpenClaw agents | Standalone OpenClaw agent | - -**Use the federated plugin if:** -- Multiple agents run on the same machine or team (shared learnings via aggregation) -- Corrections from one agent should propagate to others -- A persistent Reflexio server is acceptable - -**Use openclaw-embedded if:** -- Single agent, offline-first operation required -- No external dependencies or API keys -- Lightweight, self-contained setup - -## Uninstall - -### Remove the plugin - -```bash -./scripts/uninstall.sh -``` - -This removes the plugin from OpenClaw, cleans up state files, and preserves user data in `~/.reflexio/`. - -### Remove user data (optional) - -```bash -./scripts/uninstall.sh --purge -``` - -This also deletes `~/.reflexio/` entirely. - -### Verify removal - -```bash -openclaw plugins list -# Should not show reflexio-federated -``` - -## Manual Testing - -See [TESTING.md](TESTING.md) for a complete step-by-step manual testing guide covering: -- Install verification -- First-session auto-setup -- Search injection -- Conversation capture and publish -- `reflexio_publish` tool usage -- Cross-session retry -- Multi-user isolation -- Graceful degradation -- Uninstall +Plugin-side config lives in `plugin/openclaw.plugin.json` (no env vars +required for normal use). The shell scripts honour these env knobs: + +* `OPENCLAW_SMART_INTERNAL=1` — recursion guard. Set automatically by + `openclaw_provider._call_cli` when the reflexio backend's own + LiteLLM-routed extraction subprocess fires hooks back into openclaw-smart. + Any handler that sees this env in the payload short-circuits. +* `OPENCLAW_SMART_USE_LOCAL_CLI=1` — opt-in for the + `openclaw_provider` LiteLLM plugin on the reflexio side. Set by + `reflexio setup openclaw`. +* `OPENCLAW_BIN` — absolute path to the openclaw CLI used by the local + LLM provider. Set by `reflexio setup openclaw`. +* `OPENCLAW_DEFAULT_MODEL` — default model passed to + `openclaw infer model run` when no model is requested explicitly. +* `OPENCLAW_SMART_STATE_DIR` — overrides the JSONL session buffer + location (default `~/.openclaw-smart/sessions/`). Used by tests. +* `OPENCLAW_SMART_BACKEND_AUTOSTART=0` — opt out of the + `session_start` backend auto-start. +* `OPENCLAW_PLUGIN_ROOT` — pointer used by slash commands; + `scripts/ensure-plugin-root.sh` symlinks `~/.reflexio/openclaw-plugin-root` + to the active install. + +## Recursion guard + +The reflexio backend uses `openclaw_provider` (a LiteLLM CustomLLM) to +invoke the openclaw CLI for extraction. That CLI can in turn fire openClaw +hooks back into openclaw-smart, which would re-publish the extractor's +own prompt into reflexio — a tight feedback loop that explodes the +buffer. The guard at `openclaw_smart.internal_call.is_internal_invocation` +detects this by: + +1. checking `OPENCLAW_SMART_INTERNAL=1` (set by the provider on spawn), +2. checking whether `workspaceDir` lives inside the reflexio install dir. + +When either is true the hook dispatcher exits with empty stdout +immediately — no buffer writes, no search, no publish. + +## Multi-session limitation + +The TS shim tracks one `activeSessionKey` per plugin instance to route +the `reflexio_publish` tool to the right session. In concurrent +multi-session use the last session-key seen wins. This matches +claude-smart's behaviour and is intentional — see spec §10. + +## Troubleshooting + +* **Hook timing out / no inject** — check + `~/.openclaw-smart/backend.log` for reflexio startup errors. The + session-start backend autostart is best-effort; you can also start it + manually via `bash plugin/scripts/backend-service.sh start`. +* **`uv` not found** — re-run `bash plugin/scripts/smart-install.sh`; on + failure it writes the reason to `~/.openclaw-smart/install-failed`. +* **Stale plugin after a code change** — `reflexio setup openclaw --repair` + re-runs the first-run installer and clears the failure marker. +* **Recursion loop / sessions multiplying** — the extractor lost the + `OPENCLAW_SMART_INTERNAL=1` env. Confirm `openclaw_provider._call_cli` + is still passing it (see + `reflexio/server/llm/providers/openclaw_provider.py`). From f5bb3c74b8139d62d2122f58ab8c751db5966f57 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 04:03:43 +0000 Subject: [PATCH 45/53] style(server/llm): clean up ruff findings in openclaw_provider --- reflexio/server/llm/providers/openclaw_provider.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/reflexio/server/llm/providers/openclaw_provider.py b/reflexio/server/llm/providers/openclaw_provider.py index fbe05f9c..7b9049a5 100644 --- a/reflexio/server/llm/providers/openclaw_provider.py +++ b/reflexio/server/llm/providers/openclaw_provider.py @@ -18,6 +18,7 @@ import os import shutil import subprocess # noqa: S404 — subprocess is the integration point. +from pathlib import Path from typing import Any import litellm @@ -37,7 +38,7 @@ # Module-level state reset by tests via the _reset_module_state fixture. _REGISTERED: bool = False -_HANDLER: "OpenClawLLM | None" = None +_HANDLER: OpenClawLLM | None = None class OpenClawCLIError(RuntimeError): @@ -63,8 +64,10 @@ def _resolve_cli_path() -> str | None: str | None: Absolute path to the openclaw binary, or None if not found. """ override = os.environ.get(ENV_CLI_PATH) - if override and os.path.isfile(override) and os.access(override, os.X_OK): - return override + if override: + path = Path(override) + if path.is_file() and os.access(override, os.X_OK): + return override return shutil.which("openclaw") @@ -166,7 +169,7 @@ def _call_cli(*, prompt: str, model: str | None, timeout_s: int) -> str: class OpenClawLLM(CustomLLM): """LiteLLM CustomLLM that routes completions through the openclaw CLI.""" - def completion(self, *args: Any, **kwargs: Any) -> ModelResponse: + def completion(self, *args: Any, **kwargs: Any) -> ModelResponse: # noqa: ARG002 — LiteLLM CustomLLM signature """Synchronous completion via ``openclaw infer model run``. The ``openclaw/`` prefix is stripped from the requested model id. From 0e823e7e5f0210eedf15948279024f1a6045f90b Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 04:15:41 +0000 Subject: [PATCH 46/53] feat(openclaw): add tsc build step for openClaw plugin loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenClaw 2026.5.12+ requires compiled JS at the path declared in package.json's openclaw.extensions. Add a 'build' script that emits ./dist/index.js via tsc, point the manifest at it, and have smart-install.sh run npm run build on first install. Switch index.ts to import.meta.url-based path resolution since the compiled output is ESM and __dirname is unavailable. Verified locally: openclaw plugins install --link + openclaw plugins inspect reflexio-openclaw-smart → Status: loaded. --- reflexio/integrations/openclaw/plugin/index.ts | 6 +++++- .../integrations/openclaw/plugin/package.json | 14 +++++++++++++- .../openclaw/plugin/scripts/smart-install.sh | 15 +++++++++++++++ .../openclaw/plugin/tsconfig.build.json | 18 ++++++++++++++++++ 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 reflexio/integrations/openclaw/plugin/tsconfig.build.json diff --git a/reflexio/integrations/openclaw/plugin/index.ts b/reflexio/integrations/openclaw/plugin/index.ts index 42c717b8..0e0ef5d3 100644 --- a/reflexio/integrations/openclaw/plugin/index.ts +++ b/reflexio/integrations/openclaw/plugin/index.ts @@ -4,10 +4,14 @@ // All logic lives in src/openclaw_smart/. This file only does SDK wiring. import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import * as path from "node:path"; +import { fileURLToPath } from "node:url"; type AnyHandler = (event: unknown, ctx: unknown) => unknown; -const PLUGIN_ROOT = __dirname; +// Compiled output lives at /dist/index.js; scripts/ + skills/ stay +// at the plugin root, so we resolve one level up from this module's dir. +const _MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); +const PLUGIN_ROOT = path.resolve(_MODULE_DIR, ".."); const HOOK_ENTRY = path.join(PLUGIN_ROOT, "scripts", "hook_entry.sh"); const CLI_SCRIPT = path.join(PLUGIN_ROOT, "scripts", "cli.sh"); diff --git a/reflexio/integrations/openclaw/plugin/package.json b/reflexio/integrations/openclaw/plugin/package.json index a4c84164..a65f942e 100644 --- a/reflexio/integrations/openclaw/plugin/package.json +++ b/reflexio/integrations/openclaw/plugin/package.json @@ -5,17 +5,29 @@ "private": true, "type": "module", "scripts": { + "build": "tsc -p tsconfig.build.json", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit" }, "openclaw": { - "extensions": ["./index.ts"], + "extensions": ["./dist/index.js"], "compat": { "pluginApi": ">=2026.3.24", "minGatewayVersion": "2026.3.24" } }, + "files": [ + "dist/**/*.js", + "scripts/**/*", + "skills/**/*", + "src/**/*.py", + "types/**/*.d.ts", + "openclaw.plugin.json", + "pyproject.toml", + "uv.lock", + "README.md" + ], "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0", diff --git a/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh b/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh index cf06c67f..dad5e471 100755 --- a/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +++ b/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh @@ -221,6 +221,21 @@ if ! uv sync --locked --python 3.12 --quiet >&2; then write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose" fi +# Compile the TS shim to ./dist/index.js. openClaw 2026.5.12+ requires +# compiled JS at the path declared in package.json's openclaw.extensions. +# Source checkouts ship dist/ via the publisher; in dev mode (or when a +# user installs from a fresh git clone) we (re)build here so the loader +# can find ./dist/index.js. Skip silently when npm or the script is +# missing; the dashboard/install banner will surface the resulting load +# failure to the user. +if [ ! -f "$PLUGIN_ROOT/dist/index.js" ] && command -v npm >/dev/null 2>&1; then + if [ -f "$PLUGIN_ROOT/package.json" ]; then + echo "[openclaw-smart] compiling TS shim to dist/..." >&2 + (cd "$PLUGIN_ROOT" && npm install --silent && npm run build --silent) >&2 || \ + echo "[openclaw-smart] WARNING: npm install / build failed; openClaw may refuse to load the plugin" >&2 + fi +fi + # Reflexio's CLI reads ~/.reflexio/.env (see reflexio/cli/env_loader.py); # append our two opt-in flags there so `reflexio services start` picks # them up regardless of which directory the user runs it from. Keep diff --git a/reflexio/integrations/openclaw/plugin/tsconfig.build.json b/reflexio/integrations/openclaw/plugin/tsconfig.build.json new file mode 100644 index 00000000..c59d9251 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tsconfig.build.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": false, + "declaration": false, + "sourceMap": false, + "resolveJsonModule": true, + "outDir": "./dist", + "types": ["node"] + }, + "include": ["index.ts", "types/**/*.d.ts"], + "exclude": ["node_modules", "src", "tests", "tests-ts", "scripts", "dist"] +} From 5cce131ad40213eeaba4f0b7f32ebcb3120e9b12 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 04:40:23 +0000 Subject: [PATCH 47/53] fix(openclaw): address PR #80 code review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses 12 major findings from CodeRabbit on the openclaw-smart PR: * state.py — sanitize session_id before constructing filesystem paths to prevent path traversal via crafted session keys (e.g. '../escape'). Rejected ids cause append/read calls to silently no-op. * openclaw_provider.py — guard against kwargs.get('model') being None (was being coerced to literal 'None') and against an invalid OPENCLAW_CLI_TIMEOUT env var crashing the completion path. * setup_cmd.py — resolve openclaw CLI to its absolute path once and use it for every subprocess call, avoiding mid-setup TOCTOU drift. * ids.py — resolve git binary via shutil.which() and catch generic OSError so resolve_project_id() truly never raises. * internal_call.py — guard the parents[5] indexing so an unfamiliar install layout (PyPI wheel, alternate cache path) doesn't crash on module import. * after_tool_call.py — wrap non-dict tool_input in a {'_raw': ...} dict so malformed payloads can't trip _redact()'s dict assumption. * query_compose.py — coerce non-string tool payload fields through _as_str() so an unexpected payload type can't crash hook execution. * cli.py (plugin) — catch OSError from subprocess.run() so missing exec-bit / permission-denied surfaces as a controlled status code. * hook_entry.sh — re-check install-failed marker after inline bootstrap; smart-install.sh can fail post-uv-install (e.g. uv sync). * cli.sh — don't let smart-install.sh's non-zero exit short-circuit set -e past the structured failure-marker checks. * smart-install.sh — flock failure falls back to lockfile path instead of bailing out and silently skipping the install. * README.md — fix 'Five user-invocable skills' header to match the actual six entries in the skills table. Updated test_install_openclaw_uses_new_plugin_id and added test_path_traversal_session_id_rejected / test_safe_session_ids_accepted regression coverage. Plugin tests: 138 passed, 2 skipped. Skipped per scope: * publish.py per-session lock (heavy lift; spec §10 already documents the multi-session limitation) * cli.py clear-all backend-restart refactor (invasive; safe failure path is already a controlled 'failed' status) --- reflexio/cli/commands/setup_cmd.py | 23 ++++--- reflexio/integrations/openclaw/README.md | 2 +- .../openclaw/plugin/scripts/cli.sh | 6 +- .../openclaw/plugin/scripts/hook_entry.sh | 10 +++ .../openclaw/plugin/scripts/smart-install.sh | 11 ++-- .../openclaw/plugin/src/openclaw_smart/cli.py | 13 +++- .../openclaw_smart/events/after_tool_call.py | 10 ++- .../openclaw/plugin/src/openclaw_smart/ids.py | 20 ++++-- .../src/openclaw_smart/internal_call.py | 23 ++++--- .../src/openclaw_smart/query_compose.py | 19 +++++- .../plugin/src/openclaw_smart/state.py | 62 ++++++++++++++++--- .../openclaw/plugin/tests/test_state.py | 39 +++++++++++- .../server/llm/providers/openclaw_provider.py | 22 ++++++- tests/cli/test_setup_cmd.py | 10 ++- 14 files changed, 224 insertions(+), 46 deletions(-) diff --git a/reflexio/cli/commands/setup_cmd.py b/reflexio/cli/commands/setup_cmd.py index d780302e..206c3b04 100644 --- a/reflexio/cli/commands/setup_cmd.py +++ b/reflexio/cli/commands/setup_cmd.py @@ -642,9 +642,15 @@ def _install_openclaw_integration(env_path: Path) -> bool: _write_openclaw_env(env_path, openclaw_bin) + # Use the resolved absolute path for every subprocess invocation so a + # mid-setup PATH change (a sibling install, a shell rc reload) can't + # silently flip us to a different ``openclaw`` binary partway through. + # Mirrors the same lookup discipline the runtime hooks use via OPENCLAW_BIN. + cli = openclaw_bin + # Clean install: remove any existing installation and stale extension dir subprocess.run( - ["openclaw", "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID], + [cli, "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID], check=False, capture_output=True, text=True, @@ -654,13 +660,13 @@ def _install_openclaw_integration(env_path: Path) -> bool: try: subprocess.run( - ["openclaw", "plugins", "install", str(plugin_dir)], + [cli, "plugins", "install", str(plugin_dir)], check=True, capture_output=True, text=True, ) subprocess.run( - ["openclaw", "plugins", "enable", _OPENCLAW_PLUGIN_ID], + [cli, "plugins", "enable", _OPENCLAW_PLUGIN_ID], check=True, capture_output=True, text=True, @@ -674,7 +680,7 @@ def _install_openclaw_integration(env_path: Path) -> bool: # Gateway restart is best-effort — older openclaw builds may not expose it subprocess.run( - ["openclaw", "gateway", "restart"], + [cli, "gateway", "restart"], check=False, capture_output=True, text=True, @@ -683,7 +689,7 @@ def _install_openclaw_integration(env_path: Path) -> bool: # Verify — match exact "Status: loaded" to avoid false positives from # "not loaded" or "unloaded" result = subprocess.run( - ["openclaw", "plugins", "inspect", _OPENCLAW_PLUGIN_ID], + [cli, "plugins", "inspect", _OPENCLAW_PLUGIN_ID], capture_output=True, text=True, ) @@ -710,15 +716,16 @@ def _uninstall_openclaw(env_path: Path | None = None, purge: bool = False) -> No "This will remove the Reflexio integration from openClaw. Continue?", abort=True, ) - if shutil.which("openclaw"): + cli = shutil.which("openclaw") + if cli: subprocess.run( - ["openclaw", "plugins", "disable", _OPENCLAW_PLUGIN_ID], + [cli, "plugins", "disable", _OPENCLAW_PLUGIN_ID], check=False, capture_output=True, text=True, ) subprocess.run( - ["openclaw", "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID], + [cli, "plugins", "uninstall", "--force", _OPENCLAW_PLUGIN_ID], check=False, capture_output=True, text=True, diff --git a/reflexio/integrations/openclaw/README.md b/reflexio/integrations/openclaw/README.md index a4eb8ed6..b02414f2 100644 --- a/reflexio/integrations/openclaw/README.md +++ b/reflexio/integrations/openclaw/README.md @@ -57,7 +57,7 @@ The full design lives in ## Skills -Five user-invocable skills ship under `plugin/skills/`: +Six skill folders ship under `plugin/skills/` (`reflexio` is the always-on contract, the other five are user-invocable): | Skill | Purpose | | ----------- | -------------------------------------------------------------------------- | diff --git a/reflexio/integrations/openclaw/plugin/scripts/cli.sh b/reflexio/integrations/openclaw/plugin/scripts/cli.sh index ea27d7af..a77983ca 100755 --- a/reflexio/integrations/openclaw/plugin/scripts/cli.sh +++ b/reflexio/integrations/openclaw/plugin/scripts/cli.sh @@ -44,7 +44,11 @@ if ! command -v uv >/dev/null 2>&1; then fi if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then echo "openclaw-smart: 'uv' not found — bootstrapping dependencies (~1-3 min on first install)..." >&2 - OPENCLAW_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2 + # ``set -e`` would short-circuit on a non-zero installer exit and skip + # the structured failure-marker checks below. Capture the failure + # explicitly so the wrapper continues to the unified error reporting. + OPENCLAW_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2 || \ + echo "openclaw-smart: smart-install.sh exited non-zero; continuing to error report" >&2 openclaw_smart_prepend_astral_bins openclaw_smart_prepend_node_bins fi diff --git a/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh b/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh index d266f251..eb2a0355 100755 --- a/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +++ b/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh @@ -92,5 +92,15 @@ if ! command -v uv >/dev/null 2>&1; then fi fi +# Re-check the failure marker after the inline bootstrap. ``smart-install.sh`` +# can write ``install-failed`` *after* uv is on PATH (e.g. ``uv sync`` failed), +# in which case we still have a working ``uv`` but a non-functional plugin. +# Without this gate we would proceed to ``uv run`` and crash with a confusing +# downstream error instead of surfacing the recorded failure reason. +if [ -f "$FAILURE_MARKER" ]; then + echo '' + exit 0 +fi + # Stdin is the hook payload JSON — stream it through to the Python CLI. exec uv run --project "$PLUGIN_ROOT" --quiet python -m openclaw_smart.hook "$HOST" "$EVENT" diff --git a/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh b/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh index dad5e471..67b724b1 100755 --- a/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +++ b/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh @@ -46,12 +46,13 @@ acquire_install_lock() { if command -v flock >/dev/null 2>&1; then exec 9>"$INSTALL_LOCK" - if ! flock 9; then - echo "[openclaw-smart] install lock failed; continuing without serialization" >&2 - echo '' - exit 0 + if flock 9; then + return 0 fi - return 0 + # flock unexpectedly failed (rare; usually a malformed lockfile fd + # rather than a busy lock). Fall through to the portable lockfile + # path below instead of bailing out — the installer must still run. + echo "[openclaw-smart] flock failed; falling back to lockfile serialization" >&2 fi while ! ( set -C; printf '%s\n' "$$" > "$INSTALL_LOCK" ) 2>/dev/null; do diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py index b606ff81..e19e144e 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py @@ -158,6 +158,10 @@ def _run_service(script: Path, subcmd: str) -> int: return 0 except subprocess.CalledProcessError as exc: return exc.returncode or 1 + except OSError as exc: + # Script lost executable bit, permission denied, missing interpreter, etc. + sys.stderr.write(f"error: failed to execute {script}: {exc}\n") + return 1 def _service_status(script: Path, wait_ready_s: float = 3.0) -> str: @@ -166,9 +170,12 @@ def _service_status(script: Path, wait_ready_s: float = 3.0) -> str: return "script missing" deadline = time.monotonic() + wait_ready_s while True: - result = subprocess.run( - [str(script), "status"], capture_output=True, text=True, check=False - ) + try: + result = subprocess.run( + [str(script), "status"], capture_output=True, text=True, check=False + ) + except OSError as exc: + return f"script error: {exc}" status = result.stdout.strip() or "unknown" if status != "not running" or time.monotonic() >= deadline: return status diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py index 2f7a8475..66e54fa2 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py @@ -141,11 +141,19 @@ def handle(payload: dict[str, Any]) -> None: tool_response = fields["tool_response"] output_text = _flatten_tool_response_text(tool_response) + # ``_redact`` expects a dict; non-dict payloads (raw string, list, or None) + # are wrapped under ``_raw`` so the redaction path stays uniform without + # crashing on the unusual shape. + raw_tool_input = fields["tool_input"] + if isinstance(raw_tool_input, dict): + safe_tool_input: dict[str, Any] = _redact(raw_tool_input) + else: + safe_tool_input = {"_raw": _redact_string(str(raw_tool_input))} record = { "ts": int(time.time()), "role": "Assistant_tool", "tool_name": tool_name, - "tool_input": _redact(fields["tool_input"]), + "tool_input": safe_tool_input, "tool_output": _redact_string(output_text) if output_text else "", "status": _derive_status(tool_response), } diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py index 3bfc7380..e286cb8e 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py @@ -17,6 +17,7 @@ import logging import os +import shutil import subprocess # noqa: S404 — git invocation with a fixed flag set. from pathlib import Path @@ -24,10 +25,20 @@ def _resolve_from_git(cwd: Path) -> str | None: - """Return the git toplevel basename for *cwd*, or ``None`` if not a repo.""" + """Return the git toplevel basename for *cwd*, or ``None`` if not a repo. + + Must never raise — callers depend on this returning ``None`` cleanly so + hook handlers can fall back to other identifiers. Resolves ``git`` to an + absolute path first to avoid PATH-dependent process spawning, and catches + any ``OSError`` (e.g., ``cwd`` is unreadable or has been deleted) along + with the previously-handled ``FileNotFoundError`` / ``TimeoutExpired``. + """ + git_bin = shutil.which("git") + if not git_bin: + return None try: - result = subprocess.run( # noqa: S603, S607 — fixed argv, cwd is a Path. - ["git", "rev-parse", "--show-toplevel"], + result = subprocess.run( # noqa: S603 — fixed argv, cwd is a Path. + [git_bin, "rev-parse", "--show-toplevel"], cwd=cwd, capture_output=True, text=True, @@ -38,7 +49,8 @@ def _resolve_from_git(cwd: Path) -> str | None: toplevel = result.stdout.strip() if toplevel: return Path(toplevel).name - except (FileNotFoundError, subprocess.TimeoutExpired) as exc: + except (OSError, subprocess.TimeoutExpired) as exc: + # OSError covers FileNotFoundError + PermissionError + missing-cwd. _LOGGER.debug("git toplevel resolution failed: %s", exc) return None diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py index a6e385f4..ac8af8bb 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py @@ -25,17 +25,26 @@ INTERNAL_ENV = "OPENCLAW_SMART_INTERNAL" -# Plugin layout: +# Plugin layout (in-repo / editable): # /reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py # parents[5] = , the directory we want to fence off. # -# In install mode the in-repo layout is absent — the env marker is the -# primary signal and this path never matches. ``OPENCLAW_SMART_REFLEXIO_DIR`` -# lets callers (and tests) override the path without touching the module. +# In install-mode layouts (PyPI wheel, ~/.openclaw/plugins/cache/...), this +# fixed depth doesn't hold. Guarding the index lookup keeps module import +# from crashing on unfamiliar layouts — the env marker is the primary +# signal there. ``OPENCLAW_SMART_REFLEXIO_DIR`` lets callers (and tests) +# override the path without touching the module. _THIS_DIR = Path(__file__).resolve().parent -_REFLEXIO_DIR = Path( - os.environ.get("OPENCLAW_SMART_REFLEXIO_DIR") or _THIS_DIR.parents[5] -) +_override_reflexio_dir = os.environ.get("OPENCLAW_SMART_REFLEXIO_DIR") +if _override_reflexio_dir: + _REFLEXIO_DIR: Path = Path(_override_reflexio_dir).resolve() +else: + _parents = _THIS_DIR.parents + # In an unexpected layout we fall back to _THIS_DIR itself, which makes + # the relative_to() check below match (correctly) only when cwd is + # literally inside this module's own directory — effectively a no-op + # fence, ceding all detection responsibility to the env marker. + _REFLEXIO_DIR = _parents[5] if len(_parents) > 5 else _THIS_DIR def is_internal_invocation(payload: dict[str, Any]) -> bool: diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py index 075d6975..8817fb52 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py @@ -39,14 +39,27 @@ def from_tool_call(tool_name: str, tool_input: Mapping[str, Any]) -> str: return "" +def _as_str(value: Any) -> str: + """Coerce a tool-payload field to a string, treating non-strings as empty. + + Tool inputs come from external openClaw payloads; a malformed event with + a non-string ``new_string`` or ``command`` would otherwise crash the + hook with ``AttributeError`` / ``TypeError``. We prefer a clean empty + query over a partial failure. + """ + return value if isinstance(value, str) else "" + + def _from_file_edit(tool_input: Mapping[str, Any]) -> str: - path = tool_input.get("file_path") or "" - snippet = tool_input.get("new_string") or tool_input.get("content") or "" + path = _as_str(tool_input.get("file_path")) + snippet = _as_str(tool_input.get("new_string")) or _as_str( + tool_input.get("content") + ) basename = Path(path).name if path else "" return f"{basename} {snippet[:_MAX_SNIPPET_LEN]}".strip() def _from_bash(tool_input: Mapping[str, Any]) -> str: - command = tool_input.get("command") or "" + command = _as_str(tool_input.get("command")) first_line = command.splitlines()[0] if command else "" return first_line[:_MAX_SNIPPET_LEN].strip() diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py index ac99254d..beee0540 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py @@ -19,6 +19,7 @@ import json import logging import os +import re from pathlib import Path from typing import Any, Iterable @@ -61,14 +62,53 @@ def state_dir() -> Path: return Path(override) if override else _DEFAULT_STATE_DIR -def session_path(session_id: str) -> Path: - """Return the JSONL path for a given session id.""" - return state_dir() / f"{session_id}.jsonl" +# openClaw session ids are opaque tokens (typically UUID-like). Restrict to a +# conservative alphanumeric + dot/underscore/hyphen set so a crafted value +# with path separators (e.g. ``../escape``) or shell metacharacters can't +# write or read outside ``state_dir()``. Any session id outside this charset +# is treated as malformed and the resulting filesystem op becomes a no-op +# upstream (see ``append`` / ``read_all``). +# +# We additionally reject ids that are pure-dot tokens (``.``, ``..``) or that +# contain a ``..`` substring even though the underlying regex would accept +# them — those are not valid filenames and signal an escape attempt. +_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$") -def injected_path(session_id: str) -> Path: - """Return the JSONL path for the per-session oc-cite registry.""" - return state_dir() / f"{session_id}.injected.jsonl" +def _safe_session_id(session_id: str) -> str | None: + """Return ``session_id`` iff it matches the safe-id charset, else ``None``. + + Args: + session_id (str): Caller-supplied session key. + + Returns: + str | None: The session id unchanged when safe, ``None`` otherwise. + """ + if not isinstance(session_id, str): + return None + if not _SESSION_ID_RE.fullmatch(session_id): + return None + if ".." in session_id: + return None + if set(session_id) == {"."}: + return None + return session_id + + +def session_path(session_id: str) -> Path | None: + """Return the JSONL path for a given session id, or ``None`` if unsafe.""" + sid = _safe_session_id(session_id) + if sid is None: + return None + return state_dir() / f"{sid}.jsonl" + + +def injected_path(session_id: str) -> Path | None: + """Return the per-session oc-cite registry path, or ``None`` if unsafe.""" + sid = _safe_session_id(session_id) + if sid is None: + return None + return state_dir() / f"{sid}.injected.jsonl" def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None: @@ -83,6 +123,9 @@ def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None: if not records: return path = injected_path(session_id) + if path is None: + _LOGGER.warning("rejected unsafe session_id for append_injected: %r", session_id) + return path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as fh: if fcntl is not None: @@ -102,7 +145,7 @@ def read_injected(session_id: str) -> dict[str, dict[str, Any]]: record only refreshes metadata). """ path = injected_path(session_id) - if not path.exists(): + if path is None or not path.exists(): return {} registry: dict[str, dict[str, Any]] = {} with path.open("r", encoding="utf-8") as fh: @@ -130,6 +173,9 @@ def append(session_id: str, record: dict[str, Any]) -> None: flush size. """ path = session_path(session_id) + if path is None: + _LOGGER.warning("rejected unsafe session_id for append: %r", session_id) + return path.parent.mkdir(parents=True, exist_ok=True) line = json.dumps(record, ensure_ascii=False) + "\n" with path.open("a", encoding="utf-8") as fh: @@ -144,7 +190,7 @@ def append(session_id: str, record: dict[str, Any]) -> None: def read_all(session_id: str) -> list[dict[str, Any]]: """Return every record in the buffer as a list of dicts. Missing file → [].""" path = session_path(session_id) - if not path.exists(): + if path is None or not path.exists(): return [] records: list[dict[str, Any]] = [] with path.open("r", encoding="utf-8") as fh: diff --git a/reflexio/integrations/openclaw/plugin/tests/test_state.py b/reflexio/integrations/openclaw/plugin/tests/test_state.py index f9bf3c87..f9f54800 100644 --- a/reflexio/integrations/openclaw/plugin/tests/test_state.py +++ b/reflexio/integrations/openclaw/plugin/tests/test_state.py @@ -3,10 +3,8 @@ from __future__ import annotations import json -from pathlib import Path import pytest - from openclaw_smart import state @@ -139,3 +137,40 @@ def test_read_injected_later_wins(): state.append_injected("s6", [{"id": "s1-abcd", "title": "new"}]) registry = state.read_injected("s6") assert registry["s1-abcd"]["title"] == "new" + + +def test_path_traversal_session_id_rejected(isolate_state_dir, tmp_path): + """A crafted session id with path separators must not escape state_dir().""" + escape_attempts = [ + "../escape", + "../../etc/passwd", + "a/b/c", + "sub/sess", + "..", + "", + "a" * 200, # over 128 char cap + ] + for sid in escape_attempts: + assert state.session_path(sid) is None, ( + f"unsafe session_path accepted: {sid!r}" + ) + assert state.injected_path(sid) is None + # Append + read must silently no-op rather than writing anywhere. + state.append(sid, {"role": "User", "content": "x"}) + state.append_injected(sid, [{"id": "s1-abcd", "title": "x"}]) + assert state.read_all(sid) == [] + assert state.read_injected(sid) == {} + + # The state dir itself should remain empty — nothing escaped. + if isolate_state_dir.exists(): + assert not any(isolate_state_dir.glob("**/*.jsonl")) + # And nowhere outside it either. + assert not (tmp_path / "escape.jsonl").exists() + + +def test_safe_session_ids_accepted(): + """The safe-id charset (alphanumeric + dot/underscore/hyphen) is allowed.""" + for sid in ("sess1", "a.b.c", "a_b", "a-b", "01234", "S.1_2-3"): + path = state.session_path(sid) + assert path is not None + assert path.name == f"{sid}.jsonl" diff --git a/reflexio/server/llm/providers/openclaw_provider.py b/reflexio/server/llm/providers/openclaw_provider.py index 7b9049a5..36f0a9fa 100644 --- a/reflexio/server/llm/providers/openclaw_provider.py +++ b/reflexio/server/llm/providers/openclaw_provider.py @@ -183,7 +183,10 @@ def completion(self, *args: Any, **kwargs: Any) -> ModelResponse: # noqa: ARG00 Returns: ModelResponse: Reply text in ``choices[0].message.content``. """ - model_kwarg = str(kwargs.get("model", "")) + # ``kwargs.get("model")`` can be ``None`` — coercing via ``str(...)`` + # would yield the literal "None" and skip the default-model fallback. + raw_model = kwargs.get("model") + model_kwarg = "" if raw_model is None else str(raw_model) if "/" in model_kwarg: model = model_kwarg.split("/", 1)[1] or None else: @@ -193,7 +196,22 @@ def completion(self, *args: Any, **kwargs: Any) -> ModelResponse: # noqa: ARG00 messages = kwargs.get("messages", []) prompt = _messages_to_prompt(messages) - timeout_s = int(os.environ.get(ENV_TIMEOUT, str(_DEFAULT_TIMEOUT_SECONDS))) + # An invalid OPENCLAW_CLI_TIMEOUT must not crash the completion path — + # fall back to the default with a warning so misconfiguration degrades + # to working-but-slow rather than working-then-erroring. + raw_timeout = os.environ.get(ENV_TIMEOUT) + try: + timeout_s = ( + int(raw_timeout) if raw_timeout is not None else _DEFAULT_TIMEOUT_SECONDS + ) + except ValueError: + _LOGGER.warning( + "Invalid %s=%r; using default %d", + ENV_TIMEOUT, + raw_timeout, + _DEFAULT_TIMEOUT_SECONDS, + ) + timeout_s = _DEFAULT_TIMEOUT_SECONDS text = _call_cli(prompt=prompt, model=model, timeout_s=timeout_s) diff --git a/tests/cli/test_setup_cmd.py b/tests/cli/test_setup_cmd.py index 06841515..464877ad 100644 --- a/tests/cli/test_setup_cmd.py +++ b/tests/cli/test_setup_cmd.py @@ -976,7 +976,9 @@ def __init__(self, stdout: str = "Status: loaded\n") -> None: def fake_run(argv, **_kw): # noqa: ANN001 calls.append(list(argv)) - if argv[:3] == ["openclaw", "plugins", "inspect"]: + # CLI is invoked by absolute path (TOCTOU fix), so check the + # subcommand position rather than the executable string. + if argv[1:3] == ["plugins", "inspect"]: return _Result("Status: loaded\n") return _Result("") @@ -988,6 +990,12 @@ def fake_run(argv, **_kw): # noqa: ANN001 flat_args = [arg for call in calls for arg in call] assert "reflexio-openclaw-smart" in flat_args assert "reflexio-federated" not in flat_args + # Every install-side call should target the absolute openclaw_bin + # path, not the bare "openclaw" string. + for call in calls: + assert call[0] == "/usr/bin/openclaw", ( + f"expected absolute CLI path, got {call[0]!r}" + ) body = env_path.read_text() assert 'OPENCLAW_BIN="/usr/bin/openclaw"' in body assert 'OPENCLAW_SMART_USE_LOCAL_CLI="1"' in body From c2ecd4336035dcd7b5d15c614856c651dae9d677 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 05:27:29 +0000 Subject: [PATCH 48/53] fix(openclaw): align default backend port with claude-smart (8071/8072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openclaw-smart's bundled reflexio backend was binding to 8081, the reflexio library default. That port is also what a developer running `./run_services.sh` against the main repo uses, so installing the plugin on a dev machine caused two failure modes: * port_occupied check skipped autostart, leaving hooks unable to publish * or the plugin silently attached to the dev backend, contaminating each other's data Switch to 8071/8072 — literally the same ports claude-smart uses — so the two plugins share one bundled backend (one SQLite, one extractor) and 8081 stays reserved for the developer's own instance. Affects the adapter default URL, backend-service.sh PORT/EMBEDDING_PORT, the integration test REFLEXIO_URL defaults, the README port reference, and the unit-test assertion. The integration suite that previously skipped (no backend reachable at 8081) now passes against the running shared backend on 8071. --- reflexio/integrations/openclaw/README.md | 12 +++++++----- .../openclaw/plugin/scripts/backend-service.sh | 11 ++++++++--- .../plugin/src/openclaw_smart/reflexio_adapter.py | 2 +- .../test_publish_to_local_reflexio_integration.py | 4 ++-- .../integration/test_search_inject_integration.py | 2 +- .../openclaw/plugin/tests/test_reflexio_adapter.py | 6 ++++-- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/reflexio/integrations/openclaw/README.md b/reflexio/integrations/openclaw/README.md index b02414f2..34f290cd 100644 --- a/reflexio/integrations/openclaw/README.md +++ b/reflexio/integrations/openclaw/README.md @@ -4,11 +4,13 @@ openclaw-smart is the openClaw plugin counterpart of [claude-smart](https://github.com/reflexio-ai/claude-smart): a thin TS shim plus a Python (`openclaw_smart`) package that wires openClaw's plugin hooks into a local [Reflexio](https://github.com/reflexio-ai/reflexio) backend -running at `http://localhost:8081/`. Conversations are buffered to a JSONL -session log, published for extraction (skills + preferences) via your own -LLM, and the top-matching items are injected back into every subsequent -turn as `prependContext`. The plugin degrades silently when the backend is -unreachable. +running at `http://localhost:8071/`. The port matches claude-smart so both +plugins share one bundled reflexio (one SQLite store, one extractor), and +leaves reflexio's own 8081 default free for developer use. Conversations +are buffered to a JSONL session log, published for extraction (skills + +preferences) via your own LLM, and the top-matching items are injected +back into every subsequent turn as `prependContext`. The plugin degrades +silently when the backend is unreachable. ## Quick install diff --git a/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh b/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh index e619e70c..58c98ef1 100755 --- a/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +++ b/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh @@ -1,8 +1,13 @@ #!/usr/bin/env bash -# Auto-start the reflexio FastAPI backend (port 8081) if it's not already +# Auto-start the reflexio FastAPI backend (port 8071) if it's not already # running. Detached spawn, returns immediately so the SessionStart hook # doesn't block the session. # +# Port choice: openclaw-smart shares 8071/8072 with the sibling claude-smart +# plugin so the two plugins coexist on a single shared backend (one SQLite +# store, one extractor process) — not reflexio's 8081 default, which is +# reserved for a developer's own local reflexio instance. +# # Subcommands: # start probe /health; if nothing we recognize is on the port, # spawn `uv run reflexio services start --only backend @@ -24,8 +29,8 @@ openclaw_smart_source_login_path openclaw_smart_prepend_astral_bins CMD="${1:-start}" -PORT=8081 -EMBEDDING_PORT="${EMBEDDING_PORT:-8082}" +PORT=8071 +EMBEDDING_PORT="${EMBEDDING_PORT:-8072}" # Pass through to `reflexio services start/stop` so the spawned backend # binds to PORT. export BACKEND_PORT="$PORT" diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py index 99c36203..cc473349 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) _ENV_URL = "REFLEXIO_URL" -_DEFAULT_URL = "http://localhost:8081/" +_DEFAULT_URL = "http://localhost:8071/" _SEARCH_MODE_HYBRID = "hybrid" # reflexio.models.config_schema.SearchMode.HYBRID _UNIFIED_ENTITY_TYPES = ("profiles", "user_playbooks", "agent_playbooks") _AGENT_PLAYBOOK_APPROVAL_STATUSES = ("pending", "approved") diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py index 161713b5..1929dc0e 100644 --- a/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py @@ -1,7 +1,7 @@ """Integration: publish buffered turns to a local SQLite-backed reflexio. Requires a reflexio backend reachable at ``REFLEXIO_URL`` (default -``http://localhost:8081/``). Skipped automatically when the backend is +``http://localhost:8071/``). Skipped automatically when the backend is unreachable so the suite is portable across machines. """ @@ -16,7 +16,7 @@ def _reflexio_url() -> str: - return os.environ.get("REFLEXIO_URL", "http://localhost:8081/") + return os.environ.get("REFLEXIO_URL", "http://localhost:8071/") def _backend_alive(url: str) -> bool: diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py index 21936d1e..83357ae0 100644 --- a/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py @@ -24,7 +24,7 @@ def _reflexio_url() -> str: - return os.environ.get("REFLEXIO_URL", "http://localhost:8081/") + return os.environ.get("REFLEXIO_URL", "http://localhost:8071/") def _backend_alive(url: str) -> bool: diff --git a/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py b/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py index f85a01ff..44ae6722 100644 --- a/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +++ b/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py @@ -9,9 +9,11 @@ from openclaw_smart.reflexio_adapter import Adapter -def test_default_url_is_8081(): +def test_default_url_is_8071(): + # 8071/8072 matches claude-smart so the two plugins share one local + # reflexio backend; 8081 is reserved for a developer's own instance. adapter = Adapter() - assert adapter.url == "http://localhost:8081/" + assert adapter.url == "http://localhost:8071/" def test_env_var_overrides_url(monkeypatch): From 13ac704e580f130922b2ebd179960ea3ea2c60df Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 06:00:11 +0000 Subject: [PATCH 49/53] fix(openclaw): drop reflexio_publish tool + use child_process.spawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin was failing to register on the openClaw gateway with: TypeError: Cannot read properties of undefined (reading 'runCommandWithTimeout') and 'openclaw plugins doctor' explained the root cause: plugin must declare contracts.tools before registering agent tools Two issues — both fixed here: 1. Calling api.registerTool() requires a contracts.tools manifest entry that we don't have. The registerTool call also caused the gateway to gate api.runtime.system, which is why the runner=runtime.system... line then crashed. Dropping the in-agent tool lets the plugin register cleanly as 'hook-only' (a supported compatibility path). The same publish action remains available via the 'learn' skill. 2. Even with the tool removed, runtime.system is only injected for trusted plugins. Switching the shell runner to Node's built-in child_process.spawn removes the dependency entirely — no trust gate to worry about. Result: 'openclaw plugins doctor' now reports only the benign 'is hook-only / supported compatibility path' info, gateway boots without errors, and hook events fire through to scripts/hook_entry.sh without touching runtime.system. Vitest mocks were rewritten to fake node:child_process.spawn; all 5 tests pass. --- .../integrations/openclaw/plugin/index.ts | 106 ++++++++++------- .../tests-ts/test_shim_dispatch.test.ts | 110 ++++++++++++++---- 2 files changed, 157 insertions(+), 59 deletions(-) diff --git a/reflexio/integrations/openclaw/plugin/index.ts b/reflexio/integrations/openclaw/plugin/index.ts index 0e0ef5d3..bbd2290f 100644 --- a/reflexio/integrations/openclaw/plugin/index.ts +++ b/reflexio/integrations/openclaw/plugin/index.ts @@ -2,18 +2,32 @@ // Python openclaw_smart package via bash + uv. // // All logic lives in src/openclaw_smart/. This file only does SDK wiring. +// +// Spawn-runner choice: openClaw's runtime currently injects +// `runtime.system.runCommandWithTimeout` only for *trusted* in-process +// plugins. User-installed plugins (us) get the runtime without `system`, +// so capturing it at register-time threw +// `TypeError: Cannot read properties of undefined (reading 'runCommandWithTimeout')` +// and prevented our plugin from loading. We spawn via Node's built-in +// `child_process.spawn` instead, which is not gated by the trust boundary. import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { spawn } from "node:child_process"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; type AnyHandler = (event: unknown, ctx: unknown) => unknown; +interface SpawnResult { + stdout: string; + stderr: string; + code: number | null; +} + // Compiled output lives at /dist/index.js; scripts/ + skills/ stay // at the plugin root, so we resolve one level up from this module's dir. const _MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); const PLUGIN_ROOT = path.resolve(_MODULE_DIR, ".."); const HOOK_ENTRY = path.join(PLUGIN_ROOT, "scripts", "hook_entry.sh"); -const CLI_SCRIPT = path.join(PLUGIN_ROOT, "scripts", "cli.sh"); // Map openClaw hook name → bash event token + per-event timeout (ms). const HOOKS: { name: string; token: string; timeoutMs: number }[] = [ @@ -25,6 +39,49 @@ const HOOKS: { name: string; token: string; timeoutMs: number }[] = [ { name: "session_end", token: "session-end", timeoutMs: 60000 }, ]; +function runScript( + argv: string[], + opts: { timeoutMs: number; input?: string }, +): Promise { + return new Promise((resolve) => { + const [cmd, ...rest] = argv; + const child = spawn(cmd, rest, { stdio: ["pipe", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + let settled = false; + + const finish = (code: number | null) => { + if (settled) return; + settled = true; + if (timer) clearTimeout(timer); + resolve({ stdout, stderr, code }); + }; + + const timer = setTimeout(() => { + try { + child.kill("SIGKILL"); + } catch { + // ignore — the close listener still fires. + } + finish(null); + }, opts.timeoutMs); + + child.stdout.on("data", (d) => { + stdout += d.toString(); + }); + child.stderr.on("data", (d) => { + stderr += d.toString(); + }); + child.on("error", () => finish(null)); + child.on("close", (code) => finish(code)); + + if (opts.input !== undefined) { + child.stdin.write(opts.input); + } + child.stdin.end(); + }); +} + export default definePluginEntry({ id: "reflexio-openclaw-smart", name: "Reflexio openClaw Smart", @@ -33,16 +90,17 @@ export default definePluginEntry({ "injects relevant profiles and playbooks before each response.", register(api) { const log = api.logger; - const runner = api.runtime.system.runCommandWithTimeout; const pluginConfig = api.pluginConfig ?? {}; - // Track the most recent session key for the reflexio_publish tool callback. - let activeSessionKey = ""; - + // Hook-only plugin — `openclaw plugins doctor` reports we are on the + // supported compatibility path. Force-publish from inside a session is + // exposed via the `learn` skill (plugin/skills/learn/SKILL.md), which + // shells out to the same scripts/cli.sh entry point as a tool would. + // Registering an agent tool here requires a manifest contracts.tools + // declaration; we don't need one — keeping the surface skill-only. for (const { name, token, timeoutMs } of HOOKS) { const handler: AnyHandler = async (event, ctx) => { const ctxObj = (ctx ?? {}) as { sessionKey?: string }; - if (ctxObj.sessionKey) activeSessionKey = ctxObj.sessionKey; // Python handlers read a flat dict (e.g. payload.get("sessionKey"), // payload.get("prompt")). Merge ctx first, event on top so any // event-specific overrides win on key clash. @@ -52,15 +110,15 @@ export default definePluginEntry({ plugin_config: pluginConfig, }; try { - const r = await runner( + const r = await runScript( ["bash", HOOK_ENTRY, "openclaw", token], { timeoutMs, input: JSON.stringify(payload) }, ); if (r.code !== 0) { - log.debug?.(`hook ${name} exited ${r.code}: ${r.stderr?.slice(0, 200)}`); + log.debug?.(`hook ${name} exited ${r.code}: ${r.stderr.slice(0, 200)}`); return undefined; } - const out = (r.stdout ?? "").trim(); + const out = r.stdout.trim(); if (!out) return undefined; return JSON.parse(out); } catch (e) { @@ -73,35 +131,5 @@ export default definePluginEntry({ // so `as never` opts out of the specialized signatures. api.on(name as never, handler as never); } - - // Agent-invoked immediate publish. - api.registerTool({ - name: "reflexio_publish", - description: - "Immediately publish all buffered conversation turns to the Reflexio server. " + - "Use after user corrections or high-signal moments when you don't want to wait " + - "for the automatic session-end publish.", - parameters: { type: "object", properties: {} }, - optional: true, - async execute(_id, _params) { - try { - const r = await runner( - ["bash", CLI_SCRIPT, "learn", "--session", activeSessionKey], - { timeoutMs: 30000 }, - ); - const text = - r.code === 0 - ? r.stdout ?? "publish queued" - : `publish failed: ${r.stderr?.slice(0, 200)}`; - return { content: [{ type: "text" as const, text }] }; - } catch (e) { - return { - content: [ - { type: "text" as const, text: `publish error: ${(e as Error).message}` }, - ], - }; - } - }, - }); }, }); diff --git a/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts b/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts index 4d0e380b..30ecb30d 100644 --- a/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +++ b/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts @@ -1,12 +1,79 @@ -import { describe, it, expect, vi } from "vitest"; -import plugin from "../index.ts"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { EventEmitter } from "node:events"; + +// Mock node:child_process.spawn before importing the plugin so the shim's +// runScript() helper goes through our fake instead of a real subprocess. +type FakeProcShape = { + stdout: string; + stderr: string; + code: number | null; + /** When true, the simulated process never emits 'close' so the runScript timeout fires. */ + hang?: boolean; +}; + +const fakeProc: { current: FakeProcShape } = { + current: { stdout: "", stderr: "", code: 0 }, +}; + +const lastSpawn: { cmd?: string; args?: string[]; input?: string } = {}; + +vi.mock("node:child_process", () => ({ + spawn: (cmd: string, args: string[]) => { + lastSpawn.cmd = cmd; + lastSpawn.args = args; + const child = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + stdin: { write: (s: string) => void; end: () => void }; + kill: (signal?: string) => void; + }; + child.stdout = new EventEmitter(); + child.stderr = new EventEmitter(); + child.stdin = { + write: (s: string) => { + lastSpawn.input = s; + }, + end: () => {}, + }; + child.kill = () => { + child.emit("close", null); + }; + + if (!fakeProc.current.hang) { + // Defer to next tick so listeners attached after spawn() are wired up + // before we start emitting. + setImmediate(() => { + if (fakeProc.current.stdout) { + child.stdout.emit("data", Buffer.from(fakeProc.current.stdout)); + } + if (fakeProc.current.stderr) { + child.stderr.emit("data", Buffer.from(fakeProc.current.stderr)); + } + child.emit("close", fakeProc.current.code); + }); + } + return child; + }, +})); + +// Import AFTER vi.mock so the plugin captures the mocked spawn. +const { default: plugin } = await import("../index.ts"); + +beforeEach(() => { + fakeProc.current = { stdout: "", stderr: "", code: 0 }; + lastSpawn.cmd = undefined; + lastSpawn.args = undefined; + lastSpawn.input = undefined; +}); describe("openclaw-smart TS shim", () => { - it("registers all 6 hooks", async () => { + it("registers all 6 hooks without touching runtime.system", async () => { const onCalls: string[] = []; + // Intentionally omit runtime.system to match the untrusted-plugin reality + // — if the plugin ever reaches for it, register() crashes here. const api = { logger: {}, - runtime: { system: { runCommandWithTimeout: vi.fn() } }, + runtime: {}, pluginConfig: {}, registerTool: vi.fn(), on: (name: string, _h: unknown) => { @@ -24,31 +91,32 @@ describe("openclaw-smart TS shim", () => { ]); }); - it("registers the reflexio_publish tool", async () => { + it("does not register agent tools (hook-only plugin)", async () => { + // Registering tools requires a manifest contracts.tools declaration; + // we intentionally keep the surface skill-only so register() never + // touches api.registerTool. The `learn` skill is the equivalent path. const reg = vi.fn(); const api = { logger: {}, - runtime: { system: { runCommandWithTimeout: vi.fn() } }, + runtime: {}, pluginConfig: {}, registerTool: reg, on: vi.fn(), }; await plugin.register(api as never); - expect(reg).toHaveBeenCalledWith( - expect.objectContaining({ name: "reflexio_publish" }), - ); + expect(reg).not.toHaveBeenCalled(); }); - it("forwards a hook payload via bash and returns parsed JSON", async () => { - const run = vi.fn(async () => ({ + it("forwards a hook payload via bash + spawn and parses JSON stdout", async () => { + fakeProc.current = { stdout: '{"prependContext":"hi"}', stderr: "", code: 0, - })); + }; const handlers: Record = {}; const api = { logger: {}, - runtime: { system: { runCommandWithTimeout: run } }, + runtime: {}, pluginConfig: {}, registerTool: vi.fn(), on: (name: string, h: Function) => { @@ -58,18 +126,20 @@ describe("openclaw-smart TS shim", () => { await plugin.register(api as never); const result = await handlers["session_start"]({}, { sessionKey: "s1" }); expect(result).toEqual({ prependContext: "hi" }); - expect(run).toHaveBeenCalledWith( - expect.arrayContaining(["bash"]), - expect.objectContaining({ input: expect.any(String) }), + expect(lastSpawn.cmd).toBe("bash"); + expect(lastSpawn.args).toEqual( + expect.arrayContaining([expect.stringMatching(/hook_entry\.sh$/), "openclaw", "session-start"]), ); + expect(lastSpawn.input).toBeDefined(); + expect(JSON.parse(lastSpawn.input!)).toMatchObject({ sessionKey: "s1" }); }); it("returns undefined when subprocess exits non-zero", async () => { - const run = vi.fn(async () => ({ stdout: "", stderr: "err", code: 1 })); + fakeProc.current = { stdout: "", stderr: "boom", code: 1 }; const handlers: Record = {}; const api = { logger: { debug: vi.fn() }, - runtime: { system: { runCommandWithTimeout: run } }, + runtime: {}, pluginConfig: {}, registerTool: vi.fn(), on: (name: string, h: Function) => { @@ -82,11 +152,11 @@ describe("openclaw-smart TS shim", () => { }); it("returns undefined on empty stdout (no-op event)", async () => { - const run = vi.fn(async () => ({ stdout: "", stderr: "", code: 0 })); + fakeProc.current = { stdout: "", stderr: "", code: 0 }; const handlers: Record = {}; const api = { logger: { debug: vi.fn() }, - runtime: { system: { runCommandWithTimeout: run } }, + runtime: {}, pluginConfig: {}, registerTool: vi.fn(), on: (name: string, h: Function) => { From 9b7f244ad0bbcd70b3d70e90be15c0308bf9d298 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 06:18:40 +0000 Subject: [PATCH 50/53] fix(openclaw): activation.onStartup + allowConversationAccess gate hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gating issues prevented the gateway from dispatching events to our plugin even after it 'registered' cleanly: 1. Without manifest 'activation.onStartup: true', the gateway lazy-loads the plugin only when one of its activation triggers fires (providers, channels, routes). A hook-only plugin has none, so it's never loaded for actual session dispatch — even though plugins inspect reports 'Status: loaded' (that's config-state, not gateway-state). 2. Non-bundled plugins must explicitly set plugins.entries..hooks.allowConversationAccess=true before the gateway will dispatch typed hooks to them. Otherwise every hook invocation is silently dropped with: [plugins] typed hook "agent_end" blocked because non-bundled plugins must set plugins.entries..hooks.allowConversationAccess=true Fix: * manifest: add activation.onStartup=true so the gateway loads us during startup and includes us in the dispatch fan-out. * reflexio setup openclaw: set the allowConversationAccess flag during install so fresh installs don't hit the same silent-drop trap. Verified locally: after both changes, gateway boot now lists '11 plugins: ... reflexio-openclaw-smart, ...' (was 10), the 'typed hook blocked' log line is gone, and plugins doctor reports only the benign 'is hook-only / supported compatibility path' info. --- reflexio/cli/commands/setup_cmd.py | 17 +++++++++++++++++ .../openclaw/plugin/openclaw.plugin.json | 3 +++ 2 files changed, 20 insertions(+) diff --git a/reflexio/cli/commands/setup_cmd.py b/reflexio/cli/commands/setup_cmd.py index 206c3b04..9732240c 100644 --- a/reflexio/cli/commands/setup_cmd.py +++ b/reflexio/cli/commands/setup_cmd.py @@ -671,6 +671,23 @@ def _install_openclaw_integration(env_path: Path) -> bool: capture_output=True, text=True, ) + # Conversation-access opt-in is required for non-bundled plugins to + # receive typed hooks (agent_end, before_prompt_build, etc.). Without + # this, the gateway silently drops every plugin-side hook dispatch + # with: '[plugins] typed hook "agent_end" blocked because non-bundled + # plugins must set ...hooks.allowConversationAccess=true'. + subprocess.run( + [ + cli, + "config", + "set", + f"plugins.entries.{_OPENCLAW_PLUGIN_ID}.hooks.allowConversationAccess", + "true", + ], + check=False, + capture_output=True, + text=True, + ) except subprocess.CalledProcessError as exc: typer.echo(f"Error: openclaw command failed: {exc.stderr or exc.stdout}") raise typer.Exit(1) from exc diff --git a/reflexio/integrations/openclaw/plugin/openclaw.plugin.json b/reflexio/integrations/openclaw/plugin/openclaw.plugin.json index fb44ce83..abbf034d 100644 --- a/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +++ b/reflexio/integrations/openclaw/plugin/openclaw.plugin.json @@ -3,6 +3,9 @@ "name": "Reflexio openClaw Smart", "description": "Cross-session memory and self-improvement for openClaw via local reflexio backend.", "skills": ["./skills"], + "activation": { + "onStartup": true + }, "configSchema": { "type": "object", "additionalProperties": false, From 749ca69c78b7c8e470bfe903ea8138b52ff69d20 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Tue, 19 May 2026 06:51:55 +0000 Subject: [PATCH 51/53] fix(openclaw): real Telegram round-trip now persists JSONL + publishes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two final fixes that unblocked the end-to-end Telegram pipeline: 1. state.py session-id charset now allows colons. openClaw sessionKeys for channel-routed agents are shaped agent:: (e.g. agent:main:telegram:default:direct:). My recent path-traversal regex rejected colons, so state.append() silently no-op'd and the sessions dir stayed empty even with hooks firing correctly. Colons are not POSIX path separators — they're safe inside filenames — and the explicit '..' / pure-dot guards still block real traversal attempts. Added colon-containing session ids to the regression tests. 2. index.ts subscribes to message_received and normalizes its event.content to event.prompt so the Python before_prompt_build handler receives a uniform payload shape across channel-side and agent-side hook invocations. Per the SDK type definitions the agent side delivers event.prompt while channel side delivers event.content — the normalization keeps the Python side handler shape stable. Verified end-to-end via Telegram: - Bot DM produced ~/.openclaw-smart/sessions/agent:main:telegram:default:direct:.jsonl - Buffer contains User → Assistant_tool → Assistant turns - agent_end fired and published to the reflexio backend on port 8071 - {"published_up_to": 4} watermark stamped, confirming publish succeeded Also pruned the diagnostic 'subscribe-all' instrumentation back to the 7 hooks we actually rely on (session_start, message_received, before_prompt_build, before_tool_call, after_tool_call, agent_end, session_end). Diagnostic log.info hook-fire spam moved to log.debug. Plugin tests: 140 passed. TS shim tests: 5 passed. --- .../integrations/openclaw/plugin/index.ts | 56 +++++++++++++++---- .../plugin/src/openclaw_smart/state.py | 22 +++++--- .../tests-ts/test_shim_dispatch.test.ts | 3 +- .../openclaw/plugin/tests/test_state.py | 17 +++++- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/reflexio/integrations/openclaw/plugin/index.ts b/reflexio/integrations/openclaw/plugin/index.ts index bbd2290f..38973777 100644 --- a/reflexio/integrations/openclaw/plugin/index.ts +++ b/reflexio/integrations/openclaw/plugin/index.ts @@ -30,8 +30,29 @@ const PLUGIN_ROOT = path.resolve(_MODULE_DIR, ".."); const HOOK_ENTRY = path.join(PLUGIN_ROOT, "scripts", "hook_entry.sh"); // Map openClaw hook name → bash event token + per-event timeout (ms). +// +// Empirically-verified mapping (Telegram → agent flow, openClaw 2026.5.x): +// - message_received → user message arrived (channel-side) +// - before_prompt_build → final prompt built before model call +// - before_tool_call → agent about to call a tool +// - after_tool_call → tool returned +// - agent_end → agent finished a turn — triggers publish +// - session_start/end → session lifecycle (not always fired) +// +// We deliberately exclude: +// - Gating hooks (inbound_claim, before_dispatch, reply_dispatch, +// before_install) — returning undefined from those is interpreted as +// "deny", silently stopping the channel → agent message pipeline. +// - Per-token hooks (llm_input, llm_output, model_call_*) — they fire +// too often and would spawn a child process per token. +// +// before_agent_start / before_agent_reply / before_agent_run all also +// fire and contain `event.prompt`, but they duplicate the user turn +// captured by message_received with a "Delivery preamble" prefix — +// noisier learning input. Stick to message_received for the clean prompt. const HOOKS: { name: string; token: string; timeoutMs: number }[] = [ { name: "session_start", token: "session-start", timeoutMs: 30000 }, + { name: "message_received", token: "before-prompt-build", timeoutMs: 15000 }, { name: "before_prompt_build", token: "before-prompt-build", timeoutMs: 15000 }, { name: "before_tool_call", token: "before-tool-call", timeoutMs: 10000 }, { name: "after_tool_call", token: "after-tool-call", timeoutMs: 15000 }, @@ -92,21 +113,28 @@ export default definePluginEntry({ const log = api.logger; const pluginConfig = api.pluginConfig ?? {}; + log.info?.( + `[reflexio-openclaw-smart] register() running; PLUGIN_ROOT=${PLUGIN_ROOT}`, + ); + // Hook-only plugin — `openclaw plugins doctor` reports we are on the // supported compatibility path. Force-publish from inside a session is - // exposed via the `learn` skill (plugin/skills/learn/SKILL.md), which - // shells out to the same scripts/cli.sh entry point as a tool would. - // Registering an agent tool here requires a manifest contracts.tools - // declaration; we don't need one — keeping the surface skill-only. + // exposed via the `learn` skill (plugin/skills/learn/SKILL.md). for (const { name, token, timeoutMs } of HOOKS) { const handler: AnyHandler = async (event, ctx) => { const ctxObj = (ctx ?? {}) as { sessionKey?: string }; - // Python handlers read a flat dict (e.g. payload.get("sessionKey"), - // payload.get("prompt")). Merge ctx first, event on top so any - // event-specific overrides win on key clash. + const eventObj = (event ?? {}) as Record; + // For channel-side hooks (e.g. message_received), the user prompt + // arrives in event.content; agent-side hooks deliver it in + // event.prompt. Normalize to ``prompt`` so the Python handlers stay + // simple — they only need to know about one shape. + const normalizedPrompt = + (eventObj["prompt"] as string | undefined) ?? + (eventObj["content"] as string | undefined); const payload = { ...(ctxObj as Record), - ...((event ?? {}) as Record), + ...eventObj, + ...(normalizedPrompt !== undefined ? { prompt: normalizedPrompt } : {}), plugin_config: pluginConfig, }; try { @@ -115,14 +143,19 @@ export default definePluginEntry({ { timeoutMs, input: JSON.stringify(payload) }, ); if (r.code !== 0) { - log.debug?.(`hook ${name} exited ${r.code}: ${r.stderr.slice(0, 200)}`); + log.debug?.( + `[reflexio-openclaw-smart] ${name} exit=${r.code}` + + (r.stderr ? ` stderr=${r.stderr.slice(0, 200)}` : ""), + ); return undefined; } const out = r.stdout.trim(); if (!out) return undefined; return JSON.parse(out); } catch (e) { - log.debug?.(`hook ${name} failed: ${(e as Error).message}`); + log.debug?.( + `[reflexio-openclaw-smart] ${name} failed: ${(e as Error).message}`, + ); return undefined; } }; @@ -131,5 +164,8 @@ export default definePluginEntry({ // so `as never` opts out of the specialized signatures. api.on(name as never, handler as never); } + log.info?.( + `[reflexio-openclaw-smart] subscribed to ${HOOKS.length} hooks`, + ); }, }); diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py index beee0540..3dedf88e 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py @@ -62,17 +62,21 @@ def state_dir() -> Path: return Path(override) if override else _DEFAULT_STATE_DIR -# openClaw session ids are opaque tokens (typically UUID-like). Restrict to a -# conservative alphanumeric + dot/underscore/hyphen set so a crafted value -# with path separators (e.g. ``../escape``) or shell metacharacters can't -# write or read outside ``state_dir()``. Any session id outside this charset -# is treated as malformed and the resulting filesystem op becomes a no-op +# openClaw session ids are opaque tokens. The real shape we see in +# production is ``agent::`` (e.g. ``agent:main:main``) — colons +# are part of the protocol, not a path separator on POSIX, so they're +# safe in filesystem paths. Restrict to a conservative +# alphanumeric + dot/underscore/hyphen/colon set so a crafted value +# with `/` or `\` path separators or shell metacharacters can't write or +# read outside ``state_dir()``. Any session id outside this charset is +# treated as malformed and the resulting filesystem op becomes a no-op # upstream (see ``append`` / ``read_all``). # -# We additionally reject ids that are pure-dot tokens (``.``, ``..``) or that -# contain a ``..`` substring even though the underlying regex would accept -# them — those are not valid filenames and signal an escape attempt. -_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._-]{1,128}$") +# We additionally reject ids that are pure-dot tokens (``.``, ``..``) or +# contain a ``..`` substring even though the underlying regex would +# accept them — those are not valid filenames and signal an escape +# attempt. +_SESSION_ID_RE = re.compile(r"^[A-Za-z0-9._:-]{1,128}$") def _safe_session_id(session_id: str) -> str | None: diff --git a/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts b/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts index 30ecb30d..0ab1af9e 100644 --- a/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +++ b/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts @@ -67,7 +67,7 @@ beforeEach(() => { }); describe("openclaw-smart TS shim", () => { - it("registers all 6 hooks without touching runtime.system", async () => { + it("registers the empirically-verified hook set without touching runtime.system", async () => { const onCalls: string[] = []; // Intentionally omit runtime.system to match the untrusted-plugin reality // — if the plugin ever reaches for it, register() crashes here. @@ -83,6 +83,7 @@ describe("openclaw-smart TS shim", () => { await plugin.register(api as never); expect(onCalls).toEqual([ "session_start", + "message_received", "before_prompt_build", "before_tool_call", "after_tool_call", diff --git a/reflexio/integrations/openclaw/plugin/tests/test_state.py b/reflexio/integrations/openclaw/plugin/tests/test_state.py index f9f54800..d3f0d1ef 100644 --- a/reflexio/integrations/openclaw/plugin/tests/test_state.py +++ b/reflexio/integrations/openclaw/plugin/tests/test_state.py @@ -169,8 +169,19 @@ def test_path_traversal_session_id_rejected(isolate_state_dir, tmp_path): def test_safe_session_ids_accepted(): - """The safe-id charset (alphanumeric + dot/underscore/hyphen) is allowed.""" - for sid in ("sess1", "a.b.c", "a_b", "a-b", "01234", "S.1_2-3"): + """The safe-id charset (alphanumeric + dot/underscore/hyphen/colon) is allowed.""" + # Colons appear in real openClaw sessionKeys (``agent:main:main``); they're + # not POSIX path separators so they're safe inside filenames. + for sid in ( + "sess1", + "a.b.c", + "a_b", + "a-b", + "01234", + "S.1_2-3", + "agent:main:main", + "agent:work:abc123", + ): path = state.session_path(sid) - assert path is not None + assert path is not None, f"safe id {sid!r} should have been accepted" assert path.name == f"{sid}.jsonl" From f72e4fcc0a2be89607ee2dc27fc6b641b3f744e2 Mon Sep 17 00:00:00 2001 From: yilu331 Date: Wed, 20 May 2026 17:53:25 -0700 Subject: [PATCH 52/53] fix(openclaw): address PR review follow-ups --- reflexio/cli/commands/setup_cmd.py | 35 ++++++++- .../plugin/scripts/backend-service.sh | 6 +- .../openclaw/plugin/src/openclaw_smart/cli.py | 8 +- .../plugin/src/openclaw_smart/publish.py | 56 ++++++++++---- .../plugin/src/openclaw_smart/state.py | 18 ++++- ...t_publish_to_local_reflexio_integration.py | 4 +- .../test_search_inject_integration.py | 2 +- .../openclaw/plugin/tests/test_cli.py | 29 ++++++- .../plugin/tests/test_events_session_end.py | 4 + .../openclaw/plugin/tests/test_publish.py | 49 ++++++++++++ .../openclaw/plugin/tests/test_state.py | 30 ++++++++ tests/cli/test_setup_cmd.py | 76 +++++++++++++++++++ 12 files changed, 284 insertions(+), 33 deletions(-) create mode 100644 reflexio/integrations/openclaw/plugin/tests/test_publish.py diff --git a/reflexio/cli/commands/setup_cmd.py b/reflexio/cli/commands/setup_cmd.py index 9732240c..c8cc40e3 100644 --- a/reflexio/cli/commands/setup_cmd.py +++ b/reflexio/cli/commands/setup_cmd.py @@ -598,6 +598,19 @@ def _remove_env_keys(env_path: Path, keys: tuple[str, ...]) -> None: env_path.write_text("\n".join(kept) + ("\n" if kept else "")) +def _read_env_key(env_path: Path | None, key: str) -> str | None: + """Read a simple KEY=value assignment from a .env file.""" + if env_path is None or not env_path.exists(): + return None + prefix = f"{key}=" + for line in env_path.read_text().splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#") or not stripped.startswith(prefix): + continue + return stripped[len(prefix) :].strip().strip('"').strip("'") or None + return None + + def _run_smart_install(plugin_dir: Path) -> None: """Run the plugin's first-run installer so uv/.venv land before first hook.""" script = plugin_dir / "scripts" / "smart-install.sh" @@ -676,7 +689,7 @@ def _install_openclaw_integration(env_path: Path) -> bool: # this, the gateway silently drops every plugin-side hook dispatch # with: '[plugins] typed hook "agent_end" blocked because non-bundled # plugins must set ...hooks.allowConversationAccess=true'. - subprocess.run( + access_cfg = subprocess.run( [ cli, "config", @@ -688,6 +701,12 @@ def _install_openclaw_integration(env_path: Path) -> bool: capture_output=True, text=True, ) + if access_cfg.returncode != 0: + typer.echo( + "Error: could not persist openClaw conversation-access permission: " + f"{access_cfg.stderr or access_cfg.stdout}" + ) + raise typer.Exit(1) except subprocess.CalledProcessError as exc: typer.echo(f"Error: openclaw command failed: {exc.stderr or exc.stdout}") raise typer.Exit(1) from exc @@ -733,7 +752,7 @@ def _uninstall_openclaw(env_path: Path | None = None, purge: bool = False) -> No "This will remove the Reflexio integration from openClaw. Continue?", abort=True, ) - cli = shutil.which("openclaw") + cli = _read_env_key(env_path, "OPENCLAW_BIN") or shutil.which("openclaw") if cli: subprocess.run( [cli, "plugins", "disable", _OPENCLAW_PLUGIN_ID], @@ -748,7 +767,10 @@ def _uninstall_openclaw(env_path: Path | None = None, purge: bool = False) -> No text=True, ) else: - typer.echo("Warning: openclaw CLI not found on PATH, skipping plugin removal") + typer.echo( + "Warning: openclaw CLI not found in OPENCLAW_BIN or PATH, " + "skipping plugin removal" + ) if env_path is not None: _remove_env_keys(env_path, ("OPENCLAW_BIN", "OPENCLAW_SMART_USE_LOCAL_CLI")) @@ -834,6 +856,13 @@ def openclaw( typer.echo("Error: could not locate or create a .env file") raise typer.Exit(1) + if repair and (uninstall or purge): + typer.echo("Error: --repair cannot be combined with --uninstall or --purge") + raise typer.Exit(1) + if purge and not uninstall: + typer.echo("Error: --purge requires --uninstall") + raise typer.Exit(1) + if repair: _repair_openclaw() return diff --git a/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh b/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh index 58c98ef1..167fc860 100755 --- a/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +++ b/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh @@ -49,11 +49,11 @@ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)" # Pin the openclaw CLI explicitly so the reflexio backend's openclaw_provider # can find it from a hook context whose PATH lacks the user's normal CLI dir. -if [ -z "${OPENCLAW_SMART_CLI_PATH:-}" ]; then +if [ -z "${OPENCLAW_BIN:-}" ]; then if _oc_cli_path=$(command -v openclaw 2>/dev/null) && [ -n "$_oc_cli_path" ]; then - export OPENCLAW_SMART_CLI_PATH="$_oc_cli_path" + export OPENCLAW_BIN="$_oc_cli_path" elif [ -x "$HOME/.local/bin/openclaw" ]; then - export OPENCLAW_SMART_CLI_PATH="$HOME/.local/bin/openclaw" + export OPENCLAW_BIN="$HOME/.local/bin/openclaw" fi unset _oc_cli_path fi diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py index e19e144e..107df20f 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py @@ -523,6 +523,7 @@ def cmd_clear_all(args: argparse.Namespace) -> int: ) return stop_rc or 1 + clear_all_failed = False removed_targets = 0 try: for target in targets: @@ -530,17 +531,18 @@ def cmd_clear_all(args: argparse.Namespace) -> int: removed_targets += 1 except (OSError, _ClearAllError) as exc: sys.stderr.write(f"error: could not remove reflexio data: {exc}\n") - return 1 + clear_all_failed = True removed_buffers = 0 root = state.state_dir() - if root.is_dir(): + if not clear_all_failed and root.is_dir(): for buf in root.glob("*.jsonl"): try: buf.unlink() removed_buffers += 1 except OSError as exc: sys.stderr.write(f"warning: could not remove {buf}: {exc}\n") + clear_all_failed = True start_rc = 0 if was_running: @@ -556,7 +558,7 @@ def cmd_clear_all(args: argparse.Namespace) -> int: f"Cleared reflexio: {target_summary}. " f"Removed {removed_buffers} local session buffer(s).\n" ) - return start_rc or 0 + return start_rc or (1 if clear_all_failed else 0) def _build_parser() -> argparse.ArgumentParser: diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py index ee8e2c38..0b37c5a6 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py @@ -8,14 +8,37 @@ from __future__ import annotations +import contextlib +from collections.abc import Iterator from typing import Literal from openclaw_smart import state from openclaw_smart.reflexio_adapter import Adapter +try: + import fcntl # POSIX only — Windows falls back to no publish lock. +except ImportError: # pragma: no cover — non-POSIX platforms + fcntl = None # type: ignore[assignment] + PublishStatus = Literal["nothing", "ok", "failed"] +@contextlib.contextmanager +def _session_publish_lock(session_id: str) -> Iterator[None]: + """Serialize read-publish-watermark for one session buffer.""" + lock_path = state.publish_lock_path(session_id) + if lock_path is None or fcntl is None: + yield + return + lock_path.parent.mkdir(parents=True, exist_ok=True) + with lock_path.open("a", encoding="utf-8") as fh: + fcntl.flock(fh.fileno(), fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(fh.fileno(), fcntl.LOCK_UN) + + def publish_unpublished( *, session_id: str, @@ -54,19 +77,20 @@ def publish_unpublished( was unreachable. On ``"failed"`` the watermark is not advanced, so the next hook retries the same batch. """ - records = state.read_all(session_id) - _, interactions = state.unpublished_slice(records) - if not interactions: - return ("nothing", 0) - client = adapter if adapter is not None else Adapter() - ok = client.publish( - session_id=session_id, - project_id=project_id, - interactions=interactions, - force_extraction=force_extraction, - skip_aggregation=skip_aggregation, - ) - if ok: - state.append(session_id, {"published_up_to": len(records)}) - return ("ok", len(interactions)) - return ("failed", len(interactions)) + with _session_publish_lock(session_id): + records = state.read_all(session_id) + _, interactions = state.unpublished_slice(records) + if not interactions: + return ("nothing", 0) + client = adapter if adapter is not None else Adapter() + ok = client.publish( + session_id=session_id, + project_id=project_id, + interactions=interactions, + force_extraction=force_extraction, + skip_aggregation=skip_aggregation, + ) + if ok: + state.append(session_id, {"published_up_to": len(records)}) + return ("ok", len(interactions)) + return ("failed", len(interactions)) diff --git a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py index 3dedf88e..c0ab9802 100644 --- a/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +++ b/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py @@ -20,8 +20,9 @@ import logging import os import re +from collections.abc import Iterable from pathlib import Path -from typing import Any, Iterable +from typing import Any try: import fcntl # POSIX only — Windows hooks fall back to append-without-lock. @@ -115,6 +116,14 @@ def injected_path(session_id: str) -> Path | None: return state_dir() / f"{sid}.injected.jsonl" +def publish_lock_path(session_id: str) -> Path | None: + """Return the per-session publish lock path, or ``None`` if unsafe.""" + sid = _safe_session_id(session_id) + if sid is None: + return None + return state_dir() / f"{sid}.publish.lock" + + def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None: """Append citation-registry entries to the per-session injected-items file. @@ -264,7 +273,9 @@ def unpublished_slice( turns: list[dict[str, Any]] = [] for idx, rec in enumerate(records): if "published_up_to" in rec: - published = rec["published_up_to"] + marker = rec.get("published_up_to") + if isinstance(marker, int) and marker >= 0: + published = marker pending_tools = [] turns = [] continue @@ -272,7 +283,8 @@ def unpublished_slice( continue role = rec.get("role") if role == "Assistant_tool": - tool_input = rec.get("tool_input") or {} + raw_tool_input = rec.get("tool_input") + tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {} tool_output = rec.get("tool_output") or "" tool_entry: dict[str, Any] = { "tool_name": rec.get("tool_name", ""), diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py index 1929dc0e..161713b5 100644 --- a/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py @@ -1,7 +1,7 @@ """Integration: publish buffered turns to a local SQLite-backed reflexio. Requires a reflexio backend reachable at ``REFLEXIO_URL`` (default -``http://localhost:8071/``). Skipped automatically when the backend is +``http://localhost:8081/``). Skipped automatically when the backend is unreachable so the suite is portable across machines. """ @@ -16,7 +16,7 @@ def _reflexio_url() -> str: - return os.environ.get("REFLEXIO_URL", "http://localhost:8071/") + return os.environ.get("REFLEXIO_URL", "http://localhost:8081/") def _backend_alive(url: str) -> bool: diff --git a/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py b/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py index 83357ae0..21936d1e 100644 --- a/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +++ b/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py @@ -24,7 +24,7 @@ def _reflexio_url() -> str: - return os.environ.get("REFLEXIO_URL", "http://localhost:8071/") + return os.environ.get("REFLEXIO_URL", "http://localhost:8081/") def _backend_alive(url: str) -> bool: diff --git a/reflexio/integrations/openclaw/plugin/tests/test_cli.py b/reflexio/integrations/openclaw/plugin/tests/test_cli.py index e6f4a6b0..9a354468 100644 --- a/reflexio/integrations/openclaw/plugin/tests/test_cli.py +++ b/reflexio/integrations/openclaw/plugin/tests/test_cli.py @@ -3,10 +3,9 @@ from __future__ import annotations from argparse import Namespace -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest - from openclaw_smart import cli @@ -127,6 +126,32 @@ def test_cmd_clear_all_with_yes_proceeds(monkeypatch, tmp_path): assert not (sessions / "old.jsonl").exists() +def test_cmd_clear_all_restarts_backend_after_delete_failure(tmp_path): + target = cli._ClearAllTarget( + path=tmp_path / "reflexio-openclaw-test", + kind="dir", + label="test target", + ) + service_calls: list[str] = [] + + def fake_run_service(_script, command) -> int: # noqa: ANN001 + service_calls.append(command) + return 0 + + with patch( + "openclaw_smart.cli._resolve_clear_all_targets", return_value=[target] + ), patch("openclaw_smart.cli._service_status", return_value="running on 8071"), patch( + "openclaw_smart.cli._remove_clear_all_target", + side_effect=cli._ClearAllError("boom"), + ), patch( + "openclaw_smart.cli._run_service", side_effect=fake_run_service + ): + rc = cli.cmd_clear_all(Namespace(yes=True)) + + assert rc == 1 + assert service_calls == ["stop", "start"] + + def test_build_parser_accepts_show(): parser = cli._build_parser() args = parser.parse_args(["show"]) diff --git a/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py b/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py index 404a2e60..f3b07198 100644 --- a/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +++ b/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py @@ -33,5 +33,9 @@ def test_handle_falls_back_to_session_id_key(): return_value="proj-y", ): session_end.handle({"sessionId": "s2"}) + pub.publish_unpublished.assert_called_once() kwargs = pub.publish_unpublished.call_args[1] assert kwargs["session_id"] == "s2" + assert kwargs["project_id"] == "proj-y" + assert kwargs["force_extraction"] is True + assert kwargs["skip_aggregation"] is False diff --git a/reflexio/integrations/openclaw/plugin/tests/test_publish.py b/reflexio/integrations/openclaw/plugin/tests/test_publish.py new file mode 100644 index 00000000..6caee7d9 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests/test_publish.py @@ -0,0 +1,49 @@ +"""Tests for openclaw_smart.publish.""" + +from __future__ import annotations + +import json + +import pytest +from openclaw_smart import publish, state + + +@pytest.fixture(autouse=True) +def isolate_state_dir(monkeypatch, tmp_path): + sessions = tmp_path / "sessions" + monkeypatch.setenv("OPENCLAW_SMART_STATE_DIR", str(sessions)) + return sessions + + +class _Adapter: + def __init__(self) -> None: + self.calls = 0 + + def publish(self, **_kwargs) -> bool: # noqa: ANN003 + self.calls += 1 + return True + + +def test_publish_unpublished_serializes_with_lock_and_stamps_watermark( + isolate_state_dir, +): + state.append("s1", {"role": "User", "content": "hi"}) + adapter = _Adapter() + + status, count = publish.publish_unpublished( + session_id="s1", + project_id="proj", + force_extraction=False, + skip_aggregation=False, + adapter=adapter, + ) + + assert (status, count) == ("ok", 1) + assert adapter.calls == 1 + assert (isolate_state_dir / "s1.publish.lock").exists() + records = [ + json.loads(line) + for line in (isolate_state_dir / "s1.jsonl").read_text().splitlines() + ] + assert records[-1] == {"published_up_to": 1} + diff --git a/reflexio/integrations/openclaw/plugin/tests/test_state.py b/reflexio/integrations/openclaw/plugin/tests/test_state.py index d3f0d1ef..bd36d33a 100644 --- a/reflexio/integrations/openclaw/plugin/tests/test_state.py +++ b/reflexio/integrations/openclaw/plugin/tests/test_state.py @@ -114,6 +114,36 @@ def test_unpublished_slice_truncates_long_tool_fields(): assert truncated == "x" * 256 +def test_unpublished_slice_ignores_malformed_watermark_and_tool_input(): + records = [ + {"published_up_to": "not-an-int"}, + { + "role": "Assistant_tool", + "tool_name": "Bash", + "tool_input": ["not", "a", "mapping"], + "tool_output": "ok", + }, + {"role": "Assistant", "content": "Done."}, + ] + + watermark, turns = state.unpublished_slice(records) + + assert watermark == 0 + assert turns == [ + { + "role": "Assistant", + "content": "Done.", + "tools_used": [ + { + "tool_name": "Bash", + "status": "success", + "tool_data": {"output": "ok"}, + } + ], + } + ] + + def test_append_injected_writes_registry(): state.append_injected( "s4", diff --git a/tests/cli/test_setup_cmd.py b/tests/cli/test_setup_cmd.py index 464877ad..49b1878e 100644 --- a/tests/cli/test_setup_cmd.py +++ b/tests/cli/test_setup_cmd.py @@ -951,6 +951,22 @@ def test_remove_env_keys_strips_lines(self, tmp_path: Path) -> None: remaining = env.read_text().splitlines() assert remaining == ["OTHER=keep", "STILL=keep"] + def test_openclaw_rejects_conflicting_flags( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Repair, uninstall, and purge modes must be unambiguous.""" + from reflexio.cli import env_loader + from reflexio.cli.commands.setup_cmd import openclaw + + env = tmp_path / ".env" + env.write_text("") + monkeypatch.setattr(env_loader, "ensure_user_env_for_setup", lambda: env) + + with pytest.raises(typer.Exit): + openclaw(repair=True, uninstall=True, purge=False) + with pytest.raises(typer.Exit): + openclaw(repair=False, uninstall=False, purge=True) + def test_install_openclaw_uses_new_plugin_id( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1000,6 +1016,66 @@ def fake_run(argv, **_kw): # noqa: ANN001 assert 'OPENCLAW_BIN="/usr/bin/openclaw"' in body assert 'OPENCLAW_SMART_USE_LOCAL_CLI="1"' in body + def test_install_openclaw_fails_if_conversation_access_not_persisted( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Setup must not report success if typed-hook access cannot be saved.""" + from reflexio.cli.commands import setup_cmd + + env_path = tmp_path / ".env" + env_path.write_text("") + plugin_dir = tmp_path / "plugin" + plugin_dir.mkdir() + + monkeypatch.setattr(setup_cmd.shutil, "which", lambda _: "/usr/bin/openclaw") + monkeypatch.setattr(setup_cmd, "_openclaw_plugin_dir", lambda: plugin_dir) + + class _Result: + def __init__(self, returncode: int = 0) -> None: + self.stdout = "" + self.stderr = "denied" + self.returncode = returncode + + def fake_run(argv, **_kw): # noqa: ANN001 + if argv[1:3] == ["config", "set"]: + return _Result(returncode=1) + return _Result() + + monkeypatch.setattr(setup_cmd.subprocess, "run", fake_run) + + with pytest.raises(typer.Exit): + setup_cmd._install_openclaw_integration(env_path) + + def test_uninstall_openclaw_reuses_openclaw_bin_from_env( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Uninstall should remove the plugin even if PATH no longer has openclaw.""" + from reflexio.cli.commands import setup_cmd + + env_path = tmp_path / ".env" + env_path.write_text('OPENCLAW_BIN="/opt/openclaw/bin/openclaw"\n') + monkeypatch.setattr(setup_cmd.typer, "confirm", lambda *_a, **_kw: True) + monkeypatch.setattr(setup_cmd.shutil, "which", lambda _: None) + + calls: list[list[str]] = [] + + def fake_run(argv, **_kw): # noqa: ANN001 + calls.append(list(argv)) + + class _Result: + returncode = 0 + stdout = "" + stderr = "" + + return _Result() + + monkeypatch.setattr(setup_cmd.subprocess, "run", fake_run) + + setup_cmd._uninstall_openclaw(env_path=env_path, purge=False) + + assert calls + assert all(call[0] == "/opt/openclaw/bin/openclaw" for call in calls) + def test_claude_code_invalid_embedding_flag_exits(self) -> None: """``setup claude-code --embedding=opneai`` must fail fast on the typo.""" from reflexio.cli.commands.setup_cmd import claude_code_setup From 0aaff8953982de4b2281d38dd3b51f9835f992bb Mon Sep 17 00:00:00 2001 From: yilu331 Date: Wed, 20 May 2026 18:07:16 -0700 Subject: [PATCH 53/53] feat(openclaw): add npm install surface --- reflexio/integrations/openclaw/README.md | 24 +- .../npm/openclaw-smart/bin/openclaw-smart.js | 13 ++ .../openclaw/npm/openclaw-smart/package.json | 15 ++ .../integrations/openclaw/plugin/README.md | 37 ++- .../integrations/openclaw/plugin/package.json | 7 +- .../openclaw/plugin/scripts/npm-cli.js | 215 ++++++++++++++++++ .../plugin/tests-ts/test_npm_cli.test.ts | 56 +++++ 7 files changed, 361 insertions(+), 6 deletions(-) create mode 100755 reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js create mode 100644 reflexio/integrations/openclaw/npm/openclaw-smart/package.json create mode 100755 reflexio/integrations/openclaw/plugin/scripts/npm-cli.js create mode 100644 reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts diff --git a/reflexio/integrations/openclaw/README.md b/reflexio/integrations/openclaw/README.md index 34f290cd..db0172ed 100644 --- a/reflexio/integrations/openclaw/README.md +++ b/reflexio/integrations/openclaw/README.md @@ -14,11 +14,29 @@ silently when the backend is unreachable. ## Quick install +Guided Reflexio setup: + ```bash reflexio setup openclaw ``` -This walks you through: +Or install the OpenClaw plugin from npm: + +```bash +npx openclaw-smart install +``` + +The unscoped `openclaw-smart` npm package is a thin `npx` alias around the +scoped plugin package, `@reflexioai/openclaw-smart`. + +If your OpenClaw version supports npm package specs in its plugin installer, +you can also use the native installer directly: + +```bash +openclaw plugins install @reflexioai/openclaw-smart +``` + +The guided `reflexio setup openclaw` command walks you through: 1. Picking an LLM provider and storage backend (SQLite by default). 2. Writing `OPENCLAW_BIN` + `OPENCLAW_SMART_USE_LOCAL_CLI=1` to @@ -32,7 +50,9 @@ This walks you through: the plugin loaded. `reflexio setup openclaw --uninstall [--purge]` reverses everything; -`--repair` re-runs only the first-run installer. +`--repair` re-runs only the first-run installer. The npm wrapper exposes +the same maintenance operations as `openclaw-smart uninstall [--purge]` +and `openclaw-smart repair`. ## How it works diff --git a/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js b/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js new file mode 100755 index 00000000..48e79172 --- /dev/null +++ b/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +// Unscoped npm alias for `npx openclaw-smart install`. + +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const cli = require.resolve("@reflexioai/openclaw-smart/scripts/npm-cli.js"); +const result = spawnSync(process.execPath, [cli, ...process.argv.slice(2)], { + stdio: "inherit", +}); + +process.exit(result.status ?? 1); diff --git a/reflexio/integrations/openclaw/npm/openclaw-smart/package.json b/reflexio/integrations/openclaw/npm/openclaw-smart/package.json new file mode 100644 index 00000000..94d1f795 --- /dev/null +++ b/reflexio/integrations/openclaw/npm/openclaw-smart/package.json @@ -0,0 +1,15 @@ +{ + "name": "openclaw-smart", + "version": "0.1.0", + "description": "npx installer alias for @reflexioai/openclaw-smart.", + "type": "module", + "bin": { + "openclaw-smart": "./bin/openclaw-smart.js" + }, + "files": [ + "bin/**/*.js" + ], + "dependencies": { + "@reflexioai/openclaw-smart": "0.1.0" + } +} diff --git a/reflexio/integrations/openclaw/plugin/README.md b/reflexio/integrations/openclaw/plugin/README.md index 3aea5fca..90cc5c9a 100644 --- a/reflexio/integrations/openclaw/plugin/README.md +++ b/reflexio/integrations/openclaw/plugin/README.md @@ -1,5 +1,38 @@ # openclaw-smart -openClaw plugin: cross-session memory via local reflexio backend. +openClaw plugin: cross-session memory via a local Reflexio backend. -See ../README.md for usage. See `docs/superpowers/specs/2026-05-19-openclaw-smart-design.md` in the parent repo for design. +## Install + +```bash +npx openclaw-smart install +``` + +The unscoped `openclaw-smart` package is a thin alias for this scoped plugin +package, `@reflexioai/openclaw-smart`. + +If your OpenClaw version supports npm package specs in `plugins install`, you +can install the plugin package directly: + +```bash +openclaw plugins install @reflexioai/openclaw-smart +``` + +For users who already have Reflexio installed, the guided setup remains: + +```bash +reflexio setup openclaw +``` + +## Commands + +```bash +openclaw-smart install +openclaw-smart repair +openclaw-smart uninstall +openclaw-smart uninstall --purge +``` + +`install` registers `reflexio-openclaw-smart` with OpenClaw, enables typed hook +access, writes `OPENCLAW_BIN` to `~/.reflexio/.env`, warms Python dependencies, +and verifies the plugin is loaded. diff --git a/reflexio/integrations/openclaw/plugin/package.json b/reflexio/integrations/openclaw/plugin/package.json index a65f942e..312c16f3 100644 --- a/reflexio/integrations/openclaw/plugin/package.json +++ b/reflexio/integrations/openclaw/plugin/package.json @@ -1,11 +1,14 @@ { - "name": "openclaw-smart", + "name": "@reflexioai/openclaw-smart", "version": "0.1.0", "description": "Self-improving openClaw plugin — learns from corrections across sessions via reflexio.", - "private": true, "type": "module", + "bin": { + "openclaw-smart": "./scripts/npm-cli.js" + }, "scripts": { "build": "tsc -p tsconfig.build.json", + "prepack": "npm run build", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit" diff --git a/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js b/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js new file mode 100755 index 00000000..cf2e117d --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js @@ -0,0 +1,215 @@ +#!/usr/bin/env node +// npm-facing installer for openclaw-smart. +// +// This intentionally mirrors the core non-interactive install steps from +// `reflexio setup openclaw` so npm and Python installs do not drift. + +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const PLUGIN_ID = "reflexio-openclaw-smart"; +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const PLUGIN_ROOT = resolve(SCRIPT_DIR, ".."); +const REFLEXIO_DIR = join(homedir(), ".reflexio"); +const REFLEXIO_ENV = join(REFLEXIO_DIR, ".env"); +const STALE_EXTENSION_DIR = join(homedir(), ".openclaw", "extensions", PLUGIN_ID); + +function usage() { + console.log(`openclaw-smart + +Usage: + openclaw-smart install + openclaw-smart uninstall [--purge] + openclaw-smart repair + +Install registers the bundled OpenClaw plugin, enables typed hook access, +writes OPENCLAW_BIN to ~/.reflexio/.env, warms dependencies, and verifies +that OpenClaw loaded the plugin.`); +} + +function fail(message, code = 1) { + console.error(`openclaw-smart: ${message}`); + process.exit(code); +} + +function run(argv, opts = {}) { + const result = spawnSync(argv[0], argv.slice(1), { + encoding: "utf8", + stdio: opts.stdio ?? "pipe", + ...opts, + }); + if (result.error) { + return { + status: 1, + stdout: result.stdout ?? "", + stderr: result.error.message, + }; + } + return { + status: result.status ?? 1, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + }; +} + +function findExecutable(name) { + const candidates = []; + if (process.env.PATH) { + for (const dir of process.env.PATH.split(process.platform === "win32" ? ";" : ":")) { + if (!dir) continue; + candidates.push(join(dir, name)); + if (process.platform === "win32") candidates.push(join(dir, `${name}.cmd`)); + if (process.platform === "win32") candidates.push(join(dir, `${name}.exe`)); + } + } + for (const candidate of candidates) { + if (existsSync(candidate)) return candidate; + } + return null; +} + +function resolveOpenClawBin() { + const configured = process.env.OPENCLAW_BIN; + if (configured && existsSync(configured)) return configured; + return findExecutable("openclaw"); +} + +function shellQuote(value) { + return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; +} + +function upsertEnv(envPath, updates) { + mkdirSync(dirname(envPath), { recursive: true }); + const existing = existsSync(envPath) ? readFileSync(envPath, "utf8").split(/\r?\n/) : []; + const remaining = existing.filter((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) return true; + return !Object.keys(updates).some((key) => trimmed.startsWith(`${key}=`)); + }); + for (const [key, value] of Object.entries(updates)) { + remaining.push(`${key}=${shellQuote(value)}`); + } + writeFileSync(envPath, `${remaining.filter((line) => line.length > 0).join("\n")}\n`); +} + +function removeEnvKeys(envPath, keys) { + if (!existsSync(envPath)) return; + const kept = readFileSync(envPath, "utf8") + .split(/\r?\n/) + .filter((line) => !keys.some((key) => line.trim().startsWith(`${key}=`))) + .filter((line) => line.length > 0); + writeFileSync(envPath, kept.length ? `${kept.join("\n")}\n` : ""); +} + +function ensurePluginRoot() { + if (!existsSync(join(PLUGIN_ROOT, "openclaw.plugin.json"))) { + fail(`plugin root is incomplete: ${PLUGIN_ROOT}`); + } +} + +function runOpenClaw(cli, args, opts = {}) { + return run([cli, ...args], opts); +} + +function printCommandFailure(label, result) { + const detail = (result.stderr || result.stdout || "").trim(); + console.error(`openclaw-smart: ${label} failed${detail ? `: ${detail}` : ""}`); +} + +function runSmartInstall() { + const script = join(PLUGIN_ROOT, "scripts", "smart-install.sh"); + if (!existsSync(script)) { + console.warn(`openclaw-smart: ${script} missing; skipping dependency warmup`); + return; + } + const result = run(["bash", script], { stdio: "inherit" }); + if (result.status !== 0) { + console.warn( + `openclaw-smart: smart-install.sh exited ${result.status}; first session may bootstrap dependencies`, + ); + } +} + +function inspectLoaded(cli) { + const result = runOpenClaw(cli, ["plugins", "inspect", PLUGIN_ID]); + return result.status === 0 && /Status:\s*loaded\b/.test(result.stdout); +} + +function install() { + ensurePluginRoot(); + const cli = resolveOpenClawBin(); + if (!cli) fail("openclaw CLI not found. Install OpenClaw first or set OPENCLAW_BIN."); + + upsertEnv(REFLEXIO_ENV, { + OPENCLAW_BIN: cli, + OPENCLAW_SMART_USE_LOCAL_CLI: "1", + }); + + runOpenClaw(cli, ["plugins", "uninstall", "--force", PLUGIN_ID]); + rmSync(STALE_EXTENSION_DIR, { recursive: true, force: true }); + + const installResult = runOpenClaw(cli, ["plugins", "install", PLUGIN_ROOT]); + if (installResult.status !== 0) { + printCommandFailure("plugins install", installResult); + process.exit(1); + } + + const enableResult = runOpenClaw(cli, ["plugins", "enable", PLUGIN_ID]); + if (enableResult.status !== 0) { + printCommandFailure("plugins enable", enableResult); + process.exit(1); + } + + const accessResult = runOpenClaw(cli, [ + "config", + "set", + `plugins.entries.${PLUGIN_ID}.hooks.allowConversationAccess`, + "true", + ]); + if (accessResult.status !== 0) { + printCommandFailure("config set allowConversationAccess", accessResult); + process.exit(1); + } + + runSmartInstall(); + runOpenClaw(cli, ["gateway", "restart"]); + + if (!inspectLoaded(cli)) { + fail(`plugin not loaded; check 'openclaw plugins inspect ${PLUGIN_ID}'`); + } + console.log("openclaw-smart installed and registered."); +} + +function uninstall({ purge = false } = {}) { + const cli = resolveOpenClawBin(); + if (cli) { + runOpenClaw(cli, ["plugins", "disable", PLUGIN_ID]); + runOpenClaw(cli, ["plugins", "uninstall", "--force", PLUGIN_ID]); + } else { + console.warn("openclaw-smart: openclaw CLI not found; skipping plugin removal"); + } + removeEnvKeys(REFLEXIO_ENV, ["OPENCLAW_BIN", "OPENCLAW_SMART_USE_LOCAL_CLI"]); + if (purge) rmSync(join(homedir(), ".openclaw-smart"), { recursive: true, force: true }); + console.log("openclaw-smart uninstalled."); +} + +function repair() { + runSmartInstall(); + console.log("openclaw-smart repair complete."); +} + +const [command, ...args] = process.argv.slice(2); +if (!command || command === "-h" || command === "--help") { + usage(); + process.exit(0); +} +if (command === "install") install(); +else if (command === "uninstall") uninstall({ purge: args.includes("--purge") }); +else if (command === "repair") repair(); +else { + usage(); + fail(`unknown command: ${command}`); +} diff --git a/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts b/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts new file mode 100644 index 00000000..73ff0be5 --- /dev/null +++ b/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { chmodSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +const CLI = resolve("scripts/npm-cli.js"); + +function runCli(args: string[], env: Record) { + return spawnSync(process.execPath, [CLI, ...args], { + cwd: resolve("."), + env: { ...process.env, ...env }, + encoding: "utf8", + }); +} + +describe("openclaw-smart npm CLI", () => { + it("prints help", () => { + const result = runCli(["--help"], {}); + expect(result.status).toBe(0); + expect(result.stdout).toContain("openclaw-smart install"); + }); + + it("installs through openclaw and persists OPENCLAW_BIN", () => { + const home = mkdtempSync(join(tmpdir(), "openclaw-smart-home-")); + const bin = mkdtempSync(join(tmpdir(), "openclaw-smart-bin-")); + const log = join(home, "openclaw.log"); + const fakeOpenClaw = join(bin, "openclaw"); + const fakeBash = join(bin, "bash"); + + writeFileSync( + fakeOpenClaw, + `#!/usr/bin/env sh\nprintf '%s\\n' "$*" >> "${log}"\nif [ "$1 $2" = "plugins inspect" ]; then printf 'Status: loaded\\n'; fi\nexit 0\n`, + ); + writeFileSync(fakeBash, "#!/usr/bin/env sh\nexit 0\n"); + chmodSync(fakeOpenClaw, 0o755); + chmodSync(fakeBash, 0o755); + + const result = runCli(["install"], { + HOME: home, + PATH: `${bin}:${process.env.PATH ?? ""}`, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("openclaw-smart installed and registered"); + expect(readFileSync(join(home, ".reflexio", ".env"), "utf8")).toContain( + `OPENCLAW_BIN="${fakeOpenClaw}"`, + ); + const calls = readFileSync(log, "utf8"); + expect(calls).toContain("plugins install"); + expect(calls).toContain( + "config set plugins.entries.reflexio-openclaw-smart.hooks.allowConversationAccess true", + ); + }); +});