Skip to content

Commit 8406b7f

Browse files
author
Chris Warren-Smith
committed
LLAMA: nitro read local settings, replace disclose with unwrap fn
1 parent 6389849 commit 8406b7f

1 file changed

Lines changed: 96 additions & 20 deletions

File tree

llama/nitro.cpp

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@
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

5760
namespace 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+
//
352352
static 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+
//
365373
static 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

Comments
 (0)