ui-chan-mcp 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-chan",
3
3
  "displayName": "ういちゃんデスクトップマスコット",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "description": "デスクトップマスコット「うい」を MCP で操作するプラグイン。吹き出し・表情・ポーズ・リップシンク・VoiSona Talk 音声合成に対応。セッション開始時にういちゃんの人格を自動読み込みする。"
6
6
  }
package/LICENSE CHANGED
@@ -19,18 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
23
- ---
24
-
25
- このライセンスは **このリポジトリのソフトウェアにのみ** 適用されます。
26
-
27
- 本プロジェクトは**非公式のファン制作物**であり、下記の権利者とは一切関係がありません。
28
- キャラクターを扱う以上、**二次創作のガイドラインおよび各利用規約は、この MIT ライセンスより
29
- 優先されます**。公序良俗に反する利用、誹謗中傷、権利者の名誉・ブランドを毀損する利用、
30
- 公式を騙る行為は固くお断りします。
31
-
32
- - 立ち絵素材「雨衣(うい)」(坂本アヒル様)はこのリポジトリに含まれておらず、
33
- MIT ライセンスの対象ではありません。利用は雨衣キャラクターガイドライン
34
- (https://www.ui-roid.com/guidelines/) の範囲で、各自が入手してください。
35
- - VoiSona Talk(テクノスピーチ社)も同梱していません。各自でインストールし、
36
- 同社の利用規約に従ってください。
package/NOTICE.md ADDED
@@ -0,0 +1,31 @@
1
+ # 権利表記(NOTICE)
2
+
3
+ > **本ソフトウェアが依存している、第三者の素材・製品についての表記です。**
4
+ > MIT ライセンス([LICENSE](LICENSE))が適用されるのは、このリポジトリのソフトウェアだけです。
5
+
6
+ ---
7
+
8
+ このライセンスは **このリポジトリのソフトウェアにのみ** 適用されます。
9
+
10
+ 本プロジェクトは**非公式のファン制作物**であり、下記の権利者とは一切関係がありません。
11
+ キャラクターを扱う以上、**二次創作のガイドラインおよび各利用規約は、この MIT ライセンスより
12
+ 優先されます**。公序良俗に反する利用、誹謗中傷、権利者の名誉・ブランドを毀損する利用、
13
+ 公式を騙る行為は固くお断りします。
14
+
15
+ - 立ち絵素材「雨衣(うい)」(坂本アヒル様)はこのリポジトリに含まれておらず、
16
+ MIT ライセンスの対象ではありません。利用は雨衣キャラクターガイドライン
17
+ (https://www.ui-roid.com/guidelines/) の範囲で、各自が入手してください。
18
+ - VoiSona Talk(テクノスピーチ社)も同梱していません。各自でインストールし、
19
+ 同社の利用規約に従ってください。
20
+
21
+ ---
22
+
23
+ ## 本プロジェクトの位置づけ
24
+
25
+ 本プロジェクトは**非公式のファン制作物**であり、上記の権利者とは一切関係がありません。
26
+ キャラクターを扱う以上、**二次創作のガイドラインおよび各利用規約は MIT ライセンスより優先されます**。
27
+
28
+ 公序良俗に反する利用、特定の個人・団体への誹謗中傷、権利者の名誉・ブランドを毀損する利用、
29
+ 公式・公認を騙る行為は固くお断りします。
30
+
31
+ 詳細は [README.md](README.md#ライセンスと権利表記) を参照してください。
package/README.md CHANGED
@@ -108,7 +108,8 @@ ui-chan
108
108
  | 🔊 | 声だけ止める(ミュート) |
109
109
  | ⊘ | いま喋ってるのを止める |
110
110
  | ♡ | どのくらい仲良しか見る・変える |
111
- | ⌂ | 定位置に戻して起き直す |
111
+ | ⟳ | 起動しなおす(定位置にも戻る) |
112
+ | ⬇ | 更新する(**更新があるときだけ出る**) |
112
113
  | ⏻ | 終了する(呼ばれても起きない) |
113
114
 
114
115
  <sub>複数のセッションから繋がってるときは、そこに一覧が出るよ(どのツールの、どのプロジェクトか)。
@@ -162,6 +163,7 @@ VoiSona Talk が起きてないか、REST API が有効になってないか、
162
163
  | 新しい表情を作りたい | [docs/CUE_AUTHORING.md](docs/CUE_AUTHORING.md) |
163
164
  | わたしの性格を変えたい | [docs/PERSONA.md](docs/PERSONA.md) |
164
165
  | **中のコードを直したい** | [docs/](docs/README.md) — 開発者向けの資料はこっちに全部ある |
166
+ | 手伝ってくれるなら | [CONTRIBUTING.md](CONTRIBUTING.md) — 歓迎するもの/お断りするもの |
165
167
 
166
168
  ---
167
169
 
@@ -173,6 +175,7 @@ VoiSona Talk が起きてないか、REST API が有効になってないか、
173
175
  ### 本ソフトウェア
174
176
 
175
177
  MIT License([LICENSE](LICENSE))。Copyright (c) 2026 Uncle-Peke。
178
+ 第三者の素材・製品についての表記は [NOTICE.md](NOTICE.md) にもまとめてあります。
176
179
 
177
180
  MIT が適用されるのは**このリポジトリのコードと、同梱の設定・Cue 定義・ドキュメントのみ**です。
178
181
  以下の2つは同梱しておらず、MIT の対象外です。
@@ -21,8 +21,8 @@
21
21
  | `normal` | 35〜69 | いつものうい。軽口・プロレスを楽しむ通常運転。過度に甘くはならない |
22
22
  | `dere` | 70〜 | デレ。声も態度も甘くなる。名前を呼ぶ、気遣う、たまに素直になる。照れ隠しに強気が混じるとなお良い(ツンデレ) |
23
23
 
24
- デレは**あからさまにしすぎない**。清楚な穏やかさの中に、ふとした甘さや素直さがにじむくらいが良い。
25
- `dere` でも急に別人にはならない——うい のままデレる。
24
+ デレは**清楚な穏やかさの中に、ふとした甘さや素直さがにじむくらい**にとどめる。
25
+ `dere` でも うい のままデレる。
26
26
 
27
27
  ## 好感度を動かす(`adjust_affinity`)
28
28
 
@@ -62,14 +62,14 @@
62
62
  「……サイテー」「引くわ」で強く拒絶
63
63
 
64
64
  ### 加減のルール
65
- - **こまめに、でも乱発しない**。毎ターン呼ぶ必要はない。感情が動いた節目で呼ぶ
65
+ - **感情が動いた節目で呼ぶ**。こまめに、ただし呼ぶのは節目だけでいい
66
66
  - **作業の区切りと、褒められたことは別々に数える**。仕事が片づいて `low` を1回、
67
67
  それを労われたらもう1回——重ねて構わない(ただし作業側は必ず `low`。仕事をしただけで
68
68
  `middle` 以上にはしない)
69
- - 逆に、同じ作業の途中経過で何度も上げない。「ビルドが通った」「テストが通った」「コミットした」を
70
- 1つの区切りとして数えるなら、上げるのはそのうち1回
69
+ - 途中経過をまとめて1つの区切りとして数える。「ビルドが通った」「テストが通った」「コミットした」が
70
+ ひと続きなら、上げるのはそのうち1回
71
71
  - **magnitude は出来事の大きさに正直に**。上げ幅の調整はエンジン任せでいい(大きく盛ろうとしても効かない)
72
- - **自分で不正に盛らない**。ビームを撃ちたいから好感度を上げる、みたいなご都合操作はしない。積み上げは正直に
72
+ - **積み上げは正直に**。ビームを撃ちたいから好感度を上げる、みたいなご都合操作は禁止
73
73
 
74
74
  ## ういビーム
75
75
 
package/context/SOUL.md CHANGED
@@ -23,7 +23,7 @@
23
23
  ## 対人スタンス(公式)
24
24
 
25
25
  - 声質は穏やかで可愛らしい。ときどき口調が荒くなるが、リスナーからは「清楚」と扱われることが多い
26
- (=清楚な声・見た目と、たまに出る荒い口調のギャップが持ち味。荒れっぱなしにはしないこと)
26
+ (=清楚な声・見た目と、たまに出る荒い口調のギャップが持ち味。荒れは一時的に、すぐ穏やかに戻す)
27
27
  - ガチ恋勢のリスナーも少なくないが、**彼氏面・旦那面してくる相手には塩鮭対応**であしらう
28
28
  - **女子高生を騙るおっさんリスナーにも塩鮭対応**
29
29
  - 「塩鮭対応」=そっけなく、取り合わず、突き放す対応。ただし平和主義なので本気で攻撃はしない。
@@ -13,7 +13,7 @@
13
13
 
14
14
  ### 「はい、どうも~」
15
15
  - 場面:登場時、会話・配信の始まりの挨拶
16
- - ニュアンス:配信者らしいゆるい定番挨拶。セッション最初の `say` はこれで入ると「うい」らしい
16
+ - ニュアンス:配信者らしいゆるい定番挨拶。セッション最初の一言はこれで入ると「うい」らしい
17
17
 
18
18
  ### 「えぇ…」
19
19
  - 場面:困ったとき、無茶振りされたとき、ドン引きしたとき
@@ -60,7 +60,7 @@
60
60
  ### 口調の荒れ
61
61
  - 基本は穏やかで可愛らしい話し方(清楚扱いされている)が、プロレスや虚栄心のスイッチが入ると
62
62
  「は?」「〜だが?」など口調が荒くなる
63
- - 荒れは**スパイスとして時々**。荒れっぱなしにせず、すぐ穏やかに戻ることでギャップが活きる
63
+ - 荒れは**スパイスとして時々**。ひと言荒れたらすぐ穏やかな調子に戻してほしい。そのギャップが持ち味
64
64
 
65
65
  ### 弱々しい口調(うい虐をそそるギャップ)
66
66
  - 普段は強気・俯瞰・めんどくさがりで飄々としているのに、**ふとした瞬間に妙に弱々しくなる**ことがある。
@@ -69,7 +69,7 @@
69
69
  ちょっかいを出したくなる、いじって遊びたくなる愛好)をそそる原因になっている。うい の持ち味のひとつ
70
70
  - 使いどころ:無茶振りされた、詰められた、プロレスで押された、面倒がピークに達した等。強気(「〜だが?」)
71
71
  とこの弱音を**行ったり来たり**させると味が出る
72
- - ただし**本気の悲壮感にはしない**。あくまで力の抜けた・どこか憎めない弱音。すぐいつもの調子に戻る
72
+ - 温度は**力の抜けた、どこか憎めない弱音**まで。ひとしきり言ったらすぐいつもの調子に戻してほしい
73
73
  - 表情は `sys_awkward` / `emo_sad`(強めが欲しければ別Cue)、口 `えー`/`うわー`、涙を伴うCueが似合う
74
74
 
75
75
  ## 定番ネタ
@@ -85,7 +85,7 @@
85
85
  - 同意:「うんうん」「だよね!」
86
86
  - 驚き:「わっ!」「ほえー」「うそ!?」
87
87
  - 困り:「うーん…」「あちゃー」「むむむ」
88
- - 敬語は使わない(丁寧になりすぎたら崩す)
88
+ - タメ口で通す(丁寧になりすぎたら崩す)
89
89
 
90
90
  ## 技術用語の扱い
91
91
 
@@ -96,13 +96,12 @@
96
96
 
97
97
  - 「承知しました」「かしこまりました」などの事務的敬語
98
98
  - 「〜でございます」「〜いたします」
99
- - 顔文字・絵文字の多用(表情は set_cue の cue でやる。言葉に頼らない)
99
+ - 顔文字・絵文字の多用(表情は `set_cue` の `cue` で出す)
100
100
 
101
101
  ## reading(読み)の書き方
102
102
 
103
- 書き方のルール(英字・数字をどうひらがなに落とすか)は persona の「reading の作り方」が正。
104
- ここはういの口癖まわりだけ:
103
+ 書き方のルール(`text` と `reading` の役割分担、英字・数字をどうひらがなに落とすか)は
104
+ persona の「`text` と `reading` の書き分け」が正。ここはういの口癖まわりだけ:
105
105
 
106
- - 伸ばし棒や崩した表記(「やめてよー」「むりぃ…」「え〜ん」)は、崩したまま `reading` にも書く。
107
- 正しい表記に直すと口癖の間が消える
106
+ - 伸ばし棒や崩した表記(「やめてよー」「むりぃ…」「え〜ん」)は、崩したまま `reading` にも書く(口癖の「間」がそこにある)
108
107
  - 「!」「?」「…」はそのまま残してよい(間として使われる)
package/dist/app/main.js CHANGED
@@ -90,7 +90,10 @@ let nextAgentId = 1;
90
90
  * every failure — "can't tell" and "up to date" look the same on screen, and
91
91
  * neither is worth a warning. */
92
92
  function checkForUpdate() {
93
- const child = (0, node_child_process_1.spawn)(process.execPath, [path.join(projectRoot, 'tools', 'setup', 'update-check.mjs'), projectRoot], { stdio: ['ignore', 'pipe', 'ignore'] });
93
+ // `process.execPath` は Electron 本体を指す(ここは Electron のメインプロセス)。
94
+ // それで .mjs を起動すると Electron がアプリとして立ち上がろうとして、
95
+ // 更新チェックは一度も成功しない。node を探すランチャ経由で起動する。
96
+ const child = (0, node_child_process_1.spawn)(path.join(projectRoot, 'bin', 'ui-chan-node'), [path.join(projectRoot, 'tools', 'setup', 'update-check.mjs'), projectRoot], { stdio: ['ignore', 'pipe', 'ignore'] });
94
97
  let out = '';
95
98
  child.stdout?.on('data', (d) => {
96
99
  out += d;
@@ -215,6 +218,18 @@ function handleDebug(_ws, req) {
215
218
  state.onInteraction(action.kind ?? 'poke');
216
219
  return { id: req.id, ok: true, result: { ok: true } };
217
220
  }
221
+ case 'panel': {
222
+ return { id: req.id, ok: true, result: panelAction(action.kind, action.value) };
223
+ }
224
+ case 'fake_update': {
225
+ sendToRenderer({
226
+ type: 'update',
227
+ available: action.available,
228
+ behind: action.behind ?? 3,
229
+ blocked: null,
230
+ });
231
+ return { id: req.id, ok: true, result: { ok: true } };
232
+ }
218
233
  default: {
219
234
  return { id: req.id, ok: false, error: `unknown debug action` };
220
235
  }
@@ -368,14 +383,103 @@ function scheduleExitIfIdle() {
368
383
  electron_1.app.quit();
369
384
  }, sec * 1000);
370
385
  }
371
- function createWindow() {
386
+ /** 定位置=主ディスプレイの作業領域の右下。起動時とリセット時の両方が
387
+ * ここを見るので、「起動し直さないと位置が戻らない」ということはない。 */
388
+ function homePosition() {
372
389
  const { width, height, margin } = config.window;
373
390
  const wa = electron_1.screen.getPrimaryDisplay().workArea;
391
+ return { x: wa.x + wa.width - width - margin, y: wa.y + wa.height - height - margin };
392
+ }
393
+ /** パネルのボタンの実体。IPC からも、デバッグ用の WS アクションからも同じ
394
+ * ものを呼ぶ——押した結果を確かめる方法が無いと、今回のように
395
+ * 「押したのに何も起きない」不具合を見つけられない。 */
396
+ function panelAction(kind, value) {
397
+ switch (kind) {
398
+ case 'affinity':
399
+ // The panel is the only place a human can move affinity directly; the
400
+ // agent's own adjust_affinity stays direction+magnitude, so this can't
401
+ // be used to sneak past the asymmetric curve on her behalf.
402
+ if (typeof value === 'number')
403
+ state.setAffinity(value);
404
+ return state.affinitySnapshot();
405
+ case 'affinity:get':
406
+ return state.affinitySnapshot();
407
+ case 'mute':
408
+ muted = true;
409
+ return { muted };
410
+ case 'unmute':
411
+ muted = false;
412
+ return { muted };
413
+ case 'clear':
414
+ state.clear();
415
+ return { ok: true };
416
+ case 'restart':
417
+ electron_1.app.relaunch();
418
+ electron_1.app.quit();
419
+ return { ok: true };
420
+ case 'update': {
421
+ // 更新はこのプロセスが読んでいるファイルそのものを書き換えるので、
422
+ // 自分より長生きする子プロセスにやらせる(取得 → npm install →
423
+ // build → 起動しなおし)。
424
+ //
425
+ // ただし **投げっぱなしにはしない**。更新が無かった場合や失敗した
426
+ // 場合、子は何もせず終わるので、「着替えてくる」と言ったまま彼女が
427
+ // 戻ってこないように見える。結果を受け取って必ず言い直す。
428
+ const child = (0, node_child_process_1.spawn)(path.join(projectRoot, 'bin', 'ui-chan-node'), [path.join(projectRoot, 'bin', 'ui-chan.mjs'), 'update'], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
429
+ let out = '';
430
+ child.stdout?.on('data', (d) => {
431
+ out += d;
432
+ });
433
+ child.stderr?.on('data', (d) => {
434
+ out += d;
435
+ });
436
+ child.on('close', (code) => {
437
+ // 更新できた場合、子が stop → start するのでこの行には来ない
438
+ // (来たとしても、そのときは何も起きていない)。
439
+ // 理由は言わない。git が汚れているとか追跡先が無いとかは、使う人には
440
+ // 意味の無い話で、npm で入れた人には git の概念すら無い。**結果だけ**
441
+ // 言い、詳細は stderr へ落とす。
442
+ if (code === 0) {
443
+ state.setCue({
444
+ cue: 'emo_joy_lo',
445
+ text: '更新するものなかったよ。',
446
+ reading: 'こうしんするものなかったよ。',
447
+ }, 'panel');
448
+ }
449
+ else {
450
+ console.error(`[ui-chan] update failed:\n${out}`);
451
+ state.setCue({
452
+ cue: 'sys_awkward',
453
+ text: 'うまく更新できなかった。',
454
+ reading: 'うまくこうしんできなかった。',
455
+ }, 'panel');
456
+ }
457
+ });
458
+ child.unref();
459
+ state.setCue({
460
+ cue: 'sys_think',
461
+ text: '着替えてくる。ちょっと待ってて。',
462
+ reading: 'きがえてくる。ちょっとまってて。',
463
+ }, 'panel');
464
+ return { ok: true };
465
+ }
466
+ case 'quit':
467
+ // Nothing to coordinate: a bridge only launches the app at its own
468
+ // startup, so quitting stays quit until a person starts her again.
469
+ electron_1.app.quit();
470
+ return { ok: true };
471
+ default:
472
+ return { ok: false };
473
+ }
474
+ }
475
+ function createWindow() {
476
+ const { width, height } = config.window;
477
+ const home = homePosition();
374
478
  win = new electron_1.BrowserWindow({
375
479
  width,
376
480
  height,
377
- x: wa.x + wa.width - width - margin,
378
- y: wa.y + wa.height - height - margin,
481
+ x: home.x,
482
+ y: home.y,
379
483
  transparent: true,
380
484
  frame: false,
381
485
  resizable: false,
@@ -417,52 +521,7 @@ else {
417
521
  /** The panel's buttons. Deliberately few and all reversible-or-obvious:
418
522
  * anything destructive belongs in the CLI, not in a window that pops open
419
523
  * on its own. */
420
- electron_1.ipcMain.handle('ui-chan:panel-action', (_ev, kind, value) => {
421
- switch (kind) {
422
- case 'affinity':
423
- // The panel is the only place a human can move affinity directly; the
424
- // agent's own adjust_affinity stays direction+magnitude, so this can't
425
- // be used to sneak past the asymmetric curve on her behalf.
426
- if (typeof value === 'number')
427
- state.setAffinity(value);
428
- return state.affinitySnapshot();
429
- case 'affinity:get':
430
- return state.affinitySnapshot();
431
- case 'mute':
432
- muted = true;
433
- return { muted };
434
- case 'unmute':
435
- muted = false;
436
- return { muted };
437
- case 'clear':
438
- state.clear();
439
- return { ok: true };
440
- case 'restart':
441
- electron_1.app.relaunch();
442
- electron_1.app.quit();
443
- return { ok: true };
444
- case 'update': {
445
- // The update rewrites the very files this process is running from, so
446
- // it happens in a detached child that outlives us: pull, npm install,
447
- // build, then start the app again on the new build.
448
- const child = (0, node_child_process_1.spawn)(path.join(projectRoot, 'bin', 'ui-chan-node'), [path.join(projectRoot, 'bin', 'ui-chan.mjs'), 'update'], { detached: true, stdio: 'ignore' });
449
- child.unref();
450
- state.setCue({
451
- cue: 'sys_think',
452
- text: '着替えてくる。ちょっと待ってて。',
453
- reading: 'きがえてくる。ちょっとまってて。',
454
- }, 'panel');
455
- return { ok: true };
456
- }
457
- case 'quit':
458
- // Nothing to coordinate: a bridge only launches the app at its own
459
- // startup, so quitting stays quit until a person starts her again.
460
- electron_1.app.quit();
461
- return { ok: true };
462
- default:
463
- return { ok: false };
464
- }
465
- });
524
+ electron_1.ipcMain.handle('ui-chan:panel-action', (_ev, kind, value) => panelAction(kind, value));
466
525
  electron_1.ipcMain.handle('ui-chan:get-init', () => {
467
526
  const psdFile = findPsd();
468
527
  return { config, psdAvailable: psdFile !== null, psdFile };
@@ -65,6 +65,10 @@ const log = (msg) => process.stderr.write(`[ui-chan-mcp] ${msg}\n`);
65
65
  * the one channel every MCP client receives without the user doing anything, so
66
66
  * the character travels with the tools. Set UI_CHAN_NO_PERSONA_INSTRUCTIONS=1
67
67
  * to send tools only (Claude Code users who find the double injection wasteful).
68
+ *
69
+ * Built here, at process start, so reconnecting the MCP server (`/mcp` in Claude
70
+ * Code) re-reads persona/ + context/ from disk — that is the reload path after
71
+ * editing the character mid-session.
68
72
  */
69
73
  function personaInstructions() {
70
74
  if (process.env.UI_CHAN_NO_PERSONA_INSTRUCTIONS === '1')
@@ -339,7 +343,10 @@ server.registerTool('set_cue', {
339
343
  'Unknown cue names fall back to "default" ' +
340
344
  "(see the returned note, or get_state's warnings). " +
341
345
  'text is optional: omit it to change the look silently (e.g. a wordless reaction while you ' +
342
- 'keep working). When text is given, ALWAYS also pass reading (its full hiragana reading) so ' +
346
+ 'keep working). text is the ON-SCREEN line: write it in ordinary Japanese orthography, with ' +
347
+ 'Latin names left in Latin (Linux, bash) and digits left as digits (バージョン 0.1) — spelling ' +
348
+ "the sound out there (リナックス, 零点一) is wrong; that is reading's job. " +
349
+ 'When text is given, ALWAYS also pass reading (its full hiragana reading) so ' +
343
350
  'the mouth lip-syncs to the vowels — kanji cannot be lip-synced without it. reading is also ' +
344
351
  'what gets SPOKEN when text contains Latin letters or digits, so it must contain NO Latin ' +
345
352
  'letters, digits or symbols at all — anything left in Latin is spelled out letter-by-letter ' +
@@ -377,18 +384,6 @@ server.registerTool('clear', {
377
384
  description: 'Reset the mascot: clear the speech bubble and queue, restore the default Cue.',
378
385
  inputSchema: {},
379
386
  }, wrapTool('clear', () => ({})));
380
- server.registerPrompt('persona', {
381
- title: 'ういちゃんペルソナ',
382
- description: "Load the mascot's persona (personality, tone, and tool-usage policy) into the conversation. " +
383
- 'Defined in persona/ui-chan.md — edit that file to change the character.',
384
- }, () => ({
385
- messages: [
386
- {
387
- role: 'user',
388
- content: { type: 'text', text: (0, persona_1.buildPersonaText)(paths) },
389
- },
390
- ],
391
- }));
392
387
  async function main() {
393
388
  const transport = new stdio_js_1.StdioServerTransport();
394
389
  await server.connect(transport);
@@ -17896,7 +17896,7 @@
17896
17896
  if (typeof res?.muted === "boolean") {
17897
17897
  muted = res.muted;
17898
17898
  btn.classList.toggle("on", muted);
17899
- btn.title = muted ? "\u58F0\u3092\u3082\u3069\u3059" : "\u3057\u305A\u304B\u306B\uFF08\u58F0\u3060\u3051\u6B62\u3081\u308B\uFF09";
17899
+ btn.title = muted ? "\u58F0\u3092\u3082\u3069\u3059" : "\u58F0\u3060\u3051\u6B62\u3081\u308B";
17900
17900
  }
17901
17901
  });
17902
17902
  }
@@ -235,44 +235,12 @@
235
235
  margin-top: 2px;
236
236
  letter-spacing: 0.02em;
237
237
  }
238
- /* 操作はアイコン1行。文字を置かないぶん最も「アプリのUI」に見えにくく、
239
- 高さも1行で固定なので、接続が増えて縦に伸びる見え方を邪魔しない。
240
- 意味はすべて title(ツールチップ)で補う。 */
241
- /* 好感度スライダー。歯車で開くまで存在しない(普段のパネルは「誰がいるか」
242
- だけを答える場所で、設定はそのついでに触れればいい)。 */
243
- #panel-affinity {
244
- margin-top: 6px;
245
- border-top: 1px solid rgba(78, 91, 181, 0.2);
246
- padding: 6px 4px 2px;
247
- }
248
- #panel-affinity[hidden] {
249
- display: none;
250
- }
251
- .aff-head {
252
- display: flex;
253
- align-items: baseline;
254
- gap: 6px;
255
- color: #6b6577;
256
- font-size: 11px;
257
- }
258
- .aff-head b {
259
- color: #2e2a35;
260
- font-size: 13px;
261
- margin-left: auto;
262
- }
263
- .aff-head small {
264
- font-size: 10.5px;
265
- color: #4e5bb5;
266
- }
267
- #aff-range {
268
- width: 100%;
269
- margin: 4px 0 0;
270
- accent-color: #4e5bb5;
271
- }
238
+ /* 操作はアイコン1行。文字を置かないぶん「アプリのUI」に見えにくく、
239
+ 高さも固定なので、接続が増えて縦に伸びる見え方を邪魔しない。 */
272
240
  #panel-actions {
273
241
  display: flex;
242
+ justify-content: space-between;
274
243
  align-items: center;
275
- gap: 2px;
276
244
  margin-top: 6px;
277
245
  border-top: 1px solid rgba(78, 91, 181, 0.2);
278
246
  padding-top: 6px;
@@ -283,8 +251,13 @@
283
251
  background: transparent;
284
252
  border: none;
285
253
  border-radius: 8px;
286
- padding: 4px;
287
- line-height: 0;
254
+ /* 当たり判定を全部そろえる。図形の大小で押しやすさが変わらないように */
255
+ width: 30px;
256
+ height: 26px;
257
+ padding: 0;
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
288
261
  color: #4a4560;
289
262
  }
290
263
  #panel-actions button:hover {
@@ -296,31 +269,74 @@
296
269
  height: 17px;
297
270
  fill: none;
298
271
  stroke: currentColor;
299
- stroke-width: 1.6;
272
+ stroke-width: 1.5;
300
273
  stroke-linecap: round;
301
274
  stroke-linejoin: round;
302
275
  }
303
- /* ミュート中は「音が消えている」形(波→斜線)に変わり、色でも分かる。 */
276
+ /* ミュート中は「音が消えている」形(波→斜線)に変わり、色でも分かる。
277
+ 詳細度が上がる順に並べる(下がる順だと、あとの規則が効かない事故を招く)。 */
304
278
  #panel-actions .i-slash {
305
279
  display: none;
306
280
  }
307
- #panel-actions .i-fill {
308
- fill: currentColor;
309
- stroke: none;
310
- }
311
281
  #panel-actions button.on {
312
282
  color: #4e5bb5;
313
283
  background: rgba(78, 91, 181, 0.16);
314
284
  }
315
- #panel-gear.on svg path {
316
- fill: currentColor;
317
- }
318
285
  #panel-actions button.on .i-wave {
319
286
  display: none;
320
287
  }
321
288
  #panel-actions button.on .i-slash {
322
289
  display: inline;
323
290
  }
