Memory NoteというプログラマブルなTodoアプリのミドルウェアを書きました。 ややこしいですが、大雑把に言えばReminder的なTodoリストを扱うREST APIをCloudflare Workersで動かす仕組みです。 Headless Todo Appという単語がしっくりくるのかもしれません。

単体だと何ができるのかよくわからないものですが、Todoサービスを自分用に作れる仕組みです。 対象ユーザーは主に自分ですが、Memory NoteのREADMEにセットアップ方法や関連するクライアントの実装も公開しています。

自分の場合は、iOSのショートカットから音声入力で、メモをGitHub Projectのボードにカードして記録しています。 この記録したメモを、iOSのWidgetsとしてホーム画面に出したり、AlfredのHotKeyでワンタッチで表示したり、部屋に電子ペーパーを使った物理的なダッシュボードがあるので、そこに表示しています。 また、GitHub Actionsで買い物っぽいメモは買い物リストに自動的に整理して、"買い物メモリー"とSiriに言えば、その内容を音声で教えてくれるような形にしています。

GitHub Project Board

GitHub Project Boardに記録されているメモ

iOS Widget

Web WidgetをiOS Widgetsとして表示している様子 Siriで読み上げにも対応している

mac Alfred

Alfred workflow でメモを表示してる様子

この記事の目的

この記事はなんでMemory Noteを作ったのかやなんでこんな仕組みになったのかを書いているメモ的な記事です。

実際に使いたい人は、リポジトリのREADMEやコード(数百行ぐらいです)を読んだ方が早いと思います。

Memory Noteの概要

Memory Noteは、Cloudflare Workersで動かしてTodoリストを扱うAPIサービスです。

Memory Note自体はフロントエンドを持っていなくて、iOSのショートカットやmacOSのAlfredなどを使ってメモを読み書きする想定です。(HTTPリクエストを送れればクライアントはなんでも良いです)

Memory Note経由で記録したメモはCloudflareのCloudflare Workers KV(CDN上のKVS)に保存もできますが、GitHubのプロジェクトボードをメモを記録するバックエンドとして使うこともできます。 また、バックエンド自体はStorageAdapterのインターフェイスに沿って実装することで、メモの記録先は自由に実装できます。

Memory Noteのアーキテクチャ

Memory Noteは実質ただのAPIなので、自由に使えます。

iOSのショートカットアプリとかAlfredとかは、ほとんどコード書かなくてもある程度APIを読み書きできるので、フロントエンドに関してはコードらしいコードは書いてません。 iOSのショートカットはHTTPリクエストも遅れるし、JSONの読み書きもできるし、音声の入出力もできて、通知もできるので、自分の用途だとこれで必要十分になりました。

使いたい人はMemory NoteのREADMEを参照してください。

Memory Noteの目的

後で何かしないといけないみたいことを忘れるのが嫌で、その忘れてしまうものを素早くメモする仕組みが欲しくなりました。(短期的なメモを簡単にしたい)

例えば、牛乳を買わないといけないことに気づいたけどその時メモらなかったので、そういう情報はすぐ忘れてしまい、買い忘れてしまうみたいな状態になってしまいます。 このような状態を避ける方法はないのかなと考えていました。

買い物リストだと、iOSの買うものかごというアプリをよく使っていて、音声入力するとそのまま買うものに追加してくれる(キー入力や決定がいらない)が体験良かったです。しかし、アプリを起動して入力開始ボタンをタップするまでの2ステップ(実際はアプリを探すがあるので3ステップ)が遠くて、これを1ステップにしてメモの記録開始ができるものないと、記憶から消える前にメモするのは難しいなと思いました。

そのため、1ステップでのメモができる仕組みを作ろうと思いました。

どういうのを作ろうかメモを書きながら作っていましたが、次のようなことが目的になっていました。

- いつでも入力
- いつでも見れる
- とにかく早い、ストレスフリー

via https://gist.github.com/azu/bc855269882c520c70990fd293aa9893

Cloudflare WorkersでTodoリストを作る

色々ためしているうちに、アプリの切り替えなしにメモを書き始めるには、音声コマンドで入力を開始して音声入力が一番良さそうだと思いました。 iOSにはSiriと自由にプログラミングできるショートカットアプリがあるので、これを使えば実現できそうでした。 また、家ではHomePod miniをおいていて、多少離れてて聞き取ってくれるので、一行メモする程度なら十分な精度がでます。

📝 Homepodで音声を受け取って、iOSのショートカットアプリで作ったショートカットを実行できます(iPhoneとHomePodを接続してる状態になって、音声の入出力だけHomepodになる)

