CC ?= gcc CFLAGS ?= -std=c11 -Wall -Wextra -O2 -g SD_DIR := vendor/sashidown CJSON_DIR := vendor/cjson PKG_CONFIG ?= pkg-config PREFIX ?= /usr/local DESTDIR ?= UNAME_S := $(shell uname -s 2>/dev/null) UNAME_O := $(shell uname -o 2>/dev/null) # Android (Termux): pthreads live in libc. PTHREAD_LIBS := -lpthread ifeq ($(UNAME_O),Android) PTHREAD_LIBS := endif # macOS: Homebrew ncurses/curl are keg-only, so point pkg-config at them. # (macOS ships GNU make 3.81, whose $(shell) ignores exported variables.) PKG_ENV := ifeq ($(UNAME_S),Darwin) BREW_PREFIX := $(shell brew --prefix 2>/dev/null) ifneq ($(BREW_PREFIX),) PKG_ENV := PKG_CONFIG_PATH="$(BREW_PREFIX)/opt/ncurses/lib/pkgconfig:$(BREW_PREFIX)/opt/curl/lib/pkgconfig:$$PKG_CONFIG_PATH" endif CPPFLAGS += -D_DARWIN_C_SOURCE endif PKGC = $(PKG_ENV) $(PKG_CONFIG) # Wide-character curses: ncursesw, else ncurses (macOS/BSD build it wide). CURSES_PC := $(shell for p in ncursesw ncurses; do $(PKGC) --exists $$p 2>/dev/null && { echo $$p; exit 0; }; done) ifneq ($(CURSES_PC),) CURSES_CFLAGS := $(shell $(PKGC) --cflags $(CURSES_PC)) CURSES_LIBS := $(shell $(PKGC) --libs $(CURSES_PC)) else ifeq ($(UNAME_S),Darwin) CURSES_LIBS := -lncurses else CURSES_LIBS := -lncursesw endif CURL_CFLAGS := $(shell $(PKGC) --cflags libcurl 2>/dev/null) CURL_LIBS := $(shell $(PKGC) --libs libcurl 2>/dev/null) ifeq ($(strip $(CURL_LIBS)),) CURL_LIBS := -lcurl endif # WebUI (HTTP + SSE front end): src/net.c src/http_srv.c src/web_md.c # src/webui.c. CST_WEBUI=0 swaps in the stub (--web then prints a hint). CST_WEBUI ?= 1 PKG_CFLAGS := $(CURL_CFLAGS) $(CURSES_CFLAGS) PKG_LIBS := $(CURL_LIBS) $(CURSES_LIBS) CPPFLAGS += -Iinclude -I.gen -I$(SD_DIR)/include -I$(CJSON_DIR) -D_XOPEN_SOURCE_EXTENDED $(PKG_CFLAGS) LDFLAGS += -L$(SD_DIR) LDLIBS += -lsashidown -lm $(PKG_LIBS) $(PTHREAD_LIBS) WEB_DIR := web WEB_ASSETS := $(wildcard $(WEB_DIR)/*.html) $(wildcard $(WEB_DIR)/*.js) \ $(wildcard $(WEB_DIR)/*.css) $(wildcard $(WEB_DIR)/*.svg) ifeq ($(CST_WEBUI),0) WEB_SRC := src/net.c src/http_srv.c src/web_md.c src/webui_stub.c # The stub body is guarded by this, so the switch has to define it too. CPPFLAGS += -DCST_NO_WEBUI=1 else WEB_SRC := src/net.c src/http_srv.c src/web_md.c src/webui.c endif CORE_SRC = \ $(CJSON_DIR)/cJSON.c \ src/util.c \ src/ctl_parse.c \ src/os_compat.c \ src/os_win.c \ src/sandbox.c \ src/sandbox_exec.c \ src/keymap.c \ src/skills.c \ src/extensions.c src/plugins.c src/mcp.c src/subagent.c src/attach.c src/clipboard.c \ src/session.c \ src/tools/tools.c \ src/tools/patch.c \ src/tools/registry.c \ src/tools/fs_tools.c \ src/tools/agent_tools.c \ src/tools/web.c \ src/tools/diff.c \ src/undo.c \ src/permissions.c \ src/settings.c \ src/llm_openai.c \ src/agent_loop.c \ src/agent_store.c \ src/json_baseline.c \ src/creds.c src/provider_switch.c \ src/mm_tool.c \ src/models.c src/model_registry.c \ src/metrics.c \ $(WEB_SRC) SRC = $(CORE_SRC) src/textsel.c src/tui.c src/main.c OBJ = $(SRC:.c=.o) HDRS = $(wildcard include/*.h) $(wildcard src/tools/*.h) $(CJSON_DIR)/cJSON.h \ .gen/models_builtin.inc .gen/web_assets.inc TESTS = \ tests/test_ctl_frontier \ tests/test_ctl_heredoc \ tests/test_json_baseline \ tests/test_replay_starvation \ tests/test_replay_real \ tests/test_mm_tool \ tests/test_llm_body \ tests/test_prefix_stable \ tests/test_sse_decode \ tests/test_apply_patch \ tests/test_session_roundtrip \ tests/test_attach \ tests/test_shell \ tests/test_diff \ tests/test_tools_fs \ tests/test_registry \ tests/test_undo \ tests/test_config \ tests/test_models \ tests/test_model_registry \ tests/test_sandbox \ tests/test_keymap \ tests/test_textsel \ tests/test_markdown \ tests/test_shell_compact \ tests/test_skills \ tests/test_plugins \ tests/test_mcp \ tests/test_subagent \ tests/test_lowil \ tests/test_win_utf8 \ tests/test_os_dir \ tests/test_tmpdir # test_webui drives the real server; there is nothing to test with the stub. ifneq ($(CST_WEBUI),0) TESTS += tests/test_webui endif TEST_LIBS = -L$(SD_DIR) -lsashidown -lm $(CURL_LIBS) $(PTHREAD_LIBS) .PHONY: all clean check demo sashidown-lib FORCE cmake-gcc cmake-clang asan install uninstall all: sashidown-lib cst-agent cmake-gcc: cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON cmake --build build-gcc ctest --test-dir build-gcc --output-on-failure cmake-clang: cmake -B build-clang -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON cmake --build build-clang sashidown-lib: $(SD_DIR)/libsashidown.a # The sub-make decides whether the archive is stale; FORCE only makes sure it # is asked, and -j builds wait for it before linking. $(SD_DIR)/libsashidown.a: FORCE $(MAKE) -C $(SD_DIR) libsashidown.a FORCE: cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS) # Built-in model data as a C byte list (included by model_registry.c). .gen/models_builtin.inc: src/models_builtin.json @mkdir -p .gen od -An -v -tx1 $< | sed 's/\([0-9a-f][0-9a-f]\)/0x\1,/g' > $@.tmp && mv $@.tmp $@ # The WebUI shell is embedded the same way, one array per file. .gen/web_assets.inc: $(WEB_ASSETS) scripts/embed_web.sh @mkdir -p .gen @sh scripts/embed_web.sh $@ $(WEB_DIR) %.o: %.c $(HDRS) $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< src/tui.o: src/tui.c $(HDRS) $(SD_DIR)/include/sashidown.h $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_frontier.c src/ctl_parse.c tests/test_ctl_heredoc: tests/test_ctl_heredoc.c src/ctl_parse.c include/ctl.h $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_heredoc.c src/ctl_parse.c tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(HDRS) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c -lm tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c -lm tests/test_replay_real: tests/test_replay_real.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_real.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c -lm tests/test_mm_tool: tests/test_mm_tool.c src/mm_tool.c include/mm_tool.h $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_mm_tool.c src/mm_tool.c tests/test_textsel: tests/test_textsel.c src/textsel.c src/os_compat.c src/os_win.c src/util.c include/cst_textsel.h $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_textsel.c src/textsel.c src/os_compat.c src/os_win.c src/util.c tests/test_markdown: tests/test_markdown.c $(SD_DIR)/libsashidown.a $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_markdown.c $(LDFLAGS) -lsashidown -lm tests/test_%: tests/test_%.c $(CORE_SRC) $(HDRS) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CORE_SRC) $(TEST_LIBS) $(TESTS): sashidown-lib check: $(TESTS) @set -e; for t in $(TESTS); do echo "== $$t"; ./$$t; done # AddressSanitizer + UBSan build of the agent and all tests. # Under a ptrace-based sandbox (some Termux setups) LeakSanitizer aborts with # "Tracer caught signal 11"; add ASAN_OPTIONS=detect_leaks=0 there. asan: $(MAKE) clean $(MAKE) sashidown-lib $(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" cst-agent check demo: cst-agent ./cst-agent --demo-ctl --workspace . clean: rm -f $(OBJ) cst-agent $(TESTS) .gen/models_builtin.inc .gen/web_assets.inc install: cst-agent install -d "$(DESTDIR)$(PREFIX)/bin" install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent" uninstall: rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent"