diff --git a/cmd/nook/main.go b/cmd/nook/main.go index 77f16e3..9bba275 100644 --- a/cmd/nook/main.go +++ b/cmd/nook/main.go @@ -11,6 +11,7 @@ import ( "io/fs" "net" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -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": @@ -135,6 +138,7 @@ func usage() { more: unshare [nook] mode [nook] transfer [nook] versions [nook] rollback [nook] [version] + logs [nook] [-n lines] what a nook with a run command has printed export [nook] [-o file] import [--name n] [--share] data list|get|create|update|delete|export|import ... secret set|list|unset [nook] KEY values come from stdin or a prompt, never argv @@ -183,6 +187,10 @@ var helpText = map[string]string{ "transfer": `nook transfer [nook] 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] @@ -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 { diff --git a/cmd/nook/mcp.go b/cmd/nook/mcp.go index 9a7d614..850dc25 100644 --- a/cmd/nook/mcp.go +++ b/cmd/nook/mcp.go @@ -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")}, @@ -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)}))