Skip to content
This repository was archived by the owner on Oct 13, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions cmd/nook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io/fs"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -80,6 +81,8 @@ func main() {
err = transfer(args, asJSON)
case "versions":
err = versions(args, asJSON)
case "logs":
err = logs(args, asJSON)
case "rollback":
err = rollback(args, asJSON)
case "export":
Expand Down Expand Up @@ -135,6 +138,7 @@ func usage() {
more:
unshare [nook] <email> mode [nook] <private|org|link|public>
transfer [nook] <email> versions [nook] rollback [nook] [version]
logs [nook] [-n lines] what a nook with a run command has printed
export [nook] [-o file] import <file> [--name n] [--share]
data <nook> list|get|create|update|delete|export|import <collection> ...
secret set|list|unset [nook] KEY values come from stdin or a prompt, never argv
Expand Down Expand Up @@ -183,6 +187,10 @@ var helpText = map[string]string{
"transfer": `nook transfer [nook] <email>
make someone else the owner. you stay on as an editor. any secrets are cleared and the
new owner is told which names to set.`,
"logs": `nook logs [nook] [-n lines]
what a nook with a run command has printed: its own output, its scheduled jobs, and the
supervisor's restart notes. the last 200 lines unless -n says otherwise. only the owner may
read them. a nook without a run command has no logs; its code runs in the browser.`,
"versions": `nook versions [nook]
every deploy of the nook, newest first, with its version number.`,
"rollback": `nook rollback [nook] [version]
Expand Down Expand Up @@ -966,6 +974,34 @@ func importNook(args []string, asJSON bool) error {
return emit(asJSON, out, fmt.Sprintf("imported %v (%v files, %v documents)\n%v", out["name"], out["files"], out["documents"], out["url"]))
}

func logs(args []string, asJSON bool) error {
n, rest := flagValue(args, "-n")
name, _, err := nookName(rest, nil)
if err != nil {
return err
}
q := ""
if n != "" {
q = "?lines=" + url.QueryEscape(n)
}
out, err := call(http.MethodGet, "/v1/nooks/"+name+"/logs"+q, nil, "")
if err != nil {
return err
}
if asJSON {
return json.NewEncoder(os.Stdout).Encode(out)
}
lines, _ := out["lines"].([]any)
if len(lines) == 0 {
fmt.Fprintf(os.Stderr, "no logs yet for %s (%v)\n", name, out["source"])
return nil
}
for _, l := range lines {
fmt.Println(l)
}
return nil
}

func versions(args []string, asJSON bool) error {
name, _, err := nookName(args, nil)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions cmd/nook/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ var mcpTools = []mcpTool{
{"nook_data_create", "Create a document (a JSON object) in a nook collection.", obj(map[string]any{"nook": str("nook name"), "collection": str("collection name"), "data": map[string]any{"type": "object", "description": "the document"}}, "nook", "collection", "data")},
{"nook_data_update", "Merge fields into an existing document.", obj(map[string]any{"nook": str("nook name"), "collection": str("collection name"), "id": str("document id"), "data": map[string]any{"type": "object"}}, "nook", "collection", "id", "data")},
{"nook_data_delete", "Delete a document.", obj(map[string]any{"nook": str("nook name"), "collection": str("collection name"), "id": str("document id")}, "nook", "collection", "id")},
{"nook_logs", "What a nook with a run command has printed on its server: the last lines of its output, scheduled jobs, and restarts. Owner only.", obj(map[string]any{"nook": str("nook name"), "lines": map[string]any{"type": "integer", "description": "how many recent lines (default 200)"}}, "nook")},
{"nook_rollback", "Roll a nook back to a previous version (default: the one before current).", obj(map[string]any{"nook": str("nook name"), "version": map[string]any{"type": "integer"}}, "nook")},
{"nook_remix", "Copy a nook the user can open into one they own, with its files and none of its data.", obj(map[string]any{"nook": str("source nook name"), "name": str("name for the copy (optional)")}, "nook")},
{"nook_pull", "Download a nook's current files into a directory so they can be edited and redeployed.", obj(map[string]any{"nook": str("nook name"), "dir": str("target directory (default: the nook name)")}, "nook")},
Expand Down Expand Up @@ -161,6 +162,12 @@ func mcpCall(name string, a map[string]any) (string, bool) {
return res(call(http.MethodDelete, "/v1/nooks/"+argStr(a, "nook")+"/share/"+argStr(a, "email"), nil, ""))
case "nook_mode":
return res(callJSON(http.MethodPut, "/v1/nooks/"+argStr(a, "nook")+"/mode", map[string]string{"mode": argStr(a, "mode")}))
case "nook_logs":
q := ""
if n, ok := a["lines"].(float64); ok && n > 0 {
q = fmt.Sprintf("?lines=%d", int(n))
}
return res(call(http.MethodGet, "/v1/nooks/"+argStr(a, "nook")+"/logs"+q, nil, ""))
case "nook_rollback":
v, _ := a["version"].(float64)
return res(callJSON(http.MethodPost, "/v1/nooks/"+argStr(a, "nook")+"/rollback", map[string]int{"version": int(v)}))
Expand Down
Loading