1919litellm .suppress_debug_info = True
2020from 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
2323from openkb .converter import convert_document
2424from openkb .log import append_log
2525from openkb .schema import AGENTS_MD
3030def _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