はじめに
外出先でふと思いついた TODO をスマホのメモアプリに書き込むこと、あると思います。でものちのち確認する際に TODO がごちゃごちゃしており、その整理に時間を取られること、ありませんか?
自分は LINE から Obsidian にメモを送り、PC 側でまとめて確認することがあります。LINEからのデータ取得にはLINE Notes Syncを使っています。しかしその際、1 ファイルに 1 つだけ記載されるメモを複数展開して TODO を確認、まとめる作業がめんどくさいと感じました。
そこで、ディレクトリ内の TODO を 1 つのファイルにまとめつつ、それらの振り分けを AI にやってもらうことで TODO 把握がより簡単になると考え、ワンタッチでできるプラグインとして開発してみました。
システム構成とフロー
今回考えたシステム構成はこのようになっています。
処理フローとしては、
- LINE Notes Sync を利用して Obsidian に TODO のメモを貯める
- あらかじめ指定したディレクトリ内で新規の TODO のみを収集
- 新規 TODO を cloudflare workers 上のサーバに送信
- サーバから Gemini に対して TODO の分類を依頼
- Gemini 側から分類結果をサーバが受信、そのまま Obsidian に送信
- 出力ファイルに既存 TODO+新規 TODO を書き出し、TODO リストを更新完了!
といった感じにしました。
開発過程で悩んだこと
ディレクトリ内の TODO が収集済みであるか判断する方法
新規の TODO だけ集めるために、ファイル内の TODO が分類済みであるかを判断するフラグが必要となります。
自分が活用しているメモ方法では 1 つのファイルに 1 つの TODO が記載されます。そこで、フロントマターを収集時に付与することで、回収済みフラグとしました。TODO としてメモしたファイルはほぼ使い捨てなので、分類時にいじっても問題ないだろうと判断します。
フロントマターへの記載方法は、
- フロントマターの位置特定
- YAML 内容の抽出
- parseYaml()での解析
という手順を踏みました。ネストを深くしてしまったので、正直もっと頭の良い方法はある気がしてます。
interface TodoFrontmatter {
source?: string;
date?: string;
messageId?: string;
userId?: string;
add_todo?: boolean;
}
// フロントマターの位置を特定
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === "---") {
if (!inFrontmatter) {
inFrontmatter = true;
frontmatterStart = i;
} else {
frontmatterEnd = i;
hasFrontmatter = true;
break;
}
}
}
// 既存のフロントマターを解析
if (hasFrontmatter) {
const frontmatterContent = lines
.slice(frontmatterStart + 1, frontmatterEnd)
.join("\n");
try {
currentFrontmatter = parseYaml(frontmatterContent)
as TodoFrontmatter;
} catch (e) {
console.error("Failed to parse:", e);
}
}
// add_todoがtrueの場合はスキップ
if (currentFrontmatter.add_todo) { continue; }
データ受け渡しの形式
設計段階では md 形式の文章を AI から受け取り、そのままファイルに出力しようとしました。
## 買い物関連
- [ ] チョコミントを買う(2025-06-03)
- [ ] ストロベリーフレーバー買う(2025-06-18)
## 開発関連
- [ ] バグ修正(2025-07-10)
しかし、スペース(空白)の出力が Gemini とのやり取り毎に不安定な出力がされることから、受け取った md 文章の解析がうまくいかず、エラーが多発しました。
そこで、分類結果を Gemini に json 形式で出力してもらい、Obsidian 内で md 形式に変換するよう方針転換しました。
"groups": {
"買い物関連": [
{
"text": "チョコミントを買う",
"completed": false,
"source": "ファイル名"
},
{
"text": "ストロベリーフレーバーを買う",
"completed": false,
"source": "ファイル名"
}
],
"開発関連": [
{
"text": "バグ修正",
"completed": false,
"source": "ファイル名"
}
],
}
この辺りを Cursor に丸投げで実装していたので、json の偉大さに気づきつつ、機械が好む形式でやり取りできているか、考える必要があると思いました。
実装結果
テストとしてファイルに以下のようなTODOリストを用意しました。
#t 傘を買いに行く
#td プログラミングの勉強をする
#t 人参を買う
#t 上腕二頭筋を鍛える
先頭にタグが付いているのはTODOを見分けるためのものです。プラグインで独自に設定ができるようになっています。
プラグインはリボンボタンで実行できるようになっています。
実行するとTODOのソースファイルにはフロントマターが記載され、TODO回収済みであることが記載されます。
また、ファイルには以下のように記載されます。
---
add_todo: true
---
#t 傘を買いに行く
#td プログラミングの勉強をする
#t 人参を買う
#t 上腕二頭筋を鍛える
回収した新規のTODOをいい感じにグループ分けして別ファイルにまとめてくれました!
今後の改善点と展望
メモ投稿時のタグ付け
現状の実装ではモバイルからのメモ時にタグを付け、そのタグが付いた TODO メモを抽出しています。
タグ付けしていないメモすべてを LLM に投げて振り分けてもらうのが理想ですが、現状の AI に対する信頼性を考慮したときに止めておこうという結論になりました。重要な TODO を見落とす可能性が捨てきれないためです。AI さんともっと強い信頼関係を築きたい。。。
サーバを経由する必要性
Cloudflare workers を間に挟まなくても Obsidian ↔ Gemini で直接やり取りできればそっちのほうが良くね?と思いました。サーバ運用のコストや速度を考慮すると、そっちのほうが丸そう。設計段階で気づけばよかった。