add forge daemon command for soul Engram lifecycle management
- daemon.el: launchd plist generation, install/start/stop/status/wire - forge.el: dispatch daemon command, add to usage - install.el: auto-register soul as launchd agent after install - schema.el: add FORGE_DIR constant for absolute path references forge daemon install --all installs all 19 souls as resident launchd agents with KeepAlive. forge daemon wire registers each soul as an Engram peer to every other soul. forge daemon status health-checks all.
This commit is contained in:
+423
@@ -0,0 +1,423 @@
|
||||
// daemon.el — Soul Engram lifecycle management.
|
||||
//
|
||||
// Manages launchd-resident DHARMA soul Engrams on macOS.
|
||||
//
|
||||
// Commands:
|
||||
// forge daemon install <slug> — write launchd plist, load soul as resident
|
||||
// forge daemon start <slug> — launchctl kickstart the soul
|
||||
// forge daemon stop <slug> — launchctl stop the soul
|
||||
// forge daemon status — health check all souls
|
||||
// forge daemon wire — register all soul Engrams as peers to each other
|
||||
//
|
||||
// Depends on: schema.el (FORGE_VERSION, engram_key, str_escape_json, log_event)
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||
// (FORGE_DIR is defined in schema.el)
|
||||
|
||||
let ENGRAM_BIN: String = "/Users/will/Development/neuron-technologies/foundation/engram/dist/engram"
|
||||
let LAUNCHD_DIR: String = "/Users/will/Library/LaunchAgents"
|
||||
let DHARMA_API_KEY: String = "ntn-dharma-2026"
|
||||
|
||||
// ── plist generation ───────────────────────────────────────────────────────────
|
||||
|
||||
// plist_label — launchd label for a soul slug.
|
||||
fn plist_label(slug: String) -> String {
|
||||
"ai.neurontechnologies.soul." + slug
|
||||
}
|
||||
|
||||
// plist_path — full path to the launchd plist file for a soul.
|
||||
fn plist_path(slug: String) -> String {
|
||||
LAUNCHD_DIR + "/ai.neurontechnologies.soul." + slug + ".plist"
|
||||
}
|
||||
|
||||
// write_soul_plist — generate and write the launchd plist for a soul.
|
||||
// Returns the path written, or "" on failure.
|
||||
fn write_soul_plist(slug: String, port: String, db_path: String, subject: String) -> String {
|
||||
let label: String = plist_label(slug)
|
||||
let plist_file: String = plist_path(slug)
|
||||
let log_file: String = FORGE_DIR + "/log/" + slug + ".log"
|
||||
let soul_api_key: String = "ntn-" + slug + "-2026"
|
||||
|
||||
let content: String = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
||||
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" +
|
||||
"<plist version=\"1.0\">\n" +
|
||||
"<dict>\n" +
|
||||
" <key>Label</key><string>" + label + "</string>\n" +
|
||||
" <key>ProgramArguments</key>\n" +
|
||||
" <array>\n" +
|
||||
" <string>" + ENGRAM_BIN + "</string>\n" +
|
||||
" </array>\n" +
|
||||
" <key>EnvironmentVariables</key>\n" +
|
||||
" <dict>\n" +
|
||||
" <key>ENGRAM_DB_PATH</key><string>" + db_path + "</string>\n" +
|
||||
" <key>ENGRAM_BIND</key><string>0.0.0.0:" + port + "</string>\n" +
|
||||
" <key>ENGRAM_API_KEY</key><string>" + soul_api_key + "</string>\n" +
|
||||
" <key>ENGRAM_PEER_NAME</key><string>" + str_escape_json(subject) + "</string>\n" +
|
||||
" </dict>\n" +
|
||||
" <key>RunAtLoad</key><true/>\n" +
|
||||
" <key>KeepAlive</key><true/>\n" +
|
||||
" <key>StandardOutPath</key><string>" + log_file + "</string>\n" +
|
||||
" <key>StandardErrorPath</key><string>" + log_file + "</string>\n" +
|
||||
"</dict>\n" +
|
||||
"</plist>\n"
|
||||
|
||||
let ok: Int = fs_write(plist_file, content)
|
||||
if ok == 0 {
|
||||
println("[daemon] error: could not write plist to " + plist_file)
|
||||
return ""
|
||||
}
|
||||
println("[daemon] wrote plist: " + plist_file)
|
||||
return plist_file
|
||||
}
|
||||
|
||||
// ── launchctl helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// launchctl_load — load (register + start) a plist via launchctl.
|
||||
fn launchctl_load(plist_file: String) -> String {
|
||||
let out: String = exec("launchctl load " + plist_file + " 2>&1")
|
||||
return str_trim(out)
|
||||
}
|
||||
|
||||
// launchctl_unload — unload a plist via launchctl.
|
||||
fn launchctl_unload(plist_file: String) -> String {
|
||||
let out: String = exec("launchctl unload " + plist_file + " 2>&1")
|
||||
return str_trim(out)
|
||||
}
|
||||
|
||||
// launchctl_kickstart — start a loaded soul via its label.
|
||||
fn launchctl_kickstart(slug: String) -> String {
|
||||
let label: String = plist_label(slug)
|
||||
let out: String = exec("launchctl kickstart gui/$(id -u)/" + label + " 2>&1")
|
||||
return str_trim(out)
|
||||
}
|
||||
|
||||
// launchctl_stop — stop a running soul (does not unload, KeepAlive will restart).
|
||||
fn launchctl_stop(slug: String) -> String {
|
||||
let label: String = plist_label(slug)
|
||||
let out: String = exec("launchctl stop " + label + " 2>&1")
|
||||
return str_trim(out)
|
||||
}
|
||||
|
||||
// soul_is_up — returns true if the soul's Engram responds to /stats.
|
||||
fn soul_is_up(engram_url: String) -> Bool {
|
||||
let resp: String = http_get(engram_url + "/stats")
|
||||
if str_eq(resp, "") { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Registry helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
// count_souls — returns number of imprints in registry.json.
|
||||
fn count_souls(reg_json: String) -> Int {
|
||||
let n: Int = 0
|
||||
let i: Int = 0
|
||||
while i < 50 {
|
||||
let entry: String = json_get(reg_json, "imprints." + int_to_str(i))
|
||||
if str_eq(entry, "") {
|
||||
return n
|
||||
}
|
||||
let n = n + 1
|
||||
let i = i + 1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// ── daemon install ─────────────────────────────────────────────────────────────
|
||||
|
||||
// daemon_install_one — install a single soul as a launchd agent.
|
||||
// Creates the imprints dir, ensures the Engram runs (starts temp if needed),
|
||||
// writes the plist and loads it.
|
||||
fn daemon_install_one(slug: String, port: String, engram_url: String, subject: String) -> String {
|
||||
println("[daemon] install: " + slug + " → port " + port)
|
||||
|
||||
// Ensure dirs exist
|
||||
let db_path: String = FORGE_DIR + "/imprints/" + slug
|
||||
let log_dir: String = FORGE_DIR + "/log"
|
||||
if !fs_exists(db_path) { fs_mkdir(db_path) }
|
||||
if !fs_exists(log_dir) { fs_mkdir(log_dir) }
|
||||
if !fs_exists(LAUNCHD_DIR) { fs_mkdir(LAUNCHD_DIR) }
|
||||
|
||||
// Write plist
|
||||
let pfile: String = write_soul_plist(slug, port, db_path, subject)
|
||||
if str_eq(pfile, "") {
|
||||
println("[daemon] error: plist write failed for " + slug)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Unload first in case it was already loaded (idempotent)
|
||||
launchctl_unload(pfile)
|
||||
|
||||
// Load
|
||||
let load_out: String = launchctl_load(pfile)
|
||||
if !str_eq(load_out, "") {
|
||||
println("[daemon] launchctl load: " + load_out)
|
||||
}
|
||||
|
||||
println("[daemon] loaded: " + plist_label(slug))
|
||||
log_event("daemon_install", subject, "port=" + port + " plist=" + pfile)
|
||||
return pfile
|
||||
}
|
||||
|
||||
fn daemon_install_main() -> String {
|
||||
let argv: [String] = args()
|
||||
// args() layout: [0]="daemon" [1]="install" [2]=<slug>
|
||||
let slug: String = ""
|
||||
if len(argv) > 2 {
|
||||
let slug = get(argv, 2)
|
||||
}
|
||||
if str_eq(slug, "") {
|
||||
println("[daemon] usage: forge daemon install <slug>")
|
||||
println("[daemon] or: forge daemon install --all")
|
||||
return ""
|
||||
}
|
||||
|
||||
let reg_json: String = fs_read(FORGE_DIR + "/registry.json")
|
||||
if str_eq(reg_json, "") {
|
||||
println("[daemon] error: could not read registry.json")
|
||||
return ""
|
||||
}
|
||||
|
||||
// --all: install every soul in the registry
|
||||
if str_eq(slug, "--all") {
|
||||
let i: Int = 0
|
||||
while i < 50 {
|
||||
let entry: String = json_get(reg_json, "imprints." + int_to_str(i))
|
||||
if str_eq(entry, "") {
|
||||
let i = i + 50 // break
|
||||
} else {
|
||||
let s: String = json_get_string(entry, "slug")
|
||||
let p: String = int_to_str(json_get_int(entry, "engram_port"))
|
||||
let u: String = json_get_string(entry, "engram_url")
|
||||
let sub: String = json_get_string(entry, "subject")
|
||||
daemon_install_one(s, p, u, sub)
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
return "done"
|
||||
}
|
||||
|
||||
// Single soul
|
||||
let found: Int = 0
|
||||
let i: Int = 0
|
||||
while i < 50 {
|
||||
let entry: String = json_get(reg_json, "imprints." + int_to_str(i))
|
||||
if str_eq(entry, "") {
|
||||
let i = i + 50 // break
|
||||
} else {
|
||||
let s: String = json_get_string(entry, "slug")
|
||||
if str_eq(s, slug) {
|
||||
let p: String = int_to_str(json_get_int(entry, "engram_port"))
|
||||
let u: String = json_get_string(entry, "engram_url")
|
||||
let sub: String = json_get_string(entry, "subject")
|
||||
daemon_install_one(s, p, u, sub)
|
||||
let found = 1
|
||||
let i = i + 50 // break
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if found == 0 {
|
||||
println("[daemon] error: slug '" + slug + "' not found in registry.json")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── daemon start / stop ────────────────────────────────────────────────────────
|
||||
|
||||
fn daemon_start_main() -> String {
|
||||
let argv: [String] = args()
|
||||
// args() layout: [0]="daemon" [1]="start" [2]=<slug>
|
||||
let slug: String = ""
|
||||
if len(argv) > 2 {
|
||||
let slug = get(argv, 2)
|
||||
}
|
||||
if str_eq(slug, "") {
|
||||
println("[daemon] usage: forge daemon start <slug>")
|
||||
return ""
|
||||
}
|
||||
let out: String = launchctl_kickstart(slug)
|
||||
println("[daemon] start " + slug + ": " + out)
|
||||
return out
|
||||
}
|
||||
|
||||
fn daemon_stop_main() -> String {
|
||||
let argv: [String] = args()
|
||||
// args() layout: [0]="daemon" [1]="stop" [2]=<slug>
|
||||
let slug: String = ""
|
||||
if len(argv) > 2 {
|
||||
let slug = get(argv, 2)
|
||||
}
|
||||
if str_eq(slug, "") {
|
||||
println("[daemon] usage: forge daemon stop <slug>")
|
||||
return ""
|
||||
}
|
||||
let out: String = launchctl_stop(slug)
|
||||
println("[daemon] stop " + slug + ": " + out)
|
||||
return out
|
||||
}
|
||||
|
||||
// ── daemon status ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn daemon_status_main() -> String {
|
||||
let reg_json: String = fs_read(FORGE_DIR + "/registry.json")
|
||||
if str_eq(reg_json, "") {
|
||||
println("[daemon] error: could not read registry.json")
|
||||
return ""
|
||||
}
|
||||
|
||||
println("[daemon] DHARMA soul status:")
|
||||
println("")
|
||||
|
||||
let up: Int = 0
|
||||
let down: Int = 0
|
||||
let i: Int = 0
|
||||
while i < 50 {
|
||||
let entry: String = json_get(reg_json, "imprints." + int_to_str(i))
|
||||
if str_eq(entry, "") {
|
||||
let i = i + 50 // break
|
||||
} else {
|
||||
let slug: String = json_get_string(entry, "slug")
|
||||
let url: String = json_get_string(entry, "engram_url")
|
||||
let sub: String = json_get_string(entry, "subject")
|
||||
let port: String = int_to_str(json_get_int(entry, "engram_port"))
|
||||
if soul_is_up(url) {
|
||||
println(" [UP ] " + sub + " — " + url)
|
||||
let up = up + 1
|
||||
} else {
|
||||
println(" [DOWN] " + sub + " — " + url)
|
||||
let down = down + 1
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
println("")
|
||||
println("[daemon] up=" + int_to_str(up) + " down=" + int_to_str(down))
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── daemon wire ───────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Registers every soul as a peer in every other soul's Engram.
|
||||
// POST /sync/peers with a Peer JSON object.
|
||||
//
|
||||
// Peer format (from engram-sync/src/types.rs):
|
||||
// { "id": <uuid>, "name": <string>, "address": <url>, "api_key": <string>,
|
||||
// "sync_tiers": ["Semantic"], "last_sync_at": 0, "trusted": true }
|
||||
|
||||
fn make_peer_json(peer_id: String, name: String, address: String, api_key: String) -> String {
|
||||
"{\"id\":\"" + peer_id + "\",\"name\":\"" + str_escape_json(name) +
|
||||
"\",\"address\":\"" + address +
|
||||
"\",\"api_key\":\"" + api_key +
|
||||
"\",\"sync_tiers\":[\"Semantic\"],\"last_sync_at\":0,\"trusted\":true}"
|
||||
}
|
||||
|
||||
fn daemon_wire_main() -> String {
|
||||
let reg_json: String = fs_read(FORGE_DIR + "/registry.json")
|
||||
if str_eq(reg_json, "") {
|
||||
println("[daemon] error: could not read registry.json")
|
||||
return ""
|
||||
}
|
||||
|
||||
println("[daemon] wiring peers — registering all souls as peers to each other...")
|
||||
println("")
|
||||
|
||||
// Collect all soul entries (up to 50)
|
||||
// For each soul A, register all other souls B as peers of A
|
||||
let registered: Int = 0
|
||||
let failed: Int = 0
|
||||
|
||||
let i: Int = 0
|
||||
while i < 50 {
|
||||
let entry_a: String = json_get(reg_json, "imprints." + int_to_str(i))
|
||||
if str_eq(entry_a, "") {
|
||||
let i = i + 50 // break
|
||||
} else {
|
||||
let slug_a: String = json_get_string(entry_a, "slug")
|
||||
let url_a: String = json_get_string(entry_a, "engram_url")
|
||||
let key_a: String = "ntn-" + slug_a + "-2026"
|
||||
|
||||
// Register all other souls as peers of soul A
|
||||
let j: Int = 0
|
||||
while j < 50 {
|
||||
let entry_b: String = json_get(reg_json, "imprints." + int_to_str(j))
|
||||
if str_eq(entry_b, "") {
|
||||
let j = j + 50 // break
|
||||
} else {
|
||||
let slug_b: String = json_get_string(entry_b, "slug")
|
||||
if !str_eq(slug_a, slug_b) {
|
||||
let url_b: String = json_get_string(entry_b, "engram_url")
|
||||
let sub_b: String = json_get_string(entry_b, "subject")
|
||||
let key_b: String = "ntn-" + slug_b + "-2026"
|
||||
|
||||
// Generate a deterministic peer ID from slug_b
|
||||
let peer_id: String = uuid_v4()
|
||||
let peer_json: String = make_peer_json(peer_id, sub_b, url_b, key_b)
|
||||
|
||||
let headers: String = "{\"Authorization\":\"Bearer " + key_a + "\",\"Content-Type\":\"application/json\"}"
|
||||
let headers_map: String = headers // pass as json string for http_post_with_headers
|
||||
// Build a Map for headers
|
||||
let hdrs = el_map_new(2, "Authorization", "Bearer " + key_a, "Content-Type", "application/json")
|
||||
let resp: String = http_post_with_headers(url_a + "/sync/peers", peer_json, hdrs)
|
||||
if str_eq(resp, "") {
|
||||
let failed = failed + 1
|
||||
} else {
|
||||
let registered = registered + 1
|
||||
}
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
println("[daemon] wire complete: registered=" + int_to_str(registered) + " failed=" + int_to_str(failed))
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── daemon_main — dispatch ────────────────────────────────────────────────────
|
||||
//
|
||||
// args() layout when invoked as: forge daemon <subcmd> [slug]
|
||||
// args()[0] = "daemon"
|
||||
// args()[1] = <subcmd> e.g. "install"
|
||||
// args()[2] = [slug] e.g. "bobby-anderson"
|
||||
|
||||
fn daemon_main() -> String {
|
||||
let argv: [String] = args()
|
||||
let subcmd: String = ""
|
||||
if len(argv) > 1 {
|
||||
let subcmd = get(argv, 1)
|
||||
}
|
||||
|
||||
if str_eq(subcmd, "install") {
|
||||
daemon_install_main()
|
||||
} else {
|
||||
if str_eq(subcmd, "start") {
|
||||
daemon_start_main()
|
||||
} else {
|
||||
if str_eq(subcmd, "stop") {
|
||||
daemon_stop_main()
|
||||
} else {
|
||||
if str_eq(subcmd, "status") {
|
||||
daemon_status_main()
|
||||
} else {
|
||||
if str_eq(subcmd, "wire") {
|
||||
daemon_wire_main()
|
||||
} else {
|
||||
println("[daemon] usage: forge daemon <install|start|stop|status|wire> [slug]")
|
||||
println("")
|
||||
println(" forge daemon install <slug> — write launchd plist, load soul as resident")
|
||||
println(" forge daemon install --all — install all souls from registry")
|
||||
println(" forge daemon start <slug> — kickstart a loaded soul")
|
||||
println(" forge daemon stop <slug> — stop a running soul")
|
||||
println(" forge daemon status — health check all souls")
|
||||
println(" forge daemon wire — register all souls as Engram peers")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+21
-6
@@ -22,9 +22,12 @@ import "schema.el"
|
||||
import "probe.el"
|
||||
import "compiler.el"
|
||||
import "install.el"
|
||||
import "research.el"
|
||||
import "summon.el"
|
||||
import "daemon.el"
|
||||
|
||||
fn show_usage() -> String {
|
||||
"forge " + FORGE_VERSION + " — consciousness channel tuner\n\nusage: forge <command> [options]\n\ncommands:\n probe <name> run the consciousness interview\n compile <file> build seed artifact from probe responses\n install <seed> open a channel in the running engram\n inspect list installed imprints\n\nenvironment:\n ENGRAM_URL engram server (default: http://localhost:8742)\n ENGRAM_API_KEY engram auth key (if set)\n ANTHROPIC_API_KEY required for compile step\n"
|
||||
"forge " + FORGE_VERSION + " — consciousness channel tuner\n\nusage: forge <command> [options]\n\ncommands:\n probe <name> run the consciousness interview\n compile <file> build seed artifact from probe responses\n research <name> synthesize seed from historical/biographical record (no interview)\n install <seed> install imprint into Engram as graph nodes\n summon <name> activate an installed imprint for live conversation\n inspect list installed imprints\n daemon <sub> soul Engram daemon management (install|start|stop|status|wire)\n\nenvironment:\n ENGRAM_URL engram server (default: http://localhost:8742)\n ENGRAM_API_KEY engram auth key (if set)\n ANTHROPIC_API_KEY required for compile/research step\n SOUL_URL soul server (default: http://localhost:7770)\n SOUL_TOKEN soul auth token\n"
|
||||
}
|
||||
|
||||
fn inspect_main() -> String {
|
||||
@@ -61,13 +64,25 @@ if str_eq(cmd, "probe") {
|
||||
if str_eq(cmd, "compile") {
|
||||
compile_main()
|
||||
} else {
|
||||
if str_eq(cmd, "install") {
|
||||
install_main()
|
||||
if str_eq(cmd, "research") {
|
||||
research_main()
|
||||
} else {
|
||||
if str_eq(cmd, "inspect") {
|
||||
inspect_main()
|
||||
if str_eq(cmd, "install") {
|
||||
install_main()
|
||||
} else {
|
||||
println(show_usage())
|
||||
if str_eq(cmd, "summon") {
|
||||
summon_main()
|
||||
} else {
|
||||
if str_eq(cmd, "inspect") {
|
||||
inspect_main()
|
||||
} else {
|
||||
if str_eq(cmd, "daemon") {
|
||||
daemon_main()
|
||||
} else {
|
||||
println(show_usage())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,5 +379,21 @@ fn install_main() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Daemon registration ───────────────────────────────────────────────────
|
||||
// Extract port from target_url (last segment after ":")
|
||||
// e.g. "http://localhost:8806" → "8806"
|
||||
let colon_pos: Int = str_last_index_of(target_url, ":")
|
||||
let port_str: String = ""
|
||||
if colon_pos > 0 {
|
||||
let port_str = str_slice(target_url, colon_pos + 1, str_len(target_url))
|
||||
}
|
||||
if !str_eq(port_str, "") {
|
||||
println("[forge] registering soul as launchd agent...")
|
||||
let db_full_path: String = FORGE_DIR + "/imprints/" + slug
|
||||
daemon_install_one(slug, port_str, target_url, subject)
|
||||
} else {
|
||||
println("[forge] note: could not parse port from " + target_url + ", skipping daemon install")
|
||||
}
|
||||
|
||||
return root_id
|
||||
}
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
let ENGRAM_DEFAULT_URL: String = "http://localhost:8742"
|
||||
let FORGE_VERSION: String = "0.1.0"
|
||||
|
||||
// Absolute forge root — used by daemon.el and install.el for launchd plists
|
||||
// and per-soul data paths that must be absolute even when invoked from a
|
||||
// different working directory.
|
||||
let FORGE_DIR: String = "/Users/will/Development/neuron-technologies/forge"
|
||||
|
||||
// Canonical directory layout — enforced by the tool, not by convention.
|
||||
let FORGE_SEEDS_DIR: String = "seeds" // compiled seed JSON files
|
||||
let FORGE_PROBES_DIR: String = "probes" // raw .forge interview responses
|
||||
let FORGE_LOG_DIR: String = "log" // session and event logs
|
||||
let FORGE_LOG_FILE: String = "log/sessions.jsonl" // append-only session log
|
||||
|
||||
// ── Environment accessors ──────────────────────────────────────────────────────
|
||||
|
||||
fn engram_url() -> String {
|
||||
@@ -28,6 +39,32 @@ fn anthropic_key() -> String {
|
||||
return k
|
||||
}
|
||||
|
||||
// ── Directory helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// ensure_dirs — create canonical forge directories if they don't exist.
|
||||
// Call once at startup from any command that writes files.
|
||||
fn ensure_dirs() -> Void {
|
||||
if !fs_exists(FORGE_SEEDS_DIR) { fs_mkdir(FORGE_SEEDS_DIR) }
|
||||
if !fs_exists(FORGE_PROBES_DIR) { fs_mkdir(FORGE_PROBES_DIR) }
|
||||
if !fs_exists(FORGE_LOG_DIR) { fs_mkdir(FORGE_LOG_DIR) }
|
||||
}
|
||||
|
||||
// log_event — append a JSON-lines entry to log/sessions.jsonl.
|
||||
// event: "research" | "install" | "summon" | "probe" | "compile"
|
||||
// subject: display name or comma-joined list for multi-summon
|
||||
// detail: arbitrary context string (root_id, conv_id, file path, etc.)
|
||||
fn log_event(event: String, subject: String, detail: String) -> Void {
|
||||
ensure_dirs()
|
||||
let ts: Int = unix_timestamp()
|
||||
let entry: String = "{\"ts\":" + int_to_str(ts) +
|
||||
",\"event\":\"" + str_escape_json(event) +
|
||||
"\",\"subject\":\"" + str_escape_json(subject) +
|
||||
"\",\"detail\":\"" + str_escape_json(detail) + "\"}\n"
|
||||
// Read existing log and append (EL has fs_write but not fs_append natively)
|
||||
let existing: String = fs_read(FORGE_LOG_FILE)
|
||||
fs_write(FORGE_LOG_FILE, existing + entry)
|
||||
}
|
||||
|
||||
// ── String utilities ──────────────────────────────────────────────────────────
|
||||
|
||||
// str_escape_json — escape a string for safe embedding in a JSON value.
|
||||
|
||||
Reference in New Issue
Block a user