5050#include < string>
5151#include < vector>
5252#include < curl/curl.h>
53+ #include < iostream>
54+
5355#include " llama-sb.h"
5456#include " llama-sb-rag.h"
57+
5558#include < notcurses/notcurses.h>
5659
5760namespace fs = std::filesystem;
@@ -344,12 +347,13 @@ static const std::vector<std::string> CODE_EXTENSIONS = {
344347
345348//
346349// Settings persistence (~/.config/nitro/nitro.settings.json)
347- //
348- // A minimal hand-rolled JSON reader/writer for the flat key-value settings
349- // we care about. We deliberately avoid a full JSON library dependency.
350-
351350// Returns the canonical settings path: ~/.config/nitro/settings.json
351+ //
352352static std::string settings_path () {
353+ // Attempt to read settings from the current working directory first
354+ if (fs::exists (" settings.json" )) {
355+ return " settings.json" ;
356+ }
353357 const char *home = getenv (" HOME" );
354358 std::string base = home ? std::string (home) : " ." ;
355359 return base + " /.config/nitro/settings.json" ;
@@ -362,6 +366,10 @@ static std::string history_path() {
362366 return base + " /.config/nitro/history.txt" ;
363367}
364368
369+ //
370+ // A minimal hand-rolled JSON reader/writer for the flat key-value settings
371+ // we care about. We deliberately avoid a full JSON library dependency.
372+ //
365373static bool json_get_string (const std::string &json,
366374 const std::string &key,
367375 std::string &out) {
@@ -551,25 +559,93 @@ static std::string trim(std::string_view str) {
551559 return std::string (str.substr (start, end - start + 1 ));
552560}
553561
554- //
555- // Removes any front and back characters
556- //
557- static std::string disclose (const std::string &input, char c1, char c2) {
558- // Check if string has at least 2 characters
559- if (input.length () < 2 ) {
562+ /*
563+ * unwrap() - Remove a matching outer "wrapper" from a string.
564+ *
565+ * Trims leading/trailing whitespace first, then checks (in order):
566+ *
567+ * 1. Same-character pairs "..." '...' |...| `...`
568+ * 2. Mirror pairs (...) [...] {...}
569+ * 3. HTML-like tags <tag>...</tag>
570+ * 4. Plain angle brackets <...> (fallback if tags don't match)
571+ *
572+ * If none of the above apply, returns the whitespace-trimmed input unchanged.
573+ *
574+ * Examples:
575+ * unwrap("\"hello\"") -> "hello"
576+ * unwrap(" [foo] ") -> "foo"
577+ * unwrap("<b>bold</b>") -> "bold"
578+ * unwrap("<file>x</file>") -> "x"
579+ * unwrap("<hello>") -> "hello"
580+ * unwrap("plain") -> "plain"
581+ * unwrap("") -> ""
582+ */
583+ std::string unwrap (const std::string &input) {
584+ if (input.empty ()) {
560585 return input;
561586 }
587+
588+ size_t left = 0 ;
589+ size_t right = input.length () - 1 ;
590+
591+ while (left <= right && std::isspace (static_cast <unsigned char >(input[left]))) {
592+ left++;
593+ }
594+ while (left <= right && std::isspace (static_cast <unsigned char >(input[right]))) {
595+ right--;
596+ }
597+
598+ if (left > right) {
599+ return " " ;
600+ }
601+
602+ // Same-character pairs: "", '', ||, ``
603+ // Note: [], {} are NOT same-char pairs — they belong in mirror pairs only
604+ if (input[left] == input[right]) {
605+ if (input[left] == ' "' || input[left] == ' \' ' ||
606+ input[left] == ' |' || input[left] == ' `' ) {
607+ return input.substr (left + 1 , right - left - 1 );
608+ }
609+ }
610+
611+ // Mirror pairs: (), [], {}, but NOT <> (handled below as possible HTML tags)
612+ if (input[left] != input[right]) {
613+ if ((input[left] == ' (' && input[right] == ' )' ) ||
614+ (input[left] == ' [' && input[right] == ' ]' ) ||
615+ (input[left] == ' {' && input[right] == ' }' )) {
616+ return input.substr (left + 1 , right - left - 1 );
617+ }
618+ }
619+
620+ // HTML-like tags: <tag>content</tag>
621+ // Also handles plain <...> as a fallback at the end
622+ if (input[left] == ' <' && input[right] == ' >' ) {
623+ // Find end of opening tag
624+ size_t openTagEnd = left + 1 ;
625+ while (openTagEnd <= right && input[openTagEnd] != ' >' ) openTagEnd++;
626+
627+ if (openTagEnd < right) {
628+ std::string openTagName = input.substr (left + 1 , openTagEnd - left - 1 );
629+
630+ // Find start of closing tag (search backwards for '<')
631+ size_t closeTagStart = right;
632+ while (closeTagStart > openTagEnd && input[closeTagStart] != ' <' ) closeTagStart--;
633+
634+ if (closeTagStart > openTagEnd && input[closeTagStart + 1 ] == ' /' ) {
635+ std::string closeTagName = input.substr (closeTagStart + 2 , right - closeTagStart - 2 );
636+
637+ if (!openTagName.empty () && openTagName == closeTagName) {
638+ // Return content between the tags
639+ return input.substr (openTagEnd + 1 , closeTagStart - openTagEnd - 1 );
640+ }
641+ }
642+ }
562643
563- // Check if first and last characters match the specified delimiters
564- if (input[0 ] == c1 && input[input.length () - 1 ] == c2) {
565- // Remove first and last characters
566- std::string result = input;
567- result.erase (0 , 1 );
568- result.erase (input.length () - 1 , 1 );
569- return result;
644+ // Fallback: plain <...> with no matching HTML tags — unwrap the angle brackets
645+ return input.substr (left + 1 , right - left - 1 );
570646 }
571647
572- return input;
648+ return input. substr (left, right - left + 1 ) ;
573649}
574650
575651// ─── colour helpers ──────────────────────────────────────────────────────
@@ -1810,7 +1886,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &
18101886 if (p[0 ] == ' /' ) {
18111887 return p;
18121888 }
1813- return join_path (sandbox, disclose ( disclose (p, ' < ' , ' > ' ), ' [ ' , ' ] ' ));
1889+ return join_path (sandbox, unwrap (p ));
18141890 };
18151891
18161892 tui.append_line (" [tool] → " + op);
@@ -1855,7 +1931,7 @@ std::string AgentState::process_tool(const std::string &cmd, const NitroConfig &
18551931 if (!tui.confirm_dialog (std::format (" Allow model to write {}?" , p))) {
18561932 return " ERROR: action prevented by user" ;
18571933 }
1858- std::string content = disclose ( disclose ( strip_code_fences (arg1, arg2), ' ` ' , ' ` ' ), ' " ' , ' " ' );
1934+ std::string content = unwrap ( strip_code_fences (arg1, arg2));
18591935 return write_file (p, content) ? " OK: written to " + arg1 : " ERROR: write failed for " + arg1;
18601936 }
18611937 if (op == " TOOL:MKDIR" ) {
0 commit comments