3535 with_project_context ,
3636)
3737from .permissions import PermissionGate , Policy , deny_all
38+ from .prompts import confirm
3839from .repl import Session
3940from .sandbox import (
4041 DEFAULT_CPUS ,
4647 SandboxError ,
4748)
4849from .subagent import SPAWN_TOOL , make_spawn_handler
49- from .tools import TOOLS
50+ from .tools import tools_for
5051from .ui import FAIL , Renderer , make_approver , turn_footer , use_utf8_stdout
5152
5253SUBCOMMANDS = {"login" , "logout" , "auth" , "doctor" }
@@ -195,7 +196,9 @@ def wants_sandbox(args: argparse.Namespace) -> bool:
195196 return bool (args .sandbox or args .container or args .mount or args .no_network )
196197
197198
198- def make_executor (args : argparse .Namespace , console : Console ) -> tuple [Any , list ]:
199+ def make_executor (
200+ args : argparse .Namespace , console : Console , renderer : Renderer | None = None
201+ ) -> tuple [Any , list ]:
199202 if not wants_sandbox (args ):
200203 root = Path (args .workdir ).resolve ()
201204 inner = LocalExecutor (root )
@@ -209,7 +212,7 @@ def make_executor(args: argparse.Namespace, console: Console) -> tuple[Any, list
209212 approver = deny_all # unreachable while yes_to_everything is set
210213 elif sys .stdin .isatty ():
211214 policy = Policy ()
212- approver = make_approver (console )
215+ approver = make_approver (console , renderer )
213216 else :
214217 # Nothing can be asked, so nothing destructive may happen. Silently
215218 # approving here is how an automated run rewrites someone's files.
@@ -246,6 +249,10 @@ def build_agent_extras(
246249 """The optional bits: project instructions and sub-agent delegation."""
247250 extras : dict [str , Any ] = {}
248251
252+ # Always set, because the schemas one provider requires are the ones
253+ # another rejects. Whichever provider the user picked has to work.
254+ extras ["tools" ] = tools_for (getattr (args , "provider" , None ) or default_provider ())
255+
249256 if getattr (args , "context" , True ):
250257 context , source = load_project_context (
251258 "." if wants_sandbox (args ) else args .workdir
@@ -255,7 +262,7 @@ def build_agent_extras(
255262 console .print (f"[dim]using project instructions from { source } [/dim]" )
256263
257264 if getattr (args , "subagents" , False ):
258- extras ["tools" ] = [* TOOLS , SPAWN_TOOL ]
265+ extras ["tools" ] = [* extras [ "tools" ] , SPAWN_TOOL ]
259266 extras ["extra_tool_handlers" ] = {
260267 "spawn_subagent" : make_spawn_handler (
261268 executor , client , model , context_budget = args .context_budget
@@ -264,9 +271,14 @@ def build_agent_extras(
264271 return extras
265272
266273
267- def run_once (args : argparse .Namespace , executor : Any , client : Any , model : str ) -> int :
268- console = Console (quiet = args .quiet and not args .json )
269- renderer = Renderer (console , show_steps = args .steps )
274+ def run_once (
275+ args : argparse .Namespace ,
276+ executor : Any ,
277+ client : Any ,
278+ model : str ,
279+ console : Console ,
280+ renderer : Renderer ,
281+ ) -> int :
270282 started = time .monotonic ()
271283 try :
272284 result = agent_loop (
@@ -333,9 +345,12 @@ def main(argv: list[str] | None = None) -> int:
333345 # .env is a convenience for running from a checkout; installed users have a
334346 # saved login instead. Optional so the package does not hard-depend on it.
335347 try :
336- from dotenv import load_dotenv
348+ from dotenv import find_dotenv , load_dotenv
337349
338- load_dotenv ()
350+ # usecwd, because the default searches upward from *this file*. Inside
351+ # a pipx install that is site-packages, so an installed dietcode never
352+ # saw the .env sitting in the directory the user was standing in.
353+ load_dotenv (find_dotenv (usecwd = True ))
339354 except ImportError :
340355 pass
341356
@@ -356,8 +371,27 @@ def main(argv: list[str] | None = None) -> int:
356371
357372 try :
358373 api_key , base_url , model = resolve_model_config (args )
374+ except AuthError as exc :
375+ # Dead-ending on "no credentials" makes the user go read the help, come
376+ # back, and run a different command. Offer to fix it right here instead.
377+ if not sys .stdin .isatty ():
378+ console .print (f"[red]{ exc } [/red]" )
379+ return 2
380+ console .print (f"[orange3]{ exc } [/orange3]\n " )
381+ if not confirm (console , "Set one up now?" ):
382+ return 2
383+ if login (console , args .provider , None ) != 0 :
384+ return 2
385+ try :
386+ api_key , base_url , model = resolve_model_config (args )
387+ except AuthError as retry_exc :
388+ console .print (f"[red]{ retry_exc } [/red]" )
389+ return 2
390+ console .print ()
391+
392+ try :
359393 client = make_client (api_key = api_key , base_url = base_url )
360- except ( AuthError , RuntimeError ) as exc :
394+ except RuntimeError as exc :
361395 console .print (f"[red]{ exc } [/red]" )
362396 return 2
363397
@@ -368,8 +402,9 @@ def main(argv: list[str] | None = None) -> int:
368402 if stale :
369403 console .print (f"[dim]cleaned up { stale } orphaned container(s)[/dim]" )
370404
405+ renderer = Renderer (console , show_steps = args .steps )
371406 try :
372- executor , mounts = make_executor (args , console )
407+ executor , mounts = make_executor (args , console , renderer )
373408 except SandboxError as exc :
374409 # Docker is optional now that --here exists, so a missing daemon should
375410 # be a signpost rather than a dead end.
@@ -385,7 +420,7 @@ def main(argv: list[str] | None = None) -> int:
385420
386421 try :
387422 if args .task :
388- return run_once (args , executor , client , model )
423+ return run_once (args , executor , client , model , console , renderer )
389424 return Session (
390425 executor ,
391426 client ,
@@ -399,6 +434,7 @@ def main(argv: list[str] | None = None) -> int:
399434 max_total_tokens = args .max_tokens ,
400435 provider = args .provider or default_provider (),
401436 extras = build_agent_extras (args , executor , client , model , console ),
437+ renderer = renderer ,
402438 ).run ()
403439 finally :
404440 executor .close ()
0 commit comments