Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
name: Release

on:
push:
tags: ['v*']

jobs:
build-linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4

- name: Setup Conan
run: |
pip install "conan>=2.0,<3.0"
conan profile detect

- name: Cache Conan
uses: actions/cache@v4
with:
path: ~/.conan2
key: ${{ runner.os }}-conan-${{ hashFiles('conanfile.txt') }}
restore-keys: ${{ runner.os }}-conan-

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-

- name: Configure CMake
run: |
conan install . --build=missing -s build_type=Release
cmake -B build -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release

- name: Build C++
run: cmake --build build -j

- name: Build WebUI
run: |
cd webui
npm ci
npm run build

- name: Assemble green package
run: |
mkdir -p merak-pkg/webui merak-pkg/config
cp build/cli/merak merak-pkg/
cp -r webui/dist/* merak-pkg/webui/
cp config/prompts merak-pkg/config/ -r 2>/dev/null || true
cp scripts/start.sh merak-pkg/ 2>/dev/null || true

- name: Package
run: tar czf merak-linux-x64.tar.gz -C merak-pkg .

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: merak-linux-x64
path: merak-linux-x64.tar.gz

build-windows:
runs-on: windows-2022
steps:
- uses: actions/checkout@v4

- name: Setup Conan
run: |
pip install "conan>=2.0,<3.0"
conan profile detect

- name: Cache Conan
uses: actions/cache@v4
with:
path: ~/.conan2
key: ${{ runner.os }}-conan-${{ hashFiles('conanfile.txt') }}
restore-keys: ${{ runner.os }}-conan-

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'

- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-

- name: Configure CMake
run: |
conan install . --build=missing -s build_type=Release
cmake -B build -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release

- name: Build C++
run: cmake --build build --config Release -j

- name: Build WebUI
run: |
cd webui
npm ci
npm run build

- name: Assemble green package
shell: bash
run: |
mkdir -p merak-pkg/webui merak-pkg/config
cp build/cli/Release/merak.exe merak-pkg/ || cp build/cli/merak.exe merak-pkg/
cp build/cli/Release/merak-launcher.exe merak-pkg/ 2>/dev/null || true
cp -r webui/dist/* merak-pkg/webui/
cp config/prompts merak-pkg/config/ -r 2>/dev/null || true

- name: Package
shell: bash
run: |
7z a merak-windows-x64.zip ./merak-pkg/*

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: merak-windows-x64
path: merak-windows-x64.zip

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-linux, build-windows]
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
merak-linux-x64/merak-linux-x64.tar.gz
merak-windows-x64/merak-windows-x64.zip
draft: true
generate_release_notes: true
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ add_subdirectory(libs/prompts)
add_subdirectory(libs/loop)
add_subdirectory(libs/storage)
add_subdirectory(libs/worldbuilding)
add_subdirectory(libs/portable_pg)
add_subdirectory(libs/runtime)
add_subdirectory(libs/http)
add_subdirectory(cli)
Expand Down
140 changes: 133 additions & 7 deletions cli/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,129 @@ FetchContent_Declare(
GIT_REPOSITORY https://github.com/tree-sitter/tree-sitter
GIT_TAG v0.22.6
)
FetchContent_MakeAvailable(tree-sitter)
FetchContent_GetProperties(tree-sitter)
if(NOT tree-sitter_POPULATED)
FetchContent_Populate(tree-sitter)
endif()

# tree-sitter v0.22+ has no CMakeLists.txt (Rust project).
# Build the C library manually with WASM stubs since we use compiled-in grammars.
enable_language(C)
if(NOT TARGET tree-sitter)
# WASM stubs — grammar parser.c files reference these symbols, but we
# never load WASM grammars, so provide no-op implementations.
set(TS_WASM_STUB "${CMAKE_CURRENT_BINARY_DIR}/wasm_stubs.c")
file(WRITE ${TS_WASM_STUB} [=[
#include <tree_sitter/api.h>
#include <stdint.h>
typedef uint16_t TSStateId;
typedef struct TSLexer TSLexer;
// wasm_store stubs — grammars compiled as C never use WASM,
// but parser.c and language.c reference these symbols.
bool ts_language_is_wasm(const TSLanguage *self) { (void)self; return false; }
void ts_wasm_store_delete(TSWasmStore *self) { (void)self; }
bool ts_wasm_store_start(TSWasmStore *s, TSLexer *l, const TSLanguage *lang) {
(void)s; (void)l; (void)lang; return false;
}
void ts_wasm_store_reset(TSWasmStore *self) { (void)self; }
bool ts_wasm_store_has_error(const TSWasmStore *self) { (void)self; return false; }
bool ts_wasm_store_call_lex_main(TSWasmStore *s, TSStateId id) { (void)s; (void)id; return false; }
bool ts_wasm_store_call_lex_keyword(TSWasmStore *s, TSStateId id) { (void)s; (void)id; return false; }
uint32_t ts_wasm_store_call_scanner_create(TSWasmStore *s) { (void)s; return 0; }
void ts_wasm_store_call_scanner_destroy(TSWasmStore *s, uint32_t idx) { (void)s; (void)idx; }
bool ts_wasm_store_call_scanner_scan(TSWasmStore *s, uint32_t idx, uint32_t state) { (void)s; (void)idx; (void)state; return false; }
uint32_t ts_wasm_store_call_scanner_serialize(TSWasmStore *s, uint32_t idx, char *buf) { (void)s; (void)idx; (void)buf; return 0; }
void ts_wasm_store_call_scanner_deserialize(TSWasmStore *s, uint32_t idx, const char *buf, unsigned len) { (void)s; (void)idx; (void)buf; (void)len; }
void ts_wasm_language_retain(const TSLanguage *self) { (void)self; }
void ts_wasm_language_release(const TSLanguage *self) { (void)self; }
]=])
file(GLOB TS_SOURCES "${tree-sitter_SOURCE_DIR}/lib/src/*.c")
list(FILTER TS_SOURCES EXCLUDE REGEX ".*/lib\\.c$")
list(FILTER TS_SOURCES EXCLUDE REGEX ".*/wasm_store\\.c$")
add_library(tree-sitter STATIC ${TS_SOURCES} ${TS_WASM_STUB})
target_include_directories(tree-sitter PUBLIC
${tree-sitter_SOURCE_DIR}/lib/include
)
target_include_directories(tree-sitter PRIVATE
${tree-sitter_SOURCE_DIR}/lib/src
)
target_compile_features(tree-sitter PRIVATE c_std_11)
set_target_properties(tree-sitter PROPERTIES LINKER_LANGUAGE C)
endif()