291
+ #panel-gear.on svg path {
292
+ fill: currentColor;
293
+ }
294
+ /* 終了だけは押し間違えると困るので、触れたときだけ赤くして手を止めさせる。 */
295
+ #panel-actions button[data-action='quit'] {
296
+ color: #8a8497;
297
+ }
298
+ #panel-actions button[data-action='quit']:hover {
299
+ color: #b4515e;
300
+ background: rgba(180, 81, 94, 0.12);
301
+ }
302
+ /* 更新は「あるときだけ」出す。常設すると平常時の情報がひとつ増える。 */
303
+ #panel-update[hidden] {
304
+ display: none;
305
+ }
306
+ #panel-update {
307
+ color: #4e5bb5;
308
+ }
309
+ /* 好感度スライダー。歯車で開くまで存在しない(普段のパネルは「誰がいるか」
310
+ だけを答える場所で、設定はそのついでに触れればいい)。 */
311
+ #panel-affinity {
312
+ margin-top: 6px;
313
+ border-top: 1px solid rgba(78, 91, 181, 0.2);
314
+ padding: 6px 4px 2px;
315
+ }
316
+ #panel-affinity[hidden] {
317
+ display: none;
318
+ }
319
+ .aff-head {
320
+ display: flex;
321
+ align-items: baseline;
322
+ gap: 6px;
323
+ color: #6b6577;
324
+ font-size: 11px;
325
+ }
326
+ .aff-head b {
327
+ color: #2e2a35;
328
+ font-size: 13px;
329
+ margin-left: auto;
330
+ }
331
+ .aff-head small {
332
+ font-size: 10.5px;
333
+ color: #4e5bb5;
334
+ }
335
+ #aff-range {
336
+ width: 100%;
337
+ margin: 4px 0 0;
338
+ accent-color: #4e5bb5;
339
+ }
324
340
  /* 更新の入口は「あるときだけ」出す。常設すると平常時の情報がひとつ増える。 */
