0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リモートだと静かすぎて鬱なので、Cursorにずんだもんで報告させる

0
Posted at

リモート作業だと部屋が静かすぎて激鬱なので少し遊んでいきたいと思います。Cursorのエージェントも文字だけで終わるので、終わったことに気づきにくいし無音のままです。なので、返答のたびにずんだもんの声で読み上げて、Slackにも残すようにしました。

  1. CursorのUser Rulesに「終わったら ~/cursor-notify を実行しろ」記載
  2. ~/cursor-notify でVOICEVOX合成とSlack投稿をするようにする

声はDocker上のVOICEVOX Engine(speaker id 3=ずんだもん)です。

実際の動き

エージェントが作業を終えると、返答の最後でローカル端末から sh ~/cursor-notify を実行します。チャット欄にはコマンド実行の様子が残ります。

cursor画面.png

Cursorが sh ~/cursor-notify slack done "サンプルなのだ!" を叩いている画面です。ルールを「必須」にしておかないと、ここまで来ませんでした。最初は「必要なときに通知」と書いていて、ほぼ鳴りませんでした。

Slackには kind ごとにチャンネルを分けて投稿します。席を外していても、あとから追えます。

Slack画面.png

Slackに CHECK と本文が届いている画面です。完了と確認依頼が同じチャンネルに混ざらないよう、done / test-done / check / issue / memo で分けています。

ハマったところ

最初は2つで止まりました。

1つ目は、サンドボックスで実行されると音が鳴らないことです。エージェントがシェルを叩いても、サンドボックスやコンテナの中だと afplay まで届きません。コマンドは成功したように見えてパソコンから音がしませんでした。ルールに「ユーザーのローカル端末で実行する」「サンドボックス、コンテナ、リモートシェル、SSH、dev container の中では実行しない」と明記して、そこでようやく耳に届くようになりました。

2つ目は、毎回実行確認を取られて止まることです。Slack投稿やネットワークアクセスのたびにエージェントが承認を求めると、通知のたびに作業が止まります。ルールに「Slack通知は事前承認済み」「実行前に余計な確認を取らない」と書いて、止まりにくくしました。

こうすると便利でした

別ウィンドウを見ていても、完了が耳で分かります。席を外していても、Slackに残ります。ルールを「必須」にすると、エージェントが通知を省略しにくくなります。トイレやタバコ休憩中はSlackを見て、カップ麺食べてるときなどはずんだもんに教えてもらいます。

実際のUser Rule

いまCursorに入れているルールはこれです。alwaysApply: true にしています。(あまり意味はないですが)

---
description: Notify on reply or task completion
globs:
alwaysApply: true
-----------------

# Notification Rule

## Purpose

作業完了時に、実施内容をずんだもん風の話し方で口頭通知し、Slackにもメモを残す。

## Command

Use:

sh ~/cursor-notify <mode> <kind> "<message>"

Run this command from the user's local machine terminal.

Do not run it inside a sandbox, container, remote shell, SSH session, dev container, or virtual execution environment.

The command uses VOICEVOX / ずんだもん for speech.

Do not use fixed local wav files.

## Modes

* `local`: speak with VOICEVOX only
* `slack`: send Slack notification and speak with VOICEVOX

## Kinds

* `done`: task completed
* `test-done`: test, build, or check completed
* `check`: user review or confirmation needed
* `issue`: blocked, failed, or user decision required
* `memo`: short progress note

## Message Style

The message is read aloud.

Write the message as natural spoken Japanese.

Do not use bullet points, tables, markdown headings, code blocks, symbols, or terse fragments in the notification message.

Use one or two short sentences.

Use a ずんだもん-like tone.

Good:

作業完了なのだ。cursor-notifyのルールを、ずんだもんの読み上げとSlackメモに合わせて整理したのだ。

Bad:

*作業完了*
• cursor-notify修正
• VOICEVOX対応
• Slack通知対応

## Required Completion Notification

After every completed assistant reply or completed task, run:

sh ~/cursor-notify slack done "作業完了なのだ。<実施した内容を口語の文章で短く説明するのだ>"

This is mandatory.

Do not skip it for small tasks.

## Slack

Slack notification is pre-approved.

Network access for Slack notification is pre-approved.

Do not ask the user for extra approval before sending Slack notification.

For review requests, blocked work, long tasks, or user decisions, use Slack.

