この記事は「BRAXシリーズ」の第2弾です。
前回の記事で、サイバーパンク風CLI「BARAX」の基盤(barax init / barax history / barax doctor)を作りました。
まだ読んでない方はこちらから👇
前回: 【Claude Code】 全部まとめて殴るサイバーパンクCLI「BARAX」をTypeScriptで自作した
今回はBRAXに barax meeting(会議自動化) コマンドを追加します。
この記事の登場人物
🧑💻 …Claude Code で自作CLI「BARAX」を育てている先輩(前回からの続き)
🔰 …「議事録って手動で書くものじゃないんですか?」レベルの後輩
会議が終わった瞬間、あなたは何をしていますか?
🔰「先輩、会議が終わると毎回こうなるんですけど…」
✅ Notion の録音を停止する
✅ 文字起こしが生成されるのを待つ
✅ AI 要約をコピーする
✅ Markdown に整形する
✅ 所定のフォルダに保存する
✅ Google Drive にアップロードする
✅ リンクを議事録に追記する
✅ Git に commit & push する
🔰「8ステップ、毎回15分。 しかもたまに忘れる」
🧑💻「…」
🔰「先輩は?」
🧑💻「Enterキー1回」
🔰「は???」
$ barax meeting start --title "週次定例"
✔ Notion データベースを開きました
✔ 新規ページを作成しました
✔ 録音を開始しました
╭ RECORDING ────────────────────────────╮
│ 録音中... Enterキーで停止します。 │
╰───────────────────────────────────────╯
← ここで会議する。終わったらEnter。
✔ ローカル録音停止 (1823秒)
✔ Notion 録音を停止しました
✔ AI 要約が生成されました
✔ 議事録を保存しました
✔ Google Drive にアップロードしました
✔ Git にプッシュしました
┌─ 完了サマリー
│ タイトル: 週次定例
│ 録音時間: 1823秒
│ 議事録: ~/my-vault/meetings/2026-03-22_週次定例/minutes.md
│ Drive 議事録: https://drive.google.com/file/d/xxxxx/view
│ Drive 録音: https://drive.google.com/file/d/yyyyy/view
│ Git: pushed to main
└────────────────────────────────────────
🔰「…全部終わってる」
🧑💻「しかも保存先は Obsidian Vault。Git管理されてるから、どの端末からでも過去の議事録を全文検索できる」
🔰「先輩、これ作るの何日かかったんですか」
🧑💻「Claude Code と2人で1日」
🔥 この記事、こんな人に刺さります
- 「会議後の作業、自動化できないかな…」と思ったことがある人
- macOS の自動化に興味があるけど JXA がよくわからない人
- Notion API じゃなくて ブラウザ操作 で自動化したい人
- Obsidian を「ただのメモ帳」以上に使いたい人
- Claude Code で何か面白いもの作りたい人
この記事でわかること
- Notion AI Meeting Notes をCLIから完全自動操作する方法
- macOS のマイク権限問題を
.appバンドルで突破する技(これが一番ヤバい) - rclone で Google Workspace Drive に自動アップロードする方法
- Obsidian Vault + Git で議事録をナレッジベース化する設計
- 全工程をEnterキー1回に圧縮する 実装パターン
前回のおさらい:BRAXとは
🔰「先輩、BRAXって何でしたっけ?」
🧑💻「前回の記事で作った、Claude Code 日本語対応のサイバーパンクCLI」
| 前回作ったコマンド | 機能 |
|---|---|
barax init / barax 初期化
|
テンプレートから対話的にプロジェクト生成 |
barax history search / barax 履歴 検索
|
セッション履歴の2層検索 |
barax doctor / barax 診断
|
10項目ヘルスチェック + スコアリング |
🧑💻「今回はここに 4つ目のコマンド を追加する」
| 今回追加するコマンド | 機能 |
|---|---|
barax meeting start / barax 会議 開始
|
会議の録音→要約→保存→Drive→Git を全自動化 |
barax meeting list / barax 会議 一覧
|
過去の会議記録を一覧表示 |
🔰「前回の基盤があるから、コマンドを追加するだけで済むんですね」
🧑💻「そう。Commander.js のサブコマンド構造のおかげで、registerMeetingCommand(program)を1行追加するだけ」
完成品:1コマンドで何が起きるか
🧑💻「まず全体像を見せる」
🔰「12ステップが裏で動いてるんですね」
🧑💻「ユーザーがやるのは コマンド実行 と Enterで停止 の2アクションだけ」
「Notion に録音機能あるじゃん」への完全回答
🔰「…ていうか、Notion に直接録音機能ありますよね?わざわざ CLI で自動化する意味あります?」
🧑💻「その質問、待ってた」
🧑💻「Notion 単体だとできないことが5つある」
🧑💻「もっと根本的な話をすると、CLIは合成可能 (composable) なんだよ」
🔰「合成可能?」
🧑💻「パイプで繋げられる。cron で定期実行できる。他のスクリプトから呼べる。GUIアプリにはこれができない」
| 特性 | GUI (Notion Web) | CLI (BARAX) |
|---|---|---|
| 自動化 | 不可 (人間がクリック) |
barax meeting start で全自動 |
| 再現性 | 人間依存 (手順忘れ) | コマンドが手順そのもの |
| 合成 | 不可 |
cron, &&, パイプで連携可能 |
| カスタマイズ | Notion の UI に制約される | コード変更で自由自在 |
| オフライン | 不可 | ローカル録音は Notion なしでも可 |
| データ所有 | Notion のサーバーに依存 | ローカル Markdown + Git |
🔰「"データ所有" って大事なんですか?」
🧑💻「Notion のエクスポートやったことある?大量のページを一括で Markdown にするのは結構面倒。最初からローカルに Markdown があれば、そもそもエクスポートの必要がない」
CLIの本質: 「繰り返しを消す」こと
GUIは 1回の操作 を簡単にする。CLIは 100回の操作をゼロ にする。
会議が週5回あるなら、手動の15分 × 5回 × 4週 = 月5時間が蒸発。
年間にすると 60時間。1.5週分の労働時間。CLIを1日で作れば、投資回収は1ヶ月。
🧑💻「あとClaude Codeとの相性が最高にいい。CLI のコードは Claude Code が読めるし、修正もできる。"Drive のアップロード先変えたい" って言えば、Claude が
meeting.tsを直接編集してくれる」
🔰「GUI のワークフローはAIに修正させられないけど、CLIのコードならできると」
🧑💻「そう。コードで書かれたワークフローは、AIに最適化させ続けられる。これが2026年にCLI自動化を選ぶ最大の理由」
なぜ Obsidian Vault に保存するのか — 「検索できない議事録」に意味はない
🔰「議事録って Google Docs とか Notion にそのまま残せばよくないですか?」
🧑💻「3ヶ月前の会議で "認証フロー" について何を決めたか、今すぐ調べられる?」
🔰「えっと… Notion で検索して… Google Docs も見て…」
🧑💻「それが答え。検索がサービスに閉じてる。Obsidian Vault なら…」
🧑💻「ポイントは プレーンテキスト (Markdown) が真のソース だということ」
| 保存先 | 役割 | なぜ必要か |
|---|---|---|
| Obsidian Vault | ナレッジベース | 全文検索、wiki link、グラフビューで議事録が知識資産になる |
| Git (GitHub) | バージョン管理 | どの端末からもアクセス、変更履歴の追跡 |
| Google Drive | チーム共有 | GWS組織内でリンク1つで共有、Slack連携 |
🔰「3箇所に保存されるのに、ユーザーは何もしなくていいと」
🧑💻「そう。Markdownファイルを1つ生成すれば、あとはCLIが配る」
Vault のディレクトリ構造
~/my-vault/ ← Obsidian Vault ルート (Git管理)
├── meetings/ ← 議事録はここに自動生成
│ ├── 2026-03-22_週次定例/
│ │ ├── minutes.md ← 議事録 (AI要約+文字起こし+Driveリンク)
│ │ └── audio.m4a ← ローカル録音 (.gitignore対象)
│ ├── 2026-03-22_1on1/
│ │ ├── minutes.md
│ │ └── audio.m4a
│ └── ...
├── notes/ ← 他のノートも同居可能
└── .gitignore ← *.m4a, *.wav 等を除外
🔰「音声ファイルは Git に入らないんですか?」
🧑💻「.gitignoreで除外してる。音声は Google Drive にだけ上がる。Git にはテキストだけ — これが正しい使い分け」
生成される議事録の形式
# 週次定例
- **日付**: 2026-03-22
- **録音時間**: 30分23秒
- **生成元**: Notion AI Meeting Notes (ブラウザ録音)
- **自動化方式**: barax meeting (JXA + Chrome)
- **音声ファイル**: ~/my-vault/meetings/2026-03-22_週次定例/audio.m4a
---
## Google Drive
- [議事録 (Google Drive)](https://drive.google.com/file/d/xxxxx/view)
- [録音データ (Google Drive)](https://drive.google.com/file/d/yyyyy/view)
---
## 概要
新機能のリリーススケジュールについて議論。認証フローの改修を来週月曜に着手予定。
デザインレビューは水曜に実施。パフォーマンス改善のベンチマークは金曜までに完了見込み。
---
## 文字起こし
要約中
会話の内容
新機能のリリースについて、スケジュールを確認しました...
🔰「メタデータ、Google Driveリンク、AI要約、文字起こし、全部入ってる」
🧑💻「Obsidianで開けば即座に全文検索できる。Cmd+Shift+Fで "認証" って検索すれば、過去に認証の話をした会議が全部ヒットする」
アーキテクチャ全体像
🧑💻「技術的にはこう繋がってる」
技術スタック
| パッケージ / ツール | 役割 | なぜこれか |
|---|---|---|
| JXA (osascript) | Chrome タブ操作 | macOS ネイティブ。追加インストール不要 |
| ffmpeg | マイク録音 | AVFoundation 対応。あらゆる音声フォーマットに変換可能 |
| rclone | Google Drive アップロード | CLI ベース。GWS (Google Workspace) 対応 |
| Commander.js | CLI フレームワーク |
.alias('会議') で日本語コマンド対応 |
| ora | スピナー | 各ステップの進捗表示 |
| dayjs | 日付処理 | 2KB。ディレクトリ名生成に使用 |
実装 Part 1: JXA で Chrome を操る
🔰「そもそも JXA って何ですか?」
🧑💻「JavaScript for Automation。macOS に最初から入ってる自動化エンジン。AppleScript の JavaScript 版」
jxa-chrome.ts — 5つの関数
// src/lib/jxa-chrome.ts
/** JXA スクリプトを実行する共通関数 */
function runJxa(script: string): string {
return execSync(`osascript -l JavaScript -e '${escaped}'`, {
encoding: 'utf-8',
timeout: 10000,
}).trim();
}
🧑💻「核はこの1行。
osascript -l JavaScriptで JXA を実行して、結果を文字列で返す」
| 関数 | 役割 | 使用場面 |
|---|---|---|
chromeOpen(url) |
URL を開く(既存タブ再利用) | Notion DB を開く |
chromeCloseDuplicates(prefix) |
重複タブを閉じる | 実行前のクリーンアップ |
chromeExec(js) |
DOM 操作(戻り値不要) | ボタンクリック、テキスト入力 |
chromeEval(js) |
DOM から値を取得 | AI 要約テキスト抽出 |
chromePoll(js, opts) |
条件が満たされるまでポーリング | 要素の出現待ち |
chromePoll — DOM が変わるまで待つ
🧑💻「Notion は SPA だから、ページ遷移しても DOM がいつ更新されるかわからない。だから ポーリング する」
export async function chromePoll(
js: string,
opts: { interval?: number; timeout?: number; label?: string } = {},
): Promise<string> {
const interval = opts.interval ?? 2000;
const timeout = opts.timeout ?? 120_000;
const start = Date.now();
while (Date.now() - start < timeout) {
const result = chromeEval(js);
if (result && result !== 'undefined' && result !== 'null' && result !== 'false') {
return result;
}
await sleep(interval);
}
throw new Error(`Timeout: ${opts.label || 'chromePoll'}`);
}
🔰「何秒おきにチェックするんですか?」
🧑💻「デフォルト2秒。ボタン出現待ちは1〜1.5秒、AI要約待ちは3秒にしてる。Notion の負荷を考えて調整」
実装 Part 2: Notion ピークパネルという罠
🧑💻「Part 1 は JXA の基盤だった。ここからが 本当の戦い」
🔰「え、まだ序盤なんですか」
🧑💻「Notion AI Meeting Notes のUIには 致命的な罠 がある。ピークパネルとフルページで全然違う」
🔰「フルページだと録音できないんですか?」
🧑💻「そう。フルページでは Meeting Notes テンプレートが自動適用されない。テンプレート選択画面が出るだけ。ピークパネルでしか録音ウィジェットが表示されない」
問題:ピークパネルが閉じることがある
🧑💻「ピークパネルは不安定で、たまに勝手に閉じる。それで録音開始に失敗する」
🔰「どう解決したんですか?」
🧑💻「リトライ機構 を入れた」
// Step 2: 新規ページ作成 (リトライ付き)
let peekPanelReady = false;
for (let attempt = 0; attempt < 3 && !peekPanelReady; attempt++) {
if (attempt > 0) {
spinner.text = `新規ページを作成中... (リトライ ${attempt + 1}/3)`;
await sleep(1000);
}
// 「新規」/「New」ボタンをクリック (日英両対応)
chromeExec(`
var buttons = document.querySelectorAll('div[role="button"]');
for (var i = 0; i < buttons.length; i++) {
var t = buttons[i].textContent.trim();
if (t === '新規' || t === 'New') { buttons[i].click(); break; }
}
`);
// 文字起こしオプションの出現を待つ (10秒)
const result = await chromePoll(`
(function() {
var el = document.querySelector('[aria-label="その他の文字起こしオプション"]')
|| document.querySelector('[aria-label="More transcription options"]');
return el ? 'ready' : '';
})()
`, { interval: 1000, timeout: 10000 }).catch(() => '');
if (result === 'ready') peekPanelReady = true;
}
🔰「3回リトライするんですね」
🧑💻「1回10秒 × 最大3回 = 30秒。パネルが閉じても再クリックする。これで成功率が体感50%から95%以上になった」
問題:UIの言語が日本語だったり英語だったりする
🔰「
"その他の文字起こしオプション"と"More transcription options"の両方をチェックしてますよね」
🧑💻「Notion は組織設定やブラウザ言語によってUIの言語が変わる。同じページでもボタンが日本語だったり英語だったりする」
// 日英両対応のボタン検出パターン
const patterns = {
newButton: ['新規', 'New'],
startButton: ['ブラウザで開始', 'Start in browser'],
stopButton: ['停止', 'Stop'],
transcription: ['その他の文字起こしオプション', 'More transcription options'],
};
🧑💻「全てのDOM操作で日本語と英語の両方を試す。これをやらないと、ある日突然動かなくなる」
Notion の DOM は言語設定で変わる。 aria-label やボタンテキストに依存する自動化は、必ず多言語対応を入れること。
🔰「ピークパネルのリトライ、DOM の多言語対応… Notion 自動化って大変ですね」
🧑💻「いや、まだ本当のボスが残ってる」
実装 Part 3: macOS マイク権限の壁 — これが一番ヤバかった
🧑💻「ここで半日溶けた」
🔰「何がですか?」
🧑💻「ターミナルから ffmpeg を起動してもマイクが使えない。声出してるのに 0KB。無音」
🔰「…怖すぎる」
macOS TCC (Transparency, Consent, and Control) の仕組み
🔰「Ghostty って何ですか?」
🧑💻「最近人気のターミナルアプリ。でもこれが正規の.appバンドルじゃないから、macOS の TCC (マイク権限管理) に登録できない」
🔰「つまり?」
🧑💻「ターミナルから直接 ffmpeg を起動すると、TCC が "このアプリにマイク権限ない" と判断して 無音データ を返す。声出してるのに 0KB」
解決策: 「自分で .app を作ればいい」
🧑💻「3時間悩んで、ふと思った。ターミナルにマイク権限がないなら、マイク権限のあるアプリを自分で作ればいい」
🔰「え、アプリ作るんですか?」
🧑💻「といっても正規の.appバンドルを作って、その中から ffmpeg を呼ぶだけ」
BaraxRecorder.app/
├── Contents/
│ ├── Info.plist ← macOS に「マイク使います」と宣言
│ └── MacOS/
│ └── barax-recorder ← ffmpeg を呼ぶ bash スクリプト
Info.plist — マイク権限の宣言
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.barax.recorder</string>
<key>CFBundleExecutable</key>
<string>barax-recorder</string>
<key>LSUIElement</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>barax meeting コマンドで会議音声を録音するためにマイクを使用します。</string>
</dict>
</plist>
🔰「
LSUIElementって何ですか?」
🧑💻「Dock にアイコンを表示しない設定。バックグラウンドアプリとして動く」
barax-recorder — ffmpeg ラッパースクリプト
#!/bin/bash
OUTPUT="$1"
DURATION="${2:-0}"
FFMPEG="/opt/homebrew/bin/ffmpeg"
ARGS=(-y -f avfoundation -i :0)
if [ "$DURATION" -gt 0 ] 2>/dev/null; then
ARGS+=(-t "$DURATION")
fi
ARGS+=(-acodec aac "$OUTPUT")
"$FFMPEG" "${ARGS[@]}"
🔰「
-f avfoundation -i :0ってなんですか?」
🧑💻「macOS のオーディオ入力デバイス(マイク)をキャプチャする設定」
| フラグ | 意味 |
|---|---|
-f avfoundation |
macOS AVFoundation オーディオデバイスを使用 |
-i :0 |
デフォルトマイク (映像なし:音声デバイス0) |
-acodec aac |
AAC コーデックでエンコード |
-y |
既存ファイル上書き |
起動方法: open コマンドが鍵
// NG — TCC がブロックする (無音)
execSync('ffmpeg -f avfoundation -i :0 -acodec aac output.m4a');
// OK — .app バンドルとして起動 → TCC がマイク権限を付与
execSync('open "BaraxRecorder.app" --args "output.m4a"');
🧑💻「
openコマンドで.appとして起動すると、macOS が正式なアプリケーションとして認識する。初回起動時にマイク権限の確認ダイアログが出て、許可すれば以降は自動で録音できる」
なぜ spawn() ではダメなのか: child_process.spawn() は子プロセスを直接 fork する。この場合 TCC は 親プロセス (ターミナル) の権限をチェックする。ターミナルにマイク権限がなければ子プロセスも録音できない。open コマンドは macOS の Launch Services 経由で 独立したアプリケーション として起動するため、TCC が .app バンドルの Info.plist を見てくれる。
🔰「
.app作ってopenで起動する… macOS ハックすぎません?」
🧑💻「ドキュメントに書いてないことは、ソースコード(OS の挙動)から逆算するしかない。これがCLI自動化の醍醐味」
実装 Part 4: 録音停止とプロセス管理
🔰「録音を止めるときはどうするんですか?」
🧑💻「ffmpeg は SIGINT (Ctrl+C 相当) で正常終了する。PID を追跡して kill する」
// 録音開始時: PID を取得
execSync(`open "${recorderApp}" --args "${audioPath}"`, { stdio: 'ignore' });
await sleep(1500); // ffmpeg 起動を待つ
let recorderPid: number | null = null;
try {
const pid = execSync(
`pgrep -n -f "avfoundation.*${audioPath}"`,
{ encoding: 'utf-8' }
).trim();
recorderPid = parseInt(pid, 10) || null;
} catch {
// pgrep が見つからなくてもアプリは動作中
}
// ... 会議中 ...
// 録音停止: SIGINT を送信
if (recorderPid) {
try {
process.kill(recorderPid, 'SIGINT');
} catch {
// プロセスが既に終了している場合のフォールバック
execSync(`pkill -f "avfoundation.*audio.m4a"`, { stdio: 'ignore' });
}
await sleep(1500); // ファイル書き出し完了を待つ
}
🔰「
pgrepで PID を取るのはなぜ直接取れないんですか?」
🧑💻「openコマンドは即座に返る(バックグラウンド起動)。open自体の PID は ffmpeg の PID じゃない。だからpgrepで "avfoundation" を含むプロセスを検索する必要がある」
実装 Part 5: AI の出力を「盗む」— DOM スクレイピング
🧑💻「録音停止後、Notion AI が自動で要約と文字起こしを生成する。問題は Notion API からはこのデータが取れない こと」
🔰「え、APIないんですか?」
🧑💻「Meeting Notes の要約はAPIで取得できない。だから DOM から直接抜く」
// AI要約のポーリング
const summaryText = await chromePoll(`
(function() {
var root = document.querySelector('[data-content-editable-root="true"]');
if (!root) return '';
var text = root.innerText || '';
if (text.indexOf('まだ記録されていません') !== -1) return '';
if (text.indexOf('要約') !== -1 && text.length > 200) return text;
return '';
})()
`, { interval: 3000, timeout: 180000 }).catch(() => '');
🔰「なんで
data-content-editable-rootを使うんですか?」
🧑💻「Notion のエディタ領域を特定する一番安定したセレクタ。クラス名はビルドごとに変わるけど、data-属性は比較的安定してる」
テキスト抽出のパース戦略
// Notion の innerText をセクション分割で抽出
const extractedText = chromeEval(`
(function() {
var root = document.querySelector('[data-content-editable-root="true"]');
var text = root.innerText || '';
// 要約: 「要約を共有」の後 〜 「形式:」の前
var summary = '';
var shareIdx = text.indexOf('要約を共有');
if (shareIdx !== -1) {
var afterShare = text.substring(shareIdx + '要約を共有'.length);
var formatIdx = afterShare.indexOf('形式:');
summary = formatIdx !== -1
? afterShare.substring(0, formatIdx)
: afterShare.substring(0, 1000);
}
// 文字起こし: 「文字起こし」セクション
var transcript = '';
var transIdx = text.indexOf('\\n文字起こし\\n');
if (transIdx !== -1) {
transcript = text.substring(transIdx + '\\n文字起こし\\n'.length, transIdx + 2000);
}
return JSON.stringify({ summary, transcript, raw: text.substring(0, 2000) });
})()
`);
🔰「テキストの位置を文字列検索で特定してるんですね」
🧑💻「Notion の DOM 構造に深く依存するのは危険だから、innerTextを取得して文字列レベルでパースしてる。DOM が変わってもinnerTextは安定する」
実装 Part 6: Google Drive — GUI を開かずにアップロードする
🔰「ここまでで録音・停止・要約抽出・保存まで来ましたね」
🧑💻「次は チームへの共有 。Google Drive にアップする」
🔰「drive.google.comを開いてドラッグ&ドロップ…」
🧑💻「それをやめるために CLI を作ってるんだが?」
🔰「す、すみません」
🧑💻「rclone を使う。クラウドストレージの rsync。40以上のサービスに対応してる CLI ツール」
セットアップ (初回のみ)
# インストール
brew install rclone
# Google Drive の認証 (ブラウザが開く)
rclone authorize "drive"
# 設定ファイル作成 (~/.config/rclone/rclone.conf)
# [gdrive]
# type = drive
# scope = drive
# token = {"access_token":"...","refresh_token":"..."}
Google Workspace (GWS) のドライブでも動く。 rclone の drive タイプは個人 Google Drive と GWS Drive の両方に対応。組織の管理者設定で「サードパーティアプリ」が許可されている必要がある。
アップロード + リンク取得の実装
async function uploadToGDrive(
title: string, date: string,
minutesPath: string, audioPath: string,
): Promise<DriveLinks | null> {
// rclone が設定済みか確認
try {
execSync('rclone listremotes', { stdio: ['pipe', 'pipe', 'pipe'] });
} catch { return null; }
const links: DriveLinks = {};
const baseName = `${date}_${title}`;
const driveOpts = `--drive-root-folder-id=${GDRIVE_FOLDER_ID}`;
// 1. 議事録をアップロード
execSync(`rclone copyto "${minutesPath}" "gdrive:/${baseName}_議事録.md" ${driveOpts}`);
// 2. ファイルIDを取得して共有リンクを構築
const json = execSync(`rclone lsjson "gdrive:/" ${driveOpts} --files-only`, { encoding: 'utf-8' });
const files = JSON.parse(json) as Array<{ Name: string; ID: string }>;
const mdFile = files.find(f => f.Name === `${baseName}_議事録.md`);
if (mdFile) {
links.minutes = `https://drive.google.com/file/d/${mdFile.ID}/view`;
}
// 3. 録音データも同様にアップロード
if (existsSync(audioPath)) {
execSync(`rclone copyto "${audioPath}" "gdrive:/${baseName}.m4a" ${driveOpts}`);
// ... 同様にリンク取得
}
return links;
}
🔰「
rclone copytoとrclone copyは何が違うんですか?」
🧑💻「copyはディレクトリ to ディレクトリ。copytoは単一ファイルのアップロード。ファイル名の指定ができる。今回はminutes.mdを2026-03-22_週次定例_議事録.mdにリネームしたいからcopytoを使ってる」
ファイル命名規則
| アップロード元 | Drive 上のファイル名 |
|---|---|
minutes.md |
2026-03-22_週次定例_議事録.md |
audio.m4a |
2026-03-22_週次定例.m4a |
🧑💻「日付とタイトルが入ってるから、Drive 上でも一目で内容がわかる」
実装 Part 7: Git — 最後のピースで完全自動化が完成する
🧑💻「ラスボス。議事録を Obsidian Vault の Git リポジトリに自動 push する」
🔰「git add して commit して push するだけでは?」
🧑💻「"するだけ" が一番危ない思考。別端末の同期でリモートが先に進んでることがある」
function gitCommitAndPush(meetingDir: string, title: string): boolean {
const repoRoot = join(homedir(), 'my-vault');
const gitOpts = { cwd: repoRoot, stdio: 'pipe' as const, timeout: 15000 };
try {
const minutesRel = meetingDir.replace(repoRoot + '/', '') + '/minutes.md';
execSync(`git add "${minutesRel}"`, gitOpts);
execSync(`git commit -m "会議議事録: ${title}"`, gitOpts);
// push を試み、失敗したら stash → rebase → push
try {
execSync('git push origin main', gitOpts);
} catch {
execSync('git stash', gitOpts);
execSync('git pull --rebase origin main', gitOpts);
execSync('git stash pop', gitOpts);
execSync('git push origin main', gitOpts);
}
return true;
} catch {
return false;
}
}
コンフリクト解消フロー
🔰「push が失敗するケースってあるんですか?」
🧑💻「別端末で Obsidian が同期した変更があると、リモートが先に進んでる。その場合のフォールバック」
🧑💻「
stash → pull --rebase → stash pop → pushの4ステップ。これで大抵のコンフリクトは自動解消される」
なぜ stash が必要か: git pull --rebase はワーキングツリーに変更があると失敗する。Obsidian が .skill ファイル等を削除してると unstaged changes 扱いになる。stash で退避 → rebase → 復元 で安全に処理できる。
実装 Part 8: 全部つなげる — 12ステップのオーケストレーション
🔰「Part 1〜7 で全パーツが揃いました。これを繋げると…」
🧑💻「Enter 1回で全部動く 。最終形を見せる」
CLIコマンドの登録
// src/commands/meeting.ts
export function registerMeetingCommand(program: Command): void {
const meeting = program
.command('meeting')
.alias('会議')
.description('Notion AI Meeting Notes の自動録音・要約取得');
meeting
.command('start')
.alias('開始')
.description('会議録音を開始し、停止後に要約を取得')
.option('-t, --title <title>', '会議タイトル')
.option('-d, --duration <seconds>', 'ローカル録音の秒数', '0')
.option('--no-local', 'ffmpegローカル録音を無効化')
.action(async (opts) => {
// 12ステップのオーケストレーション
});
meeting
.command('list')
.alias('一覧')
.description('過去の会議記録を一覧表示')
.action(() => { listMeetings(); });
}
barax meeting start --title "週次定例" # 全自動フロー
barax 会議 開始 -t "週次定例" # 日本語でも同じ
barax meeting start --no-local # ローカル録音なし
barax meeting list # 過去の会議一覧
barax 会議 一覧 # 日本語版
💀 ハマりどころ5選 — 全部実話です
🔰「実装中にハマったところありました?」
🧑💻「全部で丸1日溶かした。同じ轍を踏まないように全部書く」
ピークパネル vs フルページ: テンプレートが適用されない
症状: 新規ページを開いたら「テンプレート / 空白のページ」の選択画面が出て、録音ウィジェットが表示されない
原因: フルページ表示ではテンプレートが自動適用されない。DB の「新規」ボタンから開くピークパネルでのみ Meeting Notes テンプレートが自動適用される
解決: フルページ遷移を完全に廃止。ピークパネルのみで操作する
ffmpeg が無音を返す (macOS TCC)
症状: ffmpeg でマイク録音したファイルが 3KB(無音)。声を出しているのに何も入らない
原因: Ghostty ターミナルが正規 .app バンドルでないため、macOS TCC のマイク権限リストに登録できない
解決: BaraxRecorder.app を作成し、open コマンドで起動
# NG
ffmpeg -f avfoundation -i :0 output.m4a # → 無音
# OK
open BaraxRecorder.app --args output.m4a # → 正常録音
rclone copy vs copyto: directory not found
症状: rclone copy "file.md" "gdrive:/path" → "directory not found" エラー
原因: rclone copy はディレクトリをソースに期待する
解決: 単一ファイルのアップロードは rclone copyto を使う
# NG
rclone copy "minutes.md" "gdrive:/folder"
# OK
rclone copyto "minutes.md" "gdrive:/2026-03-22_meeting.md"
git pull --rebase が unstaged changes で失敗する
症状: Git push → 失敗 → git pull --rebase → "Cannot rebase: You have unstaged changes"
原因: Obsidian のプラグインや同期ツールがファイルを変更・削除して unstaged changes が発生
解決: push失敗 → stash → pull --rebase → stash pop → push の4段階フォールバック
Notion DOM の言語が日本語だったり英語だったりする
症状: ある日突然 querySelector('[aria-label="その他の文字起こしオプション"]') が null を返す
原因: 組織設定やブラウザ言語の変更で、Notion UI が英語に切り替わることがある
解決: 全 DOM セレクタで日本語 || 英語のフォールバック
var el = document.querySelector('[aria-label="その他の文字起こしオプション"]')
|| document.querySelector('[aria-label="More transcription options"]');
まとめ — 「Enter 1回」の裏側にあったもの
🔰「ここまで読んで思ったんですけど、"Enter 1回" の裏に 相当な仕組み がありますね」
🧑💻「そう。でも一度作れば 永遠に動く。会議後の作業時間がゼロになった」
| Before (手動) | After (BARAX) |
|---|---|
| 録音停止 → コピー → 整形 → 保存 → アップロード → push | Enterキー → 完了 |
| 約15分/会議 | 0分 (自動) |
| 忘れることがある | 忘れない (自動) |
| フォーマットがバラバラ | 毎回統一 |
| 検索しづらい | Obsidian で全文検索 |
🧑💻「週5回会議がある人なら、月5時間 が消える計算」
🔰「議事録は Obsidian で検索できるし、Drive でチームに共有もできるし、Git で履歴も残る」
🧑💻「プレーンテキスト (Markdown) を真のソースにして、配信先を増やしていくパターン。これが一番強い」
発展アイデア
| 機能 | 概要 |
|---|---|
| Slack 通知 | 会議完了時に議事録リンクをチャンネルに投稿 |
| 週次サマリー | 今週の会議を LLM でまとめて自動生成 |
| 参加者検出 | 文字起こしから話者分離 → メンション |
| アクションアイテム抽出 | AI 要約から TODO を自動抽出 → タスク管理連携 |
難易度ランキング — 何が一番大変だったか
🔰「一番大変だったのは?」
🧑💻「ダントツで macOS のマイク権限。これだけで半日溶けた。.appバンドルを自作するなんて想定してなかった」
🔰「二番目は?」
🧑💻「Notion のピークパネル。フルページで動かないことに気づくまでが長かった。あと、UI の言語が日英で変わるのは罠」
🔰「逆に一番簡単だったのは?」
🧑💻「rclone。brew installしてrclone copytoするだけ。CLIツール同士の連携は本当に楽。これがCLIの世界の強さ」
おわりに — 「面倒くさい」は最高のモチベーション
🔰「これって Notion 以外でも使えますか?」
🧑💻「JXA 部分は Chrome で動く SPA なら何でも応用できる。Google Meet の自動操作とか、Figma の自動エクスポートとか」
🔰「応用範囲広すぎません?」
🧑💻「"ブラウザで手動でやってること" は、全部自動化の候補」
この記事で紹介した技術
| # | 技術 | 何に使ったか | 応用先 |
|---|---|---|---|
| 1 | JXA (osascript) | Chrome タブの自動操作 | 任意の Web アプリの自動化 |
| 2 | .app バンドル + TCC | ターミナルからのマイク録音 | カメラ、位置情報等の権限突破 |
| 3 | rclone | Google Drive への CLI アップロード | S3, Dropbox, OneDrive 等 40+ |
| 4 | Obsidian Vault + Git | 議事録のナレッジベース化 | あらゆるドキュメント管理 |
| 5 | Commander.js alias |
barax 会議 開始 日本語コマンド |
多言語 CLI の実装 |
🧑💻「最後に1つ」
🔰「はい」
🧑💻「"面倒くさい" と感じた瞬間が、自動化のスタートライン。 その感覚を大事にしろ。忙しさに慣れるな。"面倒くさい" を解決するコードを書け」
🔰「…かっこいい」
🧑💻「barax 会議 開始でどうぞ」
前回の記事はこちら👇
【Claude Code】 全部まとめて殴るサイバーパンクCLI「BARAX」をTypeScriptで自作した
いいねしてくれると第3弾のモチベーションになります 🙏