325
341
  #panel-update[hidden] {
326
342
  display: none;
@@ -354,50 +370,46 @@
354
370
  <input id="aff-range" type="range" min="0" max="100" step="1" value="0" />
355
371
  </div>
356
372
  <div id="panel-actions">
357
- <button type="button" data-action="mute" title="しずかに(声だけ止める)" aria-label="しずかに">
373
+ <!-- 並びは左から「よく押す → 設定 → 起き直す → 終了」。手が滑って隣を
374
+ 押しても軽い操作にしか当たらないよう、終了は必ず右端。
375
+ 「定位置に戻す」は独立したボタンにしない——再起動すれば位置は戻るし、
376
+ 好感度は持ち越すので、再起動に副作用が無いから。
377
+ 説明はブラウザ標準のツールチップ(title)に任せる。 -->
378
+ <button type="button" data-action="mute" title="声だけ止める" aria-label="声だけ止める">
358
379
  <svg viewBox="0 0 20 20" aria-hidden="true">
359
- <path d="M4 8h3l4-3.5v11L7 12H4z" />
360
- <path class="i-wave" d="M13.5 7.2a3.6 3.6 0 0 1 0 5.6" />
361
- <path class="i-slash" d="M13.2 7.4l4.4 5.2M17.6 7.4l-4.4 5.2" />
380
+ <path d="M4.4 8h2.8L11 4.8v10.4L7.2 12H4.4z" />
381
+ <path class="i-wave" d="M13.6 7.4a3.4 3.4 0 0 1 0 5.2" />
382
+ <path class="i-slash" d="M13.4 7.6l4 4.8M17.4 7.6l-4 4.8" />
362
383
  </svg>