set(TS_LANGUAGES cpp python go javascript typescript rust bash json yaml markdown sql java csharp ruby)

# Map language → repo URL + branch + source subdirectory.
# Default repo: tree-sitter/tree-sitter-${lang}
# Default branch: master
# Default src: src/
foreach(lang IN LISTS TS_LANGUAGES)
if(lang STREQUAL "typescript")
set(TS_REPO_URL "https://github.com/tree-sitter/tree-sitter-typescript")
set(TS_GIT_TAG "master")
set(TS_SRC_SUBDIR "typescript/src")
elseif(lang STREQUAL "csharp")
set(TS_REPO_URL "https://github.com/tree-sitter/tree-sitter-c-sharp")
set(TS_GIT_TAG "master")
set(TS_SRC_SUBDIR "src")
elseif(lang STREQUAL "yaml")
set(TS_REPO_URL "https://github.com/ikatyang/tree-sitter-yaml")
set(TS_GIT_TAG "master")
set(TS_SRC_SUBDIR "src")
elseif(lang STREQUAL "markdown")
set(TS_REPO_URL "https://github.com/tree-sitter-grammars/tree-sitter-markdown")
set(TS_GIT_TAG "split_parser")
set(TS_SRC_SUBDIR "tree-sitter-markdown/src")
elseif(lang STREQUAL "sql")
set(TS_REPO_URL "https://github.com/m-novikov/tree-sitter-sql")
set(TS_GIT_TAG "main")
set(TS_SRC_SUBDIR "src")
else()
set(TS_REPO_URL "https://github.com/tree-sitter/tree-sitter-${lang}")
set(TS_GIT_TAG "master")
set(TS_SRC_SUBDIR "src")
endif()

FetchContent_Declare(
tree-sitter-${lang}
GIT_REPOSITORY https://github.com/tree-sitter/tree-sitter-${lang}
GIT_TAG master
GIT_REPOSITORY ${TS_REPO_URL}
GIT_TAG ${TS_GIT_TAG}
)
FetchContent_MakeAvailable(tree-sitter-${lang})
FetchContent_GetProperties(tree-sitter-${lang})
if(NOT tree-sitter-${lang}_POPULATED)
FetchContent_Populate(tree-sitter-${lang})
endif()
if(NOT TARGET tree-sitter-${lang})
add_library(tree-sitter-${lang} STATIC
${tree-sitter-${lang}_SOURCE_DIR}/${TS_SRC_SUBDIR}/parser.c
)
set(TS_LINK_LANG C)
if(EXISTS ${tree-sitter-${lang}_SOURCE_DIR}/${TS_SRC_SUBDIR}/scanner.c)
target_sources(tree-sitter-${lang} PRIVATE
${tree-sitter-${lang}_SOURCE_DIR}/${TS_SRC_SUBDIR}/scanner.c
)
elseif(EXISTS ${tree-sitter-${lang}_SOURCE_DIR}/${TS_SRC_SUBDIR}/scanner.cc)
target_sources(tree-sitter-${lang} PRIVATE
${tree-sitter-${lang}_SOURCE_DIR}/${TS_SRC_SUBDIR}/scanner.cc
)
set_source_files_properties(
${tree-sitter-${lang}_SOURCE_DIR}/${TS_SRC_SUBDIR}/scanner.cc
PROPERTIES LANGUAGE CXX
)
set(TS_LINK_LANG CXX)
endif()
target_include_directories(tree-sitter-${lang} PRIVATE
${tree-sitter-${lang}_SOURCE_DIR}/${TS_SRC_SUBDIR}
)
set_target_properties(tree-sitter-${lang} PROPERTIES LINKER_LANGUAGE ${TS_LINK_LANG})
target_link_libraries(tree-sitter-${lang} PRIVATE tree-sitter)
endif()
endforeach()

