main.rs 154 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608
  1. mod init;
  2. mod input;
  3. mod render;
  4. use std::collections::BTreeSet;
  5. use std::env;
  6. use std::fmt::Write as _;
  7. use std::fs;
  8. use std::io::{self, Read, Write};
  9. use std::net::TcpListener;
  10. use std::path::{Path, PathBuf};
  11. use std::process::Command;
  12. use std::sync::mpsc::{self, RecvTimeoutError};
  13. use std::sync::{Arc, Mutex};
  14. use std::thread;
  15. use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
  16. use api::{
  17. resolve_startup_auth_source, AnthropicClient, AuthSource, ContentBlockDelta, InputContentBlock,
  18. InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
  19. StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
  20. };
  21. use commands::{
  22. handle_plugins_slash_command, render_slash_command_help, resume_supported_slash_commands,
  23. slash_command_specs, SlashCommand,
  24. };
  25. use compat_harness::{extract_manifest, UpstreamPaths};
  26. use init::initialize_repo;
  27. use plugins::{PluginManager, PluginManagerConfig, PluginRegistry};
  28. use render::{MarkdownStreamState, Spinner, TerminalRenderer};
  29. use runtime::{
  30. clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
  31. parse_oauth_callback_request_target, save_oauth_credentials, ApiClient, ApiRequest,
  32. AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock,
  33. ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig,
  34. OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, RuntimeError,
  35. Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
  36. };
  37. use serde_json::json;
  38. use tools::GlobalToolRegistry;
  39. const DEFAULT_MODEL: &str = "claude-opus-4-6";
  40. fn max_tokens_for_model(model: &str) -> u32 {
  41. if model.contains("opus") {
  42. 32_000
  43. } else {
  44. 64_000
  45. }
  46. }
  47. const DEFAULT_DATE: &str = "2026-03-31";
  48. const DEFAULT_OAUTH_CALLBACK_PORT: u16 = 4545;
  49. const VERSION: &str = env!("CARGO_PKG_VERSION");
  50. const BUILD_TARGET: Option<&str> = option_env!("TARGET");
  51. const GIT_SHA: Option<&str> = option_env!("GIT_SHA");
  52. const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3);
  53. type AllowedToolSet = BTreeSet<String>;
  54. fn main() {
  55. if let Err(error) = run() {
  56. eprintln!(
  57. "error: {error}
  58. Run `claw --help` for usage."
  59. );
  60. std::process::exit(1);
  61. }
  62. }
  63. fn run() -> Result<(), Box<dyn std::error::Error>> {
  64. let args: Vec<String> = env::args().skip(1).collect();
  65. match parse_args(&args)? {
  66. CliAction::DumpManifests => dump_manifests(),
  67. CliAction::BootstrapPlan => print_bootstrap_plan(),
  68. CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
  69. CliAction::Version => print_version(),
  70. CliAction::ResumeSession {
  71. session_path,
  72. commands,
  73. } => resume_session(&session_path, &commands),
  74. CliAction::Prompt {
  75. prompt,
  76. model,
  77. output_format,
  78. allowed_tools,
  79. permission_mode,
  80. } => LiveCli::new(model, true, allowed_tools, permission_mode)?
  81. .run_turn_with_output(&prompt, output_format)?,
  82. CliAction::Login => run_login()?,
  83. CliAction::Logout => run_logout()?,
  84. CliAction::Init => run_init()?,
  85. CliAction::Repl {
  86. model,
  87. allowed_tools,
  88. permission_mode,
  89. } => run_repl(model, allowed_tools, permission_mode)?,
  90. CliAction::Help => print_help(),
  91. }
  92. Ok(())
  93. }
  94. #[derive(Debug, Clone, PartialEq, Eq)]
  95. enum CliAction {
  96. DumpManifests,
  97. BootstrapPlan,
  98. PrintSystemPrompt {
  99. cwd: PathBuf,
  100. date: String,
  101. },
  102. Version,
  103. ResumeSession {
  104. session_path: PathBuf,
  105. commands: Vec<String>,
  106. },
  107. Prompt {
  108. prompt: String,
  109. model: String,
  110. output_format: CliOutputFormat,
  111. allowed_tools: Option<AllowedToolSet>,
  112. permission_mode: PermissionMode,
  113. },
  114. Login,
  115. Logout,
  116. Init,
  117. Repl {
  118. model: String,
  119. allowed_tools: Option<AllowedToolSet>,
  120. permission_mode: PermissionMode,
  121. },
  122. // prompt-mode formatting is only supported for non-interactive runs
  123. Help,
  124. }
  125. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  126. enum CliOutputFormat {
  127. Text,
  128. Json,
  129. }
  130. impl CliOutputFormat {
  131. fn parse(value: &str) -> Result<Self, String> {
  132. match value {
  133. "text" => Ok(Self::Text),
  134. "json" => Ok(Self::Json),
  135. other => Err(format!(
  136. "unsupported value for --output-format: {other} (expected text or json)"
  137. )),
  138. }
  139. }
  140. }
  141. #[allow(clippy::too_many_lines)]
  142. fn parse_args(args: &[String]) -> Result<CliAction, String> {
  143. let mut model = DEFAULT_MODEL.to_string();
  144. let mut output_format = CliOutputFormat::Text;
  145. let mut permission_mode = default_permission_mode();
  146. let mut wants_version = false;
  147. let mut allowed_tool_values = Vec::new();
  148. let mut rest = Vec::new();
  149. let mut index = 0;
  150. while index < args.len() {
  151. match args[index].as_str() {
  152. "--version" | "-V" => {
  153. wants_version = true;
  154. index += 1;
  155. }
  156. "--model" => {
  157. let value = args
  158. .get(index + 1)
  159. .ok_or_else(|| "missing value for --model".to_string())?;
  160. model = resolve_model_alias(value).to_string();
  161. index += 2;
  162. }
  163. flag if flag.starts_with("--model=") => {
  164. model = resolve_model_alias(&flag[8..]).to_string();
  165. index += 1;
  166. }
  167. "--output-format" => {
  168. let value = args
  169. .get(index + 1)
  170. .ok_or_else(|| "missing value for --output-format".to_string())?;
  171. output_format = CliOutputFormat::parse(value)?;
  172. index += 2;
  173. }
  174. "--permission-mode" => {
  175. let value = args
  176. .get(index + 1)
  177. .ok_or_else(|| "missing value for --permission-mode".to_string())?;
  178. permission_mode = parse_permission_mode_arg(value)?;
  179. index += 2;
  180. }
  181. flag if flag.starts_with("--output-format=") => {
  182. output_format = CliOutputFormat::parse(&flag[16..])?;
  183. index += 1;
  184. }
  185. flag if flag.starts_with("--permission-mode=") => {
  186. permission_mode = parse_permission_mode_arg(&flag[18..])?;
  187. index += 1;
  188. }
  189. "--dangerously-skip-permissions" => {
  190. permission_mode = PermissionMode::DangerFullAccess;
  191. index += 1;
  192. }
  193. "-p" => {
  194. // Claw Code compat: -p "prompt" = one-shot prompt
  195. let prompt = args[index + 1..].join(" ");
  196. if prompt.trim().is_empty() {
  197. return Err("-p requires a prompt string".to_string());
  198. }
  199. return Ok(CliAction::Prompt {
  200. prompt,
  201. model: resolve_model_alias(&model).to_string(),
  202. output_format,
  203. allowed_tools: normalize_allowed_tools(&allowed_tool_values)?,
  204. permission_mode,
  205. });
  206. }
  207. "--print" => {
  208. // Claw Code compat: --print makes output non-interactive
  209. output_format = CliOutputFormat::Text;
  210. index += 1;
  211. }
  212. "--allowedTools" | "--allowed-tools" => {
  213. let value = args
  214. .get(index + 1)
  215. .ok_or_else(|| "missing value for --allowedTools".to_string())?;
  216. allowed_tool_values.push(value.clone());
  217. index += 2;
  218. }
  219. flag if flag.starts_with("--allowedTools=") => {
  220. allowed_tool_values.push(flag[15..].to_string());
  221. index += 1;
  222. }
  223. flag if flag.starts_with("--allowed-tools=") => {
  224. allowed_tool_values.push(flag[16..].to_string());
  225. index += 1;
  226. }
  227. other => {
  228. rest.push(other.to_string());
  229. index += 1;
  230. }
  231. }
  232. }
  233. if wants_version {
  234. return Ok(CliAction::Version);
  235. }
  236. let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
  237. if rest.is_empty() {
  238. return Ok(CliAction::Repl {
  239. model,
  240. allowed_tools,
  241. permission_mode,
  242. });
  243. }
  244. if matches!(rest.first().map(String::as_str), Some("--help" | "-h")) {
  245. return Ok(CliAction::Help);
  246. }
  247. if rest.first().map(String::as_str) == Some("--resume") {
  248. return parse_resume_args(&rest[1..]);
  249. }
  250. match rest[0].as_str() {
  251. "dump-manifests" => Ok(CliAction::DumpManifests),
  252. "bootstrap-plan" => Ok(CliAction::BootstrapPlan),
  253. "system-prompt" => parse_system_prompt_args(&rest[1..]),
  254. "login" => Ok(CliAction::Login),
  255. "logout" => Ok(CliAction::Logout),
  256. "init" => Ok(CliAction::Init),
  257. "prompt" => {
  258. let prompt = rest[1..].join(" ");
  259. if prompt.trim().is_empty() {
  260. return Err("prompt subcommand requires a prompt string".to_string());
  261. }
  262. Ok(CliAction::Prompt {
  263. prompt,
  264. model,
  265. output_format,
  266. allowed_tools,
  267. permission_mode,
  268. })
  269. }
  270. other if !other.starts_with('/') => Ok(CliAction::Prompt {
  271. prompt: rest.join(" "),
  272. model,
  273. output_format,
  274. allowed_tools,
  275. permission_mode,
  276. }),
  277. other => Err(format!("unknown subcommand: {other}")),
  278. }
  279. }
  280. fn resolve_model_alias(model: &str) -> &str {
  281. match model {
  282. "opus" => "claude-opus-4-6",
  283. "sonnet" => "claude-sonnet-4-6",
  284. "haiku" => "claude-haiku-4-5-20251213",
  285. _ => model,
  286. }
  287. }
  288. fn normalize_allowed_tools(values: &[String]) -> Result<Option<AllowedToolSet>, String> {
  289. current_tool_registry()?.normalize_allowed_tools(values)
  290. }
  291. fn current_tool_registry() -> Result<GlobalToolRegistry, String> {
  292. let cwd = env::current_dir().map_err(|error| error.to_string())?;
  293. let loader = ConfigLoader::default_for(&cwd);
  294. let runtime_config = loader.load().map_err(|error| error.to_string())?;
  295. let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
  296. let plugin_tools = plugin_manager
  297. .aggregated_tools()
  298. .map_err(|error| error.to_string())?;
  299. GlobalToolRegistry::with_plugin_tools(plugin_tools)
  300. }
  301. fn parse_permission_mode_arg(value: &str) -> Result<PermissionMode, String> {
  302. normalize_permission_mode(value)
  303. .ok_or_else(|| {
  304. format!(
  305. "unsupported permission mode '{value}'. Use read-only, workspace-write, or danger-full-access."
  306. )
  307. })
  308. .map(permission_mode_from_label)
  309. }
  310. fn permission_mode_from_label(mode: &str) -> PermissionMode {
  311. match mode {
  312. "read-only" => PermissionMode::ReadOnly,
  313. "workspace-write" => PermissionMode::WorkspaceWrite,
  314. "danger-full-access" => PermissionMode::DangerFullAccess,
  315. other => panic!("unsupported permission mode label: {other}"),
  316. }
  317. }
  318. fn default_permission_mode() -> PermissionMode {
  319. env::var("RUSTY_CLAUDE_PERMISSION_MODE")
  320. .ok()
  321. .as_deref()
  322. .and_then(normalize_permission_mode)
  323. .map_or(PermissionMode::DangerFullAccess, permission_mode_from_label)
  324. }
  325. fn filter_tool_specs(
  326. tool_registry: &GlobalToolRegistry,
  327. allowed_tools: Option<&AllowedToolSet>,
  328. ) -> Vec<ToolDefinition> {
  329. tool_registry.definitions(allowed_tools)
  330. }
  331. fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
  332. let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
  333. let mut date = DEFAULT_DATE.to_string();
  334. let mut index = 0;
  335. while index < args.len() {
  336. match args[index].as_str() {
  337. "--cwd" => {
  338. let value = args
  339. .get(index + 1)
  340. .ok_or_else(|| "missing value for --cwd".to_string())?;
  341. cwd = PathBuf::from(value);
  342. index += 2;
  343. }
  344. "--date" => {
  345. let value = args
  346. .get(index + 1)
  347. .ok_or_else(|| "missing value for --date".to_string())?;
  348. date.clone_from(value);
  349. index += 2;
  350. }
  351. other => return Err(format!("unknown system-prompt option: {other}")),
  352. }
  353. }
  354. Ok(CliAction::PrintSystemPrompt { cwd, date })
  355. }
  356. fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
  357. let session_path = args
  358. .first()
  359. .ok_or_else(|| "missing session path for --resume".to_string())
  360. .map(PathBuf::from)?;
  361. let commands = args[1..].to_vec();
  362. if commands
  363. .iter()
  364. .any(|command| !command.trim_start().starts_with('/'))
  365. {
  366. return Err("--resume trailing arguments must be slash commands".to_string());
  367. }
  368. Ok(CliAction::ResumeSession {
  369. session_path,
  370. commands,
  371. })
  372. }
  373. fn dump_manifests() {
  374. let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
  375. let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
  376. match extract_manifest(&paths) {
  377. Ok(manifest) => {
  378. println!("commands: {}", manifest.commands.entries().len());
  379. println!("tools: {}", manifest.tools.entries().len());
  380. println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
  381. }
  382. Err(error) => {
  383. eprintln!("failed to extract manifests: {error}");
  384. std::process::exit(1);
  385. }
  386. }
  387. }
  388. fn print_bootstrap_plan() {
  389. for phase in runtime::BootstrapPlan::claude_code_default().phases() {
  390. println!("- {phase:?}");
  391. }
  392. }
  393. fn default_oauth_config() -> OAuthConfig {
  394. OAuthConfig {
  395. client_id: String::from("9d1c250a-e61b-44d9-88ed-5944d1962f5e"),
  396. authorize_url: String::from("https://platform.claude.com/oauth/authorize"),
  397. token_url: String::from("https://platform.claude.com/v1/oauth/token"),
  398. callback_port: None,
  399. manual_redirect_url: None,
  400. scopes: vec![
  401. String::from("user:profile"),
  402. String::from("user:inference"),
  403. String::from("user:sessions:claude_code"),
  404. ],
  405. }
  406. }
  407. fn run_login() -> Result<(), Box<dyn std::error::Error>> {
  408. let cwd = env::current_dir()?;
  409. let config = ConfigLoader::default_for(&cwd).load()?;
  410. let default_oauth = default_oauth_config();
  411. let oauth = config.oauth().unwrap_or(&default_oauth);
  412. let callback_port = oauth.callback_port.unwrap_or(DEFAULT_OAUTH_CALLBACK_PORT);
  413. let redirect_uri = runtime::loopback_redirect_uri(callback_port);
  414. let pkce = generate_pkce_pair()?;
  415. let state = generate_state()?;
  416. let authorize_url =
  417. OAuthAuthorizationRequest::from_config(oauth, redirect_uri.clone(), state.clone(), &pkce)
  418. .build_url();
  419. println!("Starting Claude OAuth login...");
  420. println!("Listening for callback on {redirect_uri}");
  421. if let Err(error) = open_browser(&authorize_url) {
  422. eprintln!("warning: failed to open browser automatically: {error}");
  423. println!("Open this URL manually:\n{authorize_url}");
  424. }
  425. let callback = wait_for_oauth_callback(callback_port)?;
  426. if let Some(error) = callback.error {
  427. let description = callback
  428. .error_description
  429. .unwrap_or_else(|| "authorization failed".to_string());
  430. return Err(io::Error::other(format!("{error}: {description}")).into());
  431. }
  432. let code = callback.code.ok_or_else(|| {
  433. io::Error::new(io::ErrorKind::InvalidData, "callback did not include code")
  434. })?;
  435. let returned_state = callback.state.ok_or_else(|| {
  436. io::Error::new(io::ErrorKind::InvalidData, "callback did not include state")
  437. })?;
  438. if returned_state != state {
  439. return Err(io::Error::new(io::ErrorKind::InvalidData, "oauth state mismatch").into());
  440. }
  441. let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(api::read_base_url());
  442. let exchange_request =
  443. OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri);
  444. let runtime = tokio::runtime::Runtime::new()?;
  445. let token_set = runtime.block_on(client.exchange_oauth_code(oauth, &exchange_request))?;
  446. save_oauth_credentials(&runtime::OAuthTokenSet {
  447. access_token: token_set.access_token,
  448. refresh_token: token_set.refresh_token,
  449. expires_at: token_set.expires_at,
  450. scopes: token_set.scopes,
  451. })?;
  452. println!("Claude OAuth login complete.");
  453. Ok(())
  454. }
  455. fn run_logout() -> Result<(), Box<dyn std::error::Error>> {
  456. clear_oauth_credentials()?;
  457. println!("Claude OAuth credentials cleared.");
  458. Ok(())
  459. }
  460. fn open_browser(url: &str) -> io::Result<()> {
  461. let commands = if cfg!(target_os = "macos") {
  462. vec![("open", vec![url])]
  463. } else if cfg!(target_os = "windows") {
  464. vec![("cmd", vec!["/C", "start", "", url])]
  465. } else {
  466. vec![("xdg-open", vec![url])]
  467. };
  468. for (program, args) in commands {
  469. match Command::new(program).args(args).spawn() {
  470. Ok(_) => return Ok(()),
  471. Err(error) if error.kind() == io::ErrorKind::NotFound => {}
  472. Err(error) => return Err(error),
  473. }
  474. }
  475. Err(io::Error::new(
  476. io::ErrorKind::NotFound,
  477. "no supported browser opener command found",
  478. ))
  479. }
  480. fn wait_for_oauth_callback(
  481. port: u16,
  482. ) -> Result<runtime::OAuthCallbackParams, Box<dyn std::error::Error>> {
  483. let listener = TcpListener::bind(("127.0.0.1", port))?;
  484. let (mut stream, _) = listener.accept()?;
  485. let mut buffer = [0_u8; 4096];
  486. let bytes_read = stream.read(&mut buffer)?;
  487. let request = String::from_utf8_lossy(&buffer[..bytes_read]);
  488. let request_line = request.lines().next().ok_or_else(|| {
  489. io::Error::new(io::ErrorKind::InvalidData, "missing callback request line")
  490. })?;
  491. let target = request_line.split_whitespace().nth(1).ok_or_else(|| {
  492. io::Error::new(
  493. io::ErrorKind::InvalidData,
  494. "missing callback request target",
  495. )
  496. })?;
  497. let callback = parse_oauth_callback_request_target(target)
  498. .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
  499. let body = if callback.error.is_some() {
  500. "Claude OAuth login failed. You can close this window."
  501. } else {
  502. "Claude OAuth login succeeded. You can close this window."
  503. };
  504. let response = format!(
  505. "HTTP/1.1 200 OK\r\ncontent-type: text/plain; charset=utf-8\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
  506. body.len(),
  507. body
  508. );
  509. stream.write_all(response.as_bytes())?;
  510. Ok(callback)
  511. }
  512. fn print_system_prompt(cwd: PathBuf, date: String) {
  513. match load_system_prompt(cwd, date, env::consts::OS, "unknown") {
  514. Ok(sections) => println!("{}", sections.join("\n\n")),
  515. Err(error) => {
  516. eprintln!("failed to build system prompt: {error}");
  517. std::process::exit(1);
  518. }
  519. }
  520. }
  521. fn print_version() {
  522. println!("{}", render_version_report());
  523. }
  524. fn resume_session(session_path: &Path, commands: &[String]) {
  525. let session = match Session::load_from_path(session_path) {
  526. Ok(session) => session,
  527. Err(error) => {
  528. eprintln!("failed to restore session: {error}");
  529. std::process::exit(1);
  530. }
  531. };
  532. if commands.is_empty() {
  533. println!(
  534. "Restored session from {} ({} messages).",
  535. session_path.display(),
  536. session.messages.len()
  537. );
  538. return;
  539. }
  540. let mut session = session;
  541. for raw_command in commands {
  542. let Some(command) = SlashCommand::parse(raw_command) else {
  543. eprintln!("unsupported resumed command: {raw_command}");
  544. std::process::exit(2);
  545. };
  546. match run_resume_command(session_path, &session, &command) {
  547. Ok(ResumeCommandOutcome {
  548. session: next_session,
  549. message,
  550. }) => {
  551. session = next_session;
  552. if let Some(message) = message {
  553. println!("{message}");
  554. }
  555. }
  556. Err(error) => {
  557. eprintln!("{error}");
  558. std::process::exit(2);
  559. }
  560. }
  561. }
  562. }
  563. #[derive(Debug, Clone)]
  564. struct ResumeCommandOutcome {
  565. session: Session,
  566. message: Option<String>,
  567. }
  568. #[derive(Debug, Clone)]
  569. struct StatusContext {
  570. cwd: PathBuf,
  571. session_path: Option<PathBuf>,
  572. loaded_config_files: usize,
  573. discovered_config_files: usize,
  574. memory_file_count: usize,
  575. project_root: Option<PathBuf>,
  576. git_branch: Option<String>,
  577. }
  578. #[derive(Debug, Clone, Copy)]
  579. struct StatusUsage {
  580. message_count: usize,
  581. turns: u32,
  582. latest: TokenUsage,
  583. cumulative: TokenUsage,
  584. estimated_tokens: usize,
  585. }
  586. fn format_model_report(model: &str, message_count: usize, turns: u32) -> String {
  587. format!(
  588. "Model
  589. Current model {model}
  590. Session messages {message_count}
  591. Session turns {turns}
  592. Usage
  593. Inspect current model with /model
  594. Switch models with /model <name>"
  595. )
  596. }
  597. fn format_model_switch_report(previous: &str, next: &str, message_count: usize) -> String {
  598. format!(
  599. "Model updated
  600. Previous {previous}
  601. Current {next}
  602. Preserved msgs {message_count}"
  603. )
  604. }
  605. fn format_permissions_report(mode: &str) -> String {
  606. let modes = [
  607. ("read-only", "Read/search tools only", mode == "read-only"),
  608. (
  609. "workspace-write",
  610. "Edit files inside the workspace",
  611. mode == "workspace-write",
  612. ),
  613. (
  614. "danger-full-access",
  615. "Unrestricted tool access",
  616. mode == "danger-full-access",
  617. ),
  618. ]
  619. .into_iter()
  620. .map(|(name, description, is_current)| {
  621. let marker = if is_current {
  622. "● current"
  623. } else {
  624. "○ available"
  625. };
  626. format!(" {name:<18} {marker:<11} {description}")
  627. })
  628. .collect::<Vec<_>>()
  629. .join(
  630. "
  631. ",
  632. );
  633. format!(
  634. "Permissions
  635. Active mode {mode}
  636. Mode status live session default
  637. Modes
  638. {modes}
  639. Usage
  640. Inspect current mode with /permissions
  641. Switch modes with /permissions <mode>"
  642. )
  643. }
  644. fn format_permissions_switch_report(previous: &str, next: &str) -> String {
  645. format!(
  646. "Permissions updated
  647. Result mode switched
  648. Previous mode {previous}
  649. Active mode {next}
  650. Applies to subsequent tool calls
  651. Usage /permissions to inspect current mode"
  652. )
  653. }
  654. fn format_cost_report(usage: TokenUsage) -> String {
  655. format!(
  656. "Cost
  657. Input tokens {}
  658. Output tokens {}
  659. Cache create {}
  660. Cache read {}
  661. Total tokens {}",
  662. usage.input_tokens,
  663. usage.output_tokens,
  664. usage.cache_creation_input_tokens,
  665. usage.cache_read_input_tokens,
  666. usage.total_tokens(),
  667. )
  668. }
  669. fn format_resume_report(session_path: &str, message_count: usize, turns: u32) -> String {
  670. format!(
  671. "Session resumed
  672. Session file {session_path}
  673. Messages {message_count}
  674. Turns {turns}"
  675. )
  676. }
  677. fn format_compact_report(removed: usize, resulting_messages: usize, skipped: bool) -> String {
  678. if skipped {
  679. format!(
  680. "Compact
  681. Result skipped
  682. Reason session below compaction threshold
  683. Messages kept {resulting_messages}"
  684. )
  685. } else {
  686. format!(
  687. "Compact
  688. Result compacted
  689. Messages removed {removed}
  690. Messages kept {resulting_messages}"
  691. )
  692. }
  693. }
  694. fn format_auto_compaction_notice(removed: usize) -> String {
  695. format!("[auto-compacted: removed {removed} messages]")
  696. }
  697. fn parse_git_status_metadata(status: Option<&str>) -> (Option<PathBuf>, Option<String>) {
  698. let Some(status) = status else {
  699. return (None, None);
  700. };
  701. let branch = status.lines().next().and_then(|line| {
  702. line.strip_prefix("## ")
  703. .map(|line| {
  704. line.split(['.', ' '])
  705. .next()
  706. .unwrap_or_default()
  707. .to_string()
  708. })
  709. .filter(|value| !value.is_empty())
  710. });
  711. let project_root = find_git_root().ok();
  712. (project_root, branch)
  713. }
  714. fn find_git_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
  715. let output = std::process::Command::new("git")
  716. .args(["rev-parse", "--show-toplevel"])
  717. .current_dir(env::current_dir()?)
  718. .output()?;
  719. if !output.status.success() {
  720. return Err("not a git repository".into());
  721. }
  722. let path = String::from_utf8(output.stdout)?.trim().to_string();
  723. if path.is_empty() {
  724. return Err("empty git root".into());
  725. }
  726. Ok(PathBuf::from(path))
  727. }
  728. #[allow(clippy::too_many_lines)]
  729. fn run_resume_command(
  730. session_path: &Path,
  731. session: &Session,
  732. command: &SlashCommand,
  733. ) -> Result<ResumeCommandOutcome, Box<dyn std::error::Error>> {
  734. match command {
  735. SlashCommand::Help => Ok(ResumeCommandOutcome {
  736. session: session.clone(),
  737. message: Some(render_repl_help()),
  738. }),
  739. SlashCommand::Compact => {
  740. let result = runtime::compact_session(
  741. session,
  742. CompactionConfig {
  743. max_estimated_tokens: 0,
  744. ..CompactionConfig::default()
  745. },
  746. );
  747. let removed = result.removed_message_count;
  748. let kept = result.compacted_session.messages.len();
  749. let skipped = removed == 0;
  750. result.compacted_session.save_to_path(session_path)?;
  751. Ok(ResumeCommandOutcome {
  752. session: result.compacted_session,
  753. message: Some(format_compact_report(removed, kept, skipped)),
  754. })
  755. }
  756. SlashCommand::Clear { confirm } => {
  757. if !confirm {
  758. return Ok(ResumeCommandOutcome {
  759. session: session.clone(),
  760. message: Some(
  761. "clear: confirmation required; rerun with /clear --confirm".to_string(),
  762. ),
  763. });
  764. }
  765. let cleared = Session::new();
  766. cleared.save_to_path(session_path)?;
  767. Ok(ResumeCommandOutcome {
  768. session: cleared,
  769. message: Some(format!(
  770. "Cleared resumed session file {}.",
  771. session_path.display()
  772. )),
  773. })
  774. }
  775. SlashCommand::Status => {
  776. let tracker = UsageTracker::from_session(session);
  777. let usage = tracker.cumulative_usage();
  778. Ok(ResumeCommandOutcome {
  779. session: session.clone(),
  780. message: Some(format_status_report(
  781. "restored-session",
  782. StatusUsage {
  783. message_count: session.messages.len(),
  784. turns: tracker.turns(),
  785. latest: tracker.current_turn_usage(),
  786. cumulative: usage,
  787. estimated_tokens: 0,
  788. },
  789. default_permission_mode().as_str(),
  790. &status_context(Some(session_path))?,
  791. )),
  792. })
  793. }
  794. SlashCommand::Cost => {
  795. let usage = UsageTracker::from_session(session).cumulative_usage();
  796. Ok(ResumeCommandOutcome {
  797. session: session.clone(),
  798. message: Some(format_cost_report(usage)),
  799. })
  800. }
  801. SlashCommand::Config { section } => Ok(ResumeCommandOutcome {
  802. session: session.clone(),
  803. message: Some(render_config_report(section.as_deref())?),
  804. }),
  805. SlashCommand::Memory => Ok(ResumeCommandOutcome {
  806. session: session.clone(),
  807. message: Some(render_memory_report()?),
  808. }),
  809. SlashCommand::Init => Ok(ResumeCommandOutcome {
  810. session: session.clone(),
  811. message: Some(init_claude_md()?),
  812. }),
  813. SlashCommand::Diff => Ok(ResumeCommandOutcome {
  814. session: session.clone(),
  815. message: Some(render_diff_report()?),
  816. }),
  817. SlashCommand::Version => Ok(ResumeCommandOutcome {
  818. session: session.clone(),
  819. message: Some(render_version_report()),
  820. }),
  821. SlashCommand::Export { path } => {
  822. let export_path = resolve_export_path(path.as_deref(), session)?;
  823. fs::write(&export_path, render_export_text(session))?;
  824. Ok(ResumeCommandOutcome {
  825. session: session.clone(),
  826. message: Some(format!(
  827. "Export\n Result wrote transcript\n File {}\n Messages {}",
  828. export_path.display(),
  829. session.messages.len(),
  830. )),
  831. })
  832. }
  833. SlashCommand::Bughunter { .. }
  834. | SlashCommand::Commit
  835. | SlashCommand::Pr { .. }
  836. | SlashCommand::Issue { .. }
  837. | SlashCommand::Ultraplan { .. }
  838. | SlashCommand::Teleport { .. }
  839. | SlashCommand::DebugToolCall
  840. | SlashCommand::Resume { .. }
  841. | SlashCommand::Model { .. }
  842. | SlashCommand::Permissions { .. }
  843. | SlashCommand::Session { .. }
  844. | SlashCommand::Plugins { .. }
  845. | SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),
  846. }
  847. }
  848. fn run_repl(
  849. model: String,
  850. allowed_tools: Option<AllowedToolSet>,
  851. permission_mode: PermissionMode,
  852. ) -> Result<(), Box<dyn std::error::Error>> {
  853. let mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
  854. let mut editor = input::LineEditor::new("> ", slash_command_completion_candidates());
  855. println!("{}", cli.startup_banner());
  856. loop {
  857. match editor.read_line()? {
  858. input::ReadOutcome::Submit(input) => {
  859. let trimmed = input.trim().to_string();
  860. if trimmed.is_empty() {
  861. continue;
  862. }
  863. if matches!(trimmed.as_str(), "/exit" | "/quit") {
  864. cli.persist_session()?;
  865. break;
  866. }
  867. if let Some(command) = SlashCommand::parse(&trimmed) {
  868. if cli.handle_repl_command(command)? {
  869. cli.persist_session()?;
  870. }
  871. continue;
  872. }
  873. editor.push_history(input);
  874. cli.run_turn(&trimmed)?;
  875. }
  876. input::ReadOutcome::Cancel => {}
  877. input::ReadOutcome::Exit => {
  878. cli.persist_session()?;
  879. break;
  880. }
  881. }
  882. }
  883. Ok(())
  884. }
  885. #[derive(Debug, Clone)]
  886. struct SessionHandle {
  887. id: String,
  888. path: PathBuf,
  889. }
  890. #[derive(Debug, Clone)]
  891. struct ManagedSessionSummary {
  892. id: String,
  893. path: PathBuf,
  894. modified_epoch_secs: u64,
  895. message_count: usize,
  896. }
  897. struct LiveCli {
  898. model: String,
  899. allowed_tools: Option<AllowedToolSet>,
  900. permission_mode: PermissionMode,
  901. system_prompt: Vec<String>,
  902. runtime: ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>,
  903. session: SessionHandle,
  904. }
  905. impl LiveCli {
  906. fn new(
  907. model: String,
  908. enable_tools: bool,
  909. allowed_tools: Option<AllowedToolSet>,
  910. permission_mode: PermissionMode,
  911. ) -> Result<Self, Box<dyn std::error::Error>> {
  912. let system_prompt = build_system_prompt()?;
  913. let session = create_managed_session_handle()?;
  914. let runtime = build_runtime(
  915. Session::new(),
  916. model.clone(),
  917. system_prompt.clone(),
  918. enable_tools,
  919. true,
  920. allowed_tools.clone(),
  921. permission_mode,
  922. None,
  923. )?;
  924. let cli = Self {
  925. model,
  926. allowed_tools,
  927. permission_mode,
  928. system_prompt,
  929. runtime,
  930. session,
  931. };
  932. cli.persist_session()?;
  933. Ok(cli)
  934. }
  935. fn startup_banner(&self) -> String {
  936. let cwd = env::current_dir().map_or_else(
  937. |_| "<unknown>".to_string(),
  938. |path| path.display().to_string(),
  939. );
  940. format!(
  941. "\x1b[38;5;196m\
  942. ██████╗██╗ █████╗ ██╗ ██╗\n\
  943. ██╔════╝██║ ██╔══██╗██║ ██║\n\
  944. ██║ ██║ ███████║██║ █╗ ██║\n\
  945. ██║ ██║ ██╔══██║██║███╗██║\n\
  946. ╚██████╗███████╗██║ ██║╚███╔███╔╝\n\
  947. ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\x1b[0m \x1b[38;5;208mCode\x1b[0m 🦞\n\n\
  948. \x1b[2mModel\x1b[0m {}\n\
  949. \x1b[2mPermissions\x1b[0m {}\n\
  950. \x1b[2mDirectory\x1b[0m {}\n\
  951. \x1b[2mSession\x1b[0m {}\n\n\
  952. Type \x1b[1m/help\x1b[0m for commands · \x1b[2mShift+Enter\x1b[0m for newline",
  953. self.model,
  954. self.permission_mode.as_str(),
  955. cwd,
  956. self.session.id,
  957. )
  958. }
  959. fn run_turn(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
  960. let mut spinner = Spinner::new();
  961. let mut stdout = io::stdout();
  962. spinner.tick(
  963. "🦀 Thinking...",
  964. TerminalRenderer::new().color_theme(),
  965. &mut stdout,
  966. )?;
  967. let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
  968. let result = self.runtime.run_turn(input, Some(&mut permission_prompter));
  969. match result {
  970. Ok(summary) => {
  971. spinner.finish(
  972. "✨ Done",
  973. TerminalRenderer::new().color_theme(),
  974. &mut stdout,
  975. )?;
  976. println!();
  977. if let Some(event) = summary.auto_compaction {
  978. println!(
  979. "{}",
  980. format_auto_compaction_notice(event.removed_message_count)
  981. );
  982. }
  983. self.persist_session()?;
  984. Ok(())
  985. }
  986. Err(error) => {
  987. spinner.fail(
  988. "❌ Request failed",
  989. TerminalRenderer::new().color_theme(),
  990. &mut stdout,
  991. )?;
  992. Err(Box::new(error))
  993. }
  994. }
  995. }
  996. fn run_turn_with_output(
  997. &mut self,
  998. input: &str,
  999. output_format: CliOutputFormat,
  1000. ) -> Result<(), Box<dyn std::error::Error>> {
  1001. match output_format {
  1002. CliOutputFormat::Text => self.run_turn(input),
  1003. CliOutputFormat::Json => self.run_prompt_json(input),
  1004. }
  1005. }
  1006. fn run_prompt_json(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
  1007. let session = self.runtime.session().clone();
  1008. let mut runtime = build_runtime(
  1009. session,
  1010. self.model.clone(),
  1011. self.system_prompt.clone(),
  1012. true,
  1013. false,
  1014. self.allowed_tools.clone(),
  1015. self.permission_mode,
  1016. None,
  1017. )?;
  1018. let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
  1019. let summary = runtime.run_turn(input, Some(&mut permission_prompter))?;
  1020. self.runtime = runtime;
  1021. self.persist_session()?;
  1022. println!(
  1023. "{}",
  1024. json!({
  1025. "message": final_assistant_text(&summary),
  1026. "model": self.model,
  1027. "iterations": summary.iterations,
  1028. "auto_compaction": summary.auto_compaction.map(|event| json!({
  1029. "removed_messages": event.removed_message_count,
  1030. "notice": format_auto_compaction_notice(event.removed_message_count),
  1031. })),
  1032. "tool_uses": collect_tool_uses(&summary),
  1033. "tool_results": collect_tool_results(&summary),
  1034. "usage": {
  1035. "input_tokens": summary.usage.input_tokens,
  1036. "output_tokens": summary.usage.output_tokens,
  1037. "cache_creation_input_tokens": summary.usage.cache_creation_input_tokens,
  1038. "cache_read_input_tokens": summary.usage.cache_read_input_tokens,
  1039. }
  1040. })
  1041. );
  1042. Ok(())
  1043. }
  1044. fn handle_repl_command(
  1045. &mut self,
  1046. command: SlashCommand,
  1047. ) -> Result<bool, Box<dyn std::error::Error>> {
  1048. Ok(match command {
  1049. SlashCommand::Help => {
  1050. println!("{}", render_repl_help());
  1051. false
  1052. }
  1053. SlashCommand::Status => {
  1054. self.print_status();
  1055. false
  1056. }
  1057. SlashCommand::Bughunter { scope } => {
  1058. self.run_bughunter(scope.as_deref())?;
  1059. false
  1060. }
  1061. SlashCommand::Commit => {
  1062. self.run_commit()?;
  1063. true
  1064. }
  1065. SlashCommand::Pr { context } => {
  1066. self.run_pr(context.as_deref())?;
  1067. false
  1068. }
  1069. SlashCommand::Issue { context } => {
  1070. self.run_issue(context.as_deref())?;
  1071. false
  1072. }
  1073. SlashCommand::Ultraplan { task } => {
  1074. self.run_ultraplan(task.as_deref())?;
  1075. false
  1076. }
  1077. SlashCommand::Teleport { target } => {
  1078. self.run_teleport(target.as_deref())?;
  1079. false
  1080. }
  1081. SlashCommand::DebugToolCall => {
  1082. self.run_debug_tool_call()?;
  1083. false
  1084. }
  1085. SlashCommand::Compact => {
  1086. self.compact()?;
  1087. false
  1088. }
  1089. SlashCommand::Model { model } => self.set_model(model)?,
  1090. SlashCommand::Permissions { mode } => self.set_permissions(mode)?,
  1091. SlashCommand::Clear { confirm } => self.clear_session(confirm)?,
  1092. SlashCommand::Cost => {
  1093. self.print_cost();
  1094. false
  1095. }
  1096. SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
  1097. SlashCommand::Config { section } => {
  1098. Self::print_config(section.as_deref())?;
  1099. false
  1100. }
  1101. SlashCommand::Memory => {
  1102. Self::print_memory()?;
  1103. false
  1104. }
  1105. SlashCommand::Init => {
  1106. run_init()?;
  1107. false
  1108. }
  1109. SlashCommand::Diff => {
  1110. Self::print_diff()?;
  1111. false
  1112. }
  1113. SlashCommand::Version => {
  1114. Self::print_version();
  1115. false
  1116. }
  1117. SlashCommand::Export { path } => {
  1118. self.export_session(path.as_deref())?;
  1119. false
  1120. }
  1121. SlashCommand::Session { action, target } => {
  1122. self.handle_session_command(action.as_deref(), target.as_deref())?
  1123. }
  1124. SlashCommand::Plugins { action, target } => {
  1125. self.handle_plugins_command(action.as_deref(), target.as_deref())?
  1126. }
  1127. SlashCommand::Unknown(name) => {
  1128. eprintln!("unknown slash command: /{name}");
  1129. false
  1130. }
  1131. })
  1132. }
  1133. fn persist_session(&self) -> Result<(), Box<dyn std::error::Error>> {
  1134. self.runtime.session().save_to_path(&self.session.path)?;
  1135. Ok(())
  1136. }
  1137. fn print_status(&self) {
  1138. let cumulative = self.runtime.usage().cumulative_usage();
  1139. let latest = self.runtime.usage().current_turn_usage();
  1140. println!(
  1141. "{}",
  1142. format_status_report(
  1143. &self.model,
  1144. StatusUsage {
  1145. message_count: self.runtime.session().messages.len(),
  1146. turns: self.runtime.usage().turns(),
  1147. latest,
  1148. cumulative,
  1149. estimated_tokens: self.runtime.estimated_tokens(),
  1150. },
  1151. self.permission_mode.as_str(),
  1152. &status_context(Some(&self.session.path)).expect("status context should load"),
  1153. )
  1154. );
  1155. }
  1156. fn set_model(&mut self, model: Option<String>) -> Result<bool, Box<dyn std::error::Error>> {
  1157. let Some(model) = model else {
  1158. println!(
  1159. "{}",
  1160. format_model_report(
  1161. &self.model,
  1162. self.runtime.session().messages.len(),
  1163. self.runtime.usage().turns(),
  1164. )
  1165. );
  1166. return Ok(false);
  1167. };
  1168. let model = resolve_model_alias(&model).to_string();
  1169. if model == self.model {
  1170. println!(
  1171. "{}",
  1172. format_model_report(
  1173. &self.model,
  1174. self.runtime.session().messages.len(),
  1175. self.runtime.usage().turns(),
  1176. )
  1177. );
  1178. return Ok(false);
  1179. }
  1180. let previous = self.model.clone();
  1181. let session = self.runtime.session().clone();
  1182. let message_count = session.messages.len();
  1183. self.runtime = build_runtime(
  1184. session,
  1185. model.clone(),
  1186. self.system_prompt.clone(),
  1187. true,
  1188. true,
  1189. self.allowed_tools.clone(),
  1190. self.permission_mode,
  1191. None,
  1192. )?;
  1193. self.model.clone_from(&model);
  1194. println!(
  1195. "{}",
  1196. format_model_switch_report(&previous, &model, message_count)
  1197. );
  1198. Ok(true)
  1199. }
  1200. fn set_permissions(
  1201. &mut self,
  1202. mode: Option<String>,
  1203. ) -> Result<bool, Box<dyn std::error::Error>> {
  1204. let Some(mode) = mode else {
  1205. println!(
  1206. "{}",
  1207. format_permissions_report(self.permission_mode.as_str())
  1208. );
  1209. return Ok(false);
  1210. };
  1211. let normalized = normalize_permission_mode(&mode).ok_or_else(|| {
  1212. format!(
  1213. "unsupported permission mode '{mode}'. Use read-only, workspace-write, or danger-full-access."
  1214. )
  1215. })?;
  1216. if normalized == self.permission_mode.as_str() {
  1217. println!("{}", format_permissions_report(normalized));
  1218. return Ok(false);
  1219. }
  1220. let previous = self.permission_mode.as_str().to_string();
  1221. let session = self.runtime.session().clone();
  1222. self.permission_mode = permission_mode_from_label(normalized);
  1223. self.runtime = build_runtime(
  1224. session,
  1225. self.model.clone(),
  1226. self.system_prompt.clone(),
  1227. true,
  1228. true,
  1229. self.allowed_tools.clone(),
  1230. self.permission_mode,
  1231. None,
  1232. )?;
  1233. println!(
  1234. "{}",
  1235. format_permissions_switch_report(&previous, normalized)
  1236. );
  1237. Ok(true)
  1238. }
  1239. fn clear_session(&mut self, confirm: bool) -> Result<bool, Box<dyn std::error::Error>> {
  1240. if !confirm {
  1241. println!(
  1242. "clear: confirmation required; run /clear --confirm to start a fresh session."
  1243. );
  1244. return Ok(false);
  1245. }
  1246. self.session = create_managed_session_handle()?;
  1247. self.runtime = build_runtime(
  1248. Session::new(),
  1249. self.model.clone(),
  1250. self.system_prompt.clone(),
  1251. true,
  1252. true,
  1253. self.allowed_tools.clone(),
  1254. self.permission_mode,
  1255. None,
  1256. )?;
  1257. println!(
  1258. "Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
  1259. self.model,
  1260. self.permission_mode.as_str(),
  1261. self.session.id,
  1262. );
  1263. Ok(true)
  1264. }
  1265. fn print_cost(&self) {
  1266. let cumulative = self.runtime.usage().cumulative_usage();
  1267. println!("{}", format_cost_report(cumulative));
  1268. }
  1269. fn resume_session(
  1270. &mut self,
  1271. session_path: Option<String>,
  1272. ) -> Result<bool, Box<dyn std::error::Error>> {
  1273. let Some(session_ref) = session_path else {
  1274. println!("Usage: /resume <session-path>");
  1275. return Ok(false);
  1276. };
  1277. let handle = resolve_session_reference(&session_ref)?;
  1278. let session = Session::load_from_path(&handle.path)?;
  1279. let message_count = session.messages.len();
  1280. self.runtime = build_runtime(
  1281. session,
  1282. self.model.clone(),
  1283. self.system_prompt.clone(),
  1284. true,
  1285. true,
  1286. self.allowed_tools.clone(),
  1287. self.permission_mode,
  1288. None,
  1289. )?;
  1290. self.session = handle;
  1291. println!(
  1292. "{}",
  1293. format_resume_report(
  1294. &self.session.path.display().to_string(),
  1295. message_count,
  1296. self.runtime.usage().turns(),
  1297. )
  1298. );
  1299. Ok(true)
  1300. }
  1301. fn print_config(section: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
  1302. println!("{}", render_config_report(section)?);
  1303. Ok(())
  1304. }
  1305. fn print_memory() -> Result<(), Box<dyn std::error::Error>> {
  1306. println!("{}", render_memory_report()?);
  1307. Ok(())
  1308. }
  1309. fn print_diff() -> Result<(), Box<dyn std::error::Error>> {
  1310. println!("{}", render_diff_report()?);
  1311. Ok(())
  1312. }
  1313. fn print_version() {
  1314. println!("{}", render_version_report());
  1315. }
  1316. fn export_session(
  1317. &self,
  1318. requested_path: Option<&str>,
  1319. ) -> Result<(), Box<dyn std::error::Error>> {
  1320. let export_path = resolve_export_path(requested_path, self.runtime.session())?;
  1321. fs::write(&export_path, render_export_text(self.runtime.session()))?;
  1322. println!(
  1323. "Export\n Result wrote transcript\n File {}\n Messages {}",
  1324. export_path.display(),
  1325. self.runtime.session().messages.len(),
  1326. );
  1327. Ok(())
  1328. }
  1329. fn handle_session_command(
  1330. &mut self,
  1331. action: Option<&str>,
  1332. target: Option<&str>,
  1333. ) -> Result<bool, Box<dyn std::error::Error>> {
  1334. match action {
  1335. None | Some("list") => {
  1336. println!("{}", render_session_list(&self.session.id)?);
  1337. Ok(false)
  1338. }
  1339. Some("switch") => {
  1340. let Some(target) = target else {
  1341. println!("Usage: /session switch <session-id>");
  1342. return Ok(false);
  1343. };
  1344. let handle = resolve_session_reference(target)?;
  1345. let session = Session::load_from_path(&handle.path)?;
  1346. let message_count = session.messages.len();
  1347. self.runtime = build_runtime(
  1348. session,
  1349. self.model.clone(),
  1350. self.system_prompt.clone(),
  1351. true,
  1352. true,
  1353. self.allowed_tools.clone(),
  1354. self.permission_mode,
  1355. None,
  1356. )?;
  1357. self.session = handle;
  1358. println!(
  1359. "Session switched\n Active session {}\n File {}\n Messages {}",
  1360. self.session.id,
  1361. self.session.path.display(),
  1362. message_count,
  1363. );
  1364. Ok(true)
  1365. }
  1366. Some(other) => {
  1367. println!("Unknown /session action '{other}'. Use /session list or /session switch <session-id>.");
  1368. Ok(false)
  1369. }
  1370. }
  1371. }
  1372. fn handle_plugins_command(
  1373. &mut self,
  1374. action: Option<&str>,
  1375. target: Option<&str>,
  1376. ) -> Result<bool, Box<dyn std::error::Error>> {
  1377. let cwd = env::current_dir()?;
  1378. let loader = ConfigLoader::default_for(&cwd);
  1379. let runtime_config = loader.load()?;
  1380. let mut manager = build_plugin_manager(&cwd, &loader, &runtime_config);
  1381. let result = handle_plugins_slash_command(action, target, &mut manager)?;
  1382. println!("{}", result.message);
  1383. if result.reload_runtime {
  1384. self.reload_runtime_features()?;
  1385. }
  1386. Ok(false)
  1387. }
  1388. fn reload_runtime_features(&mut self) -> Result<(), Box<dyn std::error::Error>> {
  1389. self.runtime = build_runtime(
  1390. self.runtime.session().clone(),
  1391. self.model.clone(),
  1392. self.system_prompt.clone(),
  1393. true,
  1394. true,
  1395. self.allowed_tools.clone(),
  1396. self.permission_mode,
  1397. None,
  1398. )?;
  1399. self.persist_session()
  1400. }
  1401. fn compact(&mut self) -> Result<(), Box<dyn std::error::Error>> {
  1402. let result = self.runtime.compact(CompactionConfig::default());
  1403. let removed = result.removed_message_count;
  1404. let kept = result.compacted_session.messages.len();
  1405. let skipped = removed == 0;
  1406. self.runtime = build_runtime(
  1407. result.compacted_session,
  1408. self.model.clone(),
  1409. self.system_prompt.clone(),
  1410. true,
  1411. true,
  1412. self.allowed_tools.clone(),
  1413. self.permission_mode,
  1414. None,
  1415. )?;
  1416. self.persist_session()?;
  1417. println!("{}", format_compact_report(removed, kept, skipped));
  1418. Ok(())
  1419. }
  1420. fn run_internal_prompt_text_with_progress(
  1421. &self,
  1422. prompt: &str,
  1423. enable_tools: bool,
  1424. progress: Option<InternalPromptProgressReporter>,
  1425. ) -> Result<String, Box<dyn std::error::Error>> {
  1426. let session = self.runtime.session().clone();
  1427. let mut runtime = build_runtime(
  1428. session,
  1429. self.model.clone(),
  1430. self.system_prompt.clone(),
  1431. enable_tools,
  1432. false,
  1433. self.allowed_tools.clone(),
  1434. self.permission_mode,
  1435. progress,
  1436. )?;
  1437. let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
  1438. let summary = runtime.run_turn(prompt, Some(&mut permission_prompter))?;
  1439. Ok(final_assistant_text(&summary).trim().to_string())
  1440. }
  1441. fn run_internal_prompt_text(
  1442. &self,
  1443. prompt: &str,
  1444. enable_tools: bool,
  1445. ) -> Result<String, Box<dyn std::error::Error>> {
  1446. self.run_internal_prompt_text_with_progress(prompt, enable_tools, None)
  1447. }
  1448. fn run_bughunter(&self, scope: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
  1449. let scope = scope.unwrap_or("the current repository");
  1450. let prompt = format!(
  1451. "You are /bughunter. Inspect {scope} and identify the most likely bugs or correctness issues. Prioritize concrete findings with file paths, severity, and suggested fixes. Use tools if needed."
  1452. );
  1453. println!("{}", self.run_internal_prompt_text(&prompt, true)?);
  1454. Ok(())
  1455. }
  1456. fn run_ultraplan(&self, task: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
  1457. let task = task.unwrap_or("the current repo work");
  1458. let prompt = format!(
  1459. "You are /ultraplan. Produce a deep multi-step execution plan for {task}. Include goals, risks, implementation sequence, verification steps, and rollback considerations. Use tools if needed."
  1460. );
  1461. let mut progress = InternalPromptProgressRun::start_ultraplan(task);
  1462. match self.run_internal_prompt_text_with_progress(
  1463. &prompt,
  1464. true,
  1465. Some(progress.reporter()),
  1466. ) {
  1467. Ok(plan) => {
  1468. progress.finish_success();
  1469. println!("{plan}");
  1470. Ok(())
  1471. }
  1472. Err(error) => {
  1473. progress.finish_failure(&error.to_string());
  1474. Err(error)
  1475. }
  1476. }
  1477. }
  1478. #[allow(clippy::unused_self)]
  1479. fn run_teleport(&self, target: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
  1480. let Some(target) = target.map(str::trim).filter(|value| !value.is_empty()) else {
  1481. println!("Usage: /teleport <symbol-or-path>");
  1482. return Ok(());
  1483. };
  1484. println!("{}", render_teleport_report(target)?);
  1485. Ok(())
  1486. }
  1487. fn run_debug_tool_call(&self) -> Result<(), Box<dyn std::error::Error>> {
  1488. println!("{}", render_last_tool_debug_report(self.runtime.session())?);
  1489. Ok(())
  1490. }
  1491. fn run_commit(&mut self) -> Result<(), Box<dyn std::error::Error>> {
  1492. let status = git_output(&["status", "--short"])?;
  1493. if status.trim().is_empty() {
  1494. println!("Commit\n Result skipped\n Reason no workspace changes");
  1495. return Ok(());
  1496. }
  1497. git_status_ok(&["add", "-A"])?;
  1498. let staged_stat = git_output(&["diff", "--cached", "--stat"])?;
  1499. let prompt = format!(
  1500. "Generate a git commit message in plain text Lore format only. Base it on this staged diff summary:\n\n{}\n\nRecent conversation context:\n{}",
  1501. truncate_for_prompt(&staged_stat, 8_000),
  1502. recent_user_context(self.runtime.session(), 6)
  1503. );
  1504. let message = sanitize_generated_message(&self.run_internal_prompt_text(&prompt, false)?);
  1505. if message.trim().is_empty() {
  1506. return Err("generated commit message was empty".into());
  1507. }
  1508. let path = write_temp_text_file("claw-commit-message.txt", &message)?;
  1509. let output = Command::new("git")
  1510. .args(["commit", "--file"])
  1511. .arg(&path)
  1512. .current_dir(env::current_dir()?)
  1513. .output()?;
  1514. if !output.status.success() {
  1515. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  1516. return Err(format!("git commit failed: {stderr}").into());
  1517. }
  1518. println!(
  1519. "Commit\n Result created\n Message file {}\n\n{}",
  1520. path.display(),
  1521. message.trim()
  1522. );
  1523. Ok(())
  1524. }
  1525. fn run_pr(&self, context: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
  1526. let staged = git_output(&["diff", "--stat"])?;
  1527. let prompt = format!(
  1528. "Generate a pull request title and body from this conversation and diff summary. Output plain text in this format exactly:\nTITLE: <title>\nBODY:\n<body markdown>\n\nContext hint: {}\n\nDiff summary:\n{}",
  1529. context.unwrap_or("none"),
  1530. truncate_for_prompt(&staged, 10_000)
  1531. );
  1532. let draft = sanitize_generated_message(&self.run_internal_prompt_text(&prompt, false)?);
  1533. let (title, body) = parse_titled_body(&draft)
  1534. .ok_or_else(|| "failed to parse generated PR title/body".to_string())?;
  1535. if command_exists("gh") {
  1536. let body_path = write_temp_text_file("claw-pr-body.md", &body)?;
  1537. let output = Command::new("gh")
  1538. .args(["pr", "create", "--title", &title, "--body-file"])
  1539. .arg(&body_path)
  1540. .current_dir(env::current_dir()?)
  1541. .output()?;
  1542. if output.status.success() {
  1543. let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
  1544. println!(
  1545. "PR\n Result created\n Title {title}\n URL {}",
  1546. if stdout.is_empty() { "<unknown>" } else { &stdout }
  1547. );
  1548. return Ok(());
  1549. }
  1550. }
  1551. println!("PR draft\n Title {title}\n\n{body}");
  1552. Ok(())
  1553. }
  1554. fn run_issue(&self, context: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
  1555. let prompt = format!(
  1556. "Generate a GitHub issue title and body from this conversation. Output plain text in this format exactly:\nTITLE: <title>\nBODY:\n<body markdown>\n\nContext hint: {}\n\nConversation context:\n{}",
  1557. context.unwrap_or("none"),
  1558. truncate_for_prompt(&recent_user_context(self.runtime.session(), 10), 10_000)
  1559. );
  1560. let draft = sanitize_generated_message(&self.run_internal_prompt_text(&prompt, false)?);
  1561. let (title, body) = parse_titled_body(&draft)
  1562. .ok_or_else(|| "failed to parse generated issue title/body".to_string())?;
  1563. if command_exists("gh") {
  1564. let body_path = write_temp_text_file("claw-issue-body.md", &body)?;
  1565. let output = Command::new("gh")
  1566. .args(["issue", "create", "--title", &title, "--body-file"])
  1567. .arg(&body_path)
  1568. .current_dir(env::current_dir()?)
  1569. .output()?;
  1570. if output.status.success() {
  1571. let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
  1572. println!(
  1573. "Issue\n Result created\n Title {title}\n URL {}",
  1574. if stdout.is_empty() { "<unknown>" } else { &stdout }
  1575. );
  1576. return Ok(());
  1577. }
  1578. }
  1579. println!("Issue draft\n Title {title}\n\n{body}");
  1580. Ok(())
  1581. }
  1582. }
  1583. fn sessions_dir() -> Result<PathBuf, Box<dyn std::error::Error>> {
  1584. let cwd = env::current_dir()?;
  1585. let path = cwd.join(".claude").join("sessions");
  1586. fs::create_dir_all(&path)?;
  1587. Ok(path)
  1588. }
  1589. fn create_managed_session_handle() -> Result<SessionHandle, Box<dyn std::error::Error>> {
  1590. let id = generate_session_id();
  1591. let path = sessions_dir()?.join(format!("{id}.json"));
  1592. Ok(SessionHandle { id, path })
  1593. }
  1594. fn generate_session_id() -> String {
  1595. let millis = SystemTime::now()
  1596. .duration_since(UNIX_EPOCH)
  1597. .map(|duration| duration.as_millis())
  1598. .unwrap_or_default();
  1599. format!("session-{millis}")
  1600. }
  1601. fn resolve_session_reference(reference: &str) -> Result<SessionHandle, Box<dyn std::error::Error>> {
  1602. let direct = PathBuf::from(reference);
  1603. let path = if direct.exists() {
  1604. direct
  1605. } else {
  1606. sessions_dir()?.join(format!("{reference}.json"))
  1607. };
  1608. if !path.exists() {
  1609. return Err(format!("session not found: {reference}").into());
  1610. }
  1611. let id = path
  1612. .file_stem()
  1613. .and_then(|value| value.to_str())
  1614. .unwrap_or(reference)
  1615. .to_string();
  1616. Ok(SessionHandle { id, path })
  1617. }
  1618. fn list_managed_sessions() -> Result<Vec<ManagedSessionSummary>, Box<dyn std::error::Error>> {
  1619. let mut sessions = Vec::new();
  1620. for entry in fs::read_dir(sessions_dir()?)? {
  1621. let entry = entry?;
  1622. let path = entry.path();
  1623. if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
  1624. continue;
  1625. }
  1626. let metadata = entry.metadata()?;
  1627. let modified_epoch_secs = metadata
  1628. .modified()
  1629. .ok()
  1630. .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
  1631. .map(|duration| duration.as_secs())
  1632. .unwrap_or_default();
  1633. let message_count = Session::load_from_path(&path)
  1634. .map(|session| session.messages.len())
  1635. .unwrap_or_default();
  1636. let id = path
  1637. .file_stem()
  1638. .and_then(|value| value.to_str())
  1639. .unwrap_or("unknown")
  1640. .to_string();
  1641. sessions.push(ManagedSessionSummary {
  1642. id,
  1643. path,
  1644. modified_epoch_secs,
  1645. message_count,
  1646. });
  1647. }
  1648. sessions.sort_by(|left, right| right.modified_epoch_secs.cmp(&left.modified_epoch_secs));
  1649. Ok(sessions)
  1650. }
  1651. fn render_session_list(active_session_id: &str) -> Result<String, Box<dyn std::error::Error>> {
  1652. let sessions = list_managed_sessions()?;
  1653. let mut lines = vec![
  1654. "Sessions".to_string(),
  1655. format!(" Directory {}", sessions_dir()?.display()),
  1656. ];
  1657. if sessions.is_empty() {
  1658. lines.push(" No managed sessions saved yet.".to_string());
  1659. return Ok(lines.join("\n"));
  1660. }
  1661. for session in sessions {
  1662. let marker = if session.id == active_session_id {
  1663. "● current"
  1664. } else {
  1665. "○ saved"
  1666. };
  1667. lines.push(format!(
  1668. " {id:<20} {marker:<10} msgs={msgs:<4} modified={modified} path={path}",
  1669. id = session.id,
  1670. msgs = session.message_count,
  1671. modified = session.modified_epoch_secs,
  1672. path = session.path.display(),
  1673. ));
  1674. }
  1675. Ok(lines.join("\n"))
  1676. }
  1677. fn render_repl_help() -> String {
  1678. [
  1679. "REPL".to_string(),
  1680. " /exit Quit the REPL".to_string(),
  1681. " /quit Quit the REPL".to_string(),
  1682. " Up/Down Navigate prompt history".to_string(),
  1683. " Tab Complete slash commands".to_string(),
  1684. " Ctrl-C Clear input (or exit on empty prompt)".to_string(),
  1685. " Shift+Enter/Ctrl+J Insert a newline".to_string(),
  1686. String::new(),
  1687. render_slash_command_help(),
  1688. ]
  1689. .join(
  1690. "
  1691. ",
  1692. )
  1693. }
  1694. fn status_context(
  1695. session_path: Option<&Path>,
  1696. ) -> Result<StatusContext, Box<dyn std::error::Error>> {
  1697. let cwd = env::current_dir()?;
  1698. let loader = ConfigLoader::default_for(&cwd);
  1699. let discovered_config_files = loader.discover().len();
  1700. let runtime_config = loader.load()?;
  1701. let project_context = ProjectContext::discover_with_git(&cwd, DEFAULT_DATE)?;
  1702. let (project_root, git_branch) =
  1703. parse_git_status_metadata(project_context.git_status.as_deref());
  1704. Ok(StatusContext {
  1705. cwd,
  1706. session_path: session_path.map(Path::to_path_buf),
  1707. loaded_config_files: runtime_config.loaded_entries().len(),
  1708. discovered_config_files,
  1709. memory_file_count: project_context.instruction_files.len(),
  1710. project_root,
  1711. git_branch,
  1712. })
  1713. }
  1714. fn format_status_report(
  1715. model: &str,
  1716. usage: StatusUsage,
  1717. permission_mode: &str,
  1718. context: &StatusContext,
  1719. ) -> String {
  1720. [
  1721. format!(
  1722. "Status
  1723. Model {model}
  1724. Permission mode {permission_mode}
  1725. Messages {}
  1726. Turns {}
  1727. Estimated tokens {}",
  1728. usage.message_count, usage.turns, usage.estimated_tokens,
  1729. ),
  1730. format!(
  1731. "Usage
  1732. Latest total {}
  1733. Cumulative input {}
  1734. Cumulative output {}
  1735. Cumulative total {}",
  1736. usage.latest.total_tokens(),
  1737. usage.cumulative.input_tokens,
  1738. usage.cumulative.output_tokens,
  1739. usage.cumulative.total_tokens(),
  1740. ),
  1741. format!(
  1742. "Workspace
  1743. Cwd {}
  1744. Project root {}
  1745. Git branch {}
  1746. Session {}
  1747. Config files loaded {}/{}
  1748. Memory files {}",
  1749. context.cwd.display(),
  1750. context
  1751. .project_root
  1752. .as_ref()
  1753. .map_or_else(|| "unknown".to_string(), |path| path.display().to_string()),
  1754. context.git_branch.as_deref().unwrap_or("unknown"),
  1755. context.session_path.as_ref().map_or_else(
  1756. || "live-repl".to_string(),
  1757. |path| path.display().to_string()
  1758. ),
  1759. context.loaded_config_files,
  1760. context.discovered_config_files,
  1761. context.memory_file_count,
  1762. ),
  1763. ]
  1764. .join(
  1765. "
  1766. ",
  1767. )
  1768. }
  1769. fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::error::Error>> {
  1770. let cwd = env::current_dir()?;
  1771. let loader = ConfigLoader::default_for(&cwd);
  1772. let discovered = loader.discover();
  1773. let runtime_config = loader.load()?;
  1774. let mut lines = vec![
  1775. format!(
  1776. "Config
  1777. Working directory {}
  1778. Loaded files {}
  1779. Merged keys {}",
  1780. cwd.display(),
  1781. runtime_config.loaded_entries().len(),
  1782. runtime_config.merged().len()
  1783. ),
  1784. "Discovered files".to_string(),
  1785. ];
  1786. for entry in discovered {
  1787. let source = match entry.source {
  1788. ConfigSource::User => "user",
  1789. ConfigSource::Project => "project",
  1790. ConfigSource::Local => "local",
  1791. };
  1792. let status = if runtime_config
  1793. .loaded_entries()
  1794. .iter()
  1795. .any(|loaded_entry| loaded_entry.path == entry.path)
  1796. {
  1797. "loaded"
  1798. } else {
  1799. "missing"
  1800. };
  1801. lines.push(format!(
  1802. " {source:<7} {status:<7} {}",
  1803. entry.path.display()
  1804. ));
  1805. }
  1806. if let Some(section) = section {
  1807. lines.push(format!("Merged section: {section}"));
  1808. let value = match section {
  1809. "env" => runtime_config.get("env"),
  1810. "hooks" => runtime_config.get("hooks"),
  1811. "model" => runtime_config.get("model"),
  1812. "plugins" => runtime_config
  1813. .get("plugins")
  1814. .or_else(|| runtime_config.get("enabledPlugins")),
  1815. other => {
  1816. lines.push(format!(
  1817. " Unsupported config section '{other}'. Use env, hooks, model, or plugins."
  1818. ));
  1819. return Ok(lines.join(
  1820. "
  1821. ",
  1822. ));
  1823. }
  1824. };
  1825. lines.push(format!(
  1826. " {}",
  1827. match value {
  1828. Some(value) => value.render(),
  1829. None => "<unset>".to_string(),
  1830. }
  1831. ));
  1832. return Ok(lines.join(
  1833. "
  1834. ",
  1835. ));
  1836. }
  1837. lines.push("Merged JSON".to_string());
  1838. lines.push(format!(" {}", runtime_config.as_json().render()));
  1839. Ok(lines.join(
  1840. "
  1841. ",
  1842. ))
  1843. }
  1844. fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
  1845. let cwd = env::current_dir()?;
  1846. let project_context = ProjectContext::discover(&cwd, DEFAULT_DATE)?;
  1847. let mut lines = vec![format!(
  1848. "Memory
  1849. Working directory {}
  1850. Instruction files {}",
  1851. cwd.display(),
  1852. project_context.instruction_files.len()
  1853. )];
  1854. if project_context.instruction_files.is_empty() {
  1855. lines.push("Discovered files".to_string());
  1856. lines.push(
  1857. " No CLAUDE instruction files discovered in the current directory ancestry."
  1858. .to_string(),
  1859. );
  1860. } else {
  1861. lines.push("Discovered files".to_string());
  1862. for (index, file) in project_context.instruction_files.iter().enumerate() {
  1863. let preview = file.content.lines().next().unwrap_or("").trim();
  1864. let preview = if preview.is_empty() {
  1865. "<empty>"
  1866. } else {
  1867. preview
  1868. };
  1869. lines.push(format!(" {}. {}", index + 1, file.path.display(),));
  1870. lines.push(format!(
  1871. " lines={} preview={}",
  1872. file.content.lines().count(),
  1873. preview
  1874. ));
  1875. }
  1876. }
  1877. Ok(lines.join(
  1878. "
  1879. ",
  1880. ))
  1881. }
  1882. fn init_claude_md() -> Result<String, Box<dyn std::error::Error>> {
  1883. let cwd = env::current_dir()?;
  1884. Ok(initialize_repo(&cwd)?.render())
  1885. }
  1886. fn run_init() -> Result<(), Box<dyn std::error::Error>> {
  1887. println!("{}", init_claude_md()?);
  1888. Ok(())
  1889. }
  1890. fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
  1891. match mode.trim() {
  1892. "read-only" => Some("read-only"),
  1893. "workspace-write" => Some("workspace-write"),
  1894. "danger-full-access" => Some("danger-full-access"),
  1895. _ => None,
  1896. }
  1897. }
  1898. fn render_diff_report() -> Result<String, Box<dyn std::error::Error>> {
  1899. let output = std::process::Command::new("git")
  1900. .args(["diff", "--", ":(exclude).omx"])
  1901. .current_dir(env::current_dir()?)
  1902. .output()?;
  1903. if !output.status.success() {
  1904. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  1905. return Err(format!("git diff failed: {stderr}").into());
  1906. }
  1907. let diff = String::from_utf8(output.stdout)?;
  1908. if diff.trim().is_empty() {
  1909. return Ok(
  1910. "Diff\n Result clean working tree\n Detail no current changes"
  1911. .to_string(),
  1912. );
  1913. }
  1914. Ok(format!("Diff\n\n{}", diff.trim_end()))
  1915. }
  1916. fn render_teleport_report(target: &str) -> Result<String, Box<dyn std::error::Error>> {
  1917. let cwd = env::current_dir()?;
  1918. let file_list = Command::new("rg")
  1919. .args(["--files"])
  1920. .current_dir(&cwd)
  1921. .output()?;
  1922. let file_matches = if file_list.status.success() {
  1923. String::from_utf8(file_list.stdout)?
  1924. .lines()
  1925. .filter(|line| line.contains(target))
  1926. .take(10)
  1927. .map(ToOwned::to_owned)
  1928. .collect::<Vec<_>>()
  1929. } else {
  1930. Vec::new()
  1931. };
  1932. let content_output = Command::new("rg")
  1933. .args(["-n", "-S", "--color", "never", target, "."])
  1934. .current_dir(&cwd)
  1935. .output()?;
  1936. let mut lines = vec![format!("Teleport\n Target {target}")];
  1937. if !file_matches.is_empty() {
  1938. lines.push(String::new());
  1939. lines.push("File matches".to_string());
  1940. lines.extend(file_matches.into_iter().map(|path| format!(" {path}")));
  1941. }
  1942. if content_output.status.success() {
  1943. let matches = String::from_utf8(content_output.stdout)?;
  1944. if !matches.trim().is_empty() {
  1945. lines.push(String::new());
  1946. lines.push("Content matches".to_string());
  1947. lines.push(truncate_for_prompt(&matches, 4_000));
  1948. }
  1949. }
  1950. if lines.len() == 1 {
  1951. lines.push(" Result no matches found".to_string());
  1952. }
  1953. Ok(lines.join("\n"))
  1954. }
  1955. fn render_last_tool_debug_report(session: &Session) -> Result<String, Box<dyn std::error::Error>> {
  1956. let last_tool_use = session
  1957. .messages
  1958. .iter()
  1959. .rev()
  1960. .find_map(|message| {
  1961. message.blocks.iter().rev().find_map(|block| match block {
  1962. ContentBlock::ToolUse { id, name, input } => {
  1963. Some((id.clone(), name.clone(), input.clone()))
  1964. }
  1965. _ => None,
  1966. })
  1967. })
  1968. .ok_or_else(|| "no prior tool call found in session".to_string())?;
  1969. let tool_result = session.messages.iter().rev().find_map(|message| {
  1970. message.blocks.iter().rev().find_map(|block| match block {
  1971. ContentBlock::ToolResult {
  1972. tool_use_id,
  1973. tool_name,
  1974. output,
  1975. is_error,
  1976. } if tool_use_id == &last_tool_use.0 => {
  1977. Some((tool_name.clone(), output.clone(), *is_error))
  1978. }
  1979. _ => None,
  1980. })
  1981. });
  1982. let mut lines = vec![
  1983. "Debug tool call".to_string(),
  1984. format!(" Tool id {}", last_tool_use.0),
  1985. format!(" Tool name {}", last_tool_use.1),
  1986. " Input".to_string(),
  1987. indent_block(&last_tool_use.2, 4),
  1988. ];
  1989. match tool_result {
  1990. Some((tool_name, output, is_error)) => {
  1991. lines.push(" Result".to_string());
  1992. lines.push(format!(" name {tool_name}"));
  1993. lines.push(format!(
  1994. " status {}",
  1995. if is_error { "error" } else { "ok" }
  1996. ));
  1997. lines.push(indent_block(&output, 4));
  1998. }
  1999. None => lines.push(" Result missing tool result".to_string()),
  2000. }
  2001. Ok(lines.join("\n"))
  2002. }
  2003. fn indent_block(value: &str, spaces: usize) -> String {
  2004. let indent = " ".repeat(spaces);
  2005. value
  2006. .lines()
  2007. .map(|line| format!("{indent}{line}"))
  2008. .collect::<Vec<_>>()
  2009. .join("\n")
  2010. }
  2011. fn git_output(args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
  2012. let output = Command::new("git")
  2013. .args(args)
  2014. .current_dir(env::current_dir()?)
  2015. .output()?;
  2016. if !output.status.success() {
  2017. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  2018. return Err(format!("git {} failed: {stderr}", args.join(" ")).into());
  2019. }
  2020. Ok(String::from_utf8(output.stdout)?)
  2021. }
  2022. fn git_status_ok(args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
  2023. let output = Command::new("git")
  2024. .args(args)
  2025. .current_dir(env::current_dir()?)
  2026. .output()?;
  2027. if !output.status.success() {
  2028. let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
  2029. return Err(format!("git {} failed: {stderr}", args.join(" ")).into());
  2030. }
  2031. Ok(())
  2032. }
  2033. fn command_exists(name: &str) -> bool {
  2034. Command::new("which")
  2035. .arg(name)
  2036. .output()
  2037. .map(|output| output.status.success())
  2038. .unwrap_or(false)
  2039. }
  2040. fn write_temp_text_file(
  2041. filename: &str,
  2042. contents: &str,
  2043. ) -> Result<PathBuf, Box<dyn std::error::Error>> {
  2044. let path = env::temp_dir().join(filename);
  2045. fs::write(&path, contents)?;
  2046. Ok(path)
  2047. }
  2048. fn recent_user_context(session: &Session, limit: usize) -> String {
  2049. let requests = session
  2050. .messages
  2051. .iter()
  2052. .filter(|message| message.role == MessageRole::User)
  2053. .filter_map(|message| {
  2054. message.blocks.iter().find_map(|block| match block {
  2055. ContentBlock::Text { text } => Some(text.trim().to_string()),
  2056. _ => None,
  2057. })
  2058. })
  2059. .rev()
  2060. .take(limit)
  2061. .collect::<Vec<_>>();
  2062. if requests.is_empty() {
  2063. "<no prior user messages>".to_string()
  2064. } else {
  2065. requests
  2066. .into_iter()
  2067. .rev()
  2068. .enumerate()
  2069. .map(|(index, text)| format!("{}. {}", index + 1, text))
  2070. .collect::<Vec<_>>()
  2071. .join("\n")
  2072. }
  2073. }
  2074. fn truncate_for_prompt(value: &str, limit: usize) -> String {
  2075. if value.chars().count() <= limit {
  2076. value.trim().to_string()
  2077. } else {
  2078. let truncated = value.chars().take(limit).collect::<String>();
  2079. format!("{}\n…[truncated]", truncated.trim_end())
  2080. }
  2081. }
  2082. fn sanitize_generated_message(value: &str) -> String {
  2083. value.trim().trim_matches('`').trim().replace("\r\n", "\n")
  2084. }
  2085. fn parse_titled_body(value: &str) -> Option<(String, String)> {
  2086. let normalized = sanitize_generated_message(value);
  2087. let title = normalized
  2088. .lines()
  2089. .find_map(|line| line.strip_prefix("TITLE:").map(str::trim))?;
  2090. let body_start = normalized.find("BODY:")?;
  2091. let body = normalized[body_start + "BODY:".len()..].trim();
  2092. Some((title.to_string(), body.to_string()))
  2093. }
  2094. fn render_version_report() -> String {
  2095. let git_sha = GIT_SHA.unwrap_or("unknown");
  2096. let target = BUILD_TARGET.unwrap_or("unknown");
  2097. format!(
  2098. "Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Target {target}\n Build date {DEFAULT_DATE}"
  2099. )
  2100. }
  2101. fn render_export_text(session: &Session) -> String {
  2102. let mut lines = vec!["# Conversation Export".to_string(), String::new()];
  2103. for (index, message) in session.messages.iter().enumerate() {
  2104. let role = match message.role {
  2105. MessageRole::System => "system",
  2106. MessageRole::User => "user",
  2107. MessageRole::Assistant => "assistant",
  2108. MessageRole::Tool => "tool",
  2109. };
  2110. lines.push(format!("## {}. {role}", index + 1));
  2111. for block in &message.blocks {
  2112. match block {
  2113. ContentBlock::Text { text } => lines.push(text.clone()),
  2114. ContentBlock::ToolUse { id, name, input } => {
  2115. lines.push(format!("[tool_use id={id} name={name}] {input}"));
  2116. }
  2117. ContentBlock::ToolResult {
  2118. tool_use_id,
  2119. tool_name,
  2120. output,
  2121. is_error,
  2122. } => {
  2123. lines.push(format!(
  2124. "[tool_result id={tool_use_id} name={tool_name} error={is_error}] {output}"
  2125. ));
  2126. }
  2127. }
  2128. }
  2129. lines.push(String::new());
  2130. }
  2131. lines.join("\n")
  2132. }
  2133. fn default_export_filename(session: &Session) -> String {
  2134. let stem = session
  2135. .messages
  2136. .iter()
  2137. .find_map(|message| match message.role {
  2138. MessageRole::User => message.blocks.iter().find_map(|block| match block {
  2139. ContentBlock::Text { text } => Some(text.as_str()),
  2140. _ => None,
  2141. }),
  2142. _ => None,
  2143. })
  2144. .map_or("conversation", |text| {
  2145. text.lines().next().unwrap_or("conversation")
  2146. })
  2147. .chars()
  2148. .map(|ch| {
  2149. if ch.is_ascii_alphanumeric() {
  2150. ch.to_ascii_lowercase()
  2151. } else {
  2152. '-'
  2153. }
  2154. })
  2155. .collect::<String>()
  2156. .split('-')
  2157. .filter(|part| !part.is_empty())
  2158. .take(8)
  2159. .collect::<Vec<_>>()
  2160. .join("-");
  2161. let fallback = if stem.is_empty() {
  2162. "conversation"
  2163. } else {
  2164. &stem
  2165. };
  2166. format!("{fallback}.txt")
  2167. }
  2168. fn resolve_export_path(
  2169. requested_path: Option<&str>,
  2170. session: &Session,
  2171. ) -> Result<PathBuf, Box<dyn std::error::Error>> {
  2172. let cwd = env::current_dir()?;
  2173. let file_name =
  2174. requested_path.map_or_else(|| default_export_filename(session), ToOwned::to_owned);
  2175. let final_name = if Path::new(&file_name)
  2176. .extension()
  2177. .is_some_and(|ext| ext.eq_ignore_ascii_case("txt"))
  2178. {
  2179. file_name
  2180. } else {
  2181. format!("{file_name}.txt")
  2182. };
  2183. Ok(cwd.join(final_name))
  2184. }
  2185. fn build_system_prompt() -> Result<Vec<String>, Box<dyn std::error::Error>> {
  2186. Ok(load_system_prompt(
  2187. env::current_dir()?,
  2188. DEFAULT_DATE,
  2189. env::consts::OS,
  2190. "unknown",
  2191. )?)
  2192. }
  2193. fn build_runtime_plugin_state() -> Result<
  2194. (
  2195. runtime::RuntimeFeatureConfig,
  2196. PluginRegistry,
  2197. GlobalToolRegistry,
  2198. ),
  2199. Box<dyn std::error::Error>,
  2200. > {
  2201. let cwd = env::current_dir()?;
  2202. let loader = ConfigLoader::default_for(&cwd);
  2203. let runtime_config = loader.load()?;
  2204. let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
  2205. let plugin_registry = plugin_manager.plugin_registry()?;
  2206. let tool_registry = GlobalToolRegistry::with_plugin_tools(plugin_registry.aggregated_tools()?)?;
  2207. Ok((
  2208. runtime_config.feature_config().clone(),
  2209. plugin_registry,
  2210. tool_registry,
  2211. ))
  2212. }
  2213. fn build_plugin_manager(
  2214. cwd: &Path,
  2215. loader: &ConfigLoader,
  2216. runtime_config: &runtime::RuntimeConfig,
  2217. ) -> PluginManager {
  2218. let plugin_settings = runtime_config.plugins();
  2219. let mut plugin_config = PluginManagerConfig::new(loader.config_home().to_path_buf());
  2220. plugin_config.enabled_plugins = plugin_settings.enabled_plugins().clone();
  2221. plugin_config.external_dirs = plugin_settings
  2222. .external_directories()
  2223. .iter()
  2224. .map(|path| resolve_plugin_path(cwd, loader.config_home(), path))
  2225. .collect();
  2226. plugin_config.install_root = plugin_settings
  2227. .install_root()
  2228. .map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
  2229. plugin_config.registry_path = plugin_settings
  2230. .registry_path()
  2231. .map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
  2232. plugin_config.bundled_root = plugin_settings
  2233. .bundled_root()
  2234. .map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
  2235. PluginManager::new(plugin_config)
  2236. }
  2237. fn resolve_plugin_path(cwd: &Path, config_home: &Path, value: &str) -> PathBuf {
  2238. let path = PathBuf::from(value);
  2239. if path.is_absolute() {
  2240. path
  2241. } else if value.starts_with('.') {
  2242. cwd.join(path)
  2243. } else {
  2244. config_home.join(path)
  2245. }
  2246. }
  2247. #[derive(Debug, Clone, PartialEq, Eq)]
  2248. struct InternalPromptProgressState {
  2249. command_label: &'static str,
  2250. task_label: String,
  2251. step: usize,
  2252. phase: String,
  2253. detail: Option<String>,
  2254. saw_final_text: bool,
  2255. }
  2256. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  2257. enum InternalPromptProgressEvent {
  2258. Started,
  2259. Update,
  2260. Heartbeat,
  2261. Complete,
  2262. Failed,
  2263. }
  2264. #[derive(Debug)]
  2265. struct InternalPromptProgressShared {
  2266. state: Mutex<InternalPromptProgressState>,
  2267. output_lock: Mutex<()>,
  2268. started_at: Instant,
  2269. }
  2270. #[derive(Debug, Clone)]
  2271. struct InternalPromptProgressReporter {
  2272. shared: Arc<InternalPromptProgressShared>,
  2273. }
  2274. #[derive(Debug)]
  2275. struct InternalPromptProgressRun {
  2276. reporter: InternalPromptProgressReporter,
  2277. heartbeat_stop: Option<mpsc::Sender<()>>,
  2278. heartbeat_handle: Option<thread::JoinHandle<()>>,
  2279. }
  2280. impl InternalPromptProgressReporter {
  2281. fn ultraplan(task: &str) -> Self {
  2282. Self {
  2283. shared: Arc::new(InternalPromptProgressShared {
  2284. state: Mutex::new(InternalPromptProgressState {
  2285. command_label: "Ultraplan",
  2286. task_label: task.to_string(),
  2287. step: 0,
  2288. phase: "planning started".to_string(),
  2289. detail: Some(format!("task: {task}")),
  2290. saw_final_text: false,
  2291. }),
  2292. output_lock: Mutex::new(()),
  2293. started_at: Instant::now(),
  2294. }),
  2295. }
  2296. }
  2297. fn emit(&self, event: InternalPromptProgressEvent, error: Option<&str>) {
  2298. let snapshot = self.snapshot();
  2299. let line =
  2300. format_internal_prompt_progress_line(event, &snapshot, self.elapsed(), error);
  2301. self.write_line(&line);
  2302. }
  2303. fn mark_model_phase(&self) {
  2304. let snapshot = {
  2305. let mut state = self
  2306. .shared
  2307. .state
  2308. .lock()
  2309. .expect("internal prompt progress state poisoned");
  2310. state.step += 1;
  2311. state.phase = if state.step == 1 {
  2312. "analyzing request".to_string()
  2313. } else {
  2314. "reviewing findings".to_string()
  2315. };
  2316. state.detail = Some(format!("task: {}", state.task_label));
  2317. state.clone()
  2318. };
  2319. self.write_line(&format_internal_prompt_progress_line(
  2320. InternalPromptProgressEvent::Update,
  2321. &snapshot,
  2322. self.elapsed(),
  2323. None,
  2324. ));
  2325. }
  2326. fn mark_tool_phase(&self, name: &str, input: &str) {
  2327. let detail = describe_tool_progress(name, input);
  2328. let snapshot = {
  2329. let mut state = self
  2330. .shared
  2331. .state
  2332. .lock()
  2333. .expect("internal prompt progress state poisoned");
  2334. state.step += 1;
  2335. state.phase = format!("running {name}");
  2336. state.detail = Some(detail);
  2337. state.clone()
  2338. };
  2339. self.write_line(&format_internal_prompt_progress_line(
  2340. InternalPromptProgressEvent::Update,
  2341. &snapshot,
  2342. self.elapsed(),
  2343. None,
  2344. ));
  2345. }
  2346. fn mark_text_phase(&self, text: &str) {
  2347. let trimmed = text.trim();
  2348. if trimmed.is_empty() {
  2349. return;
  2350. }
  2351. let detail = truncate_for_summary(first_visible_line(trimmed), 120);
  2352. let snapshot = {
  2353. let mut state = self
  2354. .shared
  2355. .state
  2356. .lock()
  2357. .expect("internal prompt progress state poisoned");
  2358. if state.saw_final_text {
  2359. return;
  2360. }
  2361. state.saw_final_text = true;
  2362. state.step += 1;
  2363. state.phase = "drafting final plan".to_string();
  2364. state.detail = (!detail.is_empty()).then_some(detail);
  2365. state.clone()
  2366. };
  2367. self.write_line(&format_internal_prompt_progress_line(
  2368. InternalPromptProgressEvent::Update,
  2369. &snapshot,
  2370. self.elapsed(),
  2371. None,
  2372. ));
  2373. }
  2374. fn emit_heartbeat(&self) {
  2375. let snapshot = self.snapshot();
  2376. self.write_line(&format_internal_prompt_progress_line(
  2377. InternalPromptProgressEvent::Heartbeat,
  2378. &snapshot,
  2379. self.elapsed(),
  2380. None,
  2381. ));
  2382. }
  2383. fn snapshot(&self) -> InternalPromptProgressState {
  2384. self.shared
  2385. .state
  2386. .lock()
  2387. .expect("internal prompt progress state poisoned")
  2388. .clone()
  2389. }
  2390. fn elapsed(&self) -> Duration {
  2391. self.shared.started_at.elapsed()
  2392. }
  2393. fn write_line(&self, line: &str) {
  2394. let _guard = self
  2395. .shared
  2396. .output_lock
  2397. .lock()
  2398. .expect("internal prompt progress output lock poisoned");
  2399. let mut stdout = io::stdout();
  2400. let _ = writeln!(stdout, "{line}");
  2401. let _ = stdout.flush();
  2402. }
  2403. }
  2404. impl InternalPromptProgressRun {
  2405. fn start_ultraplan(task: &str) -> Self {
  2406. let reporter = InternalPromptProgressReporter::ultraplan(task);
  2407. reporter.emit(InternalPromptProgressEvent::Started, None);
  2408. let (heartbeat_stop, heartbeat_rx) = mpsc::channel();
  2409. let heartbeat_reporter = reporter.clone();
  2410. let heartbeat_handle = thread::spawn(move || {
  2411. loop {
  2412. match heartbeat_rx.recv_timeout(INTERNAL_PROGRESS_HEARTBEAT_INTERVAL) {
  2413. Ok(()) | Err(RecvTimeoutError::Disconnected) => break,
  2414. Err(RecvTimeoutError::Timeout) => heartbeat_reporter.emit_heartbeat(),
  2415. }
  2416. }
  2417. });
  2418. Self {
  2419. reporter,
  2420. heartbeat_stop: Some(heartbeat_stop),
  2421. heartbeat_handle: Some(heartbeat_handle),
  2422. }
  2423. }
  2424. fn reporter(&self) -> InternalPromptProgressReporter {
  2425. self.reporter.clone()
  2426. }
  2427. fn finish_success(&mut self) {
  2428. self.stop_heartbeat();
  2429. self.reporter.emit(InternalPromptProgressEvent::Complete, None);
  2430. }
  2431. fn finish_failure(&mut self, error: &str) {
  2432. self.stop_heartbeat();
  2433. self.reporter
  2434. .emit(InternalPromptProgressEvent::Failed, Some(error));
  2435. }
  2436. fn stop_heartbeat(&mut self) {
  2437. if let Some(sender) = self.heartbeat_stop.take() {
  2438. let _ = sender.send(());
  2439. }
  2440. if let Some(handle) = self.heartbeat_handle.take() {
  2441. let _ = handle.join();
  2442. }
  2443. }
  2444. }
  2445. impl Drop for InternalPromptProgressRun {
  2446. fn drop(&mut self) {
  2447. self.stop_heartbeat();
  2448. }
  2449. }
  2450. fn format_internal_prompt_progress_line(
  2451. event: InternalPromptProgressEvent,
  2452. snapshot: &InternalPromptProgressState,
  2453. elapsed: Duration,
  2454. error: Option<&str>,
  2455. ) -> String {
  2456. let elapsed_seconds = elapsed.as_secs();
  2457. let step_label = if snapshot.step == 0 {
  2458. "current step pending".to_string()
  2459. } else {
  2460. format!("current step {}", snapshot.step)
  2461. };
  2462. let mut status_bits = vec![step_label, format!("phase {}", snapshot.phase)];
  2463. if let Some(detail) = snapshot.detail.as_deref().filter(|detail| !detail.is_empty()) {
  2464. status_bits.push(detail.to_string());
  2465. }
  2466. let status = status_bits.join(" · ");
  2467. match event {
  2468. InternalPromptProgressEvent::Started => {
  2469. format!("🧭 {} status · planning started · {status}", snapshot.command_label)
  2470. }
  2471. InternalPromptProgressEvent::Update => {
  2472. format!("… {} status · {status}", snapshot.command_label)
  2473. }
  2474. InternalPromptProgressEvent::Heartbeat => format!(
  2475. "… {} heartbeat · {elapsed_seconds}s elapsed · {status}",
  2476. snapshot.command_label
  2477. ),
  2478. InternalPromptProgressEvent::Complete => format!(
  2479. "✔ {} status · completed · {elapsed_seconds}s elapsed · {} steps total",
  2480. snapshot.command_label,
  2481. snapshot.step
  2482. ),
  2483. InternalPromptProgressEvent::Failed => format!(
  2484. "✘ {} status · failed · {elapsed_seconds}s elapsed · {}",
  2485. snapshot.command_label,
  2486. error.unwrap_or("unknown error")
  2487. ),
  2488. }
  2489. }
  2490. fn describe_tool_progress(name: &str, input: &str) -> String {
  2491. let parsed: serde_json::Value =
  2492. serde_json::from_str(input).unwrap_or(serde_json::Value::String(input.to_string()));
  2493. match name {
  2494. "bash" | "Bash" => {
  2495. let command = parsed
  2496. .get("command")
  2497. .and_then(|value| value.as_str())
  2498. .unwrap_or_default();
  2499. if command.is_empty() {
  2500. "running shell command".to_string()
  2501. } else {
  2502. format!("command {}", truncate_for_summary(command.trim(), 100))
  2503. }
  2504. }
  2505. "read_file" | "Read" => format!("reading {}", extract_tool_path(&parsed)),
  2506. "write_file" | "Write" => format!("writing {}", extract_tool_path(&parsed)),
  2507. "edit_file" | "Edit" => format!("editing {}", extract_tool_path(&parsed)),
  2508. "glob_search" | "Glob" => {
  2509. let pattern = parsed
  2510. .get("pattern")
  2511. .and_then(|value| value.as_str())
  2512. .unwrap_or("?");
  2513. let scope = parsed
  2514. .get("path")
  2515. .and_then(|value| value.as_str())
  2516. .unwrap_or(".");
  2517. format!("glob `{pattern}` in {scope}")
  2518. }
  2519. "grep_search" | "Grep" => {
  2520. let pattern = parsed
  2521. .get("pattern")
  2522. .and_then(|value| value.as_str())
  2523. .unwrap_or("?");
  2524. let scope = parsed
  2525. .get("path")
  2526. .and_then(|value| value.as_str())
  2527. .unwrap_or(".");
  2528. format!("grep `{pattern}` in {scope}")
  2529. }
  2530. "web_search" | "WebSearch" => parsed
  2531. .get("query")
  2532. .and_then(|value| value.as_str())
  2533. .map_or_else(
  2534. || "running web search".to_string(),
  2535. |query| format!("query {}", truncate_for_summary(query, 100)),
  2536. ),
  2537. _ => {
  2538. let summary = summarize_tool_payload(input);
  2539. if summary.is_empty() {
  2540. format!("running {name}")
  2541. } else {
  2542. format!("{name}: {summary}")
  2543. }
  2544. }
  2545. }
  2546. }
  2547. #[allow(clippy::needless_pass_by_value)]
  2548. fn build_runtime(
  2549. session: Session,
  2550. model: String,
  2551. system_prompt: Vec<String>,
  2552. enable_tools: bool,
  2553. emit_output: bool,
  2554. allowed_tools: Option<AllowedToolSet>,
  2555. permission_mode: PermissionMode,
  2556. progress_reporter: Option<InternalPromptProgressReporter>,
  2557. ) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
  2558. {
  2559. let (feature_config, plugin_registry, tool_registry) = build_runtime_plugin_state()?;
  2560. Ok(ConversationRuntime::new_with_plugins(
  2561. session,
  2562. AnthropicRuntimeClient::new(
  2563. model,
  2564. enable_tools,
  2565. emit_output,
  2566. allowed_tools.clone(),
  2567. tool_registry.clone(),
  2568. progress_reporter,
  2569. )?,
  2570. CliToolExecutor::new(allowed_tools.clone(), emit_output, tool_registry.clone()),
  2571. permission_policy(permission_mode, &tool_registry),
  2572. system_prompt,
  2573. feature_config,
  2574. plugin_registry,
  2575. )?)
  2576. }
  2577. struct CliPermissionPrompter {
  2578. current_mode: PermissionMode,
  2579. }
  2580. impl CliPermissionPrompter {
  2581. fn new(current_mode: PermissionMode) -> Self {
  2582. Self { current_mode }
  2583. }
  2584. }
  2585. impl runtime::PermissionPrompter for CliPermissionPrompter {
  2586. fn decide(
  2587. &mut self,
  2588. request: &runtime::PermissionRequest,
  2589. ) -> runtime::PermissionPromptDecision {
  2590. println!();
  2591. println!("Permission approval required");
  2592. println!(" Tool {}", request.tool_name);
  2593. println!(" Current mode {}", self.current_mode.as_str());
  2594. println!(" Required mode {}", request.required_mode.as_str());
  2595. println!(" Input {}", request.input);
  2596. print!("Approve this tool call? [y/N]: ");
  2597. let _ = io::stdout().flush();
  2598. let mut response = String::new();
  2599. match io::stdin().read_line(&mut response) {
  2600. Ok(_) => {
  2601. let normalized = response.trim().to_ascii_lowercase();
  2602. if matches!(normalized.as_str(), "y" | "yes") {
  2603. runtime::PermissionPromptDecision::Allow
  2604. } else {
  2605. runtime::PermissionPromptDecision::Deny {
  2606. reason: format!(
  2607. "tool '{}' denied by user approval prompt",
  2608. request.tool_name
  2609. ),
  2610. }
  2611. }
  2612. }
  2613. Err(error) => runtime::PermissionPromptDecision::Deny {
  2614. reason: format!("permission approval failed: {error}"),
  2615. },
  2616. }
  2617. }
  2618. }
  2619. struct AnthropicRuntimeClient {
  2620. runtime: tokio::runtime::Runtime,
  2621. client: AnthropicClient,
  2622. model: String,
  2623. enable_tools: bool,
  2624. emit_output: bool,
  2625. allowed_tools: Option<AllowedToolSet>,
  2626. tool_registry: GlobalToolRegistry,
  2627. progress_reporter: Option<InternalPromptProgressReporter>,
  2628. }
  2629. impl AnthropicRuntimeClient {
  2630. fn new(
  2631. model: String,
  2632. enable_tools: bool,
  2633. emit_output: bool,
  2634. allowed_tools: Option<AllowedToolSet>,
  2635. tool_registry: GlobalToolRegistry,
  2636. progress_reporter: Option<InternalPromptProgressReporter>,
  2637. ) -> Result<Self, Box<dyn std::error::Error>> {
  2638. Ok(Self {
  2639. runtime: tokio::runtime::Runtime::new()?,
  2640. client: AnthropicClient::from_auth(resolve_cli_auth_source()?)
  2641. .with_base_url(api::read_base_url()),
  2642. model,
  2643. enable_tools,
  2644. emit_output,
  2645. allowed_tools,
  2646. tool_registry,
  2647. progress_reporter,
  2648. })
  2649. }
  2650. }
  2651. fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
  2652. Ok(resolve_startup_auth_source(|| {
  2653. let cwd = env::current_dir().map_err(api::ApiError::from)?;
  2654. let config = ConfigLoader::default_for(&cwd).load().map_err(|error| {
  2655. api::ApiError::Auth(format!("failed to load runtime OAuth config: {error}"))
  2656. })?;
  2657. Ok(config.oauth().cloned())
  2658. })?)
  2659. }
  2660. impl ApiClient for AnthropicRuntimeClient {
  2661. #[allow(clippy::too_many_lines)]
  2662. fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
  2663. if let Some(progress_reporter) = &self.progress_reporter {
  2664. progress_reporter.mark_model_phase();
  2665. }
  2666. let message_request = MessageRequest {
  2667. model: self.model.clone(),
  2668. max_tokens: max_tokens_for_model(&self.model),
  2669. messages: convert_messages(&request.messages),
  2670. system: (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n")),
  2671. tools: self
  2672. .enable_tools
  2673. .then(|| filter_tool_specs(&self.tool_registry, self.allowed_tools.as_ref())),
  2674. tool_choice: self.enable_tools.then_some(ToolChoice::Auto),
  2675. stream: true,
  2676. };
  2677. self.runtime.block_on(async {
  2678. let mut stream = self
  2679. .client
  2680. .stream_message(&message_request)
  2681. .await
  2682. .map_err(|error| RuntimeError::new(error.to_string()))?;
  2683. let mut stdout = io::stdout();
  2684. let mut sink = io::sink();
  2685. let out: &mut dyn Write = if self.emit_output {
  2686. &mut stdout
  2687. } else {
  2688. &mut sink
  2689. };
  2690. let renderer = TerminalRenderer::new();
  2691. let mut markdown_stream = MarkdownStreamState::default();
  2692. let mut events = Vec::new();
  2693. let mut pending_tool: Option<(String, String, String)> = None;
  2694. let mut saw_stop = false;
  2695. while let Some(event) = stream
  2696. .next_event()
  2697. .await
  2698. .map_err(|error| RuntimeError::new(error.to_string()))?
  2699. {
  2700. match event {
  2701. ApiStreamEvent::MessageStart(start) => {
  2702. for block in start.message.content {
  2703. push_output_block(block, out, &mut events, &mut pending_tool, true)?;
  2704. }
  2705. }
  2706. ApiStreamEvent::ContentBlockStart(start) => {
  2707. push_output_block(
  2708. start.content_block,
  2709. out,
  2710. &mut events,
  2711. &mut pending_tool,
  2712. true,
  2713. )?;
  2714. }
  2715. ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta {
  2716. ContentBlockDelta::TextDelta { text } => {
  2717. if !text.is_empty() {
  2718. if let Some(progress_reporter) = &self.progress_reporter {
  2719. progress_reporter.mark_text_phase(&text);
  2720. }
  2721. if let Some(rendered) = markdown_stream.push(&renderer, &text) {
  2722. write!(out, "{rendered}")
  2723. .and_then(|()| out.flush())
  2724. .map_err(|error| RuntimeError::new(error.to_string()))?;
  2725. }
  2726. events.push(AssistantEvent::TextDelta(text));
  2727. }
  2728. }
  2729. ContentBlockDelta::InputJsonDelta { partial_json } => {
  2730. if let Some((_, _, input)) = &mut pending_tool {
  2731. input.push_str(&partial_json);
  2732. }
  2733. }
  2734. ContentBlockDelta::ThinkingDelta { .. }
  2735. | ContentBlockDelta::SignatureDelta { .. } => {}
  2736. },
  2737. ApiStreamEvent::ContentBlockStop(_) => {
  2738. if let Some(rendered) = markdown_stream.flush(&renderer) {
  2739. write!(out, "{rendered}")
  2740. .and_then(|()| out.flush())
  2741. .map_err(|error| RuntimeError::new(error.to_string()))?;
  2742. }
  2743. if let Some((id, name, input)) = pending_tool.take() {
  2744. if let Some(progress_reporter) = &self.progress_reporter {
  2745. progress_reporter.mark_tool_phase(&name, &input);
  2746. }
  2747. // Display tool call now that input is fully accumulated
  2748. writeln!(out, "\n{}", format_tool_call_start(&name, &input))
  2749. .and_then(|()| out.flush())
  2750. .map_err(|error| RuntimeError::new(error.to_string()))?;
  2751. events.push(AssistantEvent::ToolUse { id, name, input });
  2752. }
  2753. }
  2754. ApiStreamEvent::MessageDelta(delta) => {
  2755. events.push(AssistantEvent::Usage(TokenUsage {
  2756. input_tokens: delta.usage.input_tokens,
  2757. output_tokens: delta.usage.output_tokens,
  2758. cache_creation_input_tokens: 0,
  2759. cache_read_input_tokens: 0,
  2760. }));
  2761. }
  2762. ApiStreamEvent::MessageStop(_) => {
  2763. saw_stop = true;
  2764. if let Some(rendered) = markdown_stream.flush(&renderer) {
  2765. write!(out, "{rendered}")
  2766. .and_then(|()| out.flush())
  2767. .map_err(|error| RuntimeError::new(error.to_string()))?;
  2768. }
  2769. events.push(AssistantEvent::MessageStop);
  2770. }
  2771. }
  2772. }
  2773. if !saw_stop
  2774. && events.iter().any(|event| {
  2775. matches!(event, AssistantEvent::TextDelta(text) if !text.is_empty())
  2776. || matches!(event, AssistantEvent::ToolUse { .. })
  2777. })
  2778. {
  2779. events.push(AssistantEvent::MessageStop);
  2780. }
  2781. if events
  2782. .iter()
  2783. .any(|event| matches!(event, AssistantEvent::MessageStop))
  2784. {
  2785. return Ok(events);
  2786. }
  2787. let response = self
  2788. .client
  2789. .send_message(&MessageRequest {
  2790. stream: false,
  2791. ..message_request.clone()
  2792. })
  2793. .await
  2794. .map_err(|error| RuntimeError::new(error.to_string()))?;
  2795. response_to_events(response, out)
  2796. })
  2797. }
  2798. }
  2799. fn final_assistant_text(summary: &runtime::TurnSummary) -> String {
  2800. summary
  2801. .assistant_messages
  2802. .last()
  2803. .map(|message| {
  2804. message
  2805. .blocks
  2806. .iter()
  2807. .filter_map(|block| match block {
  2808. ContentBlock::Text { text } => Some(text.as_str()),
  2809. _ => None,
  2810. })
  2811. .collect::<Vec<_>>()
  2812. .join("")
  2813. })
  2814. .unwrap_or_default()
  2815. }
  2816. fn collect_tool_uses(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
  2817. summary
  2818. .assistant_messages
  2819. .iter()
  2820. .flat_map(|message| message.blocks.iter())
  2821. .filter_map(|block| match block {
  2822. ContentBlock::ToolUse { id, name, input } => Some(json!({
  2823. "id": id,
  2824. "name": name,
  2825. "input": input,
  2826. })),
  2827. _ => None,
  2828. })
  2829. .collect()
  2830. }
  2831. fn collect_tool_results(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
  2832. summary
  2833. .tool_results
  2834. .iter()
  2835. .flat_map(|message| message.blocks.iter())
  2836. .filter_map(|block| match block {
  2837. ContentBlock::ToolResult {
  2838. tool_use_id,
  2839. tool_name,
  2840. output,
  2841. is_error,
  2842. } => Some(json!({
  2843. "tool_use_id": tool_use_id,
  2844. "tool_name": tool_name,
  2845. "output": output,
  2846. "is_error": is_error,
  2847. })),
  2848. _ => None,
  2849. })
  2850. .collect()
  2851. }
  2852. fn slash_command_completion_candidates() -> Vec<String> {
  2853. slash_command_specs()
  2854. .iter()
  2855. .map(|spec| format!("/{}", spec.name))
  2856. .collect()
  2857. }
  2858. fn format_tool_call_start(name: &str, input: &str) -> String {
  2859. let parsed: serde_json::Value =
  2860. serde_json::from_str(input).unwrap_or(serde_json::Value::String(input.to_string()));
  2861. let detail = match name {
  2862. "bash" | "Bash" => format_bash_call(&parsed),
  2863. "read_file" | "Read" => {
  2864. let path = extract_tool_path(&parsed);
  2865. format!("\x1b[2m📄 Reading {path}…\x1b[0m")
  2866. }
  2867. "write_file" | "Write" => {
  2868. let path = extract_tool_path(&parsed);
  2869. let lines = parsed
  2870. .get("content")
  2871. .and_then(|value| value.as_str())
  2872. .map_or(0, |content| content.lines().count());
  2873. format!("\x1b[1;32m✏️ Writing {path}\x1b[0m \x1b[2m({lines} lines)\x1b[0m")
  2874. }
  2875. "edit_file" | "Edit" => {
  2876. let path = extract_tool_path(&parsed);
  2877. let old_value = parsed
  2878. .get("old_string")
  2879. .or_else(|| parsed.get("oldString"))
  2880. .and_then(|value| value.as_str())
  2881. .unwrap_or_default();
  2882. let new_value = parsed
  2883. .get("new_string")
  2884. .or_else(|| parsed.get("newString"))
  2885. .and_then(|value| value.as_str())
  2886. .unwrap_or_default();
  2887. format!(
  2888. "\x1b[1;33m📝 Editing {path}\x1b[0m{}",
  2889. format_patch_preview(old_value, new_value)
  2890. .map(|preview| format!("\n{preview}"))
  2891. .unwrap_or_default()
  2892. )
  2893. }
  2894. "glob_search" | "Glob" => format_search_start("🔎 Glob", &parsed),
  2895. "grep_search" | "Grep" => format_search_start("🔎 Grep", &parsed),
  2896. "web_search" | "WebSearch" => parsed
  2897. .get("query")
  2898. .and_then(|value| value.as_str())
  2899. .unwrap_or("?")
  2900. .to_string(),
  2901. _ => summarize_tool_payload(input),
  2902. };
  2903. let border = "─".repeat(name.len() + 8);
  2904. format!(
  2905. "\x1b[38;5;245m╭─ \x1b[1;36m{name}\x1b[0;38;5;245m ─╮\x1b[0m\n\x1b[38;5;245m│\x1b[0m {detail}\n\x1b[38;5;245m╰{border}╯\x1b[0m"
  2906. )
  2907. }
  2908. fn format_tool_result(name: &str, output: &str, is_error: bool) -> String {
  2909. let icon = if is_error {
  2910. "\x1b[1;31m✗\x1b[0m"
  2911. } else {
  2912. "\x1b[1;32m✓\x1b[0m"
  2913. };
  2914. if is_error {
  2915. let summary = truncate_for_summary(output.trim(), 160);
  2916. return if summary.is_empty() {
  2917. format!("{icon} \x1b[38;5;245m{name}\x1b[0m")
  2918. } else {
  2919. format!("{icon} \x1b[38;5;245m{name}\x1b[0m\n\x1b[38;5;203m{summary}\x1b[0m")
  2920. };
  2921. }
  2922. let parsed: serde_json::Value =
  2923. serde_json::from_str(output).unwrap_or(serde_json::Value::String(output.to_string()));
  2924. match name {
  2925. "bash" | "Bash" => format_bash_result(icon, &parsed),
  2926. "read_file" | "Read" => format_read_result(icon, &parsed),
  2927. "write_file" | "Write" => format_write_result(icon, &parsed),
  2928. "edit_file" | "Edit" => format_edit_result(icon, &parsed),
  2929. "glob_search" | "Glob" => format_glob_result(icon, &parsed),
  2930. "grep_search" | "Grep" => format_grep_result(icon, &parsed),
  2931. _ => format_generic_tool_result(icon, name, &parsed),
  2932. }
  2933. }
  2934. const DISPLAY_TRUNCATION_NOTICE: &str =
  2935. "\x1b[2m… output truncated for display; full result preserved in session.\x1b[0m";
  2936. const READ_DISPLAY_MAX_LINES: usize = 80;
  2937. const READ_DISPLAY_MAX_CHARS: usize = 6_000;
  2938. const TOOL_OUTPUT_DISPLAY_MAX_LINES: usize = 60;
  2939. const TOOL_OUTPUT_DISPLAY_MAX_CHARS: usize = 4_000;
  2940. fn extract_tool_path(parsed: &serde_json::Value) -> String {
  2941. parsed
  2942. .get("file_path")
  2943. .or_else(|| parsed.get("filePath"))
  2944. .or_else(|| parsed.get("path"))
  2945. .and_then(|value| value.as_str())
  2946. .unwrap_or("?")
  2947. .to_string()
  2948. }
  2949. fn format_search_start(label: &str, parsed: &serde_json::Value) -> String {
  2950. let pattern = parsed
  2951. .get("pattern")
  2952. .and_then(|value| value.as_str())
  2953. .unwrap_or("?");
  2954. let scope = parsed
  2955. .get("path")
  2956. .and_then(|value| value.as_str())
  2957. .unwrap_or(".");
  2958. format!("{label} {pattern}\n\x1b[2min {scope}\x1b[0m")
  2959. }
  2960. fn format_patch_preview(old_value: &str, new_value: &str) -> Option<String> {
  2961. if old_value.is_empty() && new_value.is_empty() {
  2962. return None;
  2963. }
  2964. Some(format!(
  2965. "\x1b[38;5;203m- {}\x1b[0m\n\x1b[38;5;70m+ {}\x1b[0m",
  2966. truncate_for_summary(first_visible_line(old_value), 72),
  2967. truncate_for_summary(first_visible_line(new_value), 72)
  2968. ))
  2969. }
  2970. fn format_bash_call(parsed: &serde_json::Value) -> String {
  2971. let command = parsed
  2972. .get("command")
  2973. .and_then(|value| value.as_str())
  2974. .unwrap_or_default();
  2975. if command.is_empty() {
  2976. String::new()
  2977. } else {
  2978. format!(
  2979. "\x1b[48;5;236;38;5;255m $ {} \x1b[0m",
  2980. truncate_for_summary(command, 160)
  2981. )
  2982. }
  2983. }
  2984. fn first_visible_line(text: &str) -> &str {
  2985. text.lines()
  2986. .find(|line| !line.trim().is_empty())
  2987. .unwrap_or(text)
  2988. }
  2989. fn format_bash_result(icon: &str, parsed: &serde_json::Value) -> String {
  2990. let mut lines = vec![format!("{icon} \x1b[38;5;245mbash\x1b[0m")];
  2991. if let Some(task_id) = parsed
  2992. .get("backgroundTaskId")
  2993. .and_then(|value| value.as_str())
  2994. {
  2995. write!(&mut lines[0], " backgrounded ({task_id})").expect("write to string");
  2996. } else if let Some(status) = parsed
  2997. .get("returnCodeInterpretation")
  2998. .and_then(|value| value.as_str())
  2999. .filter(|status| !status.is_empty())
  3000. {
  3001. write!(&mut lines[0], " {status}").expect("write to string");
  3002. }
  3003. if let Some(stdout) = parsed.get("stdout").and_then(|value| value.as_str()) {
  3004. if !stdout.trim().is_empty() {
  3005. lines.push(truncate_output_for_display(
  3006. stdout,
  3007. TOOL_OUTPUT_DISPLAY_MAX_LINES,
  3008. TOOL_OUTPUT_DISPLAY_MAX_CHARS,
  3009. ));
  3010. }
  3011. }
  3012. if let Some(stderr) = parsed.get("stderr").and_then(|value| value.as_str()) {
  3013. if !stderr.trim().is_empty() {
  3014. lines.push(format!(
  3015. "\x1b[38;5;203m{}\x1b[0m",
  3016. truncate_output_for_display(
  3017. stderr,
  3018. TOOL_OUTPUT_DISPLAY_MAX_LINES,
  3019. TOOL_OUTPUT_DISPLAY_MAX_CHARS,
  3020. )
  3021. ));
  3022. }
  3023. }
  3024. lines.join("\n\n")
  3025. }
  3026. fn format_read_result(icon: &str, parsed: &serde_json::Value) -> String {
  3027. let file = parsed.get("file").unwrap_or(parsed);
  3028. let path = extract_tool_path(file);
  3029. let start_line = file
  3030. .get("startLine")
  3031. .and_then(serde_json::Value::as_u64)
  3032. .unwrap_or(1);
  3033. let num_lines = file
  3034. .get("numLines")
  3035. .and_then(serde_json::Value::as_u64)
  3036. .unwrap_or(0);
  3037. let total_lines = file
  3038. .get("totalLines")
  3039. .and_then(serde_json::Value::as_u64)
  3040. .unwrap_or(num_lines);
  3041. let content = file
  3042. .get("content")
  3043. .and_then(|value| value.as_str())
  3044. .unwrap_or_default();
  3045. let end_line = start_line.saturating_add(num_lines.saturating_sub(1));
  3046. format!(
  3047. "{icon} \x1b[2m📄 Read {path} (lines {}-{} of {})\x1b[0m\n{}",
  3048. start_line,
  3049. end_line.max(start_line),
  3050. total_lines,
  3051. truncate_output_for_display(content, READ_DISPLAY_MAX_LINES, READ_DISPLAY_MAX_CHARS)
  3052. )
  3053. }
  3054. fn format_write_result(icon: &str, parsed: &serde_json::Value) -> String {
  3055. let path = extract_tool_path(parsed);
  3056. let kind = parsed
  3057. .get("type")
  3058. .and_then(|value| value.as_str())
  3059. .unwrap_or("write");
  3060. let line_count = parsed
  3061. .get("content")
  3062. .and_then(|value| value.as_str())
  3063. .map_or(0, |content| content.lines().count());
  3064. format!(
  3065. "{icon} \x1b[1;32m✏️ {} {path}\x1b[0m \x1b[2m({line_count} lines)\x1b[0m",
  3066. if kind == "create" { "Wrote" } else { "Updated" },
  3067. )
  3068. }
  3069. fn format_structured_patch_preview(parsed: &serde_json::Value) -> Option<String> {
  3070. let hunks = parsed.get("structuredPatch")?.as_array()?;
  3071. let mut preview = Vec::new();
  3072. for hunk in hunks.iter().take(2) {
  3073. let lines = hunk.get("lines")?.as_array()?;
  3074. for line in lines.iter().filter_map(|value| value.as_str()).take(6) {
  3075. match line.chars().next() {
  3076. Some('+') => preview.push(format!("\x1b[38;5;70m{line}\x1b[0m")),
  3077. Some('-') => preview.push(format!("\x1b[38;5;203m{line}\x1b[0m")),
  3078. _ => preview.push(line.to_string()),
  3079. }
  3080. }
  3081. }
  3082. if preview.is_empty() {
  3083. None
  3084. } else {
  3085. Some(preview.join("\n"))
  3086. }
  3087. }
  3088. fn format_edit_result(icon: &str, parsed: &serde_json::Value) -> String {
  3089. let path = extract_tool_path(parsed);
  3090. let suffix = if parsed
  3091. .get("replaceAll")
  3092. .and_then(serde_json::Value::as_bool)
  3093. .unwrap_or(false)
  3094. {
  3095. " (replace all)"
  3096. } else {
  3097. ""
  3098. };
  3099. let preview = format_structured_patch_preview(parsed).or_else(|| {
  3100. let old_value = parsed
  3101. .get("oldString")
  3102. .and_then(|value| value.as_str())
  3103. .unwrap_or_default();
  3104. let new_value = parsed
  3105. .get("newString")
  3106. .and_then(|value| value.as_str())
  3107. .unwrap_or_default();
  3108. format_patch_preview(old_value, new_value)
  3109. });
  3110. match preview {
  3111. Some(preview) => format!("{icon} \x1b[1;33m📝 Edited {path}{suffix}\x1b[0m\n{preview}"),
  3112. None => format!("{icon} \x1b[1;33m📝 Edited {path}{suffix}\x1b[0m"),
  3113. }
  3114. }
  3115. fn format_glob_result(icon: &str, parsed: &serde_json::Value) -> String {
  3116. let num_files = parsed
  3117. .get("numFiles")
  3118. .and_then(serde_json::Value::as_u64)
  3119. .unwrap_or(0);
  3120. let filenames = parsed
  3121. .get("filenames")
  3122. .and_then(|value| value.as_array())
  3123. .map(|files| {
  3124. files
  3125. .iter()
  3126. .filter_map(|value| value.as_str())
  3127. .take(8)
  3128. .collect::<Vec<_>>()
  3129. .join("\n")
  3130. })
  3131. .unwrap_or_default();
  3132. if filenames.is_empty() {
  3133. format!("{icon} \x1b[38;5;245mglob_search\x1b[0m matched {num_files} files")
  3134. } else {
  3135. format!("{icon} \x1b[38;5;245mglob_search\x1b[0m matched {num_files} files\n{filenames}")
  3136. }
  3137. }
  3138. fn format_grep_result(icon: &str, parsed: &serde_json::Value) -> String {
  3139. let num_matches = parsed
  3140. .get("numMatches")
  3141. .and_then(serde_json::Value::as_u64)
  3142. .unwrap_or(0);
  3143. let num_files = parsed
  3144. .get("numFiles")
  3145. .and_then(serde_json::Value::as_u64)
  3146. .unwrap_or(0);
  3147. let content = parsed
  3148. .get("content")
  3149. .and_then(|value| value.as_str())
  3150. .unwrap_or_default();
  3151. let filenames = parsed
  3152. .get("filenames")
  3153. .and_then(|value| value.as_array())
  3154. .map(|files| {
  3155. files
  3156. .iter()
  3157. .filter_map(|value| value.as_str())
  3158. .take(8)
  3159. .collect::<Vec<_>>()
  3160. .join("\n")
  3161. })
  3162. .unwrap_or_default();
  3163. let summary = format!(
  3164. "{icon} \x1b[38;5;245mgrep_search\x1b[0m {num_matches} matches across {num_files} files"
  3165. );
  3166. if !content.trim().is_empty() {
  3167. format!(
  3168. "{summary}\n{}",
  3169. truncate_output_for_display(
  3170. content,
  3171. TOOL_OUTPUT_DISPLAY_MAX_LINES,
  3172. TOOL_OUTPUT_DISPLAY_MAX_CHARS,
  3173. )
  3174. )
  3175. } else if !filenames.is_empty() {
  3176. format!("{summary}\n{filenames}")
  3177. } else {
  3178. summary
  3179. }
  3180. }
  3181. fn format_generic_tool_result(icon: &str, name: &str, parsed: &serde_json::Value) -> String {
  3182. let rendered_output = match parsed {
  3183. serde_json::Value::String(text) => text.clone(),
  3184. serde_json::Value::Null => String::new(),
  3185. serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
  3186. serde_json::to_string_pretty(parsed).unwrap_or_else(|_| parsed.to_string())
  3187. }
  3188. _ => parsed.to_string(),
  3189. };
  3190. let preview = truncate_output_for_display(
  3191. &rendered_output,
  3192. TOOL_OUTPUT_DISPLAY_MAX_LINES,
  3193. TOOL_OUTPUT_DISPLAY_MAX_CHARS,
  3194. );
  3195. if preview.is_empty() {
  3196. format!("{icon} \x1b[38;5;245m{name}\x1b[0m")
  3197. } else if preview.contains('\n') {
  3198. format!("{icon} \x1b[38;5;245m{name}\x1b[0m\n{preview}")
  3199. } else {
  3200. format!("{icon} \x1b[38;5;245m{name}:\x1b[0m {preview}")
  3201. }
  3202. }
  3203. fn summarize_tool_payload(payload: &str) -> String {
  3204. let compact = match serde_json::from_str::<serde_json::Value>(payload) {
  3205. Ok(value) => value.to_string(),
  3206. Err(_) => payload.trim().to_string(),
  3207. };
  3208. truncate_for_summary(&compact, 96)
  3209. }
  3210. fn truncate_for_summary(value: &str, limit: usize) -> String {
  3211. let mut chars = value.chars();
  3212. let truncated = chars.by_ref().take(limit).collect::<String>();
  3213. if chars.next().is_some() {
  3214. format!("{truncated}…")
  3215. } else {
  3216. truncated
  3217. }
  3218. }
  3219. fn truncate_output_for_display(content: &str, max_lines: usize, max_chars: usize) -> String {
  3220. let original = content.trim_end_matches('\n');
  3221. if original.is_empty() {
  3222. return String::new();
  3223. }
  3224. let mut preview_lines = Vec::new();
  3225. let mut used_chars = 0usize;
  3226. let mut truncated = false;
  3227. for (index, line) in original.lines().enumerate() {
  3228. if index >= max_lines {
  3229. truncated = true;
  3230. break;
  3231. }
  3232. let newline_cost = usize::from(!preview_lines.is_empty());
  3233. let available = max_chars.saturating_sub(used_chars + newline_cost);
  3234. if available == 0 {
  3235. truncated = true;
  3236. break;
  3237. }
  3238. let line_chars = line.chars().count();
  3239. if line_chars > available {
  3240. preview_lines.push(line.chars().take(available).collect::<String>());
  3241. truncated = true;
  3242. break;
  3243. }
  3244. preview_lines.push(line.to_string());
  3245. used_chars += newline_cost + line_chars;
  3246. }
  3247. let mut preview = preview_lines.join("\n");
  3248. if truncated {
  3249. if !preview.is_empty() {
  3250. preview.push('\n');
  3251. }
  3252. preview.push_str(DISPLAY_TRUNCATION_NOTICE);
  3253. }
  3254. preview
  3255. }
  3256. fn push_output_block(
  3257. block: OutputContentBlock,
  3258. out: &mut (impl Write + ?Sized),
  3259. events: &mut Vec<AssistantEvent>,
  3260. pending_tool: &mut Option<(String, String, String)>,
  3261. streaming_tool_input: bool,
  3262. ) -> Result<(), RuntimeError> {
  3263. match block {
  3264. OutputContentBlock::Text { text } => {
  3265. if !text.is_empty() {
  3266. let rendered = TerminalRenderer::new().markdown_to_ansi(&text);
  3267. write!(out, "{rendered}")
  3268. .and_then(|()| out.flush())
  3269. .map_err(|error| RuntimeError::new(error.to_string()))?;
  3270. events.push(AssistantEvent::TextDelta(text));
  3271. }
  3272. }
  3273. OutputContentBlock::ToolUse { id, name, input } => {
  3274. // During streaming, the initial content_block_start has an empty input ({}).
  3275. // The real input arrives via input_json_delta events. In
  3276. // non-streaming responses, preserve a legitimate empty object.
  3277. let initial_input = if streaming_tool_input
  3278. && input.is_object()
  3279. && input.as_object().is_some_and(serde_json::Map::is_empty)
  3280. {
  3281. String::new()
  3282. } else {
  3283. input.to_string()
  3284. };
  3285. *pending_tool = Some((id, name, initial_input));
  3286. }
  3287. OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {}
  3288. }
  3289. Ok(())
  3290. }
  3291. fn response_to_events(
  3292. response: MessageResponse,
  3293. out: &mut (impl Write + ?Sized),
  3294. ) -> Result<Vec<AssistantEvent>, RuntimeError> {
  3295. let mut events = Vec::new();
  3296. let mut pending_tool = None;
  3297. for block in response.content {
  3298. push_output_block(block, out, &mut events, &mut pending_tool, false)?;
  3299. if let Some((id, name, input)) = pending_tool.take() {
  3300. events.push(AssistantEvent::ToolUse { id, name, input });
  3301. }
  3302. }
  3303. events.push(AssistantEvent::Usage(TokenUsage {
  3304. input_tokens: response.usage.input_tokens,
  3305. output_tokens: response.usage.output_tokens,
  3306. cache_creation_input_tokens: response.usage.cache_creation_input_tokens,
  3307. cache_read_input_tokens: response.usage.cache_read_input_tokens,
  3308. }));
  3309. events.push(AssistantEvent::MessageStop);
  3310. Ok(events)
  3311. }
  3312. struct CliToolExecutor {
  3313. renderer: TerminalRenderer,
  3314. emit_output: bool,
  3315. allowed_tools: Option<AllowedToolSet>,
  3316. tool_registry: GlobalToolRegistry,
  3317. }
  3318. impl CliToolExecutor {
  3319. fn new(
  3320. allowed_tools: Option<AllowedToolSet>,
  3321. emit_output: bool,
  3322. tool_registry: GlobalToolRegistry,
  3323. ) -> Self {
  3324. Self {
  3325. renderer: TerminalRenderer::new(),
  3326. emit_output,
  3327. allowed_tools,
  3328. tool_registry,
  3329. }
  3330. }
  3331. }
  3332. impl ToolExecutor for CliToolExecutor {
  3333. fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
  3334. if self
  3335. .allowed_tools
  3336. .as_ref()
  3337. .is_some_and(|allowed| !allowed.contains(tool_name))
  3338. {
  3339. return Err(ToolError::new(format!(
  3340. "tool `{tool_name}` is not enabled by the current --allowedTools setting"
  3341. )));
  3342. }
  3343. let value = serde_json::from_str(input)
  3344. .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
  3345. match self.tool_registry.execute(tool_name, &value) {
  3346. Ok(output) => {
  3347. if self.emit_output {
  3348. let markdown = format_tool_result(tool_name, &output, false);
  3349. self.renderer
  3350. .stream_markdown(&markdown, &mut io::stdout())
  3351. .map_err(|error| ToolError::new(error.to_string()))?;
  3352. }
  3353. Ok(output)
  3354. }
  3355. Err(error) => {
  3356. if self.emit_output {
  3357. let markdown = format_tool_result(tool_name, &error, true);
  3358. self.renderer
  3359. .stream_markdown(&markdown, &mut io::stdout())
  3360. .map_err(|stream_error| ToolError::new(stream_error.to_string()))?;
  3361. }
  3362. Err(ToolError::new(error))
  3363. }
  3364. }
  3365. }
  3366. }
  3367. fn permission_policy(mode: PermissionMode, tool_registry: &GlobalToolRegistry) -> PermissionPolicy {
  3368. tool_registry.permission_specs(None).into_iter().fold(
  3369. PermissionPolicy::new(mode),
  3370. |policy, (name, required_permission)| {
  3371. policy.with_tool_requirement(name, required_permission)
  3372. },
  3373. )
  3374. }
  3375. fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
  3376. messages
  3377. .iter()
  3378. .filter_map(|message| {
  3379. let role = match message.role {
  3380. MessageRole::System | MessageRole::User | MessageRole::Tool => "user",
  3381. MessageRole::Assistant => "assistant",
  3382. };
  3383. let content = message
  3384. .blocks
  3385. .iter()
  3386. .map(|block| match block {
  3387. ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() },
  3388. ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
  3389. id: id.clone(),
  3390. name: name.clone(),
  3391. input: serde_json::from_str(input)
  3392. .unwrap_or_else(|_| serde_json::json!({ "raw": input })),
  3393. },
  3394. ContentBlock::ToolResult {
  3395. tool_use_id,
  3396. output,
  3397. is_error,
  3398. ..
  3399. } => InputContentBlock::ToolResult {
  3400. tool_use_id: tool_use_id.clone(),
  3401. content: vec![ToolResultContentBlock::Text {
  3402. text: output.clone(),
  3403. }],
  3404. is_error: *is_error,
  3405. },
  3406. })
  3407. .collect::<Vec<_>>();
  3408. (!content.is_empty()).then(|| InputMessage {
  3409. role: role.to_string(),
  3410. content,
  3411. })
  3412. })
  3413. .collect()
  3414. }
  3415. fn print_help_to(out: &mut impl Write) -> io::Result<()> {
  3416. writeln!(out, "claw v{VERSION}")?;
  3417. writeln!(out)?;
  3418. writeln!(out, "Usage:")?;
  3419. writeln!(
  3420. out,
  3421. " claw [--model MODEL] [--allowedTools TOOL[,TOOL...]]"
  3422. )?;
  3423. writeln!(out, " Start the interactive REPL")?;
  3424. writeln!(
  3425. out,
  3426. " claw [--model MODEL] [--output-format text|json] prompt TEXT"
  3427. )?;
  3428. writeln!(out, " Send one prompt and exit")?;
  3429. writeln!(
  3430. out,
  3431. " claw [--model MODEL] [--output-format text|json] TEXT"
  3432. )?;
  3433. writeln!(out, " Shorthand non-interactive prompt mode")?;
  3434. writeln!(
  3435. out,
  3436. " claw --resume SESSION.json [/status] [/compact] [...]"
  3437. )?;
  3438. writeln!(
  3439. out,
  3440. " Inspect or maintain a saved session without entering the REPL"
  3441. )?;
  3442. writeln!(out, " claw dump-manifests")?;
  3443. writeln!(out, " claw bootstrap-plan")?;
  3444. writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
  3445. writeln!(out, " claw login")?;
  3446. writeln!(out, " claw logout")?;
  3447. writeln!(out, " claw init")?;
  3448. writeln!(out)?;
  3449. writeln!(out, "Flags:")?;
  3450. writeln!(
  3451. out,
  3452. " --model MODEL Override the active model"
  3453. )?;
  3454. writeln!(
  3455. out,
  3456. " --output-format FORMAT Non-interactive output format: text or json"
  3457. )?;
  3458. writeln!(
  3459. out,
  3460. " --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
  3461. )?;
  3462. writeln!(
  3463. out,
  3464. " --dangerously-skip-permissions Skip all permission checks"
  3465. )?;
  3466. writeln!(out, " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)")?;
  3467. writeln!(
  3468. out,
  3469. " --version, -V Print version and build information locally"
  3470. )?;
  3471. writeln!(out)?;
  3472. writeln!(out, "Interactive slash commands:")?;
  3473. writeln!(out, "{}", render_slash_command_help())?;
  3474. writeln!(out)?;
  3475. let resume_commands = resume_supported_slash_commands()
  3476. .into_iter()
  3477. .map(|spec| match spec.argument_hint {
  3478. Some(argument_hint) => format!("/{} {}", spec.name, argument_hint),
  3479. None => format!("/{}", spec.name),
  3480. })
  3481. .collect::<Vec<_>>()
  3482. .join(", ");
  3483. writeln!(out, "Resume-safe commands: {resume_commands}")?;
  3484. writeln!(out, "Examples:")?;
  3485. writeln!(out, " claw --model claude-opus \"summarize this repo\"")?;
  3486. writeln!(
  3487. out,
  3488. " claw --output-format json prompt \"explain src/main.rs\""
  3489. )?;
  3490. writeln!(
  3491. out,
  3492. " claw --allowedTools read,glob \"summarize Cargo.toml\""
  3493. )?;
  3494. writeln!(
  3495. out,
  3496. " claw --resume session.json /status /diff /export notes.txt"
  3497. )?;
  3498. writeln!(out, " claw login")?;
  3499. writeln!(out, " claw init")?;
  3500. Ok(())
  3501. }
  3502. fn print_help() {
  3503. let _ = print_help_to(&mut io::stdout());
  3504. }
  3505. #[cfg(test)]
  3506. mod tests {
  3507. use super::{
  3508. describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report,
  3509. format_internal_prompt_progress_line, format_model_report,
  3510. format_model_switch_report, format_permissions_report,
  3511. format_permissions_switch_report, format_resume_report, format_status_report,
  3512. format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args,
  3513. parse_git_status_metadata, permission_policy, print_help_to, push_output_block,
  3514. render_config_report, render_memory_report, render_repl_help, resolve_model_alias,
  3515. response_to_events, resume_supported_slash_commands, status_context, CliAction,
  3516. CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, SlashCommand,
  3517. StatusUsage, DEFAULT_MODEL,
  3518. };
  3519. use api::{MessageResponse, OutputContentBlock, Usage};
  3520. use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
  3521. use runtime::{AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode};
  3522. use serde_json::json;
  3523. use std::path::PathBuf;
  3524. use std::time::Duration;
  3525. use tools::GlobalToolRegistry;
  3526. fn registry_with_plugin_tool() -> GlobalToolRegistry {
  3527. GlobalToolRegistry::with_plugin_tools(vec![PluginTool::new(
  3528. "plugin-demo@external",
  3529. "plugin-demo",
  3530. PluginToolDefinition {
  3531. name: "plugin_echo".to_string(),
  3532. description: Some("Echo plugin payload".to_string()),
  3533. input_schema: json!({
  3534. "type": "object",
  3535. "properties": {
  3536. "message": { "type": "string" }
  3537. },
  3538. "required": ["message"],
  3539. "additionalProperties": false
  3540. }),
  3541. },
  3542. "echo".to_string(),
  3543. Vec::new(),
  3544. PluginToolPermission::WorkspaceWrite,
  3545. None,
  3546. )])
  3547. .expect("plugin tool registry should build")
  3548. }
  3549. #[test]
  3550. fn defaults_to_repl_when_no_args() {
  3551. assert_eq!(
  3552. parse_args(&[]).expect("args should parse"),
  3553. CliAction::Repl {
  3554. model: DEFAULT_MODEL.to_string(),
  3555. allowed_tools: None,
  3556. permission_mode: PermissionMode::DangerFullAccess,
  3557. }
  3558. );
  3559. }
  3560. #[test]
  3561. fn parses_prompt_subcommand() {
  3562. let args = vec![
  3563. "prompt".to_string(),
  3564. "hello".to_string(),
  3565. "world".to_string(),
  3566. ];
  3567. assert_eq!(
  3568. parse_args(&args).expect("args should parse"),
  3569. CliAction::Prompt {
  3570. prompt: "hello world".to_string(),
  3571. model: DEFAULT_MODEL.to_string(),
  3572. output_format: CliOutputFormat::Text,
  3573. allowed_tools: None,
  3574. permission_mode: PermissionMode::DangerFullAccess,
  3575. }
  3576. );
  3577. }
  3578. #[test]
  3579. fn parses_bare_prompt_and_json_output_flag() {
  3580. let args = vec![
  3581. "--output-format=json".to_string(),
  3582. "--model".to_string(),
  3583. "claude-opus".to_string(),
  3584. "explain".to_string(),
  3585. "this".to_string(),
  3586. ];
  3587. assert_eq!(
  3588. parse_args(&args).expect("args should parse"),
  3589. CliAction::Prompt {
  3590. prompt: "explain this".to_string(),
  3591. model: "claude-opus".to_string(),
  3592. output_format: CliOutputFormat::Json,
  3593. allowed_tools: None,
  3594. permission_mode: PermissionMode::DangerFullAccess,
  3595. }
  3596. );
  3597. }
  3598. #[test]
  3599. fn resolves_model_aliases_in_args() {
  3600. let args = vec![
  3601. "--model".to_string(),
  3602. "opus".to_string(),
  3603. "explain".to_string(),
  3604. "this".to_string(),
  3605. ];
  3606. assert_eq!(
  3607. parse_args(&args).expect("args should parse"),
  3608. CliAction::Prompt {
  3609. prompt: "explain this".to_string(),
  3610. model: "claude-opus-4-6".to_string(),
  3611. output_format: CliOutputFormat::Text,
  3612. allowed_tools: None,
  3613. permission_mode: PermissionMode::DangerFullAccess,
  3614. }
  3615. );
  3616. }
  3617. #[test]
  3618. fn resolves_known_model_aliases() {
  3619. assert_eq!(resolve_model_alias("opus"), "claude-opus-4-6");
  3620. assert_eq!(resolve_model_alias("sonnet"), "claude-sonnet-4-6");
  3621. assert_eq!(resolve_model_alias("haiku"), "claude-haiku-4-5-20251213");
  3622. assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
  3623. }
  3624. #[test]
  3625. fn parses_version_flags_without_initializing_prompt_mode() {
  3626. assert_eq!(
  3627. parse_args(&["--version".to_string()]).expect("args should parse"),
  3628. CliAction::Version
  3629. );
  3630. assert_eq!(
  3631. parse_args(&["-V".to_string()]).expect("args should parse"),
  3632. CliAction::Version
  3633. );
  3634. }
  3635. #[test]
  3636. fn parses_permission_mode_flag() {
  3637. let args = vec!["--permission-mode=read-only".to_string()];
  3638. assert_eq!(
  3639. parse_args(&args).expect("args should parse"),
  3640. CliAction::Repl {
  3641. model: DEFAULT_MODEL.to_string(),
  3642. allowed_tools: None,
  3643. permission_mode: PermissionMode::ReadOnly,
  3644. }
  3645. );
  3646. }
  3647. #[test]
  3648. fn parses_allowed_tools_flags_with_aliases_and_lists() {
  3649. let args = vec![
  3650. "--allowedTools".to_string(),
  3651. "read,glob".to_string(),
  3652. "--allowed-tools=write_file".to_string(),
  3653. ];
  3654. assert_eq!(
  3655. parse_args(&args).expect("args should parse"),
  3656. CliAction::Repl {
  3657. model: DEFAULT_MODEL.to_string(),
  3658. allowed_tools: Some(
  3659. ["glob_search", "read_file", "write_file"]
  3660. .into_iter()
  3661. .map(str::to_string)
  3662. .collect()
  3663. ),
  3664. permission_mode: PermissionMode::DangerFullAccess,
  3665. }
  3666. );
  3667. }
  3668. #[test]
  3669. fn rejects_unknown_allowed_tools() {
  3670. let error = parse_args(&["--allowedTools".to_string(), "teleport".to_string()])
  3671. .expect_err("tool should be rejected");
  3672. assert!(error.contains("unsupported tool in --allowedTools: teleport"));
  3673. }
  3674. #[test]
  3675. fn parses_system_prompt_options() {
  3676. let args = vec![
  3677. "system-prompt".to_string(),
  3678. "--cwd".to_string(),
  3679. "/tmp/project".to_string(),
  3680. "--date".to_string(),
  3681. "2026-04-01".to_string(),
  3682. ];
  3683. assert_eq!(
  3684. parse_args(&args).expect("args should parse"),
  3685. CliAction::PrintSystemPrompt {
  3686. cwd: PathBuf::from("/tmp/project"),
  3687. date: "2026-04-01".to_string(),
  3688. }
  3689. );
  3690. }
  3691. #[test]
  3692. fn parses_login_and_logout_subcommands() {
  3693. assert_eq!(
  3694. parse_args(&["login".to_string()]).expect("login should parse"),
  3695. CliAction::Login
  3696. );
  3697. assert_eq!(
  3698. parse_args(&["logout".to_string()]).expect("logout should parse"),
  3699. CliAction::Logout
  3700. );
  3701. assert_eq!(
  3702. parse_args(&["init".to_string()]).expect("init should parse"),
  3703. CliAction::Init
  3704. );
  3705. }
  3706. #[test]
  3707. fn parses_resume_flag_with_slash_command() {
  3708. let args = vec![
  3709. "--resume".to_string(),
  3710. "session.json".to_string(),
  3711. "/compact".to_string(),
  3712. ];
  3713. assert_eq!(
  3714. parse_args(&args).expect("args should parse"),
  3715. CliAction::ResumeSession {
  3716. session_path: PathBuf::from("session.json"),
  3717. commands: vec!["/compact".to_string()],
  3718. }
  3719. );
  3720. }
  3721. #[test]
  3722. fn parses_resume_flag_with_multiple_slash_commands() {
  3723. let args = vec![
  3724. "--resume".to_string(),
  3725. "session.json".to_string(),
  3726. "/status".to_string(),
  3727. "/compact".to_string(),
  3728. "/cost".to_string(),
  3729. ];
  3730. assert_eq!(
  3731. parse_args(&args).expect("args should parse"),
  3732. CliAction::ResumeSession {
  3733. session_path: PathBuf::from("session.json"),
  3734. commands: vec![
  3735. "/status".to_string(),
  3736. "/compact".to_string(),
  3737. "/cost".to_string(),
  3738. ],
  3739. }
  3740. );
  3741. }
  3742. #[test]
  3743. fn filtered_tool_specs_respect_allowlist() {
  3744. let allowed = ["read_file", "grep_search"]
  3745. .into_iter()
  3746. .map(str::to_string)
  3747. .collect();
  3748. let filtered = filter_tool_specs(&GlobalToolRegistry::builtin(), Some(&allowed));
  3749. let names = filtered
  3750. .into_iter()
  3751. .map(|spec| spec.name)
  3752. .collect::<Vec<_>>();
  3753. assert_eq!(names, vec!["read_file", "grep_search"]);
  3754. }
  3755. #[test]
  3756. fn filtered_tool_specs_include_plugin_tools() {
  3757. let filtered = filter_tool_specs(&registry_with_plugin_tool(), None);
  3758. let names = filtered
  3759. .into_iter()
  3760. .map(|definition| definition.name)
  3761. .collect::<Vec<_>>();
  3762. assert!(names.contains(&"bash".to_string()));
  3763. assert!(names.contains(&"plugin_echo".to_string()));
  3764. }
  3765. #[test]
  3766. fn permission_policy_uses_plugin_tool_permissions() {
  3767. let policy = permission_policy(PermissionMode::ReadOnly, &registry_with_plugin_tool());
  3768. let required = policy.required_mode_for("plugin_echo");
  3769. assert_eq!(required, PermissionMode::WorkspaceWrite);
  3770. }
  3771. #[test]
  3772. fn shared_help_uses_resume_annotation_copy() {
  3773. let help = commands::render_slash_command_help();
  3774. assert!(help.contains("Slash commands"));
  3775. assert!(help.contains("works with --resume SESSION.json"));
  3776. }
  3777. #[test]
  3778. fn repl_help_includes_shared_commands_and_exit() {
  3779. let help = render_repl_help();
  3780. assert!(help.contains("REPL"));
  3781. assert!(help.contains("/help"));
  3782. assert!(help.contains("/status"));
  3783. assert!(help.contains("/model [model]"));
  3784. assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
  3785. assert!(help.contains("/clear [--confirm]"));
  3786. assert!(help.contains("/cost"));
  3787. assert!(help.contains("/resume <session-path>"));
  3788. assert!(help.contains("/config [env|hooks|model|plugins]"));
  3789. assert!(help.contains("/memory"));
  3790. assert!(help.contains("/init"));
  3791. assert!(help.contains("/diff"));
  3792. assert!(help.contains("/version"));
  3793. assert!(help.contains("/export [file]"));
  3794. assert!(help.contains("/session [list|switch <session-id>]"));
  3795. assert!(help.contains(
  3796. "/plugins [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
  3797. ));
  3798. assert!(help.contains("/exit"));
  3799. }
  3800. #[test]
  3801. fn resume_supported_command_list_matches_expected_surface() {
  3802. let names = resume_supported_slash_commands()
  3803. .into_iter()
  3804. .map(|spec| spec.name)
  3805. .collect::<Vec<_>>();
  3806. assert_eq!(
  3807. names,
  3808. vec![
  3809. "help", "status", "compact", "clear", "cost", "config", "memory", "init", "diff",
  3810. "version", "export",
  3811. ]
  3812. );
  3813. }
  3814. #[test]
  3815. fn resume_report_uses_sectioned_layout() {
  3816. let report = format_resume_report("session.json", 14, 6);
  3817. assert!(report.contains("Session resumed"));
  3818. assert!(report.contains("Session file session.json"));
  3819. assert!(report.contains("Messages 14"));
  3820. assert!(report.contains("Turns 6"));
  3821. }
  3822. #[test]
  3823. fn compact_report_uses_structured_output() {
  3824. let compacted = format_compact_report(8, 5, false);
  3825. assert!(compacted.contains("Compact"));
  3826. assert!(compacted.contains("Result compacted"));
  3827. assert!(compacted.contains("Messages removed 8"));
  3828. let skipped = format_compact_report(0, 3, true);
  3829. assert!(skipped.contains("Result skipped"));
  3830. }
  3831. #[test]
  3832. fn cost_report_uses_sectioned_layout() {
  3833. let report = format_cost_report(runtime::TokenUsage {
  3834. input_tokens: 20,
  3835. output_tokens: 8,
  3836. cache_creation_input_tokens: 3,
  3837. cache_read_input_tokens: 1,
  3838. });
  3839. assert!(report.contains("Cost"));
  3840. assert!(report.contains("Input tokens 20"));
  3841. assert!(report.contains("Output tokens 8"));
  3842. assert!(report.contains("Cache create 3"));
  3843. assert!(report.contains("Cache read 1"));
  3844. assert!(report.contains("Total tokens 32"));
  3845. }
  3846. #[test]
  3847. fn permissions_report_uses_sectioned_layout() {
  3848. let report = format_permissions_report("workspace-write");
  3849. assert!(report.contains("Permissions"));
  3850. assert!(report.contains("Active mode workspace-write"));
  3851. assert!(report.contains("Modes"));
  3852. assert!(report.contains("read-only ○ available Read/search tools only"));
  3853. assert!(report.contains("workspace-write ● current Edit files inside the workspace"));
  3854. assert!(report.contains("danger-full-access ○ available Unrestricted tool access"));
  3855. }
  3856. #[test]
  3857. fn permissions_switch_report_is_structured() {
  3858. let report = format_permissions_switch_report("read-only", "workspace-write");
  3859. assert!(report.contains("Permissions updated"));
  3860. assert!(report.contains("Result mode switched"));
  3861. assert!(report.contains("Previous mode read-only"));
  3862. assert!(report.contains("Active mode workspace-write"));
  3863. assert!(report.contains("Applies to subsequent tool calls"));
  3864. }
  3865. #[test]
  3866. fn init_help_mentions_direct_subcommand() {
  3867. let mut help = Vec::new();
  3868. print_help_to(&mut help).expect("help should render");
  3869. let help = String::from_utf8(help).expect("help should be utf8");
  3870. assert!(help.contains("claw init"));
  3871. }
  3872. #[test]
  3873. fn model_report_uses_sectioned_layout() {
  3874. let report = format_model_report("claude-sonnet", 12, 4);
  3875. assert!(report.contains("Model"));
  3876. assert!(report.contains("Current model claude-sonnet"));
  3877. assert!(report.contains("Session messages 12"));
  3878. assert!(report.contains("Switch models with /model <name>"));
  3879. }
  3880. #[test]
  3881. fn model_switch_report_preserves_context_summary() {
  3882. let report = format_model_switch_report("claude-sonnet", "claude-opus", 9);
  3883. assert!(report.contains("Model updated"));
  3884. assert!(report.contains("Previous claude-sonnet"));
  3885. assert!(report.contains("Current claude-opus"));
  3886. assert!(report.contains("Preserved msgs 9"));
  3887. }
  3888. #[test]
  3889. fn status_line_reports_model_and_token_totals() {
  3890. let status = format_status_report(
  3891. "claude-sonnet",
  3892. StatusUsage {
  3893. message_count: 7,
  3894. turns: 3,
  3895. latest: runtime::TokenUsage {
  3896. input_tokens: 5,
  3897. output_tokens: 4,
  3898. cache_creation_input_tokens: 1,
  3899. cache_read_input_tokens: 0,
  3900. },
  3901. cumulative: runtime::TokenUsage {
  3902. input_tokens: 20,
  3903. output_tokens: 8,
  3904. cache_creation_input_tokens: 2,
  3905. cache_read_input_tokens: 1,
  3906. },
  3907. estimated_tokens: 128,
  3908. },
  3909. "workspace-write",
  3910. &super::StatusContext {
  3911. cwd: PathBuf::from("/tmp/project"),
  3912. session_path: Some(PathBuf::from("session.json")),
  3913. loaded_config_files: 2,
  3914. discovered_config_files: 3,
  3915. memory_file_count: 4,
  3916. project_root: Some(PathBuf::from("/tmp")),
  3917. git_branch: Some("main".to_string()),
  3918. },
  3919. );
  3920. assert!(status.contains("Status"));
  3921. assert!(status.contains("Model claude-sonnet"));
  3922. assert!(status.contains("Permission mode workspace-write"));
  3923. assert!(status.contains("Messages 7"));
  3924. assert!(status.contains("Latest total 10"));
  3925. assert!(status.contains("Cumulative total 31"));
  3926. assert!(status.contains("Cwd /tmp/project"));
  3927. assert!(status.contains("Project root /tmp"));
  3928. assert!(status.contains("Git branch main"));
  3929. assert!(status.contains("Session session.json"));
  3930. assert!(status.contains("Config files loaded 2/3"));
  3931. assert!(status.contains("Memory files 4"));
  3932. }
  3933. #[test]
  3934. fn config_report_supports_section_views() {
  3935. let report = render_config_report(Some("env")).expect("config report should render");
  3936. assert!(report.contains("Merged section: env"));
  3937. let plugins_report =
  3938. render_config_report(Some("plugins")).expect("plugins config report should render");
  3939. assert!(plugins_report.contains("Merged section: plugins"));
  3940. }
  3941. #[test]
  3942. fn memory_report_uses_sectioned_layout() {
  3943. let report = render_memory_report().expect("memory report should render");
  3944. assert!(report.contains("Memory"));
  3945. assert!(report.contains("Working directory"));
  3946. assert!(report.contains("Instruction files"));
  3947. assert!(report.contains("Discovered files"));
  3948. }
  3949. #[test]
  3950. fn config_report_uses_sectioned_layout() {
  3951. let report = render_config_report(None).expect("config report should render");
  3952. assert!(report.contains("Config"));
  3953. assert!(report.contains("Discovered files"));
  3954. assert!(report.contains("Merged JSON"));
  3955. }
  3956. #[test]
  3957. fn parses_git_status_metadata() {
  3958. let (root, branch) = parse_git_status_metadata(Some(
  3959. "## rcc/cli...origin/rcc/cli
  3960. M src/main.rs",
  3961. ));
  3962. assert_eq!(branch.as_deref(), Some("rcc/cli"));
  3963. let _ = root;
  3964. }
  3965. #[test]
  3966. fn status_context_reads_real_workspace_metadata() {
  3967. let context = status_context(None).expect("status context should load");
  3968. assert!(context.cwd.is_absolute());
  3969. assert_eq!(context.discovered_config_files, 5);
  3970. assert!(context.loaded_config_files <= context.discovered_config_files);
  3971. }
  3972. #[test]
  3973. fn normalizes_supported_permission_modes() {
  3974. assert_eq!(normalize_permission_mode("read-only"), Some("read-only"));
  3975. assert_eq!(
  3976. normalize_permission_mode("workspace-write"),
  3977. Some("workspace-write")
  3978. );
  3979. assert_eq!(
  3980. normalize_permission_mode("danger-full-access"),
  3981. Some("danger-full-access")
  3982. );
  3983. assert_eq!(normalize_permission_mode("unknown"), None);
  3984. }
  3985. #[test]
  3986. fn clear_command_requires_explicit_confirmation_flag() {
  3987. assert_eq!(
  3988. SlashCommand::parse("/clear"),
  3989. Some(SlashCommand::Clear { confirm: false })
  3990. );
  3991. assert_eq!(
  3992. SlashCommand::parse("/clear --confirm"),
  3993. Some(SlashCommand::Clear { confirm: true })
  3994. );
  3995. }
  3996. #[test]
  3997. fn parses_resume_and_config_slash_commands() {
  3998. assert_eq!(
  3999. SlashCommand::parse("/resume saved-session.json"),
  4000. Some(SlashCommand::Resume {
  4001. session_path: Some("saved-session.json".to_string())
  4002. })
  4003. );
  4004. assert_eq!(
  4005. SlashCommand::parse("/clear --confirm"),
  4006. Some(SlashCommand::Clear { confirm: true })
  4007. );
  4008. assert_eq!(
  4009. SlashCommand::parse("/config"),
  4010. Some(SlashCommand::Config { section: None })
  4011. );
  4012. assert_eq!(
  4013. SlashCommand::parse("/config env"),
  4014. Some(SlashCommand::Config {
  4015. section: Some("env".to_string())
  4016. })
  4017. );
  4018. assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
  4019. assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));
  4020. }
  4021. #[test]
  4022. fn init_template_mentions_detected_rust_workspace() {
  4023. let rendered = crate::init::render_init_claude_md(std::path::Path::new("."));
  4024. assert!(rendered.contains("# CLAUDE.md"));
  4025. assert!(rendered.contains("cargo clippy --workspace --all-targets -- -D warnings"));
  4026. }
  4027. #[test]
  4028. fn converts_tool_roundtrip_messages() {
  4029. let messages = vec![
  4030. ConversationMessage::user_text("hello"),
  4031. ConversationMessage::assistant(vec![ContentBlock::ToolUse {
  4032. id: "tool-1".to_string(),
  4033. name: "bash".to_string(),
  4034. input: "{\"command\":\"pwd\"}".to_string(),
  4035. }]),
  4036. ConversationMessage {
  4037. role: MessageRole::Tool,
  4038. blocks: vec![ContentBlock::ToolResult {
  4039. tool_use_id: "tool-1".to_string(),
  4040. tool_name: "bash".to_string(),
  4041. output: "ok".to_string(),
  4042. is_error: false,
  4043. }],
  4044. usage: None,
  4045. },
  4046. ];
  4047. let converted = super::convert_messages(&messages);
  4048. assert_eq!(converted.len(), 3);
  4049. assert_eq!(converted[1].role, "assistant");
  4050. assert_eq!(converted[2].role, "user");
  4051. }
  4052. #[test]
  4053. fn repl_help_mentions_history_completion_and_multiline() {
  4054. let help = render_repl_help();
  4055. assert!(help.contains("Up/Down"));
  4056. assert!(help.contains("Tab"));
  4057. assert!(help.contains("Shift+Enter/Ctrl+J"));
  4058. }
  4059. #[test]
  4060. fn tool_rendering_helpers_compact_output() {
  4061. let start = format_tool_call_start("read_file", r#"{"path":"src/main.rs"}"#);
  4062. assert!(start.contains("read_file"));
  4063. assert!(start.contains("src/main.rs"));
  4064. let done = format_tool_result(
  4065. "read_file",
  4066. r#"{"file":{"filePath":"src/main.rs","content":"hello","numLines":1,"startLine":1,"totalLines":1}}"#,
  4067. false,
  4068. );
  4069. assert!(done.contains("📄 Read src/main.rs"));
  4070. assert!(done.contains("hello"));
  4071. }
  4072. #[test]
  4073. fn tool_rendering_truncates_large_read_output_for_display_only() {
  4074. let content = (0..200)
  4075. .map(|index| format!("line {index:03}"))
  4076. .collect::<Vec<_>>()
  4077. .join("\n");
  4078. let output = json!({
  4079. "file": {
  4080. "filePath": "src/main.rs",
  4081. "content": content,
  4082. "numLines": 200,
  4083. "startLine": 1,
  4084. "totalLines": 200
  4085. }
  4086. })
  4087. .to_string();
  4088. let rendered = format_tool_result("read_file", &output, false);
  4089. assert!(rendered.contains("line 000"));
  4090. assert!(rendered.contains("line 079"));
  4091. assert!(!rendered.contains("line 199"));
  4092. assert!(rendered.contains("full result preserved in session"));
  4093. assert!(output.contains("line 199"));
  4094. }
  4095. #[test]
  4096. fn tool_rendering_truncates_large_bash_output_for_display_only() {
  4097. let stdout = (0..120)
  4098. .map(|index| format!("stdout {index:03}"))
  4099. .collect::<Vec<_>>()
  4100. .join("\n");
  4101. let output = json!({
  4102. "stdout": stdout,
  4103. "stderr": "",
  4104. "returnCodeInterpretation": "completed successfully"
  4105. })
  4106. .to_string();
  4107. let rendered = format_tool_result("bash", &output, false);
  4108. assert!(rendered.contains("stdout 000"));
  4109. assert!(rendered.contains("stdout 059"));
  4110. assert!(!rendered.contains("stdout 119"));
  4111. assert!(rendered.contains("full result preserved in session"));
  4112. assert!(output.contains("stdout 119"));
  4113. }
  4114. #[test]
  4115. fn tool_rendering_truncates_generic_long_output_for_display_only() {
  4116. let items = (0..120)
  4117. .map(|index| format!("payload {index:03}"))
  4118. .collect::<Vec<_>>();
  4119. let output = json!({
  4120. "summary": "plugin payload",
  4121. "items": items,
  4122. })
  4123. .to_string();
  4124. let rendered = format_tool_result("plugin_echo", &output, false);
  4125. assert!(rendered.contains("plugin_echo"));
  4126. assert!(rendered.contains("payload 000"));
  4127. assert!(rendered.contains("payload 040"));
  4128. assert!(!rendered.contains("payload 080"));
  4129. assert!(!rendered.contains("payload 119"));
  4130. assert!(rendered.contains("full result preserved in session"));
  4131. assert!(output.contains("payload 119"));
  4132. }
  4133. #[test]
  4134. fn tool_rendering_truncates_raw_generic_output_for_display_only() {
  4135. let output = (0..120)
  4136. .map(|index| format!("raw {index:03}"))
  4137. .collect::<Vec<_>>()
  4138. .join("\n");
  4139. let rendered = format_tool_result("plugin_echo", &output, false);
  4140. assert!(rendered.contains("plugin_echo"));
  4141. assert!(rendered.contains("raw 000"));
  4142. assert!(rendered.contains("raw 059"));
  4143. assert!(!rendered.contains("raw 119"));
  4144. assert!(rendered.contains("full result preserved in session"));
  4145. assert!(output.contains("raw 119"));
  4146. }
  4147. #[test]
  4148. fn push_output_block_renders_markdown_text() {
  4149. let mut out = Vec::new();
  4150. let mut events = Vec::new();
  4151. let mut pending_tool = None;
  4152. push_output_block(
  4153. OutputContentBlock::Text {
  4154. text: "# Heading".to_string(),
  4155. },
  4156. &mut out,
  4157. &mut events,
  4158. &mut pending_tool,
  4159. false,
  4160. )
  4161. .expect("text block should render");
  4162. let rendered = String::from_utf8(out).expect("utf8");
  4163. assert!(rendered.contains("Heading"));
  4164. assert!(rendered.contains('\u{1b}'));
  4165. }
  4166. #[test]
  4167. fn push_output_block_skips_empty_object_prefix_for_tool_streams() {
  4168. let mut out = Vec::new();
  4169. let mut events = Vec::new();
  4170. let mut pending_tool = None;
  4171. push_output_block(
  4172. OutputContentBlock::ToolUse {
  4173. id: "tool-1".to_string(),
  4174. name: "read_file".to_string(),
  4175. input: json!({}),
  4176. },
  4177. &mut out,
  4178. &mut events,
  4179. &mut pending_tool,
  4180. true,
  4181. )
  4182. .expect("tool block should accumulate");
  4183. assert!(events.is_empty());
  4184. assert_eq!(
  4185. pending_tool,
  4186. Some(("tool-1".to_string(), "read_file".to_string(), String::new(),))
  4187. );
  4188. }
  4189. #[test]
  4190. fn response_to_events_preserves_empty_object_json_input_outside_streaming() {
  4191. let mut out = Vec::new();
  4192. let events = response_to_events(
  4193. MessageResponse {
  4194. id: "msg-1".to_string(),
  4195. kind: "message".to_string(),
  4196. model: "claude-opus-4-6".to_string(),
  4197. role: "assistant".to_string(),
  4198. content: vec![OutputContentBlock::ToolUse {
  4199. id: "tool-1".to_string(),
  4200. name: "read_file".to_string(),
  4201. input: json!({}),
  4202. }],
  4203. stop_reason: Some("tool_use".to_string()),
  4204. stop_sequence: None,
  4205. usage: Usage {
  4206. input_tokens: 1,
  4207. output_tokens: 1,
  4208. cache_creation_input_tokens: 0,
  4209. cache_read_input_tokens: 0,
  4210. },
  4211. request_id: None,
  4212. },
  4213. &mut out,
  4214. )
  4215. .expect("response conversion should succeed");
  4216. assert!(matches!(
  4217. &events[0],
  4218. AssistantEvent::ToolUse { name, input, .. }
  4219. if name == "read_file" && input == "{}"
  4220. ));
  4221. }
  4222. #[test]
  4223. fn response_to_events_preserves_non_empty_json_input_outside_streaming() {
  4224. let mut out = Vec::new();
  4225. let events = response_to_events(
  4226. MessageResponse {
  4227. id: "msg-2".to_string(),
  4228. kind: "message".to_string(),
  4229. model: "claude-opus-4-6".to_string(),
  4230. role: "assistant".to_string(),
  4231. content: vec![OutputContentBlock::ToolUse {
  4232. id: "tool-2".to_string(),
  4233. name: "read_file".to_string(),
  4234. input: json!({ "path": "rust/Cargo.toml" }),
  4235. }],
  4236. stop_reason: Some("tool_use".to_string()),
  4237. stop_sequence: None,
  4238. usage: Usage {
  4239. input_tokens: 1,
  4240. output_tokens: 1,
  4241. cache_creation_input_tokens: 0,
  4242. cache_read_input_tokens: 0,
  4243. },
  4244. request_id: None,
  4245. },
  4246. &mut out,
  4247. )
  4248. .expect("response conversion should succeed");
  4249. assert!(matches!(
  4250. &events[0],
  4251. AssistantEvent::ToolUse { name, input, .. }
  4252. if name == "read_file" && input == "{\"path\":\"rust/Cargo.toml\"}"
  4253. ));
  4254. }
  4255. #[test]
  4256. fn response_to_events_ignores_thinking_blocks() {
  4257. let mut out = Vec::new();
  4258. let events = response_to_events(
  4259. MessageResponse {
  4260. id: "msg-3".to_string(),
  4261. kind: "message".to_string(),
  4262. model: "claude-opus-4-6".to_string(),
  4263. role: "assistant".to_string(),
  4264. content: vec![
  4265. OutputContentBlock::Thinking {
  4266. thinking: "step 1".to_string(),
  4267. signature: Some("sig_123".to_string()),
  4268. },
  4269. OutputContentBlock::Text {
  4270. text: "Final answer".to_string(),
  4271. },
  4272. ],
  4273. stop_reason: Some("end_turn".to_string()),
  4274. stop_sequence: None,
  4275. usage: Usage {
  4276. input_tokens: 1,
  4277. output_tokens: 1,
  4278. cache_creation_input_tokens: 0,
  4279. cache_read_input_tokens: 0,
  4280. },
  4281. request_id: None,
  4282. },
  4283. &mut out,
  4284. )
  4285. .expect("response conversion should succeed");
  4286. assert!(matches!(
  4287. &events[0],
  4288. AssistantEvent::TextDelta(text) if text == "Final answer"
  4289. ));
  4290. assert!(!String::from_utf8(out).expect("utf8").contains("step 1"));
  4291. }
  4292. }