ui-chan-mcp 0.5.0

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.
Files changed (151) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +6 -0
  3. package/.env.example +13 -0
  4. package/LICENSE +36 -0
  5. package/README.md +214 -0
  6. package/agents/mode.md +47 -0
  7. package/agents/talk.md +23 -0
  8. package/bin/ui-chan-mcp.mjs +11 -0
  9. package/bin/ui-chan-node +35 -0
  10. package/bin/ui-chan.mjs +350 -0
  11. package/context/AFFINITY.md +94 -0
  12. package/context/SOUL.md +79 -0
  13. package/context/VOCABULARY.md +108 -0
  14. package/cue.schema.json +109 -0
  15. package/cues/default.json +21 -0
  16. package/cues/emo_anger.json +14 -0
  17. package/cues/emo_anger_hi.json +19 -0
  18. package/cues/emo_anger_lo.json +16 -0
  19. package/cues/emo_antic.json +18 -0
  20. package/cues/emo_antic_hi.json +16 -0
  21. package/cues/emo_antic_lo.json +6 -0
  22. package/cues/emo_disgust.json +19 -0
  23. package/cues/emo_disgust_hi.json +20 -0
  24. package/cues/emo_disgust_lo.json +6 -0
  25. package/cues/emo_fear.json +19 -0
  26. package/cues/emo_fear_hi.json +19 -0
  27. package/cues/emo_fear_lo.json +19 -0
  28. package/cues/emo_joy.json +17 -0
  29. package/cues/emo_joy_hi.json +15 -0
  30. package/cues/emo_joy_lo.json +13 -0
  31. package/cues/emo_sad.json +15 -0
  32. package/cues/emo_sad_hi.json +18 -0
  33. package/cues/emo_sad_lo.json +14 -0
  34. package/cues/emo_surprise.json +15 -0
  35. package/cues/emo_surprise_hi.json +14 -0
  36. package/cues/emo_surprise_lo.json +11 -0
  37. package/cues/emo_trust.json +16 -0
  38. package/cues/emo_trust_hi.json +20 -0
  39. package/cues/emo_trust_lo.json +6 -0
  40. package/cues/idling_doze_1.json +9 -0
  41. package/cues/idling_doze_2.json +9 -0
  42. package/cues/idling_doze_3.json +9 -0
  43. package/cues/idling_giggle_1.json +9 -0
  44. package/cues/idling_giggle_2.json +9 -0
  45. package/cues/idling_lookaround_1.json +10 -0
  46. package/cues/idling_lookaround_2.json +10 -0
  47. package/cues/idling_lookaround_3.json +10 -0
  48. package/cues/idling_ponder.json +9 -0
  49. package/cues/idling_sigh_1.json +11 -0
  50. package/cues/idling_sigh_2.json +11 -0
  51. package/cues/idling_yawn_1.json +9 -0
  52. package/cues/idling_yawn_2.json +12 -0
  53. package/cues/idling_yawn_3.json +12 -0
  54. package/cues/mix_anger_antic.json +16 -0
  55. package/cues/mix_antic_joy.json +16 -0
  56. package/cues/mix_disgust_anger.json +17 -0
  57. package/cues/mix_fear_surprise.json +18 -0
  58. package/cues/mix_joy_trust.json +19 -0
  59. package/cues/mix_joy_trust_hi.json +18 -0
  60. package/cues/mix_sad_disgust.json +12 -0
  61. package/cues/mix_surprise_sad.json +21 -0
  62. package/cues/mix_surprise_sad_hi.json +14 -0
  63. package/cues/mix_trust_fear.json +18 -0
  64. package/cues/pose_arms_crossed.json +9 -0
  65. package/cues/pose_banzai.json +17 -0
  66. package/cues/pose_smug_arms.json +11 -0
  67. package/cues/pose_smug_hips.json +11 -0
  68. package/cues/pose_think.json +8 -0
  69. package/cues/pose_umbrella.json +9 -0
  70. package/cues/self_guilt.json +20 -0
  71. package/cues/self_pride.json +10 -0
  72. package/cues/self_shame.json +19 -0
  73. package/cues/self_shy.json +16 -0
  74. package/cues/self_shy_hi.json +17 -0
  75. package/cues/sys_address.json +9 -0
  76. package/cues/sys_awkward.json +18 -0
  77. package/cues/sys_beam.json +18 -0
  78. package/cues/sys_blank.json +11 -0
  79. package/cues/sys_care.json +18 -0
  80. package/cues/sys_confused.json +12 -0
  81. package/cues/sys_dazed.json +10 -0
  82. package/cues/sys_dread.json +21 -0
  83. package/cues/sys_fluster.json +20 -0
  84. package/cues/sys_intro.json +15 -0
  85. package/cues/sys_laugh.json +20 -0
  86. package/cues/sys_neutral.json +5 -0
  87. package/cues/sys_present.json +9 -0
  88. package/cues/sys_rain.json +15 -0
  89. package/cues/sys_refuse.json +18 -0
  90. package/cues/sys_relief.json +15 -0
  91. package/cues/sys_sleepy.json +8 -0
  92. package/cues/sys_smirk.json +10 -0
  93. package/cues/sys_stream.json +15 -0
  94. package/cues/sys_success.json +15 -0
  95. package/cues/sys_think.json +9 -0
  96. package/dist/app/assets.js +54 -0
  97. package/dist/app/cues.js +130 -0
  98. package/dist/app/editor-main.js +199 -0
  99. package/dist/app/editor-preload.js +17 -0
  100. package/dist/app/main.js +527 -0
  101. package/dist/app/persona.js +136 -0
  102. package/dist/app/preload.js +14 -0
  103. package/dist/app/state.js +850 -0
  104. package/dist/app/tts.js +257 -0
  105. package/dist/mcp-server.js +408 -0
  106. package/dist/renderer/bundle.js +17983 -0
  107. package/dist/renderer/editor.html +150 -0
  108. package/dist/renderer/editor.js +17917 -0
  109. package/dist/renderer/index.html +425 -0
  110. package/dist/shared/paths.js +169 -0
  111. package/dist/shared/set-cue-schema.js +66 -0
  112. package/dist/shared/types.js +14 -0
  113. package/docs/CLIENTS.md +110 -0
  114. package/docs/CUE_AUTHORING.md +76 -0
  115. package/docs/DEVELOPMENT.md +129 -0
  116. package/docs/PERSONA.md +61 -0
  117. package/docs/PSD_LAYERS.md +123 -0
  118. package/docs/README.md +43 -0
  119. package/docs/SETUP.html +684 -0
  120. package/docs/STYLE.md +107 -0
  121. package/docs/TOOLS.md +33 -0
  122. package/docs/TROUBLESHOOTING.md +122 -0
  123. package/docs/TTS.md +61 -0
  124. package/docs/design/CUE_CATALOG.md +259 -0
  125. package/docs/images/faces.png +0 -0
  126. package/docs/images/panel.png +0 -0
  127. package/hooks/fire-event.js +14 -0
  128. package/hooks/hooks.json +77 -0
  129. package/hooks/lib/mascot.js +76 -0
  130. package/hooks/notify.js +15 -0
  131. package/hooks/reaction.js +46 -0
  132. package/hooks/session-start.js +80 -0
  133. package/package.json +96 -0
  134. package/persona/ui-chan.md +111 -0
  135. package/plugins/hermes/ui-chan/__init__.py +98 -0
  136. package/plugins/hermes/ui-chan/plugin.yaml +7 -0
  137. package/plugins/opencode/ui-chan.mjs +105 -0
  138. package/skills/beam/SKILL.md +37 -0
  139. package/skills/eli14/SKILL.md +139 -0
  140. package/skills/eli14/references/base.html +495 -0
  141. package/skills/mode/SKILL.md +67 -0
  142. package/skills/talk/SKILL.md +27 -0
  143. package/tools/setup/check-package.mjs +47 -0
  144. package/tools/setup/clients.mjs +399 -0
  145. package/tools/setup/doctor.mjs +105 -0
  146. package/tools/setup/home.mjs +126 -0
  147. package/tools/setup/prompt.mjs +134 -0
  148. package/tools/setup/update-check.mjs +8 -0
  149. package/tools/setup/update.mjs +350 -0
  150. package/tools/stop-app.mjs +22 -0
  151. package/ui-chan.config.json +973 -0