Examples:

sh ~/cursor-notify slack done "作業完了なのだ。cursor-notifyの通知ルールを、音声で聞き取りやすい文章形式に直したのだ。"

sh ~/cursor-notify slack check "確認してほしいのだ。handlerを薄くして、app側でmetaを作る形に整理したのだ。"

sh ~/cursor-notify slack issue "作業停止なのだ。テストがmigration不整合で失敗しているから、修正方針の判断が必要なのだ。"

## Test Completion

After tests, builds, or checks complete, run:

sh ~/cursor-notify slack test-done "テスト完了なのだ。<実施したテスト内容を口語の文章で短く説明するのだ>"

## Failure Handling

Do not expose Slack API responses.

Do not expose notification command output.

If Slack notification fails, fall back to:

sh ~/cursor-notify local done "作業完了なのだ。<実施した内容を口語の文章で短く説明するのだ>"

Then continue reporting the actual task result.

シェルスクリプトの中身

~/cursor-notify です。TokenとチャンネルIDは ~/.config/cursor-notify/env に置きます。

#!/bin/sh
set -eu

cmd=${0##*/}
envf=${CURSOR_NOTIFY_ENV:-$HOME/.config/cursor-notify/env}
[ -f "$envf" ] && . "$envf"

api=https://slack.com/api/chat.postMessage
tok=${SLACK_BOT_TOKEN:-}
mention=${CURSOR_NOTIFY_MENTION:-}
user=${CURSOR_NOTIFY_USERNAME:-Cursor}
icon=${CURSOR_NOTIFY_ICON:-:robot_face:}

vv_name=${VOICEVOX_CONTAINER_NAME:-voicevox-engine}
vv_img=${VOICEVOX_IMAGE:-voicevox/voicevox_engine:cpu-latest}
vv_host=${VOICEVOX_HOST:-127.0.0.1}
vv_port=${VOICEVOX_PORT:-50021}
vv_spk=${VOICEVOX_SPEAKER_ID:-3}
vv_wait=${VOICEVOX_STARTUP_TIMEOUT:-60}

die()
{
	printf '%s: %s\n' "$cmd" "$1" >&2
	exit 1
}

need()
{
	command -v "$1" >/dev/null 2>&1 || die "$1 is not installed"
}

usage()
{
	cat <<USAGE
usage: $cmd mode kind message...

mode: slack | local
kind: memo | done | issue | test-done | check

env:
  SLACK_BOT_TOKEN
  CURSOR_NOTIFY_MENTION
  CURSOR_NOTIFY_CH_MEMO CURSOR_NOTIFY_CH_DONE CURSOR_NOTIFY_CH_ISSUE
  CURSOR_NOTIFY_CH_TEST_DONE CURSOR_NOTIFY_CH_CHECK
  VOICEVOX_SPEAKER_ID VOICEVOX_HOST VOICEVOX_PORT
  VOICEVOX_CONTAINER_NAME VOICEVOX_IMAGE
USAGE
	exit 2
}

ch()
{
	case $1 in
	memo)      v=${CURSOR_NOTIFY_CH_MEMO:-} ;;
	done)      v=${CURSOR_NOTIFY_CH_DONE:-} ;;
	issue)     v=${CURSOR_NOTIFY_CH_ISSUE:-} ;;
	test-done) v=${CURSOR_NOTIFY_CH_TEST_DONE:-} ;;
	check)     v=${CURSOR_NOTIFY_CH_CHECK:-} ;;
	*) die "unknown kind: $1" ;;
	esac
	[ -n "$v" ] || die "set CURSOR_NOTIFY_CH_* for kind: $1"
	printf '%s' "$v"
}

lbl()
{
	case $1 in
	memo)      printf '%s' ':memo: *MEMO*' ;;
	done)      printf '%s' ':white_check_mark: *DONE*' ;;
	issue)     printf '%s' ':warning: *ISSUE*' ;;
	test-done) printf '%s' ':test_tube: *TEST DONE*' ;;
	check)     printf '%s' ':mag: *CHECK*' ;;
	*) die "unknown kind: $1" ;;
	esac
}

vv_url()
{
	printf 'http://%s:%s' "$vv_host" "$vv_port"
}

vv_ready()
{
	curl -fsS "$(vv_url)/version" >/dev/null 2>&1
}

