lune

created pr with ps-216 on 2026-04-22T02:46:44Z · by 613b58b7
changed status to closed on 2026-06-21T16:45:31Z · by 66ddd677
Closing as superceded by 126
cmds
checkout latest patchset:
ssh pr.pico.sh print pr-124 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 124
set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 124
set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 124

Patchset ps-216 on 2026-04-22T02:46:44Z · commit c03fc8d

listing real name of tasks
juan 2026-04-21T03:21:16Z
construction of db for management of decrypted names of tasks
juan 2026-04-21T05:43:09Z
fix UTF8 chracteres
juan 2026-04-21T21:13:24Z
filter by status and coloring types of status
juan 2026-04-21T21:18:53Z
update help
juan 2026-04-21T21:21:36Z
list areas names
juan 2026-04-21T21:44:53Z
adjust table tasks to truncate name and list area
juan 2026-04-21T21:56:40Z
partial implementation of listing tasks by areas
juan 2026-04-21T22:05:20Z
final implementation of listing tasks by areas with UTF-8 chracteres
juan 2026-04-21T22:10:20Z
sort on task list to show first do now
juan 2026-04-21T22:33:29Z
update with 8 char of id and db.go
juan 2026-04-21T23:27:54Z
partial implementation of TUI menu on task update
juan 2026-04-21T23:35:35Z
task name correcion on TUI menu of Update task
juan 2026-04-21T23:38:19Z
TUI menu for update task with others flags
juan 2026-04-21T23:43:02Z
helper for TUI update task
juan 2026-04-21T23:49:56Z
search by first 8 char of note id for editing it
juan 2026-04-22T00:00:17Z
partial implementation on note TUI menu for update
juan 2026-04-22T00:05:46Z
TUI menu notebook just with tags
juan 2026-04-22T00:20:55Z
Note update now let you insert only the first 8 char of notebook id in update
juan 2026-04-22T00:25:53Z
implementation of (lune note notebook-list) for listing all notebooks and its ID
juan 2026-04-22T01:10:51Z
partial implementation of lune note show-content ID
juan 2026-04-22T02:01:11Z
convert \n in new line and \ in space for lune note show-content ID
juan 2026-04-22T02:18:42Z
listing real name of tasks
juan 2026-04-21T03:21:16Z
Semantic diff summary
14 added, 12 modified, 0 signature changed, 6 removed across 6 analyzed files (2 files skipped: unsupported file type)
+25 -0 cmd/init/apikey.go #
@@ -16,6 +16,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"git.secluded.site/lune/internal/client"
+	"git.secluded.site/lune/internal/db"
 	"git.secluded.site/lune/internal/ui"
 )
 
@@ -219,3 +220,27 @@ func validateTokenWithPing(token string) error {
 
 	return err
 }
+
+func runMasterPasswordStep(cmd *cobra.Command) wizardNav {
+	var password string
+	err := huh.NewInput().
+		Title("Lunatask Master Password").
+		Description("Optional: provide your master password to enable local database decryption (stored securely).").
+		EchoMode(huh.EchoModePassword).
+		Value(&password).
+		Run()
+	if err != nil {
+		if errors.Is(err, huh.ErrUserAborted) {
+			return navQuit
+		}
+		return navNext // Continue anyway
+	}
+
+	if password != "" {
+		if err := db.SetMasterPassword(password); err != nil {
+			fmt.Fprintln(cmd.OutOrStdout(), ui.Error.Render("Failed to save master password: "+err.Error()))
+		}
+	}
+
+	return navNext
+}
+3 -0 cmd/init/init.go #
@@ -120,6 +120,7 @@ func runFreshSetup(cmd *cobra.Command, cfg *config.Config) error {
 		func() wizardNav { return runHabitsStep(cfg) },
 		func() wizardNav { return runDefaultsStep(cfg) },
 		func() wizardNav { return runAccessTokenStep(cmd) },
+		func() wizardNav { return runMasterPasswordStep(cmd) },
 	}
 
 	step := 0