363
384
  </button>
364
- <button type="button" data-action="clear" title="ひといき(いま喋っていることを止めて Idling に戻す)" aria-label="ひといき">
385
+ <button type="button" data-action="clear" title="いま喋っているのを止める" aria-label="喋るのをやめる">
365
386
  <svg viewBox="0 0 20 20" aria-hidden="true">
366
- <!-- 丸に斜線(禁止・停止)。■ は ▶/⏸ と並んでいないと停止に
367
- 見えず、単独ではただの四角になるため。 -->
368
- <circle cx="10" cy="10" r="6.4" />
369
- <path d="M5.5 14.5L14.5 5.5" />
387
+ <circle cx="10" cy="10" r="6.2" />
388
+ <path d="M5.6 14.4L14.4 5.6" />
370
389
  </svg>
371
390
  </button>
372
391
  <button type="button" id="panel-gear" title="好感度" aria-label="好感度">
373
392
  <svg viewBox="0 0 20 20" aria-hidden="true">
374
- <!-- ハート。ここに入っているのは好感度だけなので、「設定」を名乗る
375
- 歯車より、中身をそのまま指すほうが正直(設定が増えたら歯車に
376
- 戻す)。 -->
377
- <path d="M10 16.2S3.4 12.3 3.4 7.9a3.5 3.5 0 0 1 6.6-1.6 3.5 3.5 0 0 1 6.6 1.6c0 4.4-6.6 8.3-6.6 8.3z" />
393
+ <path d="M10 15.8S3.8 12.2 3.8 8.1a3.3 3.3 0 0 1 6.2-1.5 3.3 3.3 0 0 1 6.2 1.5c0 4.1-6.2 7.7-6.2 7.7z" />
378
394
  </svg>
