I started using claude.ai in earnest this week and I prompted for a space lua debugging solution which it gave me in a single page.
I've used the 'dbg' functions (included in the page below) successfully so far but can't guarantee the code. I am using sb 2.10. Hope others might find it useful.
Here is the single prompt I used
You are an expert on silverbullet v2.10 and later versions. You especially understand space lua functions, queries, commands, widgets, plugs etc. I want to be able to debug space lua scripts with breakpoints and examination of objects and variables. Give me some ideas and guidance
Cheers!
Debug helper library for Space Lua. Drop this page anywhere in your space
(a "Library/" folder is the usual convention). Everything below lives in
one `space-lua` block so it loads as a single unit.
```space-lua
-- ============================================================
-- dbg: a poor-man's debugger for Space Lua
-- ============================================================
-- Guard against wiping state when this block reloads/reindexes.
dbg = dbg or {}
dbg._log = dbg._log or {}
dbg._watches = dbg._watches or {}
dbg._counter = dbg._counter or 0
dbg._maxLog = dbg._maxLog or 200
-- ---------- inspection / pretty-printing ----------
-- Recursively render any Lua value as a readable string.
-- Handles cycles and limits depth so you don't lock up on huge tables.
local function inspectValue(value, indent, seen, depth, maxDepth)
indent = indent or ""
seen = seen or {}
depth = depth or 0
maxDepth = maxDepth or 4
local t = type(value)
if t == "table" then
if seen[value] then
return "<circular reference>"
end
if depth >= maxDepth then
return "{ ...max depth reached... }"
end
seen[value] = true
local parts = {}
for k, v in pairs(value) do
local rendered = inspectValue(v, indent .. " ", seen, depth + 1, maxDepth)
table.insert(parts, indent .. " " .. tostring(k) .. " = " .. rendered)
end
seen[value] = nil
if #parts == 0 then
return "{}"
end
return "{\n" .. table.concat(parts, ",\n") .. "\n" .. indent .. "}"
elseif t == "string" then
return string.format("%q", value)
else
return tostring(value)
end
end
-- Public: dbg.dump(value) -> pretty string
function dbg.dump(value)
return inspectValue(value)
end
-- ---------- logging (a stand-in for "print and inspect here") ----------
-- dbg.log(label, value) logs to the browser console AND an in-memory
-- buffer you can review later, then returns `value` unchanged so you
-- can wrap any expression without altering behavior:
-- local total = dbg.log("total", price * qty)
function dbg.log(label, value)
dbg._counter = dbg._counter + 1
local rendered = dbg.dump(value)
local entry = {
seq = dbg._counter,
label = label,
value = rendered
}
table.insert(dbg._log, entry)
-- trim buffer so it can't grow forever in a long session
if #dbg._log > dbg._maxLog then
table.remove(dbg._log, 1)
end
js.log("[dbg #" .. entry.seq .. "] " .. tostring(label) .. " = " .. rendered)
return value
end
-- dbg.clear() resets the log buffer and watches
function dbg.clear()
dbg._log = {}
dbg._watches = {}
dbg._counter = 0
editor.flashNotification("dbg: log and watches cleared")
end
-- dbg.dumpLog() returns the whole log buffer as one string, newest last
function dbg.dumpLog()
local lines = {}
for _, entry in ipairs(dbg._log) do
table.insert(lines, "#" .. entry.seq .. " " .. entry.label .. " = " .. entry.value)
end
if #lines == 0 then
return "(log is empty)"
end
return table.concat(lines, "\n\n")
end
-- ---------- named watches (a lightweight "watch window") ----------
-- dbg.watch(name, value) records a named value you can inspect later
-- from a panel page. Returns value unchanged, same wrapping trick as log.
function dbg.watch(name, value)
dbg._watches[name] = dbg.dump(value)
return value
end
-- dbg.snapshot() returns a SilverBullet widget table you can drop into
-- any page as: ${dbg.snapshot()}
-- It renders current watches as a markdown table, refreshed on preview.
function dbg.snapshot()
local rows = {}
table.insert(rows, "| name | value |")
table.insert(rows, "|---|---|")
local any = false
for name, value in pairs(dbg._watches) do
any = true
local safe = value:gsub("\n", " ")
safe = safe:gsub("|", "\\|")
table.insert(rows, "| " .. name .. " | `" .. safe .. "` |")
end
if not any then
return { markdown = "_(no watches set yet — call dbg.watch(name, value) somewhere)_" }
end
return { markdown = table.concat(rows, "\n") }
end
-- ---------- true pausing breakpoint ----------
-- dbg.breakpoint(label, context) actually PAUSES script execution.
-- It bridges to the browser's native window.alert, which is a blocking
-- call — so Space Lua execution halts until you click OK. This is the
-- closest thing to a real breakpoint available in this environment.
--
-- WARNING: this freezes the whole browser tab's UI while the alert is
-- open, not just the script. Remove or comment out calls before leaving
-- the page in a shared/production space.
function dbg.breakpoint(label, context)
local msg = "Breakpoint: " .. tostring(label)
if context ~= nil then
msg = msg .. "\n\n" .. dbg.dump(context)
end
js.window.alert(msg)
end
-- dbg.breakpointIf(condition, label, context) only pauses when
-- condition is true — your equivalent of a conditional breakpoint.
function dbg.breakpointIf(condition, label, context)
if condition then
dbg.breakpoint(label, context)
end
end
-- ---------- commands ----------
command.define {
name = "Debug: Show Log",
run = function()
editor.flashNotification(dbg.dumpLog())
end
}
command.define {
name = "Debug: Clear Log And Watches",
run = function()
dbg.clear()
end
}
command.define {
name = "Debug: Write Log To Page",
run = function()
space.writePage("Debug Log", "# Debug Log\n\n```\n" .. dbg.dumpLog() .. "\n```")
editor.flashNotification("Written to page 'Debug Log' — open it from the page picker")
end
}
```
## How to use it
**Inline inspection (replaces "print and check the console"):**
```
local rows = query[[ from p = index.tag "task" select p ]]
local total = dbg.log("task count", #rows)
```
`dbg.log` prints a pretty-formatted value to the browser dev console
*and* returns the value unchanged, so dropping it into an existing
expression never changes behavior — remove it later with a one-line
diff.
**Named watches, viewed from a separate panel page:**
```
dbg.watch("currentUser", user)
dbg.watch("filteredResults", results)
```
Then on any page (a dedicated "Debug Panel" page is handy):
```
${dbg.snapshot()}
```
This live-previews as a small markdown table of whatever was last
watched. Alt-click it or reload the page to refresh.
**A real pause, with inspection, mid-script:**
```
for _, item in ipairs(items) do
if item.price == nil then
dbg.breakpoint("missing price", item)
end
-- processing continues here once you click OK
end
```
Because `js.window.alert` is a blocking browser call, execution
genuinely halts at that line until you dismiss the dialog — you get to
read the dumped table before anything downstream runs. Use
`dbg.breakpointIf(cond, label, context)` for the conditional-breakpoint
version so you're not clicking through every iteration.
**Commands available from the palette:**
- `Debug: Show Log` — flashes the whole log buffer as a notification
- `Debug: Clear Log And Watches` — resets state between debugging sessions
- `Debug: Write Log To Page` — dumps the log buffer into a page called
"Debug Log" for slower review (notification flashes truncate long output)
## Known limitations, honestly
- `dbg.breakpoint` freezes the entire tab, not just your script — fine
for solo debugging, bad if you leave one in and someone else hits that
page.
- The log buffer is in-memory per browser session; it won't survive a
full page reload unless you also call `Debug: Write Log To Page`.
- This can't show you a call stack or let you step line-by-line — it's
strictly "pause here and look at these values," not a real debugger.
If SilverBullet ships native debugging tooling later, retire this.