mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
ac41c28fe8
* feat: add API key metadata catalog source * feat: add API key catalog generator * feat: generate API key catalog and regenerate .env.template * ci: sync API key catalog from tool configs * feat: load global ~/.tooluniverse/.env from any workspace * feat: add .env read/mask/merge helper for key setup * feat: add graphical API key setup server * feat: add /tooluniverse:setup-keys command and package it * feat: redesign setup-keys page with domain grouping and per-key context - Group keys by research domain (Genomics & Variants, Drugs & Chemistry, Proteins & Structure, Literature & Patents, Clinical & Safety, Models & Infrastructure) instead of Required/Optional/Endpoints. - Each key shows its purpose (what it unlocks) and a without-it note (blocked / demo mode / lower limits / leave blank unless self-hosting). - Light product UI: white cards in responsive grid, indigo accent, Hanken Grotesk + IBM Plex Mono, live progress meter, show/hide toggles. - Enrich api_key_metadata.json with domain, purpose, without fields; generator emits them in api_keys_catalog.json; .env.template regrouped by domain with per-key Without notes. * fix: exclude __pycache__ from plugin scripts dir during build cp -r was shipping stray .pyc files / __pycache__ dirs into the built plugin if a developer had run the scripts locally before building. Switch to rsync with explicit excludes, and add a regression test that plants a canary __pycache__ before the build and asserts dist/ stays clean. * fix: split one-line try/except in pycache regression test (ruff E701) * refactor: declare API key metadata inline in tool configs (api_key_info) Instead of a separate api_key_metadata.json, each tool config now carries an api_key_info block next to its required/optional_api_keys. The generator scans configs for both the key names and their info, so adding a new key is a one-file edit. Deletes api_key_metadata.json; runtime code is untouched (required_api_keys stays a list of strings). Catalog + .env.template output is byte-identical. * chore: sync API key catalog [skip ci] * docs: document api_key_info convention in custom-tool skill The JSON-tool authoring guide now explains declaring required/optional_api_keys and the inline api_key_info block (domain/purpose/without/register_url/type) that flows into the /tooluniverse:setup-keys UI and .env.template, plus the generator + CI auto-sync step. * fix: add UMLS, ICD-11, Gemini, OpenRouter keys to the catalog These keys are read by umls_tool / icd_tool / llm_clients via os.getenv but were never declared in any tool config, so the generated catalog and .env.template omitted them (UMLS_API_KEY + ICD_CLIENT_ID/SECRET regressed vs main). Declare them as optional_api_keys with api_key_info on the relevant tools so they appear in /tooluniverse:setup-keys and .env.template. Runtime load behavior unchanged (optional, not required). --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
60 lines
1.8 KiB
Python
60 lines
1.8 KiB
Python
"""Read, mask, and merge API-key values in a .env file. Stdlib only."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$")
|
|
|
|
|
|
def read_env(path) -> dict:
|
|
"""Return KEY->value for a .env file (missing file -> {})."""
|
|
path = Path(path)
|
|
values: dict = {}
|
|
if not path.exists():
|
|
return values
|
|
for line in path.read_text().splitlines():
|
|
if line.lstrip().startswith("#"):
|
|
continue
|
|
m = _LINE.match(line)
|
|
if m:
|
|
values[m.group(1)] = m.group(2).strip()
|
|
return values
|
|
|
|
|
|
def mask(value: str) -> str:
|
|
"""Mask a secret for display, keeping the last 4 characters."""
|
|
if not value:
|
|
return ""
|
|
if len(value) <= 4:
|
|
return "*" * len(value)
|
|
return "*" * (len(value) - 4) + value[-4:]
|
|
|
|
|
|
def merge_env(path, updates: dict) -> None:
|
|
"""Merge updates into the .env at path, preserving unrelated lines.
|
|
|
|
Value "" removes the key. Keys absent from updates are left as-is.
|
|
Writes with mode 0o600.
|
|
"""
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
existing = path.read_text().splitlines() if path.exists() else []
|
|
out, seen = [], set()
|
|
for line in existing:
|
|
m = None if line.lstrip().startswith("#") else _LINE.match(line)
|
|
if m and m.group(1) in updates:
|
|
name = m.group(1)
|
|
seen.add(name)
|
|
if updates[name] != "":
|
|
out.append(f"{name}={updates[name]}")
|
|
else:
|
|
out.append(line)
|
|
for name, val in updates.items():
|
|
if name not in seen and val != "":
|
|
out.append(f"{name}={val}")
|
|
text = ("\n".join(out).rstrip() + "\n") if out else ""
|
|
path.write_text(text)
|
|
os.chmod(path, 0o600)
|