Compare commits

..
2 Commits
Author SHA1 Message Date
Nicolò Boschi 1f397bc8fc feat: add comprehensive logging to upgrade tests
- Modify VersionRunner to write server logs to /tmp/upgrade-test-*.log files
- Add pytest hook to automatically dump server logs on test failure
- Add CI workflow step to show upgrade test logs (always runs)
- Improves debuggability when upgrade tests fail in CI

This addresses the issue where upgrade test failures in CI were
impossible to debug because API server logs were not visible.
2026-02-03 09:45:18 +01:00
Nicolò Boschi 6d982f9cea fix(sec): upgrade vulnerable deps 2026-02-02 14:24:32 +01:00
27 changed files with 710 additions and 3184 deletions
+3 -7
View File
@@ -749,9 +749,9 @@ jobs:
test-embed:
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
HINDSIGHT_EMBED_LLM_PROVIDER: groq
HINDSIGHT_EMBED_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_EMBED_LLM_MODEL: openai/gpt-oss-20b
# Prefer CPU-only PyTorch in CI
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
@@ -782,10 +782,6 @@ jobs:
${{ runner.os }}-huggingface-embed-
${{ runner.os }}-huggingface-
- name: Run unit and integration tests
working-directory: ./hindsight-embed
run: uv run pytest tests/ -v
- name: Run smoke test
working-directory: ./hindsight-embed
run: ./test.sh
@@ -11,7 +11,6 @@ from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from pgvector.sqlalchemy import Vector
from sqlalchemy import text
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
@@ -24,21 +23,8 @@ depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema - create all tables from scratch."""
# Note: pgvector extension is installed globally BEFORE migrations run
# See migrations.py:run_migrations() - this ensures the extension is available
# to all schemas, not just the one being migrated
# We keep this here as a fallback for backwards compatibility
# This may fail if user lacks permissions, which is fine if extension already exists
try:
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
except Exception:
# Extension might already exist or user lacks permissions - verify it exists
conn = op.get_bind()
result = conn.execute(text("SELECT 1 FROM pg_extension WHERE extname = 'vector'")).fetchone()
if not result:
# Extension truly doesn't exist - re-raise the error
raise
# Enable required extensions
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
# Create banks table
op.create_table(
+1 -1
View File
@@ -1410,7 +1410,7 @@ def create_app(
poll_interval_ms=config.worker_poll_interval_ms,
max_retries=config.worker_max_retries,
schema=schema,
tenant_extension=memory._tenant_extension,
tenant_extension=getattr(memory, "_tenant_extension", None),
max_slots=config.worker_max_slots,
consolidation_max_slots=config.worker_consolidation_max_slots,
)
+110 -16
View File
@@ -1,10 +1,11 @@
"""
Daemon mode support for Hindsight API.
Provides idle timeout for running as a background daemon.
Provides idle timeout and lockfile management for running as a background daemon.
"""
import asyncio
import fcntl
import logging
import os
import sys
@@ -16,9 +17,8 @@ logger = logging.getLogger(__name__)
# Default daemon configuration
DEFAULT_DAEMON_PORT = 8888
DEFAULT_IDLE_TIMEOUT = 0 # 0 = no auto-exit (hindsight-embed passes its own timeout)
# Allow override via environment variable for profile-specific logs
DAEMON_LOG_PATH = Path(os.getenv("HINDSIGHT_API_DAEMON_LOG", str(Path.home() / ".hindsight" / "daemon.log")))
LOCKFILE_PATH = Path.home() / ".hindsight" / "daemon.lock"
DAEMON_LOG_PATH = Path.home() / ".hindsight" / "daemon.log"
class IdleTimeoutMiddleware:
@@ -58,27 +58,97 @@ class IdleTimeoutMiddleware:
os.kill(os.getpid(), signal.SIGTERM)
class DaemonLock:
"""
File-based lock to prevent multiple daemon instances.
Uses fcntl.flock for atomic locking on Unix systems.
"""
def __init__(self, lockfile: Path = LOCKFILE_PATH):
self.lockfile = lockfile
self._fd = None
def acquire(self) -> bool:
"""
Try to acquire the daemon lock.
Returns True if lock acquired, False if another daemon is running.
"""
self.lockfile.parent.mkdir(parents=True, exist_ok=True)
try:
self._fd = open(self.lockfile, "w")
fcntl.flock(self._fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
# Write PID for debugging
self._fd.write(str(os.getpid()))
self._fd.flush()
return True
except (IOError, OSError):
# Lock is held by another process
if self._fd:
self._fd.close()
self._fd = None
return False
def release(self):
"""Release the daemon lock."""
if self._fd:
try:
fcntl.flock(self._fd.fileno(), fcntl.LOCK_UN)
self._fd.close()
except Exception:
pass
finally:
self._fd = None
# Remove lockfile
try:
self.lockfile.unlink()
except Exception:
pass
def is_locked(self) -> bool:
"""Check if the lock is held by another process."""
if not self.lockfile.exists():
return False
try:
fd = open(self.lockfile, "r")
fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
# We got the lock, so no one else has it
fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
fd.close()
return False
except (IOError, OSError):
return True
def get_pid(self) -> int | None:
"""Get the PID of the daemon holding the lock."""
if not self.lockfile.exists():
return None
try:
with open(self.lockfile, "r") as f:
return int(f.read().strip())
except (ValueError, IOError):
return None
def daemonize():
"""
Fork the current process into a background daemon.
Uses double-fork technique to properly detach from terminal.
"""
# First fork - detach from parent
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as e:
sys.stderr.write(f"fork #1 failed: {e}\n")
sys.exit(1)
# First fork
pid = os.fork()
if pid > 0:
# Parent exits
sys.exit(0)
# Decouple from parent environment
os.chdir("/")
# Create new session
os.setsid()
os.umask(0)
# Second fork - prevent zombie
# Second fork to prevent zombie processes
pid = os.fork()
if pid > 0:
sys.exit(0)
@@ -111,3 +181,27 @@ def check_daemon_running(port: int = DEFAULT_DAEMON_PORT) -> bool:
return result == 0
except Exception:
return False
def stop_daemon(port: int = DEFAULT_DAEMON_PORT) -> bool:
"""Stop a running daemon by sending SIGTERM to the process."""
lock = DaemonLock()
pid = lock.get_pid()
if pid is None:
return False
try:
import signal
os.kill(pid, signal.SIGTERM)
# Wait for process to exit
for _ in range(50): # Wait up to 5 seconds
time.sleep(0.1)
try:
os.kill(pid, 0) # Check if process exists
except OSError:
return True # Process exited
return False
except OSError:
return False
@@ -459,11 +459,7 @@ class MemoryEngine(MemoryEngineInterface):
# Store operation validator extension (optional)
self._operation_validator = operation_validator
# Store tenant extension (always set, use default if none provided)
if tenant_extension is None:
from ..extensions.builtin.tenant import DefaultTenantExtension
tenant_extension = DefaultTenantExtension(config={})
# Store tenant extension (optional)
self._tenant_extension = tenant_extension
async def _validate_operation(self, validation_coro) -> None:
@@ -501,18 +497,22 @@ class MemoryEngine(MemoryEngineInterface):
Raises:
AuthenticationError: If authentication fails or request_context is missing when required.
"""
if self._tenant_extension is None:
_current_schema.set("public")
return "public"
from hindsight_api.extensions import AuthenticationError
if request_context is None:
raise AuthenticationError("RequestContext is required")
raise AuthenticationError("RequestContext is required when tenant extension is configured")
# For internal/background operations (e.g., worker tasks), skip extension authentication.
# The task was already authenticated at submission time, and execute_task sets _current_schema
# from the task's _schema field.
# from the task's _schema field. For public schema tasks, _current_schema keeps its default "public".
if request_context.internal:
return _current_schema.get()
# Authenticate through tenant extension (always set, may be default no-auth extension)
# Let AuthenticationError propagate - HTTP layer will convert to 401
tenant_context = await self._tenant_extension.authenticate(request_context)
_current_schema.set(tenant_context.schema_name)
@@ -939,34 +939,30 @@ class MemoryEngine(MemoryEngineInterface):
if not self.db_url:
raise ValueError("Database URL is required for migrations")
# Migrate all schemas from the tenant extension
# The tenant extension is the single source of truth for which schemas exist
logger.info("Running database migrations...")
try:
tenants = await self._tenant_extension.list_tenants()
if tenants:
logger.info(f"Running migrations on {len(tenants)} schema(s)...")
for tenant in tenants:
schema = tenant.schema
if schema:
try:
run_migrations(self.db_url, schema=schema)
except Exception as e:
logger.warning(f"Failed to migrate schema {schema}: {e}")
logger.info("Schema migrations completed")
# Use configured database schema for migrations (defaults to "public")
run_migrations(self.db_url, schema=get_config().database_schema)
# Ensure embedding column dimension matches the model's dimension
# This is done after migrations and after embeddings.initialize()
for tenant in tenants:
schema = tenant.schema
if schema:
try:
ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=schema)
except Exception as e:
logger.warning(f"Failed to ensure embedding dimension for schema {schema}: {e}")
except Exception as e:
logger.warning(f"Failed to run schema migrations: {e}")
# Migrate all existing tenant schemas (if multi-tenant)
if self._tenant_extension is not None:
try:
tenants = await self._tenant_extension.list_tenants()
if tenants:
logger.info(f"Running migrations on {len(tenants)} tenant schemas...")
for tenant in tenants:
schema = tenant.schema
if schema and schema != "public":
try:
run_migrations(self.db_url, schema=schema)
except Exception as e:
logger.warning(f"Failed to migrate tenant schema {schema}: {e}")
logger.info("Tenant schema migrations completed")
except Exception as e:
logger.warning(f"Failed to run tenant schema migrations: {e}")
# Ensure embedding column dimension matches the model's dimension
# This is done after migrations and after embeddings.initialize()
ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=get_config().database_schema)
logger.info(f"Connecting to PostgreSQL at {self.db_url}")
@@ -5,42 +5,6 @@ from hindsight_api.extensions.tenant import AuthenticationError, Tenant, TenantC
from hindsight_api.models import RequestContext
class DefaultTenantExtension(TenantExtension):
"""
Default single-tenant extension with no authentication.
This is the default extension used when no tenant extension is configured.
It provides single-tenant behavior using the configured schema from
HINDSIGHT_API_DATABASE_SCHEMA (defaults to 'public').
Features:
- No authentication required (passes all requests)
- Uses configured schema from environment
- Perfect for single-tenant deployments without auth
Configuration:
HINDSIGHT_API_DATABASE_SCHEMA=your-schema (optional, defaults to 'public')
This is automatically enabled by default. To use custom authentication,
configure a different tenant extension:
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
"""
def __init__(self, config: dict[str, str]):
super().__init__(config)
# Cache the schema at initialization for consistency
# Support explicit schema override via config, otherwise use environment
self._schema = config.get("schema", get_config().database_schema)
async def authenticate(self, context: RequestContext) -> TenantContext:
"""Return configured schema without any authentication."""
return TenantContext(schema_name=self._schema)
async def list_tenants(self) -> list[Tenant]:
"""Return configured schema for single-tenant setup."""
return [Tenant(schema=self._schema)]
class ApiKeyTenantExtension(TenantExtension):
"""
Built-in tenant extension that validates API key against an environment variable.
+20 -4
View File
@@ -27,6 +27,7 @@ from .config import DEFAULT_WORKERS, ENV_WORKERS, HindsightConfig, get_config
from .daemon import (
DEFAULT_DAEMON_PORT,
DEFAULT_IDLE_TIMEOUT,
DaemonLock,
IdleTimeoutMiddleware,
daemonize,
)
@@ -135,15 +136,30 @@ def main():
# Daemon mode handling
if args.daemon:
# Use port from args (may be custom for profiles)
if args.port == config.port: # No custom port specified
args.port = DEFAULT_DAEMON_PORT
# Use fixed daemon port
args.port = DEFAULT_DAEMON_PORT
args.host = "127.0.0.1" # Only bind to localhost for security
# Check if another daemon is already running
daemon_lock = DaemonLock()
if not daemon_lock.acquire():
print(f"Daemon already running (PID: {daemon_lock.get_pid()})", file=sys.stderr)
sys.exit(1)
# Fork into background
# No lockfile needed - port binding prevents duplicate daemons
daemonize()
# Re-acquire lock in child process
daemon_lock = DaemonLock()
if not daemon_lock.acquire():
sys.exit(1)
# Register cleanup to release lock
def release_lock():
daemon_lock.release()
atexit.register(release_lock)
# Print banner (not in daemon mode)
if not args.daemon:
print()
-75
View File
@@ -165,81 +165,6 @@ def run_migrations(
logger.debug("Migration advisory lock acquired")
try:
# Ensure pgvector extension is installed globally BEFORE schema migrations
# This is critical: the extension must exist database-wide before any schema
# migrations run, otherwise custom schemas won't have access to vector types
logger.debug("Checking pgvector extension availability...")
# First, check if extension already exists
ext_check = conn.execute(
text(
"SELECT extname, nspname FROM pg_extension e "
"JOIN pg_namespace n ON e.extnamespace = n.oid "
"WHERE extname = 'vector'"
)
).fetchone()
if ext_check:
# Extension exists - check if in correct schema
ext_schema = ext_check[1]
if ext_schema == "public":
logger.info("pgvector extension found in public schema - ready to use")
else:
# Extension in wrong schema - try to fix if we have permissions
logger.warning(
f"pgvector extension found in schema '{ext_schema}' instead of 'public'. "
f"Attempting to relocate..."
)
try:
conn.execute(text("DROP EXTENSION vector CASCADE"))
conn.execute(text("SET search_path TO public"))
conn.execute(text("CREATE EXTENSION vector"))
conn.commit()
logger.info("pgvector extension relocated to public schema")
except Exception as e:
# Failed to relocate - log but don't fail if extension exists somewhere
logger.warning(
f"Could not relocate pgvector extension to public schema: {e}. "
f"Continuing with extension in '{ext_schema}' schema."
)
conn.rollback()
else:
# Extension doesn't exist - try to install
logger.info("pgvector extension not found, attempting to install...")
try:
conn.execute(text("SET search_path TO public"))
conn.execute(text("CREATE EXTENSION vector"))
conn.commit()
logger.info("pgvector extension installed in public schema")
except Exception as e:
# Installation failed - this is only fatal if extension truly doesn't exist
# Check one more time in case another process installed it
conn.rollback()
ext_recheck = conn.execute(
text(
"SELECT nspname FROM pg_extension e "
"JOIN pg_namespace n ON e.extnamespace = n.oid "
"WHERE extname = 'vector'"
)
).fetchone()
if ext_recheck:
logger.warning(
f"Could not install pgvector extension (permission denied?), "
f"but extension exists in '{ext_recheck[0]}' schema. Continuing..."
)
else:
# Extension truly doesn't exist and we can't install it
logger.error(
f"pgvector extension is not installed and cannot be installed: {e}. "
f"Please ensure pgvector is installed by a database administrator. "
f"See: https://github.com/pgvector/pgvector#installation"
)
raise RuntimeError(
"pgvector extension is required but not installed. "
"Please install it with: CREATE EXTENSION vector;"
) from e
# Run migrations while holding the lock
_run_migrations_internal(database_url, script_location, schema=schema)
finally:
+8 -33
View File
@@ -176,7 +176,7 @@ def main():
nonlocal memory, poller
import uvicorn
from ..extensions import OperationValidatorExtension, TenantExtension, load_extension
from ..extensions import TenantExtension, load_extension
# Load tenant extension BEFORE creating MemoryEngine so it can
# set correct schema context during task execution. Without this,
@@ -184,12 +184,6 @@ def main():
# causing worker writes to land in the wrong schema.
tenant_extension = load_extension("TENANT", TenantExtension)
# Load operation validator so workers can record usage metering
# for async operations (e.g. refresh_mental_model after consolidation)
operation_validator = load_extension("OPERATION_VALIDATOR", OperationValidatorExtension)
if operation_validator:
logger.info(f"Loaded operation validator: {operation_validator.__class__.__name__}")
# Initialize MemoryEngine
# Workers use SyncTaskBackend because they execute tasks directly,
# they don't need to store tasks (they poll from DB)
@@ -197,7 +191,6 @@ def main():
run_migrations=False, # Workers don't run migrations
task_backend=SyncTaskBackend(),
tenant_extension=tenant_extension,
operation_validator=operation_validator,
)
await memory.initialize()
@@ -229,30 +222,15 @@ def main():
# Create the HTTP app for metrics/health
app = create_worker_app(poller, memory)
# Setup signal handlers for graceful shutdown using asyncio
# Setup signal handlers for graceful shutdown
shutdown_requested = asyncio.Event()
force_exit = False
loop = asyncio.get_event_loop()
def signal_handler(signum, frame):
print(f"\nReceived signal {signum}, initiating graceful shutdown...")
shutdown_requested.set()
def signal_handler():
nonlocal force_exit
if shutdown_requested.is_set():
# Second signal = force exit
print("\nReceived second signal, forcing immediate exit...")
force_exit = True
# Restore default handler so third signal kills process
loop.remove_signal_handler(signal.SIGINT)
loop.remove_signal_handler(signal.SIGTERM)
sys.exit(1)
else:
print("\nReceived shutdown signal, initiating graceful shutdown...")
print("(Press Ctrl+C again to force immediate exit)")
shutdown_requested.set()
# Use asyncio's signal handlers which work properly with the event loop
loop.add_signal_handler(signal.SIGINT, signal_handler)
loop.add_signal_handler(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Create uvicorn config and server
uvicorn_config = uvicorn.Config(
@@ -271,10 +249,7 @@ def main():
print(f"Worker started. Metrics available at http://{args.http_host}:{args.http_port}/metrics")
# Wait for shutdown signal
try:
await shutdown_requested.wait()
except KeyboardInterrupt:
print("\nReceived interrupt, initiating graceful shutdown...")
await shutdown_requested.wait()
# Graceful shutdown
print("Shutting down HTTP server...")
+10 -14
View File
@@ -72,9 +72,9 @@ class WorkerPoller:
executor: Async function to execute tasks (typically MemoryEngine.execute_task)
poll_interval_ms: Interval between polls when no tasks found (milliseconds)
max_retries: Maximum retry attempts before marking task as failed
schema: Database schema for single-tenant support (deprecated, use tenant_extension)
tenant_extension: Extension for dynamic multi-tenant discovery. If None, creates a
DefaultTenantExtension with the configured schema.
schema: Database schema for single-tenant support (ignored if tenant_extension is set)
tenant_extension: Extension for dynamic multi-tenant discovery. If set, list_tenants()
is called on each poll cycle to discover schemas dynamically.
max_slots: Maximum concurrent tasks per worker
consolidation_max_slots: Maximum concurrent consolidation tasks per worker
"""
@@ -84,13 +84,6 @@ class WorkerPoller:
self._poll_interval_ms = poll_interval_ms
self._max_retries = max_retries
self._schema = schema
# Always set tenant extension (use DefaultTenantExtension if none provided)
if tenant_extension is None:
from ..extensions.builtin.tenant import DefaultTenantExtension
# Pass schema parameter to DefaultTenantExtension if explicitly provided
config = {"schema": schema} if schema else {}
tenant_extension = DefaultTenantExtension(config=config)
self._tenant_extension = tenant_extension
self._max_slots = max_slots
self._consolidation_max_slots = consolidation_max_slots
@@ -107,11 +100,14 @@ class WorkerPoller:
async def _get_schemas(self) -> list[str | None]:
"""Get list of schemas to poll. Returns [None] for default schema (no prefix)."""
from ..config import DEFAULT_DATABASE_SCHEMA
if self._tenant_extension is not None:
from ..config import DEFAULT_DATABASE_SCHEMA
tenants = await self._tenant_extension.list_tenants()
# Convert default schema to None for SQL compatibility (no prefix), keep others as-is
return [t.schema if t.schema != DEFAULT_DATABASE_SCHEMA else None for t in tenants]
tenants = await self._tenant_extension.list_tenants()
# Convert default schema to None for SQL compatibility (no prefix), keep others as-is
return [t.schema if t.schema != DEFAULT_DATABASE_SCHEMA else None for t in tenants]
# Single schema mode
return [self._schema]
async def _get_available_slots(self) -> tuple[int, int]:
"""
@@ -26,12 +26,6 @@ export GEMINI_API_KEY="your-key"
# Option D: Groq (uses openai/gpt-oss-20b for memory extraction)
export GROQ_API_KEY="your-key"
# Option E: Claude Code (uses claude-sonnet-4-20250514, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
# Option F: OpenAI Codex (uses o3-mini, no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
```
**Step 2: Install the plugin**
@@ -47,7 +41,7 @@ openclaw gateway
```
The plugin will automatically:
- Start a local Hindsight daemon (port 9077)
- Start a local Hindsight daemon (port 8888)
- Capture conversations after each turn
- Inject relevant memories before agent responses
@@ -74,7 +68,6 @@ Optional settings in `~/.openclaw/openclaw.json`:
"hindsight-openclaw": {
"enabled": true,
"config": {
"apiPort": 9077,
"daemonIdleTimeout": 0,
"embedVersion": "latest"
}
@@ -85,7 +78,6 @@ Optional settings in `~/.openclaw/openclaw.json`:
```
**Options:**
- `apiPort` - Port for the openclaw profile daemon (default: `9077`)
- `daemonIdleTimeout` - Seconds before daemon shuts down from inactivity (default: `0` = never)
- `embedVersion` - hindsight-embed version (default: `"latest"`)
- `bankMission` - Custom context for the memory bank (optional)
@@ -94,14 +86,12 @@ Optional settings in `~/.openclaw/openclaw.json`:
The plugin auto-detects your LLM provider from these environment variables:
| Provider | Env Var | Default Model | Notes |
|----------|---------|---------------|-------|
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` | |
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` | |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` | |
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` | |
| Claude Code | `HINDSIGHT_API_LLM_PROVIDER=claude-code` | `claude-sonnet-4-20250514` | No API key needed |
| OpenAI Codex | `HINDSIGHT_API_LLM_PROVIDER=openai-codex` | `o3-mini` | No API key needed |
| Provider | Env Var | Default Model |
|----------|---------|---------------|
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` |
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` |
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` |
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
**Override with explicit config:**
@@ -143,32 +133,32 @@ Useful for shared memory across multiple OpenClaw instances or production deploy
View the daemon config that was written by the plugin:
```bash
cat ~/.hindsight/profiles/openclaw.env
cat ~/.hindsight/embed
```
This shows the LLM provider, model, port, and other settings the daemon is using.
This shows the LLM provider, model, and other settings the daemon is using.
### Check Daemon Status
```bash
# Check if daemon is running
uvx hindsight-embed@latest -p openclaw daemon status
uvx hindsight-embed@latest daemon status
# View daemon logs
tail -f ~/.hindsight/profiles/openclaw.log
tail -f ~/.hindsight/daemon.log
```
### Query Memories
```bash
# Search memories
uvx hindsight-embed@latest -p openclaw memory recall openclaw "user preferences"
uvx hindsight-embed@latest memory recall openclaw "user preferences"
# View recent memories
uvx hindsight-embed@latest -p openclaw memory list openclaw --limit 10
uvx hindsight-embed@latest memory list openclaw --limit 10
# Open web UI (uses openclaw profile's daemon)
uvx hindsight-embed@latest -p openclaw ui
# Open web UI
uvx hindsight-embed@latest ui
```
## Troubleshooting
@@ -186,40 +176,27 @@ openclaw plugins install @vectorize-io/hindsight-openclaw
### Daemon not starting
```bash
# Check daemon status (note: -p openclaw uses the openclaw profile)
uvx hindsight-embed@latest -p openclaw daemon status
# Check daemon status
uvx hindsight-embed@latest daemon status
# View logs for errors
tail -f ~/.hindsight/profiles/openclaw.log
tail -f ~/.hindsight/daemon.log
# Check configuration
cat ~/.hindsight/profiles/openclaw.env
# List all profiles
uvx hindsight-embed@latest profile list
cat ~/.hindsight/embed
```
### No API key error
Make sure you've set one of the provider API keys (or use a provider that doesn't require one):
Make sure you've set one of the provider API keys:
```bash
# Option 1: OpenAI
export OPENAI_API_KEY="sk-your-key"
# Option 2: Anthropic
# or
export ANTHROPIC_API_KEY="your-key"
# Option 3: Claude Code (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
# Option 4: OpenAI Codex (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
# Verify it's set
echo $OPENAI_API_KEY
# or
echo $HINDSIGHT_API_LLM_PROVIDER
```
### Verify it's working
@@ -231,8 +208,6 @@ tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
# Should see on startup:
# [Hindsight] ✓ Using provider: openai, model: gpt-4o-mini
# or
# [Hindsight] ✓ Using provider: claude-code, model: claude-sonnet-4-20250514
# Should see after conversations:
# [Hindsight] Retained X messages for session ...
+6 -107
View File
@@ -25,7 +25,7 @@ uvx hindsight-embed --help
## Quick Start
```bash
# Interactive setup (configures default profile)
# Interactive setup (recommended)
hindsight-embed configure
# Or set your LLM API key manually
@@ -38,25 +38,14 @@ hindsight-embed memory retain default "User prefers dark mode"
hindsight-embed memory recall default "What are user preferences?"
```
All commands use the "default" profile unless you specify a different one with `--profile` or `HINDSIGHT_EMBED_PROFILE`.
## Commands
### configure
Configure the default profile or create/update named profiles:
Interactive setup wizard:
```bash
# Interactive setup for default profile
hindsight-embed configure
# Create/update named profile with single command
hindsight-embed configure --profile my-app \
--env HINDSIGHT_EMBED_LLM_PROVIDER=openai \
--env HINDSIGHT_EMBED_LLM_API_KEY=sk-xxx
# Create/update named profile interactively
hindsight-embed configure --profile staging
```
This will:
@@ -105,27 +94,6 @@ List all memory banks:
hindsight-embed bank list
```
### profile
Manage configuration profiles:
```bash
# List all profiles with status
hindsight-embed profile list
# Show current active profile
hindsight-embed profile show
# Set active profile (persists across commands)
hindsight-embed profile set-active my-app
# Clear active profile (revert to default)
hindsight-embed profile set-active --none
# Delete a profile
hindsight-embed profile delete my-app
```
### daemon
Manage the background daemon:
@@ -149,7 +117,6 @@ Run `hindsight-embed configure` for a guided setup that saves to `~/.hindsight/e
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_EMBED_PROFILE` | Profile name to use (overrides active profile) | None (uses default profile) |
| `HINDSIGHT_EMBED_LLM_API_KEY` | LLM API key (or use `OPENAI_API_KEY`) | Required |
| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider (`openai`, `groq`, `google`, `ollama`) | `openai` |
| `HINDSIGHT_EMBED_LLM_MODEL` | LLM model | `gpt-4o-mini` |
@@ -180,82 +147,14 @@ hindsight-embed daemon start
**Note:** All banks share a single database. Bank isolation happens within the database via the `bank_id` parameter passed to CLI commands.
### Configuration Profiles
Profiles let you maintain multiple independent configurations (e.g., different API endpoints, LLM providers, or projects). Each profile runs its own daemon on a unique port (8889-9888).
**The Default Profile:**
When you run `hindsight-embed configure` without specifying a profile, it configures the "default" profile. This uses the backward-compatible configuration at `~/.hindsight/embed` and runs on port 8888.
**Creating Named Profiles:**
```bash
# Create a profile with single command
hindsight-embed configure --profile my-app \
--env HINDSIGHT_EMBED_LLM_PROVIDER=openai \
--env HINDSIGHT_EMBED_LLM_API_KEY=sk-xxx \
--env HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini
# Create a profile interactively
hindsight-embed configure --profile staging
```
**Using Profiles:**
```bash
# Option 1: Environment variable (recommended for apps)
HINDSIGHT_EMBED_PROFILE=my-app hindsight-embed memory retain default "text"
# Option 2: CLI flag
hindsight-embed --profile my-app memory recall default "query"
# Option 3: Set as active (persists across commands)
hindsight-embed profile set-active my-app
hindsight-embed memory recall default "query" # Uses my-app profile
# Clear active profile (revert to default)
hindsight-embed profile set-active --none
```
**Profile Management:**
```bash
# List all profiles with status
hindsight-embed profile list
# Show active profile
hindsight-embed profile show
# Delete a profile
hindsight-embed profile delete my-app
```
**Profile Resolution Priority:**
1. `HINDSIGHT_EMBED_PROFILE` environment variable (highest)
2. `--profile` CLI flag
3. Active profile from `~/.hindsight/active_profile` file
4. Default profile (lowest)
**Note:** If a profile is specified but doesn't exist, the command will fail with an error. Profiles must be explicitly created using `hindsight-embed configure --profile <name>`.
### Files
**Default Profile:**
| Path | Description |
|------|-------------|
| `~/.hindsight/embed` | Configuration file for default profile |
| `~/.hindsight/daemon.log` | Daemon logs for default profile |
| `~/.hindsight/daemon.lock` | Daemon lock file (PID) for default profile |
**Named Profiles:**
| Path | Description |
|------|-------------|
| `~/.hindsight/profiles/<name>.env` | Configuration file for profile |
| `~/.hindsight/profiles/<name>.log` | Daemon logs for profile |
| `~/.hindsight/profiles/<name>.lock` | Daemon lock file (PID) for profile |
| `~/.hindsight/profiles/metadata.json` | Profile metadata (ports, timestamps) |
| `~/.hindsight/active_profile` | Active profile name (when set with `profile set-active`) |
| `~/.hindsight/embed` | Configuration file |
| `~/.hindsight/config.env` | Alternative config file location |
| `~/.hindsight/daemon.log` | Daemon logs |
| `~/.hindsight/daemon.lock` | Daemon lock file (PID) |
## Use with AI Coding Assistants
File diff suppressed because it is too large Load Diff
+74 -338
View File
@@ -6,65 +6,21 @@ Handles daemon lifecycle (start if needed) and API requests via the Python clien
import logging
import os
import re
import shlex
import subprocess
import time
from pathlib import Path
import httpx # Used only for health check
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from .profile_manager import ProfileManager, resolve_active_profile
console = Console(stderr=True)
logger = logging.getLogger(__name__)
# Suppress noisy httpx logs
logging.getLogger("httpx").setLevel(logging.WARNING)
# Default port for default profile
DEFAULT_DAEMON_PORT = 8888
DAEMON_PORT = DEFAULT_DAEMON_PORT # Backward compatibility
DAEMON_PORT = 8888
DAEMON_URL = f"http://127.0.0.1:{DAEMON_PORT}"
DAEMON_STARTUP_TIMEOUT = 180 # seconds - needs to be long for first run (downloads dependencies)
# Default idle timeout: 5 minutes - users can override with HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT env var
DEFAULT_DAEMON_IDLE_TIMEOUT = 300
def get_daemon_port(profile: str | None = None) -> int:
"""Get daemon port for a profile.
Args:
profile: Profile name (None = resolve from priority).
Returns:
Port number for daemon.
"""
if profile is None:
profile = resolve_active_profile()
pm = ProfileManager()
paths = pm.resolve_profile_paths(profile)
return paths.port
def get_daemon_url(profile: str | None = None) -> str:
"""Get daemon URL for a profile.
Args:
profile: Profile name (None = resolve from priority).
Returns:
URL for daemon.
"""
port = get_daemon_port(profile)
return f"http://127.0.0.1:{port}"
# CLI paths - check multiple locations
CLI_INSTALL_DIRS = [
Path.home() / ".local" / "bin", # Standard location from get-cli installer
@@ -90,47 +46,25 @@ def _find_hindsight_api_command() -> list[str]:
return ["uvx", f"hindsight-api@{api_version}"]
def _is_daemon_running(profile: str | None = None) -> bool:
"""Check if daemon is running and responsive.
Args:
profile: Profile name (None = resolve from priority).
Returns:
True if daemon is running and responsive.
"""
daemon_url = get_daemon_url(profile)
def _is_daemon_running() -> bool:
"""Check if daemon is running and responsive."""
try:
with httpx.Client(timeout=2) as client:
response = client.get(f"{daemon_url}/health")
response = client.get(f"{DAEMON_URL}/health")
return response.status_code == 200
except Exception:
return False
def _start_daemon(config: dict, profile: str | None = None) -> bool:
def _start_daemon(config: dict) -> bool:
"""
Start the daemon in background.
Args:
config: Configuration dict with LLM settings.
profile: Profile name (None = resolve from priority).
Returns:
True if daemon started successfully.
Returns True if daemon started successfully.
"""
import sys
if profile is None:
profile = resolve_active_profile()
# Get profile-specific paths
pm = ProfileManager()
paths = pm.resolve_profile_paths(profile)
profile_label = f"profile '{profile}'" if profile else "default profile"
daemon_log = paths.log
port = paths.port
logger.info("Starting daemon...")
# Build environment with LLM config
env = os.environ.copy()
@@ -141,21 +75,14 @@ def _start_daemon(config: dict, profile: str | None = None) -> bool:
if config.get("llm_model"):
env["HINDSIGHT_API_LLM_MODEL"] = config["llm_model"]
# Use profile-specific pg0 database for isolation
# Use single shared pg0 database for all banks (banks are isolated within the database)
# Allow override via HINDSIGHT_EMBED_API_DATABASE_URL for external PostgreSQL
# (e.g. when running as root where embedded pg0 cannot use initdb)
if "HINDSIGHT_EMBED_API_DATABASE_URL" not in env:
# Sanitize profile name for use in database name (allow only alphanumeric, dash, underscore)
safe_profile = re.sub(r"[^a-zA-Z0-9_-]", "-", profile or "default")
env["HINDSIGHT_API_DATABASE_URL"] = f"pg0://hindsight-embed-{safe_profile}"
env["HINDSIGHT_API_DATABASE_URL"] = "pg0://hindsight-embed"
else:
# Pass through the embed-specific env var to the daemon as the standard API env var
env["HINDSIGHT_API_DATABASE_URL"] = env["HINDSIGHT_EMBED_API_DATABASE_URL"]
# Store database URL for display later
database_url = env["HINDSIGHT_API_DATABASE_URL"]
is_pg0 = database_url.startswith("pg0://")
env["HINDSIGHT_API_LOG_LEVEL"] = "info"
# On macOS, force CPU for embeddings/reranker to avoid MPS/Metal/XPC issues in daemon mode
@@ -171,25 +98,23 @@ def _start_daemon(config: dict, profile: str | None = None) -> bool:
# Get idle timeout from environment or use default
idle_timeout = int(os.getenv("HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT", str(DEFAULT_DAEMON_IDLE_TIMEOUT)))
# Use profile-specific log file
daemon_log.parent.mkdir(parents=True, exist_ok=True)
cmd = _find_hindsight_api_command() + ["--daemon", "--idle-timeout", str(idle_timeout)]
# Tell hindsight-api daemon where to write its logs
env["HINDSIGHT_API_DAEMON_LOG"] = str(daemon_log)
# Create log directory
log_dir = Path.home() / ".hindsight"
log_dir.mkdir(parents=True, exist_ok=True)
daemon_log = log_dir / "daemon.log"
# Pass profile-specific port (no lockfile - we use port-based discovery)
cmd = _find_hindsight_api_command() + [
"--daemon",
"--idle-timeout",
str(idle_timeout),
"--port",
str(paths.port),
]
print(f"Starting daemon with command: {' '.join(cmd)}", file=sys.stderr)
print(f" Log file: {daemon_log}", file=sys.stderr)
try:
# Start daemon directly (hindsight-api handles its own log redirection via HINDSIGHT_API_DAEMON_LOG)
# Start daemon with shell redirection (works across forks)
# The >> appends to log file, 2>&1 redirects stderr to stdout
shell_cmd = f"{' '.join(cmd)} >> {shlex.quote(str(daemon_log))} 2>&1"
subprocess.Popen(
cmd,
shell_cmd,
shell=True,
env=env,
start_new_session=True,
)
@@ -200,253 +125,70 @@ def _start_daemon(config: dict, profile: str | None = None) -> bool:
# to detect failures - we must use the health check.
start_time = time.time()
last_check_time = start_time
last_log_position = 0 # Track position in log file for tailing
log_lines = [f"Starting daemon for {profile_label}...", ""] # Accumulate log lines for display
while time.time() - start_time < DAEMON_STARTUP_TIMEOUT:
if _is_daemon_running():
# Health check passed - but daemon might crash during initialization
# Wait a moment and verify it's still healthy (stability check)
print(" Daemon responding, verifying stability...", file=sys.stderr)
time.sleep(2)
if _is_daemon_running():
logger.info("Daemon started successfully")
return True
else:
# Daemon crashed after initial health check
print(" Daemon crashed during initialization", file=sys.stderr)
break
# Build title with profile and port info
if profile:
title = f"[bold cyan]Starting Daemon[/bold cyan] [dim]({profile} @ :{port})[/dim]"
else:
title = f"[bold cyan]Starting Daemon[/bold cyan] [dim](:{port})[/dim]"
# Periodically log progress
if time.time() - last_check_time > 5:
elapsed = int(time.time() - start_time)
print(f" Still waiting for daemon... ({elapsed}s elapsed)", file=sys.stderr)
last_check_time = time.time()
# Use Rich Live display for beautiful real-time updates
with Live(console=console, auto_refresh=False) as live:
# Show initial panel
content = Text("\n".join(log_lines), style="dim")
panel = Panel(
content,
title=title,
border_style="cyan",
padding=(1, 2),
)
live.update(panel)
live.refresh()
while time.time() - start_time < DAEMON_STARTUP_TIMEOUT:
# Tail daemon logs if available
if daemon_log.exists():
try:
with open(daemon_log, "r") as f:
f.seek(last_log_position)
new_lines = f.readlines()
last_log_position = f.tell()
# Add new log lines (keep last 4 for display)
for line in new_lines:
line = line.rstrip()
if line:
log_lines.append(line)
# Keep only last 4 lines
log_lines = log_lines[-4:]
except Exception:
pass # Silently ignore log read errors
if _is_daemon_running(profile):
# Health check passed - but daemon might crash during initialization
# Add status message to logs
log_lines.append("")
log_lines.append("✓ Daemon responding, verifying stability...")
# Update display with success status
content = Text("\n".join(log_lines), style="dim")
panel = Panel(
content,
title=title,
border_style="cyan",
padding=(1, 2),
)
live.update(panel)
live.refresh()
time.sleep(2)
if _is_daemon_running(profile):
log_lines.append("✓ Daemon started successfully!")
log_lines.append("")
log_lines.append(f"Logs: {daemon_log}")
# Show pg0 location if using pg0
if is_pg0:
# pg0 stores data in ~/.pg0/instances/<database_name>
pg0_name = database_url.replace("pg0://", "")
pg0_path = Path.home() / ".pg0" / "instances" / pg0_name
log_lines.append(f"Database: {pg0_path}")
content = Text("\n".join(log_lines), style="dim")
# Build success title with profile and port
if profile:
success_title = (
f"[bold green]✓ Daemon Started[/bold green] [dim]({profile} @ :{port})[/dim]"
)
else:
success_title = f"[bold green]✓ Daemon Started[/bold green] [dim](:{port})[/dim]"
panel = Panel(
content,
title=success_title,
border_style="green",
padding=(1, 2),
)
live.update(panel)
live.refresh()
console.print() # Add newline after panel
return True
else:
# Daemon crashed after initial health check
log_lines.append("")
log_lines.append("✗ Daemon crashed during initialization")
content = Text("\n".join(log_lines), style="dim")
# Build failure title with profile and port
if profile:
fail_title = f"[bold red]✗ Daemon Failed[/bold red] [dim]({profile} @ :{port})[/dim]"
else:
fail_title = f"[bold red]✗ Daemon Failed[/bold red] [dim](:{port})[/dim]"
panel = Panel(
content,
title=fail_title,
border_style="red",
padding=(1, 2),
)
live.update(panel)
live.refresh()
console.print()
break
# Periodically log progress
if time.time() - last_check_time > 3:
elapsed = int(time.time() - start_time)
# Update last status line or add new one
status_msg = f"⏳ Waiting for daemon... ({elapsed}s elapsed)"
if log_lines and log_lines[-1].startswith(""):
log_lines[-1] = status_msg
else:
log_lines.append(status_msg)
last_check_time = time.time()
# Update the live display
content = Text("\n".join(log_lines), style="dim")
panel = Panel(
content,
title=title,
border_style="cyan",
padding=(1, 2),
)
live.update(panel)
live.refresh()
time.sleep(0.5)
# Timeout - show failure
log_lines.append("")
log_lines.append("✗ Daemon failed to start (timeout)")
log_lines.append("")
log_lines.append(f"See full log: {daemon_log}")
content = Text("\n".join(log_lines), style="dim")
# Build timeout title with profile and port
if profile:
timeout_title = f"[bold red]✗ Daemon Failed (Timeout)[/bold red] [dim]({profile} @ :{port})[/dim]"
else:
timeout_title = f"[bold red]✗ Daemon Failed (Timeout)[/bold red] [dim](:{port})[/dim]"
panel = Panel(
content,
title=timeout_title,
border_style="red",
padding=(1, 2),
)
console.print(panel)
console.print()
time.sleep(0.5)
logger.error("Daemon failed to start")
# Show logs on failure
if daemon_log.exists():
log_content = daemon_log.read_text()
if log_content:
# Show last 3000 chars of log
print(f"\n Daemon log ({daemon_log}):", file=sys.stderr)
print(f"{log_content[-3000:]}", file=sys.stderr)
return False
except FileNotFoundError as e:
error_msg = f"Command not found: {cmd[0]}\nFull command: {' '.join(cmd)}\n\nInstall hindsight-api with: pip install hindsight-api"
error_panel = Panel(
Text(error_msg, style="red"),
title="[bold red]✗ Command Not Found[/bold red]",
border_style="red",
padding=(1, 2),
)
console.print(error_panel)
console.print()
print(f"Command not found: {cmd[0]}", file=sys.stderr)
print(f" Full command: {' '.join(cmd)}", file=sys.stderr)
logger.error("hindsight-api command not found. Install with: pip install hindsight-api")
return False
except Exception as e:
error_msg = f"Failed to start daemon: {e}\n\nCommand: {' '.join(cmd)}\nLog file: {daemon_log}"
error_panel = Panel(
Text(error_msg, style="red"),
title="[bold red]✗ Startup Error[/bold red]",
border_style="red",
padding=(1, 2),
)
console.print(error_panel)
console.print()
print(f"Failed to start daemon: {e}", file=sys.stderr)
logger.error(f"Failed to start daemon: {e}")
return False
def ensure_daemon_running(config: dict, profile: str | None = None) -> bool:
def ensure_daemon_running(config: dict) -> bool:
"""
Ensure daemon is running, starting it if needed.
Args:
config: Configuration dict with LLM settings.
profile: Profile name (None = resolve from priority).
Returns:
True if daemon is running.
Returns True if daemon is running.
"""
if profile is None:
profile = resolve_active_profile()
if _is_daemon_running(profile):
logger.debug(f"Daemon already running for profile '{profile or 'default'}'")
if _is_daemon_running():
logger.debug("Daemon already running")
return True
return _start_daemon(config, profile)
return _start_daemon(config)
def stop_daemon(profile: str | None = None) -> bool:
"""Stop the running daemon and wait for it to fully stop.
Args:
profile: Profile name (None = resolve from priority).
Returns:
True if daemon stopped successfully.
"""
import subprocess
if profile is None:
profile = resolve_active_profile()
# Check if daemon is actually running via health check
if not _is_daemon_running(profile):
logger.debug(f"Daemon not running for profile '{profile or 'default'}'")
return True
# Get profile-specific port
pm = ProfileManager()
paths = pm.resolve_profile_paths(profile)
port = paths.port
# Find PID by port using lsof (works on macOS/Linux, handles stale lockfiles)
try:
result = subprocess.run(
["lsof", "-ti", f":{port}", "-sTCP:LISTEN"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0 and result.stdout.strip():
pid = int(result.stdout.strip().split()[0])
logger.debug(f"Found daemon PID {pid} on port {port}")
# Send SIGTERM
os.kill(pid, 15)
def stop_daemon() -> bool:
"""Stop the running daemon and wait for it to fully stop."""
# Try to kill by PID from lockfile
lockfile = Path.home() / ".hindsight" / "daemon.lock"
if lockfile.exists():
try:
pid = int(lockfile.read_text().strip())
os.kill(pid, 15) # SIGTERM
# Wait for process to exit
for _ in range(50):
time.sleep(0.1)
@@ -454,18 +196,16 @@ def stop_daemon(profile: str | None = None) -> bool:
os.kill(pid, 0)
except OSError:
break # Process exited
else:
logger.warning(f"Could not find PID for port {port}")
except (subprocess.TimeoutExpired, ValueError, OSError, FileNotFoundError) as e:
logger.warning(f"Could not find/kill daemon by port: {e}")
except (ValueError, OSError):
pass
# Wait for health check to fail (daemon fully stopped)
for _ in range(30): # Wait up to 3 seconds
if not _is_daemon_running(profile):
if not _is_daemon_running():
return True
time.sleep(0.1)
return not _is_daemon_running(profile)
return not _is_daemon_running()
def find_cli_binary() -> Path | None:
@@ -554,7 +294,7 @@ def ensure_cli_installed() -> bool:
return install_cli()
def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
def run_cli(args: list[str], config: dict) -> int:
"""
Run the hindsight CLI with the given arguments.
@@ -563,7 +303,6 @@ def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
Args:
args: CLI arguments (e.g., ["memory", "retain", "bank", "content"])
config: Configuration dict with llm settings
profile: Profile name (None = resolve from priority)
Returns:
Exit code from CLI
@@ -571,9 +310,6 @@ def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
import subprocess
import sys
if profile is None:
profile = resolve_active_profile()
# Ensure CLI is installed
if not ensure_cli_installed():
return 1
@@ -591,10 +327,10 @@ def run_cli(args: list[str], config: dict, profile: str | None = None) -> int:
if not api_url:
# No external API specified - ensure our daemon is running
if not ensure_daemon_running(config, profile):
if not ensure_daemon_running(config):
print("Error: Failed to start daemon", file=sys.stderr)
return 1
api_url = get_daemon_url(profile)
api_url = DAEMON_URL
else:
# Using external API - skip daemon startup
logger.debug(f"Using external API at {api_url}")
@@ -1,463 +0,0 @@
"""Profile management for hindsight-embed.
Handles creation, deletion, and management of configuration profiles.
Each profile has its own config, daemon lock, log file, and port.
"""
import fcntl
import hashlib
import json
import os
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
import httpx
# Configuration paths
CONFIG_DIR = Path.home() / ".hindsight"
PROFILES_DIR = CONFIG_DIR / "profiles"
METADATA_FILE = PROFILES_DIR / "metadata.json"
ACTIVE_PROFILE_FILE = CONFIG_DIR / "active_profile"
# Port allocation
DEFAULT_PORT = 8888
PROFILE_PORT_BASE = 8889
PROFILE_PORT_RANGE = 1000 # 8889-9888
@dataclass
class ProfilePaths:
"""Paths and port for a profile."""
config: Path
lock: Path
log: Path
port: int
@dataclass
class ProfileInfo:
"""Profile information including metadata."""
name: str
port: int
created_at: str
last_used: Optional[str] = None
is_active: bool = False
daemon_running: bool = False
@dataclass
class ProfileMetadata:
"""Metadata for all profiles."""
version: int = 1
profiles: dict[str, dict] = field(default_factory=dict)
class ProfileManager:
"""Manages configuration profiles for hindsight-embed."""
def __init__(self):
"""Initialize the profile manager."""
self._ensure_directories()
def _ensure_directories(self):
"""Ensure profile directories exist."""
PROFILES_DIR.mkdir(parents=True, exist_ok=True)
def list_profiles(self) -> list[ProfileInfo]:
"""List all profiles with their status.
Returns:
List of ProfileInfo objects with daemon status.
"""
metadata = self._load_metadata()
active_profile = self.get_active_profile()
profiles = []
# Add default profile if config exists
default_config = CONFIG_DIR / "embed"
if default_config.exists():
profiles.append(
ProfileInfo(
name="", # Empty name = default
port=DEFAULT_PORT,
created_at="", # Don't track for default
last_used=None,
is_active=active_profile == "",
daemon_running=self._check_daemon_running(DEFAULT_PORT),
)
)
# Add named profiles
for name, info in metadata.profiles.items():
profiles.append(
ProfileInfo(
name=name,
port=info["port"],
created_at=info.get("created_at", ""),
last_used=info.get("last_used"),
is_active=active_profile == name,
daemon_running=self._check_daemon_running(info["port"]),
)
)
return sorted(profiles, key=lambda p: (p.name != "", p.name))
def profile_exists(self, name: str) -> bool:
"""Check if a profile exists.
Args:
name: Profile name (empty string for default).
Returns:
True if profile exists.
"""
if not name:
# Default profile exists if config file exists
return (CONFIG_DIR / "embed").exists()
# Named profile exists if config file exists
config_path = PROFILES_DIR / f"{name}.env"
return config_path.exists()
def get_profile(self, name: str) -> Optional[ProfileInfo]:
"""Get profile information.
Args:
name: Profile name (empty string for default).
Returns:
ProfileInfo if profile exists, None otherwise.
"""
profiles = self.list_profiles()
for profile in profiles:
if profile.name == name:
return profile
return None
def create_profile(self, name: str, port_or_config: int | dict[str, str], config: dict[str, str] | None = None):
"""Create or update a profile.
Args:
name: Profile name.
port_or_config: Port number (int) or configuration dict. For backward compatibility,
if this is a dict, it's treated as config and port is auto-allocated.
config: Configuration dict (KEY=VALUE pairs). Only used if port_or_config is an int.
Raises:
ValueError: If profile name is invalid or port is invalid.
"""
# Handle backward compatibility - allow (name, config) or (name, port, config)
if isinstance(port_or_config, dict):
# Called with (name, config) - auto-allocate port
port = None
config = port_or_config
else:
# Called with (name, port, config)
port = port_or_config
if config is None:
raise ValueError("Config must be provided when port is specified")
if not name:
raise ValueError("Profile name cannot be empty")
if not name.replace("-", "").replace("_", "").isalnum():
raise ValueError(f"Invalid profile name '{name}'. Use alphanumeric chars, hyphens, and underscores.")
if port is not None and (port < 1024 or port > 65535):
raise ValueError(f"Invalid port {port}. Must be between 1024-65535.")
# Ensure profile directory exists
self._ensure_directories()
# Load metadata to check if profile already exists
metadata = self._load_metadata()
# Determine port: use provided port, preserve existing, or allocate new
if port is None:
if name in metadata.profiles and "port" in metadata.profiles[name]:
port = metadata.profiles[name]["port"]
else:
port = self._allocate_port(name)
# Write config file
config_path = PROFILES_DIR / f"{name}.env"
config_lines = [f"{key}={value}" for key, value in config.items()]
config_path.write_text("\n".join(config_lines) + "\n")
# Update metadata (reuse metadata loaded earlier to avoid race conditions)
now_iso = datetime.now(timezone.utc).isoformat()
if name in metadata.profiles:
# Update existing profile
metadata.profiles[name]["last_used"] = now_iso
metadata.profiles[name]["port"] = port
else:
# Create new profile
metadata.profiles[name] = {
"port": port,
"created_at": now_iso,
"last_used": now_iso,
}
self._save_metadata(metadata)
def delete_profile(self, name: str):
"""Delete a profile.
Args:
name: Profile name.
Raises:
ValueError: If profile name is invalid or doesn't exist.
"""
if not name:
raise ValueError("Cannot delete default profile")
if not self.profile_exists(name):
raise ValueError(f"Profile '{name}' does not exist")
# Remove config file
config_path = PROFILES_DIR / f"{name}.env"
if config_path.exists():
config_path.unlink()
# Remove lock file
lock_path = PROFILES_DIR / f"{name}.lock"
if lock_path.exists():
lock_path.unlink()
# Remove log file
log_path = PROFILES_DIR / f"{name}.log"
if log_path.exists():
log_path.unlink()
# Update metadata
metadata = self._load_metadata()
if name in metadata.profiles:
del metadata.profiles[name]
self._save_metadata(metadata)
# Clear active profile if it was deleted
if self.get_active_profile() == name:
self.set_active_profile(None)
def set_active_profile(self, name: Optional[str]):
"""Set the active profile.
Args:
name: Profile name to activate, or None to clear.
Raises:
ValueError: If profile doesn't exist.
"""
if name and not self.profile_exists(name):
raise ValueError(f"Profile '{name}' does not exist")
if name:
ACTIVE_PROFILE_FILE.write_text(name)
else:
# Clear active profile
if ACTIVE_PROFILE_FILE.exists():
ACTIVE_PROFILE_FILE.unlink()
def get_active_profile(self) -> str:
"""Get the currently active profile name.
Returns:
Profile name, or empty string if no active profile.
"""
if ACTIVE_PROFILE_FILE.exists():
return ACTIVE_PROFILE_FILE.read_text().strip()
return ""
def resolve_profile_paths(self, name: str) -> ProfilePaths:
"""Resolve paths for a profile.
Args:
name: Profile name (empty string for default).
Returns:
ProfilePaths with config, lock, log, and port.
"""
if not name:
# Default profile
return ProfilePaths(
config=CONFIG_DIR / "embed",
lock=CONFIG_DIR / "daemon.lock",
log=CONFIG_DIR / "daemon.log",
port=DEFAULT_PORT,
)
# Named profile
metadata = self._load_metadata()
port = metadata.profiles.get(name, {}).get("port", self._allocate_port(name))
return ProfilePaths(
config=PROFILES_DIR / f"{name}.env",
lock=PROFILES_DIR / f"{name}.lock",
log=PROFILES_DIR / f"{name}.log",
port=port,
)
def _allocate_port(self, name: str) -> int:
"""Allocate a port for a profile using hash-based strategy.
Args:
name: Profile name.
Returns:
Port number (8889-9888).
"""
# Hash profile name to get consistent port
hash_val = int(hashlib.sha256(name.encode()).hexdigest(), 16)
port = PROFILE_PORT_BASE + (hash_val % PROFILE_PORT_RANGE)
# Check if port is already allocated in metadata
metadata = self._load_metadata()
allocated_ports = {info["port"] for info in metadata.profiles.values() if info.get("port")}
# If collision, find next available port
attempt = 0
while port in allocated_ports and attempt < PROFILE_PORT_RANGE:
port = PROFILE_PORT_BASE + ((hash_val + attempt) % PROFILE_PORT_RANGE)
attempt += 1
if attempt >= PROFILE_PORT_RANGE:
# Fallback: find first available port
for p in range(PROFILE_PORT_BASE, PROFILE_PORT_BASE + PROFILE_PORT_RANGE):
if p not in allocated_ports:
return p
raise RuntimeError("No available ports for profile")
return port
def _check_daemon_running(self, port: int) -> bool:
"""Check if daemon is running on a port.
Args:
port: Port number to check.
Returns:
True if daemon is responding.
"""
try:
with httpx.Client() as client:
response = client.get(f"http://127.0.0.1:{port}/health", timeout=1)
return response.status_code == 200
except Exception:
return False
def _load_metadata(self) -> ProfileMetadata:
"""Load profile metadata from disk.
Returns:
ProfileMetadata object.
"""
if not METADATA_FILE.exists():
return ProfileMetadata()
try:
with open(METADATA_FILE) as f:
data = json.load(f)
return ProfileMetadata(version=data.get("version", 1), profiles=data.get("profiles", {}))
except (json.JSONDecodeError, IOError) as e:
print(
f"Warning: Failed to load metadata: {e}. Using empty metadata.",
file=sys.stderr,
)
# Backup corrupted metadata
backup_path = METADATA_FILE.with_suffix(".json.bak")
if METADATA_FILE.exists():
METADATA_FILE.rename(backup_path)
return ProfileMetadata()
def _save_metadata(self, metadata: ProfileMetadata):
"""Save profile metadata to disk with file locking.
Args:
metadata: ProfileMetadata to save.
"""
self._ensure_directories()
# Use atomic write with temp file
temp_file = METADATA_FILE.with_suffix(".json.tmp")
with open(temp_file, "w") as f:
# Acquire exclusive lock
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
json.dump(
{"version": metadata.version, "profiles": metadata.profiles},
f,
indent=2,
)
f.flush()
os.fsync(f.fileno())
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
# Atomic rename
temp_file.rename(METADATA_FILE)
def resolve_active_profile() -> str:
"""Resolve which profile to use based on priority.
Priority (highest to lowest):
1. HINDSIGHT_EMBED_PROFILE environment variable
2. CLI --profile flag (from global context)
3. Active profile from file
4. Default (empty string)
Returns:
Profile name to use (empty string for default).
"""
# 1. Environment variable
if env_profile := os.getenv("HINDSIGHT_EMBED_PROFILE"):
return env_profile
# 2. CLI flag (set by caller before invoking commands)
from . import cli
if cli_profile := cli.get_cli_profile_override():
return cli_profile
# 3. Active profile file
pm = ProfileManager()
if active_profile := pm.get_active_profile():
return active_profile
# 4. Default
return ""
def validate_profile_exists(profile: str):
"""Validate that a profile exists, exit if not.
Args:
profile: Profile name to validate.
Exits:
If profile doesn't exist, prints error and exits.
"""
if not profile:
# Default profile - always valid
return
pm = ProfileManager()
if not pm.profile_exists(profile):
print(
f"Error: Profile '{profile}' not found.",
file=sys.stderr,
)
print(
f"Create it with: hindsight-embed configure --profile {profile}",
file=sys.stderr,
)
sys.exit(1)
-1
View File
@@ -10,7 +10,6 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27.0",
"rich>=13.0.0",
]
[project.scripts]
+53 -8
View File
@@ -2,7 +2,6 @@
#
# Smoke test for hindsight-embed CLI with daemon mode
# Tests retain and recall operations via the background daemon
# Verifies daemon lifecycle, memory retention, and recall functionality
#
set -e
@@ -17,8 +16,8 @@ if [ -f ~/.hindsight/config.env ]; then
source ~/.hindsight/config.env
fi
if [ -z "$HINDSIGHT_API_LLM_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
echo "Error: HINDSIGHT_API_LLM_API_KEY or OPENAI_API_KEY is required"
if [ -z "$HINDSIGHT_EMBED_LLM_API_KEY" ] && [ -z "$OPENAI_API_KEY" ]; then
echo "Error: HINDSIGHT_EMBED_LLM_API_KEY or OPENAI_API_KEY is required"
exit 1
fi
@@ -28,14 +27,60 @@ echo "Using bank ID: $BANK_ID"
echo "Script dir: $SCRIPT_DIR"
echo "API dir: $API_DIR"
# Debug: Check if hindsight CLI is available
echo ""
echo "Checking hindsight CLI availability..."
if command -v hindsight &> /dev/null; then
echo " hindsight CLI found at: $(which hindsight)"
else
echo " hindsight CLI not in PATH"
if [ -f ~/.local/bin/hindsight ]; then
echo " Found at ~/.local/bin/hindsight"
else
echo " Not found at ~/.local/bin/hindsight - attempting installation..."
# Try to install the CLI
if curl -fsSL https://hindsight.vectorize.io/get-cli | bash; then
echo " CLI installation completed"
if [ -f ~/.local/bin/hindsight ]; then
echo " CLI now available at ~/.local/bin/hindsight"
export PATH="$HOME/.local/bin:$PATH"
else
echo " WARNING: CLI still not found after installation"
ls -la ~/.local/bin/ 2>/dev/null || echo " ~/.local/bin does not exist"
fi
else
echo " WARNING: CLI installation failed"
fi
fi
fi
# Show environment info for debugging
echo ""
echo "Environment:"
echo " HINDSIGHT_API_LLM_PROVIDER: ${HINDSIGHT_API_LLM_PROVIDER:-not set}"
echo " HINDSIGHT_API_LLM_MODEL: ${HINDSIGHT_API_LLM_MODEL:-not set}"
echo " HINDSIGHT_API_LLM_API_KEY: ${HINDSIGHT_API_LLM_API_KEY:+set (hidden)}"
echo " Python: $(python3 --version 2>&1)"
echo " uv: $(uv --version 2>&1)"
echo " HINDSIGHT_EMBED_LLM_PROVIDER: ${HINDSIGHT_EMBED_LLM_PROVIDER:-not set}"
echo " HINDSIGHT_EMBED_LLM_MODEL: ${HINDSIGHT_EMBED_LLM_MODEL:-not set}"
echo " HINDSIGHT_EMBED_LLM_API_KEY: ${HINDSIGHT_EMBED_LLM_API_KEY:+set (hidden)}"
echo " PATH includes ~/.local/bin: $(echo $PATH | grep -q "$HOME/.local/bin" && echo yes || echo no)"
# Final check that CLI is available before proceeding
echo ""
echo "Final CLI check before tests..."
CLI_PATH=""
if [ -f ~/.local/bin/hindsight ]; then
CLI_PATH="$HOME/.local/bin/hindsight"
elif command -v hindsight &> /dev/null; then
CLI_PATH="$(which hindsight)"
fi
if [ -n "$CLI_PATH" ]; then
echo " CLI found at: $CLI_PATH"
echo " CLI version: $($CLI_PATH --version 2>&1 || echo 'unknown')"
else
echo " ERROR: hindsight CLI not found. Tests cannot proceed."
echo " The hindsight-embed package forwards commands to the hindsight CLI."
echo " Please ensure the CLI is installed or check the get-cli installer output above."
exit 1
fi
# Stop any existing daemon
echo ""
+3 -3
View File
@@ -96,7 +96,7 @@ class TestRunCli:
call_args = mock_subprocess_run.call_args
# Verify environment contains the local daemon URL
assert call_args.kwargs["env"]["HINDSIGHT_API_URL"] == daemon_client.get_daemon_url()
assert call_args.kwargs["env"]["HINDSIGHT_API_URL"] == daemon_client.DAEMON_URL
# Verify exit code
assert exit_code == 0
@@ -246,8 +246,8 @@ class TestStartDaemon:
# Start daemon (will fail health check, but we just want to verify env)
daemon_client._start_daemon(config)
# Verify the default database URL was set (profile-specific)
assert captured_env.get("HINDSIGHT_API_DATABASE_URL") == "pg0://hindsight-embed-default"
# Verify the default database URL was set
assert captured_env.get("HINDSIGHT_API_DATABASE_URL") == "pg0://hindsight-embed"
class TestIsDaemonRunning:
@@ -1,388 +0,0 @@
"""Integration tests for profile functionality."""
import json
import os
import re
import subprocess
from pathlib import Path
import pytest
def strip_ansi(text):
"""Remove ANSI escape sequences from text."""
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
return ansi_escape.sub("", text)
@pytest.fixture
def temp_home(tmp_path, monkeypatch):
"""Create a temporary home directory for integration tests."""
temp_home = tmp_path / "home"
temp_home.mkdir()
monkeypatch.setenv("HOME", str(temp_home))
return temp_home
@pytest.fixture
def hindsight_embed_cmd():
"""Get the hindsight-embed command."""
return ["uv", "run", "hindsight-embed"]
class TestProfileIntegration:
"""Integration tests for profile workflows."""
def test_create_and_list_profile(self, temp_home, hindsight_embed_cmd):
"""Test creating a profile and listing it."""
# Create profile
result = subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"test-app",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
"--env",
"HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini",
],
capture_output=True,
text=True,
env={**os.environ, "HOME": str(temp_home)},
)
assert result.returncode == 0
assert "Profile 'test-app' configured successfully" in result.stdout
# Verify profile file was created
profile_path = temp_home / ".hindsight" / "profiles" / "test-app.env"
assert profile_path.exists()
config_content = profile_path.read_text()
assert "HINDSIGHT_EMBED_LLM_PROVIDER=openai" in config_content
assert "HINDSIGHT_EMBED_LLM_API_KEY=sk-test" in config_content
# List profiles
result = subprocess.run(
hindsight_embed_cmd + ["profile", "list"],
capture_output=True,
text=True,
env={**os.environ, "HOME": str(temp_home)},
)
assert result.returncode == 0
assert "test-app" in result.stdout
assert "Port:" in result.stdout
def test_profile_show_default(self, temp_home, hindsight_embed_cmd):
"""Test showing the default profile."""
# Create default config
config_dir = temp_home / ".hindsight"
config_dir.mkdir(parents=True, exist_ok=True)
(config_dir / "embed").write_text("KEY=value")
result = subprocess.run(
hindsight_embed_cmd + ["profile", "show"],
capture_output=True,
text=True,
env={**os.environ, "HOME": str(temp_home)},
)
assert result.returncode == 0
output = strip_ansi(result.stdout)
assert "Active profile: default" in output
assert "Source: Default" in output
assert "Port: 8888" in output
def test_profile_show_with_env_var(self, temp_home, hindsight_embed_cmd):
"""Test profile resolution with HINDSIGHT_EMBED_PROFILE env var."""
# Create profile
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"test-app",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
],
capture_output=True,
env={**os.environ, "HOME": str(temp_home)},
)
# Show profile with env var
env = {**os.environ, "HOME": str(temp_home), "HINDSIGHT_EMBED_PROFILE": "test-app"}
result = subprocess.run(
hindsight_embed_cmd + ["profile", "show"], capture_output=True, text=True, env=env
)
assert result.returncode == 0
output = strip_ansi(result.stdout)
assert "Active profile: test-app" in output
assert "Source: HINDSIGHT_EMBED_PROFILE environment variable" in output
def test_set_active_profile(self, temp_home, hindsight_embed_cmd):
"""Test setting and using active profile."""
# Create profile
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"test-app",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
],
capture_output=True,
env={**os.environ, "HOME": str(temp_home)},
)
# Set as active
result = subprocess.run(
hindsight_embed_cmd + ["profile", "set-active", "test-app"],
capture_output=True,
text=True,
env={**os.environ, "HOME": str(temp_home)},
)
assert result.returncode == 0
output = strip_ansi(result.stdout)
assert "Active profile set to 'test-app'" in output
# Verify active_profile file
active_file = temp_home / ".hindsight" / "active_profile"
assert active_file.exists()
assert active_file.read_text() == "test-app"
# Show profile (should show as active)
result = subprocess.run(
hindsight_embed_cmd + ["profile", "show"],
capture_output=True,
text=True,
env={**os.environ, "HOME": str(temp_home)},
)
assert result.returncode == 0
output = strip_ansi(result.stdout)
assert "Active profile: test-app" in output
def test_delete_profile(self, temp_home, hindsight_embed_cmd):
"""Test deleting a profile."""
# Create profile
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"test-app",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
],
capture_output=True,
)
# Delete profile (with 'y' confirmation)
result = subprocess.run(
hindsight_embed_cmd + ["profile", "delete", "test-app"],
input="y\n",
capture_output=True,
text=True,
)
assert result.returncode == 0 or "deleted" in result.stdout.lower()
# Verify profile file is gone
profile_path = temp_home / ".hindsight" / "profiles" / "test-app.env"
# Note: Test may still have file if daemon was running and user cancelled
def test_multiple_profiles_different_ports(self, temp_home, hindsight_embed_cmd):
"""Test that multiple profiles get different ports."""
# Create first profile
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"app1",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
],
capture_output=True,
)
# Create second profile
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"app2",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
],
capture_output=True,
)
# Read metadata
metadata_path = temp_home / ".hindsight" / "profiles" / "metadata.json"
assert metadata_path.exists()
metadata = json.loads(metadata_path.read_text())
app1_port = metadata["profiles"]["app1"]["port"]
app2_port = metadata["profiles"]["app2"]["port"]
# Ports should be different
assert app1_port != app2_port
assert 8889 <= app1_port <= 9888
assert 8889 <= app2_port <= 9888
def test_profile_validation_fails_for_nonexistent(self, temp_home, hindsight_embed_cmd):
"""Test that using non-existent profile fails."""
# Try to use non-existent profile
env = {**os.environ, "HOME": str(temp_home), "HINDSIGHT_EMBED_PROFILE": "nonexistent"}
result = subprocess.run(
hindsight_embed_cmd + ["profile", "show"], capture_output=True, text=True, env=env
)
assert result.returncode == 1
assert "Profile 'nonexistent' not found" in result.stderr
def test_backward_compatibility_default_profile(self, temp_home, hindsight_embed_cmd):
"""Test that default profile works without any profile commands."""
# Create default config manually (simulating old behavior)
config_dir = temp_home / ".hindsight"
config_dir.mkdir(parents=True, exist_ok=True)
(config_dir / "embed").write_text(
"HINDSIGHT_EMBED_LLM_PROVIDER=openai\n"
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test\n"
"HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini\n"
)
# Show profile should work
result = subprocess.run(
hindsight_embed_cmd + ["profile", "show"],
capture_output=True,
text=True,
env={**os.environ, "HOME": str(temp_home)},
)
assert result.returncode == 0
output = strip_ansi(result.stdout)
assert "Active profile: default" in output
assert "Port: 8888" in output
def test_configure_without_profile_flag(self, temp_home, hindsight_embed_cmd):
"""Test that configure without --profile still works (backward compatibility)."""
# Configure without profile flag (should configure default)
# Use non-interactive mode by providing env vars
env = {
**os.environ,
"HOME": str(temp_home),
"HINDSIGHT_EMBED_LLM_PROVIDER": "openai",
"HINDSIGHT_EMBED_LLM_API_KEY": "sk-test",
"HINDSIGHT_EMBED_LLM_MODEL": "gpt-4o-mini",
}
result = subprocess.run(
hindsight_embed_cmd + ["configure"], capture_output=True, text=True, env=env
)
# Should succeed
assert result.returncode == 0
# Verify default config was created
config_path = temp_home / ".hindsight" / "embed"
assert config_path.exists()
def test_clear_active_profile(self, temp_home, hindsight_embed_cmd):
"""Test clearing the active profile."""
# Create and set active profile
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"test-app",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
],
capture_output=True,
)
subprocess.run(
hindsight_embed_cmd + ["profile", "set-active", "test-app"],
capture_output=True,
)
# Clear active profile
result = subprocess.run(
hindsight_embed_cmd + ["profile", "set-active", "--none"],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert "Active profile cleared" in result.stdout
# Verify active_profile file is gone
active_file = temp_home / ".hindsight" / "active_profile"
assert not active_file.exists()
def test_profile_port_persistence(self, temp_home, hindsight_embed_cmd):
"""Test that profile port is persistent across recreations."""
# Create profile
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"test-app",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=openai",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=sk-test",
],
capture_output=True,
)
# Get port
metadata_path = temp_home / ".hindsight" / "profiles" / "metadata.json"
metadata1 = json.loads(metadata_path.read_text())
port1 = metadata1["profiles"]["test-app"]["port"]
# Update profile (recreate with different config)
subprocess.run(
hindsight_embed_cmd
+ [
"configure",
"--profile",
"test-app",
"--env",
"HINDSIGHT_EMBED_LLM_PROVIDER=groq",
"--env",
"HINDSIGHT_EMBED_LLM_API_KEY=gsk-test",
],
capture_output=True,
)
# Get port again
metadata2 = json.loads(metadata_path.read_text())
port2 = metadata2["profiles"]["test-app"]["port"]
# Port should be the same
assert port1 == port2
@@ -1,405 +0,0 @@
"""Tests for profile_manager module."""
import json
import os
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
from hindsight_embed.profile_manager import (
CONFIG_DIR,
PROFILES_DIR,
ProfileInfo,
ProfileManager,
ProfilePaths,
resolve_active_profile,
validate_profile_exists,
)
@pytest.fixture
def temp_hindsight_dir(tmp_path, monkeypatch):
"""Create a temporary hindsight directory for tests."""
temp_config = tmp_path / ".hindsight"
temp_config.mkdir()
monkeypatch.setattr("hindsight_embed.profile_manager.CONFIG_DIR", temp_config)
monkeypatch.setattr("hindsight_embed.profile_manager.PROFILES_DIR", temp_config / "profiles")
monkeypatch.setattr(
"hindsight_embed.profile_manager.METADATA_FILE", temp_config / "profiles" / "metadata.json"
)
monkeypatch.setattr("hindsight_embed.profile_manager.ACTIVE_PROFILE_FILE", temp_config / "active_profile")
return temp_config
@pytest.fixture
def profile_manager(temp_hindsight_dir):
"""Create a ProfileManager with temp directory."""
return ProfileManager()
class TestProfileManager:
"""Tests for ProfileManager class."""
def test_create_profile_success(self, profile_manager, temp_hindsight_dir):
"""Test creating a new profile."""
config = {
"HINDSIGHT_EMBED_LLM_PROVIDER": "openai",
"HINDSIGHT_EMBED_LLM_API_KEY": "sk-test",
"HINDSIGHT_EMBED_LLM_MODEL": "gpt-4o-mini",
}
profile_manager.create_profile("test-profile", config)
# Verify config file was created
config_path = temp_hindsight_dir / "profiles" / "test-profile.env"
assert config_path.exists()
# Verify config contents
config_content = config_path.read_text()
assert "HINDSIGHT_EMBED_LLM_PROVIDER=openai" in config_content
assert "HINDSIGHT_EMBED_LLM_API_KEY=sk-test" in config_content
assert "HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini" in config_content
# Verify metadata was created
metadata_path = temp_hindsight_dir / "profiles" / "metadata.json"
assert metadata_path.exists()
metadata = json.loads(metadata_path.read_text())
assert "test-profile" in metadata["profiles"]
assert "port" in metadata["profiles"]["test-profile"]
assert "created_at" in metadata["profiles"]["test-profile"]
def test_create_profile_invalid_name(self, profile_manager):
"""Test creating profile with invalid name fails."""
config = {"KEY": "value"}
# Empty name
with pytest.raises(ValueError, match="Profile name cannot be empty"):
profile_manager.create_profile("", config)
# Invalid characters
with pytest.raises(ValueError, match="Invalid profile name"):
profile_manager.create_profile("test profile", config)
with pytest.raises(ValueError, match="Invalid profile name"):
profile_manager.create_profile("test@profile", config)
def test_profile_exists(self, profile_manager, temp_hindsight_dir):
"""Test checking if a profile exists."""
# Default profile doesn't exist initially
assert not profile_manager.profile_exists("")
# Create default config
(temp_hindsight_dir / "embed").write_text("KEY=value")
assert profile_manager.profile_exists("")
# Named profile doesn't exist
assert not profile_manager.profile_exists("test")
# Create named profile
profile_manager.create_profile("test", {"KEY": "value"})
assert profile_manager.profile_exists("test")
def test_delete_profile(self, profile_manager, temp_hindsight_dir):
"""Test deleting a profile."""
# Create profile
profile_manager.create_profile("test-profile", {"KEY": "value"})
assert profile_manager.profile_exists("test-profile")
# Delete profile
profile_manager.delete_profile("test-profile")
assert not profile_manager.profile_exists("test-profile")
# Verify all files are removed
profiles_dir = temp_hindsight_dir / "profiles"
assert not (profiles_dir / "test-profile.env").exists()
assert not (profiles_dir / "test-profile.lock").exists()
assert not (profiles_dir / "test-profile.log").exists()
# Verify metadata no longer contains profile
metadata_path = profiles_dir / "metadata.json"
if metadata_path.exists():
metadata = json.loads(metadata_path.read_text())
assert "test-profile" not in metadata.get("profiles", {})
def test_delete_nonexistent_profile(self, profile_manager):
"""Test deleting a non-existent profile fails."""
with pytest.raises(ValueError, match="does not exist"):
profile_manager.delete_profile("nonexistent")
def test_delete_default_profile_fails(self, profile_manager):
"""Test deleting default profile fails."""
with pytest.raises(ValueError, match="Cannot delete default profile"):
profile_manager.delete_profile("")
def test_set_active_profile(self, profile_manager, temp_hindsight_dir):
"""Test setting active profile."""
# Create a profile
profile_manager.create_profile("test-profile", {"KEY": "value"})
# Set as active
profile_manager.set_active_profile("test-profile")
# Verify active profile file was created
active_file = temp_hindsight_dir / "active_profile"
assert active_file.exists()
assert active_file.read_text() == "test-profile"
# Get active profile
assert profile_manager.get_active_profile() == "test-profile"
def test_clear_active_profile(self, profile_manager, temp_hindsight_dir):
"""Test clearing active profile."""
# Create and set active profile
profile_manager.create_profile("test-profile", {"KEY": "value"})
profile_manager.set_active_profile("test-profile")
assert profile_manager.get_active_profile() == "test-profile"
# Clear active profile
profile_manager.set_active_profile(None)
assert profile_manager.get_active_profile() == ""
# Verify file was removed
active_file = temp_hindsight_dir / "active_profile"
assert not active_file.exists()
def test_set_active_nonexistent_profile_fails(self, profile_manager):
"""Test setting non-existent profile as active fails."""
with pytest.raises(ValueError, match="does not exist"):
profile_manager.set_active_profile("nonexistent")
def test_list_profiles(self, profile_manager, temp_hindsight_dir):
"""Test listing profiles."""
# Initially no profiles
profiles = profile_manager.list_profiles()
assert len(profiles) == 0
# Create default config
(temp_hindsight_dir / "embed").write_text("KEY=value")
# List profiles - should show default
profiles = profile_manager.list_profiles()
assert len(profiles) == 1
assert profiles[0].name == ""
assert profiles[0].port == 8888
# Create named profiles
profile_manager.create_profile("profile1", {"KEY": "value"})
profile_manager.create_profile("profile2", {"KEY": "value"})
# List all profiles
profiles = profile_manager.list_profiles()
assert len(profiles) == 3
# Verify sorting (default first, then alphabetical)
assert profiles[0].name == ""
assert profiles[1].name == "profile1"
assert profiles[2].name == "profile2"
def test_list_profiles_with_active(self, profile_manager, temp_hindsight_dir):
"""Test listing profiles shows active status."""
profile_manager.create_profile("profile1", {"KEY": "value"})
profile_manager.create_profile("profile2", {"KEY": "value"})
# Set profile1 as active
profile_manager.set_active_profile("profile1")
# List profiles
profiles = profile_manager.list_profiles()
# Find profile1
profile1 = next(p for p in profiles if p.name == "profile1")
profile2 = next(p for p in profiles if p.name == "profile2")
assert profile1.is_active is True
assert profile2.is_active is False
def test_resolve_profile_paths_default(self, profile_manager, temp_hindsight_dir):
"""Test resolving paths for default profile."""
paths = profile_manager.resolve_profile_paths("")
assert paths.config == temp_hindsight_dir / "embed"
assert paths.lock == temp_hindsight_dir / "daemon.lock"
assert paths.log == temp_hindsight_dir / "daemon.log"
assert paths.port == 8888
def test_resolve_profile_paths_named(self, profile_manager, temp_hindsight_dir):
"""Test resolving paths for named profile."""
# Create profile first
profile_manager.create_profile("test-profile", {"KEY": "value"})
paths = profile_manager.resolve_profile_paths("test-profile")
assert paths.config == temp_hindsight_dir / "profiles" / "test-profile.env"
assert paths.lock == temp_hindsight_dir / "profiles" / "test-profile.lock"
assert paths.log == temp_hindsight_dir / "profiles" / "test-profile.log"
assert 8889 <= paths.port <= 9888 # Port in valid range
def test_port_allocation_deterministic(self, profile_manager):
"""Test that port allocation is deterministic for same profile name."""
profile_manager.create_profile("test1", {"KEY": "value"})
paths1 = profile_manager.resolve_profile_paths("test1")
# Delete and recreate
profile_manager.delete_profile("test1")
profile_manager.create_profile("test1", {"KEY": "value"})
paths2 = profile_manager.resolve_profile_paths("test1")
# Port should be the same
assert paths1.port == paths2.port
def test_port_allocation_unique(self, profile_manager):
"""Test that different profiles get different ports."""
profile_manager.create_profile("profile1", {"KEY": "value"})
profile_manager.create_profile("profile2", {"KEY": "value"})
paths1 = profile_manager.resolve_profile_paths("profile1")
paths2 = profile_manager.resolve_profile_paths("profile2")
# Ports should be different
assert paths1.port != paths2.port
def test_daemon_running_status(self, profile_manager):
"""Test that daemon running status is checked correctly."""
with patch("httpx.Client") as mock_client:
# Mock successful health check
mock_response = Mock()
mock_response.status_code = 200
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
profile_manager.create_profile("test", {"KEY": "value"})
profiles = profile_manager.list_profiles()
test_profile = next(p for p in profiles if p.name == "test")
assert test_profile.daemon_running is True
def test_update_existing_profile(self, profile_manager, temp_hindsight_dir):
"""Test updating an existing profile."""
# Create profile
profile_manager.create_profile("test", {"KEY": "old_value"})
# Update profile
profile_manager.create_profile("test", {"KEY": "new_value"})
# Verify config was updated
config_path = temp_hindsight_dir / "profiles" / "test.env"
config_content = config_path.read_text()
assert "KEY=new_value" in config_content
assert "KEY=old_value" not in config_content
def test_metadata_persistence(self, profile_manager, temp_hindsight_dir):
"""Test that metadata persists across ProfileManager instances."""
# Create profile with first manager
profile_manager.create_profile("test", {"KEY": "value"})
# Create new manager and verify it sees the profile
new_manager = ProfileManager()
assert new_manager.profile_exists("test")
profiles = new_manager.list_profiles()
test_profile = next(p for p in profiles if p.name == "test")
assert test_profile.port > 0
def test_delete_active_profile_clears_active_file(self, profile_manager, temp_hindsight_dir):
"""Test that deleting active profile clears the active_profile file."""
profile_manager.create_profile("test", {"KEY": "value"})
profile_manager.set_active_profile("test")
# Verify active
assert profile_manager.get_active_profile() == "test"
# Delete profile
profile_manager.delete_profile("test")
# Active profile should be cleared
assert profile_manager.get_active_profile() == ""
class TestResolveActiveProfile:
"""Tests for resolve_active_profile function."""
def test_priority_env_var(self, monkeypatch, profile_manager):
"""Test that HINDSIGHT_EMBED_PROFILE env var has highest priority."""
# Set up all possible sources
monkeypatch.setenv("HINDSIGHT_EMBED_PROFILE", "from-env")
profile_manager.create_profile("from-file", {"KEY": "value"})
profile_manager.set_active_profile("from-file")
# Import cli to set override
from hindsight_embed import cli
cli.set_cli_profile_override("from-flag")
# Env var should win
assert resolve_active_profile() == "from-env"
def test_priority_cli_flag(self, monkeypatch, profile_manager):
"""Test that CLI flag has second highest priority."""
# No env var
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
profile_manager.create_profile("from-file", {"KEY": "value"})
profile_manager.set_active_profile("from-file")
# Import cli to set override
from hindsight_embed import cli
cli.set_cli_profile_override("from-flag")
# CLI flag should win
assert resolve_active_profile() == "from-flag"
def test_priority_active_file(self, monkeypatch, profile_manager):
"""Test that active file has third priority."""
# No env var or CLI flag
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
from hindsight_embed import cli
cli.set_cli_profile_override(None)
profile_manager.create_profile("from-file", {"KEY": "value"})
profile_manager.set_active_profile("from-file")
# Active file should be used
assert resolve_active_profile() == "from-file"
def test_priority_default(self, monkeypatch, profile_manager):
"""Test that default is used when no sources are set."""
# No env var, CLI flag, or active file
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
from hindsight_embed import cli
cli.set_cli_profile_override(None)
# Default should be used
assert resolve_active_profile() == ""
class TestValidateProfileExists:
"""Tests for validate_profile_exists function."""
def test_validate_default_profile_always_passes(self):
"""Test that default profile always passes validation."""
# Default profile (empty string) should never fail
validate_profile_exists("") # Should not raise
def test_validate_existing_profile_passes(self, profile_manager):
"""Test that existing profile passes validation."""
profile_manager.create_profile("test", {"KEY": "value"})
validate_profile_exists("test") # Should not raise
def test_validate_nonexistent_profile_fails(self, profile_manager, capsys):
"""Test that non-existent profile fails validation."""
with pytest.raises(SystemExit) as exc_info:
validate_profile_exists("nonexistent")
assert exc_info.value.code == 1
# Check error message
captured = capsys.readouterr()
assert "Profile 'nonexistent' not found" in captured.err
assert "hindsight-embed configure --profile nonexistent" in captured.err
+2 -42
View File
@@ -5,15 +5,9 @@ Biomimetic long-term memory for [OpenClaw](https://openclaw.ai) using [Hindsight
## Quick Start
```bash
# 1. Configure your LLM provider for memory extraction
# Option A: OpenAI
# 1. Configure your LLM provider
export OPENAI_API_KEY="sk-your-key"
# Option B: Claude Code (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=claude-code
# Option C: OpenAI Codex (no API key needed)
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
openclaw config set 'agents.defaults.models."openai/gpt-4o-mini"' '{}'
# 2. Install and enable the plugin
openclaw plugins install @vectorize-io/hindsight-openclaw
@@ -30,40 +24,6 @@ For full documentation, configuration options, troubleshooting, and development
**[OpenClaw Integration Documentation](https://vectorize.io/hindsight/sdks/integrations/openclaw)**
## Development
To test local changes to the Hindsight package before publishing:
1. Add `embedPackagePath` to your plugin config in `~/.openclaw/openclaw.json`:
```json
{
"plugins": {
"entries": {
"hindsight-openclaw": {
"enabled": true,
"config": {
"embedPackagePath": "/path/to/hindsight-wt3/hindsight-embed"
}
}
}
}
}
```
2. The plugin will use `uv run --directory <path> hindsight-embed` instead of `uvx hindsight-embed@latest`
3. To use a specific profile for testing:
```bash
# Check daemon status
uvx hindsight-embed@latest -p openclaw daemon status
# View logs
tail -f ~/.hindsight/profiles/openclaw.log
# List profiles
uvx hindsight-embed@latest profile list
```
## Links
- [Hindsight Documentation](https://vectorize.io/hindsight)
@@ -27,8 +27,8 @@
},
"llmProvider": {
"type": "string",
"description": "LLM provider for Hindsight memory (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama', 'openai-codex', 'claude-code'). Takes priority over auto-detection but not over HINDSIGHT_API_LLM_PROVIDER env var.",
"enum": ["openai", "anthropic", "gemini", "groq", "ollama", "openai-codex", "claude-code"]
"description": "LLM provider for Hindsight memory (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama'). Takes priority over auto-detection but not over HINDSIGHT_API_LLM_PROVIDER env var.",
"enum": ["openai", "anthropic", "gemini", "groq", "ollama"]
},
"llmModel": {
"type": "string",
@@ -37,15 +37,6 @@
"llmApiKeyEnv": {
"type": "string",
"description": "Name of the env var holding the API key (e.g. 'MY_CUSTOM_KEY'). If not set, uses the standard env var for the chosen provider."
},
"embedPackagePath": {
"type": "string",
"description": "Local path to hindsight package for development (e.g. '/path/to/hindsight'). When set, uses 'uv run --directory <path>' instead of 'uvx hindsight-embed@latest'."
},
"apiPort": {
"type": "number",
"description": "Port for the openclaw profile daemon (default: 9077)",
"default": 9077
}
},
"additionalProperties": false
@@ -78,14 +69,6 @@
"llmApiKeyEnv": {
"label": "API Key Env Var",
"placeholder": "e.g. MY_CUSTOM_API_KEY (optional)"
},
"embedPackagePath": {
"label": "Local Package Path (Dev)",
"placeholder": "/path/to/hindsight (for local development)"
},
"apiPort": {
"label": "API Port",
"placeholder": "9077 (default)"
}
}
}
+24 -26
View File
@@ -16,28 +16,12 @@ export class HindsightClient {
private llmApiKey: string;
private llmModel?: string;
private embedVersion: string;
private embedPackagePath?: string;
constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion: string = 'latest', embedPackagePath?: string) {
constructor(llmProvider: string, llmApiKey: string, llmModel?: string, embedVersion: string = 'latest') {
this.llmProvider = llmProvider;
this.llmApiKey = llmApiKey;
this.llmModel = llmModel;
this.embedVersion = embedVersion || 'latest';
this.embedPackagePath = embedPackagePath;
}
/**
* Get the command prefix to run hindsight-embed (either local or from PyPI)
*/
private getEmbedCommandPrefix(): string {
if (this.embedPackagePath) {
// Local package: uv run --directory <path> hindsight-embed
return `uv run --directory ${this.embedPackagePath} hindsight-embed`;
} else {
// PyPI package: uvx hindsight-embed@version
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
return `uvx ${embedPackage}`;
}
}
setBankId(bankId: string): void {
@@ -50,11 +34,11 @@ export class HindsightClient {
}
const escapedMission = mission.replace(/'/g, "'\\''"); // Escape single quotes
const embedCmd = this.getEmbedCommandPrefix();
const cmd = `${embedCmd} --profile openclaw bank mission ${this.bankId} '${escapedMission}'`;
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const cmd = `uvx ${embedPackage} bank mission ${this.bankId} '${escapedMission}'`;
try {
const { stdout } = await execAsync(cmd);
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
console.log(`[Hindsight] Bank mission set: ${stdout.trim()}`);
} catch (error) {
// Don't fail if mission set fails - bank might not exist yet, will be created on first retain
@@ -62,15 +46,29 @@ export class HindsightClient {
}
}
private getEnv(): Record<string, string> {
const env: Record<string, string> = {
...process.env,
HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
};
if (this.llmModel) {
env.HINDSIGHT_EMBED_LLM_MODEL = this.llmModel;
}
return env;
}
async retain(request: RetainRequest): Promise<RetainResponse> {
const content = request.content.replace(/'/g, "'\\''"); // Escape single quotes
const docId = request.document_id || 'conversation';
const embedCmd = this.getEmbedCommandPrefix();
const cmd = `${embedCmd} --profile openclaw memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const cmd = `uvx ${embedPackage} memory retain ${this.bankId} '${content}' --doc-id '${docId}' --async`;
try {
const { stdout } = await execAsync(cmd);
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
console.log(`[Hindsight] Retained (async): ${stdout.trim()}`);
// Return a simple response
@@ -88,11 +86,11 @@ export class HindsightClient {
const query = request.query.replace(/'/g, "'\\''"); // Escape single quotes
const maxTokens = request.max_tokens || 1024;
const embedCmd = this.getEmbedCommandPrefix();
const cmd = `${embedCmd} --profile openclaw memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const cmd = `uvx ${embedPackage} memory recall ${this.bankId} '${query}' --output json --max-tokens ${maxTokens}`;
try {
const { stdout } = await execAsync(cmd);
const { stdout } = await execAsync(cmd, { env: this.getEnv() });
// Parse JSON output - returns { entities: {...}, results: [...] }
const response = JSON.parse(stdout);
@@ -15,7 +15,6 @@ export class HindsightEmbedManager {
private llmBaseUrl?: string;
private daemonIdleTimeout: number;
private embedVersion: string;
private embedPackagePath?: string;
constructor(
port: number,
@@ -24,12 +23,10 @@ export class HindsightEmbedManager {
llmModel?: string,
llmBaseUrl?: string,
daemonIdleTimeout: number = 0, // Default: never timeout
embedVersion: string = 'latest', // Default: latest
embedPackagePath?: string // Local path to hindsight package
embedVersion: string = 'latest' // Default: latest
) {
// Use the configured port (default: 9077 from config)
this.port = port;
this.baseUrl = `http://127.0.0.1:${port}`;
this.port = 8888; // hindsight-embed daemon uses same port as API
this.baseUrl = `http://127.0.0.1:8888`;
this.embedDir = join(homedir(), '.openclaw', 'hindsight-embed');
this.llmProvider = llmProvider;
this.llmApiKey = llmApiKey;
@@ -37,36 +34,21 @@ export class HindsightEmbedManager {
this.llmBaseUrl = llmBaseUrl;
this.daemonIdleTimeout = daemonIdleTimeout;
this.embedVersion = embedVersion || 'latest';
this.embedPackagePath = embedPackagePath;
}
/**
* Get the command to run hindsight-embed (either local or from PyPI)
*/
private getEmbedCommand(): string[] {
if (this.embedPackagePath) {
// Local package: uv run --directory <path> hindsight-embed
return ['uv', 'run', '--directory', this.embedPackagePath, 'hindsight-embed'];
} else {
// PyPI package: uvx hindsight-embed@version
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
return ['uvx', embedPackage];
}
}
async start(): Promise<void> {
console.log(`[Hindsight] Starting hindsight-embed daemon...`);
// Build environment variables using standard HINDSIGHT_API_LLM_* variables
// Build environment variables
const env: NodeJS.ProcessEnv = {
...process.env,
HINDSIGHT_API_LLM_PROVIDER: this.llmProvider,
HINDSIGHT_API_LLM_API_KEY: this.llmApiKey,
HINDSIGHT_EMBED_LLM_PROVIDER: this.llmProvider,
HINDSIGHT_EMBED_LLM_API_KEY: this.llmApiKey,
HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: this.daemonIdleTimeout.toString(),
};
if (this.llmModel) {
env['HINDSIGHT_API_LLM_MODEL'] = this.llmModel;
env['HINDSIGHT_EMBED_LLM_MODEL'] = this.llmModel;
}
// Pass through base URL for OpenAI-compatible providers (OpenRouter, etc.)
@@ -80,16 +62,16 @@ export class HindsightEmbedManager {
env['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
}
// Configure "openclaw" profile using hindsight-embed configure (non-interactive)
console.log('[Hindsight] Configuring "openclaw" profile...');
await this.configureProfile(env);
// Write env vars to ~/.hindsight/config.env for daemon persistence
await this.writeConfigEnv(env);
// Start hindsight-embed daemon with openclaw profile
const embedCmd = this.getEmbedCommand();
// Start hindsight-embed daemon (it manages itself)
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const startDaemon = spawn(
embedCmd[0],
[...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'start'],
'uvx',
[embedPackage, 'daemon', 'start'],
{
env,
stdio: 'pipe',
}
);
@@ -132,8 +114,8 @@ export class HindsightEmbedManager {
async stop(): Promise<void> {
console.log('[Hindsight] Stopping hindsight-embed daemon...');
const embedCmd = this.getEmbedCommand();
const stopDaemon = spawn(embedCmd[0], [...embedCmd.slice(1), 'daemon', '--profile', 'openclaw', 'stop'], {
const embedPackage = this.embedVersion ? `hindsight-embed@${this.embedVersion}` : 'hindsight-embed@latest';
const stopDaemon = spawn('uvx', [embedPackage, 'daemon', 'stop'], {
stdio: 'pipe',
});
@@ -190,60 +172,75 @@ export class HindsightEmbedManager {
}
}
private async configureProfile(env: NodeJS.ProcessEnv): Promise<void> {
// Build profile create command args with --merge, --port and --env flags
// Use --merge to allow updating existing profile
const createArgs = ['profile', 'create', 'openclaw', '--merge', '--port', this.port.toString()];
private async writeConfigEnv(env: NodeJS.ProcessEnv): Promise<void> {
const hindsightDir = join(homedir(), '.hindsight');
const embedConfigPath = join(hindsightDir, 'embed');
// Add all environment variables as --env flags
const envVars = [
'HINDSIGHT_API_LLM_PROVIDER',
'HINDSIGHT_API_LLM_MODEL',
'HINDSIGHT_API_LLM_API_KEY',
'HINDSIGHT_API_LLM_BASE_URL',
'HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT',
'HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU',
'HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU',
];
// Ensure directory exists
await fs.mkdir(hindsightDir, { recursive: true });
for (const envVar of envVars) {
if (env[envVar]) {
createArgs.push('--env', `${envVar}=${env[envVar]}`);
// Read existing config to preserve extra settings
let existingContent = '';
let extraSettings: string[] = [];
try {
existingContent = await fs.readFile(embedConfigPath, 'utf-8');
// Extract non-LLM settings (like FORCE_CPU flags)
const lines = existingContent.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') &&
!trimmed.startsWith('HINDSIGHT_EMBED_LLM_') &&
!trimmed.startsWith('HINDSIGHT_API_LLM_') &&
!trimmed.startsWith('HINDSIGHT_EMBED_BANK_ID') &&
!trimmed.startsWith('HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT')) {
extraSettings.push(line);
}
}
} catch {
// File doesn't exist yet, that's fine
}
// Run profile create command (non-interactive, overwrites if exists)
const embedCmd = this.getEmbedCommand();
const create = spawn(embedCmd[0], [...embedCmd.slice(1), ...createArgs], {
stdio: 'pipe',
});
// Build config file with header
const configLines: string[] = [
'# Hindsight Embed Configuration',
'# Generated by OpenClaw Hindsight plugin',
'',
];
let output = '';
create.stdout?.on('data', (data) => {
const text = data.toString();
output += text;
console.log(`[Hindsight] ${text.trim()}`);
});
// Add LLM config
if (env.HINDSIGHT_EMBED_LLM_PROVIDER) {
configLines.push(`HINDSIGHT_EMBED_LLM_PROVIDER=${env.HINDSIGHT_EMBED_LLM_PROVIDER}`);
}
if (env.HINDSIGHT_EMBED_LLM_MODEL) {
configLines.push(`HINDSIGHT_EMBED_LLM_MODEL=${env.HINDSIGHT_EMBED_LLM_MODEL}`);
}
if (env.HINDSIGHT_EMBED_LLM_API_KEY) {
configLines.push(`HINDSIGHT_EMBED_LLM_API_KEY=${env.HINDSIGHT_EMBED_LLM_API_KEY}`);
}
if (env.HINDSIGHT_API_LLM_BASE_URL) {
configLines.push(`HINDSIGHT_API_LLM_BASE_URL=${env.HINDSIGHT_API_LLM_BASE_URL}`);
}
if (env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT) {
configLines.push(`HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=${env.HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT}`);
}
create.stderr?.on('data', (data) => {
const text = data.toString();
output += text;
console.error(`[Hindsight] ${text.trim()}`);
});
// Add platform-specific config (macOS FORCE_CPU flags)
if (env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU) {
configLines.push(`HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU}`);
}
if (env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU) {
configLines.push(`HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=${env.HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU}`);
}
await new Promise<void>((resolve, reject) => {
create.on('exit', (code) => {
if (code === 0) {
console.log('[Hindsight] Profile "openclaw" configured successfully');
resolve();
} else {
reject(new Error(`Profile create failed with code ${code}: ${output}`));
}
});
// Add extra settings if they exist
if (extraSettings.length > 0) {
configLines.push('');
configLines.push('# Additional settings');
configLines.push(...extraSettings);
}
create.on('error', (error) => {
reject(error);
});
});
// Write to file
await fs.writeFile(embedConfigPath, configLines.join('\n') + '\n', 'utf-8');
console.log(`[Hindsight] Wrote config to ${embedConfigPath}`);
}
}
+18 -31
View File
@@ -35,8 +35,6 @@ const PROVIDER_DETECTION = [
{ name: 'gemini', keyEnv: 'GEMINI_API_KEY', defaultModel: 'gemini-2.5-flash' },
{ name: 'groq', keyEnv: 'GROQ_API_KEY', defaultModel: 'openai/gpt-oss-20b' },
{ name: 'ollama', keyEnv: '', defaultModel: 'llama3.2' },
{ name: 'openai-codex', keyEnv: '', defaultModel: 'gpt-5.2-codex' },
{ name: 'claude-code', keyEnv: '', defaultModel: 'claude-sonnet-4-5-20250929' },
];
function detectLLMConfig(pluginConfig?: PluginConfig): {
@@ -54,9 +52,7 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
// Priority 1: If provider is explicitly set via env var, use that
if (overrideProvider) {
// Providers that don't require an API key (use OAuth or local models)
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
if (!overrideKey && !noKeyRequired.includes(overrideProvider)) {
if (!overrideKey && overrideProvider !== 'ollama') {
throw new Error(
`HINDSIGHT_API_LLM_PROVIDER is set to "${overrideProvider}" but HINDSIGHT_API_LLM_API_KEY is not set.\n` +
`Please set: export HINDSIGHT_API_LLM_API_KEY=your-api-key`
@@ -85,9 +81,7 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
apiKey = process.env[providerInfo.keyEnv] || '';
}
// Providers that don't require an API key (use OAuth or local models)
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
if (!apiKey && !noKeyRequired.includes(pluginConfig.llmProvider)) {
if (!apiKey && pluginConfig.llmProvider !== 'ollama') {
const keySource = pluginConfig.llmApiKeyEnv || providerInfo?.keyEnv || 'unknown';
throw new Error(
`Plugin config llmProvider is set to "${pluginConfig.llmProvider}" but no API key found.\n` +
@@ -109,9 +103,8 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
for (const providerInfo of PROVIDER_DETECTION) {
const apiKey = providerInfo.keyEnv ? process.env[providerInfo.keyEnv] : '';
// Skip providers that don't use API keys in auto-detection (must be explicitly requested)
const noKeyRequired = ['ollama', 'openai-codex', 'claude-code'];
if (noKeyRequired.includes(providerInfo.name)) {
// Skip ollama in auto-detection (must be explicitly requested)
if (providerInfo.name === 'ollama') {
continue;
}
@@ -132,14 +125,11 @@ function detectLLMConfig(pluginConfig?: PluginConfig): {
`Option 1: Set a standard provider API key (auto-detect):\n` +
` export OPENAI_API_KEY=sk-your-key # Uses gpt-4o-mini\n` +
` export ANTHROPIC_API_KEY=your-key # Uses claude-3-5-haiku\n` +
` export GEMINI_API_KEY=your-key # Uses gemini-2.5-flash\n` +
` export GROQ_API_KEY=your-key # Uses openai/gpt-oss-20b\n\n` +
`Option 2: Use Codex or Claude Code (no API key needed):\n` +
` export HINDSIGHT_API_LLM_PROVIDER=openai-codex # Requires 'codex auth login'\n` +
` export HINDSIGHT_API_LLM_PROVIDER=claude-code # Requires Claude Code CLI\n\n` +
`Option 3: Set llmProvider in openclaw.json plugin config:\n` +
` export GEMINI_API_KEY=your-key # Uses gemini-2.0-flash-exp\n` +
` export GROQ_API_KEY=your-key # Uses llama-3.3-70b-versatile\n\n` +
`Option 2: Set llmProvider in openclaw.json plugin config:\n` +
` "llmProvider": "openai", "llmModel": "gpt-4o-mini"\n\n` +
`Option 4: Override with Hindsight-specific env vars:\n` +
`Option 3: Override with Hindsight-specific env vars:\n` +
` export HINDSIGHT_API_LLM_PROVIDER=openai\n` +
` export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n` +
` export HINDSIGHT_API_LLM_API_KEY=sk-your-key\n` +
@@ -157,7 +147,6 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
embedPort: config.embedPort || 0,
daemonIdleTimeout: config.daemonIdleTimeout !== undefined ? config.daemonIdleTimeout : 0,
embedVersion: config.embedVersion || 'latest',
embedPackagePath: config.embedPackagePath,
llmProvider: config.llmProvider,
llmModel: config.llmModel,
llmApiKeyEnv: config.llmApiKeyEnv,
@@ -189,9 +178,9 @@ export default function (api: MoltbotPluginAPI) {
}
console.log(`[Hindsight] Daemon idle timeout: ${pluginConfig.daemonIdleTimeout}s (0 = never timeout)`);
// Get API port from config (default: 9077)
const apiPort = pluginConfig.apiPort || 9077;
console.log(`[Hindsight] API Port: ${apiPort}`);
// Determine port
const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
console.log(`[Hindsight] Port: ${port}`);
// Initialize in background (non-blocking)
console.log('[Hindsight] Starting initialization in background...');
@@ -200,14 +189,13 @@ export default function (api: MoltbotPluginAPI) {
// Initialize embed manager
console.log('[Hindsight] Creating HindsightEmbedManager...');
embedManager = new HindsightEmbedManager(
apiPort,
port,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion,
pluginConfig.embedPackagePath
pluginConfig.embedVersion
);
// Start the embedded server
@@ -216,7 +204,7 @@ export default function (api: MoltbotPluginAPI) {
// Initialize client
console.log('[Hindsight] Creating HindsightClient...');
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
// Use openclaw bank
console.log(`[Hindsight] Using bank: ${BANK_NAME}`);
@@ -275,22 +263,21 @@ export default function (api: MoltbotPluginAPI) {
console.log('[Hindsight] Reinitializing daemon...');
const pluginConfig = getPluginConfig(api);
const llmConfig = detectLLMConfig(pluginConfig);
const apiPort = pluginConfig.apiPort || 9077;
const port = pluginConfig.embedPort || Math.floor(Math.random() * 10000) + 10000;
embedManager = new HindsightEmbedManager(
apiPort,
port,
llmConfig.provider,
llmConfig.apiKey,
llmConfig.model,
llmConfig.baseUrl,
pluginConfig.daemonIdleTimeout,
pluginConfig.embedVersion,
pluginConfig.embedPackagePath
pluginConfig.embedVersion
);
await embedManager.start();
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion, pluginConfig.embedPackagePath);
client = new HindsightClient(llmConfig.provider, llmConfig.apiKey, llmConfig.model, pluginConfig.embedVersion);
client.setBankId(BANK_NAME);
if (pluginConfig.bankMission) {
@@ -32,11 +32,9 @@ export interface PluginConfig {
embedPort?: number;
daemonIdleTimeout?: number; // Seconds before daemon shuts down (0 = never)
embedVersion?: string; // hindsight-embed version (default: "latest")
embedPackagePath?: string; // Local path to hindsight package (e.g. '/path/to/hindsight')
llmProvider?: string; // LLM provider override (e.g. 'openai', 'anthropic', 'gemini', 'groq', 'ollama')
llmModel?: string; // LLM model override (e.g. 'gpt-4o-mini', 'claude-3-5-haiku-20241022')
llmApiKeyEnv?: string; // Env var name holding the API key (e.g. 'MY_CUSTOM_KEY')
apiPort?: number; // Port for openclaw profile daemon (default: 9077)
}
export interface ServiceConfig {
Generated
+157 -161
View File
@@ -1688,7 +1688,6 @@ version = "0.4.7"
source = { editable = "hindsight-embed" }
dependencies = [
{ name = "httpx" },
{ name = "rich" },
]
[package.dev-dependencies]
@@ -1699,10 +1698,7 @@ dev = [
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "rich", specifier = ">=13.0.0" },
]
requires-dist = [{ name = "httpx", specifier = ">=0.27.0" }]
[package.metadata.requires-dev]
dev = [
@@ -1786,7 +1782,7 @@ wheels = [
[[package]]
name = "huggingface-hub"
version = "1.3.7"
version = "1.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -1800,9 +1796,9 @@ dependencies = [
{ name = "typer-slim" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/3f/352efd52136bfd8aa9280c6d4a445869226ae2ccd49ddad4f62e90cfd168/huggingface_hub-1.3.7.tar.gz", hash = "sha256:5f86cd48f27131cdbf2882699cbdf7a67dd4cbe89a81edfdc31211f42e4a5fd1", size = 627537 }
sdist = { url = "https://files.pythonhosted.org/packages/67/e9/2658cb9bc4c72a67b7f87650e827266139befaf499095883d30dabc4d49f/huggingface_hub-1.3.5.tar.gz", hash = "sha256:8045aca8ddab35d937138f3c386c6d43a275f53437c5c64cdc9aa8408653b4ed", size = 627456 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/89/bfbfde252d649fae8d5f09b14a2870e5672ed160c1a6629301b3e5302621/huggingface_hub-1.3.7-py3-none-any.whl", hash = "sha256:8155ce937038fa3d0cb4347d752708079bc85e6d9eb441afb44c84bcf48620d2", size = 536728 },
{ url = "https://files.pythonhosted.org/packages/f9/84/a579b95c46fe8e319f89dc700c087596f665141575f4dcf136aaa97d856f/huggingface_hub-1.3.5-py3-none-any.whl", hash = "sha256:fe332d7f86a8af874768452295c22cd3f37730fb2463cf6cc3295e26036f8ef9", size = 536675 },
]
[[package]]
@@ -1915,87 +1911,87 @@ wheels = [
[[package]]
name = "jiter"
version = "0.13.0"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847 }
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157 },
{ url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729 },
{ url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766 },
{ url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587 },
{ url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537 },
{ url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717 },
{ url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683 },
{ url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345 },
{ url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775 },
{ url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325 },
{ url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709 },
{ url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560 },
{ url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608 },
{ url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958 },
{ url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597 },
{ url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821 },
{ url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163 },
{ url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709 },
{ url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480 },
{ url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735 },
{ url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814 },
{ url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990 },
{ url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021 },
{ url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024 },
{ url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424 },
{ url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818 },
{ url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897 },
{ url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507 },
{ url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560 },
{ url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232 },
{ url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727 },
{ url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799 },
{ url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120 },
{ url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664 },
{ url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543 },
{ url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262 },
{ url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630 },
{ url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602 },
{ url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939 },
{ url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616 },
{ url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850 },
{ url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551 },
{ url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950 },
{ url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852 },
{ url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804 },
{ url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787 },
{ url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880 },
{ url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702 },
{ url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319 },
{ url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289 },
{ url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165 },
{ url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634 },
{ url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933 },
{ url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842 },
{ url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108 },
{ url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027 },
{ url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199 },
{ url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423 },
{ url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438 },
{ url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774 },
{ url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238 },
{ url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892 },
{ url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309 },
{ url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607 },
{ url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986 },
{ url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756 },
{ url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196 },
{ url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215 },
{ url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152 },
{ url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016 },
{ url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024 },
{ url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337 },
{ url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395 },
{ url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169 },
{ url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808 },
{ url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384 },
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768 },
{ url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435 },
{ url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548 },
{ url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915 },
{ url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966 },
{ url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047 },
{ url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835 },
{ url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587 },
{ url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492 },
{ url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046 },
{ url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392 },
{ url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096 },
{ url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899 },
{ url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070 },
{ url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 },
{ url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 },
{ url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 },
{ url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590 },
{ url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462 },
{ url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983 },
{ url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328 },
{ url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740 },
{ url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875 },
{ url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457 },
{ url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546 },
{ url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196 },
{ url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100 },
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 },
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 },
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 },
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 },
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 },
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 },
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 },
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 },
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 },
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 },
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 },
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 },
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 },
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 },
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 },
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 },
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 },
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 },
{ url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 },
{ url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 },
{ url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 },
{ url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 },
{ url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 },
{ url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 },
{ url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 },
{ url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 },
{ url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 },
{ url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 },
{ url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 },
{ url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 },
{ url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 },
{ url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 },
{ url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 },
{ url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 },
{ url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 },
{ url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 },
{ url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 },
{ url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 },
{ url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 },
{ url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 },
{ url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 },
{ url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 },
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 },
{ url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144 },
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877 },
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419 },
{ url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212 },
{ url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974 },
{ url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233 },
{ url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537 },
{ url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 },
]
[[package]]
@@ -2099,7 +2095,7 @@ wheels = [
[[package]]
name = "langchain-core"
version = "1.2.8"
version = "1.2.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonpatch" },
@@ -2111,9 +2107,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/75/cc/55bf57b83cbc164cbf84cbf0c5e4fb640d673546af131db70797b97b125b/langchain_core-1.2.8.tar.gz", hash = "sha256:76d933c3f4cfd8484d8131c39bf25f562e2df4d0d5fe3218e05ff773210713b6", size = 814506 }
sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/d4/37fef9639b701c1fb1eea9e68447b72d86852ca3dc3253cdfd9c0afe228d/langchain_core-1.2.8-py3-none-any.whl", hash = "sha256:c732301272d63cfbcd75d114540257678627878f11b87046241272a25ba12ea7", size = 495753 },
{ url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232 },
]
[[package]]
@@ -2130,7 +2126,7 @@ wheels = [
[[package]]
name = "langsmith"
version = "0.6.8"
version = "0.6.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -2143,9 +2139,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/15/35f49a0b2efd33002fdcb9a7b0bdb65d77e40b4739104ffe843a3479874a/langsmith-0.6.8.tar.gz", hash = "sha256:3a7eb7155f2839dc729a5aa5b0bfc4aa1cb617b09a2290cf77031041271a7cdf", size = 973475 }
sdist = { url = "https://files.pythonhosted.org/packages/66/bb/b8a196c9b9a7ca8b8845eaec7dbae35bcfcb0da3068794e76b29211eae2b/langsmith-0.6.7.tar.gz", hash = "sha256:d89c604a18fc606b7835d8e7924f7cdbe130ca2207bdff8f989590e50d65b802", size = 963940 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/2d/2389e65522ebeab17489df72b4fabcfc661fced8af178aa6c2bc3b9afff5/langsmith-0.6.8-py3-none-any.whl", hash = "sha256:d17da18aeef15fdb4c3baec348bad64056591d785629cd5ba4846fd93cab166b", size = 319165 },
{ url = "https://files.pythonhosted.org/packages/ec/9a/5bc17ea3c746363e73c11df5e89068fea1ed175ca9c00fdb6886efc35b21/langsmith-0.6.7-py3-none-any.whl", hash = "sha256:4bd4372b8bf724b86314f64644562b5598407614e04e74b536c09490d153bd61", size = 309369 },
]
[[package]]
@@ -2893,79 +2889,79 @@ wheels = [
[[package]]
name = "orjson"
version = "3.11.7"
version = "3.11.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992 }
sdist = { url = "https://files.pythonhosted.org/packages/70/a3/4e09c61a5f0c521cba0bb433639610ae037437669f1a4cbc93799e731d78/orjson-3.11.6.tar.gz", hash = "sha256:0a54c72259f35299fd033042367df781c2f66d10252955ca1efb7db309b954cb", size = 6175856 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664 },
{ url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344 },
{ url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404 },
{ url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677 },
{ url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950 },
{ url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756 },
{ url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812 },
{ url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444 },
{ url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609 },
{ url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918 },
{ url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998 },
{ url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802 },
{ url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828 },
{ url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941 },
{ url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245 },
{ url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545 },
{ url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224 },
{ url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154 },
{ url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548 },
{ url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000 },
{ url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686 },
{ url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812 },
{ url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440 },
{ url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386 },
{ url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853 },
{ url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130 },
{ url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818 },
{ url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923 },
{ url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007 },
{ url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089 },
{ url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390 },
{ url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189 },
{ url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106 },
{ url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363 },
{ url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007 },
{ url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667 },
{ url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832 },
{ url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373 },
{ url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307 },
{ url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695 },
{ url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099 },
{ url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806 },
{ url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914 },
{ url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986 },
{ url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045 },
{ url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391 },
{ url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188 },
{ url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097 },
{ url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364 },
{ url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076 },
{ url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705 },
{ url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855 },
{ url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386 },
{ url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295 },
{ url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720 },
{ url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152 },
{ url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814 },
{ url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997 },
{ url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985 },
{ url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038 },
{ url = "https://files.pythonhosted.org/packages/f3/fd/d6b0a36854179b93ed77839f107c4089d91cccc9f9ba1b752b6e3bac5f34/orjson-3.11.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e259e85a81d76d9665f03d6129e09e4435531870de5961ddcd0bf6e3a7fde7d7", size = 250029 },
{ url = "https://files.pythonhosted.org/packages/a3/bb/22902619826641cf3b627c24aab62e2ad6b571bdd1d34733abb0dd57f67a/orjson-3.11.6-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:52263949f41b4a4822c6b1353bcc5ee2f7109d53a3b493501d3369d6d0e7937a", size = 134518 },
{ url = "https://files.pythonhosted.org/packages/72/90/7a818da4bba1de711a9653c420749c0ac95ef8f8651cbc1dca551f462fe0/orjson-3.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6439e742fa7834a24698d358a27346bb203bff356ae0402e7f5df8f749c621a8", size = 137917 },
{ url = "https://files.pythonhosted.org/packages/59/0f/02846c1cac8e205cb3822dd8aa8f9114acda216f41fd1999ace6b543418d/orjson-3.11.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b81ffd68f084b4e993e3867acb554a049fa7787cc8710bbcc1e26965580d99be", size = 134923 },
{ url = "https://files.pythonhosted.org/packages/94/cf/aeaf683001b474bb3c3c757073a4231dfdfe8467fceaefa5bfd40902c99f/orjson-3.11.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5a5468e5e60f7ef6d7f9044b06c8f94a3c56ba528c6e4f7f06ae95164b595ec", size = 140752 },
{ url = "https://files.pythonhosted.org/packages/fc/fe/dad52d8315a65f084044a0819d74c4c9daf9ebe0681d30f525b0d29a31f0/orjson-3.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72c5005eb45bd2535632d4f3bec7ad392832cfc46b62a3021da3b48a67734b45", size = 144201 },
{ url = "https://files.pythonhosted.org/packages/36/bc/ab070dd421565b831801077f1e390c4d4af8bfcecafc110336680a33866b/orjson-3.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b14dd49f3462b014455a28a4d810d3549bf990567653eb43765cd847df09145", size = 142380 },
{ url = "https://files.pythonhosted.org/packages/e6/d8/4b581c725c3a308717f28bf45a9fdac210bca08b67e8430143699413ff06/orjson-3.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bb2c1ea30ef302f0f89f9bf3e7f9ab5e2af29dc9f80eb87aa99788e4e2d65", size = 145582 },
{ url = "https://files.pythonhosted.org/packages/5b/a2/09aab99b39f9a7f175ea8fa29adb9933a3d01e7d5d603cdee7f1c40c8da2/orjson-3.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:825e0a85d189533c6bff7e2fc417a28f6fcea53d27125c4551979aecd6c9a197", size = 147270 },
{ url = "https://files.pythonhosted.org/packages/b8/2f/5ef8eaf7829dc50da3bf497c7775b21ee88437bc8c41f959aa3504ca6631/orjson-3.11.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b04575417a26530637f6ab4b1f7b4f666eb0433491091da4de38611f97f2fcf3", size = 421222 },
{ url = "https://files.pythonhosted.org/packages/3b/b0/dd6b941294c2b5b13da5fdc7e749e58d0c55a5114ab37497155e83050e95/orjson-3.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b83eb2e40e8c4da6d6b340ee6b1d6125f5195eb1b0ebb7eac23c6d9d4f92d224", size = 155562 },
{ url = "https://files.pythonhosted.org/packages/8e/09/43924331a847476ae2f9a16bd6d3c9dab301265006212ba0d3d7fd58763a/orjson-3.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1f42da604ee65a6b87eef858c913ce3e5777872b19321d11e6fc6d21de89b64f", size = 147432 },
{ url = "https://files.pythonhosted.org/packages/5d/e9/d9865961081816909f6b49d880749dbbd88425afd7c5bbce0549e2290d77/orjson-3.11.6-cp311-cp311-win32.whl", hash = "sha256:5ae45df804f2d344cffb36c43fdf03c82fb6cd247f5faa41e21891b40dfbf733", size = 139623 },
{ url = "https://files.pythonhosted.org/packages/b4/f9/6836edb92f76eec1082919101eb1145d2f9c33c8f2c5e6fa399b82a2aaa8/orjson-3.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:f4295948d65ace0a2d8f2c4ccc429668b7eb8af547578ec882e16bf79b0050b2", size = 136647 },
{ url = "https://files.pythonhosted.org/packages/b3/0c/4954082eea948c9ae52ee0bcbaa2f99da3216a71bcc314ab129bde22e565/orjson-3.11.6-cp311-cp311-win_arm64.whl", hash = "sha256:314e9c45e0b81b547e3a1cfa3df3e07a815821b3dac9fe8cb75014071d0c16a4", size = 135327 },
{ url = "https://files.pythonhosted.org/packages/14/ba/759f2879f41910b7e5e0cdbd9cf82a4f017c527fb0e972e9869ca7fe4c8e/orjson-3.11.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6f03f30cd8953f75f2a439070c743c7336d10ee940da918d71c6f3556af3ddcf", size = 249988 },
{ url = "https://files.pythonhosted.org/packages/f0/70/54cecb929e6c8b10104fcf580b0cc7dc551aa193e83787dd6f3daba28bb5/orjson-3.11.6-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:af44baae65ef386ad971469a8557a0673bb042b0b9fd4397becd9c2dfaa02588", size = 134445 },
{ url = "https://files.pythonhosted.org/packages/f2/6f/ec0309154457b9ba1ad05f11faa4441f76037152f75e1ac577db3ce7ca96/orjson-3.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c310a48542094e4f7dbb6ac076880994986dda8ca9186a58c3cb70a3514d3231", size = 137708 },
{ url = "https://files.pythonhosted.org/packages/20/52/3c71b80840f8bab9cb26417302707b7716b7d25f863f3a541bcfa232fe6e/orjson-3.11.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8dfa7a5d387f15ecad94cb6b2d2d5f4aeea64efd8d526bfc03c9812d01e1cc0", size = 134798 },
{ url = "https://files.pythonhosted.org/packages/30/51/b490a43b22ff736282360bd02e6bded455cf31dfc3224e01cd39f919bbd2/orjson-3.11.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba8daee3e999411b50f8b50dbb0a3071dd1845f3f9a1a0a6fa6de86d1689d84d", size = 140839 },
{ url = "https://files.pythonhosted.org/packages/95/bc/4bcfe4280c1bc63c5291bb96f98298845b6355da2226d3400e17e7b51e53/orjson-3.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89d104c974eafd7436d7a5fdbc57f7a1e776789959a2f4f1b2eab5c62a339f4", size = 144080 },
{ url = "https://files.pythonhosted.org/packages/01/74/22970f9ead9ab1f1b5f8c227a6c3aa8d71cd2c5acd005868a1d44f2362fa/orjson-3.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e2e2456788ca5ea75616c40da06fc885a7dc0389780e8a41bf7c5389ba257b", size = 142435 },
{ url = "https://files.pythonhosted.org/packages/29/34/d564aff85847ab92c82ee43a7a203683566c2fca0723a5f50aebbe759603/orjson-3.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a42efebc45afabb1448001e90458c4020d5c64fbac8a8dc4045b777db76cb5a", size = 145631 },
{ url = "https://files.pythonhosted.org/packages/e7/ef/016957a3890752c4aa2368326ea69fa53cdc1fdae0a94a542b6410dbdf52/orjson-3.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71b7cbef8471324966c3738c90ba38775563ef01b512feb5ad4805682188d1b9", size = 147058 },
{ url = "https://files.pythonhosted.org/packages/56/cc/9a899c3972085645b3225569f91a30e221f441e5dc8126e6d060b971c252/orjson-3.11.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f8515e5910f454fe9a8e13c2bb9dc4bae4c1836313e967e72eb8a4ad874f0248", size = 421161 },
{ url = "https://files.pythonhosted.org/packages/21/a8/767d3fbd6d9b8fdee76974db40619399355fd49bf91a6dd2c4b6909ccf05/orjson-3.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:300360edf27c8c9bf7047345a94fddf3a8b8922df0ff69d71d854a170cb375cf", size = 155757 },
{ url = "https://files.pythonhosted.org/packages/ad/0b/205cd69ac87e2272e13ef3f5f03a3d4657e317e38c1b08aaa2ef97060bbc/orjson-3.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:caaed4dad39e271adfadc106fab634d173b2bb23d9cf7e67bd645f879175ebfc", size = 147446 },
{ url = "https://files.pythonhosted.org/packages/de/c5/dd9f22aa9f27c54c7d05cc32f4580c9ac9b6f13811eeb81d6c4c3f50d6b1/orjson-3.11.6-cp312-cp312-win32.whl", hash = "sha256:955368c11808c89793e847830e1b1007503a5923ddadc108547d3b77df761044", size = 139717 },
{ url = "https://files.pythonhosted.org/packages/23/a1/e62fc50d904486970315a1654b8cfb5832eb46abb18cd5405118e7e1fc79/orjson-3.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:2c68de30131481150073d90a5d227a4a421982f42c025ecdfb66157f9579e06f", size = 136711 },
{ url = "https://files.pythonhosted.org/packages/04/3d/b4fefad8bdf91e0fe212eb04975aeb36ea92997269d68857efcc7eb1dda3/orjson-3.11.6-cp312-cp312-win_arm64.whl", hash = "sha256:65dfa096f4e3a5e02834b681f539a87fbe85adc82001383c0db907557f666bfc", size = 135212 },
{ url = "https://files.pythonhosted.org/packages/ae/45/d9c71c8c321277bc1ceebf599bc55ba826ae538b7c61f287e9a7e71bd589/orjson-3.11.6-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e4ae1670caabb598a88d385798692ce2a1b2f078971b3329cfb85253c6097f5b", size = 249828 },
{ url = "https://files.pythonhosted.org/packages/ac/7e/4afcf4cfa9c2f93846d70eee9c53c3c0123286edcbeb530b7e9bd2aea1b2/orjson-3.11.6-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:2c6b81f47b13dac2caa5d20fbc953c75eb802543abf48403a4703ed3bff225f0", size = 134339 },
{ url = "https://files.pythonhosted.org/packages/40/10/6d2b8a064c8d2411d3d0ea6ab43125fae70152aef6bea77bb50fa54d4097/orjson-3.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:647d6d034e463764e86670644bdcaf8e68b076e6e74783383b01085ae9ab334f", size = 137662 },
{ url = "https://files.pythonhosted.org/packages/5a/50/5804ea7d586baf83ee88969eefda97a24f9a5bdba0727f73e16305175b26/orjson-3.11.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8523b9cc4ef174ae52414f7699e95ee657c16aa18b3c3c285d48d7966cce9081", size = 134626 },
{ url = "https://files.pythonhosted.org/packages/9e/2e/f0492ed43e376722bb4afd648e06cc1e627fc7ec8ff55f6ee739277813ea/orjson-3.11.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313dfd7184cde50c733fc0d5c8c0e2f09017b573afd11dc36bd7476b30b4cb17", size = 140873 },
{ url = "https://files.pythonhosted.org/packages/10/15/6f874857463421794a303a39ac5494786ad46a4ab46d92bda6705d78c5aa/orjson-3.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905ee036064ff1e1fd1fb800055ac477cdcb547a78c22c1bc2bbf8d5d1a6fb42", size = 144044 },
{ url = "https://files.pythonhosted.org/packages/d2/c7/b7223a3a70f1d0cc2d86953825de45f33877ee1b124a91ca1f79aa6e643f/orjson-3.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce374cb98411356ba906914441fc993f271a7a666d838d8de0e0900dd4a4bc12", size = 142396 },
{ url = "https://files.pythonhosted.org/packages/87/e3/aa1b6d3ad3cd80f10394134f73ae92a1d11fdbe974c34aa199cc18bb5fcf/orjson-3.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cded072b9f65fcfd188aead45efa5bd528ba552add619b3ad2a81f67400ec450", size = 145600 },
{ url = "https://files.pythonhosted.org/packages/f6/cf/e4aac5a46cbd39d7e769ef8650efa851dfce22df1ba97ae2b33efe893b12/orjson-3.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ab85bdbc138e1f73a234db6bb2e4cc1f0fcec8f4bd2bd2430e957a01aadf746", size = 146967 },
{ url = "https://files.pythonhosted.org/packages/0b/04/975b86a4bcf6cfeda47aad15956d52fbeda280811206e9967380fa9355c8/orjson-3.11.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:351b96b614e3c37a27b8ab048239ebc1e0be76cc17481a430d70a77fb95d3844", size = 421003 },
{ url = "https://files.pythonhosted.org/packages/28/d1/0369d0baf40eea5ff2300cebfe209883b2473ab4aa4c4974c8bd5ee42bb2/orjson-3.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f9959c85576beae5cdcaaf39510b15105f1ee8b70d5dacd90152617f57be8c83", size = 155695 },
{ url = "https://files.pythonhosted.org/packages/ab/1f/d10c6d6ae26ff1d7c3eea6fd048280ef2e796d4fb260c5424fd021f68ecf/orjson-3.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75682d62b1b16b61a30716d7a2ec1f4c36195de4a1c61f6665aedd947b93a5d5", size = 147392 },
{ url = "https://files.pythonhosted.org/packages/8d/43/7479921c174441a0aa5277c313732e20713c0969ac303be9f03d88d3db5d/orjson-3.11.6-cp313-cp313-win32.whl", hash = "sha256:40dc277999c2ef227dcc13072be879b4cfd325502daeb5c35ed768f706f2bf30", size = 139718 },
{ url = "https://files.pythonhosted.org/packages/88/bc/9ffe7dfbf8454bc4e75bb8bf3a405ed9e0598df1d3535bb4adcd46be07d0/orjson-3.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0f6e9f8ff7905660bc3c8a54cd4a675aa98f7f175cf00a59815e2ff42c0d916", size = 136635 },
{ url = "https://files.pythonhosted.org/packages/6f/7e/51fa90b451470447ea5023b20d83331ec741ae28d1e6d8ed547c24e7de14/orjson-3.11.6-cp313-cp313-win_arm64.whl", hash = "sha256:1608999478664de848e5900ce41f25c4ecdfc4beacbc632b6fd55e1a586e5d38", size = 135175 },
{ url = "https://files.pythonhosted.org/packages/31/9f/46ca908abaeeec7560638ff20276ab327b980d73b3cc2f5b205b4a1c60b3/orjson-3.11.6-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6026db2692041d2a23fe2545606df591687787825ad5821971ef0974f2c47630", size = 249823 },
{ url = "https://files.pythonhosted.org/packages/ff/78/ca478089818d18c9cd04f79c43f74ddd031b63c70fa2a946eb5e85414623/orjson-3.11.6-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:132b0ab2e20c73afa85cf142e547511feb3d2f5b7943468984658f3952b467d4", size = 134328 },
{ url = "https://files.pythonhosted.org/packages/39/5e/cbb9d830ed4e47f4375ad8eef8e4fff1bf1328437732c3809054fc4e80be/orjson-3.11.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b376fb05f20a96ec117d47987dd3b39265c635725bda40661b4c5b73b77b5fde", size = 137651 },
{ url = "https://files.pythonhosted.org/packages/7c/3a/35df6558c5bc3a65ce0961aefee7f8364e59af78749fc796ea255bfa0cf5/orjson-3.11.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:954dae4e080574672a1dfcf2a840eddef0f27bd89b0e94903dd0824e9c1db060", size = 134596 },
{ url = "https://files.pythonhosted.org/packages/cd/8e/3d32dd7b7f26a19cc4512d6ed0ae3429567c71feef720fe699ff43c5bc9e/orjson-3.11.6-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe515bb89d59e1e4b48637a964f480b35c0a2676de24e65e55310f6016cca7ce", size = 140923 },
{ url = "https://files.pythonhosted.org/packages/6c/9c/1efbf5c99b3304f25d6f0d493a8d1492ee98693637c10ce65d57be839d7b/orjson-3.11.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:380f9709c275917af28feb086813923251e11ee10687257cd7f1ea188bcd4485", size = 144068 },
{ url = "https://files.pythonhosted.org/packages/82/83/0d19eeb5be797de217303bbb55dde58dba26f996ed905d301d98fd2d4637/orjson-3.11.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8173e0d3f6081e7034c51cf984036d02f6bab2a2126de5a759d79f8e5a140e7", size = 142493 },
{ url = "https://files.pythonhosted.org/packages/32/a7/573fec3df4dc8fc259b7770dc6c0656f91adce6e19330c78d23f87945d1e/orjson-3.11.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dddf9ba706294906c56ef5150a958317b09aa3a8a48df1c52ccf22ec1907eac", size = 145616 },
{ url = "https://files.pythonhosted.org/packages/c2/0e/23551b16f21690f7fd5122e3cf40fdca5d77052a434d0071990f97f5fe2f/orjson-3.11.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cbae5c34588dc79938dffb0b6fbe8c531f4dc8a6ad7f39759a9eb5d2da405ef2", size = 146951 },
{ url = "https://files.pythonhosted.org/packages/b8/63/5e6c8f39805c39123a18e412434ea364349ee0012548d08aa586e2bd6aa9/orjson-3.11.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f75c318640acbddc419733b57f8a07515e587a939d8f54363654041fd1f4e465", size = 421024 },
{ url = "https://files.pythonhosted.org/packages/1d/4d/724975cf0087f6550bd01fd62203418afc0ea33fd099aed318c5bcc52df8/orjson-3.11.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e0ab8d13aa2a3e98b4a43487c9205b2c92c38c054b4237777484d503357c8437", size = 155774 },
{ url = "https://files.pythonhosted.org/packages/a8/a3/f4c4e3f46b55db29e0a5f20493b924fc791092d9a03ff2068c9fe6c1002f/orjson-3.11.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f884c7fb1020d44612bd7ac0db0babba0e2f78b68d9a650c7959bf99c783773f", size = 147393 },
{ url = "https://files.pythonhosted.org/packages/ee/86/6f5529dd27230966171ee126cecb237ed08e9f05f6102bfaf63e5b32277d/orjson-3.11.6-cp314-cp314-win32.whl", hash = "sha256:8d1035d1b25732ec9f971e833a3e299d2b1a330236f75e6fd945ad982c76aaf3", size = 139760 },
{ url = "https://files.pythonhosted.org/packages/d3/b5/91ae7037b2894a6b5002fb33f4fbccec98424a928469835c3837fbb22a9b/orjson-3.11.6-cp314-cp314-win_amd64.whl", hash = "sha256:931607a8865d21682bb72de54231655c86df1870502d2962dbfd12c82890d077", size = 136633 },
{ url = "https://files.pythonhosted.org/packages/55/74/f473a3ec7a0a7ebc825ca8e3c86763f7d039f379860c81ba12dcdd456547/orjson-3.11.6-cp314-cp314-win_arm64.whl", hash = "sha256:fe71f6b283f4f1832204ab8235ce07adad145052614f77c876fcf0dac97bc06f", size = 135168 },
]
[[package]]
name = "packaging"
version = "26.0"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 },
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
@@ -3764,7 +3760,7 @@ wheels = [
[[package]]
name = "python-fasthtml"
version = "0.12.40"
version = "0.12.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
@@ -3778,9 +3774,9 @@ dependencies = [
{ name = "starlette" },
{ name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/03/5e/e01017b6214b18627df267e4bf0a6927b3e1d18bed025957012618d8fb69/python_fasthtml-0.12.40.tar.gz", hash = "sha256:5cfeb7d37a72cf59cbb1e328fa1f29dbdc428e8f8d5a7c41d9bb8d5443047e7b", size = 71572 }
sdist = { url = "https://files.pythonhosted.org/packages/b3/60/6b3b7ec0ab8054928b74cfa7ee49d90c3cde576268e054ba9f17989b7c1e/python_fasthtml-0.12.39.tar.gz", hash = "sha256:ae324b34a1586698b052cad8e9ff9255bd6cb486b01b946e95b9b63f220a5532", size = 70842 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/f2/fd91c0ed5a75b2f2b55348c3b9aa7382534744c223d8e6dd104164a087c7/python_fasthtml-0.12.40-py3-none-any.whl", hash = "sha256:6d87e67ed40c69d2e98af8df1965ef47b411b1fd9ef55e257269b83c22428611", size = 75204 },
{ url = "https://files.pythonhosted.org/packages/f7/1b/354a0ab669703f87e9ab0464670be8791a9de59c2693ffb0d9a584927b5e/python_fasthtml-0.12.39-py3-none-any.whl", hash = "sha256:d9d2a173714852f906d1821f5eeb40640db2ef2b2f997edee63471feb58127be", size = 73179 },
]
[[package]]