Skip to content

Commit 6685881

Browse files
TKassisclaude
andcommitted
chore: release v2.19.0 with upstream skills ab2f84ab1059
Refresh the vendored writing-skill subset from K-Dense-AI/scientific-agent-skills, moving the pin from release v2.60.0 (223f5a30f892) to main HEAD ab2f84ab1059 (upstream version 2.61.0). Pinned to the commit rather than a tag because upstream has not yet published a v2.61.0 release. The refresh carries three upstream fixes into this package: - Image generation requests the correct google/gemini-3.1-flash-image slug instead of the text-only google/gemini-3.1-flash-image-preview, which failed with "No endpoints found that support the requested output modalities" across scientific-schematics, infographics, latex-posters, literature-review, citation-management, and scientific-slides. - OPENROUTER_API_KEY now resolves from --api-key, then the environment, then the first .env found walking up from the working directory, using only the standard library, so a missing python-dotenv no longer reads as a missing credential. - soffice.py in docx/pptx/xlsx gained more robust LibreOffice binary discovery and error reporting. skills.lock.json and all three snapshots (skills/, .claude/skills/, scientific_writer/.claude/skills/) were regenerated; the same 26 selected skills continue to ship. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 572f0c7 commit 6685881

85 files changed

Lines changed: 2135 additions & 753 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "claude-scientific-writer",
33
"metadata": {
44
"description": "Skills and setup for scientific writing (papers, grants, posters, clinical reports) using the scientific-writer toolkit.",
5-
"version": "2.18.0",
5+
"version": "2.19.0",
66
"homepage": "https://github.com/K-Dense-AI/claude-scientific-writer"
77
},
88
"owner": {

.claude/skills.lock.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"schema_version": 1,
33
"repository": "https://github.com/K-Dense-AI/scientific-agent-skills",
4-
"ref": "v2.60.0",
5-
"commit": "223f5a30f8928bb76b2e93db282ebe0d6ff5b2ea",
6-
"snapshot_sha256": "493be8334f11bf1ca4c41910a8b49ec116f753c36b93e3ce08cc0de4c91b6c9a",
4+
"ref": "ab2f84ab10597c59fac186ecda6d5edd5dcc8b92",
5+
"commit": "ab2f84ab10597c59fac186ecda6d5edd5dcc8b92",
6+
"snapshot_sha256": "01d64156c9acf8acb0bce8a9a527907a579afcc4306d456da19a91aae852cf2a",
77
"skills": [
88
{
99
"source": "citation-management",
1010
"destination": "citation-management",
11-
"sha256": "68e4425938dde655a3ecb9a2fca14a773cfc50e074eb47d4858dea0d0222fe5a"
11+
"sha256": "5809be57bc54ed6b04c629a2c8a231b1d3d81d68f1873c7519cd4bbc7c25c056"
1212
},
1313
{
1414
"source": "clinical-decision-support",
@@ -23,7 +23,7 @@
2323
{
2424
"source": "docx",
2525
"destination": "document-skills/docx",
26-
"sha256": "894ee6d6e177a4415e04db3996427d4d29299278e6eba91d7c87152cf51ac220"
26+
"sha256": "297eabe17185286e63696c5e99cc263838e00cffb9ad5e44add9b0455487307f"
2727
},
2828
{
2929
"source": "pdf",
@@ -33,12 +33,12 @@
3333
{
3434
"source": "pptx",
3535
"destination": "document-skills/pptx",
36-
"sha256": "e2e470674c9e4f191a76689f22b75250029c593625021bb11799af2e8780767a"
36+
"sha256": "bdb280197d933df17f4436a3f34be5aa06e1b4e6cf52d73d74b62157c5915915"
3737
},
3838
{
3939
"source": "xlsx",
4040
"destination": "document-skills/xlsx",
41-
"sha256": "3c41d5cb1df12e0566594bc1c53c97aa340e749751df12bcf3554550f3bb57c5"
41+
"sha256": "dbb2a5ae521c3e16c0864329bdfd5c8491d7e2e52295bfaefe432b570d67e882"
4242
},
4343
{
4444
"source": "generate-image",
@@ -53,17 +53,17 @@
5353
{
5454
"source": "infographics",
5555
"destination": "infographics",
56-
"sha256": "4864cc43c508cf87a070173c9804f06750cc1f25a74e462c04e34378b04a5473"
56+
"sha256": "a7f703b672b5211a5b26fb180c25d386310ed74ca6fff4f97b8225403dffe847"
5757
},
5858
{
5959
"source": "latex-posters",
6060
"destination": "latex-posters",
61-
"sha256": "46149bd50691db8002de8538a1e2dfe0355d983029b332e029b472d008570368"
61+
"sha256": "06f0c07b758fce3de413c8eb861cf9a9a404436633d452b04220b6396bc9d92c"
6262
},
6363
{
6464
"source": "literature-review",
6565
"destination": "literature-review",
66-
"sha256": "354195c3dd25918bbfc82d2f4702fcfec377f50248fbed3ee9f2b0892c32043a"
66+
"sha256": "e7960c4d80794b43df6ad64ce0362d8b1bee95c55d166574b27d108fec4ac8d1"
6767
},
6868
{
6969
"source": "market-research-reports",
@@ -113,12 +113,12 @@
113113
{
114114
"source": "scientific-schematics",
115115
"destination": "scientific-schematics",
116-
"sha256": "6ecc55c1e1074916c09be818546ca3305d841cdb169a7dac206168972d6e910f"
116+
"sha256": "b35312e248959157be8497ed0dc1252d187aa5ee6fef44dc1333376f680bd4b5"
117117
},
118118
{
119119
"source": "scientific-slides",
120120
"destination": "scientific-slides",
121-
"sha256": "521904cdaf0c3883f712ae471c96ebb6cab43b58baf4f7d168e3400aaebeab35"
121+
"sha256": "83ad63b39f8afda6098d0da69978522ae0648b0a4f8328a3a9d0f3d7b4f914fe"
122122
},
123123
{
124124
"source": "scientific-writing",

.claude/skills/citation-management/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: Comprehensive citation management for academic research. Search Goo
44
allowed-tools: Read Write Edit Bash
55
license: MIT License
66
metadata:
7-
version: "1.5"
7+
version: "1.6"
88
skill-author: K-Dense Inc.
99
openclaw:
1010
primaryEnv: OPENROUTER_API_KEY

.claude/skills/citation-management/scripts/generate_schematic.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,43 @@
4040
)
4141

4242

43+
def resolve_api_key(explicit=None):
44+
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
45+
46+
The .env scan walks up from the working directory and finally checks the
47+
script's own directory, so running from anywhere inside a project picks up
48+
the key at its root. The child process is handed the resolved value through
49+
build_subprocess_env, so it never has to repeat this search.
50+
"""
51+
if explicit:
52+
return explicit
53+
54+
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
55+
if from_env:
56+
return from_env
57+
58+
cwd = Path.cwd()
59+
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
60+
env_file = directory / ".env"
61+
if not env_file.is_file():
62+
continue
63+
try:
64+
content = env_file.read_text(encoding="utf-8", errors="replace")
65+
except OSError:
66+
continue
67+
for raw in content.splitlines():
68+
line = raw.strip()
69+
if line.startswith("#") or "=" not in line:
70+
continue
71+
name, _, value = line.partition("=")
72+
if name.strip() == "OPENROUTER_API_KEY":
73+
value = value.strip().strip('"').strip("'")
74+
if value:
75+
return value
76+
77+
return None
78+
79+
4380
def build_subprocess_env(api_key):
4481
"""Return a minimal environment for the AI generation subprocess."""
4582
env = {name: os.environ[name] for name in FORWARDED_ENV_VARS if name in os.environ}
@@ -111,15 +148,16 @@ def main():
111148

112149
args = parser.parse_args()
113150

114-
# Check for API key
115-
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
151+
# Check for API key — resolves --api-key, the environment, then any .env file
152+
api_key = resolve_api_key(args.api_key)
116153
if not api_key:
117-
print("Error: OPENROUTER_API_KEY environment variable not set")
154+
print("Error: OPENROUTER_API_KEY not found")
118155
print("\nFor AI generation, you need an OpenRouter API key.")
119156
print("Get one at: https://openrouter.ai/keys")
120157
print("\nSet it with:")
121158
print(" export OPENROUTER_API_KEY='your_api_key'")
122-
print("\nOr use --api-key flag")
159+
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
160+
print("Or use --api-key flag")
123161
sys.exit(1)
124162

125163
# Find AI generation script

.claude/skills/citation-management/scripts/generate_schematic_ai.py

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,42 @@
3333
print("Error: requests library not found. Install with: pip install requests")
3434
sys.exit(1)
3535

36-
# Try to load .env file from multiple potential locations
37-
def _load_env_file():
38-
"""Load .env file from current directory or script directory only."""
39-
try:
40-
from dotenv import load_dotenv
41-
except ImportError:
42-
return False
43-
44-
for candidate in [Path.cwd() / ".env", Path(__file__).resolve().parent / ".env"]:
45-
if candidate.exists():
46-
load_dotenv(dotenv_path=candidate, override=False)
47-
return True
36+
def _resolve_api_key(explicit: Optional[str] = None) -> Optional[str]:
37+
"""Resolve the OpenRouter key from --api-key, the environment, then any .env file.
38+
39+
The .env scan walks up from the working directory and finally checks the
40+
script's own directory, so running from anywhere inside a project picks up
41+
the key at its root. Only the standard library is used: python-dotenv is a
42+
common omission, and a missing optional dependency should not read as a
43+
missing credential.
44+
"""
45+
if explicit:
46+
return explicit
47+
48+
from_env = os.environ.get("OPENROUTER_API_KEY", "").strip()
49+
if from_env:
50+
return from_env
51+
52+
cwd = Path.cwd()
53+
for directory in [cwd, *cwd.parents, Path(__file__).resolve().parent]:
54+
env_file = directory / ".env"
55+
if not env_file.is_file():
56+
continue
57+
try:
58+
content = env_file.read_text(encoding="utf-8", errors="replace")
59+
except OSError:
60+
continue
61+
for raw in content.splitlines():
62+
line = raw.strip()
63+
if line.startswith("#") or "=" not in line:
64+
continue
65+
name, _, value = line.partition("=")
66+
if name.strip() == "OPENROUTER_API_KEY":
67+
value = value.strip().strip('"').strip("'")
68+
if value:
69+
return value
4870

49-
return False
71+
return None
5072

5173

5274
class ScientificSchematicGenerator:
@@ -125,13 +147,8 @@ def __init__(self, api_key: Optional[str] = None, verbose: bool = False):
125147
verbose: Print detailed progress information
126148
"""
127149
# Priority: 1) explicit api_key param, 2) environment variable, 3) .env file
128-
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
129-
130-
# If not found in environment, try loading from .env file
131-
if not self.api_key:
132-
_load_env_file()
133-
self.api_key = os.getenv("OPENROUTER_API_KEY")
134-
150+
self.api_key = _resolve_api_key(api_key)
151+
135152
if not self.api_key:
136153
raise ValueError(
137154
"OPENROUTER_API_KEY not found. Please either:\n"
@@ -144,9 +161,11 @@ def __init__(self, api_key: Optional[str] = None, verbose: bool = False):
144161
self.verbose = verbose
145162
self._last_error = None # Track last error for better reporting
146163
self.base_url = "https://openrouter.ai/api/v1"
147-
# Nano Banana 2 - Google's advanced image generation model
148-
# https://openrouter.ai/google/gemini-3.6-flash
149-
self.image_model = "google/gemini-3.1-flash-image-preview"
164+
# Nano Banana 2 - Google's advanced image generation model. The slug must
165+
# be an image-output model; a text-only chat model is rejected with
166+
# "No endpoints found that support the requested output modalities".
167+
# https://openrouter.ai/google/gemini-3.1-flash-image
168+
self.image_model = "google/gemini-3.1-flash-image"
150169
# Gemini 3.6 Flash for quality review - excellent vision and reasoning
151170
self.review_model = "google/gemini-3.6-flash"
152171

@@ -776,13 +795,14 @@ def main():
776795

777796
args = parser.parse_args()
778797

779-
# Check for API key
780-
api_key = args.api_key or os.getenv("OPENROUTER_API_KEY")
798+
# Check for API key — resolves --api-key, the environment, then any .env file
799+
api_key = _resolve_api_key(args.api_key)
781800
if not api_key:
782-
print("Error: OPENROUTER_API_KEY environment variable not set")
801+
print("Error: OPENROUTER_API_KEY not found")
783802
print("\nSet it with:")
784803
print(" export OPENROUTER_API_KEY='your_api_key'")
785-
print("\nOr provide via --api-key flag")
804+
print("\nOr add OPENROUTER_API_KEY=your_api_key to a .env file")
805+
print("Or provide via --api-key flag")
786806
sys.exit(1)
787807

788808
# Validate iterations - enforce max of 2

.claude/skills/document-skills/docx/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: docx
33
description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files) or Word templates (.dotx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', '.dotx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx or .dotx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."
44
license: Proprietary. LICENSE.txt has complete terms
55
metadata:
6-
version: "2.0"
6+
version: "2.1"
77
skill-author: Anthropic, PBC
88
source: https://github.com/anthropics/skills/tree/main/skills/docx
99
---

.claude/skills/document-skills/docx/scripts/office/soffice.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
callers that build their own argv (they must pass -env:UserInstallation too).
1616
"""
1717

18+
import atexit
1819
import contextlib
1920
import os
21+
import shutil
2022
import socket
2123
import subprocess
2224
import tempfile
@@ -73,7 +75,13 @@ def run_soffice(args: Iterable[str], **kwargs) -> subprocess.CompletedProcess:
7375

7476

7577

76-
_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so"
78+
#: Compiled once per process, not cached on disk between runs. An earlier version
79+
#: built the shim at a fixed /tmp/lo_socket_shim.so and reused whatever was already
80+
#: there, which let any local user pre-plant a shared object at that predictable
81+
#: path and get it LD_PRELOADed into every subsequent soffice run. mkdtemp gives an
82+
#: unpredictable directory created 0700 and owned by us, so neither the .c we
83+
#: compile nor the .so we load can be swapped by another user.
84+
_shim_so: Path | None = None
7785

7886

7987
def _needs_shim() -> bool:
@@ -86,18 +94,24 @@ def _needs_shim() -> bool:
8694

8795

8896
def _ensure_shim() -> Path:
89-
if _SHIM_SO.exists():
90-
return _SHIM_SO
97+
global _shim_so
98+
if _shim_so is not None and _shim_so.exists():
99+
return _shim_so
91100

92-
src = Path(tempfile.gettempdir()) / "lo_socket_shim.c"
101+
shim_dir = Path(tempfile.mkdtemp(prefix="lo-shim-"))
102+
atexit.register(shutil.rmtree, shim_dir, True)
103+
104+
src = shim_dir / "lo_socket_shim.c"
105+
so = shim_dir / "lo_socket_shim.so"
93106
src.write_text(_SHIM_SOURCE)
94107
subprocess.run(
95-
["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"],
108+
["gcc", "-shared", "-fPIC", "-o", str(so), str(src), "-ldl"],
96109
check=True,
97110
capture_output=True,
98111
)
99112
src.unlink()
100-
return _SHIM_SO
113+
_shim_so = so
114+
return _shim_so
101115

102116

103117

.claude/skills/document-skills/pptx/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: pptx
33
description: "Use this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates (.potx), layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx or .potx filename, regardless of what they plan to do with the content afterward. If a .pptx or .potx file needs to be opened, created, or touched, use this skill."
44
license: Proprietary. LICENSE.txt has complete terms
55
metadata:
6-
version: "2.0"
6+
version: "2.1"
77
skill-author: Anthropic, PBC
88
source: https://github.com/anthropics/skills/tree/main/skills/pptx
99
---

.claude/skills/document-skills/pptx/scripts/office/soffice.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
callers that build their own argv (they must pass -env:UserInstallation too).
1616
"""
1717

18+
import atexit
1819
import contextlib
1920
import os
21+
import shutil
2022
import socket
2123
import subprocess
2224
import tempfile
@@ -73,7 +75,13 @@ def run_soffice(args: Iterable[str], **kwargs) -> subprocess.CompletedProcess:
7375

7476

7577

76-
_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so"
78+
#: Compiled once per process, not cached on disk between runs. An earlier version
79+
#: built the shim at a fixed /tmp/lo_socket_shim.so and reused whatever was already
80+
#: there, which let any local user pre-plant a shared object at that predictable
81+
#: path and get it LD_PRELOADed into every subsequent soffice run. mkdtemp gives an
82+
#: unpredictable directory created 0700 and owned by us, so neither the .c we
83+
#: compile nor the .so we load can be swapped by another user.
84+
_shim_so: Path | None = None
7785

7886

7987
def _needs_shim() -> bool:
@@ -86,18 +94,24 @@ def _needs_shim() -> bool:
8694

8795

8896
def _ensure_shim() -> Path:
89-
if _SHIM_SO.exists():
90-
return _SHIM_SO
97+
global _shim_so
98+
if _shim_so is not None and _shim_so.exists():
99+
return _shim_so
91100

92-
src = Path(tempfile.gettempdir()) / "lo_socket_shim.c"
101+
shim_dir = Path(tempfile.mkdtemp(prefix="lo-shim-"))
102+
atexit.register(shutil.rmtree, shim_dir, True)
103+
104+
src = shim_dir / "lo_socket_shim.c"
105+
so = shim_dir / "lo_socket_shim.so"
93106
src.write_text(_SHIM_SOURCE)
94107
subprocess.run(
95-
["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"],
108+
["gcc", "-shared", "-fPIC", "-o", str(so), str(src), "-ldl"],
96109
check=True,
97110
capture_output=True,
98111
)
99112
src.unlink()
100-
return _SHIM_SO
113+
_shim_so = so
114+
return _shim_so
101115

102116

103117

0 commit comments

Comments
 (0)