{"id":18837063,"url":"https://github.com/bkuhlmann/tana","last_synced_at":"2025-10-25T11:50:45.125Z","repository":{"id":218832601,"uuid":"747494416","full_name":"bkuhlmann/tana","owner":"bkuhlmann","description":"A monadic API client for the Tana Personal Knowledge Management system.","archived":false,"fork":false,"pushed_at":"2025-06-05T23:04:10.000Z","size":185,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-07-28T18:08:31.124Z","etag":null,"topics":["api","client","knowledge","tana"],"latest_commit_sha":null,"homepage":"https://alchemists.io/projects/tana","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bkuhlmann.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.adoc","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["bkuhlmann"]}},"created_at":"2024-01-24T03:16:44.000Z","updated_at":"2025-06-05T23:00:56.000Z","dependencies_parsed_at":"2024-05-30T16:05:58.312Z","dependency_job_id":"dc831b0a-66d4-4895-92be-f03083eb1e40","html_url":"https://github.com/bkuhlmann/tana","commit_stats":null,"previous_names":["bkuhlmann/tana"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/bkuhlmann/tana","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftana","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftana/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftana/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftana/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bkuhlmann","download_url":"https://codeload.github.com/bkuhlmann/tana/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftana/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268625009,"owners_count":24280188,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-03T02:00:12.545Z","response_time":2577,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","client","knowledge","tana"],"created_at":"2024-11-08T02:33:41.603Z","updated_at":"2025-10-25T11:50:40.087Z","avatar_url":"https://github.com/bkuhlmann.png","language":"Ruby","funding_links":["https://github.com/sponsors/bkuhlmann"],"categories":[],"sub_categories":[],"readme":":toc: macro\n:toclevels: 5\n:figure-caption!:\n\n:api_link: link:https://tana.inc/docs/input-api[Input API]\n:bundler_inline_link: link:https://alchemists.io/articles/ruby_bundler_inline[Bundler Inline]\n:data_link: link:https://alchemists.io/articles/ruby_data[Data]\n:dry_monads_link: link:https://dry-rb.org/gems/dry-monads[Dry Monads]\n:dry_schema_link: link:https://dry-rb.org/gems/dry-schema[Dry Schema]\n:dry_validation_link: link:https://dry-rb.org/gems/dry-validation[Dry Validation]\n:function_composition_link: link:https://alchemists.io/articles/ruby_function_composition[Function Composition]\n:pattern_matching_link: link:https://alchemists.io/articles/ruby_pattern_matching[Pattern Matching]\n:pipeable_link: link:https://alchemists.io/projects/pipeable[Pipeable]\n:tana_link: link:https://tana.inc[Tana]\n\n= Tana\n\n‼️ *This gem is deprecated and will be fully destroyed on 2026-01-15. There is no replacement. Please update accordingly.* ‼️\n\nThis gem is a monadic API client for the {tana_link} Personal Knowledge Management (PKM) system. This allows you to build more sophisticated workflows atop the {api_link} using a design which leverages {function_composition_link} for a powerful, fault tolerant, workflow.\n\nWith this gem, you have a convenient building block to automate your workflows or even use this gem to transfer data from other PKM systems into {tana_link}. 🎉\n\ntoc::[]\n\n== Features\n\n* Provides an API client which implements Tana's limited, early access {api_link}.\n* Provides HTTP request and response verification using {dry_schema_link} and {dry_validation_link}.\n* Uses {function_composition_link} -- coupled with {pipeable_link} -- to process each HTTP request and response.\n\n== Requirements\n\n. link:https://www.ruby-lang.org[Ruby].\n. {tana_link}.\n\n== Setup\n\nTo install _with_ security, run:\n\n[source,bash]\n----\n# 💡 Skip this line if you already have the public certificate installed.\ngem cert --add \u003c(curl --compressed --location https://alchemists.io/gems.pem)\ngem install tana --trust-policy HighSecurity\n----\n\nTo install _without_ security, run:\n\n[source,bash]\n----\ngem install tana\n----\n\nYou can also add the gem directly to your project:\n\n[source,bash]\n----\nbundle add tana\n----\n\nOnce the gem is installed, you only need to require it:\n\n[source,ruby]\n----\nrequire \"tana\"\n----\n\n== Usage\n\nAll interaction is via a `Tana` instance.\n\n=== Initialization\n\nYou can initialize the API client -- using the defaults as described in the _Environment_ section below -- as follows:\n\n[source,ruby]\n----\nclient = Tana.new\n----\n\nFurther customization can be done via a block:\n\n[source,ruby]\n----\nclient = Tana.new do |config|\n  config.accept = \"application/json\"   # Use custom HTTP header.\n  config.token = \"abc123\"              # Use custom/personal API key.\n  config.url = \"https://api.tana.inc\"  # Use custom API root.\nend\n----\n\n=== Environment\n\nEnvironment variable support can be managed using link:https://direnv.net[direnv]. These are the defaults:\n\n[source,bash]\n----\nTANA_API_ACCEPT=application/json\nTANA_API_TOKEN=\nTANA_API_URL=https://europe-west1-tagr-prod.cloudfunctions.net\n----\n\n_You must provide a `TANA_API_TOKEN` value to make authenticated API requests._ This can be done by creating an API token via the API tokens UI:\n\nimage:https://alchemists.io/images/projects/tana/screenshots/api_tokens.png[API Tokens,width=714,height=334,role=focal_point]\n\nHere are the steps to view and make use of the UI shown above:\n\n. Click on _Settings_ within your Tana client.\n. Click on _API tokens_.\n. Click `Create` if you haven't already.\n. Copy and paste your API token as value for `TANA_API_TOKEN` key.\n. Refresh your environment accordingly.\n\n=== Identification\n\nWhen making API requests, you'll often need to acquire the IDs of the nodes you wish to add. This can be done two ways. The first is by selecting the node you are interested in, using `CONTROL + K` to launch the command prompt, and fuzzy type for _Copy link_. Example:\n\nimage:https://alchemists.io/images/projects/tana/screenshots/copy_link.png[Copy Link,width=793,height=512,role=focal_point]\n\nOnce copied the URL might look like `https://app.tana.inc?nodeid=z-p8LdQk6I76` but you'll only need the ID (i.e. `z-p8LdQk6I76`) for API requests.\n\nFor supertags/fields, you can select the node you are interested in using `CONTROL + K` to launch the command prompt and fuzzy type for _Show API schema_. Example:\n\nimage:https://alchemists.io/images/projects/tana/screenshots/show_api_schema.png[Show API Schema,width=578,height=875,role=focal_point]\n\n=== Endpoints\n\nAt the moment, {tana_link} only provides the {api_link} which is a single endpoint for adding nodes only. This API has the following limitations:\n\n* Rate Limiting\n** One call per second per token.\n** Max 100 nodes created per call.\n** Max 5,000 characters in one request.\n* Additional Limitations\n** Can't target a relative Today node.\n** Must know the IDs of the supertag.\n** Each payload is capped at five kilobytes.\n** Can't add a checkbox child to a normal node.\n** No support for child templates.\n** No support for in-application links (i.e. anything that is not a http/https scheme).\n\n==== Add\n\nTo add nodes (i.e. {api_link}), you only need to send the `#add` message. Here's a quick example of adding a simple node to your {tana_link} Inbox.\n\n[source,ruby]\n----\nclient = Tana.new\n\nresult = client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        name: \"With plain node\",\n        description: \"A demonstration.\"\n      }\n    ]\n  }\n)\n\nresult\n# Success(#\u003cdata Tana::Models::Root children=[#\u003cdata Tana::Models::Node id=\"agite1C3Tben\", name=\"With plain node\", description=\"A demonstration.\", type=\"node\", children=[]\u003e]\u003e)\n----\n\nThe above will yield the following in your {tana_link} Inbox:\n\nimage:https://alchemists.io/images/projects/tana/screenshots/inbox.png[Inbox,width=784,height=517,role=focal_point]\n\nYou'll also notice, the result is a monad (i.e. {dry_monads_link}) which means you'll only get a `Success` or `Failure` which you can pipe with additional functionality or use {pattern_matching_link}.\n\nFor successes, you'll be given a {data_link} object with a simple Object API for accessing the children of the response. At the root, you'll have a `Tana::Models::Root` instance which can be one or more `Tana::Models::Node` children. When you unpack the `Success` -- and to illustrate further -- you'll end up with the following:\n\n[source,ruby]\n----\nTana::Models::Root[\n  children: [\n    Tana::Models::Node[\n      id: \"agite1C3Tben\",\n      name: \"With plain node\",\n      description: \"A demonstration.\",\n      type: \"node\",\n      children: []\n    ]\n  ]\n]\n----\n\nThis simplifies and reduces the amount of work you have to do in your own program when processing the API result. For a `Failure`, you either get a `HTTP::Response` or a structured response that is a plain `Hash`. Example:\n\n[source,ruby]\n----\n{\n  \"formErrors\" =\u003e [\"Invalid input\"],\n  \"fieldErrors\" =\u003e {}\n}\n----\n\nUsually, errors are due to invalid authentication credentials or wrong data format. To experiment further, you can use this {bundler_inline_link} script:\n\n[source,ruby]\n----\n#! /usr/bin/env ruby\n# frozen_string_literal: true\n\n# Save as `demo`, then `chmod 755 demo`, and run as `./demo`.\n\nrequire \"bundler/inline\"\n\ngemfile true do\n  source \"https://rubygems.org\"\n\n  gem \"amazing_print\"\n  gem \"debug\"\n  gem \"tana\"\nend\n\nrequire \"base64\"\n\ninclude Dry::Monads[:result]\n\nrender = lambda do |result|\n  case result\n    in Success(record) then puts record\n    in Failure(HTTP::Response =\u003e error) then puts error.body\n    in Failure(error) then ap error.errors\n    else abort \"Unable to process result.\"\n  end\nend\n\nclient = Tana.new\n----\n\nWhen you save the above and run it locally, you have a quick way to experiment with the API print out the results by using the `render` function which uses {pattern_matching_link} that I hinted at earlier. The following are additional examples you can experiment with by adding to the above script:\n\n*With Nesting*\n\nThe following will allow you to create a deeply nested set of nodes. At the moment, your are limited to ten levels deep due to recursion limitations with the {dry_schema_link} and {dry_validation_link} gems.\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        name: \"One\",\n        children: [\n          {\n            name: \"Two\",\n            children: [\n              {\n                name: \"Three\",\n                children: [\n                  {\n                    name: \"Four\",\n                    children: [\n                      {\n                        name: \"Five\",\n                        children: [\n                          {\n                            name: \"Six\",\n                            children: [\n                              {\n                                name: \"Seven\",\n                                children: [\n                                  {\n                                    name: \"Eight\",\n                                    children: [\n                                      name: \"Nine\",\n                                      children: [\n                                        {name: \"Ten\"}\n                                      ]\n                                    ]\n                                  }\n                                ]\n                              }\n                            ]\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  }\n)\n----\n\n*With Field*\n\nThe following allows you to create a node with a field (_you'll want to replace the attribute ID with your ID_).\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        name: \"With field\",\n        description: \"A demonstration.\",\n        children: [\n          {\n            attributeId: \"zM582yzfcs-q\",\n            type: \"field\",\n            children: [\n              {name: \"💡 Idea\"}\n            ]\n          }\n        ]\n      }\n    ]\n  }\n)\n----\n\n*With Supertags*\n\nThe following allows you to create a node with supertags (_you'll want to replace the IDs with your own IDs_).\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        name: \"With supertags\",\n        description: \"A demonstration.\",\n        supertags: [\n          {id: \"S9aMn7puHzUT\"},\n          {id: \"iWKs80kHI0SK\"}\n        ]\n      }\n    ]\n  }\n)\n----\n\n*With Reference*\n\nThe following will allow you to create a node with a reference to another node (_you'll want to replace with your own ID_):\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        dataType: \"reference\",\n        id: \"H-vAUdPi8taR\"\n      }\n    ]\n  }\n)\n----\n\n*With Date*\n\nThe following will allow you to create a node with a date:\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        dataType: \"date\",\n        name: \"2024-01-01T00:00:00Z\"\n      }\n    ]\n  }\n)\n----\n\n*With URL*\n\nThe following will allow you to create a node with a URL field (_you'll want to replace with your own ID_):\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        type: \"field\",\n        attributeId: \"OceDtN8c0CbR\",\n        children: [\n          {\n            dataType: \"url\",\n            name: \"https://alchemists.io\"\n          }\n        ]\n      }\n    ]\n  }\n)\n----\n\n*With Checkbox*\n\nThe following will allow you to create a node with a checkbox:\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        name: \"With checkbox\",\n        dataType: \"boolean\",\n        value: true\n      }\n    ]\n  }\n)\n----\n\n*With Attachment*\n\nThe following will allow you to create a node with an attachment. This requires the _Base64_ gem as shown required in the script above because you need to encode your attachment before making the API request.\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"INBOX\",\n    nodes: [\n      {\n        dataType: \"file\",\n        file: Base64.strict_encode64(Pathname(\"sunglasses.jpeg\").read),\n        filename: \"sunglasses.jpeg\",\n        contentType: \"image/jpeg\"\n      }\n    ]\n  }\n)\n----\n\n*With Schema Field*\n\nThe following will allow you to create a Schema field:\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"SCHEMA\",\n    nodes: [\n      {\n        name: \"demo\",\n        description: \"With Schema field.\",\n        supertags: [{id: \"SYS_T02\"}]\n      }\n    ]\n  }\n)\n----\n\n*With Schema Supertag*\n\nThe following will allow you to create a Schema supertag:\n\n[source,ruby]\n----\nrender.call client.add(\n  {\n    targetNodeId: \"SCHEMA\",\n    nodes: [\n      {\n        name: \"demo\",\n        description: \"With Schema supertag.\",\n        supertags: [{id: \"SYS_T01\"}]\n      }\n    ]\n  }\n)\n----\n\n== Development\n\nTo contribute, run:\n\n[source,bash]\n----\ngit clone https://github.com/bkuhlmann/tana\ncd tana\nbin/setup\n----\n\nYou can also use the IRB console for direct access to all objects:\n\n[source,bash]\n----\nbin/console\n----\n\n== Tests\n\nTo test, run:\n\n[source,bash]\n----\nbin/rake\n----\n\n== link:https://alchemists.io/policies/license[License]\n\n== link:https://alchemists.io/policies/security[Security]\n\n== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]\n\n== link:https://alchemists.io/policies/contributions[Contributions]\n\n== link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin]\n\n== link:https://alchemists.io/projects/tana/versions[Versions]\n\n== link:https://alchemists.io/community[Community]\n\n== Credits\n\n* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].\n* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Ftana","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbkuhlmann%2Ftana","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Ftana/lists"}