Update skill versions to 1.4 and 1.5 across multiple skills, including infographics, latex-posters, literature-review, scientific-schematics, and scientific-slides. Enhance API key resolution logic in various scripts to improve credential handling and error messaging. Adjust image model references for clarity in infographic and schematic generation scripts.

This commit is contained in:
Timothy Kassis
2026-07-28 17:32:42 -07:00
parent 07021b927e
commit f1b60f8ef4
22 changed files with 637 additions and 212 deletions

View File

@@ -548,7 +548,7 @@ This repository contains **158 scientific and research skills** organized across
- Diagrams: Scientific Schematics, Markdown & Mermaid Writing
- Infographics: Infographics (10 types, 8 styles, colorblind-safe palettes)
- Citations: Citation Management, pyzotero
- Illustration: Generate Image (AI image generation with FLUX.2 Pro and Gemini 3.1 Flash Image Preview / Nano Banana 2)
- Illustration: Generate Image (AI image generation with FLUX.2 Pro and Gemini 3.1 Flash Image / Nano Banana 2)
#### 🔬 **Scientific Databases & Data Access** (10 skills → 100+ databases total)
> A unified database-lookup skill provides deterministic REST API access to 78 public databases across all domains, with retrieval contracts, pagination/count reconciliation, and endpoint provenance. Dedicated skills cover specialized data platforms. Multi-database packages like BioServices (~40 bioinformatics services), BioPython (39 NCBI sub-databases via Entrez), and gget (20+ genomics databases) add further coverage.

View File

@@ -4,7 +4,7 @@ description: Comprehensive citation management for academic research. Search Goo
allowed-tools: Read Write Edit Bash
license: MIT License
metadata:
version: "1.5"
version: "1.6"
skill-author: K-Dense Inc.
openclaw:
primaryEnv: OPENROUTER_API_KEY

View File

@@ -40,6 +40,43 @@ FORWARDED_ENV_VARS = (
)
def resolve_api_key(explicit=None):
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. The child process is handed the resolved value through
build_subprocess_env, so it never has to repeat this search.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
def build_subprocess_env(api_key):
"""Return a minimal environment for the AI generation subprocess."""
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -111,15 +148,16 @@ Environment Variables:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or use --api-key flag")
sys.exit(1)
# Find AI generation script

View File