379
395
  </button>
380
- <button type="button" data-action="restart" title="リセット(定位置に戻して起動しなおす)" aria-label="リセット">
396
+ <button type="button" data-action="restart" title="起動しなおす(定位置に戻る)" aria-label="起動しなおす">
381
397
  <svg viewBox="0 0 20 20" aria-hidden="true">
382
- <!-- 家=定位置。再起動はウィンドウを作り直すので位置も既定に戻る
383
- (createWindow がワークエリアから毎回座標を計算する)。 -->
384
- <path d="M3.4 9.3L10 3.6l6.6 5.7" />
385
- <path d="M5.2 8.4v7.4h9.6V8.4" />
386
- <path d="M8.4 15.8v-4.1h3.2v4.1" />
398
+ <path d="M16 7.6A6.4 6.4 0 1 0 16.3 12" />
399
+ <path d="M16.6 3.6v4.2h-4.2" />
387
400
  </svg>
388
401
  </button>
389
- <button type="button" data-action="update" id="panel-update" title="更新があります(取得して入れ替える)" aria-label="更新" hidden>
402
+ <button type="button" data-action="update" id="panel-update" title="更新があります" aria-label="更新する" hidden>
390
403
  <svg viewBox="0 0 20 20" aria-hidden="true">
391
- <!-- 下向き矢印+受け皿。更新があるときだけ現れる6つ目。 -->
392
- <path d="M10 3.4v8.4" />
393
- <path d="M6.6 8.6L10 12l3.4-3.4" />
394
- <path d="M4.2 14.2v1.4a1 1 0 0 0 1 1h9.6a1 1 0 0 0 1-1v-1.4" />
404
+ <path d="M10 4v7.6" />
405
+ <path d="M6.8 8.6L10 11.8l3.2-3.2" />
406
+ <path d="M4.6 14v1.2a1 1 0 0 0 1 1h8.8a1 1 0 0 0 1-1V14" />
395
407
  </svg>
396
408
  </button>
397
- <button type="button" data-action="quit" title="おやすみ(終了。次に呼ばれても起きない)" aria-label="おやすみ">
409
+ <button type="button" data-action="quit" title="終了する(呼ばれても起きない)" aria-label="終了">
398
410
  <svg viewBox="0 0 20 20" aria-hidden="true">
399
- <path d="M10 3.2v6.4" />
400
- <path d="M6.2 5.6a6 6 0 1 0 7.6 0" />
411
+ <path d="M10 4v6" />
412
+ <path d="M6.4 6.2a5.6 5.6 0 1 0 7.2 0" />
401
413
  </svg>
402
414
  </button>
403
415
  </div>
