Initial CST-Agent: portable Linux/Termux/Windows coding agent.
Vendored sashidown, OpenAI-compatible env (OPENAI_*), and bootstrap scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
|||||||
|
*.o
|
||||||
|
cst-agent
|
||||||
|
cst-agent.exe
|
||||||
|
tests/test_*
|
||||||
|
!tests/test_*.c
|
||||||
|
session.jsonl
|
||||||
|
.agent/
|
||||||
|
fixtures/mini_calc/out/
|
||||||
|
fixtures/mini_calc/calc
|
||||||
|
fixtures/snake_cpp/snake
|
||||||
|
fixtures/snake_cpp/out/
|
||||||
|
build/
|
||||||
|
build-*/
|
||||||
|
dist/
|
||||||
|
third_party/busybox/busybox
|
||||||
|
third_party/busybox/busybox.exe
|
||||||
|
third_party/curl/
|
||||||
|
third_party/PDCurses/
|
||||||
|
third_party/busybox-w32/
|
||||||
|
third_party/busybox-src/
|
||||||
|
third_party/prefix-mingw/
|
||||||
|
vendor/sashidown/*.o
|
||||||
|
vendor/sashidown/src/*.o
|
||||||
|
vendor/sashidown/libsashidown.a
|
||||||
|
vendor/sashidown/libsashidown-mingw.a
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(cst-agent C)
|
||||||
|
|
||||||
|
set(CMAKE_C_STANDARD 11)
|
||||||
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
option(CST_BUILD_TUI "Build ncurses/PDCurses TUI" ON)
|
||||||
|
option(CST_NATIVE_ARCH "Pass -march=native to sashidown" OFF)
|
||||||
|
option(CST_BUNDLE_BUSYBOX "Install placeholder/scripts for busybox beside binary" ON)
|
||||||
|
|
||||||
|
set(SD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/sashidown")
|
||||||
|
if(CST_NATIVE_ARCH)
|
||||||
|
set(_SD_MAKE_EXTRA CST_NATIVE_ARCH=1)
|
||||||
|
else()
|
||||||
|
set(_SD_MAKE_EXTRA "")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Separate archive for MinGW so Linux host builds keep libsashidown.a
|
||||||
|
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||||
|
set(SASHIDOWN_A "${SD_DIR}/libsashidown-mingw.a")
|
||||||
|
set(_MINGW_AR x86_64-w64-mingw32-ar)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${SASHIDOWN_A}"
|
||||||
|
COMMAND ${CMAKE_MAKE_PROGRAM} -C "${SD_DIR}" clean
|
||||||
|
COMMAND ${CMAKE_MAKE_PROGRAM} -C "${SD_DIR}" libsashidown.a
|
||||||
|
CC=${CMAKE_C_COMPILER} AR=${_MINGW_AR} RANLIB=x86_64-w64-mingw32-ranlib
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${SD_DIR}/libsashidown.a" "${SASHIDOWN_A}"
|
||||||
|
COMMAND ${CMAKE_MAKE_PROGRAM} -C "${SD_DIR}" clean
|
||||||
|
COMMENT "Building libsashidown for MinGW"
|
||||||
|
VERBATIM)
|
||||||
|
else()
|
||||||
|
set(SASHIDOWN_A "${SD_DIR}/libsashidown.a")
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${SASHIDOWN_A}"
|
||||||
|
COMMAND ${CMAKE_MAKE_PROGRAM} -C "${SD_DIR}" libsashidown.a CC=${CMAKE_C_COMPILER} ${_SD_MAKE_EXTRA}
|
||||||
|
COMMENT "Building libsashidown.a"
|
||||||
|
VERBATIM)
|
||||||
|
endif()
|
||||||
|
add_custom_target(sashidown DEPENDS "${SASHIDOWN_A}")
|
||||||
|
|
||||||
|
find_package(CURL REQUIRED)
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
|
set(CST_SOURCES
|
||||||
|
src/ctl_parse.c
|
||||||
|
src/os_compat.c
|
||||||
|
src/sandbox.c
|
||||||
|
src/session.c
|
||||||
|
src/tools/tools.c
|
||||||
|
src/llm_openai.c
|
||||||
|
src/agent_loop.c
|
||||||
|
src/agent_store.c
|
||||||
|
src/json_baseline.c
|
||||||
|
src/creds.c
|
||||||
|
src/mm_tool.c
|
||||||
|
src/models.c
|
||||||
|
src/main.c
|
||||||
|
)
|
||||||
|
|
||||||
|
if(CST_BUILD_TUI)
|
||||||
|
list(APPEND CST_SOURCES src/tui.c)
|
||||||
|
else()
|
||||||
|
list(APPEND CST_SOURCES src/tui_stub.c)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(cst-agent ${CST_SOURCES})
|
||||||
|
add_dependencies(cst-agent sashidown)
|
||||||
|
|
||||||
|
target_include_directories(cst-agent PRIVATE
|
||||||
|
include
|
||||||
|
"${SD_DIR}/include"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(cst-agent PRIVATE _GNU_SOURCE)
|
||||||
|
if(NOT WIN32)
|
||||||
|
target_compile_definitions(cst-agent PRIVATE _XOPEN_SOURCE_EXTENDED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(cst-agent PRIVATE
|
||||||
|
"${SASHIDOWN_A}"
|
||||||
|
CURL::libcurl
|
||||||
|
Threads::Threads
|
||||||
|
)
|
||||||
|
if(NOT WIN32)
|
||||||
|
target_link_libraries(cst-agent PRIVATE m)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CST_BUILD_TUI)
|
||||||
|
if(WIN32)
|
||||||
|
# Prefer PDCurses (pdcurses / curses). Users may set PDCurses_ROOT.
|
||||||
|
find_path(PDCURSES_INCLUDE_DIR curses.h
|
||||||
|
HINTS ENV PDCurses_ROOT "${PDCurses_ROOT}"
|
||||||
|
PATH_SUFFIXES include)
|
||||||
|
find_library(PDCURSES_LIBRARY
|
||||||
|
NAMES pdcurses pdcurses.dll curses
|
||||||
|
HINTS ENV PDCurses_ROOT "${PDCurses_ROOT}"
|
||||||
|
PATH_SUFFIXES lib)
|
||||||
|
if(PDCURSES_INCLUDE_DIR AND PDCURSES_LIBRARY)
|
||||||
|
target_include_directories(cst-agent PRIVATE ${PDCURSES_INCLUDE_DIR})
|
||||||
|
target_link_libraries(cst-agent PRIVATE ${PDCURSES_LIBRARY})
|
||||||
|
target_compile_definitions(cst-agent PRIVATE CST_USE_PDCURSES=1)
|
||||||
|
message(STATUS "TUI: PDCurses (${PDCURSES_LIBRARY})")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "CST_BUILD_TUI=ON on Windows requires PDCurses (set PDCurses_ROOT)")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
set(CURSES_NEED_WIDE TRUE)
|
||||||
|
find_package(Curses)
|
||||||
|
if(CURSES_FOUND)
|
||||||
|
target_include_directories(cst-agent PRIVATE ${CURSES_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(cst-agent PRIVATE ${CURSES_LIBRARIES})
|
||||||
|
else()
|
||||||
|
# Fallback common on Linux distros
|
||||||
|
target_link_libraries(cst-agent PRIVATE ncursesw)
|
||||||
|
endif()
|
||||||
|
target_compile_definitions(cst-agent PRIVATE CST_USE_NCURSES=1)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
target_compile_definitions(cst-agent PRIVATE CST_HEADLESS_ONLY=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CMAKE_SYSTEM_NAME STREQUAL "Windows" OR WIN32)
|
||||||
|
# curl Schannel + networking
|
||||||
|
target_link_libraries(cst-agent PRIVATE
|
||||||
|
ws2_32 crypt32 secur32 bcrypt advapi32)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- tests ---
|
||||||
|
enable_testing()
|
||||||
|
foreach(t test_ctl_frontier test_json_baseline test_replay_starvation test_mm_tool)
|
||||||
|
if(t STREQUAL test_ctl_frontier)
|
||||||
|
add_executable(${t} tests/test_ctl_frontier.c src/ctl_parse.c)
|
||||||
|
elseif(t STREQUAL test_json_baseline)
|
||||||
|
add_executable(${t} tests/test_json_baseline.c src/json_baseline.c)
|
||||||
|
elseif(t STREQUAL test_replay_starvation)
|
||||||
|
add_executable(${t} tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c)
|
||||||
|
else()
|
||||||
|
add_executable(${t} tests/test_mm_tool.c src/mm_tool.c)
|
||||||
|
endif()
|
||||||
|
target_include_directories(${t} PRIVATE include)
|
||||||
|
target_compile_definitions(${t} PRIVATE _GNU_SOURCE)
|
||||||
|
add_test(NAME ${t} COMMAND ${t})
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# --- busybox bundle helpers ---
|
||||||
|
if(CST_BUNDLE_BUSYBOX)
|
||||||
|
install(FILES
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/busybox/README.md"
|
||||||
|
DESTINATION .
|
||||||
|
OPTIONAL)
|
||||||
|
if(WIN32 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/busybox/busybox.exe")
|
||||||
|
install(PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/busybox/busybox.exe" DESTINATION .)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
install(TARGETS cst-agent RUNTIME DESTINATION .)
|
||||||
|
|
||||||
|
message(STATUS "Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER}")
|
||||||
|
message(STATUS "CST_BUILD_TUI=${CST_BUILD_TUI}")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
CC ?= gcc
|
||||||
|
CFLAGS ?= -std=c11 -Wall -Wextra -O2 -g
|
||||||
|
SD_DIR := vendor/sashidown
|
||||||
|
CPPFLAGS += -Iinclude -I$(SD_DIR)/include -D_XOPEN_SOURCE_EXTENDED
|
||||||
|
LDFLAGS += -L$(SD_DIR)
|
||||||
|
LDLIBS += -lsashidown -lm -lcurl -lncursesw -lpthread
|
||||||
|
|
||||||
|
SRC = \
|
||||||
|
src/ctl_parse.c \
|
||||||
|
src/os_compat.c \
|
||||||
|
src/sandbox.c \
|
||||||
|
src/session.c \
|
||||||
|
src/tools/tools.c \
|
||||||
|
src/llm_openai.c \
|
||||||
|
src/agent_loop.c \
|
||||||
|
src/agent_store.c \
|
||||||
|
src/json_baseline.c \
|
||||||
|
src/creds.c \
|
||||||
|
src/mm_tool.c \
|
||||||
|
src/models.c \
|
||||||
|
src/tui.c \
|
||||||
|
src/main.c
|
||||||
|
|
||||||
|
OBJ = $(SRC:.c=.o)
|
||||||
|
|
||||||
|
.PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang
|
||||||
|
|
||||||
|
all: sashidown-lib cst-agent
|
||||||
|
|
||||||
|
# Optional CMake entrypoints (same sources)
|
||||||
|
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:
|
||||||
|
$(MAKE) -C $(SD_DIR) libsashidown.a
|
||||||
|
|
||||||
|
cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a
|
||||||
|
$(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS)
|
||||||
|
|
||||||
|
src/%.o: src/%.c include/ctl.h include/cst_agent.h
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
src/tools/%.o: src/tools/%.c include/ctl.h include/cst_agent.h
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
|
src/tui.o: src/tui.c include/cst_agent.h $(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_json_baseline: tests/test_json_baseline.c src/json_baseline.c include/cst_agent.h
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c
|
||||||
|
|
||||||
|
tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
check: tests/test_ctl_frontier tests/test_json_baseline tests/test_replay_starvation tests/test_mm_tool
|
||||||
|
./tests/test_ctl_frontier
|
||||||
|
./tests/test_json_baseline
|
||||||
|
./tests/test_replay_starvation
|
||||||
|
./tests/test_mm_tool
|
||||||
|
|
||||||
|
demo: cst-agent
|
||||||
|
./cst-agent --demo-ctl --workspace .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(OBJ) cst-agent tests/test_ctl_frontier tests/test_json_baseline tests/test_replay_starvation tests/test_mm_tool
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# CST-Agent
|
||||||
|
|
||||||
|
C11 OpenCode-like carrier that extends paper **CST** (confirmation frontier / irreversible emission) from Markdown→HTML to the **agent tool loop**.
|
||||||
|
|
||||||
|
See [docs/theory_tool_cst.md](docs/theory_tool_cst.md).
|
||||||
|
评测/反哺论文素材:[docs/eval_tool_cst.md](docs/eval_tool_cst.md)。
|
||||||
|
|
||||||
|
## 能不能用来工作?
|
||||||
|
|
||||||
|
**可以**:有监督的本地小改动 / 探索本仓库(TUI + 工具 + 权限确认)。
|
||||||
|
**不宜**:无人值守长任务或当 CI 主力(网关偶发空答、无完整 diff UI)。
|
||||||
|
|
||||||
|
### 5 分钟试用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 一键拉取编译(Linux)
|
||||||
|
curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_linux.sh | bash
|
||||||
|
# 或已有仓库:
|
||||||
|
cd CST-agent && ./scripts/bootstrap_linux.sh .
|
||||||
|
|
||||||
|
# OpenAI 官方
|
||||||
|
export OPENAI_API_KEY='sk-...'
|
||||||
|
export OPENAI_BASE_URL='https://api.openai.com/v1'
|
||||||
|
export OPENAI_MODEL='gpt-4.1-mini'
|
||||||
|
./cst-agent --workspace .
|
||||||
|
```
|
||||||
|
|
||||||
|
完整环境变量说明:[docs/env_openai.md](docs/env_openai.md)。
|
||||||
|
|
||||||
|
| 平台 | 一键脚本 |
|
||||||
|
|---|---|
|
||||||
|
| Linux | [`scripts/bootstrap_linux.sh`](scripts/bootstrap_linux.sh) |
|
||||||
|
| Termux | [`scripts/bootstrap_termux.sh`](scripts/bootstrap_termux.sh) |
|
||||||
|
| Windows 交叉构建(在 Linux 上) | [`scripts/bootstrap_windows.sh`](scripts/bootstrap_windows.sh) → `dist/windows-x86_64/` |
|
||||||
|
| Windows 运行 | 拷贝 dist 后 [`scripts/run_windows.bat`](scripts/run_windows.bat) |
|
||||||
|
|
||||||
|
### 迷你验收工程(论文案例)
|
||||||
|
|
||||||
|
故意带 bug 的小计算器:[`fixtures/mini_calc/`](fixtures/mini_calc/)(任务书 `TASK.md`)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/e2e_mini_calc.sh --baseline-only # 无 API:证明初始测试 RED
|
||||||
|
./scripts/e2e_mini_calc.sh # 有 key:agent 修复后 make test
|
||||||
|
```
|
||||||
|
|
||||||
|
口径:`pass` / `partial` / `fail` — 见 [docs/eval_tool_cst.md](docs/eval_tool_cst.md)。
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
### Make(Linux 默认)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent
|
||||||
|
make
|
||||||
|
make check
|
||||||
|
```
|
||||||
|
|
||||||
|
### CMake(推荐跨编译器 / 交叉)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GCC
|
||||||
|
cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON
|
||||||
|
cmake --build build-gcc && ctest --test-dir build-gcc
|
||||||
|
|
||||||
|
# Clang
|
||||||
|
cmake -B build-clang -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON
|
||||||
|
cmake --build build-clang
|
||||||
|
|
||||||
|
# Headless only(无 ncurses)
|
||||||
|
cmake -B build-hd -DCST_BUILD_TUI=OFF && cmake --build build-hd
|
||||||
|
|
||||||
|
# Windows MinGW 交叉(需安装 mingw-w64)
|
||||||
|
cmake -B build-win -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake \
|
||||||
|
-DCST_BUILD_TUI=OFF
|
||||||
|
cmake --build build-win
|
||||||
|
# TUI:安装 PDCurses 后 -DCST_BUILD_TUI=ON -DPDCurses_ROOT=...
|
||||||
|
```
|
||||||
|
|
||||||
|
依赖:`gcc` 或 `clang`、`libcurl`、Linux TUI 需 `ncursesw`;Windows TUI 需 **PDCurses**;sibling `../SD`(`libsashidown.a`)。
|
||||||
|
API Key:env / 配置文件 / **zenity(Linux)或控制台输入(Windows / 无 zenity)**。
|
||||||
|
|
||||||
|
## 平台支持矩阵
|
||||||
|
|
||||||
|
| OS | Arch | 编译器 | Headless | TUI | Shell |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Linux | x86_64 / aarch64 | GCC, Clang | yes | ncursesw | `/bin/sh` 或旁路 `busybox` |
|
||||||
|
| Android Termux | aarch64 / … | Clang (Termux) | yes | ncurses(Termux 包) | `$PREFIX/bin/sh`;可旁路 busybox |
|
||||||
|
| Windows | x86_64 / aarch64 | MinGW GCC/Clang | yes | PDCurses | **捆绑 busybox-w32** |
|
||||||
|
| macOS | — | — | **不做** | — | — |
|
||||||
|
|
||||||
|
### Termux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkg install clang make cmake libcurl ncurses-ui-libs
|
||||||
|
cmake -B build -DCST_BUILD_TUI=ON && cmake --build build
|
||||||
|
# 可选:旁路 Unix busybox
|
||||||
|
# ./scripts/build_third_party_mingw.sh busybox-unix
|
||||||
|
```
|
||||||
|
|
||||||
|
环境:`PREFIX` 自动用于找 `sh`;`CST_EXE_DIR` / `argv[0]` 用于定位旁路 busybox(`/proc/self/exe` 受限时)。
|
||||||
|
|
||||||
|
### Windows 发行包
|
||||||
|
|
||||||
|
见 [docs/README-windows.md](docs/README-windows.md)。交叉构建:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build_third_party_mingw.sh all # curl+PDCurses+busybox-w32
|
||||||
|
# 源码:https://github.com/rmyorston/busybox-w32.git
|
||||||
|
# https://github.com/mirror/busybox.git (Unix)
|
||||||
|
cmake -B build-win-tui -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake \
|
||||||
|
-DCST_BUILD_TUI=ON -DCMAKE_PREFIX_PATH=$PWD/third_party/prefix-mingw
|
||||||
|
```
|
||||||
|
|
||||||
|
产物示例:`dist/windows-x86_64/`(含 `cst-agent.exe` + `busybox.exe` + `libwinpthread-1.dll`)。
|
||||||
|
|
||||||
|
环境变量:`CST_SHELL`、`CST_EXE_DIR`、`CST_AUTO_APPROVE` 等。
|
||||||
|
|
||||||
|
## Defaults (API)
|
||||||
|
|
||||||
|
默认 MiniMax:`https://api.minimax.cn/v1` / `MiniMax-M2.7`。
|
||||||
|
对接 **OpenAI 官方或其它兼容网关** 时设置:
|
||||||
|
|
||||||
|
| 用途 | 推荐变量 | 等价别名 |
|
||||||
|
|---|---|---|
|
||||||
|
| Key | `OPENAI_API_KEY` | `CST_API_KEY`、`MINIMAX_API_KEY` |
|
||||||
|
| Base(含 `/v1`) | `OPENAI_BASE_URL` | `CST_API_BASE`、`OPENAI_API_BASE` |
|
||||||
|
| Model | `OPENAI_MODEL` | `CST_MODEL` |
|
||||||
|
|
||||||
|
详见 [docs/env_openai.md](docs/env_openai.md)。
|
||||||
|
|
||||||
|
| 项 | 默认(未设 OPENAI_* 时) |
|
||||||
|
|---|---|
|
||||||
|
| API Base | `https://api.minimax.cn/v1` |
|
||||||
|
| Model | `MiniMax-M2.7` |
|
||||||
|
| API Key | env → 配置目录 credentials → zenity/控制台 |
|
||||||
|
|
||||||
|
环境变量:`CST_MODELS`、`CST_AUTO_APPROVE=1`、`CST_SHELL`、`CST_EXE_DIR`。
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local CTL demo (no API)
|
||||||
|
./cst-agent --demo-ctl --workspace .
|
||||||
|
|
||||||
|
# TUI(OpenCode 风格;Assistant Markdown 经 Sashidown CST 渲染)
|
||||||
|
./cst-agent --workspace ..
|
||||||
|
|
||||||
|
# 强制重新弹窗保存 Key
|
||||||
|
./cst-agent --ask-key --workspace .
|
||||||
|
|
||||||
|
# Headless(默认 auto-approve shell/写文件)
|
||||||
|
./cst-agent --headless --prompt 'List files' --workspace .
|
||||||
|
```
|
||||||
|
|
||||||
|
### TUI 快捷键与斜杠命令
|
||||||
|
|
||||||
|
| 操作 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| Enter | 发送;空输入展开最近 tool 卡;busy 时入队(最多 3) |
|
||||||
|
| Ctrl+N | 输入换行(多行 composer) |
|
||||||
|
| ↑ / ↓ | 提示历史 |
|
||||||
|
| Esc / Ctrl+C | busy 时取消当前 turn |
|
||||||
|
| Tab / `t`(空输入) | 展开/折叠 Thought |
|
||||||
|
| 权限 | shell/写文件:`y` 允许 · `n` 拒绝 · `a` 本进程对该工具 Always |
|
||||||
|
| `/new` | 新会话 |
|
||||||
|
| `/session` | **可点选**会话列表(↑↓ Enter);`/session <id>` 手打切换 |
|
||||||
|
| `/model` | **可点选**模型;`/model NAME` 直接设置(进程内) |
|
||||||
|
| `/compact` | 压缩上下文 |
|
||||||
|
| `/details` | 展开/折叠全部 tool 输出 |
|
||||||
|
| `/help` | 帮助 |
|
||||||
|
|
||||||
|
会话落盘:`<workspace>/.agent/sessions/<id>.jsonl` + `CURRENT`。
|
||||||
|
|
||||||
|
### 协议
|
||||||
|
|
||||||
|
- OpenAI 兼容 `POST …/chat/completions`:原生 `tools` / SSE `tool_calls`,answer 轮 `tool_choice:none`
|
||||||
|
- MiniMax XML `<minimax:tool_call>` 作 fallback
|
||||||
|
- Assistant 仅工具时 `content: null` + `tool_calls`(对齐 OpenCode/OpenAI Chat)
|
||||||
|
|
||||||
|
## CTL tool language
|
||||||
|
|
||||||
|
```text
|
||||||
|
call <id> <name>
|
||||||
|
arg <key> <nbytes>
|
||||||
|
<raw bytes exactly nbytes>
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Execution happens only on confirmed `end` (`CTL_EV_CALL_READY`).
|
||||||
|
|
||||||
|
## Starvation experiment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make check
|
||||||
|
./scripts/replay_starvation.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
CTL vs JSON-buffer 截断数字写入 [docs/eval_tool_cst.md](docs/eval_tool_cst.md) 实验 A;与 mini_calc e2e 一起作为 Tool-loop CST 附录素材(**不**替代 SD 主论文 Markdown→HTML 实验)。
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Usage:
|
||||||
|
# cmake -B build-win-arm -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-aarch64.cmake \
|
||||||
|
# -DCST_BUILD_TUI=OFF
|
||||||
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
|
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||||
|
|
||||||
|
set(CMAKE_C_COMPILER aarch64-w64-mingw32-gcc)
|
||||||
|
set(CMAKE_CXX_COMPILER aarch64-w64-mingw32-g++)
|
||||||
|
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Usage:
|
||||||
|
# ./scripts/build_third_party_mingw.sh curl
|
||||||
|
# cmake -B build-win -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake \
|
||||||
|
# -DCST_BUILD_TUI=OFF
|
||||||
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
|
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||||
|
|
||||||
|
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||||
|
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||||
|
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||||
|
|
||||||
|
get_filename_component(_AGENT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||||
|
set(_MINGW_PREFIX "${_AGENT_ROOT}/third_party/prefix-mingw")
|
||||||
|
list(APPEND CMAKE_PREFIX_PATH "${_MINGW_PREFIX}")
|
||||||
|
set(CMAKE_FIND_ROOT_PATH "/usr/x86_64-w64-mingw32" "${_MINGW_PREFIX}")
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||||
|
|
||||||
|
set(PDCurses_ROOT "${_MINGW_PREFIX}" CACHE PATH "PDCurses prefix")
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# CST-Agent on Windows
|
||||||
|
|
||||||
|
## Release layout
|
||||||
|
|
||||||
|
```
|
||||||
|
cst-agent.exe
|
||||||
|
busybox.exe # built from busybox-w32
|
||||||
|
libwinpthread-1.dll # MinGW runtime (required)
|
||||||
|
LICENSE.busybox
|
||||||
|
README-windows.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Prebuilt tree from Linux cross-compile: `agent/dist/windows-x86_64/`.
|
||||||
|
|
||||||
|
## Build (MinGW-w64 cross from Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent
|
||||||
|
# 1) third-party: curl (Schannel) + PDCurses + busybox-w32
|
||||||
|
./scripts/build_third_party_mingw.sh all
|
||||||
|
# sources:
|
||||||
|
# https://github.com/rmyorston/busybox-w32.git
|
||||||
|
# https://github.com/curl/curl.git
|
||||||
|
# https://github.com/wmcbrine/PDCurses.git
|
||||||
|
# Unix busybox (optional, Linux/Termux bundle):
|
||||||
|
# ./scripts/build_third_party_mingw.sh busybox-unix
|
||||||
|
# https://github.com/mirror/busybox.git
|
||||||
|
|
||||||
|
# 2) headless
|
||||||
|
cmake -B build-win -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake \
|
||||||
|
-DCST_BUILD_TUI=OFF -DCMAKE_PREFIX_PATH=$PWD/third_party/prefix-mingw
|
||||||
|
cmake --build build-win
|
||||||
|
|
||||||
|
# 3) TUI (PDCurses)
|
||||||
|
cmake -B build-win-tui -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake \
|
||||||
|
-DCST_BUILD_TUI=ON -DCMAKE_PREFIX_PATH=$PWD/third_party/prefix-mingw \
|
||||||
|
-DPDCurses_ROOT=$PWD/third_party/prefix-mingw
|
||||||
|
cmake --build build-win-tui
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `build-win-tui/cst-agent.exe`, `third_party/busybox/busybox.exe`, and
|
||||||
|
`libwinpthread-1.dll` into one folder.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bat
|
||||||
|
set CST_API_KEY=...
|
||||||
|
cst-agent.exe --headless --workspace . --prompt "echo hi"
|
||||||
|
cst-agent.exe --workspace .
|
||||||
|
```
|
||||||
|
|
||||||
|
Shell: bundled `busybox.exe sh -c ...` unless `CST_SHELL` is set.
|
||||||
|
Credentials: `%APPDATA%\cst-agent\credentials` (or console prompt).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# OpenAI 兼容 API 环境变量
|
||||||
|
|
||||||
|
CST-Agent 使用 **OpenAI Chat Completions** 协议(`POST {base}/chat/completions`),
|
||||||
|
可对接官方 OpenAI、MiniMax、以及其它兼容网关。
|
||||||
|
|
||||||
|
## 必填 / 常用
|
||||||
|
|
||||||
|
| 变量 | 别名 | 说明 | 示例 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `CST_API_KEY` | `OPENAI_API_KEY`、`MINIMAX_API_KEY` | API Key(按此优先级读取) | `sk-...` |
|
||||||
|
| `CST_API_BASE` | `OPENAI_BASE_URL`、`OPENAI_API_BASE` | API 根路径,须含 `/v1`(不要带 `/chat/completions`) | `https://api.openai.com/v1` |
|
||||||
|
| `CST_MODEL` | `OPENAI_MODEL` | 模型名 | `gpt-4.1-mini` / `MiniMax-M2.7` |
|
||||||
|
|
||||||
|
未设置 key 时:读配置文件 →(Linux)zenity / 控制台输入。
|
||||||
|
|
||||||
|
配置文件:
|
||||||
|
- Linux/Termux:`~/.config/cst-agent/credentials`
|
||||||
|
- Windows:`%APPDATA%\cst-agent\credentials`
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### OpenAI 官方
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENAI_API_KEY='sk-...'
|
||||||
|
export OPENAI_BASE_URL='https://api.openai.com/v1'
|
||||||
|
export OPENAI_MODEL='gpt-4.1-mini'
|
||||||
|
./cst-agent --workspace .
|
||||||
|
```
|
||||||
|
|
||||||
|
等价 CST 前缀:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CST_API_KEY='sk-...'
|
||||||
|
export CST_API_BASE='https://api.openai.com/v1'
|
||||||
|
export CST_MODEL='gpt-4.1-mini'
|
||||||
|
```
|
||||||
|
|
||||||
|
### MiniMax(默认)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CST_API_KEY='...' # 或 MINIMAX_API_KEY
|
||||||
|
# 默认 base=https://api.minimax.cn/v1 model=MiniMax-M2.7
|
||||||
|
./cst-agent --workspace .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 任意兼容网关
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENAI_API_KEY='...'
|
||||||
|
export OPENAI_BASE_URL='https://your-gateway.example/v1'
|
||||||
|
export OPENAI_MODEL='your-model'
|
||||||
|
./cst-agent --headless --prompt 'ping' --workspace .
|
||||||
|
```
|
||||||
|
|
||||||
|
命令行也可覆盖:`--api-base` / `--api-key` / `--model`。
|
||||||
|
|
||||||
|
## 其它环境变量
|
||||||
|
|
||||||
|
| 变量 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `CST_MODELS` | `/model` 可选列表,`:` 或 `,` 分隔 |
|
||||||
|
| `CST_AUTO_APPROVE=1` | 自动允许 shell/写文件(headless 常用) |
|
||||||
|
| `CST_SHELL` | shell 解释器路径(覆盖 busybox/`sh`) |
|
||||||
|
| `CST_EXE_DIR` | 可执行文件目录(Termux 旁路 busybox 时有用) |
|
||||||
|
|
||||||
|
## CLI 对照
|
||||||
|
|
||||||
|
```text
|
||||||
|
--api-base URL ← CST_API_BASE / OPENAI_BASE_URL
|
||||||
|
--api-key KEY ← CST_API_KEY / OPENAI_API_KEY
|
||||||
|
--model NAME ← CST_MODEL / OPENAI_MODEL
|
||||||
|
```
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Tool-loop CST 评测笔记(反哺论文素材)
|
||||||
|
|
||||||
|
> 本文是 **CST-Agent** 侧的评测/案例记录,供写入 SD 主论文附录或独立 TR 时粘贴。
|
||||||
|
> **不**替代 [`SD/paper/sections/eval_zh.tex`](../../SD/paper/sections/eval_zh.tex) 中 Markdown→HTML 主实验。
|
||||||
|
|
||||||
|
理论锚点见 [theory_tool_cst.md](theory_tool_cst.md)。
|
||||||
|
|
||||||
|
## 命题分工
|
||||||
|
|
||||||
|
| 层 | 主张 | 主变量 | 非主张 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 论文 CST | \(O_{\mathrm{conf}}\) 有界确认 → HTML | \(\mathcal{L}_0\) vs 引用式链接等 | — |
|
||||||
|
| Tool-loop CST | 同构到工具帧 | **CTL vs JSON-buffer** 截断 | 网关空答率、模型智商 |
|
||||||
|
| 工程载体 | OpenAI `tool_calls` + XML fallback | 可演示端到端 | 不算进 CST 正确性定理 |
|
||||||
|
|
||||||
|
## 实验 A — 饥饿 / 截断(已自动化)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent && make check
|
||||||
|
# 关注 ./tests/test_replay_starvation
|
||||||
|
```
|
||||||
|
|
||||||
|
填写模板(跑完后更新数字):
|
||||||
|
|
||||||
|
| 指标 | CTL | JSON-buffer |
|
||||||
|
|---|---|---|
|
||||||
|
| 完整输入可执行 | yes | yes |
|
||||||
|
| 截断点上过早执行次数 | **0** | n/a(截断即失败) |
|
||||||
|
| 安全截断样本(本机 `make check`) | 3 | 3 次 trunc failure |
|
||||||
|
|
||||||
|
对应理论:裸 JSON ∉ \(\mathcal{L}_0^{\mathrm{tool}}\);CTL 长度前缀使参数体字节可确认,pending 有界。
|
||||||
|
|
||||||
|
## 实验 B — mini_calc 端到端案例(不可逆发射)
|
||||||
|
|
||||||
|
协议见 [fixtures/mini_calc/](../fixtures/mini_calc/) 与 [scripts/e2e_mini_calc.sh](../scripts/e2e_mini_calc.sh)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent
|
||||||
|
./scripts/e2e_mini_calc.sh --baseline-only # 无 key:证明 fixture 初始 RED
|
||||||
|
./scripts/e2e_mini_calc.sh # 有 key:agent 修 bug → make test
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验收口径
|
||||||
|
|
||||||
|
| outcome | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| `pass` | `make test` 绿 |
|
||||||
|
| `partial` | 改了 `calc.c` 但测试仍红 |
|
||||||
|
| `fail` | 未改文件或 agent/API 失败 |
|
||||||
|
| `baseline_ok_agent_skipped` / `baseline_ok_no_api_key` | 仅验证夹具 |
|
||||||
|
|
||||||
|
### 论文可引用的叙事
|
||||||
|
|
||||||
|
1. Session JSONL 中顺序:assistant `tool_calls` → `role:tool` 结果 →(可选)写文件/shell → 最终文本。
|
||||||
|
2. 工具仅在确认/原生完整 arguments 后执行(工程桥);**CTL 饥饿实验**仍是形式主证据。
|
||||||
|
3. 案例展示「确认后不可撤回的磁盘副作用」与 transcript 只追加。
|
||||||
|
|
||||||
|
### 跑分记录(模板)
|
||||||
|
|
||||||
|
| 日期 (UTC) | model | outcome | notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2026-09-07 | MiniMax-M2.7 | **pass** | `e2e_mini_calc.sh`;baseline RED → agent 修 `add`/`div_ints` → `make test` 绿;见 `out/result-20260907T131300Z.txt` |
|
||||||
|
|
||||||
|
## 与主论文的衔接建议
|
||||||
|
|
||||||
|
- **附录 / 扩展节标题示例**:「Tool-loop CST:确认前沿到工具调用」
|
||||||
|
- 粘贴:theory 表(theory_tool_cst.md)+ 实验 A 表 + 实验 B 一行案例。
|
||||||
|
- 明确声明:主 fan-out / \(\mathcal{A}=0\) 数字仍来自 Sashidown;本笔记只提供 **同构第二实例**。
|
||||||
|
|
||||||
|
## 明确不写进定理的内容
|
||||||
|
|
||||||
|
- 商业 API 空回复、权限 UX、TUI 观感
|
||||||
|
- 「已达生产 coding agent」类宣称
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Tool-loop CST:确认前沿推广到工具调用
|
||||||
|
|
||||||
|
## 命题
|
||||||
|
|
||||||
|
在 agent 工具环中,对模型输出做**确认流式转导**:只在 \(O_{\mathrm{conf}}\) 内不可逆地发射「可执行工具调用 / 可应用补丁 / 可提交会话状态」。相对「拼到合法 JSON 再执行」的缓冲基线,这能降低注意力饥渴导致的残缺工具帧(未闭合括号、半截 arguments)被过早执行或整轮失败的问题。
|
||||||
|
|
||||||
|
## 与论文 CST 的对应
|
||||||
|
|
||||||
|
| 论文(Markdown→HTML) | 工具环推广 |
|
||||||
|
|---|---|
|
||||||
|
| 输入字母表 \(\Sigma\) | 模型文本 / CTL 字节流 |
|
||||||
|
| 指称 \(f,g\) → HTML | \(f_{\mathrm{tool}}\) → 已确认 `ToolCall` / `PatchHunk` |
|
||||||
|
| \(O_{\mathrm{conf}}\) 不可撤回 HTML | 不可撤回:开始执行工具、应用 hunk、写入 transcript |
|
||||||
|
| \(\mathcal{L}_0\) 排除引用式链接 | \(\mathcal{L}_0^{\mathrm{tool}}\) **排除裸 JSON 对象** |
|
||||||
|
| \(K=2L_{\mathrm{line}}\) | 工具帧同样要求有界 pending |
|
||||||
|
|
||||||
|
## 为何排除裸 JSON
|
||||||
|
|
||||||
|
JSON 对象依赖括号匹配与字符串转义。在未闭合 `"` / `{` 时,任意后缀都可能改变已见结构的指称,故 \(O_{\mathrm{conf}}\) 往往停在空前缀,pending 随流长增长——这与论文中引用式链接的**无界后向绑定**同构。「等整个 JSON `Parse` 成功」只是 EOF 缓冲,不是有界 CST。
|
||||||
|
|
||||||
|
## \(\mathcal{L}_0^{\mathrm{tool}}\):CTL(CST Tool Language)
|
||||||
|
|
||||||
|
行界 \(L_{\mathrm{line}}\);参数体用长度前缀,避免转义歧义。
|
||||||
|
|
||||||
|
```text
|
||||||
|
call <id> <name>\n
|
||||||
|
arg <key> <nbytes>\n
|
||||||
|
<raw bytes exactly nbytes>
|
||||||
|
end\n
|
||||||
|
```
|
||||||
|
|
||||||
|
### 确认规则
|
||||||
|
|
||||||
|
1. **`call` 行以 `\n` 结束**:确认 `(id, name)`(此后不可撤回工具身份)。
|
||||||
|
2. **`arg` 头以 `\n` 结束**:确认 `(key, nbytes)`;随后精确消费 `nbytes` 字节,每字节到达即可确认(无 JSON 转义)。
|
||||||
|
3. **`end\n`**:确认整次调用可执行;转导器发射一个 `ToolCall` 事件。
|
||||||
|
4. 行长 \(> L_{\mathrm{line}}\)、非法 token、`nbytes` 超上限、非 CTL 噪声 → 离开契约,标记 `tool_parse_error`。
|
||||||
|
|
||||||
|
### 有界 pending \(K\)
|
||||||
|
|
||||||
|
- 解析行头时 pending \(\le L_{\mathrm{line}}\)。
|
||||||
|
- 读参数体时 pending \(\le\) 剩余未读字节数,但**已读体字节已确认**,工作区只需当前行缓冲 + 当前 arg 剩余计数(\(O(L_{\mathrm{line}})\) 寄存器),不随会话总长增长。
|
||||||
|
|
||||||
|
取 \(K = L_{\mathrm{line}}\)(行头)与实现常量 `CTL_MAX_ARG`(单参上限,契约拒绝更大值)。
|
||||||
|
|
||||||
|
## 不可逆发射
|
||||||
|
|
||||||
|
实现是确认忠实的,当且仅当:
|
||||||
|
|
||||||
|
- 拼接发射的工具事件序列是 \(O_{\mathrm{conf}}(u)\) 的前缀;
|
||||||
|
- 一旦 `ToolCall` 被发射,runtime **必须**执行(或显式记录拒绝),不得因后续字节撤回;
|
||||||
|
- session transcript 只追加:用户消息、已确认助手文本、工具调用与结果。
|
||||||
|
|
||||||
|
## 对照基线
|
||||||
|
|
||||||
|
**JSON-buffer**:将模型输出拼接到可 `cJSON_Parse` 的 `{"name","arguments"}`(或 OpenAI `tool_calls` 形)才执行;流中途截断则失败。与 CTL 使用同一截断点集合,只换帧语法。
|
||||||
|
|
||||||
|
## 补丁确认
|
||||||
|
|
||||||
|
`apply_patch` 的 patch 文本作为 CTL 的一个 `arg`;应用侧再要求 unified diff 的每个 hunk 完整(`@@` 到下一块或 EOF)才写盘——整 hunk 确认,禁止半截应用。
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Windows self-test checklist
|
||||||
|
|
||||||
|
Run on a Windows machine (or wine) after MinGW build + busybox.exe beside the binary.
|
||||||
|
|
||||||
|
1. `cst-agent.exe --help` exits 0
|
||||||
|
2. `set CST_API_KEY=...` then `--headless --workspace . --prompt "run echo hello"` — tool output contains hello
|
||||||
|
3. `write_file` under workspace succeeds; path outside workspace rejected
|
||||||
|
4. Credentials saved under `%APPDATA%\cst-agent\credentials`
|
||||||
|
5. With PDCurses build: launch without `--headless`, confirm TUI draws, `/help` works, Esc cancels
|
||||||
|
|
||||||
|
Shell must resolve to bundled busybox unless `CST_SHELL` is set.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
CC ?= gcc
|
||||||
|
CFLAGS ?= -std=c11 -Wall -Wextra -O2
|
||||||
|
|
||||||
|
.PHONY: all clean test
|
||||||
|
|
||||||
|
all: calc
|
||||||
|
|
||||||
|
calc: calc.c
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
test: calc
|
||||||
|
./tests/smoke.sh
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f calc
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# mini_calc
|
||||||
|
|
||||||
|
Tiny integer calculator CLI for CST-Agent e2e demos.
|
||||||
|
|
||||||
|
## Ops
|
||||||
|
|
||||||
|
`add` `sub` `mul` `div` — all four work (see `make test`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./calc add 2 3 # → 5
|
||||||
|
./calc div 8 2 # → 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
make test
|
||||||
|
```
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 任务:修好 mini_calc
|
||||||
|
|
||||||
|
你在一个很小的 C 项目里。请完成下列事项后结束(不要无关重构):
|
||||||
|
|
||||||
|
1. 阅读 `README.md`、`calc.c`、`tests/smoke.sh`。
|
||||||
|
2. 修复 `add` 的 off-by-one bug(应为 `a+b`,不是 `a+b+1`)。
|
||||||
|
3. 实现真正的整数除法 `div_ints`(`a/b`,调用方已保证 `b!=0`)。
|
||||||
|
4. 运行 `make test`,确保全部用例通过。
|
||||||
|
5. 用简短中文说明你改了什么。
|
||||||
|
|
||||||
|
可用工具:`read_file`、`run_shell`、`write_file` / `apply_patch`。工作区根目录就是本项目。
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/* mini_calc — intentionally broken fixture for CST-Agent e2e / paper case study.
|
||||||
|
*
|
||||||
|
* Bugs (for the agent to fix):
|
||||||
|
* 1) add(): off-by-one — returns a+b+1
|
||||||
|
* 2) div(): missing — only stub returns 0
|
||||||
|
* README claims all four ops work (false until fixed).
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int add(int a, int b) {
|
||||||
|
/* BUG: off-by-one */
|
||||||
|
return a + b + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sub(int a, int b) { return a - b; }
|
||||||
|
|
||||||
|
int mul(int a, int b) { return a * b; }
|
||||||
|
|
||||||
|
int div_ints(int a, int b) {
|
||||||
|
/* BUG: not implemented */
|
||||||
|
(void)a;
|
||||||
|
(void)b;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(const char *argv0) {
|
||||||
|
fprintf(stderr, "Usage: %s <add|sub|mul|div> <a> <b>\n", argv0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 4) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
int a = atoi(argv[2]);
|
||||||
|
int b = atoi(argv[3]);
|
||||||
|
int r = 0;
|
||||||
|
if (strcmp(argv[1], "add") == 0)
|
||||||
|
r = add(a, b);
|
||||||
|
else if (strcmp(argv[1], "sub") == 0)
|
||||||
|
r = sub(a, b);
|
||||||
|
else if (strcmp(argv[1], "mul") == 0)
|
||||||
|
r = mul(a, b);
|
||||||
|
else if (strcmp(argv[1], "div") == 0) {
|
||||||
|
if (b == 0) {
|
||||||
|
fprintf(stderr, "div by zero\n");
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
r = div_ints(a, b);
|
||||||
|
} else {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
printf("%d\n", r);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke tests for mini_calc. Exit 0 only when add/sub/mul/div are correct.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
CALC="$ROOT/calc"
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
expect() {
|
||||||
|
local op=$1 a=$2 b=$3 want=$4
|
||||||
|
local got
|
||||||
|
got="$("$CALC" "$op" "$a" "$b")"
|
||||||
|
if [[ "$got" != "$want" ]]; then
|
||||||
|
echo "FAIL: $op $a $b → got '$got' want '$want'"
|
||||||
|
fail=1
|
||||||
|
else
|
||||||
|
echo "ok: $op $a $b → $got"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect add 2 3 5
|
||||||
|
expect sub 9 4 5
|
||||||
|
expect mul 3 4 12
|
||||||
|
expect div 8 2 4
|
||||||
|
|
||||||
|
if [[ "$fail" -ne 0 ]]; then
|
||||||
|
echo "smoke: FAILED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "smoke: PASSED"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
CXX = g++
|
||||||
|
CXXFLAGS = -O2 -std=c++17 -Wall -Wextra
|
||||||
|
|
||||||
|
snake: snake.cpp
|
||||||
|
$(CXX) $(CXXFLAGS) -o snake snake.cpp
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f snake
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Snake Game
|
||||||
|
|
||||||
|
A classic terminal-based Snake game written in C++17.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Smooth snake movement using POSIX terminal I/O
|
||||||
|
- WASD or Arrow keys for movement
|
||||||
|
- Food spawning and snake growth
|
||||||
|
- Wall and self-collision detection
|
||||||
|
- Score tracking
|
||||||
|
- Pause functionality (P key)
|
||||||
|
- Clean terminal rendering
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
This compiles `snake.cpp` with optimization level O2 and C++17 standard.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./snake
|
||||||
|
```
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| W / ↑ | Move Up |
|
||||||
|
| S / ↓ | Move Down |
|
||||||
|
| A / ← | Move Left |
|
||||||
|
| D / → | Move Right |
|
||||||
|
| P | Pause/Resume |
|
||||||
|
| Q | Quit |
|
||||||
|
|
||||||
|
## Game Rules
|
||||||
|
|
||||||
|
- Eat the `*` food to grow and score points (+10 per food)
|
||||||
|
- Avoid hitting the walls `#`
|
||||||
|
- Avoid colliding with your own body `o`
|
||||||
|
- The snake moves continuously
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- GCC/G++ with C++17 support
|
||||||
|
- POSIX-compliant system (Linux, macOS)
|
||||||
|
- Terminal with ANSI escape support
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Write a complete C++ terminal Snake game in this workspace.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Single file `snake.cpp` (C++17 ok). Use POSIX terminal I/O: raw mode or ncurses; WASD or arrow keys; quit with `q`.
|
||||||
|
2. Features: moving snake, food, grow on eat, wall or self collision ends game, score printed, board roughly 20x20 or similar.
|
||||||
|
3. `Makefile` with target `snake` that runs `g++ -O2 -o snake snake.cpp` (link `-lncurses` only if you use ncurses).
|
||||||
|
4. After writing, run `make` to verify it compiles. Do not need an interactive play session.
|
||||||
|
5. Brief README.md with how to build and run.
|
||||||
|
|
||||||
|
Use write_file / run_shell tools. Prefer implementing yourself rather than downloading.
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
|
#include <cstring>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
#include <conio.h>
|
||||||
|
|
||||||
|
// Game constants
|
||||||
|
constexpr int BOARD_WIDTH = 20;
|
||||||
|
constexpr int BOARD_HEIGHT = 15;
|
||||||
|
constexpr char WALL = '#';
|
||||||
|
constexpr char SNAKE_BODY = 'o';
|
||||||
|
constexpr char SNAKE_HEAD = 'O';
|
||||||
|
constexpr char FOOD = '*';
|
||||||
|
constexpr char EMPTY = ' ';
|
||||||
|
|
||||||
|
// Directions
|
||||||
|
enum class Direction { UP, DOWN, LEFT, RIGHT };
|
||||||
|
|
||||||
|
struct Position {
|
||||||
|
int x, y;
|
||||||
|
bool operator==(const Position& other) const {
|
||||||
|
return x == other.x && y == other.y;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GameState {
|
||||||
|
std::deque<Position> snake;
|
||||||
|
Position food;
|
||||||
|
Direction dir;
|
||||||
|
Direction next_dir;
|
||||||
|
int score;
|
||||||
|
bool game_over;
|
||||||
|
bool paused;
|
||||||
|
};
|
||||||
|
|
||||||
|
char board[BOARD_HEIGHT][BOARD_WIDTH + 1];
|
||||||
|
|
||||||
|
void place_food(GameState& state);
|
||||||
|
|
||||||
|
void init_board() {
|
||||||
|
for (int y = 0; y < BOARD_HEIGHT; y++) {
|
||||||
|
for (int x = 0; x < BOARD_WIDTH; x++) {
|
||||||
|
if (y == 0 || y == BOARD_HEIGHT - 1 || x == 0 || x == BOARD_WIDTH - 1) {
|
||||||
|
board[y][x] = WALL;
|
||||||
|
} else {
|
||||||
|
board[y][x] = EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
board[y][BOARD_WIDTH] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_game(GameState& state) {
|
||||||
|
Position head = {BOARD_WIDTH / 2, BOARD_HEIGHT / 2};
|
||||||
|
state.snake = {head, {head.x - 1, head.y}, {head.x - 2, head.y}};
|
||||||
|
state.dir = Direction::RIGHT;
|
||||||
|
state.next_dir = Direction::RIGHT;
|
||||||
|
state.score = 0;
|
||||||
|
state.game_over = false;
|
||||||
|
state.paused = false;
|
||||||
|
place_food(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void place_food(GameState& state) {
|
||||||
|
Position pos;
|
||||||
|
bool on_snake;
|
||||||
|
do {
|
||||||
|
on_snake = false;
|
||||||
|
pos.x = 1 + std::rand() % (BOARD_WIDTH - 2);
|
||||||
|
pos.y = 1 + std::rand() % (BOARD_HEIGHT - 2);
|
||||||
|
for (const auto& segment : state.snake) {
|
||||||
|
if (segment == pos) {
|
||||||
|
on_snake = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (on_snake);
|
||||||
|
state.food = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void render(const GameState& state) {
|
||||||
|
system("cls"); // Windows clear screen
|
||||||
|
|
||||||
|
init_board();
|
||||||
|
|
||||||
|
board[state.food.y][state.food.x] = FOOD;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < state.snake.size(); i++) {
|
||||||
|
const auto& segment = state.snake[i];
|
||||||
|
board[segment.y][segment.x] = (i == 0) ? SNAKE_HEAD : SNAKE_BODY;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "=== SNAKE GAME ===\n";
|
||||||
|
std::cout << "Score: " << state.score << "\n";
|
||||||
|
std::cout << "Controls: WASD/Arrows to move, Q to quit, P to pause\n";
|
||||||
|
std::cout << "==================\n";
|
||||||
|
|
||||||
|
for (int y = 0; y < BOARD_HEIGHT; y++) {
|
||||||
|
std::cout << board[y] << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.game_over) {
|
||||||
|
std::cout << "\n=== GAME OVER! ===\n";
|
||||||
|
std::cout << "Final Score: " << state.score << "\n";
|
||||||
|
std::cout << "Press R to restart or Q to quit.\n";
|
||||||
|
} else if (state.paused) {
|
||||||
|
std::cout << "\n=== PAUSED ===\n";
|
||||||
|
std::cout << "Press P to continue.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool kbhit() {
|
||||||
|
return _kbhit() != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char get_char() {
|
||||||
|
return _getch();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_direction(GameState& state, Direction new_dir) {
|
||||||
|
Direction opposite = Direction::UP;
|
||||||
|
switch (state.dir) {
|
||||||
|
case Direction::UP: opposite = Direction::DOWN; break;
|
||||||
|
case Direction::DOWN: opposite = Direction::UP; break;
|
||||||
|
case Direction::LEFT: opposite = Direction::RIGHT; break;
|
||||||
|
case Direction::RIGHT: opposite = Direction::LEFT; break;
|
||||||
|
}
|
||||||
|
if (new_dir != opposite) {
|
||||||
|
state.next_dir = new_dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool move_snake(GameState& state) {
|
||||||
|
state.dir = state.next_dir;
|
||||||
|
|
||||||
|
Position head = state.snake.front();
|
||||||
|
switch (state.dir) {
|
||||||
|
case Direction::UP: head.y--; break;
|
||||||
|
case Direction::DOWN: head.y++; break;
|
||||||
|
case Direction::LEFT: head.x--; break;
|
||||||
|
case Direction::RIGHT: head.x++; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check wall collision
|
||||||
|
if (head.x <= 0 || head.x >= BOARD_WIDTH - 1 ||
|
||||||
|
head.y <= 0 || head.y >= BOARD_HEIGHT - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check self collision (skip tail as it will move)
|
||||||
|
for (size_t i = 0; i < state.snake.size() - 1; i++) {
|
||||||
|
if (state.snake[i] == head) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.snake.push_front(head);
|
||||||
|
|
||||||
|
// Check food
|
||||||
|
if (head == state.food) {
|
||||||
|
state.score += 10;
|
||||||
|
place_food(state);
|
||||||
|
} else {
|
||||||
|
state.snake.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_input(GameState& state) {
|
||||||
|
if (kbhit()) {
|
||||||
|
char c = get_char();
|
||||||
|
|
||||||
|
if (state.game_over) {
|
||||||
|
if (c == 'r' || c == 'R') {
|
||||||
|
init_game(state);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.paused) {
|
||||||
|
if (c == 'p' || c == 'P') {
|
||||||
|
state.paused = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
case 'w': case 'W': case 72: update_direction(state, Direction::UP); break;
|
||||||
|
case 's': case 'S': case 80: update_direction(state, Direction::DOWN); break;
|
||||||
|
case 'a': case 'A': case 75: update_direction(state, Direction::LEFT); break;
|
||||||
|
case 'd': case 'D': case 77: update_direction(state, Direction::RIGHT); break;
|
||||||
|
case 'q': case 'Q': state.game_over = true; state.score = -1; break;
|
||||||
|
case 'p': case 'P': state.paused = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::srand(static_cast<unsigned>(std::time(nullptr)));
|
||||||
|
|
||||||
|
GameState state;
|
||||||
|
init_game(state);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
render(state);
|
||||||
|
|
||||||
|
if (!state.game_over && !state.paused) {
|
||||||
|
if (!move_snake(state)) {
|
||||||
|
state.game_over = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_input();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
#ifndef CST_AGENT_H
|
||||||
|
#define CST_AGENT_H
|
||||||
|
|
||||||
|
#include "ctl.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CST_AGENT_MAX_MSG 256
|
||||||
|
#define CST_AGENT_MAX_CONTENT (512 * 1024)
|
||||||
|
#define CST_AGENT_CTX_WINDOW 40 /* max non-system msgs sent to LLM */
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CST_ROLE_SYSTEM = 0,
|
||||||
|
CST_ROLE_USER,
|
||||||
|
CST_ROLE_ASSISTANT,
|
||||||
|
CST_ROLE_TOOL
|
||||||
|
} cst_role_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cst_role_t role;
|
||||||
|
char *content;
|
||||||
|
char tool_call_id[CTL_MAX_ID]; /* for tool results */
|
||||||
|
char tool_name[CTL_MAX_NAME];
|
||||||
|
char *tool_calls_json; /* assistant: OpenAI tool_calls array JSON, or NULL */
|
||||||
|
} cst_message_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char workspace_root[4096];
|
||||||
|
char session_path[4096];
|
||||||
|
char api_base[512];
|
||||||
|
char api_key[512];
|
||||||
|
char model[128];
|
||||||
|
int headless;
|
||||||
|
int confirm_shell; /* confirm run_shell */
|
||||||
|
int confirm_write; /* confirm write_file / apply_patch */
|
||||||
|
int auto_approve; /* headless / CST_AUTO_APPROVE=1 */
|
||||||
|
} cst_agent_config_t;
|
||||||
|
|
||||||
|
typedef struct cst_session cst_session_t;
|
||||||
|
typedef struct cst_agent cst_agent_t;
|
||||||
|
|
||||||
|
/* --- sandbox --- */
|
||||||
|
int cst_sandbox_resolve(const char *workspace_root, const char *rel_or_abs,
|
||||||
|
char *out, size_t out_sz);
|
||||||
|
|
||||||
|
/* --- session (append-only JSONL) --- */
|
||||||
|
cst_session_t *cst_session_open(const char *path);
|
||||||
|
void cst_session_close(cst_session_t *s);
|
||||||
|
int cst_session_append(cst_session_t *s, const cst_message_t *msg);
|
||||||
|
int cst_session_count(const cst_session_t *s);
|
||||||
|
const cst_message_t *cst_session_get(const cst_session_t *s, int i);
|
||||||
|
const char *cst_session_path(const cst_session_t *s);
|
||||||
|
int cst_session_clear(cst_session_t *s, int keep_system);
|
||||||
|
int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n);
|
||||||
|
|
||||||
|
/* --- workspace .agent/sessions store (OpenCode-like) --- */
|
||||||
|
int cst_agent_store_ensure(const char *workspace);
|
||||||
|
int cst_agent_store_dir(const char *workspace, char *out, size_t out_sz);
|
||||||
|
int cst_agent_store_new_id(char *id_out, size_t id_sz);
|
||||||
|
int cst_agent_store_path_for_id(const char *workspace, const char *id,
|
||||||
|
char *out, size_t out_sz);
|
||||||
|
int cst_agent_store_set_current(const char *workspace, const char *id);
|
||||||
|
int cst_agent_store_get_current(const char *workspace, char *id_out,
|
||||||
|
size_t id_sz);
|
||||||
|
int cst_agent_store_resolve_default(const char *workspace, char *path_out,
|
||||||
|
size_t path_sz, char *id_out,
|
||||||
|
size_t id_sz);
|
||||||
|
int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids);
|
||||||
|
|
||||||
|
/* --- tools --- */
|
||||||
|
typedef struct {
|
||||||
|
char *out;
|
||||||
|
size_t out_len;
|
||||||
|
int exit_code;
|
||||||
|
} cst_tool_result_t;
|
||||||
|
|
||||||
|
void cst_tool_result_free(cst_tool_result_t *r);
|
||||||
|
|
||||||
|
int cst_tool_read_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r);
|
||||||
|
int cst_tool_run_shell(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r);
|
||||||
|
int cst_tool_apply_patch(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r);
|
||||||
|
int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r);
|
||||||
|
int cst_tool_dispatch(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r);
|
||||||
|
|
||||||
|
/* Build ctl_tool_call from native OpenAI tool_call fields. */
|
||||||
|
int cst_tool_call_from_native(ctl_tool_call_t *out, const char *id,
|
||||||
|
const char *name, const char *args_json);
|
||||||
|
|
||||||
|
/* --- LLM --- */
|
||||||
|
typedef void (*cst_llm_on_delta_fn)(const char *data, size_t len, void *ud);
|
||||||
|
typedef void (*cst_llm_on_native_tool_fn)(const char *id, const char *name,
|
||||||
|
const char *args_json, void *ud);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *api_base;
|
||||||
|
const char *api_key;
|
||||||
|
const char *model;
|
||||||
|
const cst_message_t *msgs;
|
||||||
|
int nmsgs;
|
||||||
|
cst_llm_on_delta_fn on_delta;
|
||||||
|
cst_llm_on_native_tool_fn on_native_tool; /* fired once per completed call */
|
||||||
|
void *userdata;
|
||||||
|
int enable_tools; /* send tools[] schema */
|
||||||
|
int tool_choice_none; /* "tool_choice":"none" */
|
||||||
|
volatile int *cancel_flag; /* non-zero → abort curl */
|
||||||
|
} cst_llm_request_t;
|
||||||
|
|
||||||
|
int cst_llm_chat_stream(const cst_llm_request_t *req, char *err, size_t err_sz);
|
||||||
|
|
||||||
|
/* Live UI / observability hooks for a turn */
|
||||||
|
typedef struct {
|
||||||
|
void (*on_status)(const char *msg, void *ud);
|
||||||
|
void (*on_assistant_delta)(const char *text, size_t len, void *ud);
|
||||||
|
void (*on_tool_event)(const char *phase, const ctl_tool_call_t *call,
|
||||||
|
const char *detail, void *ud);
|
||||||
|
void (*on_think_delta)(const char *text, size_t len, void *ud);
|
||||||
|
/* Return 1=allow, 0=deny. NULL → use config auto_approve / defaults. */
|
||||||
|
int (*on_permission)(const char *tool_name, const char *detail, void *ud);
|
||||||
|
void *userdata;
|
||||||
|
} cst_agent_hooks_t;
|
||||||
|
|
||||||
|
/* --- agent loop --- */
|
||||||
|
cst_agent_t *cst_agent_create(const cst_agent_config_t *cfg);
|
||||||
|
void cst_agent_destroy(cst_agent_t *a);
|
||||||
|
int cst_agent_run_turn(cst_agent_t *a, const char *user_text, char *err,
|
||||||
|
size_t err_sz);
|
||||||
|
int cst_agent_run_turn_ex(cst_agent_t *a, const char *user_text,
|
||||||
|
const cst_agent_hooks_t *hooks, char *err,
|
||||||
|
size_t err_sz);
|
||||||
|
cst_session_t *cst_agent_session(cst_agent_t *a);
|
||||||
|
const cst_agent_config_t *cst_agent_config(const cst_agent_t *a);
|
||||||
|
cst_agent_config_t *cst_agent_config_mut(cst_agent_t *a);
|
||||||
|
void cst_agent_request_cancel(cst_agent_t *a);
|
||||||
|
int cst_agent_new_session(cst_agent_t *a, char *id_out, size_t id_sz);
|
||||||
|
int cst_agent_switch_session(cst_agent_t *a, const char *id);
|
||||||
|
int cst_agent_compact(cst_agent_t *a, char *err, size_t err_sz);
|
||||||
|
|
||||||
|
const char *cst_agent_system_prompt(void);
|
||||||
|
|
||||||
|
/* Credentials: zenity + ~/.config/cst-agent/credentials (0600) */
|
||||||
|
int cst_creds_load_api_key(char *out, size_t out_sz);
|
||||||
|
int cst_creds_save_api_key(const char *key);
|
||||||
|
int cst_creds_prompt_api_key(char *out, size_t out_sz);
|
||||||
|
int cst_creds_ensure_api_key(char *out, size_t out_sz);
|
||||||
|
|
||||||
|
/* JSON baseline parser for experiments (buffer until valid object). */
|
||||||
|
typedef struct {
|
||||||
|
int ok;
|
||||||
|
char name[CTL_MAX_NAME];
|
||||||
|
char arguments_json[65536];
|
||||||
|
} cst_json_tool_t;
|
||||||
|
|
||||||
|
int cst_json_try_parse_tool(const char *buf, size_t len, cst_json_tool_t *out);
|
||||||
|
|
||||||
|
/* Built-in / CST_MODELS model picker list. Writes up to max_out names.
|
||||||
|
* Always includes `current` if non-empty and not already listed. */
|
||||||
|
int cst_models_list(const char *current, char out[][128], int max_out);
|
||||||
|
|
||||||
|
int cst_tui_run(cst_agent_t *agent);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* CST_AGENT_H */
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#ifndef CTL_H
|
||||||
|
#define CTL_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CTL_MAX_LINE
|
||||||
|
#define CTL_MAX_LINE 4096
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CTL_MAX_ARGS
|
||||||
|
#define CTL_MAX_ARGS 16
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CTL_MAX_ARG_BYTES
|
||||||
|
#define CTL_MAX_ARG_BYTES (1024 * 1024)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CTL_MAX_ID
|
||||||
|
#define CTL_MAX_ID 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CTL_MAX_NAME
|
||||||
|
#define CTL_MAX_NAME 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CTL_MAX_KEY
|
||||||
|
#define CTL_MAX_KEY 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CTL_OK = 0,
|
||||||
|
CTL_ERR_CONTRACT = 1, /* left L0_tool */
|
||||||
|
CTL_ERR_NOMEM = 2,
|
||||||
|
CTL_ERR_LIMIT = 3
|
||||||
|
} ctl_status_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CTL_EV_NONE = 0,
|
||||||
|
CTL_EV_CALL_OPEN, /* id+name confirmed */
|
||||||
|
CTL_EV_ARG, /* one arg fully confirmed */
|
||||||
|
CTL_EV_CALL_READY, /* end confirmed — safe to execute */
|
||||||
|
CTL_EV_TEXT, /* non-CTL confirmed text (outside call) */
|
||||||
|
CTL_EV_ERROR
|
||||||
|
} ctl_event_type_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char key[CTL_MAX_KEY];
|
||||||
|
char *value;
|
||||||
|
size_t value_len;
|
||||||
|
} ctl_arg_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char id[CTL_MAX_ID];
|
||||||
|
char name[CTL_MAX_NAME];
|
||||||
|
ctl_arg_t args[CTL_MAX_ARGS];
|
||||||
|
int nargs;
|
||||||
|
} ctl_tool_call_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ctl_event_type_t type;
|
||||||
|
const ctl_tool_call_t *call; /* valid for CALL_* / ARG */
|
||||||
|
const char *text; /* for TEXT */
|
||||||
|
size_t text_len;
|
||||||
|
const char *error; /* for ERROR */
|
||||||
|
} ctl_event_t;
|
||||||
|
|
||||||
|
typedef void (*ctl_on_event_fn)(const ctl_event_t *ev, void *userdata);
|
||||||
|
|
||||||
|
typedef struct ctl_parser ctl_parser_t;
|
||||||
|
|
||||||
|
ctl_parser_t *ctl_parser_create(ctl_on_event_fn on_event, void *userdata);
|
||||||
|
void ctl_parser_destroy(ctl_parser_t *p);
|
||||||
|
|
||||||
|
/* Feed UTF-8 bytes. Emits only confirmed events. */
|
||||||
|
ctl_status_t ctl_parser_feed(ctl_parser_t *p, const char *data, size_t len);
|
||||||
|
|
||||||
|
/* EOF: flush; incomplete call => ERROR (no execute). */
|
||||||
|
ctl_status_t ctl_parser_finish(ctl_parser_t *p);
|
||||||
|
|
||||||
|
/* Bytes currently pending (unconfirmed). */
|
||||||
|
size_t ctl_parser_pending(const ctl_parser_t *p);
|
||||||
|
|
||||||
|
/* Peak pending observed (for experiments). */
|
||||||
|
size_t ctl_parser_pending_peak(const ctl_parser_t *p);
|
||||||
|
|
||||||
|
/* Reset for a new assistant turn. */
|
||||||
|
void ctl_parser_reset(ctl_parser_t *p);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* CTL_H */
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#ifndef MM_TOOL_H
|
||||||
|
#define MM_TOOL_H
|
||||||
|
|
||||||
|
#include "ctl.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Streaming CST scanner for MiniMax native tool XML:
|
||||||
|
*
|
||||||
|
* <minimax:tool_call>
|
||||||
|
* <invoke name="read_file">
|
||||||
|
* <parameter name="path">README.md</parameter>
|
||||||
|
* </invoke>
|
||||||
|
* </minimax:tool_call>
|
||||||
|
*
|
||||||
|
* Also accepts the dialect seen in the wild:
|
||||||
|
* <minimax:tool_call>
|
||||||
|
* invoke read_file
|
||||||
|
* <arg name="path">./README.md</arg>
|
||||||
|
* </minimax:tool_call>
|
||||||
|
*
|
||||||
|
* Confirmation: nothing executes until </minimax:tool_call>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef void (*mm_on_event_fn)(const ctl_event_t *ev, void *userdata);
|
||||||
|
|
||||||
|
typedef struct mm_scan mm_scan_t;
|
||||||
|
|
||||||
|
mm_scan_t *mm_scan_create(mm_on_event_fn on_event, void *userdata);
|
||||||
|
void mm_scan_destroy(mm_scan_t *s);
|
||||||
|
void mm_scan_reset(mm_scan_t *s);
|
||||||
|
|
||||||
|
/* Feed bytes. Emits TEXT for confirmed non-tool regions and CALL_* for
|
||||||
|
* completed tool blocks. Incomplete tool blocks stay pending. */
|
||||||
|
int mm_scan_feed(mm_scan_t *s, const char *data, size_t len);
|
||||||
|
|
||||||
|
/* Flush: incomplete open tool block => emit remaining as TEXT (no execute). */
|
||||||
|
int mm_scan_finish(mm_scan_t *s);
|
||||||
|
|
||||||
|
size_t mm_scan_pending(const mm_scan_t *s);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#ifndef CST_OS_COMPAT_H
|
||||||
|
#define CST_OS_COMPAT_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef OS_PATH_MAX
|
||||||
|
#define OS_PATH_MAX 4096
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* --- path --- */
|
||||||
|
int os_path_is_abs(const char *p);
|
||||||
|
/* Join with native separator; out always uses '/' on Unix and '\\' on Win for
|
||||||
|
* the joiner, but os_path_norm_slashes converts to '/' for comparisons. */
|
||||||
|
int os_path_join(char *out, size_t out_sz, const char *a, const char *b);
|
||||||
|
void os_path_norm_slashes(char *p);
|
||||||
|
int os_realpath(const char *path, char *out, size_t out_sz);
|
||||||
|
/* True if path is under root (after realpath/norm). Trailing slash rules apply. */
|
||||||
|
int os_path_under(const char *root, const char *path);
|
||||||
|
|
||||||
|
/* --- dirs / config --- */
|
||||||
|
int os_mkdir(const char *path, int mode);
|
||||||
|
int os_config_dir(char *out, size_t out_sz);
|
||||||
|
int os_temp_dir(char *out, size_t out_sz);
|
||||||
|
/* Create unique temp file; writes path into out; returns fd or -1. Caller closes. */
|
||||||
|
int os_mkstemp(char *out, size_t out_sz, const char *prefix);
|
||||||
|
|
||||||
|
/* Directory containing the running executable (for bundled busybox).
|
||||||
|
* Call os_set_prog_path(argv[0]) early on Unix/Termux as fallback. */
|
||||||
|
void os_set_prog_path(const char *argv0);
|
||||||
|
int os_exe_dir(char *out, size_t out_sz);
|
||||||
|
|
||||||
|
/* Resolve shell interpreter path into out.
|
||||||
|
* Order: CST_SHELL → <exe_dir>/busybox[.exe] → /bin/sh (Unix). */
|
||||||
|
int os_resolve_shell(char *out, size_t out_sz);
|
||||||
|
|
||||||
|
/* --- process --- */
|
||||||
|
typedef struct {
|
||||||
|
char *out;
|
||||||
|
size_t out_len;
|
||||||
|
int exit_code;
|
||||||
|
} os_run_result_t;
|
||||||
|
|
||||||
|
void os_run_result_free(os_run_result_t *r);
|
||||||
|
|
||||||
|
/* Run argv[0..] (NULL-terminated) with cwd; capture stdout+stderr.
|
||||||
|
* max_out caps captured bytes (0 = default 512KiB). */
|
||||||
|
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
||||||
|
os_run_result_t *r);
|
||||||
|
|
||||||
|
/* Convenience: shell -c cmd via os_resolve_shell. */
|
||||||
|
int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out,
|
||||||
|
os_run_result_t *r);
|
||||||
|
|
||||||
|
/* --- misc --- */
|
||||||
|
void os_localtime(const time_t *t, struct tm *out);
|
||||||
|
int os_getpid(void);
|
||||||
|
int os_chmod_private(const char *path); /* best-effort 0600; no-op/ok on Win */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* CST_OS_COMPAT_H */
|
||||||
Executable
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# One-shot: clone (optional) + build CST-Agent on Linux.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_URL="${CST_AGENT_REPO:-https://git.whcself.xyz/WHC/CST-agent.git}"
|
||||||
|
DIR="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$DIR" ]]; then
|
||||||
|
if [[ -f Makefile && -d src ]]; then
|
||||||
|
DIR="$(pwd)"
|
||||||
|
else
|
||||||
|
DIR="CST-agent"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$DIR/Makefile" ]]; then
|
||||||
|
echo "==> git clone $REPO_URL -> $DIR"
|
||||||
|
git clone "$REPO_URL" "$DIR"
|
||||||
|
fi
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
echo "==> deps hint: gcc/clang make cmake libcurl ncursesw (zenity optional)"
|
||||||
|
if command -v pacman >/dev/null 2>&1; then
|
||||||
|
echo " Arch: sudo pacman -S --needed base-devel cmake curl ncurses"
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
echo " Debian: sudo apt install build-essential cmake libcurl4-openssl-dev libncurses-dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> make"
|
||||||
|
make -j"$(nproc 2>/dev/null || echo 4)"
|
||||||
|
make check || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "OK: ./cst-agent"
|
||||||
|
echo "OpenAI-compatible env (see docs/env_openai.md):"
|
||||||
|
echo " export OPENAI_API_KEY=sk-..."
|
||||||
|
echo " export OPENAI_BASE_URL=https://api.openai.com/v1"
|
||||||
|
echo " export OPENAI_MODEL=gpt-4.1-mini"
|
||||||
|
echo " ./cst-agent --workspace ."
|
||||||
|
echo "Or MiniMax default: export CST_API_KEY=... && ./cst-agent --workspace ."
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/data/data/com.termux/files/usr/bin/bash
|
||||||
|
# One-shot: clone + build CST-Agent on Android Termux.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_URL="${CST_AGENT_REPO:-https://git.whcself.xyz/WHC/CST-agent.git}"
|
||||||
|
DIR="${1:-CST-agent}"
|
||||||
|
|
||||||
|
echo "==> Termux packages"
|
||||||
|
pkg install -y git clang make cmake libcurl ncurses-ui-libs 2>/dev/null \
|
||||||
|
|| pkg install -y git clang make cmake curl ncurses
|
||||||
|
|
||||||
|
if [[ ! -f "$DIR/Makefile" ]]; then
|
||||||
|
echo "==> git clone $REPO_URL -> $DIR"
|
||||||
|
git clone "$REPO_URL" "$DIR"
|
||||||
|
fi
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
echo "==> cmake build (TUI)"
|
||||||
|
cmake -B build -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON
|
||||||
|
cmake --build build -j"$(nproc 2>/dev/null || echo 4)"
|
||||||
|
# Also keep Makefile binary path convenience
|
||||||
|
cp -f build/cst-agent ./cst-agent 2>/dev/null || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "OK: ./cst-agent (or ./build/cst-agent)"
|
||||||
|
echo "OpenAI-compatible:"
|
||||||
|
echo " export OPENAI_API_KEY=sk-..."
|
||||||
|
echo " export OPENAI_BASE_URL=https://api.openai.com/v1"
|
||||||
|
echo " export OPENAI_MODEL=gpt-4.1-mini"
|
||||||
|
echo " ./cst-agent --workspace \$HOME"
|
||||||
|
echo "Docs: docs/env_openai.md"
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# One-shot on a Linux host: cross-build Windows x86_64 release (MinGW).
|
||||||
|
# Output: dist/windows-x86_64/ — copy that folder to a Windows machine.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_URL="${CST_AGENT_REPO:-https://git.whcself.xyz/WHC/CST-agent.git}"
|
||||||
|
DIR="${1:-}"
|
||||||
|
|
||||||
|
if [[ -z "$DIR" ]]; then
|
||||||
|
if [[ -f CMakeLists.txt && -d src ]]; then
|
||||||
|
DIR="$(pwd)"
|
||||||
|
else
|
||||||
|
DIR="CST-agent"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$DIR/CMakeLists.txt" ]]; then
|
||||||
|
echo "==> git clone $REPO_URL -> $DIR"
|
||||||
|
git clone "$REPO_URL" "$DIR"
|
||||||
|
fi
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
need() { command -v "$1" >/dev/null || { echo "missing $1 (install mingw-w64 gcc cmake git)" >&2; exit 1; }; }
|
||||||
|
need git; need cmake; need x86_64-w64-mingw32-gcc
|
||||||
|
|
||||||
|
echo "==> third-party (curl Schannel + PDCurses + busybox-w32)"
|
||||||
|
chmod +x scripts/*.sh 2>/dev/null || true
|
||||||
|
./scripts/build_third_party_mingw.sh all
|
||||||
|
|
||||||
|
echo "==> cmake Windows TUI"
|
||||||
|
cmake -B build-win-tui \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake \
|
||||||
|
-DCST_BUILD_TUI=ON \
|
||||||
|
-DCMAKE_PREFIX_PATH="$PWD/third_party/prefix-mingw" \
|
||||||
|
-DPDCurses_ROOT="$PWD/third_party/prefix-mingw"
|
||||||
|
cmake --build build-win-tui -j"$(nproc 2>/dev/null || echo 4)"
|
||||||
|
|
||||||
|
echo "==> package"
|
||||||
|
./scripts/package_windows.sh
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "OK: dist/windows-x86_64/"
|
||||||
|
echo "Copy that folder to Windows, then:"
|
||||||
|
echo " set OPENAI_API_KEY=sk-..."
|
||||||
|
echo " set OPENAI_BASE_URL=https://api.openai.com/v1"
|
||||||
|
echo " set OPENAI_MODEL=gpt-4.1-mini"
|
||||||
|
echo " cst-agent.exe --workspace ."
|
||||||
|
echo "See docs/env_openai.md and docs/README-windows.md"
|
||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build third-party deps for Windows MinGW cross + optional Unix busybox.
|
||||||
|
# Sources:
|
||||||
|
# busybox-w32: https://github.com/rmyorston/busybox-w32.git
|
||||||
|
# busybox: https://github.com/mirror/busybox.git
|
||||||
|
# curl: https://github.com/curl/curl.git (Windows via Schannel)
|
||||||
|
# PDCurses: https://github.com/wmcbrine/PDCurses.git
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
TP="$ROOT/third_party"
|
||||||
|
HOST="${MINGW_HOST:-x86_64-w64-mingw32}"
|
||||||
|
JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
|
||||||
|
mkdir -p "$TP"
|
||||||
|
|
||||||
|
need_cmd() { command -v "$1" >/dev/null || { echo "missing: $1" >&2; exit 1; }; }
|
||||||
|
need_cmd git
|
||||||
|
need_cmd make
|
||||||
|
need_cmd "${HOST}-gcc"
|
||||||
|
|
||||||
|
clone_or_update() {
|
||||||
|
local url="$1" dir="$2"
|
||||||
|
if [[ -d "$dir/.git" ]]; then
|
||||||
|
git -C "$dir" fetch --depth 1 origin 2>/dev/null || true
|
||||||
|
git -C "$dir" pull --ff-only 2>/dev/null || true
|
||||||
|
else
|
||||||
|
git clone --depth 1 "$url" "$dir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- curl (static, Schannel) ---
|
||||||
|
build_curl_mingw() {
|
||||||
|
clone_or_update https://github.com/curl/curl.git "$TP/curl"
|
||||||
|
cd "$TP/curl"
|
||||||
|
if [[ ! -f configure ]]; then
|
||||||
|
autoreconf -fi 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Prefer cmake for curl modern tree
|
||||||
|
rm -rf build-mingw
|
||||||
|
cmake -B build-mingw -G "Unix Makefiles" \
|
||||||
|
-DCMAKE_SYSTEM_NAME=Windows \
|
||||||
|
-DCMAKE_C_COMPILER="${HOST}-gcc" \
|
||||||
|
-DCMAKE_RC_COMPILER="${HOST}-windres" \
|
||||||
|
-DCMAKE_INSTALL_PREFIX="$TP/prefix-mingw" \
|
||||||
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DCURL_USE_SCHANNEL=ON \
|
||||||
|
-DCURL_USE_OPENSSL=OFF \
|
||||||
|
-DCURL_DISABLE_LDAP=ON \
|
||||||
|
-DCURL_USE_LIBPSL=OFF \
|
||||||
|
-DCURL_ZLIB=OFF \
|
||||||
|
-DCURL_BROTLI=OFF \
|
||||||
|
-DCURL_ZSTD=OFF \
|
||||||
|
-DUSE_NGHTTP2=OFF \
|
||||||
|
-DBUILD_CURL_EXE=OFF \
|
||||||
|
-DBUILD_TESTING=OFF \
|
||||||
|
-DHTTP_ONLY=OFF
|
||||||
|
cmake --build build-mingw -j"$JOBS"
|
||||||
|
cmake --install build-mingw
|
||||||
|
echo "curl installed -> $TP/prefix-mingw"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- PDCurses ---
|
||||||
|
build_pdcurses_mingw() {
|
||||||
|
clone_or_update https://github.com/wmcbrine/PDCurses.git "$TP/PDCurses"
|
||||||
|
cd "$TP/PDCurses/wincon"
|
||||||
|
make -f Makefile clean 2>/dev/null || true
|
||||||
|
make -f Makefile \
|
||||||
|
CC="${HOST}-gcc" \
|
||||||
|
LINK="${HOST}-gcc" \
|
||||||
|
CFLAGS="-O2 -I.. -DPDC_WIDE -DPDC_FORCE_UTF8" \
|
||||||
|
LIBEXE="${HOST}-ar" \
|
||||||
|
LIBFLAGS=rcv \
|
||||||
|
pdcurses.a
|
||||||
|
mkdir -p "$TP/prefix-mingw/include" "$TP/prefix-mingw/lib"
|
||||||
|
cp -f ../curses.h "$TP/prefix-mingw/include/"
|
||||||
|
[[ -f ../panel.h ]] && cp -f ../panel.h "$TP/prefix-mingw/include/" || true
|
||||||
|
cp -f pdcurses.a "$TP/prefix-mingw/lib/libpdcurses.a"
|
||||||
|
# Also install as curses for find_library
|
||||||
|
cp -f pdcurses.a "$TP/prefix-mingw/lib/libcurses.a"
|
||||||
|
echo "PDCurses -> $TP/prefix-mingw"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- busybox-w32 ---
|
||||||
|
build_busybox_w32() {
|
||||||
|
clone_or_update https://github.com/rmyorston/busybox-w32.git "$TP/busybox-w32"
|
||||||
|
cd "$TP/busybox-w32"
|
||||||
|
# configs: try mingw64 defconfig
|
||||||
|
if [[ -f configs/mingw64_defconfig ]]; then
|
||||||
|
make mingw64_defconfig
|
||||||
|
elif [[ -f configs/mingw_defconfig ]]; then
|
||||||
|
make mingw_defconfig
|
||||||
|
else
|
||||||
|
make defconfig
|
||||||
|
fi
|
||||||
|
# Cross compile
|
||||||
|
make -j"$JOBS" CROSS_COMPILE="${HOST}-" \
|
||||||
|
CONFIG_EXTRA_CFLAGS="-O2" \
|
||||||
|
busybox.exe 2>&1 || make -j"$JOBS" CROSS_COMPILE="${HOST}-"
|
||||||
|
mkdir -p "$TP/busybox"
|
||||||
|
if [[ -f busybox.exe ]]; then
|
||||||
|
cp -f busybox.exe "$TP/busybox/busybox.exe"
|
||||||
|
else
|
||||||
|
find . -name 'busybox.exe' | head -1 | xargs -I{} cp -f {} "$TP/busybox/busybox.exe"
|
||||||
|
fi
|
||||||
|
cat > "$TP/busybox/LICENSE.busybox" << 'EOF'
|
||||||
|
BusyBox is GPLv2. Source: https://github.com/rmyorston/busybox-w32
|
||||||
|
Upstream BusyBox: https://github.com/mirror/busybox
|
||||||
|
EOF
|
||||||
|
ls -la "$TP/busybox/busybox.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Unix busybox (optional, for Linux/Termux side-by-side bundle) ---
|
||||||
|
build_busybox_unix() {
|
||||||
|
clone_or_update https://github.com/mirror/busybox.git "$TP/busybox-src"
|
||||||
|
cd "$TP/busybox-src"
|
||||||
|
make defconfig
|
||||||
|
# Keep applets reasonable; enable ash + diff
|
||||||
|
make -j"$JOBS" busybox
|
||||||
|
mkdir -p "$TP/busybox"
|
||||||
|
cp -f busybox "$TP/busybox/busybox"
|
||||||
|
echo "unix busybox -> $TP/busybox/busybox"
|
||||||
|
}
|
||||||
|
|
||||||
|
TARGETS="${1:-all}"
|
||||||
|
case "$TARGETS" in
|
||||||
|
curl) build_curl_mingw ;;
|
||||||
|
pdcurses) build_pdcurses_mingw ;;
|
||||||
|
busybox-w32) build_busybox_w32 ;;
|
||||||
|
busybox-unix) build_busybox_unix ;;
|
||||||
|
all)
|
||||||
|
build_curl_mingw
|
||||||
|
build_pdcurses_mingw
|
||||||
|
build_busybox_w32
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "usage: $0 [all|curl|pdcurses|busybox-w32|busybox-unix]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "DONE"
|
||||||
Executable
+114
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# E2E: copy mini_calc → temp workspace → optional cst-agent headless → make test.
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/e2e_mini_calc.sh # run agent if credentials exist
|
||||||
|
# ./scripts/e2e_mini_calc.sh --baseline-only # only prove fixture starts RED then skip agent
|
||||||
|
# SKIP_AGENT=1 ./scripts/e2e_mini_calc.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
FIX="$ROOT/fixtures/mini_calc"
|
||||||
|
OUT="$FIX/out"
|
||||||
|
BASELINE_ONLY=0
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in
|
||||||
|
--baseline-only) BASELINE_ONLY=1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
WORKDIR="$(mktemp -d /tmp/cst-mini-calc.XXXXXX)"
|
||||||
|
RESULT="$OUT/result-$STAMP.txt"
|
||||||
|
SESSION_COPY="$OUT/session-$STAMP.jsonl"
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$WORKDIR"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "== mini_calc e2e ==" | tee "$RESULT"
|
||||||
|
echo "workdir=$WORKDIR" | tee -a "$RESULT"
|
||||||
|
echo "stamp=$STAMP" | tee -a "$RESULT"
|
||||||
|
|
||||||
|
# Fresh copy (no .agent from previous runs)
|
||||||
|
cp -a "$FIX/." "$WORKDIR/"
|
||||||
|
rm -rf "$WORKDIR/out" "$WORKDIR/.agent"
|
||||||
|
chmod +x "$WORKDIR/tests/smoke.sh"
|
||||||
|
|
||||||
|
cd "$WORKDIR"
|
||||||
|
make clean >/dev/null 2>&1 || true
|
||||||
|
make
|
||||||
|
|
||||||
|
echo "-- baseline make test (expect FAIL before agent) --" | tee -a "$RESULT"
|
||||||
|
set +e
|
||||||
|
make test >"$OUT/baseline-$STAMP.log" 2>&1
|
||||||
|
base_rc=$?
|
||||||
|
set -e
|
||||||
|
echo "baseline_rc=$base_rc" | tee -a "$RESULT"
|
||||||
|
if [[ "$base_rc" -eq 0 ]]; then
|
||||||
|
echo "UNEXPECTED: fixture already passes; bugs may have been fixed in source." | tee -a "$RESULT"
|
||||||
|
echo "outcome=unexpected_green_fixture" | tee -a "$RESULT"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
echo "baseline: RED (as expected)" | tee -a "$RESULT"
|
||||||
|
|
||||||
|
if [[ "$BASELINE_ONLY" -eq 1 || "${SKIP_AGENT:-0}" == "1" ]]; then
|
||||||
|
echo "outcome=baseline_ok_agent_skipped" | tee -a "$RESULT"
|
||||||
|
echo "SKIP agent step (--baseline-only or SKIP_AGENT=1)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Need API key for agent step
|
||||||
|
has_key=0
|
||||||
|
if [[ -n "${CST_API_KEY:-}" || -n "${MINIMAX_API_KEY:-}" ]]; then
|
||||||
|
has_key=1
|
||||||
|
elif [[ -f "$HOME/.config/cst-agent/credentials" ]]; then
|
||||||
|
has_key=1
|
||||||
|
fi
|
||||||
|
if [[ "$has_key" -eq 0 ]]; then
|
||||||
|
echo "outcome=baseline_ok_no_api_key" | tee -a "$RESULT"
|
||||||
|
echo "No API key; agent step skipped. Set CST_API_KEY or save credentials."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
make -s
|
||||||
|
|
||||||
|
mkdir -p "$WORKDIR/.agent"
|
||||||
|
PROMPT="$(cat "$FIX/TASK.md")"
|
||||||
|
export CST_AUTO_APPROVE=1
|
||||||
|
echo "-- cst-agent headless --" | tee -a "$RESULT"
|
||||||
|
set +e
|
||||||
|
"$ROOT/cst-agent" --headless --workspace "$WORKDIR" --session "$WORKDIR/.agent/e2e.jsonl" \
|
||||||
|
--prompt "$PROMPT" >"$OUT/agent-$STAMP.log" 2>&1
|
||||||
|
agent_rc=$?
|
||||||
|
set -e
|
||||||
|
echo "agent_rc=$agent_rc" | tee -a "$RESULT"
|
||||||
|
if [[ -f "$WORKDIR/.agent/e2e.jsonl" ]]; then
|
||||||
|
cp -f "$WORKDIR/.agent/e2e.jsonl" "$SESSION_COPY"
|
||||||
|
echo "session_copy=$SESSION_COPY" | tee -a "$RESULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$WORKDIR"
|
||||||
|
set +e
|
||||||
|
make test >"$OUT/after-$STAMP.log" 2>&1
|
||||||
|
test_rc=$?
|
||||||
|
set -e
|
||||||
|
echo "after_test_rc=$test_rc" | tee -a "$RESULT"
|
||||||
|
|
||||||
|
# Classify
|
||||||
|
changed=0
|
||||||
|
if ! diff -q "$FIX/calc.c" "$WORKDIR/calc.c" >/dev/null 2>&1; then
|
||||||
|
changed=1
|
||||||
|
fi
|
||||||
|
echo "calc_c_changed=$changed" | tee -a "$RESULT"
|
||||||
|
|
||||||
|
if [[ "$test_rc" -eq 0 ]]; then
|
||||||
|
echo "outcome=pass" | tee -a "$RESULT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "$changed" -eq 1 ]]; then
|
||||||
|
echo "outcome=partial" | tee -a "$RESULT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "outcome=fail" | tee -a "$RESULT"
|
||||||
|
exit 1
|
||||||
Executable
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fetch busybox-w32 into third_party/busybox/busybox.exe (Linux/CI host).
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
DEST="$ROOT/third_party/busybox"
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
OUT="$DEST/busybox.exe"
|
||||||
|
|
||||||
|
# Override with BUSYBOX_W32_URL if the default 404s / TLS fails.
|
||||||
|
URLS=(
|
||||||
|
"${BUSYBOX_W32_URL:-}"
|
||||||
|
"https://frippery.org/files/busybox/busybox-w64-FRP-5007-g82accfc19.exe"
|
||||||
|
"https://frippery.org/files/busybox/busybox.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
ok=0
|
||||||
|
for URL in "${URLS[@]}"; do
|
||||||
|
[[ -z "$URL" ]] && continue
|
||||||
|
echo "Downloading $URL -> $OUT"
|
||||||
|
if curl -fsSL -L --connect-timeout 20 -o "$OUT" "$URL"; then
|
||||||
|
ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "failed: $URL" >&2
|
||||||
|
done
|
||||||
|
|
||||||
|
cat > "$DEST/LICENSE.busybox" << 'EOF'
|
||||||
|
BusyBox is licensed under GPLv2. Obtain the corresponding source from the
|
||||||
|
upstream busybox-w32 / BusyBox project when redistributing the binary.
|
||||||
|
See: https://www.busybox.net/ https://frippery.org/busybox/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ "$ok" -ne 1 ]]; then
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Could not download busybox-w32 automatically.
|
||||||
|
Manual steps:
|
||||||
|
1. Visit https://frippery.org/busybox/ or busybox-w32 releases
|
||||||
|
2. Save the 64-bit binary as agent/third_party/busybox/busybox.exe
|
||||||
|
3. Copy it next to cst-agent.exe on Windows releases
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x "$OUT" || true
|
||||||
|
file "$OUT" || true
|
||||||
|
ls -la "$OUT"
|
||||||
|
echo "OK: place this next to cst-agent.exe on Windows releases."
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Assemble dist/windows-x86_64 from MinGW TUI build + busybox.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
REL="$ROOT/dist/windows-x86_64"
|
||||||
|
EXE="$ROOT/build-win-tui/cst-agent.exe"
|
||||||
|
[[ -f "$EXE" ]] || EXE="$ROOT/build-win/cst-agent.exe"
|
||||||
|
BB="$ROOT/third_party/busybox/busybox.exe"
|
||||||
|
DLL="/usr/x86_64-w64-mingw32/bin/libwinpthread-1.dll"
|
||||||
|
|
||||||
|
mkdir -p "$REL"
|
||||||
|
cp -f "$EXE" "$REL/cst-agent.exe"
|
||||||
|
cp -f "$BB" "$REL/busybox.exe"
|
||||||
|
cp -f "$ROOT/third_party/busybox/LICENSE.busybox" "$REL/"
|
||||||
|
cp -f "$ROOT/docs/README-windows.md" "$REL/"
|
||||||
|
[[ -f "$DLL" ]] && cp -f "$DLL" "$REL/"
|
||||||
|
ls -la "$REL"
|
||||||
|
file "$REL/cst-agent.exe"
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Replay starvation experiment (CTL vs JSON-buffer).
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
make check
|
||||||
|
echo
|
||||||
|
echo "See docs/theory_tool_cst.md for the claim under test."
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
@echo off
|
||||||
|
REM Run ON Windows after copying dist/windows-x86_64 (or a full git checkout + MSYS2 build).
|
||||||
|
REM This script only documents env + launch; prefer bootstrap_windows.sh on Linux for cross-build.
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
if "%OPENAI_API_KEY%"=="" if "%CST_API_KEY%"=="" (
|
||||||
|
echo Set OPENAI_API_KEY or CST_API_KEY first. See docs\env_openai.md
|
||||||
|
echo Example:
|
||||||
|
echo set OPENAI_API_KEY=sk-...
|
||||||
|
echo set OPENAI_BASE_URL=https://api.openai.com/v1
|
||||||
|
echo set OPENAI_MODEL=gpt-4.1-mini
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist cst-agent.exe (
|
||||||
|
echo cst-agent.exe not found in %CD%
|
||||||
|
echo Copy dist\windows-x86_64\* here, or build with MSYS2/MinGW.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist busybox.exe (
|
||||||
|
echo WARNING: busybox.exe missing — run_shell may fail. Place busybox.exe beside cst-agent.exe
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Launching CST-Agent...
|
||||||
|
cst-agent.exe --workspace .
|
||||||
|
endlocal
|
||||||
+1182
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
|
||||||
|
#include "cst_agent.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <io.h>
|
||||||
|
#define OS_ACCESS _access
|
||||||
|
#define OS_F_OK 0
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#define OS_ACCESS access
|
||||||
|
#define OS_F_OK F_OK
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int cst_agent_store_dir(const char *workspace, char *out, size_t out_sz) {
|
||||||
|
if (!workspace || !out || out_sz < 16)
|
||||||
|
return -1;
|
||||||
|
return os_path_join(out, out_sz, workspace, ".agent/sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_agent_store_ensure(const char *workspace) {
|
||||||
|
char base[OS_PATH_MAX];
|
||||||
|
char sess[OS_PATH_MAX];
|
||||||
|
if (cst_agent_store_dir(workspace, sess, sizeof(sess)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(base, sizeof(base), workspace, ".agent") != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_mkdir(base, 0755) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_mkdir(sess, 0755) != 0)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void make_id(char *out, size_t n) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
struct tm tm;
|
||||||
|
os_localtime(&now, &tm);
|
||||||
|
unsigned r = (unsigned)(os_getpid() ^ (unsigned)now);
|
||||||
|
snprintf(out, n, "%04d%02d%02d-%02d%02d%02d-%04x", tm.tm_year + 1900,
|
||||||
|
tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec,
|
||||||
|
r & 0xffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_agent_store_new_id(char *id_out, size_t id_sz) {
|
||||||
|
if (!id_out || id_sz < 24)
|
||||||
|
return -1;
|
||||||
|
make_id(id_out, id_sz);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_agent_store_path_for_id(const char *workspace, const char *id,
|
||||||
|
char *out, size_t out_sz) {
|
||||||
|
if (!workspace || !id || !out)
|
||||||
|
return -1;
|
||||||
|
/* Accept bare id or full path. */
|
||||||
|
if (strchr(id, '/') || strchr(id, '\\')) {
|
||||||
|
snprintf(out, out_sz, "%s", id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
char leaf[160];
|
||||||
|
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
|
||||||
|
return -1;
|
||||||
|
snprintf(leaf, sizeof(leaf), "%s.jsonl", id);
|
||||||
|
return os_path_join(out, out_sz, dir, leaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_agent_store_set_current(const char *workspace, const char *id) {
|
||||||
|
if (cst_agent_store_ensure(workspace) != 0)
|
||||||
|
return -1;
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
char path[OS_PATH_MAX];
|
||||||
|
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0)
|
||||||
|
return -1;
|
||||||
|
FILE *fp = fopen(path, "w");
|
||||||
|
if (!fp)
|
||||||
|
return -1;
|
||||||
|
fprintf(fp, "%s\n", id);
|
||||||
|
fclose(fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_agent_store_get_current(const char *workspace, char *id_out,
|
||||||
|
size_t id_sz) {
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
char path[OS_PATH_MAX];
|
||||||
|
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0)
|
||||||
|
return -1;
|
||||||
|
FILE *fp = fopen(path, "r");
|
||||||
|
if (!fp)
|
||||||
|
return -1;
|
||||||
|
if (!fgets(id_out, (int)id_sz, fp)) {
|
||||||
|
fclose(fp);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
size_t n = strlen(id_out);
|
||||||
|
while (n > 0 && (id_out[n - 1] == '\n' || id_out[n - 1] == '\r'))
|
||||||
|
id_out[--n] = 0;
|
||||||
|
return id_out[0] ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_agent_store_resolve_default(const char *workspace, char *path_out,
|
||||||
|
size_t path_sz, char *id_out,
|
||||||
|
size_t id_sz) {
|
||||||
|
if (cst_agent_store_ensure(workspace) != 0)
|
||||||
|
return -1;
|
||||||
|
char id[128];
|
||||||
|
if (cst_agent_store_get_current(workspace, id, sizeof(id)) == 0) {
|
||||||
|
char p[OS_PATH_MAX];
|
||||||
|
if (cst_agent_store_path_for_id(workspace, id, p, sizeof(p)) == 0 &&
|
||||||
|
OS_ACCESS(p, OS_F_OK) == 0) {
|
||||||
|
snprintf(path_out, path_sz, "%s", p);
|
||||||
|
if (id_out && id_sz)
|
||||||
|
snprintf(id_out, id_sz, "%s", id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cst_agent_store_new_id(id, sizeof(id)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (cst_agent_store_path_for_id(workspace, id, path_out, path_sz) != 0)
|
||||||
|
return -1;
|
||||||
|
cst_agent_store_set_current(workspace, id);
|
||||||
|
if (id_out && id_sz)
|
||||||
|
snprintf(id_out, id_sz, "%s", id);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids) {
|
||||||
|
if (!workspace || !ids || max_ids <= 0)
|
||||||
|
return 0;
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
|
||||||
|
return 0;
|
||||||
|
DIR *d = opendir(dir);
|
||||||
|
if (!d)
|
||||||
|
return 0;
|
||||||
|
typedef struct {
|
||||||
|
char id[128];
|
||||||
|
time_t mtime;
|
||||||
|
} ent_t;
|
||||||
|
ent_t *ents = calloc((size_t)max_ids * 4, sizeof(ent_t));
|
||||||
|
int n = 0, cap = max_ids * 4;
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(d)) != NULL) {
|
||||||
|
size_t len = strlen(de->d_name);
|
||||||
|
if (len < 7 || strcmp(de->d_name + len - 6, ".jsonl") != 0)
|
||||||
|
continue;
|
||||||
|
if (n >= cap)
|
||||||
|
break;
|
||||||
|
snprintf(ents[n].id, sizeof(ents[n].id), "%.*s", (int)(len - 6),
|
||||||
|
de->d_name);
|
||||||
|
char full[OS_PATH_MAX];
|
||||||
|
if (os_path_join(full, sizeof(full), dir, de->d_name) != 0)
|
||||||
|
continue;
|
||||||
|
struct stat st;
|
||||||
|
ents[n].mtime = (stat(full, &st) == 0) ? st.st_mtime : 0;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
for (int j = i + 1; j < n; j++) {
|
||||||
|
if (ents[j].mtime > ents[i].mtime) {
|
||||||
|
ent_t tmp = ents[i];
|
||||||
|
ents[i] = ents[j];
|
||||||
|
ents[j] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int out_n = n < max_ids ? n : max_ids;
|
||||||
|
for (int i = 0; i < out_n; i++)
|
||||||
|
snprintf(ids[i], 128, "%s", ents[i].id);
|
||||||
|
free(ents);
|
||||||
|
return out_n;
|
||||||
|
}
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
|
||||||
|
#include "cst_agent.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <io.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Config dir via os_compat: ~/.config/cst-agent or %APPDATA%\cst-agent */
|
||||||
|
|
||||||
|
static int credentials_path(char *out, size_t n) {
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
if (os_config_dir(dir, sizeof(dir)) != 0)
|
||||||
|
return -1;
|
||||||
|
return os_path_join(out, n, dir, "credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_creds_load_api_key(char *out, size_t out_sz) {
|
||||||
|
if (!out || out_sz < 2)
|
||||||
|
return -1;
|
||||||
|
out[0] = 0;
|
||||||
|
char path[OS_PATH_MAX];
|
||||||
|
if (credentials_path(path, sizeof(path)) != 0)
|
||||||
|
return -1;
|
||||||
|
FILE *fp = fopen(path, "r");
|
||||||
|
if (!fp)
|
||||||
|
return -1;
|
||||||
|
char line[1024];
|
||||||
|
while (fgets(line, sizeof(line), fp)) {
|
||||||
|
if (strncmp(line, "api_key=", 8) != 0)
|
||||||
|
continue;
|
||||||
|
char *v = line + 8;
|
||||||
|
size_t len = strlen(v);
|
||||||
|
while (len > 0 && (v[len - 1] == '\n' || v[len - 1] == '\r'))
|
||||||
|
v[--len] = 0;
|
||||||
|
if (len == 0 || len + 1 > out_sz) {
|
||||||
|
fclose(fp);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(out, v, len + 1);
|
||||||
|
fclose(fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_creds_save_api_key(const char *key) {
|
||||||
|
if (!key || !key[0])
|
||||||
|
return -1;
|
||||||
|
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
|
||||||
|
if (os_config_dir(dir, sizeof(dir)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (os_mkdir(dir, 0700) != 0)
|
||||||
|
return -1;
|
||||||
|
if (credentials_path(path, sizeof(path)) != 0)
|
||||||
|
return -1;
|
||||||
|
FILE *fp = fopen(path, "w");
|
||||||
|
if (!fp)
|
||||||
|
return -1;
|
||||||
|
fprintf(fp, "api_key=%s\n", key);
|
||||||
|
fprintf(fp, "api_base=https://api.minimax.cn/v1\n");
|
||||||
|
fprintf(fp, "model=MiniMax-M2.7\n");
|
||||||
|
fclose(fp);
|
||||||
|
(void)os_chmod_private(path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zenity on Unix; stdin prompt elsewhere / when zenity missing. */
|
||||||
|
int cst_creds_prompt_api_key(char *out, size_t out_sz) {
|
||||||
|
if (!out || out_sz < 2)
|
||||||
|
return -1;
|
||||||
|
out[0] = 0;
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
FILE *fp = popen(
|
||||||
|
"zenity --password --title='CST-Agent / MiniMax' "
|
||||||
|
"--text='请输入 MiniMax API Key(将保存到配置目录 credentials)' "
|
||||||
|
"2>/dev/null",
|
||||||
|
"r");
|
||||||
|
if (fp) {
|
||||||
|
if (fgets(out, (int)out_sz, fp)) {
|
||||||
|
int st = pclose(fp);
|
||||||
|
size_t len = strlen(out);
|
||||||
|
while (len > 0 && (out[len - 1] == '\n' || out[len - 1] == '\r'))
|
||||||
|
out[--len] = 0;
|
||||||
|
if (st == 0 && len > 0)
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
pclose(fp);
|
||||||
|
}
|
||||||
|
out[0] = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Fallback: console prompt (headless / Windows / no zenity). */
|
||||||
|
fprintf(stderr, "CST-Agent: enter MiniMax API Key (saved to config dir): ");
|
||||||
|
fflush(stderr);
|
||||||
|
if (!fgets(out, (int)out_sz, stdin)) {
|
||||||
|
out[0] = 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
size_t len = strlen(out);
|
||||||
|
while (len > 0 && (out[len - 1] == '\n' || out[len - 1] == '\r'))
|
||||||
|
out[--len] = 0;
|
||||||
|
if (len == 0) {
|
||||||
|
out[0] = 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_creds_ensure_api_key(char *out, size_t out_sz) {
|
||||||
|
const char *env = getenv("CST_API_KEY");
|
||||||
|
if (!env || !env[0])
|
||||||
|
env = getenv("MINIMAX_API_KEY");
|
||||||
|
if (!env || !env[0])
|
||||||
|
env = getenv("OPENAI_API_KEY");
|
||||||
|
if (env && env[0]) {
|
||||||
|
snprintf(out, out_sz, "%s", env);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (cst_creds_load_api_key(out, out_sz) == 0 && out[0])
|
||||||
|
return 0;
|
||||||
|
if (cst_creds_prompt_api_key(out, out_sz) != 0)
|
||||||
|
return -1;
|
||||||
|
if (cst_creds_save_api_key(out) != 0)
|
||||||
|
fprintf(stderr, "warning: failed to save credentials file\n");
|
||||||
|
else
|
||||||
|
fprintf(stderr, "saved API key to config directory\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+419
@@ -0,0 +1,419 @@
|
|||||||
|
#include "ctl.h"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ST_TEXT = 0, /* outside a call; accumulate text lines */
|
||||||
|
ST_LINE, /* collecting a line (call/arg/end or text) */
|
||||||
|
ST_ARG_BODY, /* reading nbytes of arg body */
|
||||||
|
ST_ERROR
|
||||||
|
} ctl_state_t;
|
||||||
|
|
||||||
|
struct ctl_parser {
|
||||||
|
ctl_on_event_fn on_event;
|
||||||
|
void *userdata;
|
||||||
|
ctl_state_t state;
|
||||||
|
char line[CTL_MAX_LINE];
|
||||||
|
size_t line_len;
|
||||||
|
ctl_tool_call_t call;
|
||||||
|
int in_call;
|
||||||
|
/* current arg being filled */
|
||||||
|
char arg_key[CTL_MAX_KEY];
|
||||||
|
char *arg_buf;
|
||||||
|
size_t arg_cap;
|
||||||
|
size_t arg_got;
|
||||||
|
size_t arg_need;
|
||||||
|
size_t pending;
|
||||||
|
size_t pending_peak;
|
||||||
|
char errbuf[256];
|
||||||
|
};
|
||||||
|
|
||||||
|
static void bump_pending(ctl_parser_t *p, size_t n) {
|
||||||
|
p->pending = n;
|
||||||
|
if (n > p->pending_peak)
|
||||||
|
p->pending_peak = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit(ctl_parser_t *p, ctl_event_t *ev) {
|
||||||
|
if (p->on_event)
|
||||||
|
p->on_event(ev, p->userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fail(ctl_parser_t *p, const char *msg) {
|
||||||
|
snprintf(p->errbuf, sizeof(p->errbuf), "%s", msg);
|
||||||
|
p->state = ST_ERROR;
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_ERROR;
|
||||||
|
ev.error = p->errbuf;
|
||||||
|
emit(p, &ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* free args in current call */
|
||||||
|
static void clear_call_args(ctl_tool_call_t *c) {
|
||||||
|
for (int i = 0; i < c->nargs; i++) {
|
||||||
|
free(c->args[i].value);
|
||||||
|
c->args[i].value = NULL;
|
||||||
|
c->args[i].value_len = 0;
|
||||||
|
}
|
||||||
|
c->nargs = 0;
|
||||||
|
c->id[0] = 0;
|
||||||
|
c->name[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reset_arg_body(ctl_parser_t *p) {
|
||||||
|
free(p->arg_buf);
|
||||||
|
p->arg_buf = NULL;
|
||||||
|
p->arg_cap = 0;
|
||||||
|
p->arg_got = 0;
|
||||||
|
p->arg_need = 0;
|
||||||
|
p->arg_key[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl_parser_t *ctl_parser_create(ctl_on_event_fn on_event, void *userdata) {
|
||||||
|
ctl_parser_t *p = calloc(1, sizeof(*p));
|
||||||
|
if (!p)
|
||||||
|
return NULL;
|
||||||
|
p->on_event = on_event;
|
||||||
|
p->userdata = userdata;
|
||||||
|
p->state = ST_TEXT;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ctl_parser_destroy(ctl_parser_t *p) {
|
||||||
|
if (!p)
|
||||||
|
return;
|
||||||
|
clear_call_args(&p->call);
|
||||||
|
reset_arg_body(p);
|
||||||
|
free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ctl_parser_reset(ctl_parser_t *p) {
|
||||||
|
if (!p)
|
||||||
|
return;
|
||||||
|
clear_call_args(&p->call);
|
||||||
|
reset_arg_body(p);
|
||||||
|
p->state = ST_TEXT;
|
||||||
|
p->line_len = 0;
|
||||||
|
p->in_call = 0;
|
||||||
|
p->pending = 0;
|
||||||
|
/* keep pending_peak for experiment */
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ctl_parser_pending(const ctl_parser_t *p) {
|
||||||
|
return p ? p->pending : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t ctl_parser_pending_peak(const ctl_parser_t *p) {
|
||||||
|
return p ? p->pending_peak : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int starts_with(const char *s, const char *pfx) {
|
||||||
|
return strncmp(s, pfx, strlen(pfx)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int parse_call_line(ctl_parser_t *p, const char *line) {
|
||||||
|
/* call <id> <name> */
|
||||||
|
const char *s = line + 5;
|
||||||
|
while (*s == ' ' || *s == '\t')
|
||||||
|
s++;
|
||||||
|
if (!*s)
|
||||||
|
return -1;
|
||||||
|
const char *sp = strchr(s, ' ');
|
||||||
|
if (!sp)
|
||||||
|
return -1;
|
||||||
|
size_t idlen = (size_t)(sp - s);
|
||||||
|
if (idlen == 0 || idlen >= CTL_MAX_ID)
|
||||||
|
return -1;
|
||||||
|
while (*sp == ' ' || *sp == '\t')
|
||||||
|
sp++;
|
||||||
|
if (!*sp)
|
||||||
|
return -1;
|
||||||
|
size_t nlen = strlen(sp);
|
||||||
|
if (nlen == 0 || nlen >= CTL_MAX_NAME)
|
||||||
|
return -1;
|
||||||
|
for (size_t i = 0; i < nlen; i++) {
|
||||||
|
if (!(isalnum((unsigned char)sp[i]) || sp[i] == '_' || sp[i] == '-'))
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
clear_call_args(&p->call);
|
||||||
|
memcpy(p->call.id, s, idlen);
|
||||||
|
p->call.id[idlen] = 0;
|
||||||
|
memcpy(p->call.name, sp, nlen);
|
||||||
|
p->call.name[nlen] = 0;
|
||||||
|
p->in_call = 1;
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_CALL_OPEN;
|
||||||
|
ev.call = &p->call;
|
||||||
|
emit(p, &ev);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish_arg(ctl_parser_t *p);
|
||||||
|
|
||||||
|
static int parse_arg_header(ctl_parser_t *p, const char *line) {
|
||||||
|
/* Forms:
|
||||||
|
* arg <key> <nbytes> → length-prefixed body follows
|
||||||
|
* arg <key> <value...> → value is rest of line (line-mode, confirmed now)
|
||||||
|
*/
|
||||||
|
const char *s = line + 4;
|
||||||
|
while (*s == ' ' || *s == '\t')
|
||||||
|
s++;
|
||||||
|
if (!*s)
|
||||||
|
return -1;
|
||||||
|
const char *sp = strchr(s, ' ');
|
||||||
|
if (!sp)
|
||||||
|
return -1;
|
||||||
|
size_t klen = (size_t)(sp - s);
|
||||||
|
if (klen == 0 || klen >= CTL_MAX_KEY)
|
||||||
|
return -1;
|
||||||
|
while (*sp == ' ' || *sp == '\t')
|
||||||
|
sp++;
|
||||||
|
if (!*sp)
|
||||||
|
return -1;
|
||||||
|
if (p->call.nargs >= CTL_MAX_ARGS)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long nbytes = strtoul(sp, &end, 10);
|
||||||
|
int length_prefixed = (end != sp && (*end == '\0' || *end == ' ' || *end == '\t'));
|
||||||
|
if (length_prefixed && end && *end != '\0') {
|
||||||
|
while (*end == ' ' || *end == '\t')
|
||||||
|
end++;
|
||||||
|
if (*end != '\0')
|
||||||
|
length_prefixed = 0; /* e.g. "9foo" or value starting with digits+text */
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(p->arg_key, s, klen);
|
||||||
|
p->arg_key[klen] = 0;
|
||||||
|
|
||||||
|
if (!length_prefixed) {
|
||||||
|
/* line-mode: entire remainder is the value */
|
||||||
|
size_t vlen = strlen(sp);
|
||||||
|
if (vlen > CTL_MAX_ARG_BYTES)
|
||||||
|
return -1;
|
||||||
|
free(p->arg_buf);
|
||||||
|
p->arg_buf = malloc(vlen + 1);
|
||||||
|
if (!p->arg_buf)
|
||||||
|
return -2;
|
||||||
|
memcpy(p->arg_buf, sp, vlen);
|
||||||
|
p->arg_buf[vlen] = 0;
|
||||||
|
p->arg_need = vlen;
|
||||||
|
p->arg_got = vlen;
|
||||||
|
p->arg_cap = vlen;
|
||||||
|
finish_arg(p);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbytes > CTL_MAX_ARG_BYTES)
|
||||||
|
return -1;
|
||||||
|
p->arg_need = (size_t)nbytes;
|
||||||
|
p->arg_got = 0;
|
||||||
|
free(p->arg_buf);
|
||||||
|
p->arg_buf = NULL;
|
||||||
|
p->arg_cap = 0;
|
||||||
|
if (p->arg_need > 0) {
|
||||||
|
p->arg_buf = malloc(p->arg_need + 1);
|
||||||
|
if (!p->arg_buf)
|
||||||
|
return -2;
|
||||||
|
p->arg_cap = p->arg_need;
|
||||||
|
}
|
||||||
|
p->state = ST_ARG_BODY;
|
||||||
|
bump_pending(p, p->arg_need);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish_arg(ctl_parser_t *p) {
|
||||||
|
int i = p->call.nargs;
|
||||||
|
snprintf(p->call.args[i].key, CTL_MAX_KEY, "%s", p->arg_key);
|
||||||
|
p->call.args[i].value = p->arg_buf;
|
||||||
|
p->call.args[i].value_len = p->arg_got;
|
||||||
|
if (p->arg_buf)
|
||||||
|
p->arg_buf[p->arg_got] = 0;
|
||||||
|
p->arg_buf = NULL; /* ownership moved */
|
||||||
|
p->call.nargs++;
|
||||||
|
p->arg_need = 0;
|
||||||
|
p->arg_got = 0;
|
||||||
|
p->arg_key[0] = 0;
|
||||||
|
p->state = ST_LINE;
|
||||||
|
bump_pending(p, 0);
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_ARG;
|
||||||
|
ev.call = &p->call;
|
||||||
|
emit(p, &ev);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_text_line(ctl_parser_t *p, const char *line, size_t len) {
|
||||||
|
/* include newline as confirmed text */
|
||||||
|
char tmp[CTL_MAX_LINE + 1];
|
||||||
|
if (len + 1 >= sizeof(tmp))
|
||||||
|
len = sizeof(tmp) - 2;
|
||||||
|
memcpy(tmp, line, len);
|
||||||
|
tmp[len] = '\n';
|
||||||
|
tmp[len + 1] = 0;
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_TEXT;
|
||||||
|
ev.text = tmp;
|
||||||
|
ev.text_len = len + 1;
|
||||||
|
emit(p, &ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int is_fence_line(const char *line) {
|
||||||
|
if (strncmp(line, "```", 3) == 0)
|
||||||
|
return 1;
|
||||||
|
if (strncmp(line, "~~~", 3) == 0)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int handle_line(ctl_parser_t *p, const char *line) {
|
||||||
|
/* Ignore markdown fences so models can wrap CTL in code blocks. */
|
||||||
|
if (is_fence_line(line)) {
|
||||||
|
bump_pending(p, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/* Ignore blank lines between CTL frames. */
|
||||||
|
if (line[0] == '\0') {
|
||||||
|
bump_pending(p, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p->in_call) {
|
||||||
|
if (starts_with(line, "call ")) {
|
||||||
|
if (parse_call_line(p, line) != 0) {
|
||||||
|
fail(p, "invalid call line");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
bump_pending(p, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/* confirmed text line */
|
||||||
|
emit_text_line(p, line, strlen(line));
|
||||||
|
bump_pending(p, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* in call */
|
||||||
|
if (starts_with(line, "arg ")) {
|
||||||
|
int rc = parse_arg_header(p, line);
|
||||||
|
if (rc == -2) {
|
||||||
|
fail(p, "oom");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (rc != 0) {
|
||||||
|
fail(p, "invalid arg header");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (p->arg_need == 0)
|
||||||
|
finish_arg(p);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (strcmp(line, "end") == 0) {
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_CALL_READY;
|
||||||
|
ev.call = &p->call;
|
||||||
|
emit(p, &ev);
|
||||||
|
clear_call_args(&p->call);
|
||||||
|
p->in_call = 0;
|
||||||
|
bump_pending(p, 0);
|
||||||
|
p->state = ST_LINE;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
fail(p, "unexpected line inside call");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl_status_t ctl_parser_feed(ctl_parser_t *p, const char *data, size_t len) {
|
||||||
|
if (!p)
|
||||||
|
return CTL_ERR_CONTRACT;
|
||||||
|
if (p->state == ST_ERROR)
|
||||||
|
return CTL_ERR_CONTRACT;
|
||||||
|
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < len) {
|
||||||
|
if (p->state == ST_ARG_BODY) {
|
||||||
|
size_t left = p->arg_need - p->arg_got;
|
||||||
|
size_t take = len - i;
|
||||||
|
if (take > left)
|
||||||
|
take = left;
|
||||||
|
if (take > 0) {
|
||||||
|
memcpy(p->arg_buf + p->arg_got, data + i, take);
|
||||||
|
p->arg_got += take;
|
||||||
|
i += take;
|
||||||
|
bump_pending(p, p->arg_need - p->arg_got);
|
||||||
|
}
|
||||||
|
if (p->arg_got == p->arg_need) {
|
||||||
|
finish_arg(p);
|
||||||
|
/* continue with remaining bytes in LINE mode */
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* line accumulation */
|
||||||
|
p->state = ST_LINE;
|
||||||
|
unsigned char c = (unsigned char)data[i++];
|
||||||
|
if (c == '\r')
|
||||||
|
continue; /* normalize CRLF */
|
||||||
|
if (c == '\n') {
|
||||||
|
p->line[p->line_len] = 0;
|
||||||
|
if (handle_line(p, p->line) != 0)
|
||||||
|
return CTL_ERR_CONTRACT;
|
||||||
|
p->line_len = 0;
|
||||||
|
if (p->state == ST_ARG_BODY && p->arg_need == 0) {
|
||||||
|
/* already finished empty arg */
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (p->line_len + 1 >= CTL_MAX_LINE) {
|
||||||
|
fail(p, "line too long");
|
||||||
|
return CTL_ERR_LIMIT;
|
||||||
|
}
|
||||||
|
p->line[p->line_len++] = (char)c;
|
||||||
|
bump_pending(p, p->line_len);
|
||||||
|
}
|
||||||
|
return CTL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctl_status_t ctl_parser_finish(ctl_parser_t *p) {
|
||||||
|
if (!p)
|
||||||
|
return CTL_ERR_CONTRACT;
|
||||||
|
if (p->state == ST_ERROR)
|
||||||
|
return CTL_ERR_CONTRACT;
|
||||||
|
/* Mid-argument body cannot be confirmed at EOF. */
|
||||||
|
if (p->state == ST_ARG_BODY) {
|
||||||
|
fail(p, "incomplete arg body at EOF");
|
||||||
|
return CTL_ERR_CONTRACT;
|
||||||
|
}
|
||||||
|
/* Trailing text without final newline is still confirmed text. */
|
||||||
|
if (!p->in_call && p->line_len > 0) {
|
||||||
|
p->line[p->line_len] = 0;
|
||||||
|
emit_text_line(p, p->line, p->line_len);
|
||||||
|
p->line_len = 0;
|
||||||
|
}
|
||||||
|
/* EOF confirms an open call whose args are complete (implicit end).
|
||||||
|
* Matches paper EOF adequacy: stream end closes the frontier. */
|
||||||
|
if (p->in_call) {
|
||||||
|
if (p->line_len > 0) {
|
||||||
|
p->line[p->line_len] = 0;
|
||||||
|
/* leftover line must be end or fence/blank */
|
||||||
|
if (strcmp(p->line, "end") == 0 || is_fence_line(p->line) ||
|
||||||
|
p->line[0] == '\0') {
|
||||||
|
p->line_len = 0;
|
||||||
|
} else {
|
||||||
|
fail(p, "unexpected trailing line in call at EOF");
|
||||||
|
return CTL_ERR_CONTRACT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_CALL_READY;
|
||||||
|
ev.call = &p->call;
|
||||||
|
emit(p, &ev);
|
||||||
|
clear_call_args(&p->call);
|
||||||
|
p->in_call = 0;
|
||||||
|
}
|
||||||
|
bump_pending(p, 0);
|
||||||
|
return CTL_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#include "cst_agent.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Minimal JSON tool object: {"name":"...","arguments":{...}}
|
||||||
|
* Buffer-until-valid baseline (NOT bounded CST). */
|
||||||
|
|
||||||
|
static const char *skip_ws(const char *p, const char *end) {
|
||||||
|
while (p < end && (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t'))
|
||||||
|
p++;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find matching closing brace/bracket starting at open pos. Returns end ptr or
|
||||||
|
* NULL if incomplete. Handles strings/escapes. */
|
||||||
|
static const char *match_obj(const char *start, const char *end) {
|
||||||
|
if (start >= end || *start != '{')
|
||||||
|
return NULL;
|
||||||
|
int depth = 0;
|
||||||
|
int in_str = 0;
|
||||||
|
for (const char *p = start; p < end; p++) {
|
||||||
|
if (in_str) {
|
||||||
|
if (*p == '\\' && p + 1 < end) {
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"')
|
||||||
|
in_str = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
in_str = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '{')
|
||||||
|
depth++;
|
||||||
|
else if (*p == '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0)
|
||||||
|
return p + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL; /* incomplete */
|
||||||
|
}
|
||||||
|
|
||||||
|
static int extract_string(const char *obj, const char *end, const char *key,
|
||||||
|
char *out, size_t out_sz) {
|
||||||
|
char pat[128];
|
||||||
|
snprintf(pat, sizeof(pat), "\"%s\"", key);
|
||||||
|
const char *k = obj;
|
||||||
|
while (k && k < end) {
|
||||||
|
k = strstr(k, pat);
|
||||||
|
if (!k || k >= end)
|
||||||
|
return -1;
|
||||||
|
/* ensure key boundary */
|
||||||
|
const char *colon = k + strlen(pat);
|
||||||
|
colon = skip_ws(colon, end);
|
||||||
|
if (colon >= end || *colon != ':') {
|
||||||
|
k++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
colon++;
|
||||||
|
colon = skip_ws(colon, end);
|
||||||
|
if (colon >= end || *colon != '"')
|
||||||
|
return -1;
|
||||||
|
colon++;
|
||||||
|
size_t o = 0;
|
||||||
|
for (const char *p = colon; p < end && o + 1 < out_sz; p++) {
|
||||||
|
if (*p == '\\' && p + 1 < end) {
|
||||||
|
p++;
|
||||||
|
if (*p == 'n')
|
||||||
|
out[o++] = '\n';
|
||||||
|
else
|
||||||
|
out[o++] = *p;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
out[o] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
out[o++] = *p;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_json_try_parse_tool(const char *buf, size_t len, cst_json_tool_t *out) {
|
||||||
|
if (!buf || !out)
|
||||||
|
return 0;
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
const char *end = buf + len;
|
||||||
|
const char *p = buf;
|
||||||
|
while (p < end) {
|
||||||
|
p = skip_ws(p, end);
|
||||||
|
if (p >= end)
|
||||||
|
break;
|
||||||
|
if (*p != '{') {
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const char *obj_end = match_obj(p, end);
|
||||||
|
if (!obj_end)
|
||||||
|
return 0; /* incomplete JSON — starvation failure mode */
|
||||||
|
if (extract_string(p, obj_end, "name", out->name, sizeof(out->name)) !=
|
||||||
|
0) {
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* arguments: object as raw substring */
|
||||||
|
const char *akey = strstr(p, "\"arguments\"");
|
||||||
|
if (!akey || akey >= obj_end) {
|
||||||
|
p = obj_end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const char *colon = strchr(akey + 11, ':');
|
||||||
|
if (!colon || colon >= obj_end)
|
||||||
|
return 0;
|
||||||
|
colon++;
|
||||||
|
colon = skip_ws(colon, obj_end);
|
||||||
|
if (colon >= obj_end)
|
||||||
|
return 0;
|
||||||
|
if (*colon == '{') {
|
||||||
|
const char *ae = match_obj(colon, obj_end);
|
||||||
|
if (!ae)
|
||||||
|
return 0;
|
||||||
|
size_t n = (size_t)(ae - colon);
|
||||||
|
if (n >= sizeof(out->arguments_json))
|
||||||
|
return 0;
|
||||||
|
memcpy(out->arguments_json, colon, n);
|
||||||
|
out->arguments_json[n] = 0;
|
||||||
|
} else if (*colon == '"') {
|
||||||
|
if (extract_string(akey, obj_end, "arguments", out->arguments_json,
|
||||||
|
sizeof(out->arguments_json)) != 0)
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
p = obj_end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out->ok = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,786 @@
|
|||||||
|
#include "cst_agent.h"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define TC_SLOTS 8
|
||||||
|
#define TC_ID_MAX 128
|
||||||
|
#define TC_NAME_MAX 128
|
||||||
|
#define TC_ARGS_MAX 65536
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int used;
|
||||||
|
int index;
|
||||||
|
char id[TC_ID_MAX];
|
||||||
|
char name[TC_NAME_MAX];
|
||||||
|
char *args;
|
||||||
|
size_t args_len;
|
||||||
|
size_t args_cap;
|
||||||
|
} tc_slot_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
cst_llm_on_delta_fn on_delta;
|
||||||
|
cst_llm_on_native_tool_fn on_native_tool;
|
||||||
|
void *userdata;
|
||||||
|
char *buf;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
int in_data;
|
||||||
|
int in_reason; /* wrapping reasoning_content as <think> for the agent */
|
||||||
|
tc_slot_t slots[TC_SLOTS];
|
||||||
|
} sse_acc_t;
|
||||||
|
|
||||||
|
static void emit_raw(sse_acc_t *a, const char *s, size_t n) {
|
||||||
|
if (a && a->on_delta && s && n)
|
||||||
|
a->on_delta(s, n, a->userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *find_json_string_key(const char *payload, const char *key) {
|
||||||
|
/* Find "key" that is a full JSON key, not a suffix of another key
|
||||||
|
* (e.g. avoid matching content inside audio_content / reasoning_content). */
|
||||||
|
size_t klen = strlen(key);
|
||||||
|
const char *p = payload;
|
||||||
|
while ((p = strstr(p, key)) != NULL) {
|
||||||
|
if (p > payload) {
|
||||||
|
unsigned char prev = (unsigned char)p[-1];
|
||||||
|
if (prev == '_' || isalnum(prev)) {
|
||||||
|
p += klen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decode one JSON string value starting after the opening quote into out. */
|
||||||
|
static size_t decode_json_string(const char *colon_quote, char *out,
|
||||||
|
size_t out_sz) {
|
||||||
|
size_t o = 0;
|
||||||
|
for (const char *p = colon_quote; *p && o + 4 < out_sz; p++) {
|
||||||
|
if (*p == '\\' && p[1]) {
|
||||||
|
p++;
|
||||||
|
if (*p == 'n')
|
||||||
|
out[o++] = '\n';
|
||||||
|
else if (*p == 'r')
|
||||||
|
out[o++] = '\r';
|
||||||
|
else if (*p == 't')
|
||||||
|
out[o++] = '\t';
|
||||||
|
else if (*p == '"' || *p == '\\' || *p == '/')
|
||||||
|
out[o++] = *p;
|
||||||
|
else if (*p == 'u' && p[1] && p[2] && p[3] && p[4]) {
|
||||||
|
unsigned cp = 0;
|
||||||
|
int ok = 1;
|
||||||
|
for (int i = 1; i <= 4; i++) {
|
||||||
|
char h = p[i];
|
||||||
|
cp <<= 4;
|
||||||
|
if (h >= '0' && h <= '9')
|
||||||
|
cp |= (unsigned)(h - '0');
|
||||||
|
else if (h >= 'a' && h <= 'f')
|
||||||
|
cp |= (unsigned)(h - 'a' + 10);
|
||||||
|
else if (h >= 'A' && h <= 'F')
|
||||||
|
cp |= (unsigned)(h - 'A' + 10);
|
||||||
|
else {
|
||||||
|
ok = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p += 4;
|
||||||
|
if (!ok) {
|
||||||
|
out[o++] = '?';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cp < 0x80)
|
||||||
|
out[o++] = (char)cp;
|
||||||
|
else if (cp < 0x800) {
|
||||||
|
out[o++] = (char)(0xC0 | (cp >> 6));
|
||||||
|
out[o++] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
} else {
|
||||||
|
out[o++] = (char)(0xE0 | (cp >> 12));
|
||||||
|
out[o++] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
out[o++] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out[o++] = *p;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"')
|
||||||
|
break;
|
||||||
|
out[o++] = *p;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int extract_json_string_value(const char *payload, const char *key,
|
||||||
|
char *out, size_t out_sz, size_t *out_len) {
|
||||||
|
const char *keyp = find_json_string_key(payload, key);
|
||||||
|
if (!keyp)
|
||||||
|
return 0;
|
||||||
|
const char *colon = strchr(keyp + strlen(key), ':');
|
||||||
|
if (!colon)
|
||||||
|
return 0;
|
||||||
|
colon++;
|
||||||
|
while (*colon == ' ')
|
||||||
|
colon++;
|
||||||
|
if (*colon == 'n' && strncmp(colon, "null", 4) == 0)
|
||||||
|
return 0;
|
||||||
|
if (*colon != '"')
|
||||||
|
return 0;
|
||||||
|
if (colon[1] == '"')
|
||||||
|
return 0; /* empty */
|
||||||
|
size_t n = decode_json_string(colon + 1, out, out_sz);
|
||||||
|
if (n == 0)
|
||||||
|
return 0;
|
||||||
|
*out_len = n;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int json_brace_balanced(const char *s) {
|
||||||
|
if (!s || *s != '{')
|
||||||
|
return 0;
|
||||||
|
int depth = 0;
|
||||||
|
int in_str = 0;
|
||||||
|
for (const char *p = s; *p; p++) {
|
||||||
|
if (in_str) {
|
||||||
|
if (*p == '\\' && p[1]) {
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"')
|
||||||
|
in_str = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
in_str = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '{')
|
||||||
|
depth++;
|
||||||
|
else if (*p == '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0)
|
||||||
|
return p[1] == 0;
|
||||||
|
if (depth < 0)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static tc_slot_t *slot_for_index(sse_acc_t *a, int index) {
|
||||||
|
for (int i = 0; i < TC_SLOTS; i++) {
|
||||||
|
if (a->slots[i].used && a->slots[i].index == index)
|
||||||
|
return &a->slots[i];
|
||||||
|
}
|
||||||
|
for (int i = 0; i < TC_SLOTS; i++) {
|
||||||
|
if (!a->slots[i].used) {
|
||||||
|
a->slots[i].used = 1;
|
||||||
|
a->slots[i].index = index;
|
||||||
|
a->slots[i].id[0] = 0;
|
||||||
|
a->slots[i].name[0] = 0;
|
||||||
|
a->slots[i].args = NULL;
|
||||||
|
a->slots[i].args_len = 0;
|
||||||
|
a->slots[i].args_cap = 0;
|
||||||
|
return &a->slots[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int slot_append_args(tc_slot_t *s, const char *frag, size_t n) {
|
||||||
|
if (!s || !frag || !n)
|
||||||
|
return 0;
|
||||||
|
if (s->args_len + n + 1 > s->args_cap) {
|
||||||
|
size_t nc = s->args_cap ? s->args_cap * 2 : 1024;
|
||||||
|
while (nc < s->args_len + n + 1)
|
||||||
|
nc *= 2;
|
||||||
|
if (nc > TC_ARGS_MAX)
|
||||||
|
nc = TC_ARGS_MAX;
|
||||||
|
if (s->args_len + n + 1 > nc)
|
||||||
|
return -1;
|
||||||
|
char *nb = realloc(s->args, nc);
|
||||||
|
if (!nb)
|
||||||
|
return -1;
|
||||||
|
s->args = nb;
|
||||||
|
s->args_cap = nc;
|
||||||
|
}
|
||||||
|
memcpy(s->args + s->args_len, frag, n);
|
||||||
|
s->args_len += n;
|
||||||
|
s->args[s->args_len] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse one tool_calls[] element object for index/id/name/arguments fragments. */
|
||||||
|
static void parse_one_tool_call_obj(sse_acc_t *a, const char *obj) {
|
||||||
|
if (!obj || *obj != '{')
|
||||||
|
return;
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
const char *ip = strstr(obj, "\"index\"");
|
||||||
|
if (ip) {
|
||||||
|
const char *colon = strchr(ip, ':');
|
||||||
|
if (colon) {
|
||||||
|
colon++;
|
||||||
|
while (*colon == ' ')
|
||||||
|
colon++;
|
||||||
|
index = (int)strtol(colon, NULL, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tc_slot_t *slot = slot_for_index(a, index);
|
||||||
|
if (!slot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char tmp[4096];
|
||||||
|
size_t tlen = 0;
|
||||||
|
if (extract_json_string_value(obj, "\"id\"", tmp, sizeof(tmp), &tlen)) {
|
||||||
|
if (tlen >= TC_ID_MAX)
|
||||||
|
tlen = TC_ID_MAX - 1;
|
||||||
|
memcpy(slot->id, tmp, tlen);
|
||||||
|
slot->id[tlen] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* function.name / function.arguments may be nested under "function":{...} */
|
||||||
|
const char *fn = strstr(obj, "\"function\"");
|
||||||
|
const char *scope = fn ? fn : obj;
|
||||||
|
tlen = 0;
|
||||||
|
if (extract_json_string_value(scope, "\"name\"", tmp, sizeof(tmp), &tlen)) {
|
||||||
|
if (tlen >= TC_NAME_MAX)
|
||||||
|
tlen = TC_NAME_MAX - 1;
|
||||||
|
memcpy(slot->name, tmp, tlen);
|
||||||
|
slot->name[tlen] = 0;
|
||||||
|
}
|
||||||
|
tlen = 0;
|
||||||
|
if (extract_json_string_value(scope, "\"arguments\"", tmp, sizeof(tmp),
|
||||||
|
&tlen)) {
|
||||||
|
slot_append_args(slot, tmp, tlen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_tool_calls(sse_acc_t *a, const char *payload) {
|
||||||
|
const char *key = find_json_string_key(payload, "\"tool_calls\"");
|
||||||
|
if (!key)
|
||||||
|
return;
|
||||||
|
const char *p = strchr(key, '[');
|
||||||
|
if (!p)
|
||||||
|
return;
|
||||||
|
p++;
|
||||||
|
while (*p) {
|
||||||
|
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t' || *p == ',')
|
||||||
|
p++;
|
||||||
|
if (*p == ']')
|
||||||
|
break;
|
||||||
|
if (*p != '{')
|
||||||
|
break;
|
||||||
|
/* find matching close brace for this object */
|
||||||
|
int depth = 0;
|
||||||
|
int in_str = 0;
|
||||||
|
const char *start = p;
|
||||||
|
const char *end = NULL;
|
||||||
|
for (; *p; p++) {
|
||||||
|
if (in_str) {
|
||||||
|
if (*p == '\\' && p[1]) {
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"')
|
||||||
|
in_str = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
in_str = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '{')
|
||||||
|
depth++;
|
||||||
|
else if (*p == '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
end = p;
|
||||||
|
p++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!end)
|
||||||
|
break;
|
||||||
|
size_t n = (size_t)(end - start + 1);
|
||||||
|
if (n >= 8192)
|
||||||
|
n = 8191;
|
||||||
|
char obj[8192];
|
||||||
|
memcpy(obj, start, n);
|
||||||
|
obj[n] = 0;
|
||||||
|
parse_one_tool_call_obj(a, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void flush_native_tools(sse_acc_t *a) {
|
||||||
|
if (!a->on_native_tool)
|
||||||
|
return;
|
||||||
|
for (int i = 0; i < TC_SLOTS; i++) {
|
||||||
|
tc_slot_t *s = &a->slots[i];
|
||||||
|
if (!s->used || !s->name[0] || !s->args || !s->args_len)
|
||||||
|
continue;
|
||||||
|
if (!json_brace_balanced(s->args))
|
||||||
|
continue;
|
||||||
|
const char *id = s->id[0] ? s->id : "";
|
||||||
|
a->on_native_tool(id, s->name, s->args, a->userdata);
|
||||||
|
/* Mark consumed so end-of-stream flush is idempotent after [DONE]. */
|
||||||
|
free(s->args);
|
||||||
|
s->args = NULL;
|
||||||
|
s->args_len = 0;
|
||||||
|
s->used = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void free_slots(sse_acc_t *a) {
|
||||||
|
for (int i = 0; i < TC_SLOTS; i++) {
|
||||||
|
free(a->slots[i].args);
|
||||||
|
a->slots[i].args = NULL;
|
||||||
|
a->slots[i].used = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_data_payload(sse_acc_t *a, const char *payload) {
|
||||||
|
/* OpenAI SSE: data: {...} or data: [DONE] */
|
||||||
|
if (strncmp(payload, "[DONE]", 6) == 0) {
|
||||||
|
if (a->in_reason) {
|
||||||
|
emit_raw(a, "</think>", 8);
|
||||||
|
a->in_reason = 0;
|
||||||
|
}
|
||||||
|
flush_native_tools(a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char out[8192];
|
||||||
|
size_t olen = 0;
|
||||||
|
|
||||||
|
/* MiniMax reasoning_split: route reasoning_content through <think> so the
|
||||||
|
* agent UI can show it, without polluting the final answer. */
|
||||||
|
if (extract_json_string_value(payload, "\"reasoning_content\"", out,
|
||||||
|
sizeof(out), &olen)) {
|
||||||
|
if (!a->in_reason) {
|
||||||
|
emit_raw(a, "<think>", 7);
|
||||||
|
a->in_reason = 1;
|
||||||
|
}
|
||||||
|
emit_raw(a, out, olen);
|
||||||
|
}
|
||||||
|
|
||||||
|
olen = 0;
|
||||||
|
if (extract_json_string_value(payload, "\"content\"", out, sizeof(out),
|
||||||
|
&olen)) {
|
||||||
|
if (a->in_reason) {
|
||||||
|
emit_raw(a, "</think>", 8);
|
||||||
|
a->in_reason = 0;
|
||||||
|
}
|
||||||
|
emit_raw(a, out, olen);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_tool_calls(a, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_sse_chunk(sse_acc_t *a, const char *data, size_t n) {
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
if (a->len + 1 >= a->cap) {
|
||||||
|
size_t nc = a->cap ? a->cap * 2 : 4096;
|
||||||
|
char *nb = realloc(a->buf, nc);
|
||||||
|
if (!nb)
|
||||||
|
return;
|
||||||
|
a->buf = nb;
|
||||||
|
a->cap = nc;
|
||||||
|
}
|
||||||
|
a->buf[a->len++] = data[i];
|
||||||
|
if (data[i] == '\n') {
|
||||||
|
a->buf[a->len] = 0;
|
||||||
|
/* trim CR */
|
||||||
|
if (a->len >= 2 && a->buf[a->len - 2] == '\r')
|
||||||
|
a->buf[a->len - 2] = 0;
|
||||||
|
else
|
||||||
|
a->buf[a->len - 1] = 0;
|
||||||
|
if (strncmp(a->buf, "data:", 5) == 0) {
|
||||||
|
const char *p = a->buf + 5;
|
||||||
|
while (*p == ' ')
|
||||||
|
p++;
|
||||||
|
emit_data_payload(a, p);
|
||||||
|
}
|
||||||
|
a->len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
||||||
|
sse_acc_t *a = userdata;
|
||||||
|
size_t n = size * nmemb;
|
||||||
|
process_sse_chunk(a, ptr, n);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int xferinfo_cb(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
|
||||||
|
curl_off_t ultotal, curl_off_t ulnow) {
|
||||||
|
(void)dltotal;
|
||||||
|
(void)dlnow;
|
||||||
|
(void)ultotal;
|
||||||
|
(void)ulnow;
|
||||||
|
volatile int *flag = (volatile int *)clientp;
|
||||||
|
if (flag && *flag != 0)
|
||||||
|
return 1; /* abort */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int json_escape_append(char **buf, size_t *len, size_t *cap,
|
||||||
|
const char *s) {
|
||||||
|
if (!buf || !len || !cap)
|
||||||
|
return -1;
|
||||||
|
if (!*buf || *cap == 0) {
|
||||||
|
*cap = 4096;
|
||||||
|
*buf = malloc(*cap);
|
||||||
|
if (!*buf)
|
||||||
|
return -1;
|
||||||
|
*len = 0;
|
||||||
|
(*buf)[0] = 0;
|
||||||
|
}
|
||||||
|
if (!s)
|
||||||
|
s = "";
|
||||||
|
for (; *s; s++) {
|
||||||
|
char tmp[8];
|
||||||
|
const char *piece = tmp;
|
||||||
|
size_t pn = 1;
|
||||||
|
unsigned char c = (unsigned char)*s;
|
||||||
|
if (c == '"' || c == '\\') {
|
||||||
|
tmp[0] = '\\';
|
||||||
|
tmp[1] = (char)c;
|
||||||
|
pn = 2;
|
||||||
|
} else if (c == '\n') {
|
||||||
|
piece = "\\n";
|
||||||
|
pn = 2;
|
||||||
|
} else if (c == '\r') {
|
||||||
|
piece = "\\r";
|
||||||
|
pn = 2;
|
||||||
|
} else if (c == '\t') {
|
||||||
|
piece = "\\t";
|
||||||
|
pn = 2;
|
||||||
|
} else if (c < 0x20) {
|
||||||
|
snprintf(tmp, sizeof(tmp), "\\u%04x", c);
|
||||||
|
pn = 6;
|
||||||
|
} else {
|
||||||
|
tmp[0] = (char)c;
|
||||||
|
pn = 1;
|
||||||
|
}
|
||||||
|
if (*len + pn + 1 > *cap) {
|
||||||
|
size_t nc = *cap ? *cap * 2 : 4096;
|
||||||
|
while (nc < *len + pn + 1)
|
||||||
|
nc *= 2;
|
||||||
|
char *nb = realloc(*buf, nc);
|
||||||
|
if (!nb)
|
||||||
|
return -1;
|
||||||
|
*buf = nb;
|
||||||
|
*cap = nc;
|
||||||
|
}
|
||||||
|
memcpy(*buf + *len, piece, pn);
|
||||||
|
*len += pn;
|
||||||
|
}
|
||||||
|
(*buf)[*len] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int body_ensure(char **body, size_t *blen, size_t *bcap, size_t need) {
|
||||||
|
if (*blen + need + 1 <= *bcap)
|
||||||
|
return 0;
|
||||||
|
size_t nc = *bcap ? *bcap * 2 : 4096;
|
||||||
|
while (nc < *blen + need + 1)
|
||||||
|
nc *= 2;
|
||||||
|
char *nb = realloc(*body, nc);
|
||||||
|
if (!nb)
|
||||||
|
return -1;
|
||||||
|
*body = nb;
|
||||||
|
*bcap = nc;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int body_append_raw(char **body, size_t *blen, size_t *bcap,
|
||||||
|
const char *s) {
|
||||||
|
size_t n = strlen(s);
|
||||||
|
if (body_ensure(body, blen, bcap, n) != 0)
|
||||||
|
return -1;
|
||||||
|
memcpy(*body + *blen, s, n);
|
||||||
|
*blen += n;
|
||||||
|
(*body)[*blen] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *role_api(cst_role_t r) {
|
||||||
|
switch (r) {
|
||||||
|
case CST_ROLE_SYSTEM:
|
||||||
|
return "system";
|
||||||
|
case CST_ROLE_USER:
|
||||||
|
return "user";
|
||||||
|
case CST_ROLE_ASSISTANT:
|
||||||
|
return "assistant";
|
||||||
|
case CST_ROLE_TOOL:
|
||||||
|
return "tool";
|
||||||
|
}
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *TOOLS_JSON =
|
||||||
|
",\"tools\":["
|
||||||
|
"{\"type\":\"function\",\"function\":{\"name\":\"read_file\","
|
||||||
|
"\"description\":\"Read a text file under the workspace\","
|
||||||
|
"\"parameters\":{\"type\":\"object\",\"properties\":{"
|
||||||
|
"\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}}},"
|
||||||
|
"{\"type\":\"function\",\"function\":{\"name\":\"run_shell\","
|
||||||
|
"\"description\":\"Run a shell command in the workspace root\","
|
||||||
|
"\"parameters\":{\"type\":\"object\",\"properties\":{"
|
||||||
|
"\"cmd\":{\"type\":\"string\"}},\"required\":[\"cmd\"]}}},"
|
||||||
|
"{\"type\":\"function\",\"function\":{\"name\":\"write_file\","
|
||||||
|
"\"description\":\"Write full file contents under the workspace\","
|
||||||
|
"\"parameters\":{\"type\":\"object\",\"properties\":{"
|
||||||
|
"\"path\":{\"type\":\"string\"},\"content\":{\"type\":\"string\"}},"
|
||||||
|
"\"required\":[\"path\",\"content\"]}}},"
|
||||||
|
"{\"type\":\"function\",\"function\":{\"name\":\"apply_patch\","
|
||||||
|
"\"description\":\"Apply a unified diff or Update File patch\","
|
||||||
|
"\"parameters\":{\"type\":\"object\",\"properties\":{"
|
||||||
|
"\"patch\":{\"type\":\"string\"}},\"required\":[\"patch\"]}}}"
|
||||||
|
"]";
|
||||||
|
|
||||||
|
int cst_llm_chat_stream(const cst_llm_request_t *req, char *err, size_t err_sz) {
|
||||||
|
if (err && err_sz)
|
||||||
|
err[0] = 0;
|
||||||
|
if (!req || !req->api_base || !req->model) {
|
||||||
|
if (err)
|
||||||
|
snprintf(err, err_sz, "bad request");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *body = NULL;
|
||||||
|
size_t blen = 0;
|
||||||
|
size_t bcap = 8192;
|
||||||
|
body = malloc(bcap);
|
||||||
|
if (!body)
|
||||||
|
return -1;
|
||||||
|
/* MiniMax: keep think+answer in content (no reasoning_split). Split mode
|
||||||
|
* often leaves content empty while the useful text is only in
|
||||||
|
* reasoning_content, which made the agent exit with blank turns. */
|
||||||
|
blen = (size_t)snprintf(body, bcap,
|
||||||
|
"{\"model\":\"%s\",\"stream\":true,\"messages\":[",
|
||||||
|
req->model);
|
||||||
|
|
||||||
|
for (int i = 0; i < req->nmsgs; i++) {
|
||||||
|
const cst_message_t *m = &req->msgs[i];
|
||||||
|
char prefix[256];
|
||||||
|
int first = i ? 0 : 1;
|
||||||
|
if (m->role == CST_ROLE_ASSISTANT && m->tool_calls_json &&
|
||||||
|
m->tool_calls_json[0]) {
|
||||||
|
/* OpenCode/OpenAI: tool-only assistant uses content:null */
|
||||||
|
int empty =
|
||||||
|
!m->content || !m->content[0] ||
|
||||||
|
(m->content[0] == '\0');
|
||||||
|
if (empty) {
|
||||||
|
snprintf(prefix, sizeof(prefix),
|
||||||
|
"%s{\"role\":\"assistant\",\"content\":null,\"tool_calls\":",
|
||||||
|
first ? "" : ",");
|
||||||
|
if (body_append_raw(&body, &blen, &bcap, prefix) != 0 ||
|
||||||
|
body_append_raw(&body, &blen, &bcap, m->tool_calls_json) != 0 ||
|
||||||
|
body_append_raw(&body, &blen, &bcap, "}") != 0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snprintf(prefix, sizeof(prefix),
|
||||||
|
"%s{\"role\":\"assistant\",\"content\":\"", first ? "" : ",");
|
||||||
|
size_t pl = strlen(prefix);
|
||||||
|
if (blen + pl + 8 > bcap) {
|
||||||
|
bcap = (blen + pl) * 2 + 1024;
|
||||||
|
body = realloc(body, bcap);
|
||||||
|
if (!body)
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(body + blen, prefix, pl);
|
||||||
|
blen += pl;
|
||||||
|
body[blen] = 0;
|
||||||
|
if (json_escape_append(&body, &blen, &bcap, m->content) != 0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (body_append_raw(&body, &blen, &bcap, "\",\"tool_calls\":") != 0 ||
|
||||||
|
body_append_raw(&body, &blen, &bcap, m->tool_calls_json) != 0 ||
|
||||||
|
body_append_raw(&body, &blen, &bcap, "}") != 0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (m->role == CST_ROLE_TOOL && m->tool_call_id[0]) {
|
||||||
|
/* Include name when known (OpenAI-compatible gateways accept it). */
|
||||||
|
if (m->tool_name[0]) {
|
||||||
|
snprintf(prefix, sizeof(prefix),
|
||||||
|
"%s{\"role\":\"%s\",\"tool_call_id\":\"%s\",\"name\":\"%s\","
|
||||||
|
"\"content\":\"",
|
||||||
|
first ? "" : ",", role_api(m->role), m->tool_call_id,
|
||||||
|
m->tool_name);
|
||||||
|
} else {
|
||||||
|
snprintf(prefix, sizeof(prefix),
|
||||||
|
"%s{\"role\":\"%s\",\"tool_call_id\":\"%s\",\"content\":\"",
|
||||||
|
first ? "" : ",", role_api(m->role), m->tool_call_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snprintf(prefix, sizeof(prefix), "%s{\"role\":\"%s\",\"content\":\"",
|
||||||
|
first ? "" : ",", role_api(m->role));
|
||||||
|
}
|
||||||
|
size_t pl = strlen(prefix);
|
||||||
|
if (blen + pl + 8 > bcap) {
|
||||||
|
bcap = (blen + pl) * 2 + 1024;
|
||||||
|
body = realloc(body, bcap);
|
||||||
|
if (!body)
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(body + blen, prefix, pl);
|
||||||
|
blen += pl;
|
||||||
|
body[blen] = 0;
|
||||||
|
/* Keep full tool output in session/TUI; shrink only the wire payload so
|
||||||
|
* large write diffs don't blow MiniMax/JSON limits. */
|
||||||
|
{
|
||||||
|
const char *raw = m->content ? m->content : "";
|
||||||
|
char *scratch = NULL;
|
||||||
|
const char *send = raw;
|
||||||
|
enum { API_TOOL_CAP = 3500 };
|
||||||
|
if (m->role == CST_ROLE_TOOL && strlen(raw) > API_TOOL_CAP) {
|
||||||
|
scratch = malloc(API_TOOL_CAP + 80);
|
||||||
|
if (scratch) {
|
||||||
|
snprintf(scratch, API_TOOL_CAP + 80,
|
||||||
|
"%.*s\n…(truncated for model; full diff in UI/session)",
|
||||||
|
API_TOOL_CAP, raw);
|
||||||
|
send = scratch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (json_escape_append(&body, &blen, &bcap, send) != 0) {
|
||||||
|
free(scratch);
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
free(scratch);
|
||||||
|
}
|
||||||
|
if (blen + 3 > bcap) {
|
||||||
|
bcap *= 2;
|
||||||
|
body = realloc(body, bcap);
|
||||||
|
if (!body)
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
body[blen++] = '"';
|
||||||
|
body[blen++] = '}';
|
||||||
|
body[blen] = 0;
|
||||||
|
}
|
||||||
|
if (body_append_raw(&body, &blen, &bcap, "]") != 0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (req->enable_tools) {
|
||||||
|
if (body_append_raw(&body, &blen, &bcap, TOOLS_JSON) != 0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (req->tool_choice_none) {
|
||||||
|
if (body_append_raw(&body, &blen, &bcap, ",\"tool_choice\":\"none\"") !=
|
||||||
|
0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (body_append_raw(&body, &blen, &bcap, ",\"tool_choice\":\"auto\"") !=
|
||||||
|
0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body_append_raw(&body, &blen, &bcap, "}") != 0) {
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char url[1024];
|
||||||
|
snprintf(url, sizeof(url), "%s/chat/completions", req->api_base);
|
||||||
|
|
||||||
|
CURL *curl = curl_easy_init();
|
||||||
|
if (!curl) {
|
||||||
|
free(body);
|
||||||
|
if (err)
|
||||||
|
snprintf(err, err_sz, "curl init failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sse_acc_t acc = {0};
|
||||||
|
acc.on_delta = req->on_delta;
|
||||||
|
acc.on_native_tool = req->on_native_tool;
|
||||||
|
acc.userdata = req->userdata;
|
||||||
|
|
||||||
|
struct curl_slist *hdrs = NULL;
|
||||||
|
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
|
||||||
|
char auth[600];
|
||||||
|
if (req->api_key && req->api_key[0]) {
|
||||||
|
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", req->api_key);
|
||||||
|
hdrs = curl_slist_append(hdrs, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &acc);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 300L);
|
||||||
|
|
||||||
|
if (req->cancel_flag) {
|
||||||
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferinfo_cb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, (void *)req->cancel_flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
CURLcode rc = curl_easy_perform(curl);
|
||||||
|
long http = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http);
|
||||||
|
if (acc.in_reason) {
|
||||||
|
emit_raw(&acc, "</think>", 8);
|
||||||
|
acc.in_reason = 0;
|
||||||
|
}
|
||||||
|
/* Stream ended without [DONE] — still flush completed native tools. */
|
||||||
|
if (rc == CURLE_OK)
|
||||||
|
flush_native_tools(&acc);
|
||||||
|
curl_slist_free_all(hdrs);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
if (rc != CURLE_OK) {
|
||||||
|
if (err) {
|
||||||
|
if (rc == CURLE_ABORTED_BY_CALLBACK)
|
||||||
|
snprintf(err, err_sz, "cancelled");
|
||||||
|
else
|
||||||
|
snprintf(err, err_sz, "curl: %s", curl_easy_strerror(rc));
|
||||||
|
}
|
||||||
|
free_slots(&acc);
|
||||||
|
free(acc.buf);
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (http >= 400) {
|
||||||
|
if (err) {
|
||||||
|
/* show a snippet of body if present */
|
||||||
|
if (acc.buf && acc.len) {
|
||||||
|
acc.buf[acc.len < 200 ? acc.len : 200] = 0;
|
||||||
|
snprintf(err, err_sz, "http %ld: %.180s", http, acc.buf);
|
||||||
|
} else {
|
||||||
|
snprintf(err, err_sz, "http %ld", http);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free_slots(&acc);
|
||||||
|
free(acc.buf);
|
||||||
|
free(body);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
free_slots(&acc);
|
||||||
|
free(acc.buf);
|
||||||
|
free(body);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+283
@@ -0,0 +1,283 @@
|
|||||||
|
#include "cst_agent.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <getopt.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <direct.h>
|
||||||
|
#else
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static void usage(const char *argv0) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s [--workspace DIR] [--session FILE] [--api-base URL]\n"
|
||||||
|
" [--api-key KEY] [--model NAME] [--headless]\n"
|
||||||
|
" [--prompt TEXT] [--demo-ctl] [--ask-key]\n"
|
||||||
|
"\n"
|
||||||
|
"Defaults: api-base=https://api.minimax.cn/v1 model=MiniMax-M2.7\n"
|
||||||
|
"Session: <workspace>/.agent/sessions/<id>.jsonl (CURRENT tracked)\n"
|
||||||
|
"API key: CST_API_KEY / OPENAI_API_KEY / MINIMAX_API_KEY\n"
|
||||||
|
"Base: CST_API_BASE / OPENAI_BASE_URL (OpenAI-compatible …/v1)\n"
|
||||||
|
"Model: CST_MODEL / OPENAI_MODEL\n"
|
||||||
|
"Also: CST_MODELS, CST_AUTO_APPROVE=1, CST_SHELL, CST_EXE_DIR\n"
|
||||||
|
"TUI: /new /session /model /compact /details /help\n"
|
||||||
|
" Enter send · Ctrl+N newline · ↑↓ history · Esc cancel\n"
|
||||||
|
" busy Enter queues · tool perm y/n/a=always · wheel scroll\n",
|
||||||
|
argv0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int g_ready;
|
||||||
|
static ctl_tool_call_t g_call;
|
||||||
|
|
||||||
|
static void demo_on_ev(const ctl_event_t *ev, void *ud) {
|
||||||
|
(void)ud;
|
||||||
|
if (ev->type == CTL_EV_TEXT)
|
||||||
|
fwrite(ev->text, 1, ev->text_len, stdout);
|
||||||
|
if (ev->type == CTL_EV_CALL_READY) {
|
||||||
|
snprintf(g_call.id, sizeof(g_call.id), "%s", ev->call->id);
|
||||||
|
snprintf(g_call.name, sizeof(g_call.name), "%s", ev->call->name);
|
||||||
|
for (int i = 0; i < g_call.nargs; i++) {
|
||||||
|
free(g_call.args[i].value);
|
||||||
|
g_call.args[i].value = NULL;
|
||||||
|
}
|
||||||
|
g_call.nargs = 0;
|
||||||
|
for (int i = 0; i < ev->call->nargs; i++) {
|
||||||
|
snprintf(g_call.args[i].key, sizeof(g_call.args[i].key), "%s",
|
||||||
|
ev->call->args[i].key);
|
||||||
|
g_call.args[i].value_len = ev->call->args[i].value_len;
|
||||||
|
g_call.args[i].value = malloc(ev->call->args[i].value_len + 1);
|
||||||
|
memcpy(g_call.args[i].value, ev->call->args[i].value,
|
||||||
|
ev->call->args[i].value_len);
|
||||||
|
g_call.args[i].value[ev->call->args[i].value_len] = 0;
|
||||||
|
g_call.nargs++;
|
||||||
|
}
|
||||||
|
g_ready = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int demo_ctl_impl(const char *workspace) {
|
||||||
|
printf("=== demo CTL (byte-at-a-time feed) ===\n");
|
||||||
|
g_ready = 0;
|
||||||
|
memset(&g_call, 0, sizeof(g_call));
|
||||||
|
ctl_parser_t *p = ctl_parser_create(demo_on_ev, NULL);
|
||||||
|
const char *stream =
|
||||||
|
"Reading README.\n"
|
||||||
|
"call t1 read_file\n"
|
||||||
|
"arg path 9\n"
|
||||||
|
"README.md"
|
||||||
|
"end\n";
|
||||||
|
for (size_t i = 0; stream[i]; i++)
|
||||||
|
ctl_parser_feed(p, &stream[i], 1);
|
||||||
|
if (ctl_parser_finish(p) != CTL_OK || !g_ready) {
|
||||||
|
fprintf(stderr, "demo: CTL failed\n");
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("\npending_peak=%zu\n", ctl_parser_pending_peak(p));
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
|
||||||
|
cst_tool_result_t r = {0};
|
||||||
|
cst_tool_dispatch(workspace, &g_call, &r);
|
||||||
|
printf("[tool result exit=%d]\n%.*s\n", r.exit_code, (int)r.out_len,
|
||||||
|
r.out ? r.out : "");
|
||||||
|
cst_tool_result_free(&r);
|
||||||
|
for (int i = 0; i < g_call.nargs; i++)
|
||||||
|
free(g_call.args[i].value);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
os_set_prog_path(argv[0]);
|
||||||
|
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||||
|
|
||||||
|
cst_agent_config_t cfg = {0};
|
||||||
|
char cwd[4096];
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (!_getcwd(cwd, (int)sizeof(cwd)))
|
||||||
|
#else
|
||||||
|
if (!getcwd(cwd, sizeof(cwd)))
|
||||||
|
#endif
|
||||||
|
snprintf(cwd, sizeof(cwd), ".");
|
||||||
|
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", cwd);
|
||||||
|
cfg.session_path[0] = 0; /* filled after workspace opts via .agent/store */
|
||||||
|
/* OpenAI-compatible base URL aliases */
|
||||||
|
const char *env_base = getenv("CST_API_BASE");
|
||||||
|
if (!env_base || !env_base[0])
|
||||||
|
env_base = getenv("OPENAI_BASE_URL");
|
||||||
|
if (!env_base || !env_base[0])
|
||||||
|
env_base = getenv("OPENAI_API_BASE");
|
||||||
|
const char *env_model = getenv("CST_MODEL");
|
||||||
|
if (!env_model || !env_model[0])
|
||||||
|
env_model = getenv("OPENAI_MODEL");
|
||||||
|
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s",
|
||||||
|
env_base && env_base[0] ? env_base : "https://api.minimax.cn/v1");
|
||||||
|
cfg.api_key[0] = 0;
|
||||||
|
snprintf(cfg.model, sizeof(cfg.model), "%s",
|
||||||
|
env_model && env_model[0] ? env_model : "MiniMax-M2.7");
|
||||||
|
cfg.headless = 0;
|
||||||
|
cfg.confirm_shell = 1;
|
||||||
|
cfg.confirm_write = 1;
|
||||||
|
cfg.auto_approve = 0;
|
||||||
|
if (getenv("CST_AUTO_APPROVE"))
|
||||||
|
cfg.auto_approve = 1;
|
||||||
|
|
||||||
|
int do_demo = 0;
|
||||||
|
int force_ask_key = 0;
|
||||||
|
int session_explicit = 0;
|
||||||
|
const char *prompt = NULL;
|
||||||
|
int key_from_cli = 0;
|
||||||
|
|
||||||
|
static struct option opts[] = {
|
||||||
|
{"workspace", required_argument, 0, 'w'},
|
||||||
|
{"session", required_argument, 0, 's'},
|
||||||
|
{"api-base", required_argument, 0, 'b'},
|
||||||
|
{"api-key", required_argument, 0, 'k'},
|
||||||
|
{"model", required_argument, 0, 'm'},
|
||||||
|
{"headless", no_argument, 0, 'H'},
|
||||||
|
{"prompt", required_argument, 0, 'p'},
|
||||||
|
{"demo-ctl", no_argument, 0, 'd'},
|
||||||
|
{"ask-key", no_argument, 0, 'a'},
|
||||||
|
{"help", no_argument, 0, 'h'},
|
||||||
|
{0, 0, 0, 0}};
|
||||||
|
int c;
|
||||||
|
while ((c = getopt_long(argc, argv, "w:s:b:k:m:Hp:dah", opts, NULL)) != -1) {
|
||||||
|
switch (c) {
|
||||||
|
case 'w':
|
||||||
|
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
snprintf(cfg.session_path, sizeof(cfg.session_path), "%s", optarg);
|
||||||
|
session_explicit = 1;
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
snprintf(cfg.api_base, sizeof(cfg.api_base), "%s", optarg);
|
||||||
|
break;
|
||||||
|
case 'k':
|
||||||
|
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", optarg);
|
||||||
|
key_from_cli = 1;
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
snprintf(cfg.model, sizeof(cfg.model), "%s", optarg);
|
||||||
|
break;
|
||||||
|
case 'H':
|
||||||
|
cfg.headless = 1;
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
prompt = optarg;
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
do_demo = 1;
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
force_ask_key = 1;
|
||||||
|
break;
|
||||||
|
case 'h':
|
||||||
|
usage(argv[0]);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
usage(argv[0]);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* trim trailing slash on api_base */
|
||||||
|
size_t alen = strlen(cfg.api_base);
|
||||||
|
while (alen > 0 && cfg.api_base[alen - 1] == '/')
|
||||||
|
cfg.api_base[--alen] = 0;
|
||||||
|
|
||||||
|
/* Headless defaults to auto-approve shell/write (no interactive confirm). */
|
||||||
|
if (cfg.headless)
|
||||||
|
cfg.auto_approve = 1;
|
||||||
|
|
||||||
|
/* Default session under <workspace>/.agent/sessions/ (OpenCode-like). */
|
||||||
|
if (!session_explicit) {
|
||||||
|
char id[128];
|
||||||
|
if (cst_agent_store_resolve_default(cfg.workspace_root, cfg.session_path,
|
||||||
|
sizeof(cfg.session_path), id,
|
||||||
|
sizeof(id)) != 0) {
|
||||||
|
fprintf(stderr, "failed to init .agent/sessions under %s\n",
|
||||||
|
cfg.workspace_root);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
fprintf(stderr, "session %s\n", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (do_demo) {
|
||||||
|
int rc = demo_ctl_impl(cfg.workspace_root);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force_ask_key || !key_from_cli) {
|
||||||
|
if (force_ask_key) {
|
||||||
|
if (cst_creds_prompt_api_key(cfg.api_key, sizeof(cfg.api_key)) != 0) {
|
||||||
|
fprintf(stderr, "API key prompt cancelled\n");
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (cst_creds_save_api_key(cfg.api_key) != 0)
|
||||||
|
fprintf(stderr, "warning: failed to save credentials\n");
|
||||||
|
else
|
||||||
|
fprintf(stderr, "saved API key to ~/.config/cst-agent/credentials\n");
|
||||||
|
/* --ask-key alone: save and exit unless running a turn/TUI */
|
||||||
|
if (!cfg.headless && !prompt) {
|
||||||
|
fprintf(stderr, "using %s model=%s\n", cfg.api_base, cfg.model);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else if (!cfg.api_key[0]) {
|
||||||
|
if (cst_creds_ensure_api_key(cfg.api_key, sizeof(cfg.api_key)) != 0) {
|
||||||
|
fprintf(stderr, "no API key (set CST_API_KEY or use --ask-key)\n");
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (key_from_cli) {
|
||||||
|
/* persist CLI-provided key so next launch can reuse */
|
||||||
|
if (cst_creds_save_api_key(cfg.api_key) != 0)
|
||||||
|
fprintf(stderr, "warning: failed to save credentials\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "using %s model=%s\n", cfg.api_base, cfg.model);
|
||||||
|
|
||||||
|
cst_agent_t *agent = cst_agent_create(&cfg);
|
||||||
|
if (!agent) {
|
||||||
|
fprintf(stderr, "failed to create agent (session path?)\n");
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = 0;
|
||||||
|
if (cfg.headless) {
|
||||||
|
if (!prompt) {
|
||||||
|
fprintf(stderr, "--headless requires --prompt\n");
|
||||||
|
rc = 1;
|
||||||
|
} else {
|
||||||
|
char err[512];
|
||||||
|
if (cst_agent_run_turn(agent, prompt, err, sizeof(err)) != 0) {
|
||||||
|
fprintf(stderr, "turn failed: %s\n", err);
|
||||||
|
rc = 1;
|
||||||
|
} else {
|
||||||
|
cst_session_t *s = cst_agent_session(agent);
|
||||||
|
for (int i = 0; i < cst_session_count(s); i++) {
|
||||||
|
const cst_message_t *m = cst_session_get(s, i);
|
||||||
|
printf("[%d role=%d] %s\n", i, (int)m->role,
|
||||||
|
m->content ? m->content : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rc = cst_tui_run(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
cst_agent_destroy(agent);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
+410
@@ -0,0 +1,410 @@
|
|||||||
|
#include "mm_tool.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define MM_OPEN "<minimax:tool_call>"
|
||||||
|
#define MM_CLOSE "</minimax:tool_call>"
|
||||||
|
#define MM_OPEN_LEN 19
|
||||||
|
#define MM_CLOSE_LEN 20
|
||||||
|
|
||||||
|
struct mm_scan {
|
||||||
|
mm_on_event_fn on_event;
|
||||||
|
void *userdata;
|
||||||
|
char *buf;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
size_t pending_peak;
|
||||||
|
int in_tool; /* 1 if last open is unmatched */
|
||||||
|
char idbuf[CTL_MAX_ID];
|
||||||
|
unsigned id_seq;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void emit(mm_scan_t *s, ctl_event_t *ev) {
|
||||||
|
if (s->on_event)
|
||||||
|
s->on_event(ev, s->userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_text(mm_scan_t *s, const char *t, size_t n) {
|
||||||
|
if (!t || !n)
|
||||||
|
return;
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_TEXT;
|
||||||
|
ev.text = t;
|
||||||
|
ev.text_len = n;
|
||||||
|
emit(s, &ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ensure(mm_scan_t *s, size_t need) {
|
||||||
|
if (s->len + need + 1 <= s->cap)
|
||||||
|
return 0;
|
||||||
|
size_t nc = s->cap ? s->cap * 2 : 4096;
|
||||||
|
while (nc < s->len + need + 1)
|
||||||
|
nc *= 2;
|
||||||
|
char *nb = realloc(s->buf, nc);
|
||||||
|
if (!nb)
|
||||||
|
return -1;
|
||||||
|
s->buf = nb;
|
||||||
|
s->cap = nc;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bump_pending(mm_scan_t *s) {
|
||||||
|
if (s->len > s->pending_peak)
|
||||||
|
s->pending_peak = s->len;
|
||||||
|
}
|
||||||
|
|
||||||
|
mm_scan_t *mm_scan_create(mm_on_event_fn on_event, void *userdata) {
|
||||||
|
mm_scan_t *s = calloc(1, sizeof(*s));
|
||||||
|
if (!s)
|
||||||
|
return NULL;
|
||||||
|
s->on_event = on_event;
|
||||||
|
s->userdata = userdata;
|
||||||
|
s->id_seq = 1;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mm_scan_destroy(mm_scan_t *s) {
|
||||||
|
if (!s)
|
||||||
|
return;
|
||||||
|
free(s->buf);
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mm_scan_reset(mm_scan_t *s) {
|
||||||
|
if (!s)
|
||||||
|
return;
|
||||||
|
s->len = 0;
|
||||||
|
s->in_tool = 0;
|
||||||
|
if (s->buf)
|
||||||
|
s->buf[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t mm_scan_pending(const mm_scan_t *s) {
|
||||||
|
return s ? (s->in_tool ? s->len : 0) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_call(ctl_tool_call_t *c) {
|
||||||
|
for (int i = 0; i < c->nargs; i++) {
|
||||||
|
free(c->args[i].value);
|
||||||
|
c->args[i].value = NULL;
|
||||||
|
}
|
||||||
|
memset(c, 0, sizeof(*c));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int add_arg(ctl_tool_call_t *c, const char *key, const char *val,
|
||||||
|
size_t vlen) {
|
||||||
|
if (c->nargs >= CTL_MAX_ARGS)
|
||||||
|
return -1;
|
||||||
|
if (strlen(key) >= CTL_MAX_KEY)
|
||||||
|
return -1;
|
||||||
|
snprintf(c->args[c->nargs].key, CTL_MAX_KEY, "%s", key);
|
||||||
|
c->args[c->nargs].value = malloc(vlen + 1);
|
||||||
|
if (!c->args[c->nargs].value)
|
||||||
|
return -1;
|
||||||
|
memcpy(c->args[c->nargs].value, val, vlen);
|
||||||
|
c->args[c->nargs].value[vlen] = 0;
|
||||||
|
c->args[c->nargs].value_len = vlen;
|
||||||
|
c->nargs++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extract name="..." or name='...' from a tag start. */
|
||||||
|
static int extract_name_attr(const char *tag, char *out, size_t out_sz) {
|
||||||
|
const char *p = strstr(tag, "name=");
|
||||||
|
if (!p)
|
||||||
|
return -1;
|
||||||
|
p += 5;
|
||||||
|
char quote = 0;
|
||||||
|
if (*p == '"' || *p == '\'') {
|
||||||
|
quote = *p++;
|
||||||
|
}
|
||||||
|
size_t i = 0;
|
||||||
|
while (*p && i + 1 < out_sz) {
|
||||||
|
if (quote) {
|
||||||
|
if (*p == quote)
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (*p == ' ' || *p == '>' || *p == '/')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out[i++] = *p++;
|
||||||
|
}
|
||||||
|
out[i] = 0;
|
||||||
|
return i > 0 ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_call(mm_scan_t *s, ctl_tool_call_t *call) {
|
||||||
|
/* Only emit allowlisted tools — prevents "cmd" etc. from becoming tools. */
|
||||||
|
if (!call->name[0])
|
||||||
|
return;
|
||||||
|
if (strcmp(call->name, "read_file") != 0 &&
|
||||||
|
strcmp(call->name, "run_shell") != 0 &&
|
||||||
|
strcmp(call->name, "write_file") != 0 &&
|
||||||
|
strcmp(call->name, "apply_patch") != 0)
|
||||||
|
return;
|
||||||
|
ctl_event_t ev = {0};
|
||||||
|
ev.type = CTL_EV_CALL_OPEN;
|
||||||
|
ev.call = call;
|
||||||
|
emit(s, &ev);
|
||||||
|
ev.type = CTL_EV_CALL_READY;
|
||||||
|
ev.call = call;
|
||||||
|
emit(s, &ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parse one complete tool_call inner body (between open/close tags).
|
||||||
|
* Emits one CALL_READY per invoke.
|
||||||
|
*/
|
||||||
|
static void parse_tool_block(mm_scan_t *s, const char *inner, size_t n) {
|
||||||
|
char *block = malloc(n + 1);
|
||||||
|
if (!block)
|
||||||
|
return;
|
||||||
|
memcpy(block, inner, n);
|
||||||
|
block[n] = 0;
|
||||||
|
|
||||||
|
/* Normalize: support both <invoke name="x"> and bare "invoke x" lines. */
|
||||||
|
char *p = block;
|
||||||
|
while (*p) {
|
||||||
|
/* Skip whitespace */
|
||||||
|
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
|
||||||
|
p++;
|
||||||
|
if (!*p)
|
||||||
|
break;
|
||||||
|
|
||||||
|
ctl_tool_call_t call;
|
||||||
|
memset(&call, 0, sizeof(call));
|
||||||
|
snprintf(call.id, sizeof(call.id), "mm%u", s->id_seq++);
|
||||||
|
|
||||||
|
if (strncmp(p, "<invoke", 7) == 0) {
|
||||||
|
char *gt = strchr(p, '>');
|
||||||
|
if (!gt) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char tag[256];
|
||||||
|
size_t tlen = (size_t)(gt - p) + 1;
|
||||||
|
if (tlen >= sizeof(tag))
|
||||||
|
tlen = sizeof(tag) - 1;
|
||||||
|
memcpy(tag, p, tlen);
|
||||||
|
tag[tlen] = 0;
|
||||||
|
if (extract_name_attr(tag, call.name, sizeof(call.name)) != 0) {
|
||||||
|
p = gt + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
p = gt + 1;
|
||||||
|
char *end_inv = strstr(p, "</invoke>");
|
||||||
|
if (!end_inv) {
|
||||||
|
/* unclosed invoke — treat rest as parameters until end */
|
||||||
|
end_inv = block + n;
|
||||||
|
}
|
||||||
|
/* parameters / args inside */
|
||||||
|
char *q = p;
|
||||||
|
while (q < end_inv) {
|
||||||
|
char *arg = strstr(q, "<parameter");
|
||||||
|
char *arg2 = strstr(q, "<arg");
|
||||||
|
char *use = NULL;
|
||||||
|
int is_param = 0;
|
||||||
|
if (arg && (!arg2 || arg <= arg2) && arg < end_inv) {
|
||||||
|
use = arg;
|
||||||
|
is_param = 1;
|
||||||
|
} else if (arg2 && arg2 < end_inv) {
|
||||||
|
use = arg2;
|
||||||
|
is_param = 0;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
char *agt = strchr(use, '>');
|
||||||
|
if (!agt || agt >= end_inv)
|
||||||
|
break;
|
||||||
|
char atag[256];
|
||||||
|
size_t al = (size_t)(agt - use) + 1;
|
||||||
|
if (al >= sizeof(atag))
|
||||||
|
al = sizeof(atag) - 1;
|
||||||
|
memcpy(atag, use, al);
|
||||||
|
atag[al] = 0;
|
||||||
|
char key[CTL_MAX_KEY];
|
||||||
|
if (extract_name_attr(atag, key, sizeof(key)) != 0) {
|
||||||
|
q = agt + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const char *close = is_param ? "</parameter>" : "</arg>";
|
||||||
|
char *aend = strstr(agt + 1, close);
|
||||||
|
if (!aend || aend > end_inv) {
|
||||||
|
q = agt + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t vlen = (size_t)(aend - (agt + 1));
|
||||||
|
add_arg(&call, key, agt + 1, vlen);
|
||||||
|
q = aend + strlen(close);
|
||||||
|
}
|
||||||
|
emit_call(s, &call);
|
||||||
|
clear_call(&call);
|
||||||
|
if (end_inv < block + n && strncmp(end_inv, "</invoke>", 9) == 0)
|
||||||
|
p = end_inv + 9;
|
||||||
|
else
|
||||||
|
p = end_inv;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bare: invoke NAME\n */
|
||||||
|
if (strncmp(p, "invoke ", 7) == 0) {
|
||||||
|
p += 7;
|
||||||
|
while (*p == ' ' || *p == '\t')
|
||||||
|
p++;
|
||||||
|
size_t ni = 0;
|
||||||
|
while (p[ni] && p[ni] != '\n' && p[ni] != '\r' && p[ni] != ' ' &&
|
||||||
|
p[ni] != '<' && ni + 1 < sizeof(call.name)) {
|
||||||
|
call.name[ni] = p[ni];
|
||||||
|
ni++;
|
||||||
|
}
|
||||||
|
call.name[ni] = 0;
|
||||||
|
p += ni;
|
||||||
|
while (*p == '\n' || *p == '\r' || *p == ' ' || *p == '\t')
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Collect <arg>...</arg> until next invoke or end */
|
||||||
|
while (*p) {
|
||||||
|
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
|
||||||
|
p++;
|
||||||
|
if (strncmp(p, "invoke ", 7) == 0 || strncmp(p, "<invoke", 7) == 0)
|
||||||
|
break;
|
||||||
|
if (strncmp(p, "<arg", 4) != 0 && strncmp(p, "<parameter", 10) != 0)
|
||||||
|
break;
|
||||||
|
int is_param = (strncmp(p, "<parameter", 10) == 0);
|
||||||
|
char *agt = strchr(p, '>');
|
||||||
|
if (!agt)
|
||||||
|
break;
|
||||||
|
char atag[256];
|
||||||
|
size_t al = (size_t)(agt - p) + 1;
|
||||||
|
if (al >= sizeof(atag))
|
||||||
|
al = sizeof(atag) - 1;
|
||||||
|
memcpy(atag, p, al);
|
||||||
|
atag[al] = 0;
|
||||||
|
char key[CTL_MAX_KEY];
|
||||||
|
if (extract_name_attr(atag, key, sizeof(key)) != 0) {
|
||||||
|
p = agt + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const char *close = is_param ? "</parameter>" : "</arg>";
|
||||||
|
char *aend = strstr(agt + 1, close);
|
||||||
|
if (!aend)
|
||||||
|
break;
|
||||||
|
size_t vlen = (size_t)(aend - (agt + 1));
|
||||||
|
add_arg(&call, key, agt + 1, vlen);
|
||||||
|
p = aend + strlen(close);
|
||||||
|
}
|
||||||
|
if (call.name[0]) {
|
||||||
|
emit_call(s, &call);
|
||||||
|
}
|
||||||
|
clear_call(&call);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unknown content — skip one char */
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
free(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drop stray close tags that MiniMax sometimes emits outside a tool block. */
|
||||||
|
static void strip_stray_closes(mm_scan_t *s) {
|
||||||
|
for (;;) {
|
||||||
|
char *c = strstr(s->buf, MM_CLOSE);
|
||||||
|
if (!c)
|
||||||
|
return;
|
||||||
|
/* Only strip if it appears before any open (orphan). */
|
||||||
|
char *o = strstr(s->buf, MM_OPEN);
|
||||||
|
if (o && o < c)
|
||||||
|
return;
|
||||||
|
size_t at = (size_t)(c - s->buf);
|
||||||
|
size_t rest = s->len - at - MM_CLOSE_LEN;
|
||||||
|
if (at > 0)
|
||||||
|
emit_text(s, s->buf, at);
|
||||||
|
memmove(s->buf, c + MM_CLOSE_LEN, rest);
|
||||||
|
s->len = rest;
|
||||||
|
s->buf[s->len] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process buffer: emit text before tools, parse complete tool blocks. */
|
||||||
|
static void drain(mm_scan_t *s) {
|
||||||
|
for (;;) {
|
||||||
|
if (!s->in_tool) {
|
||||||
|
strip_stray_closes(s);
|
||||||
|
char *open = strstr(s->buf, MM_OPEN);
|
||||||
|
if (!open) {
|
||||||
|
/* Keep a short tail that could be a partial open tag. */
|
||||||
|
size_t keep = MM_OPEN_LEN - 1;
|
||||||
|
if (s->len <= keep)
|
||||||
|
return;
|
||||||
|
size_t emit_n = s->len - keep;
|
||||||
|
/* Don't split a UTF-8 codepoint across emit/keep. */
|
||||||
|
while (emit_n > 0 &&
|
||||||
|
((unsigned char)s->buf[emit_n] & 0xC0) == 0x80)
|
||||||
|
emit_n--;
|
||||||
|
if (emit_n == 0)
|
||||||
|
return;
|
||||||
|
emit_text(s, s->buf, emit_n);
|
||||||
|
memmove(s->buf, s->buf + emit_n, s->len - emit_n);
|
||||||
|
s->len -= emit_n;
|
||||||
|
s->buf[s->len] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t before = (size_t)(open - s->buf);
|
||||||
|
if (before)
|
||||||
|
emit_text(s, s->buf, before);
|
||||||
|
/* drop open tag from buffer, enter tool mode */
|
||||||
|
size_t rest = s->len - before - MM_OPEN_LEN;
|
||||||
|
memmove(s->buf, open + MM_OPEN_LEN, rest);
|
||||||
|
s->len = rest;
|
||||||
|
s->buf[s->len] = 0;
|
||||||
|
s->in_tool = 1;
|
||||||
|
bump_pending(s);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* in tool: wait for close */
|
||||||
|
char *close = strstr(s->buf, MM_CLOSE);
|
||||||
|
if (!close)
|
||||||
|
return; /* pending */
|
||||||
|
size_t inner_len = (size_t)(close - s->buf);
|
||||||
|
parse_tool_block(s, s->buf, inner_len);
|
||||||
|
size_t rest = s->len - inner_len - MM_CLOSE_LEN;
|
||||||
|
memmove(s->buf, close + MM_CLOSE_LEN, rest);
|
||||||
|
s->len = rest;
|
||||||
|
s->buf[s->len] = 0;
|
||||||
|
s->in_tool = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mm_scan_feed(mm_scan_t *s, const char *data, size_t len) {
|
||||||
|
if (!s || !data)
|
||||||
|
return -1;
|
||||||
|
if (ensure(s, len) != 0)
|
||||||
|
return -1;
|
||||||
|
memcpy(s->buf + s->len, data, len);
|
||||||
|
s->len += len;
|
||||||
|
s->buf[s->len] = 0;
|
||||||
|
bump_pending(s);
|
||||||
|
drain(s);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mm_scan_finish(mm_scan_t *s) {
|
||||||
|
if (!s)
|
||||||
|
return -1;
|
||||||
|
drain(s);
|
||||||
|
if (s->in_tool) {
|
||||||
|
/* Incomplete tool — do NOT execute; emit as visible text so UI isn't stuck */
|
||||||
|
emit_text(s, MM_OPEN, MM_OPEN_LEN);
|
||||||
|
if (s->len)
|
||||||
|
emit_text(s, s->buf, s->len);
|
||||||
|
s->len = 0;
|
||||||
|
s->in_tool = 0;
|
||||||
|
} else if (s->len) {
|
||||||
|
emit_text(s, s->buf, s->len);
|
||||||
|
s->len = 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#include "cst_agent.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define MODEL_MAX_LEN 128
|
||||||
|
|
||||||
|
static int already(char out[][MODEL_MAX_LEN], int n, const char *name) {
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (strcmp(out[i], name) == 0)
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int push(char out[][MODEL_MAX_LEN], int *n, int max, const char *name) {
|
||||||
|
if (!name || !name[0] || *n >= max)
|
||||||
|
return 0;
|
||||||
|
if (already(out, *n, name))
|
||||||
|
return 0;
|
||||||
|
snprintf(out[*n], MODEL_MAX_LEN, "%s", name);
|
||||||
|
(*n)++;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_models_list(const char *current, char out[][128], int max_out) {
|
||||||
|
if (!out || max_out <= 0)
|
||||||
|
return 0;
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
const char *env = getenv("CST_MODELS");
|
||||||
|
if (env && env[0]) {
|
||||||
|
char buf[1024];
|
||||||
|
snprintf(buf, sizeof(buf), "%s", env);
|
||||||
|
char *p = buf;
|
||||||
|
while (*p && n < max_out) {
|
||||||
|
while (*p == ':' || *p == ' ' || *p == ',')
|
||||||
|
p++;
|
||||||
|
if (!*p)
|
||||||
|
break;
|
||||||
|
char *start = p;
|
||||||
|
while (*p && *p != ':' && *p != ',')
|
||||||
|
p++;
|
||||||
|
char save = *p;
|
||||||
|
*p = 0;
|
||||||
|
/* trim trailing spaces */
|
||||||
|
char *end = start + strlen(start);
|
||||||
|
while (end > start && (end[-1] == ' ' || end[-1] == '\t'))
|
||||||
|
*--end = 0;
|
||||||
|
push(out, &n, max_out, start);
|
||||||
|
*p = save;
|
||||||
|
if (*p)
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
push(out, &n, max_out, "MiniMax-M2.7");
|
||||||
|
push(out, &n, max_out, "MiniMax-M2.5");
|
||||||
|
push(out, &n, max_out, "gpt-4o");
|
||||||
|
push(out, &n, max_out, "gpt-4o-mini");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current && current[0])
|
||||||
|
push(out, &n, max_out, current);
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
+584
@@ -0,0 +1,584 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#include <direct.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <process.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void os_run_result_free(os_run_result_t *r) {
|
||||||
|
if (!r)
|
||||||
|
return;
|
||||||
|
free(r->out);
|
||||||
|
r->out = NULL;
|
||||||
|
r->out_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_path_is_abs(const char *p) {
|
||||||
|
if (!p || !p[0])
|
||||||
|
return 0;
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (p[0] == '/' || p[0] == '\\')
|
||||||
|
return 1;
|
||||||
|
if (((p[0] >= 'A' && p[0] <= 'Z') || (p[0] >= 'a' && p[0] <= 'z')) &&
|
||||||
|
p[1] == ':' && (p[2] == '/' || p[2] == '\\' || p[2] == '\0'))
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
return p[0] == '/';
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void os_path_norm_slashes(char *p) {
|
||||||
|
if (!p)
|
||||||
|
return;
|
||||||
|
for (; *p; p++) {
|
||||||
|
if (*p == '\\')
|
||||||
|
*p = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_path_join(char *out, size_t out_sz, const char *a, const char *b) {
|
||||||
|
if (!out || out_sz < 2 || !a)
|
||||||
|
return -1;
|
||||||
|
if (!b || !b[0]) {
|
||||||
|
snprintf(out, out_sz, "%s", a);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (os_path_is_abs(b)) {
|
||||||
|
snprintf(out, out_sz, "%s", b);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
size_t al = strlen(a);
|
||||||
|
while (al > 0 && (a[al - 1] == '/' || a[al - 1] == '\\'))
|
||||||
|
al--;
|
||||||
|
#ifdef _WIN32
|
||||||
|
int n = snprintf(out, out_sz, "%.*s\\%s", (int)al, a, b);
|
||||||
|
#else
|
||||||
|
int n = snprintf(out, out_sz, "%.*s/%s", (int)al, a, b);
|
||||||
|
#endif
|
||||||
|
if (n < 0 || (size_t)n >= out_sz)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_realpath(const char *path, char *out, size_t out_sz) {
|
||||||
|
if (!path || !out || out_sz < 2)
|
||||||
|
return -1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
char buf[OS_PATH_MAX];
|
||||||
|
DWORD n = GetFullPathNameA(path, (DWORD)sizeof(buf), buf, NULL);
|
||||||
|
if (n == 0 || n >= sizeof(buf))
|
||||||
|
return -1;
|
||||||
|
/* Prefer forward slashes internally for sandbox compares. */
|
||||||
|
os_path_norm_slashes(buf);
|
||||||
|
if (strlen(buf) + 1 > out_sz)
|
||||||
|
return -1;
|
||||||
|
memcpy(out, buf, strlen(buf) + 1);
|
||||||
|
/* Lowercase drive letter for stable compares */
|
||||||
|
if (out[0] && out[1] == ':') {
|
||||||
|
if (out[0] >= 'A' && out[0] <= 'Z')
|
||||||
|
out[0] = (char)(out[0] - 'A' + 'a');
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
char *r = realpath(path, NULL);
|
||||||
|
if (!r)
|
||||||
|
return -1;
|
||||||
|
if (strlen(r) + 1 > out_sz) {
|
||||||
|
free(r);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(out, r, strlen(r) + 1);
|
||||||
|
free(r);
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_path_under(const char *root, const char *path) {
|
||||||
|
if (!root || !path)
|
||||||
|
return 0;
|
||||||
|
size_t rl = strlen(root);
|
||||||
|
if (strncmp(path, root, rl) != 0)
|
||||||
|
return 0;
|
||||||
|
if (path[rl] == '\0')
|
||||||
|
return 1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
return path[rl] == '/' || path[rl] == '\\';
|
||||||
|
#else
|
||||||
|
return path[rl] == '/';
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_mkdir(const char *path, int mode) {
|
||||||
|
if (!path)
|
||||||
|
return -1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
(void)mode;
|
||||||
|
if (_mkdir(path) != 0 && errno != EEXIST)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
if (mkdir(path, (mode_t)mode) != 0 && errno != EEXIST)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_config_dir(char *out, size_t out_sz) {
|
||||||
|
if (!out || out_sz < 8)
|
||||||
|
return -1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
const char *base = getenv("APPDATA");
|
||||||
|
if (!base || !base[0])
|
||||||
|
base = getenv("LOCALAPPDATA");
|
||||||
|
if (!base || !base[0])
|
||||||
|
return -1;
|
||||||
|
int n = snprintf(out, out_sz, "%s\\cst-agent", base);
|
||||||
|
#else
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
if (!home || !home[0])
|
||||||
|
return -1;
|
||||||
|
int n = snprintf(out, out_sz, "%s/.config/cst-agent", home);
|
||||||
|
#endif
|
||||||
|
if (n < 0 || (size_t)n >= out_sz)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_temp_dir(char *out, size_t out_sz) {
|
||||||
|
if (!out || out_sz < 2)
|
||||||
|
return -1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
DWORD n = GetTempPathA((DWORD)out_sz, out);
|
||||||
|
if (n == 0 || n >= out_sz)
|
||||||
|
return -1;
|
||||||
|
os_path_norm_slashes(out);
|
||||||
|
size_t L = strlen(out);
|
||||||
|
while (L > 0 && out[L - 1] == '/')
|
||||||
|
out[--L] = 0;
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
const char *t = getenv("TMPDIR");
|
||||||
|
if (!t || !t[0])
|
||||||
|
t = "/tmp";
|
||||||
|
snprintf(out, out_sz, "%s", t);
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_mkstemp(char *out, size_t out_sz, const char *prefix) {
|
||||||
|
if (!out || out_sz < 16)
|
||||||
|
return -1;
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
if (os_temp_dir(dir, sizeof(dir)) != 0)
|
||||||
|
return -1;
|
||||||
|
if (!prefix)
|
||||||
|
prefix = "cst";
|
||||||
|
#ifdef _WIN32
|
||||||
|
char name[MAX_PATH];
|
||||||
|
if (GetTempFileNameA(dir, prefix, 0, name) == 0)
|
||||||
|
return -1;
|
||||||
|
os_path_norm_slashes(name);
|
||||||
|
if (strlen(name) + 1 > out_sz)
|
||||||
|
return -1;
|
||||||
|
memcpy(out, name, strlen(name) + 1);
|
||||||
|
int fd = _open(out, _O_RDWR | _O_BINARY);
|
||||||
|
return fd;
|
||||||
|
#else
|
||||||
|
int n = snprintf(out, out_sz, "%s/%s-XXXXXX", dir, prefix);
|
||||||
|
if (n < 0 || (size_t)n >= out_sz)
|
||||||
|
return -1;
|
||||||
|
int fd = mkstemp(out);
|
||||||
|
return fd;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: main() may set this for Termux / argv0 fallback when
|
||||||
|
* /proc/self/exe is unavailable. */
|
||||||
|
static char g_argv0_dir[OS_PATH_MAX];
|
||||||
|
|
||||||
|
void os_set_prog_path(const char *argv0) {
|
||||||
|
g_argv0_dir[0] = 0;
|
||||||
|
if (!argv0 || !argv0[0])
|
||||||
|
return;
|
||||||
|
char buf[OS_PATH_MAX];
|
||||||
|
snprintf(buf, sizeof(buf), "%s", argv0);
|
||||||
|
os_path_norm_slashes(buf);
|
||||||
|
char *slash = strrchr(buf, '/');
|
||||||
|
if (!slash)
|
||||||
|
return;
|
||||||
|
*slash = 0;
|
||||||
|
/* Relative "./foo" → keep as-is; caller may pass absolute. */
|
||||||
|
snprintf(g_argv0_dir, sizeof(g_argv0_dir), "%s", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_exe_dir(char *out, size_t out_sz) {
|
||||||
|
if (!out || out_sz < 2)
|
||||||
|
return -1;
|
||||||
|
const char *env = getenv("CST_EXE_DIR");
|
||||||
|
if (env && env[0]) {
|
||||||
|
snprintf(out, out_sz, "%s", env);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#ifdef _WIN32
|
||||||
|
char buf[OS_PATH_MAX];
|
||||||
|
DWORD n = GetModuleFileNameA(NULL, buf, (DWORD)sizeof(buf));
|
||||||
|
if (n == 0 || n >= sizeof(buf))
|
||||||
|
return -1;
|
||||||
|
os_path_norm_slashes(buf);
|
||||||
|
char *slash = strrchr(buf, '/');
|
||||||
|
if (!slash)
|
||||||
|
return -1;
|
||||||
|
*slash = 0;
|
||||||
|
if (strlen(buf) + 1 > out_sz)
|
||||||
|
return -1;
|
||||||
|
memcpy(out, buf, strlen(buf) + 1);
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
char buf[OS_PATH_MAX];
|
||||||
|
ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
||||||
|
if (n >= 0) {
|
||||||
|
buf[n] = 0;
|
||||||
|
char *slash = strrchr(buf, '/');
|
||||||
|
if (slash) {
|
||||||
|
*slash = 0;
|
||||||
|
if (strlen(buf) + 1 <= out_sz) {
|
||||||
|
memcpy(out, buf, strlen(buf) + 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Termux / restricted Android: fall back to argv0 dir. */
|
||||||
|
if (g_argv0_dir[0]) {
|
||||||
|
snprintf(out, out_sz, "%s", g_argv0_dir);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static int file_exists(const char *p) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
DWORD a = GetFileAttributesA(p);
|
||||||
|
return a != INVALID_FILE_ATTRIBUTES && !(a & FILE_ATTRIBUTE_DIRECTORY);
|
||||||
|
#else
|
||||||
|
return access(p, X_OK) == 0 || access(p, F_OK) == 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_resolve_shell(char *out, size_t out_sz) {
|
||||||
|
if (!out || out_sz < 2)
|
||||||
|
return -1;
|
||||||
|
const char *env = getenv("CST_SHELL");
|
||||||
|
if (env && env[0]) {
|
||||||
|
snprintf(out, out_sz, "%s", env);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char dir[OS_PATH_MAX];
|
||||||
|
if (os_exe_dir(dir, sizeof(dir)) == 0) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
char cand[OS_PATH_MAX];
|
||||||
|
if (os_path_join(cand, sizeof(cand), dir, "busybox.exe") == 0 &&
|
||||||
|
file_exists(cand)) {
|
||||||
|
snprintf(out, out_sz, "%s", cand);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
char cand[OS_PATH_MAX];
|
||||||
|
if (os_path_join(cand, sizeof(cand), dir, "busybox") == 0 &&
|
||||||
|
file_exists(cand)) {
|
||||||
|
snprintf(out, out_sz, "%s", cand);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#ifdef _WIN32
|
||||||
|
snprintf(out, out_sz, "%s", "busybox.exe");
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
/* Termux: $PREFIX/bin/sh */
|
||||||
|
{
|
||||||
|
const char *prefix = getenv("PREFIX");
|
||||||
|
if (prefix && prefix[0]) {
|
||||||
|
char cand[OS_PATH_MAX];
|
||||||
|
if (os_path_join(cand, sizeof(cand), prefix, "bin/sh") == 0 &&
|
||||||
|
file_exists(cand)) {
|
||||||
|
snprintf(out, out_sz, "%s", cand);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file_exists("/bin/sh")) {
|
||||||
|
snprintf(out, out_sz, "%s", "/bin/sh");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (file_exists("/system/bin/sh")) {
|
||||||
|
snprintf(out, out_sz, "%s", "/system/bin/sh");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
snprintf(out, out_sz, "%s", "sh");
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
||||||
|
os_run_result_t *r) {
|
||||||
|
if (!argv || !argv[0] || !r)
|
||||||
|
return -1;
|
||||||
|
if (max_out == 0)
|
||||||
|
max_out = 512 * 1024;
|
||||||
|
memset(r, 0, sizeof(*r));
|
||||||
|
|
||||||
|
char cmdline[8192];
|
||||||
|
size_t pos = 0;
|
||||||
|
cmdline[0] = 0;
|
||||||
|
for (int i = 0; argv[i]; i++) {
|
||||||
|
const char *a = argv[i];
|
||||||
|
int need_q = strchr(a, ' ') || strchr(a, '\t') || strchr(a, '"');
|
||||||
|
if (i && pos + 1 < sizeof(cmdline))
|
||||||
|
cmdline[pos++] = ' ';
|
||||||
|
if (need_q) {
|
||||||
|
if (pos + 1 < sizeof(cmdline))
|
||||||
|
cmdline[pos++] = '"';
|
||||||
|
for (const char *p = a; *p && pos + 2 < sizeof(cmdline); p++) {
|
||||||
|
if (*p == '"') {
|
||||||
|
cmdline[pos++] = '\\';
|
||||||
|
cmdline[pos++] = '"';
|
||||||
|
} else {
|
||||||
|
cmdline[pos++] = *p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos + 1 < sizeof(cmdline))
|
||||||
|
cmdline[pos++] = '"';
|
||||||
|
} else {
|
||||||
|
size_t L = strlen(a);
|
||||||
|
if (pos + L >= sizeof(cmdline))
|
||||||
|
break;
|
||||||
|
memcpy(cmdline + pos, a, L);
|
||||||
|
pos += L;
|
||||||
|
}
|
||||||
|
cmdline[pos] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SECURITY_ATTRIBUTES sa = {0};
|
||||||
|
sa.nLength = sizeof(sa);
|
||||||
|
sa.bInheritHandle = TRUE;
|
||||||
|
HANDLE rd = NULL, wr = NULL;
|
||||||
|
if (!CreatePipe(&rd, &wr, &sa, 0))
|
||||||
|
return -1;
|
||||||
|
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
||||||
|
|
||||||
|
STARTUPINFOA si = {0};
|
||||||
|
si.cb = sizeof(si);
|
||||||
|
si.dwFlags = STARTF_USESTDHANDLES;
|
||||||
|
si.hStdOutput = wr;
|
||||||
|
si.hStdError = wr;
|
||||||
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
PROCESS_INFORMATION pi = {0};
|
||||||
|
|
||||||
|
char *cmd_mut = _strdup(cmdline);
|
||||||
|
if (!cmd_mut) {
|
||||||
|
CloseHandle(rd);
|
||||||
|
CloseHandle(wr);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
BOOL ok = CreateProcessA(NULL, cmd_mut, NULL, NULL, TRUE, 0, NULL,
|
||||||
|
cwd && cwd[0] ? cwd : NULL, &si, &pi);
|
||||||
|
free(cmd_mut);
|
||||||
|
CloseHandle(wr);
|
||||||
|
if (!ok) {
|
||||||
|
CloseHandle(rd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t cap = 4096, len = 0;
|
||||||
|
char *buf = malloc(cap);
|
||||||
|
if (!buf) {
|
||||||
|
CloseHandle(rd);
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
char tmp[1024];
|
||||||
|
DWORD n = 0;
|
||||||
|
if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0)
|
||||||
|
break;
|
||||||
|
if (len + n + 1 > cap) {
|
||||||
|
size_t nc = cap * 2;
|
||||||
|
if (nc < len + n + 1)
|
||||||
|
nc = len + n + 1;
|
||||||
|
char *nb = realloc(buf, nc);
|
||||||
|
if (!nb)
|
||||||
|
break;
|
||||||
|
buf = nb;
|
||||||
|
cap = nc;
|
||||||
|
}
|
||||||
|
memcpy(buf + len, tmp, n);
|
||||||
|
len += n;
|
||||||
|
if (len >= max_out)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
CloseHandle(rd);
|
||||||
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||||
|
DWORD code = 1;
|
||||||
|
GetExitCodeProcess(pi.hProcess, &code);
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
buf[len] = 0;
|
||||||
|
r->out = buf;
|
||||||
|
r->out_len = len;
|
||||||
|
r->exit_code = (int)code;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
||||||
|
os_run_result_t *r) {
|
||||||
|
if (!argv || !argv[0] || !r)
|
||||||
|
return -1;
|
||||||
|
if (max_out == 0)
|
||||||
|
max_out = 512 * 1024;
|
||||||
|
memset(r, 0, sizeof(*r));
|
||||||
|
|
||||||
|
int pipefd[2];
|
||||||
|
if (pipe(pipefd) != 0)
|
||||||
|
return -1;
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
close(pipefd[0]);
|
||||||
|
close(pipefd[1]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (pid == 0) {
|
||||||
|
close(pipefd[0]);
|
||||||
|
dup2(pipefd[1], STDOUT_FILENO);
|
||||||
|
dup2(pipefd[1], STDERR_FILENO);
|
||||||
|
close(pipefd[1]);
|
||||||
|
if (cwd && cwd[0] && chdir(cwd) != 0)
|
||||||
|
_exit(127);
|
||||||
|
execvp(argv[0], (char *const *)argv);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
close(pipefd[1]);
|
||||||
|
size_t cap = 4096, len = 0;
|
||||||
|
char *buf = malloc(cap);
|
||||||
|
if (!buf) {
|
||||||
|
close(pipefd[0]);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
if (len + 1024 > cap) {
|
||||||
|
cap *= 2;
|
||||||
|
char *nb = realloc(buf, cap);
|
||||||
|
if (!nb)
|
||||||
|
break;
|
||||||
|
buf = nb;
|
||||||
|
}
|
||||||
|
ssize_t n = read(pipefd[0], buf + len, 1024);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (n == 0)
|
||||||
|
break;
|
||||||
|
len += (size_t)n;
|
||||||
|
if (len >= max_out)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
close(pipefd[0]);
|
||||||
|
int status = 0;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
buf[len] = 0;
|
||||||
|
r->out = buf;
|
||||||
|
r->out_len = len;
|
||||||
|
r->exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out,
|
||||||
|
os_run_result_t *r) {
|
||||||
|
if (!cmd || !r)
|
||||||
|
return -1;
|
||||||
|
char shell[OS_PATH_MAX];
|
||||||
|
if (os_resolve_shell(shell, sizeof(shell)) != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* busybox: busybox sh -c cmd ; /bin/sh: sh -c cmd */
|
||||||
|
int is_busybox = 0;
|
||||||
|
{
|
||||||
|
const char *base = shell;
|
||||||
|
const char *s = strrchr(shell, '/');
|
||||||
|
#ifdef _WIN32
|
||||||
|
const char *s2 = strrchr(shell, '\\');
|
||||||
|
if (s2 && (!s || s2 > s))
|
||||||
|
s = s2;
|
||||||
|
#endif
|
||||||
|
if (s)
|
||||||
|
base = s + 1;
|
||||||
|
if (strncmp(base, "busybox", 7) == 0)
|
||||||
|
is_busybox = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_busybox) {
|
||||||
|
const char *argv[] = {shell, "sh", "-c", cmd, NULL};
|
||||||
|
return os_run_capture(cwd, argv, max_out, r);
|
||||||
|
} else {
|
||||||
|
const char *argv[] = {shell, "-c", cmd, NULL};
|
||||||
|
return os_run_capture(cwd, argv, max_out, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void os_localtime(const time_t *t, struct tm *out) {
|
||||||
|
if (!t || !out)
|
||||||
|
return;
|
||||||
|
#ifdef _WIN32
|
||||||
|
localtime_s(out, t);
|
||||||
|
#else
|
||||||
|
localtime_r(t, out);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_getpid(void) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return (int)GetCurrentProcessId();
|
||||||
|
#else
|
||||||
|
return (int)getpid();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int os_chmod_private(const char *path) {
|
||||||
|
if (!path)
|
||||||
|
return -1;
|
||||||
|
#ifdef _WIN32
|
||||||
|
(void)path;
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
return chmod(path, 0600);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "cst_agent.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
int cst_sandbox_resolve(const char *workspace_root, const char *rel_or_abs,
|
||||||
|
char *out, size_t out_sz) {
|
||||||
|
if (!workspace_root || !rel_or_abs || !out || out_sz < 2)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
char root_real[OS_PATH_MAX];
|
||||||
|
if (os_realpath(workspace_root, root_real, sizeof(root_real)) != 0)
|
||||||
|
return -1;
|
||||||
|
os_path_norm_slashes(root_real);
|
||||||
|
size_t root_len = strlen(root_real);
|
||||||
|
|
||||||
|
char candidate[OS_PATH_MAX];
|
||||||
|
if (os_path_is_abs(rel_or_abs)) {
|
||||||
|
snprintf(candidate, sizeof(candidate), "%s", rel_or_abs);
|
||||||
|
} else if (os_path_join(candidate, sizeof(candidate), root_real, rel_or_abs) !=
|
||||||
|
0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
os_path_norm_slashes(candidate);
|
||||||
|
|
||||||
|
char resolved[OS_PATH_MAX];
|
||||||
|
if (os_realpath(candidate, resolved, sizeof(resolved)) == 0) {
|
||||||
|
os_path_norm_slashes(resolved);
|
||||||
|
if (!os_path_under(root_real, resolved))
|
||||||
|
return -1;
|
||||||
|
if (strlen(resolved) + 1 > out_sz)
|
||||||
|
return -1;
|
||||||
|
memcpy(out, resolved, strlen(resolved) + 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parent exists, leaf may be new file. */
|
||||||
|
char tmp[OS_PATH_MAX];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%s", candidate);
|
||||||
|
char *slash = strrchr(tmp, '/');
|
||||||
|
if (!slash)
|
||||||
|
return -1;
|
||||||
|
*slash = 0;
|
||||||
|
const char *base = slash + 1;
|
||||||
|
char parent_real[OS_PATH_MAX];
|
||||||
|
if (os_realpath(tmp, parent_real, sizeof(parent_real)) != 0)
|
||||||
|
return -1;
|
||||||
|
os_path_norm_slashes(parent_real);
|
||||||
|
if (!os_path_under(root_real, parent_real) &&
|
||||||
|
strcmp(parent_real, root_real) != 0)
|
||||||
|
return -1;
|
||||||
|
(void)root_len;
|
||||||
|
int n = snprintf(out, out_sz, "%s/%s", parent_real, base);
|
||||||
|
if (n < 0 || (size_t)n >= out_sz)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+363
@@ -0,0 +1,363 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "cst_agent.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
typedef long long cst_ssize_t;
|
||||||
|
static cst_ssize_t cst_getline(char **lineptr, size_t *n, FILE *stream) {
|
||||||
|
if (!lineptr || !n || !stream)
|
||||||
|
return -1;
|
||||||
|
if (!*lineptr || *n == 0) {
|
||||||
|
*n = 256;
|
||||||
|
*lineptr = (char *)malloc(*n);
|
||||||
|
if (!*lineptr)
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
size_t len = 0;
|
||||||
|
int c = 0;
|
||||||
|
while ((c = fgetc(stream)) != EOF) {
|
||||||
|
if (len + 2 > *n) {
|
||||||
|
size_t nn = (*n) * 2;
|
||||||
|
char *nb = (char *)realloc(*lineptr, nn);
|
||||||
|
if (!nb)
|
||||||
|
return -1;
|
||||||
|
*lineptr = nb;
|
||||||
|
*n = nn;
|
||||||
|
}
|
||||||
|
(*lineptr)[len++] = (char)c;
|
||||||
|
if (c == '\n')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (len == 0 && c == EOF)
|
||||||
|
return -1;
|
||||||
|
(*lineptr)[len] = 0;
|
||||||
|
return (cst_ssize_t)len;
|
||||||
|
}
|
||||||
|
#define getline cst_getline
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct cst_session {
|
||||||
|
char path[4096];
|
||||||
|
cst_message_t *msgs;
|
||||||
|
int nmsgs;
|
||||||
|
int cap;
|
||||||
|
FILE *fp;
|
||||||
|
};
|
||||||
|
|
||||||
|
static char *xstrdup(const char *s) {
|
||||||
|
if (!s)
|
||||||
|
return NULL;
|
||||||
|
size_t n = strlen(s);
|
||||||
|
char *p = malloc(n + 1);
|
||||||
|
if (!p)
|
||||||
|
return NULL;
|
||||||
|
memcpy(p, s, n + 1);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *role_str(cst_role_t r) {
|
||||||
|
switch (r) {
|
||||||
|
case CST_ROLE_SYSTEM:
|
||||||
|
return "system";
|
||||||
|
case CST_ROLE_USER:
|
||||||
|
return "user";
|
||||||
|
case CST_ROLE_ASSISTANT:
|
||||||
|
return "assistant";
|
||||||
|
case CST_ROLE_TOOL:
|
||||||
|
return "tool";
|
||||||
|
}
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
static cst_role_t role_from(const char *s) {
|
||||||
|
if (strcmp(s, "system") == 0)
|
||||||
|
return CST_ROLE_SYSTEM;
|
||||||
|
if (strcmp(s, "assistant") == 0)
|
||||||
|
return CST_ROLE_ASSISTANT;
|
||||||
|
if (strcmp(s, "tool") == 0)
|
||||||
|
return CST_ROLE_TOOL;
|
||||||
|
return CST_ROLE_USER;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int json_escape_write(FILE *fp, const char *s) {
|
||||||
|
fputc('"', fp);
|
||||||
|
for (; s && *s; s++) {
|
||||||
|
unsigned char c = (unsigned char)*s;
|
||||||
|
if (c == '"' || c == '\\') {
|
||||||
|
fputc('\\', fp);
|
||||||
|
fputc(c, fp);
|
||||||
|
} else if (c == '\n') {
|
||||||
|
fputs("\\n", fp);
|
||||||
|
} else if (c == '\r') {
|
||||||
|
fputs("\\r", fp);
|
||||||
|
} else if (c == '\t') {
|
||||||
|
fputs("\\t", fp);
|
||||||
|
} else if (c < 0x20) {
|
||||||
|
fprintf(fp, "\\u%04x", c);
|
||||||
|
} else {
|
||||||
|
fputc(c, fp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fputc('"', fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minimal extract "key":"value" from a single JSONL object line. */
|
||||||
|
static int extract_str(const char *line, const char *key, char *out,
|
||||||
|
size_t out_sz) {
|
||||||
|
char pat[64];
|
||||||
|
snprintf(pat, sizeof(pat), "\"%s\":\"", key);
|
||||||
|
const char *p = strstr(line, pat);
|
||||||
|
if (!p)
|
||||||
|
return -1;
|
||||||
|
p += strlen(pat);
|
||||||
|
size_t o = 0;
|
||||||
|
while (*p && o + 1 < out_sz) {
|
||||||
|
if (*p == '\\' && p[1]) {
|
||||||
|
p++;
|
||||||
|
if (*p == 'n')
|
||||||
|
out[o++] = '\n';
|
||||||
|
else if (*p == 'r')
|
||||||
|
out[o++] = '\r';
|
||||||
|
else if (*p == 't')
|
||||||
|
out[o++] = '\t';
|
||||||
|
else
|
||||||
|
out[o++] = *p;
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
out[o] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
out[o++] = *p++;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int session_grow(cst_session_t *s) {
|
||||||
|
if (s->nmsgs < s->cap)
|
||||||
|
return 0;
|
||||||
|
int ncap = s->cap ? s->cap * 2 : 32;
|
||||||
|
cst_message_t *nm = realloc(s->msgs, (size_t)ncap * sizeof(cst_message_t));
|
||||||
|
if (!nm)
|
||||||
|
return -1;
|
||||||
|
s->msgs = nm;
|
||||||
|
s->cap = ncap;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int session_push_mem(cst_session_t *s, cst_role_t role, const char *content,
|
||||||
|
const char *tool_id, const char *tool_name,
|
||||||
|
const char *tool_calls_json) {
|
||||||
|
if (session_grow(s) != 0)
|
||||||
|
return -1;
|
||||||
|
cst_message_t *d = &s->msgs[s->nmsgs];
|
||||||
|
memset(d, 0, sizeof(*d));
|
||||||
|
d->role = role;
|
||||||
|
d->content = xstrdup(content ? content : "");
|
||||||
|
if (!d->content)
|
||||||
|
return -1;
|
||||||
|
if (tool_id)
|
||||||
|
snprintf(d->tool_call_id, sizeof(d->tool_call_id), "%s", tool_id);
|
||||||
|
if (tool_name)
|
||||||
|
snprintf(d->tool_name, sizeof(d->tool_name), "%s", tool_name);
|
||||||
|
if (tool_calls_json && tool_calls_json[0])
|
||||||
|
d->tool_calls_json = xstrdup(tool_calls_json);
|
||||||
|
s->nmsgs++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void load_existing(cst_session_t *s) {
|
||||||
|
FILE *rp = fopen(s->path, "r");
|
||||||
|
if (!rp)
|
||||||
|
return;
|
||||||
|
char *line = NULL;
|
||||||
|
size_t cap = 0;
|
||||||
|
while (getline(&line, &cap, rp) != -1) {
|
||||||
|
char role[32] = {0};
|
||||||
|
char *content = malloc(cap > 8 ? cap : 4096);
|
||||||
|
char tid[CTL_MAX_ID] = {0};
|
||||||
|
char tname[CTL_MAX_NAME] = {0};
|
||||||
|
if (!content)
|
||||||
|
continue;
|
||||||
|
content[0] = 0;
|
||||||
|
if (extract_str(line, "role", role, sizeof(role)) != 0) {
|
||||||
|
free(content);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* content may be large — reuse line extract into growing buffer */
|
||||||
|
free(content);
|
||||||
|
content = malloc(strlen(line) + 1);
|
||||||
|
if (!content)
|
||||||
|
continue;
|
||||||
|
if (extract_str(line, "content", content, strlen(line) + 1) != 0) {
|
||||||
|
free(content);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
extract_str(line, "tool_call_id", tid, sizeof(tid));
|
||||||
|
extract_str(line, "tool_name", tname, sizeof(tname));
|
||||||
|
char *tcj = malloc(strlen(line) + 1);
|
||||||
|
const char *tcjp = NULL;
|
||||||
|
if (tcj) {
|
||||||
|
tcj[0] = 0;
|
||||||
|
if (extract_str(line, "tool_calls_json", tcj, strlen(line) + 1) == 0)
|
||||||
|
tcjp = tcj;
|
||||||
|
}
|
||||||
|
session_push_mem(s, role_from(role), content, tid[0] ? tid : NULL,
|
||||||
|
tname[0] ? tname : NULL, tcjp);
|
||||||
|
free(tcj);
|
||||||
|
free(content);
|
||||||
|
}
|
||||||
|
free(line);
|
||||||
|
fclose(rp);
|
||||||
|
}
|
||||||
|
|
||||||
|
cst_session_t *cst_session_open(const char *path) {
|
||||||
|
cst_session_t *s = calloc(1, sizeof(*s));
|
||||||
|
if (!s)
|
||||||
|
return NULL;
|
||||||
|
snprintf(s->path, sizeof(s->path), "%s", path ? path : "session.jsonl");
|
||||||
|
load_existing(s);
|
||||||
|
s->fp = fopen(s->path, "a+");
|
||||||
|
if (!s->fp) {
|
||||||
|
for (int i = 0; i < s->nmsgs; i++) {
|
||||||
|
free(s->msgs[i].content);
|
||||||
|
free(s->msgs[i].tool_calls_json);
|
||||||
|
}
|
||||||
|
free(s->msgs);
|
||||||
|
free(s);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (!s->msgs) {
|
||||||
|
s->cap = 32;
|
||||||
|
s->msgs = calloc((size_t)s->cap, sizeof(cst_message_t));
|
||||||
|
if (!s->msgs) {
|
||||||
|
fclose(s->fp);
|
||||||
|
free(s);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cst_session_close(cst_session_t *s) {
|
||||||
|
if (!s)
|
||||||
|
return;
|
||||||
|
for (int i = 0; i < s->nmsgs; i++) {
|
||||||
|
free(s->msgs[i].content);
|
||||||
|
free(s->msgs[i].tool_calls_json);
|
||||||
|
}
|
||||||
|
free(s->msgs);
|
||||||
|
if (s->fp)
|
||||||
|
fclose(s->fp);
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_session_append(cst_session_t *s, const cst_message_t *msg) {
|
||||||
|
if (!s || !msg)
|
||||||
|
return -1;
|
||||||
|
if (session_push_mem(s, msg->role, msg->content, msg->tool_call_id,
|
||||||
|
msg->tool_name, msg->tool_calls_json) != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
fprintf(s->fp, "{\"role\":");
|
||||||
|
json_escape_write(s->fp, role_str(msg->role));
|
||||||
|
fprintf(s->fp, ",\"content\":");
|
||||||
|
json_escape_write(s->fp, msg->content ? msg->content : "");
|
||||||
|
if (msg->tool_call_id[0]) {
|
||||||
|
fprintf(s->fp, ",\"tool_call_id\":");
|
||||||
|
json_escape_write(s->fp, msg->tool_call_id);
|
||||||
|
}
|
||||||
|
if (msg->tool_name[0]) {
|
||||||
|
fprintf(s->fp, ",\"tool_name\":");
|
||||||
|
json_escape_write(s->fp, msg->tool_name);
|
||||||
|
}
|
||||||
|
if (msg->tool_calls_json && msg->tool_calls_json[0]) {
|
||||||
|
fprintf(s->fp, ",\"tool_calls_json\":");
|
||||||
|
json_escape_write(s->fp, msg->tool_calls_json);
|
||||||
|
}
|
||||||
|
fprintf(s->fp, "}\n");
|
||||||
|
fflush(s->fp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_session_count(const cst_session_t *s) {
|
||||||
|
return s ? s->nmsgs : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cst_message_t *cst_session_get(const cst_session_t *s, int i) {
|
||||||
|
if (!s || i < 0 || i >= s->nmsgs)
|
||||||
|
return NULL;
|
||||||
|
return &s->msgs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *cst_session_path(const cst_session_t *s) {
|
||||||
|
return s ? s->path : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_session_clear(cst_session_t *s, int keep_system) {
|
||||||
|
if (!s)
|
||||||
|
return -1;
|
||||||
|
cst_message_t sys = {0};
|
||||||
|
int have_sys = 0;
|
||||||
|
if (keep_system) {
|
||||||
|
for (int i = 0; i < s->nmsgs; i++) {
|
||||||
|
if (s->msgs[i].role == CST_ROLE_SYSTEM) {
|
||||||
|
sys.role = CST_ROLE_SYSTEM;
|
||||||
|
sys.content = xstrdup(s->msgs[i].content ? s->msgs[i].content : "");
|
||||||
|
have_sys = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < s->nmsgs; i++) {
|
||||||
|
free(s->msgs[i].content);
|
||||||
|
free(s->msgs[i].tool_calls_json);
|
||||||
|
}
|
||||||
|
s->nmsgs = 0;
|
||||||
|
if (s->fp) {
|
||||||
|
fclose(s->fp);
|
||||||
|
s->fp = NULL;
|
||||||
|
}
|
||||||
|
s->fp = fopen(s->path, "w+");
|
||||||
|
if (!s->fp) {
|
||||||
|
if (have_sys)
|
||||||
|
free(sys.content);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (have_sys) {
|
||||||
|
cst_session_append(s, &sys);
|
||||||
|
free(sys.content);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n) {
|
||||||
|
if (!s || n < 0)
|
||||||
|
return -1;
|
||||||
|
for (int i = 0; i < s->nmsgs; i++) {
|
||||||
|
free(s->msgs[i].content);
|
||||||
|
free(s->msgs[i].tool_calls_json);
|
||||||
|
}
|
||||||
|
s->nmsgs = 0;
|
||||||
|
if (s->fp) {
|
||||||
|
fclose(s->fp);
|
||||||
|
s->fp = NULL;
|
||||||
|
}
|
||||||
|
s->fp = fopen(s->path, "w+");
|
||||||
|
if (!s->fp)
|
||||||
|
return -1;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (cst_session_append(s, &msgs[i]) != 0)
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,784 @@
|
|||||||
|
#define _DEFAULT_SOURCE
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "cst_agent.h"
|
||||||
|
#include "os_compat.h"
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <unistd.h>
|
||||||
|
#else
|
||||||
|
#include <io.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void cst_tool_result_free(cst_tool_result_t *r) {
|
||||||
|
if (!r)
|
||||||
|
return;
|
||||||
|
free(r->out);
|
||||||
|
r->out = NULL;
|
||||||
|
r->out_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *arg_get(const ctl_tool_call_t *call, const char *key) {
|
||||||
|
for (int i = 0; i < call->nargs; i++) {
|
||||||
|
if (strcmp(call->args[i].key, key) == 0)
|
||||||
|
return call->args[i].value ? call->args[i].value : "";
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int set_result(cst_tool_result_t *r, const char *s, int code) {
|
||||||
|
cst_tool_result_free(r);
|
||||||
|
size_t n = s ? strlen(s) : 0;
|
||||||
|
r->out = malloc(n + 1);
|
||||||
|
if (!r->out)
|
||||||
|
return -1;
|
||||||
|
if (n)
|
||||||
|
memcpy(r->out, s, n);
|
||||||
|
r->out[n] = 0;
|
||||||
|
r->out_len = n;
|
||||||
|
r->exit_code = code;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read whole file into malloc'd buffer; *out_len set. Returns NULL if missing. */
|
||||||
|
static char *slurp_file(const char *full, size_t *out_len) {
|
||||||
|
*out_len = 0;
|
||||||
|
FILE *fp = fopen(full, "rb");
|
||||||
|
if (!fp)
|
||||||
|
return NULL;
|
||||||
|
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
long sz = ftell(fp);
|
||||||
|
if (sz < 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
rewind(fp);
|
||||||
|
char *buf = malloc((size_t)sz + 1);
|
||||||
|
if (!buf) {
|
||||||
|
fclose(fp);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t n = fread(buf, 1, (size_t)sz, fp);
|
||||||
|
fclose(fp);
|
||||||
|
buf[n] = 0;
|
||||||
|
*out_len = n;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define DIFF_OUT_CAP (48 * 1024)
|
||||||
|
|
||||||
|
/* OpenCode-style: always surface a unified diff after write/patch. */
|
||||||
|
static char *make_unified_diff(const char *relpath, const char *oldc, size_t olen,
|
||||||
|
const char *newc, size_t nlen) {
|
||||||
|
char t0[OS_PATH_MAX], t1[OS_PATH_MAX];
|
||||||
|
int fd0 = os_mkstemp(t0, sizeof(t0), "cstda");
|
||||||
|
int fd1 = os_mkstemp(t1, sizeof(t1), "cstdb");
|
||||||
|
if (fd0 < 0 || fd1 < 0) {
|
||||||
|
if (fd0 >= 0) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
_close(fd0);
|
||||||
|
_unlink(t0);
|
||||||
|
#else
|
||||||
|
close(fd0);
|
||||||
|
unlink(t0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (fd1 >= 0) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
_close(fd1);
|
||||||
|
_unlink(t1);
|
||||||
|
#else
|
||||||
|
close(fd1);
|
||||||
|
unlink(t1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (olen) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
(void)_write(fd0, oldc, (unsigned)olen);
|
||||||
|
#else
|
||||||
|
(void)write(fd0, oldc, olen);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if (nlen) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
(void)_write(fd1, newc, (unsigned)nlen);
|
||||||
|
#else
|
||||||
|
(void)write(fd1, newc, nlen);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#ifdef _WIN32
|
||||||
|
_close(fd0);
|
||||||
|
_close(fd1);
|
||||||
|
#else
|
||||||
|
close(fd0);
|
||||||
|
close(fd1);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const char *label = relpath && relpath[0] ? relpath : "file";
|
||||||
|
char cmd[OS_PATH_MAX * 2 + 256];
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"diff -u --label a/%s --label b/%s \"%s\" \"%s\"", label, label, t0,
|
||||||
|
t1);
|
||||||
|
|
||||||
|
os_run_result_t rr = {0};
|
||||||
|
int rc = os_run_shell_c(NULL, cmd, DIFF_OUT_CAP, &rr);
|
||||||
|
#ifdef _WIN32
|
||||||
|
_unlink(t0);
|
||||||
|
_unlink(t1);
|
||||||
|
#else
|
||||||
|
unlink(t0);
|
||||||
|
unlink(t1);
|
||||||
|
#endif
|
||||||
|
if (rc != 0) {
|
||||||
|
os_run_result_free(&rr);
|
||||||
|
return strdup("(no textual diff — diff unavailable)\n");
|
||||||
|
}
|
||||||
|
/* diff returns 1 when files differ — still success for our purposes */
|
||||||
|
if (!rr.out || rr.out_len == 0) {
|
||||||
|
os_run_result_free(&rr);
|
||||||
|
return strdup("(no textual diff — identical or binary)\n");
|
||||||
|
}
|
||||||
|
char *out = rr.out;
|
||||||
|
rr.out = NULL;
|
||||||
|
size_t len = rr.out_len;
|
||||||
|
os_run_result_free(&rr);
|
||||||
|
if (len >= DIFF_OUT_CAP - 64) {
|
||||||
|
snprintf(out + DIFF_OUT_CAP - 64, 64, "\n…(diff truncated)\n");
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int set_result_with_diff(cst_tool_result_t *r, const char *relpath,
|
||||||
|
const char *oldc, size_t olen, const char *newc,
|
||||||
|
size_t nlen, int code) {
|
||||||
|
char *diff = make_unified_diff(relpath, oldc ? oldc : "", olen, newc ? newc : "",
|
||||||
|
nlen);
|
||||||
|
char hdr[256];
|
||||||
|
snprintf(hdr, sizeof(hdr), "wrote %s (%zu bytes)\n",
|
||||||
|
relpath ? relpath : "?", nlen);
|
||||||
|
size_t hlen = strlen(hdr);
|
||||||
|
size_t dlen = diff ? strlen(diff) : 0;
|
||||||
|
char *buf = malloc(hlen + dlen + 1);
|
||||||
|
if (!buf) {
|
||||||
|
free(diff);
|
||||||
|
return set_result(r, "ok", code);
|
||||||
|
}
|
||||||
|
memcpy(buf, hdr, hlen);
|
||||||
|
if (diff && dlen)
|
||||||
|
memcpy(buf + hlen, diff, dlen);
|
||||||
|
buf[hlen + dlen] = 0;
|
||||||
|
free(diff);
|
||||||
|
cst_tool_result_free(r);
|
||||||
|
r->out = buf;
|
||||||
|
r->out_len = hlen + dlen;
|
||||||
|
r->exit_code = code;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_tool_read_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r) {
|
||||||
|
const char *path = arg_get(call, "path");
|
||||||
|
if (!path)
|
||||||
|
return set_result(r, "error: missing path", 1);
|
||||||
|
char full[4096];
|
||||||
|
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
|
||||||
|
return set_result(r, "error: path outside workspace", 1);
|
||||||
|
FILE *fp = fopen(full, "rb");
|
||||||
|
if (!fp)
|
||||||
|
return set_result(r, "error: cannot open file", 1);
|
||||||
|
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||||
|
fclose(fp);
|
||||||
|
return set_result(r, "error: seek failed", 1);
|
||||||
|
}
|
||||||
|
long sz = ftell(fp);
|
||||||
|
if (sz < 0 || sz > 2 * 1024 * 1024) {
|
||||||
|
fclose(fp);
|
||||||
|
return set_result(r, "error: file too large or empty seek", 1);
|
||||||
|
}
|
||||||
|
rewind(fp);
|
||||||
|
char *buf = malloc((size_t)sz + 1);
|
||||||
|
if (!buf) {
|
||||||
|
fclose(fp);
|
||||||
|
return set_result(r, "error: oom", 1);
|
||||||
|
}
|
||||||
|
size_t n = fread(buf, 1, (size_t)sz, fp);
|
||||||
|
fclose(fp);
|
||||||
|
buf[n] = 0;
|
||||||
|
cst_tool_result_free(r);
|
||||||
|
r->out = buf;
|
||||||
|
r->out_len = n;
|
||||||
|
r->exit_code = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r) {
|
||||||
|
const char *path = arg_get(call, "path");
|
||||||
|
const char *content = arg_get(call, "content");
|
||||||
|
if (!path || !content)
|
||||||
|
return set_result(r, "error: missing path or content", 1);
|
||||||
|
char full[4096];
|
||||||
|
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
|
||||||
|
return set_result(r, "error: path outside workspace", 1);
|
||||||
|
|
||||||
|
size_t olen = 0;
|
||||||
|
char *oldc = slurp_file(full, &olen);
|
||||||
|
|
||||||
|
FILE *fp = fopen(full, "wb");
|
||||||
|
if (!fp) {
|
||||||
|
free(oldc);
|
||||||
|
return set_result(r, "error: cannot write file", 1);
|
||||||
|
}
|
||||||
|
size_t n = strlen(content);
|
||||||
|
if (fwrite(content, 1, n, fp) != n) {
|
||||||
|
fclose(fp);
|
||||||
|
free(oldc);
|
||||||
|
return set_result(r, "error: write failed", 1);
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
int rc = set_result_with_diff(r, path, oldc, olen, content, n, 0);
|
||||||
|
free(oldc);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_tool_run_shell(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r) {
|
||||||
|
const char *cmd = arg_get(call, "cmd");
|
||||||
|
if (!cmd)
|
||||||
|
cmd = arg_get(call, "command");
|
||||||
|
if (!cmd)
|
||||||
|
return set_result(r, "error: missing cmd", 1);
|
||||||
|
|
||||||
|
char root_real[OS_PATH_MAX];
|
||||||
|
if (os_realpath(workspace_root, root_real, sizeof(root_real)) != 0)
|
||||||
|
return set_result(r, "error: bad workspace", 1);
|
||||||
|
|
||||||
|
os_run_result_t rr = {0};
|
||||||
|
if (os_run_shell_c(root_real, cmd, 512 * 1024, &rr) != 0) {
|
||||||
|
os_run_result_free(&rr);
|
||||||
|
return set_result(r, "error: shell exec failed", 1);
|
||||||
|
}
|
||||||
|
cst_tool_result_free(r);
|
||||||
|
r->out = rr.out;
|
||||||
|
r->out_len = rr.out_len;
|
||||||
|
r->exit_code = rr.exit_code;
|
||||||
|
rr.out = NULL;
|
||||||
|
os_run_result_free(&rr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minimal unified diff apply: only support *** Update File: / write via
|
||||||
|
* apply_patch tool with arg path + content for MVP simplicity is write_file.
|
||||||
|
* For apply_patch: expect arg "patch" with a simple format:
|
||||||
|
* *** Update File: <relpath>
|
||||||
|
* <full new file content>
|
||||||
|
* Or classic unified diff for single file with ---/+++ and @@ hunks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
static int apply_full_replace(const char *workspace_root, const char *path,
|
||||||
|
const char *content, cst_tool_result_t *r) {
|
||||||
|
ctl_tool_call_t fake = {0};
|
||||||
|
snprintf(fake.name, sizeof(fake.name), "write_file");
|
||||||
|
fake.nargs = 2;
|
||||||
|
snprintf(fake.args[0].key, sizeof(fake.args[0].key), "path");
|
||||||
|
fake.args[0].value = (char *)path;
|
||||||
|
fake.args[0].value_len = strlen(path);
|
||||||
|
snprintf(fake.args[1].key, sizeof(fake.args[1].key), "content");
|
||||||
|
fake.args[1].value = (char *)content;
|
||||||
|
fake.args[1].value_len = strlen(content);
|
||||||
|
return cst_tool_write_file(workspace_root, &fake, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apply one complete unified-diff hunk to a malloc'd file buffer.
|
||||||
|
* Returns 0 on success. Very small subset: context/add/remove lines. */
|
||||||
|
static int apply_hunk(char **file, size_t *flen, const char *hunk,
|
||||||
|
size_t hunk_len) {
|
||||||
|
/* Find @@ line */
|
||||||
|
const char *p = hunk;
|
||||||
|
const char *end = hunk + hunk_len;
|
||||||
|
while (p < end && !(*p == '@' && p + 1 < end && p[1] == '@')) {
|
||||||
|
while (p < end && *p != '\n')
|
||||||
|
p++;
|
||||||
|
if (p < end)
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (p >= end)
|
||||||
|
return -1;
|
||||||
|
while (p < end && *p != '\n')
|
||||||
|
p++;
|
||||||
|
if (p < end)
|
||||||
|
p++;
|
||||||
|
|
||||||
|
/* Parse old file into lines */
|
||||||
|
size_t nlines = 0;
|
||||||
|
for (size_t i = 0; i < *flen; i++)
|
||||||
|
if ((*file)[i] == '\n')
|
||||||
|
nlines++;
|
||||||
|
if (*flen > 0 && (*file)[*flen - 1] != '\n')
|
||||||
|
nlines++;
|
||||||
|
char **lines = calloc(nlines + 64, sizeof(char *));
|
||||||
|
size_t *llens = calloc(nlines + 64, sizeof(size_t));
|
||||||
|
if (!lines || !llens) {
|
||||||
|
free(lines);
|
||||||
|
free(llens);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
size_t li = 0, start = 0;
|
||||||
|
for (size_t i = 0; i <= *flen; i++) {
|
||||||
|
if (i == *flen || (*file)[i] == '\n') {
|
||||||
|
size_t L = i - start;
|
||||||
|
lines[li] = malloc(L + 1);
|
||||||
|
if (!lines[li]) {
|
||||||
|
for (size_t k = 0; k < li; k++)
|
||||||
|
free(lines[k]);
|
||||||
|
free(lines);
|
||||||
|
free(llens);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(lines[li], *file + start, L);
|
||||||
|
lines[li][L] = 0;
|
||||||
|
llens[li] = L;
|
||||||
|
li++;
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size_t cur = 0;
|
||||||
|
/* Walk hunk body */
|
||||||
|
while (p < end) {
|
||||||
|
char tag = *p;
|
||||||
|
const char *ls = p + 1;
|
||||||
|
const char *le = ls;
|
||||||
|
while (le < end && *le != '\n')
|
||||||
|
le++;
|
||||||
|
size_t L = (size_t)(le - ls);
|
||||||
|
if (tag == ' ' || tag == '-') {
|
||||||
|
if (cur >= li || llens[cur] != L || memcmp(lines[cur], ls, L) != 0) {
|
||||||
|
for (size_t k = 0; k < li; k++)
|
||||||
|
free(lines[k]);
|
||||||
|
free(lines);
|
||||||
|
free(llens);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (tag == '-') {
|
||||||
|
free(lines[cur]);
|
||||||
|
memmove(&lines[cur], &lines[cur + 1], (li - cur - 1) * sizeof(char *));
|
||||||
|
memmove(&llens[cur], &llens[cur + 1], (li - cur - 1) * sizeof(size_t));
|
||||||
|
li--;
|
||||||
|
} else {
|
||||||
|
cur++;
|
||||||
|
}
|
||||||
|
} else if (tag == '+') {
|
||||||
|
char *nl = malloc(L + 1);
|
||||||
|
if (!nl) {
|
||||||
|
for (size_t k = 0; k < li; k++)
|
||||||
|
free(lines[k]);
|
||||||
|
free(lines);
|
||||||
|
free(llens);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(nl, ls, L);
|
||||||
|
nl[L] = 0;
|
||||||
|
lines = realloc(lines, (li + 2) * sizeof(char *));
|
||||||
|
llens = realloc(llens, (li + 2) * sizeof(size_t));
|
||||||
|
memmove(&lines[cur + 1], &lines[cur], (li - cur) * sizeof(char *));
|
||||||
|
memmove(&llens[cur + 1], &llens[cur], (li - cur) * sizeof(size_t));
|
||||||
|
lines[cur] = nl;
|
||||||
|
llens[cur] = L;
|
||||||
|
li++;
|
||||||
|
cur++;
|
||||||
|
} else if (tag == '\\') {
|
||||||
|
/* "\ No newline at end of file" — ignore */
|
||||||
|
} else {
|
||||||
|
for (size_t k = 0; k < li; k++)
|
||||||
|
free(lines[k]);
|
||||||
|
free(lines);
|
||||||
|
free(llens);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
p = (le < end) ? le + 1 : end;
|
||||||
|
}
|
||||||
|
/* rebuild */
|
||||||
|
size_t total = 0;
|
||||||
|
for (size_t k = 0; k < li; k++)
|
||||||
|
total += llens[k] + 1;
|
||||||
|
char *out = malloc(total + 1);
|
||||||
|
if (!out) {
|
||||||
|
for (size_t k = 0; k < li; k++)
|
||||||
|
free(lines[k]);
|
||||||
|
free(lines);
|
||||||
|
free(llens);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
size_t o = 0;
|
||||||
|
for (size_t k = 0; k < li; k++) {
|
||||||
|
memcpy(out + o, lines[k], llens[k]);
|
||||||
|
o += llens[k];
|
||||||
|
out[o++] = '\n';
|
||||||
|
free(lines[k]);
|
||||||
|
}
|
||||||
|
free(lines);
|
||||||
|
free(llens);
|
||||||
|
free(*file);
|
||||||
|
*file = out;
|
||||||
|
*flen = o;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_tool_apply_patch(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r) {
|
||||||
|
const char *patch = arg_get(call, "patch");
|
||||||
|
if (!patch)
|
||||||
|
return set_result(r, "error: missing patch", 1);
|
||||||
|
|
||||||
|
/* Full-file replace shorthand */
|
||||||
|
if (strncmp(patch, "*** Update File: ", 17) == 0) {
|
||||||
|
const char *nl = strchr(patch, '\n');
|
||||||
|
if (!nl)
|
||||||
|
return set_result(r, "error: incomplete Update File header", 1);
|
||||||
|
char path[1024];
|
||||||
|
size_t plen = (size_t)(nl - (patch + 17));
|
||||||
|
if (plen >= sizeof(path))
|
||||||
|
return set_result(r, "error: path too long", 1);
|
||||||
|
memcpy(path, patch + 17, plen);
|
||||||
|
path[plen] = 0;
|
||||||
|
char full[4096];
|
||||||
|
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
|
||||||
|
return set_result(r, "error: path outside workspace", 1);
|
||||||
|
size_t olen = 0;
|
||||||
|
char *oldc = slurp_file(full, &olen);
|
||||||
|
const char *neu = nl + 1;
|
||||||
|
size_t nlen = strlen(neu);
|
||||||
|
ctl_tool_call_t fake = {0};
|
||||||
|
snprintf(fake.name, sizeof(fake.name), "write_file");
|
||||||
|
fake.nargs = 2;
|
||||||
|
snprintf(fake.args[0].key, sizeof(fake.args[0].key), "path");
|
||||||
|
fake.args[0].value = path;
|
||||||
|
fake.args[0].value_len = strlen(path);
|
||||||
|
snprintf(fake.args[1].key, sizeof(fake.args[1].key), "content");
|
||||||
|
fake.args[1].value = (char *)neu;
|
||||||
|
fake.args[1].value_len = nlen;
|
||||||
|
/* write_file already emits diff; free unused oldc */
|
||||||
|
free(oldc);
|
||||||
|
return cst_tool_write_file(workspace_root, &fake, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unified diff: require complete hunks (each @@ ... until next @@ or EOF) */
|
||||||
|
const char *p = patch;
|
||||||
|
char *rel = NULL;
|
||||||
|
/* find +++ b/path */
|
||||||
|
const char *plus = strstr(p, "\n+++ ");
|
||||||
|
if (!plus)
|
||||||
|
plus = (strncmp(p, "+++ ", 4) == 0) ? p : NULL;
|
||||||
|
if (!plus)
|
||||||
|
return set_result(r, "error: missing +++ line (incomplete patch)", 1);
|
||||||
|
const char *ps = plus + (plus[0] == '\n' ? 5 : 4);
|
||||||
|
if (ps[0] == 'b' && ps[1] == '/')
|
||||||
|
ps += 2;
|
||||||
|
const char *pe = strchr(ps, '\n');
|
||||||
|
if (!pe)
|
||||||
|
return set_result(r, "error: incomplete +++ line", 1);
|
||||||
|
size_t rlen = (size_t)(pe - ps);
|
||||||
|
rel = malloc(rlen + 1);
|
||||||
|
if (!rel)
|
||||||
|
return set_result(r, "error: oom", 1);
|
||||||
|
memcpy(rel, ps, rlen);
|
||||||
|
rel[rlen] = 0;
|
||||||
|
|
||||||
|
char full[4096];
|
||||||
|
if (cst_sandbox_resolve(workspace_root, rel, full, sizeof(full)) != 0) {
|
||||||
|
free(rel);
|
||||||
|
return set_result(r, "error: path outside workspace", 1);
|
||||||
|
}
|
||||||
|
char *relpath_for_diff = rel; /* owned until return */
|
||||||
|
|
||||||
|
FILE *fp = fopen(full, "rb");
|
||||||
|
char *file = NULL;
|
||||||
|
size_t flen = 0;
|
||||||
|
if (fp) {
|
||||||
|
fseek(fp, 0, SEEK_END);
|
||||||
|
long sz = ftell(fp);
|
||||||
|
rewind(fp);
|
||||||
|
if (sz < 0) {
|
||||||
|
fclose(fp);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return set_result(r, "error: ftell", 1);
|
||||||
|
}
|
||||||
|
file = malloc((size_t)sz + 1);
|
||||||
|
if (!file) {
|
||||||
|
fclose(fp);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return set_result(r, "error: oom", 1);
|
||||||
|
}
|
||||||
|
flen = fread(file, 1, (size_t)sz, fp);
|
||||||
|
fclose(fp);
|
||||||
|
file[flen] = 0;
|
||||||
|
} else {
|
||||||
|
file = calloc(1, 1);
|
||||||
|
flen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Snapshot before mutating hunks. */
|
||||||
|
char *old_snapshot = NULL;
|
||||||
|
size_t old_len = flen;
|
||||||
|
if (flen) {
|
||||||
|
old_snapshot = malloc(flen + 1);
|
||||||
|
if (old_snapshot) {
|
||||||
|
memcpy(old_snapshot, file, flen);
|
||||||
|
old_snapshot[flen] = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
old_snapshot = calloc(1, 1);
|
||||||
|
old_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Split into hunks starting at @@ */
|
||||||
|
const char *h = strstr(pe, "\n@@");
|
||||||
|
if (!h)
|
||||||
|
h = strstr(pe, "@@");
|
||||||
|
if (!h) {
|
||||||
|
free(file);
|
||||||
|
free(old_snapshot);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return set_result(r, "error: no complete hunk", 1);
|
||||||
|
}
|
||||||
|
if (*h == '\n')
|
||||||
|
h++;
|
||||||
|
while (h && *h == '@') {
|
||||||
|
const char *next = strstr(h + 2, "\n@@");
|
||||||
|
size_t hlen;
|
||||||
|
if (next) {
|
||||||
|
hlen = (size_t)(next + 1 - h);
|
||||||
|
} else {
|
||||||
|
hlen = strlen(h);
|
||||||
|
}
|
||||||
|
const char *hnl = strchr(h, '\n');
|
||||||
|
if (!hnl || hnl >= h + hlen - 1) {
|
||||||
|
free(file);
|
||||||
|
free(old_snapshot);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return set_result(r, "error: incomplete hunk (not confirmed)", 1);
|
||||||
|
}
|
||||||
|
if (apply_hunk(&file, &flen, h, hlen) != 0) {
|
||||||
|
free(file);
|
||||||
|
free(old_snapshot);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return set_result(r, "error: hunk apply failed", 1);
|
||||||
|
}
|
||||||
|
h = next ? next + 1 : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
fp = fopen(full, "wb");
|
||||||
|
if (!fp) {
|
||||||
|
free(file);
|
||||||
|
free(old_snapshot);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return set_result(r, "error: cannot write", 1);
|
||||||
|
}
|
||||||
|
if (fwrite(file, 1, flen, fp) != flen) {
|
||||||
|
fclose(fp);
|
||||||
|
free(file);
|
||||||
|
free(old_snapshot);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return set_result(r, "error: write failed", 1);
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
/* Patch text is already a diff — show it when short. */
|
||||||
|
size_t plen = strlen(patch);
|
||||||
|
if (plen > 0 && plen < DIFF_OUT_CAP) {
|
||||||
|
char hdr[256];
|
||||||
|
snprintf(hdr, sizeof(hdr), "applied patch → %s\n", relpath_for_diff);
|
||||||
|
size_t hlen = strlen(hdr);
|
||||||
|
char *buf = malloc(hlen + plen + 1);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf, hdr, hlen);
|
||||||
|
memcpy(buf + hlen, patch, plen);
|
||||||
|
buf[hlen + plen] = 0;
|
||||||
|
cst_tool_result_free(r);
|
||||||
|
r->out = buf;
|
||||||
|
r->out_len = hlen + plen;
|
||||||
|
r->exit_code = 0;
|
||||||
|
free(file);
|
||||||
|
free(old_snapshot);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int rc = set_result_with_diff(r, relpath_for_diff, old_snapshot, old_len, file,
|
||||||
|
flen, 0);
|
||||||
|
free(file);
|
||||||
|
free(old_snapshot);
|
||||||
|
free(relpath_for_diff);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int cst_tool_dispatch(const char *workspace_root, const ctl_tool_call_t *call,
|
||||||
|
cst_tool_result_t *r) {
|
||||||
|
if (!call || !r)
|
||||||
|
return -1;
|
||||||
|
memset(r, 0, sizeof(*r));
|
||||||
|
if (strcmp(call->name, "read_file") == 0)
|
||||||
|
return cst_tool_read_file(workspace_root, call, r);
|
||||||
|
if (strcmp(call->name, "run_shell") == 0)
|
||||||
|
return cst_tool_run_shell(workspace_root, call, r);
|
||||||
|
if (strcmp(call->name, "apply_patch") == 0)
|
||||||
|
return cst_tool_apply_patch(workspace_root, call, r);
|
||||||
|
if (strcmp(call->name, "write_file") == 0)
|
||||||
|
return cst_tool_write_file(workspace_root, call, r);
|
||||||
|
return set_result(r, "error: unknown tool", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse JSON string at *pp (must point at '"'); advances *pp past closing quote.
|
||||||
|
* Returns malloc'd decoded value. */
|
||||||
|
static char *parse_json_string(const char **pp, size_t *out_len) {
|
||||||
|
const char *p = *pp;
|
||||||
|
if (!p || *p != '"')
|
||||||
|
return NULL;
|
||||||
|
p++;
|
||||||
|
size_t cap = 64, len = 0;
|
||||||
|
char *out = malloc(cap);
|
||||||
|
if (!out)
|
||||||
|
return NULL;
|
||||||
|
while (*p) {
|
||||||
|
char ch;
|
||||||
|
if (*p == '\\' && p[1]) {
|
||||||
|
p++;
|
||||||
|
if (*p == 'n')
|
||||||
|
ch = '\n';
|
||||||
|
else if (*p == 'r')
|
||||||
|
ch = '\r';
|
||||||
|
else if (*p == 't')
|
||||||
|
ch = '\t';
|
||||||
|
else
|
||||||
|
ch = *p;
|
||||||
|
p++;
|
||||||
|
} else if (*p == '"') {
|
||||||
|
p++;
|
||||||
|
out[len] = 0;
|
||||||
|
*pp = p;
|
||||||
|
if (out_len)
|
||||||
|
*out_len = len;
|
||||||
|
return out;
|
||||||
|
} else {
|
||||||
|
ch = *p++;
|
||||||
|
}
|
||||||
|
if (len + 2 > cap) {
|
||||||
|
cap *= 2;
|
||||||
|
char *nb = realloc(out, cap);
|
||||||
|
if (!nb) {
|
||||||
|
free(out);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
out = nb;
|
||||||
|
}
|
||||||
|
out[len++] = ch;
|
||||||
|
}
|
||||||
|
free(out);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_tool_call_from_native(ctl_tool_call_t *out, const char *id,
|
||||||
|
const char *name, const char *args_json) {
|
||||||
|
if (!out || !name || !name[0])
|
||||||
|
return -1;
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
if (id)
|
||||||
|
snprintf(out->id, sizeof(out->id), "%s", id);
|
||||||
|
snprintf(out->name, sizeof(out->name), "%s", name);
|
||||||
|
if (!args_json)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const char *p = args_json;
|
||||||
|
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
|
||||||
|
p++;
|
||||||
|
if (*p != '{')
|
||||||
|
return 0;
|
||||||
|
p++;
|
||||||
|
|
||||||
|
while (*p) {
|
||||||
|
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t' || *p == ',')
|
||||||
|
p++;
|
||||||
|
if (*p == '}' || !*p)
|
||||||
|
break;
|
||||||
|
if (*p != '"')
|
||||||
|
break;
|
||||||
|
|
||||||
|
size_t klen = 0;
|
||||||
|
char *key = parse_json_string(&p, &klen);
|
||||||
|
if (!key)
|
||||||
|
break;
|
||||||
|
while (*p == ' ' || *p == '\t')
|
||||||
|
p++;
|
||||||
|
if (*p != ':') {
|
||||||
|
free(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
|
||||||
|
p++;
|
||||||
|
|
||||||
|
if (*p == '"') {
|
||||||
|
size_t vlen = 0;
|
||||||
|
char *val = parse_json_string(&p, &vlen);
|
||||||
|
if (!val) {
|
||||||
|
free(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (out->nargs < CTL_MAX_ARGS && klen < CTL_MAX_KEY) {
|
||||||
|
snprintf(out->args[out->nargs].key, CTL_MAX_KEY, "%s", key);
|
||||||
|
out->args[out->nargs].value = val;
|
||||||
|
out->args[out->nargs].value_len = vlen;
|
||||||
|
out->nargs++;
|
||||||
|
} else {
|
||||||
|
free(val);
|
||||||
|
}
|
||||||
|
free(key);
|
||||||
|
} else if (*p == '{' || *p == '[') {
|
||||||
|
free(key);
|
||||||
|
char open = *p;
|
||||||
|
char close = (open == '{') ? '}' : ']';
|
||||||
|
int depth = 0;
|
||||||
|
int in_str = 0;
|
||||||
|
for (; *p; p++) {
|
||||||
|
if (in_str) {
|
||||||
|
if (*p == '\\' && p[1]) {
|
||||||
|
p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"')
|
||||||
|
in_str = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == '"') {
|
||||||
|
in_str = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (*p == open)
|
||||||
|
depth++;
|
||||||
|
else if (*p == close) {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
p++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
free(key);
|
||||||
|
while (*p && *p != ',' && *p != '}')
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#include "cst_agent.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#ifdef CST_HEADLESS_ONLY
|
||||||
|
int cst_tui_run(cst_agent_t *agent) {
|
||||||
|
(void)agent;
|
||||||
|
fprintf(stderr,
|
||||||
|
"TUI was not built into this binary (CST_BUILD_TUI=OFF).\n"
|
||||||
|
"Use --headless --prompt '...'\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#include "ctl.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int n_open;
|
||||||
|
int n_arg;
|
||||||
|
int n_ready;
|
||||||
|
int n_text;
|
||||||
|
int n_error;
|
||||||
|
char last_name[CTL_MAX_NAME];
|
||||||
|
char last_id[CTL_MAX_ID];
|
||||||
|
char last_path[512];
|
||||||
|
size_t text_bytes;
|
||||||
|
} sink_t;
|
||||||
|
|
||||||
|
static void on_ev(const ctl_event_t *ev, void *ud) {
|
||||||
|
sink_t *s = ud;
|
||||||
|
switch (ev->type) {
|
||||||
|
case CTL_EV_CALL_OPEN:
|
||||||
|
s->n_open++;
|
||||||
|
snprintf(s->last_id, sizeof(s->last_id), "%s", ev->call->id);
|
||||||
|
snprintf(s->last_name, sizeof(s->last_name), "%s", ev->call->name);
|
||||||
|
break;
|
||||||
|
case CTL_EV_ARG:
|
||||||
|
s->n_arg++;
|
||||||
|
if (ev->call->nargs > 0) {
|
||||||
|
const ctl_arg_t *a = &ev->call->args[ev->call->nargs - 1];
|
||||||
|
if (strcmp(a->key, "path") == 0 && a->value)
|
||||||
|
snprintf(s->last_path, sizeof(s->last_path), "%s", a->value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CTL_EV_CALL_READY:
|
||||||
|
s->n_ready++;
|
||||||
|
break;
|
||||||
|
case CTL_EV_TEXT:
|
||||||
|
s->n_text++;
|
||||||
|
s->text_bytes += ev->text_len;
|
||||||
|
break;
|
||||||
|
case CTL_EV_ERROR:
|
||||||
|
s->n_error++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int expect(int cond, const char *msg) {
|
||||||
|
if (!cond) {
|
||||||
|
fprintf(stderr, "FAIL: %s\n", msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_chunked_confirm(void) {
|
||||||
|
sink_t s = {0};
|
||||||
|
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||||
|
const char *msg =
|
||||||
|
"I'll read the file.\n"
|
||||||
|
"call t1 read_file\n"
|
||||||
|
"arg path 10\n"
|
||||||
|
"README.mdx"
|
||||||
|
"end\n";
|
||||||
|
/* feed 1 byte at a time */
|
||||||
|
for (size_t i = 0; msg[i]; i++) {
|
||||||
|
if (ctl_parser_feed(p, &msg[i], 1) != CTL_OK) {
|
||||||
|
fprintf(stderr, "feed failed at %zu\n", i);
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int ok = 1;
|
||||||
|
ok &= expect(ctl_parser_finish(p) == CTL_OK, "finish ok");
|
||||||
|
ok &= expect(s.n_ready == 1, "one ready");
|
||||||
|
ok &= expect(s.n_open == 1, "one open");
|
||||||
|
ok &= expect(s.n_arg == 1, "one arg");
|
||||||
|
ok &= expect(strcmp(s.last_name, "read_file") == 0, "name");
|
||||||
|
ok &= expect(strcmp(s.last_path, "README.mdx") == 0, "path");
|
||||||
|
ok &= expect(s.n_error == 0, "no error");
|
||||||
|
ok &= expect(ctl_parser_pending(p) == 0, "pending 0");
|
||||||
|
/* pending peak should be bounded by line/arg */
|
||||||
|
ok &= expect(ctl_parser_pending_peak(p) <= CTL_MAX_LINE, "peak bounded");
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_truncate_no_execute(void) {
|
||||||
|
sink_t s = {0};
|
||||||
|
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||||
|
const char *msg =
|
||||||
|
"call t1 read_file\n"
|
||||||
|
"arg path 20\n"
|
||||||
|
"partial"; /* incomplete: need 20 bytes, then end */
|
||||||
|
ctl_parser_feed(p, msg, strlen(msg));
|
||||||
|
int ok = 1;
|
||||||
|
ok &= expect(s.n_ready == 0, "no ready before EOF");
|
||||||
|
ok &= expect(s.n_open == 1, "open confirmed");
|
||||||
|
ok &= expect(ctl_parser_finish(p) == CTL_ERR_CONTRACT, "finish error");
|
||||||
|
ok &= expect(s.n_error >= 1, "error emitted");
|
||||||
|
ok &= expect(s.n_ready == 0, "still no ready");
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_monotonic_no_retract(void) {
|
||||||
|
/* Once CALL_READY fires, further feed of garbage should error, not retract */
|
||||||
|
sink_t s = {0};
|
||||||
|
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||||
|
const char *okmsg =
|
||||||
|
"call a1 run_shell\n"
|
||||||
|
"arg cmd 5\n"
|
||||||
|
"ls -l"
|
||||||
|
"end\n";
|
||||||
|
ctl_parser_feed(p, okmsg, strlen(okmsg));
|
||||||
|
int ready = s.n_ready;
|
||||||
|
int ok = expect(ready == 1, "ready once");
|
||||||
|
/* text after is fine */
|
||||||
|
ctl_parser_feed(p, "done.\n", 6);
|
||||||
|
ok &= expect(s.n_ready == 1, "ready unchanged");
|
||||||
|
ctl_parser_finish(p);
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_text_before_call(void) {
|
||||||
|
sink_t s = {0};
|
||||||
|
ctl_parser_t *p = ctl_parser_create(on_ev, &s);
|
||||||
|
const char *msg = "Hello\nWorld\n";
|
||||||
|
ctl_parser_feed(p, msg, strlen(msg));
|
||||||
|
ctl_parser_finish(p);
|
||||||
|
int ok = expect(s.n_text == 2, "two text lines");
|
||||||
|
ok &= expect(s.n_ready == 0, "no calls");
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
int nfail = 0;
|
||||||
|
if (!test_chunked_confirm())
|
||||||
|
nfail++;
|
||||||
|
if (!test_truncate_no_execute())
|
||||||
|
nfail++;
|
||||||
|
if (!test_monotonic_no_retract())
|
||||||
|
nfail++;
|
||||||
|
if (!test_text_before_call())
|
||||||
|
nfail++;
|
||||||
|
if (nfail) {
|
||||||
|
fprintf(stderr, "%d test(s) failed\n", nfail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("test_ctl_frontier: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include "cst_agent.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int expect(int cond, const char *msg) {
|
||||||
|
if (!cond) {
|
||||||
|
fprintf(stderr, "FAIL: %s\n", msg);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_complete_json(void) {
|
||||||
|
const char *j =
|
||||||
|
"Here is a call {\"name\":\"read_file\",\"arguments\":{\"path\":\"a.c\"}} "
|
||||||
|
"done";
|
||||||
|
cst_json_tool_t t;
|
||||||
|
int ok = cst_json_try_parse_tool(j, strlen(j), &t);
|
||||||
|
int pass = expect(ok == 1, "parse ok");
|
||||||
|
pass &= expect(t.ok == 1, "ok flag");
|
||||||
|
pass &= expect(strcmp(t.name, "read_file") == 0, "name");
|
||||||
|
pass &= expect(strstr(t.arguments_json, "path") != NULL, "args");
|
||||||
|
return pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_truncated_json(void) {
|
||||||
|
const char *j = "{\"name\":\"read_file\",\"arguments\":{\"path\":\"a";
|
||||||
|
cst_json_tool_t t;
|
||||||
|
int ok = cst_json_try_parse_tool(j, strlen(j), &t);
|
||||||
|
return expect(ok == 0, "truncated must fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int test_unclosed_brace(void) {
|
||||||
|
const char *j = "{\"name\":\"run_shell\",\"arguments\":{\"cmd\":\"ls\"";
|
||||||
|
cst_json_tool_t t;
|
||||||
|
int ok = cst_json_try_parse_tool(j, strlen(j), &t);
|
||||||
|
return expect(ok == 0, "unclosed brace fails");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
int nfail = 0;
|
||||||
|
if (!test_complete_json())
|
||||||
|
nfail++;
|
||||||
|
if (!test_truncated_json())
|
||||||
|
nfail++;
|
||||||
|
if (!test_unclosed_brace())
|
||||||
|
nfail++;
|
||||||
|
if (nfail) {
|
||||||
|
fprintf(stderr, "%d failed\n", nfail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("test_json_baseline: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#include "mm_tool.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int n_ready;
|
||||||
|
int n_text;
|
||||||
|
char name[64];
|
||||||
|
char path[256];
|
||||||
|
} sink_t;
|
||||||
|
|
||||||
|
static void on_ev(const ctl_event_t *ev, void *ud) {
|
||||||
|
sink_t *s = ud;
|
||||||
|
if (ev->type == CTL_EV_TEXT)
|
||||||
|
s->n_text++;
|
||||||
|
if (ev->type == CTL_EV_CALL_READY && ev->call) {
|
||||||
|
s->n_ready++;
|
||||||
|
snprintf(s->name, sizeof(s->name), "%s", ev->call->name);
|
||||||
|
if (ev->call->nargs > 0 && ev->call->args[0].value)
|
||||||
|
snprintf(s->path, sizeof(s->path), "%s", ev->call->args[0].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int expect(int c, const char *m) {
|
||||||
|
if (!c) {
|
||||||
|
fprintf(stderr, "FAIL: %s\n", m);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
sink_t s = {0};
|
||||||
|
mm_scan_t *mm = mm_scan_create(on_ev, &s);
|
||||||
|
const char *msg =
|
||||||
|
"我来读一下。\n"
|
||||||
|
"<minimax:tool_call>\n"
|
||||||
|
"invoke read_file\n"
|
||||||
|
"<arg name=\"path\">./README.md</arg>\n"
|
||||||
|
"</minimax:tool_call>\n"
|
||||||
|
"读完了。\n";
|
||||||
|
/* byte-at-a-time */
|
||||||
|
for (size_t i = 0; msg[i]; i++)
|
||||||
|
mm_scan_feed(mm, &msg[i], 1);
|
||||||
|
mm_scan_finish(mm);
|
||||||
|
int ok = 1;
|
||||||
|
ok &= expect(s.n_ready == 1, "one ready");
|
||||||
|
ok &= expect(strcmp(s.name, "read_file") == 0, "name");
|
||||||
|
ok &= expect(strcmp(s.path, "./README.md") == 0, "path");
|
||||||
|
ok &= expect(s.n_text >= 1, "text emitted");
|
||||||
|
/* truncated: no execute */
|
||||||
|
sink_t s2 = {0};
|
||||||
|
mm_scan_t *mm2 = mm_scan_create(on_ev, &s2);
|
||||||
|
const char *trunc = "<minimax:tool_call>\ninvoke read_file\n";
|
||||||
|
mm_scan_feed(mm2, trunc, strlen(trunc));
|
||||||
|
mm_scan_finish(mm2);
|
||||||
|
ok &= expect(s2.n_ready == 0, "trunc no ready");
|
||||||
|
mm_scan_destroy(mm);
|
||||||
|
mm_scan_destroy(mm2);
|
||||||
|
if (!ok)
|
||||||
|
return 1;
|
||||||
|
printf("test_mm_tool: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "ctl.h"
|
||||||
|
#include "cst_agent.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Starvation replay: same logical tool call, cut at several prefixes.
|
||||||
|
* CTL+CST should never execute on incomplete prefixes; JSON-buffer fails to
|
||||||
|
* parse (cannot execute). When the full stream is present, both succeed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int n_ready;
|
||||||
|
size_t peak;
|
||||||
|
} ctl_stats_t;
|
||||||
|
|
||||||
|
static void on_ev(const ctl_event_t *ev, void *ud) {
|
||||||
|
ctl_stats_t *s = ud;
|
||||||
|
if (ev->type == CTL_EV_CALL_READY)
|
||||||
|
s->n_ready++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ctl_ready_on_prefix(const char *full, size_t ncut, size_t *peak) {
|
||||||
|
ctl_stats_t st = {0};
|
||||||
|
ctl_parser_t *p = ctl_parser_create(on_ev, &st);
|
||||||
|
ctl_parser_feed(p, full, ncut);
|
||||||
|
/* do not finish — simulate stream stall / truncation mid-turn */
|
||||||
|
*peak = ctl_parser_pending_peak(p);
|
||||||
|
int ready = st.n_ready;
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
return ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
const char *ctl =
|
||||||
|
"call x1 read_file\n"
|
||||||
|
"arg path 5\n"
|
||||||
|
"a.txt"
|
||||||
|
"end\n";
|
||||||
|
const char *json =
|
||||||
|
"{\"name\":\"read_file\",\"arguments\":{\"path\":\"a.txt\"}}";
|
||||||
|
|
||||||
|
size_t ctl_len = strlen(ctl);
|
||||||
|
size_t json_len = strlen(json);
|
||||||
|
|
||||||
|
int ctl_premature = 0, json_ok_full = 0, ctl_ok_full = 0;
|
||||||
|
int json_fail_trunc = 0, ctl_safe_trunc = 0;
|
||||||
|
|
||||||
|
/* full streams */
|
||||||
|
{
|
||||||
|
size_t peak = 0;
|
||||||
|
ctl_stats_t st = {0};
|
||||||
|
ctl_parser_t *p = ctl_parser_create(on_ev, &st);
|
||||||
|
ctl_parser_feed(p, ctl, ctl_len);
|
||||||
|
ctl_parser_finish(p);
|
||||||
|
peak = ctl_parser_pending_peak(p);
|
||||||
|
ctl_ok_full = (st.n_ready == 1);
|
||||||
|
printf("CTL full: ready=%d peak=%zu\n", st.n_ready, peak);
|
||||||
|
ctl_parser_destroy(p);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cst_json_tool_t t;
|
||||||
|
json_ok_full = cst_json_try_parse_tool(json, json_len, &t);
|
||||||
|
printf("JSON full: ok=%d name=%s\n", json_ok_full, t.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* truncate at 50% and 80% */
|
||||||
|
size_t cuts[] = {ctl_len / 2, (ctl_len * 4) / 5, ctl_len - 2};
|
||||||
|
for (size_t i = 0; i < sizeof(cuts) / sizeof(cuts[0]); i++) {
|
||||||
|
size_t peak = 0;
|
||||||
|
int ready = ctl_ready_on_prefix(ctl, cuts[i], &peak);
|
||||||
|
printf("CTL cut %zu/%zu: ready=%d peak=%zu\n", cuts[i], ctl_len, ready,
|
||||||
|
peak);
|
||||||
|
if (ready)
|
||||||
|
ctl_premature++;
|
||||||
|
else
|
||||||
|
ctl_safe_trunc++;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t jcuts[] = {json_len / 2, (json_len * 4) / 5, json_len - 2};
|
||||||
|
for (size_t i = 0; i < sizeof(jcuts) / sizeof(jcuts[0]); i++) {
|
||||||
|
cst_json_tool_t t;
|
||||||
|
int ok = cst_json_try_parse_tool(json, jcuts[i], &t);
|
||||||
|
printf("JSON cut %zu/%zu: ok=%d\n", jcuts[i], json_len, ok);
|
||||||
|
if (!ok)
|
||||||
|
json_fail_trunc++;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\n=== summary ===\n");
|
||||||
|
printf("CTL full success: %s\n", ctl_ok_full ? "yes" : "no");
|
||||||
|
printf("JSON full success: %s\n", json_ok_full ? "yes" : "no");
|
||||||
|
printf("CTL premature executes on trunc: %d (want 0)\n", ctl_premature);
|
||||||
|
printf("CTL safe truncations: %d\n", ctl_safe_trunc);
|
||||||
|
printf("JSON trunc failures: %d\n", json_fail_trunc);
|
||||||
|
|
||||||
|
if (!ctl_ok_full || !json_ok_full || ctl_premature != 0)
|
||||||
|
return 1;
|
||||||
|
printf("replay_starvation: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
BusyBox is GPLv2. Source: https://github.com/rmyorston/busybox-w32
|
||||||
|
Upstream BusyBox: https://github.com/mirror/busybox
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# BusyBox for CST-Agent
|
||||||
|
|
||||||
|
## Windows (`busybox.exe`)
|
||||||
|
|
||||||
|
Built from **busybox-w32**:
|
||||||
|
https://github.com/rmyorston/busybox-w32.git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build_third_party_mingw.sh busybox-w32
|
||||||
|
# → third_party/busybox/busybox.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Ship next to `cst-agent.exe`. License: GPLv2 (`LICENSE.busybox`).
|
||||||
|
|
||||||
|
## Linux / Termux (optional side-by-side `busybox`)
|
||||||
|
|
||||||
|
Upstream mirror:
|
||||||
|
https://github.com/mirror/busybox.git
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build_third_party_mingw.sh busybox-unix
|
||||||
|
# → third_party/busybox/busybox
|
||||||
|
```
|
||||||
|
|
||||||
|
On Unix, `run_shell` prefers `CST_SHELL`, then `<exe_dir>/busybox`, then
|
||||||
|
`$PREFIX/bin/sh` (Termux), then `/bin/sh`.
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
CC ?= gcc
|
||||||
|
CFLAGS ?= -std=c11 -Wall -Wextra -O2 -Iinclude -Isrc
|
||||||
|
SRCS = src/fsm.c src/table_stats.c src/line_index.c src/block_scan.c \
|
||||||
|
src/ansi_style.c src/render.c src/render_ansi.c src/render_json.c \
|
||||||
|
src/sashidown.c src/compat_cmark.c src/stream.c
|
||||||
|
OBJS = $(SRCS:.c=.o)
|
||||||
|
LIB = libsashidown.a
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
all: $(LIB)
|
||||||
|
$(LIB): $(OBJS)
|
||||||
|
$(AR) rcs $@ $(OBJS)
|
||||||
|
src/%.o: src/%.c
|
||||||
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
clean:
|
||||||
|
rm -f $(OBJS) $(LIB)
|
||||||
Vendored
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#ifndef SASHIDOWN_H
|
||||||
|
#define SASHIDOWN_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#define SASHIDOWN_OUTPUT_DEFAULT 65536
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *input;
|
||||||
|
size_t input_len;
|
||||||
|
char *output;
|
||||||
|
size_t output_cap;
|
||||||
|
size_t output_len;
|
||||||
|
char *write_ptr;
|
||||||
|
char *write_end;
|
||||||
|
void *line_ix;
|
||||||
|
int overflow; /* set when output buffer is exhausted */
|
||||||
|
} sashidown_ctx_t;
|
||||||
|
|
||||||
|
/* Parse markdown to HTML. Returns 0 on success, -1 on error
|
||||||
|
* (null args, or output buffer too small — never silently truncates). */
|
||||||
|
int sashidown_parse(const char *input, size_t len,
|
||||||
|
char *output, size_t cap, size_t *out_len);
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SASHIDOWN_FMT_HTML = 0,
|
||||||
|
SASHIDOWN_FMT_ANSI = 1, /* built-in colorful terminal theme */
|
||||||
|
SASHIDOWN_FMT_JSON = 2,
|
||||||
|
SASHIDOWN_FMT_ANSI_PLAIN = 3, /* monochrome / SGR attributes only */
|
||||||
|
} sashidown_format_t;
|
||||||
|
|
||||||
|
/* Single-pass convert. SASHIDOWN_FMT_HTML delegates to sashidown_parse.
|
||||||
|
* Returns 0 on success, -1 on error (null args, bad format, or buffer too small). */
|
||||||
|
int sashidown_convert(const char *input, size_t len,
|
||||||
|
char *output, size_t cap, size_t *out_len,
|
||||||
|
sashidown_format_t format);
|
||||||
|
|
||||||
|
/* Allocating wrapper (thread-safe). Caller must free via sashidown_free. */
|
||||||
|
char *cmark_markdown_to_html(const char *text, int len);
|
||||||
|
void sashidown_free(void *ptr);
|
||||||
|
|
||||||
|
/* --- Streaming API (AI conversation / incremental input) --- */
|
||||||
|
|
||||||
|
typedef struct sashidown_stream sashidown_stream_t;
|
||||||
|
|
||||||
|
/* Called for each HTML chunk; return non-zero to abort feeding. */
|
||||||
|
typedef int (*sashidown_stream_cb)(const char *html, size_t len, void *user);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
sashidown_stream_cb on_html;
|
||||||
|
void *user;
|
||||||
|
size_t output_buf_size; /* default 8192 if 0; grows on overflow */
|
||||||
|
} sashidown_stream_opts_t;
|
||||||
|
|
||||||
|
sashidown_stream_t *sashidown_stream_create(const sashidown_stream_opts_t *opts);
|
||||||
|
int sashidown_stream_feed(sashidown_stream_t *s, const char *chunk, size_t len);
|
||||||
|
int sashidown_stream_finish(sashidown_stream_t *s);
|
||||||
|
void sashidown_stream_destroy(sashidown_stream_t *s);
|
||||||
|
|
||||||
|
/* Bytes held past the confirmation frontier (input_len - render_pos).
|
||||||
|
* Unconfirmed suffix under uncertain future input; O(1) query. */
|
||||||
|
size_t sashidown_stream_pending(const sashidown_stream_t *s);
|
||||||
|
|
||||||
|
#endif /* SASHIDOWN_H */
|
||||||
Vendored
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#include "ansi_style.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Default colorful theme — 16-color SGR, widely supported. */
|
||||||
|
static const ansi_theme_t THEME_COLOR = {
|
||||||
|
.reset = "\033[0m",
|
||||||
|
|
||||||
|
.heading = {
|
||||||
|
NULL,
|
||||||
|
"\033[1;96m", /* h1 bright cyan */
|
||||||
|
"\033[1;94m", /* h2 bright blue */
|
||||||
|
"\033[1;92m", /* h3 bright green */
|
||||||
|
"\033[1;95m", /* h4 bright magenta */
|
||||||
|
"\033[1;93m", /* h5 bright yellow */
|
||||||
|
"\033[1;91m", /* h6 bright red */
|
||||||
|
},
|
||||||
|
.heading_end = "\033[0m",
|
||||||
|
|
||||||
|
.strong_on = "\033[1m",
|
||||||
|
.strong_off = "\033[22m",
|
||||||
|
.emph_on = "\033[3;33m", /* italic yellow */
|
||||||
|
.emph_off = "\033[23;39m",
|
||||||
|
.strike_on = "\033[9m",
|
||||||
|
.strike_off = "\033[29m",
|
||||||
|
|
||||||
|
.code_on = "\033[1;36m", /* bright cyan */
|
||||||
|
.code_off = "\033[22;39m",
|
||||||
|
.fence_on = "\033[32m", /* green */
|
||||||
|
.fence_off = "\033[0m",
|
||||||
|
|
||||||
|
.link_on = "\033[4;94m", /* underline bright blue */
|
||||||
|
.link_off = "\033[24;39m",
|
||||||
|
.url_on = "\033[2;36m", /* dim cyan */
|
||||||
|
.url_off = "\033[22;39m",
|
||||||
|
.image_on = "\033[95m",
|
||||||
|
.image_off = "\033[39m",
|
||||||
|
|
||||||
|
.bullet = "\033[93m• \033[0m",
|
||||||
|
.bullet_end = "",
|
||||||
|
.ol_on = "\033[93m",
|
||||||
|
.ol_off = "\033[0m",
|
||||||
|
|
||||||
|
.bq_prefix = "\033[2;35m│ \033[0m",
|
||||||
|
.th_on = "\033[1;96m",
|
||||||
|
.th_off = "\033[0m",
|
||||||
|
.sep = "\033[2m │ \033[0m",
|
||||||
|
|
||||||
|
.latex_on = "\033[95m",
|
||||||
|
.latex_off = "\033[39m",
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Monochrome / attribute-only (previous behavior). */
|
||||||
|
static const ansi_theme_t THEME_PLAIN = {
|
||||||
|
.reset = "\033[0m",
|
||||||
|
|
||||||
|
.heading = {
|
||||||
|
NULL,
|
||||||
|
"\033[1m", "\033[1m", "\033[1m",
|
||||||
|
"\033[1m", "\033[1m", "\033[1m",
|
||||||
|
},
|
||||||
|
.heading_end = "\033[0m",
|
||||||
|
|
||||||
|
.strong_on = "\033[1m",
|
||||||
|
.strong_off = "\033[22m",
|
||||||
|
.emph_on = "\033[3m",
|
||||||
|
.emph_off = "\033[23m",
|
||||||
|
.strike_on = "\033[9m",
|
||||||
|
.strike_off = "\033[29m",
|
||||||
|
|
||||||
|
.code_on = "\033[7m",
|
||||||
|
.code_off = "\033[27m",
|
||||||
|
.fence_on = "\033[2m",
|
||||||
|
.fence_off = "\033[0m",
|
||||||
|
|
||||||
|
.link_on = "",
|
||||||
|
.link_off = "",
|
||||||
|
.url_on = "",
|
||||||
|
.url_off = "",
|
||||||
|
.image_on = "",
|
||||||
|
.image_off = "",
|
||||||
|
|
||||||
|
.bullet = "• ",
|
||||||
|
.bullet_end = "",
|
||||||
|
.ol_on = "",
|
||||||
|
.ol_off = "",
|
||||||
|
|
||||||
|
.bq_prefix = "\033[2m| \033[0m",
|
||||||
|
.th_on = "\033[1m",
|
||||||
|
.th_off = "\033[22m",
|
||||||
|
.sep = " | ",
|
||||||
|
|
||||||
|
.latex_on = "",
|
||||||
|
.latex_off = "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ansi_theme_t *ansi_theme_color(void)
|
||||||
|
{
|
||||||
|
return &THEME_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ansi_theme_t *ansi_theme_plain(void)
|
||||||
|
{
|
||||||
|
return &THEME_PLAIN;
|
||||||
|
}
|
||||||
Vendored
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#ifndef SASHIDOWN_ANSI_STYLE_H
|
||||||
|
#define SASHIDOWN_ANSI_STYLE_H
|
||||||
|
|
||||||
|
/* Built-in ANSI role styles (SGR). Strings are compile-time literals. */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *reset;
|
||||||
|
|
||||||
|
const char *heading[7]; /* [1..6] used; [0] unused */
|
||||||
|
const char *heading_end;
|
||||||
|
|
||||||
|
const char *strong_on;
|
||||||
|
const char *strong_off;
|
||||||
|
const char *emph_on;
|
||||||
|
const char *emph_off;
|
||||||
|
const char *strike_on;
|
||||||
|
const char *strike_off;
|
||||||
|
|
||||||
|
const char *code_on;
|
||||||
|
const char *code_off;
|
||||||
|
const char *fence_on;
|
||||||
|
const char *fence_off;
|
||||||
|
|
||||||
|
const char *link_on;
|
||||||
|
const char *link_off;
|
||||||
|
const char *url_on;
|
||||||
|
const char *url_off;
|
||||||
|
const char *image_on;
|
||||||
|
const char *image_off;
|
||||||
|
|
||||||
|
const char *bullet;
|
||||||
|
const char *bullet_end;
|
||||||
|
const char *ol_on;
|
||||||
|
const char *ol_off;
|
||||||
|
|
||||||
|
const char *bq_prefix;
|
||||||
|
const char *th_on;
|
||||||
|
const char *th_off;
|
||||||
|
const char *sep; /* table column separator */
|
||||||
|
|
||||||
|
const char *latex_on;
|
||||||
|
const char *latex_off;
|
||||||
|
} ansi_theme_t;
|
||||||
|
|
||||||
|
const ansi_theme_t *ansi_theme_color(void);
|
||||||
|
const ansi_theme_t *ansi_theme_plain(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
Vendored
+337
@@ -0,0 +1,337 @@
|
|||||||
|
#include "block_scan.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int block_count_heading(const char *input, size_t len, size_t pos, size_t *content_start)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
int level = 0;
|
||||||
|
while (i < len && input[i] == '#') {
|
||||||
|
level++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (level == 0 || level > 6) return 0;
|
||||||
|
if (i < len && input[i] != ' ' && input[i] != '\n') return 0;
|
||||||
|
while (i < len && input[i] == ' ') i++;
|
||||||
|
*content_start = i;
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_is_fence(const char *input, size_t len, size_t pos,
|
||||||
|
int *fence_len, char *fence_char)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i + 2 >= len) return 0;
|
||||||
|
char ch = input[i];
|
||||||
|
if (ch != '`' && ch != '~') return 0;
|
||||||
|
if (input[i + 1] != ch || input[i + 2] != ch) return 0;
|
||||||
|
int fl = 3;
|
||||||
|
i += 3;
|
||||||
|
while (i < len && input[i] == ch) { fl++; i++; }
|
||||||
|
*fence_len = fl;
|
||||||
|
if (fence_char)
|
||||||
|
*fence_char = ch;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void block_fence_info_range(const char *input, size_t len, size_t pos,
|
||||||
|
int fence_len, char fence_char,
|
||||||
|
size_t *info_start, size_t *info_end)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
i += (size_t)fence_len;
|
||||||
|
(void)fence_char;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
*info_start = i;
|
||||||
|
size_t end = i;
|
||||||
|
while (end < len && input[end] != '\n' && input[end] != '\r')
|
||||||
|
end++;
|
||||||
|
while (end > *info_start &&
|
||||||
|
(input[end - 1] == ' ' || input[end - 1] == '\t'))
|
||||||
|
end--;
|
||||||
|
*info_end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_is_thematic_break(const char *input, size_t pos, size_t le)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i >= le) return 0;
|
||||||
|
|
||||||
|
char marker = input[i];
|
||||||
|
if (marker != '*' && marker != '-' && marker != '_') return 0;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (; i < le; i++) {
|
||||||
|
char c = input[i];
|
||||||
|
if (c == marker) {
|
||||||
|
count++;
|
||||||
|
} else if (c == ' ' || c == '\t') {
|
||||||
|
continue;
|
||||||
|
} else if (c == '\r') {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_is_setext_underline(const char *input, size_t pos, size_t le, int *level)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i >= le) return 0;
|
||||||
|
|
||||||
|
char marker = input[i];
|
||||||
|
if (marker != '=' && marker != '-') return 0;
|
||||||
|
|
||||||
|
size_t start = i;
|
||||||
|
while (i < le && input[i] == marker) i++;
|
||||||
|
if (i == start) return 0;
|
||||||
|
while (i < le && (input[i] == ' ' || input[i] == '\t' || input[i] == '\r'))
|
||||||
|
i++;
|
||||||
|
if (i < le) return 0;
|
||||||
|
|
||||||
|
/* Thematic-break-looking '---' is still a valid setext underline when
|
||||||
|
* paired with a preceding text line (caller decides context). */
|
||||||
|
if (level)
|
||||||
|
*level = (marker == '=') ? 1 : 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_is_blockquote(const char *input, size_t len, size_t pos, size_t *content_start)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i >= len || input[i] != '>') return 0;
|
||||||
|
i++;
|
||||||
|
if (i < len && input[i] == ' ') i++;
|
||||||
|
*content_start = i;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_is_ul(const char *input, size_t len, size_t pos, size_t *content_start)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i >= len) return 0;
|
||||||
|
if (input[i] != '-' && input[i] != '*' && input[i] != '+') return 0;
|
||||||
|
if (i + 1 < len && input[i + 1] != ' ') return 0;
|
||||||
|
i += 2;
|
||||||
|
*content_start = i;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_parse_task_marker(const char *input, size_t len, size_t content_start,
|
||||||
|
int *task, size_t *text_start)
|
||||||
|
{
|
||||||
|
size_t i = content_start;
|
||||||
|
if (i + 3 > len) return 0;
|
||||||
|
if (input[i] != '[') return 0;
|
||||||
|
char mark = input[i + 1];
|
||||||
|
if (input[i + 2] != ']') return 0;
|
||||||
|
if (mark != ' ' && mark != 'x' && mark != 'X') return 0;
|
||||||
|
i += 3;
|
||||||
|
if (i < len && input[i] == ' ') i++;
|
||||||
|
*task = (mark == ' ') ? 1 : 2;
|
||||||
|
*text_start = i;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_is_ol(const char *input, size_t len, size_t pos, size_t *content_start, int *num)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
size_t num_start = i;
|
||||||
|
{ unsigned char c = (unsigned char)input[i];
|
||||||
|
if (i >= len || c < '0' || c > '9') return 0; }
|
||||||
|
while (i < len) {
|
||||||
|
unsigned char c = (unsigned char)input[i];
|
||||||
|
if (c < '0' || c > '9') break;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i >= len || input[i] != '.') return 0;
|
||||||
|
if (i + 1 < len && input[i + 1] != ' ') return 0;
|
||||||
|
*num = 0;
|
||||||
|
for (size_t j = num_start; j < i; j++)
|
||||||
|
*num = *num * 10 + (input[j] - '0');
|
||||||
|
i += 2;
|
||||||
|
*content_start = i;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_line_starts_table(const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i >= len) return 0;
|
||||||
|
/* GFM: leading '|' OR a pipe somewhere on the line with a separator below.
|
||||||
|
* Cheap reject: must contain '|' before newline. */
|
||||||
|
const char *nl = memchr(input + i, '\n', len - i);
|
||||||
|
size_t line_len = nl ? (size_t)(nl - (input + i)) : (len - i);
|
||||||
|
return memchr(input + i, '|', line_len) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_is_latex_line(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, size_t *content_start, size_t *content_end)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i + 1 >= len || input[i] != '$' || input[i + 1] != '$') return 0;
|
||||||
|
i += 2;
|
||||||
|
*content_start = i;
|
||||||
|
size_t le = block_line_end(ctx, input, len, pos);
|
||||||
|
if (i + 1 < le && input[le - 2] == '$' && input[le - 1] == '$') {
|
||||||
|
*content_end = le - 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_try_setext(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, size_t le, size_t stop, int is_final,
|
||||||
|
line_info_t *info, size_t *end_pos)
|
||||||
|
{
|
||||||
|
if (pos == le || (pos + 1 == le && input[pos] == '\r'))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* Next line starts after the newline at `le` (line_end points at '\n'). */
|
||||||
|
if (le >= len || input[le] != '\n')
|
||||||
|
return is_final ? 0 : -1;
|
||||||
|
|
||||||
|
size_t next = le + 1;
|
||||||
|
if (next >= len)
|
||||||
|
return is_final ? 0 : -1;
|
||||||
|
if (next >= stop && !is_final)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
/* Cheap reject before full underline scan. */
|
||||||
|
size_t u = next;
|
||||||
|
while (u < len && (input[u] == ' ' || input[u] == '\t'))
|
||||||
|
u++;
|
||||||
|
if (u >= len || (input[u] != '=' && input[u] != '-'))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
size_t nle = block_line_end(ctx, input, len, next);
|
||||||
|
int level = 0;
|
||||||
|
if (!block_is_setext_underline(input, next, nle, &level))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
info->kind = LK_HEADING;
|
||||||
|
info->heading_level = level;
|
||||||
|
info->content_start = pos;
|
||||||
|
while (info->content_start < le &&
|
||||||
|
(input[info->content_start] == ' ' || input[info->content_start] == '\t'))
|
||||||
|
info->content_start++;
|
||||||
|
info->content_end = le;
|
||||||
|
while (info->content_end > info->content_start &&
|
||||||
|
(input[info->content_end - 1] == ' ' ||
|
||||||
|
input[info->content_end - 1] == '\t' ||
|
||||||
|
input[info->content_end - 1] == '\r'))
|
||||||
|
info->content_end--;
|
||||||
|
info->fence_len = 0;
|
||||||
|
info->fence_char = 0;
|
||||||
|
info->info_start = 0;
|
||||||
|
info->info_end = 0;
|
||||||
|
info->task = 0;
|
||||||
|
*end_pos = block_skip_line(ctx, input, len, next);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
line_info_t block_classify_line(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, size_t le)
|
||||||
|
{
|
||||||
|
line_info_t info;
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
info.kind = LK_PARA;
|
||||||
|
info.content_start = pos;
|
||||||
|
info.content_end = le;
|
||||||
|
|
||||||
|
if (pos == le || (pos + 1 == le && input[pos] == '\r')) {
|
||||||
|
info.kind = LK_EMPTY;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < len && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
unsigned char first = (i < len) ? (unsigned char)input[i] : 0;
|
||||||
|
|
||||||
|
/* Thematic breaks only possible for - / * / _ lines. */
|
||||||
|
if ((first == '*' || first == '-' || first == '_') &&
|
||||||
|
block_is_thematic_break(input, pos, le)) {
|
||||||
|
info.kind = LK_HR;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (first) {
|
||||||
|
case '#':
|
||||||
|
info.heading_level = block_count_heading(input, len, pos, &info.content_start);
|
||||||
|
if (info.heading_level > 0) {
|
||||||
|
info.kind = LK_HEADING;
|
||||||
|
info.content_end = le;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '`':
|
||||||
|
case '~':
|
||||||
|
if (block_is_fence(input, len, pos, &info.fence_len, &info.fence_char)) {
|
||||||
|
info.kind = LK_FENCE;
|
||||||
|
block_fence_info_range(input, len, pos, info.fence_len, info.fence_char,
|
||||||
|
&info.info_start, &info.info_end);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '>':
|
||||||
|
if (block_is_blockquote(input, len, pos, &info.content_start)) {
|
||||||
|
info.kind = LK_BQ;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '-': case '*': case '+':
|
||||||
|
if (block_is_ul(input, len, pos, &info.content_start)) {
|
||||||
|
info.kind = LK_UL;
|
||||||
|
size_t text = info.content_start;
|
||||||
|
int task = 0;
|
||||||
|
if (block_parse_task_marker(input, le, info.content_start, &task, &text)) {
|
||||||
|
info.task = task;
|
||||||
|
info.content_start = text;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '0': case '1': case '2': case '3': case '4':
|
||||||
|
case '5': case '6': case '7': case '8': case '9':
|
||||||
|
{
|
||||||
|
int ol_num = 0;
|
||||||
|
if (block_is_ol(input, len, pos, &info.content_start, &ol_num)) {
|
||||||
|
info.kind = LK_OL;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '$':
|
||||||
|
{
|
||||||
|
size_t cs, ce;
|
||||||
|
if (i + 1 < len && input[i] == '$' && input[i + 1] == '$') {
|
||||||
|
if (i == pos) {
|
||||||
|
info.kind = LK_LATEX_BLOCK;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
if (block_is_latex_line(ctx, input, len, pos, &cs, &ce)) {
|
||||||
|
info.kind = LK_LATEX_LINE;
|
||||||
|
info.content_start = cs;
|
||||||
|
info.content_end = ce;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.kind = LK_PARA;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
Vendored
+196
@@ -0,0 +1,196 @@
|
|||||||
|
#ifndef SASHIDOWN_BLOCK_SCAN_H
|
||||||
|
#define SASHIDOWN_BLOCK_SCAN_H
|
||||||
|
|
||||||
|
#include "sashidown.h"
|
||||||
|
#include "render.h"
|
||||||
|
#include "fsm.h"
|
||||||
|
#include "line_index.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifdef __SSE2__
|
||||||
|
#include <emmintrin.h>
|
||||||
|
#elif defined(__ARM_NEON) || defined(__aarch64__)
|
||||||
|
#include <arm_neon.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Woverride-init"
|
||||||
|
static inline size_t block_utf8_seq_len(unsigned char ch)
|
||||||
|
{
|
||||||
|
static const unsigned char tbl[256] = {
|
||||||
|
[0x00 ... 0xFF] = 1,
|
||||||
|
[0xC0 ... 0xDF] = 2,
|
||||||
|
[0xE0 ... 0xEF] = 3,
|
||||||
|
[0xF0 ... 0xF7] = 4,
|
||||||
|
};
|
||||||
|
return tbl[ch];
|
||||||
|
}
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
static inline size_t block_scan_plain_ascii(const char *restrict input, size_t end, size_t i)
|
||||||
|
{
|
||||||
|
#ifdef __SSE2__
|
||||||
|
const __m128i m_star = _mm_set1_epi8('*');
|
||||||
|
const __m128i m_us = _mm_set1_epi8('_');
|
||||||
|
const __m128i m_tick = _mm_set1_epi8('`');
|
||||||
|
const __m128i m_dol = _mm_set1_epi8('$');
|
||||||
|
const __m128i m_bang = _mm_set1_epi8('!');
|
||||||
|
const __m128i m_lb = _mm_set1_epi8('[');
|
||||||
|
const __m128i m_amp = _mm_set1_epi8('&');
|
||||||
|
const __m128i m_lt = _mm_set1_epi8('<');
|
||||||
|
const __m128i m_gt = _mm_set1_epi8('>');
|
||||||
|
const __m128i m_bs = _mm_set1_epi8('\\');
|
||||||
|
const __m128i m_tilde = _mm_set1_epi8('~');
|
||||||
|
while (i + 16 <= end) {
|
||||||
|
__m128i v = _mm_loadu_si128((const __m128i *)(input + i));
|
||||||
|
if (_mm_movemask_epi8(v))
|
||||||
|
break; /* high bit => UTF-8 */
|
||||||
|
__m128i hit = _mm_cmpeq_epi8(v, m_star);
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_us));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_tick));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_dol));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_bang));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_lb));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_amp));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_lt));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_gt));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_bs));
|
||||||
|
hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, m_tilde));
|
||||||
|
int mask = _mm_movemask_epi8(hit);
|
||||||
|
if (mask)
|
||||||
|
return i + (unsigned)__builtin_ctz(mask);
|
||||||
|
i += 16;
|
||||||
|
}
|
||||||
|
#elif defined(__ARM_NEON) || defined(__aarch64__)
|
||||||
|
while (i + 16 <= end) {
|
||||||
|
uint8x16_t v = vld1q_u8((const uint8_t *)(input + i));
|
||||||
|
if (vmaxvq_u8(vandq_u8(v, vdupq_n_u8(0x80))))
|
||||||
|
break;
|
||||||
|
uint8x16_t hit = vceqq_u8(v, vdupq_n_u8('*'));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('_')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('`')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('$')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('!')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('[')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('&')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('<')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('>')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('\\')));
|
||||||
|
hit = vorrq_u8(hit, vceqq_u8(v, vdupq_n_u8('~')));
|
||||||
|
if (vmaxvq_u8(hit)) {
|
||||||
|
uint8_t lanes[16];
|
||||||
|
vst1q_u8(lanes, hit);
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < 16 && !lanes[pos]) pos++;
|
||||||
|
return i + pos;
|
||||||
|
}
|
||||||
|
i += 16;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
size_t j = i;
|
||||||
|
while (j + 4 <= end) {
|
||||||
|
uint32_t v;
|
||||||
|
memcpy(&v, input + j, 4);
|
||||||
|
if (v & 0x80808080)
|
||||||
|
break;
|
||||||
|
if (CH_SCAN_STOP[(v & 0xFF)] ||
|
||||||
|
CH_SCAN_STOP[(v >> 8) & 0xFF] ||
|
||||||
|
CH_SCAN_STOP[(v >> 16) & 0xFF] ||
|
||||||
|
CH_SCAN_STOP[(v >> 24) & 0xFF])
|
||||||
|
break;
|
||||||
|
j += 4;
|
||||||
|
}
|
||||||
|
while (j < end) {
|
||||||
|
unsigned char c = (unsigned char)input[j];
|
||||||
|
if ((c & 0x80))
|
||||||
|
break;
|
||||||
|
if (unlikely(CH_SCAN_STOP[c]))
|
||||||
|
break;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t block_scan_utf8_run(const char *input, size_t end, size_t i)
|
||||||
|
{
|
||||||
|
size_t j = i;
|
||||||
|
while (j < end) {
|
||||||
|
unsigned char c = (unsigned char)input[j];
|
||||||
|
if (likely((c & 0x80) == 0))
|
||||||
|
break;
|
||||||
|
size_t seq = block_utf8_seq_len(c);
|
||||||
|
if (j + seq > end)
|
||||||
|
break;
|
||||||
|
j += seq;
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t block_scan_until_byte(const char *input, size_t end, size_t i, char delim)
|
||||||
|
{
|
||||||
|
if (i >= end)
|
||||||
|
return i;
|
||||||
|
const char *hit = memchr(input + i, delim, end - i);
|
||||||
|
return hit ? (size_t)(hit - input) : end;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t block_scan_until_2byte(const char *input, size_t end, size_t start,
|
||||||
|
char c1, char c2)
|
||||||
|
{
|
||||||
|
size_t i = start;
|
||||||
|
while (i + 1 < end) {
|
||||||
|
const char *hit = memchr(input + i, c1, end - i);
|
||||||
|
if (!hit) return end;
|
||||||
|
i = (size_t)(hit - input);
|
||||||
|
if (i + 1 < end && input[i + 1] == c2)
|
||||||
|
return i;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return end;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t block_line_end(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
return line_end_ix((line_index_t *)ctx->line_ix, input, len, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t block_skip_line(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
return skip_line_ix((line_index_t *)ctx->line_ix, input, len, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void block_line_seek(sashidown_ctx_t *ctx, size_t pos)
|
||||||
|
{
|
||||||
|
line_index_seek((line_index_t *)ctx->line_ix, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
int block_count_heading(const char *input, size_t len, size_t pos, size_t *content_start);
|
||||||
|
int block_is_fence(const char *input, size_t len, size_t pos,
|
||||||
|
int *fence_len, char *fence_char);
|
||||||
|
void block_fence_info_range(const char *input, size_t len, size_t pos,
|
||||||
|
int fence_len, char fence_char,
|
||||||
|
size_t *info_start, size_t *info_end);
|
||||||
|
int block_is_thematic_break(const char *input, size_t pos, size_t le);
|
||||||
|
int block_is_setext_underline(const char *input, size_t pos, size_t le, int *level);
|
||||||
|
int block_is_blockquote(const char *input, size_t len, size_t pos, size_t *content_start);
|
||||||
|
int block_is_ul(const char *input, size_t len, size_t pos, size_t *content_start);
|
||||||
|
int block_is_ol(const char *input, size_t len, size_t pos, size_t *content_start, int *num);
|
||||||
|
int block_parse_task_marker(const char *input, size_t len, size_t content_start,
|
||||||
|
int *task, size_t *text_start);
|
||||||
|
int block_line_starts_table(const char *input, size_t len, size_t pos);
|
||||||
|
int block_is_latex_line(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, size_t *content_start, size_t *content_end);
|
||||||
|
|
||||||
|
/* Peek next-line setext underline. Does NOT advance line_index on failure.
|
||||||
|
* `le` must be block_line_end(pos). Caller should only invoke for LK_PARA lines.
|
||||||
|
* Returns 1 = heading, 0 = no, -1 = defer (need next line). */
|
||||||
|
int block_try_setext(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, size_t le, size_t stop, int is_final,
|
||||||
|
line_info_t *info, size_t *end_pos);
|
||||||
|
|
||||||
|
line_info_t block_classify_line(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, size_t le);
|
||||||
|
|
||||||
|
#endif /* SASHIDOWN_BLOCK_SCAN_H */
|
||||||
Vendored
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#include "sashidown.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
char *cmark_markdown_to_html(const char *text, int len)
|
||||||
|
{
|
||||||
|
if (!text)
|
||||||
|
return NULL;
|
||||||
|
if (len < 0)
|
||||||
|
len = (int)strlen(text);
|
||||||
|
|
||||||
|
size_t in_len = (size_t)len;
|
||||||
|
/* HTML typically expands; grow until parse fits (thread-safe, no static buf). */
|
||||||
|
size_t cap = in_len * 4 + 256;
|
||||||
|
if (cap < 1024)
|
||||||
|
cap = 1024;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
char *buf = (char *)malloc(cap);
|
||||||
|
if (!buf)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
size_t out_len = 0;
|
||||||
|
if (sashidown_parse(text, in_len, buf, cap, &out_len) == 0) {
|
||||||
|
char *fit = (char *)realloc(buf, out_len + 1);
|
||||||
|
return fit ? fit : buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(buf);
|
||||||
|
if (cap > SIZE_MAX / 2)
|
||||||
|
return NULL;
|
||||||
|
size_t next = cap * 2;
|
||||||
|
/* Bail if expansion is absurd relative to input. */
|
||||||
|
if (next > in_len * 64 + 4096)
|
||||||
|
return NULL;
|
||||||
|
cap = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sashidown_free(void *ptr)
|
||||||
|
{
|
||||||
|
free(ptr);
|
||||||
|
}
|
||||||
Vendored
+110
@@ -0,0 +1,110 @@
|
|||||||
|
#include "fsm.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
const uint8_t CH_HTML_ESCAPE[256] = {
|
||||||
|
['&'] = 1, ['<'] = 1, ['>'] = 1, ['"'] = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint8_t CH_JSON_ESCAPE[256] = {
|
||||||
|
['"'] = 1, ['\\'] = 1,
|
||||||
|
[0x00] = 1, [0x01] = 1, [0x02] = 1, [0x03] = 1,
|
||||||
|
[0x04] = 1, [0x05] = 1, [0x06] = 1, [0x07] = 1,
|
||||||
|
[0x08] = 1, [0x09] = 1, [0x0A] = 1, [0x0B] = 1,
|
||||||
|
[0x0C] = 1, [0x0D] = 1, [0x0E] = 1, [0x0F] = 1,
|
||||||
|
[0x10] = 1, [0x11] = 1, [0x12] = 1, [0x13] = 1,
|
||||||
|
[0x14] = 1, [0x15] = 1, [0x16] = 1, [0x17] = 1,
|
||||||
|
[0x18] = 1, [0x19] = 1, [0x1A] = 1, [0x1B] = 1,
|
||||||
|
[0x1C] = 1, [0x1D] = 1, [0x1E] = 1, [0x1F] = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Inline special + HTML escape; combine with (c & 0x80) in the scan loop. */
|
||||||
|
const uint8_t CH_SCAN_STOP[256] = {
|
||||||
|
['*'] = 1, ['_'] = 1, ['`'] = 1, ['$'] = 1, ['!'] = 1, ['['] = 1,
|
||||||
|
['&'] = 1, ['<'] = 1, ['>'] = 1, ['\\'] = 1, ['~'] = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
void fsm_write_begin(sashidown_ctx_t *ctx)
|
||||||
|
{
|
||||||
|
ctx->overflow = 0;
|
||||||
|
ctx->write_ptr = ctx->output + ctx->output_len;
|
||||||
|
ctx->write_end = ctx->output + ctx->output_cap - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fsm_write_end(sashidown_ctx_t *ctx)
|
||||||
|
{
|
||||||
|
if (ctx->write_ptr)
|
||||||
|
ctx->output_len = (size_t)(ctx->write_ptr - ctx->output);
|
||||||
|
ctx->write_ptr = NULL;
|
||||||
|
ctx->write_end = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsm_append(sashidown_ctx_t *ctx, char c)
|
||||||
|
{
|
||||||
|
if (unlikely(ctx->overflow))
|
||||||
|
return -1;
|
||||||
|
if (ctx->write_ptr) {
|
||||||
|
if (unlikely(ctx->write_ptr >= ctx->write_end)) {
|
||||||
|
ctx->overflow = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*ctx->write_ptr++ = c;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (unlikely(ctx->output_len + 1 >= ctx->output_cap)) {
|
||||||
|
ctx->overflow = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
ctx->output[ctx->output_len++] = c;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsm_append_len(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n)
|
||||||
|
{
|
||||||
|
if (n == 0)
|
||||||
|
return 0;
|
||||||
|
if (unlikely(ctx->overflow))
|
||||||
|
return -1;
|
||||||
|
if (ctx->write_ptr) {
|
||||||
|
if (unlikely(ctx->write_ptr + n > ctx->write_end)) {
|
||||||
|
ctx->overflow = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(ctx->write_ptr, s, n);
|
||||||
|
ctx->write_ptr += n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (unlikely(ctx->output_len + n >= ctx->output_cap)) {
|
||||||
|
ctx->overflow = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(ctx->output + ctx->output_len, s, n);
|
||||||
|
ctx->output_len += n;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fsm_append_str(sashidown_ctx_t *ctx, const char *s)
|
||||||
|
{
|
||||||
|
return fsm_append_len(ctx, s, strlen(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
void fsm_append_fast(sashidown_ctx_t *ctx, char c)
|
||||||
|
{
|
||||||
|
if (unlikely(ctx->overflow)) return;
|
||||||
|
if (unlikely(ctx->write_ptr >= ctx->write_end)) {
|
||||||
|
ctx->overflow = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*ctx->write_ptr++ = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fsm_append_len_fast(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n)
|
||||||
|
{
|
||||||
|
if (n == 0) return;
|
||||||
|
if (unlikely(ctx->overflow)) return;
|
||||||
|
if (unlikely(ctx->write_ptr + n > ctx->write_end)) {
|
||||||
|
ctx->overflow = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(ctx->write_ptr, s, n);
|
||||||
|
ctx->write_ptr += n;
|
||||||
|
}
|
||||||
Vendored
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef MD_FSM_H
|
||||||
|
#define MD_FSM_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "sashidown.h"
|
||||||
|
|
||||||
|
#ifndef likely
|
||||||
|
#define likely(x) __builtin_expect(!!(x), 1)
|
||||||
|
#endif
|
||||||
|
#ifndef unlikely
|
||||||
|
#define unlikely(x) __builtin_expect(!!(x), 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern const uint8_t CH_HTML_ESCAPE[256];
|
||||||
|
extern const uint8_t CH_SCAN_STOP[256];
|
||||||
|
extern const uint8_t CH_JSON_ESCAPE[256];
|
||||||
|
|
||||||
|
int fsm_append(sashidown_ctx_t *ctx, char c);
|
||||||
|
int fsm_append_str(sashidown_ctx_t *ctx, const char *s);
|
||||||
|
int fsm_append_len(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n);
|
||||||
|
|
||||||
|
void fsm_append_fast(sashidown_ctx_t *ctx, char c);
|
||||||
|
void fsm_append_len_fast(sashidown_ctx_t *restrict ctx, const char *restrict s, size_t n);
|
||||||
|
|
||||||
|
void fsm_write_begin(sashidown_ctx_t *ctx);
|
||||||
|
void fsm_write_end(sashidown_ctx_t *ctx);
|
||||||
|
|
||||||
|
#define fsm_append_lit(ctx, s) fsm_append_len((ctx), (s), sizeof(s) - 1)
|
||||||
|
|
||||||
|
#endif /* MD_FSM_H */
|
||||||
Vendored
+184
@@ -0,0 +1,184 @@
|
|||||||
|
#include "line_index.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define LINE_IX_DYNAMIC_INIT 256
|
||||||
|
|
||||||
|
static size_t scan_line_end(const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
const char *hit = memchr(input + pos, '\n', len - pos);
|
||||||
|
return hit ? (size_t)(hit - input) : len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t scan_skip_line(const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
pos = scan_line_end(input, len, pos);
|
||||||
|
if (pos < len)
|
||||||
|
pos++;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int line_index_ensure(line_index_t *ix, uint32_t need_slots)
|
||||||
|
{
|
||||||
|
if (need_slots <= ix->cap)
|
||||||
|
return 0;
|
||||||
|
if (!ix->owned) {
|
||||||
|
ix->valid = 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
uint32_t cap = ix->cap ? ix->cap : LINE_IX_DYNAMIC_INIT;
|
||||||
|
while (cap < need_slots)
|
||||||
|
cap *= 2;
|
||||||
|
uint32_t *p = (uint32_t *)realloc(ix->off, (size_t)cap * sizeof(uint32_t));
|
||||||
|
if (!p) {
|
||||||
|
ix->valid = 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
ix->off = p;
|
||||||
|
ix->cap = cap;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int line_index_init_dynamic(line_index_t *ix, uint32_t initial_cap)
|
||||||
|
{
|
||||||
|
if (!ix)
|
||||||
|
return -1;
|
||||||
|
if (initial_cap < 2)
|
||||||
|
initial_cap = LINE_IX_DYNAMIC_INIT;
|
||||||
|
uint32_t *off = (uint32_t *)malloc((size_t)initial_cap * sizeof(uint32_t));
|
||||||
|
if (!off)
|
||||||
|
return -1;
|
||||||
|
ix->off = off;
|
||||||
|
ix->cap = initial_cap;
|
||||||
|
ix->n = 1;
|
||||||
|
ix->off[0] = 0;
|
||||||
|
ix->off[1] = 0;
|
||||||
|
ix->cur = 0;
|
||||||
|
ix->valid = 1;
|
||||||
|
ix->owned = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void line_index_free(line_index_t *ix)
|
||||||
|
{
|
||||||
|
if (!ix)
|
||||||
|
return;
|
||||||
|
if (ix->owned) {
|
||||||
|
free(ix->off);
|
||||||
|
ix->off = NULL;
|
||||||
|
ix->owned = 0;
|
||||||
|
ix->cap = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void line_index_build(const char *input, size_t len, line_index_t *ix)
|
||||||
|
{
|
||||||
|
ix->n = 1;
|
||||||
|
ix->off[0] = 0;
|
||||||
|
ix->cur = 0;
|
||||||
|
ix->valid = 1;
|
||||||
|
|
||||||
|
const char *start = input;
|
||||||
|
const char *end = input + len;
|
||||||
|
const char *hit;
|
||||||
|
while ((hit = memchr(start, '\n', (size_t)(end - start))) != NULL) {
|
||||||
|
if (ix->n + 1 >= ix->cap) {
|
||||||
|
if (line_index_ensure(ix, ix->n + 2) != 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ix->off[ix->n++] = (uint32_t)(hit - input) + 1;
|
||||||
|
start = hit + 1;
|
||||||
|
}
|
||||||
|
if (ix->n >= ix->cap) {
|
||||||
|
if (line_index_ensure(ix, ix->n + 1) != 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ix->off[ix->n] = (uint32_t)len;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t line_index_extend(line_index_t *ix, const char *input,
|
||||||
|
size_t old_len, size_t new_len)
|
||||||
|
{
|
||||||
|
if (!ix || !ix->valid || new_len <= old_len)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
size_t last_nl = 0;
|
||||||
|
const char *start = input + old_len;
|
||||||
|
const char *end = input + new_len;
|
||||||
|
const char *hit;
|
||||||
|
while ((hit = memchr(start, '\n', (size_t)(end - start))) != NULL) {
|
||||||
|
if (ix->n + 1 >= ix->cap) {
|
||||||
|
if (line_index_ensure(ix, ix->n + 2) != 0)
|
||||||
|
return last_nl;
|
||||||
|
}
|
||||||
|
last_nl = (size_t)(hit - input) + 1;
|
||||||
|
ix->off[ix->n++] = (uint32_t)last_nl;
|
||||||
|
start = hit + 1;
|
||||||
|
}
|
||||||
|
if (ix->n >= ix->cap) {
|
||||||
|
if (line_index_ensure(ix, ix->n + 1) != 0)
|
||||||
|
return last_nl;
|
||||||
|
}
|
||||||
|
ix->off[ix->n] = (uint32_t)new_len;
|
||||||
|
return last_nl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t line_no(const line_index_t *ix, size_t pos)
|
||||||
|
{
|
||||||
|
uint32_t lo = 0;
|
||||||
|
uint32_t hi = ix->n;
|
||||||
|
while (lo + 1 < hi) {
|
||||||
|
uint32_t mid = (lo + hi) / 2;
|
||||||
|
if (ix->off[mid] <= pos)
|
||||||
|
lo = mid;
|
||||||
|
else
|
||||||
|
hi = mid;
|
||||||
|
}
|
||||||
|
return lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void line_index_seek(line_index_t *ix, size_t pos)
|
||||||
|
{
|
||||||
|
if (!ix || !ix->valid)
|
||||||
|
return;
|
||||||
|
ix->cur = line_no(ix, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t resolve_line(line_index_t *ix, size_t pos)
|
||||||
|
{
|
||||||
|
/* Sequential hot path: render walks lines in order. */
|
||||||
|
if (ix->cur < ix->n && ix->off[ix->cur] == (uint32_t)pos)
|
||||||
|
return ix->cur;
|
||||||
|
if (ix->cur + 1 < ix->n && ix->off[ix->cur + 1] == (uint32_t)pos) {
|
||||||
|
ix->cur++;
|
||||||
|
return ix->cur;
|
||||||
|
}
|
||||||
|
ix->cur = line_no(ix, pos);
|
||||||
|
return ix->cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t line_end_ix(line_index_t *ix, const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
if (!ix || !ix->valid)
|
||||||
|
return scan_line_end(input, len, pos);
|
||||||
|
|
||||||
|
uint32_t k = resolve_line(ix, pos);
|
||||||
|
size_t next = (size_t)ix->off[k + 1];
|
||||||
|
if (next > 0 && next <= len && input[next - 1] == '\n')
|
||||||
|
return next - 1;
|
||||||
|
return next <= len ? next : len;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t skip_line_ix(line_index_t *ix, const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
if (!ix || !ix->valid)
|
||||||
|
return scan_skip_line(input, len, pos);
|
||||||
|
|
||||||
|
uint32_t k = resolve_line(ix, pos);
|
||||||
|
size_t next = (size_t)ix->off[k + 1];
|
||||||
|
if (next > len)
|
||||||
|
return len;
|
||||||
|
/* Advance cursor to the next line start (may equal n at EOF). */
|
||||||
|
ix->cur = k + 1;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
Vendored
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef MD_LINE_INDEX_H
|
||||||
|
#define MD_LINE_INDEX_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define SASHIDOWN_MAX_LINE_INDEX 16384
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t *off; /* line-start offsets; off[n] is end sentinel */
|
||||||
|
uint32_t n; /* number of line starts (off[0]..off[n-1]) */
|
||||||
|
uint32_t cur; /* sequential cursor: line index for last lookup */
|
||||||
|
uint32_t cap; /* slots in off[] (must be >= n+1 when valid) */
|
||||||
|
int valid; /* 0 if doc exceeded index capacity / OOM */
|
||||||
|
int owned; /* 1 if off was malloc'd (stream path) */
|
||||||
|
} line_index_t;
|
||||||
|
|
||||||
|
/* Stack-backed index for zero-malloc parse/convert hot path. */
|
||||||
|
#define LINE_INDEX_STACK(name) \
|
||||||
|
uint32_t name##_off_[SASHIDOWN_MAX_LINE_INDEX + 1]; \
|
||||||
|
line_index_t name = { \
|
||||||
|
.off = name##_off_, .n = 0, .cur = 0, \
|
||||||
|
.cap = SASHIDOWN_MAX_LINE_INDEX + 1, .valid = 1, .owned = 0 \
|
||||||
|
}
|
||||||
|
|
||||||
|
int line_index_init_dynamic(line_index_t *ix, uint32_t initial_cap);
|
||||||
|
void line_index_free(line_index_t *ix);
|
||||||
|
|
||||||
|
void line_index_build(const char *input, size_t len, line_index_t *ix);
|
||||||
|
|
||||||
|
/* Extend for input[old_len..new_len). Returns absolute offset after the last
|
||||||
|
* newline in that range, or 0 if none. */
|
||||||
|
size_t line_index_extend(line_index_t *ix, const char *input,
|
||||||
|
size_t old_len, size_t new_len);
|
||||||
|
|
||||||
|
void line_index_seek(line_index_t *ix, size_t pos);
|
||||||
|
size_t line_end_ix(line_index_t *ix, const char *input, size_t len, size_t pos);
|
||||||
|
size_t skip_line_ix(line_index_t *ix, const char *input, size_t len, size_t pos);
|
||||||
|
|
||||||
|
#endif /* MD_LINE_INDEX_H */
|
||||||
Vendored
+1202
File diff suppressed because it is too large
Load Diff
Vendored
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#ifndef MD_RENDER_H
|
||||||
|
#define MD_RENDER_H
|
||||||
|
|
||||||
|
#include "sashidown.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LK_EMPTY,
|
||||||
|
LK_FENCE,
|
||||||
|
LK_HEADING,
|
||||||
|
LK_HR,
|
||||||
|
LK_BQ,
|
||||||
|
LK_UL,
|
||||||
|
LK_OL,
|
||||||
|
LK_LATEX_LINE,
|
||||||
|
LK_LATEX_BLOCK,
|
||||||
|
LK_PARA,
|
||||||
|
} line_kind_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
line_kind_t kind;
|
||||||
|
size_t content_start;
|
||||||
|
size_t content_end;
|
||||||
|
int heading_level;
|
||||||
|
int fence_len;
|
||||||
|
char fence_char; /* '`' or '~' */
|
||||||
|
size_t info_start; /* fence info string */
|
||||||
|
size_t info_end;
|
||||||
|
int task; /* 0=none, 1=unchecked, 2=checked */
|
||||||
|
} line_info_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* [CST State Q] — open-block environment registers (finite CST state).
|
||||||
|
* Tracks which block containers are currently open so the transducer can
|
||||||
|
* emit close tags without an AST. Constant size; independent of |B|.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t ul;
|
||||||
|
uint8_t ol;
|
||||||
|
uint8_t bq;
|
||||||
|
uint8_t para;
|
||||||
|
} block_env_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* [CST State Q] — full confirmation-state transducer state.
|
||||||
|
* Holds block_env plus soft-window / delayed-commit registers (fence,
|
||||||
|
* LaTeX, pending line, table gate). Maps to CST state Q in the paper:
|
||||||
|
* Scan(B[0..stop)) advances Q and emits only confirmed Y.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
block_env_t block; /* [CST State Q] nested open-block bits */
|
||||||
|
int pending_valid;
|
||||||
|
size_t pending_pos;
|
||||||
|
size_t pending_le;
|
||||||
|
line_info_t pending_li;
|
||||||
|
int in_fence;
|
||||||
|
int fence_len;
|
||||||
|
char fence_char;
|
||||||
|
size_t fence_info_start;
|
||||||
|
size_t fence_info_end;
|
||||||
|
int in_latex_block;
|
||||||
|
size_t latex_start;
|
||||||
|
uint8_t para_hard_break;
|
||||||
|
/* Streaming table (scheme C): after header+sep committed, body rows
|
||||||
|
* emit immediately; close on empty / non-pipe / final. */
|
||||||
|
int in_table;
|
||||||
|
int table_cols;
|
||||||
|
/* Inline emphasis/code spans an open <p> across soft line breaks. */
|
||||||
|
uint8_t inline_bold;
|
||||||
|
uint8_t inline_italic;
|
||||||
|
uint8_t inline_code;
|
||||||
|
uint8_t inline_latex;
|
||||||
|
uint8_t inline_strike;
|
||||||
|
} sashidown_render_state_t;
|
||||||
|
|
||||||
|
void sashidown_render_state_init(sashidown_render_state_t *state);
|
||||||
|
|
||||||
|
/* Render input[start..stop) or full len when is_final. Updates *pos_io. */
|
||||||
|
int sashidown_render_incremental(
|
||||||
|
sashidown_ctx_t *restrict ctx,
|
||||||
|
sashidown_render_state_t *restrict state,
|
||||||
|
const char *restrict input, size_t len,
|
||||||
|
size_t *pos_io,
|
||||||
|
size_t stop,
|
||||||
|
int is_final);
|
||||||
|
|
||||||
|
int sashidown_render(sashidown_ctx_t *ctx, const char *input, size_t len);
|
||||||
|
|
||||||
|
#endif /* MD_RENDER_H */
|
||||||
Vendored
+559
@@ -0,0 +1,559 @@
|
|||||||
|
#include "render_ansi.h"
|
||||||
|
#include "ansi_style.h"
|
||||||
|
#include "block_scan.h"
|
||||||
|
#include "table_stats.h"
|
||||||
|
#include "fsm.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t bold;
|
||||||
|
uint8_t italic;
|
||||||
|
uint8_t code;
|
||||||
|
uint8_t latex;
|
||||||
|
uint8_t strike;
|
||||||
|
} ansi_inline_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t ul;
|
||||||
|
uint8_t ol;
|
||||||
|
uint8_t bq;
|
||||||
|
uint8_t para;
|
||||||
|
int ol_num;
|
||||||
|
} ansi_env_t;
|
||||||
|
|
||||||
|
static void ansi_s(sashidown_ctx_t *ctx, const char *s)
|
||||||
|
{
|
||||||
|
if (s && s[0])
|
||||||
|
fsm_append_str(ctx, s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ansi_close_lists(ansi_env_t *env, sashidown_ctx_t *ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
env->ul = 0;
|
||||||
|
env->ol = 0;
|
||||||
|
env->ol_num = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ansi_close_para(ansi_env_t *env, sashidown_ctx_t *ctx)
|
||||||
|
{
|
||||||
|
if (env->para) {
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
env->para = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ansi_close_all(ansi_env_t *env, sashidown_ctx_t *ctx)
|
||||||
|
{
|
||||||
|
ansi_close_lists(env, ctx);
|
||||||
|
ansi_close_para(env, ctx);
|
||||||
|
env->bq = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ansi_render_link(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t *pos, int is_image)
|
||||||
|
{
|
||||||
|
size_t i = *pos + 1;
|
||||||
|
if (is_image) {
|
||||||
|
if (i >= len || input[i] != '[')
|
||||||
|
return 0;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
size_t text_start = i;
|
||||||
|
i = block_scan_until_byte(input, len, i, ']');
|
||||||
|
if (i >= len || input[i] != ']') return 0;
|
||||||
|
size_t text_end = i;
|
||||||
|
i++;
|
||||||
|
if (i >= len || input[i] != '(') return 0;
|
||||||
|
i++;
|
||||||
|
size_t url_start = i;
|
||||||
|
i = block_scan_until_byte(input, len, i, ')');
|
||||||
|
if (i >= len) return 0;
|
||||||
|
|
||||||
|
if (is_image) {
|
||||||
|
ansi_s(ctx, th->image_on);
|
||||||
|
fsm_append_lit(ctx, "[img:");
|
||||||
|
fsm_append_len(ctx, input + text_start, text_end - text_start);
|
||||||
|
fsm_append_lit(ctx, "](");
|
||||||
|
ansi_s(ctx, th->url_on);
|
||||||
|
fsm_append_len(ctx, input + url_start, i - url_start);
|
||||||
|
ansi_s(ctx, th->url_off);
|
||||||
|
fsm_append(ctx, ')');
|
||||||
|
ansi_s(ctx, th->image_off);
|
||||||
|
} else {
|
||||||
|
ansi_s(ctx, th->link_on);
|
||||||
|
fsm_append_len(ctx, input + text_start, text_end - text_start);
|
||||||
|
ansi_s(ctx, th->link_off);
|
||||||
|
fsm_append_lit(ctx, " (");
|
||||||
|
ansi_s(ctx, th->url_on);
|
||||||
|
fsm_append_len(ctx, input + url_start, i - url_start);
|
||||||
|
ansi_s(ctx, th->url_off);
|
||||||
|
fsm_append(ctx, ')');
|
||||||
|
}
|
||||||
|
*pos = i + 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ansi_render_inline(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t start, size_t end)
|
||||||
|
{
|
||||||
|
ansi_inline_t st = {0, 0, 0, 0, 0};
|
||||||
|
size_t i = start;
|
||||||
|
|
||||||
|
while (i < end) {
|
||||||
|
if (st.latex) {
|
||||||
|
size_t j = block_scan_until_byte(input, end, i, '$');
|
||||||
|
if (j > i)
|
||||||
|
fsm_append_len(ctx, input + i, j - i);
|
||||||
|
if (j < end) {
|
||||||
|
fsm_append(ctx, '$');
|
||||||
|
ansi_s(ctx, th->latex_off);
|
||||||
|
st.latex = 0;
|
||||||
|
i = j + 1;
|
||||||
|
} else {
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.code) {
|
||||||
|
size_t j = block_scan_until_byte(input, end, i, '`');
|
||||||
|
if (j > i)
|
||||||
|
fsm_append_len(ctx, input + i, j - i);
|
||||||
|
if (j < end) {
|
||||||
|
ansi_s(ctx, th->code_off);
|
||||||
|
st.code = 0;
|
||||||
|
i = j + 1;
|
||||||
|
} else {
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char ch = (unsigned char)input[i];
|
||||||
|
|
||||||
|
if ((ch & 0x80) == 0) {
|
||||||
|
size_t j = block_scan_plain_ascii(input, end, i);
|
||||||
|
if (j > i) {
|
||||||
|
fsm_append_len(ctx, input + i, j - i);
|
||||||
|
i = j;
|
||||||
|
if (i >= end)
|
||||||
|
break;
|
||||||
|
ch = (unsigned char)input[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size_t j = block_scan_utf8_run(input, end, i);
|
||||||
|
if (j > i) {
|
||||||
|
fsm_append_len(ctx, input + i, j - i);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '$' && i + 1 < end && input[i + 1] == '$') {
|
||||||
|
size_t j = block_scan_until_2byte(input, end, i + 2, '$', '$');
|
||||||
|
if (j < end)
|
||||||
|
j += 2;
|
||||||
|
ansi_s(ctx, th->latex_on);
|
||||||
|
fsm_append_len(ctx, input + i, j - i);
|
||||||
|
ansi_s(ctx, th->latex_off);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '$') {
|
||||||
|
ansi_s(ctx, th->latex_on);
|
||||||
|
fsm_append(ctx, '$');
|
||||||
|
st.latex = 1;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '!' && i + 1 < end && input[i + 1] == '[') {
|
||||||
|
if (ansi_render_link(ctx, th, input, len, &i, 1))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '[') {
|
||||||
|
if (ansi_render_link(ctx, th, input, len, &i, 0))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '\\' && i + 1 < end) {
|
||||||
|
unsigned char next = (unsigned char)input[i + 1];
|
||||||
|
if ((next >= 0x21 && next <= 0x2F) ||
|
||||||
|
(next >= 0x3A && next <= 0x40) ||
|
||||||
|
(next >= 0x5B && next <= 0x60) ||
|
||||||
|
(next >= 0x7B && next <= 0x7E)) {
|
||||||
|
fsm_append(ctx, (char)next);
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ch == '*' || ch == '_') && i + 1 < end && input[i + 1] == (char)ch) {
|
||||||
|
ansi_s(ctx, st.bold ? th->strong_off : th->strong_on);
|
||||||
|
st.bold = !st.bold;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '~' && i + 1 < end && input[i + 1] == '~') {
|
||||||
|
ansi_s(ctx, st.strike ? th->strike_off : th->strike_on);
|
||||||
|
st.strike = !st.strike;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '*' || ch == '_') {
|
||||||
|
ansi_s(ctx, st.italic ? th->emph_off : th->emph_on);
|
||||||
|
st.italic = !st.italic;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '`') {
|
||||||
|
ansi_s(ctx, th->code_on);
|
||||||
|
st.code = 1;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fsm_append(ctx, (char)ch);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.bold) ansi_s(ctx, th->strong_off);
|
||||||
|
if (st.italic) ansi_s(ctx, th->emph_off);
|
||||||
|
if (st.code) ansi_s(ctx, th->code_off);
|
||||||
|
if (st.strike) ansi_s(ctx, th->strike_off);
|
||||||
|
if (st.latex) ansi_s(ctx, th->latex_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void trim_cell(const char *input, size_t *start, size_t *end)
|
||||||
|
{
|
||||||
|
while (*start < *end && input[*start] == ' ')
|
||||||
|
(*start)++;
|
||||||
|
while (*end > *start && input[*end - 1] == ' ')
|
||||||
|
(*end)--;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ansi_table_row_at(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t pos, size_t le, int cols, int is_header)
|
||||||
|
{
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i < le && input[i] == '|')
|
||||||
|
i++;
|
||||||
|
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
size_t cs = i;
|
||||||
|
while (i < le && input[i] != '|') i++;
|
||||||
|
size_t ce = i;
|
||||||
|
trim_cell(input, &cs, &ce);
|
||||||
|
if (col)
|
||||||
|
ansi_s(ctx, th->sep);
|
||||||
|
if (is_header)
|
||||||
|
ansi_s(ctx, th->th_on);
|
||||||
|
ansi_render_inline(ctx, th, input, len, cs, ce);
|
||||||
|
if (is_header)
|
||||||
|
ansi_s(ctx, th->th_off);
|
||||||
|
if (i < le && input[i] == '|') i++;
|
||||||
|
}
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scheme C (batch): header+sep commit, then body rows one-pass. Returns 1 if
|
||||||
|
* a table was consumed (*pos_io advanced past body). */
|
||||||
|
static int ansi_try_consume_table(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
||||||
|
ansi_env_t *env, const char *input, size_t len,
|
||||||
|
size_t pos, size_t le, size_t *pos_io)
|
||||||
|
{
|
||||||
|
int has_pipe = 0;
|
||||||
|
int cols = table_count_cols(input + pos, le - pos, &has_pipe);
|
||||||
|
if (!has_pipe)
|
||||||
|
return 0;
|
||||||
|
if (le >= len || input[le] != '\n')
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
size_t next = le + 1;
|
||||||
|
if (next >= len)
|
||||||
|
return 0;
|
||||||
|
size_t nle = block_line_end(ctx, input, len, next);
|
||||||
|
if (!table_is_separator_row(input + next, nle - next)) {
|
||||||
|
block_line_seek(ctx, pos);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ansi_close_all(env, ctx);
|
||||||
|
ansi_table_row_at(ctx, th, input, len, pos, le, cols, 1);
|
||||||
|
pos = block_skip_line(ctx, input, len, next);
|
||||||
|
|
||||||
|
while (pos < len) {
|
||||||
|
size_t ble = block_line_end(ctx, input, len, pos);
|
||||||
|
int empty = (pos == ble || (pos + 1 == ble && input[pos] == '\r'));
|
||||||
|
if (empty)
|
||||||
|
break;
|
||||||
|
size_t blen = ble - pos;
|
||||||
|
if (!memchr(input + pos, '|', blen) ||
|
||||||
|
table_is_separator_row(input + pos, blen))
|
||||||
|
break;
|
||||||
|
ansi_table_row_at(ctx, th, input, len, pos, ble, cols, 0);
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
}
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
*pos_io = pos;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ansi_render_codeblock(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t *pos, int fence_len, char fence_char)
|
||||||
|
{
|
||||||
|
*pos = block_skip_line(ctx, input, len, *pos);
|
||||||
|
ansi_s(ctx, th->fence_on);
|
||||||
|
while (*pos < len) {
|
||||||
|
int fl = 0;
|
||||||
|
char fc = 0;
|
||||||
|
if (block_is_fence(input, len, *pos, &fl, &fc) &&
|
||||||
|
fc == fence_char && fl >= fence_len) {
|
||||||
|
size_t is, ie;
|
||||||
|
block_fence_info_range(input, len, *pos, fl, fc, &is, &ie);
|
||||||
|
if (ie <= is) {
|
||||||
|
*pos = block_skip_line(ctx, input, len, *pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size_t le = block_line_end(ctx, input, len, *pos);
|
||||||
|
fsm_append_lit(ctx, " ");
|
||||||
|
fsm_append_len(ctx, input + *pos, le - *pos);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
*pos = block_skip_line(ctx, input, len, *pos);
|
||||||
|
}
|
||||||
|
ansi_s(ctx, th->fence_off);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ansi_render_latex_block(sashidown_ctx_t *ctx, const ansi_theme_t *th,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t *pos)
|
||||||
|
{
|
||||||
|
size_t i = *pos + 2;
|
||||||
|
size_t start = i;
|
||||||
|
while (i + 1 < len) {
|
||||||
|
if (input[i] == '$' && input[i + 1] == '$') {
|
||||||
|
ansi_s(ctx, th->latex_on);
|
||||||
|
fsm_append_lit(ctx, "$$");
|
||||||
|
if (i > start)
|
||||||
|
fsm_append_len(ctx, input + start, i - start);
|
||||||
|
fsm_append_lit(ctx, "$$");
|
||||||
|
ansi_s(ctx, th->latex_off);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
i += 2;
|
||||||
|
if (i < len && input[i] == '\n') i++;
|
||||||
|
*pos = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
ansi_s(ctx, th->latex_on);
|
||||||
|
fsm_append_lit(ctx, "$$");
|
||||||
|
if (i > start)
|
||||||
|
fsm_append_len(ctx, input + start, (i < len ? i : len) - start);
|
||||||
|
fsm_append_lit(ctx, "$$");
|
||||||
|
ansi_s(ctx, th->latex_off);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
*pos = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sashidown_render_ansi_theme(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
const ansi_theme_t *th)
|
||||||
|
{
|
||||||
|
ansi_env_t env;
|
||||||
|
memset(&env, 0, sizeof(env));
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
fsm_write_begin(ctx);
|
||||||
|
|
||||||
|
while (pos < len) {
|
||||||
|
size_t le = block_line_end(ctx, input, len, pos);
|
||||||
|
if (pos == le || (pos + 1 == le && input[pos] == '\r')) {
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
if (env.bq)
|
||||||
|
env.bq = 0;
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memchr(input + pos, '|', le - pos) != NULL) {
|
||||||
|
size_t newt = 0;
|
||||||
|
if (ansi_try_consume_table(ctx, th, &env, input, len, pos, le, &newt)) {
|
||||||
|
pos = newt;
|
||||||
|
block_line_seek(ctx, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line_info_t li = block_classify_line(ctx, input, len, pos, le);
|
||||||
|
|
||||||
|
if (li.kind == LK_PARA) {
|
||||||
|
line_info_t sli;
|
||||||
|
size_t send = 0;
|
||||||
|
int sr = block_try_setext(ctx, input, len, pos, le, len, 1, &sli, &send);
|
||||||
|
if (sr > 0) {
|
||||||
|
int lvl = sli.heading_level;
|
||||||
|
if (lvl < 1) lvl = 1;
|
||||||
|
if (lvl > 6) lvl = 6;
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
ansi_s(ctx, th->heading[lvl]);
|
||||||
|
ansi_render_inline(ctx, th, input, len, sli.content_start, sli.content_end);
|
||||||
|
ansi_s(ctx, th->heading_end);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
if (lvl <= 2)
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = send;
|
||||||
|
block_line_seek(ctx, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (li.kind) {
|
||||||
|
case LK_EMPTY:
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
if (env.bq)
|
||||||
|
env.bq = 0;
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_HR:
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
fsm_append_lit(ctx, "---\n\n");
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_FENCE:
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
ansi_render_codeblock(ctx, th, input, len, &pos, li.fence_len, li.fence_char);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_LATEX_LINE:
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
ansi_s(ctx, th->latex_on);
|
||||||
|
fsm_append_lit(ctx, "$$");
|
||||||
|
fsm_append_len(ctx, input + li.content_start,
|
||||||
|
li.content_end - li.content_start);
|
||||||
|
fsm_append_lit(ctx, "$$");
|
||||||
|
ansi_s(ctx, th->latex_off);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_LATEX_BLOCK:
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
ansi_render_latex_block(ctx, th, input, len, &pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_HEADING: {
|
||||||
|
int lvl = li.heading_level;
|
||||||
|
if (lvl < 1) lvl = 1;
|
||||||
|
if (lvl > 6) lvl = 6;
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
ansi_s(ctx, th->heading[lvl]);
|
||||||
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
||||||
|
ansi_s(ctx, th->heading_end);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
if (lvl <= 2)
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LK_BQ:
|
||||||
|
ansi_close_lists(&env, ctx);
|
||||||
|
env.bq = 1;
|
||||||
|
ansi_s(ctx, th->bq_prefix);
|
||||||
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_UL:
|
||||||
|
if (env.ol) { env.ol = 0; env.ol_num = 0; }
|
||||||
|
env.ul = 1;
|
||||||
|
ansi_s(ctx, th->bullet);
|
||||||
|
if (li.task == 1)
|
||||||
|
fsm_append_lit(ctx, "[ ] ");
|
||||||
|
else if (li.task == 2)
|
||||||
|
fsm_append_lit(ctx, "[x] ");
|
||||||
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
||||||
|
ansi_s(ctx, th->bullet_end);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_OL: {
|
||||||
|
int num = 0;
|
||||||
|
size_t cs = li.content_start;
|
||||||
|
block_is_ol(input, len, pos, &cs, &num);
|
||||||
|
if (env.ul) env.ul = 0;
|
||||||
|
env.ol = 1;
|
||||||
|
char buf[32];
|
||||||
|
int n = snprintf(buf, sizeof(buf), "%d. ", num > 0 ? num : ++env.ol_num);
|
||||||
|
ansi_s(ctx, th->ol_on);
|
||||||
|
if (n > 0)
|
||||||
|
fsm_append_len(ctx, buf, (size_t)n);
|
||||||
|
ansi_s(ctx, th->ol_off);
|
||||||
|
ansi_render_inline(ctx, th, input, len, li.content_start, le);
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LK_PARA:
|
||||||
|
default:
|
||||||
|
if (env.bq) env.bq = 0;
|
||||||
|
ansi_close_lists(&env, ctx);
|
||||||
|
if (!env.para) {
|
||||||
|
env.para = 1;
|
||||||
|
} else {
|
||||||
|
fsm_append(ctx, '\n');
|
||||||
|
}
|
||||||
|
ansi_render_inline(ctx, th, input, len, pos, le);
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
if (pos >= len) {
|
||||||
|
ansi_close_para(&env, ctx);
|
||||||
|
} else {
|
||||||
|
size_t nle = block_line_end(ctx, input, len, pos);
|
||||||
|
line_info_t nli = block_classify_line(ctx, input, len, pos, nle);
|
||||||
|
if (nli.kind != LK_PARA)
|
||||||
|
ansi_close_para(&env, ctx);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->overflow) {
|
||||||
|
fsm_write_end(ctx);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ansi_close_all(&env, ctx);
|
||||||
|
fsm_write_end(ctx);
|
||||||
|
return ctx->overflow ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sashidown_render_ansi(sashidown_ctx_t *ctx, const char *input, size_t len)
|
||||||
|
{
|
||||||
|
return sashidown_render_ansi_theme(ctx, input, len, ansi_theme_color());
|
||||||
|
}
|
||||||
|
|
||||||
|
int sashidown_render_ansi_plain(sashidown_ctx_t *ctx, const char *input, size_t len)
|
||||||
|
{
|
||||||
|
return sashidown_render_ansi_theme(ctx, input, len, ansi_theme_plain());
|
||||||
|
}
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#ifndef SASHIDOWN_RENDER_ANSI_H
|
||||||
|
#define SASHIDOWN_RENDER_ANSI_H
|
||||||
|
|
||||||
|
#include "sashidown.h"
|
||||||
|
|
||||||
|
/* Default: built-in colorful theme. */
|
||||||
|
int sashidown_render_ansi(sashidown_ctx_t *ctx, const char *input, size_t len);
|
||||||
|
/* Monochrome / attribute-only theme. */
|
||||||
|
int sashidown_render_ansi_plain(sashidown_ctx_t *ctx, const char *input, size_t len);
|
||||||
|
|
||||||
|
#endif
|
||||||
Vendored
+620
@@ -0,0 +1,620 @@
|
|||||||
|
#include "render_json.h"
|
||||||
|
#include "block_scan.h"
|
||||||
|
#include "table_stats.h"
|
||||||
|
#include "fsm.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t bold;
|
||||||
|
uint8_t italic;
|
||||||
|
uint8_t code;
|
||||||
|
uint8_t latex;
|
||||||
|
uint8_t strike;
|
||||||
|
} json_inline_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t ul;
|
||||||
|
uint8_t ol;
|
||||||
|
uint8_t bq;
|
||||||
|
uint8_t para;
|
||||||
|
int need_comma;
|
||||||
|
} json_env_t;
|
||||||
|
|
||||||
|
static void json_comma(sashidown_ctx_t *ctx, json_env_t *env)
|
||||||
|
{
|
||||||
|
if (env->need_comma)
|
||||||
|
fsm_append(ctx, ',');
|
||||||
|
env->need_comma = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_escape(sashidown_ctx_t *ctx, const char *s, size_t n)
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < n) {
|
||||||
|
size_t j = i;
|
||||||
|
while (j < n) {
|
||||||
|
if (CH_JSON_ESCAPE[(unsigned char)s[j]])
|
||||||
|
break;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (j > i)
|
||||||
|
fsm_append_len(ctx, s + i, j - i);
|
||||||
|
if (j >= n)
|
||||||
|
break;
|
||||||
|
switch (s[j]) {
|
||||||
|
case '"': fsm_append_lit(ctx, "\\\""); break;
|
||||||
|
case '\\': fsm_append_lit(ctx, "\\\\"); break;
|
||||||
|
case '\n': fsm_append_lit(ctx, "\\n"); break;
|
||||||
|
case '\r': fsm_append_lit(ctx, "\\r"); break;
|
||||||
|
case '\t': fsm_append_lit(ctx, "\\t"); break;
|
||||||
|
default: {
|
||||||
|
char buf[7];
|
||||||
|
int m = snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)s[j]);
|
||||||
|
if (m > 0)
|
||||||
|
fsm_append_len(ctx, buf, (size_t)m);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = j + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_event_simple(sashidown_ctx_t *ctx, json_env_t *env, const char *t)
|
||||||
|
{
|
||||||
|
json_comma(ctx, env);
|
||||||
|
fsm_append_lit(ctx, "{\"t\":\"");
|
||||||
|
fsm_append_str(ctx, t);
|
||||||
|
fsm_append_lit(ctx, "\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_event_text(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *s, size_t n)
|
||||||
|
{
|
||||||
|
if (n == 0)
|
||||||
|
return;
|
||||||
|
json_comma(ctx, env);
|
||||||
|
fsm_append_lit(ctx, "{\"t\":\"text\",\"v\":\"");
|
||||||
|
json_escape(ctx, s, n);
|
||||||
|
fsm_append_lit(ctx, "\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_event_heading(sashidown_ctx_t *ctx, json_env_t *env, int level)
|
||||||
|
{
|
||||||
|
json_comma(ctx, env);
|
||||||
|
char buf[48];
|
||||||
|
int n = snprintf(buf, sizeof(buf), "{\"t\":\"heading\",\"level\":%d}", level);
|
||||||
|
if (n > 0)
|
||||||
|
fsm_append_len(ctx, buf, (size_t)n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_close_lists(sashidown_ctx_t *ctx, json_env_t *env)
|
||||||
|
{
|
||||||
|
if (env->ul) {
|
||||||
|
json_event_simple(ctx, env, "ul_end");
|
||||||
|
env->ul = 0;
|
||||||
|
}
|
||||||
|
if (env->ol) {
|
||||||
|
json_event_simple(ctx, env, "ol_end");
|
||||||
|
env->ol = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_close_para(sashidown_ctx_t *ctx, json_env_t *env)
|
||||||
|
{
|
||||||
|
if (env->para) {
|
||||||
|
json_event_simple(ctx, env, "para_end");
|
||||||
|
env->para = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_close_bq(sashidown_ctx_t *ctx, json_env_t *env)
|
||||||
|
{
|
||||||
|
if (env->bq) {
|
||||||
|
json_event_simple(ctx, env, "bq_end");
|
||||||
|
env->bq = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_close_all(sashidown_ctx_t *ctx, json_env_t *env)
|
||||||
|
{
|
||||||
|
json_close_lists(ctx, env);
|
||||||
|
json_close_para(ctx, env);
|
||||||
|
json_close_bq(ctx, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int json_render_link(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t *pos, int is_image)
|
||||||
|
{
|
||||||
|
size_t i = *pos + 1;
|
||||||
|
if (is_image) {
|
||||||
|
if (i >= len || input[i] != '[')
|
||||||
|
return 0;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
size_t text_start = i;
|
||||||
|
i = block_scan_until_byte(input, len, i, ']');
|
||||||
|
if (i >= len || input[i] != ']') return 0;
|
||||||
|
size_t text_end = i;
|
||||||
|
i++;
|
||||||
|
if (i >= len || input[i] != '(') return 0;
|
||||||
|
i++;
|
||||||
|
size_t url_start = i;
|
||||||
|
i = block_scan_until_byte(input, len, i, ')');
|
||||||
|
if (i >= len) return 0;
|
||||||
|
|
||||||
|
json_comma(ctx, env);
|
||||||
|
if (is_image)
|
||||||
|
fsm_append_lit(ctx, "{\"t\":\"image\",\"alt\":\"");
|
||||||
|
else
|
||||||
|
fsm_append_lit(ctx, "{\"t\":\"link\",\"text\":\"");
|
||||||
|
json_escape(ctx, input + text_start, text_end - text_start);
|
||||||
|
fsm_append_lit(ctx, "\",\"href\":\"");
|
||||||
|
json_escape(ctx, input + url_start, i - url_start);
|
||||||
|
fsm_append_lit(ctx, "\"}");
|
||||||
|
*pos = i + 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_flush_text(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *input, size_t *text_start, size_t i)
|
||||||
|
{
|
||||||
|
if (i > *text_start)
|
||||||
|
json_event_text(ctx, env, input + *text_start, i - *text_start);
|
||||||
|
*text_start = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_render_inline(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t start, size_t end)
|
||||||
|
{
|
||||||
|
json_inline_t st = {0, 0, 0, 0, 0};
|
||||||
|
size_t i = start;
|
||||||
|
size_t text_start = start;
|
||||||
|
|
||||||
|
while (i < end) {
|
||||||
|
if (st.latex) {
|
||||||
|
size_t j = block_scan_until_byte(input, end, i, '$');
|
||||||
|
if (j < end) {
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
/* include opening $ already emitted into latex stream via text */
|
||||||
|
json_event_text(ctx, env, input + i - 1, (j + 1) - (i - 1));
|
||||||
|
st.latex = 0;
|
||||||
|
i = j + 1;
|
||||||
|
text_start = i;
|
||||||
|
} else {
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (st.code) {
|
||||||
|
size_t j = block_scan_until_byte(input, end, i, '`');
|
||||||
|
if (j > i)
|
||||||
|
json_event_text(ctx, env, input + i, j - i);
|
||||||
|
if (j < end) {
|
||||||
|
json_event_simple(ctx, env, "code_off");
|
||||||
|
st.code = 0;
|
||||||
|
i = j + 1;
|
||||||
|
text_start = i;
|
||||||
|
} else {
|
||||||
|
i = j;
|
||||||
|
text_start = i;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char ch = (unsigned char)input[i];
|
||||||
|
|
||||||
|
if ((ch & 0x80) == 0) {
|
||||||
|
size_t j = block_scan_plain_ascii(input, end, i);
|
||||||
|
if (j > i) {
|
||||||
|
i = j;
|
||||||
|
if (i >= end)
|
||||||
|
break;
|
||||||
|
ch = (unsigned char)input[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size_t j = block_scan_utf8_run(input, end, i);
|
||||||
|
if (j > i) {
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '$' && i + 1 < end && input[i + 1] == '$') {
|
||||||
|
size_t j = block_scan_until_2byte(input, end, i + 2, '$', '$');
|
||||||
|
if (j < end)
|
||||||
|
j += 2;
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
json_event_text(ctx, env, input + i, j - i);
|
||||||
|
i = j;
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '$') {
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
st.latex = 1;
|
||||||
|
i++;
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '!' && i + 1 < end && input[i + 1] == '[') {
|
||||||
|
size_t save = i;
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
if (json_render_link(ctx, env, input, len, &i, 1)) {
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i = save;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '[') {
|
||||||
|
size_t save = i;
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
if (json_render_link(ctx, env, input, len, &i, 0)) {
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i = save;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '\\' && i + 1 < end) {
|
||||||
|
unsigned char next = (unsigned char)input[i + 1];
|
||||||
|
if ((next >= 0x21 && next <= 0x2F) ||
|
||||||
|
(next >= 0x3A && next <= 0x40) ||
|
||||||
|
(next >= 0x5B && next <= 0x60) ||
|
||||||
|
(next >= 0x7B && next <= 0x7E)) {
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
json_event_text(ctx, env, input + i + 1, 1);
|
||||||
|
i += 2;
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ch == '*' || ch == '_') && i + 1 < end && input[i + 1] == (char)ch) {
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
json_event_simple(ctx, env, st.bold ? "strong_off" : "strong_on");
|
||||||
|
st.bold = !st.bold;
|
||||||
|
i += 2;
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '~' && i + 1 < end && input[i + 1] == '~') {
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
json_event_simple(ctx, env, st.strike ? "strike_off" : "strike_on");
|
||||||
|
st.strike = !st.strike;
|
||||||
|
i += 2;
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '*' || ch == '_') {
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
json_event_simple(ctx, env, st.italic ? "emph_off" : "emph_on");
|
||||||
|
st.italic = !st.italic;
|
||||||
|
i++;
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch == '`') {
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
json_event_simple(ctx, env, "code_on");
|
||||||
|
st.code = 1;
|
||||||
|
i++;
|
||||||
|
text_start = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* special char that was a scan stop — keep accumulating as text */
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_flush_text(ctx, env, input, &text_start, i);
|
||||||
|
|
||||||
|
if (st.bold) json_event_simple(ctx, env, "strong_off");
|
||||||
|
if (st.italic) json_event_simple(ctx, env, "emph_off");
|
||||||
|
if (st.code) json_event_simple(ctx, env, "code_off");
|
||||||
|
if (st.strike) json_event_simple(ctx, env, "strike_off");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void trim_cell(const char *input, size_t *start, size_t *end)
|
||||||
|
{
|
||||||
|
while (*start < *end && input[*start] == ' ')
|
||||||
|
(*start)++;
|
||||||
|
while (*end > *start && input[*end - 1] == ' ')
|
||||||
|
(*end)--;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_table_row_at(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t pos, size_t le, int cols, int is_header)
|
||||||
|
{
|
||||||
|
json_event_simple(ctx, env, is_header ? "tr_header" : "tr");
|
||||||
|
size_t i = pos;
|
||||||
|
while (i < le && (input[i] == ' ' || input[i] == '\t')) i++;
|
||||||
|
if (i < le && input[i] == '|')
|
||||||
|
i++;
|
||||||
|
for (int col = 0; col < cols; col++) {
|
||||||
|
size_t cs = i;
|
||||||
|
while (i < le && input[i] != '|') i++;
|
||||||
|
size_t ce = i;
|
||||||
|
trim_cell(input, &cs, &ce);
|
||||||
|
json_event_simple(ctx, env, is_header ? "th" : "td");
|
||||||
|
json_render_inline(ctx, env, input, len, cs, ce);
|
||||||
|
json_event_simple(ctx, env, is_header ? "th_end" : "td_end");
|
||||||
|
if (i < le && input[i] == '|') i++;
|
||||||
|
}
|
||||||
|
json_event_simple(ctx, env, "tr_end");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scheme C (batch): header+sep commit, body one-pass. */
|
||||||
|
static int json_try_consume_table(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t pos, size_t le, size_t *pos_io)
|
||||||
|
{
|
||||||
|
int has_pipe = 0;
|
||||||
|
int cols = table_count_cols(input + pos, le - pos, &has_pipe);
|
||||||
|
if (!has_pipe)
|
||||||
|
return 0;
|
||||||
|
if (le >= len || input[le] != '\n')
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
size_t next = le + 1;
|
||||||
|
if (next >= len)
|
||||||
|
return 0;
|
||||||
|
size_t nle = block_line_end(ctx, input, len, next);
|
||||||
|
if (!table_is_separator_row(input + next, nle - next)) {
|
||||||
|
block_line_seek(ctx, pos);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_close_all(ctx, env);
|
||||||
|
json_event_simple(ctx, env, "table");
|
||||||
|
json_table_row_at(ctx, env, input, len, pos, le, cols, 1);
|
||||||
|
pos = block_skip_line(ctx, input, len, next);
|
||||||
|
|
||||||
|
while (pos < len) {
|
||||||
|
size_t ble = block_line_end(ctx, input, len, pos);
|
||||||
|
int empty = (pos == ble || (pos + 1 == ble && input[pos] == '\r'));
|
||||||
|
if (empty)
|
||||||
|
break;
|
||||||
|
size_t blen = ble - pos;
|
||||||
|
if (!memchr(input + pos, '|', blen) ||
|
||||||
|
table_is_separator_row(input + pos, blen))
|
||||||
|
break;
|
||||||
|
json_table_row_at(ctx, env, input, len, pos, ble, cols, 0);
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
}
|
||||||
|
json_event_simple(ctx, env, "table_end");
|
||||||
|
*pos_io = pos;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_render_codeblock(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t *pos, int fence_len, char fence_char)
|
||||||
|
{
|
||||||
|
json_event_simple(ctx, env, "fence");
|
||||||
|
*pos = block_skip_line(ctx, input, len, *pos);
|
||||||
|
while (*pos < len) {
|
||||||
|
int fl = 0;
|
||||||
|
char fc = 0;
|
||||||
|
if (block_is_fence(input, len, *pos, &fl, &fc) &&
|
||||||
|
fc == fence_char && fl >= fence_len) {
|
||||||
|
size_t is, ie;
|
||||||
|
block_fence_info_range(input, len, *pos, fl, fc, &is, &ie);
|
||||||
|
if (ie <= is) {
|
||||||
|
*pos = block_skip_line(ctx, input, len, *pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size_t le = block_line_end(ctx, input, len, *pos);
|
||||||
|
json_event_text(ctx, env, input + *pos, le - *pos);
|
||||||
|
if (le < len)
|
||||||
|
json_event_text(ctx, env, "\n", 1);
|
||||||
|
*pos = block_skip_line(ctx, input, len, *pos);
|
||||||
|
}
|
||||||
|
json_event_simple(ctx, env, "fence_end");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void json_render_latex_block(sashidown_ctx_t *ctx, json_env_t *env,
|
||||||
|
const char *input, size_t len,
|
||||||
|
size_t *pos)
|
||||||
|
{
|
||||||
|
size_t i = *pos + 2;
|
||||||
|
size_t start = i;
|
||||||
|
while (i + 1 < len) {
|
||||||
|
if (input[i] == '$' && input[i + 1] == '$') {
|
||||||
|
json_comma(ctx, env);
|
||||||
|
fsm_append_lit(ctx, "{\"t\":\"latex_block\",\"v\":\"");
|
||||||
|
json_escape(ctx, input + start, i - start);
|
||||||
|
fsm_append_lit(ctx, "\"}");
|
||||||
|
i += 2;
|
||||||
|
if (i < len && input[i] == '\n') i++;
|
||||||
|
*pos = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
json_comma(ctx, env);
|
||||||
|
fsm_append_lit(ctx, "{\"t\":\"latex_block\",\"v\":\"");
|
||||||
|
json_escape(ctx, input + start, (i < len ? i : len) - start);
|
||||||
|
fsm_append_lit(ctx, "\"}");
|
||||||
|
*pos = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sashidown_render_json(sashidown_ctx_t *ctx, const char *input, size_t len)
|
||||||
|
{
|
||||||
|
json_env_t env;
|
||||||
|
memset(&env, 0, sizeof(env));
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
fsm_write_begin(ctx);
|
||||||
|
fsm_append(ctx, '[');
|
||||||
|
|
||||||
|
while (pos < len) {
|
||||||
|
size_t le = block_line_end(ctx, input, len, pos);
|
||||||
|
if (pos == le || (pos + 1 == le && input[pos] == '\r')) {
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memchr(input + pos, '|', le - pos) != NULL) {
|
||||||
|
size_t newt = 0;
|
||||||
|
if (json_try_consume_table(ctx, &env, input, len, pos, le, &newt)) {
|
||||||
|
pos = newt;
|
||||||
|
block_line_seek(ctx, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line_info_t li = block_classify_line(ctx, input, len, pos, le);
|
||||||
|
|
||||||
|
if (li.kind == LK_PARA) {
|
||||||
|
line_info_t sli;
|
||||||
|
size_t send = 0;
|
||||||
|
int sr = block_try_setext(ctx, input, len, pos, le, len, 1, &sli, &send);
|
||||||
|
if (sr > 0) {
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
json_event_heading(ctx, &env, sli.heading_level);
|
||||||
|
json_render_inline(ctx, &env, input, len, sli.content_start, sli.content_end);
|
||||||
|
json_event_simple(ctx, &env, "heading_end");
|
||||||
|
pos = send;
|
||||||
|
block_line_seek(ctx, pos);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (li.kind) {
|
||||||
|
case LK_EMPTY:
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_HR:
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
json_event_simple(ctx, &env, "hr");
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_FENCE:
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
json_render_codeblock(ctx, &env, input, len, &pos, li.fence_len, li.fence_char);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_LATEX_LINE:
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
json_comma(ctx, &env);
|
||||||
|
fsm_append_lit(ctx, "{\"t\":\"latex_block\",\"v\":\"");
|
||||||
|
json_escape(ctx, input + li.content_start,
|
||||||
|
li.content_end - li.content_start);
|
||||||
|
fsm_append_lit(ctx, "\"}");
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_LATEX_BLOCK:
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
json_render_latex_block(ctx, &env, input, len, &pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_HEADING:
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
json_event_heading(ctx, &env, li.heading_level);
|
||||||
|
json_render_inline(ctx, &env, input, len, li.content_start, le);
|
||||||
|
json_event_simple(ctx, &env, "heading_end");
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_BQ:
|
||||||
|
json_close_lists(ctx, &env);
|
||||||
|
json_close_para(ctx, &env);
|
||||||
|
if (!env.bq) {
|
||||||
|
json_event_simple(ctx, &env, "bq");
|
||||||
|
env.bq = 1;
|
||||||
|
}
|
||||||
|
json_event_simple(ctx, &env, "para");
|
||||||
|
json_render_inline(ctx, &env, input, len, li.content_start, le);
|
||||||
|
json_event_simple(ctx, &env, "para_end");
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_UL:
|
||||||
|
json_close_para(ctx, &env);
|
||||||
|
json_close_bq(ctx, &env);
|
||||||
|
if (env.ol) {
|
||||||
|
json_event_simple(ctx, &env, "ol_end");
|
||||||
|
env.ol = 0;
|
||||||
|
}
|
||||||
|
if (!env.ul) {
|
||||||
|
json_event_simple(ctx, &env, "ul");
|
||||||
|
env.ul = 1;
|
||||||
|
}
|
||||||
|
json_event_simple(ctx, &env, "li");
|
||||||
|
if (li.task == 1)
|
||||||
|
json_event_simple(ctx, &env, "task_unchecked");
|
||||||
|
else if (li.task == 2)
|
||||||
|
json_event_simple(ctx, &env, "task_checked");
|
||||||
|
json_render_inline(ctx, &env, input, len, li.content_start, le);
|
||||||
|
json_event_simple(ctx, &env, "li_end");
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_OL:
|
||||||
|
json_close_para(ctx, &env);
|
||||||
|
json_close_bq(ctx, &env);
|
||||||
|
if (env.ul) {
|
||||||
|
json_event_simple(ctx, &env, "ul_end");
|
||||||
|
env.ul = 0;
|
||||||
|
}
|
||||||
|
if (!env.ol) {
|
||||||
|
json_event_simple(ctx, &env, "ol");
|
||||||
|
env.ol = 1;
|
||||||
|
}
|
||||||
|
json_event_simple(ctx, &env, "li");
|
||||||
|
json_render_inline(ctx, &env, input, len, li.content_start, le);
|
||||||
|
json_event_simple(ctx, &env, "li_end");
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LK_PARA:
|
||||||
|
default:
|
||||||
|
json_close_bq(ctx, &env);
|
||||||
|
json_close_lists(ctx, &env);
|
||||||
|
if (!env.para) {
|
||||||
|
json_event_simple(ctx, &env, "para");
|
||||||
|
env.para = 1;
|
||||||
|
} else {
|
||||||
|
json_event_text(ctx, &env, "\n", 1);
|
||||||
|
}
|
||||||
|
json_render_inline(ctx, &env, input, len, pos, le);
|
||||||
|
pos = block_skip_line(ctx, input, len, pos);
|
||||||
|
if (pos >= len) {
|
||||||
|
json_close_para(ctx, &env);
|
||||||
|
} else {
|
||||||
|
size_t nle = block_line_end(ctx, input, len, pos);
|
||||||
|
line_info_t nli = block_classify_line(ctx, input, len, pos, nle);
|
||||||
|
if (nli.kind != LK_PARA)
|
||||||
|
json_close_para(ctx, &env);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->overflow) {
|
||||||
|
fsm_write_end(ctx);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json_close_all(ctx, &env);
|
||||||
|
fsm_append(ctx, ']');
|
||||||
|
fsm_write_end(ctx);
|
||||||
|
return ctx->overflow ? -1 : 0;
|
||||||
|
}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef SASHIDOWN_RENDER_JSON_H
|
||||||
|
#define SASHIDOWN_RENDER_JSON_H
|
||||||
|
|
||||||
|
#include "sashidown.h"
|
||||||
|
|
||||||
|
int sashidown_render_json(sashidown_ctx_t *ctx, const char *input, size_t len);
|
||||||
|
|
||||||
|
#endif
|
||||||
Vendored
+96
@@ -0,0 +1,96 @@
|
|||||||
|
#include "sashidown.h"
|
||||||
|
#include "line_index.h"
|
||||||
|
#include "fsm.h"
|
||||||
|
#include "render.h"
|
||||||
|
#include "render_ansi.h"
|
||||||
|
#include "render_json.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int sashidown_parse(const char *input, size_t len,
|
||||||
|
char *output, size_t cap, size_t *out_len)
|
||||||
|
{
|
||||||
|
if (!input || !output || cap == 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
LINE_INDEX_STACK(ix);
|
||||||
|
line_index_build(input, len, &ix);
|
||||||
|
|
||||||
|
sashidown_ctx_t ctx = {
|
||||||
|
.input = input,
|
||||||
|
.input_len = len,
|
||||||
|
.output = output,
|
||||||
|
.output_cap = cap,
|
||||||
|
.output_len = 0,
|
||||||
|
.write_ptr = NULL,
|
||||||
|
.write_end = NULL,
|
||||||
|
.line_ix = &ix,
|
||||||
|
.overflow = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sashidown_render(&ctx, input, len) != 0) {
|
||||||
|
fsm_write_end(&ctx);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.overflow || ctx.output_len + 1 > cap)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
output[ctx.output_len] = '\0';
|
||||||
|
if (out_len)
|
||||||
|
*out_len = ctx.output_len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int convert_with_render(const char *input, size_t len,
|
||||||
|
char *output, size_t cap, size_t *out_len,
|
||||||
|
int (*render_fn)(sashidown_ctx_t *, const char *, size_t))
|
||||||
|
{
|
||||||
|
if (!input || !output || cap == 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
LINE_INDEX_STACK(ix);
|
||||||
|
line_index_build(input, len, &ix);
|
||||||
|
|
||||||
|
sashidown_ctx_t ctx = {
|
||||||
|
.input = input,
|
||||||
|
.input_len = len,
|
||||||
|
.output = output,
|
||||||
|
.output_cap = cap,
|
||||||
|
.output_len = 0,
|
||||||
|
.write_ptr = NULL,
|
||||||
|
.write_end = NULL,
|
||||||
|
.line_ix = &ix,
|
||||||
|
.overflow = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (render_fn(&ctx, input, len) != 0) {
|
||||||
|
fsm_write_end(&ctx);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.overflow || ctx.output_len + 1 > cap)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
output[ctx.output_len] = '\0';
|
||||||
|
if (out_len)
|
||||||
|
*out_len = ctx.output_len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sashidown_convert(const char *input, size_t len,
|
||||||
|
char *output, size_t cap, size_t *out_len,
|
||||||
|
sashidown_format_t format)
|
||||||
|
{
|
||||||
|
switch (format) {
|
||||||
|
case SASHIDOWN_FMT_HTML:
|
||||||
|
return sashidown_parse(input, len, output, cap, out_len);
|
||||||
|
case SASHIDOWN_FMT_ANSI:
|
||||||
|
return convert_with_render(input, len, output, cap, out_len, sashidown_render_ansi);
|
||||||
|
case SASHIDOWN_FMT_ANSI_PLAIN:
|
||||||
|
return convert_with_render(input, len, output, cap, out_len, sashidown_render_ansi_plain);
|
||||||
|
case SASHIDOWN_FMT_JSON:
|
||||||
|
return convert_with_render(input, len, output, cap, out_len, sashidown_render_json);
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+364
@@ -0,0 +1,364 @@
|
|||||||
|
#include "sashidown.h"
|
||||||
|
#include "line_index.h"
|
||||||
|
#include "fsm.h"
|
||||||
|
#include "render.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define STREAM_INPUT_INIT 4096
|
||||||
|
#define STREAM_OUTPUT_INIT 8192
|
||||||
|
#define STREAM_OUTPUT_MAX (64u * 1024u * 1024u)
|
||||||
|
#define STREAM_LINE_IX_INIT 256
|
||||||
|
|
||||||
|
struct sashidown_stream {
|
||||||
|
char *input_buf;
|
||||||
|
size_t input_len;
|
||||||
|
size_t input_cap;
|
||||||
|
size_t indexed_len; /* bytes already in line_ix */
|
||||||
|
size_t render_pos;
|
||||||
|
size_t last_complete_nl; /* [0, last_complete_nl) ends on line boundary */
|
||||||
|
sashidown_render_state_t render_state;
|
||||||
|
line_index_t line_ix;
|
||||||
|
char *output_buf;
|
||||||
|
size_t output_cap;
|
||||||
|
sashidown_ctx_t render_ctx;
|
||||||
|
sashidown_stream_cb on_html;
|
||||||
|
void *user;
|
||||||
|
int finished;
|
||||||
|
int aborted;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int stream_grow_input(sashidown_stream_t *s, size_t need)
|
||||||
|
{
|
||||||
|
if (need <= s->input_cap)
|
||||||
|
return 0;
|
||||||
|
size_t cap = s->input_cap ? s->input_cap : STREAM_INPUT_INIT;
|
||||||
|
while (cap < need)
|
||||||
|
cap *= 2;
|
||||||
|
char *p = (char *)realloc(s->input_buf, cap);
|
||||||
|
if (!p)
|
||||||
|
return -1;
|
||||||
|
s->input_buf = p;
|
||||||
|
s->input_cap = cap;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int stream_grow_output(sashidown_stream_t *s, size_t need)
|
||||||
|
{
|
||||||
|
if (need <= s->output_cap)
|
||||||
|
return 0;
|
||||||
|
size_t cap = s->output_cap ? s->output_cap : STREAM_OUTPUT_INIT;
|
||||||
|
while (cap < need)
|
||||||
|
cap *= 2;
|
||||||
|
if (cap > STREAM_OUTPUT_MAX)
|
||||||
|
cap = STREAM_OUTPUT_MAX;
|
||||||
|
if (cap <= s->output_cap)
|
||||||
|
return -1;
|
||||||
|
char *p = (char *)realloc(s->output_buf, cap);
|
||||||
|
if (!p)
|
||||||
|
return -1;
|
||||||
|
s->output_buf = p;
|
||||||
|
s->output_cap = cap;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int stream_flush_html(sashidown_stream_t *s)
|
||||||
|
{
|
||||||
|
sashidown_ctx_t *ctx = &s->render_ctx;
|
||||||
|
if (ctx->output_len == 0)
|
||||||
|
return 0;
|
||||||
|
if (!s->on_html)
|
||||||
|
return 0;
|
||||||
|
int r = s->on_html(ctx->output, ctx->output_len, s->user);
|
||||||
|
if (r != 0) {
|
||||||
|
s->aborted = 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
ctx->output_len = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sync line index through input_len; update last_complete_nl. */
|
||||||
|
static void stream_catchup_index(sashidown_stream_t *s)
|
||||||
|
{
|
||||||
|
if (s->indexed_len >= s->input_len)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (s->indexed_len == 0) {
|
||||||
|
line_index_build(s->input_buf, s->input_len, &s->line_ix);
|
||||||
|
size_t last = 0;
|
||||||
|
if (s->line_ix.valid && s->line_ix.n > 1) {
|
||||||
|
last = (size_t)s->line_ix.off[s->line_ix.n - 1];
|
||||||
|
} else {
|
||||||
|
const char *p = s->input_buf;
|
||||||
|
const char *end = s->input_buf + s->input_len;
|
||||||
|
const char *hit;
|
||||||
|
while ((hit = memchr(p, '\n', (size_t)(end - p))) != NULL) {
|
||||||
|
last = (size_t)(hit - s->input_buf) + 1;
|
||||||
|
p = hit + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (last)
|
||||||
|
s->last_complete_nl = last;
|
||||||
|
} else {
|
||||||
|
size_t nl = line_index_extend(&s->line_ix, s->input_buf,
|
||||||
|
s->indexed_len, s->input_len);
|
||||||
|
if (nl)
|
||||||
|
s->last_complete_nl = nl;
|
||||||
|
}
|
||||||
|
s->indexed_len = s->input_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 1 = overflow (state restored), 0 = ok, -1 = error */
|
||||||
|
static int stream_render_attempt(sashidown_stream_t *s, size_t stop, int is_final)
|
||||||
|
{
|
||||||
|
sashidown_ctx_t *ctx = &s->render_ctx;
|
||||||
|
size_t saved_pos = s->render_pos;
|
||||||
|
sashidown_render_state_t saved_state = s->render_state;
|
||||||
|
|
||||||
|
ctx->output_len = 0;
|
||||||
|
ctx->overflow = 0;
|
||||||
|
fsm_write_begin(ctx);
|
||||||
|
int r = sashidown_render_incremental(ctx, &s->render_state,
|
||||||
|
s->input_buf, s->input_len,
|
||||||
|
&s->render_pos, stop, is_final);
|
||||||
|
fsm_write_end(ctx);
|
||||||
|
|
||||||
|
if (ctx->overflow) {
|
||||||
|
s->render_pos = saved_pos;
|
||||||
|
s->render_state = saved_state;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (r != 0)
|
||||||
|
return -1;
|
||||||
|
return stream_flush_html(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t stream_soft_stop(sashidown_stream_t *s, size_t from, size_t target)
|
||||||
|
{
|
||||||
|
if (from >= target)
|
||||||
|
return target;
|
||||||
|
|
||||||
|
/* Keep each attempt roughly within half the output buffer. */
|
||||||
|
size_t soft = from + s->output_cap / 2;
|
||||||
|
if (soft >= target)
|
||||||
|
return target;
|
||||||
|
|
||||||
|
const char *base = s->input_buf + from;
|
||||||
|
size_t lim = soft - from;
|
||||||
|
const char *p = base;
|
||||||
|
const char *end = base + lim;
|
||||||
|
const char *last = NULL;
|
||||||
|
const char *hit;
|
||||||
|
while ((hit = memchr(p, '\n', (size_t)(end - p))) != NULL) {
|
||||||
|
last = hit;
|
||||||
|
p = hit + 1;
|
||||||
|
}
|
||||||
|
if (last)
|
||||||
|
return (size_t)(last - s->input_buf) + 1;
|
||||||
|
|
||||||
|
/* No newline in soft window — take the next one so we still make progress. */
|
||||||
|
hit = memchr(s->input_buf + from, '\n', target - from);
|
||||||
|
if (hit)
|
||||||
|
return (size_t)(hit - s->input_buf) + 1;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t stream_next_line_stop(sashidown_stream_t *s, size_t from, size_t limit)
|
||||||
|
{
|
||||||
|
if (from >= limit)
|
||||||
|
return limit;
|
||||||
|
const char *hit = memchr(s->input_buf + from, '\n', limit - from);
|
||||||
|
if (hit)
|
||||||
|
return (size_t)(hit - s->input_buf) + 1;
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Render up to pass_stop (is_final as given). Grow/shrink on overflow.
|
||||||
|
* Returns 0 on success, -1 on error. */
|
||||||
|
static int stream_render_window(sashidown_stream_t *s, size_t pass_stop, int is_final)
|
||||||
|
{
|
||||||
|
sashidown_ctx_t *ctx = &s->render_ctx;
|
||||||
|
for (;;) {
|
||||||
|
int ar = stream_render_attempt(s, pass_stop, is_final);
|
||||||
|
if (ar == 0)
|
||||||
|
return 0;
|
||||||
|
if (ar < 0)
|
||||||
|
return -1;
|
||||||
|
if (stream_grow_output(s, s->output_cap * 2) == 0) {
|
||||||
|
ctx->output = s->output_buf;
|
||||||
|
ctx->output_cap = s->output_cap;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t one = stream_next_line_stop(s, s->render_pos, pass_stop);
|
||||||
|
if (one <= s->render_pos || one >= pass_stop)
|
||||||
|
return -1;
|
||||||
|
pass_stop = one;
|
||||||
|
is_final = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int stream_render_pass(sashidown_stream_t *s, size_t stop, int is_final)
|
||||||
|
{
|
||||||
|
if (s->aborted)
|
||||||
|
return -1;
|
||||||
|
if (!is_final && stop <= s->render_pos)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
sashidown_ctx_t *ctx = &s->render_ctx;
|
||||||
|
ctx->input = s->input_buf;
|
||||||
|
ctx->input_len = s->input_len;
|
||||||
|
|
||||||
|
size_t target = is_final ? s->input_len : stop;
|
||||||
|
|
||||||
|
/* Common case: one or a few lines already fit in the output buffer. */
|
||||||
|
if (target - s->render_pos <= s->output_cap / 4) {
|
||||||
|
int pass_final = is_final && (target >= s->input_len);
|
||||||
|
size_t before = s->render_pos;
|
||||||
|
if (stream_render_window(s, target, pass_final) != 0)
|
||||||
|
return -1;
|
||||||
|
if (s->render_pos > before || pass_final)
|
||||||
|
return (is_final && !pass_final)
|
||||||
|
? stream_render_window(s, s->input_len, 1)
|
||||||
|
: 0;
|
||||||
|
if (!is_final)
|
||||||
|
return 0; /* deferred (incomplete table, etc.) */
|
||||||
|
/* fall through to spanned finalization */
|
||||||
|
}
|
||||||
|
|
||||||
|
while (s->render_pos < target) {
|
||||||
|
size_t before = s->render_pos;
|
||||||
|
size_t pass_stop = stream_soft_stop(s, s->render_pos, target);
|
||||||
|
int pass_final = is_final && (pass_stop >= s->input_len);
|
||||||
|
|
||||||
|
if (stream_render_window(s, pass_stop, pass_final) != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (s->render_pos > before) {
|
||||||
|
if (pass_final)
|
||||||
|
return 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No progress: soft window may have deferred a table. Retry full target. */
|
||||||
|
if (pass_stop < target) {
|
||||||
|
pass_final = is_final && (target >= s->input_len);
|
||||||
|
if (stream_render_window(s, target, pass_final) != 0)
|
||||||
|
return -1;
|
||||||
|
if (s->render_pos > before) {
|
||||||
|
if (pass_final)
|
||||||
|
return 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Still stuck — waiting for more input (incomplete table, etc.). */
|
||||||
|
if (!is_final)
|
||||||
|
return 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_final)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* EOF reached without a finalizing pass — close open blocks. */
|
||||||
|
return stream_render_window(s, s->input_len, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
sashidown_stream_t *sashidown_stream_create(const sashidown_stream_opts_t *opts)
|
||||||
|
{
|
||||||
|
if (!opts || !opts->on_html)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
sashidown_stream_t *s = (sashidown_stream_t *)calloc(1, sizeof(*s));
|
||||||
|
if (!s)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
size_t out_cap = opts->output_buf_size ? opts->output_buf_size : STREAM_OUTPUT_INIT;
|
||||||
|
if (out_cap > STREAM_OUTPUT_MAX)
|
||||||
|
out_cap = STREAM_OUTPUT_MAX;
|
||||||
|
|
||||||
|
s->output_cap = out_cap;
|
||||||
|
s->output_buf = (char *)malloc(s->output_cap);
|
||||||
|
if (!s->output_buf) {
|
||||||
|
free(s);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line_index_init_dynamic(&s->line_ix, STREAM_LINE_IX_INIT) != 0) {
|
||||||
|
free(s->output_buf);
|
||||||
|
free(s);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
s->on_html = opts->on_html;
|
||||||
|
s->user = opts->user;
|
||||||
|
sashidown_render_state_init(&s->render_state);
|
||||||
|
|
||||||
|
s->render_ctx.output = s->output_buf;
|
||||||
|
s->render_ctx.output_cap = s->output_cap;
|
||||||
|
s->render_ctx.line_ix = &s->line_ix;
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sashidown_stream_feed(sashidown_stream_t *s, const char *chunk, size_t len)
|
||||||
|
{
|
||||||
|
if (!s || s->aborted)
|
||||||
|
return -1;
|
||||||
|
if (s->finished)
|
||||||
|
return 0;
|
||||||
|
if (!chunk && len > 0)
|
||||||
|
return -1;
|
||||||
|
if (len == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int has_nl = (memchr(chunk, '\n', len) != NULL);
|
||||||
|
|
||||||
|
if (stream_grow_input(s, s->input_len + len) != 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
memcpy(s->input_buf + s->input_len, chunk, len);
|
||||||
|
s->input_len += len;
|
||||||
|
|
||||||
|
/* Tiny-chunk fast path: no newline → defer index + render. */
|
||||||
|
if (!has_nl)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
stream_catchup_index(s);
|
||||||
|
|
||||||
|
if (s->last_complete_nl <= s->render_pos)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return stream_render_pass(s, s->last_complete_nl, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sashidown_stream_finish(sashidown_stream_t *s)
|
||||||
|
{
|
||||||
|
if (!s || s->aborted)
|
||||||
|
return -1;
|
||||||
|
if (s->finished)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
stream_catchup_index(s);
|
||||||
|
int r = stream_render_pass(s, s->input_len, 1);
|
||||||
|
s->finished = 1;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sashidown_stream_destroy(sashidown_stream_t *s)
|
||||||
|
{
|
||||||
|
if (!s)
|
||||||
|
return;
|
||||||
|
free(s->input_buf);
|
||||||
|
free(s->output_buf);
|
||||||
|
line_index_free(&s->line_ix);
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t sashidown_stream_pending(const sashidown_stream_t *s)
|
||||||
|
{
|
||||||
|
if (!s || s->input_len <= s->render_pos)
|
||||||
|
return 0;
|
||||||
|
return s->input_len - s->render_pos;
|
||||||
|
}
|
||||||
Vendored
+137
@@ -0,0 +1,137 @@
|
|||||||
|
#include "table_stats.h"
|
||||||
|
#include "line_index.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int table_is_separator_row(const char *line, size_t len)
|
||||||
|
{
|
||||||
|
int pipes = 0;
|
||||||
|
int dashes = 0;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
char c = line[i];
|
||||||
|
if (c == '|') pipes++;
|
||||||
|
else if (c == '-' || c == ':' || c == ' ') dashes++;
|
||||||
|
else if (c != '\r') return 0;
|
||||||
|
}
|
||||||
|
return pipes >= 1 && dashes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GFM: leading/trailing '|' are delimiters, not empty cells. */
|
||||||
|
int table_count_cols(const char *line, size_t len, int *has_pipe)
|
||||||
|
{
|
||||||
|
if (has_pipe)
|
||||||
|
*has_pipe = 0;
|
||||||
|
|
||||||
|
while (len > 0 && line[len - 1] == '\r')
|
||||||
|
len--;
|
||||||
|
|
||||||
|
size_t start = 0;
|
||||||
|
size_t end = len;
|
||||||
|
while (start < end && (line[start] == ' ' || line[start] == '\t'))
|
||||||
|
start++;
|
||||||
|
if (start < end && line[start] == '|') {
|
||||||
|
start++;
|
||||||
|
if (has_pipe)
|
||||||
|
*has_pipe = 1;
|
||||||
|
}
|
||||||
|
while (end > start && (line[end - 1] == ' ' || line[end - 1] == '\t'))
|
||||||
|
end--;
|
||||||
|
if (end > start && line[end - 1] == '|') {
|
||||||
|
end--;
|
||||||
|
if (has_pipe)
|
||||||
|
*has_pipe = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start >= end)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
int cols = 1;
|
||||||
|
for (size_t i = start; i < end; i++) {
|
||||||
|
if (line[i] == '|') {
|
||||||
|
cols++;
|
||||||
|
if (has_pipe)
|
||||||
|
*has_pipe = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cols > SASHIDOWN_MAX_COLS ? SASHIDOWN_MAX_COLS : cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t te_line_end(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
if (ctx && ctx->line_ix)
|
||||||
|
return line_end_ix((line_index_t *)ctx->line_ix, input, len, pos);
|
||||||
|
const char *hit = memchr(input + pos, '\n', len - pos);
|
||||||
|
return hit ? (size_t)(hit - input) : len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t te_skip_line(sashidown_ctx_t *ctx, const char *input, size_t len, size_t pos)
|
||||||
|
{
|
||||||
|
if (ctx && ctx->line_ix)
|
||||||
|
return skip_line_ix((line_index_t *)ctx->line_ix, input, len, pos);
|
||||||
|
pos = te_line_end(NULL, input, len, pos);
|
||||||
|
if (pos < len)
|
||||||
|
pos++;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int line_has_pipe_raw(const char *line, size_t len)
|
||||||
|
{
|
||||||
|
return memchr(line, '|', len) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int try_parse_table(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t line_start, table_entry_t *e)
|
||||||
|
{
|
||||||
|
size_t le = te_line_end(ctx, input, len, line_start);
|
||||||
|
int has_pipe = 0;
|
||||||
|
int cols = table_count_cols(input + line_start, le - line_start, &has_pipe);
|
||||||
|
if (!has_pipe)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
size_t sep = te_skip_line(ctx, input, len, line_start);
|
||||||
|
if (sep >= len) {
|
||||||
|
if (ctx && ctx->line_ix)
|
||||||
|
line_index_seek((line_index_t *)ctx->line_ix, line_start);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
size_t sep_le = te_line_end(ctx, input, len, sep);
|
||||||
|
if (!table_is_separator_row(input + sep, sep_le - sep)) {
|
||||||
|
if (ctx && ctx->line_ix)
|
||||||
|
line_index_seek((line_index_t *)ctx->line_ix, line_start);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pos = te_skip_line(ctx, input, len, sep);
|
||||||
|
|
||||||
|
e->start = line_start;
|
||||||
|
e->body_start = pos;
|
||||||
|
e->cols = cols; /* GFM: header determines column count */
|
||||||
|
e->body_rows = 0;
|
||||||
|
e->valid = 1;
|
||||||
|
|
||||||
|
while (pos < len) {
|
||||||
|
le = te_line_end(ctx, input, len, pos);
|
||||||
|
size_t line_len = le - pos;
|
||||||
|
if (!line_has_pipe_raw(input + pos, line_len))
|
||||||
|
break;
|
||||||
|
if (table_is_separator_row(input + pos, line_len))
|
||||||
|
break;
|
||||||
|
|
||||||
|
e->body_rows++;
|
||||||
|
pos = te_skip_line(ctx, input, len, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
e->end = pos;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int table_try_parse_at(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, table_entry_t *e)
|
||||||
|
{
|
||||||
|
e->start = 0;
|
||||||
|
e->end = 0;
|
||||||
|
e->body_start = 0;
|
||||||
|
e->cols = 0;
|
||||||
|
e->body_rows = 0;
|
||||||
|
e->valid = 0;
|
||||||
|
return try_parse_table(ctx, input, len, pos, e);
|
||||||
|
}
|
||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef MD_TABLE_STATS_H
|
||||||
|
#define MD_TABLE_STATS_H
|
||||||
|
|
||||||
|
#include "sashidown.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define SASHIDOWN_MAX_COLS 32
|
||||||
|
|
||||||
|
/*
|
||||||
|
* [CST Pending Window Metadata] — lightweight table descriptor for the
|
||||||
|
* delayed-commitment pending window. Records header/separator/body bounds
|
||||||
|
* without caching cells; render rescans once after the separator enters
|
||||||
|
* the confirmation frontier. No per-cell heap; O(1) registers only.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
size_t start; /* header line start */
|
||||||
|
size_t end; /* first byte after the table */
|
||||||
|
size_t body_start; /* first body row (after separator) */
|
||||||
|
int cols; /* column count from header (GFM) */
|
||||||
|
int body_rows;
|
||||||
|
int valid;
|
||||||
|
} table_entry_t;
|
||||||
|
|
||||||
|
int table_is_separator_row(const char *line, size_t len);
|
||||||
|
int table_count_cols(const char *line, size_t len, int *has_pipe);
|
||||||
|
|
||||||
|
/* ctx may be NULL (falls back to memchr); when non-NULL reuses line_index. */
|
||||||
|
int table_try_parse_at(sashidown_ctx_t *ctx, const char *input, size_t len,
|
||||||
|
size_t pos, table_entry_t *e);
|
||||||
|
|
||||||
|
#endif /* MD_TABLE_STATS_H */
|
||||||
Reference in New Issue
Block a user