Skip to content

Commit 739c8eb

Browse files
committed
feat: warn when no LLM API key found instead of failing silently
1 parent 0bc0b44 commit 739c8eb

1 file changed

Lines changed: 90 additions & 22 deletions

File tree

openkb/cli.py

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
litellm.suppress_debug_info = True
2020
from dotenv import load_dotenv
2121

22-
from openkb.config import DEFAULT_CONFIG, load_config, save_config
22+
from openkb.config import DEFAULT_CONFIG, load_config, save_config, load_global_config, register_kb
2323
from openkb.converter import convert_document
2424
from openkb.log import append_log
2525
from openkb.schema import AGENTS_MD
@@ -30,8 +30,11 @@
3030
def _setup_llm_key(kb_dir: Path | None = None) -> None:
3131
"""Set LiteLLM API key from LLM_API_KEY env var if present.
3232
33-
If *kb_dir* is given, also loads ``.env`` from the KB root so that
34-
the key is found even when the CLI is invoked from another directory.
33+
Load order (override=False, so first one wins):
34+
1. System environment variables (already set)
35+
2. KB-local .env (kb_dir/.env)
36+
3. Global .env (~/.config/openkb/.env)
37+
3538
Also propagates to provider-specific env vars (OPENAI_API_KEY, etc.)
3639
so that the Agents SDK litellm provider can pick them up.
3740
"""
@@ -40,8 +43,23 @@ def _setup_llm_key(kb_dir: Path | None = None) -> None:
4043
if env_file.exists():
4144
load_dotenv(env_file, override=False)
4245

46+
from openkb.config import GLOBAL_CONFIG_DIR
47+
global_env = GLOBAL_CONFIG_DIR / ".env"
48+
if global_env.exists():
49+
load_dotenv(global_env, override=False)
50+
4351
api_key = os.environ.get("LLM_API_KEY", "")
44-
if api_key:
52+
if not api_key:
53+
# Check if any provider key is already set
54+
has_key = any(os.environ.get(k) for k in ("OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"))
55+
if not has_key:
56+
click.echo(
57+
"Warning: No LLM API key found. Set one of:\n"
58+
f" 1. {kb_dir / '.env' if kb_dir else '<kb_dir>/.env'} — LLM_API_KEY=sk-...\n"
59+
f" 2. {GLOBAL_CONFIG_DIR / '.env'} — LLM_API_KEY=sk-...\n"
60+
" 3. Export LLM_API_KEY in your shell profile"
61+
)
62+
else:
4563
litellm.api_key = api_key
4664
for env_var in ("OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"):
4765
if not os.environ.get(env_var):
@@ -74,11 +92,29 @@ def _display_type(raw_type: str) -> str:
7492
# Helpers
7593
# ---------------------------------------------------------------------------
7694

77-
def _find_kb_dir() -> Path | None:
78-
"""Return the knowledge-base root if .openkb/ exists in cwd, else None."""
79-
candidate = Path(".openkb")
80-
if candidate.exists() and candidate.is_dir():
81-
return Path(".")
95+
def _find_kb_dir(override: Path | None = None) -> Path | None:
96+
"""Find the KB root: explicit override → walk up from cwd → global default_kb."""
97+
# 0. Explicit override (--kb-dir or OPENKB_DIR)
98+
if override is not None:
99+
if (override / ".openkb").is_dir():
100+
return override
101+
return None
102+
# 1. Walk up from cwd
103+
current = Path.cwd().resolve()
104+
while True:
105+
if (current / ".openkb").is_dir():
106+
return current
107+
parent = current.parent
108+
if parent == current:
109+
break
110+
current = parent
111+
# 2. Fall back to global config default_kb
112+
gc = load_global_config()
113+
default = gc.get("default_kb")
114+
if default:
115+
p = Path(default)
116+
if (p / ".openkb").is_dir():
117+
return p
82118
return None
83119

84120

@@ -174,14 +210,37 @@ def _add_single_file(file_path: Path, kb_dir: Path) -> None:
174210