# Map language names to CMake target names: "cpp" → "tree-sitter-cpp"
set(TS_GRAMMAR_TARGETS "")
foreach(lang IN LISTS TS_LANGUAGES)
list(APPEND TS_GRAMMAR_TARGETS "tree-sitter-${lang}")
endforeach()

add_executable(merak-cli
Expand All @@ -30,8 +142,9 @@ set_target_properties(merak-cli PROPERTIES OUTPUT_NAME merak)
target_link_libraries(merak-cli PRIVATE
merak-http merak-runtime merak-storage merak-loop merak-config
merak-llm merak-tools merak-mcp merak-memory merak-context
merak-portable-pg
CURL::libcurl
tree-sitter ${TS_LANGUAGES}
tree-sitter ${TS_GRAMMAR_TARGETS}
)
target_include_directories(merak-cli PRIVATE src)

Expand All @@ -41,8 +154,11 @@ target_compile_features(test_buffer PRIVATE cxx_std_17)
add_test(NAME test_buffer COMMAND test_buffer)

add_executable(test_syntax tests/test_syntax.cpp)
target_link_libraries(test_syntax PRIVATE tree-sitter tree-sitter-python)
target_include_directories(test_syntax PRIVATE src)
target_link_libraries(test_syntax PRIVATE tree-sitter ${TS_GRAMMAR_TARGETS} nlohmann_json::nlohmann_json)
target_include_directories(test_syntax PRIVATE
src
${tree-sitter_SOURCE_DIR}/lib/include
)
target_compile_features(test_syntax PRIVATE cxx_std_17)
add_test(NAME test_syntax COMMAND test_syntax)

Expand All @@ -53,3 +169,13 @@ target_compile_features(test_persistence PRIVATE cxx_std_17)
add_test(NAME test_persistence COMMAND test_persistence)

install(TARGETS merak-cli RUNTIME DESTINATION bin)

if(WIN32)
add_executable(merak-launcher src/win_launcher.cpp)
target_compile_features(merak-launcher PRIVATE cxx_std_23)
target_link_libraries(merak-launcher PRIVATE ws2_32 shell32 user32)
set_target_properties(merak-launcher PROPERTIES
RUNTIME_OUTPUT_NAME "merak-launcher"
WIN32_EXECUTABLE TRUE
)
endif()
43 changes: 42 additions & 1 deletion cli/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <merak/http_server.hpp>
#include <merak/mcp_client.hpp>
#include <merak/openai_provider.hpp>
#include <merak/portable_pg.hpp>
#include <merak/worldbuilding/worldbuilding_service.hpp>
#include <merak/worldbuilding/worldbuilding_tools.hpp>
#include <merak/worldbuilding_http_handler.hpp>
Expand Down Expand Up @@ -78,8 +79,39 @@ static std::string normalize_team_pattern(std::string pattern){
return pattern;
}

#ifdef _WIN32
#include <windows.h>
#endif

static std::filesystem::path exe_dir_path() {
#ifdef _WIN32
WCHAR path[MAX_PATH];
if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0) return {};
std::filesystem::path p(path);
return p.parent_path();
#else
return std::filesystem::canonical("/proc/self/exe").parent_path();
#endif
}

static int run_server(int argc,char**argv) {
auto cfg=load_config();
// --- Portable PostgreSQL ---
std::unique_ptr<PortablePg> portable_pg;
{
auto exe = exe_dir_path();
auto pg_path = exe / "pg";
if (!exe.empty() && std::filesystem::exists(pg_path) && cfg.memory.db_connection.empty()) {
portable_pg = std::make_unique<PortablePg>(pg_path);
if (portable_pg->start()) {
cfg.memory.db_connection = portable_pg->connection_string();
std::cout << "Portable PostgreSQL started on port " << portable_pg->port() << "\n";
} else {
std::cerr << "Warning: portable PostgreSQL failed to start\n";
portable_pg.reset();
}
}
}
// Instantiate WorldbuildingService
std::shared_ptr<worldbuilding::WorldbuildingService> wb_service;
try {
Expand Down Expand Up @@ -154,7 +186,16 @@ static int run_server(int argc,char**argv) {
metadata.tools = tools->all_tools();
metadata.mcp_servers = mcp_status;
metadata.agents = runtime->agents();
HttpServer server(runtime,metadata);
HttpServer server(runtime, metadata, merak_home(), llm);
// Serve static WebUI files
{
auto exe = exe_dir_path();
auto webui_path = exe / "webui";
if (!exe.empty() && std::filesystem::exists(webui_path)) {
server.serve_static_dir("/", webui_path.string());
std::cout << "Serving WebUI from " << webui_path << "\n";
}
}
// Register Worldbuilding HTTP routes
if (wb_service) {
auto wb_handler = std::make_shared<WorldbuildingHttpHandler>(wb_service);
Expand Down
Loading