diff --git a/repl.lua b/repl.lua index a865dcf..12212a8 100644 --- a/repl.lua +++ b/repl.lua @@ -188,6 +188,8 @@ Meta commands: :cost summary of session token/cost usage :cost detail per-model + per-category breakdown :cost reset zero the cost meter (also clears warn flags) + :config show [full] show config sources (user / project overlay) + + effective config; sensitive keys masked as (set) :highlight [on|off|status] toggle tree-sitter syntax highlighting on assistant code fences (requires external tree-sitter CLI + @@ -2045,6 +2047,99 @@ function M.run(config) -- :cost reset zero the meter (also clears warn flags) -- R7 sort: (cost desc, model asc, category asc) — table.sort is -- unstable, so the 3-level key ensures deterministic output. + -- Phase 9: :config show meta. Reads config._sources (R3 cfg- + -- embedded source map from main.lua's load_config_with_overlay) + -- + the effective config. Sanitizes token-bearing fields per + -- the masking heuristic (any key containing token/secret/auth/ + -- key, case-insensitive -> "(set)" instead of the value). + -- + -- R6: `:config show` (default) shows top-level keys with nested + -- tables collapsed to their inner-key list; `:config show full` + -- recurses and applies the same masking heuristic at every level. + -- N2 known false positive: key_env / auth_env hold env-var + -- names (not secrets) but match the heuristic; future polish + -- exempts `*_env` patterns. + local _CFG_MASK_RE = "token|secret|auth|key" -- pipe-OR via gsub + local function _is_sensitive_key(k) + local lk = tostring(k):lower() + return lk:find("token", 1, true) + or lk:find("secret", 1, true) + or lk:find("auth", 1, true) + or lk:find("key", 1, true) + end + local function _fmt_value(v, full, depth) + depth = depth or 0 + if type(v) == "string" then return string.format("%q", v) end + if type(v) == "number" or type(v) == "boolean" then return tostring(v) end + if type(v) == "function" then return "" end + if type(v) == "table" then + local keys = {} + for k, _ in pairs(v) do keys[#keys + 1] = tostring(k) end + table.sort(keys) + if not full then + return "{" .. table.concat(keys, ", ") .. "}" + end + -- Full mode: recurse, masking sensitive keys. + if depth >= 5 then return "{...}" end -- defensive depth cap + local parts = {} + for _, k in ipairs(keys) do + local val + if _is_sensitive_key(k) then + val = "(set)" + else + val = _fmt_value(v[k], true, depth + 1) + end + parts[#parts + 1] = ("%s = %s"):format(k, val) + end + return "{" .. table.concat(parts, ", ") .. "}" + end + return "<" .. type(v) .. ">" + end + meta.config = function(args) + local sub = ((args or ""):match("^%s*(%S*)") or ""):lower() + if sub ~= "" and sub ~= "show" then + renderer.status("usage: :config show [full]") + return + end + local full = (args or ""):lower():match("show%s+full") ~= nil + local sources = config._sources + if sources then + -- Group by source for the "config sources" listing. + local user_keys, project_keys = {}, {} + for k, src in pairs(sources) do + if src == "project" then project_keys[#project_keys + 1] = k + else user_keys[#user_keys + 1] = k end + end + table.sort(user_keys); table.sort(project_keys) + renderer.status("config sources:") + io.write((" user: %s keys: %s\n"):format( + #user_keys, table.concat(user_keys, ", "))) + if #project_keys > 0 then + io.write((" project: %s keys: %s\n"):format( + #project_keys, table.concat(project_keys, ", "))) + end + else + renderer.status("config sources: (unknown — main didn't pass _sources)") + end + renderer.status(full and "effective config (full, sensitive masked):" + or "effective config (top-level; ':config show full' for deep):") + local top_keys = {} + for k, _ in pairs(config) do + if k ~= "_sources" then top_keys[#top_keys + 1] = k end + end + table.sort(top_keys) + for _, k in ipairs(top_keys) do + local source_tag = sources and sources[k] or "?" + local val + if _is_sensitive_key(k) then + val = "(set)" + else + val = _fmt_value(config[k], full) + end + io.write((" %-20s = %s [%s]\n"):format(k, val, source_tag)) + end + end + -- R10: $%.6f for sub-cent precision (cloud costs can be ~3e-05). -- R6: annotation uses the per-slot is_local sticky flag rather -- than a fragile cost==0 heuristic.