lune
created pr with
124.1
changed status to
closed
Closing as superceded by 126
cmds
checkout latest patchset:
ssh pr.pico.sh print 124 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 124.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 124set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 124set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 124
Patchset
124.1
→ listing real name of tasks
juan
construction of db for management of decrypted names of tasks
2026-04-21T03:21:16Zjuan
fix UTF8 chracteres
2026-04-21T05:43:09Zjuan
filter by status and coloring types of status
2026-04-21T21:13:24Zjuan
update help
2026-04-21T21:18:53Zjuan
list areas names
2026-04-21T21:21:36Zjuan
adjust table tasks to truncate name and list area
2026-04-21T21:44:53Zjuan
partial implementation of listing tasks by areas
2026-04-21T21:56:40Zjuan
final implementation of listing tasks by areas with UTF-8 chracteres
2026-04-21T22:05:20Zjuan
sort on task list to show first do now
2026-04-21T22:10:20Zjuan
update with 8 char of id and db.go
2026-04-21T22:33:29Zjuan
partial implementation of TUI menu on task update
2026-04-21T23:27:54Zjuan
task name correcion on TUI menu of Update task
2026-04-21T23:35:35Zjuan
TUI menu for update task with others flags
2026-04-21T23:38:19Zjuan
helper for TUI update task
2026-04-21T23:43:02Zjuan
search by first 8 char of note id for editing it
2026-04-21T23:49:56Zjuan
partial implementation on note TUI menu for update
2026-04-22T00:00:17Zjuan
TUI menu notebook just with tags
2026-04-22T00:05:46Zjuan
Note update now let you insert only the first 8 char of notebook id in update
2026-04-22T00:20:55Zjuan
implementation of (lune note notebook-list) for listing all notebooks and its ID
2026-04-22T00:25:53Zjuan
partial implementation of lune note show-content ID
2026-04-22T01:10:51Zjuan
convert \n in new line and \ in space for lune note show-content ID
2026-04-22T02:01:11Zjuan
2026-04-22T02:18:42Z
listing real name of tasks
juan
2026-04-21T03:21:16ZSemantic diff summary
14 added,
12 modified,
0 signature changed,
6 removed
across 6 analyzed files
(2 files skipped: unsupported file type)
cmd/task/list.go
-
chunklines 14-20modified -
type_declarationenrichedTaskadded -
function_declarationrunListmodified -
function_declarationapplyFiltersEnrichedadded -
function_declarationoutputJSONEnrichedadded -
function_declarationapplyFiltersremoved -
function_declarationoutputJSONremoved -
function_declarationoutputTableEnrichedadded -
function_declarationoutputTableremoved
internal/client/client.go
-
chunklines 6-15modified -
function_declarationDeleteTokenmodified -
function_declarationHasKeyringTokenmodified -
function_declarationGetTokenmodified -
function_declarationSetTokenmodified -
type_declarationSecretStoreadded -
function_declarationNewSecretStoreadded -
method_declarationSetadded -
method_declarationGetadded
+25
-0
cmd/init/apikey.go
#
| ... | ... | @@ -219,3 +220,27 @@ func validateTokenWithPing(token string) error { | |
| 219 | 220 | ||
| 220 | 221 | return err | |
| 221 | 222 | } | |
| 223 | + | ||
| 224 | + | func runMasterPasswordStep(cmd *cobra.Command) wizardNav { | |
| 225 | + | var password string | |
| 226 | + | err := huh.NewInput(). | |
| 227 | + | Title("Lunatask Master Password"). | |
| 228 | + | Description("Optional: provide your master password to enable local database decryption (stored securely)."). | |
| 229 | + | EchoMode(huh.EchoModePassword). | |
| 230 | + | Value(&password). | |
| 231 | + | Run() | |
| 232 | + | if err != nil { | |
| 233 | + | if errors.Is(err, huh.ErrUserAborted) { | |
| 234 | + | return navQuit | |
| 235 | + | } | |
| 236 | + | return navNext // Continue anyway | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | if password != "" { | |
| 240 | + | if err := db.SetMasterPassword(password); err != nil { | |
| 241 | + | fmt.Fprintln(cmd.OutOrStdout(), ui.Error.Render("Failed to save master password: "+err.Error())) | |
| 242 | + | } | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | return navNext | |
| 246 | + | } |
+3
-0
cmd/init/init.go
#
| ... | ... | @@ -120,6 +120,7 @@ func runFreshSetup(cmd *cobra.Command, cfg *config.Config) error { | |
| 120 | 120 | func() wizardNav { return runHabitsStep(cfg) }, | |
| 121 | 121 | func() wizardNav { return runDefaultsStep(cfg) }, | |
| 122 | 122 | func() wizardNav { return runAccessTokenStep(cmd) }, | |
| 123 | + | func() wizardNav { return runMasterPasswordStep(cmd) }, | |
| 123 | 124 | } | |
| 124 | 125 | ||
| 125 | 126 | step := 0 |
| ... | ... | @@ -173,6 +174,7 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error { | |
| 173 | 174 | "defaults": func() error { return configureDefaults(cfg) }, | |
| 174 | 175 | "ui": func() error { return configureUIPrefs(cfg) }, | |
| 175 | 176 | "apikey": func() error { return configureAccessToken(cmd) }, | |
| 177 | + | "password": func() error { runMasterPasswordStep(cmd); return nil }, | |
| 176 | 178 | "reset": func() error { return resetConfig(cmd, cfg) }, | |
| 177 | 179 | } | |
| 178 | 180 |
| ... | ... | @@ -188,6 +190,7 @@ func runReconfigure(cmd *cobra.Command, cfg *config.Config) error { | |
| 188 | 190 | huh.NewOption("Set defaults", "defaults"), | |
| 189 | 191 | huh.NewOption("UI preferences", "ui"), | |
| 190 | 192 | huh.NewOption("Access token", "apikey"), | |
| 193 | + | huh.NewOption("Master password (for LocalDB)", "password"), | |
| 191 | 194 | huh.NewOption("Reset all configuration", "reset"), | |
| 192 | 195 | huh.NewOption("Done", choiceDone), | |
| 193 | 196 | ). |
+34
-21
cmd/note/list.go
#
| ... | ... | @@ -13,12 +13,18 @@ import ( | |
| 13 | 13 | "git.secluded.site/lune/internal/client" | |
| 14 | 14 | "git.secluded.site/lune/internal/completion" | |
| 15 | 15 | "git.secluded.site/lune/internal/config" | |
| 16 | + | "git.secluded.site/lune/internal/db" | |
| 16 | 17 | "git.secluded.site/lune/internal/ui" | |
| 17 | 18 | "github.com/charmbracelet/lipgloss" | |
| 18 | 19 | "github.com/charmbracelet/lipgloss/table" | |
| 19 | 20 | "github.com/spf13/cobra" | |
| 20 | 21 | ) | |
| 21 | 22 | ||
| 23 | + | type enrichedNote struct { | |
| 24 | + | lunatask.Note | |
| 25 | + | Name string `json:"name,omitempty"` | |
| 26 | + | } | |
| 27 | + | ||
| 22 | 28 | // ListCmd lists notes. Exported for potential use by shortcuts. | |
| 23 | 29 | var ListCmd = &cobra.Command{ | |
| 24 | 30 | Use: "list", |
| ... | ... | @@ -54,26 +60,42 @@ func runList(cmd *cobra.Command, _ []string) error { | |
| 54 | 60 | return err | |
| 55 | 61 | } | |
| 56 | 62 | ||
| 63 | + | cfg, _ := config.Load() | |
| 64 | + | enriched := make([]enrichedNote, 0, len(notes)) | |
| 65 | + | for _, n := range notes { | |
| 66 | + | name := "" | |
| 67 | + | if cfg != nil && cfg.Experimental.LocalDB { | |
| 68 | + | name, _ = db.EnrichTask(n.ID) // EnrichTask is generic enough for IDs | |
| 69 | + | } | |
| 70 | + | enriched = append(enriched, enrichedNote{Note: n, Name: name}) | |
| 71 | + | } | |
| 72 | + | ||
| 57 | 73 | notebookID, err := resolveNotebookFilter(cmd) | |
| 58 | 74 | if err != nil { | |
| 59 | 75 | return err | |
| 60 | 76 | } | |
| 61 | 77 | ||
| 78 | + | filtered := enriched | |
| 62 | 79 | if notebookID != "" { | |
| 63 | - | notes = filterByNotebook(notes, notebookID) | |
| 80 | + | filtered = make([]enrichedNote, 0) | |
| 81 | + | for _, en := range enriched { | |
| 82 | + | if en.NotebookID != nil && *en.NotebookID == notebookID { | |
| 83 | + | filtered = append(filtered, en) | |
| 84 | + | } | |
| 85 | + | } | |
| 64 | 86 | } | |
| 65 | 87 | ||
| 66 | - | if len(notes) == 0 { | |
| 88 | + | if len(filtered) == 0 { | |
| 67 | 89 | fmt.Fprintln(cmd.OutOrStdout(), "No notes found") | |
| 68 | 90 | ||
| 69 | 91 | return nil | |
| 70 | 92 | } | |
| 71 | 93 | ||
| 72 | 94 | if mustGetBoolFlag(cmd, "json") { | |
| 73 | - | return outputJSON(cmd, notes) | |
| 95 | + | return outputJSONEnriched(cmd, filtered) | |
| 74 | 96 | } | |
| 75 | 97 | ||
| 76 | - | return outputTable(cmd, notes) | |
| 98 | + | return outputTableEnriched(cmd, filtered) | |
| 77 | 99 | } | |
| 78 | 100 | ||
| 79 | 101 | func buildListOptions(cmd *cobra.Command) *lunatask.ListNotesOptions { |
| ... | ... | @@ -119,18 +141,6 @@ func resolveNotebookFilter(cmd *cobra.Command) (string, error) { | |
| 119 | 141 | return notebook.ID, nil | |
| 120 | 142 | } | |
| 121 | 143 | ||
| 122 | - | func filterByNotebook(notes []lunatask.Note, notebookID string) []lunatask.Note { | |
| 123 | - | filtered := make([]lunatask.Note, 0, len(notes)) | |
| 124 | - | ||
| 125 | - | for _, note := range notes { | |
| 126 | - | if note.NotebookID != nil && *note.NotebookID == notebookID { | |
| 127 | - | filtered = append(filtered, note) | |
| 128 | - | } | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | return filtered | |
| 132 | - | } | |
| 133 | - | ||
| 134 | 144 | func mustGetStringFlag(cmd *cobra.Command, name string) string { | |
| 135 | 145 | f := cmd.Flags().Lookup(name) | |
| 136 | 146 | if f == nil { |
| ... | ... | @@ -149,7 +159,7 @@ func mustGetBoolFlag(cmd *cobra.Command, name string) bool { | |
| 149 | 159 | return f.Value.String() == "true" | |
| 150 | 160 | } | |
| 151 | 161 | ||
| 152 | - | func outputJSON(cmd *cobra.Command, notes []lunatask.Note) error { | |
| 162 | + | func outputJSONEnriched(cmd *cobra.Command, notes []enrichedNote) error { | |
| 153 | 163 | enc := json.NewEncoder(cmd.OutOrStdout()) | |
| 154 | 164 | enc.SetIndent("", " ") | |
| 155 | 165 |
| ... | ... | @@ -160,7 +170,7 @@ func outputJSON(cmd *cobra.Command, notes []lunatask.Note) error { | |
| 160 | 170 | return nil | |
| 161 | 171 | } | |
| 162 | 172 | ||
| 163 | - | func outputTable(cmd *cobra.Command, notes []lunatask.Note) error { | |
| 173 | + | func outputTableEnriched(cmd *cobra.Command, notes []enrichedNote) error { | |
| 164 | 174 | cfg, _ := config.Load() | |
| 165 | 175 | rows := make([][]string, 0, len(notes)) | |
| 166 | 176 |
| ... | ... | @@ -185,13 +195,16 @@ func outputTable(cmd *cobra.Command, notes []lunatask.Note) error { | |
| 185 | 195 | pinned = "📌" | |
| 186 | 196 | } | |
| 187 | 197 | ||
| 188 | - | created := ui.FormatDate(note.CreatedAt) | |
| 198 | + | name := note.Name | |
| 199 | + | if name == "" { | |
| 200 | + | name = note.ID[:8] + "..." | |
| 201 | + | } | |
| 189 | 202 | ||
| 190 | - | rows = append(rows, []string{note.ID, notebook, dateOn, pinned, created}) | |
| 203 | + | rows = append(rows, []string{name, notebook, dateOn, pinned}) | |
| 191 | 204 | } | |
| 192 | 205 | ||
| 193 | 206 | tbl := table.New(). | |
| 194 | - | Headers("ID", "NOTEBOOK", "DATE", "📌", "CREATED"). | |
| 207 | + | Headers("NAME", "NOTEBOOK", "DATE", "📌"). | |
| 195 | 208 | Rows(rows...). | |
| 196 | 209 | StyleFunc(func(row, col int) lipgloss.Style { | |
| 197 | 210 | if row == table.HeaderRow { |
+48
-13
cmd/task/list.go
#
| ... | ... | @@ -14,6 +14,7 @@ import ( | |
| 14 | 14 | "git.secluded.site/lune/internal/client" | |
| 15 | 15 | "git.secluded.site/lune/internal/completion" | |
| 16 | 16 | "git.secluded.site/lune/internal/config" | |
| 17 | + | "git.secluded.site/lune/internal/db" | |
| 17 | 18 | "git.secluded.site/lune/internal/ui" | |
| 18 | 19 | "git.secluded.site/lune/internal/validate" | |
| 19 | 20 | "github.com/charmbracelet/lipgloss" |
| ... | ... | @@ -21,6 +22,11 @@ import ( | |
| 21 | 22 | "github.com/spf13/cobra" | |
| 22 | 23 | ) | |
| 23 | 24 | ||
| 25 | + | type enrichedTask struct { | |
| 26 | + | lunatask.Task | |
| 27 | + | Name string `json:"name,omitempty"` | |
| 28 | + | } | |
| 29 | + | ||
| 24 | 30 | // ErrUnknownArea indicates the specified area key was not found in config. | |
| 25 | 31 | var ErrUnknownArea = errors.New("unknown area key") | |
| 26 | 32 |
| ... | ... | @@ -61,6 +67,16 @@ func runList(cmd *cobra.Command, _ []string) error { | |
| 61 | 67 | return err | |
| 62 | 68 | } | |
| 63 | 69 | ||
| 70 | + | cfg, _ := config.Load() | |
| 71 | + | enriched := make([]enrichedTask, 0, len(tasks)) | |
| 72 | + | for _, t := range tasks { | |
| 73 | + | name := "" | |
| 74 | + | if cfg != nil && cfg.Experimental.LocalDB { | |
| 75 | + | name, _ = db.EnrichTask(t.ID) | |
| 76 | + | } | |
| 77 | + | enriched = append(enriched, enrichedTask{Task: t, Name: name}) | |
| 78 | + | } | |
| 79 | + | ||
| 64 | 80 | areaID, err := resolveAreaFilter(cmd) | |
| 65 | 81 | if err != nil { | |
| 66 | 82 | return err |
| ... | ... | @@ -72,19 +88,19 @@ func runList(cmd *cobra.Command, _ []string) error { | |
| 72 | 88 | } | |
| 73 | 89 | ||
| 74 | 90 | showAll := mustGetBoolFlag(cmd, "all") | |
| 75 | - | tasks = applyFilters(tasks, areaID, statusFilter, showAll) | |
| 91 | + | filtered := applyFiltersEnriched(enriched, areaID, statusFilter, showAll) | |
| 76 | 92 | ||
| 77 | - | if len(tasks) == 0 { | |
| 93 | + | if len(filtered) == 0 { | |
| 78 | 94 | fmt.Fprintln(cmd.OutOrStdout(), "No tasks found") | |
| 79 | 95 | ||
| 80 | 96 | return nil | |
| 81 | 97 | } | |
| 82 | 98 | ||
| 83 | 99 | if mustGetBoolFlag(cmd, "json") { | |
| 84 | - | return outputJSON(cmd, tasks) | |
| 100 | + | return outputJSONEnriched(cmd, filtered) | |
| 85 | 101 | } | |
| 86 | 102 | ||
| 87 | - | return outputTable(cmd, tasks) | |
| 103 | + | return outputTableEnriched(cmd, filtered) | |
| 88 | 104 | } | |
| 89 | 105 | ||
| 90 | 106 | // mustGetStringFlag returns the string flag value. Panics if flag doesn't exist |
| ... | ... | @@ -156,25 +172,41 @@ func resolveStatusFilter(cmd *cobra.Command) (string, error) { | |
| 156 | 172 | return string(s), nil | |
| 157 | 173 | } | |
| 158 | 174 | ||
| 159 | - | func applyFilters(tasks []lunatask.Task, areaID, statusFilter string, showAll bool) []lunatask.Task { | |
| 175 | + | func applyFiltersEnriched(tasks []enrichedTask, areaID, statusFilter string, showAll bool) []enrichedTask { | |
| 176 | + | // Extract basic tasks for library filter | |
| 177 | + | baseTasks := make([]lunatask.Task, len(tasks)) | |
| 178 | + | for i, t := range tasks { | |
| 179 | + | baseTasks[i] = t.Task | |
| 180 | + | } | |
| 181 | + | ||
| 160 | 182 | opts := &lunatask.TaskFilterOptions{ | |
| 161 | 183 | IncludeCompleted: showAll, | |
| 162 | 184 | Today: time.Now(), | |
| 163 | 185 | } | |
| 164 | - | ||
| 165 | 186 | if areaID != "" { | |
| 166 | 187 | opts.AreaID = &areaID | |
| 167 | 188 | } | |
| 168 | - | ||
| 169 | 189 | if statusFilter != "" { | |
| 170 | 190 | s := lunatask.TaskStatus(statusFilter) | |
| 171 | 191 | opts.Status = &s | |
| 172 | 192 | } | |
| 173 | 193 | ||
| 174 | - | return lunatask.FilterTasks(tasks, opts) | |
| 194 | + | filteredBase := lunatask.FilterTasks(baseTasks, opts) | |
| 195 | + | ||
| 196 | + | // Map back to enriched | |
| 197 | + | res := make([]enrichedTask, 0, len(filteredBase)) | |
| 198 | + | for _, fb := range filteredBase { | |
| 199 | + | for _, et := range tasks { | |
| 200 | + | if et.ID == fb.ID { | |
| 201 | + | res = append(res, et) | |
| 202 | + | break | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | return res | |
| 175 | 207 | } | |
| 176 | 208 | ||
| 177 | - | func outputJSON(cmd *cobra.Command, tasks []lunatask.Task) error { | |
| 209 | + | func outputJSONEnriched(cmd *cobra.Command, tasks []enrichedTask) error { | |
| 178 | 210 | enc := json.NewEncoder(cmd.OutOrStdout()) | |
| 179 | 211 | enc.SetIndent("", " ") | |
| 180 | 212 |
| ... | ... | @@ -185,7 +217,7 @@ func outputJSON(cmd *cobra.Command, tasks []lunatask.Task) error { | |
| 185 | 217 | return nil | |
| 186 | 218 | } | |
| 187 | 219 | ||
| 188 | - | func outputTable(cmd *cobra.Command, tasks []lunatask.Task) error { | |
| 220 | + | func outputTableEnriched(cmd *cobra.Command, tasks []enrichedTask) error { | |
| 189 | 221 | rows := make([][]string, 0, len(tasks)) | |
| 190 | 222 | ||
| 191 | 223 | for _, task := range tasks { |
| ... | ... | @@ -199,13 +231,16 @@ func outputTable(cmd *cobra.Command, tasks []lunatask.Task) error { | |
| 199 | 231 | scheduled = ui.FormatDate(task.ScheduledOn.Time) | |
| 200 | 232 | } | |
| 201 | 233 | ||
| 202 | - | created := ui.FormatDate(task.CreatedAt) | |
| 234 | + | name := task.Name | |
| 235 | + | if name == "" { | |
| 236 | + | name = task.ID[:8] + "..." | |
| 237 | + | } | |
| 203 | 238 | ||
| 204 | - | rows = append(rows, []string{task.ID, status, scheduled, created}) | |
| 239 | + | rows = append(rows, []string{name, status, scheduled}) | |
| 205 | 240 | } | |
| 206 | 241 | ||
| 207 | 242 | tbl := table.New(). | |
| 208 | - | Headers("ID", "STATUS", "SCHEDULED", "CREATED"). | |
| 243 | + | Headers("NAME", "STATUS", "SCHEDULED"). | |
| 209 | 244 | Rows(rows...). | |
| 210 | 245 | StyleFunc(func(row, col int) lipgloss.Style { | |
| 211 | 246 | if row == table.HeaderRow { |
+2
-0
go.mod
#
| ... | ... | @@ -47,6 +47,7 @@ require ( | |
| 47 | 47 | github.com/dustin/go-humanize v1.0.1 // indirect | |
| 48 | 48 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | |
| 49 | 49 | github.com/godbus/dbus/v5 v5.2.2 // indirect | |
| 50 | + | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect | |
| 50 | 51 | github.com/google/uuid v1.6.0 // indirect | |
| 51 | 52 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
| 52 | 53 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect |
| ... | ... | @@ -62,6 +63,7 @@ require ( | |
| 62 | 63 | github.com/muesli/termenv v0.16.0 // indirect | |
| 63 | 64 | github.com/rivo/uniseg v0.4.7 // indirect | |
| 64 | 65 | github.com/spf13/pflag v1.0.10 // indirect | |
| 66 | + | github.com/syndtr/goleveldb v1.0.0 // indirect | |
| 65 | 67 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect | |
| 66 | 68 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect | |
| 67 | 69 | 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 | |
| 73 | 73 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | |
| 74 | 74 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= | |
| 75 | 75 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | |
| 76 | + | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | |
| 76 | 77 | github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= | |
| 77 | 78 | github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= | |
| 78 | 79 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= | |
| 79 | 80 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | |
| 81 | + | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | |
| 82 | + | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= | |
| 83 | + | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | |
| 80 | 84 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | |
| 81 | 85 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | |
| 82 | 86 | 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 | |
| 85 | 89 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= | |
| 86 | 90 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | |
| 87 | 91 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | |
| 92 | + | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | |
| 88 | 93 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | |
| 89 | 94 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | |
| 90 | 95 | github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70= |
| ... | ... | @@ -115,6 +120,9 @@ github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= | |
| 115 | 120 | github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= | |
| 116 | 121 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= | |
| 117 | 122 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= | |
| 123 | + | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | |
| 124 | + | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | |
| 125 | + | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | |
| 118 | 126 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |
| 119 | 127 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |
| 120 | 128 | 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= | |
| 129 | 137 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | |
| 130 | 138 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= | |
| 131 | 139 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= | |
| 140 | + | github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= | |
| 141 | + | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= | |
| 132 | 142 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | |
| 133 | 143 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | |
| 134 | 144 | 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= | |
| 140 | 150 | golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= | |
| 141 | 151 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | |
| 142 | 152 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | |
| 153 | + | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | |
| 143 | 154 | golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= | |
| 144 | 155 | golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= | |
| 156 | + | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |
| 145 | 157 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= | |
| 146 | 158 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= | |
| 159 | + | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |
| 147 | 160 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 148 | 161 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |
| 149 | 162 | golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= | |
| 150 | 163 | golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | |
| 164 | + | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |
| 151 | 165 | golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= | |
| 152 | 166 | golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= | |
| 153 | 167 | golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= | |
| 154 | 168 | golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= | |
| 155 | 169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |
| 170 | + | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | |
| 171 | + | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | |
| 172 | + | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |
| 156 | 173 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |
| 157 | 174 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
+59
-30
internal/client/client.go
#
| ... | ... | @@ -39,58 +40,86 @@ func New() (*lunatask.Client, error) { | |
| 39 | 40 | } | |
| 40 | 41 | ||
| 41 | 42 | // GetToken returns the access token from LUNE_ACCESS_TOKEN environment variable | |
| 42 | - | // or keyring. Returns empty string and nil error if not found in either location; | |
| 43 | - | // returns error for keyring access problems. Environment variable takes precedence. | |
| 43 | + | // or keyring/secret store. Environment variable takes precedence. | |
| 44 | 44 | func GetToken() (string, error) { | |
| 45 | - | // Env var takes precedence for explicit override | |
| 46 | 45 | if token := os.Getenv("LUNE_ACCESS_TOKEN"); token != "" { | |
| 47 | 46 | return token, nil | |
| 48 | 47 | } | |
| 49 | 48 | ||
| 50 | 49 | token, err := keyring.Get(keyringService, keyringUser) | |
| 51 | - | if err != nil { | |
| 52 | - | if errors.Is(err, keyring.ErrNotFound) { | |
| 53 | - | return "", nil | |
| 54 | - | } | |
| 55 | - | ||
| 56 | - | return "", fmt.Errorf("accessing system keyring: %w", err) | |
| 50 | + | if err == nil { | |
| 51 | + | return token, nil | |
| 57 | 52 | } | |
| 58 | 53 | ||
| 59 | - | return token, nil | |
| 54 | + | // Keyring failed, try SecretStore | |
| 55 | + | store := NewSecretStore() | |
| 56 | + | return store.Get(keyringUser) | |
| 60 | 57 | } | |
| 61 | 58 | ||
| 62 | - | // SetToken stores the access token in the system keyring. | |
| 59 | + | // SetToken stores the access token in the system keyring or SecretStore. | |
| 63 | 60 | func SetToken(token string) error { | |
| 64 | - | if err := keyring.Set(keyringService, keyringUser, token); err != nil { | |
| 65 | - | return fmt.Errorf("keyring set: %w", err) | |
| 61 | + | err := keyring.Set(keyringService, keyringUser, token) | |
| 62 | + | if err == nil { | |
| 63 | + | return nil | |
| 66 | 64 | } | |
| 67 | 65 | ||
| 68 | - | return nil | |
| 66 | + | // Keyring failed, save to SecretStore | |
| 67 | + | store := NewSecretStore() | |
| 68 | + | return store.Set(keyringUser, token) | |
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | - | // DeleteToken removes the access token from the system keyring. | |
| 72 | - | func DeleteToken() error { | |
| 73 | - | if err := keyring.Delete(keyringService, keyringUser); err != nil { | |
| 74 | - | return fmt.Errorf("keyring delete: %w", err) | |
| 71 | + | // SecretStore implementation moved to client for simplicity or kept in db package. | |
| 72 | + | // For now, I'll add a minimal version here or import it correctly. | |
| 73 | + | type SecretStore struct { | |
| 74 | + | Path string | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func NewSecretStore() *SecretStore { | |
| 78 | + | home, _ := os.UserHomeDir() | |
| 79 | + | return &SecretStore{ | |
| 80 | + | Path: filepath.Join(home, ".config", "lune", "secrets.json"), | |
| 75 | 81 | } | |
| 82 | + | } | |
| 76 | 83 | ||
| 77 | - | return nil | |
| 84 | + | func (s *SecretStore) Set(key, value string) error { | |
| 85 | + | data := make(map[string]string) | |
| 86 | + | if f, err := os.ReadFile(s.Path); err == nil { | |
| 87 | + | json.Unmarshal(f, &data) | |
| 88 | + | } | |
| 89 | + | data[key] = value | |
| 90 | + | f, _ := json.Marshal(data) | |
| 91 | + | os.MkdirAll(filepath.Dir(s.Path), 0700) | |
| 92 | + | return os.WriteFile(s.Path, f, 0600) | |
| 78 | 93 | } | |
| 79 | 94 | ||
| 80 | - | // HasKeyringToken checks if an access token is stored in the keyring. | |
| 81 | - | // Returns (true, nil) if found, (false, nil) if not found, | |
| 82 | - | // or (false, error) if there was a keyring access problem. | |
| 83 | - | func HasKeyringToken() (bool, error) { | |
| 84 | - | _, err := keyring.Get(keyringService, keyringUser) | |
| 95 | + | func (s *SecretStore) Get(key string) (string, error) { | |
| 96 | + | f, err := os.ReadFile(s.Path) | |
| 85 | 97 | if err != nil { | |
| 86 | - | if errors.Is(err, keyring.ErrNotFound) { | |
| 87 | - | return false, nil | |
| 88 | - | } | |
| 98 | + | return "", nil | |
| 99 | + | } | |
| 100 | + | data := make(map[string]string) | |
| 101 | + | json.Unmarshal(f, &data) | |
| 102 | + | return data[key], nil | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // DeleteToken removes the access token from the system keyring and SecretStore. | |
| 106 | + | func DeleteToken() error { | |
| 107 | + | keyring.Delete(keyringService, keyringUser) | |
| 108 | + | store := NewSecretStore() | |
| 109 | + | return store.Set(keyringUser, "") | |
| 110 | + | } | |
| 89 | 111 | ||
| 90 | - | return false, fmt.Errorf("accessing system keyring: %w", err) | |
| 112 | + | // HasKeyringToken checks if an access token is stored in the keyring or SecretStore. | |
| 113 | + | func HasKeyringToken() (bool, error) { | |
| 114 | + | _, err := keyring.Get(keyringService, keyringUser) | |
| 115 | + | if err == nil { | |
| 116 | + | return true, nil | |
| 91 | 117 | } | |
| 92 | 118 | ||
| 93 | - | return true, nil | |
| 119 | + | // Keyring failed or not found, try SecretStore | |
| 120 | + | store := NewSecretStore() | |
| 121 | + | token, _ := store.Get(keyringUser) | |
| 122 | + | return token != "", nil | |
| 94 | 123 | } | |
| 95 | 124 | ||
| 96 | 125 | // 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") | |
| 20 | 20 | ||
| 21 | 21 | // Config represents the lune configuration file structure. | |
| 22 | 22 | type Config struct { | |
| 23 | - | UI UIConfig `toml:"ui"` | |
| 24 | - | Defaults Defaults `toml:"defaults"` | |
| 25 | - | MCP MCPConfig `toml:"mcp"` | |
| 26 | - | Areas []Area `toml:"areas"` | |
| 27 | - | Notebooks []Notebook `toml:"notebooks"` | |
| 28 | - | Habits []Habit `toml:"habits"` | |
| 23 | + | UI UIConfig `toml:"ui"` | |
| 24 | + | Defaults Defaults `toml:"defaults"` | |
| 25 | + | MCP MCPConfig `toml:"mcp"` | |
| 26 | + | Experimental ExperimentalConfig `toml:"experimental"` | |
| 27 | + | Areas []Area `toml:"areas"` | |
| 28 | + | Notebooks []Notebook `toml:"notebooks"` | |
| 29 | + | Habits []Habit `toml:"habits"` | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // ExperimentalConfig holds experimental features. | |
| 33 | + | type ExperimentalConfig struct { | |
| 34 | + | LocalDB bool `toml:"local_db"` | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // ApplyDefaults enables all tools if none are explicitly configured. | |
| 38 | + | func (c *Config) ApplyDefaults() { | |
| 39 | + | c.MCP.MCPDefaults() | |
| 40 | + | // Other defaults can go here | |
| 29 | 41 | } | |
| 30 | 42 | ||
| 31 | 43 | // MCPConfig holds MCP server settings. |