Revert "add init command"

This reverts commit a181ef153d.
This commit is contained in:
Musa 2026-01-13 11:00:41 -08:00
parent 1adbd81104
commit 8650aa7419
7 changed files with 0 additions and 806 deletions

View file

@ -1,8 +1,5 @@
import os
# Brand color - Plano purple
PLANO_COLOR = "#969FF4"
SERVICE_NAME_ARCHGW = "plano"
PLANO_DOCKER_NAME = "plano"
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.2")

View file

@ -1,701 +0,0 @@
import re
import os
from dataclasses import dataclass
from pathlib import Path
import rich_click as click
from rich.console import Console
from rich.panel import Panel
from planoai.consts import PLANO_COLOR
from planoai.utils import get_llm_provider_access_keys, find_repo_root
@dataclass(frozen=True)
class Template:
"""
A Plano config template.
- id: stable identifier used by --template
- title/description: UI strings
- yaml_text: embedded template contents (works in PyPI installs)
- repo_path: optional path to a real demos/.../config.yaml when running in-repo
"""
id: str
title: str
description: str
yaml_text: str | None = None
repo_path: str | None = None
BUILTIN_TEMPLATES: list[Template] = [
Template(
id="samples_python/weather_forecast",
title="samples_python/weather_forecast",
description="prompt targets + multiple LLMs (OpenAI/Groq/Anthropic)",
yaml_text="""version: v0.1.0
listeners:
ingress_traffic:
address: 0.0.0.0
port: 10000
message_format: openai
timeout: 30s
egress_traffic:
address: 0.0.0.0
port: 12000
message_format: openai
timeout: 30s
endpoints:
weather_forecast_service:
endpoint: host.docker.internal:18083
connect_timeout: 0.005s
overrides:
prompt_target_intent_matching_threshold: 0.6
llm_providers:
- access_key: $GROQ_API_KEY
model: groq/llama-3.2-3b-preview
- access_key: $OPENAI_API_KEY
model: openai/gpt-4o
default: true
- access_key: $OPENAI_API_KEY
model: openai/gpt-4o-mini
- access_key: $ANTHROPIC_API_KEY
model: anthropic/claude-sonnet-4-20250514
system_prompt: |
You are a helpful assistant.
prompt_targets:
- name: get_current_weather
description: Get current weather at a location.
parameters:
- name: location
description: The location to get the weather for
required: true
type: string
format: City, State
- name: days
description: the number of days for the request
required: true
type: int
endpoint:
name: weather_forecast_service
path: /weather
http_method: POST
- name: default_target
default: true
description: This is the default target for all unmatched prompts.
endpoint:
name: weather_forecast_service
path: /default_target
http_method: POST
system_prompt: |
You are a helpful assistant! Summarize the user's request and provide a helpful response.
auto_llm_dispatch_on_response: false
tracing:
random_sampling: 100
trace_arch_internal: true
""",
),
Template(
id="samples_python/stock_quote",
title="samples_python/stock_quote",
description="external API headers ($TWELVEDATA_API_KEY) + prompt targets",
yaml_text="""version: v0.1.0
listeners:
ingress_traffic:
address: 0.0.0.0
port: 10000
message_format: openai
timeout: 30s
llm_providers:
- access_key: $OPENAI_API_KEY
model: openai/gpt-4o
endpoints:
twelvedata_api:
endpoint: api.twelvedata.com
protocol: https
system_prompt: |
You are a helpful assistant.
prompt_targets:
- name: stock_quote
description: get current stock exchange rate for a given symbol
parameters:
- name: symbol
description: Stock symbol
required: true
type: str
endpoint:
name: twelvedata_api
path: /quote
http_headers:
Authorization: "apikey $TWELVEDATA_API_KEY"
system_prompt: |
You are a helpful stock exchange assistant. Parse the JSON and present it in a human-readable format. Be concise.
- name: stock_quote_time_series
description: get historical stock exchange rate for a given symbol
parameters:
- name: symbol
description: Stock symbol
required: true
type: str
- name: interval
description: Time interval
default: 1day
enum:
- 1h
- 1day
type: str
endpoint:
name: twelvedata_api
path: /time_series
http_headers:
Authorization: "apikey $TWELVEDATA_API_KEY"
system_prompt: |
You are a helpful stock exchange assistant. Parse the JSON and present it in a human-readable format. Be concise.
tracing:
random_sampling: 100
trace_arch_internal: true
""",
),
Template(
id="use_cases/claude_code_router",
title="use_cases/claude_code_router",
description="multi-model routing preferences + model_aliases (good for CLI agents)",
yaml_text="""version: v0.1
listeners:
egress_traffic:
address: 0.0.0.0
port: 12000
message_format: openai
timeout: 30s
llm_providers:
- model: openai/gpt-5-2025-08-07
access_key: $OPENAI_API_KEY
routing_preferences:
- name: code generation
description: generating new code snippets, functions, or boilerplate based on user prompts or requirements
- model: openai/gpt-4.1-2025-04-14
access_key: $OPENAI_API_KEY
routing_preferences:
- name: code understanding
description: understand and explain existing code snippets, functions, or libraries
- model: anthropic/claude-sonnet-4-5
default: true
access_key: $ANTHROPIC_API_KEY
- model: anthropic/claude-haiku-4-5
access_key: $ANTHROPIC_API_KEY
- model: ollama/llama3.1
base_url: http://host.docker.internal:11434
model_aliases:
arch.claude.code.small.fast:
target: claude-haiku-4-5
tracing:
random_sampling: 100
""",
),
Template(
id="use_cases/ollama",
title="use_cases/ollama",
description="local LLM via base_url (OpenAI-compatible provider_interface)",
yaml_text="""version: v0.1.0
listeners:
egress_traffic:
address: 0.0.0.0
port: 12000
message_format: openai
timeout: 30s
llm_providers:
- model: my_llm_provider/llama3.2
provider_interface: openai
base_url: http://host.docker.internal:11434
default: true
system_prompt: |
You are a helpful assistant.
tracing:
random_sampling: 100
trace_arch_internal: true
""",
),
]
def _discover_repo_demo_templates(repo_root: str | None) -> dict[str, str]:
"""
Returns mapping from template id -> absolute config.yaml path for repo demos.
This is best-effort and should be fast; built-in templates remain the default.
"""
if not repo_root:
return {}
demos_dir = Path(repo_root) / "demos"
if not demos_dir.exists():
return {}
result: dict[str, str] = {}
# keep it bounded: just walk demos and match config.yaml (small tree)
for cfg in demos_dir.rglob("config.yaml"):
try:
rel = cfg.relative_to(demos_dir).as_posix()
except Exception:
continue
template_id = rel.removesuffix("/config.yaml")
result[template_id] = str(cfg)
return result
def _get_templates() -> list[Template]:
repo_root = find_repo_root()
repo_templates = _discover_repo_demo_templates(repo_root)
templates: list[Template] = []
for t in BUILTIN_TEMPLATES:
repo_path = repo_templates.get(t.id)
templates.append(
Template(
id=t.id,
title=t.title,
description=t.description,
yaml_text=t.yaml_text,
repo_path=repo_path,
)
)
# Add any extra demo configs not represented by built-ins (no embedded yaml).
builtin_ids = {t.id for t in templates}
for template_id, path in sorted(repo_templates.items()):
if template_id in builtin_ids:
continue
templates.append(
Template(
id=template_id,
title=template_id,
description="(repo demo)",
yaml_text=None,
repo_path=path,
)
)
return templates
def _resolve_template(template_id_or_path: str | None) -> Template | None:
if not template_id_or_path:
return None
# 1) explicit path
p = Path(template_id_or_path)
if p.exists() and p.is_file():
return Template(
id=str(p),
title=str(p),
description="(file)",
yaml_text=None,
repo_path=str(p.resolve()),
)
# 2) known id
templates = _get_templates()
for t in templates:
if t.id == template_id_or_path:
return t
return None
def _ensure_parent_dir(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
def _write_clean_config(path: Path, force: bool) -> None:
_ensure_parent_dir(path)
if path.exists() and not force:
raise FileExistsError(str(path))
# user asked for NOTHING in it: empty file, with just a newline for POSIX friendliness
path.write_text("\n", encoding="utf-8")
def _write_template_config(path: Path, template: Template, force: bool) -> str:
_ensure_parent_dir(path)
if path.exists() and not force:
raise FileExistsError(str(path))
if template.repo_path:
src = Path(template.repo_path)
text = src.read_text(encoding="utf-8")
path.write_text(text, encoding="utf-8")
return f"repo:{template.repo_path}"
if template.yaml_text is None:
raise ValueError(f"Template '{template.id}' is not available in this install.")
path.write_text(template.yaml_text, encoding="utf-8")
return "builtin"
_ENV_VAR_PATTERN = re.compile(r"\$\{?([A-Z_][A-Z0-9_]*)\}?")
def _extract_env_vars(config_path: Path) -> list[str]:
"""
Extract env vars referenced by the config so we can offer .env placeholders.
Uses existing logic (headers/model providers/etc) plus a regex fallback.
"""
keys: set[str] = set()
try:
extracted = get_llm_provider_access_keys(str(config_path))
for item in extracted:
if not item:
continue
if item.startswith("$"):
keys.add(item[1:])
else:
# some cases may return raw vars
keys.add(item)
except Exception:
# best-effort; still run regex scan
pass
try:
text = config_path.read_text(encoding="utf-8")
for m in _ENV_VAR_PATTERN.findall(text):
keys.add(m)
except Exception:
pass
# Filter obvious false positives if any ever appear
keys.discard("HOST")
keys.discard("PORT")
return sorted(keys)
def _read_env_file_keys(env_path: Path) -> set[str]:
if not env_path.exists():
return set()
keys: set[str] = set()
for line in env_path.read_text(encoding="utf-8").splitlines():
s = line.strip()
if not s or s.startswith("#") or "=" not in s:
continue
k = s.split("=", 1)[0].strip()
if k:
keys.add(k)
return keys
def _upsert_env_placeholders(env_path: Path, keys: list[str]) -> list[str]:
"""
Create or append missing keys with blank values. Returns the keys actually added.
"""
_ensure_parent_dir(env_path)
existing = _read_env_file_keys(env_path)
missing = [k for k in keys if k not in existing]
if not missing:
return []
header = ""
if env_path.exists():
header = "\n# Added by `planoai init`\n"
addition = header + "\n".join([f"{k}=" for k in missing]) + "\n"
with env_path.open("a", encoding="utf-8") as f:
f.write(addition)
return missing
def _questionary_style():
# prompt_toolkit style string format
from prompt_toolkit.styles import Style
return Style.from_dict(
{
"qmark": f"fg:{PLANO_COLOR} bold",
"question": "bold",
"answer": f"fg:{PLANO_COLOR} bold",
"pointer": f"fg:{PLANO_COLOR} bold",
"highlighted": f"fg:{PLANO_COLOR} bold",
"selected": f"fg:{PLANO_COLOR}",
"instruction": "fg:#888888",
"text": "",
"disabled": "fg:#666666",
}
)
def _force_truecolor_for_prompt_toolkit() -> None:
"""
Ensure prompt_toolkit uses truecolor so our brand hex (#969FF4) renders correctly.
Without this, some terminals or environments downgrade to 8-bit and the color
can look like a generic blue.
"""
# Only set if user hasn't explicitly chosen a depth.
os.environ.setdefault("PROMPT_TOOLKIT_COLOR_DEPTH", "DEPTH_24_BIT")
@click.command()
@click.option(
"--template",
"template_id_or_path",
default=None,
help="Create config.yaml from a template id (e.g. use_cases/claude_code_router) or a path to a YAML file.",
)
@click.option(
"--clean",
is_flag=True,
help="Create an empty config.yaml with no contents.",
)
@click.option(
"--output",
"-o",
"output_path",
default="config.yaml",
show_default=True,
help="Where to write the generated config.",
)
@click.option(
"--force",
is_flag=True,
help="Overwrite existing config file if it already exists.",
)
@click.option(
"--no-env",
is_flag=True,
help="Do not create/update a .env file.",
)
@click.option(
"--yes",
"-y",
is_flag=True,
help="Skip interactive prompts and accept defaults (will NOT overwrite without --force).",
)
@click.option(
"--list-templates",
is_flag=True,
help="List available template ids and exit.",
)
@click.pass_context
def init(
ctx, template_id_or_path, clean, output_path, force, no_env, yes, list_templates
):
"""Initialize a Plano config quickly (arrow-key interactive wizard by default)."""
import sys
console = Console()
if clean and template_id_or_path:
raise click.UsageError("Use either --clean or --template, not both.")
templates = _get_templates()
if list_templates:
console.print(f"[bold {PLANO_COLOR}]Available templates[/bold {PLANO_COLOR}]\n")
for t in templates:
origin = (
"repo" if t.repo_path else "builtin" if t.yaml_text else "repo-only"
)
console.print(
f" [bold]{t.id}[/bold] [dim]({origin})[/dim] - {t.description}"
)
return
out_path = Path(output_path).expanduser()
# Non-interactive fast paths
if yes or clean or template_id_or_path:
if clean:
try:
_write_clean_config(out_path, force=force)
except FileExistsError:
raise click.ClickException(
f"Refusing to overwrite existing file: {out_path} (use --force)"
)
console.print(f"[green]✓[/green] Wrote [bold]{out_path}[/bold]")
return
if template_id_or_path:
template = _resolve_template(template_id_or_path)
if not template:
raise click.ClickException(
f"Unknown template: {template_id_or_path}\n"
f"Run: planoai init --list-templates"
)
try:
origin = _write_template_config(out_path, template, force=force)
except FileExistsError:
raise click.ClickException(
f"Refusing to overwrite existing file: {out_path} (use --force)"
)
console.print(
f"[green]✓[/green] Wrote [bold]{out_path}[/bold] [dim]({template.id}, {origin})[/dim]"
)
if no_env:
return
env_vars = _extract_env_vars(out_path)
if env_vars:
env_path = out_path.parent / ".env"
added = _upsert_env_placeholders(env_path, env_vars)
if added:
console.print(
f"[green]✓[/green] Updated [bold]{env_path}[/bold] [dim](added: {', '.join(added)})[/dim]"
)
else:
console.print(f"[dim]✓ .env already contains required keys[/dim]")
return
# yes without clean/template means: do nothing useful
raise click.UsageError(
"Non-interactive mode requires --template or --clean (or omit --yes for the interactive wizard)."
)
# Interactive wizard
if not (sys.stdin.isatty() and sys.stdout.isatty()):
raise click.ClickException(
"Interactive mode requires a TTY.\n"
"Use one of:\n"
" planoai init --template <id>\n"
" planoai init --clean\n"
" planoai init --list-templates"
)
_force_truecolor_for_prompt_toolkit()
# Lazy import so non-interactive users don't pay the import/compat cost
import questionary
from questionary import Choice
# Step 1: mode
mode = questionary.select(
"Welcome to Plano! Pick a starting point:",
choices=[
Choice("Start from a demo template (recommended)", value="template"),
Choice("Create a clean config.yaml (empty)", value="clean"),
Choice("Cancel", value="cancel"),
],
style=_questionary_style(),
pointer="",
).ask()
if mode in (None, "cancel"):
console.print("[dim]Cancelled.[/dim]")
return
# Step 2: output path (default: config.yaml)
out_answer = questionary.text(
"Where should I write the config?",
default=str(out_path),
style=_questionary_style(),
).ask()
if not out_answer:
console.print("[dim]Cancelled.[/dim]")
return
out_path = Path(out_answer).expanduser()
if out_path.exists() and not force:
overwrite = questionary.confirm(
f"{out_path} already exists. Overwrite?",
default=False,
style=_questionary_style(),
).ask()
if not overwrite:
console.print("[dim]Cancelled.[/dim]")
return
force = True
if mode == "clean":
_write_clean_config(out_path, force=True)
console.print(f"[green]✓[/green] Wrote [bold]{out_path}[/bold]")
return
# Step 3: choose template (curated at top, plus any repo-only demos)
# Keep the list compact and readable.
template_choices: list[Choice] = []
for t in templates:
label = f"{t.title}{t.description}"
template_choices.append(Choice(label, value=t))
template = questionary.select(
"Choose a template",
choices=template_choices,
style=_questionary_style(),
pointer="",
use_indicator=True,
).ask()
if not template:
console.print("[dim]Cancelled.[/dim]")
return
origin = _write_template_config(out_path, template, force=True)
console.print(
f"[green]✓[/green] Wrote [bold]{out_path}[/bold] [dim]({template.id}, {origin})[/dim]"
)
# Step 4: .env placeholders (recommended, fast)
if not no_env:
env_vars = _extract_env_vars(out_path)
if env_vars:
env_path = out_path.parent / ".env"
do_env = questionary.confirm(
"Create/update a .env file with placeholders for required keys?",
default=True,
style=_questionary_style(),
).ask()
if do_env:
added = _upsert_env_placeholders(env_path, env_vars)
if added:
console.print(
f"[green]✓[/green] Updated [bold]{env_path}[/bold] [dim](added: {', '.join(added)})[/dim]"
)
else:
console.print(f"[dim]✓ .env already contains required keys[/dim]")
# Step 5: next step shortcuts (validate/up/done)
next_step = questionary.select(
"Next step",
choices=[
Choice(f"Run: planoai validate {out_path}", value="validate"),
Choice(f"Run: planoai up {out_path}", value="up"),
Choice("Done", value="done"),
],
default="validate",
style=_questionary_style(),
pointer="",
).ask()
if next_step == "validate":
# Reuse existing click command implementation
from planoai.main import validate as validate_cmd
ctx.invoke(validate_cmd, config_file=str(out_path), path=".", quiet=False)
elif next_step == "up":
from planoai.main import up as up_cmd
ctx.invoke(up_cmd, file=str(out_path), path=".", foreground=False)

View file

@ -93,7 +93,6 @@ from planoai.core import (
stop_docker_container,
start_cli_agent,
)
from planoai.init_cmd import init as init_cmd
from planoai.consts import (
PLANO_DOCKER_IMAGE,
PLANO_DOCKER_NAME,
@ -880,7 +879,6 @@ main.add_command(logs)
main.add_command(cli_agent)
main.add_command(generate_prompt_targets)
main.add_command(validate)
main.add_command(init_cmd, name="init")
if __name__ == "__main__":
main()

View file

@ -9,7 +9,6 @@ dependencies = [
"click>=8.1.7,<9.0.0",
"jinja2>=3.1.4,<4.0.0",
"jsonschema>=4.23.0,<5.0.0",
"questionary>=2.1.1,<3.0.0",
"pyyaml>=6.0.2,<7.0.0",
"requests>=2.31.0,<3.0.0",
"rich>=14.2.0",

View file

@ -1,59 +0,0 @@
from click.testing import CliRunner
from planoai.init_cmd import init
def test_init_clean_writes_empty_config(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
runner = CliRunner()
result = runner.invoke(init, ["--clean"])
assert result.exit_code == 0, result.output
config_path = tmp_path / "config.yaml"
assert config_path.exists()
assert config_path.read_text(encoding="utf-8") == "\n"
def test_init_template_builtin_writes_config_and_env(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
runner = CliRunner()
result = runner.invoke(
init, ["--template", "use_cases/claude_code_router", "--yes"]
)
assert result.exit_code == 0, result.output
config_path = tmp_path / "config.yaml"
assert config_path.exists()
config_text = config_path.read_text(encoding="utf-8")
assert "llm_providers:" in config_text
env_path = tmp_path / ".env"
assert env_path.exists()
env_text = env_path.read_text(encoding="utf-8")
assert "OPENAI_API_KEY=" in env_text
assert "ANTHROPIC_API_KEY=" in env_text
def test_init_refuses_overwrite_without_force(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "config.yaml").write_text("hello", encoding="utf-8")
runner = CliRunner()
result = runner.invoke(init, ["--clean"])
assert result.exit_code != 0
assert "Refusing to overwrite" in result.output
def test_init_force_overwrites(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "config.yaml").write_text("hello", encoding="utf-8")
runner = CliRunner()
result = runner.invoke(init, ["--clean", "--force"])
assert result.exit_code == 0, result.output
assert (tmp_path / "config.yaml").read_text(encoding="utf-8") == "\n"

35
cli/uv.lock generated
View file

@ -271,7 +271,6 @@ dependencies = [
{ name = "jinja2" },
{ name = "jsonschema" },
{ name = "pyyaml" },
{ name = "questionary" },
{ name = "requests" },
{ name = "rich" },
{ name = "rich-click" },
@ -294,7 +293,6 @@ requires-dist = [
{ name = "jsonschema", specifier = ">=4.23.0,<5.0.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1,<9.0.0" },
{ name = "pyyaml", specifier = ">=6.0.2,<7.0.0" },
{ name = "questionary", specifier = ">=2.1.1,<3.0.0" },
{ name = "requests", specifier = ">=2.31.0,<3.0.0" },
{ name = "rich", specifier = ">=14.2.0" },
{ name = "rich-click", specifier = ">=1.9.5" },
@ -313,18 +311,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "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, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
@ -396,18 +382,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[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, upload-time = "2025-08-28T19:00:20.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
]
[[package]]
name = "referencing"
version = "0.36.2"
@ -656,12 +630,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[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, upload-time = "2025-09-22T16:29:53.023Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
]

View file

@ -1,5 +0,0 @@
AUTH_SECRET=R/dTqODnkO4dHsra7q9J78GkBTHG2YKWVnRZdTIYxmw=
AI_GATEWAY_API_KEY=vck_8OmwGvVjwIZvNnOUtXQe7QrwbJY9iqT1VkVmM1JtstIbJsazj71lCMGf
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_m2VnAfbbbo7gcEMm_o6M0Zj7v7OLcz8W4fuJXTzjK6dcTSV
POSTGRES_URL=postgresql://neondb_owner:npg_bkr8fYJiw7HA@ep-wandering-surf-ahvhyf3o-pooler.c-3.us-east-1.aws.neon.tech/neondb?sslmode=require
REDIS_URL="rediss://default:ATU7AAIncDJkMmFlOTMxNjA2ZTI0ZTJmOGU0YWFhNTYxYjhmZmU1OXAyMTM2Mjc@ethical-monster-13627.upstash.io:6379"