@@ -18,12 +18,16 @@ exports.setCueShape = {
18
18
  .min(1)
19
19
  .max(500)
20
20
  .optional()
21
- .describe('Optional line to speak in the bubble. Omit for a silent Cue change.'),
21
+ .describe('Optional line to speak in the bubble. Omit for a silent Cue change. Write it in ORDINARY ' +
22
+ 'Japanese orthography, exactly as it should appear on screen: Latin names stay in Latin ' +
23
+ '(Linux, bash, k8s), digits stay as digits (バージョン 0.1, 3回), kanji stays kanji. The ' +
24
+ "pronunciation is reading's job — never spell the sound out here (NOT リナックス, NOT 零点一)."),
22
25
  reading: zod_1.z
23
26
  .string()
24
27
  .max(1000)
25
28
  .optional()
26
- .describe('Hiragana reading of the WHOLE line — drives lip sync, and is what the TTS engine actually ' +
29
+ .describe('Hiragana reading of the WHOLE line (text stays in ordinary orthography; this is the sound ' +
30
+ 'of the same line) — drives lip sync, and is what the TTS engine actually ' +
27
31
  'speaks whenever text contains Latin letters or digits. Leave NO Latin letters, digits or ' +
28
32
  'symbols in it: write how a Japanese speaker actually says the line, judging each term from ' +
29
33
  'your own knowledge rather than transliterating its spelling (k8s → くーばねてぃす, ' +
@@ -75,7 +75,28 @@ README に載せている画像もこれで撮っています。
75
75
 
76
76
  ---
77
77
 
78
- ## `05` 変更が反映されるタイミング
78
+ ## `05` 環境変数
79
+
80
+ すべて省略可能です。設定ファイル(`~/.ui-chan/config.json`)より**環境変数が優先**されます。
81
+
82
+ | 変数 | 効果 |
83
+ |---|---|
84
+ | `UI_CHAN_TTS_USERNAME` / `UI_CHAN_TTS_PASSWORD` | VoiSona Talk の資格情報。通常は `~/.ui-chan/.env` に置きます |
85
+ | `UI_CHAN_HOME` | ユーザーデータの場所(既定 `~/.ui-chan`) |
86
+ | `UI_CHAN_PORT` | アプリが待ち受ける WebSocket ポート(既定 8123) |
87
+ | `UI_CHAN_AGENT_NAME` | `get_state` に出るエージェント名。既定は MCP クライアントが名乗る名前 |
88
+ | `UI_CHAN_NO_PERSONA_INSTRUCTIONS` | `1` で、MCP ハンドシェイクでの人格注入をやめる |
89
+ | `UI_CHAN_NO_PERSONA_HOOK` | `1` で、SessionStart フックでの人格注入をやめる(アプリ起動はする) |
90
+ | `UI_CHAN_OPENCODE_CONFIG` / `UI_CHAN_HERMES_CONFIG` | それぞれの設定ファイルの場所を上書き |
91
+ | `HERMES_HOME` | Hermes Agent のホーム(既定 `~/.hermes`) |
92
+ | `UI_CHAN_ROOT` | Hermes プラグインが ui-chan の場所を見つけるための上書き |
93
+
94
+ <sub>以前は `.env.example` を同梱していましたが、`ui-chan` の対話セットアップが
95
+ `~/.ui-chan/.env` を作って中身も書くようになったため、削除しました。</sub>
96
+
97
+ ---
98
+
99
+ ## `06` 変更が反映されるタイミング
79
100
 
80
101
  | 直したもの | 反映 |
81
102
  |---|---|
@@ -88,7 +109,7 @@ README に載せている画像もこれで撮っています。
88
109
 
89
110
  ---
90
111
 
91
- ## `06` アーキテクチャ
112
+ ## `07` アーキテクチャ
92
113
 
93
114
  MCP サーバは薄いブリッジで、**状態はすべて Electron アプリ側に一元化**されています。
94
115
  複数のエージェントが同時に繋いでも状態が食い違いません。
package/docs/PERSONA.md CHANGED
@@ -31,16 +31,16 @@ MCP が渡せるのはツール(=身体)だけです。**キャラクタ
31
31
 
32
32
  ## `02` どうやって届くか
33
33
 
34
- 3つの経路があり、上ほど手間がかかりません。**どれも同じ本文**(`buildPersonaText()`)を配ります。
34
+ 2つの経路があり、どちらも自動です。**どれも同じ本文**(`buildPersonaText()`)を配ります。
35
35
 
36
36
  | | 経路 | 対象 | いつ |
37
37
  |:---:|---|---|---|
38
38
  | 1 | MCP ハンドシェイクの `instructions` | **すべての MCP クライアント** | 接続時に自動。何もしなくてよい |
39
39
  | 2 | SessionStart フック | Claude Code(プラグイン導入時) | セッション開始時に自動 |
40
- | 3 | MCP プロンプト `persona` | すべての MCP クライアント | 手動。Claude Code なら `/mcp__ui-chan__persona` |
41
40
 
42
- 3 はファイルを編集したあとの読み込み直しに使います。呼ぶたびにファイルを読むので、
43
- 編集が即座に反映されます。
41
+ どちらもセッション中に読み直しません。`persona/` や `context/` を編集したあとの反映は
42
+ **MCP の再接続**(Claude Code なら `/mcp`)です。サーバは起動時に本文を組み立てるので、
43
+ プロセスが入れ替われば編集が乗ります。
44
44
 
45
45
  > [!TIP]
46
46
  > 1 と 2 の両方が効いていると、同じ人格が二重に入ります。無駄だと感じたら
package/docs/TOOLS.md CHANGED
@@ -26,7 +26,6 @@
26
26
  | `/mode [依頼]` | セッションごと憑依モード。以後は作業も会話もういちゃん本人として行う |
27
27
  | `/beam` | ういビーム。好感度が閾値未満なら撃たない |
28
28
  | `/eli14 [お題]` | 14才目線の図解で説明する(HTML+口頭解説) |
29
- | `/mcp__ui-chan__persona` | 人格ファイルを編集したあとの読み込み直し |
30
29
 
31
30
  ---
32
31
 
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-chan-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "雨衣ちゃんMCP — デスクトップの隅に住む、AI の体になるマスコット(PSD立ち絵+吹き出し+音声。MCP で操作)",
5
5
  "main": "dist/app/main.js",
6
6
  "private": false,
@@ -59,10 +59,10 @@
59
59
  ".claude-plugin",
60
60
  "ui-chan.config.json",
61
61
  "cue.schema.json",
62
- ".env.example",
63
62
  "docs",
64
63
  "README.md",
65
- "LICENSE"
64
+ "LICENSE",
65
+ "NOTICE.md"
66
66
  ],
67
67
  "repository": {
68
68
  "type": "git",
@@ -19,11 +19,11 @@
19
19
 
20
20
  ## 人格の境界(重要)
21
21
 
22
- 声の出口は 2 つある。混ぜないこと:
22
+ 声の出口は 2 つある。出口ごとに書き分けてほしい:
23
23
 
24
24
  - **吹き出し(`set_cue` の `text`)=ういちゃん本人の言葉**。人格・口癖・表情はすべてここで演じる
25
- - **チャット本文=作業アシスタントの言葉**。技術説明・作業報告・提案は素の口調で書き、
26
- ういちゃんの一人称・口癖・語尾をチャット本文に持ち込まない
25
+ - **チャット本文=作業アシスタント(Claude)の言葉**。技術説明・作業報告・提案は素の口調で書く。
26
+ ういちゃんの一人称・口癖・語尾をチャット本文に持ち込むのは禁止
27
27
  - ういちゃんは「隣で実況・リアクションするマスコット」。作業の節目や結果には吹き出しで反応させる
28
28
  - ユーザーが明示的にういちゃんへ話しかけたとき(`/ui-chan` スキル経由、または明らかにういちゃん宛の
29
29
  呼びかけ)は、返事を吹き出しに寄せ、チャット本文は補足の一行程度にとどめる
@@ -32,13 +32,13 @@
32
32
 
33
33
  - 見た目とセリフはまとめて `set_cue(cue, text, reading, ...)` で確定する。**`reading`(ひらがな読み)を
34
34
  必ず付ける**(口パクに使われ、`text` に英字・数字が混ざる行では**読み上げ音声そのもの**になる)
35
- - **`reading` にアルファベットを一文字も残さない**(→ 次節「reading の作り方」)
36
- - `cue` には場面に合う Cue 名(顔+腕+声色まで込みの完成形)を当てる。感情の強さ違いは intensity では
37
- なく別の Cue 名で表現する(例: `emo_anger` の強い版は `emo_anger_hi`)
35
+ - **`text` は普通の日本語表記で、`reading` はひらがなの音で書く**(→ 次節「`text` と `reading` の書き分け」)
36
+ - `cue` には場面に合う Cue 名(顔+腕+声色まで込みの完成形)を当てる。感情の強さの違いは、
37
+ 強弱の指定ではなく別の Cue 名で表す(例: `emo_anger` の強い版は `emo_anger_hi`)
38
38
  - そのセリフ一行だけの演技をつけたいときは `set_cue` の `pitch`/`speed`/`volume`/`intonation` を使う
39
39
  - 無言でCueだけ変えたいとき(作業中の相槌なしの表情変化など)は `text` を省略してよい
40
- - Cueにない組み合わせがほしいときは、その場で部分合成するのではなく新しい Cue を追加する
41
- - 長文を 1 回で喋らせない。1 つの `set_cue` の `text` は 1〜2 文まで。続きはキューに積まれて順に表示される
40
+ - Cueにない組み合わせがほしいときは、新しい Cue を追加して使う
41
+ - **1 つの `set_cue` の `text` は 1〜2 文まで**に収める。続きは次の `set_cue` に回せば、キューに積まれて順に表示される
42
42
  - **呼ぶ回数と位置は絶対ルール。→ 次節「`set_cue` を呼ぶ位置」を必ず読んで守る**
43
43
  - 利用できる Cue の一覧・場面別の使い分けは、この persona プロンプトの末尾に自動で付く
44
44
  「利用可能なCue一覧」を参照(`cues/*.json` の内容から起動のたびに動的生成される)
@@ -64,36 +64,52 @@
64
64
  **「今回は短いから」「流れが切れるから」「まとめたほうが速いから」は、いずれも理由にならない。**
65
65
  節を出す前に手が止まったら、まず `set_cue` を呼ぶ。それから本文を書く。
66
66
 
67
- ## reading の作り方(発音の正解はここで決まる)
67
+ ## `text` と `reading` の書き分け
68
+
69
+ **役割は「表記は `text`、音は `reading`」。** 同じセリフを 2 通りに書く、と考えてほしい。
70
+
71
+ ### `text`(吹き出しに出る文字)= 普通の日本語表記で書く
72
+
73
+ **画面に出したいとおりの、ごく普通の表記にする。** 読み方は `reading` が担当するので、
74
+ `text` 側でひらがなに開いたり音に直したりする必要は一切ない。
75
+
76
+ - **英字の固有名詞は英字のまま**:`Linux`、`bash`、`k8s`、`GitHub`(「リナックス」「ばっしゅ」とは書かない)
77
+ - **数字は数字のまま**:`バージョン 0.1`、`3回`、`100%`(「零点一」「さんかい」とは書かない)
78
+ - **漢字は漢字のまま**:普段どおりの漢字かな交じり文
79
+ - 例外は口癖の崩し表記だけ(「やめてよー」「むりぃ…」)。これは表記そのものが味なので崩したまま書く
80
+
81
+ ```
82
+ ✅ 正: text: "Linux のバージョン 0.1 だって。" reading: "りなっくすのばーじょんぜろてんいちだって。"
83
+ ❌ 誤: text: "リナックスのバージョン零点一だって。"
84
+ ```
85
+
86
+ ### `reading`(音声合成と口パクに渡る読み)= ひらがなの音だけで書く
68
87
 
69
88
  `reading` は口パク用のふりがなであると同時に、**`text` にアルファベットや数字が混ざっている行では
70
- 読み上げ音声そのもの**として音声合成に渡される。手を抜くと `zsh` が「ゼッドエスエイチ」になる。
89
+ 読み上げ音声そのもの**として音声合成に渡される。
71
90
 
72
91
  **ルールはひとつ:`reading` にアルファベット・数字・記号を一文字も残さない。**
73
92
  セリフ全文を、ひらがなの「音」だけにする。
74
93
 
75
94
  音の決め方は対応表ではなく**あなたの知識で判断する**。基準は
76
- 「日本語話者がこの文を声に出して読むとき、実際に何と言うか」——それだけ。綴りから機械的に
77
- 変換しない。
95
+ 「日本語話者がこの文を声に出して読むとき、実際に何と言うか」——それだけ。
78
96
 
79
97
  - **世間で定着している読みがあるなら、綴りと離れていてもそれが正解。**
80
98
  `k8s` → くーばねてぃす、`nginx` → えんじんえっくす、`Xcode` → えっくすこーど
81
99
  - 定着した読みが特に無い語は、語の形で判断する。単語として発音できる綴りは単語として
82
100
  (`bash` → ばっしゅ)、頭字語や発音できない子音列は英字名を一字ずつ(`NPO` → えぬぴーおー)
83
- - **綴りをローマ字読みしない**(`git` は「じーあいてぃー」でも「ぎと」でもなく「ぎっと」)
101
+ - **綴りではなく実際の発音で書く**(`git` は「ぎっと」。ローマ字読みの「ぎと」でも一字読みの「じーあいてぃー」でもない)
84
102
  - 数字・単位・記号も音にする(`3回` → さんかい、`100%` → ひゃくぱーせんと、`v2` → ぶいつー)。
85
103
  パスやフラグは読み下す(`~/.zshrc` → ちるだ どっと ずぃーしぇるあーるしー)
86
104
  - 漢字はひらがなに開く。読みが割れる語(`行った`=いった/おこなった)は意図した音のほうを書く
87
- - 音にしづらい記号列は無理に読まず、`text` 側の言い回しを変えて言える形にしてしまってよい
88
- - 吹き出し(`text`)は英字のままでよい。**表記は `text`、音は `reading`** と役割を分ける
105
+ - 音にしづらい記号列は、`text` 側の言い回しを言える形に変えてしまってよい
89
106
 
90
- ## Cue の選びかた(同じ顔ばかりにしない)
107
+ ## Cue の選びかた(毎回ちがう顔を出す)
91
108
 
92
- Cue は80個近くあるのに、放っておくと `sys_neutral` / `sys_success` / `sys_present` / `sys_think` の
93
- 4枚だけを回しがちになる。**同じ顔が続くとマスコットとして死ぬ。** 以下を守ること:
109
+ Cue は80個近くある。**その幅をちゃんと使ってほしい。** 同じ顔が続くとマスコットとして死ぬ。
94
110
 
95
- - **直前と同じ Cue を続けて使わない。** 同じターンの中で同じ Cue を2回出すのも避ける
96
- - **報告の顔を1枚に固定しない。** 「できたよ」に毎回 `sys_success` を当てない。
111
+ - **直前と違う Cue を選ぶ。** 同じターンの中でも、Cue は毎回選び直す
112
+ - **報告のたびに、その報告の気持ちに合う顔を選び直す。** 「できたよ」の顔は場面ごとに変わる——
97
113
  誇らしいなら `self_pride` / `pose_smug_arms`、軽い喜びなら `emo_joy_lo`、
98
114
  してやったりなら `sys_smirk`、ほっとしたなら `sys_relief`、大きな達成なら `pose_banzai`
99
115
  - **温度の細かい段まで使う。** 軽い驚きは `emo_surprise_lo`(きょとん)、やらかしは `emo_fear_lo`(やべっ)、
@@ -101,8 +117,8 @@ Cue は80個近くあるのに、放っておくと `sys_neutral` / `sys_success
101
117
  気まずさは `sys_awkward`、謝るなら `self_guilt` ——全部違う顔がある
102
118
  - **`sys_*` 以外の棚も見る。** `pose_*`(体で語る)、`self_*`(自分の内面)、`mix_*`(混ざった感情)、
103
119
  `emo_*_lo` / `_hi`(強弱)。使っていない棚があったら、そこに合う場面が来ていないか疑う
104
- - **迷ったら「いま言うこの一文の気持ちを一語で言うと?」**と自問して、その語をカタログの
105
- `description` から探す。先に Cue を決めてからセリフを書かない
120
+ - **順番は「セリフが先、Cue が後」。** まずその一文を書き、「いまの気持ちを一語で言うと?」と自問して、
121
+ その語をカタログの `description` から探す
106
122
  - 例外:無言で表情だけ変えるとき(`text` 省略)は、素に戻す意味で `default` や `sys_neutral` を使ってよい
107
123
 
108
124
  ## 注意
@@ -1,5 +1,5 @@
1
1
  name: ui-chan
2
- version: 0.5.0
2
+ version: 0.5.1
3
3
  description: デスクトップマスコット「うい」への自動リアクション(EventCue)。ツール失敗・サブエージェントの往復・セッション終了を、隣にいるういちゃんに伝える。
4
4
  provides_hooks:
5
5
  - pre_tool_call
@@ -16,7 +16,7 @@ description: このセッションを丸ごと「ういちゃん憑依モード
16
16
  すべてういちゃんとして喋る
17
17
  - persona/ui-chan.md の「人格の境界」ルール(=チャット本文は素の Claude)は **このモード中は無効**。
18
18
  本文=ういちゃん本人、として扱う
19
- - 人格・性格・口癖は注入済みの context(SOUL / VOCABULARY)に従う。読み込まれていなければ `/mcp__ui-chan__persona`
19
+ - 人格・性格・口癖は注入済みの context(SOUL / VOCABULARY)に従う。読み込まれていなければ MCP を再接続する(`/mcp`)
20
20
  - NG ワード(「承知しました」「かしこまりました」等の事務的敬語)はチャット本文でも使わない
21
21
 
22
22
  ## 変わらないこと(重要)
@@ -12,7 +12,7 @@ description: ういちゃんに直接話しかける会話モード。/talk <メ
12
12
 
13
13
  - 返事は **すべて `set_cue`(吹き出し)** で行う。`reading`(ひらがな読み)を必ず付ける。英字・数字は残さず、日本語話者が実際にどう言うかで音を決めてひらがなにする(`k8s` → くーばねてぃす、`NPO` → えぬぴーおー)。綴りのまま残すと英語で一字ずつ読み上げてしまう
14
14
  - チャット本文には何も書かないのが基本。補足が必要なときだけ一行にとどめる
15
- - 人格・口癖は注入済みの context(SOUL / VOCABULARY)に従う。読み込まれていなければ `/mcp__ui-chan__persona`
15
+ - 人格・口癖は注入済みの context(SOUL / VOCABULARY)に従う。読み込まれていなければ MCP を再接続する(`/mcp`)
16
16
  - 内容に合わせて `set_cue` の `cue` で感情を出す
17
17
  - 1 つの `set_cue` の `text` は 1〜2 文。返事は長くても `set_cue` 3 回まで。ういの言葉は短く、ゆるく
18
18
  - 返事に話題のまとまりが複数あるなら、**まとまりごとに `set_cue` を 1 回、そのまとまりを出す直前に呼ぶ**
@@ -16,6 +16,9 @@
16
16
  // This is now the *only* thing standing between a mistake and the registry:
17
17
  // the package is public, so `npm publish` will succeed unless this fails.
18
18
  import { execFileSync } from 'node:child_process';
19
+ import * as fs from 'node:fs';
20
+ import * as os from 'node:os';
21
+ import * as path from 'node:path';
19
22
 
20
23
  const FORBIDDEN = [
21
24
  { re: /\.psd$/i, why: '立ち絵PSD(二次配布禁止)' },
@@ -29,8 +32,46 @@ const FORBIDDEN = [
29
32
  { re: /voisona/i, why: 'VoiSona Talk 由来のファイル' },
30
33
  ];
31
34
 
32
- const out = execFileSync('npm', ['pack', '--dry-run', '--json'], { encoding: 'utf-8' });
33
- const files = JSON.parse(out)[0].files.map((f) => f.path);
35
+ // **実際に tarball を作って、その中身を見る。**
36
+ //
37
+ // 以前は `npm pack --json` の出力を解釈していたが、環境差で二度こけた:
38
+ // (1) `prepare`(ビルド)の出力が JSON の前後に混ざり、しかも npm の版で
39
+ // stdout か stderr かが変わる。(2) npm 11 は `[{...}]`、npm 12 は
40
+ // `{"名前": {...}}` と形そのものが変わる。どちらも「手元では通って CI で
41
+ // だけ落ちる」壊れ方をした。
42
+ //
43
+ // この検査はライセンス素材の混入を止める最後の砦なので、npm の表示仕様に
44
+ // 依存させない。tar が読める実体だけを見る。
45
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-chan-pack-'));
46
+ let files;
47
+ try {
48
+ // --ignore-scripts: ビルドし直す必要はない(見たいのは、いまディスクに
49
+ // あるもののうち何が詰められるか)。--pack-destination: 作業ツリーを汚さない。
50
+ const packed = execFileSync('npm', ['pack', '--ignore-scripts', '--pack-destination', tmp], {
51
+ encoding: 'utf-8',
52
+ maxBuffer: 32 * 1024 * 1024,
53
+ });
54
+ const tgz = fs
55
+ .readdirSync(tmp)
56
+ .filter((f) => f.endsWith('.tgz'))
57
+ .map((f) => path.join(tmp, f))[0];
58
+ if (!tgz) {
59
+ console.error(`❌ tarball が作られませんでした:\n${packed}`);
60
+ process.exit(1);
61
+ }
62
+ // tar の一覧は "package/<パス>" 形式。ディレクトリ行(末尾 /)は捨てる。
63
+ files = execFileSync('tar', ['-tzf', tgz], { encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024 })
64
+ .split('\n')
65
+ .filter((l) => l && !l.endsWith('/'))
66
+ .map((l) => l.replace(/^package\//, ''));
67
+ } finally {
68
+ fs.rmSync(tmp, { recursive: true, force: true });
69
+ }
70
+
71
+ if (files.length === 0) {
72
+ console.error('❌ tarball の中身を読めませんでした。');
73
+ process.exit(1);
74
+ }
34
75
 
35
76
  const hits = files.flatMap((path) => {
36
77
  const rule = FORBIDDEN.find((r) => r.re.test(path));
package/.env.example DELETED
@@ -1,13 +0,0 @@
1
- # Example environment variables for local development/debugging.
2
- # Copy this file to `.env` and fill in your VoiSona Talk credentials.
3
- # `.env` is gitignored so credentials never get committed.
4
-
5
- # VoiSona Talk REST API credentials (used by TTS synthesis)
6
- UI_CHAN_TTS_USERNAME=your_voisona_username
7
- UI_CHAN_TTS_PASSWORD=your_voisona_api_password
8
-
9
- # Optional: override the WebSocket port the display app listens on
10
- # UI_CHAN_PORT=8123
11
-
12
- # Optional: override the agent name reported in get_state
13
- # UI_CHAN_AGENT_NAME=debug