Add private config setup for Zotero workflow
This commit is contained in:
parent
70978a3642
commit
ecb090e613
|
|
@ -1,6 +1,8 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
.env
|
||||
config/config.local.json
|
||||
config/*.secret.json
|
||||
*.log
|
||||
*.tmp
|
||||
.zotero-ai-notes-index.json
|
||||
|
|
|
|||
19
SKILL.md
19
SKILL.md
|
|
@ -10,6 +10,7 @@ Use this skill for the user's Zotero + Obsidian literature workflow. It covers Z
|
|||
Core scripts:
|
||||
|
||||
```powershell
|
||||
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\init_private_config.py"
|
||||
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\generate_zotero_ai_note.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-key SXAIQUJT --skip-existing
|
||||
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\audit_zotero_ai_notes.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --keys-only
|
||||
```
|
||||
|
|
@ -22,10 +23,22 @@ py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\audit_zote
|
|||
- `01 ...md` research-article template
|
||||
- `02 ...md` review-article template
|
||||
- `03 ...md` AI prompt
|
||||
- `ZOTERO_API_KEY` must be available in the process environment for Zotero Web API writes.
|
||||
- LLM settings come from `AWESOMEGPT_API_KEY`, `AWESOMEGPT_BASE_URL`, `AWESOMEGPT_MODEL`, or AwesomeGPT preferences in the default Zotero profile.
|
||||
- `ZOTERO_API_KEY` must be available for Zotero Web API writes.
|
||||
- LLM settings come from `AWESOMEGPT_API_KEY`, `AWESOMEGPT_BASE_URL`, `AWESOMEGPT_MODEL`, the private local config, or AwesomeGPT preferences in the default Zotero profile.
|
||||
|
||||
Never store API keys in the skill, vault helper files, Git commits, zip files, terminal output, or chat. If keys appear in chat or logs, tell the user to rotate them.
|
||||
Never store API keys in `SKILL.md`, references, committed files, zip files, terminal output, or chat. If keys appear in chat or logs, tell the user to rotate them.
|
||||
|
||||
## Private Config
|
||||
|
||||
Use a local-only config for Zotero and DeepSeek/AwesomeGPT keys. The committed template is `config/config.template.json`; the real file is `config/config.local.json`, which is ignored by Git.
|
||||
|
||||
Initialize or update it with hidden prompts:
|
||||
|
||||
```powershell
|
||||
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\init_private_config.py"
|
||||
```
|
||||
|
||||
Do not print the config contents. When checking setup, report only whether each key is present. Environment variables override the private config; the private config overrides AwesomeGPT preferences for fields that are present.
|
||||
|
||||
## Zotero Note Workflows
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"zotero": {
|
||||
"api_key": "",
|
||||
"user_id": ""
|
||||
},
|
||||
"awesomegpt": {
|
||||
"api_key": "",
|
||||
"base_url": "https://api.deepseek.com",
|
||||
"model": "deepseek-v4-pro"
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ Required environment variables:
|
|||
|
||||
Optional:
|
||||
ZOTERO_USER_ID If omitted, resolved from /keys/current
|
||||
|
||||
Private local config:
|
||||
<skill>\config\config.local.json, created by scripts\init_private_config.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -37,6 +40,8 @@ except Exception:
|
|||
LOCAL_ZOTERO = "http://127.0.0.1:23119/api/users/0"
|
||||
ZOTERO_WEB = "https://api.zotero.org"
|
||||
DEFAULT_VAULT = Path.cwd()
|
||||
SKILL_DIR = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_PRIVATE_CONFIG = SKILL_DIR / "config" / "config.local.json"
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
|
|
@ -57,6 +62,27 @@ def load_dotenv(path: Path) -> None:
|
|||
os.environ.setdefault(key, value)
|
||||
|
||||
|
||||
def load_private_config(path: Path) -> None:
|
||||
if not path.exists():
|
||||
return
|
||||
try:
|
||||
config = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
fail(f"invalid private config JSON at {path}: {exc}")
|
||||
zotero = config.get("zotero", {}) if isinstance(config.get("zotero"), dict) else {}
|
||||
awesomegpt = config.get("awesomegpt", {}) if isinstance(config.get("awesomegpt"), dict) else {}
|
||||
mappings = {
|
||||
"ZOTERO_API_KEY": zotero.get("api_key"),
|
||||
"ZOTERO_USER_ID": zotero.get("user_id"),
|
||||
"AWESOMEGPT_API_KEY": awesomegpt.get("api_key"),
|
||||
"AWESOMEGPT_BASE_URL": awesomegpt.get("base_url"),
|
||||
"AWESOMEGPT_MODEL": awesomegpt.get("model"),
|
||||
}
|
||||
for key, value in mappings.items():
|
||||
if value:
|
||||
os.environ.setdefault(key, str(value))
|
||||
|
||||
|
||||
def zotero_profile_prefs() -> Path | None:
|
||||
profiles_ini = Path.home() / "AppData/Roaming/Zotero/Zotero/profiles.ini"
|
||||
profiles_root = profiles_ini.parent
|
||||
|
|
@ -502,10 +528,12 @@ def main() -> None:
|
|||
parser.add_argument("--dry-run", action="store_true", help="generate but do not write Zotero note")
|
||||
parser.add_argument("--vault", default=str(DEFAULT_VAULT), help="Obsidian vault containing 00 Templater")
|
||||
parser.add_argument("--env-file", help="Optional .env path; defaults to <vault>/.env")
|
||||
parser.add_argument("--config", default=str(DEFAULT_PRIVATE_CONFIG), help="Private local config JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
vault = Path(args.vault).expanduser().resolve()
|
||||
load_dotenv(Path(args.env_file).expanduser().resolve() if args.env_file else vault / ".env")
|
||||
load_private_config(Path(args.config).expanduser().resolve())
|
||||
load_awesomegpt_prefs()
|
||||
|
||||
keys = list(args.item_key)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_CONFIG = SKILL_DIR / "config" / "config.local.json"
|
||||
|
||||
|
||||
def ask_secret(label: str, existing: str = "") -> str:
|
||||
suffix = " [keep existing]" if existing else ""
|
||||
value = getpass.getpass(f"{label}{suffix}: ").strip()
|
||||
return value or existing
|
||||
|
||||
|
||||
def ask_text(label: str, default: str = "") -> str:
|
||||
suffix = f" [{default}]" if default else ""
|
||||
value = input(f"{label}{suffix}: ").strip()
|
||||
return value or default
|
||||
|
||||
|
||||
def load_existing(path: Path) -> dict:
|
||||
if not path.exists():
|
||||
return {}
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_private_config(path: Path, data: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
try:
|
||||
os.chmod(path, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Create private local config for zotero-obsidian-literature.")
|
||||
parser.add_argument("--config", default=str(DEFAULT_CONFIG))
|
||||
args = parser.parse_args()
|
||||
|
||||
path = Path(args.config).expanduser().resolve()
|
||||
existing = load_existing(path)
|
||||
zotero = existing.get("zotero", {}) if isinstance(existing.get("zotero"), dict) else {}
|
||||
llm = existing.get("awesomegpt", {}) if isinstance(existing.get("awesomegpt"), dict) else {}
|
||||
|
||||
data = {
|
||||
"zotero": {
|
||||
"api_key": ask_secret("Zotero Web API key", zotero.get("api_key", "")),
|
||||
"user_id": ask_text("Zotero user ID, optional", zotero.get("user_id", "")),
|
||||
},
|
||||
"awesomegpt": {
|
||||
"api_key": ask_secret("DeepSeek/AwesomeGPT API key", llm.get("api_key", "")),
|
||||
"base_url": ask_text("DeepSeek/AwesomeGPT base URL", llm.get("base_url", "https://api.deepseek.com")),
|
||||
"model": ask_text("DeepSeek/AwesomeGPT model", llm.get("model", "deepseek-v4-pro")),
|
||||
},
|
||||
}
|
||||
write_private_config(path, data)
|
||||
print(f"Wrote private config: {path}")
|
||||
print("Do not commit this file. It is ignored by the skill .gitignore.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Reference in New Issue