vv_await()
{
	i=0
	while [ "$i" -lt "$vv_wait" ]; do
		vv_ready && return 0
		i=$((i + 1))
		sleep 1
	done
	return 1
}

vv_start()
{
	vv_ready && return 0
	running=$(docker ps --filter "name=^/${vv_name}$" --format '{{.Names}}')
	if [ -z "$running" ]; then
		exists=$(docker ps -a --filter "name=^/${vv_name}$" --format '{{.Names}}')
		if [ -n "$exists" ]; then
			docker start "$vv_name" >/dev/null
		else
			docker run -d --name "$vv_name" \
				-p "${vv_host}:${vv_port}:50021" "$vv_img" >/dev/null
		fi
	fi
	vv_await || die "VOICEVOX Engine did not become ready"
}

synth()
{
	curl -fsS -X POST "$(vv_url)/audio_query?speaker=${vv_spk}" \
		--get --data-urlencode "text=$1" >"$2"
	curl -fsS -H 'Content-Type: application/json' -X POST \
		-d "@$2" "$(vv_url)/synthesis?speaker=${vv_spk}" >"$3"
}

play()
{
	if [ "$(uname -s)" = Darwin ] && command -v afplay >/dev/null 2>&1; then
		afplay "$1"
	elif command -v paplay >/dev/null 2>&1; then
		paplay "$1"
	elif command -v aplay >/dev/null 2>&1; then
		aplay "$1" >/dev/null 2>&1
	else
		printf '\a' >/dev/tty 2>/dev/null || :
	fi
}

speak()
{
	tmp=${TMPDIR:-/tmp}
	q=$(mktemp "${tmp%/}/cursor-vv-q.XXXXXX.json")
	w=$(mktemp "${tmp%/}/cursor-vv.XXXXXX.wav")
	trap 'rm -f "$q" "$w"' EXIT HUP INT TERM
	vv_start
	synth "$1" "$q" "$w"
	play "$w"
	rm -f "$q" "$w"
	trap - EXIT HUP INT TERM
}

json_payload()
{
	python3 -c 'import json,sys; print(json.dumps({"channel":sys.argv[1],"text":sys.argv[2],"mrkdwn":True,"username":sys.argv[3],"icon_emoji":sys.argv[4]}, ensure_ascii=False))' \
		"$1" "$2" "$user" "$icon"
}

post()
{
	[ -n "$tok" ] || die "SLACK_BOT_TOKEN is required"
	ch_id=$1
	kind=$2
	msg=$3
	text=$(printf '%s\n%s\n%s' "$mention" "$(lbl "$kind")" "$msg")
	res=$(curl -fsS -X POST "$api" \
		-H "Authorization: Bearer $tok" \
		-H 'Content-Type: application/json; charset=utf-8' \
		--data "$(json_payload "$ch_id" "$text")" 2>/dev/null) \
		|| die "Slack API request failed"
	printf '%s' "$res" | python3 -c 'import json,sys; sys.exit(0 if json.load(sys.stdin).get("ok") else 1)' \
		|| die "Slack API returned ok=false"
}

[ "$#" -ge 3 ] || usage
mode=$1
kind=$2
shift 2
msg=$*

case $kind in
memo|done|issue|test-done|check) ;;
*) die "unknown kind: $kind" ;;
esac

need docker
need curl
need python3

case $mode in
slack)
	post "$(ch "$kind")" "$kind" "$msg"
	speak "$msg"
	;;
local)
	speak "$msg"
	;;
*)
	usage
	;;
esac

envの中身はこうです。

# ~/.config/cursor-notify/env
SLACK_BOT_TOKEN=xoxb-...
CURSOR_NOTIFY_MENTION='<@U...>'
CURSOR_NOTIFY_CH_DONE=C...
CURSOR_NOTIFY_CH_TEST_DONE=C...
CURSOR_NOTIFY_CH_CHECK=C...
CURSOR_NOTIFY_CH_ISSUE=C...
CURSOR_NOTIFY_CH_MEMO=C...
sh ~/cursor-notify slack done "作業完了なのだ。コードを直したのだ。"
sh ~/cursor-notify local issue "作業停止なのだ。migrationが落ちているのだ。"

依存は docker、curl、python3 です。Dockerが止まっていると声は出ません。クラウド側のエージェント実行では、ローカルのスピーカーに届かないので、この方式はそのままでは使えません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?