@@ -0,0 +1,425 @@
1
+ <!doctype html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; media-src blob:" />
6
+ <title>ui-chan</title>
7
+ <style>
8
+ html, body {
9
+ margin: 0;
10
+ padding: 0;
11
+ width: 100%;
12
+ height: 100%;
13
+ overflow: hidden;
14
+ background: transparent;
15
+ font-family: 'Hiragino Maru Gothic ProN', 'Hiragino Sans', sans-serif;
16
+ user-select: none;
17
+ }
18
+ #stage {
19
+ position: relative;
20
+ width: 100%;
21
+ height: 100%;
22
+ display: flex;
23
+ flex-direction: column;
24
+ }
25
+ #bubble-area {
26
+ flex: 0 0 auto;
27
+ min-height: 120px;
28
+ display: flex;
29
+ align-items: flex-end;
30
+ justify-content: center;
31
+ padding: 8px 10px 0;
32
+ box-sizing: border-box;
33
+ /* キャラクターの頭上(PSDの傘用余白ぶん)まで吹き出しを下げる */
34
+ transform: translateY(46px);
35
+ z-index: 1;
36
+ position: relative;
37
+ }
38
+ #bubble {
39
+ position: relative;
40
+ max-width: 100%;
41
+ /* 背景は髪の実ピクセル色 rgb(243,235,233)〜rgb(224,209,212)(淡い銀グレー)、
42
+ 枠・矢印・影はリボン/靴下の実ピクセル色 rgb(78,91,181) = #4e5bb5 */
43
+ background: linear-gradient(180deg, rgba(250, 245, 243, 0.97), rgba(228, 216, 219, 0.97));
44
+ border: 2px solid #4e5bb5;
45
+ border-radius: 16px;
46
+ padding: 10px 14px;
47
+ font-size: 14px;
48
+ line-height: 1.5;
49
+ color: #2e2a35;
50
+ box-shadow: 0 3px 10px rgba(78, 91, 181, 0.28);
51
+ opacity: 0;
52
+ transform: translateY(6px);
53
+ transition: opacity 0.18s ease, transform 0.18s ease;
54
+ word-break: break-word;
55
+ /* 文ごとの改行(bubbleText)と、明示的な \n を効かせる */
56
+ white-space: pre-wrap;
57
+ }
58
+ #bubble.visible {
59
+ opacity: 1;
60
+ transform: translateY(0);
61
+ }
62
+ #bubble::after {
63
+ content: '';
64
+ position: absolute;
65
+ bottom: -9px;
66
+ left: 50%;
67
+ margin-left: -7px;
68
+ border-width: 9px 7px 0;
69
+ border-style: solid;
70
+ border-color: #4e5bb5 transparent transparent;
71
+ }
72
+ #canvas-wrap {
73
+ flex: 1 1 auto;
74
+ position: relative;
75
+ /* No -webkit-app-region: drag — it swallows the mouse events the fidget
76
+ needs (and triggers macOS double-click-to-reposition). Window dragging
77
+ is done manually in renderer.ts (grab her body to move her). */
78
+ }
79
+ #canvas {
80
+ position: absolute;
81
+ inset: 0;
82
+ width: 100%;
83
+ height: 100%;
84
+ }
85
+ #placeholder {
86
+ display: none;
87
+ position: absolute;
88
+ inset: 12px;
89
+ background: rgba(30, 34, 52, 0.92);
90
+ border: 2px dashed #7a8ac0;
91
+ border-radius: 16px;
92
+ color: #e8ecff;
93
+ padding: 20px;
94
+ font-size: 13px;
95
+ line-height: 1.8;
96
+ }
97
+ #placeholder h1 {
98
+ font-size: 16px;
99
+ margin: 0 0 12px;
100
+ }
101
+ #placeholder code {
102
+ background: rgba(255, 255, 255, 0.12);
103
+ border-radius: 4px;
104
+ padding: 1px 5px;
105
+ font-family: monospace;
106
+ }
107
+ /* ---- 接続パネル -------------------------------------------------
108
+ 世界観を壊さないための約束:平常時はリボン色の小さなつまみだけ。
109
+ クリックで開き、複数セッションを検知したときだけ自分から開いて、
110
+ 用が済んだら畳まる。立ち絵と吹き出しには一切干渉しない。 */
111
+ #panel {
112
+ position: absolute;
113
+ top: 6px;
114
+ right: 6px;
115
+ z-index: 5;
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: flex-end;
119
+ font-size: 12px;
120
+ color: #2e2a35;
121
+ }
122
+ #panel-tab {
123
+ -webkit-app-region: no-drag;
124
+ appearance: none;
125
+ border: none;
126
+ cursor: pointer;
127
+ width: 26px;
128
+ height: 14px;
129
+ padding: 0;
130
+ border-radius: 7px;
131
+ background: rgba(78, 91, 181, 0.28);
132
+ box-shadow: 0 1px 3px rgba(78, 91, 181, 0.25);
133
+ opacity: 0.35;
134
+ transition: opacity 0.2s ease, background 0.2s ease, width 0.2s ease;
135
+ color: #fff;
136
+ font: inherit;
137
+ font-size: 10px;
138
+ line-height: 14px;
139
+ text-align: center;
140
+ }
141
+ #panel:hover #panel-tab,
142
+ #panel.open #panel-tab {
143
+ opacity: 1;
144
+ background: rgba(78, 91, 181, 0.92);
145
+ }
146
+ /* Collapsed with 0–1 sessions: a hamburger, drawn in CSS so it stays crisp
147
+ and needs no font. With 2+ sessions the count replaces it. */
148
+ #panel-tab::before {
149
+ content: '';
150
+ display: block;
151
+ width: 12px;
152
+ height: 8px;
153
+ margin: 0 auto;
154
+ border-top: 1.5px solid currentColor;
155
+ border-bottom: 1.5px solid currentColor;
156
+ box-sizing: border-box;
157
+ background:
158
+ linear-gradient(currentColor, currentColor) center / 12px 1.5px no-repeat;
159
+ }
160
+ #panel.crowded #panel-tab::before {
161
+ display: none;
162
+ }
163
+ #panel.crowded #panel-tab {
164
+ width: 30px;
165
+ }
166
+ #panel-body {
167
+ margin-top: 6px;
168
+ /* Narrow on purpose: open, it must not sit on top of her face. */
169
+ width: 176px;
170
+ background: linear-gradient(180deg, rgba(250, 245, 243, 0.98), rgba(232, 223, 226, 0.98));
171
+ border: 1.5px solid #4e5bb5;
172
+ border-radius: 12px;
173
+ box-shadow: 0 4px 14px rgba(78, 91, 181, 0.3);
174
+ padding: 8px;
175
+ box-sizing: border-box;
176
+ opacity: 0;
177
+ transform: translateY(-6px) scale(0.98);
178
+ transform-origin: top right;
179
+ pointer-events: none;
180
+ transition: opacity 0.18s ease, transform 0.18s ease;
181
+ }
182
+ #panel.open #panel-body {
183
+ opacity: 1;
184
+ transform: translateY(0) scale(1);
185
+ pointer-events: auto;
186
+ }
187
+ .panel-row {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 6px;
191
+ padding: 4px 4px;
192
+ border-radius: 7px;
193
+ }
194
+ .panel-row.active {
195
+ background: rgba(78, 91, 181, 0.14);
196
+ }
197
+ .panel-dot {
198
+ width: 7px;
199
+ height: 7px;
200
+ border-radius: 50%;
201
+ background: rgba(78, 91, 181, 0.28);
202
+ flex: 0 0 auto;
203
+ }
204
+ .panel-row.active .panel-dot {
205
+ background: #4e5bb5;
206
+ box-shadow: 0 0 0 3px rgba(78, 91, 181, 0.18);
207
+ }
208
+ .panel-text {
209
+ display: flex;
210
+ flex-direction: column;
211
+ line-height: 1.3;
212
+ overflow: hidden;
213
+ }
214
+ .panel-text b {
215
+ font-weight: 600;
216
+ white-space: nowrap;
217
+ overflow: hidden;
218
+ text-overflow: ellipsis;
219
+ }
220
+ .panel-text small {
221
+ color: #6b6577;
222
+ font-size: 10.5px;
223
+ white-space: nowrap;
224
+ overflow: hidden;
225
+ text-overflow: ellipsis;
226
+ }
227
+ .panel-empty {
228
+ color: #6b6577;
229
+ padding: 4px;
230
+ text-align: center;
231
+ }
232
+ .panel-face {
233
+ display: block;
234
+ white-space: nowrap;
235
+ margin-top: 2px;
236
+ letter-spacing: 0.02em;
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
+ }
272
+ #panel-actions {
273
+ display: flex;
274
+ align-items: center;
275
+ gap: 2px;
276
+ margin-top: 6px;
277
+ border-top: 1px solid rgba(78, 91, 181, 0.2);
278
+ padding-top: 6px;
279
+ }
280
+ #panel-actions button {
281
+ appearance: none;
282
+ cursor: pointer;
283
+ background: transparent;
284
+ border: none;
285
+ border-radius: 8px;
286
+ padding: 4px;
287
+ line-height: 0;
288
+ color: #4a4560;
289
+ }
290
+ #panel-actions button:hover {
291
+ background: rgba(78, 91, 181, 0.14);
292
+ color: #4e5bb5;
293
+ }
294
+ #panel-actions svg {
295
+ width: 17px;
296
+ height: 17px;
297
+ fill: none;
298
+ stroke: currentColor;
299
+ stroke-width: 1.6;
300
+ stroke-linecap: round;
301
+ stroke-linejoin: round;
302
+ }
303
+ /* ミュート中は「音が消えている」形(波→斜線)に変わり、色でも分かる。 */
304
+ #panel-actions .i-slash {
305
+ display: none;
306
+ }
307
+ #panel-actions .i-fill {
308
+ fill: currentColor;
309
+ stroke: none;
310
+ }
311
+ #panel-actions button.on {
312
+ color: #4e5bb5;
313
+ background: rgba(78, 91, 181, 0.16);
314
+ }
315
+ #panel-gear.on svg path {
316
+ fill: currentColor;
317
+ }
318
+ #panel-actions button.on .i-wave {
319
+ display: none;
320
+ }
321
+ #panel-actions button.on .i-slash {
322
+ display: inline;
323
+ }
324
+ /* 更新の入口は「あるときだけ」出す。常設すると平常時の情報がひとつ増える。 */
325
+ #panel-update[hidden] {
326
+ display: none;
327
+ }
328
+ #panel-update {
329
+ color: #4e5bb5;
330
+ margin-left: auto;
331
+ }
332
+ #panel-update + button[data-action='quit'] {
333
+ margin-left: 4px;
334
+ }
335
+ /* 終了だけは押し間違えると困るので、右端に離して置く。 */
336
+ #panel-actions button[data-action='quit'] {
337
+ margin-left: auto;
338
+ color: #8a8497;
339
+ }
340
+ #panel-actions button[data-action='quit']:hover {
341
+ color: #b4515e;
342
+ background: rgba(180, 81, 94, 0.12);
343
+ }
344
+ </style>
345
+ </head>
346
+ <body>
347
+ <div id="stage">
348
+ <div id="panel">
349
+ <button id="panel-tab" type="button" title="接続中のセッション"><span id="panel-count"></span></button>
350
+ <div id="panel-body">
351
+ <div id="panel-list"></div>
352
+ <div id="panel-affinity" hidden>
353
+ <div class="aff-head"><span>好感度</span><b id="aff-value">–</b><small id="aff-band"></small></div>
354
+ <input id="aff-range" type="range" min="0" max="100" step="1" value="0" />
355
+ </div>
356
+ <div id="panel-actions">
357
+ <button type="button" data-action="mute" title="しずかに(声だけ止める)" aria-label="しずかに">
358
+ <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" />
362
+ </svg>
363
+ </button>
364
+ <button type="button" data-action="clear" title="ひといき(いま喋っていることを止めて Idling に戻す)" aria-label="ひといき">
365
+ <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" />
370
+ </svg>
371
+ </button>
372
+ <button type="button" id="panel-gear" title="好感度" aria-label="好感度">
373
+ <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" />
378
+ </svg>
379
+ </button>
380
+ <button type="button" data-action="restart" title="リセット(定位置に戻して起動しなおす)" aria-label="リセット">
381
+ <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" />
387
+ </svg>
388
+ </button>
389
+ <button type="button" data-action="update" id="panel-update" title="更新があります(取得して入れ替える)" aria-label="更新" hidden>
390
+ <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" />
395
+ </svg>
396
+ </button>
397
+ <button type="button" data-action="quit" title="おやすみ(終了。次に呼ばれても起きない)" aria-label="おやすみ">
398
+ <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" />
401
+ </svg>
402
+ </button>
403
+ </div>
404
+ </div>
405
+ </div>
406
+ <div id="bubble-area"><div id="bubble"></div></div>
407
+ <div id="canvas-wrap">
408
+ <canvas id="canvas"></canvas>
409
+ <div id="placeholder">
410
+ <h1>ui-chan: 素材が見つかりません</h1>
411
+ <p>
412
+ 立ち絵のPSDファイルを <code id="assets-dir">ui_sozai/</code> ディレクトリに
413
+ 配置して、アプリを再起動してください。
414
+ </p>
415
+ <p>
416
+ PSDTool形式(<code>!</code>=必須レイヤー、<code>*</code>=ラジオ切替)の
417
+ PSDに対応しています。レイヤー名と表情のマッピングは
418
+ <code>ui-chan.config.json</code> で設定できます。
419
+ </p>
420
+ </div>
421
+ </div>
422
+ </div>
423
+ <script src="bundle.js"></script>
424
+ </body>
425
+ </html>
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installKind = installKind;
37
+ exports.uiChanHome = uiChanHome;
38
+ exports.loadEnvFiles = loadEnvFiles;
39
+ exports.deepMerge = deepMerge;
40
+ exports.resolvePaths = resolvePaths;
41
+ // Where ui-chan's files live — the one module that answers "package or user?".
42
+ //
43
+ // Everything ships twice: the package carries defaults (config, cues, persona,
44
+ // context) and the user may carry overrides in a **home directory**
45
+ // (`UI_CHAN_HOME`, default `~/.ui-chan`). The package half is read-only and
46
+ // replaced wholesale on every update; the home half is never touched by an
47
+ // update. That split is what makes `npm update` / a fresh install safe — before
48
+ // it, a user's PSD, `.env` and hand-authored cues sat inside the install
49
+ // directory and died with it (see the plugin-cache incident in the README).
50
+ //
51
+ // Resolution is per-resource, not all-or-nothing:
52
+ // config home/config.json deep-merged **over** the packaged defaults, so a
53
+ // user file with three keys keeps inheriting everything added later.
54
+ // cues both dirs load, home wins on same name — add a cue without
55
+ // copying the catalog.
56
+ // context same (filename-keyed).
57
+ // persona home file if present, else packaged.
58
+ // assets home first, then packaged (PSD is licensed, so usually home-only).
59
+ // .env home first, then package (env vars still win over both).
60
+ const fs = __importStar(require("node:fs"));
61
+ const os = __importStar(require("node:os"));
62
+ const path = __importStar(require("node:path"));
63
+ function installKind(pkgRoot) {
64
+ if (fs.existsSync(path.join(pkgRoot, '.git')))
65
+ return 'git';
66
+ // npm puts packages in `<prefix>/lib/node_modules/<name>` (global) or
67
+ // `<project>/node_modules/<name>` (local). Both end in node_modules/<name>,
68
+ // which a clone never does.
69
+ const parent = path.basename(path.dirname(pkgRoot));
70
+ if (parent === 'node_modules' ||
71
+ path.basename(path.dirname(path.dirname(pkgRoot))) === 'node_modules') {
72
+ return pkgRoot.includes(`${path.sep}lib${path.sep}node_modules${path.sep}`) ||
73
+ !fs.existsSync(path.join(pkgRoot, '..', '..', 'package.json'))
74
+ ? 'npm-global'
75
+ : 'npm-local';
76
+ }
77
+ return 'copy';
78
+ }
79
+ function uiChanHome() {
80
+ return process.env.UI_CHAN_HOME ?? path.join(os.homedir(), '.ui-chan');
81
+ }
82
+ /** `.env` files, lowest precedence first. Loaded, never overwriting real env
83
+ * vars — `loadEnvFile` leaves already-set variables alone. */
84
+ function loadEnvFiles(pkgRoot) {
85
+ const loaded = [];
86
+ for (const file of [path.join(pkgRoot, '.env'), path.join(uiChanHome(), '.env')]) {
87
+ try {
88
+ process.loadEnvFile(file);
89
+ loaded.push(file);
90
+ }
91
+ catch {
92
+ /* absent or unreadable — credentials can still come from the environment */
93
+ }
94
+ }
95
+ return loaded;
96
+ }
97
+ function isPlainObject(v) {
98
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
99
+ }
100
+ /** Deep merge for config only: objects merge key-wise, arrays and scalars are
101
+ * replaced outright (a user's `idlingCues` list means "this list", not "append
102
+ * to the packaged one"). */
103
+ function deepMerge(base, override) {
104
+ if (!isPlainObject(base) || !isPlainObject(override))
105
+ return (override ?? base);
106
+ const out = { ...base };
107
+ for (const [k, v] of Object.entries(override)) {
108
+ out[k] = k in out ? deepMerge(out[k], v) : v;
109
+ }
110
+ return out;
111
+ }
112
+ function readJson(file) {
113
+ try {
114
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ function existingDirs(dirs) {
121
+ return dirs.filter((d) => fs.existsSync(d));
122
+ }
123
+ /**
124
+ * Resolve every path ui-chan reads, given the package root.
125
+ * `pkgRoot` is `dist/..` for the MCP server and `dist/../..` for the Electron
126
+ * app — both callers pass their own, this module never guesses.
127
+ */
128
+ function resolvePaths(pkgRoot) {
129
+ const home = uiChanHome();
130
+ const homeExists = fs.existsSync(home);
131
+ const kind = installKind(pkgRoot);
132
+ const packaged = (readJson(path.join(pkgRoot, 'ui-chan.config.json')) ?? {});
133
+ const configFile = path.join(home, 'config.json');
134
+ const override = homeExists ? readJson(configFile) : null;
135
+ const config = override ? deepMerge(packaged, override) : packaged;
136
+ const cueDirs = existingDirs([
137
+ path.join(pkgRoot, config.cuesDir ?? 'cues'),
138
+ path.join(home, 'cues'),
139
+ ]);
140
+ const homePersona = path.join(home, 'persona', 'ui-chan.md');
141
+ return {
142
+ pkgRoot,
143
+ kind,
144
+ home,
145
+ homeExists,
146
+ config,
147
+ configFile,
148
+ cueDirs,
149
+ // Last existing dir wins: with a home dir set up, edits land in the user's
150
+ // data; in a bare git clone (no home), they land in the repo where the
151
+ // author expects them.
152
+ // Where a saved cue goes. In a clone that's the repo (the author is editing
153
+ // the catalog itself); anywhere else the package dir belongs to npm or to
154
+ // whoever unpacked it and will be replaced wholesale on the next update, so
155
+ // the user's cue has to live in their home dir even if it doesn't exist yet.
156
+ cueWriteDir: kind === 'git'
157
+ ? (cueDirs[cueDirs.length - 1] ?? path.join(pkgRoot, config.cuesDir ?? 'cues'))
158
+ : path.join(home, 'cues'),
159
+ cueSchemaFile: path.join(pkgRoot, 'cue.schema.json'),
160
+ assetsDirs: existingDirs([
161
+ path.join(home, config.assetsDir ?? 'assets'),
162
+ path.join(pkgRoot, config.assetsDir ?? 'assets'),
163
+ ]),
164
+ personaFile: fs.existsSync(homePersona)
165
+ ? homePersona
166
+ : path.join(pkgRoot, config.personaFile ?? 'persona/ui-chan.md'),
167
+ contextDirs: existingDirs([path.join(pkgRoot, 'context'), path.join(home, 'context')]),
168
+ };
169
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setCueArgsSchema = exports.setCueShape = void 0;
4
+ const zod_1 = require("zod");
5
+ /** Single source of truth for set_cue's argument shape. `mcp-server.ts` uses
6
+ * `setCueShape` directly as the MCP tool's inputSchema (a zod raw shape);
7
+ * `main.ts` parses the WS bridge payload with `setCueArgsSchema`; `state.ts`
8
+ * uses the inferred `SetCueArgs` type. Adding or renaming a knob is a
9
+ * one-file change instead of three hand-synced ones.
10
+ *
11
+ * Kept out of `shared/types.ts` on purpose: that module is also imported
12
+ * (type-only) by the browser-bundled renderer, and this file pulls in the
13
+ * zod runtime, which the renderer has no use for. */
14
+ exports.setCueShape = {
15
+ cue: zod_1.z.string().describe('Cue name (unknown names fall back to "default")'),
16
+ text: zod_1.z
17
+ .string()
18
+ .min(1)
19
+ .max(500)
20
+ .optional()
21
+ .describe('Optional line to speak in the bubble. Omit for a silent Cue change.'),
22
+ reading: zod_1.z
23
+ .string()
24
+ .max(1000)
25
+ .optional()
26
+ .describe('Hiragana reading of the WHOLE line — drives lip sync, and is what the TTS engine actually ' +
27
+ 'speaks whenever text contains Latin letters or digits. Leave NO Latin letters, digits or ' +
28
+ 'symbols in it: write how a Japanese speaker actually says the line, judging each term from ' +
29
+ 'your own knowledge rather than transliterating its spelling (k8s → くーばねてぃす, ' +
30
+ 'bash → ばっしゅ, NPO → えぬぴーおー, 3回 → さんかい). Anything left in Latin is read out ' +
31
+ 'letter-by-letter in English. Provide whenever text is given.'),
32
+ duration_ms: zod_1.z
33
+ .number()
34
+ .int()
35
+ .min(500)
36
+ .max(60_000)
37
+ .optional()
38
+ .describe('With text: how long to display the bubble, ms (default derived from text length). ' +
39
+ 'Without text: how long to hold this Cue before easing back to default, ms ' +
40
+ '(default: holds until the next set_cue).'),
41
+ pitch: zod_1.z
42
+ .number()
43
+ .min(-600)
44
+ .max(600)
45
+ .optional()
46
+ .describe('Ad-lib pitch shift for this line only, in cents (-600..600, default 0)'),
47
+ speed: zod_1.z
48
+ .number()
49
+ .min(0.2)
50
+ .max(5)
51
+ .optional()
52
+ .describe('Ad-lib speech speed for this line only (0.2..5, default 1)'),
53
+ volume: zod_1.z
54
+ .number()
55
+ .min(-8)
56
+ .max(8)
57
+ .optional()
58
+ .describe('Ad-lib volume for this line only, in dB (-8..8, default 0)'),
59
+ intonation: zod_1.z
60
+ .number()
61
+ .min(0)
62
+ .max(2)
63
+ .optional()
64
+ .describe('Ad-lib intonation strength for this line only (0..2, default 1)'),
65
+ };
66
+ exports.setCueArgsSchema = zod_1.z.object(exports.setCueShape);
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_AFFINITY_STEPS = exports.AFFINITY_MAGNITUDES = exports.DEFAULT_CUE_NAME = void 0;
4
+ /** Not a config field — a fixed naming convention. The Cue named "default"
5
+ * is composed underneath every other Cue as the shared base look: same
6
+ * file format, same directory, no special-casing. */
7
+ exports.DEFAULT_CUE_NAME = 'default';
8
+ exports.AFFINITY_MAGNITUDES = ['low', 'middle', 'high'];
9
+ /** Built-in base magnitudes if config.affinity.steps is absent. */
10
+ exports.DEFAULT_AFFINITY_STEPS = {
11
+ low: 3,
12
+ middle: 6,
13
+ high: 12,
14
+ };