@@ -33,20 +33,42 @@ except ImportError:
print("Error: requests library not found. Install with: pip install requests")
sys.exit(1)
# Try to load .env file from multiple potential locations
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
return False
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
class ScientificSchematicGenerator:
@@ -125,13 +147,8 @@ IMPORTANT - NO FIGURE NUMBERS:
verbose: Print detailed progress information
"""
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
# If not found in environment, try loading from .env file
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -144,9 +161,11 @@ IMPORTANT - NO FIGURE NUMBERS:
self.verbose = verbose
self._last_error = None # Track last error for better reporting
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana 2 - Google's advanced image generation model
# https://openrouter.ai/google/gemini-3.6-flash
self.image_model = "google/gemini-3.1-flash-image-preview"
# Nano Banana 2 - Google's advanced image generation model. The slug must
# be an image-output model; a text-only chat model is rejected with
# "No endpoints found that support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - excellent vision and reasoning
self.review_model = "google/gemini-3.6-flash"
@@ -776,13 +795,14 @@ Environment:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr provide via --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
# Validate iterations - enforce max of 2

View File

@@ -3,7 +3,7 @@ name: infographics
description: "Create professional infographics using Nano Banana Pro AI with smart iterative refinement. Uses Gemini 3.6 Flash for quality review. Integrates research-lookup and web search for accurate data. Supports 10 infographic types, 8 industry styles, and colorblind-safe palettes."
allowed-tools: Read Write Edit Bash
metadata:
version: "1.3"
version: "1.4"
openclaw:
primaryEnv: OPENROUTER_API_KEY
envVars:

View File

@@ -3,7 +3,7 @@
Generate professional infographics using Nano Banana Pro.
This script generates infographics with smart iterative refinement:
- Uses Nano Banana Pro (Gemini 3.6 Flash Image Preview) for generation
- Uses Nano Banana Pro (Gemini 3.1 Flash Image) for generation
- Uses Gemini 3.6 Flash for quality review
- Only regenerates if quality is below threshold
- Supports 10 infographic types and industry style presets
@@ -53,6 +53,43 @@ FORWARDED_ENV_VARS = (
)
def resolve_api_key(explicit=None):
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. The child process is handed the resolved value through
build_subprocess_env, so it never has to repeat this search.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
def build_subprocess_env(api_key):
"""Return a minimal environment for the AI generation subprocess."""
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -194,15 +231,16 @@ Environment Variables:
if not args.output:
parser.error("--output is required")
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or use --api-key flag")
sys.exit(1)
# Find AI generation script

View File

@@ -36,19 +36,42 @@ except ImportError:
sys.exit(1)
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
return False
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
# Infographic type configurations with detailed prompting
@@ -315,12 +338,8 @@ IMPORTANT - NO META CONTENT:
def __init__(self, api_key: Optional[str] = None, verbose: bool = False):
"""Initialize the generator."""
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -333,9 +352,12 @@ IMPORTANT - NO META CONTENT:
self.verbose = verbose
self._last_error = None
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana Pro for image generation
self.image_model = "google/gemini-3.6-flash"
# Gemini 3.6 Flash for quality review
# Nano Banana Pro for image generation. The slug must be an image-output
# model; a text-only chat model is rejected with "No endpoints found that
# support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - reads the image, answers in text
self.review_model = "google/gemini-3.6-flash"
def _log(self, message: str):
@@ -1278,13 +1300,14 @@ Environment:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr provide via --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
try:

View File

@@ -3,7 +3,7 @@ name: latex-posters
description: "Create professional research posters in LaTeX using beamerposter, tikzposter, or baposter. Support for conference presentations, academic posters, and scientific communication. Includes layout design, color schemes, multi-column formats, figure integration, and poster-specific best practices for visual communication."
allowed-tools: Read Write Edit Bash
metadata:
version: "1.3"
version: "1.4"
openclaw:
primaryEnv: OPENROUTER_API_KEY
envVars:

View File

@@ -40,6 +40,43 @@ FORWARDED_ENV_VARS = (
)
def resolve_api_key(explicit=None):
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. The child process is handed the resolved value through
build_subprocess_env, so it never has to repeat this search.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
def build_subprocess_env(api_key):
"""Return a minimal environment for the AI generation subprocess."""
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -111,15 +148,16 @@ Environment Variables:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or use --api-key flag")
sys.exit(1)
# Find AI generation script

View File

@@ -33,20 +33,42 @@ except ImportError:
print("Error: requests library not found. Install with: pip install requests")
sys.exit(1)
# Try to load .env file from multiple potential locations
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
return False
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
class ScientificSchematicGenerator:
@@ -125,13 +147,8 @@ IMPORTANT - NO FIGURE NUMBERS:
verbose: Print detailed progress information
"""
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
# If not found in environment, try loading from .env file
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -144,9 +161,11 @@ IMPORTANT - NO FIGURE NUMBERS:
self.verbose = verbose
self._last_error = None # Track last error for better reporting
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana 2 - Google's advanced image generation model
# https://openrouter.ai/google/gemini-3.6-flash
self.image_model = "google/gemini-3.1-flash-image-preview"
# Nano Banana 2 - Google's advanced image generation model. The slug must
# be an image-output model; a text-only chat model is rejected with
# "No endpoints found that support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - excellent vision and reasoning
self.review_model = "google/gemini-3.6-flash"
@@ -776,13 +795,14 @@ Environment:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr provide via --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
# Validate iterations - enforce max of 2

View File

@@ -4,7 +4,7 @@ description: Conduct comprehensive, systematic literature reviews using multiple
allowed-tools: Read Write Edit Bash
license: MIT license
metadata:
version: "1.4"
version: "1.5"
skill-author: K-Dense Inc.
openclaw:
primaryEnv: OPENROUTER_API_KEY

View File

@@ -40,6 +40,43 @@ FORWARDED_ENV_VARS = (
)
def resolve_api_key(explicit=None):
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. The child process is handed the resolved value through
build_subprocess_env, so it never has to repeat this search.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
def build_subprocess_env(api_key):
"""Return a minimal environment for the AI generation subprocess."""
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -111,15 +148,16 @@ Environment Variables:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or use --api-key flag")
sys.exit(1)
# Find AI generation script

View File