175211
@click.group()
176212
@click.option("-v", "--verbose", is_flag=True, default=False, help="Enable verbose logging.")
177-
def cli(verbose):
213+
@click.option("--kb-dir", "kb_dir_override", default=None, type=click.Path(exists=True, file_okay=False, resolve_path=True), help="Path to a KB root directory (overrides auto-detection).")
214+
@click.pass_context
215+
def cli(ctx, verbose, kb_dir_override):
178216
"""OpenKB — Karpathy's LLM Knowledge Base workflow, powered by PageIndex."""
179217
logging.basicConfig(
180218
format="%(name)s %(levelname)s: %(message)s",
181219
level=logging.WARNING,
182220
)
183221
if verbose:
184222
logging.getLogger("openkb").setLevel(logging.DEBUG)
223+
ctx.ensure_object(dict)
224+
if kb_dir_override:
225+
ctx.obj["kb_dir_override"] = Path(kb_dir_override)
226+
else:
227+
env_kb = os.environ.get("OPENKB_DIR")
228+
if env_kb:
229+
ctx.obj["kb_dir_override"] = Path(env_kb).resolve()
230+
else:
231+
ctx.obj["kb_dir_override"] = None
232+
233+
234+
@cli.command()
235+
@click.argument("path", default=".")
236+
def use(path):
237+
"""Set PATH as the default knowledge base."""
238+
target = Path(path).resolve()
239+
if not (target / ".openkb").is_dir():
240+
click.echo(f"Not a knowledge base: {target}")
241+
return
242+
register_kb(target)
243+
click.echo(f"Default KB set to: {target}")
185244

186245

187246
@cli.command()
@@ -229,14 +288,18 @@ def init():
229288
save_config(openkb_dir / "config.yaml", config)
230289
(openkb_dir / "hashes.json").write_text(json.dumps({}), encoding="utf-8")
231290

291+
# Register this KB in the global config
292+
register_kb(Path.cwd())
293+
232294
click.echo("Knowledge base initialised.")
233295

234296

235297
@cli.command()
236298
@click.argument("path")
237-
def add(path):
299+
@click.pass_context
300+
def add(ctx, path):
238301
"""Add a document or directory of documents at PATH to the knowledge base."""
239-
kb_dir = _find_kb_dir()
302+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
240303
if kb_dir is None:
241304
click.echo("No knowledge base found. Run `openkb init` first.")
242305
return
@@ -272,9 +335,10 @@ def add(path):
272335
@cli.command()
273336
@click.argument("question")
274337
@click.option("--save", is_flag=True, default=False, help="Save the answer to wiki/explorations/.")
275-
def query(question, save):
338+
@click.pass_context
339+
def query(ctx, question, save):
276340
"""Query the knowledge base with QUESTION."""
277-
kb_dir = _find_kb_dir()
341+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
278342
if kb_dir is None:
279343
click.echo("No knowledge base found. Run `openkb init` first.")
280344
return
@@ -307,9 +371,10 @@ def query(question, save):
307371

308372

309373
@cli.command()
310-
def watch():
374+
@click.pass_context
375+
def watch(ctx):
311376
"""Watch the raw/ directory for new documents and process them automatically."""
312-
kb_dir = _find_kb_dir()
377+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
313378
if kb_dir is None:
314379
click.echo("No knowledge base found. Run `openkb init` first.")
315380
return
@@ -336,11 +401,12 @@ def on_new_files(paths):
336401

337402
@cli.command()
338403
@click.option("--fix", is_flag=True, default=False, help="Automatically fix lint issues (not yet implemented).")
339-
def lint(fix):
404+
@click.pass_context
405+
def lint(ctx, fix):
340406
"""Lint the knowledge base for structural and semantic inconsistencies."""
341407
if fix:
342408
click.echo("Warning: --fix is not yet implemented. Running lint in report-only mode.")
343-
kb_dir = _find_kb_dir()
409+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
344410
if kb_dir is None:
345411
click.echo("No knowledge base found. Run `openkb init` first.")
346412
return
@@ -379,9 +445,10 @@ def lint(fix):
379445

380446

381447
@cli.command(name="list")
382-
def list_cmd():
448+
@click.pass_context
449+
def list_cmd(ctx):
383450
"""List all documents in the knowledge base."""
384-
kb_dir = _find_kb_dir()
451+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
385452
if kb_dir is None:
386453
click.echo("No knowledge base found. Run `openkb init` first.")
387454
return
@@ -439,9 +506,10 @@ def list_cmd():
439506

440507

441508
@cli.command()
442-
def status():
509+
@click.pass_context
510+
def status(ctx):
443511
"""Show the current status of the knowledge base."""
444-
kb_dir = _find_kb_dir()
512+
kb_dir = _find_kb_dir(ctx.obj.get("kb_dir_override"))
445513
if kb_dir is None:
446514
click.echo("No knowledge base found. Run `openkb init` first.")
447515
return

0 commit comments

Comments
 (0)