Skip to content
Open
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
107 changes: 97 additions & 10 deletions cmd/nlm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var (
chunkedResponse bool // Control rt=c parameter for chunked vs JSON array response
useDirectRPC bool // Use direct RPC calls instead of orchestration service
skipSources bool // Skip fetching sources for chat (useful when project is inaccessible)
full bool // Display full content (for notes, etc)
)

// ChatSession represents a persistent chat conversation
Expand All @@ -47,7 +48,7 @@ type ChatSession struct {

// ChatMessage represents a single message in the conversation
type ChatMessage struct {
Role string `json:"role"` // "user" or "assistant"
Role string `json:"role"` // "user" or "assistant"
Content string `json:"content"`
Timestamp time.Time `json:"timestamp"`
}
Expand All @@ -64,6 +65,7 @@ func init() {
flag.StringVar(&authToken, "auth", os.Getenv("NLM_AUTH_TOKEN"), "auth token (or set NLM_AUTH_TOKEN)")
flag.StringVar(&cookies, "cookies", os.Getenv("NLM_COOKIES"), "cookies for authentication (or set NLM_COOKIES)")
flag.StringVar(&mimeType, "mime", "", "specify MIME type for content (e.g. 'text/xml', 'application/json')")
flag.BoolVar(&full, "full", false, "display full content")

flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: nlm <command> [arguments]\n\n")
Expand All @@ -84,7 +86,10 @@ func init() {
fmt.Fprintf(os.Stderr, " discover-sources <id> <query> Discover relevant sources\n\n")

fmt.Fprintf(os.Stderr, "Note Commands:\n")
fmt.Fprintf(os.Stderr, " notes <id> List notes in notebook\n")
fmt.Fprintf(os.Stderr, " notes <notebook-id> [--full]\n")
fmt.Fprintf(os.Stderr, " List notes in notebook (use --full for content)\n")
fmt.Fprintf(os.Stderr, " read-note <notebook-id> <note-id>\n")
fmt.Fprintf(os.Stderr, " Display content of a specific note\n")
fmt.Fprintf(os.Stderr, " new-note <id> <title> Create new note\n")
fmt.Fprintf(os.Stderr, " update-note <id> <note-id> <content> <title> Edit note\n")
fmt.Fprintf(os.Stderr, " rm-note <note-id> Remove note\n\n")
Expand Down Expand Up @@ -397,8 +402,14 @@ func validateArgs(cmd string, args []string) error {
return fmt.Errorf("invalid arguments")
}
case "notes":
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "usage: nlm notes <notebook-id>\n")
// Allow 1 arg (notebook-id) or 2 args (notebook-id + --full flag)
if len(args) < 1 || len(args) > 2 {
fmt.Fprintf(os.Stderr, "usage: nlm notes <notebook-id> [--full]\n")
return fmt.Errorf("invalid arguments")
}
case "read-note":
if len(args) != 2 {
fmt.Fprintf(os.Stderr, "usage: nlm read-note <notebook-id> <note-id>\n")
return fmt.Errorf("invalid arguments")
}
case "feedback":
Expand All @@ -416,7 +427,7 @@ func isValidCommand(cmd string) bool {
"help", "-h", "--help",
"list", "ls", "create", "rm", "analytics", "list-featured",
"sources", "add", "rm-source", "rename-source", "refresh-source", "check-source", "discover-sources",
"notes", "new-note", "update-note", "rm-note",
"notes", "read-note", "new-note", "update-note", "rm-note",
"audio-create", "audio-get", "audio-rm", "audio-share", "audio-list", "audio-download", "video-create", "video-list", "video-download",
"create-artifact", "get-artifact", "list-artifacts", "artifacts", "rename-artifact", "delete-artifact",
"generate-guide", "generate-outline", "generate-section", "generate-magic", "generate-mindmap", "generate-chat", "chat", "chat-list",
Expand Down Expand Up @@ -702,7 +713,25 @@ func runCmd(client *api.Client, cmd string, args ...string) error {

// Note operations
case "notes":
err = listNotes(client, args[0])
// Handle --full flag if passed as argument
showFull := full
cleanArgs := []string{}
for _, arg := range args {
if arg == "--full" || arg == "-full" {
showFull = true
} else {
cleanArgs = append(cleanArgs, arg)
}
}

if len(cleanArgs) != 1 {
fmt.Fprintf(os.Stderr, "usage: nlm notes <notebook-id> [--full]\n")
return fmt.Errorf("invalid arguments")
}

err = listNotes(client, cleanArgs[0], showFull)
case "read-note":
err = readNote(client, args[0], args[1])
case "new-note":
err = createNote(client, args[0], args[1])
case "update-note":
Expand Down Expand Up @@ -1017,24 +1046,82 @@ func removeNote(c *api.Client, notebookID, noteID string) error {
}

// Note operations
func listNotes(c *api.Client, notebookID string) error {
func listNotes(c *api.Client, notebookID string, showFull bool) error {
notes, err := c.GetNotes(notebookID)
if err != nil {
return fmt.Errorf("list notes: %w", err)
}

if len(notes) == 0 {
fmt.Println("No notes found in this notebook.")
return nil
}

if showFull {
for _, note := range notes {
currentID := note.GetSourceId().GetSourceId()
if currentID == "" {
currentID = note.GetSourceId().String()
}
fmt.Printf("Title: %s\n", note.Title)
fmt.Printf("ID: %s\n", currentID)
fmt.Printf("Last Modified: %s\n", note.GetMetadata().GetLastModifiedTime().AsTime().Format(time.RFC3339))
fmt.Println("-------------------")
fmt.Println(note.Content)
fmt.Println("================================================================================")
}
return nil
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
fmt.Fprintln(w, "ID\tTITLE\tLAST MODIFIED")
for _, note := range notes {
lastModified := "unknown"
if meta := note.GetMetadata(); meta != nil && meta.LastModifiedTime != nil {
lastModified = meta.LastModifiedTime.AsTime().Format(time.RFC3339)
}
// Extract the actual note ID from the SourceId wrapper
noteID := note.GetSourceId().GetSourceId()
if noteID == "" {
noteID = note.GetSourceId().String()
}
title := note.Title
if title == "" {
title = "(untitled)"
}
fmt.Fprintf(w, "%s\t%s\t%s\n",
note.GetSourceId(),
note.Title,
note.GetMetadata().LastModifiedTime.AsTime().Format(time.RFC3339),
noteID,
title,
lastModified,
)
}
return w.Flush()
}

func readNote(c *api.Client, notebookID string, targetNoteID string) error {
notes, err := c.GetNotes(notebookID)
if err != nil {
return fmt.Errorf("read note: %w", err)
}

for _, note := range notes {
// Check both direct ID and source ID wrapper
currentID := note.GetSourceId().GetSourceId()
if currentID == "" {
currentID = note.GetSourceId().String()
}

if currentID == targetNoteID {
fmt.Printf("Title: %s\n", note.Title)
fmt.Printf("ID: %s\n", currentID)
fmt.Println("-------------------")
fmt.Println(note.Content)
return nil
}
}
return fmt.Errorf("note not found: %s", targetNoteID)
}

// Audio operations
func getAudioOverview(c *api.Client, projectID string) error {
fmt.Fprintf(os.Stderr, "Fetching audio overview...\n")
Expand Down
24 changes: 20 additions & 4 deletions cmd/nlm/testdata/note_commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,35 @@
# === NOTES COMMAND ===
# Test notes without arguments
! exec ./nlm_test notes
stderr 'usage: nlm notes <notebook-id>'
stderr 'usage: nlm notes'
! stderr 'panic'

# Test notes with too many arguments
! exec ./nlm_test notes notebook123 extra
stderr 'usage: nlm notes <notebook-id>'
# Test notes with --full flag (should require auth, not error on args)
! exec ./nlm_test notes notebook123 --full
stderr 'Authentication required'
! stderr 'panic'

# Test notes without authentication
! exec ./nlm_test notes notebook123
stderr 'Authentication required'
! stderr 'panic'

# === READ-NOTE COMMAND ===
# Test read-note without arguments
! exec ./nlm_test read-note
stderr 'usage: nlm read-note'
! stderr 'panic'

# Test read-note with only notebook ID
! exec ./nlm_test read-note notebook123
stderr 'usage: nlm read-note'
! stderr 'panic'

# Test read-note without authentication
! exec ./nlm_test read-note notebook123 note456
stderr 'Authentication required'
! stderr 'panic'

# === NEW-NOTE COMMAND ===
# Test new-note without arguments
! exec ./nlm_test new-note
Expand Down
1 change: 1 addition & 0 deletions gen/notebooklm/v1alpha1/notebooklm.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading