freshjots 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 878a80b88154f8d8421f1e2aea48643ecdf00dda54ebdd2d3c758ae35d66a99f
4
+ data.tar.gz: e4e4fe520d5007bc38d93fc29ed96ad1c36eadabfbe09aec36a91dda22cd14dc
5
+ SHA512:
6
+ metadata.gz: 4a34ff09969147fda0da083a3559ed4c4b4751f8f9a48ebd67c750f72079900dd1f0a17cf7b82236354c7b139209fb60fa8069fb7caddbf566b084b667ffe18a
7
+ data.tar.gz: a2619e3b7a4d32837374585fb8a1265ab98f3e28482463bca33d5dce7075a3e491d29fcc1a328b6a1ac4e0f4bbc5744f4fca29588447fd51a80ad2903d434931
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Goran Arsov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # freshjots — Ruby
2
+
3
+ Tiny Ruby client for the [Fresh Jots](https://freshjots.com) API. One file,
4
+ no runtime dependencies (uses `net/http` from stdlib).
5
+
6
+ ## Install
7
+
8
+ ```ruby
9
+ # Gemfile
10
+ gem "freshjots"
11
+ ```
12
+
13
+ ```sh
14
+ bundle install
15
+ ```
16
+
17
+ Or globally:
18
+
19
+ ```sh
20
+ gem install freshjots
21
+ ```
22
+
23
+ ## Use
24
+
25
+ ```ruby
26
+ require "freshjots"
27
+
28
+ # Reads FRESHJOTS_TOKEN from the environment by default.
29
+ client = Freshjots::Client.new
30
+
31
+ # Append text to a note (creates it if missing).
32
+ client.append("cron-jobs-prod", "backup ok #{Time.now.utc.iso8601}")
33
+
34
+ # Read a note's body.
35
+ puts client.note("cron-jobs-prod")[:plain_body]
36
+
37
+ # List your notes.
38
+ client.notes.each { |note| puts "#{note[:filename]}\t#{note[:title]}" }
39
+
40
+ # Create a note. The API derives the filename from the title — for a
41
+ # note addressable by an exact filename, use append instead.
42
+ created = client.create(title: "Research 2026 Q2", body: "Initial outline.")
43
+ puts created[:filename] # server-derived stream name
44
+ ```
45
+
46
+ The whole API is four methods: `notes`, `note(filename)`, `create(title:, body:)`, `append(filename, text)`. `note` and `create` return the note hash directly (no `{ note: … }` wrapper); `notes` returns the array.
47
+
48
+ ## Errors
49
+
50
+ Any non-2xx response raises `Freshjots::ApiError` with `status`, `code`,
51
+ `message`, and (when present) `details` from the API's stable error
52
+ envelope:
53
+
54
+ ```ruby
55
+ begin
56
+ client.append("huge", "x" * 5_000_000)
57
+ rescue Freshjots::ApiError => e
58
+ puts "#{e.status} #{e.code}: #{e.message}"
59
+ # 413 content_too_large: body exceeds the per-note 3 MB cap
60
+ end
61
+ ```
62
+
63
+ Stable error codes: `unauthenticated`, `forbidden`, `not_found`,
64
+ `validation_failed`, `cap_exceeded`, `storage_cap_exceeded`,
65
+ `content_too_large`, `content_type_mismatch`, `rate_limited`. Full list:
66
+ <https://freshjots.com/docs>.
67
+
68
+ ## Auth
69
+
70
+ Mint a token at <https://freshjots.com/settings/api_tokens> (Dev or
71
+ Dev-pro tier required). Set it once:
72
+
73
+ ```sh
74
+ export FRESHJOTS_TOKEN=<your-token>
75
+ ```
76
+
77
+ Or pass explicitly:
78
+
79
+ ```ruby
80
+ Freshjots::Client.new(token: "fjk_…")
81
+ ```
82
+
83
+ ## License
84
+
85
+ MIT.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Freshjots
4
+ VERSION = "0.2.1"
5
+ end
data/lib/freshjots.rb ADDED
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ require_relative "freshjots/version"
8
+
9
+ # Tiny client for the Fresh Jots API (https://freshjots.com/docs).
10
+ #
11
+ # Usage:
12
+ #
13
+ # client = Freshjots::Client.new # reads FRESHJOTS_TOKEN from ENV
14
+ # client.append("cron-jobs-prod", "backup ok")
15
+ # client.note("cron-jobs-prod")[:plain_body]
16
+ # client.create(title: "Deploy log")[:filename] # server-derived
17
+ #
18
+ # All methods raise Freshjots::ApiError on non-2xx, with code/status/details
19
+ # from the API's stable error envelope.
20
+ #
21
+ # Response shapes: GET /notes is the only endpoint that wraps its payload
22
+ # ({ notes: [...] }). show / show-by-filename / create return the note
23
+ # hash at the TOP LEVEL — there is no { note: ... } wrapper.
24
+ module Freshjots
25
+ class ApiError < StandardError
26
+ attr_reader :status, :code, :details
27
+
28
+ def initialize(status:, code:, message:, details: nil)
29
+ super(message)
30
+ @status, @code, @details = status, code, details
31
+ end
32
+ end
33
+
34
+ class Client
35
+ DEFAULT_BASE_URL = "https://freshjots.com/api/v1"
36
+
37
+ def initialize(token: ENV["FRESHJOTS_TOKEN"], base_url: DEFAULT_BASE_URL)
38
+ raise ArgumentError, "FRESHJOTS_TOKEN missing — pass token: or set the env var" if token.nil? || token.empty?
39
+
40
+ @token, @base_url = token, base_url
41
+ end
42
+
43
+ def notes
44
+ request(:get, "/notes")[:notes]
45
+ end
46
+
47
+ # show-by-filename renders the serializer at the top level (no
48
+ # { note: ... } wrapper), so the response *is* the note hash.
49
+ def note(filename)
50
+ request(:get, "/notes/by-filename/#{escape(filename)}")
51
+ end
52
+
53
+ # Create a note. The API permits note[title, plain_body, format, ...]
54
+ # — NOT filename: the server DERIVES the filename from the title. For
55
+ # a note addressable by an exact, caller-chosen filename, use append
56
+ # (the by-filename endpoint creates it with that exact name on first
57
+ # call). Returns the created note hash (top level); read [:filename]
58
+ # for the server-derived stream name.
59
+ def create(title:, body: "")
60
+ if title.nil? || title.to_s.empty?
61
+ raise ArgumentError,
62
+ "create requires a title — the API derives the filename from it. " \
63
+ "For a note addressable by an exact filename, use append."
64
+ end
65
+ payload = { note: { title: title, plain_body: body, format: "plain" } }
66
+ request(:post, "/notes", payload)
67
+ end
68
+
69
+ def append(filename, text)
70
+ request(:post, "/notes/by-filename/#{escape(filename)}/append", { text: text })
71
+ true
72
+ end
73
+
74
+ private
75
+
76
+ def request(method, path, body = nil)
77
+ uri = URI("#{@base_url}#{path}")
78
+ req = Net::HTTP.const_get(method.to_s.capitalize).new(uri)
79
+ req["Authorization"] = "Bearer #{@token}"
80
+ if body
81
+ req["Content-Type"] = "application/json"
82
+ req.body = body.to_json
83
+ end
84
+
85
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
86
+ data = res.body.to_s.empty? ? {} : JSON.parse(res.body, symbolize_names: true)
87
+ raise_for_error!(res, data)
88
+ data
89
+ end
90
+
91
+ def raise_for_error!(res, data)
92
+ return if res.is_a?(Net::HTTPSuccess)
93
+
94
+ err = data[:error] || {}
95
+ raise ApiError.new(
96
+ status: res.code.to_i,
97
+ code: err[:code] || "unknown",
98
+ message: err[:message] || "request failed",
99
+ details: err[:details]
100
+ )
101
+ end
102
+
103
+ def escape(value)
104
+ URI.encode_www_form_component(value.to_s)
105
+ end
106
+ end
107
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: freshjots
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Goran Arsov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Append-only notebooks for cron jobs, deploy scripts, and bots. Wraps
13
+ the four-endpoint plain-text REST API at freshjots.com/api/v1.
14
+ email:
15
+ - arsphy@yahoo.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/freshjots.rb
23
+ - lib/freshjots/version.rb
24
+ homepage: https://github.com/Goran-Arsov/freshjots-ruby
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ homepage_uri: https://github.com/Goran-Arsov/freshjots-ruby
29
+ source_code_uri: https://github.com/Goran-Arsov/freshjots-ruby
30
+ bug_tracker_uri: https://github.com/Goran-Arsov/freshjots-ruby/issues
31
+ documentation_uri: https://freshjots.com/docs
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 3.7.1
47
+ specification_version: 4
48
+ summary: Tiny Ruby client for the Fresh Jots API
49
+ test_files: []