///|
// mnemo_api.mbt — expose 12 mnemo tools via globalThis.__mnemo_api
// Each tool accepts a JSON string and returns a JSON string.
// ---------------------------------------------------------------------------
// JS FFI: JSON helpers
// ---------------------------------------------------------------------------
extern "js" fn _api_json_parse(s : String) -> Json =
#| (s) => JSON.parse(s)
extern "js" fn _api_json_stringify(v : Json) -> String =
#| (v) => JSON.stringify(v)
// ---------------------------------------------------------------------------
// JS FFI: env / path helpers
// ---------------------------------------------------------------------------
extern "js" fn _api_get_mnemo_home() -> String =
#| () => {
#| const os = require("node:os");
#| const path = require("node:path");
#| return process.env.MNEMO_HOME || path.join(os.homedir(), ".mnemo");
#| }
extern "js" fn _api_get_db_path() -> String =
#| () => {
#| const os = require("node:os");
#| const path = require("node:path");
#| const home = process.env.MNEMO_HOME || path.join(os.homedir(), ".mnemo");
#| return path.join(home, "sessions.db");
#| }
// ---------------------------------------------------------------------------
// JS FFI: string field extraction helpers
// ---------------------------------------------------------------------------
extern "js" fn _api_field_str(obj : Json, key : String) -> String =
#| (obj, key) => (obj && typeof obj[key] === "string") ? obj[key] : ""
extern "js" fn _api_field_str_or_null(obj : Json, key : String) -> String =
#| (obj, key) => (obj && obj[key] != null && obj[key] !== undefined) ? String(obj[key]) : "\u0000"
extern "js" fn _api_field_int(obj : Json, key : String, def : Int) -> Int =
#| (obj, key, def) => (obj && typeof obj[key] === "number") ? (obj[key] | 0) : def
extern "js" fn _api_field_int64(obj : Json, key : String, def : Int64) -> Int64 =
#| (obj, key, def) => {
#| if (!obj || obj[key] == null) return def;
#| const v = obj[key];
#| if (typeof v === "bigint") return v;
#| if (typeof v === "number") return BigInt(Math.trunc(v));
#| return def;
#| }
extern "js" fn _api_field_bool(obj : Json, key : String, def : Bool) -> Bool =
#| (obj, key, def) => (obj && typeof obj[key] === "boolean") ? obj[key] : def
extern "js" fn _api_field_array_str(obj : Json, key : String) -> Array[String] =
#| (obj, key) => (obj && Array.isArray(obj[key])) ? obj[key].map(String) : []
// ---------------------------------------------------------------------------
// JS FFI: JSON result constructors (avoid MoonBit Json literals)
// ---------------------------------------------------------------------------
extern "js" fn _api_ok_json(fields_json : String) -> String =
#| (fieldsJson) => JSON.stringify(Object.assign({ ok: true }, JSON.parse(fieldsJson)))
extern "js" fn _api_err_json(error : String, error_code : String) -> String =
#| (error, error_code) => JSON.stringify({ ok: false, error, error_code })
// ---------------------------------------------------------------------------
// Helper: parse null-sentinel for optional strings
// ---------------------------------------------------------------------------
fn is_null_sentinel(s : String) -> Bool {
s == "\u0000"
}
fn sentinel_to_option(s : String) -> String? {
if is_null_sentinel(s) { None } else { Some(s) }
}
// ---------------------------------------------------------------------------
// tool_memory_add
// ---------------------------------------------------------------------------
pub async fn tool_memory_add(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let target_str = _api_field_str(obj, "target")
let content = _api_field_str(obj, "content")
let target : Target = if target_str == "user" { User } else { Memory }
let mnemo_home = _api_get_mnemo_home()
let store = MemoryStore::load(mnemo_home)
match store.add(target, content) {
AddOk(used, max) =>
_api_ok_json(
"{\"usage\":{\"used_chars\":" + used.to_string() + ",\"max_chars\":" + max.to_string() + "}}",
)
AddErr(msg, code) => _api_err_json(msg, code)
}
}
// ---------------------------------------------------------------------------
// tool_memory_replace
// ---------------------------------------------------------------------------
pub async fn tool_memory_replace(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let target_str = _api_field_str(obj, "target")
let old_substring = _api_field_str(obj, "old_substring")
let new_content = _api_field_str(obj, "new_content")
let target : Target = if target_str == "user" { User } else { Memory }
let mnemo_home = _api_get_mnemo_home()
let store = MemoryStore::load(mnemo_home)
match store.replace(target, old_substring, new_content) {
OpOk(used, max) =>
_api_ok_json(
"{\"usage\":{\"used_chars\":" + used.to_string() + ",\"max_chars\":" + max.to_string() + "}}",
)
OpErr(msg, code) => _api_err_json(msg, code)
}
}
// ---------------------------------------------------------------------------
// tool_memory_remove
// ---------------------------------------------------------------------------
pub async fn tool_memory_remove(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let target_str = _api_field_str(obj, "target")
let old_substring = _api_field_str(obj, "old_substring")
let target : Target = if target_str == "user" { User } else { Memory }
let mnemo_home = _api_get_mnemo_home()
let store = MemoryStore::load(mnemo_home)
match store.remove(target, old_substring) {
OpOk(used, max) =>
_api_ok_json(
"{\"usage\":{\"used_chars\":" + used.to_string() + ",\"max_chars\":" + max.to_string() + "}}",
)
OpErr(msg, code) => _api_err_json(msg, code)
}
}
// ---------------------------------------------------------------------------
// tool_memory_read
// ---------------------------------------------------------------------------
pub fn tool_memory_read(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let target_str = _api_field_str(obj, "target")
let target : Target = if target_str == "user" { User } else { Memory }
let mnemo_home = _api_get_mnemo_home()
let store = MemoryStore::load(mnemo_home)
let content = store.snapshot(target)
let max = match target {
Memory => store.max_memory
User => store.max_user
}
let used = content.length()
_api_ok_json(
"{\"content\":" +
_api_str_to_json_str(content) +
",\"usage\":{\"used_chars\":" +
used.to_string() +
",\"max_chars\":" +
max.to_string() +
"}}",
)
}
// ---------------------------------------------------------------------------
// JS FFI: JSON.stringify a string value
// ---------------------------------------------------------------------------
extern "js" fn _api_str_to_json_str(s : String) -> String =
#| (s) => JSON.stringify(s)
// ---------------------------------------------------------------------------
// tool_skills_list
// ---------------------------------------------------------------------------
extern "js" fn _api_skills_list_js(
mnemo_home : String,
available_tools_json : String,
platform_str : String
) -> String =
#| (mnemoHome, toolsJson, platformStr) => {
#| // This function returns JSON-encoded result
#| // It's called by tool_skills_list via direct extern
#| return null; // placeholder — actual impl in tool_skills_list below
#| }
pub fn tool_skills_list(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let mnemo_home = _api_get_mnemo_home()
let available_tools = _api_field_array_str(obj, "available_tools")
let available_toolsets = _api_field_array_str(obj, "available_toolsets")
let disabled = _api_field_array_str(obj, "disabled")
let platform_str = _api_field_str(obj, "platform")
let category_filter = _api_field_str(obj, "category")
// Load skills from mnemo_home/skills
let skills_dir = mnemo_home + "/skills"
let store = skills_store_load(skills_dir, [])
let platform_opt : String? = if platform_str.is_empty() { None } else { Some(platform_str) }
// Match proto: when both arrays are empty, treat as absent (None) → no tool/toolset filtering.
// When either is non-empty, pass both as Some([...]) to enable filtering.
let has_filter = available_tools.length() > 0 || available_toolsets.length() > 0
let tools_opt : Array[String]? = if has_filter { Some(available_tools) } else { None }
let toolsets_opt : Array[String]? = if has_filter { Some(available_toolsets) } else { None }
let filtered = skills_store_list(
store, tools_opt, platform_opt,
available_toolsets=toolsets_opt, disabled=disabled,
)
// Apply category filter
let results = if category_filter.is_empty() {
filtered
} else {
filtered.filter(fn(s) { s.category == category_filter })
}
// Build JSON output
let parts : Array[String] = []
for s in results {
parts.push(
"{\"name\":" +
_api_str_to_json_str(s.name) +
",\"description\":" +
_api_str_to_json_str(s.description) +
",\"category\":" +
_api_str_to_json_str(s.category) +
"}",
)
}
let skills_json = "[" + parts.join(",") + "]"
_api_ok_json("{\"skills\":" + skills_json + ",\"count\":" + results.length().to_string() + "}")
}
// ---------------------------------------------------------------------------
// tool_skill_view
// ---------------------------------------------------------------------------
pub fn tool_skill_view(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let name = _api_field_str(obj, "name")
let mnemo_home = _api_get_mnemo_home()
let skills_dir = mnemo_home + "/skills"
let store = skills_store_load(skills_dir, [])
match skills_store_view(store, name) {
ViewOk(skill_name, content) =>
_api_ok_json(
"{\"name\":" +
_api_str_to_json_str(skill_name) +
",\"content\":" +
_api_str_to_json_str(content) +
"}",
)
ViewErr(msg) => _api_err_json(msg, "not_found")
}
}
// ---------------------------------------------------------------------------
// Session tools — shared db open helper
// ---------------------------------------------------------------------------
fn open_api_session_db() -> SessionDb {
let db_path = _api_get_db_path()
open_session_db(db_path)
}
// ---------------------------------------------------------------------------
// tool_session_create
// ---------------------------------------------------------------------------
pub async fn tool_session_create(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let source = _api_field_str(obj, "source")
let system_prompt = _api_field_str(obj, "system_prompt")
let model_s = _api_field_str_or_null(obj, "model")
let user_id_s = _api_field_str_or_null(obj, "user_id")
let parent_s = _api_field_str_or_null(obj, "parent_session_id")
let sdb = open_api_session_db()
let sid = create_session(
sdb,
source,
system_prompt,
model=sentinel_to_option(model_s),
user_id=sentinel_to_option(user_id_s),
parent_session_id=sentinel_to_option(parent_s),
)
_api_ok_json("{\"session_id\":" + _api_str_to_json_str(sid) + "}")
}
// ---------------------------------------------------------------------------
// tool_session_append
// ---------------------------------------------------------------------------
pub async fn tool_session_append(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let session_id = _api_field_str(obj, "session_id")
let role = _api_field_str(obj, "role")
let timestamp = _api_field_int64(obj, "timestamp", 0L)
let content_s = _api_field_str_or_null(obj, "content")
let tool_name_s = _api_field_str_or_null(obj, "tool_name")
let token_count_raw = _api_field_int(obj, "token_count", -1)
let token_count_opt : Int? = if token_count_raw < 0 { None } else { Some(token_count_raw) }
let sdb = open_api_session_db()
let mid = append_message(
sdb,
session_id,
role,
timestamp,
content=sentinel_to_option(content_s),
tool_name=sentinel_to_option(tool_name_s),
token_count=token_count_opt,
)
_api_ok_json("{\"message_id\":" + mid.to_string() + "}")
}
// ---------------------------------------------------------------------------
// Session row serializers (pure MoonBit)
// ---------------------------------------------------------------------------
fn _opt_str_json(v : String?, j : (String) -> String) -> String {
match v {
None => "null"
Some(s) => j(s)
}
}
fn _session_row_to_json(s : SessionRow) -> String {
let j = _api_str_to_json_str
let user_id_j = _opt_str_json(s.user_id, j)
let model_j = _opt_str_json(s.model, j)
let parent_j = _opt_str_json(s.parent_session_id, j)
let ended_at_j = match s.ended_at {
None => "null"
Some(v) => v.to_string()
}
let end_reason_j = _opt_str_json(s.end_reason, j)
let title_j = _opt_str_json(s.title, j)
"{\"id\":" + j(s.id) +
",\"source\":" + j(s.source) +
",\"user_id\":" + user_id_j +
",\"model\":" + model_j +
",\"system_prompt\":" + j(s.system_prompt) +
",\"parent_session_id\":" + parent_j +
",\"started_at\":" + s.started_at.to_string() +
",\"ended_at\":" + ended_at_j +
",\"end_reason\":" + end_reason_j +
",\"message_count\":" + s.message_count.to_string() +
",\"input_tokens\":" + s.input_tokens.to_string() +
",\"output_tokens\":" + s.output_tokens.to_string() +
",\"cache_read_tokens\":" + s.cache_read_tokens.to_string() +
",\"cache_write_tokens\":" + s.cache_write_tokens.to_string() +
",\"title\":" + title_j +
"}"
}
// ---------------------------------------------------------------------------
// tool_session_get — pure MoonBit implementation
// ---------------------------------------------------------------------------
pub fn tool_session_get(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let session_id = _api_field_str(obj, "session_id")
let sdb = open_api_session_db()
match get_session(sdb, session_id) {
None => _api_err_json("session not found", "not_found")
Some(sess) => {
// Fetch messages: id, role, content, tool_name, timestamp, token_count
let stmt = prepare(sdb.db,
"SELECT id, role, content, tool_name, timestamp, token_count FROM messages WHERE session_id = ? ORDER BY id ASC"
)
_stmt_bind_text(stmt, 1, session_id)
let rows = _stmt_all(stmt)
let n = _rows_length(rows)
let msg_parts : Array[String] = []
let mut i = 0
while i < n {
let row = _row_at(rows, i)
let j = _api_str_to_json_str
let content_j = if _row_is_null(row, "content") {
"null"
} else {
j(_row_get_text(row, "content"))
}
let tool_j = if _row_is_null(row, "tool_name") {
"null"
} else {
j(_row_get_text(row, "tool_name"))
}
let tc_j = if _row_is_null(row, "token_count") {
"null"
} else {
_row_get_int(row, "token_count").to_string()
}
msg_parts.push(
"{\"id\":" + _row_get_int(row, "id").to_string() +
",\"role\":" + j(_row_get_text(row, "role")) +
",\"content\":" + content_j +
",\"tool_name\":" + tool_j +
",\"timestamp\":" + _row_get_int(row, "timestamp").to_string() +
",\"token_count\":" + tc_j +
"}",
)
i = i + 1
}
let msgs_json = "[" + msg_parts.join(",") + "]"
_api_ok_json(
"{\"session\":" + _session_row_to_json(sess) + ",\"messages\":" + msgs_json + "}",
)
}
}
}
// ---------------------------------------------------------------------------
// tool_session_list — pure MoonBit implementation
// ---------------------------------------------------------------------------
pub fn tool_session_list(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let limit = _api_field_int(obj, "limit", 20)
let offset = _api_field_int(obj, "offset", 0)
let source = _api_field_str(obj, "source")
let sdb = open_api_session_db()
let (sessions, total) = list_sessions(sdb, limit=limit, offset=offset, source=source)
let parts : Array[String] = []
for s in sessions {
parts.push(_session_row_to_json(s))
}
let sessions_json = "[" + parts.join(",") + "]"
_api_ok_json("{\"sessions\":" + sessions_json + ",\"total\":" + total.to_string() + "}")
}
// ---------------------------------------------------------------------------
// tool_session_search
// ---------------------------------------------------------------------------
extern "js" fn _api_search_results_to_json(results_json : String) -> String =
#| (s) => s
pub async fn tool_session_search(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let query = _api_field_str(obj, "query")
let limit = _api_field_int(obj, "limit", 3)
let role_filter_s = _api_field_str_or_null(obj, "role_filter")
let sdb = open_api_session_db()
let resp = session_search(
sdb,
query,
limit=limit,
role_filter=sentinel_to_option(role_filter_s),
)
// Build JSON manually
let result_parts : Array[String] = []
for r in resp.results {
result_parts.push(
"{\"session_id\":" +
_api_str_to_json_str(r.session_id) +
",\"when\":" +
_api_str_to_json_str(r.when) +
",\"source\":" +
_api_str_to_json_str(r.source) +
",\"model\":" +
_api_str_to_json_str(r.model) +
",\"summary\":" +
_api_str_to_json_str(r.summary) +
"}",
)
}
let results_json = "[" + result_parts.join(",") + "]"
"{\"ok\":true,\"query\":" +
_api_str_to_json_str(resp.query) +
",\"results\":" +
results_json +
",\"count\":" +
resp.count.to_string() +
",\"sessions_searched\":" +
resp.sessions_searched.to_string() +
"}"
}
// ---------------------------------------------------------------------------
// tool_session_resolve
// ---------------------------------------------------------------------------
pub fn tool_session_resolve(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let prefix = _api_field_str(obj, "prefix")
if prefix == "" {
return _api_err_json("prefix must be non-empty", "invalid_input")
}
let sdb = open_api_session_db()
match resolve_session_id(sdb, prefix) {
Ok(sid) => _api_ok_json("{\"session_id\":" + _api_str_to_json_str(sid) + "}")
Err(e) => {
let code = if e.contains("session_not_found") {
"not_found"
} else if e.contains("session_ambiguous") {
"ambiguous"
} else {
"internal_error"
}
_api_err_json(e, code)
}
}
}
// ---------------------------------------------------------------------------
// tool_prompt_build
// ---------------------------------------------------------------------------
pub fn tool_prompt_build(json_args : String) -> String {
let obj = _api_json_parse(json_args)
let cwd = _api_field_str(obj, "cwd")
let platform = _api_field_str(obj, "platform")
let model = _api_field_str(obj, "model")
let available_tools = _api_field_array_str(obj, "available_tools")
let disabled_skills = _api_field_array_str(obj, "disabled_skills")
let mnemo_home_override = _api_field_str(obj, "hermes_home")
let mnemo_home = if mnemo_home_override.is_empty() {
_api_get_mnemo_home()
} else {
mnemo_home_override
}
let opts : BuildPromptOptions = {
hermes_home: mnemo_home,
cwd,
platform,
model,
available_tools,
disabled_skills,
}
let prompt = build_system_prompt(opts)
_api_ok_json("{\"prompt\":" + _api_str_to_json_str(prompt) + "}")
}
// ---------------------------------------------------------------------------
// register_api — sets globalThis.__mnemo_api
// ---------------------------------------------------------------------------
extern "js" fn _register_api_js(
mem_add : (String) -> @ffi.Promise[String],
mem_replace : (String) -> @ffi.Promise[String],
mem_remove : (String) -> @ffi.Promise[String],
mem_read : (String) -> String,
sk_list : (String) -> String,
sk_view : (String) -> String,
sess_create : (String) -> @ffi.Promise[String],
sess_append : (String) -> @ffi.Promise[String],
sess_get : (String) -> String,
sess_list : (String) -> String,
sess_search : (String) -> @ffi.Promise[String],
sess_resolve : (String) -> String,
prompt_build : (String) -> String
) -> Unit =
#| (mem_add, mem_replace, mem_remove, mem_read,
#| sk_list, sk_view,
#| sess_create, sess_append, sess_get, sess_list, sess_search, sess_resolve,
#| prompt_build) => {
#| const wrap = (fn_async) => (s) => Promise.resolve(fn_async(s)).catch(e => JSON.stringify({ok: false, error: String(e), error_code: "internal_error"}));
#| const wrapSync = (fn_sync) => (s) => { try { return fn_sync(s); } catch(e) { return JSON.stringify({ok: false, error: String(e), error_code: "internal_error"}); } };
#| globalThis.__mnemo_api = {
#| memory_add: wrap(mem_add),
#| memory_replace: wrap(mem_replace),
#| memory_remove: wrap(mem_remove),
#| memory_read: wrapSync(mem_read),
#| skills_list: wrapSync(sk_list),
#| skill_view: wrapSync(sk_view),
#| session_create: wrap(sess_create),
#| session_append: wrap(sess_append),
#| session_get: wrapSync(sess_get),
#| session_list: wrapSync(sess_list),
#| session_search: wrap(sess_search),
#| session_resolve: wrapSync(sess_resolve),
#| prompt_build: wrapSync(prompt_build),
#| };
#| }
extern "js" fn _resolved_promise(s : String) -> @ffi.Promise[String] =
#| (s) => Promise.resolve(s)
extern "js" fn _make_async_promise(f : () -> Unit) -> @ffi.Promise[String] =
#| (f) => new Promise((resolve) => { f(); resolve("__pending__"); })
fn async_tool(f : async () -> String) -> @ffi.Promise[String] {
_make_deferred_promise(fn(resolve, reject) {
@ffi.run_async(async fn() noraise {
let result = try {
f()
} catch {
e => {
reject(@ffi.identity(e))
return
}
}
resolve(result)
})
})
}
extern "js" fn _make_deferred_promise(
executor : ((String) -> Unit, (@ffi.JsValue) -> Unit) -> Unit
) -> @ffi.Promise[String] =
#| (executor) => new Promise((resolve, reject) => executor(resolve, reject))
pub fn register_api() -> Unit {
_register_api_js(
fn(s) { async_tool(async fn() { tool_memory_add(s) }) },
fn(s) { async_tool(async fn() { tool_memory_replace(s) }) },
fn(s) { async_tool(async fn() { tool_memory_remove(s) }) },
fn(s) { tool_memory_read(s) },
fn(s) { tool_skills_list(s) },
fn(s) { tool_skill_view(s) },
fn(s) { async_tool(async fn() { tool_session_create(s) }) },
fn(s) { async_tool(async fn() { tool_session_append(s) }) },
fn(s) { tool_session_get(s) },
fn(s) { tool_session_list(s) },
fn(s) { async_tool(async fn() { tool_session_search(s) }) },
fn(s) { tool_session_resolve(s) },
fn(s) { tool_prompt_build(s) },
)
}