📝 余談: macOSでTalonを使って音声コマンドを扱ってる話 体の動きや音声入力でアプリケーションをハンズフリー操作したりプログラミングしたり文章を書いたりしてみる | Web Scratch

最初は、iOSとmacOSネイティブのReminderを音声で読み書きする仕組みをやってみて、iOS単体で完結するならこれで十分そうでした。(ショートカットアプリでReminderを読み書きできるので、音声入力と出力につなげるだけ) ただし、自分の場合は、macOSのAlfredで扱いたかったり、常に視界に入る物理的なダッシュボードにも最近のメモを表示したかったため、Web APIがないReminderだと厳しそうでした。

この目的を満たせるTodoサービスを探そうとしましたが、Todo自体に求めてるのはリストがあって読み書きできるAPIだけだったので、自分で作ることにしました。

Memory Noteは単純化すれば、一つの配列にpushとpopするAPIがあればいいだけだったので、とにかく早く動くものということでCloudflare Workersで動くものを書き始めました。

Memory NoteのStack

📝 参考: CloudflareのCDNのベンチマーク

最初は、Cloudflare WorkersとそのKVSであるCloudflare Workers KVを使って作ってました。タグ付けや日付ごとに分割したり色々やっていましたが、シンプルなメモアプリが欲しいので、データ構造ももっとシンプルにしないと不自然だなと思いました。 色々削っていくと、必要なのはリストとアイテム、リストの取得とアイテムの追加と削除だけで十分そうなのかわかってきました。

具体的には、次の3つのAPIさえあれば、ReminderのようなシンプルなTodoリストには十分そうでした。 また、アイテムの編集は、アイテムを削除してから追加しなおす(created dateが変わる制限はある)で実現できます。

  • リストからアイテムの一覧を取得
  • リストにアイテムを追加
  • リストからアイテムを削除

バックエンドをGitHub Projectにする

先ほどの3つのAPIをTypeScriptのインターフェイスで書いてみると次のようになりました。

/**
 * Storage Adapter should implement these methods
 */
export type StorageAdapter = {
    /**
     * Return an array of Note
     * @param listId
     */
    getNotes(listId: string): Promise<Note[]>;
    /**
     * Add the note to the list
     * Return the added note
     * @param listId
     * @param note
     */
    appendNote(listId: string, note: AppendNote): Promise<Note>;
    /**
     * Remove the note from the list
     * Return the deleted note
     * @param listId
     * @param id
     */
    deleteNote(listId: string, id: Note["id"]): Promise<Note>;
};

ここまで、必要なAPIが少ないならメモの保存先をCloudflare Workers KVに限定しないで、 使う人が自由に保存先のバックエンドとの接続部分(アダプター)をかけるような作りにした方がきれいな状態になりそうだなーと思いました。 (複数のバックエンドでも同じAPIが使えるのではと思ったのと、複数のバックエンドがあったほうがAPIをキレイに保つ強制力がでる)

自分は普段Todoリスト(長期的なTodo)にはGitHub Issuesを使うようにしていたため、Memory Noteの保存先もGitHubにまとめてしまえるのではと思って、GitHub Projectを保存先にするアダプターを実装しました。

これで、短期的なメモはMemory Note経由でGitHub ProjectsにCardとして記録され、そこから作業が必要ならCardをIssueにして長期的なメモ書きを加えていくみたいな運用ができました。

思いついたことをすぐメモとして追加するので、GitHub Projectのボードにカードがどんどん溜まっていきますが、カードはまとめてアーカイブできるので邪魔になったらアーカイブする形にできそうです。(GitHub Projectのアーカイブはイベントとして残るので、Revertして復活もできるので気にせずアーカイブできる)

GitHub Projectのボードにメモ(カード)がたまるため、メモが追加されるたびにGitHub Actions(on: project_cardに反応できる)を実行して、メモを自動的に分類したりIssueにしたりなどの自動化もできます。 メモに対してコードで自動化処理をかけるのが自由度が高くていい感じです。

他のサービスだとZapierを使ったり、ClickUpなどはAutomationsなどの機能を持ってるので、これを利用する感じな気はします。

Memory Noteの参考したもの

Memory Noteを作っていて、記憶障害のリハビリでメモリーノートブックという仕組みがあることを知りました。 記憶障害者の日常生活におけるメモリーノート利用の実態 : 利用場面および利用内容の違いに着目してを読んでいて、メモを読み書きするを習慣付けするには必然性や外部から継続して利用を促す話などがでてきます。(メモを書く本人がメモを読むことを忘れてしまう問題)

