eojeon-cli 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +25 -0
- package/LICENSE +26 -0
- package/README.md +216 -2
- package/cli/eojeon.js +78 -0
- package/court.config.json +316 -0
- package/package.json +60 -4
- package/public/app.js +397 -0
- package/public/court-types.js +33 -0
- package/public/demo.js +100 -0
- package/public/fonts/D2Coding-Ver1.4.0-20261003.ttf +0 -0
- package/public/fonts/Galmuri11-Bold.woff2 +0 -0
- package/public/fonts/Galmuri11.woff2 +0 -0
- package/public/fonts/Galmuri7.woff2 +0 -0
- package/public/fonts/Galmuri9.woff2 +0 -0
- package/public/fonts/LICENSE-D2Coding.txt +97 -0
- package/public/fonts/LICENSE-Galmuri.txt +93 -0
- package/public/index.html +97 -0
- package/public/scene.js +1079 -0
- package/public/style.css +314 -0
- package/scripts/fix-pty.js +25 -0
- package/server/agents.js +78 -0
- package/server/hook.js +47 -0
- package/server/index.js +430 -0
- package/server/sanitize.js +155 -0
- package/server/sessions.js +115 -0
- package/templates/agents/studio/animation-expert.md +29 -0
- package/templates/agents/studio/apple-designer.md +28 -0
- package/templates/agents/studio/branding-expert.md +29 -0
- package/templates/agents/studio/copywriter.md +28 -0
- package/templates/agents/studio/data-analyst.md +29 -0
- package/templates/agents/studio/marketer.md +28 -0
- package/templates/agents/studio/pr-lead.md +28 -0
- package/templates/agents/studio/product-designer.md +29 -0
- package/templates/agents/studio/sns-manager.md +28 -0
- package/templates/agents/studio/uiux-expert.md +31 -0
- package/templates/agents/studio/video-producer.md +29 -0
- package/templates/agents/yukjo/byeongjo.md +29 -0
- package/templates/agents/yukjo/gongjo.md +29 -0
- package/templates/agents/yukjo/hojo.md +29 -0
- package/templates/agents/yukjo/hyeongjo.md +29 -0
- package/templates/agents/yukjo/ijo.md +29 -0
- package/templates/agents/yukjo/yejo.md +29 -0
package/public/style.css
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
@font-face { font-family: 'Galmuri11'; src: url('/fonts/Galmuri11.woff2') format('woff2'); font-display: swap; }
|
|
2
|
+
@font-face { font-family: 'Galmuri11'; font-weight: 700; src: url('/fonts/Galmuri11-Bold.woff2') format('woff2'); font-display: swap; }
|
|
3
|
+
@font-face { font-family: 'Galmuri9'; src: url('/fonts/Galmuri9.woff2') format('woff2'); font-display: swap; }
|
|
4
|
+
@font-face { font-family: 'Galmuri7'; src: url('/fonts/Galmuri7.woff2') format('woff2'); font-display: swap; }
|
|
5
|
+
/* Terminal: Hangul glyphs are exactly two Latin cells wide, so Korean lines up in xterm. Bundled, no CDN. */
|
|
6
|
+
@font-face { font-family: 'D2Coding'; src: local('D2Coding'), url('/fonts/D2Coding-Ver1.4.0-20261003.ttf') format('truetype'); font-display: block; }
|
|
7
|
+
|
|
8
|
+
/* Palette: concept.md 4-1 (dark). Overridden at runtime from court.config.json. */
|
|
9
|
+
:root {
|
|
10
|
+
--paper: #15171C;
|
|
11
|
+
--card: #22252C;
|
|
12
|
+
--ink: #EFE8DA;
|
|
13
|
+
--ink2: #A8A090;
|
|
14
|
+
--hong: #E8705F;
|
|
15
|
+
--cheong: #6FAAD6;
|
|
16
|
+
--nok: #74B48E;
|
|
17
|
+
--hwang: #D9AE52;
|
|
18
|
+
--heuk: #2B2F38;
|
|
19
|
+
--inju: #F07A6A;
|
|
20
|
+
--px-hong: #A8322A;
|
|
21
|
+
--px-cheong: #1F5A82;
|
|
22
|
+
--px-nok: #2F6B4A;
|
|
23
|
+
--px-inju: #C0392B;
|
|
24
|
+
--px-paper: #F3EDE1;
|
|
25
|
+
--line: #343842;
|
|
26
|
+
--pixel: 'Galmuri11', 'Galmuri9', ui-monospace, monospace;
|
|
27
|
+
--sans: -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Pretendard', 'Noto Sans KR', 'Segoe UI', sans-serif;
|
|
28
|
+
color-scheme: dark;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
* { box-sizing: border-box; }
|
|
32
|
+
html, body { margin: 0; height: 100%; }
|
|
33
|
+
body {
|
|
34
|
+
background: var(--paper);
|
|
35
|
+
color: var(--ink);
|
|
36
|
+
font-family: var(--sans);
|
|
37
|
+
font-size: 14px;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
}
|
|
42
|
+
button, input, select { font: inherit; color: inherit; }
|
|
43
|
+
code { font-family: ui-monospace, Menlo, monospace; color: var(--hwang); }
|
|
44
|
+
.muted { color: var(--ink2); }
|
|
45
|
+
|
|
46
|
+
/* ---------- header ---------- */
|
|
47
|
+
.top {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
gap: 16px;
|
|
52
|
+
flex-wrap: wrap;
|
|
53
|
+
padding: 10px 16px;
|
|
54
|
+
border-bottom: 1px solid var(--line);
|
|
55
|
+
background: var(--card);
|
|
56
|
+
}
|
|
57
|
+
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
58
|
+
.brand h1 { font-family: var(--pixel); font-size: 18px; margin: 0; letter-spacing: 0.5px; }
|
|
59
|
+
.brand .sub { margin: 2px 0 0; color: var(--ink2); font-size: 12px; }
|
|
60
|
+
.seal {
|
|
61
|
+
width: 34px; height: 34px; flex: none;
|
|
62
|
+
display: grid; place-items: center;
|
|
63
|
+
border: 2px solid var(--inju); color: var(--inju);
|
|
64
|
+
font-family: serif; font-size: 20px; line-height: 1;
|
|
65
|
+
transform: rotate(-8deg);
|
|
66
|
+
}
|
|
67
|
+
.launch { display: flex; align-items: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
68
|
+
.field { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: var(--ink2); }
|
|
69
|
+
.field input, .field select {
|
|
70
|
+
background: var(--paper); border: 1px solid var(--line); border-radius: 4px;
|
|
71
|
+
padding: 6px 8px; font-size: 13px; color: var(--ink);
|
|
72
|
+
}
|
|
73
|
+
.field.folder input { width: 300px; max-width: 70vw; font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
|
|
74
|
+
.field.session select { width: 300px; max-width: 70vw; }
|
|
75
|
+
.field.pmode select { width: 170px; }
|
|
76
|
+
|
|
77
|
+
/* ---------- 육조 dialog ---------- */
|
|
78
|
+
.dlg { background: var(--card); color: var(--ink); border: 1px solid var(--hwang); border-radius: 6px; max-width: 460px; padding: 16px 18px; }
|
|
79
|
+
.dlg::backdrop { background: rgba(0,0,0,0.55); }
|
|
80
|
+
.dlg h2 { font-family: var(--pixel); font-size: 16px; margin: 0 0 6px; color: var(--hwang); }
|
|
81
|
+
.dlg p { margin: 6px 0; line-height: 1.5; }
|
|
82
|
+
.dlg .small { font-size: 12px; }
|
|
83
|
+
.yukjo-list { margin: 8px 0; padding-left: 18px; line-height: 1.6; }
|
|
84
|
+
.yukjo-list .have { color: var(--ink2); text-decoration: line-through; }
|
|
85
|
+
.chk { display: flex; gap: 6px; align-items: center; margin: 8px 0; }
|
|
86
|
+
.dlg-btns { display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap; margin-top: 12px; }
|
|
87
|
+
.btn {
|
|
88
|
+
background: var(--heuk); border: 1px solid var(--line); border-radius: 4px;
|
|
89
|
+
padding: 7px 12px; cursor: pointer; font-family: var(--pixel); font-size: 12px;
|
|
90
|
+
}
|
|
91
|
+
.btn:hover { border-color: var(--ink2); }
|
|
92
|
+
.btn.primary { background: var(--px-hong); border-color: var(--px-hong); color: #fff; }
|
|
93
|
+
.btn:disabled { opacity: 0.5; cursor: default; }
|
|
94
|
+
.status { font-family: var(--pixel); font-size: 12px; padding: 6px 8px; color: var(--ink2); }
|
|
95
|
+
.status::before { content: '●'; margin-right: 5px; color: var(--ink2); }
|
|
96
|
+
.status[data-state='running']::before { color: var(--nok); }
|
|
97
|
+
.status[data-state='error']::before { color: var(--hong); }
|
|
98
|
+
.status[data-state='demo']::before { color: var(--hwang); }
|
|
99
|
+
|
|
100
|
+
/* ---------- layout ---------- */
|
|
101
|
+
.layout {
|
|
102
|
+
flex: 1;
|
|
103
|
+
min-height: 0;
|
|
104
|
+
display: grid;
|
|
105
|
+
grid-template-columns: minmax(0, 1fr) 300px;
|
|
106
|
+
grid-template-rows: minmax(240px, 56%) minmax(220px, 1fr);
|
|
107
|
+
grid-template-areas: 'stage log' 'term log';
|
|
108
|
+
}
|
|
109
|
+
.stage-wrap { grid-area: stage; position: relative; overflow: hidden; background: #0F1115; display: grid; place-items: center; }
|
|
110
|
+
.term-wrap { grid-area: term; position: relative; display: flex; flex-direction: column; border-top: 1px solid var(--line); min-height: 0; }
|
|
111
|
+
.log-wrap { grid-area: log; border-left: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; background: var(--card); }
|
|
112
|
+
body { height: 100vh; }
|
|
113
|
+
|
|
114
|
+
@media (max-width: 900px) {
|
|
115
|
+
body { height: auto; }
|
|
116
|
+
.layout {
|
|
117
|
+
grid-template-columns: minmax(0, 1fr);
|
|
118
|
+
grid-template-rows: auto 420px 280px;
|
|
119
|
+
grid-template-areas: 'stage' 'term' 'log';
|
|
120
|
+
}
|
|
121
|
+
.stage-wrap { aspect-ratio: 400 / 224; }
|
|
122
|
+
.log-wrap { border-left: 0; border-top: 1px solid var(--line); }
|
|
123
|
+
.top { padding: 10px 16px; }
|
|
124
|
+
.field.folder input, .field.session select, .field.pmode select { width: 100%; max-width: none; }
|
|
125
|
+
.launch { width: 100%; }
|
|
126
|
+
.field.folder, .field.session { flex: 1 1 100%; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ---------- stage ---------- */
|
|
130
|
+
.stage { position: relative; width: 400px; height: 224px; transform-origin: 0 0; }
|
|
131
|
+
#scene { position: absolute; inset: 0; width: 400px; height: 224px; image-rendering: pixelated; image-rendering: crisp-edges; }
|
|
132
|
+
.overlay { position: absolute; left: 0; top: 0; pointer-events: none; font-family: 'Galmuri9', var(--pixel); }
|
|
133
|
+
.overlay > * { position: absolute; left: 0; top: 0; }
|
|
134
|
+
|
|
135
|
+
.stone-label { font-family: 'Galmuri7', var(--pixel); font-size: 8px; line-height: 8px; color: #2B2F38; pointer-events: auto; cursor: help; }
|
|
136
|
+
|
|
137
|
+
.tag {
|
|
138
|
+
font-family: 'Galmuri7', var(--pixel);
|
|
139
|
+
font-size: 8px; line-height: 9px; white-space: nowrap;
|
|
140
|
+
padding: 0 2px; color: #fff;
|
|
141
|
+
background: var(--px-nok);
|
|
142
|
+
outline: 1px solid #1E1B17;
|
|
143
|
+
pointer-events: auto; cursor: help;
|
|
144
|
+
}
|
|
145
|
+
.tag.robe-hong { background: var(--px-hong); }
|
|
146
|
+
.tag.robe-cheong { background: var(--px-cheong); }
|
|
147
|
+
.tag.robe-nok { background: var(--px-nok); }
|
|
148
|
+
.tag.small { opacity: 0.85; }
|
|
149
|
+
|
|
150
|
+
.bubble {
|
|
151
|
+
display: none;
|
|
152
|
+
max-width: 150px;
|
|
153
|
+
padding: 3px 4px 2px;
|
|
154
|
+
background: var(--px-paper);
|
|
155
|
+
color: #1E1B17;
|
|
156
|
+
outline: 1px solid #1E1B17;
|
|
157
|
+
box-shadow: 2px 2px 0 rgba(0,0,0,0.45);
|
|
158
|
+
font-size: 10px; line-height: 11px;
|
|
159
|
+
z-index: 3;
|
|
160
|
+
}
|
|
161
|
+
.bubble.on { display: block; }
|
|
162
|
+
.bubble::after {
|
|
163
|
+
content: ''; position: absolute; left: 50%; bottom: -4px; width: 4px; height: 4px;
|
|
164
|
+
background: var(--px-paper); outline: 1px solid #1E1B17; transform: translateX(-50%);
|
|
165
|
+
clip-path: polygon(0 0, 100% 0, 50% 100%);
|
|
166
|
+
}
|
|
167
|
+
.bubble.loose::after { display: none; }
|
|
168
|
+
.bubble-more {
|
|
169
|
+
display: none; left: 4px !important; top: 210px !important; z-index: 5; white-space: nowrap;
|
|
170
|
+
font-family: 'Galmuri7', var(--pixel); font-size: 8px; line-height: 9px;
|
|
171
|
+
color: var(--ink2); background: rgba(21,23,28,0.85); padding: 1px 3px; outline: 1px solid var(--line);
|
|
172
|
+
}
|
|
173
|
+
.bubble-more.on { display: block; }
|
|
174
|
+
.bubble .b-main { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
175
|
+
.bubble .b-sub { font-family: 'Galmuri7', var(--pixel); font-size: 8px; line-height: 9px; color: #6B6358; margin-top: 1px; white-space: nowrap; }
|
|
176
|
+
.bubble.blot { background: #E8DFCE; }
|
|
177
|
+
.bubble.blot::before { /* ink blot: failure is shown, not hidden */
|
|
178
|
+
content: ''; position: absolute; right: 3px; top: 3px; width: 5px; height: 4px; background: #1E1B17;
|
|
179
|
+
box-shadow: 2px 1px 0 -1px #1E1B17, -1px 2px 0 -1px #1E1B17;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* decree (royal order) scroll — unfurls with stepped width (no smooth tween) */
|
|
183
|
+
.decree {
|
|
184
|
+
left: 200px !important; top: 66px !important;
|
|
185
|
+
width: 0; overflow: hidden;
|
|
186
|
+
transform: translateX(-50%);
|
|
187
|
+
background: var(--px-paper); color: #1E1B17;
|
|
188
|
+
border-left: 4px solid #B8862B; border-right: 4px solid #B8862B;
|
|
189
|
+
outline: 1px solid #1E1B17;
|
|
190
|
+
box-shadow: 0 3px 0 rgba(0,0,0,0.4);
|
|
191
|
+
white-space: nowrap; z-index: 4;
|
|
192
|
+
visibility: hidden;
|
|
193
|
+
}
|
|
194
|
+
.decree.on { visibility: visible; animation: unfurl 0.5s steps(8, end) forwards; }
|
|
195
|
+
.decree .d-head { font-family: 'Galmuri7', var(--pixel); font-size: 8px; color: #A8322A; padding: 3px 6px 0; }
|
|
196
|
+
.decree .d-text { font-size: 10px; line-height: 12px; padding: 2px 6px; overflow: hidden; text-overflow: ellipsis; }
|
|
197
|
+
.decree .d-foot { font-family: 'Galmuri7', var(--pixel); font-size: 8px; color: #6B6358; padding: 0 6px 3px; }
|
|
198
|
+
@keyframes unfurl { from { width: 0; } to { width: 230px; } }
|
|
199
|
+
|
|
200
|
+
/* petition (permission request) */
|
|
201
|
+
.petition {
|
|
202
|
+
/* top-right over the back wall: never covers a desk, the throne or the king (scene.js PETITION_BOX) */
|
|
203
|
+
left: 238px !important; top: 4px !important;
|
|
204
|
+
width: 158px;
|
|
205
|
+
background: var(--card); color: var(--ink);
|
|
206
|
+
outline: 1px solid #1E1B17;
|
|
207
|
+
border: 1px solid var(--hwang);
|
|
208
|
+
box-shadow: 0 3px 0 rgba(0,0,0,0.5);
|
|
209
|
+
padding: 5px 6px 4px;
|
|
210
|
+
display: none; z-index: 6;
|
|
211
|
+
pointer-events: auto;
|
|
212
|
+
}
|
|
213
|
+
.petition.on { display: block; }
|
|
214
|
+
.petition .p-head { font-size: 10px; line-height: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
215
|
+
.petition .p-ask { font-family: 'Galmuri11', var(--pixel); font-weight: 700; font-size: 11px; color: var(--hwang); margin-top: 3px; }
|
|
216
|
+
.petition .p-wait { font-family: 'Galmuri7', var(--pixel); font-size: 8px; color: var(--ink2); margin-top: 1px; }
|
|
217
|
+
.petition .p-btns { display: flex; gap: 4px; margin-top: 4px; }
|
|
218
|
+
.petition .p-btn {
|
|
219
|
+
flex: 1; display: flex; align-items: center; justify-content: center; gap: 2px;
|
|
220
|
+
padding: 2px 0; font-family: 'Galmuri9', var(--pixel); font-size: 10px; line-height: 12px;
|
|
221
|
+
background: var(--heuk); border: 1px solid #1E1B17; outline: 1px solid var(--line); cursor: pointer; color: var(--ink);
|
|
222
|
+
}
|
|
223
|
+
.petition .p-ok { background: var(--px-hong); color: #fff; }
|
|
224
|
+
.petition .p-no { background: #3A3F4A; }
|
|
225
|
+
.petition .p-btn:focus-visible { outline: 2px solid var(--hwang); }
|
|
226
|
+
.petition .p-hint { display: none; } /* shown as a tooltip on the panel instead, to keep it short */
|
|
227
|
+
.petition.min { width: 110px; left: 286px !important; }
|
|
228
|
+
.petition.min .p-btns, .petition.min .p-hint, .petition.min .p-ask { display: none; }
|
|
229
|
+
|
|
230
|
+
/* stamps: shape + mark + word (never color alone) */
|
|
231
|
+
.stamp {
|
|
232
|
+
left: 317px !important; top: 30px !important;
|
|
233
|
+
width: 34px; height: 34px; transform: translate(-50%, -50%) rotate(-12deg);
|
|
234
|
+
display: none; z-index: 8;
|
|
235
|
+
color: var(--px-inju);
|
|
236
|
+
border: 2px solid var(--px-inju);
|
|
237
|
+
background: rgba(243, 237, 225, 0.92);
|
|
238
|
+
text-align: center;
|
|
239
|
+
font-family: 'Galmuri9', var(--pixel);
|
|
240
|
+
}
|
|
241
|
+
.stamp.on { display: block; }
|
|
242
|
+
.stamp.shape-circle { border-radius: 50%; }
|
|
243
|
+
.stamp.shape-square { border-radius: 0; }
|
|
244
|
+
.stamp.shape-dotted { border-radius: 50%; border-style: dashed; color: #6B6358; border-color: #6B6358; }
|
|
245
|
+
.stamp .st-mark { font-size: 10px; line-height: 12px; margin-top: 3px; }
|
|
246
|
+
.stamp .st-label { font-size: 10px; line-height: 11px; font-weight: 700; }
|
|
247
|
+
.stamp-full {
|
|
248
|
+
left: 317px !important; top: 52px !important; transform: translateX(-50%);
|
|
249
|
+
display: none; z-index: 8; white-space: nowrap;
|
|
250
|
+
font-family: 'Galmuri11', var(--pixel); font-weight: 700; font-size: 11px;
|
|
251
|
+
color: #fff; background: var(--px-inju); padding: 1px 4px; outline: 1px solid #1E1B17;
|
|
252
|
+
}
|
|
253
|
+
.stamp-full.k-hold { background: #6B6358; }
|
|
254
|
+
.stamp-full.on { display: block; }
|
|
255
|
+
|
|
256
|
+
.plaque {
|
|
257
|
+
left: 200px !important; top: 40px !important; transform: translateX(-50%);
|
|
258
|
+
display: none; z-index: 7; white-space: nowrap;
|
|
259
|
+
background: #B8862B; color: #1E1B17;
|
|
260
|
+
outline: 1px solid #1E1B17;
|
|
261
|
+
border: 2px solid #6E4B2A;
|
|
262
|
+
font-family: 'Galmuri11', var(--pixel); font-weight: 700; font-size: 11px; line-height: 12px;
|
|
263
|
+
padding: 3px 8px;
|
|
264
|
+
}
|
|
265
|
+
.plaque.on { display: block; animation: hang 0.25s steps(3, end); }
|
|
266
|
+
@keyframes hang { from { transform: translate(-50%, -12px); } to { transform: translate(-50%, 0); } }
|
|
267
|
+
|
|
268
|
+
.toast {
|
|
269
|
+
left: 200px !important; top: 196px !important; transform: translateX(-50%);
|
|
270
|
+
display: none; z-index: 5; white-space: nowrap;
|
|
271
|
+
background: rgba(21,23,28,0.92); color: var(--ink);
|
|
272
|
+
outline: 1px solid var(--line);
|
|
273
|
+
font-size: 10px; line-height: 12px; padding: 2px 6px;
|
|
274
|
+
}
|
|
275
|
+
.toast.on { display: block; }
|
|
276
|
+
|
|
277
|
+
@media (prefers-reduced-motion: reduce) {
|
|
278
|
+
.decree.on { animation: none; width: 230px; }
|
|
279
|
+
.plaque.on { animation: none; }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ---------- terminal ---------- */
|
|
283
|
+
.term { flex: 1; min-height: 0; padding: 6px 0 0 8px; background: var(--paper); }
|
|
284
|
+
.term .xterm { height: 100%; }
|
|
285
|
+
.term-hint {
|
|
286
|
+
position: absolute; inset: 0 0 46px 0; display: grid; place-content: center; text-align: center;
|
|
287
|
+
color: var(--ink); pointer-events: none; font-size: 13px;
|
|
288
|
+
}
|
|
289
|
+
.term-hint p { margin: 4px 0; }
|
|
290
|
+
.term-hint.hidden { display: none; }
|
|
291
|
+
.decree-bar { display: flex; gap: 8px; align-items: center; padding: 8px; border-top: 1px solid var(--line); background: var(--card); }
|
|
292
|
+
.decree-label { font-family: var(--pixel); font-size: 12px; color: var(--hwang); white-space: nowrap; }
|
|
293
|
+
.decree-bar input {
|
|
294
|
+
flex: 1; min-width: 0; background: var(--paper); border: 1px solid var(--line); border-radius: 4px; padding: 7px 9px; font-size: 13px;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ---------- log ---------- */
|
|
298
|
+
.log-wrap h2 { font-family: var(--pixel); font-size: 13px; margin: 0; padding: 10px 12px; border-bottom: 1px solid var(--line); color: var(--hwang); }
|
|
299
|
+
.log { list-style: none; margin: 0; padding: 4px 0; overflow-y: auto; flex: 1; font-size: 12px; }
|
|
300
|
+
.log li { display: grid; grid-template-columns: 54px 14px 1fr; gap: 4px; padding: 4px 10px; border-bottom: 1px dashed #2C3038; line-height: 1.4; }
|
|
301
|
+
.log time { color: var(--ink2); font-variant-numeric: tabular-nums; font-size: 11px; padding-top: 1px; }
|
|
302
|
+
.log .ic { text-align: center; }
|
|
303
|
+
.log .tx { word-break: break-all; }
|
|
304
|
+
.log .who { color: var(--ink2); }
|
|
305
|
+
.log li.k-decree .tx { color: var(--px-paper); font-weight: 600; }
|
|
306
|
+
.log li.k-ask .tx { color: var(--hwang); }
|
|
307
|
+
.log li.k-fail .tx { color: var(--hong); }
|
|
308
|
+
.log li.k-sys .tx { color: var(--ink2); }
|
|
309
|
+
.log .ok { color: var(--nok); margin-left: 4px; }
|
|
310
|
+
|
|
311
|
+
.foot {
|
|
312
|
+
display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
|
|
313
|
+
padding: 6px 16px; border-top: 1px solid var(--line); color: var(--ink2); font-size: 11px; background: var(--card);
|
|
314
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// node-pty 1.x prebuilds on macOS ship spawn-helper without the exec bit
|
|
2
|
+
// ("posix_spawnp failed"). Restore it after install. Safe to run repeatedly.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
for (const mod of ['node-pty', '@homebridge/node-pty-prebuilt-multiarch']) try {
|
|
9
|
+
const root = path.dirname(require.resolve(`${mod}/package.json`));
|
|
10
|
+
for (const dir of ['prebuilds', 'build/Release']) {
|
|
11
|
+
const base = path.join(root, dir);
|
|
12
|
+
if (!fs.existsSync(base)) continue;
|
|
13
|
+
const walk = (d) => {
|
|
14
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
15
|
+
const p = path.join(d, f.name);
|
|
16
|
+
if (f.isDirectory()) walk(p);
|
|
17
|
+
else if (f.name === 'spawn-helper') fs.chmodSync(p, 0o755);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
walk(base);
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
// optional dependency not installed on this platform: nothing to fix
|
|
24
|
+
if (e.code !== 'MODULE_NOT_FOUND') console.warn(`[eojeon] could not fix ${mod} spawn-helper:`, e.message);
|
|
25
|
+
}
|
package/server/agents.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Agent packs (Claude Code subagent files) shipped in templates/agents/<pack>/.
|
|
2
|
+
// install() copies them into <project>/.claude/agents/ and NEVER overwrites:
|
|
3
|
+
// a file that already exists with the same name is skipped and reported.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
export const TEMPLATES = path.join(ROOT, 'templates', 'agents');
|
|
10
|
+
export const PACKS = ['yukjo', 'studio'];
|
|
11
|
+
|
|
12
|
+
export function packFiles(pack) {
|
|
13
|
+
if (!PACKS.includes(pack)) throw new Error(`unknown pack: ${pack} (use ${PACKS.join(' | ')} | all)`);
|
|
14
|
+
const dir = path.join(TEMPLATES, pack);
|
|
15
|
+
return fs
|
|
16
|
+
.readdirSync(dir)
|
|
17
|
+
.filter((f) => f.endsWith('.md'))
|
|
18
|
+
.sort()
|
|
19
|
+
.map((f) => ({ name: f.slice(0, -3), file: f, src: path.join(dir, f) }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function expandPacks(pack) {
|
|
23
|
+
if (!pack || pack === 'yukjo') return ['yukjo'];
|
|
24
|
+
if (pack === 'all') return [...PACKS];
|
|
25
|
+
if (Array.isArray(pack)) return pack.flatMap(expandPacks);
|
|
26
|
+
return [String(pack)];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function agentsDir(projectDir) {
|
|
30
|
+
return path.join(path.resolve(projectDir), '.claude', 'agents');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The `name:` in an agent file's frontmatter (falls back to the file name).
|
|
34
|
+
function agentName(file) {
|
|
35
|
+
try {
|
|
36
|
+
const head = fs.readFileSync(file, 'utf8').slice(0, 4096);
|
|
37
|
+
const m = /^---\s*\n([\s\S]*?)\n---/.exec(head);
|
|
38
|
+
const n = m && /^name:\s*(.+)$/m.exec(m[1]);
|
|
39
|
+
if (n) return n[1].trim().replace(/^["']|["']$/g, '');
|
|
40
|
+
} catch {}
|
|
41
|
+
return path.basename(file, '.md');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// What is already in <project>/.claude/agents, and which pack files are missing.
|
|
45
|
+
export function inspect(projectDir, pack = 'yukjo') {
|
|
46
|
+
const dir = agentsDir(projectDir);
|
|
47
|
+
const exists = fs.existsSync(dir);
|
|
48
|
+
const present = exists ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')) : [];
|
|
49
|
+
const userAgents = present.map((f) => ({ file: f, name: agentName(path.join(dir, f)) }));
|
|
50
|
+
const want = expandPacks(pack).flatMap(packFiles);
|
|
51
|
+
const missing = want.filter((w) => !present.includes(w.file)).map((w) => w.name);
|
|
52
|
+
const installed = want.filter((w) => present.includes(w.file)).map((w) => w.name);
|
|
53
|
+
return { dir, exists, missing, installed, userAgents };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -> { dir, copied: [name], skipped: [name] }
|
|
57
|
+
export function install(projectDir, pack = 'yukjo') {
|
|
58
|
+
const target = path.resolve(projectDir);
|
|
59
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) throw new Error(`folder not found: ${target}`);
|
|
60
|
+
const dir = agentsDir(target);
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
const copied = [];
|
|
63
|
+
const skipped = [];
|
|
64
|
+
for (const p of expandPacks(pack)) {
|
|
65
|
+
for (const f of packFiles(p)) {
|
|
66
|
+
const dest = path.join(dir, f.file);
|
|
67
|
+
try {
|
|
68
|
+
// COPYFILE_EXCL: fails if dest exists, so an existing file is never replaced (no race either)
|
|
69
|
+
fs.copyFileSync(f.src, dest, fs.constants.COPYFILE_EXCL);
|
|
70
|
+
copied.push(f.name);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e.code === 'EEXIST') skipped.push(f.name);
|
|
73
|
+
else throw e;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { dir, copied, skipped };
|
|
78
|
+
}
|
package/server/hook.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code hook handler (injected via `claude --settings <temp file>`).
|
|
3
|
+
// Reads the hook JSON from stdin, trims it to a tiny display event, and POSTs it
|
|
4
|
+
// to the local court server. It never blocks Claude Code: hard time limit,
|
|
5
|
+
// prints nothing to stdout, and always exits 0.
|
|
6
|
+
//
|
|
7
|
+
// usage: node hook.js <port> <token>
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import { sanitize } from './sanitize.js';
|
|
10
|
+
|
|
11
|
+
const [port, token] = process.argv.slice(2);
|
|
12
|
+
const done = () => process.exit(0);
|
|
13
|
+
setTimeout(done, 1500).unref?.();
|
|
14
|
+
|
|
15
|
+
let buf = '';
|
|
16
|
+
process.stdin.setEncoding('utf8');
|
|
17
|
+
process.stdin.on('data', (c) => {
|
|
18
|
+
if (buf.length < 4_000_000) buf += c;
|
|
19
|
+
});
|
|
20
|
+
process.stdin.on('error', done);
|
|
21
|
+
process.stdin.on('end', () => {
|
|
22
|
+
let ev;
|
|
23
|
+
try {
|
|
24
|
+
ev = sanitize(JSON.parse(buf));
|
|
25
|
+
} catch {
|
|
26
|
+
return done();
|
|
27
|
+
}
|
|
28
|
+
if (!port || !token) return done();
|
|
29
|
+
const body = JSON.stringify(ev);
|
|
30
|
+
const req = http.request(
|
|
31
|
+
{
|
|
32
|
+
host: '127.0.0.1',
|
|
33
|
+
port: Number(port),
|
|
34
|
+
path: '/hook',
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'x-court-token': token },
|
|
37
|
+
timeout: 800,
|
|
38
|
+
},
|
|
39
|
+
(res) => {
|
|
40
|
+
res.resume();
|
|
41
|
+
res.on('end', done);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
req.on('timeout', () => req.destroy());
|
|
45
|
+
req.on('error', done);
|
|
46
|
+
req.end(body);
|
|
47
|
+
});
|