@@ -33,20 +33,42 @@ except ImportError:
print("Error: requests library not found. Install with: pip install requests")
sys.exit(1)
# Try to load .env file from multiple potential locations
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
return False
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
class ScientificSchematicGenerator:
@@ -125,13 +147,8 @@ IMPORTANT - NO FIGURE NUMBERS:
verbose: Print detailed progress information
"""
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
# If not found in environment, try loading from .env file
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -144,9 +161,11 @@ IMPORTANT - NO FIGURE NUMBERS:
self.verbose = verbose
self._last_error = None # Track last error for better reporting
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana 2 - Google's advanced image generation model
# https://openrouter.ai/google/gemini-3.6-flash
self.image_model = "google/gemini-3.1-flash-image-preview"
# Nano Banana 2 - Google's advanced image generation model. The slug must
# be an image-output model; a text-only chat model is rejected with
# "No endpoints found that support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - excellent vision and reasoning
self.review_model = "google/gemini-3.6-flash"
@@ -776,13 +795,14 @@ Environment:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr provide via --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
# Validate iterations - enforce max of 2

View File

@@ -4,7 +4,7 @@ description: Create publication-quality scientific diagrams using Nano Banana 2
allowed-tools: Read Write Edit Bash
license: MIT license
metadata:
version: "1.3"
version: "1.4"
skill-author: K-Dense Inc.
openclaw:
primaryEnv: OPENROUTER_API_KEY

View File

@@ -40,6 +40,43 @@ FORWARDED_ENV_VARS = (
)
def resolve_api_key(explicit=None):
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. The child process is handed the resolved value through
build_subprocess_env, so it never has to repeat this search.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
def build_subprocess_env(api_key):
"""Return a minimal environment for the AI generation subprocess."""
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -111,15 +148,16 @@ Environment Variables:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or use --api-key flag")
sys.exit(1)
# Find AI generation script

View File

@@ -33,20 +33,42 @@ except ImportError:
print("Error: requests library not found. Install with: pip install requests")
sys.exit(1)
# Try to load .env file from multiple potential locations
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
return False
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
class ScientificSchematicGenerator:
@@ -125,13 +147,8 @@ IMPORTANT - NO FIGURE NUMBERS:
verbose: Print detailed progress information
"""
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
# If not found in environment, try loading from .env file
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -144,9 +161,11 @@ IMPORTANT - NO FIGURE NUMBERS:
self.verbose = verbose
self._last_error = None # Track last error for better reporting
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana 2 - Google's advanced image generation model
# https://openrouter.ai/google/gemini-3.6-flash
self.image_model = "google/gemini-3.1-flash-image-preview"
# Nano Banana 2 - Google's advanced image generation model. The slug must
# be an image-output model; a text-only chat model is rejected with
# "No endpoints found that support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - excellent vision and reasoning
self.review_model = "google/gemini-3.6-flash"
@@ -776,13 +795,14 @@ Environment:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr provide via --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
# Validate iterations - enforce max of 2

View File

@@ -4,7 +4,7 @@ description: Build slide decks and presentations for research talks. Use this fo
allowed-tools: Read Write Edit Bash
license: MIT license
metadata:
version: "1.4"
version: "1.5"
skill-author: K-Dense Inc.
openclaw:
primaryEnv: OPENROUTER_API_KEY

View File

@@ -40,6 +40,43 @@ FORWARDED_ENV_VARS = (
)
def resolve_api_key(explicit=None):
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. The child process is handed the resolved value through
build_subprocess_env, so it never has to repeat this search.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
def build_subprocess_env(api_key):
"""Return a minimal environment for the AI generation subprocess."""
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -111,15 +148,16 @@ Environment Variables:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or use --api-key flag")
sys.exit(1)
# Find AI generation script

View File

@@ -33,20 +33,42 @@ except ImportError:
print("Error: requests library not found. Install with: pip install requests")
sys.exit(1)
# Try to load .env file from multiple potential locations
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
return False
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
class ScientificSchematicGenerator:
@@ -125,13 +147,8 @@ IMPORTANT - NO FIGURE NUMBERS:
verbose: Print detailed progress information
"""
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
# If not found in environment, try loading from .env file
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -144,9 +161,11 @@ IMPORTANT - NO FIGURE NUMBERS:
self.verbose = verbose
self._last_error = None # Track last error for better reporting
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana 2 - Google's advanced image generation model
# https://openrouter.ai/google/gemini-3.6-flash
self.image_model = "google/gemini-3.1-flash-image-preview"
# Nano Banana 2 - Google's advanced image generation model. The slug must
# be an image-output model; a text-only chat model is rejected with
# "No endpoints found that support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - excellent vision and reasoning
self.review_model = "google/gemini-3.6-flash"
@@ -776,13 +795,14 @@ Environment:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr provide via --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
# Validate iterations - enforce max of 2

View File

@@ -43,6 +43,43 @@ FORWARDED_ENV_VARS = (
)
def resolve_api_key(explicit=None):
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. The child process is handed the resolved value through
build_subprocess_env, so it never has to repeat this search.
"""
if explicit:
return explicit
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
def build_subprocess_env(api_key):
"""Return a minimal environment for the AI generation subprocess."""
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -108,15 +145,16 @@ Environment Variables:
args = parser.parse_args()
# Check for API key
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Check for API key — resolves --api-key, the environment, then any .env file
api_key = resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nFor AI generation, you need an OpenRouter API key.")
print("Get one at: https://openrouter.ai/keys")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr use --api-key flag")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or use --api-key flag")
sys.exit(1)
# Find AI generation script