これは確かにメモを記録するだけではダメだなと思って、常時表示するために物理的なダッシュボードに出したり、ワンクリック/音声コマンドでメモを表示/読み上げるような作りにしたりしています。 また、家から出たときに自動的にメモを読み上げるとかもやってみると面白い感じがしました。(ショートカットアプリのAutomationなどで実現できる)

他にも記憶周りの話として、高次脳機能障害とか脳の記憶の仕組みの書籍とかを読んでたりしました。(特に参考にしたわけではないですが、普通に興味深かった)

Memory Noteの使い方

自分用のメモの仕組みなので、自分以外の需要があるのかはよくわかりませんが、誰でもCloudflare Workersにデプロイして使えるようにしています。

詳しい使い方はREADMEを参考にしてください。

GitHub Projectをバックエンドにする場合のざっくりした使い方を書くと、次のようになります。

  1. Deploy to Cloudflare Workersをクリックする
  2. Cloudflareアカウントでmemory-noteがデプロイされる
  3. Cloudflare WorkerのSettingsからEnvironment Variablesを設定していく

Cloudflare WorkersのEnvironment Variablesには、次の環境変数を入れていきます。 (Environment Variablesを保存すると自動的にWorkersがデプロイし直されます)

  • MEMORY_NOTE_TOKEN
    • Variable name: MEMORY_NOTE_TOKEN
    • Value: Memory NoteのAPIにアクセスするときに使うランダムな文字列です。パスワードジェネレーターなどで生成してください
  • BACKEND_SERVICE:
    • Variable name: BACKEND_SERVICE
    • Value: github
  • GITHUB_OWNER:
    • Variable name: GITHUB_OWNER
    • Value: GitHubのアカウント名
  • GITHUB_REPO:
    • Variable name: GITHUB_REPO
    • Value: GitHubのリポジトリ名(GitHub Projectの置き場所)
  • GITHUB_PROJECT_ID
    • Variable name: GITHUB_PROJECT_ID
    • Value: GitHub ProjectのID
  • GITHUB_TOKEN
    • Variable name: GITHUB_TOKEN
    • Value: GitHub Personal Access Token

後、GitHub Projectの場合はそれぞれのColumnがTodoリストのリストとして扱われます。 column linkをコピーして、https://github.com/yourname/yourrepo/projects/1#column-11111111111111部分が後で必要になる:listIdとなります。

copy link

一度設定すれば、後はデプロイしたWorkersをAPIとして叩くだけです。

例えば、リストの一覧を取得するなら次のような感じで、APIを叩くとJSONが返ってきます。

curl https://example-memory-note.worker.dev/notes/1111111?token=<MEMORY_NOTE_TOKENで入れた値>

これだけだと実用性がないので、サンプルクライアントとして次のクライアントを公開しています。

また、https://{your worker url}/notes/:listId/widget?token={memory note token} というURLにアクセスすれば、シンプルなTODOリストのHTML表示を返してくれます。

iOSでは、Web Widgetなどを使えば、ホーム画面に任意のURLをWidgetsとして配置できるので、 ReminderのようにWidgets表示もできます。

iOS Widget

もし、Backend Serviceを書いたり、Memory Noteのクライアントを作ったとかあればぜひPull Requestしてください。

おわりに

Memory Noteをなぜ作ったかを大雑把にまとめてみると、忘れる前にメモしたいので、アプリを開く必要がないとにかく早いメモが欲しいという理由でした。

Cloudflare Workersを使うことで、主な処理をCDN Edgeで動かして、できるだけ体感がいいAPIを作ったり、音声コマンド/入力を使うことで1ステップでメモが開始できるようにしています。 パフォーマンス的には、バックエンドがCloudflare Workers KVの場合はレスポンスが50msぐらいで、バックエンドがGitHub Projectの場合は500msぐらいで収まっています。

Todoアプリを実装したのは、JavaScript PrimerTodoアプリ以来な感じがします。(JavaScript Primerはフロントエンドで、Memory Noteはバックエンドに近いのでちょっと違いますが)

Memory Noteを作ったことで短期的なメモも、タスク管理と同じGitHubリポジトリにまとまったので、GTDでいうところのInboxができた感じがして結構満足ではあります。 しかし、なんかもっと良い仕組みはありそうな気はしますが、こういう忘れると嫌な短期的なメモをみんなどうやって管理してるんだろ?というのが気になります。