Addded Romeo that generates some doc. WIP.

This commit is contained in:
2026-07-18 18:58:29 -04:00
parent 446670bd29
commit 0c583b58da
120 changed files with 6167 additions and 18 deletions
+290
View File
@@ -0,0 +1,290 @@
#include "AiGenerator.h"
#include "HttpClient.h"
#include "JsonParser.h"
#include <Core/Memory/MemoryArena.h>
#include <Core/Common/CoreUtils.h>
#include <stdio.h>
#include <string.h>
#include <Windows.h>
namespace Romeo
{
static Juliet::String ReadFileContent(Juliet::Arena* arena, const Juliet::String& path)
{
Assert(arena != nullptr);
if (path.Size == 0)
{
return {};
}
char pathBuf[512];
size_t copyLen = path.Size < 511 ? path.Size : 511;
memcpy(pathBuf, path.Data, copyLen);
pathBuf[copyLen] = '\0';
FILE* f = nullptr;
if (fopen_s(&f, pathBuf, "rb") != 0 || !f)
{
return {};
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size <= 0)
{
fclose(f);
return {};
}
// Limit to 20KB to keep prompt token size reasonable
if (size > 20480)
{
size = 20480;
}
char* buf = Juliet::ArenaPushArray<char>(arena, static_cast<size_t>(size + 1) JULIET_DEBUG_PARAM("FileReadContent"));
size_t readBytes = fread(buf, 1, static_cast<size_t>(size), f);
buf[readBytes] = '\0';
fclose(f);
return { buf, readBytes };
}
static Juliet::String GetEnvironmentVar(Juliet::Arena* arena, const char* varName)
{
Assert(arena != nullptr);
Assert(varName != nullptr);
DWORD size = GetEnvironmentVariableA(varName, nullptr, 0);
if (size == 0)
{
return {};
}
char* buf = Juliet::ArenaPushArray<char>(arena, size JULIET_DEBUG_PARAM("EnvVar"));
GetEnvironmentVariableA(varName, buf, size);
return { buf, static_cast<size_t>(size - 1) };
}
static bool G_OllamaReady = false;
static bool PostWithRetry(
Juliet::Arena* arena,
const Juliet::String& host,
int port,
const Juliet::String& path,
bool isHttps,
const Juliet::String& requestBody,
Juliet::String& outResponse
)
{
if (isHttps)
{
return HttpClient::Post(arena, host, port, path, isHttps, requestBody, outResponse);
}
if (G_OllamaReady)
{
return HttpClient::Post(arena, host, port, path, isHttps, requestBody, outResponse);
}
int retries = 15;
while (retries > 0)
{
if (HttpClient::Post(arena, host, port, path, isHttps, requestBody, outResponse))
{
G_OllamaReady = true;
return true;
}
retries--;
if (retries > 0)
{
printf("Romeo AI: Local Ollama serve not ready yet. Retrying in 1s (%d retries remaining)...\n", retries);
Sleep(1000);
}
}
return false;
}
static bool PullOllamaModelIfNeeded(Juliet::Arena* arena, const Juliet::String& host, int port, const Juliet::String& modelName)
{
size_t bodyCap = modelName.Size + 64;
char* showBodyBuf = Juliet::ArenaPushArray<char>(arena, bodyCap JULIET_DEBUG_PARAM("ShowBody"));
snprintf(showBodyBuf, bodyCap, "{\"name\":\"%.*s\"}", static_cast<int>(modelName.Size), CStr(modelName));
Juliet::String showBody = Juliet::WrapString(showBodyBuf);
Juliet::String showResponse = {};
bool showSuccess = PostWithRetry(arena, host, port, ConstString("/api/show"), false, showBody, showResponse);
bool needsPull = false;
if (!showSuccess || showResponse.Size == 0)
{
needsPull = true;
}
else
{
JsonValue root = {};
if (Json_Parse(arena, showResponse, root))
{
Juliet::String errorMsg = {};
if (Json_Query(root, "error", errorMsg))
{
needsPull = true;
}
}
else
{
needsPull = true;
}
}
if (needsPull)
{
printf("Romeo AI: Ollama model '%.*s' not found. Pulling model automatically (this may take a few minutes)...\n",
static_cast<int>(modelName.Size), CStr(modelName));
OutputDebugStringA("Romeo AI: Model not found. Pulling model...\n");
char* pullBodyBuf = Juliet::ArenaPushArray<char>(arena, bodyCap + 32 JULIET_DEBUG_PARAM("PullBody"));
snprintf(pullBodyBuf, bodyCap + 32, "{\"name\":\"%.*s\",\"stream\":false}", static_cast<int>(modelName.Size), CStr(modelName));
Juliet::String pullBody = Juliet::WrapString(pullBodyBuf);
Juliet::String pullResponse = {};
bool pullSuccess = PostWithRetry(arena, host, port, ConstString("/api/pull"), false, pullBody, pullResponse);
if (pullSuccess)
{
printf("Romeo AI: Model '%.*s' pulled successfully!\n", static_cast<int>(modelName.Size), CStr(modelName));
OutputDebugStringA("Romeo AI: Model pulled successfully!\n");
return true;
}
else
{
printf("Romeo AI: Failed to pull model '%.*s' from Ollama API.\n", static_cast<int>(modelName.Size), CStr(modelName));
OutputDebugStringA("Romeo AI: Failed to pull model.\n");
return false;
}
}
return true;
}
bool AI_GenerateDescription(
Juliet::Arena* arena,
FileEntry& entry
)
{
Assert(arena != nullptr);
Juliet::String headerContent = ReadFileContent(arena, entry.HeaderPath);
Juliet::String cppContent = ReadFileContent(arena, entry.CppPath);
if (headerContent.Size == 0 && cppContent.Size == 0)
{
return false;
}
// Formulate the prompt
const char* sysPrompt = "You are a technical documentation assistant. Provide a very short, concise, one-paragraph overview (MAXIMUM 50 WORDS) describing the purpose, main responsibilities, and design of this C++ component. Do not include markdown code block styling or titles in your response; return ONLY the description paragraph. If the current description provided below is already good and accurate, please return it exactly as is.";
size_t promptCap = strlen(sysPrompt) + entry.HeaderPath.Size + headerContent.Size + entry.CppPath.Size + cppContent.Size + entry.Description.Size + 512;
char* promptBuf = Juliet::ArenaPushArray<char>(arena, promptCap JULIET_DEBUG_PARAM("PromptBuf"));
int written = 0;
if (entry.Description.Size > 0)
{
written = snprintf(promptBuf, promptCap,
"%s\n\n"
"Current description:\n%.*s\n\n"
"Header file: %.*s\n"
"%.*s\n\n"
"Source file: %.*s\n"
"%.*s\n",
sysPrompt,
static_cast<int>(entry.Description.Size), CStr(entry.Description),
static_cast<int>(entry.HeaderPath.Size), CStr(entry.HeaderPath),
static_cast<int>(headerContent.Size), CStr(headerContent),
static_cast<int>(entry.CppPath.Size), CStr(entry.CppPath),
static_cast<int>(cppContent.Size), CStr(cppContent)
);
}
else
{
written = snprintf(promptBuf, promptCap,
"%s\n\n"
"Header file: %.*s\n"
"%.*s\n\n"
"Source file: %.*s\n"
"%.*s\n",
sysPrompt,
static_cast<int>(entry.HeaderPath.Size), CStr(entry.HeaderPath),
static_cast<int>(headerContent.Size), CStr(headerContent),
static_cast<int>(entry.CppPath.Size), CStr(entry.CppPath),
static_cast<int>(cppContent.Size), CStr(cppContent)
);
}
Juliet::String prompt = { promptBuf, static_cast<size_t>(written) };
Juliet::String escapedPrompt = Json_EscapeString(arena, prompt);
// Try local Ollama
// Default Host: localhost:11434
// Default Model: qwen2.5-coder:1.5b
Juliet::String host = ConstString("127.0.0.1");
int port = 11434;
Juliet::String modelName = GetEnvironmentVar(arena, "ROMEO_LLM_MODEL");
if (modelName.Size == 0)
{
modelName = ConstString("qwen3.5:4b");
}
// Check / pull model if needed
if (!PullOllamaModelIfNeeded(arena, host, port, modelName))
{
return false;
}
printf("Romeo AI: Querying local Ollama (%.*s) for '%.*s' description...\n",
static_cast<int>(modelName.Size), CStr(modelName),
static_cast<int>(entry.HeaderPath.Size > 0 ? entry.HeaderPath.Size : entry.CppPath.Size),
CStr(entry.HeaderPath.Size > 0 ? entry.HeaderPath : entry.CppPath));
size_t bodyCap = modelName.Size + escapedPrompt.Size + 256;
char* bodyBuf = Juliet::ArenaPushArray<char>(arena, bodyCap JULIET_DEBUG_PARAM("OllamaBody"));
snprintf(bodyBuf, bodyCap,
"{\"model\":\"%.*s\",\"prompt\":\"%.*s\",\"stream\":false,\"think\":false}",
static_cast<int>(modelName.Size), CStr(modelName),
static_cast<int>(escapedPrompt.Size), CStr(escapedPrompt)
);
Juliet::String body = Juliet::WrapString(bodyBuf);
Juliet::String response = {};
bool success = PostWithRetry(
arena,
host,
port,
ConstString("/api/generate"),
false, // isHttps
body,
response
);
if (success && response.Size > 0)
{
JsonValue root = {};
if (Json_Parse(arena, response, root))
{
Juliet::String desc = {};
if (Json_Query(root, "response", desc))
{
entry.Description = Juliet::StringCopy(arena, desc);
return true;
}
}
}
printf("Romeo AI: Failed to communicate with local Ollama.\n");
return false;
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <Core/Common/String.h>
#include "Database.h"
namespace Romeo
{
// Generates a description for the file entry using local Ollama or cloud Gemini API.
// Returns true if generation was successful and entry.Description was populated.
[[nodiscard]] bool AI_GenerateDescription(
Juliet::Arena* arena,
FileEntry& entry
);
}
+300
View File
@@ -0,0 +1,300 @@
#include "Database.h"
#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/Memory/MemoryArena.h>
#include <Windows.h>
namespace Romeo
{
void DB_Init(Database& db, Juliet::Arena* arena)
{
printf("Entering DB_Init...\n");
db.DbArena = arena;
db.Entries.Create(arena JULIET_DEBUG_ONLY(, "RomeoDB"));
printf("Exiting DB_Init...\n");
}
struct ScanContext
{
Database* Db;
Juliet::Arena* TempArena;
};
static void ProcessFile(ScanContext& ctx, const Juliet::String& path, const WIN32_FIND_DATAW& findData)
{
// Simple filter: only .h and .cpp
const char* pPath = CStr(path);
size_t len = StringLength(path);
if (len < 3)
{
return;
}
bool isHeader = false;
bool isCpp = false;
if (pPath[len - 2] == '.' && pPath[len - 1] == 'h')
{
isHeader = true;
}
else if (len >= 4 && pPath[len - 4] == '.' && pPath[len - 3] == 'c' && pPath[len - 2] == 'p' && pPath[len - 1] == 'p')
{
isCpp = true;
}
if (!isHeader && !isCpp)
{
return;
}
uint64_t lastModified = (static_cast<uint64_t>(findData.ftLastWriteTime.dwHighDateTime) << 32) | findData.ftLastWriteTime.dwLowDateTime;
// Try to find if we already have it in the DB (ignoring \include\ or \src\ offset)
const char* relStart = pPath;
if (const char* p1 = strstr(pPath, "\\include\\"))
{
relStart = p1 + 9;
}
else if (const char* p2 = strstr(pPath, "\\src\\"))
{
relStart = p2 + 5;
}
size_t baseLen = len - static_cast<size_t>(relStart - pPath);
baseLen -= (isHeader ? 2 : 4);
FileEntry* entry = nullptr;
for (FileEntry& e : ctx.Db->Entries)
{
Juliet::String dbPath = e.HeaderPath.Size > 0 ? e.HeaderPath : e.CppPath;
if (dbPath.Size > 0)
{
const char* pDbPath = CStr(dbPath);
const char* dbRelStart = pDbPath;
if (const char* p1 = strstr(pDbPath, "\\include\\"))
{
dbRelStart = p1 + 9;
}
else if (const char* p2 = strstr(pDbPath, "\\src\\"))
{
dbRelStart = p2 + 5;
}
bool dbIsHeader = (pDbPath[dbPath.Size - 1] == 'h');
size_t dbBaseLen = dbPath.Size - static_cast<size_t>(dbRelStart - pDbPath);
dbBaseLen -= (dbIsHeader ? 2 : 4);
if (baseLen == dbBaseLen && strncmp(relStart, dbRelStart, baseLen) == 0)
{
entry = &e;
break;
}
}
}
if (!entry)
{
FileEntry newEntry = {};
ctx.Db->Entries.PushBack(newEntry);
entry = ctx.Db->Entries.Back();
}
if (isHeader)
{
if (entry->HeaderPath.Size == 0)
{
entry->HeaderPath = Juliet::StringCopy(ctx.Db->DbArena, path);
}
entry->LastModifiedHeader = lastModified;
}
else
{
if (entry->CppPath.Size == 0)
{
entry->CppPath = Juliet::StringCopy(ctx.Db->DbArena, path);
}
entry->LastModifiedCpp = lastModified;
}
}
static void ScanDirectoryRecursive(ScanContext& ctx, const Juliet::String& currentDir)
{
size_t searchPathLen = currentDir.Size + 3; // "\*" + null
char* searchPathBuf = ArenaPushArray<char>(ctx.TempArena, searchPathLen JULIET_DEBUG_PARAM("RomeoDbSearchPathBuf"));
snprintf(searchPathBuf, searchPathLen, "%s\\*", CStr(currentDir));
Juliet::String searchPath = {searchPathBuf, searchPathLen - 1};
// Convert to wide string for Win32
int wLen = MultiByteToWideChar(CP_UTF8, 0, CStr(searchPath), -1, nullptr, 0);
if (wLen <= 0)
{
return;
}
wchar_t* wSearchPath = ArenaPushArray<wchar_t>(ctx.TempArena, static_cast<size_t>(wLen) JULIET_DEBUG_PARAM("RomeoDbWSearch"));
MultiByteToWideChar(CP_UTF8, 0, CStr(searchPath), -1, wSearchPath, wLen);
WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileExW(wSearchPath, FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0);
if (hFind == INVALID_HANDLE_VALUE)
{
return;
}
do
{
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
{
continue;
}
// Convert filename back to UTF8
int uLen = WideCharToMultiByte(CP_UTF8, 0, findData.cFileName, -1, nullptr, 0, nullptr, nullptr);
char* uFileName = ArenaPushArray<char>(ctx.TempArena, static_cast<size_t>(uLen) JULIET_DEBUG_PARAM("RomeoDbUFileName"));
WideCharToMultiByte(CP_UTF8, 0, findData.cFileName, -1, uFileName, uLen, nullptr, nullptr);
size_t fullPathLen = currentDir.Size + 1 + static_cast<size_t>(uLen); // "\" + null
char* fullPathBuf = ArenaPushArray<char>(ctx.TempArena, fullPathLen JULIET_DEBUG_PARAM("RomeoDbFullPathBuf"));
snprintf(fullPathBuf, fullPathLen, "%s\\%s", CStr(currentDir), uFileName);
Juliet::String fullPath = {fullPathBuf, fullPathLen - 1};
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// Skip external/build dirs
if (strcmp(uFileName, "External") != 0 && strcmp(uFileName, "Intermediate") != 0 && strcmp(uFileName, "bin") != 0 && strcmp(uFileName, ".git") != 0)
{
ScanDirectoryRecursive(ctx, fullPath);
}
}
else
{
ProcessFile(ctx, fullPath, findData);
}
} while (FindNextFileW(hFind, &findData) != 0);
FindClose(hFind);
}
void DB_ScanWorkspace(Database& db, Juliet::Arena* tempArena, const Juliet::String& rootDir)
{
Assert(tempArena != nullptr);
ScanContext ctx;
ctx.Db = &db;
ctx.TempArena = tempArena;
// Scan the src and include directories specifically to avoid trash
size_t srcDirLen = rootDir.Size + 12; // "\Juliet\src" + null
char* srcDirBuf = ArenaPushArray<char>(tempArena, srcDirLen JULIET_DEBUG_PARAM("RomeoDbSrcDirBuf"));
snprintf(srcDirBuf, srcDirLen, "%s\\Juliet\\src", CStr(rootDir));
Juliet::String srcDir = {srcDirBuf, srcDirLen - 1};
size_t incDirLen = rootDir.Size + 16; // "\Juliet\include" + null
char* incDirBuf = ArenaPushArray<char>(tempArena, incDirLen JULIET_DEBUG_PARAM("RomeoDbIncDirBuf"));
snprintf(incDirBuf, incDirLen, "%s\\Juliet\\include", CStr(rootDir));
Juliet::String incDir = {incDirBuf, incDirLen - 1};
ScanDirectoryRecursive(ctx, srcDir);
ScanDirectoryRecursive(ctx, incDir);
}
bool DB_Save(const Database& db, const Juliet::String& filePath)
{
FILE* file = nullptr;
if (fopen_s(&file, CStr(filePath), "wb") != 0 || !file)
{
return false;
}
uint64_t entryCount = db.Entries.Size();
fwrite(&entryCount, sizeof(uint64_t), 1, file);
for (const FileEntry& entry : db.Entries)
{
uint64_t hLen = entry.HeaderPath.Size;
fwrite(&hLen, sizeof(uint64_t), 1, file);
if (hLen > 0) fwrite(entry.HeaderPath.Data, 1, static_cast<size_t>(hLen), file);
uint64_t cLen = entry.CppPath.Size;
fwrite(&cLen, sizeof(uint64_t), 1, file);
if (cLen > 0) fwrite(entry.CppPath.Data, 1, static_cast<size_t>(cLen), file);
fwrite(&entry.LastModifiedHeader, sizeof(uint64_t), 1, file);
fwrite(&entry.LastModifiedCpp, sizeof(uint64_t), 1, file);
fwrite(&entry.LastMarkdownCreatedTime, sizeof(uint64_t), 1, file);
fwrite(&entry.LastAIGeneratedTime, sizeof(uint64_t), 1, file);
uint64_t dLen = entry.Description.Size;
fwrite(&dLen, sizeof(uint64_t), 1, file);
if (dLen > 0) fwrite(entry.Description.Data, 1, static_cast<size_t>(dLen), file);
}
fclose(file);
return true;
}
bool DB_Load(Database& db, const Juliet::String& filePath)
{
FILE* file = nullptr;
if (fopen_s(&file, CStr(filePath), "rb") != 0 || !file)
{
return false;
}
db.Entries.Clear();
uint64_t entryCount = 0;
if (fread(&entryCount, sizeof(uint64_t), 1, file) != 1)
{
fclose(file);
return false;
}
for (uint64_t i = 0; i < entryCount; ++i)
{
FileEntry entry = {};
uint64_t hLen = 0;
if (fread(&hLen, sizeof(uint64_t), 1, file) != 1) break;
if (hLen > 0)
{
entry.HeaderPath.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(hLen + 1) JULIET_DEBUG_PARAM("DbLoadHeader"));
entry.HeaderPath.Size = static_cast<size_t>(hLen);
fread(entry.HeaderPath.Data, 1, static_cast<size_t>(hLen), file);
entry.HeaderPath.Data[hLen] = '\0';
}
uint64_t cLen = 0;
if (fread(&cLen, sizeof(uint64_t), 1, file) != 1) break;
if (cLen > 0)
{
entry.CppPath.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(cLen + 1) JULIET_DEBUG_PARAM("DbLoadCpp"));
entry.CppPath.Size = static_cast<size_t>(cLen);
fread(entry.CppPath.Data, 1, static_cast<size_t>(cLen), file);
entry.CppPath.Data[cLen] = '\0';
}
fread(&entry.LastModifiedHeader, sizeof(uint64_t), 1, file);
fread(&entry.LastModifiedCpp, sizeof(uint64_t), 1, file);
fread(&entry.LastMarkdownCreatedTime, sizeof(uint64_t), 1, file);
fread(&entry.LastAIGeneratedTime, sizeof(uint64_t), 1, file);
uint64_t dLen = 0;
if (fread(&dLen, sizeof(uint64_t), 1, file) != 1) break;
if (dLen > 0)
{
entry.Description.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(dLen + 1) JULIET_DEBUG_PARAM("DbLoadDesc"));
entry.Description.Size = static_cast<size_t>(dLen);
fread(entry.Description.Data, 1, static_cast<size_t>(dLen), file);
entry.Description.Data[dLen] = '\0';
}
db.Entries.PushBack(entry);
}
fclose(file);
return true;
}
} // namespace Romeo
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <Core/Common/String.h>
#include <Core/Container/Vector.h>
#include <Core/Common/CoreUtils.h>
#include <Core/Thread/Mutex.h>
namespace Romeo
{
struct FileEntry
{
Juliet::String HeaderPath;
Juliet::String CppPath;
// Stored as Windows FILETIME (uint64_t) for easy comparison
uint64_t LastModifiedHeader = 0;
uint64_t LastModifiedCpp = 0;
uint64_t LastMarkdownCreatedTime = 0;
uint64_t LastAIGeneratedTime = 0;
Juliet::String Description;
};
struct Database
{
Juliet::VectorArena<FileEntry, 512, true> Entries = {};
Juliet::Arena* DbArena = nullptr;
Juliet::Mutex Mutex;
};
// Initialize the DB using the provided memory arena
void DB_Init(Database& db, Juliet::Arena* arena);
// Scan a workspace directory for pairs of .h/.cpp and update the database structure.
void DB_ScanWorkspace(Database& db, Juliet::Arena* tempArena, const Juliet::String& rootDir);
// Save and load DB state from disk (e.g. Romeo/docs/romeo_db.json or .bin)
bool DB_Save(const Database& db, const Juliet::String& filePath);
bool DB_Load(Database& db, const Juliet::String& filePath);
} // namespace Romeo
+196
View File
@@ -0,0 +1,196 @@
#include "HttpClient.h"
#include <Windows.h>
#include <winhttp.h>
#include <Core/Memory/MemoryArena.h>
#include <Core/Common/CoreUtils.h>
namespace Romeo
{
static wchar_t* ConvertToWide(Juliet::Arena* arena, const Juliet::String& str)
{
Assert(arena != nullptr);
if (str.Size == 0)
{
return const_cast<wchar_t*>(L"");
}
int wLen = MultiByteToWideChar(CP_UTF8, 0, CStr(str), static_cast<int>(str.Size), nullptr, 0);
if (wLen <= 0)
{
return const_cast<wchar_t*>(L"");
}
wchar_t* wBuf = Juliet::ArenaPushArray<wchar_t>(arena, static_cast<size_t>(wLen + 1) JULIET_DEBUG_PARAM("WideStr"));
MultiByteToWideChar(CP_UTF8, 0, CStr(str), static_cast<int>(str.Size), wBuf, wLen);
wBuf[wLen] = L'\0';
return wBuf;
}
bool HttpClient::Post(
Juliet::Arena* arena,
const Juliet::String& host,
int port,
const Juliet::String& path,
bool isHttps,
const Juliet::String& requestBody,
Juliet::String& outResponse
)
{
Assert(arena != nullptr);
Assert(host.Size > 0);
bool success = false;
HINTERNET hSession = nullptr;
HINTERNET hConnect = nullptr;
HINTERNET hRequest = nullptr;
hSession = WinHttpOpen(
L"Romeo/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
nullptr,
nullptr,
0
);
if (hSession != nullptr)
{
WinHttpSetTimeouts(hSession, 60000, 60000, 60000, 1800000);
wchar_t* wHost = ConvertToWide(arena, host);
hConnect = WinHttpConnect(
hSession,
wHost,
static_cast<INTERNET_PORT>(port),
0
);
}
if (hConnect != nullptr)
{
wchar_t* wPath = ConvertToWide(arena, path);
hRequest = WinHttpOpenRequest(
hConnect,
L"POST",
wPath,
nullptr,
nullptr,
nullptr,
isHttps ? WINHTTP_FLAG_SECURE : 0
);
}
if (hRequest != nullptr)
{
const wchar_t* headers = L"Content-Type: application/json\r\n";
DWORD headersLen = static_cast<DWORD>(-1);
BOOL sendRes = WinHttpSendRequest(
hRequest,
headers,
headersLen,
const_cast<char*>(CStr(requestBody)),
static_cast<DWORD>(requestBody.Size),
static_cast<DWORD>(requestBody.Size),
0
);
if (sendRes != 0)
{
BOOL recvRes = WinHttpReceiveResponse(hRequest, nullptr);
if (recvRes != 0)
{
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
size_t totalBytes = 0;
size_t capacity = 1024;
char* responseData = Juliet::ArenaPushArray<char>(arena, capacity JULIET_DEBUG_PARAM("HttpResponse"));
do
{
dwSize = 0;
if (WinHttpQueryDataAvailable(hRequest, &dwSize) == 0)
{
break;
}
if (dwSize == 0)
{
break;
}
if (totalBytes + dwSize >= capacity)
{
size_t newCapacity = capacity * 2;
if (newCapacity < totalBytes + dwSize + 1)
{
newCapacity = totalBytes + dwSize + 1;
}
char* newBuf = Juliet::ArenaPushArray<char>(arena, newCapacity JULIET_DEBUG_PARAM("HttpResponseGrow"));
memcpy(newBuf, responseData, totalBytes);
responseData = newBuf;
capacity = newCapacity;
}
dwDownloaded = 0;
if (WinHttpReadData(hRequest, responseData + totalBytes, dwSize, &dwDownloaded) != 0)
{
totalBytes += dwDownloaded;
}
else
{
break;
}
} while (dwSize > 0);
DWORD dwStatusCode = 0;
DWORD dwHeaderSize = sizeof(dwStatusCode);
WinHttpQueryHeaders(
hRequest,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
nullptr,
&dwStatusCode,
&dwHeaderSize,
nullptr
);
if (dwStatusCode == 200)
{
if (totalBytes > 0)
{
responseData[totalBytes] = '\0';
outResponse.Data = responseData;
outResponse.Size = totalBytes;
success = true;
}
}
else
{
if (totalBytes > 0)
{
responseData[totalBytes] = '\0';
printf("Romeo HttpClient: HTTP Error Status %u. Response: %s\n", static_cast<unsigned int>(dwStatusCode), responseData);
}
else
{
printf("Romeo HttpClient: HTTP Error Status %u. Empty response.\n", static_cast<unsigned int>(dwStatusCode));
}
}
}
}
}
if (hRequest != nullptr)
{
WinHttpCloseHandle(hRequest);
}
if (hConnect != nullptr)
{
WinHttpCloseHandle(hConnect);
}
if (hSession != nullptr)
{
WinHttpCloseHandle(hSession);
}
return success;
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <Core/Common/String.h>
namespace Romeo
{
class HttpClient
{
public:
// Sends a POST request over HTTP/HTTPS.
// Response data is allocated on the provided arena and returned in outResponse.
[[nodiscard]] static bool Post(
Juliet::Arena* arena,
const Juliet::String& host,
int port,
const Juliet::String& path,
bool isHttps,
const Juliet::String& requestBody,
Juliet::String& outResponse
);
};
}
+467
View File
@@ -0,0 +1,467 @@
#include "JsonParser.h"
#include <Core/Memory/MemoryArena.h>
#include <Core/Common/CoreUtils.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <new>
namespace Romeo
{
struct JsonParserState
{
Juliet::Arena* Arena;
const char* Ptr;
const char* End;
};
static void SkipWhitespace(JsonParserState& state)
{
while (state.Ptr < state.End)
{
char c = *state.Ptr;
if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
{
state.Ptr++;
}
else
{
break;
}
}
}
static bool ParseString(JsonParserState& state, Juliet::String& outStr)
{
if (state.Ptr >= state.End || *state.Ptr != '"')
{
return false;
}
state.Ptr++;
const char* start = state.Ptr;
size_t len = 0;
bool hasEscape = false;
while (state.Ptr < state.End)
{
char c = *state.Ptr;
if (c == '"')
{
break;
}
else if (c == '\\')
{
hasEscape = true;
state.Ptr++;
if (state.Ptr >= state.End)
{
return false;
}
}
state.Ptr++;
len++;
}
if (state.Ptr >= state.End)
{
return false;
}
const char* endQuote = state.Ptr;
state.Ptr++;
char* buf = Juliet::ArenaPushArray<char>(state.Arena, len + 1 JULIET_DEBUG_PARAM("JsonParsedStr"));
if (hasEscape)
{
size_t writeIdx = 0;
const char* readPtr = start;
while (readPtr < endQuote)
{
char c = *readPtr;
if (c == '\\')
{
readPtr++;
if (readPtr >= endQuote)
{
break;
}
char esc = *readPtr;
if (esc == 'n') { buf[writeIdx++] = '\n'; }
else if (esc == 'r') { buf[writeIdx++] = '\r'; }
else if (esc == 't') { buf[writeIdx++] = '\t'; }
else if (esc == 'b') { buf[writeIdx++] = '\b'; }
else if (esc == 'f') { buf[writeIdx++] = '\f'; }
else if (esc == '"') { buf[writeIdx++] = '"'; }
else if (esc == '\\') { buf[writeIdx++] = '\\'; }
else if (esc == '/') { buf[writeIdx++] = '/'; }
else if (esc == 'u')
{
if (readPtr + 4 < endQuote)
{
buf[writeIdx++] = '?';
readPtr += 4;
}
}
else
{
buf[writeIdx++] = esc;
}
}
else
{
buf[writeIdx++] = c;
}
readPtr++;
}
buf[writeIdx] = '\0';
outStr.Data = buf;
outStr.Size = writeIdx;
}
else
{
memcpy(buf, start, len);
buf[len] = '\0';
outStr.Data = buf;
outStr.Size = len;
}
return true;
}
static bool ParseValue(JsonParserState& state, JsonValue& outVal);
static bool ParseObject(JsonParserState& state, JsonValue& outVal)
{
if (state.Ptr >= state.End || *state.Ptr != '{')
{
return false;
}
state.Ptr++;
outVal.Type = JsonType::Object;
outVal.ObjectVal.Create(state.Arena JULIET_DEBUG_ONLY(, "JsonObject"));
while (state.Ptr < state.End)
{
SkipWhitespace(state);
if (state.Ptr < state.End && *state.Ptr == '}')
{
state.Ptr++;
return true;
}
Juliet::String key = {};
if (!ParseString(state, key))
{
return false;
}
SkipWhitespace(state);
if (state.Ptr >= state.End || *state.Ptr != ':')
{
return false;
}
state.Ptr++;
JsonKeyValue kv = {};
kv.Key = key;
kv.Value = Juliet::ArenaPushArray<JsonValue>(state.Arena, 1 JULIET_DEBUG_PARAM("JsonValueRef"));
new (kv.Value) JsonValue();
if (!ParseValue(state, *kv.Value))
{
return false;
}
outVal.ObjectVal.PushBack(kv);
SkipWhitespace(state);
if (state.Ptr < state.End && *state.Ptr == ',')
{
state.Ptr++;
}
else if (state.Ptr < state.End && *state.Ptr == '}')
{
state.Ptr++;
return true;
}
else
{
return false;
}
}
return false;
}
static bool ParseArray(JsonParserState& state, JsonValue& outVal)
{
if (state.Ptr >= state.End || *state.Ptr != '[')
{
return false;
}
state.Ptr++;
outVal.Type = JsonType::Array;
outVal.ArrayVal.Create(state.Arena JULIET_DEBUG_ONLY(, "JsonArray"));
while (state.Ptr < state.End)
{
SkipWhitespace(state);
if (state.Ptr < state.End && *state.Ptr == ']')
{
state.Ptr++;
return true;
}
JsonValue val = {};
if (!ParseValue(state, val))
{
return false;
}
outVal.ArrayVal.PushBack(val);
SkipWhitespace(state);
if (state.Ptr < state.End && *state.Ptr == ',')
{
state.Ptr++;
}
else if (state.Ptr < state.End && *state.Ptr == ']')
{
state.Ptr++;
return true;
}
else
{
return false;
}
}
return false;
}
static bool ParseNumber(JsonParserState& state, JsonValue& outVal)
{
const char* start = state.Ptr;
while (state.Ptr < state.End)
{
char c = *state.Ptr;
if (isdigit(c) || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E')
{
state.Ptr++;
}
else
{
break;
}
}
size_t len = static_cast<size_t>(state.Ptr - start);
if (len == 0)
{
return false;
}
char temp[64];
if (len >= sizeof(temp))
{
return false;
}
memcpy(temp, start, len);
temp[len] = '\0';
char* endPtr = nullptr;
outVal.NumVal = strtod(temp, &endPtr);
if (endPtr == temp)
{
return false;
}
outVal.Type = JsonType::Number;
return true;
}
static bool ParseValue(JsonParserState& state, JsonValue& outVal)
{
SkipWhitespace(state);
if (state.Ptr >= state.End)
{
return false;
}
char c = *state.Ptr;
if (c == '{')
{
return ParseObject(state, outVal);
}
else if (c == '[')
{
return ParseArray(state, outVal);
}
else if (c == '"')
{
outVal.Type = JsonType::String;
return ParseString(state, outVal.StrVal);
}
else if (c == 't' || c == 'f')
{
size_t remaining = static_cast<size_t>(state.End - state.Ptr);
if (c == 't' && remaining >= 4 && strncmp(state.Ptr, "true", 4) == 0)
{
outVal.Type = JsonType::Bool;
outVal.BoolVal = true;
state.Ptr += 4;
return true;
}
else if (c == 'f' && remaining >= 5 && strncmp(state.Ptr, "false", 5) == 0)
{
outVal.Type = JsonType::Bool;
outVal.BoolVal = false;
state.Ptr += 5;
return true;
}
}
else if (c == 'n')
{
size_t remaining = static_cast<size_t>(state.End - state.Ptr);
if (remaining >= 4 && strncmp(state.Ptr, "null", 4) == 0)
{
outVal.Type = JsonType::Null;
state.Ptr += 4;
return true;
}
}
else if (isdigit(c) || c == '-')
{
return ParseNumber(state, outVal);
}
return false;
}
bool Json_Parse(Juliet::Arena* arena, const Juliet::String& jsonStr, JsonValue& outValue)
{
Assert(arena != nullptr);
if (jsonStr.Size == 0)
{
return false;
}
JsonParserState state;
state.Arena = arena;
state.Ptr = CStr(jsonStr);
state.End = state.Ptr + jsonStr.Size;
return ParseValue(state, outValue);
}
bool Json_Query(const JsonValue& root, const char* path, Juliet::String& outResult)
{
Assert(path != nullptr);
const JsonValue* current = &root;
const char* p = path;
while (*p)
{
const char* start = p;
while (*p && *p != '/')
{
p++;
}
size_t len = static_cast<size_t>(p - start);
if (len == 0)
{
if (*p == '/') { p++; }
continue;
}
if (current->Type == JsonType::Object)
{
const JsonValue* next = nullptr;
for (const JsonKeyValue& kv : current->ObjectVal)
{
if (kv.Key.Size == len && strncmp(CStr(kv.Key), start, len) == 0)
{
next = kv.Value;
break;
}
}
if (!next)
{
return false;
}
current = next;
}
else if (current->Type == JsonType::Array)
{
char tempIndex[32];
if (len >= sizeof(tempIndex))
{
return false;
}
memcpy(tempIndex, start, len);
tempIndex[len] = '\0';
int index = atoi(tempIndex);
if (index < 0 || static_cast<size_t>(index) >= current->ArrayVal.Size())
{
return false;
}
current = &current->ArrayVal[static_cast<size_t>(index)];
}
else
{
return false;
}
if (*p == '/')
{
p++;
}
}
if (current->Type == JsonType::String)
{
outResult = current->StrVal;
return true;
}
return false;
}
Juliet::String Json_EscapeString(Juliet::Arena* arena, const Juliet::String& str)
{
Assert(arena != nullptr);
size_t escLen = 0;
const char* p = CStr(str);
const char* end = p + str.Size;
while (p < end)
{
char c = *p;
if (c == '"' || c == '\\' || c == '\n' || c == '\r' || c == '\t')
{
escLen += 2;
}
else
{
escLen += 1;
}
p++;
}
char* buf = Juliet::ArenaPushArray<char>(arena, escLen + 1 JULIET_DEBUG_PARAM("JsonEscapedStr"));
char* dst = buf;
p = CStr(str);
while (p < end)
{
char c = *p;
if (c == '"') { *dst++ = '\\'; *dst++ = '"'; }
else if (c == '\\') { *dst++ = '\\'; *dst++ = '\\'; }
else if (c == '\n') { *dst++ = '\\'; *dst++ = 'n'; }
else if (c == '\r') { *dst++ = '\\'; *dst++ = 'r'; }
else if (c == '\t') { *dst++ = '\\'; *dst++ = 't'; }
else { *dst++ = c; }
p++;
}
*dst = '\0';
return { buf, escLen };
}
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <Core/Common/String.h>
#include <Core/Container/Vector.h>
namespace Romeo
{
enum class JsonType
{
Null,
Bool,
Number,
String,
Array,
Object
};
struct JsonValue;
struct JsonKeyValue
{
Juliet::String Key;
// Pointer is used here since JsonValue contains JsonKeyValue, forming mutual dependency
JsonValue* Value = nullptr;
};
struct JsonValue
{
JsonType Type = JsonType::Null;
bool BoolVal = false;
double NumVal = 0.0;
Juliet::String StrVal = {};
Juliet::VectorArena<JsonValue, 32, true> ArrayVal = {};
Juliet::VectorArena<JsonKeyValue, 32, true> ObjectVal = {};
};
// Parses a JSON string. Memory is allocated on the provided arena.
[[nodiscard]] bool Json_Parse(Juliet::Arena* arena, const Juliet::String& jsonStr, JsonValue& outValue);
// Queries a nested string value by path (e.g., "candidates/0/content/parts/0/text" or "response").
[[nodiscard]] bool Json_Query(const JsonValue& root, const char* path, Juliet::String& outResult);
// Escapes a string to be safely embedded inside JSON.
[[nodiscard]] Juliet::String Json_EscapeString(Juliet::Arena* arena, const Juliet::String& str);
}
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include "Database.h"
namespace Romeo
{
void MD_GenerateDocumentation(Database& db, const Juliet::String& workspaceRoot, const Juliet::String& docsDir);
}
+549
View File
@@ -0,0 +1,549 @@
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <shellapi.h>
#include <stdio.h>
#include <Core/Common/CoreUtils.h>
#include <Core/Main.h>
#include <Core/Memory/MemoryArena.h>
#include <Core/JulietInit.h>
#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/Thread/Mutex.h>
#include <thread>
#include "Database.h"
#include "MarkdownGenerator.h"
#define ROMEO_TRAY_ICON_ID 1
#define ROMEO_WM_TRAYICON (WM_USER + 1)
static Romeo::Database G_Db;
static Juliet::Arena* G_ScratchArena = nullptr;
static Juliet::String G_WorkspaceRoot;
static Juliet::String G_DocsDir;
static Juliet::String G_DbFilePath;
static Juliet::String GetWorkspaceRoot(Juliet::Arena* arena)
{
Assert(arena != nullptr);
Juliet::String base = Juliet::GetBasePath();
// Copy to arena so we can safely modify it
char* buf = Juliet::ArenaPushArray<char>(arena, base.Size + 1 JULIET_DEBUG_PARAM("WorkspaceRootPath"));
memcpy(buf, base.Data, base.Size);
buf[base.Size] = '\0';
size_t count = 0;
for (size_t i = base.Size; i > 0; --i)
{
size_t idx = i - 1;
if (buf[idx] == '\\' || buf[idx] == '/')
{
count++;
if (count == 3)
{
buf[idx] = '\0';
return { buf, idx };
}
}
}
return { buf, base.Size };
}
static HANDLE G_ExitEvent = INVALID_HANDLE_VALUE;
static std::thread G_MonitorThread;
static bool G_MonitorRunning = false;
static HANDLE G_OllamaProcessHandle = INVALID_HANDLE_VALUE;
static HANDLE G_OllamaJobHandle = nullptr;
static void MonitorThreadProc()
{
int wLen = MultiByteToWideChar(CP_UTF8, 0, CStr(G_WorkspaceRoot), -1, nullptr, 0);
if (wLen <= 0)
{
return;
}
wchar_t* wRoot = new wchar_t[static_cast<size_t>(wLen + 1)];
MultiByteToWideChar(CP_UTF8, 0, CStr(G_WorkspaceRoot), -1, wRoot, wLen);
HANDLE hDir = CreateFileW(
wRoot,
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
nullptr
);
delete[] wRoot;
if (hDir == INVALID_HANDLE_VALUE)
{
return;
}
HANDLE hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (hEvent == nullptr)
{
CloseHandle(hDir);
return;
}
OVERLAPPED overlapped = {};
overlapped.hEvent = hEvent;
alignas(FILE_NOTIFY_INFORMATION) BYTE buffer[64 * 1024];
bool pendingChanges = false;
BOOL readRes = ReadDirectoryChangesW(
hDir,
buffer,
sizeof(buffer),
TRUE,
FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME,
nullptr,
&overlapped,
nullptr
);
if (readRes == 0)
{
CloseHandle(hEvent);
CloseHandle(hDir);
return;
}
HANDLE waitHandles[2] = { G_ExitEvent, hEvent };
while (G_MonitorRunning)
{
DWORD timeout = pendingChanges ? 500 : INFINITE;
DWORD waitRes = WaitForMultipleObjects(2, waitHandles, FALSE, timeout);
if (waitRes == WAIT_OBJECT_0)
{
break;
}
else if (waitRes == WAIT_OBJECT_0 + 1)
{
DWORD bytesTransferred = 0;
if (GetOverlappedResult(hDir, &overlapped, &bytesTransferred, FALSE) != 0)
{
FILE_NOTIFY_INFORMATION* info = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer);
while (info != nullptr)
{
size_t wCharCount = info->FileNameLength / sizeof(WCHAR);
if (wCharCount > 2)
{
const WCHAR* fName = info->FileName;
if ((fName[wCharCount - 1] == L'h' && fName[wCharCount - 2] == L'.') ||
(wCharCount > 4 && fName[wCharCount - 1] == L'p' && fName[wCharCount - 2] == L'p' && fName[wCharCount - 3] == L'c' && fName[wCharCount - 4] == L'.'))
{
bool ignore = false;
for (size_t i = 0; i < wCharCount; ++i)
{
if (i + 12 <= wCharCount && wcsncmp(fName + i, L"Intermediate", 12) == 0) { ignore = true; }
if (i + 3 <= wCharCount && wcsncmp(fName + i, L"bin", 3) == 0) { ignore = true; }
if (i + 8 <= wCharCount && wcsncmp(fName + i, L"External", 8) == 0) { ignore = true; }
if (i + 4 <= wCharCount && wcsncmp(fName + i, L".git", 4) == 0) { ignore = true; }
if (ignore) { break; }
}
if (!ignore)
{
pendingChanges = true;
}
}
}
if (info->NextEntryOffset == 0)
{
break;
}
info = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(reinterpret_cast<BYTE*>(info) + info->NextEntryOffset);
}
}
ResetEvent(hEvent);
ReadDirectoryChangesW(
hDir,
buffer,
sizeof(buffer),
TRUE,
FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME,
nullptr,
&overlapped,
nullptr
);
}
else if (waitRes == WAIT_TIMEOUT)
{
printf("Romeo Monitor: Change detected, updating documentation...\n");
OutputDebugStringA("Romeo Monitor: Change detected, updating...\n");
{
Juliet::LockGuard lock(G_Db.Mutex);
Juliet::ArenaClear(G_ScratchArena);
Romeo::DB_ScanWorkspace(G_Db, G_ScratchArena, G_WorkspaceRoot);
bool anyChanged = false;
for (const Romeo::FileEntry& entry : G_Db.Entries)
{
if (entry.LastModifiedHeader > entry.LastMarkdownCreatedTime ||
entry.LastModifiedCpp > entry.LastMarkdownCreatedTime)
{
anyChanged = true;
break;
}
}
if (anyChanged)
{
Romeo::MD_GenerateDocumentation(G_Db, G_WorkspaceRoot, G_DocsDir);
Romeo::DB_Save(G_Db, G_DbFilePath);
}
}
pendingChanges = false;
}
}
CancelIo(hDir);
CloseHandle(hEvent);
CloseHandle(hDir);
}
LRESULT CALLBACK RomeoWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case ROMEO_WM_TRAYICON:
if (LOWORD(lParam) == WM_RBUTTONUP)
{
POINT pt;
GetCursorPos(&pt);
HMENU hMenu = CreatePopupMenu();
if (hMenu)
{
InsertMenuW(hMenu, 0, MF_BYPOSITION | MF_STRING, 1001, L"Exit");
SetForegroundWindow(hwnd);
TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, nullptr);
DestroyMenu(hMenu);
}
}
else if (LOWORD(lParam) == WM_LBUTTONUP)
{
OutputDebugStringA("Tray icon left-clicked!\n");
}
return 0;
case WM_COMMAND:
if (LOWORD(wParam) == 1001)
{
DestroyWindow(hwnd);
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int JulietMain(int argc, wchar_t** argv)
{
printf("JulietMain Entry!\n");
// Single instance mutex check
HANDLE hMutex = CreateMutexW(nullptr, TRUE, L"RomeoSingleInstanceMutex");
if (hMutex == nullptr || GetLastError() == ERROR_ALREADY_EXISTS)
{
printf("Romeo is already running. Exiting.\n");
if (hMutex != nullptr)
{
CloseHandle(hMutex);
}
return 0;
}
// Launch Ollama serve subprocess
{
// Try to find ollama.exe - check common install locations
char ollamaPath[512] = {};
bool foundOllama = false;
// 1. Try PATH first via SearchPathA
if (SearchPathA(nullptr, "ollama.exe", nullptr, sizeof(ollamaPath), ollamaPath, nullptr) > 0)
{
foundOllama = true;
}
// 2. Check %LOCALAPPDATA%\Programs\Ollama\ollama.exe
if (!foundOllama)
{
char localAppData[256] = {};
DWORD envLen = GetEnvironmentVariableA("LOCALAPPDATA", localAppData, sizeof(localAppData));
if (envLen > 0 && envLen < sizeof(localAppData))
{
snprintf(ollamaPath, sizeof(ollamaPath), "%s\\Programs\\Ollama\\ollama.exe", localAppData);
DWORD attribs = GetFileAttributesA(ollamaPath);
if (attribs != INVALID_FILE_ATTRIBUTES)
{
foundOllama = true;
}
}
}
// 3. Check %USERPROFILE%\AppData\Local\Programs\Ollama\ollama.exe
if (!foundOllama)
{
char userProfile[256] = {};
DWORD envLen = GetEnvironmentVariableA("USERPROFILE", userProfile, sizeof(userProfile));
if (envLen > 0 && envLen < sizeof(userProfile))
{
snprintf(ollamaPath, sizeof(ollamaPath), "%s\\AppData\\Local\\Programs\\Ollama\\ollama.exe", userProfile);
DWORD attribs = GetFileAttributesA(ollamaPath);
if (attribs != INVALID_FILE_ATTRIBUTES)
{
foundOllama = true;
}
}
}
if (foundOllama)
{
printf("Romeo: Found Ollama at: %s\n", ollamaPath);
// Create a Job Object so child processes auto-terminate when Romeo exits (even on crash)
G_OllamaJobHandle = CreateJobObjectA(nullptr, nullptr);
if (G_OllamaJobHandle != nullptr)
{
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(G_OllamaJobHandle, JobObjectExtendedLimitInformation,
&jobInfo, sizeof(jobInfo));
}
// Build a mutable command line buffer (CreateProcessA requires mutable string)
char cmdLine[600] = {};
snprintf(cmdLine, sizeof(cmdLine), "\"%s\" serve", ollamaPath);
STARTUPINFOA si = {};
si.cb = sizeof(si);
PROCESS_INFORMATION pi = {};
BOOL createRes = CreateProcessA(
ollamaPath,
cmdLine,
nullptr,
nullptr,
FALSE,
CREATE_NEW_CONSOLE | CREATE_SUSPENDED,
nullptr,
nullptr,
&si,
&pi
);
if (createRes != 0)
{
// Assign to job object before resuming so it cannot escape
if (G_OllamaJobHandle != nullptr)
{
AssignProcessToJobObject(G_OllamaJobHandle, pi.hProcess);
}
ResumeThread(pi.hThread);
// Wait briefly to check if the process crashed immediately
DWORD waitResult = WaitForSingleObject(pi.hProcess, 2000);
if (waitResult == WAIT_TIMEOUT)
{
// Process is still running after 2 seconds - good
G_OllamaProcessHandle = pi.hProcess;
CloseHandle(pi.hThread);
printf("Romeo: Launched local Ollama serve subprocess (PID: %lu).\n", pi.dwProcessId);
}
else
{
// Process exited within 2 seconds - likely crashed or port in use
DWORD exitCode = 0;
GetExitCodeProcess(pi.hProcess, &exitCode);
printf("Romeo: WARNING - Ollama serve process exited immediately (exit code: %lu).\n", exitCode);
printf("Romeo: This may mean Ollama is already running or the port 11434 is in use.\n");
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
else
{
DWORD err = GetLastError();
printf("Romeo: ERROR - Failed to launch Ollama serve (Win32 error: %lu).\n", err);
}
}
else
{
printf("Romeo: WARNING - Could not find ollama.exe. Searched PATH and common install locations.\n");
printf("Romeo: Please ensure Ollama is installed (https://ollama.com) and available in your PATH.\n");
printf("Romeo: Continuing without auto-launching Ollama serve...\n");
}
}
Juliet::ArenaParams params{};
Juliet::Arena* mainArena = Juliet::ArenaAllocate(params JULIET_DEBUG_PARAM("RomeoMainArena"));
Assert(mainArena != nullptr);
G_ScratchArena = Juliet::ArenaAllocate(params JULIET_DEBUG_PARAM("RomeoScratchArena"));
Assert(G_ScratchArena != nullptr);
printf("Arena Allocated! Pointer: %p\n", static_cast<void*>(mainArena));
G_WorkspaceRoot = GetWorkspaceRoot(mainArena);
char docsDirBuf[512];
snprintf(docsDirBuf, sizeof(docsDirBuf), "%s\\Romeo\\docs", CStr(G_WorkspaceRoot));
G_DocsDir = Juliet::StringCopy(mainArena, Juliet::WrapString(docsDirBuf));
char dbFilePathBuf[512];
snprintf(dbFilePathBuf, sizeof(dbFilePathBuf), "%s\\Romeo\\docs\\romeo.db", CStr(G_WorkspaceRoot));
G_DbFilePath = Juliet::StringCopy(mainArena, Juliet::WrapString(dbFilePathBuf));
Romeo::DB_Init(G_Db, mainArena);
printf("DB Init!\n");
bool forceRegenerate = false;
if (argc > 1 && (wcscmp(argv[1], L"-force") == 0 || wcscmp(argv[1], L"-f") == 0))
{
forceRegenerate = true;
}
{
Juliet::LockGuard lock(G_Db.Mutex);
if (!forceRegenerate)
{
Romeo::DB_Load(G_Db, G_DbFilePath);
}
else
{
printf("Romeo: Force flag detected, clearing cache and rebuilding all documentation...\n");
}
Romeo::DB_ScanWorkspace(G_Db, G_ScratchArena, G_WorkspaceRoot);
if (forceRegenerate)
{
for (Romeo::FileEntry& entry : G_Db.Entries)
{
entry.Description = {};
entry.LastMarkdownCreatedTime = 0;
entry.LastAIGeneratedTime = 0;
}
}
Romeo::MD_GenerateDocumentation(G_Db, G_WorkspaceRoot, G_DocsDir);
Romeo::DB_Save(G_Db, G_DbFilePath);
}
char debugMsg[256];
snprintf(debugMsg, sizeof(debugMsg), "Romeo scanned workspace. Found %llu C++ files.\n", static_cast<unsigned long long>(G_Db.Entries.Size()));
printf("%s", debugMsg);
// Register hidden window class
const wchar_t CLASS_NAME[] = L"RomeoHiddenWindowClass";
WNDCLASSW wc = {};
HINSTANCE hInstance = GetModuleHandleW(nullptr);
wc.lpfnWndProc = RomeoWindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClassW(&wc);
// Create hidden window to receive messages
HWND hwnd = CreateWindowExW(
0,
CLASS_NAME,
L"Romeo Tool",
0,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
nullptr,
nullptr,
hInstance,
nullptr
);
if (hwnd == nullptr)
{
CloseHandle(hMutex);
return 0;
}
G_ExitEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (G_ExitEvent != INVALID_HANDLE_VALUE)
{
G_MonitorRunning = true;
G_MonitorThread = std::thread(MonitorThreadProc);
}
// Add tray icon
NOTIFYICONDATAW nid = {};
nid.cbSize = sizeof(NOTIFYICONDATAW);
nid.hWnd = hwnd;
nid.uID = ROMEO_TRAY_ICON_ID;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = ROMEO_WM_TRAYICON;
nid.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
wcscpy_s(nid.szTip, L"Romeo Documentation Tool");
Shell_NotifyIconW(NIM_ADD, &nid);
// Message loop
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Cleanup
Shell_NotifyIconW(NIM_DELETE, &nid);
G_MonitorRunning = false;
if (G_ExitEvent != INVALID_HANDLE_VALUE)
{
SetEvent(G_ExitEvent);
}
if (G_MonitorThread.joinable())
{
G_MonitorThread.join();
}
if (G_ExitEvent != INVALID_HANDLE_VALUE)
{
CloseHandle(G_ExitEvent);
G_ExitEvent = INVALID_HANDLE_VALUE;
}
if (G_OllamaProcessHandle != INVALID_HANDLE_VALUE)
{
TerminateProcess(G_OllamaProcessHandle, 0);
CloseHandle(G_OllamaProcessHandle);
G_OllamaProcessHandle = INVALID_HANDLE_VALUE;
}
if (G_OllamaJobHandle != nullptr)
{
CloseHandle(G_OllamaJobHandle);
G_OllamaJobHandle = nullptr;
}
Juliet::ArenaRelease(mainArena);
Juliet::ArenaRelease(G_ScratchArena);
CloseHandle(hMutex);
return 0;
}