···11(** Karakeep API client implementation *)
2233-open Lwt.Infix
44-module J = Ezjsonm
33+include Karakeep_proto
5466-(** {1 Core Types} *)
55+(** {1 Error Handling} *)
7688-type asset_id = string
99-(** Asset identifier type *)
77+type error =
88+ | Api_error of { status : int; code : string; message : string }
99+ | Json_error of { reason : string }
10101111-type bookmark_id = string
1212-(** Bookmark identifier type *)
1111+type Eio.Exn.err += E of error
13121414-type list_id = string
1515-(** List identifier type *)
1313+let err e = Eio.Exn.create (E e)
16141717-type tag_id = string
1818-(** Tag identifier type *)
1515+let is_api_error = function Api_error _ -> true | _ -> false
19162020-type highlight_id = string
2121-(** Highlight identifier type *)
1717+let is_not_found = function
1818+ | Api_error { status = 404; _ } -> true
1919+ | _ -> false
22202323-(** Type of content a bookmark can have *)
2424-type bookmark_content_type =
2525- | Link (** A URL to a webpage *)
2626- | Text (** Plain text content *)
2727- | Asset (** An attached asset (image, PDF, etc.) *)
2828- | Unknown (** Unknown content type *)
2929-3030-(** Type of asset *)
3131-type asset_type =
3232- | Screenshot (** Screenshot of a webpage *)
3333- | AssetScreenshot (** Screenshot of an asset *)
3434- | BannerImage (** Banner image *)
3535- | FullPageArchive (** Archive of a full webpage *)
3636- | Video (** Video asset *)
3737- | BookmarkAsset (** Generic bookmark asset *)
3838- | PrecrawledArchive (** Pre-crawled archive *)
3939- | Unknown (** Unknown asset type *)
2121+let error_to_string = function
2222+ | Api_error { status; code; message } ->
2323+ Printf.sprintf "API error %d (%s): %s" status code message
2424+ | Json_error { reason } -> Printf.sprintf "JSON error: %s" reason
40254141-(** Type of tagging status *)
4242-type tagging_status =
4343- | Success (** Tagging was successful *)
4444- | Failure (** Tagging failed *)
4545- | Pending (** Tagging is pending *)
4646-4747-(** Type of bookmark list *)
4848-type list_type =
4949- | Manual (** List is manually managed *)
5050- | Smart (** List is dynamically generated based on a query *)
5151-5252-(** Highlight color *)
5353-type highlight_color =
5454- | Yellow (** Yellow highlight *)
5555- | Red (** Red highlight *)
5656- | Green (** Green highlight *)
5757- | Blue (** Blue highlight *)
5858-5959-(** Type of how a tag was attached *)
6060-type tag_attachment_type =
6161- | AI (** Tag was attached by AI *)
6262- | Human (** Tag was attached by a human *)
6363-6464-type link_content = {
6565- url : string; (** The URL of the bookmarked page *)
6666- title : string option; (** Title from the link *)
6767- description : string option; (** Description from the link *)
6868- image_url : string option; (** URL of an image from the link *)
6969- image_asset_id : asset_id option; (** ID of an image asset *)
7070- screenshot_asset_id : asset_id option; (** ID of a screenshot asset *)
7171- full_page_archive_asset_id : asset_id option;
7272- (** ID of a full page archive asset *)
7373- precrawled_archive_asset_id : asset_id option;
7474- (** ID of a pre-crawled archive asset *)
7575- video_asset_id : asset_id option; (** ID of a video asset *)
7676- favicon : string option; (** URL of the favicon *)
7777- html_content : string option; (** HTML content of the page *)
7878- crawled_at : Ptime.t option; (** When the page was crawled *)
7979- author : string option; (** Author of the content *)
8080- publisher : string option; (** Publisher of the content *)
8181- date_published : Ptime.t option; (** When the content was published *)
8282- date_modified : Ptime.t option; (** When the content was last modified *)
8383-}
8484-(** Link content for a bookmark *)
8585-8686-type text_content = {
8787- text : string; (** The text content *)
8888- source_url : string option; (** Optional source URL for the text *)
8989-}
9090-(** Text content for a bookmark *)
9191-9292-type asset_content = {
9393- asset_type : [ `Image | `PDF ]; (** Type of the asset *)
9494- asset_id : asset_id; (** ID of the asset *)
9595- file_name : string option; (** Name of the file *)
9696- source_url : string option; (** Source URL for the asset *)
9797- size : int option; (** Size of the asset in bytes *)
9898- content : string option; (** Extracted content from the asset *)
9999-}
100100-(** Asset content for a bookmark *)
101101-102102-(** Content of a bookmark *)
103103-type content =
104104- | Link of link_content (** Link-type content *)
105105- | Text of text_content (** Text-type content *)
106106- | Asset of asset_content (** Asset-type content *)
107107- | Unknown (** Unknown content type *)
108108-109109-type asset = {
110110- id : asset_id; (** ID of the asset *)
111111- asset_type : asset_type; (** Type of the asset *)
112112-}
113113-(** Asset attached to a bookmark *)
114114-115115-type bookmark_tag = {
116116- id : tag_id; (** ID of the tag *)
117117- name : string; (** Name of the tag *)
118118- attached_by : tag_attachment_type; (** How the tag was attached *)
119119-}
120120-(** Tag with attachment information *)
121121-122122-type bookmark = {
123123- id : bookmark_id; (** Unique identifier for the bookmark *)
124124- created_at : Ptime.t; (** Timestamp when the bookmark was created *)
125125- modified_at : Ptime.t option; (** Optional timestamp of the last update *)
126126- title : string option; (** Optional title of the bookmarked page *)
127127- archived : bool; (** Whether the bookmark is archived *)
128128- favourited : bool; (** Whether the bookmark is marked as a favorite *)
129129- tagging_status : tagging_status option; (** Status of automatic tagging *)
130130- note : string option; (** Optional user note associated with the bookmark *)
131131- summary : string option; (** Optional AI-generated summary *)
132132- tags : bookmark_tag list; (** Tags associated with the bookmark *)
133133- content : content; (** Content of the bookmark *)
134134- assets : asset list; (** Assets attached to the bookmark *)
135135-}
136136-(** A bookmark from the Karakeep service *)
137137-138138-type paginated_bookmarks = {
139139- bookmarks : bookmark list; (** List of bookmarks in the current page *)
140140- next_cursor : string option; (** Optional cursor for fetching the next page *)
141141-}
142142-(** Paginated response of bookmarks *)
143143-144144-type _list = {
145145- id : list_id; (** ID of the list *)
146146- name : string; (** Name of the list *)
147147- description : string option; (** Optional description of the list *)
148148- icon : string; (** Icon for the list *)
149149- parent_id : list_id option; (** Optional parent list ID *)
150150- list_type : list_type; (** Type of the list *)
151151- query : string option; (** Optional query for smart lists *)
152152-}
153153-(** List in Karakeep *)
154154-155155-type tag = {
156156- id : tag_id; (** ID of the tag *)
157157- name : string; (** Name of the tag *)
158158- num_bookmarks : int; (** Number of bookmarks with this tag *)
159159- num_bookmarks_by_attached_type : (tag_attachment_type * int) list;
160160- (** Number of bookmarks by attachment type *)
161161-}
162162-(** Tag in Karakeep *)
163163-164164-type highlight = {
165165- bookmark_id : bookmark_id; (** ID of the bookmark *)
166166- start_offset : int; (** Start position of the highlight *)
167167- end_offset : int; (** End position of the highlight *)
168168- color : highlight_color; (** Color of the highlight *)
169169- text : string option; (** Text of the highlight *)
170170- note : string option; (** Note for the highlight *)
171171- id : highlight_id; (** ID of the highlight *)
172172- user_id : string; (** ID of the user who created the highlight *)
173173- created_at : Ptime.t; (** When the highlight was created *)
174174-}
175175-(** Highlight in Karakeep *)
176176-177177-type paginated_highlights = {
178178- highlights : highlight list; (** List of highlights in the current page *)
179179- next_cursor : string option; (** Optional cursor for fetching the next page *)
180180-}
181181-(** Paginated response of highlights *)
182182-183183-type user_info = {
184184- id : string; (** ID of the user *)
185185- name : string option; (** Name of the user *)
186186- email : string option; (** Email of the user *)
187187-}
188188-(** User information *)
2626+let pp_error fmt e = Format.pp_print_string fmt (error_to_string e)
18927190190-type user_stats = {
191191- num_bookmarks : int; (** Number of bookmarks *)
192192- num_favorites : int; (** Number of favorite bookmarks *)
193193- num_archived : int; (** Number of archived bookmarks *)
194194- num_tags : int; (** Number of tags *)
195195- num_lists : int; (** Number of lists *)
196196- num_highlights : int; (** Number of highlights *)
197197-}
198198-(** User statistics *)
2828+(** {1 Client} *)
19929200200-type error_response = {
201201- code : string; (** Error code *)
202202- message : string; (** Error message *)
3030+type t = {
3131+ session : Requests.t;
3232+ base_url : string;
20333}
204204-(** Error response from the API *)
20534206206-(** {1 Helper Functions} *)
207207-208208-(** Parse a date string to Ptime.t, defaulting to epoch if invalid *)
209209-let parse_date str =
210210- match Ptime.of_rfc3339 str with
211211- | Ok (date, _, _) -> date
212212- | Error _ -> (
213213- Fmt.epr "Warning: could not parse date '%s'\n" str;
214214- (* Default to epoch time *)
215215- let span_opt = Ptime.Span.of_d_ps (0, 0L) in
216216- match span_opt with
217217- | None -> failwith "Internal error: couldn't create epoch time span"
218218- | Some span -> (
219219- match Ptime.of_span span with
220220- | Some t -> t
221221- | None -> failwith "Internal error: couldn't create epoch time"))
222222-223223-(** Extract a string field from JSON, returns None if not present or not a
224224- string *)
225225-let get_string_opt json path =
226226- try Some (J.find json path |> J.get_string) with _ -> None
227227-228228-(** Extract an int field from JSON, returns None if not present or not an int *)
229229-let get_int_opt json path =
230230- try Some (J.find json path |> J.get_int) with _ -> None
231231-232232-(** Extract a date field from JSON, returns None if not present or invalid *)
233233-let get_date_opt json path =
234234- match get_string_opt json path with
235235- | Some date_str -> ( try Some (parse_date date_str) with _ -> None)
236236- | None -> None
237237-238238-(** Extract a boolean field from JSON, with default value *)
239239-let get_bool_def json path default =
240240- try J.find json path |> J.get_bool with _ -> default
241241-242242-(** Convert string to asset_type *)
243243-let asset_type_of_string = function
244244- | "screenshot" -> Screenshot
245245- | "assetScreenshot" -> AssetScreenshot
246246- | "bannerImage" -> BannerImage
247247- | "fullPageArchive" -> FullPageArchive
248248- | "video" -> Video
249249- | "bookmarkAsset" -> BookmarkAsset
250250- | "precrawledArchive" -> PrecrawledArchive
251251- | _ -> Unknown
252252-253253-(** Convert asset_type to string *)
254254-let string_of_asset_type = function
255255- | Screenshot -> "screenshot"
256256- | AssetScreenshot -> "assetScreenshot"
257257- | BannerImage -> "bannerImage"
258258- | FullPageArchive -> "fullPageArchive"
259259- | Video -> "video"
260260- | BookmarkAsset -> "bookmarkAsset"
261261- | PrecrawledArchive -> "precrawledArchive"
262262- | Unknown -> "unknown"
263263-264264-(** Convert string to tagging_status *)
265265-let tagging_status_of_string = function
266266- | "success" -> Success
267267- | "failure" -> Failure
268268- | "pending" -> Pending
269269- | _ -> Success (* Default to success if unknown *)
270270-271271-(** Convert tagging_status to string *)
272272-let string_of_tagging_status = function
273273- | Success -> "success"
274274- | Failure -> "failure"
275275- | Pending -> "pending"
276276-277277-(** Convert string to list_type *)
278278-let list_type_of_string = function
279279- | "manual" -> Manual
280280- | "smart" -> Smart
281281- | _ -> Manual (* Default to manual if unknown *)
282282-283283-(** Convert list_type to string *)
284284-let string_of_list_type = function Manual -> "manual" | Smart -> "smart"
285285-286286-(** Convert string to highlight_color *)
287287-let highlight_color_of_string = function
288288- | "yellow" -> Yellow
289289- | "red" -> Red
290290- | "green" -> Green
291291- | "blue" -> Blue
292292- | _ -> Yellow (* Default to yellow if unknown *)
293293-294294-(** Convert highlight_color to string *)
295295-let string_of_highlight_color = function
296296- | Yellow -> "yellow"
297297- | Red -> "red"
298298- | Green -> "green"
299299- | Blue -> "blue"
300300-301301-(** Convert string to tag_attachment_type *)
302302-let tag_attachment_type_of_string = function
303303- | "ai" -> AI
304304- | "human" -> Human
305305- | _ -> Human (* Default to human if unknown *)
306306-307307-(** Convert tag_attachment_type to string *)
308308-let string_of_tag_attachment_type = function AI -> "ai" | Human -> "human"
309309-310310-(** Parse link content from JSON *)
311311-let parse_link_content json =
312312- {
313313- url = J.find json [ "url" ] |> J.get_string;
314314- title = get_string_opt json [ "title" ];
315315- description = get_string_opt json [ "description" ];
316316- image_url = get_string_opt json [ "imageUrl" ];
317317- image_asset_id = get_string_opt json [ "imageAssetId" ];
318318- screenshot_asset_id = get_string_opt json [ "screenshotAssetId" ];
319319- full_page_archive_asset_id =
320320- get_string_opt json [ "fullPageArchiveAssetId" ];
321321- precrawled_archive_asset_id =
322322- get_string_opt json [ "precrawledArchiveAssetId" ];
323323- video_asset_id = get_string_opt json [ "videoAssetId" ];
324324- favicon = get_string_opt json [ "favicon" ];
325325- html_content = get_string_opt json [ "htmlContent" ];
326326- crawled_at = get_date_opt json [ "crawledAt" ];
327327- author = get_string_opt json [ "author" ];
328328- publisher = get_string_opt json [ "publisher" ];
329329- date_published = get_date_opt json [ "datePublished" ];
330330- date_modified = get_date_opt json [ "dateModified" ];
331331- }
332332-333333-(** Parse text content from JSON *)
334334-let parse_text_content json =
335335- {
336336- text = J.find json [ "text" ] |> J.get_string;
337337- source_url = get_string_opt json [ "sourceUrl" ];
338338- }
339339-340340-(** Parse asset content from JSON *)
341341-let parse_asset_content json =
342342- let asset_type_str = get_string_opt json [ "assetType" ] in
343343- let asset_type =
344344- match asset_type_str with
345345- | Some "image" -> `Image
346346- | Some "pdf" -> `PDF
347347- | _ -> `Image (* Default to image if unknown *)
348348- in
349349- {
350350- asset_type;
351351- asset_id = J.find json [ "assetId" ] |> J.get_string;
352352- file_name = get_string_opt json [ "fileName" ];
353353- source_url = get_string_opt json [ "sourceUrl" ];
354354- size = get_int_opt json [ "size" ];
355355- content = get_string_opt json [ "content" ];
356356- }
357357-358358-(** Parse content from JSON *)
359359-let parse_content json =
360360- let content_type = get_string_opt json [ "type" ] in
361361- match content_type with
362362- | Some "link" -> Link (parse_link_content json)
363363- | Some "text" -> Text (parse_text_content json)
364364- | Some "asset" -> Asset (parse_asset_content json)
365365- | _ -> Unknown
366366-367367-(** Extract a meaningful title from bookmark content *)
368368-let title = function
369369- | Link lc ->
370370- begin match lc.title with
371371- | Some t when String.trim t <> "" -> t
372372- | _ -> lc.url
373373- end
374374- | Text tc ->
375375- let text_excerpt =
376376- let s = tc.text in
377377- let max_len = 40 in
378378- if String.length s <= max_len then s
379379- else String.sub s 0 max_len ^ "..."
380380- in
381381- begin match tc.source_url with
382382- | Some url -> "Text from " ^ url
383383- | None -> "Text: " ^ text_excerpt
384384- end
385385- | Asset ac ->
386386- begin match ac.file_name with
387387- | Some fn -> fn
388388- | None ->
389389- begin match ac.source_url with
390390- | Some url -> "Asset from " ^ url
391391- | None ->
392392- match ac.asset_type with
393393- | `Image -> "Image asset"
394394- | `PDF -> "PDF asset"
395395- end
396396- end
397397- | Unknown -> "Unnamed bookmark"
398398-399399-(** Parse asset from JSON *)
400400-let parse_asset json =
401401- {
402402- id = J.find json [ "id" ] |> J.get_string;
403403- asset_type =
404404- J.find json [ "assetType" ] |> J.get_string |> asset_type_of_string;
405405- }
406406-407407-(** Parse bookmark tag from JSON *)
408408-let parse_bookmark_tag json =
409409- {
410410- id = J.find json [ "id" ] |> J.get_string;
411411- name = J.find json [ "name" ] |> J.get_string;
412412- attached_by =
413413- get_string_opt json [ "attachedBy" ]
414414- |> Option.value ~default:"human"
415415- |> tag_attachment_type_of_string;
416416- }
417417-418418-(** Parse a bookmark from JSON *)
419419-let parse_bookmark json =
420420- let id = J.find json [ "id" ] |> J.get_string in
421421- let created_at = J.find json [ "createdAt" ] |> J.get_string |> parse_date in
422422- let tags =
423423- try
424424- let tags_json = J.find json [ "tags" ] in
425425- J.get_list parse_bookmark_tag tags_json
426426- with _ -> []
3535+let create ~sw ~env ~base_url ~api_key =
3636+ let session = Requests.create ~sw env in
3737+ let session =
3838+ Requests.set_auth session (Requests.Auth.bearer ~token:api_key)
42739 in
428428- let assets =
429429- try
430430- let assets_json = J.find json [ "assets" ] in
431431- J.get_list parse_asset assets_json
432432- with _ -> []
433433- in
434434- let tagging_status =
435435- get_string_opt json [ "taggingStatus" ]
436436- |> Option.map tagging_status_of_string
437437- in
438438- {
439439- id;
440440- created_at;
441441- modified_at = get_date_opt json [ "modifiedAt" ];
442442- title = get_string_opt json [ "title" ];
443443- archived = get_bool_def json [ "archived" ] false;
444444- favourited = get_bool_def json [ "favourited" ] false;
445445- tagging_status;
446446- note = get_string_opt json [ "note" ];
447447- summary = get_string_opt json [ "summary" ];
448448- tags;
449449- content =
450450- (try parse_content (J.find json [ "content" ]) with _ -> Unknown);
451451- assets;
452452- }
4040+ { session; base_url }
45341454454-(** Get the best available title for a bookmark *)
455455-let bookmark_title bookmark =
456456- match bookmark.title with
457457- | Some t when String.trim t <> "" -> t
458458- | _ -> title bookmark.content
4242+(** {1 Internal Helpers} *)
45943460460-(** Parse paginated bookmarks from JSON *)
461461-let parse_paginated_bookmarks json =
462462- let bookmarks =
463463- try
464464- let bookmarks_json = J.find json [ "bookmarks" ] in
465465- J.get_list parse_bookmark bookmarks_json
466466- with _ -> (
467467- try
468468- let bookmarks_json = J.find json [ "data" ] in
469469- J.get_list parse_bookmark bookmarks_json
470470- with _ -> [])
4444+let ( / ) base path =
4545+ let base =
4646+ if String.ends_with ~suffix:"/" base then
4747+ String.sub base 0 (String.length base - 1)
4848+ else base
47149 in
472472- let next_cursor = get_string_opt json [ "nextCursor" ] in
473473- { bookmarks; next_cursor }
474474-475475-(** Parse tag from JSON *)
476476-let parse_tag json =
477477- let attachment_types =
478478- try
479479- let stats_json = J.find json [ "numBookmarksByAttachedType" ] in
480480- match stats_json with
481481- | `O fields ->
482482- List.map
483483- (fun (k, v) -> (tag_attachment_type_of_string k, J.get_int v))
484484- fields
485485- | _ -> []
486486- with _ -> []
5050+ let path =
5151+ if String.starts_with ~prefix:"/" path then
5252+ String.sub path 1 (String.length path - 1)
5353+ else path
48754 in
488488- {
489489- id = J.find json [ "id" ] |> J.get_string;
490490- name = J.find json [ "name" ] |> J.get_string;
491491- num_bookmarks =
492492- get_int_opt json [ "numBookmarks" ] |> Option.value ~default:0;
493493- num_bookmarks_by_attached_type = attachment_types;
494494- }
495495-496496-(** Parse list from JSON *)
497497-let parse_list json =
498498- {
499499- id = J.find json [ "id" ] |> J.get_string;
500500- name = J.find json [ "name" ] |> J.get_string;
501501- description = get_string_opt json [ "description" ];
502502- icon = get_string_opt json [ "icon" ] |> Option.value ~default:"default";
503503- parent_id = get_string_opt json [ "parentId" ];
504504- list_type =
505505- get_string_opt json [ "listType" ]
506506- |> Option.value ~default:"manual"
507507- |> list_type_of_string;
508508- query = get_string_opt json [ "query" ];
509509- }
510510-511511-(** Parse highlight from JSON *)
512512-let parse_highlight json =
513513- {
514514- bookmark_id = J.find json [ "bookmarkId" ] |> J.get_string;
515515- start_offset = J.find json [ "startOffset" ] |> J.get_int;
516516- end_offset = J.find json [ "endOffset" ] |> J.get_int;
517517- color =
518518- get_string_opt json [ "color" ]
519519- |> Option.value ~default:"yellow"
520520- |> highlight_color_of_string;
521521- text = get_string_opt json [ "text" ];
522522- note = get_string_opt json [ "note" ];
523523- id = J.find json [ "id" ] |> J.get_string;
524524- user_id = J.find json [ "userId" ] |> J.get_string;
525525- created_at = J.find json [ "createdAt" ] |> J.get_string |> parse_date;
526526- }
5555+ base ^ "/" ^ path
52756528528-(** Parse paginated highlights from JSON *)
529529-let parse_paginated_highlights json =
530530- let highlights =
531531- try
532532- let highlights_json = J.find json [ "highlights" ] in
533533- J.get_list parse_highlight highlights_json
534534- with _ -> []
535535- in
536536- let next_cursor = get_string_opt json [ "nextCursor" ] in
537537- { highlights; next_cursor }
5757+let query_string params =
5858+ match params with
5959+ | [] -> ""
6060+ | _ ->
6161+ "?"
6262+ ^ String.concat "&"
6363+ (List.map (fun (k, v) -> Uri.pct_encode k ^ "=" ^ Uri.pct_encode v) params)
53864539539-(** Parse user info from JSON *)
540540-let parse_user_info json =
541541- {
542542- id = J.find json [ "id" ] |> J.get_string;
543543- name = get_string_opt json [ "name" ];
544544- email = get_string_opt json [ "email" ];
545545- }
6565+let decode_json codec body_str =
6666+ match Jsont_bytesrw.decode_string' codec body_str with
6767+ | Ok v -> v
6868+ | Error e ->
6969+ raise (err (Json_error { reason = Jsont.Error.to_string e }))
54670547547-(** Parse user stats from JSON *)
548548-let parse_user_stats json =
549549- {
550550- num_bookmarks =
551551- get_int_opt json [ "numBookmarks" ] |> Option.value ~default:0;
552552- num_favorites =
553553- get_int_opt json [ "numFavorites" ] |> Option.value ~default:0;
554554- num_archived = get_int_opt json [ "numArchived" ] |> Option.value ~default:0;
555555- num_tags = get_int_opt json [ "numTags" ] |> Option.value ~default:0;
556556- num_lists = get_int_opt json [ "numLists" ] |> Option.value ~default:0;
557557- num_highlights =
558558- get_int_opt json [ "numHighlights" ] |> Option.value ~default:0;
559559- }
7171+let encode_json codec value =
7272+ match Jsont_bytesrw.encode_string' codec value with
7373+ | Ok s -> s
7474+ | Error e ->
7575+ raise (err (Json_error { reason = Jsont.Error.to_string e }))
56076561561-(** Parse error response from JSON *)
562562-let parse_error_response json =
563563- {
564564- code =
565565- get_string_opt json [ "code" ] |> Option.value ~default:"unknown_error";
566566- message =
567567- get_string_opt json [ "message" ] |> Option.value ~default:"Unknown error";
568568- }
7777+let handle_error_response status body =
7878+ match Jsont_bytesrw.decode_string' error_response_jsont body with
7979+ | Ok err_resp ->
8080+ raise (err (Api_error { status; code = err_resp.code; message = err_resp.message }))
8181+ | Error _ ->
8282+ raise (err (Api_error { status; code = "unknown"; message = body }))
56983570570-(** Helper function to consume and return response body data *)
571571-let consume_body body =
572572- Cohttp_lwt.Body.to_string body >>= fun _ -> Lwt.return_unit
8484+let get_json t url codec =
8585+ let response = Requests.get t.session url in
8686+ let body = Requests.Response.text response in
8787+ if not (Requests.Response.ok response) then
8888+ handle_error_response (Requests.Response.status_code response) body;
8989+ decode_json codec body
57390574574-(** Helper function to make API requests *)
575575-let make_request ~api_key ~method_ ?(headers = []) ?(body = None) url =
576576- let open Cohttp_lwt_unix in
577577- let uri = Uri.of_string url in
9191+let post_json t url req_codec req_value resp_codec =
9292+ let body_str = encode_json req_codec req_value in
9393+ let body = Requests.Body.of_string Requests.Mime.json body_str in
9494+ let response = Requests.post t.session url ~body in
9595+ let resp_body = Requests.Response.text response in
9696+ if not (Requests.Response.ok response) then
9797+ handle_error_response (Requests.Response.status_code response) resp_body;
9898+ decode_json resp_codec resp_body
57899579579- (* Set up headers with API key *)
580580- let base_headers = [ ("Authorization", "Bearer " ^ api_key) ] in
581581- let all_headers = base_headers @ headers in
582582- let headers = Cohttp.Header.of_list all_headers in
100100+let post_json_no_body t url resp_codec =
101101+ let response = Requests.post t.session url in
102102+ let resp_body = Requests.Response.text response in
103103+ if not (Requests.Response.ok response) then
104104+ handle_error_response (Requests.Response.status_code response) resp_body;
105105+ decode_json resp_codec resp_body
583106584584- let body_opt =
585585- match body with
586586- | Some content -> Some (Cohttp_lwt.Body.of_string content)
587587- | None -> None
588588- in
107107+let patch_json t url req_codec req_value resp_codec =
108108+ let body_str = encode_json req_codec req_value in
109109+ let body = Requests.Body.of_string Requests.Mime.json body_str in
110110+ let response = Requests.patch t.session url ~body in
111111+ let resp_body = Requests.Response.text response in
112112+ if not (Requests.Response.ok response) then
113113+ handle_error_response (Requests.Response.status_code response) resp_body;
114114+ decode_json resp_codec resp_body
589115590590- match method_ with
591591- | `GET -> Client.get ~headers uri
592592- | `POST -> Client.post ~headers ?body:body_opt uri
593593- | `PUT -> Client.put ~headers ?body:body_opt uri
594594- | `DELETE -> Client.delete ~headers ?body:body_opt uri
116116+let delete_json t url =
117117+ let response = Requests.delete t.session url in
118118+ let resp_body = Requests.Response.text response in
119119+ if not (Requests.Response.ok response) then
120120+ handle_error_response (Requests.Response.status_code response) resp_body
595121596596-(** Process API response *)
597597-let process_response resp body parse_fn =
598598- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
599599- if status_code >= 200 && status_code < 300 then
600600- Cohttp_lwt.Body.to_string body >>= fun body_str ->
601601- try
602602- let json = J.from_string body_str in
603603- Lwt.return (parse_fn json)
604604- with e ->
605605- Lwt.fail_with
606606- ("Failed to parse response: " ^ Printexc.to_string e
607607- ^ "\nResponse body: " ^ body_str)
608608- else
609609- Cohttp_lwt.Body.to_string body >>= fun body_str ->
610610- let error_msg =
611611- try
612612- let json = J.from_string body_str in
613613- let error_resp = parse_error_response json in
614614- Printf.sprintf "API Error (%s): %s" error_resp.code error_resp.message
615615- with _ -> Printf.sprintf "HTTP error %d: %s" status_code body_str
616616- in
617617- Lwt.fail_with error_msg
122122+let put_json t url req_codec req_value =
123123+ let body_str = encode_json req_codec req_value in
124124+ let body = Requests.Body.of_string Requests.Mime.json body_str in
125125+ let response = Requests.put t.session url ~body in
126126+ let resp_body = Requests.Response.text response in
127127+ if not (Requests.Response.ok response) then
128128+ handle_error_response (Requests.Response.status_code response) resp_body;
129129+ resp_body
618130619131(** {1 Bookmark Operations} *)
620132621621-let fetch_bookmarks ~api_key ?limit ?cursor ?include_content ?archived
622622- ?favourited base_url =
623623- (* Build query parameters *)
133133+let fetch_bookmarks t ?limit ?cursor ?include_content ?archived ?favourited () =
624134 let params = [] in
625135 let params =
626626- match limit with
627627- | Some l -> ("limit", string_of_int l) :: params
628628- | None -> params
136136+ match limit with Some l -> ("limit", string_of_int l) :: params | None -> params
629137 in
630138 let params =
631139 match cursor with Some c -> ("cursor", c) :: params | None -> params
632140 in
633141 let params =
634142 match include_content with
635635- | Some ic -> ("includeContent", string_of_bool ic) :: params
143143+ | Some true -> ("includeContent", "true") :: params
144144+ | Some false -> ("includeContent", "false") :: params
636145 | None -> params
637146 in
638147 let params =
639148 match archived with
640640- | Some a -> ("archived", string_of_bool a) :: params
149149+ | Some true -> ("archived", "true") :: params
150150+ | Some false -> ("archived", "false") :: params
641151 | None -> params
642152 in
643153 let params =
644154 match favourited with
645645- | Some f -> ("favourited", string_of_bool f) :: params
155155+ | Some true -> ("favourited", "true") :: params
156156+ | Some false -> ("favourited", "false") :: params
646157 | None -> params
647158 in
648648-649649- (* Construct URL with query parameters *)
650650- let query_str =
651651- if params = [] then ""
652652- else
653653- "?"
654654- ^ String.concat "&"
655655- (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params)
656656- in
657657- let url = base_url ^ "/api/v1/bookmarks" ^ query_str in
658658-659659- (* Make the request *)
660660- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
661661- process_response resp body parse_paginated_bookmarks
662662-663663-let fetch_all_bookmarks ~api_key ?page_size ?max_pages ?archived ?favourited
664664- base_url =
665665- let rec fetch_pages cursor acc page_num =
666666- let limit = Option.value page_size ~default:50 in
667667- fetch_bookmarks ~api_key ~limit ?cursor ?archived ?favourited base_url
668668- >>= fun response ->
669669- let all_bookmarks = acc @ response.bookmarks in
670670-671671- let more_pages = response.next_cursor <> None in
672672- let under_max =
673673- match max_pages with Some max -> page_num < max | None -> true
674674- in
159159+ let url = t.base_url / "api/v1/bookmarks" ^ query_string params in
160160+ get_json t url paginated_bookmarks_jsont
675161676676- if more_pages && under_max then
677677- fetch_pages response.next_cursor all_bookmarks (page_num + 1)
678678- else Lwt.return all_bookmarks
162162+let fetch_all_bookmarks t ?page_size ?max_pages ?archived ?favourited () =
163163+ let limit = Option.value page_size ~default:50 in
164164+ let rec fetch_all acc cursor pages_fetched =
165165+ match max_pages with
166166+ | Some max when pages_fetched >= max -> List.rev acc
167167+ | _ ->
168168+ let result = fetch_bookmarks t ~limit ?cursor ?archived ?favourited () in
169169+ let acc = List.rev_append result.bookmarks acc in
170170+ (match result.next_cursor with
171171+ | None -> List.rev acc
172172+ | Some c -> fetch_all acc (Some c) (pages_fetched + 1))
679173 in
680680-681681- fetch_pages None [] 0
174174+ fetch_all [] None 0
682175683683-let search_bookmarks ~api_key ~query ?limit ?cursor ?include_content base_url =
684684- (* Build query parameters *)
176176+let search_bookmarks t ~query ?limit ?cursor ?include_content () =
685177 let params = [ ("q", query) ] in
686178 let params =
687687- match limit with
688688- | Some l -> ("limit", string_of_int l) :: params
689689- | None -> params
179179+ match limit with Some l -> ("limit", string_of_int l) :: params | None -> params
690180 in
691181 let params =
692182 match cursor with Some c -> ("cursor", c) :: params | None -> params
693183 in
694184 let params =
695185 match include_content with
696696- | Some ic -> ("includeContent", string_of_bool ic) :: params
186186+ | Some true -> ("includeContent", "true") :: params
187187+ | Some false -> ("includeContent", "false") :: params
697188 | None -> params
698189 in
190190+ let url = t.base_url / "api/v1/bookmarks/search" ^ query_string params in
191191+ get_json t url paginated_bookmarks_jsont
699192700700- (* Construct URL with query parameters *)
701701- let query_str =
702702- "?"
703703- ^ String.concat "&"
704704- (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params)
705705- in
706706- let url = base_url ^ "/api/v1/bookmarks/search" ^ query_str in
193193+let fetch_bookmark_details t bookmark_id =
194194+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id in
195195+ get_json t url bookmark_jsont
707196708708- (* Make the request *)
709709- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
710710- process_response resp body parse_paginated_bookmarks
711711-712712-let fetch_bookmark_details ~api_key base_url bookmark_id =
713713- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id in
714714-715715- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
716716- process_response resp body parse_bookmark
717717-718718-let create_bookmark ~api_key ~url ?title ?note ?summary ?favourited ?archived
719719- ?created_at ?tags base_url =
720720- (* Prepare the bookmark request body *)
721721- let body_obj = [ ("url", `String url) ] in
722722-723723- (* Add optional fields *)
724724- let body_obj =
725725- match title with
726726- | Some t -> ("title", `String t) :: body_obj
727727- | None -> body_obj
197197+let rec create_bookmark t ~url ?title ?note ?summary ?favourited ?archived ?created_at
198198+ ?tags () =
199199+ let api_url = t.base_url / "api/v1/bookmarks" in
200200+ let req : create_bookmark_request =
201201+ {
202202+ type_ = "link";
203203+ url = Some url;
204204+ text = None;
205205+ title;
206206+ note;
207207+ summary;
208208+ archived;
209209+ favourited;
210210+ created_at;
211211+ }
728212 in
729729- let body_obj =
730730- match note with
731731- | Some n -> ("note", `String n) :: body_obj
732732- | None -> body_obj
733733- in
734734- let body_obj =
735735- match summary with
736736- | Some s -> ("summary", `String s) :: body_obj
737737- | None -> body_obj
738738- in
739739- let body_obj =
740740- match favourited with
741741- | Some f -> ("favourited", `Bool f) :: body_obj
742742- | None -> body_obj
743743- in
744744- let body_obj =
745745- match archived with
746746- | Some a -> ("archived", `Bool a) :: body_obj
747747- | None -> body_obj
748748- in
749749- let body_obj =
750750- match created_at with
751751- | Some date ->
752752- let iso_date = Ptime.to_rfc3339 date in
753753- ("createdAt", `String iso_date) :: body_obj
754754- | None -> body_obj
755755- in
756756- let body_obj =
757757- match tags with
758758- | Some tag_list when tag_list <> [] ->
759759- let tag_objs =
760760- List.map (fun t -> `O [ ("name", `String t) ]) tag_list
761761- in
762762- ("tags", `A tag_objs) :: body_obj
763763- | _ -> body_obj
764764- in
765765-766766- (* Convert to JSON *)
767767- let body_json = `O body_obj in
768768- let body_str = J.to_string body_json in
769769-770770- (* Make the request *)
771771- let headers = [ ("Content-Type", "application/json") ] in
772772- let url = base_url ^ "/api/v1/bookmarks" in
773773-774774- make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url
775775- >>= fun (resp, body) -> process_response resp body parse_bookmark
776776-777777-let update_bookmark ~api_key ?title ?note ?summary ?favourited ?archived ?url
778778- ?description ?author ?publisher ?date_published ?date_modified ?text
779779- ?asset_content base_url bookmark_id =
780780- (* Prepare the update request body *)
781781- let body_obj = [] in
782782-783783- (* Add optional fields *)
784784- let body_obj =
785785- match title with
786786- | Some t -> ("title", `String t) :: body_obj
787787- | None -> body_obj
788788- in
789789- let body_obj =
790790- match note with
791791- | Some n -> ("note", `String n) :: body_obj
792792- | None -> body_obj
793793- in
794794- let body_obj =
795795- match summary with
796796- | Some s -> ("summary", `String s) :: body_obj
797797- | None -> body_obj
798798- in
799799- let body_obj =
800800- match favourited with
801801- | Some f -> ("favourited", `Bool f) :: body_obj
802802- | None -> body_obj
803803- in
804804- let body_obj =
805805- match archived with
806806- | Some a -> ("archived", `Bool a) :: body_obj
807807- | None -> body_obj
808808- in
809809- let body_obj =
810810- match url with Some u -> ("url", `String u) :: body_obj | None -> body_obj
811811- in
812812- let body_obj =
813813- match description with
814814- | Some d -> ("description", `String d) :: body_obj
815815- | None -> body_obj
816816- in
817817- let body_obj =
818818- match author with
819819- | Some a -> ("author", `String a) :: body_obj
820820- | None -> body_obj
821821- in
822822- let body_obj =
823823- match publisher with
824824- | Some p -> ("publisher", `String p) :: body_obj
825825- | None -> body_obj
826826- in
827827- let body_obj =
828828- match date_published with
829829- | Some date ->
830830- let iso_date = Ptime.to_rfc3339 date in
831831- ("datePublished", `String iso_date) :: body_obj
832832- | None -> body_obj
833833- in
834834- let body_obj =
835835- match date_modified with
836836- | Some date ->
837837- let iso_date = Ptime.to_rfc3339 date in
838838- ("dateModified", `String iso_date) :: body_obj
839839- | None -> body_obj
840840- in
841841- let body_obj =
842842- match text with
843843- | Some t -> ("text", `String t) :: body_obj
844844- | None -> body_obj
845845- in
846846- let body_obj =
847847- match asset_content with
848848- | Some c -> ("assetContent", `String c) :: body_obj
849849- | None -> body_obj
850850- in
851851-852852- (* Only proceed if there are updates to make *)
853853- if body_obj = [] then
854854- (* No updates, just fetch the current bookmark *)
855855- fetch_bookmark_details ~api_key base_url bookmark_id
856856- else
857857- (* Convert to JSON *)
858858- let body_json = `O body_obj in
859859- let body_str = J.to_string body_json in
860860-861861- (* Make the request *)
862862- let headers = [ ("Content-Type", "application/json") ] in
863863- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id in
864864-865865- make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url
866866- >>= fun (resp, body) -> process_response resp body parse_bookmark
867867-868868-let delete_bookmark ~api_key base_url bookmark_id =
869869- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id in
870870-871871- make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) ->
872872- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
873873- if status_code >= 200 && status_code < 300 then
874874- consume_body body >>= fun () -> Lwt.return_unit
875875- else
876876- Cohttp_lwt.Body.to_string body >>= fun body_str ->
877877- Lwt.fail_with ("Failed to delete bookmark: " ^ body_str)
878878-879879-let summarize_bookmark ~api_key base_url bookmark_id =
880880- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/summarize" in
881881-882882- make_request ~api_key ~method_:`POST url >>= fun (resp, body) ->
883883- process_response resp body parse_bookmark
884884-885885-(** {1 Tag Operations} *)
213213+ let bookmark = post_json t api_url create_bookmark_request_jsont req bookmark_jsont in
214214+ (* Attach tags if provided *)
215215+ match tags with
216216+ | None | Some [] -> bookmark
217217+ | Some tag_names ->
218218+ let tag_refs = List.map (fun n -> TagName n) tag_names in
219219+ let _ =
220220+ attach_tags t ~tag_refs:(List.map (fun r -> match r with TagName n -> `TagName n | TagId i -> `TagId i) tag_refs) bookmark.id
221221+ in
222222+ (* Refetch the bookmark to get updated tags *)
223223+ fetch_bookmark_details t bookmark.id
886224887887-let attach_tags ~api_key ~tag_refs base_url bookmark_id =
888888- (* Prepare the tag request body *)
889889- let tag_objs =
225225+and attach_tags t ~tag_refs bookmark_id =
226226+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "tags" in
227227+ let tags =
890228 List.map
891891- (function
892892- | `TagId id -> `O [ ("tagId", `String id) ]
893893- | `TagName name -> `O [ ("tagName", `String name) ])
229229+ (function `TagId id -> TagId id | `TagName name -> TagName name)
894230 tag_refs
895231 in
232232+ let req = { tags } in
233233+ let resp = post_json t url attach_tags_request_jsont req attach_tags_response_jsont in
234234+ resp.attached
896235897897- let body_json = `O [ ("tags", `A tag_objs) ] in
898898- let body_str = J.to_string body_json in
236236+let update_bookmark t bookmark_id ?title ?note ?summary ?favourited ?archived () =
237237+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id in
238238+ let req : update_bookmark_request = { title; note; summary; archived; favourited } in
239239+ patch_json t url update_bookmark_request_jsont req bookmark_jsont
899240900900- (* Make the request *)
901901- let headers = [ ("Content-Type", "application/json") ] in
902902- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/tags" in
241241+let delete_bookmark t bookmark_id =
242242+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id in
243243+ delete_json t url
903244904904- make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url
905905- >>= fun (resp, body) ->
906906- process_response resp body (fun json ->
907907- try
908908- let tags_json = J.find json [ "tags" ] in
909909- J.get_list
910910- (fun tag_json -> J.find tag_json [ "id" ] |> J.get_string)
911911- tags_json
912912- with _ -> [])
245245+let summarize_bookmark t bookmark_id =
246246+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "summarize" in
247247+ post_json_no_body t url bookmark_jsont
248248+249249+(** {1 Tag Operations} *)
913250914914-let detach_tags ~api_key ~tag_refs base_url bookmark_id =
915915- (* Prepare the tag request body *)
916916- let tag_objs =
251251+let detach_tags t ~tag_refs bookmark_id =
252252+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "tags" in
253253+ let tags =
917254 List.map
918918- (function
919919- | `TagId id -> `O [ ("tagId", `String id) ]
920920- | `TagName name -> `O [ ("tagName", `String name) ])
255255+ (function `TagId id -> TagId id | `TagName name -> TagName name)
921256 tag_refs
922257 in
923923-924924- let body_json = `O [ ("tags", `A tag_objs) ] in
925925- let body_str = J.to_string body_json in
926926-927927- (* Make the request *)
928928- let headers = [ ("Content-Type", "application/json") ] in
929929- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/tags/detach" in
258258+ let req = { tags } in
259259+ (* DELETE with body - use request function directly *)
260260+ let body_str = encode_json attach_tags_request_jsont req in
261261+ let body = Requests.Body.of_string Requests.Mime.json body_str in
262262+ let response = Requests.request t.session ~method_:`DELETE ~body url in
263263+ let resp_body = Requests.Response.text response in
264264+ if not (Requests.Response.ok response) then
265265+ handle_error_response (Requests.Response.status_code response) resp_body;
266266+ let resp = decode_json detach_tags_response_jsont resp_body in
267267+ resp.detached
930268931931- make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url
932932- >>= fun (resp, body) ->
933933- process_response resp body (fun json ->
934934- try
935935- let tags_json = J.find json [ "tags" ] in
936936- J.get_list
937937- (fun tag_json -> J.find tag_json [ "id" ] |> J.get_string)
938938- tags_json
939939- with _ -> [])
269269+let fetch_all_tags t =
270270+ let url = t.base_url / "api/v1/tags" in
271271+ let resp = get_json t url tags_response_jsont in
272272+ resp.tags
940273941941-let fetch_all_tags ~api_key base_url =
942942- let url = base_url ^ "/api/v1/tags" in
274274+let fetch_tag_details t tag_id =
275275+ let url = t.base_url / "api/v1/tags" / tag_id in
276276+ get_json t url tag_jsont
943277944944- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
945945- process_response resp body (fun json ->
946946- try
947947- let tags_json = J.find json [ "tags" ] in
948948- J.get_list parse_tag tags_json
949949- with _ -> [])
950950-951951-let fetch_tag_details ~api_key base_url tag_id =
952952- let url = base_url ^ "/api/v1/tags/" ^ tag_id in
953953-954954- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
955955- process_response resp body parse_tag
956956-957957-let fetch_bookmarks_with_tag ~api_key ?limit ?cursor ?include_content base_url
958958- tag_id =
959959- (* Build query parameters *)
278278+let fetch_bookmarks_with_tag t ?limit ?cursor ?include_content tag_id =
960279 let params = [] in
961280 let params =
962962- match limit with
963963- | Some l -> ("limit", string_of_int l) :: params
964964- | None -> params
281281+ match limit with Some l -> ("limit", string_of_int l) :: params | None -> params
965282 in
966283 let params =
967284 match cursor with Some c -> ("cursor", c) :: params | None -> params
968285 in
969286 let params =
970287 match include_content with
971971- | Some ic -> ("includeContent", string_of_bool ic) :: params
288288+ | Some true -> ("includeContent", "true") :: params
289289+ | Some false -> ("includeContent", "false") :: params
972290 | None -> params
973291 in
974974-975975- (* Construct URL with query parameters *)
976976- let query_str =
977977- if params = [] then ""
978978- else
979979- "?"
980980- ^ String.concat "&"
981981- (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params)
982982- in
983983- let url = base_url ^ "/api/v1/tags/" ^ tag_id ^ "/bookmarks" ^ query_str in
984984-985985- (* Make the request *)
986986- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
987987- process_response resp body parse_paginated_bookmarks
988988-989989-let update_tag ~api_key ~name base_url tag_id =
990990- let body_json = `O [ ("name", `String name) ] in
991991- let body_str = J.to_string body_json in
992992-993993- (* Make the request *)
994994- let headers = [ ("Content-Type", "application/json") ] in
995995- let url = base_url ^ "/api/v1/tags/" ^ tag_id in
996996-997997- make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url
998998- >>= fun (resp, body) -> process_response resp body parse_tag
292292+ let url = t.base_url / "api/v1/tags" / tag_id / "bookmarks" ^ query_string params in
293293+ get_json t url paginated_bookmarks_jsont
99929410001000-let delete_tag ~api_key base_url tag_id =
10011001- let url = base_url ^ "/api/v1/tags/" ^ tag_id in
295295+let update_tag t ~name tag_id =
296296+ let url = t.base_url / "api/v1/tags" / tag_id in
297297+ let req : update_tag_request = { name } in
298298+ patch_json t url update_tag_request_jsont req tag_jsont
100229910031003- make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) ->
10041004- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
10051005- if status_code >= 200 && status_code < 300 then
10061006- consume_body body >>= fun () -> Lwt.return_unit
10071007- else
10081008- Cohttp_lwt.Body.to_string body >>= fun body_str ->
10091009- Lwt.fail_with ("Failed to delete tag: " ^ body_str)
300300+let delete_tag t tag_id =
301301+ let url = t.base_url / "api/v1/tags" / tag_id in
302302+ delete_json t url
10103031011304(** {1 List Operations} *)
101230510131013-let fetch_all_lists ~api_key base_url =
10141014- let url = base_url ^ "/api/v1/lists" in
10151015-10161016- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
10171017- process_response resp body (fun json ->
10181018- try
10191019- let lists_json = J.find json [ "lists" ] in
10201020- J.get_list parse_list lists_json
10211021- with _ -> [])
10221022-10231023-let fetch_list_details ~api_key base_url list_id =
10241024- let url = base_url ^ "/api/v1/lists/" ^ list_id in
10251025-10261026- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
10271027- process_response resp body parse_list
306306+let fetch_all_lists t =
307307+ let url = t.base_url / "api/v1/lists" in
308308+ let resp = get_json t url lists_response_jsont in
309309+ resp.lists
102831010291029-let create_list ~api_key ~name ~icon ?description ?parent_id ?list_type ?query
10301030- base_url =
10311031- (* Prepare the list request body *)
10321032- let body_obj = [ ("name", `String name); ("icon", `String icon) ] in
311311+let fetch_list_details t list_id =
312312+ let url = t.base_url / "api/v1/lists" / list_id in
313313+ get_json t url list_jsont
103331410341034- (* Add optional fields *)
10351035- let body_obj =
10361036- match description with
10371037- | Some d -> ("description", `String d) :: body_obj
10381038- | None -> body_obj
10391039- in
10401040- let body_obj =
10411041- match parent_id with
10421042- | Some id -> ("parentId", `String id) :: body_obj
10431043- | None -> body_obj
10441044- in
10451045- let body_obj =
315315+let create_list t ~name ~icon ?description ?parent_id ?list_type ?query () =
316316+ let url = t.base_url / "api/v1/lists" in
317317+ let type_ =
1046318 match list_type with
10471047- | Some lt -> ("listType", `String (string_of_list_type lt)) :: body_obj
10481048- | None -> body_obj
319319+ | Some Manual -> Some "manual"
320320+ | Some Smart -> Some "smart"
321321+ | None -> None
1049322 in
10501050- let body_obj =
10511051- match query with
10521052- | Some q -> ("query", `String q) :: body_obj
10531053- | None -> body_obj
10541054- in
323323+ let req : create_list_request = { name; icon; description; parent_id; type_; query } in
324324+ post_json t url create_list_request_jsont req list_jsont
105532510561056- (* Convert to JSON *)
10571057- let body_json = `O body_obj in
10581058- let body_str = J.to_string body_json in
10591059-10601060- (* Make the request *)
10611061- let headers = [ ("Content-Type", "application/json") ] in
10621062- let url = base_url ^ "/api/v1/lists" in
10631063-10641064- make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url
10651065- >>= fun (resp, body) -> process_response resp body parse_list
326326+let update_list t ?name ?description ?icon ?parent_id ?query list_id =
327327+ let url = t.base_url / "api/v1/lists" / list_id in
328328+ let req : update_list_request = { name; icon; description; parent_id; query } in
329329+ patch_json t url update_list_request_jsont req list_jsont
106633010671067-let update_list ~api_key ?name ?description ?icon ?parent_id ?query base_url
10681068- list_id =
10691069- (* Prepare the update request body *)
10701070- let body_obj = [] in
331331+let delete_list t list_id =
332332+ let url = t.base_url / "api/v1/lists" / list_id in
333333+ delete_json t url
107133410721072- (* Add optional fields *)
10731073- let body_obj =
10741074- match name with
10751075- | Some n -> ("name", `String n) :: body_obj
10761076- | None -> body_obj
10771077- in
10781078- let body_obj =
10791079- match description with
10801080- | Some d -> ("description", `String d) :: body_obj
10811081- | None -> body_obj
10821082- in
10831083- let body_obj =
10841084- match icon with
10851085- | Some i -> ("icon", `String i) :: body_obj
10861086- | None -> body_obj
10871087- in
10881088- let body_obj =
10891089- match parent_id with
10901090- | Some (Some id) -> ("parentId", `String id) :: body_obj
10911091- | Some None -> ("parentId", `Null) :: body_obj
10921092- | None -> body_obj
10931093- in
10941094- let body_obj =
10951095- match query with
10961096- | Some q -> ("query", `String q) :: body_obj
10971097- | None -> body_obj
10981098- in
10991099-11001100- (* Only proceed if there are updates to make *)
11011101- if body_obj = [] then
11021102- (* No updates, just fetch the current list *)
11031103- fetch_list_details ~api_key base_url list_id
11041104- else
11051105- (* Convert to JSON *)
11061106- let body_json = `O body_obj in
11071107- let body_str = J.to_string body_json in
11081108-11091109- (* Make the request *)
11101110- let headers = [ ("Content-Type", "application/json") ] in
11111111- let url = base_url ^ "/api/v1/lists/" ^ list_id in
11121112-11131113- make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url
11141114- >>= fun (resp, body) -> process_response resp body parse_list
11151115-11161116-let delete_list ~api_key base_url list_id =
11171117- let url = base_url ^ "/api/v1/lists/" ^ list_id in
11181118-11191119- make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) ->
11201120- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
11211121- if status_code >= 200 && status_code < 300 then
11221122- consume_body body >>= fun () -> Lwt.return_unit
11231123- else
11241124- Cohttp_lwt.Body.to_string body >>= fun body_str ->
11251125- Lwt.fail_with ("Failed to delete list: " ^ body_str)
11261126-11271127-let fetch_bookmarks_in_list ~api_key ?limit ?cursor ?include_content base_url
11281128- list_id =
11291129- (* Build query parameters *)
335335+let fetch_bookmarks_in_list t ?limit ?cursor ?include_content list_id =
1130336 let params = [] in
1131337 let params =
11321132- match limit with
11331133- | Some l -> ("limit", string_of_int l) :: params
11341134- | None -> params
338338+ match limit with Some l -> ("limit", string_of_int l) :: params | None -> params
1135339 in
1136340 let params =
1137341 match cursor with Some c -> ("cursor", c) :: params | None -> params
1138342 in
1139343 let params =
1140344 match include_content with
11411141- | Some ic -> ("includeContent", string_of_bool ic) :: params
345345+ | Some true -> ("includeContent", "true") :: params
346346+ | Some false -> ("includeContent", "false") :: params
1142347 | None -> params
1143348 in
11441144-11451145- (* Construct URL with query parameters *)
11461146- let query_str =
11471147- if params = [] then ""
11481148- else
11491149- "?"
11501150- ^ String.concat "&"
11511151- (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params)
11521152- in
11531153- let url = base_url ^ "/api/v1/lists/" ^ list_id ^ "/bookmarks" ^ query_str in
11541154-11551155- (* Make the request *)
11561156- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
11571157- process_response resp body parse_paginated_bookmarks
11581158-11591159-let add_bookmark_to_list ~api_key base_url list_id bookmark_id =
11601160- let url =
11611161- base_url ^ "/api/v1/lists/" ^ list_id ^ "/bookmarks/" ^ bookmark_id
11621162- in
11631163-11641164- make_request ~api_key ~method_:`POST url >>= fun (resp, body) ->
11651165- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
11661166- if status_code >= 200 && status_code < 300 then
11671167- consume_body body >>= fun () -> Lwt.return_unit
11681168- else
11691169- Cohttp_lwt.Body.to_string body >>= fun body_str ->
11701170- Lwt.fail_with ("Failed to add bookmark to list: " ^ body_str)
349349+ let url = t.base_url / "api/v1/lists" / list_id / "bookmarks" ^ query_string params in
350350+ get_json t url paginated_bookmarks_jsont
117135111721172-let remove_bookmark_from_list ~api_key base_url list_id bookmark_id =
11731173- let url =
11741174- base_url ^ "/api/v1/lists/" ^ list_id ^ "/bookmarks/" ^ bookmark_id
11751175- in
352352+let add_bookmark_to_list t list_id bookmark_id =
353353+ let url = t.base_url / "api/v1/lists" / list_id / "bookmarks" / bookmark_id in
354354+ let response = Requests.put t.session url in
355355+ let resp_body = Requests.Response.text response in
356356+ if not (Requests.Response.ok response) then
357357+ handle_error_response (Requests.Response.status_code response) resp_body
117635811771177- make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) ->
11781178- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
11791179- if status_code >= 200 && status_code < 300 then
11801180- consume_body body >>= fun () -> Lwt.return_unit
11811181- else
11821182- Cohttp_lwt.Body.to_string body >>= fun body_str ->
11831183- Lwt.fail_with ("Failed to remove bookmark from list: " ^ body_str)
359359+let remove_bookmark_from_list t list_id bookmark_id =
360360+ let url = t.base_url / "api/v1/lists" / list_id / "bookmarks" / bookmark_id in
361361+ delete_json t url
11843621185363(** {1 Highlight Operations} *)
118636411871187-let fetch_all_highlights ~api_key ?limit ?cursor base_url =
11881188- (* Build query parameters *)
365365+let fetch_all_highlights t ?limit ?cursor () =
1189366 let params = [] in
1190367 let params =
11911191- match limit with
11921192- | Some l -> ("limit", string_of_int l) :: params
11931193- | None -> params
368368+ match limit with Some l -> ("limit", string_of_int l) :: params | None -> params
1194369 in
1195370 let params =
1196371 match cursor with Some c -> ("cursor", c) :: params | None -> params
1197372 in
11981198-11991199- (* Construct URL with query parameters *)
12001200- let query_str =
12011201- if params = [] then ""
12021202- else
12031203- "?"
12041204- ^ String.concat "&"
12051205- (List.map (fun (k, v) -> k ^ "=" ^ Uri.pct_encode v) params)
12061206- in
12071207- let url = base_url ^ "/api/v1/highlights" ^ query_str in
373373+ let url = t.base_url / "api/v1/highlights" ^ query_string params in
374374+ get_json t url paginated_highlights_jsont
120837512091209- (* Make the request *)
12101210- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
12111211- process_response resp body parse_paginated_highlights
376376+let fetch_bookmark_highlights t bookmark_id =
377377+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "highlights" in
378378+ let resp = get_json t url highlights_response_jsont in
379379+ resp.highlights
121238012131213-let fetch_bookmark_highlights ~api_key base_url bookmark_id =
12141214- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/highlights" in
381381+let fetch_highlight_details t highlight_id =
382382+ let url = t.base_url / "api/v1/highlights" / highlight_id in
383383+ get_json t url highlight_jsont
121538412161216- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
12171217- process_response resp body (fun json ->
12181218- try
12191219- let highlights_json = J.find json [ "highlights" ] in
12201220- J.get_list parse_highlight highlights_json
12211221- with _ -> [])
12221222-12231223-let fetch_highlight_details ~api_key base_url highlight_id =
12241224- let url = base_url ^ "/api/v1/highlights/" ^ highlight_id in
12251225-12261226- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
12271227- process_response resp body parse_highlight
12281228-12291229-let create_highlight ~api_key ~bookmark_id ~start_offset ~end_offset ~text ?note
12301230- ?color base_url =
12311231- (* Prepare the highlight request body *)
12321232- let body_obj =
12331233- [
12341234- ("bookmarkId", `String bookmark_id);
12351235- ("startOffset", `Float (float_of_int start_offset));
12361236- ("endOffset", `Float (float_of_int end_offset));
12371237- ("text", `String text);
12381238- ]
385385+let create_highlight t ~bookmark_id ~start_offset ~end_offset ~text ?note ?color () =
386386+ let url = t.base_url / "api/v1/highlights" in
387387+ let req : create_highlight_request =
388388+ { bookmark_id; start_offset; end_offset; text; note; color }
1239389 in
12401240-12411241- (* Add optional fields *)
12421242- let body_obj =
12431243- match note with
12441244- | Some n -> ("note", `String n) :: body_obj
12451245- | None -> body_obj
12461246- in
12471247- let body_obj =
12481248- match color with
12491249- | Some c -> ("color", `String (string_of_highlight_color c)) :: body_obj
12501250- | None -> body_obj
12511251- in
12521252-12531253- (* Convert to JSON *)
12541254- let body_json = `O body_obj in
12551255- let body_str = J.to_string body_json in
12561256-12571257- (* Make the request *)
12581258- let headers = [ ("Content-Type", "application/json") ] in
12591259- let url = base_url ^ "/api/v1/highlights" in
12601260-12611261- make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url
12621262- >>= fun (resp, body) -> process_response resp body parse_highlight
12631263-12641264-let update_highlight ~api_key ?color base_url highlight_id =
12651265- (* Prepare the update request body *)
12661266- let body_obj = [] in
12671267-12681268- (* Add optional fields *)
12691269- let body_obj =
12701270- match color with
12711271- | Some c -> ("color", `String (string_of_highlight_color c)) :: body_obj
12721272- | None -> body_obj
12731273- in
12741274-12751275- (* Only proceed if there are updates to make *)
12761276- if body_obj = [] then
12771277- (* No updates, just fetch the current highlight *)
12781278- fetch_highlight_details ~api_key base_url highlight_id
12791279- else
12801280- (* Convert to JSON *)
12811281- let body_json = `O body_obj in
12821282- let body_str = J.to_string body_json in
12831283-12841284- (* Make the request *)
12851285- let headers = [ ("Content-Type", "application/json") ] in
12861286- let url = base_url ^ "/api/v1/highlights/" ^ highlight_id in
12871287-12881288- make_request ~api_key ~method_:`PUT ~headers ~body:(Some body_str) url
12891289- >>= fun (resp, body) -> process_response resp body parse_highlight
390390+ post_json t url create_highlight_request_jsont req highlight_jsont
129039112911291-let delete_highlight ~api_key base_url highlight_id =
12921292- let url = base_url ^ "/api/v1/highlights/" ^ highlight_id in
392392+let update_highlight t ?color highlight_id =
393393+ let url = t.base_url / "api/v1/highlights" / highlight_id in
394394+ let req : update_highlight_request = { color } in
395395+ patch_json t url update_highlight_request_jsont req highlight_jsont
129339612941294- make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) ->
12951295- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
12961296- if status_code >= 200 && status_code < 300 then
12971297- consume_body body >>= fun () -> Lwt.return_unit
12981298- else
12991299- Cohttp_lwt.Body.to_string body >>= fun body_str ->
13001300- Lwt.fail_with ("Failed to delete highlight: " ^ body_str)
397397+let delete_highlight t highlight_id =
398398+ let url = t.base_url / "api/v1/highlights" / highlight_id in
399399+ delete_json t url
13014001302401(** {1 Asset Operations} *)
130340213041304-let get_asset_url base_url asset_id =
13051305- Printf.sprintf "%s/api/assets/%s" base_url asset_id
13061306-13071307-let fetch_asset ~api_key base_url asset_id =
13081308- let url = get_asset_url base_url asset_id in
13091309-13101310- let open Cohttp_lwt_unix in
13111311- let headers =
13121312- Cohttp.Header.init () |> fun h ->
13131313- Cohttp.Header.add h "Authorization" ("Bearer " ^ api_key)
13141314- in
13151315-13161316- Client.get ~headers (Uri.of_string url) >>= fun (resp, body) ->
13171317- let status_code = Cohttp.Code.code_of_status resp.status in
13181318- if status_code >= 200 && status_code < 300 then Cohttp_lwt.Body.to_string body
13191319- else
13201320- Cohttp_lwt.Body.to_string body >>= fun body_str ->
13211321- Lwt.fail_with ("Failed to fetch asset: " ^ body_str)
13221322-13231323-let attach_asset ~api_key ~asset_id ~asset_type base_url bookmark_id =
13241324- (* Prepare the asset request body *)
13251325- let body_json =
13261326- `O
13271327- [
13281328- ("assetId", `String asset_id);
13291329- ("assetType", `String (string_of_asset_type asset_type));
13301330- ]
13311331- in
13321332- let body_str = J.to_string body_json in
13331333-13341334- (* Make the request *)
13351335- let headers = [ ("Content-Type", "application/json") ] in
13361336- let url = base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/assets" in
13371337-13381338- make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url
13391339- >>= fun (resp, body) -> process_response resp body parse_asset
13401340-13411341-let replace_asset ~api_key ~new_asset_id base_url bookmark_id asset_id =
13421342- (* Prepare the asset request body *)
13431343- let body_json = `O [ ("newAssetId", `String new_asset_id) ] in
13441344- let body_str = J.to_string body_json in
403403+let fetch_asset t asset_id =
404404+ let url = t.base_url / "api/assets" / asset_id in
405405+ let response = Requests.get t.session url in
406406+ let body = Requests.Response.text response in
407407+ if not (Requests.Response.ok response) then
408408+ handle_error_response (Requests.Response.status_code response) body;
409409+ body
134541013461346- (* Make the request *)
13471347- let headers = [ ("Content-Type", "application/json") ] in
13481348- let url =
13491349- base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/assets/" ^ asset_id
13501350- ^ "/replace"
13511351- in
411411+let get_asset_url t asset_id = t.base_url / "api/assets" / asset_id
135241213531353- make_request ~api_key ~method_:`POST ~headers ~body:(Some body_str) url
13541354- >>= fun (resp, body) ->
13551355- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
13561356- if status_code >= 200 && status_code < 300 then
13571357- consume_body body >>= fun () -> Lwt.return_unit
13581358- else
13591359- Cohttp_lwt.Body.to_string body >>= fun body_str ->
13601360- Lwt.fail_with ("Failed to replace asset: " ^ body_str)
413413+let attach_asset t ~asset_id ~asset_type bookmark_id =
414414+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "assets" in
415415+ let req : attach_asset_request = { id = asset_id; asset_type } in
416416+ post_json t url attach_asset_request_jsont req asset_jsont
136141713621362-let detach_asset ~api_key base_url bookmark_id asset_id =
13631363- let url =
13641364- base_url ^ "/api/v1/bookmarks/" ^ bookmark_id ^ "/assets/" ^ asset_id
13651365- in
418418+let replace_asset t ~new_asset_id bookmark_id asset_id =
419419+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "assets" / asset_id in
420420+ let req : replace_asset_request = { asset_id = new_asset_id } in
421421+ let _ = put_json t url replace_asset_request_jsont req in
422422+ ()
136642313671367- make_request ~api_key ~method_:`DELETE url >>= fun (resp, body) ->
13681368- let status_code = Cohttp.Code.code_of_status resp.Cohttp.Response.status in
13691369- if status_code >= 200 && status_code < 300 then
13701370- consume_body body >>= fun () -> Lwt.return_unit
13711371- else
13721372- Cohttp_lwt.Body.to_string body >>= fun body_str ->
13731373- Lwt.fail_with ("Failed to detach asset: " ^ body_str)
424424+let detach_asset t bookmark_id asset_id =
425425+ let url = t.base_url / "api/v1/bookmarks" / bookmark_id / "assets" / asset_id in
426426+ delete_json t url
13744271375428(** {1 User Operations} *)
137642913771377-let get_current_user ~api_key base_url =
13781378- let url = base_url ^ "/api/v1/user" in
13791379-13801380- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
13811381- process_response resp body parse_user_info
13821382-13831383-let get_user_stats ~api_key base_url =
13841384- let url = base_url ^ "/api/v1/user/stats" in
430430+let get_current_user t =
431431+ let url = t.base_url / "api/v1/user" in
432432+ get_json t url user_info_jsont
138543313861386- make_request ~api_key ~method_:`GET url >>= fun (resp, body) ->
13871387- process_response resp body parse_user_stats
434434+let get_user_stats t =
435435+ let url = t.base_url / "api/v1/user/stats" in
436436+ get_json t url user_stats_jsont
+205-741
lib/karakeep.mli
···11-(** Karakeep API client interface
11+(** Karakeep API client
2233 This module provides a client for interacting with the Karakeep bookmark
44- service API. It allows fetching, creating, and managing bookmarks stored in
55- a Karakeep instance.
44+ service API using Eio for structured concurrency.
6576 {2 Basic Usage}
8798 {[
1010- (* Setup the client *)
1111- let api_key = "your_api_key"
1212- let base_url = "https://hoard.recoil.org"
99+ Eio_main.run @@ fun env ->
1010+ Eio.Switch.run @@ fun sw ->
1111+1212+ (* Create the client *)
1313+ let client =
1414+ Karakeep.create ~sw ~env ~base_url:"https://hoard.recoil.org"
1515+ ~api_key:"your_api_key"
1616+ in
13171418 (* Fetch recent bookmarks *)
1515- let recent_bookmarks =
1616- Karakeep.fetch_bookmarks ~api_key ~limit:10 base_url
1919+ let { bookmarks; next_cursor } = Karakeep.fetch_bookmarks client () in
17201821 (* Fetch all bookmarks (handles pagination automatically) *)
1919- let all_bookmarks = Karakeep.fetch_all_bookmarks ~api_key base_url
2222+ let all_bookmarks = Karakeep.fetch_all_bookmarks client () in
20232124 (* Get a specific bookmark by ID *)
2222- let specific_bookmark =
2323- Karakeep.fetch_bookmark_details ~api_key base_url "bookmark_id"
2525+ let bookmark = Karakeep.fetch_bookmark_details client "bookmark_id" in
24262527 (* Create a new bookmark *)
2628 let new_bookmark =
2727- Karakeep.create_bookmark ~api_key ~url:"https://ocaml.org"
2828- ~title:"OCaml Programming Language" base_url
2929+ Karakeep.create_bookmark client ~url:"https://ocaml.org"
3030+ ~title:"OCaml Programming Language" ()
3131+ in
2932 ]}
30333131- {2 Pagination}
3434+ {2 Error Handling}
32353333- The Karakeep API uses pagination to return large result sets. There are two
3434- ways to handle pagination:
3636+ All operations may raise [Eio.Io] exceptions with {!E} error payload:
35373636- 1. Manually using {!fetch_bookmarks} with offset/cursor parameters for
3737- fine-grained control 2. Automatically using {!fetch_all_bookmarks} which
3838- handles pagination for you
3838+ {[
3939+ try
4040+ let bookmarks = Karakeep.fetch_bookmarks client () in
4141+ (* ... *)
4242+ with
4343+ | Eio.Io (Karakeep.E err, _) ->
4444+ Printf.eprintf "Karakeep error: %s\n" (Karakeep.error_to_string err)
4545+ ]}
39464047 {2 API Key}
41484249 All operations require an API key that can be obtained from your Karakeep
4343- instance. *)
5050+ instance settings. *)
44514545-(** {1 Core Types} *)
5252+(** {1 Protocol Types}
46534747-type asset_id = string
4848-(** Asset identifier type *)
5454+ Re-export all protocol types and codecs from {!Karakeep_proto}. *)
49555050-type bookmark_id = string
5151-(** Bookmark identifier type *)
5656+include module type of Karakeep_proto
52575353-type list_id = string
5454-(** List identifier type *)
5858+(** {1 Error Handling} *)
55595656-type tag_id = string
5757-(** Tag identifier type *)
6060+type error =
6161+ | Api_error of { status : int; code : string; message : string }
6262+ (** HTTP error from the API *)
6363+ | Json_error of { reason : string } (** JSON parsing or encoding error *)
58645959-type highlight_id = string
6060-(** Highlight identifier type *)
6565+(** Eio error type extension *)
6666+type Eio.Exn.err += E of error
61676262-(** Type of content a bookmark can have *)
6363-type bookmark_content_type =
6464- | Link (** A URL to a webpage *)
6565- | Text (** Plain text content *)
6666- | Asset (** An attached asset (image, PDF, etc.) *)
6767- | Unknown (** Unknown content type *)
6868+val err : error -> exn
6969+(** [err e] creates an Eio exception from an error.
7070+ Usage: [raise (err (Api_error { status = 404; code = "not_found"; message = "..." }))] *)
68716969-(** Type of asset *)
7070-type asset_type =
7171- | Screenshot (** Screenshot of a webpage *)
7272- | AssetScreenshot (** Screenshot of an asset *)
7373- | BannerImage (** Banner image *)
7474- | FullPageArchive (** Archive of a full webpage *)
7575- | Video (** Video asset *)
7676- | BookmarkAsset (** Generic bookmark asset *)
7777- | PrecrawledArchive (** Pre-crawled archive *)
7878- | Unknown (** Unknown asset type *)
7272+val is_api_error : error -> bool
7373+(** [is_api_error e] returns [true] if the error is an API error. *)
79748080-(** Type of tagging status *)
8181-type tagging_status =
8282- | Success (** Tagging was successful *)
8383- | Failure (** Tagging failed *)
8484- | Pending (** Tagging is pending *)
7575+val is_not_found : error -> bool
7676+(** [is_not_found e] returns [true] if the error is a 404 Not Found error. *)
85778686-val string_of_tagging_status : tagging_status -> string
7878+val error_to_string : error -> string
7979+(** [error_to_string e] returns a human-readable description of the error. *)
87808888-(** Type of bookmark list *)
8989-type list_type =
9090- | Manual (** List is manually managed *)
9191- | Smart (** List is dynamically generated based on a query *)
8181+val pp_error : Format.formatter -> error -> unit
8282+(** Pretty printer for errors. *)
92839393-(** Highlight color *)
9494-type highlight_color =
9595- | Yellow (** Yellow highlight *)
9696- | Red (** Red highlight *)
9797- | Green (** Green highlight *)
9898- | Blue (** Blue highlight *)
8484+(** {1 Client} *)
9985100100-(** Type of how a tag was attached *)
101101-type tag_attachment_type =
102102- | AI (** Tag was attached by AI *)
103103- | Human (** Tag was attached by a human *)
8686+type t
8787+(** The Karakeep client type. Wraps a Requests session with the base URL
8888+ and authentication. *)
10489105105-val string_of_tag_attachment_type : tag_attachment_type -> string
106106-107107-type link_content = {
108108- url : string; (** The URL of the bookmarked page *)
109109- title : string option; (** Title from the link *)
110110- description : string option; (** Description from the link *)
111111- image_url : string option; (** URL of an image from the link *)
112112- image_asset_id : asset_id option; (** ID of an image asset *)
113113- screenshot_asset_id : asset_id option; (** ID of a screenshot asset *)
114114- full_page_archive_asset_id : asset_id option;
115115- (** ID of a full page archive asset *)
116116- precrawled_archive_asset_id : asset_id option;
117117- (** ID of a pre-crawled archive asset *)
118118- video_asset_id : asset_id option; (** ID of a video asset *)
119119- favicon : string option; (** URL of the favicon *)
120120- html_content : string option; (** HTML content of the page *)
121121- crawled_at : Ptime.t option; (** When the page was crawled *)
122122- author : string option; (** Author of the content *)
123123- publisher : string option; (** Publisher of the content *)
124124- date_published : Ptime.t option; (** When the content was published *)
125125- date_modified : Ptime.t option; (** When the content was last modified *)
126126-}
127127-(** Link content for a bookmark *)
128128-129129-type text_content = {
130130- text : string; (** The text content *)
131131- source_url : string option; (** Optional source URL for the text *)
132132-}
133133-(** Text content for a bookmark *)
134134-135135-type asset_content = {
136136- asset_type : [ `Image | `PDF ]; (** Type of the asset *)
137137- asset_id : asset_id; (** ID of the asset *)
138138- file_name : string option; (** Name of the file *)
139139- source_url : string option; (** Source URL for the asset *)
140140- size : int option; (** Size of the asset in bytes *)
141141- content : string option; (** Extracted content from the asset *)
142142-}
143143-(** Asset content for a bookmark *)
144144-145145-(** Content of a bookmark *)
146146-type content =
147147- | Link of link_content (** Link-type content *)
148148- | Text of text_content (** Text-type content *)
149149- | Asset of asset_content (** Asset-type content *)
150150- | Unknown (** Unknown content type *)
151151-152152-val title : content -> string
153153-(** [title content] extracts a meaningful title from the bookmark content.
154154- For Link content, it returns the title if available, otherwise the URL.
155155- For Text content, it returns a short excerpt from the text.
156156- For Asset content, it returns the filename if available, otherwise a generic title.
157157- For Unknown content, it returns a generic title. *)
158158-159159-type asset = {
160160- id : asset_id; (** ID of the asset *)
161161- asset_type : asset_type; (** Type of the asset *)
162162-}
163163-(** Asset attached to a bookmark *)
9090+val create :
9191+ sw:Eio.Switch.t ->
9292+ env:< clock : _ Eio.Time.clock ; net : _ Eio.Net.t ; fs : Eio.Fs.dir_ty Eio.Path.t ; .. > ->
9393+ base_url:string ->
9494+ api_key:string ->
9595+ t
9696+(** [create ~sw ~env ~base_url ~api_key] creates a new Karakeep client.
16497165165-type bookmark_tag = {
166166- id : tag_id; (** ID of the tag *)
167167- name : string; (** Name of the tag *)
168168- attached_by : tag_attachment_type; (** How the tag was attached *)
169169-}
170170-(** Tag with attachment information *)
171171-172172-type bookmark = {
173173- id : bookmark_id; (** Unique identifier for the bookmark *)
174174- created_at : Ptime.t; (** Timestamp when the bookmark was created *)
175175- modified_at : Ptime.t option; (** Optional timestamp of the last update *)
176176- title : string option; (** Optional title of the bookmarked page *)
177177- archived : bool; (** Whether the bookmark is archived *)
178178- favourited : bool; (** Whether the bookmark is marked as a favorite *)
179179- tagging_status : tagging_status option; (** Status of automatic tagging *)
180180- note : string option; (** Optional user note associated with the bookmark *)
181181- summary : string option; (** Optional AI-generated summary *)
182182- tags : bookmark_tag list; (** Tags associated with the bookmark *)
183183- content : content; (** Content of the bookmark *)
184184- assets : asset list; (** Assets attached to the bookmark *)
185185-}
186186-(** A bookmark from the Karakeep service *)
187187-188188-val bookmark_title : bookmark -> string
189189-(** [bookmark_title bookmark] returns the best available title for a bookmark.
190190- It prioritizes the bookmark's title field if available, and falls back to
191191- extracting a title from the bookmark's content. *)
192192-193193-type paginated_bookmarks = {
194194- bookmarks : bookmark list; (** List of bookmarks in the current page *)
195195- next_cursor : string option; (** Optional cursor for fetching the next page *)
196196-}
197197-(** Paginated response of bookmarks *)
198198-199199-type _list = {
200200- id : list_id; (** ID of the list *)
201201- name : string; (** Name of the list *)
202202- description : string option; (** Optional description of the list *)
203203- icon : string; (** Icon for the list *)
204204- parent_id : list_id option; (** Optional parent list ID *)
205205- list_type : list_type; (** Type of the list *)
206206- query : string option; (** Optional query for smart lists *)
207207-}
208208-(** List in Karakeep *)
209209-210210-type tag = {
211211- id : tag_id; (** ID of the tag *)
212212- name : string; (** Name of the tag *)
213213- num_bookmarks : int; (** Number of bookmarks with this tag *)
214214- num_bookmarks_by_attached_type : (tag_attachment_type * int) list;
215215- (** Number of bookmarks by attachment type *)
216216-}
217217-(** Tag in Karakeep *)
218218-219219-type highlight = {
220220- bookmark_id : bookmark_id; (** ID of the bookmark *)
221221- start_offset : int; (** Start position of the highlight *)
222222- end_offset : int; (** End position of the highlight *)
223223- color : highlight_color; (** Color of the highlight *)
224224- text : string option; (** Text of the highlight *)
225225- note : string option; (** Note for the highlight *)
226226- id : highlight_id; (** ID of the highlight *)
227227- user_id : string; (** ID of the user who created the highlight *)
228228- created_at : Ptime.t; (** When the highlight was created *)
229229-}
230230-(** Highlight in Karakeep *)
231231-232232-type paginated_highlights = {
233233- highlights : highlight list; (** List of highlights in the current page *)
234234- next_cursor : string option; (** Optional cursor for fetching the next page *)
235235-}
236236-(** Paginated response of highlights *)
237237-238238-type user_info = {
239239- id : string; (** ID of the user *)
240240- name : string option; (** Name of the user *)
241241- email : string option; (** Email of the user *)
242242-}
243243-(** User information *)
244244-245245-type user_stats = {
246246- num_bookmarks : int; (** Number of bookmarks *)
247247- num_favorites : int; (** Number of favorite bookmarks *)
248248- num_archived : int; (** Number of archived bookmarks *)
249249- num_tags : int; (** Number of tags *)
250250- num_lists : int; (** Number of lists *)
251251- num_highlights : int; (** Number of highlights *)
252252-}
253253-(** User statistics *)
254254-255255-type error_response = {
256256- code : string; (** Error code *)
257257- message : string; (** Error message *)
258258-}
259259-(** Error response from the API *)
9898+ @param sw Switch for resource management
9999+ @param env Eio environment providing clock and network
100100+ @param base_url Base URL of the Karakeep instance (e.g., "https://hoard.recoil.org")
101101+ @param api_key API key for authentication *)
260102261103(** {1 Bookmark Operations} *)
262104263105val fetch_bookmarks :
264264- api_key:string ->
106106+ t ->
265107 ?limit:int ->
266108 ?cursor:string ->
267109 ?include_content:bool ->
268110 ?archived:bool ->
269111 ?favourited:bool ->
270270- string ->
271271- paginated_bookmarks Lwt.t
272272-(** [fetch_bookmarks ~api_key ?limit ?cursor ?include_content ?archived
273273- ?favourited base_url] fetches a page of bookmarks from a Karakeep instance.
274274-275275- This function provides fine-grained control over pagination with
276276- cursor-based pagination. It returns a {!paginated_bookmarks} that includes
277277- pagination information like the next cursor.
112112+ unit ->
113113+ paginated_bookmarks
114114+(** [fetch_bookmarks client ()] fetches a page of bookmarks.
278115279279- @param api_key API key for authentication
280116 @param limit Number of bookmarks to fetch per page (default: 50)
281281- @param cursor Optional pagination cursor for cursor-based pagination
117117+ @param cursor Optional pagination cursor
282118 @param include_content Whether to include full content (default: true)
283283- @param archived Whether to filter for archived bookmarks
284284- @param favourited Whether to filter for favourited bookmarks
285285- @param base_url Base URL of the Karakeep instance
286286- @return
287287- A Lwt promise with the bookmark response containing a single page of
288288- results *)
119119+ @param archived Filter for archived bookmarks
120120+ @param favourited Filter for favourited bookmarks
121121+ @raise Eio.Io with {!E} on API or network errors *)
289122290123val fetch_all_bookmarks :
291291- api_key:string ->
124124+ t ->
292125 ?page_size:int ->
293126 ?max_pages:int ->
294127 ?archived:bool ->
295128 ?favourited:bool ->
296296- string ->
297297- bookmark list Lwt.t
298298-(** [fetch_all_bookmarks ~api_key ?page_size ?max_pages ?archived ?favourited
299299- base_url] fetches all bookmarks from a Karakeep instance, automatically
300300- handling pagination.
129129+ unit ->
130130+ bookmark list
131131+(** [fetch_all_bookmarks client ()] fetches all bookmarks, handling pagination.
301132302302- This function handles pagination internally and returns a flattened list of
303303- all bookmarks. It will continue making API requests until all pages have
304304- been fetched or the max_pages limit is reached.
305305-306306- Use this function when you:
307307- - Want to retrieve all bookmarks with minimal code
308308- - Don't need pagination metadata
309309- - Are only interested in the bookmarks themselves
310310-311311- @param api_key API key for authentication
312312- @param page_size Number of bookmarks to fetch per page (default: 50)
313313- @param max_pages Maximum number of pages to fetch (None for all pages)
314314- @param archived Whether to filter for archived bookmarks
315315- @param favourited Whether to filter for favourited bookmarks
316316- @param base_url Base URL of the Karakeep instance
317317- @return A Lwt promise with all bookmarks combined into a single list *)
133133+ @param page_size Number of bookmarks per page (default: 50)
134134+ @param max_pages Maximum number of pages to fetch
135135+ @param archived Filter for archived bookmarks
136136+ @param favourited Filter for favourited bookmarks
137137+ @raise Eio.Io with {!E} on API or network errors *)
318138319139val search_bookmarks :
320320- api_key:string ->
140140+ t ->
321141 query:string ->
322142 ?limit:int ->
323143 ?cursor:string ->
324144 ?include_content:bool ->
325325- string ->
326326- paginated_bookmarks Lwt.t
327327-(** [search_bookmarks ~api_key ~query ?limit ?cursor ?include_content base_url]
328328- searches for bookmarks matching a query.
145145+ unit ->
146146+ paginated_bookmarks
147147+(** [search_bookmarks client ~query ()] searches for bookmarks.
329148330330- @param api_key API key for authentication
331331- @param query Search query
332332- @param limit Number of bookmarks to fetch per page (default: 50)
149149+ @param query Search query string
150150+ @param limit Number of results per page (default: 50)
333151 @param cursor Optional pagination cursor
334152 @param include_content Whether to include full content (default: true)
335335- @param base_url Base URL of the Karakeep instance
336336- @return A Lwt promise with the bookmark response containing search results
337337-*)
338338-339339-val fetch_bookmark_details :
340340- api_key:string -> string -> bookmark_id -> bookmark Lwt.t
341341-(** [fetch_bookmark_details ~api_key base_url bookmark_id] fetches detailed
342342- information for a single bookmark by ID.
153153+ @raise Eio.Io with {!E} on API or network errors *)
343154344344- This function retrieves complete details for a specific bookmark identified
345345- by its ID. It provides a convenient way to access a single bookmark's
346346- detailed information.
347347-348348- @param api_key API key for authentication
349349- @param base_url Base URL of the Karakeep instance
350350- @param bookmark_id ID of the bookmark to fetch
351351- @return A Lwt promise with the complete bookmark details *)
155155+val fetch_bookmark_details : t -> bookmark_id -> bookmark
156156+(** [fetch_bookmark_details client id] fetches a single bookmark by ID.
157157+ @raise Eio.Io with {!E} on API or network errors *)
352158353159val create_bookmark :
354354- api_key:string ->
160160+ t ->
355161 url:string ->
356162 ?title:string ->
357163 ?note:string ->
···360166 ?archived:bool ->
361167 ?created_at:Ptime.t ->
362168 ?tags:string list ->
363363- string ->
364364- bookmark Lwt.t
365365-(** [create_bookmark ~api_key ~url ?title ?note ?summary ?favourited ?archived
366366- ?created_at ?tags base_url] creates a new bookmark in Karakeep.
169169+ unit ->
170170+ bookmark
171171+(** [create_bookmark client ~url ()] creates a new URL bookmark.
367172368368- This function adds a new bookmark to the Karakeep instance for the given
369369- URL. It supports setting various bookmark attributes and adding tags.
370370-371371- Example:
372372- {[
373373- let new_bookmark =
374374- create_bookmark ~api_key ~url:"https://ocaml.org"
375375- ~title:"OCaml Programming Language"
376376- ~tags:[ "programming"; "language"; "functional" ]
377377- "https://hoard.recoil.org"
378378- ]}
379379-380380- @param api_key API key for authentication
381173 @param url The URL to bookmark
382382- @param title Optional title for the bookmark
383383- @param note Optional note to add to the bookmark
384384- @param summary Optional summary for the bookmark
385385- @param favourited
386386- Whether the bookmark should be marked as favourite (default: false)
387387- @param archived Whether the bookmark should be archived (default: false)
388388- @param created_at Optional timestamp for when the bookmark was created
389389- @param tags Optional list of tag names to add to the bookmark
390390- @param base_url Base URL of the Karakeep instance
391391- @return A Lwt promise with the created bookmark *)
174174+ @param title Optional title
175175+ @param note Optional note
176176+ @param summary Optional summary
177177+ @param favourited Whether to mark as favourite
178178+ @param archived Whether to archive
179179+ @param created_at Optional creation timestamp
180180+ @param tags Optional list of tag names to add
181181+ @raise Eio.Io with {!E} on API or network errors *)
392182393183val update_bookmark :
394394- api_key:string ->
184184+ t ->
185185+ bookmark_id ->
395186 ?title:string ->
396187 ?note:string ->
397188 ?summary:string ->
398189 ?favourited:bool ->
399190 ?archived:bool ->
400400- ?url:string ->
401401- ?description:string ->
402402- ?author:string ->
403403- ?publisher:string ->
404404- ?date_published:Ptime.t ->
405405- ?date_modified:Ptime.t ->
406406- ?text:string ->
407407- ?asset_content:string ->
408408- string ->
409409- bookmark_id ->
410410- bookmark Lwt.t
411411-(** [update_bookmark ~api_key ?title ?note ?summary ?favourited ?archived ?url
412412- ?description ?author ?publisher ?date_published ?date_modified ?text
413413- ?asset_content base_url bookmark_id] updates a bookmark by its ID.
191191+ unit ->
192192+ bookmark
193193+(** [update_bookmark client id ()] updates a bookmark.
194194+ @raise Eio.Io with {!E} on API or network errors *)
414195415415- This function updates various attributes of an existing bookmark. Only the
416416- fields provided will be updated.
417417-418418- @param api_key API key for authentication
419419- @param title Optional new title for the bookmark
420420- @param note Optional new note for the bookmark
421421- @param summary Optional new summary for the bookmark
422422- @param favourited Whether the bookmark should be marked as favourite
423423- @param archived Whether the bookmark should be archived
424424- @param url Optional new URL for the bookmark
425425- @param description Optional new description for the bookmark
426426- @param author Optional new author for the bookmark
427427- @param publisher Optional new publisher for the bookmark
428428- @param date_published Optional new publication date for the bookmark
429429- @param date_modified Optional new modification date for the bookmark
430430- @param text Optional new text content for the bookmark
431431- @param asset_content Optional new asset content for the bookmark
432432- @param base_url Base URL of the Karakeep instance
433433- @param bookmark_id ID of the bookmark to update
434434- @return A Lwt promise with the updated bookmark details *)
435435-436436-val delete_bookmark : api_key:string -> string -> bookmark_id -> unit Lwt.t
437437-(** [delete_bookmark ~api_key base_url bookmark_id] deletes a bookmark by its
438438- ID.
439439-440440- This function permanently removes a bookmark from the Karakeep instance.
441441-442442- @param api_key API key for authentication
443443- @param base_url Base URL of the Karakeep instance
444444- @param bookmark_id ID of the bookmark to delete
445445- @return A Lwt promise that completes when the bookmark is deleted *)
446446-447447-val summarize_bookmark :
448448- api_key:string -> string -> bookmark_id -> bookmark Lwt.t
449449-(** [summarize_bookmark ~api_key base_url bookmark_id] generates a summary for a
450450- bookmark.
451451-452452- This function uses AI to generate a summary of the bookmark's content. The
453453- summary is added to the bookmark.
196196+val delete_bookmark : t -> bookmark_id -> unit
197197+(** [delete_bookmark client id] deletes a bookmark.
198198+ @raise Eio.Io with {!E} on API or network errors *)
454199455455- @param api_key API key for authentication
456456- @param base_url Base URL of the Karakeep instance
457457- @param bookmark_id ID of the bookmark to summarize
458458- @return A Lwt promise with the updated bookmark including the summary *)
200200+val summarize_bookmark : t -> bookmark_id -> bookmark
201201+(** [summarize_bookmark client id] generates an AI summary for a bookmark.
202202+ @raise Eio.Io with {!E} on API or network errors *)
459203460204(** {1 Tag Operations} *)
461205462206val attach_tags :
463463- api_key:string ->
464464- tag_refs:[ `TagId of tag_id | `TagName of string ] list ->
465465- string ->
466466- bookmark_id ->
467467- tag_id list Lwt.t
468468-(** [attach_tags ~api_key ~tag_refs base_url bookmark_id] attaches tags to a
469469- bookmark.
470470-471471- This function adds one or more tags to a bookmark. Tags can be referred to
472472- either by their ID or by their name. If a tag name is provided and the tag
473473- doesn't exist, it will be created.
474474-475475- @param api_key API key for authentication
476476- @param tag_refs List of tag references (either by ID or name)
477477- @param base_url Base URL of the Karakeep instance
478478- @param bookmark_id ID of the bookmark to tag
479479- @return A Lwt promise with the list of attached tag IDs *)
207207+ t -> tag_refs:[ `TagId of tag_id | `TagName of string ] list -> bookmark_id -> tag_id list
208208+(** [attach_tags client ~tag_refs bookmark_id] attaches tags to a bookmark.
209209+ @raise Eio.Io with {!E} on API or network errors *)
480210481211val detach_tags :
482482- api_key:string ->
483483- tag_refs:[ `TagId of tag_id | `TagName of string ] list ->
484484- string ->
485485- bookmark_id ->
486486- tag_id list Lwt.t
487487-(** [detach_tags ~api_key ~tag_refs base_url bookmark_id] detaches tags from a
488488- bookmark.
489489-490490- This function removes one or more tags from a bookmark. Tags can be referred
491491- to either by their ID or by their name.
492492-493493- @param api_key API key for authentication
494494- @param tag_refs List of tag references (either by ID or name)
495495- @param base_url Base URL of the Karakeep instance
496496- @param bookmark_id ID of the bookmark to remove tags from
497497- @return A Lwt promise with the list of detached tag IDs *)
212212+ t -> tag_refs:[ `TagId of tag_id | `TagName of string ] list -> bookmark_id -> tag_id list
213213+(** [detach_tags client ~tag_refs bookmark_id] detaches tags from a bookmark.
214214+ @raise Eio.Io with {!E} on API or network errors *)
498215499499-val fetch_all_tags : api_key:string -> string -> tag list Lwt.t
500500-(** [fetch_all_tags ~api_key base_url] fetches all tags.
216216+val fetch_all_tags : t -> tag list
217217+(** [fetch_all_tags client] fetches all tags.
218218+ @raise Eio.Io with {!E} on API or network errors *)
501219502502- This function retrieves all tags from the Karakeep instance.
503503-504504- @param api_key API key for authentication
505505- @param base_url Base URL of the Karakeep instance
506506- @return A Lwt promise with the list of all tags *)
507507-508508-val fetch_tag_details : api_key:string -> string -> tag_id -> tag Lwt.t
509509-(** [fetch_tag_details ~api_key base_url tag_id] fetches detailed information
510510- for a single tag by ID.
511511-512512- This function retrieves complete details for a specific tag identified by
513513- its ID.
514514-515515- @param api_key API key for authentication
516516- @param base_url Base URL of the Karakeep instance
517517- @param tag_id ID of the tag to fetch
518518- @return A Lwt promise with the complete tag details *)
220220+val fetch_tag_details : t -> tag_id -> tag
221221+(** [fetch_tag_details client id] fetches a single tag by ID.
222222+ @raise Eio.Io with {!E} on API or network errors *)
519223520224val fetch_bookmarks_with_tag :
521521- api_key:string ->
225225+ t ->
522226 ?limit:int ->
523227 ?cursor:string ->
524228 ?include_content:bool ->
525525- string ->
526229 tag_id ->
527527- paginated_bookmarks Lwt.t
528528-(** [fetch_bookmarks_with_tag ~api_key ?limit ?cursor ?include_content base_url
529529- tag_id] fetches bookmarks with a specific tag.
230230+ paginated_bookmarks
231231+(** [fetch_bookmarks_with_tag client tag_id] fetches bookmarks with a tag.
232232+ @raise Eio.Io with {!E} on API or network errors *)
530233531531- This function retrieves bookmarks that have been tagged with a specific tag.
234234+val update_tag : t -> name:string -> tag_id -> tag
235235+(** [update_tag client ~name tag_id] updates a tag's name.
236236+ @raise Eio.Io with {!E} on API or network errors *)
532237533533- @param api_key API key for authentication
534534- @param limit Number of bookmarks to fetch per page (default: 50)
535535- @param cursor Optional pagination cursor
536536- @param include_content Whether to include full content (default: true)
537537- @param base_url Base URL of the Karakeep instance
538538- @param tag_id ID of the tag to filter by
539539- @return
540540- A Lwt promise with the bookmark response containing bookmarks with the tag
541541-*)
542542-543543-val update_tag : api_key:string -> name:string -> string -> tag_id -> tag Lwt.t
544544-(** [update_tag ~api_key ~name base_url tag_id] updates a tag's name.
545545-546546- This function changes the name of an existing tag.
547547-548548- @param api_key API key for authentication
549549- @param name New name for the tag
550550- @param base_url Base URL of the Karakeep instance
551551- @param tag_id ID of the tag to update
552552- @return A Lwt promise with the updated tag details *)
553553-554554-val delete_tag : api_key:string -> string -> tag_id -> unit Lwt.t
555555-(** [delete_tag ~api_key base_url tag_id] deletes a tag by its ID.
556556-557557- This function permanently removes a tag from the Karakeep instance and
558558- detaches it from all bookmarks.
559559-560560- @param api_key API key for authentication
561561- @param base_url Base URL of the Karakeep instance
562562- @param tag_id ID of the tag to delete
563563- @return A Lwt promise that completes when the tag is deleted *)
238238+val delete_tag : t -> tag_id -> unit
239239+(** [delete_tag client id] deletes a tag.
240240+ @raise Eio.Io with {!E} on API or network errors *)
564241565242(** {1 List Operations} *)
566243567567-val fetch_all_lists : api_key:string -> string -> _list list Lwt.t
568568-(** [fetch_all_lists ~api_key base_url] fetches all lists.
569569-570570- This function retrieves all lists from the Karakeep instance.
571571-572572- @param api_key API key for authentication
573573- @param base_url Base URL of the Karakeep instance
574574- @return A Lwt promise with the list of all lists *)
575575-576576-val fetch_list_details : api_key:string -> string -> list_id -> _list Lwt.t
577577-(** [fetch_list_details ~api_key base_url list_id] fetches detailed information
578578- for a single list by ID.
244244+val fetch_all_lists : t -> _list list
245245+(** [fetch_all_lists client] fetches all lists.
246246+ @raise Eio.Io with {!E} on API or network errors *)
579247580580- This function retrieves complete details for a specific list identified by
581581- its ID.
582582-583583- @param api_key API key for authentication
584584- @param base_url Base URL of the Karakeep instance
585585- @param list_id ID of the list to fetch
586586- @return A Lwt promise with the complete list details *)
248248+val fetch_list_details : t -> list_id -> _list
249249+(** [fetch_list_details client id] fetches a single list by ID.
250250+ @raise Eio.Io with {!E} on API or network errors *)
587251588252val create_list :
589589- api_key:string ->
253253+ t ->
590254 name:string ->
591255 icon:string ->
592256 ?description:string ->
593257 ?parent_id:list_id ->
594258 ?list_type:list_type ->
595259 ?query:string ->
596596- string ->
597597- _list Lwt.t
598598-(** [create_list ~api_key ~name ~icon ?description ?parent_id ?list_type ?query
599599- base_url] creates a new list in Karakeep.
600600-601601- This function adds a new list to the Karakeep instance. Lists can be
602602- hierarchical with parent-child relationships, and can be either manual or
603603- smart (query-based).
604604-605605- @param api_key API key for authentication
606606- @param name Name of the list (max 40 characters)
607607- @param icon Icon for the list
608608- @param description Optional description for the list (max 100 characters)
609609- @param parent_id Optional parent list ID for hierarchical organization
610610- @param list_type Type of the list (default: Manual)
611611- @param query Optional query string for smart lists
612612- @param base_url Base URL of the Karakeep instance
613613- @return A Lwt promise with the created list *)
260260+ unit ->
261261+ _list
262262+(** [create_list client ~name ~icon ()] creates a new list.
263263+ @raise Eio.Io with {!E} on API or network errors *)
614264615265val update_list :
616616- api_key:string ->
266266+ t ->
617267 ?name:string ->
618268 ?description:string ->
619269 ?icon:string ->
620270 ?parent_id:list_id option ->
621271 ?query:string ->
622622- string ->
623272 list_id ->
624624- _list Lwt.t
625625-(** [update_list ~api_key ?name ?description ?icon ?parent_id ?query base_url
626626- list_id] updates a list by its ID.
627627-628628- This function updates various attributes of an existing list. Only the
629629- fields provided will be updated.
630630-631631- @param api_key API key for authentication
632632- @param name Optional new name for the list
633633- @param description Optional new description for the list
634634- @param icon Optional new icon for the list
635635- @param parent_id Optional new parent list ID (use None to remove the parent)
636636- @param query Optional new query for smart lists
637637- @param base_url Base URL of the Karakeep instance
638638- @param list_id ID of the list to update
639639- @return A Lwt promise with the updated list details *)
640640-641641-val delete_list : api_key:string -> string -> list_id -> unit Lwt.t
642642-(** [delete_list ~api_key base_url list_id] deletes a list by its ID.
273273+ _list
274274+(** [update_list client list_id] updates a list.
275275+ @raise Eio.Io with {!E} on API or network errors *)
643276644644- This function permanently removes a list from the Karakeep instance. Note
645645- that this does not delete the bookmarks in the list.
646646-647647- @param api_key API key for authentication
648648- @param base_url Base URL of the Karakeep instance
649649- @param list_id ID of the list to delete
650650- @return A Lwt promise that completes when the list is deleted *)
277277+val delete_list : t -> list_id -> unit
278278+(** [delete_list client id] deletes a list.
279279+ @raise Eio.Io with {!E} on API or network errors *)
651280652281val fetch_bookmarks_in_list :
653653- api_key:string ->
282282+ t ->
654283 ?limit:int ->
655284 ?cursor:string ->
656285 ?include_content:bool ->
657657- string ->
658286 list_id ->
659659- paginated_bookmarks Lwt.t
660660-(** [fetch_bookmarks_in_list ~api_key ?limit ?cursor ?include_content base_url
661661- list_id] fetches bookmarks in a specific list.
287287+ paginated_bookmarks
288288+(** [fetch_bookmarks_in_list client list_id] fetches bookmarks in a list.
289289+ @raise Eio.Io with {!E} on API or network errors *)
662290663663- This function retrieves bookmarks that have been added to a specific list.
291291+val add_bookmark_to_list : t -> list_id -> bookmark_id -> unit
292292+(** [add_bookmark_to_list client list_id bookmark_id] adds a bookmark to a list.
293293+ @raise Eio.Io with {!E} on API or network errors *)
664294665665- @param api_key API key for authentication
666666- @param limit Number of bookmarks to fetch per page (default: 50)
667667- @param cursor Optional pagination cursor
668668- @param include_content Whether to include full content (default: true)
669669- @param base_url Base URL of the Karakeep instance
670670- @param list_id ID of the list to get bookmarks from
671671- @return
672672- A Lwt promise with the bookmark response containing bookmarks in the list
673673-*)
674674-675675-val add_bookmark_to_list :
676676- api_key:string -> string -> list_id -> bookmark_id -> unit Lwt.t
677677-(** [add_bookmark_to_list ~api_key base_url list_id bookmark_id] adds a bookmark
678678- to a list.
679679-680680- This function adds a bookmark to a manual list. Smart lists cannot be
681681- directly modified.
682682-683683- @param api_key API key for authentication
684684- @param base_url Base URL of the Karakeep instance
685685- @param list_id ID of the list to add the bookmark to
686686- @param bookmark_id ID of the bookmark to add
687687- @return A Lwt promise that completes when the bookmark is added to the list
688688-*)
689689-690690-val remove_bookmark_from_list :
691691- api_key:string -> string -> list_id -> bookmark_id -> unit Lwt.t
692692-(** [remove_bookmark_from_list ~api_key base_url list_id bookmark_id] removes a
693693- bookmark from a list.
694694-695695- This function removes a bookmark from a manual list. Smart lists cannot be
696696- directly modified.
697697-698698- @param api_key API key for authentication
699699- @param base_url Base URL of the Karakeep instance
700700- @param list_id ID of the list to remove the bookmark from
701701- @param bookmark_id ID of the bookmark to remove
702702- @return
703703- A Lwt promise that completes when the bookmark is removed from the list *)
295295+val remove_bookmark_from_list : t -> list_id -> bookmark_id -> unit
296296+(** [remove_bookmark_from_list client list_id bookmark_id] removes a bookmark from a list.
297297+ @raise Eio.Io with {!E} on API or network errors *)
704298705299(** {1 Highlight Operations} *)
706300707301val fetch_all_highlights :
708708- api_key:string ->
709709- ?limit:int ->
710710- ?cursor:string ->
711711- string ->
712712- paginated_highlights Lwt.t
713713-(** [fetch_all_highlights ~api_key ?limit ?cursor base_url] fetches all
714714- highlights.
715715-716716- This function retrieves highlights from the Karakeep instance with
717717- pagination.
718718-719719- @param api_key API key for authentication
720720- @param limit Number of highlights to fetch per page (default: 50)
721721- @param cursor Optional pagination cursor
722722- @param base_url Base URL of the Karakeep instance
723723- @return A Lwt promise with the paginated highlights response *)
724724-725725-val fetch_bookmark_highlights :
726726- api_key:string -> string -> bookmark_id -> highlight list Lwt.t
727727-(** [fetch_bookmark_highlights ~api_key base_url bookmark_id] fetches highlights
728728- for a specific bookmark.
729729-730730- This function retrieves all highlights that have been created for a specific
731731- bookmark.
732732-733733- @param api_key API key for authentication
734734- @param base_url Base URL of the Karakeep instance
735735- @param bookmark_id ID of the bookmark to get highlights for
736736- @return A Lwt promise with the list of highlights for the bookmark *)
737737-738738-val fetch_highlight_details :
739739- api_key:string -> string -> highlight_id -> highlight Lwt.t
740740-(** [fetch_highlight_details ~api_key base_url highlight_id] fetches detailed
741741- information for a single highlight by ID.
302302+ t -> ?limit:int -> ?cursor:string -> unit -> paginated_highlights
303303+(** [fetch_all_highlights client ()] fetches all highlights with pagination.
304304+ @raise Eio.Io with {!E} on API or network errors *)
742305743743- This function retrieves complete details for a specific highlight identified
744744- by its ID.
306306+val fetch_bookmark_highlights : t -> bookmark_id -> highlight list
307307+(** [fetch_bookmark_highlights client bookmark_id] fetches highlights for a bookmark.
308308+ @raise Eio.Io with {!E} on API or network errors *)
745309746746- @param api_key API key for authentication
747747- @param base_url Base URL of the Karakeep instance
748748- @param highlight_id ID of the highlight to fetch
749749- @return A Lwt promise with the complete highlight details *)
310310+val fetch_highlight_details : t -> highlight_id -> highlight
311311+(** [fetch_highlight_details client id] fetches a single highlight by ID.
312312+ @raise Eio.Io with {!E} on API or network errors *)
750313751314val create_highlight :
752752- api_key:string ->
315315+ t ->
753316 bookmark_id:bookmark_id ->
754317 start_offset:int ->
755318 end_offset:int ->
756319 text:string ->
757320 ?note:string ->
758321 ?color:highlight_color ->
759759- string ->
760760- highlight Lwt.t
761761-(** [create_highlight ~api_key ~bookmark_id ~start_offset ~end_offset ~text
762762- ?note ?color base_url] creates a new highlight in Karakeep.
763763-764764- This function adds a new highlight to a bookmark in the Karakeep instance.
765765- Highlights mark specific portions of the bookmark's content.
766766-767767- @param api_key API key for authentication
768768- @param bookmark_id ID of the bookmark to highlight
769769- @param start_offset Starting position of the highlight
770770- @param end_offset Ending position of the highlight
771771- @param text Text content of the highlight
772772- @param note Optional note for the highlight
773773- @param color Color of the highlight (default: Yellow)
774774- @param base_url Base URL of the Karakeep instance
775775- @return A Lwt promise with the created highlight *)
776776-777777-val update_highlight :
778778- api_key:string ->
779779- ?color:highlight_color ->
780780- string ->
781781- highlight_id ->
782782- highlight Lwt.t
783783-(** [update_highlight ~api_key ?color base_url highlight_id] updates a highlight
784784- by its ID.
785785-786786- This function updates the color of an existing highlight.
787787-788788- @param api_key API key for authentication
789789- @param color New color for the highlight
790790- @param base_url Base URL of the Karakeep instance
791791- @param highlight_id ID of the highlight to update
792792- @return A Lwt promise with the updated highlight details *)
793793-794794-val delete_highlight : api_key:string -> string -> highlight_id -> unit Lwt.t
795795-(** [delete_highlight ~api_key base_url highlight_id] deletes a highlight by its
796796- ID.
322322+ unit ->
323323+ highlight
324324+(** [create_highlight client ~bookmark_id ~start_offset ~end_offset ~text ()]
325325+ creates a new highlight.
326326+ @raise Eio.Io with {!E} on API or network errors *)
797327798798- This function permanently removes a highlight from the Karakeep instance.
328328+val update_highlight : t -> ?color:highlight_color -> highlight_id -> highlight
329329+(** [update_highlight client highlight_id] updates a highlight.
330330+ @raise Eio.Io with {!E} on API or network errors *)
799331800800- @param api_key API key for authentication
801801- @param base_url Base URL of the Karakeep instance
802802- @param highlight_id ID of the highlight to delete
803803- @return A Lwt promise that completes when the highlight is deleted *)
332332+val delete_highlight : t -> highlight_id -> unit
333333+(** [delete_highlight client id] deletes a highlight.
334334+ @raise Eio.Io with {!E} on API or network errors *)
804335805336(** {1 Asset Operations} *)
806337807807-val fetch_asset : api_key:string -> string -> asset_id -> string Lwt.t
808808-(** [fetch_asset ~api_key base_url asset_id] fetches an asset from the Karakeep
809809- server as a binary string.
810810-811811- Assets can include images, PDFs, or other files attached to bookmarks. This
812812- function retrieves the binary content of an asset by its ID.
338338+val fetch_asset : t -> asset_id -> string
339339+(** [fetch_asset client id] fetches an asset's binary data.
340340+ @raise Eio.Io with {!E} on API or network errors *)
813341814814- @param api_key API key for authentication
815815- @param base_url Base URL of the Karakeep instance
816816- @param asset_id ID of the asset to fetch
817817- @return A Lwt promise with the binary asset data *)
818818-819819-val get_asset_url : string -> asset_id -> string
820820-(** [get_asset_url base_url asset_id] returns the full URL for a given asset ID.
821821-822822- This is a pure function that constructs the URL for an asset without making
823823- any API calls. The returned URL can be used to access the asset directly,
824824- assuming proper authentication.
825825-826826- @param base_url Base URL of the Karakeep instance
827827- @param asset_id ID of the asset
828828- @return The full URL to the asset *)
342342+val get_asset_url : t -> asset_id -> string
343343+(** [get_asset_url client id] returns the URL for an asset. Pure function. *)
829344830345val attach_asset :
831831- api_key:string ->
832832- asset_id:asset_id ->
833833- asset_type:asset_type ->
834834- string ->
835835- bookmark_id ->
836836- asset Lwt.t
837837-(** [attach_asset ~api_key ~asset_id ~asset_type base_url bookmark_id] attaches
838838- an asset to a bookmark.
839839-840840- This function adds an existing asset to a bookmark.
841841-842842- @param api_key API key for authentication
843843- @param asset_id ID of the asset to attach
844844- @param asset_type Type of the asset
845845- @param base_url Base URL of the Karakeep instance
846846- @param bookmark_id ID of the bookmark to attach the asset to
847847- @return A Lwt promise with the attached asset details *)
848848-849849-val replace_asset :
850850- api_key:string ->
851851- new_asset_id:asset_id ->
852852- string ->
853853- bookmark_id ->
854854- asset_id ->
855855- unit Lwt.t
856856-(** [replace_asset ~api_key ~new_asset_id base_url bookmark_id asset_id]
857857- replaces an asset on a bookmark with a new one.
858858-859859- This function replaces an existing asset on a bookmark with a different
860860- asset.
861861-862862- @param api_key API key for authentication
863863- @param new_asset_id ID of the new asset
864864- @param base_url Base URL of the Karakeep instance
865865- @param bookmark_id ID of the bookmark
866866- @param asset_id ID of the asset to replace
867867- @return A Lwt promise that completes when the asset is replaced *)
346346+ t -> asset_id:asset_id -> asset_type:asset_type -> bookmark_id -> asset
347347+(** [attach_asset client ~asset_id ~asset_type bookmark_id] attaches an asset.
348348+ @raise Eio.Io with {!E} on API or network errors *)
868349869869-val detach_asset :
870870- api_key:string -> string -> bookmark_id -> asset_id -> unit Lwt.t
871871-(** [detach_asset ~api_key base_url bookmark_id asset_id] detaches an asset from
872872- a bookmark.
350350+val replace_asset : t -> new_asset_id:asset_id -> bookmark_id -> asset_id -> unit
351351+(** [replace_asset client ~new_asset_id bookmark_id asset_id] replaces an asset.
352352+ @raise Eio.Io with {!E} on API or network errors *)
873353874874- This function removes an asset from a bookmark.
875875-876876- @param api_key API key for authentication
877877- @param base_url Base URL of the Karakeep instance
878878- @param bookmark_id ID of the bookmark
879879- @param asset_id ID of the asset to detach
880880- @return A Lwt promise that completes when the asset is detached *)
354354+val detach_asset : t -> bookmark_id -> asset_id -> unit
355355+(** [detach_asset client bookmark_id asset_id] detaches an asset.
356356+ @raise Eio.Io with {!E} on API or network errors *)
881357882358(** {1 User Operations} *)
883359884884-val get_current_user : api_key:string -> string -> user_info Lwt.t
885885-(** [get_current_user ~api_key base_url] gets information about the current
886886- user.
360360+val get_current_user : t -> user_info
361361+(** [get_current_user client] gets the current user's info.
362362+ @raise Eio.Io with {!E} on API or network errors *)
887363888888- This function retrieves details about the authenticated user.
889889-890890- @param api_key API key for authentication
891891- @param base_url Base URL of the Karakeep instance
892892- @return A Lwt promise with the user information *)
893893-894894-val get_user_stats : api_key:string -> string -> user_stats Lwt.t
895895-(** [get_user_stats ~api_key base_url] gets statistics about the current user.
896896-897897- This function retrieves statistics about the authenticated user, such as the
898898- number of bookmarks, tags, lists, etc.
899899-900900- @param api_key API key for authentication
901901- @param base_url Base URL of the Karakeep instance
902902- @return A Lwt promise with the user statistics *)
364364+val get_user_stats : t -> user_stats
365365+(** [get_user_stats client] gets the current user's statistics.
366366+ @raise Eio.Io with {!E} on API or network errors *)
···11+(** Karakeep API protocol types and JSON codecs *)
22+33+(** {1 Helper Codecs} *)
44+55+let ptime_jsont =
66+ let dec s =
77+ match Ptime.of_rfc3339 s with
88+ | Ok (t, _, _) -> Ok t
99+ | Error _ -> Error (Printf.sprintf "Invalid timestamp: %s" s)
1010+ in
1111+ let enc t =
1212+ let (y, m, d), ((hh, mm, ss), _) = Ptime.to_date_time t in
1313+ Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ" y m d hh mm ss
1414+ in
1515+ Jsont.of_of_string ~kind:"Ptime.t" dec ~enc
1616+1717+let ptime_option_jsont =
1818+ let null = Jsont.null None in
1919+ let some = Jsont.map ~dec:(fun t -> Some t) ~enc:(function Some t -> t | None -> assert false) ptime_jsont in
2020+ Jsont.any ~dec_null:null ~dec_string:some ~enc:(function None -> null | Some _ -> some) ()
2121+2222+(** {1 ID Types} *)
2323+2424+type asset_id = string
2525+type bookmark_id = string
2626+type list_id = string
2727+type tag_id = string
2828+type highlight_id = string
2929+3030+(** {1 Enum Types} *)
3131+3232+type bookmark_content_type = Link | Text | Asset | Unknown
3333+3434+let bookmark_content_type_jsont =
3535+ let dec s =
3636+ match String.lowercase_ascii s with
3737+ | "link" -> Ok Link
3838+ | "text" -> Ok Text
3939+ | "asset" -> Ok Asset
4040+ | _ -> Ok Unknown
4141+ in
4242+ let enc = function
4343+ | Link -> "link"
4444+ | Text -> "text"
4545+ | Asset -> "asset"
4646+ | Unknown -> "unknown"
4747+ in
4848+ Jsont.of_of_string ~kind:"bookmark_content_type" dec ~enc
4949+5050+type asset_type =
5151+ | Screenshot
5252+ | AssetScreenshot
5353+ | BannerImage
5454+ | FullPageArchive
5555+ | Video
5656+ | BookmarkAsset
5757+ | PrecrawledArchive
5858+ | Unknown
5959+6060+let asset_type_jsont =
6161+ let dec s =
6262+ match s with
6363+ | "screenshot" -> Ok Screenshot
6464+ | "assetScreenshot" -> Ok AssetScreenshot
6565+ | "bannerImage" -> Ok BannerImage
6666+ | "fullPageArchive" -> Ok FullPageArchive
6767+ | "video" -> Ok Video
6868+ | "bookmarkAsset" -> Ok BookmarkAsset
6969+ | "precrawledArchive" -> Ok PrecrawledArchive
7070+ | _ -> Ok Unknown
7171+ in
7272+ let enc = function
7373+ | Screenshot -> "screenshot"
7474+ | AssetScreenshot -> "assetScreenshot"
7575+ | BannerImage -> "bannerImage"
7676+ | FullPageArchive -> "fullPageArchive"
7777+ | Video -> "video"
7878+ | BookmarkAsset -> "bookmarkAsset"
7979+ | PrecrawledArchive -> "precrawledArchive"
8080+ | Unknown -> "unknown"
8181+ in
8282+ Jsont.of_of_string ~kind:"asset_type" dec ~enc
8383+8484+type tagging_status = Success | Failure | Pending
8585+8686+let tagging_status_jsont =
8787+ let dec s =
8888+ match String.lowercase_ascii s with
8989+ | "success" -> Ok Success
9090+ | "failure" -> Ok Failure
9191+ | "pending" -> Ok Pending
9292+ | _ -> Ok Pending
9393+ in
9494+ let enc = function
9595+ | Success -> "success"
9696+ | Failure -> "failure"
9797+ | Pending -> "pending"
9898+ in
9999+ Jsont.of_of_string ~kind:"tagging_status" dec ~enc
100100+101101+let string_of_tagging_status = function
102102+ | Success -> "success"
103103+ | Failure -> "failure"
104104+ | Pending -> "pending"
105105+106106+type list_type = Manual | Smart
107107+108108+let list_type_jsont =
109109+ let dec s =
110110+ match String.lowercase_ascii s with
111111+ | "manual" -> Ok Manual
112112+ | "smart" -> Ok Smart
113113+ | _ -> Ok Manual
114114+ in
115115+ let enc = function Manual -> "manual" | Smart -> "smart"
116116+ in
117117+ Jsont.of_of_string ~kind:"list_type" dec ~enc
118118+119119+type highlight_color = Yellow | Red | Green | Blue
120120+121121+let highlight_color_jsont =
122122+ let dec s =
123123+ match String.lowercase_ascii s with
124124+ | "yellow" -> Ok Yellow
125125+ | "red" -> Ok Red
126126+ | "green" -> Ok Green
127127+ | "blue" -> Ok Blue
128128+ | _ -> Ok Yellow
129129+ in
130130+ let enc = function
131131+ | Yellow -> "yellow"
132132+ | Red -> "red"
133133+ | Green -> "green"
134134+ | Blue -> "blue"
135135+ in
136136+ Jsont.of_of_string ~kind:"highlight_color" dec ~enc
137137+138138+let string_of_highlight_color = function
139139+ | Yellow -> "yellow"
140140+ | Red -> "red"
141141+ | Green -> "green"
142142+ | Blue -> "blue"
143143+144144+type tag_attachment_type = AI | Human
145145+146146+let tag_attachment_type_jsont =
147147+ let dec s =
148148+ match String.lowercase_ascii s with
149149+ | "ai" -> Ok AI
150150+ | "human" -> Ok Human
151151+ | _ -> Ok Human
152152+ in
153153+ let enc = function AI -> "ai" | Human -> "human"
154154+ in
155155+ Jsont.of_of_string ~kind:"tag_attachment_type" dec ~enc
156156+157157+let string_of_tag_attachment_type = function AI -> "ai" | Human -> "human"
158158+159159+(** {1 Content Types} *)
160160+161161+type link_content = {
162162+ url : string;
163163+ title : string option;
164164+ description : string option;
165165+ image_url : string option;
166166+ image_asset_id : asset_id option;
167167+ screenshot_asset_id : asset_id option;
168168+ full_page_archive_asset_id : asset_id option;
169169+ precrawled_archive_asset_id : asset_id option;
170170+ video_asset_id : asset_id option;
171171+ favicon : string option;
172172+ html_content : string option;
173173+ crawled_at : Ptime.t option;
174174+ author : string option;
175175+ publisher : string option;
176176+ date_published : Ptime.t option;
177177+ date_modified : Ptime.t option;
178178+}
179179+180180+(** Helper codec for optional string that handles both absent members and null values *)
181181+let string_option = Jsont.option Jsont.string
182182+183183+let link_content_jsont =
184184+ let make url title description image_url image_asset_id screenshot_asset_id
185185+ full_page_archive_asset_id precrawled_archive_asset_id video_asset_id
186186+ favicon html_content crawled_at author publisher date_published date_modified =
187187+ { url; title; description; image_url; image_asset_id; screenshot_asset_id;
188188+ full_page_archive_asset_id; precrawled_archive_asset_id; video_asset_id;
189189+ favicon; html_content; crawled_at; author; publisher; date_published; date_modified }
190190+ in
191191+ Jsont.Object.map ~kind:"link_content" make
192192+ |> Jsont.Object.mem "url" Jsont.string ~enc:(fun l -> l.url)
193193+ |> Jsont.Object.mem "title" string_option ~dec_absent:None ~enc:(fun l -> l.title)
194194+ |> Jsont.Object.mem "description" string_option ~dec_absent:None ~enc:(fun l -> l.description)
195195+ |> Jsont.Object.mem "imageUrl" string_option ~dec_absent:None ~enc:(fun l -> l.image_url)
196196+ |> Jsont.Object.mem "imageAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.image_asset_id)
197197+ |> Jsont.Object.mem "screenshotAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.screenshot_asset_id)
198198+ |> Jsont.Object.mem "fullPageArchiveAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.full_page_archive_asset_id)
199199+ |> Jsont.Object.mem "precrawledArchiveAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.precrawled_archive_asset_id)
200200+ |> Jsont.Object.mem "videoAssetId" string_option ~dec_absent:None ~enc:(fun l -> l.video_asset_id)
201201+ |> Jsont.Object.mem "favicon" string_option ~dec_absent:None ~enc:(fun l -> l.favicon)
202202+ |> Jsont.Object.mem "htmlContent" string_option ~dec_absent:None ~enc:(fun l -> l.html_content)
203203+ |> Jsont.Object.mem "crawledAt" ptime_option_jsont ~dec_absent:None ~enc:(fun l -> l.crawled_at)
204204+ |> Jsont.Object.mem "author" string_option ~dec_absent:None ~enc:(fun l -> l.author)
205205+ |> Jsont.Object.mem "publisher" string_option ~dec_absent:None ~enc:(fun l -> l.publisher)
206206+ |> Jsont.Object.mem "datePublished" ptime_option_jsont ~dec_absent:None ~enc:(fun l -> l.date_published)
207207+ |> Jsont.Object.mem "dateModified" ptime_option_jsont ~dec_absent:None ~enc:(fun l -> l.date_modified)
208208+ |> Jsont.Object.finish
209209+210210+type text_content = {
211211+ text : string;
212212+ source_url : string option;
213213+}
214214+215215+let text_content_jsont =
216216+ let make text source_url = { text; source_url } in
217217+ Jsont.Object.map ~kind:"text_content" make
218218+ |> Jsont.Object.mem "text" Jsont.string ~enc:(fun t -> t.text)
219219+ |> Jsont.Object.mem "sourceUrl" string_option ~dec_absent:None ~enc:(fun t -> t.source_url)
220220+ |> Jsont.Object.finish
221221+222222+type asset_content = {
223223+ asset_type : [ `Image | `PDF ];
224224+ asset_id : asset_id;
225225+ file_name : string option;
226226+ source_url : string option;
227227+ size : int option;
228228+ content : string option;
229229+}
230230+231231+let asset_content_type_jsont =
232232+ let dec s =
233233+ match String.lowercase_ascii s with
234234+ | "image" -> Ok `Image
235235+ | "pdf" -> Ok `PDF
236236+ | _ -> Ok `Image
237237+ in
238238+ let enc = function `Image -> "image" | `PDF -> "pdf"
239239+ in
240240+ Jsont.of_of_string ~kind:"asset_content_type" dec ~enc
241241+242242+let int_option = Jsont.option Jsont.int
243243+244244+let asset_content_jsont =
245245+ let make asset_type asset_id file_name source_url size content =
246246+ { asset_type; asset_id; file_name; source_url; size; content }
247247+ in
248248+ Jsont.Object.map ~kind:"asset_content" make
249249+ |> Jsont.Object.mem "assetType" asset_content_type_jsont ~enc:(fun a -> a.asset_type)
250250+ |> Jsont.Object.mem "assetId" Jsont.string ~enc:(fun a -> a.asset_id)
251251+ |> Jsont.Object.mem "fileName" string_option ~dec_absent:None ~enc:(fun a -> a.file_name)
252252+ |> Jsont.Object.mem "sourceUrl" string_option ~dec_absent:None ~enc:(fun a -> a.source_url)
253253+ |> Jsont.Object.mem "size" int_option ~dec_absent:None ~enc:(fun a -> a.size)
254254+ |> Jsont.Object.mem "content" string_option ~dec_absent:None ~enc:(fun a -> a.content)
255255+ |> Jsont.Object.finish
256256+257257+type content =
258258+ | Link of link_content
259259+ | Text of text_content
260260+ | Asset of asset_content
261261+ | Unknown
262262+263263+(* Content is represented as an object with a "type" field discriminator *)
264264+let content_jsont =
265265+ let link_case = Jsont.Object.Case.map "link" link_content_jsont ~dec:(fun l -> Link l) in
266266+ let text_case = Jsont.Object.Case.map "text" text_content_jsont ~dec:(fun t -> Text t) in
267267+ let asset_case = Jsont.Object.Case.map "asset" asset_content_jsont ~dec:(fun a -> Asset a) in
268268+ let enc_case = function
269269+ | Link l -> Jsont.Object.Case.value link_case l
270270+ | Text t -> Jsont.Object.Case.value text_case t
271271+ | Asset a -> Jsont.Object.Case.value asset_case a
272272+ | Unknown -> Jsont.Object.Case.value link_case { url = ""; title = None; description = None;
273273+ image_url = None; image_asset_id = None; screenshot_asset_id = None;
274274+ full_page_archive_asset_id = None; precrawled_archive_asset_id = None;
275275+ video_asset_id = None; favicon = None; html_content = None; crawled_at = None;
276276+ author = None; publisher = None; date_published = None; date_modified = None }
277277+ in
278278+ let cases = Jsont.Object.Case.[make link_case; make text_case; make asset_case] in
279279+ Jsont.Object.map ~kind:"content" Fun.id
280280+ |> Jsont.Object.case_mem "type" Jsont.string ~enc:Fun.id ~enc_case cases
281281+ |> Jsont.Object.finish
282282+283283+let title content =
284284+ match content with
285285+ | Link l -> Option.value l.title ~default:l.url
286286+ | Text t ->
287287+ let excerpt = if String.length t.text > 50 then String.sub t.text 0 50 ^ "..." else t.text in
288288+ excerpt
289289+ | Asset a -> Option.value a.file_name ~default:"Asset"
290290+ | Unknown -> "Unknown content"
291291+292292+(** {1 Resource Types} *)
293293+294294+type asset = {
295295+ id : asset_id;
296296+ asset_type : asset_type;
297297+}
298298+299299+let asset_jsont =
300300+ let make id asset_type = { id; asset_type } in
301301+ Jsont.Object.map ~kind:"asset" make
302302+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun a -> a.id)
303303+ |> Jsont.Object.mem "assetType" asset_type_jsont ~enc:(fun a -> a.asset_type)
304304+ |> Jsont.Object.finish
305305+306306+type bookmark_tag = {
307307+ id : tag_id;
308308+ name : string;
309309+ attached_by : tag_attachment_type;
310310+}
311311+312312+let bookmark_tag_jsont =
313313+ let make id name attached_by = { id; name; attached_by } in
314314+ Jsont.Object.map ~kind:"bookmark_tag" make
315315+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun t -> t.id)
316316+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun t -> t.name)
317317+ |> Jsont.Object.mem "attachedBy" tag_attachment_type_jsont ~enc:(fun t -> t.attached_by)
318318+ |> Jsont.Object.finish
319319+320320+type bookmark = {
321321+ id : bookmark_id;
322322+ created_at : Ptime.t;
323323+ modified_at : Ptime.t option;
324324+ title : string option;
325325+ archived : bool;
326326+ favourited : bool;
327327+ tagging_status : tagging_status option;
328328+ note : string option;
329329+ summary : string option;
330330+ tags : bookmark_tag list;
331331+ content : content;
332332+ assets : asset list;
333333+}
334334+335335+let bookmark_jsont =
336336+ let make id created_at modified_at title archived favourited tagging_status note summary tags content assets =
337337+ { id; created_at; modified_at; title; archived; favourited; tagging_status; note; summary; tags; content; assets }
338338+ in
339339+ Jsont.Object.map ~kind:"bookmark" make
340340+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun b -> b.id)
341341+ |> Jsont.Object.mem "createdAt" ptime_jsont ~enc:(fun b -> b.created_at)
342342+ |> Jsont.Object.mem "modifiedAt" ptime_option_jsont ~dec_absent:None ~enc:(fun b -> b.modified_at)
343343+ |> Jsont.Object.mem "title" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun b -> b.title)
344344+ |> Jsont.Object.mem "archived" Jsont.bool ~dec_absent:false ~enc:(fun b -> b.archived)
345345+ |> Jsont.Object.mem "favourited" Jsont.bool ~dec_absent:false ~enc:(fun b -> b.favourited)
346346+ |> Jsont.Object.mem "taggingStatus" (Jsont.option tagging_status_jsont) ~dec_absent:None ~enc:(fun b -> b.tagging_status)
347347+ |> Jsont.Object.mem "note" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun b -> b.note)
348348+ |> Jsont.Object.mem "summary" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun b -> b.summary)
349349+ |> Jsont.Object.mem "tags" (Jsont.list bookmark_tag_jsont) ~dec_absent:[] ~enc:(fun b -> b.tags)
350350+ |> Jsont.Object.mem "content" content_jsont ~enc:(fun b -> b.content)
351351+ |> Jsont.Object.mem "assets" (Jsont.list asset_jsont) ~dec_absent:[] ~enc:(fun b -> b.assets)
352352+ |> Jsont.Object.finish
353353+354354+let bookmark_title bookmark =
355355+ match bookmark.title with
356356+ | Some t -> t
357357+ | None -> title bookmark.content
358358+359359+(** {1 Paginated Responses} *)
360360+361361+type paginated_bookmarks = {
362362+ bookmarks : bookmark list;
363363+ next_cursor : string option;
364364+}
365365+366366+let paginated_bookmarks_jsont =
367367+ let make bookmarks next_cursor = { bookmarks; next_cursor } in
368368+ Jsont.Object.map ~kind:"paginated_bookmarks" make
369369+ |> Jsont.Object.mem "bookmarks" (Jsont.list bookmark_jsont) ~dec_absent:[] ~enc:(fun p -> p.bookmarks)
370370+ |> Jsont.Object.mem "nextCursor" string_option ~dec_absent:None ~enc:(fun p -> p.next_cursor)
371371+ |> Jsont.Object.finish
372372+373373+(** {1 List Type} *)
374374+375375+type _list = {
376376+ id : list_id;
377377+ name : string;
378378+ description : string option;
379379+ icon : string;
380380+ parent_id : list_id option;
381381+ list_type : list_type;
382382+ query : string option;
383383+}
384384+385385+let list_jsont =
386386+ let make id name description icon parent_id list_type query =
387387+ { id; name; description; icon; parent_id; list_type; query }
388388+ in
389389+ Jsont.Object.map ~kind:"list" make
390390+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun l -> l.id)
391391+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun l -> l.name)
392392+ |> Jsont.Object.mem "description" string_option ~dec_absent:None ~enc:(fun l -> l.description)
393393+ |> Jsont.Object.mem "icon" Jsont.string ~dec_absent:"" ~enc:(fun l -> l.icon)
394394+ |> Jsont.Object.mem "parentId" string_option ~dec_absent:None ~enc:(fun l -> l.parent_id)
395395+ |> Jsont.Object.mem "type" list_type_jsont ~dec_absent:Manual ~enc:(fun l -> l.list_type)
396396+ |> Jsont.Object.mem "query" string_option ~dec_absent:None ~enc:(fun l -> l.query)
397397+ |> Jsont.Object.finish
398398+399399+type lists_response = { lists : _list list }
400400+401401+let lists_response_jsont =
402402+ let make lists = { lists } in
403403+ Jsont.Object.map ~kind:"lists_response" make
404404+ |> Jsont.Object.mem "lists" (Jsont.list list_jsont) ~dec_absent:[] ~enc:(fun r -> r.lists)
405405+ |> Jsont.Object.finish
406406+407407+(** {1 Tag Types} *)
408408+409409+type tag = {
410410+ id : tag_id;
411411+ name : string;
412412+ num_bookmarks : int;
413413+ num_bookmarks_by_attached_type : (tag_attachment_type * int) list;
414414+}
415415+416416+(* The API returns numBookmarksByAttachedType as an object like {"ai": 5, "human": 10} *)
417417+let num_bookmarks_by_type_jsont =
418418+ let make ai human =
419419+ let result = [] in
420420+ let result = if ai > 0 then (AI, ai) :: result else result in
421421+ let result = if human > 0 then (Human, human) :: result else result in
422422+ result
423423+ in
424424+ let enc_ai lst = List.assoc_opt AI lst |> Option.value ~default:0 in
425425+ let enc_human lst = List.assoc_opt Human lst |> Option.value ~default:0 in
426426+ Jsont.Object.map ~kind:"num_bookmarks_by_type" make
427427+ |> Jsont.Object.mem "ai" Jsont.int ~dec_absent:0 ~enc:enc_ai
428428+ |> Jsont.Object.mem "human" Jsont.int ~dec_absent:0 ~enc:enc_human
429429+ |> Jsont.Object.finish
430430+431431+let tag_jsont =
432432+ let make id name num_bookmarks num_bookmarks_by_attached_type =
433433+ { id; name; num_bookmarks; num_bookmarks_by_attached_type }
434434+ in
435435+ Jsont.Object.map ~kind:"tag" make
436436+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun t -> t.id)
437437+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun t -> t.name)
438438+ |> Jsont.Object.mem "numBookmarks" Jsont.int ~dec_absent:0 ~enc:(fun t -> t.num_bookmarks)
439439+ |> Jsont.Object.mem "numBookmarksByAttachedType" num_bookmarks_by_type_jsont
440440+ ~dec_absent:[] ~enc:(fun t -> t.num_bookmarks_by_attached_type)
441441+ |> Jsont.Object.finish
442442+443443+type tags_response = { tags : tag list }
444444+445445+let tags_response_jsont =
446446+ let make tags = { tags } in
447447+ Jsont.Object.map ~kind:"tags_response" make
448448+ |> Jsont.Object.mem "tags" (Jsont.list tag_jsont) ~dec_absent:[] ~enc:(fun r -> r.tags)
449449+ |> Jsont.Object.finish
450450+451451+(** {1 Highlight Types} *)
452452+453453+type highlight = {
454454+ bookmark_id : bookmark_id;
455455+ start_offset : int;
456456+ end_offset : int;
457457+ color : highlight_color;
458458+ text : string option;
459459+ note : string option;
460460+ id : highlight_id;
461461+ user_id : string;
462462+ created_at : Ptime.t;
463463+}
464464+465465+let highlight_jsont =
466466+ let make bookmark_id start_offset end_offset color text note id user_id created_at =
467467+ { bookmark_id; start_offset; end_offset; color; text; note; id; user_id; created_at }
468468+ in
469469+ Jsont.Object.map ~kind:"highlight" make
470470+ |> Jsont.Object.mem "bookmarkId" Jsont.string ~enc:(fun h -> h.bookmark_id)
471471+ |> Jsont.Object.mem "startOffset" Jsont.int ~enc:(fun h -> h.start_offset)
472472+ |> Jsont.Object.mem "endOffset" Jsont.int ~enc:(fun h -> h.end_offset)
473473+ |> Jsont.Object.mem "color" highlight_color_jsont ~dec_absent:Yellow ~enc:(fun h -> h.color)
474474+ |> Jsont.Object.mem "text" string_option ~dec_absent:None ~enc:(fun h -> h.text)
475475+ |> Jsont.Object.mem "note" string_option ~dec_absent:None ~enc:(fun h -> h.note)
476476+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun h -> h.id)
477477+ |> Jsont.Object.mem "userId" Jsont.string ~enc:(fun h -> h.user_id)
478478+ |> Jsont.Object.mem "createdAt" ptime_jsont ~enc:(fun h -> h.created_at)
479479+ |> Jsont.Object.finish
480480+481481+type paginated_highlights = {
482482+ highlights : highlight list;
483483+ next_cursor : string option;
484484+}
485485+486486+let paginated_highlights_jsont =
487487+ let make highlights next_cursor = { highlights; next_cursor } in
488488+ Jsont.Object.map ~kind:"paginated_highlights" make
489489+ |> Jsont.Object.mem "highlights" (Jsont.list highlight_jsont) ~dec_absent:[] ~enc:(fun p -> p.highlights)
490490+ |> Jsont.Object.mem "nextCursor" string_option ~dec_absent:None ~enc:(fun p -> p.next_cursor)
491491+ |> Jsont.Object.finish
492492+493493+type highlights_response = { highlights : highlight list }
494494+495495+let highlights_response_jsont =
496496+ let make highlights = { highlights } in
497497+ Jsont.Object.map ~kind:"highlights_response" make
498498+ |> Jsont.Object.mem "highlights" (Jsont.list highlight_jsont) ~dec_absent:[] ~enc:(fun r -> r.highlights)
499499+ |> Jsont.Object.finish
500500+501501+(** {1 User Types} *)
502502+503503+type user_info = {
504504+ id : string;
505505+ name : string option;
506506+ email : string option;
507507+}
508508+509509+let user_info_jsont =
510510+ let make id name email = { id; name; email } in
511511+ Jsont.Object.map ~kind:"user_info" make
512512+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun u -> u.id)
513513+ |> Jsont.Object.mem "name" string_option ~dec_absent:None ~enc:(fun u -> u.name)
514514+ |> Jsont.Object.mem "email" string_option ~dec_absent:None ~enc:(fun u -> u.email)
515515+ |> Jsont.Object.finish
516516+517517+type user_stats = {
518518+ num_bookmarks : int;
519519+ num_favorites : int;
520520+ num_archived : int;
521521+ num_tags : int;
522522+ num_lists : int;
523523+ num_highlights : int;
524524+}
525525+526526+let user_stats_jsont =
527527+ let make num_bookmarks num_favorites num_archived num_tags num_lists num_highlights =
528528+ { num_bookmarks; num_favorites; num_archived; num_tags; num_lists; num_highlights }
529529+ in
530530+ Jsont.Object.map ~kind:"user_stats" make
531531+ |> Jsont.Object.mem "numBookmarks" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_bookmarks)
532532+ |> Jsont.Object.mem "numFavourites" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_favorites)
533533+ |> Jsont.Object.mem "numArchived" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_archived)
534534+ |> Jsont.Object.mem "numTags" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_tags)
535535+ |> Jsont.Object.mem "numLists" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_lists)
536536+ |> Jsont.Object.mem "numHighlights" Jsont.int ~dec_absent:0 ~enc:(fun s -> s.num_highlights)
537537+ |> Jsont.Object.finish
538538+539539+(** {1 Error Response} *)
540540+541541+type error_response = {
542542+ code : string;
543543+ message : string;
544544+}
545545+546546+let error_response_jsont =
547547+ let make code message = { code; message } in
548548+ Jsont.Object.map ~kind:"error_response" make
549549+ |> Jsont.Object.mem "code" Jsont.string ~dec_absent:"unknown" ~enc:(fun e -> e.code)
550550+ |> Jsont.Object.mem "message" Jsont.string ~dec_absent:"Unknown error" ~enc:(fun e -> e.message)
551551+ |> Jsont.Object.finish
552552+553553+(** {1 Request Types} *)
554554+555555+type create_bookmark_request = {
556556+ type_ : string;
557557+ url : string option;
558558+ text : string option;
559559+ title : string option;
560560+ note : string option;
561561+ summary : string option;
562562+ archived : bool option;
563563+ favourited : bool option;
564564+ created_at : Ptime.t option;
565565+}
566566+567567+let create_bookmark_request_jsont =
568568+ let make type_ url text title note summary archived favourited created_at =
569569+ { type_; url; text; title; note; summary; archived; favourited; created_at }
570570+ in
571571+ Jsont.Object.map ~kind:"create_bookmark_request" make
572572+ |> Jsont.Object.mem "type" Jsont.string ~enc:(fun r -> r.type_)
573573+ |> Jsont.Object.opt_mem "url" Jsont.string ~enc:(fun r -> r.url)
574574+ |> Jsont.Object.opt_mem "text" Jsont.string ~enc:(fun r -> r.text)
575575+ |> Jsont.Object.opt_mem "title" Jsont.string ~enc:(fun r -> r.title)
576576+ |> Jsont.Object.opt_mem "note" Jsont.string ~enc:(fun r -> r.note)
577577+ |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:(fun r -> r.summary)
578578+ |> Jsont.Object.opt_mem "archived" Jsont.bool ~enc:(fun r -> r.archived)
579579+ |> Jsont.Object.opt_mem "favourited" Jsont.bool ~enc:(fun r -> r.favourited)
580580+ |> Jsont.Object.opt_mem "createdAt" ptime_jsont ~enc:(fun r -> r.created_at)
581581+ |> Jsont.Object.finish
582582+583583+type update_bookmark_request = {
584584+ title : string option;
585585+ note : string option;
586586+ summary : string option;
587587+ archived : bool option;
588588+ favourited : bool option;
589589+}
590590+591591+let update_bookmark_request_jsont =
592592+ let make title note summary archived favourited =
593593+ { title; note; summary; archived; favourited }
594594+ in
595595+ Jsont.Object.map ~kind:"update_bookmark_request" make
596596+ |> Jsont.Object.opt_mem "title" Jsont.string ~enc:(fun r -> r.title)
597597+ |> Jsont.Object.opt_mem "note" Jsont.string ~enc:(fun r -> r.note)
598598+ |> Jsont.Object.opt_mem "summary" Jsont.string ~enc:(fun r -> r.summary)
599599+ |> Jsont.Object.opt_mem "archived" Jsont.bool ~enc:(fun r -> r.archived)
600600+ |> Jsont.Object.opt_mem "favourited" Jsont.bool ~enc:(fun r -> r.favourited)
601601+ |> Jsont.Object.finish
602602+603603+type tag_ref = TagId of tag_id | TagName of string
604604+605605+let tag_ref_jsont =
606606+ (* Each tag ref is an object with either tagId or tagName *)
607607+ let make tagid tagname =
608608+ match tagid, tagname with
609609+ | Some id, _ -> TagId id
610610+ | _, Some name -> TagName name
611611+ | None, None -> TagName ""
612612+ in
613613+ Jsont.Object.map ~kind:"tag_ref" make
614614+ |> Jsont.Object.opt_mem "tagId" Jsont.string ~enc:(function TagId id -> Some id | _ -> None)
615615+ |> Jsont.Object.opt_mem "tagName" Jsont.string ~enc:(function TagName n -> Some n | _ -> None)
616616+ |> Jsont.Object.finish
617617+618618+type attach_tags_request = { tags : tag_ref list }
619619+620620+let attach_tags_request_jsont =
621621+ let make tags = { tags } in
622622+ Jsont.Object.map ~kind:"attach_tags_request" make
623623+ |> Jsont.Object.mem "tags" (Jsont.list tag_ref_jsont) ~enc:(fun r -> r.tags)
624624+ |> Jsont.Object.finish
625625+626626+type attach_tags_response = { attached : tag_id list }
627627+628628+let attach_tags_response_jsont =
629629+ let make attached = { attached } in
630630+ Jsont.Object.map ~kind:"attach_tags_response" make
631631+ |> Jsont.Object.mem "attached" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun r -> r.attached)
632632+ |> Jsont.Object.finish
633633+634634+type detach_tags_response = { detached : tag_id list }
635635+636636+let detach_tags_response_jsont =
637637+ let make detached = { detached } in
638638+ Jsont.Object.map ~kind:"detach_tags_response" make
639639+ |> Jsont.Object.mem "detached" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun r -> r.detached)
640640+ |> Jsont.Object.finish
641641+642642+type create_list_request = {
643643+ name : string;
644644+ icon : string;
645645+ description : string option;
646646+ parent_id : list_id option;
647647+ type_ : string option;
648648+ query : string option;
649649+}
650650+651651+let create_list_request_jsont =
652652+ let make name icon description parent_id type_ query =
653653+ { name; icon; description; parent_id; type_; query }
654654+ in
655655+ Jsont.Object.map ~kind:"create_list_request" make
656656+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
657657+ |> Jsont.Object.mem "icon" Jsont.string ~enc:(fun r -> r.icon)
658658+ |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun r -> r.description)
659659+ |> Jsont.Object.opt_mem "parentId" Jsont.string ~enc:(fun r -> r.parent_id)
660660+ |> Jsont.Object.opt_mem "type" Jsont.string ~enc:(fun r -> r.type_)
661661+ |> Jsont.Object.opt_mem "query" Jsont.string ~enc:(fun r -> r.query)
662662+ |> Jsont.Object.finish
663663+664664+type update_list_request = {
665665+ name : string option;
666666+ icon : string option;
667667+ description : string option;
668668+ parent_id : list_id option option;
669669+ query : string option;
670670+}
671671+672672+let update_list_request_jsont =
673673+ let make name icon description parent_id query =
674674+ (* parent_id here comes from opt_mem so it's already option *)
675675+ { name; icon; description; parent_id = Some parent_id; query }
676676+ in
677677+ Jsont.Object.map ~kind:"update_list_request" make
678678+ |> Jsont.Object.opt_mem "name" Jsont.string ~enc:(fun r -> r.name)
679679+ |> Jsont.Object.opt_mem "icon" Jsont.string ~enc:(fun r -> r.icon)
680680+ |> Jsont.Object.opt_mem "description" Jsont.string ~enc:(fun r -> r.description)
681681+ |> Jsont.Object.opt_mem "parentId" Jsont.string ~enc:(fun r -> Option.join r.parent_id)
682682+ |> Jsont.Object.opt_mem "query" Jsont.string ~enc:(fun r -> r.query)
683683+ |> Jsont.Object.finish
684684+685685+type create_highlight_request = {
686686+ bookmark_id : bookmark_id;
687687+ start_offset : int;
688688+ end_offset : int;
689689+ text : string;
690690+ note : string option;
691691+ color : highlight_color option;
692692+}
693693+694694+let create_highlight_request_jsont =
695695+ let make bookmark_id start_offset end_offset text note color =
696696+ { bookmark_id; start_offset; end_offset; text; note; color }
697697+ in
698698+ Jsont.Object.map ~kind:"create_highlight_request" make
699699+ |> Jsont.Object.mem "bookmarkId" Jsont.string ~enc:(fun r -> r.bookmark_id)
700700+ |> Jsont.Object.mem "startOffset" Jsont.int ~enc:(fun r -> r.start_offset)
701701+ |> Jsont.Object.mem "endOffset" Jsont.int ~enc:(fun r -> r.end_offset)
702702+ |> Jsont.Object.mem "text" Jsont.string ~enc:(fun r -> r.text)
703703+ |> Jsont.Object.opt_mem "note" Jsont.string ~enc:(fun r -> r.note)
704704+ |> Jsont.Object.opt_mem "color" highlight_color_jsont ~enc:(fun r -> r.color)
705705+ |> Jsont.Object.finish
706706+707707+type update_highlight_request = { color : highlight_color option }
708708+709709+let update_highlight_request_jsont =
710710+ let make color = { color } in
711711+ Jsont.Object.map ~kind:"update_highlight_request" make
712712+ |> Jsont.Object.opt_mem "color" highlight_color_jsont ~enc:(fun r -> r.color)
713713+ |> Jsont.Object.finish
714714+715715+type update_tag_request = { name : string }
716716+717717+let update_tag_request_jsont =
718718+ let make name = { name } in
719719+ Jsont.Object.map ~kind:"update_tag_request" make
720720+ |> Jsont.Object.mem "name" Jsont.string ~enc:(fun r -> r.name)
721721+ |> Jsont.Object.finish
722722+723723+type attach_asset_request = {
724724+ id : asset_id;
725725+ asset_type : asset_type;
726726+}
727727+728728+let attach_asset_request_jsont =
729729+ let make id asset_type = { id; asset_type } in
730730+ Jsont.Object.map ~kind:"attach_asset_request" make
731731+ |> Jsont.Object.mem "id" Jsont.string ~enc:(fun r -> r.id)
732732+ |> Jsont.Object.mem "assetType" asset_type_jsont ~enc:(fun r -> r.asset_type)
733733+ |> Jsont.Object.finish
734734+735735+type replace_asset_request = { asset_id : asset_id }
736736+737737+let replace_asset_request_jsont =
738738+ let make asset_id = { asset_id } in
739739+ Jsont.Object.map ~kind:"replace_asset_request" make
740740+ |> Jsont.Object.mem "assetId" Jsont.string ~enc:(fun r -> r.asset_id)
741741+ |> Jsont.Object.finish
+408
lib/proto/karakeep_proto.mli
···11+(** Karakeep API protocol types and JSON codecs
22+33+ This module provides type definitions and jsont codecs for the Karakeep
44+ bookmark service API protocol messages. *)
55+66+(** {1 ID Types} *)
77+88+type asset_id = string
99+(** Asset identifier type *)
1010+1111+type bookmark_id = string
1212+(** Bookmark identifier type *)
1313+1414+type list_id = string
1515+(** List identifier type *)
1616+1717+type tag_id = string
1818+(** Tag identifier type *)
1919+2020+type highlight_id = string
2121+(** Highlight identifier type *)
2222+2323+(** {1 Enum Types} *)
2424+2525+(** Type of content a bookmark can have *)
2626+type bookmark_content_type =
2727+ | Link (** A URL to a webpage *)
2828+ | Text (** Plain text content *)
2929+ | Asset (** An attached asset (image, PDF, etc.) *)
3030+ | Unknown (** Unknown content type *)
3131+3232+val bookmark_content_type_jsont : bookmark_content_type Jsont.t
3333+3434+(** Type of asset *)
3535+type asset_type =
3636+ | Screenshot (** Screenshot of a webpage *)
3737+ | AssetScreenshot (** Screenshot of an asset *)
3838+ | BannerImage (** Banner image *)
3939+ | FullPageArchive (** Archive of a full webpage *)
4040+ | Video (** Video asset *)
4141+ | BookmarkAsset (** Generic bookmark asset *)
4242+ | PrecrawledArchive (** Pre-crawled archive *)
4343+ | Unknown (** Unknown asset type *)
4444+4545+val asset_type_jsont : asset_type Jsont.t
4646+4747+(** Type of tagging status *)
4848+type tagging_status =
4949+ | Success (** Tagging was successful *)
5050+ | Failure (** Tagging failed *)
5151+ | Pending (** Tagging is pending *)
5252+5353+val tagging_status_jsont : tagging_status Jsont.t
5454+val string_of_tagging_status : tagging_status -> string
5555+5656+(** Type of bookmark list *)
5757+type list_type =
5858+ | Manual (** List is manually managed *)
5959+ | Smart (** List is dynamically generated based on a query *)
6060+6161+val list_type_jsont : list_type Jsont.t
6262+6363+(** Highlight color *)
6464+type highlight_color =
6565+ | Yellow (** Yellow highlight *)
6666+ | Red (** Red highlight *)
6767+ | Green (** Green highlight *)
6868+ | Blue (** Blue highlight *)
6969+7070+val highlight_color_jsont : highlight_color Jsont.t
7171+val string_of_highlight_color : highlight_color -> string
7272+7373+(** Type of how a tag was attached *)
7474+type tag_attachment_type =
7575+ | AI (** Tag was attached by AI *)
7676+ | Human (** Tag was attached by a human *)
7777+7878+val tag_attachment_type_jsont : tag_attachment_type Jsont.t
7979+val string_of_tag_attachment_type : tag_attachment_type -> string
8080+8181+(** {1 Content Types} *)
8282+8383+type link_content = {
8484+ url : string; (** The URL of the bookmarked page *)
8585+ title : string option; (** Title from the link *)
8686+ description : string option; (** Description from the link *)
8787+ image_url : string option; (** URL of an image from the link *)
8888+ image_asset_id : asset_id option; (** ID of an image asset *)
8989+ screenshot_asset_id : asset_id option; (** ID of a screenshot asset *)
9090+ full_page_archive_asset_id : asset_id option;
9191+ (** ID of a full page archive asset *)
9292+ precrawled_archive_asset_id : asset_id option;
9393+ (** ID of a pre-crawled archive asset *)
9494+ video_asset_id : asset_id option; (** ID of a video asset *)
9595+ favicon : string option; (** URL of the favicon *)
9696+ html_content : string option; (** HTML content of the page *)
9797+ crawled_at : Ptime.t option; (** When the page was crawled *)
9898+ author : string option; (** Author of the content *)
9999+ publisher : string option; (** Publisher of the content *)
100100+ date_published : Ptime.t option; (** When the content was published *)
101101+ date_modified : Ptime.t option; (** When the content was last modified *)
102102+}
103103+(** Link content for a bookmark *)
104104+105105+val link_content_jsont : link_content Jsont.t
106106+107107+type text_content = {
108108+ text : string; (** The text content *)
109109+ source_url : string option; (** Optional source URL for the text *)
110110+}
111111+(** Text content for a bookmark *)
112112+113113+val text_content_jsont : text_content Jsont.t
114114+115115+type asset_content = {
116116+ asset_type : [ `Image | `PDF ]; (** Type of the asset *)
117117+ asset_id : asset_id; (** ID of the asset *)
118118+ file_name : string option; (** Name of the file *)
119119+ source_url : string option; (** Source URL for the asset *)
120120+ size : int option; (** Size of the asset in bytes *)
121121+ content : string option; (** Extracted content from the asset *)
122122+}
123123+(** Asset content for a bookmark *)
124124+125125+val asset_content_jsont : asset_content Jsont.t
126126+127127+(** Content of a bookmark *)
128128+type content =
129129+ | Link of link_content (** Link-type content *)
130130+ | Text of text_content (** Text-type content *)
131131+ | Asset of asset_content (** Asset-type content *)
132132+ | Unknown (** Unknown content type *)
133133+134134+val content_jsont : content Jsont.t
135135+136136+val title : content -> string
137137+(** [title content] extracts a meaningful title from the bookmark content.
138138+ For Link content, it returns the title if available, otherwise the URL.
139139+ For Text content, it returns a short excerpt from the text.
140140+ For Asset content, it returns the filename if available, otherwise a
141141+ generic title.
142142+ For Unknown content, it returns a generic title. *)
143143+144144+(** {1 Resource Types} *)
145145+146146+type asset = {
147147+ id : asset_id; (** ID of the asset *)
148148+ asset_type : asset_type; (** Type of the asset *)
149149+}
150150+(** Asset attached to a bookmark *)
151151+152152+val asset_jsont : asset Jsont.t
153153+154154+type bookmark_tag = {
155155+ id : tag_id; (** ID of the tag *)
156156+ name : string; (** Name of the tag *)
157157+ attached_by : tag_attachment_type; (** How the tag was attached *)
158158+}
159159+(** Tag with attachment information *)
160160+161161+val bookmark_tag_jsont : bookmark_tag Jsont.t
162162+163163+type bookmark = {
164164+ id : bookmark_id; (** Unique identifier for the bookmark *)
165165+ created_at : Ptime.t; (** Timestamp when the bookmark was created *)
166166+ modified_at : Ptime.t option; (** Optional timestamp of the last update *)
167167+ title : string option; (** Optional title of the bookmarked page *)
168168+ archived : bool; (** Whether the bookmark is archived *)
169169+ favourited : bool; (** Whether the bookmark is marked as a favorite *)
170170+ tagging_status : tagging_status option; (** Status of automatic tagging *)
171171+ note : string option; (** Optional user note associated with the bookmark *)
172172+ summary : string option; (** Optional AI-generated summary *)
173173+ tags : bookmark_tag list; (** Tags associated with the bookmark *)
174174+ content : content; (** Content of the bookmark *)
175175+ assets : asset list; (** Assets attached to the bookmark *)
176176+}
177177+(** A bookmark from the Karakeep service *)
178178+179179+val bookmark_jsont : bookmark Jsont.t
180180+181181+val bookmark_title : bookmark -> string
182182+(** [bookmark_title bookmark] returns the best available title for a bookmark.
183183+ It prioritizes the bookmark's title field if available, and falls back to
184184+ extracting a title from the bookmark's content. *)
185185+186186+(** {1 Paginated Responses} *)
187187+188188+type paginated_bookmarks = {
189189+ bookmarks : bookmark list; (** List of bookmarks in the current page *)
190190+ next_cursor : string option; (** Optional cursor for fetching the next page *)
191191+}
192192+(** Paginated response of bookmarks *)
193193+194194+val paginated_bookmarks_jsont : paginated_bookmarks Jsont.t
195195+196196+(** {1 List Type} *)
197197+198198+type _list = {
199199+ id : list_id; (** ID of the list *)
200200+ name : string; (** Name of the list *)
201201+ description : string option; (** Optional description of the list *)
202202+ icon : string; (** Icon for the list *)
203203+ parent_id : list_id option; (** Optional parent list ID *)
204204+ list_type : list_type; (** Type of the list *)
205205+ query : string option; (** Optional query for smart lists *)
206206+}
207207+(** List in Karakeep *)
208208+209209+val list_jsont : _list Jsont.t
210210+211211+type lists_response = { lists : _list list }
212212+(** Response containing a list of lists *)
213213+214214+val lists_response_jsont : lists_response Jsont.t
215215+216216+(** {1 Tag Types} *)
217217+218218+type tag = {
219219+ id : tag_id; (** ID of the tag *)
220220+ name : string; (** Name of the tag *)
221221+ num_bookmarks : int; (** Number of bookmarks with this tag *)
222222+ num_bookmarks_by_attached_type : (tag_attachment_type * int) list;
223223+ (** Number of bookmarks by attachment type *)
224224+}
225225+(** Tag in Karakeep *)
226226+227227+val tag_jsont : tag Jsont.t
228228+229229+type tags_response = { tags : tag list }
230230+(** Response containing a list of tags *)
231231+232232+val tags_response_jsont : tags_response Jsont.t
233233+234234+(** {1 Highlight Types} *)
235235+236236+type highlight = {
237237+ bookmark_id : bookmark_id; (** ID of the bookmark *)
238238+ start_offset : int; (** Start position of the highlight *)
239239+ end_offset : int; (** End position of the highlight *)
240240+ color : highlight_color; (** Color of the highlight *)
241241+ text : string option; (** Text of the highlight *)
242242+ note : string option; (** Note for the highlight *)
243243+ id : highlight_id; (** ID of the highlight *)
244244+ user_id : string; (** ID of the user who created the highlight *)
245245+ created_at : Ptime.t; (** When the highlight was created *)
246246+}
247247+(** Highlight in Karakeep *)
248248+249249+val highlight_jsont : highlight Jsont.t
250250+251251+type paginated_highlights = {
252252+ highlights : highlight list; (** List of highlights in the current page *)
253253+ next_cursor : string option; (** Optional cursor for fetching the next page *)
254254+}
255255+(** Paginated response of highlights *)
256256+257257+val paginated_highlights_jsont : paginated_highlights Jsont.t
258258+259259+type highlights_response = { highlights : highlight list }
260260+(** Response containing a list of highlights *)
261261+262262+val highlights_response_jsont : highlights_response Jsont.t
263263+264264+(** {1 User Types} *)
265265+266266+type user_info = {
267267+ id : string; (** ID of the user *)
268268+ name : string option; (** Name of the user *)
269269+ email : string option; (** Email of the user *)
270270+}
271271+(** User information *)
272272+273273+val user_info_jsont : user_info Jsont.t
274274+275275+type user_stats = {
276276+ num_bookmarks : int; (** Number of bookmarks *)
277277+ num_favorites : int; (** Number of favorite bookmarks *)
278278+ num_archived : int; (** Number of archived bookmarks *)
279279+ num_tags : int; (** Number of tags *)
280280+ num_lists : int; (** Number of lists *)
281281+ num_highlights : int; (** Number of highlights *)
282282+}
283283+(** User statistics *)
284284+285285+val user_stats_jsont : user_stats Jsont.t
286286+287287+(** {1 Error Response} *)
288288+289289+type error_response = {
290290+ code : string; (** Error code *)
291291+ message : string; (** Error message *)
292292+}
293293+(** Error response from the API *)
294294+295295+val error_response_jsont : error_response Jsont.t
296296+297297+(** {1 Request Types} *)
298298+299299+type create_bookmark_request = {
300300+ type_ : string; (** Bookmark type: "link", "text", or "asset" *)
301301+ url : string option; (** URL for link bookmarks *)
302302+ text : string option; (** Text for text bookmarks *)
303303+ title : string option; (** Optional title *)
304304+ note : string option; (** Optional note *)
305305+ summary : string option; (** Optional summary *)
306306+ archived : bool option; (** Whether to archive *)
307307+ favourited : bool option; (** Whether to favourite *)
308308+ created_at : Ptime.t option; (** Optional creation timestamp *)
309309+}
310310+(** Request to create a bookmark *)
311311+312312+val create_bookmark_request_jsont : create_bookmark_request Jsont.t
313313+314314+type update_bookmark_request = {
315315+ title : string option;
316316+ note : string option;
317317+ summary : string option;
318318+ archived : bool option;
319319+ favourited : bool option;
320320+}
321321+(** Request to update a bookmark *)
322322+323323+val update_bookmark_request_jsont : update_bookmark_request Jsont.t
324324+325325+type tag_ref =
326326+ | TagId of tag_id
327327+ | TagName of string
328328+329329+type attach_tags_request = { tags : tag_ref list }
330330+(** Request to attach tags to a bookmark *)
331331+332332+val attach_tags_request_jsont : attach_tags_request Jsont.t
333333+334334+type attach_tags_response = { attached : tag_id list }
335335+(** Response from attaching tags *)
336336+337337+val attach_tags_response_jsont : attach_tags_response Jsont.t
338338+339339+type detach_tags_response = { detached : tag_id list }
340340+(** Response from detaching tags *)
341341+342342+val detach_tags_response_jsont : detach_tags_response Jsont.t
343343+344344+type create_list_request = {
345345+ name : string;
346346+ icon : string;
347347+ description : string option;
348348+ parent_id : list_id option;
349349+ type_ : string option; (** "manual" or "smart" *)
350350+ query : string option;
351351+}
352352+(** Request to create a list *)
353353+354354+val create_list_request_jsont : create_list_request Jsont.t
355355+356356+type update_list_request = {
357357+ name : string option;
358358+ icon : string option;
359359+ description : string option;
360360+ parent_id : list_id option option; (** None to not update, Some None to clear *)
361361+ query : string option;
362362+}
363363+(** Request to update a list *)
364364+365365+val update_list_request_jsont : update_list_request Jsont.t
366366+367367+type create_highlight_request = {
368368+ bookmark_id : bookmark_id;
369369+ start_offset : int;
370370+ end_offset : int;
371371+ text : string;
372372+ note : string option;
373373+ color : highlight_color option;
374374+}
375375+(** Request to create a highlight *)
376376+377377+val create_highlight_request_jsont : create_highlight_request Jsont.t
378378+379379+type update_highlight_request = { color : highlight_color option }
380380+(** Request to update a highlight *)
381381+382382+val update_highlight_request_jsont : update_highlight_request Jsont.t
383383+384384+type update_tag_request = { name : string }
385385+(** Request to update a tag *)
386386+387387+val update_tag_request_jsont : update_tag_request Jsont.t
388388+389389+type attach_asset_request = {
390390+ id : asset_id;
391391+ asset_type : asset_type;
392392+}
393393+(** Request to attach an asset *)
394394+395395+val attach_asset_request_jsont : attach_asset_request Jsont.t
396396+397397+type replace_asset_request = { asset_id : asset_id }
398398+(** Request to replace an asset *)
399399+400400+val replace_asset_request_jsont : replace_asset_request Jsont.t
401401+402402+(** {1 Helper Codecs} *)
403403+404404+val ptime_jsont : Ptime.t Jsont.t
405405+(** Codec for Ptime.t values (ISO 8601 format) *)
406406+407407+val ptime_option_jsont : Ptime.t option Jsont.t
408408+(** Codec for optional Ptime.t values *)
+62-70
test/asset_test.ml
···11-open Lwt.Infix
21open Karakeep
3243let () =
···87 let ic = open_in ".karakeep-api" in
98 let key = input_line ic in
109 close_in ic;
1111- key
1010+ String.trim key
1211 with _ ->
1312 Printf.eprintf "Error: Could not load API key from .karakeep-api file\n";
1413 exit 1
···1716 (* Test configuration *)
1817 let base_url = "https://hoard.recoil.org" in
19182020- (* Test asset URL and optionally fetch asset *)
2121- let run_test () =
2222- (* First get a bookmark with assets *)
2323- Printf.printf "Fetching bookmarks with assets...\n";
1919+ Eio_main.run @@ fun env ->
2020+ Eio.Switch.run @@ fun sw ->
24212525- Lwt.catch
2626- (fun () ->
2727- fetch_bookmarks ~api_key ~limit:5 base_url >>= fun response ->
2828- (* Find a bookmark with assets *)
2929- let bookmark_with_assets =
3030- List.find_opt (fun b -> List.length b.assets > 0) response.bookmarks
3131- in
2222+ let client = Karakeep.create ~sw ~env ~base_url ~api_key in
32233333- match bookmark_with_assets with
3434- | None ->
3535- Printf.printf "No bookmarks with assets found in the first 5 results.\n";
3636- Lwt.return_unit
3737- | Some bookmark -> (
3838- (* Print assets info *)
3939- let bookmark_title_str = bookmark_title bookmark in
4040- let url =
4141- match bookmark.content with
4242- | Link lc -> lc.url
4343- | Text tc -> Option.value tc.source_url ~default:"(text content)"
4444- | Asset ac -> Option.value ac.source_url ~default:"(asset content)"
4545- | Unknown -> "(unknown content)"
4646- in
4747- Printf.printf "Found bookmark \"%s\" with %d assets: %s\n"
4848- bookmark_title_str
4949- (List.length bookmark.assets)
5050- url;
2424+ (* Test asset URL and optionally fetch asset *)
2525+ Printf.printf "Fetching bookmarks with assets...\n";
51265252- List.iter
5353- (fun asset ->
5454- let asset_type_str =
5555- match asset.asset_type with
5656- | Screenshot -> "screenshot"
5757- | AssetScreenshot -> "assetScreenshot"
5858- | BannerImage -> "bannerImage"
5959- | FullPageArchive -> "fullPageArchive"
6060- | Video -> "video"
6161- | BookmarkAsset -> "bookmarkAsset"
6262- | PrecrawledArchive -> "precrawledArchive"
6363- | Unknown -> "unknown"
6464- in
6565- Printf.printf "- Asset ID: %s, Type: %s\n" asset.id asset_type_str;
2727+ (try
2828+ let response = fetch_bookmarks client ~limit:5 () in
2929+ (* Find a bookmark with assets *)
3030+ let bookmark_with_assets =
3131+ List.find_opt (fun b -> List.length b.assets > 0) response.bookmarks
3232+ in
66336767- (* Get asset URL *)
6868- let asset_url = get_asset_url base_url asset.id in
6969- Printf.printf " URL: %s\n" asset_url)
7070- bookmark.assets;
3434+ match bookmark_with_assets with
3535+ | None ->
3636+ Printf.printf "No bookmarks with assets found in the first 5 results.\n"
3737+ | Some bookmark -> (
3838+ (* Print assets info *)
3939+ let bookmark_title_str = bookmark_title bookmark in
4040+ let url =
4141+ match bookmark.content with
4242+ | Link lc -> lc.url
4343+ | Text tc -> Option.value tc.source_url ~default:"(text content)"
4444+ | Asset ac -> Option.value ac.source_url ~default:"(asset content)"
4545+ | Unknown -> "(unknown content)"
4646+ in
4747+ Printf.printf "Found bookmark \"%s\" with %d assets: %s\n"
4848+ bookmark_title_str
4949+ (List.length bookmark.assets)
5050+ url;
71517272- (* Optionally fetch one asset to verify it works *)
7373- match bookmark.assets with
7474- | asset :: _ ->
7575- Printf.printf "\nFetching asset %s...\n" asset.id;
7676- Lwt.catch
7777- (fun () ->
7878- fetch_asset ~api_key base_url asset.id >>= fun data ->
7979- Printf.printf "Successfully fetched asset. Size: %d bytes\n"
8080- (String.length data);
8181- Lwt.return_unit)
8282- (fun e ->
8383- Printf.printf "Error fetching asset: %s\n" (Printexc.to_string e);
8484- Lwt.return_unit)
8585- | [] -> Lwt.return_unit))
8686- (fun e ->
8787- Printf.printf "Error in asset test: %s\n" (Printexc.to_string e);
8888- Printf.printf "Skipping the asset test due to API error.\n";
8989- Lwt.return_unit)
9090- in
5252+ List.iter
5353+ (fun (asset : asset) ->
5454+ let asset_type_str =
5555+ match asset.asset_type with
5656+ | Screenshot -> "screenshot"
5757+ | AssetScreenshot -> "assetScreenshot"
5858+ | BannerImage -> "bannerImage"
5959+ | FullPageArchive -> "fullPageArchive"
6060+ | Video -> "video"
6161+ | BookmarkAsset -> "bookmarkAsset"
6262+ | PrecrawledArchive -> "precrawledArchive"
6363+ | Unknown -> "unknown"
6464+ in
6565+ Printf.printf "- Asset ID: %s, Type: %s\n" asset.id asset_type_str;
6666+6767+ (* Get asset URL *)
6868+ let asset_url = get_asset_url client asset.id in
6969+ Printf.printf " URL: %s\n" asset_url)
7070+ bookmark.assets;
91719292- (* Run test *)
9393- Lwt_main.run (run_test ())
7272+ (* Optionally fetch one asset to verify it works *)
7373+ match bookmark.assets with
7474+ | asset :: _ -> (
7575+ Printf.printf "\nFetching asset %s...\n" asset.id;
7676+ try
7777+ let data = fetch_asset client asset.id in
7878+ Printf.printf "Successfully fetched asset. Size: %d bytes\n"
7979+ (String.length data)
8080+ with e ->
8181+ Printf.printf "Error fetching asset: %s\n" (Printexc.to_string e))
8282+ | [] -> ())
8383+ with e ->
8484+ Printf.printf "Error in asset test: %s\n" (Printexc.to_string e);
8585+ Printf.printf "Skipping the asset test due to API error.\n")
+29-34
test/create_test.ml
···11-open Lwt.Infix
21open Karakeep
3243let () =
···87 let ic = open_in ".karakeep-api" in
98 let key = input_line ic in
109 close_in ic;
1111- key
1010+ String.trim key
1211 with _ ->
1312 Printf.eprintf "Error: Could not load API key from .karakeep-api file\n";
1413 exit 1
···1716 (* Test configuration *)
1817 let base_url = "https://hoard.recoil.org" in
19182020- (* Test creating a new bookmark *)
2121- let run_test () =
2222- Printf.printf "Creating a new bookmark...\n";
1919+ Eio_main.run @@ fun env ->
2020+ Eio.Switch.run @@ fun sw ->
23212424- let url = "https://ocaml.org" in
2525- let title = "OCaml Programming Language" in
2626- let tags = [ "programming"; "ocaml"; "functional" ] in
2222+ let client = Karakeep.create ~sw ~env ~base_url ~api_key in
27232828- Lwt.catch
2929- (fun () ->
3030- create_bookmark ~api_key ~url ~title ~tags base_url >>= fun bookmark ->
3131- Printf.printf "Successfully created bookmark:\n";
3232- Printf.printf "- ID: %s\n" bookmark.id;
3333- Printf.printf "- Title: %s\n" (bookmark_title bookmark);
2424+ (* Test creating a new bookmark *)
2525+ Printf.printf "Creating a new bookmark...\n";
34263535- let url =
3636- match bookmark.content with
3737- | Link lc -> lc.url
3838- | Text tc -> Option.value tc.source_url ~default:"(text content)"
3939- | Asset ac -> Option.value ac.source_url ~default:"(asset content)"
4040- | Unknown -> "(unknown content)"
4141- in
4242- Printf.printf "- URL: %s\n" url;
4343- Printf.printf "- Created: %s\n" (Ptime.to_rfc3339 bookmark.created_at);
4444- Printf.printf "- Tags: %s\n"
4545- (String.concat ", "
4646- (List.map (fun (tag : bookmark_tag) -> tag.name) bookmark.tags));
2727+ let url = "https://ocaml.org" in
2828+ let title = "OCaml Programming Language" in
2929+ let tags = [ "programming"; "ocaml"; "functional" ] in
47304848- Lwt.return_unit)
4949- (fun e ->
5050- Printf.printf "Error creating bookmark: %s\n" (Printexc.to_string e);
5151- Printf.printf "Skipping the creation test due to API error.\n";
5252- Lwt.return_unit)
5353- in
3131+ (try
3232+ let bookmark = create_bookmark client ~url ~title ~tags () in
3333+ Printf.printf "Successfully created bookmark:\n";
3434+ Printf.printf "- ID: %s\n" bookmark.id;
3535+ Printf.printf "- Title: %s\n" (bookmark_title bookmark);
54365555- (* Run test *)
5656- Lwt_main.run (run_test ())
3737+ let url =
3838+ match bookmark.content with
3939+ | Link lc -> lc.url
4040+ | Text tc -> Option.value tc.source_url ~default:"(text content)"
4141+ | Asset ac -> Option.value ac.source_url ~default:"(asset content)"
4242+ | Unknown -> "(unknown content)"
4343+ in
4444+ Printf.printf "- URL: %s\n" url;
4545+ Printf.printf "- Created: %s\n" (Ptime.to_rfc3339 bookmark.created_at);
4646+ Printf.printf "- Tags: %s\n"
4747+ (String.concat ", "
4848+ (List.map (fun (tag : bookmark_tag) -> tag.name) bookmark.tags))
4949+ with e ->
5050+ Printf.printf "Error creating bookmark: %s\n" (Printexc.to_string e);
5151+ Printf.printf "Skipping the creation test due to API error.\n")
···11+open Karakeep
22+33+let print_bookmark bookmark =
44+ let title = bookmark_title bookmark in
55+ let url =
66+ match bookmark.content with
77+ | Link lc -> lc.url
88+ | Text tc -> Option.value tc.source_url ~default:"(text content)"
99+ | Asset ac -> Option.value ac.source_url ~default:"(asset content)"
1010+ | Unknown -> "(unknown content)"
1111+ in
1212+ let tags_str =
1313+ String.concat ", "
1414+ (List.map (fun (tag : bookmark_tag) -> tag.name) bookmark.tags)
1515+ in
1616+ Printf.printf "- %s\n URL: %s\n Created: %s\n Tags: %s\n---\n\n" title url
1717+ (Ptime.to_rfc3339 bookmark.created_at)
1818+ tags_str
1919+2020+let () =
2121+ (* Load API key from file *)
2222+ let api_key =
2323+ try
2424+ let ic = open_in ".karakeep-api" in
2525+ let key = input_line ic in
2626+ close_in ic;
2727+ String.trim key
2828+ with _ ->
2929+ Printf.eprintf "Error: Could not load API key from .karakeep-api file\n";
3030+ exit 1
3131+ in
3232+3333+ (* Test configuration *)
3434+ let base_url = "https://hoard.recoil.org" in
3535+3636+ Eio_main.run @@ fun env ->
3737+ Eio.Switch.run @@ fun sw ->
3838+3939+ let client = Karakeep.create ~sw ~env ~base_url ~api_key in
4040+4141+ Printf.printf "=== Test: search_bookmarks ===\n";
4242+4343+ (* Use a reliable search term that should return results *)
4444+ let search_term = "ocaml" in
4545+4646+ Printf.printf "Searching for bookmarks with query: \"%s\"\n\n" search_term;
4747+4848+ (try
4949+ (* Search for bookmarks with the search term *)
5050+ let search_results = search_bookmarks client ~query:search_term ~limit:3 () in
5151+5252+ Printf.printf "Found %d matching bookmarks\n" (List.length search_results.bookmarks);
5353+ Printf.printf "Next cursor: %s\n\n"
5454+ (match search_results.next_cursor with Some c -> c | None -> "none");
5555+5656+ (* Display the search results *)
5757+ List.iter print_bookmark search_results.bookmarks;
5858+5959+ (* Test pagination if we have a next page *)
6060+ (match search_results.next_cursor with
6161+ | Some cursor ->
6262+ Printf.printf "=== Testing search pagination ===\n";
6363+ Printf.printf "Fetching next page with cursor: %s\n\n" cursor;
6464+6565+ let next_page = search_bookmarks client ~query:search_term ~limit:3 ~cursor () in
6666+6767+ Printf.printf "Found %d more bookmarks on next page\n\n"
6868+ (List.length next_page.bookmarks);
6969+7070+ List.iter print_bookmark next_page.bookmarks
7171+ | None ->
7272+ Printf.printf "No more pages available for this search query.\n")
7373+ with e ->
7474+ Printf.printf "An error occurred while searching: %s\n" (Printexc.to_string e);
7575+ Printf.printf "\nFalling back to testing with a simple search term: \"ocaml\"\n\n";
7676+7777+ try
7878+ (* Try again with a simple, reliable search term *)
7979+ let search_results = search_bookmarks client ~query:"ocaml" ~limit:3 () in
8080+8181+ Printf.printf "Found %d matching bookmarks\n" (List.length search_results.bookmarks);
8282+ Printf.printf "Next cursor: %s\n\n"
8383+ (match search_results.next_cursor with Some c -> c | None -> "none");
8484+8585+ (* Display the search results *)
8686+ List.iter print_bookmark search_results.bookmarks
8787+ with e ->
8888+ Printf.printf "Fallback search also failed: %s\n" (Printexc.to_string e))
+40-51
test/test.ml
···11-open Lwt.Infix
21open Karakeep
3243let print_bookmark bookmark =
···2524 let ic = open_in ".karakeep-api" in
2625 let key = input_line ic in
2726 close_in ic;
2828- key
2727+ String.trim key
2928 with _ ->
3029 Printf.eprintf "Error: Could not load API key from .karakeep-api file\n";
3130 exit 1
···3433 (* Test configuration *)
3534 let base_url = "https://hoard.recoil.org" in
36353737- (* Test both fetch methods *)
3838- let run_tests () =
3939- Lwt.catch
4040- (fun () ->
4141- (* Test 1: fetch_bookmarks - get a single page with pagination info *)
4242- Printf.printf "=== Test 1: fetch_bookmarks (paginated) ===\n";
4343- fetch_bookmarks ~api_key ~limit:3 base_url >>= fun response ->
4444- Printf.printf "Found bookmarks, showing %d (page 1)\n"
4545- (List.length response.bookmarks);
4646- Printf.printf "Next cursor: %s\n\n"
4747- (match response.next_cursor with Some c -> c | None -> "none");
3636+ Eio_main.run @@ fun env ->
3737+ Eio.Switch.run @@ fun sw ->
48384949- List.iter print_bookmark response.bookmarks;
3939+ let client = Karakeep.create ~sw ~env ~base_url ~api_key in
50405151- (* Test 2: fetch_all_bookmarks - get multiple pages automatically *)
5252- Printf.printf "=== Test 2: fetch_all_bookmarks (with limit) ===\n";
5353- fetch_all_bookmarks ~api_key ~page_size:2 ~max_pages:2 base_url
5454- >>= fun all_bookmarks ->
5555- Printf.printf "Fetched %d bookmarks from up to 2 pages\n\n"
5656- (List.length all_bookmarks);
4141+ (* Test 1: fetch_bookmarks - get a single page with pagination info *)
4242+ Printf.printf "=== Test 1: fetch_bookmarks (paginated) ===\n";
4343+ (try
4444+ let response = fetch_bookmarks client ~limit:3 () in
4545+ Printf.printf "Found bookmarks, showing %d (page 1)\n"
4646+ (List.length response.bookmarks);
4747+ Printf.printf "Next cursor: %s\n\n"
4848+ (match response.next_cursor with Some c -> c | None -> "none");
4949+ List.iter print_bookmark response.bookmarks;
57505858- List.iter print_bookmark
5959- (List.fold_left
6060- (fun acc x -> if List.length acc < 4 then acc @ [ x ] else acc)
6161- [] all_bookmarks);
6262- Printf.printf "... and %d more bookmarks\n\n"
6363- (max 0 (List.length all_bookmarks - 4));
5151+ (* Test 2: fetch_all_bookmarks - get multiple pages automatically *)
5252+ Printf.printf "=== Test 2: fetch_all_bookmarks (with limit) ===\n";
5353+ let all_bookmarks = fetch_all_bookmarks client ~page_size:2 ~max_pages:2 () in
5454+ Printf.printf "Fetched %d bookmarks from up to 2 pages\n\n"
5555+ (List.length all_bookmarks);
64566565- (* Test 3: fetch_bookmark_details - get a specific bookmark *)
6666- match response.bookmarks with
6767- | first_bookmark :: _ ->
6868- Printf.printf "=== Test 3: fetch_bookmark_details ===\n";
6969- Printf.printf "Fetching details for bookmark ID: %s\n\n"
7070- first_bookmark.id;
5757+ List.iter print_bookmark
5858+ (List.fold_left
5959+ (fun acc x -> if List.length acc < 4 then acc @ [ x ] else acc)
6060+ [] all_bookmarks);
6161+ Printf.printf "... and %d more bookmarks\n\n"
6262+ (max 0 (List.length all_bookmarks - 4));
71637272- Lwt.catch
7373- (fun () ->
7474- fetch_bookmark_details ~api_key base_url first_bookmark.id
7575- >>= fun bookmark ->
7676- print_bookmark bookmark;
7777- Lwt.return_unit)
7878- (fun e ->
7979- Printf.printf "Error fetching bookmark details: %s\n" (Printexc.to_string e);
8080- Lwt.return_unit)
8181- | [] ->
8282- Printf.printf "No bookmarks found to test fetch_bookmark_details\n";
8383- Lwt.return_unit)
8484- (fun e ->
8585- Printf.printf "Error in basic tests: %s\n" (Printexc.to_string e);
8686- Printf.printf "Skipping remaining tests due to API error.\n";
8787- Lwt.return_unit)
8888- in
6464+ (* Test 3: fetch_bookmark_details - get a specific bookmark *)
6565+ (match response.bookmarks with
6666+ | first_bookmark :: _ ->
6767+ Printf.printf "=== Test 3: fetch_bookmark_details ===\n";
6868+ Printf.printf "Fetching details for bookmark ID: %s\n\n"
6969+ first_bookmark.id;
89709090- (* Run all tests *)
9191- Lwt_main.run (run_tests ())
7171+ (try
7272+ let bookmark = fetch_bookmark_details client first_bookmark.id in
7373+ print_bookmark bookmark
7474+ with e ->
7575+ Printf.printf "Error fetching bookmark details: %s\n" (Printexc.to_string e))
7676+ | [] ->
7777+ Printf.printf "No bookmarks found to test fetch_bookmark_details\n")
7878+ with e ->
7979+ Printf.printf "Error in basic tests: %s\n" (Printexc.to_string e);
8080+ Printf.printf "Skipping remaining tests due to API error.\n")