···
3
+
(** List bookmarks command *)
4
+
let list_bookmarks env _xdg profile base_url limit archived favourited tags =
5
+
Eio.Switch.run @@ fun sw ->
7
+
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
12
+
Keyeio.Profile.get profile ~key:"base_url"
13
+
|> Option.value ~default:"https://hoard.recoil.org"
16
+
Printf.printf "Fetching bookmarks from %s...\n" base_url;
19
+
let bookmarks = Karakeepe.fetch_all_bookmarks
22
+
~include_content:true
23
+
?filter_tags:(if tags = [] then None else Some tags)
27
+
(* Filter by archived/favourited if requested *)
29
+
List.filter (fun (b : Karakeepe.bookmark) ->
30
+
(match archived with
31
+
| Some true -> b.archived
32
+
| Some false -> not b.archived
34
+
(match favourited with
35
+
| Some true -> b.favourited
36
+
| Some false -> not b.favourited
41
+
Printf.printf "Found %d bookmarks\n\n" (List.length bookmarks);
43
+
List.iteri (fun i (b : Karakeepe.bookmark) ->
44
+
Printf.printf "%d. %s\n" (i + 1) b.url;
46
+
| Some title -> Printf.printf " Title: %s\n" title
48
+
Printf.printf " ID: %s\n" b.id;
49
+
Printf.printf " Created: %s\n" (Ptime.to_rfc3339 b.created_at);
50
+
if b.tags <> [] then
51
+
Printf.printf " Tags: %s\n" (String.concat ", " b.tags);
52
+
if b.archived then Printf.printf " [ARCHIVED]\n";
53
+
if b.favourited then Printf.printf " [FAVOURITED]\n";
54
+
(match b.summary with
55
+
| Some s when s <> "" ->
56
+
let summary = if String.length s > 100 then String.sub s 0 100 ^ "..." else s in
57
+
Printf.printf " Summary: %s\n" summary
63
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
66
+
(** Get a single bookmark by ID *)
67
+
let get_bookmark env _xdg profile base_url bookmark_id =
68
+
Eio.Switch.run @@ fun sw ->
70
+
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
75
+
Keyeio.Profile.get profile ~key:"base_url"
76
+
|> Option.value ~default:"https://hoard.recoil.org"
80
+
let bookmark = Karakeepe.fetch_bookmark_details ~sw ~env ~api_key base_url bookmark_id in
82
+
Printf.printf "Bookmark: %s\n" bookmark.url;
83
+
Printf.printf "ID: %s\n" bookmark.id;
84
+
(match bookmark.title with
85
+
| Some title -> Printf.printf "Title: %s\n" title
87
+
(match bookmark.note with
88
+
| Some note -> Printf.printf "Note: %s\n" note
90
+
Printf.printf "Created: %s\n" (Ptime.to_rfc3339 bookmark.created_at);
91
+
(match bookmark.updated_at with
92
+
| Some t -> Printf.printf "Updated: %s\n" (Ptime.to_rfc3339 t)
95
+
if bookmark.tags <> [] then
96
+
Printf.printf "Tags: %s\n" (String.concat ", " bookmark.tags);
98
+
if bookmark.archived then Printf.printf "Status: ARCHIVED\n";
99
+
if bookmark.favourited then Printf.printf "Status: FAVOURITED\n";
101
+
(match bookmark.summary with
102
+
| Some s when s <> "" -> Printf.printf "\nSummary:\n%s\n" s
105
+
if bookmark.content <> [] then begin
106
+
Printf.printf "\nContent metadata:\n";
107
+
List.iter (fun (k, v) ->
108
+
if v <> "null" && v <> "" then
109
+
Printf.printf " %s: %s\n" k v
113
+
if bookmark.assets <> [] then begin
114
+
Printf.printf "\nAssets:\n";
115
+
List.iter (fun (id, asset_type) ->
116
+
Printf.printf " %s (%s)\n" id asset_type;
117
+
Printf.printf " URL: %s\n" (Karakeepe.get_asset_url base_url id)
123
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
126
+
(** Create a new bookmark *)
127
+
let create_bookmark env _xdg profile base_url url title note tags archived favourited =
128
+
Eio.Switch.run @@ fun sw ->
130
+
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
132
+
match base_url with
135
+
Keyeio.Profile.get profile ~key:"base_url"
136
+
|> Option.value ~default:"https://hoard.recoil.org"
140
+
Printf.printf "Creating bookmark: %s\n" url;
142
+
let tags_opt = if tags = [] then None else Some tags in
144
+
let bookmark = Karakeepe.create_bookmark
155
+
Printf.printf "โ Bookmark created successfully!\n";
156
+
Printf.printf "ID: %s\n" bookmark.id;
157
+
Printf.printf "URL: %s\n" bookmark.url;
158
+
(match bookmark.title with
159
+
| Some t -> Printf.printf "Title: %s\n" t
161
+
if bookmark.tags <> [] then
162
+
Printf.printf "Tags: %s\n" (String.concat ", " bookmark.tags);
165
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
168
+
(** Search bookmarks by tag *)
169
+
let search_bookmarks env __xdg profile base_url tags limit =
170
+
Eio.Switch.run @@ fun sw ->
172
+
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
174
+
match base_url with
177
+
Keyeio.Profile.get profile ~key:"base_url"
178
+
|> Option.value ~default:"https://hoard.recoil.org"
181
+
if tags = [] then begin
182
+
Printf.eprintf "Error: At least one tag is required for search\n";
186
+
Printf.printf "Searching for bookmarks with tags: %s\n" (String.concat ", " tags);
188
+
let bookmarks = Karakeepe.fetch_all_bookmarks
192
+
~include_content:true
196
+
Printf.printf "Found %d bookmarks\n\n" (List.length bookmarks);
198
+
List.iteri (fun i (b : Karakeepe.bookmark) ->
199
+
Printf.printf "%d. %s\n" (i + 1) b.url;
200
+
(match b.title with
201
+
| Some title -> Printf.printf " Title: %s\n" title
203
+
Printf.printf " ID: %s\n" b.id;
204
+
Printf.printf " Tags: %s\n" (String.concat ", " b.tags);
209
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
213
+
(** Command-line arguments *)
216
+
let doc = "Override Karakeep instance base URL" in
217
+
Arg.(value & opt (some string) None & info ["url"; "u"] ~docv:"URL" ~doc)
220
+
let doc = "Maximum number of bookmarks to fetch" in
221
+
Arg.(value & opt int 50 & info ["limit"; "l"] ~docv:"NUM" ~doc)
224
+
let doc = "Filter by archived status (true/false)" in
225
+
Arg.(value & opt (some bool) None & info ["archived"; "a"] ~docv:"BOOL" ~doc)
227
+
let favourited_arg =
228
+
let doc = "Filter by favourited status (true/false)" in
229
+
Arg.(value & opt (some bool) None & info ["favourited"; "f"] ~docv:"BOOL" ~doc)
232
+
let doc = "Filter by tags (comma-separated)" in
233
+
Arg.(value & opt (list ~sep:',' string) [] & info ["tags"; "t"] ~docv:"TAGS" ~doc)
235
+
let bookmark_id_arg =
236
+
let doc = "Bookmark ID" in
237
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"ID" ~doc)
240
+
let doc = "URL to bookmark" in
241
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"URL" ~doc)
244
+
let doc = "Bookmark title" in
245
+
Arg.(value & opt (some string) None & info ["title"] ~docv:"TITLE" ~doc)
248
+
let doc = "Bookmark note/description" in
249
+
Arg.(value & opt (some string) None & info ["note"; "n"] ~docv:"NOTE" ~doc)
251
+
let create_tags_arg =
252
+
let doc = "Tags to add (comma-separated)" in
253
+
Arg.(value & opt (list ~sep:',' string) [] & info ["tags"; "t"] ~docv:"TAGS" ~doc)
255
+
let create_archived_flag =
256
+
let doc = "Mark bookmark as archived" in
257
+
Arg.(value & flag & info ["archived"; "a"] ~doc)
259
+
let create_favourited_flag =
260
+
let doc = "Mark bookmark as favourited" in
261
+
Arg.(value & flag & info ["favourited"; "f"] ~doc)
263
+
(** Main commands *)
265
+
(* Init command - doesn't use eiocmd since it's creating the profile *)
267
+
let doc = "Initialize karakeepe credentials" in
269
+
`S Manpage.s_description;
270
+
`P "Create a new credentials file for karakeepe in the XDG config directory.";
271
+
`P "This will create ~/.config/karakeepe/keys/karakeepe.toml with the specified profile.";
272
+
`S Manpage.s_examples;
273
+
`P "Initialize with prompts:";
274
+
`Pre " karakeepe init";
275
+
`P "Initialize with command-line arguments:";
276
+
`Pre " karakeepe init --api-key \"ak1_xxx\" --base-url \"https://hoard.example.com\"";
277
+
`P "Initialize a production profile:";
278
+
`Pre " karakeepe init --profile production --api-key \"ak1_xxx\"";
280
+
let default_data = [
281
+
("api_key", None); (* Will prompt if not provided *)
282
+
("base_url", Some "https://hoard.recoil.org") (* Has default *)
285
+
Eio_main.run @@ fun env ->
286
+
Keyeio.Cmd.create_term
287
+
~app_name:"karakeepe"
289
+
~service:"karakeepe"
293
+
let info = Cmd.info "init" ~doc ~man in
294
+
Cmd.v info init_term
298
+
let doc = "List bookmarks" in
300
+
~info:(Cmd.info "list" ~doc)
301
+
~app_name:"karakeepe"
302
+
~service:"karakeepe"
303
+
Term.(const (fun base_url limit archived favourited tags env xdg profile ->
304
+
list_bookmarks env xdg profile base_url limit archived favourited tags)
305
+
$ base_url_arg $ limit_arg $ archived_arg $ favourited_arg $ tags_arg)
309
+
let doc = "Get a single bookmark by ID" in
311
+
~info:(Cmd.info "get" ~doc)
312
+
~app_name:"karakeepe"
313
+
~service:"karakeepe"
314
+
Term.(const (fun base_url bookmark_id env xdg profile ->
315
+
get_bookmark env xdg profile base_url bookmark_id)
316
+
$ base_url_arg $ bookmark_id_arg)
318
+
(* Create command *)
320
+
let doc = "Create a new bookmark" in
322
+
`S Manpage.s_description;
323
+
`P "Create a new bookmark in Karakeep.";
324
+
`S Manpage.s_examples;
325
+
`P "Create a simple bookmark:";
326
+
`Pre " karakeepe create \"https://example.com\"";
327
+
`P "Create with title and tags:";
328
+
`Pre " karakeepe create \"https://example.com\" --title \"Example Site\" --tags \"web,example\"";
329
+
`P "Create as favourited:";
330
+
`Pre " karakeepe create \"https://example.com\" --favourited";
333
+
~info:(Cmd.info "create" ~doc ~man)
334
+
~app_name:"karakeepe"
335
+
~service:"karakeepe"
336
+
Term.(const (fun base_url url title note tags archived favourited env xdg profile ->
337
+
create_bookmark env xdg profile base_url url title note tags archived favourited)
338
+
$ base_url_arg $ url_arg $ title_arg $ note_arg $ create_tags_arg $ create_archived_flag $ create_favourited_flag)
340
+
(* Search command *)
342
+
let doc = "Search bookmarks by tags" in
344
+
`S Manpage.s_description;
345
+
`P "Search for bookmarks by tags.";
346
+
`S Manpage.s_examples;
347
+
`P "Search for bookmarks with tag 'work':";
348
+
`Pre " karakeepe search --tags work";
349
+
`P "Search for bookmarks with multiple tags:";
350
+
`Pre " karakeepe search --tags \"work,important\"";
353
+
~info:(Cmd.info "search" ~doc ~man)
354
+
~app_name:"karakeepe"
355
+
~service:"karakeepe"
356
+
Term.(const (fun base_url tags limit env xdg profile ->
357
+
search_bookmarks env xdg profile base_url tags limit)
358
+
$ base_url_arg $ tags_arg $ limit_arg)
362
+
let doc = "Karakeep command-line client" in
364
+
`S Manpage.s_description;
365
+
`P "A command-line client for managing bookmarks in Karakeep (Hoarder).";
366
+
`P "Credentials are stored securely using keyeio in XDG config directories.";
367
+
`S Manpage.s_commands;
368
+
`S "CONFIGURATION";
369
+
`P "Initialize credentials using:";
370
+
`Pre " karakeepe init";
371
+
`P "Or manually create ~/.config/karakeepe/keys/karakeepe.toml:";
372
+
`Pre "[default]\napi_key = \"ak1_<key_id>_<secret>\"\nbase_url = \"https://hoard.example.com\"";
374
+
`P "Report bugs at https://github.com/avsm/knot/issues";
376
+
let info = Cmd.info "karakeepe" ~version:"0.1.0" ~doc ~man in
377
+
let main_cmd = Cmd.group info [init_cmd; list_cmd; get_cmd; create_cmd; search_cmd] in
379
+
exit (Cmd.eval' main_cmd)