Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953609156f |
@@ -42,6 +42,10 @@ jobs:
|
||||
working-directory: ./hindsight-integrations/litellm
|
||||
run: uv build --out-dir dist
|
||||
|
||||
- name: Build hindsight-embed
|
||||
working-directory: ./hindsight-embed
|
||||
run: uv build --out-dir dist
|
||||
|
||||
# Publish in order (client and api first, then hindsight-all which depends on them)
|
||||
- name: Publish hindsight-client to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
@@ -67,6 +71,12 @@ jobs:
|
||||
packages-dir: ./hindsight-integrations/litellm/dist
|
||||
skip-existing: true
|
||||
|
||||
- name: Publish hindsight-embed to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./hindsight-embed/dist
|
||||
skip-existing: true
|
||||
|
||||
# Upload artifacts for GitHub release
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -77,6 +87,7 @@ jobs:
|
||||
hindsight-api/dist/*
|
||||
hindsight/dist/*
|
||||
hindsight-integrations/litellm/dist/*
|
||||
hindsight-embed/dist/*
|
||||
retention-days: 1
|
||||
|
||||
release-typescript-client:
|
||||
@@ -416,6 +427,7 @@ jobs:
|
||||
cp artifacts/python-packages/hindsight-api/dist/* release-assets/ || true
|
||||
cp artifacts/python-packages/hindsight/dist/* release-assets/ || true
|
||||
cp artifacts/python-packages/hindsight-integrations/litellm/dist/* release-assets/ || true
|
||||
cp artifacts/python-packages/hindsight-embed/dist/* release-assets/ || true
|
||||
# TypeScript client
|
||||
cp artifacts/typescript-client/*.tgz release-assets/ || true
|
||||
# Control Plane
|
||||
|
||||
@@ -20,6 +20,8 @@ jobs:
|
||||
path: hindsight-api
|
||||
- name: hindsight-client
|
||||
path: hindsight-clients/python
|
||||
- name: hindsight-embed
|
||||
path: hindsight-embed
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -563,6 +565,46 @@ jobs:
|
||||
working-directory: ./hindsight-integrations/litellm
|
||||
run: uv run pytest tests -v
|
||||
|
||||
test-embed:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
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
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-embed
|
||||
run: uv sync --index-strategy unsafe-best-match
|
||||
|
||||
- name: Cache HuggingFace models
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/huggingface
|
||||
key: ${{ runner.os }}-huggingface-embed-${{ hashFiles('hindsight-embed/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-huggingface-embed-
|
||||
${{ runner.os }}-huggingface-
|
||||
|
||||
- name: Run smoke test
|
||||
working-directory: ./hindsight-embed
|
||||
run: ./test.sh
|
||||
|
||||
test-doc-examples:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-rust-cli
|
||||
|
||||
@@ -14,6 +14,7 @@ This document captures architectural decisions and coding conventions for the Hi
|
||||
hindsight/ # Python package for embedded usage
|
||||
hindsight-api/ # FastAPI server (core memory engine)
|
||||
hindsight-cli/ # Rust CLI client
|
||||
hindsight-embed/ # Embedded CLI (no server needed)
|
||||
hindsight-control-plane/ # Next.js admin UI
|
||||
hindsight-docs/ # Docusaurus documentation site
|
||||
hindsight-dev/ # Development tools and benchmarks
|
||||
@@ -148,4 +149,5 @@ Note: The maintained wrapper `hindsight_client.py` and `README.md` are preserved
|
||||
|
||||
# Branding
|
||||
## Colors
|
||||
- Primary: gradient from #0074d9 to #009296
|
||||
- Primary: gradient from #0074d9 to #009296
|
||||
|
||||
|
||||
@@ -89,6 +89,38 @@ class TaskBackend(ABC):
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class SyncTaskBackend(TaskBackend):
|
||||
"""
|
||||
Synchronous task backend that executes tasks immediately.
|
||||
|
||||
This is useful for embedded/CLI usage where we don't want background
|
||||
workers that prevent clean exit. Tasks are executed inline rather than
|
||||
being queued.
|
||||
"""
|
||||
|
||||
async def initialize(self):
|
||||
"""No-op for sync backend."""
|
||||
self._initialized = True
|
||||
logger.debug("SyncTaskBackend initialized")
|
||||
|
||||
async def submit_task(self, task_dict: dict[str, Any]):
|
||||
"""
|
||||
Execute the task immediately (synchronously).
|
||||
|
||||
Args:
|
||||
task_dict: Task dictionary to execute
|
||||
"""
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
await self._execute_task(task_dict)
|
||||
|
||||
async def shutdown(self):
|
||||
"""No-op for sync backend."""
|
||||
self._initialized = False
|
||||
logger.debug("SyncTaskBackend shutdown")
|
||||
|
||||
|
||||
class AsyncIOQueueBackend(TaskBackend):
|
||||
"""
|
||||
Task backend implementation using asyncio queues.
|
||||
|
||||
Executable
+244
@@ -0,0 +1,244 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Install Hindsight Agent Skill
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://hindsight.vectorize.io/get-skill | bash
|
||||
#
|
||||
# Options:
|
||||
# --app <app> Target app: claude, opencode, codex
|
||||
#
|
||||
# Examples:
|
||||
# curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude
|
||||
# curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app opencode
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
print_step() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}▸ $1${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_banner() {
|
||||
echo ""
|
||||
# ANSI logo
|
||||
echo -e " \033[38;2;9;127;184m▄\033[0m\033[48;2;8;130;178m\033[38;2;5;133;186m▄\033[0m \033[48;2;10;143;160m\033[38;2;10;143;165m▄\033[0m\033[38;2;7;140;156m▄\033[0m "
|
||||
echo -e " \033[38;2;8;125;192m▄\033[0m \033[38;2;3;132;191m▀\033[0m\033[38;2;2;133;192m▄\033[0m \033[38;2;3;132;180m▄\033[0m\033[38;2;1;137;184m▄\033[0m\033[38;2;3;133;174m▄\033[0m \033[38;2;3;142;176m▄\033[0m\033[38;2;4;142;169m▀\033[0m \033[38;2;10;144;164m▄\033[0m "
|
||||
echo -e "\033[38;2;6;121;195m▀\033[0m\033[38;2;5;128;203m▀\033[0m\033[48;2;5;124;195m\033[38;2;3;125;200m▄\033[0m\033[38;2;2;126;196m▄\033[0m\033[48;2;3;128;188m\033[38;2;1;131;196m▄\033[0m\033[48;2;0;152;219m\033[38;2;2;131;191m▄\033[0m\033[38;2;1;141;196m▀\033[0m\033[38;2;1;135;183m▀\033[0m\033[38;2;1;148;198m▀\033[0m\033[48;2;1;156;202m\033[38;2;2;135;180m▄\033[0m\033[48;2;4;134;169m\033[38;2;1;137;177m▄\033[0m\033[38;2;3;138;173m▄\033[0m\033[48;2;6;137;165m\033[38;2;2;140;170m▄\033[0m\033[38;2;7;144;169m▀\033[0m\033[38;2;7;139;158m▀\033[0m"
|
||||
echo -e " \033[48;2;2;128;202m\033[38;2;2;124;201m▄\033[0m\033[48;2;1;130;201m\033[38;2;0;135;212m▄\033[0m\033[38;2;2;128;196m▄\033[0m \033[48;2;2;142;204m\033[38;2;7;138;199m▄\033[0m \033[38;2;1;135;186m▄\033[0m\033[48;2;1;142;186m\033[38;2;2;144;194m▄\033[0m\033[48;2;3;138;176m\033[38;2;2;134;176m▄\033[0m "
|
||||
echo -e " \033[48;2;8;118;200m\033[38;2;8;121;209m▄\033[0m\033[38;2;3;121;203m▀\033[0m \033[38;2;3;122;192m▀\033[0m\033[38;2;1;138;216m▀\033[0m\033[48;2;0;138;210m\033[38;2;3;128;198m▄\033[0m\033[48;2;0;126;188m\033[38;2;2;131;198m▄\033[0m\033[48;2;0;142;205m\033[38;2;3;132;193m▄\033[0m\033[38;2;1;140;196m▀\033[0m \033[38;2;4;134;175m▀\033[0m\033[48;2;13;135;167m\033[38;2;8;136;174m▄\033[0m "
|
||||
echo ""
|
||||
echo -e " ${BOLD}HINDSIGHT SKILL INSTALLER${NC}"
|
||||
echo -e " ${DIM}Give your AI agent persistent memory${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Embedded SKILL.md content
|
||||
SKILL_CONTENT='---
|
||||
name: hindsight
|
||||
description: Give your agent persistent memory that works like human memory. Store facts, preferences, and context that persist across sessions.
|
||||
---
|
||||
|
||||
# Hindsight Memory Skill
|
||||
|
||||
You have access to persistent memory via the `hindsight-embed` CLI. Use it to remember important information about the user and recall it when relevant.
|
||||
|
||||
## Setup (first time only)
|
||||
|
||||
Run: `uvx hindsight-embed configure`
|
||||
|
||||
## Commands
|
||||
|
||||
### Store a memory
|
||||
|
||||
Use `retain` to store important facts, preferences, decisions, or context:
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed retain "User prefers dark mode for all UIs"
|
||||
uvx hindsight-embed retain "Project uses Python 3.11 with FastAPI" --context work
|
||||
```
|
||||
|
||||
### Recall memories
|
||||
|
||||
Use `recall` to search for relevant memories before starting tasks:
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed recall "What are the user'"'"'s UI preferences?"
|
||||
uvx hindsight-embed recall "What tech stack does this project use?"
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
### Store memories when you learn:
|
||||
- User preferences (coding style, tools, UI preferences)
|
||||
- Project context (tech stack, architecture decisions)
|
||||
- Personal information the user shares (name, role, company)
|
||||
- Important decisions or outcomes
|
||||
|
||||
### Recall memories when:
|
||||
- Starting a new task (get relevant context first)
|
||||
- Making decisions that should consider user preferences
|
||||
- Working on a project where past context would help
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be specific**: Store "User prefers 2-space indentation" not "User has preferences"
|
||||
2. **Recall first**: Before starting tasks, recall relevant context
|
||||
3. **Use context tags**: Organize with `--context` (work, personal, preferences)
|
||||
'
|
||||
|
||||
# Get skills directory for app (bash 3.x compatible)
|
||||
get_skills_dir() {
|
||||
case "$1" in
|
||||
claude) echo "$HOME/.claude/skills" ;;
|
||||
opencode) echo "$HOME/.opencode/skills" ;;
|
||||
codex) echo "$HOME/.codex/skills" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Get app display name (bash 3.x compatible)
|
||||
get_app_name() {
|
||||
case "$1" in
|
||||
claude) echo "Claude Code" ;;
|
||||
opencode) echo "OpenCode" ;;
|
||||
codex) echo "Codex CLI" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
APP=""
|
||||
|
||||
show_usage() {
|
||||
echo "Usage: $0 [--app <app>]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --app <app> Target app: claude, opencode, codex"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 --app claude"
|
||||
echo " $0 --app opencode"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--app)
|
||||
APP="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Show banner
|
||||
print_banner
|
||||
|
||||
# Validate app parameter
|
||||
if [ -z "$APP" ]; then
|
||||
echo -e "${DIM}Select your AI coding assistant:${NC}"
|
||||
echo ""
|
||||
echo " ${BOLD}1)${NC} Claude Code"
|
||||
echo " ${BOLD}2)${NC} OpenCode"
|
||||
echo " ${BOLD}3)${NC} Codex CLI"
|
||||
echo ""
|
||||
read -p "Enter choice [1]: " app_choice
|
||||
app_choice=${app_choice:-1}
|
||||
|
||||
case $app_choice in
|
||||
1) APP="claude" ;;
|
||||
2) APP="opencode" ;;
|
||||
3) APP="codex" ;;
|
||||
*) APP="claude" ;;
|
||||
esac
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Get skills directory for selected app
|
||||
SKILLS_DIR=$(get_skills_dir "$APP")
|
||||
APP_NAME=$(get_app_name "$APP")
|
||||
if [ -z "$SKILLS_DIR" ]; then
|
||||
print_error "Unknown app '$APP'. Supported: claude, opencode, codex"
|
||||
fi
|
||||
|
||||
print_info "Installing for ${BOLD}$APP_NAME${NC}"
|
||||
|
||||
# Step 1: Check for Python/uvx
|
||||
print_step "Checking prerequisites"
|
||||
|
||||
if ! command -v python3 &> /dev/null && ! command -v uvx &> /dev/null; then
|
||||
print_error "Python 3 or uvx is required.\nInstall from https://python.org or https://docs.astral.sh/uv/"
|
||||
fi
|
||||
print_success "Python/uvx available"
|
||||
|
||||
# Step 2: Configure LLM provider using the CLI
|
||||
print_step "Configuring LLM provider"
|
||||
|
||||
# Install/run hindsight-embed configure
|
||||
if command -v uvx &> /dev/null; then
|
||||
uvx hindsight-embed configure
|
||||
else
|
||||
pip install -q hindsight-embed
|
||||
hindsight-embed configure
|
||||
fi
|
||||
|
||||
# Step 3: Install skill to app's skills directory
|
||||
print_step "Installing skill to $APP_NAME"
|
||||
|
||||
mkdir -p "$SKILLS_DIR/hindsight"
|
||||
|
||||
# Write embedded SKILL.md content
|
||||
echo "$SKILL_CONTENT" > "$SKILLS_DIR/hindsight/SKILL.md"
|
||||
print_success "Installed to $SKILLS_DIR/hindsight/"
|
||||
|
||||
# Done!
|
||||
echo ""
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${GREEN} ✓ Installation Complete!${NC}"
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo -e " The Hindsight skill is now available in ${BOLD}$APP_NAME${NC}."
|
||||
echo ""
|
||||
echo -e " ${DIM}Test the CLI:${NC}"
|
||||
echo -e " ${CYAN}uvx hindsight-embed retain \"Test memory\"${NC}"
|
||||
echo -e " ${CYAN}uvx hindsight-embed recall \"test\"${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}$APP_NAME will automatically use the skill when relevant.${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}Documentation:${NC} ${BLUE}https://hindsight.vectorize.io${NC}"
|
||||
echo ""
|
||||
@@ -0,0 +1,70 @@
|
||||
# hindsight-embed
|
||||
|
||||
Hindsight embedded CLI - local memory operations without a server.
|
||||
|
||||
This package provides a simple CLI for storing and recalling memories using Hindsight's memory engine with an embedded PostgreSQL database (pg0). No external server or database setup required.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hindsight-embed
|
||||
# or with uvx (no install needed)
|
||||
uvx hindsight-embed --help
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Set your LLM API key
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
||||
# Store a memory
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
|
||||
# Recall memories
|
||||
hindsight-embed recall "What are user preferences?"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### retain
|
||||
|
||||
Store a memory:
|
||||
|
||||
```bash
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
hindsight-embed retain "Meeting on Monday" --context work
|
||||
```
|
||||
|
||||
### recall
|
||||
|
||||
Search memories:
|
||||
|
||||
```bash
|
||||
hindsight-embed recall "user preferences"
|
||||
hindsight-embed recall "upcoming events" --budget high
|
||||
hindsight-embed recall "project details" -v # verbose output
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_EMBED_LLM_API_KEY` | LLM API key (or use `OPENAI_API_KEY`) | Required |
|
||||
| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider (`openai`, `anthropic`, `google`, `ollama`) | `openai` |
|
||||
| `HINDSIGHT_EMBED_LLM_MODEL` | LLM model | `gpt-4o-mini` |
|
||||
| `HINDSIGHT_EMBED_BANK_ID` | Memory bank ID | `default` |
|
||||
|
||||
## Use with AI Coding Assistants
|
||||
|
||||
This CLI is designed to work with AI coding assistants like Claude Code, OpenCode, and Codex CLI. Install the Hindsight skill:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash
|
||||
```
|
||||
|
||||
This will configure the LLM provider and install the skill to your assistant's skills directory.
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Hindsight embedded CLI - local memory operations without a server."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,423 @@
|
||||
"""
|
||||
Hindsight Embedded CLI.
|
||||
|
||||
A simple CLI for local memory operations using embedded PostgreSQL (pg0).
|
||||
No external server required - runs everything locally.
|
||||
|
||||
Usage:
|
||||
hindsight-embed configure # Interactive setup
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
hindsight-embed recall "What are user preferences?"
|
||||
|
||||
Environment variables:
|
||||
HINDSIGHT_EMBED_LLM_API_KEY: Required. API key for LLM provider.
|
||||
HINDSIGHT_EMBED_LLM_PROVIDER: Optional. LLM provider (default: "openai").
|
||||
HINDSIGHT_EMBED_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini").
|
||||
HINDSIGHT_EMBED_BANK_ID: Optional. Memory bank ID (default: "default").
|
||||
HINDSIGHT_EMBED_LOG_LEVEL: Optional. Log level (default: "warning").
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_DIR = Path.home() / ".hindsight"
|
||||
CONFIG_FILE = CONFIG_DIR / "embed"
|
||||
|
||||
|
||||
def setup_logging(verbose: bool = False):
|
||||
"""Configure logging."""
|
||||
level_str = os.environ.get("HINDSIGHT_EMBED_LOG_LEVEL", "warning").lower()
|
||||
if verbose:
|
||||
level_str = "debug"
|
||||
|
||||
level_map = {
|
||||
"debug": logging.DEBUG,
|
||||
"info": logging.INFO,
|
||||
"warning": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
}
|
||||
level = level_map.get(level_str, logging.WARNING)
|
||||
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||
stream=sys.stderr,
|
||||
)
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_config_file():
|
||||
"""Load configuration from file if it exists."""
|
||||
if CONFIG_FILE.exists():
|
||||
with open(CONFIG_FILE) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
# Handle 'export VAR=value' format
|
||||
if line.startswith("export "):
|
||||
line = line[7:]
|
||||
key, value = line.split("=", 1)
|
||||
if key not in os.environ: # Don't override env vars
|
||||
os.environ[key] = value
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Get configuration from environment variables."""
|
||||
load_config_file()
|
||||
return {
|
||||
"llm_api_key": os.environ.get("HINDSIGHT_EMBED_LLM_API_KEY")
|
||||
or os.environ.get("HINDSIGHT_API_LLM_API_KEY")
|
||||
or os.environ.get("OPENAI_API_KEY"),
|
||||
"llm_provider": os.environ.get("HINDSIGHT_EMBED_LLM_PROVIDER")
|
||||
or os.environ.get("HINDSIGHT_API_LLM_PROVIDER", "openai"),
|
||||
"llm_model": os.environ.get("HINDSIGHT_EMBED_LLM_MODEL")
|
||||
or os.environ.get("HINDSIGHT_API_LLM_MODEL", "gpt-4o-mini"),
|
||||
"bank_id": os.environ.get("HINDSIGHT_EMBED_BANK_ID", "default"),
|
||||
}
|
||||
|
||||
|
||||
def do_configure(args):
|
||||
"""Interactive configuration setup with beautiful TUI."""
|
||||
import questionary
|
||||
from questionary import Style
|
||||
|
||||
# Custom style for the prompts
|
||||
custom_style = Style([
|
||||
('qmark', 'fg:cyan bold'),
|
||||
('question', 'fg:white bold'),
|
||||
('answer', 'fg:cyan'),
|
||||
('pointer', 'fg:cyan bold'),
|
||||
('highlighted', 'fg:cyan bold'),
|
||||
('selected', 'fg:green'),
|
||||
('text', 'fg:white'),
|
||||
])
|
||||
|
||||
print()
|
||||
print("\033[1m\033[36m ╭─────────────────────────────────────╮\033[0m")
|
||||
print("\033[1m\033[36m │ Hindsight Embed Configuration │\033[0m")
|
||||
print("\033[1m\033[36m ╰─────────────────────────────────────╯\033[0m")
|
||||
print()
|
||||
|
||||
# Check existing config
|
||||
if CONFIG_FILE.exists():
|
||||
if not questionary.confirm(
|
||||
"Existing configuration found. Reconfigure?",
|
||||
default=False,
|
||||
style=custom_style,
|
||||
).ask():
|
||||
print("\n\033[32m✓\033[0m Keeping existing configuration.")
|
||||
return 0
|
||||
print()
|
||||
|
||||
# Provider selection with descriptions
|
||||
providers = [
|
||||
questionary.Choice("OpenAI (recommended)", value=("openai", "o3-mini", "OpenAI")),
|
||||
questionary.Choice("Groq (fast & free tier)", value=("groq", "openai/gpt-oss-20b", "Groq")),
|
||||
questionary.Choice("Google Gemini", value=("google", "gemini-2.0-flash", "Google")),
|
||||
questionary.Choice("Ollama (local, no API key)", value=("ollama", "llama3.2", None)),
|
||||
]
|
||||
|
||||
result = questionary.select(
|
||||
"Select your LLM provider:",
|
||||
choices=providers,
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if result is None: # User cancelled
|
||||
print("\n\033[33m⚠\033[0m Configuration cancelled.")
|
||||
return 1
|
||||
|
||||
provider, default_model, key_name = result
|
||||
|
||||
# API key
|
||||
api_key = ""
|
||||
if key_name:
|
||||
env_keys = {
|
||||
"OpenAI": "OPENAI_API_KEY",
|
||||
"Groq": "GROQ_API_KEY",
|
||||
"Google": "GOOGLE_API_KEY",
|
||||
}
|
||||
env_key = env_keys.get(key_name, "")
|
||||
existing = os.environ.get(env_key, "")
|
||||
|
||||
if existing:
|
||||
masked = existing[:8] + "..." + existing[-4:] if len(existing) > 12 else "***"
|
||||
if questionary.confirm(
|
||||
f"Found {key_name} key in ${env_key} ({masked}). Use it?",
|
||||
default=True,
|
||||
style=custom_style,
|
||||
).ask():
|
||||
api_key = existing
|
||||
|
||||
if not api_key:
|
||||
api_key = questionary.password(
|
||||
f"Enter your {key_name} API key:",
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if not api_key:
|
||||
print("\n\033[31m✗\033[0m API key is required.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Model selection
|
||||
model = questionary.text(
|
||||
"Model name:",
|
||||
default=default_model,
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if model is None:
|
||||
return 1
|
||||
|
||||
# Bank ID
|
||||
bank_id = questionary.text(
|
||||
"Memory bank ID:",
|
||||
default="default",
|
||||
style=custom_style,
|
||||
).ask()
|
||||
|
||||
if bank_id is None:
|
||||
return 1
|
||||
|
||||
# Save configuration
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
f.write("# Hindsight Embed Configuration\n")
|
||||
f.write(f"# Generated by hindsight-embed configure\n\n")
|
||||
f.write(f"HINDSIGHT_EMBED_LLM_PROVIDER={provider}\n")
|
||||
f.write(f"HINDSIGHT_EMBED_LLM_MODEL={model}\n")
|
||||
f.write(f"HINDSIGHT_EMBED_BANK_ID={bank_id}\n")
|
||||
if api_key:
|
||||
f.write(f"HINDSIGHT_EMBED_LLM_API_KEY={api_key}\n")
|
||||
|
||||
CONFIG_FILE.chmod(0o600)
|
||||
|
||||
print()
|
||||
print("\033[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m")
|
||||
print("\033[32m ✓ Configuration saved!\033[0m")
|
||||
print("\033[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m")
|
||||
print()
|
||||
print(f" \033[2mConfig:\033[0m {CONFIG_FILE}")
|
||||
print()
|
||||
print(" \033[2mTest with:\033[0m")
|
||||
print(' \033[36mhindsight-embed retain "Test memory"\033[0m')
|
||||
print(' \033[36mhindsight-embed recall "test"\033[0m')
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
async def _create_engine(config: dict, logger):
|
||||
"""Create and initialize the memory engine."""
|
||||
logger.debug("Setting up environment variables...")
|
||||
|
||||
# Set hindsight-api environment variables from our config
|
||||
if config["llm_api_key"]:
|
||||
os.environ["HINDSIGHT_API_LLM_API_KEY"] = config["llm_api_key"]
|
||||
if config["llm_provider"]:
|
||||
os.environ["HINDSIGHT_API_LLM_PROVIDER"] = config["llm_provider"]
|
||||
if config["llm_model"]:
|
||||
os.environ["HINDSIGHT_API_LLM_MODEL"] = config["llm_model"]
|
||||
|
||||
logger.debug("Importing MemoryEngine...")
|
||||
|
||||
# Import after setting env vars
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.engine.task_backend import SyncTaskBackend
|
||||
|
||||
# Use pg0 embedded database
|
||||
db_name = f"hindsight-embed-{config['bank_id']}"
|
||||
logger.debug(f"Creating MemoryEngine with pg0://{db_name}")
|
||||
|
||||
# Use SyncTaskBackend to avoid background workers that prevent clean exit
|
||||
memory = MemoryEngine(
|
||||
db_url=f"pg0://{db_name}",
|
||||
task_backend=SyncTaskBackend(),
|
||||
)
|
||||
|
||||
logger.debug("Initializing engine...")
|
||||
await memory.initialize()
|
||||
|
||||
logger.debug("Engine initialized")
|
||||
return memory
|
||||
|
||||
|
||||
async def do_retain(args, config: dict, logger):
|
||||
"""Execute retain command."""
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
logger.info(f"Retaining memory: {args.content[:50]}...")
|
||||
|
||||
memory = await _create_engine(config, logger)
|
||||
|
||||
try:
|
||||
logger.debug("Calling retain_batch_async...")
|
||||
await memory.retain_batch_async(
|
||||
bank_id=config["bank_id"],
|
||||
contents=[{
|
||||
"content": args.content,
|
||||
"context": args.context or "general",
|
||||
}],
|
||||
request_context=RequestContext(),
|
||||
)
|
||||
msg = f"Stored memory: {args.content[:50]}..." if len(args.content) > 50 else f"Stored memory: {args.content}"
|
||||
print(msg, flush=True)
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Retain failed: {e}", exc_info=True)
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
async def do_recall(args, config: dict, logger):
|
||||
"""Execute recall command."""
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
logger.info(f"Recalling with query: {args.query}")
|
||||
|
||||
memory = await _create_engine(config, logger)
|
||||
|
||||
try:
|
||||
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
|
||||
budget_enum = budget_map.get(args.budget.lower(), Budget.LOW)
|
||||
|
||||
logger.debug(f"Calling recall_async with budget={budget_enum}...")
|
||||
result = await memory.recall_async(
|
||||
bank_id=config["bank_id"],
|
||||
query=args.query,
|
||||
fact_type=list(VALID_RECALL_FACT_TYPES),
|
||||
budget=budget_enum,
|
||||
max_tokens=args.max_tokens,
|
||||
request_context=RequestContext(),
|
||||
)
|
||||
|
||||
logger.debug(f"Recall returned {len(result.results)} results")
|
||||
|
||||
if result.results:
|
||||
print("Memories found:", flush=True)
|
||||
print("-" * 40, flush=True)
|
||||
for fact in result.results:
|
||||
print(f"- {fact.text}", flush=True)
|
||||
if args.verbose and fact.occurred_start:
|
||||
print(f" (Date: {fact.occurred_start})", flush=True)
|
||||
print("-" * 40, flush=True)
|
||||
print(f"Total: {len(result.results)} memories", flush=True)
|
||||
else:
|
||||
print("No relevant memories found.", flush=True)
|
||||
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Recall failed: {e}", exc_info=True)
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Hindsight Embedded CLI - local memory operations without a server",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
hindsight-embed configure # Interactive setup
|
||||
hindsight-embed retain "User prefers dark mode"
|
||||
hindsight-embed retain "Meeting on Monday" -c work
|
||||
hindsight-embed recall "user preferences"
|
||||
hindsight-embed recall "meetings" --budget high
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Enable verbose/debug logging"
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
# Configure command
|
||||
subparsers.add_parser("configure", help="Interactive configuration setup")
|
||||
|
||||
# Retain command
|
||||
retain_parser = subparsers.add_parser("retain", help="Store a memory")
|
||||
retain_parser.add_argument("content", help="The memory content to store")
|
||||
retain_parser.add_argument(
|
||||
"--context", "-c",
|
||||
help="Category for the memory (e.g., 'preferences', 'work')",
|
||||
default="general"
|
||||
)
|
||||
|
||||
# Recall command
|
||||
recall_parser = subparsers.add_parser("recall", help="Search memories")
|
||||
recall_parser.add_argument("query", help="Search query")
|
||||
recall_parser.add_argument(
|
||||
"--budget", "-b",
|
||||
choices=["low", "mid", "high"],
|
||||
default="low",
|
||||
help="Search budget level (default: low)"
|
||||
)
|
||||
recall_parser.add_argument(
|
||||
"--max-tokens", "-m",
|
||||
type=int,
|
||||
default=4096,
|
||||
help="Maximum tokens in results (default: 4096)"
|
||||
)
|
||||
recall_parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Show additional details"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup logging
|
||||
verbose = getattr(args, 'verbose', False)
|
||||
logger = setup_logging(verbose)
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Handle configure separately (no config needed)
|
||||
if args.command == "configure":
|
||||
exit_code = do_configure(args)
|
||||
sys.exit(exit_code)
|
||||
|
||||
config = get_config()
|
||||
|
||||
# Check for LLM API key
|
||||
if not config["llm_api_key"]:
|
||||
print("Error: LLM API key is required.", file=sys.stderr)
|
||||
print("Run 'hindsight-embed configure' to set up.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Run the appropriate command
|
||||
exit_code = 1
|
||||
try:
|
||||
if args.command == "retain":
|
||||
exit_code = asyncio.run(do_retain(args, config, logger))
|
||||
elif args.command == "recall":
|
||||
exit_code = asyncio.run(do_recall(args, config, logger))
|
||||
else:
|
||||
parser.print_help()
|
||||
exit_code = 1
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Interrupted")
|
||||
exit_code = 130
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}", exc_info=True)
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-embed"
|
||||
version = "0.1.0"
|
||||
description = "Hindsight embedded CLI - local memory operations without a server"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"hindsight-api>=0.1.11",
|
||||
"questionary>=2.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
hindsight-embed = "hindsight_embed.cli:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_embed"]
|
||||
|
||||
[tool.uv.sources]
|
||||
hindsight-api = { workspace = true }
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Simple smoke test for hindsight-embed CLI
|
||||
# Tests retain and recall operations with embedded PostgreSQL
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "=== Hindsight Embed Smoke Test ==="
|
||||
|
||||
# Check required environment
|
||||
if [ -z "$HINDSIGHT_EMBED_LLM_API_KEY" ]; then
|
||||
echo "Error: HINDSIGHT_EMBED_LLM_API_KEY is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use a unique bank ID for this test run
|
||||
export HINDSIGHT_EMBED_BANK_ID="test-$$-$(date +%s)"
|
||||
echo "Using bank ID: $HINDSIGHT_EMBED_BANK_ID"
|
||||
|
||||
# Test 1: Retain a memory
|
||||
echo ""
|
||||
echo "Test 1: Retaining a memory..."
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed retain "The user's favorite color is blue" 2>&1)
|
||||
echo "$OUTPUT"
|
||||
if ! echo "$OUTPUT" | grep -q "Stored memory"; then
|
||||
echo "FAIL: Expected 'Stored memory' in output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory retained successfully"
|
||||
|
||||
# Test 2: Recall the memory
|
||||
echo ""
|
||||
echo "Test 2: Recalling memories..."
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed recall "What is the user's favorite color?" 2>&1)
|
||||
echo "$OUTPUT"
|
||||
if ! echo "$OUTPUT" | grep -qi "blue"; then
|
||||
echo "FAIL: Expected 'blue' in recall output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory recalled successfully"
|
||||
|
||||
# Test 3: Retain with context
|
||||
echo ""
|
||||
echo "Test 3: Retaining memory with context..."
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed retain "User prefers Python over JavaScript" --context work 2>&1)
|
||||
echo "$OUTPUT"
|
||||
if ! echo "$OUTPUT" | grep -q "Stored memory"; then
|
||||
echo "FAIL: Expected 'Stored memory' in output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory with context retained successfully"
|
||||
|
||||
# Test 4: Recall with budget
|
||||
echo ""
|
||||
echo "Test 4: Recalling with budget..."
|
||||
OUTPUT=$(uv run --project "$SCRIPT_DIR" hindsight-embed recall "programming preferences" --budget mid 2>&1)
|
||||
echo "$OUTPUT"
|
||||
if ! echo "$OUTPUT" | grep -qi "python"; then
|
||||
echo "FAIL: Expected 'Python' in recall output"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Memory recalled with budget successfully"
|
||||
|
||||
echo ""
|
||||
echo "=== All tests passed! ==="
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.uv.workspace]
|
||||
members = ["hindsight", "hindsight-api", "hindsight-dev", "hindsight-mcp-server", "hindsight-openai", "hindsight-langmem", "hindsight-clients/python"]
|
||||
members = ["hindsight", "hindsight-api", "hindsight-dev", "hindsight-mcp-server", "hindsight-openai", "hindsight-langmem", "hindsight-clients/python", "hindsight-embed"]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = []
|
||||
|
||||
@@ -12,6 +12,7 @@ members = [
|
||||
"hindsight-api",
|
||||
"hindsight-client",
|
||||
"hindsight-dev",
|
||||
"hindsight-embed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1488,6 +1489,21 @@ dev = [
|
||||
{ name = "ty", specifier = ">=0.0.1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-embed"
|
||||
version = "0.1.0"
|
||||
source = { editable = "hindsight-embed" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
{ name = "questionary" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "hindsight-api", editable = "hindsight-api" },
|
||||
{ name = "questionary", specifier = ">=2.0.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
@@ -2899,6 +2915,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
@@ -3604,6 +3632,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "questionary"
|
||||
version = "2.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "prompt-toolkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "7.1.0"
|
||||
@@ -4925,6 +4965,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
|
||||
Reference in New Issue
Block a user