Project

freshjots

0.0
The project is in a healthy, maintained state
Append-only notebooks for cron jobs, deploy scripts, and bots. Wraps the plain-text REST API at freshjots.com/api/v1.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 0
 Project Readme

freshjots — Ruby

Tiny Ruby client for the Fresh Jots API. One file, no runtime dependencies (uses net/http from stdlib).

Install

# Gemfile
gem "freshjots"
bundle install

Or globally:

gem install freshjots

Use

require "freshjots"

# Reads FRESHJOTS_TOKEN from the environment by default.
client = Freshjots::Client.new

# Append text to a note (creates it if missing).
client.append("cron-jobs-prod", "backup ok #{Time.now.utc.iso8601}")

# Read a note's body.
puts client.note("cron-jobs-prod")[:plain_body]

# List your notes (most recent activity first). Sort and filter with options:
client.notes(sort: "created", folder_id: 3, limit: 20).each do |note|
  puts "#{note[:id]}\t#{note[:filename]}\t#{note[:title]}"
end

# Create a note. The API derives the filename from the title — for a
# note addressable by an exact filename, use append instead.
created = client.create(title: "Research 2026 Q2", body: "Initial outline.")
puts created[:filename] # server-derived stream name

# Update a note's fields (only the keys you pass change). By id or by filename:
client.update(42, title: "Q2 research", body: "Revised outline.")
client.set("cron-jobs-prod", folder: "Ops", deadline: 26)   # metadata only, no body needed

# Create many notes in one atomic batch (up to 50 — all land or none do).
client.bulk([{ title: "a", plain_body: "1" }, { title: "b", plain_body: "2" }])

# Organize: move into a folder (by id or name), delete (by id or filename).
client.move("cron-jobs-prod", folder: "Ops")
client.delete("old-note")

# Folders: list, read one, create, rename, delete (its notes move to the built-in default folder).
client.folders.each { |f| puts "#{f[:id]}\t#{f[:name]}" }
ops = client.create_folder("Ops")
client.rename_folder(ops[:id], "Operations")
client.delete_folder(ops[:id])

Client methods: notes(sort:, folder_id:, limit:, offset:), note(filename), note_by_id(id), create(title:, body:, client_encrypted:), append(filename, text, client_encrypted:), update(id, **fields), set(filename, **fields), bulk(notes), delete(id_or_filename), move(id_or_filename, folder:), folders, folder(id), create_folder(name), rename_folder(id, name), and delete_folder(id). Client-side crypto: Freshjots.encrypt(text, passphrase) / Freshjots.decrypt(token, passphrase) (see Encryption). note/note_by_id/create/update/set and the single-folder methods return the hash directly (no { note: … } / { folder: … } wrapper); notes and folders return arrays, and bulk returns { created: [...] }. For update/set, the fields are title:, body:, folder: (id or name — root: true un-folders), deadline:, alert_email:, webhook_url:, webhook_secret:; changing title: rewrites the body as a unit, so pass body: too. For notes, sort is created|updated|appended and folder_id may be a folder id or "none" (un-foldered only).

Encryption

Keep notes the server can't read: encrypt locally with your own passphrase, store the ciphertext, decrypt locally on read. Built in on Ruby's stdlib openssl (no gem dependencies), and interoperable with the JS and Python clients.

require "freshjots"

client = Freshjots::Client.new
pw = ENV.fetch("FRESHJOTS_PASSPHRASE")

# Store an encrypted note: encrypt the body, flag it client_encrypted.
client.create(title: "Recovery codes", body: Freshjots.encrypt("1234-5678", pw), client_encrypted: true)

# Read it back and decrypt locally.
puts Freshjots.decrypt(client.note("recovery-codes")[:plain_body], pw)

The format is fj1 (AES-256-CBC + HMAC-SHA256, PBKDF2-HMAC-SHA256), interoperable with the JS, Python, MCP, and shell (brew) clients. You hold the only key — Fresh Jots never receives it and cannot recover the note if you lose it, so back the passphrase up somewhere safe. Encryption is per-note and personal-only (not team notes); the title and metadata stay in the clear, so keep secrets out of the title. Freshjots.decrypt raises Freshjots::EncryptionError on a wrong passphrase. See https://freshjots.com/encrypted-notes.

Errors

Any non-2xx response raises Freshjots::ApiError with status, code, message, and (when present) details from the API's stable error envelope:

begin
  client.append("huge", "x" * 5_000_000)
rescue Freshjots::ApiError => e
  puts "#{e.status} #{e.code}: #{e.message}"
  # 413 content_too_large: body exceeds the per-note 3 MB cap
end

Stable error codes: unauthenticated, forbidden, not_found, validation_failed, cap_exceeded, storage_cap_exceeded, content_too_large, content_type_mismatch, rate_limited. Full list: https://freshjots.com/docs.

Auth

Mint a token at https://freshjots.com/settings/api_tokens (Dev or Team tier required). Set it once, persisted for every new shell (macOS defaults to zsh; use ~/.bashrc on bash):

echo 'export FRESHJOTS_TOKEN=<your-token>' >> ~/.zshrc && source ~/.zshrc

Or pass explicitly:

Freshjots::Client.new(token: "mn_…")

License

MIT.