Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/just-cameron/note. External storage tool for Letta agents to manage persistent notes
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164package sync
import ( "fmt" "os" "path/filepath" "strings" "time"
"github.com/cpfiffer/note/internal/api" "github.com/cpfiffer/note/internal/util")
// PushOptions configures the push operationtype PushOptions struct { Force bool Dir string}
// PushResult contains the results of a push operationtype PushResult struct { Created []string Updated []string Conflicts []string Unchanged []string Skipped []string}
// Push uploads local files to Lettafunc Push(client *api.Client, state *State, opts PushOptions) (*PushResult, error) { result := &PushResult{}
// Walk the directory to find all .md files err := filepath.Walk(opts.Dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err }
// Skip directories if info.IsDir() { // Skip hidden directories if strings.HasPrefix(info.Name(), ".") && path != opts.Dir { return filepath.SkipDir } return nil }
// Skip non-md files if !strings.HasSuffix(info.Name(), ".md") { return nil }
// Skip hidden files and conflict files if strings.HasPrefix(info.Name(), ".") { return nil } if IsConflictFile(info.Name()) { result.Skipped = append(result.Skipped, path) return nil }
// Get relative path from sync directory relPath, err := filepath.Rel(opts.Dir, path) if err != nil { return fmt.Errorf("failed to get relative path: %w", err) }
notePath := FileToPath(relPath)
// Skip note_directory if notePath == "/note_directory" { result.Skipped = append(result.Skipped, notePath) return nil }
// Read local content content, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read file %s: %w", path, err) } localContent := string(content) localHash := util.HashContent(localContent)
// Check state fileState, hasState := state.Files[notePath]
// Get remote block ownerSearch := fmt.Sprintf("owner:%s", state.AgentID) blocks, err := client.ListBlocks(ownerSearch) if err != nil { return fmt.Errorf("failed to list blocks: %w", err) }
var remoteBlock *api.Block for i := range blocks { if blocks[i].Label == notePath { remoteBlock = &blocks[i] break } }
if remoteBlock != nil { remoteHash := util.HashContent(remoteBlock.Value)
// Check for conflicts if hasState && remoteHash != fileState.RemoteHash && localHash != fileState.LocalHash { if !opts.Force { result.Conflicts = append(result.Conflicts, notePath) return nil } }
// Check if unchanged if localHash == remoteHash { result.Unchanged = append(result.Unchanged, notePath) state.Files[notePath] = FileState{ LocalHash: localHash, RemoteHash: remoteHash, SyncedAt: time.Now(), BlockID: remoteBlock.ID, } return nil }
// Update remote _, err := client.UpdateBlock(remoteBlock.ID, localContent) if err != nil { return fmt.Errorf("failed to update block %s: %w", notePath, err) } result.Updated = append(result.Updated, notePath)
state.Files[notePath] = FileState{ LocalHash: localHash, RemoteHash: localHash, // Remote now matches local SyncedAt: time.Now(), BlockID: remoteBlock.ID, } } else { // Create new block description := fmt.Sprintf("owner:%s", state.AgentID) block, err := client.CreateBlock(notePath, localContent, description) if err != nil { return fmt.Errorf("failed to create block %s: %w", notePath, err) } result.Created = append(result.Created, notePath)
state.Files[notePath] = FileState{ LocalHash: localHash, RemoteHash: localHash, SyncedAt: time.Now(), BlockID: block.ID, } }
return nil })
if err != nil { return nil, err }
return result, nil}