···11+I wish to write high quality OCaml library bindings to the Karakeep instance.
22+You can assume while testing that an API key to the <https://hoard.recoil.org>
33+instance is in .karakeep-api.
44+55+Examine the source code for Karakeep in karakeep-src/ as a reference.
66+Maintain helpful API documentation inside the doc/ directory as a set of
77+Markdown files that reference each other.
···11+(** Karakeep API client implementation *)
22+33+open Lwt.Infix
44+55+module J = Ezjsonm
66+77+(** Type representing a Karakeep bookmark *)
88+type bookmark = {
99+ id: string;
1010+ title: string option;
1111+ url: string;
1212+ note: string option;
1313+ created_at: Ptime.t;
1414+ updated_at: Ptime.t option;
1515+ favourited: bool;
1616+ archived: bool;
1717+ tags: string list;
1818+ tagging_status: string option;
1919+ summary: string option;
2020+ content: (string * string) list;
2121+ assets: (string * string) list;
2222+}
2323+2424+(** Type for Karakeep API response containing bookmarks *)
2525+type bookmark_response = {
2626+ total: int;
2727+ data: bookmark list;
2828+ next_cursor: string option;
2929+}
3030+3131+(** Parse a date string to Ptime.t, defaulting to epoch if invalid *)
3232+let parse_date str =
3333+ match Ptime.of_rfc3339 str with
3434+ | Ok (date, _, _) -> date
3535+ | Error _ ->
3636+ Fmt.epr "Warning: could not parse date '%s'\n" str;
3737+ (* Default to epoch time *)
3838+ let span_opt = Ptime.Span.of_d_ps (0, 0L) in
3939+ match span_opt with
4040+ | None -> failwith "Internal error: couldn't create epoch time span"
4141+ | Some span ->
4242+ match Ptime.of_span span with
4343+ | Some t -> t
4444+ | None -> failwith "Internal error: couldn't create epoch time"
4545+4646+(** Extract a string field from JSON, returns None if not present or not a string *)
4747+let get_string_opt json path =
4848+ try Some (J.find json path |> J.get_string)
4949+ with _ -> None
5050+5151+(** Extract a string list field from JSON, returns empty list if not present *)
5252+let get_string_list json path =
5353+ try
5454+ let items_json = J.find json path in
5555+ J.get_list (fun tag -> J.find tag ["name"] |> J.get_string) items_json
5656+ with _ -> []
5757+5858+(** Extract a boolean field from JSON, with default value *)
5959+let get_bool_def json path default =
6060+ try J.find json path |> J.get_bool
6161+ with _ -> default
6262+6363+(** Parse a single bookmark from Karakeep JSON *)
6464+let parse_bookmark json =
6565+ (* Remove debug prints for production *)
6666+ (* Printf.eprintf "%s\n%!" (J.value_to_string json); *)
6767+6868+ let id =
6969+ try J.find json ["id"] |> J.get_string
7070+ with e ->
7171+ Printf.eprintf "Error parsing bookmark ID: %s\n" (Printexc.to_string e);
7272+ Printf.eprintf "JSON: %s\n" (J.value_to_string json);
7373+ failwith "Unable to parse bookmark ID"
7474+ in
7575+7676+ (* Title can be null *)
7777+ let title =
7878+ try Some (J.find json ["title"] |> J.get_string)
7979+ with _ -> None
8080+ in
8181+ (* Remove debug prints for production *)
8282+ (* Printf.eprintf "%s -> %s\n%!" id (match title with None -> "???" | Some v -> v); *)
8383+ (* Get URL - try all possible locations *)
8484+ let url =
8585+ try J.find json ["url"] |> J.get_string (* Direct url field *)
8686+ with _ -> try
8787+ J.find json ["content"; "url"] |> J.get_string (* Inside content.url *)
8888+ with _ -> try
8989+ J.find json ["content"; "sourceUrl"] |> J.get_string (* Inside content.sourceUrl *)
9090+ with _ ->
9191+ (* For assets/PDF type links *)
9292+ match J.find_opt json ["content"; "type"] with
9393+ | Some (`String "asset") ->
9494+ (* Extract URL from sourceUrl in content *)
9595+ (try J.find json ["content"; "sourceUrl"] |> J.get_string
9696+ with _ ->
9797+ (match J.find_opt json ["id"] with
9898+ | Some (`String id) -> "karakeep-asset://" ^ id
9999+ | _ -> failwith "No URL or asset ID found in bookmark"))
100100+ | _ ->
101101+ (* Debug output to understand what we're getting *)
102102+ Printf.eprintf "Bookmark JSON structure: %s\n" (J.value_to_string json);
103103+ failwith "No URL found in bookmark"
104104+ in
105105+106106+ let note = get_string_opt json ["note"] in
107107+108108+ (* Parse dates *)
109109+ let created_at =
110110+ try J.find json ["createdAt"] |> J.get_string |> parse_date
111111+ with _ ->
112112+ try J.find json ["created_at"] |> J.get_string |> parse_date
113113+ with _ -> failwith "No creation date found"
114114+ in
115115+116116+ let updated_at =
117117+ try Some (J.find json ["updatedAt"] |> J.get_string |> parse_date)
118118+ with _ ->
119119+ try Some (J.find json ["modifiedAt"] |> J.get_string |> parse_date)
120120+ with _ -> None
121121+ in
122122+123123+ let favourited = get_bool_def json ["favourited"] false in
124124+ let archived = get_bool_def json ["archived"] false in
125125+ let tags = get_string_list json ["tags"] in
126126+127127+ (* Extract additional metadata *)
128128+ let tagging_status = get_string_opt json ["taggingStatus"] in
129129+ let summary = get_string_opt json ["summary"] in
130130+131131+ (* Extract content details *)
132132+ let content =
133133+ try
134134+ let content_json = J.find json ["content"] in
135135+ let rec extract_fields acc = function
136136+ | [] -> acc
137137+ | (k, v) :: rest ->
138138+ let value = match v with
139139+ | `String s -> s
140140+ | `Bool b -> string_of_bool b
141141+ | `Float f -> string_of_float f
142142+ | `Null -> "null"
143143+ | _ -> "complex_value" (* For objects and arrays *)
144144+ in
145145+ extract_fields ((k, value) :: acc) rest
146146+ in
147147+ match content_json with
148148+ | `O fields -> extract_fields [] fields
149149+ | _ -> []
150150+ with _ -> []
151151+ in
152152+153153+ (* Extract assets *)
154154+ let assets =
155155+ try
156156+ let assets_json = J.find json ["assets"] in
157157+ J.get_list (fun asset_json ->
158158+ let id = J.find asset_json ["id"] |> J.get_string in
159159+ let asset_type =
160160+ try J.find asset_json ["assetType"] |> J.get_string
161161+ with _ -> "unknown"
162162+ in
163163+ (id, asset_type)
164164+ ) assets_json
165165+ with _ -> []
166166+ in
167167+168168+ { id; title; url; note; created_at; updated_at; favourited; archived; tags;
169169+ tagging_status; summary; content; assets }
170170+171171+(** Parse a Karakeep bookmark response *)
172172+let parse_bookmark_response json =
173173+ (* The response format is different based on endpoint, need to handle both structures *)
174174+ (* Print the whole JSON structure for debugging *)
175175+ Printf.eprintf "Full response JSON: %s\n" (J.value_to_string json);
176176+177177+ try
178178+ (* Standard list format with total count *)
179179+ let total = J.find json ["total"] |> J.get_int in
180180+ let bookmarks_json = J.find json ["data"] in
181181+ Printf.eprintf "Found bookmarks in data array\n";
182182+ let data = J.get_list parse_bookmark bookmarks_json in
183183+184184+ (* Try to extract nextCursor if available *)
185185+ let next_cursor =
186186+ try Some (J.find json ["nextCursor"] |> J.get_string)
187187+ with _ -> None
188188+ in
189189+190190+ { total; data; next_cursor }
191191+ with e1 ->
192192+ Printf.eprintf "First format parse error: %s\n" (Printexc.to_string e1);
193193+ try
194194+ (* Format with bookmarks array *)
195195+ let bookmarks_json = J.find json ["bookmarks"] in
196196+ Printf.eprintf "Found bookmarks in bookmarks array\n";
197197+ let data =
198198+ try J.get_list parse_bookmark bookmarks_json
199199+ with e ->
200200+ Printf.eprintf "Error parsing bookmarks array: %s\n" (Printexc.to_string e);
201201+ Printf.eprintf "First bookmark sample: %s\n"
202202+ (try J.value_to_string (List.hd (J.get_list (fun x -> x) bookmarks_json))
203203+ with _ -> "Could not extract sample");
204204+ []
205205+ in
206206+207207+ (* Try to extract nextCursor if available *)
208208+ let next_cursor =
209209+ try Some (J.find json ["nextCursor"] |> J.get_string)
210210+ with _ -> None
211211+ in
212212+213213+ { total = List.length data; data; next_cursor }
214214+ with e2 ->
215215+ Printf.eprintf "Second format parse error: %s\n" (Printexc.to_string e2);
216216+ try
217217+ (* Check if it's an error response *)
218218+ let error = J.find json ["error"] |> J.get_string in
219219+ let message =
220220+ try J.find json ["message"] |> J.get_string
221221+ with _ -> "Unknown error"
222222+ in
223223+ Printf.eprintf "API Error: %s - %s\n" error message;
224224+ { total = 0; data = []; next_cursor = None }
225225+ with _ ->
226226+ try
227227+ (* Alternate format without total (for endpoints like /tags/<id>/bookmarks) *)
228228+ Printf.eprintf "Trying alternate array format\n";
229229+230230+ (* Debug the structure to identify the format *)
231231+ Printf.eprintf "JSON structure keys: %s\n"
232232+ (match json with
233233+ | `O fields ->
234234+ String.concat ", " (List.map (fun (k, _) -> k) fields)
235235+ | _ -> "not an object");
236236+237237+ (* Check if it has a nextCursor but bookmarks are nested differently *)
238238+ if J.find_opt json ["nextCursor"] <> None then begin
239239+ Printf.eprintf "Found nextCursor, checking alternate structures\n";
240240+241241+ (* Try different bookmark container paths *)
242242+ let bookmarks_json =
243243+ try Some (J.find json ["data"])
244244+ with _ -> None
245245+ in
246246+247247+ match bookmarks_json with
248248+ | Some json_array ->
249249+ Printf.eprintf "Found bookmarks in data field\n";
250250+ begin try
251251+ let data = J.get_list parse_bookmark json_array in
252252+ let next_cursor =
253253+ try Some (J.find json ["nextCursor"] |> J.get_string)
254254+ with _ -> None
255255+ in
256256+ { total = List.length data; data; next_cursor }
257257+ with e ->
258258+ Printf.eprintf "Error parsing bookmarks from data: %s\n" (Printexc.to_string e);
259259+ { total = 0; data = []; next_cursor = None }
260260+ end
261261+ | None ->
262262+ Printf.eprintf "No bookmarks found in alternate structure\n";
263263+ { total = 0; data = []; next_cursor = None }
264264+ end
265265+ else begin
266266+ (* Check if it's an array at root level *)
267267+ match json with
268268+ | `A _ ->
269269+ let data =
270270+ try J.get_list parse_bookmark json
271271+ with e ->
272272+ Printf.eprintf "Error parsing root array: %s\n" (Printexc.to_string e);
273273+ []
274274+ in
275275+ { total = List.length data; data; next_cursor = None }
276276+ | _ ->
277277+ Printf.eprintf "Not an array at root level\n";
278278+ { total = 0; data = []; next_cursor = None }
279279+ end
280280+ with e3 ->
281281+ Printf.eprintf "Third format parse error: %s\n" (Printexc.to_string e3);
282282+ { total = 0; data = []; next_cursor = None }
283283+284284+(** Helper function to consume and return response body data *)
285285+let consume_body body =
286286+ Cohttp_lwt.Body.to_string body >>= fun _ ->
287287+ Lwt.return_unit
288288+289289+(** Fetch bookmarks from a Karakeep instance with pagination support *)
290290+let fetch_bookmarks ~api_key ?(limit=50) ?(offset=0) ?cursor ?(include_content=true) ?filter_tags base_url =
291291+ let open Cohttp_lwt_unix in
292292+293293+ (* Base URL for bookmarks API *)
294294+ let url_base = Printf.sprintf "%s/api/v1/bookmarks?limit=%d&includeContent=%b"
295295+ base_url limit include_content in
296296+297297+ (* Add pagination parameter - either cursor or offset *)
298298+ let url =
299299+ match cursor with
300300+ | Some cursor_value ->
301301+ url_base ^ "&cursor=" ^ cursor_value
302302+ | None ->
303303+ url_base ^ "&offset=" ^ string_of_int offset
304304+ in
305305+306306+ (* Add tags filter if provided *)
307307+ let url = match filter_tags with
308308+ | Some tags when tags <> [] ->
309309+ (* URL encode each tag and join with commas *)
310310+ let encoded_tags =
311311+ List.map (fun tag ->
312312+ Uri.pct_encode ~component:`Query_key tag
313313+ ) tags
314314+ in
315315+ let tags_param = String.concat "," encoded_tags in
316316+ Printf.eprintf "Adding tags filter: %s\n" tags_param;
317317+ url ^ "&tags=" ^ tags_param
318318+ | _ -> url
319319+ in
320320+321321+ (* Set up headers with API key *)
322322+ let headers = Cohttp.Header.init ()
323323+ |> fun h -> Cohttp.Header.add h "Authorization" ("Bearer " ^ api_key) in
324324+325325+ Printf.eprintf "Fetching bookmarks from: %s\n" url;
326326+327327+ (* Make the request *)
328328+ Lwt.catch
329329+ (fun () ->
330330+ Client.get ~headers (Uri.of_string url) >>= fun (resp, body) ->
331331+ if resp.status = `OK then
332332+ Cohttp_lwt.Body.to_string body >>= fun body_str ->
333333+ Printf.eprintf "Received %d bytes of response data\n" (String.length body_str);
334334+335335+ Lwt.catch
336336+ (fun () ->
337337+ let json = J.from_string body_str in
338338+ Lwt.return (parse_bookmark_response json)
339339+ )
340340+ (fun e ->
341341+ Printf.eprintf "JSON parsing error: %s\n" (Printexc.to_string e);
342342+ Printf.eprintf "Response body (first 200 chars): %s\n"
343343+ (if String.length body_str > 200 then String.sub body_str 0 200 ^ "..." else body_str);
344344+ Lwt.fail e
345345+ )
346346+ else
347347+ let status_code = Cohttp.Code.code_of_status resp.status in
348348+ consume_body body >>= fun _ ->
349349+ Printf.eprintf "HTTP error %d\n" status_code;
350350+ Lwt.fail_with (Fmt.str "HTTP error: %d" status_code)
351351+ )
352352+ (fun e ->
353353+ Printf.eprintf "Network error: %s\n" (Printexc.to_string e);
354354+ Lwt.fail e
355355+ )
356356+357357+(** Fetch all bookmarks from a Karakeep instance using pagination *)
358358+let fetch_all_bookmarks ~api_key ?(page_size=50) ?max_pages ?filter_tags base_url =
359359+ let rec fetch_pages page_num cursor acc _total_count =
360360+ (* Use cursor if available, otherwise use offset-based pagination *)
361361+ (match cursor with
362362+ | Some cursor_str -> fetch_bookmarks ~api_key ~limit:page_size ~cursor:cursor_str ?filter_tags base_url
363363+ | None -> fetch_bookmarks ~api_key ~limit:page_size ~offset:(page_num * page_size) ?filter_tags base_url)
364364+ >>= fun response ->
365365+366366+ let all_bookmarks = acc @ response.data in
367367+368368+ (* Determine if we need to fetch more pages *)
369369+ let more_available =
370370+ match response.next_cursor with
371371+ | Some _ -> true (* We have a cursor, so there are more results *)
372372+ | None ->
373373+ (* Fall back to offset-based check *)
374374+ let fetched_count = (page_num * page_size) + List.length response.data in
375375+ fetched_count < response.total
376376+ in
377377+378378+ let under_max_pages = match max_pages with
379379+ | None -> true
380380+ | Some max -> page_num + 1 < max
381381+ in
382382+383383+ if more_available && under_max_pages then
384384+ fetch_pages (page_num + 1) response.next_cursor all_bookmarks response.total
385385+ else
386386+ Lwt.return all_bookmarks
387387+ in
388388+ fetch_pages 0 None [] 0
389389+390390+(** Fetch detailed information for a single bookmark by ID *)
391391+let fetch_bookmark_details ~api_key base_url bookmark_id =
392392+ let open Cohttp_lwt_unix in
393393+ let url = Printf.sprintf "%s/api/v1/bookmarks/%s" base_url bookmark_id in
394394+395395+ (* Set up headers with API key *)
396396+ let headers = Cohttp.Header.init ()
397397+ |> fun h -> Cohttp.Header.add h "Authorization" ("Bearer " ^ api_key) in
398398+399399+ Client.get ~headers (Uri.of_string url) >>= fun (resp, body) ->
400400+ if resp.status = `OK then
401401+ Cohttp_lwt.Body.to_string body >>= fun body_str ->
402402+ let json = J.from_string body_str in
403403+ Lwt.return (parse_bookmark json)
404404+ else
405405+ let status_code = Cohttp.Code.code_of_status resp.status in
406406+ consume_body body >>= fun () ->
407407+ Lwt.fail_with (Fmt.str "HTTP error: %d" status_code)
408408+409409+(** Get the asset URL for a given asset ID *)
410410+let get_asset_url base_url asset_id =
411411+ Printf.sprintf "%s/api/assets/%s" base_url asset_id
412412+413413+(** Fetch an asset from the Karakeep server as a binary string *)
414414+let fetch_asset ~api_key base_url asset_id =
415415+ let open Cohttp_lwt_unix in
416416+417417+ let url = get_asset_url base_url asset_id in
418418+419419+ (* Set up headers with API key *)
420420+ let headers = Cohttp.Header.init ()
421421+ |> fun h -> Cohttp.Header.add h "Authorization" ("Bearer " ^ api_key) in
422422+423423+ Client.get ~headers (Uri.of_string url) >>= fun (resp, body) ->
424424+ if resp.status = `OK then
425425+ Cohttp_lwt.Body.to_string body
426426+ else
427427+ let status_code = Cohttp.Code.code_of_status resp.status in
428428+ consume_body body >>= fun () ->
429429+ Lwt.fail_with (Fmt.str "Asset fetch error: %d" status_code)
430430+431431+(** Create a new bookmark in Karakeep with optional tags *)
432432+let create_bookmark ~api_key ~url ?title ?note ?tags ?(favourited=false) ?(archived=false) base_url =
433433+ let open Cohttp_lwt_unix in
434434+435435+ (* Prepare the bookmark request body *)
436436+ let body_obj = [
437437+ ("type", `String "link");
438438+ ("url", `String url);
439439+ ("favourited", `Bool favourited);
440440+ ("archived", `Bool archived);
441441+ ] in
442442+443443+ (* Add optional fields *)
444444+ let body_obj = match title with
445445+ | Some title_str -> ("title", `String title_str) :: body_obj
446446+ | None -> body_obj
447447+ in
448448+449449+ let body_obj = match note with
450450+ | Some note_str -> ("note", `String note_str) :: body_obj
451451+ | None -> body_obj
452452+ in
453453+454454+ (* Convert to JSON *)
455455+ let body_json = `O body_obj in
456456+ let body_str = J.to_string body_json in
457457+458458+ (* Set up headers with API key *)
459459+ let headers = Cohttp.Header.init ()
460460+ |> fun h -> Cohttp.Header.add h "Authorization" ("Bearer " ^ api_key)
461461+ |> fun h -> Cohttp.Header.add h "Content-Type" "application/json"
462462+ in
463463+464464+ (* Helper function to ensure we consume all response body data *)
465465+ let consume_body body =
466466+ Cohttp_lwt.Body.to_string body >>= fun _ ->
467467+ Lwt.return_unit
468468+ in
469469+470470+ (* Create the bookmark *)
471471+ let url_endpoint = Printf.sprintf "%s/api/v1/bookmarks" base_url in
472472+ Client.post ~headers ~body:(Cohttp_lwt.Body.of_string body_str) (Uri.of_string url_endpoint) >>= fun (resp, body) ->
473473+474474+ if resp.status = `Created || resp.status = `OK then
475475+ Cohttp_lwt.Body.to_string body >>= fun body_str ->
476476+ let json = J.from_string body_str in
477477+ let bookmark = parse_bookmark json in
478478+479479+ (* If tags are provided, add them to the bookmark *)
480480+ (match tags with
481481+ | Some tag_list when tag_list <> [] ->
482482+ (* Prepare the tags request body *)
483483+ let tag_objects = List.map (fun tag_name ->
484484+ `O [("tagName", `String tag_name)]
485485+ ) tag_list in
486486+487487+ let tags_body = `O [("tags", `A tag_objects)] in
488488+ let tags_body_str = J.to_string tags_body in
489489+490490+ (* Add tags to the bookmark *)
491491+ let tags_url = Printf.sprintf "%s/api/v1/bookmarks/%s/tags" base_url bookmark.id in
492492+ Client.post ~headers ~body:(Cohttp_lwt.Body.of_string tags_body_str) (Uri.of_string tags_url) >>= fun (resp, body) ->
493493+494494+ (* Always consume the response body *)
495495+ consume_body body >>= fun () ->
496496+497497+ if resp.status = `OK then
498498+ (* Fetch the bookmark again to get updated tags *)
499499+ fetch_bookmark_details ~api_key base_url bookmark.id
500500+ else
501501+ (* Return the bookmark without tags if tag addition failed *)
502502+ Lwt.return bookmark
503503+ | _ -> Lwt.return bookmark)
504504+ else
505505+ let status_code = Cohttp.Code.code_of_status resp.status in
506506+ Cohttp_lwt.Body.to_string body >>= fun error_body ->
507507+ Lwt.fail_with (Fmt.str "Failed to create bookmark. HTTP error: %d. Details: %s" status_code error_body)
508508+509509+(** Convert a Karakeep bookmark to Bushel.Link.t compatible structure *)
510510+let to_bushel_link ?base_url bookmark =
511511+ (* Try to find the best title from multiple possible sources *)
512512+ let description =
513513+ match bookmark.title with
514514+ | Some title when title <> "" -> title
515515+ | _ ->
516516+ (* Check if there's a title in the content *)
517517+ let content_title = List.assoc_opt "title" bookmark.content in
518518+ match content_title with
519519+ | Some title when title <> "" && title <> "null" -> title
520520+ | _ -> bookmark.url
521521+ in
522522+ let date = Ptime.to_date bookmark.created_at in
523523+524524+ (* Build comprehensive metadata from all available fields *)
525525+ let metadata =
526526+ (match bookmark.summary with Some s -> [("summary", s)] | None -> []) @
527527+ (match bookmark.tagging_status with Some s -> [("tagging_status", s)] | None -> []) @
528528+ (List.map (fun (id, asset_type) -> ("asset_" ^ asset_type, id)) bookmark.assets) @
529529+ (List.filter_map (fun (k, v) ->
530530+ if List.mem k ["type"; "url"; "title"; "screenshotAssetId"; "favicon"]
531531+ then Some ("content_" ^ k, v) else None) bookmark.content)
532532+ in
533533+534534+ (* Create karakeep_id if base_url is provided *)
535535+ let karakeep_id =
536536+ match base_url with
537537+ | Some url -> Some { Bushel.Link.remote_url = url; id = bookmark.id }
538538+ | None ->
539539+ (* For backward compatibility, try to extract from metadata *)
540540+ Some { Bushel.Link.remote_url = "unknown"; id = bookmark.id }
541541+ in
542542+543543+ (* Extract any tags for additional context *)
544544+ let bushel_slugs =
545545+ (* If there are tags that start with "bushel:", use them as slugs *)
546546+ List.filter_map (fun tag ->
547547+ if String.starts_with ~prefix:"bushel:" tag then
548548+ Some (String.sub tag 7 (String.length tag - 7))
549549+ else
550550+ None
551551+ ) bookmark.tags
552552+ in
553553+554554+ { Bushel.Link.url = bookmark.url; date; description; metadata; karakeep_id; bushel_slugs }
+121
lib/karakeep.mli
···11+(** Karakeep API client interface *)
22+33+(** Type representing a Karakeep bookmark *)
44+type bookmark = {
55+ id: string;
66+ title: string option;
77+ url: string;
88+ note: string option;
99+ created_at: Ptime.t;
1010+ updated_at: Ptime.t option;
1111+ favourited: bool;
1212+ archived: bool;
1313+ tags: string list;
1414+ tagging_status: string option;
1515+ summary: string option;
1616+ content: (string * string) list;
1717+ assets: (string * string) list;
1818+}
1919+2020+(** Type for Karakeep API response containing bookmarks *)
2121+type bookmark_response = {
2222+ total: int;
2323+ data: bookmark list;
2424+ next_cursor: string option;
2525+}
2626+2727+(** Parse a single bookmark from Karakeep JSON *)
2828+val parse_bookmark : Ezjsonm.value -> bookmark
2929+3030+(** Parse a Karakeep bookmark response *)
3131+val parse_bookmark_response : Ezjsonm.value -> bookmark_response
3232+3333+(** Fetch bookmarks from a Karakeep instance with pagination support
3434+ @param api_key API key for authentication
3535+ @param limit Number of bookmarks to fetch per page (default: 50)
3636+ @param offset Starting index for pagination (0-based) (default: 0)
3737+ @param cursor Optional pagination cursor for cursor-based pagination (overrides offset when provided)
3838+ @param include_content Whether to include full content (default: true)
3939+ @param filter_tags Optional list of tags to filter by
4040+ @param base_url Base URL of the Karakeep instance
4141+ @return A Lwt promise with the bookmark response *)
4242+val fetch_bookmarks :
4343+ api_key:string ->
4444+ ?limit:int ->
4545+ ?offset:int ->
4646+ ?cursor:string ->
4747+ ?include_content:bool ->
4848+ ?filter_tags:string list ->
4949+ string ->
5050+ bookmark_response Lwt.t
5151+5252+(** Fetch all bookmarks from a Karakeep instance using pagination
5353+ @param api_key API key for authentication
5454+ @param page_size Number of bookmarks to fetch per page (default: 50)
5555+ @param max_pages Maximum number of pages to fetch (None for all pages)
5656+ @param filter_tags Optional list of tags to filter by
5757+ @param base_url Base URL of the Karakeep instance
5858+ @return A Lwt promise with all bookmarks combined *)
5959+val fetch_all_bookmarks :
6060+ api_key:string ->
6161+ ?page_size:int ->
6262+ ?max_pages:int ->
6363+ ?filter_tags:string list ->
6464+ string ->
6565+ bookmark list Lwt.t
6666+6767+(** Fetch detailed information for a single bookmark by ID
6868+ @param api_key API key for authentication
6969+ @param base_url Base URL of the Karakeep instance
7070+ @param bookmark_id ID of the bookmark to fetch
7171+ @return A Lwt promise with the complete bookmark details *)
7272+val fetch_bookmark_details :
7373+ api_key:string ->
7474+ string ->
7575+ string ->
7676+ bookmark Lwt.t
7777+7878+(** Convert a Karakeep bookmark to Bushel.Link.t compatible structure
7979+ @param base_url Optional base URL of the Karakeep instance (for karakeep_id) *)
8080+val to_bushel_link : ?base_url:string -> bookmark -> Bushel.Link.t
8181+8282+(** Fetch an asset from the Karakeep server as a binary string
8383+ @param api_key API key for authentication
8484+ @param base_url Base URL of the Karakeep instance
8585+ @param asset_id ID of the asset to fetch
8686+ @return A Lwt promise with the binary asset data *)
8787+val fetch_asset :
8888+ api_key:string ->
8989+ string ->
9090+ string ->
9191+ string Lwt.t
9292+9393+(** Get the asset URL for a given asset ID
9494+ @param base_url Base URL of the Karakeep instance
9595+ @param asset_id ID of the asset
9696+ @return The full URL to the asset *)
9797+val get_asset_url :
9898+ string ->
9999+ string ->
100100+ string
101101+102102+(** Create a new bookmark in Karakeep with optional tags
103103+ @param api_key API key for authentication
104104+ @param url The URL to bookmark
105105+ @param title Optional title for the bookmark
106106+ @param note Optional note to add to the bookmark
107107+ @param tags Optional list of tag names to add to the bookmark
108108+ @param favourited Whether the bookmark should be marked as favourite (default: false)
109109+ @param archived Whether the bookmark should be archived (default: false)
110110+ @param base_url Base URL of the Karakeep instance
111111+ @return A Lwt promise with the created bookmark *)
112112+val create_bookmark :
113113+ api_key:string ->
114114+ url:string ->
115115+ ?title:string ->
116116+ ?note:string ->
117117+ ?tags:string list ->
118118+ ?favourited:bool ->
119119+ ?archived:bool ->
120120+ string ->
121121+ bookmark Lwt.t