@@ -173,6 +174,7 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error {
 		"defaults":  func() error { return configureDefaults(cfg) },
 		"ui":        func() error { return configureUIPrefs(cfg) },
 		"apikey":    func() error { return configureAccessToken(cmd) },
+		"password":  func() error { runMasterPasswordStep(cmd); return nil },
 		"reset":     func() error { return resetConfig(cmd, cfg) },
 	}
 
@@ -188,6 +190,7 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error {
 				huh.NewOption("Set defaults", "defaults"),
 				huh.NewOption("UI preferences", "ui"),
 				huh.NewOption("Access token", "apikey"),
+				huh.NewOption("Master password (for LocalDB)", "password"),
 				huh.NewOption("Reset all configuration", "reset"),
 				huh.NewOption("Done", choiceDone),
 			).
+34 -21 cmd/note/list.go #
@@ -13,12 +13,18 @@ import (
 	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/completion"
 	"git.secluded.site/lune/internal/config"
+	"git.secluded.site/lune/internal/db"
 	"git.secluded.site/lune/internal/ui"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/lipgloss/table"
 	"github.com/spf13/cobra"
 )
 
+type enrichedNote struct {
+	lunatask.Note
+	Name string `json:"name,omitempty"`
+}
+
 // ListCmd lists notes. Exported for potential use by shortcuts.
 var ListCmd = &cobra.Command{
 	Use:   "list",
@@ -54,26 +60,42 @@ func runList(cmd *cobra.Command, _ []string) error {
 		return err
 	}
 
+	cfg, _ := config.Load()
+	enriched := make([]enrichedNote, 0, len(notes))
+	for _, n := range notes {
+		name := ""
+		if cfg != nil && cfg.Experimental.LocalDB {
+			name, _ = db.EnrichTask(n.ID) // EnrichTask is generic enough for IDs
+		}
+		enriched = append(enriched, enrichedNote{Note: n, Name: name})
+	}
+
 	notebookID, err := resolveNotebookFilter(cmd)
 	if err != nil {
 		return err
 	}
 
+	filtered := enriched
 	if notebookID != "" {
-		notes = filterByNotebook(notes, notebookID)
+		filtered = make([]enrichedNote, 0)
+		for _, en := range enriched {
+			if en.NotebookID != nil && *en.NotebookID == notebookID {
+				filtered = append(filtered, en)
+			}
+		}
 	}
 
-	if len(notes) == 0 {
+	if len(filtered) == 0 {
 		fmt.Fprintln(cmd.OutOrStdout(), "No notes found")
 
 		return nil
 	}
 
 	if mustGetBoolFlag(cmd, "json") {
-		return outputJSON(cmd, notes)
+		return outputJSONEnriched(cmd, filtered)
 	}
 
-	return outputTable(cmd, notes)
+	return outputTableEnriched(cmd, filtered)
 }
 
 func buildListOptions(cmd *cobra.Command) *lunatask.ListNotesOptions {
@@ -119,18 +141,6 @@ func resolveNotebookFilter(cmd *cobra.Command) (string, error) {
 	return notebook.ID, nil
 }
 
-func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note {
-	filtered := make([]lunatask.Note, 0, len(notes))
-
-	for _, note := range notes {
-		if note.NotebookID != nil && *note.NotebookID == notebookID {
-			filtered = append(filtered, note)
-		}
-	}
-
-	return filtered
-}
-
 func mustGetStringFlag(cmd *cobra.Command, name string) string {
 	f := cmd.Flags().Lookup(name)
 	if f == nil {
@@ -149,7 +159,7 @@ func mustGetBoolFlag(cmd *cobra.Command, name string) bool {
 	return f.Value.String() == "true"
 }
 
-func outputJSON(cmd *cobra.Command, notes []lunatask.Note) error {
+func outputJSONEnriched(cmd *cobra.Command, notes []enrichedNote) error {
 	enc := json.NewEncoder(cmd.OutOrStdout())
 	enc.SetIndent("", "  ")
 
@@ -160,7 +170,7 @@ func outputJSON(cmd *cobra.Command, notes []lunatask.Note) error {
 	return nil
 }
 
-func outputTable(cmd *cobra.Command, notes []lunatask.Note) error {
+func outputTableEnriched(cmd *cobra.Command, notes []enrichedNote) error {
 	cfg, _ := config.Load()
 	rows := make([][]string, 0, len(notes))
 
@@ -185,13 +195,16 @@ func outputTable(cmd *cobra.Command, notes []lunatask.Note) error {
 			pinned = "📌"
 		}
 
-		created := ui.FormatDate(note.CreatedAt)
+		name := note.Name
+		if name == "" {
+			name = note.ID[:8] + "..."
+		}
 
-		rows = append(rows, []string{note.ID, notebook, dateOn, pinned, created})
+		rows = append(rows, []string{name, notebook, dateOn, pinned})
 	}
 
 	tbl := table.New().
-		Headers("ID", "NOTEBOOK", "DATE", "📌", "CREATED").
+		Headers("NAME", "NOTEBOOK", "DATE", "📌").
 		Rows(rows...).
 		StyleFunc(func(row, col int) lipgloss.Style {
 			if row == table.HeaderRow {
+48 -13 cmd/task/list.go #
@@ -14,6 +14,7 @@ import (
 	"git.secluded.site/lune/internal/client"
 	"git.secluded.site/lune/internal/completion"
 	"git.secluded.site/lune/internal/config"
+	"git.secluded.site/lune/internal/db"
 	"git.secluded.site/lune/internal/ui"
 	"git.secluded.site/lune/internal/validate"
 	"github.com/charmbracelet/lipgloss"
@@ -21,6 +22,11 @@ import (
 	"github.com/spf13/cobra"
 )
 
+type enrichedTask struct {
+	lunatask.Task
+	Name string `json:"name,omitempty"`
+}
+
 // ErrUnknownArea indicates the specified area key was not found in config.
 var ErrUnknownArea = errors.New("unknown area key")
 
@@ -61,6 +67,16 @@ func runList(cmd *cobra.Command, _ []string) error {
 		return err
 	}
 
+	cfg, _ := config.Load()
+	enriched := make([]enrichedTask, 0, len(tasks))
+	for _, t := range tasks {
+		name := ""
+		if cfg != nil && cfg.Experimental.LocalDB {
+			name, _ = db.EnrichTask(t.ID)
+		}
+		enriched = append(enriched, enrichedTask{Task: t, Name: name})
+	}
+
 	areaID, err := resolveAreaFilter(cmd)
 	if err != nil {
 		return err
@@ -72,19 +88,19 @@ func runList(cmd *cobra.Command, _ []string) error {
 	}
 
 	showAll := mustGetBoolFlag(cmd, "all")
-	tasks = applyFilters(tasks, areaID, statusFilter, showAll)
+	filtered := applyFiltersEnriched(enriched, areaID, statusFilter, showAll)
 
-	if len(tasks) == 0 {
+	if len(filtered) == 0 {
 		fmt.Fprintln(cmd.OutOrStdout(), "No tasks found")
 
 		return nil
 	}
 
 	if mustGetBoolFlag(cmd, "json") {
-		return outputJSON(cmd, tasks)
+		return outputJSONEnriched(cmd, filtered)
 	}
 
-	return outputTable(cmd, tasks)
+	return outputTableEnriched(cmd, filtered)
 }
 
 // mustGetStringFlag returns the string flag value. Panics if flag doesn't exist
@@ -156,25 +172,41 @@ func resolveStatusFilter(cmd *cobra.Command) (string, error) {
 	return string(s), nil
 }
 
-func applyFilters(tasks []lunatask.Task, areaID, statusFilter string, showAll bool) []lunatask.Task {
+func applyFiltersEnriched(tasks []enrichedTask, areaID, statusFilter string, showAll bool) []enrichedTask {
+	// Extract basic tasks for library filter
+	baseTasks := make([]lunatask.Task, len(tasks))
+	for i, t := range tasks {
+		baseTasks[i] = t.Task
+	}
+
 	opts := &lunatask.TaskFilterOptions{
 		IncludeCompleted: showAll,
 		Today:            time.Now(),
 	}
-
 	if areaID != "" {
 		opts.AreaID = &areaID
 	}
-
 	if statusFilter != "" {
 		s := lunatask.TaskStatus(statusFilter)
 		opts.Status = &s
 	}
 
-	return lunatask.FilterTasks(tasks, opts)
+	filteredBase := lunatask.FilterTasks(baseTasks, opts)
+	
+	// Map back to enriched
+	res := make([]enrichedTask, 0, len(filteredBase))
+	for _, fb := range filteredBase {
+		for _, et := range tasks {
+			if et.ID == fb.ID {
+				res = append(res, et)
+				break
+			}
+		}
+	}
+	return res
 }
 
-func outputJSON(cmd *cobra.Command, tasks []lunatask.Task) error {
+func outputJSONEnriched(cmd *cobra.Command, tasks []enrichedTask) error {
 	enc := json.NewEncoder(cmd.OutOrStdout())
 	enc.SetIndent("", "  ")
 
@@ -185,7 +217,7 @@ func outputJSON(cmd *cobra.Command, tasks []lunatask.Task) error {
 	return nil
 }
 
-func outputTable(cmd *cobra.Command, tasks []lunatask.Task) error {
+func outputTableEnriched(cmd *cobra.Command, tasks []enrichedTask) error {
 	rows := make([][]string, 0, len(tasks))
 
 	for _, task := range tasks {
@@ -199,13 +231,16 @@ func outputTable(cmd *cobra.Command, tasks []lunatask.Task) error {
 			scheduled = ui.FormatDate(task.ScheduledOn.Time)
 		}
 
-		created := ui.FormatDate(task.CreatedAt)
+		name := task.Name
+		if name == "" {
+			name = task.ID[:8] + "..."
+		}
 
-		rows = append(rows, []string{task.ID, status, scheduled, created})
+		rows = append(rows, []string{name, status, scheduled})
 	}
 
 	tbl := table.New().
-		Headers("ID", "STATUS", "SCHEDULED", "CREATED").
+		Headers("NAME", "STATUS", "SCHEDULED").
 		Rows(rows...).
 		StyleFunc(func(row, col int) lipgloss.Style {
 			if row == table.HeaderRow {
+2 -0 go.mod #
@@ -47,6 +47,7 @@ require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
 	github.com/godbus/dbus/v5 v5.2.2 // indirect
+	github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
@@ -62,6 +63,7 @@ require (
 	github.com/muesli/termenv v0.16.0 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/syndtr/goleveldb v1.0.0 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	golang.org/x/oauth2 v0.34.0 // indirect
+17 -0 go.sum #
@@ -73,10 +73,14 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
 github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
 github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
@@ -85,6 +89,7 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
@@ -115,6 +120,9 @@ github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
 github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
 github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
 github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -129,6 +137,8 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
@@ -140,18 +150,25 @@ golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
 golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
 golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
 golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
 golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
 golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
 golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
 golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
 golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
 golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+59 -30 internal/client/client.go #
@@ -6,9 +6,10 @@
 package client
 
 import (
+	"encoding/json"
 	"errors"
-	"fmt"
 	"os"
+	"path/filepath"
 	"runtime/debug"
 
 	"git.secluded.site/go-lunatask"
@@ -39,58 +40,86 @@ func New() (*lunatask.Client, error) {
 }
 
 // GetToken returns the access token from LUNE_ACCESS_TOKEN environment variable
-// or keyring. Returns empty string and nil error if not found in either location;
-// returns error for keyring access problems. Environment variable takes precedence.
+// or keyring/secret store. Environment variable takes precedence.
 func GetToken() (string, error) {
-	// Env var takes precedence for explicit override
 	if token := os.Getenv("LUNE_ACCESS_TOKEN"); token != "" {
 		return token, nil
 	}
 
 	token, err := keyring.Get(keyringService, keyringUser)
-	if err != nil {
-		if errors.Is(err, keyring.ErrNotFound) {
-			return "", nil
-		}
-
-		return "", fmt.Errorf("accessing system keyring: %w", err)
+	if err == nil {
+		return token, nil
 	}
 
-	return token, nil
+	// Keyring failed, try SecretStore
+	store := NewSecretStore()
+	return store.Get(keyringUser)
 }
 
-// SetToken stores the access token in the system keyring.
+// SetToken stores the access token in the system keyring or SecretStore.
 func SetToken(token string) error {
-	if err := keyring.Set(keyringService, keyringUser, token); err != nil {
-		return fmt.Errorf("keyring set: %w", err)
+	err := keyring.Set(keyringService, keyringUser, token)
+	if err == nil {
+		return nil
 	}
 
-	return nil
+	// Keyring failed, save to SecretStore
+	store := NewSecretStore()
+	return store.Set(keyringUser, token)
 }
 
-// DeleteToken removes the access token from the system keyring.
-func DeleteToken() error {
-	if err := keyring.Delete(keyringService, keyringUser); err != nil {
-		return fmt.Errorf("keyring delete: %w", err)
+// SecretStore implementation moved to client for simplicity or kept in db package.
+// For now, I'll add a minimal version here or import it correctly.
+type SecretStore struct {
+	Path string
+}
+
+func NewSecretStore() *SecretStore {
+	home, _ := os.UserHomeDir()
+	return &SecretStore{
+		Path: filepath.Join(home, ".config", "lune", "secrets.json"),
 	}
+}
 
-	return nil
+func (s *SecretStore) Set(key, value string) error {
+	data := make(map[string]string)
+	if f, err := os.ReadFile(s.Path); err == nil {
+		json.Unmarshal(f, &data)
+	}
+	data[key] = value
+	f, _ := json.Marshal(data)
+	os.MkdirAll(filepath.Dir(s.Path), 0700)
+	return os.WriteFile(s.Path, f, 0600)
 }
 
-// HasKeyringToken checks if an access token is stored in the keyring.
-// Returns (true, nil) if found, (false, nil) if not found,
-// or (false, error) if there was a keyring access problem.
-func HasKeyringToken() (bool, error) {
-	_, err := keyring.Get(keyringService, keyringUser)
+func (s *SecretStore) Get(key string) (string, error) {
+	f, err := os.ReadFile(s.Path)
 	if err != nil {
-		if errors.Is(err, keyring.ErrNotFound) {
-			return false, nil
-		}
+		return "", nil
+	}
+	data := make(map[string]string)
+	json.Unmarshal(f, &data)
+	return data[key], nil
+}
+
+// DeleteToken removes the access token from the system keyring and SecretStore.
+func DeleteToken() error {
+	keyring.Delete(keyringService, keyringUser)
+	store := NewSecretStore()
+	return store.Set(keyringUser, "")
+}
 
-		return false, fmt.Errorf("accessing system keyring: %w", err)
+// HasKeyringToken checks if an access token is stored in the keyring or SecretStore.
+func HasKeyringToken() (bool, error) {
+	_, err := keyring.Get(keyringService, keyringUser)
+	if err == nil {
+		return true, nil
 	}
 
-	return true, nil
+	// Keyring failed or not found, try SecretStore
+	store := NewSecretStore()
+	token, _ := store.Get(keyringUser)
+	return token != "", nil
 }
 
 // version returns the module version from build info, or "dev" if unavailable.
+18 -6 internal/config/config.go #
@@ -20,12 +20,24 @@ var ErrNotFound = errors.New("config file not found")
 
 // Config represents the lune configuration file structure.
 type Config struct {
-	UI        UIConfig   `toml:"ui"`
-	Defaults  Defaults   `toml:"defaults"`
-	MCP       MCPConfig  `toml:"mcp"`
-	Areas     []Area     `toml:"areas"`
-	Notebooks []Notebook `toml:"notebooks"`
-	Habits    []Habit    `toml:"habits"`
+	UI           UIConfig           `toml:"ui"`
+	Defaults     Defaults           `toml:"defaults"`
+	MCP          MCPConfig          `toml:"mcp"`
+	Experimental ExperimentalConfig `toml:"experimental"`
+	Areas        []Area             `toml:"areas"`
+	Notebooks    []Notebook         `toml:"notebooks"`
+	Habits       []Habit            `toml:"habits"`
+}
+
+// ExperimentalConfig holds experimental features.
+type ExperimentalConfig struct {
+	LocalDB bool `toml:"local_db"`
+}
+
+// ApplyDefaults enables all tools if none are explicitly configured.
+func (c *Config) ApplyDefaults() {
+	c.MCP.MCPDefaults()
+	// Other defaults can go here
 }
 
 // MCPConfig holds MCP server settings.
Back to top