View File

@@ -46,19 +46,42 @@ except ImportError:
sys.exit(1)
def _load_env_file():
"""Load .env file from current directory or script directory only."""
try:
from dotenv import load_dotenv
except ImportError:
return False
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
if candidate.exists():
load_dotenv(dotenv_path=candidate, override=False)
return True
The .env scan walks up from the working directory and finally checks the
script's own directory, so running from anywhere inside a project picks up
the key at its root. Only the standard library is used: python-dotenv is a
common omission, and a missing optional dependency should not read as a
missing credential.
"""
if explicit:
return explicit
return False
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
if from_env:
return from_env
cwd = Path.cwd()
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
env_file = directory / ".env"
if not env_file.is_file():
continue
try:
content = env_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for raw in content.splitlines():
line = raw.strip()
if line.startswith("#") or "=" not in line:
continue
name, _, value = line.partition("=")
if name.strip() == "OPENROUTER_API_KEY":
value = value.strip().strip('"').strip("'")
if value:
return value
return None
class SlideImageGenerator:
@@ -152,12 +175,8 @@ STYLE:
api_key: OpenRouter API key (or use OPENROUTER_API_KEY env var)
verbose: Print detailed progress information
"""
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
if not self.api_key:
_load_env_file()
self.api_key = os.getenv("OPENROUTER_API_KEY")
self.api_key = _resolve_api_key(api_key)
if not self.api_key:
raise ValueError(
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -170,9 +189,12 @@ STYLE:
self.verbose = verbose
self._last_error = None
self.base_url = "https://openrouter.ai/api/v1"
# Nano Banana Pro for image generation
self.image_model = "google/gemini-3.6-flash"
# Gemini 3.6 Flash for quality review
# Nano Banana Pro for image generation. The slug must be an image-output
# model; a text-only chat model is rejected with "No endpoints found that
# support the requested output modalities".
# https://openrouter.ai/google/gemini-3.1-flash-image
self.image_model = "google/gemini-3.1-flash-image"
# Gemini 3.6 Flash for quality review - reads the image, answers in text
self.review_model = "google/gemini-3.6-flash"
def _log(self, message: str):
@@ -697,11 +719,14 @@ Environment:
args = parser.parse_args()
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
# Resolves --api-key, the environment, then any .env file
api_key = _resolve_api_key(args.api_key)
if not api_key:
print("Error: OPENROUTER_API_KEY environment variable not set")
print("Error: OPENROUTER_API_KEY not found")
print("\nSet it with:")
print(" export OPENROUTER_API_KEY='your_api_key'")
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
print("Or provide via --api-key flag")
sys.exit(1)
if args.iterations < 1 or args.iterations > 2:

View File

@@ -620,6 +620,13 @@ class SlideImageInvocationTests(TemporaryDirectoryTestCase):
self.assertEqual(clamped[clamped.index("--iterations") + 1], "1")
def test_without_a_key_nothing_is_spawned(self) -> None:
# The wrapper also resolves a credential from any .env file at or above the
# working directory, so the temporary root stands in for a machine that has
# none -- otherwise a developer's own .env would satisfy the lookup.
origin = os.getcwd()
self.addCleanup(os.chdir, origin)
os.chdir(self.root)
argv = ["generate_slide_image.py", "a slide", "-o", "s.png"]
with mock.patch.object(sys, "argv", argv), \
mock.patch.dict(os.environ, {}, clear=True), \
@@ -948,6 +955,10 @@ class AiCliValidationTests(TemporaryDirectoryTestCase):
text=True,
timeout=120,
env=environment,
# The script resolves a credential from any .env file at or above the
# working directory. Running from the temporary root keeps a developer's
# real .env out of reach, so key=None genuinely means "no credential".
cwd=self.root,
)
def test_more_iterations_than_allowed_is_refused(self) -> None: