@tienne/gestalt 0.108.0 → 0.110.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 (113) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/package.json +1 -1
  3. package/dist/src/cli/commands/peer.d.ts +49 -0
  4. package/dist/src/cli/commands/peer.d.ts.map +1 -0
  5. package/dist/src/cli/commands/peer.js +302 -0
  6. package/dist/src/cli/commands/peer.js.map +1 -0
  7. package/dist/src/cli/index.d.ts.map +1 -1
  8. package/dist/src/cli/index.js +81 -2
  9. package/dist/src/cli/index.js.map +1 -1
  10. package/dist/src/peer/client.d.ts +9 -0
  11. package/dist/src/peer/client.d.ts.map +1 -0
  12. package/dist/src/peer/client.js +51 -0
  13. package/dist/src/peer/client.js.map +1 -0
  14. package/dist/src/peer/daemon.d.ts +83 -0
  15. package/dist/src/peer/daemon.d.ts.map +1 -0
  16. package/dist/src/peer/daemon.js +516 -0
  17. package/dist/src/peer/daemon.js.map +1 -0
  18. package/dist/src/peer/errors.d.ts +2 -0
  19. package/dist/src/peer/errors.d.ts.map +1 -0
  20. package/dist/src/peer/errors.js +4 -0
  21. package/dist/src/peer/errors.js.map +1 -0
  22. package/dist/src/peer/feed.d.ts +103 -0
  23. package/dist/src/peer/feed.d.ts.map +1 -0
  24. package/dist/src/peer/feed.js +167 -0
  25. package/dist/src/peer/feed.js.map +1 -0
  26. package/dist/src/peer/index.d.ts +24 -0
  27. package/dist/src/peer/index.d.ts.map +1 -0
  28. package/dist/src/peer/index.js +14 -0
  29. package/dist/src/peer/index.js.map +1 -0
  30. package/dist/src/peer/keys.d.ts +26 -0
  31. package/dist/src/peer/keys.d.ts.map +1 -0
  32. package/dist/src/peer/keys.js +95 -0
  33. package/dist/src/peer/keys.js.map +1 -0
  34. package/dist/src/peer/locate.d.ts +44 -0
  35. package/dist/src/peer/locate.d.ts.map +1 -0
  36. package/dist/src/peer/locate.js +116 -0
  37. package/dist/src/peer/locate.js.map +1 -0
  38. package/dist/src/peer/lock.d.ts +32 -0
  39. package/dist/src/peer/lock.d.ts.map +1 -0
  40. package/dist/src/peer/lock.js +147 -0
  41. package/dist/src/peer/lock.js.map +1 -0
  42. package/dist/src/peer/mirror.d.ts +21 -0
  43. package/dist/src/peer/mirror.d.ts.map +1 -0
  44. package/dist/src/peer/mirror.js +68 -0
  45. package/dist/src/peer/mirror.js.map +1 -0
  46. package/dist/src/peer/network.d.ts +148 -0
  47. package/dist/src/peer/network.d.ts.map +1 -0
  48. package/dist/src/peer/network.js +809 -0
  49. package/dist/src/peer/network.js.map +1 -0
  50. package/dist/src/peer/paths.d.ts +23 -0
  51. package/dist/src/peer/paths.d.ts.map +1 -0
  52. package/dist/src/peer/paths.js +50 -0
  53. package/dist/src/peer/paths.js.map +1 -0
  54. package/dist/src/peer/protocol.d.ts +112 -0
  55. package/dist/src/peer/protocol.d.ts.map +1 -0
  56. package/dist/src/peer/protocol.js +8 -0
  57. package/dist/src/peer/protocol.js.map +1 -0
  58. package/dist/src/peer/redact.d.ts +30 -0
  59. package/dist/src/peer/redact.d.ts.map +1 -0
  60. package/dist/src/peer/redact.js +214 -0
  61. package/dist/src/peer/redact.js.map +1 -0
  62. package/dist/src/peer/relay.d.ts +70 -0
  63. package/dist/src/peer/relay.d.ts.map +1 -0
  64. package/dist/src/peer/relay.js +410 -0
  65. package/dist/src/peer/relay.js.map +1 -0
  66. package/dist/src/peer/roster.d.ts +89 -0
  67. package/dist/src/peer/roster.d.ts.map +1 -0
  68. package/dist/src/peer/roster.js +160 -0
  69. package/dist/src/peer/roster.js.map +1 -0
  70. package/dist/src/peer/seal.d.ts +37 -0
  71. package/dist/src/peer/seal.d.ts.map +1 -0
  72. package/dist/src/peer/seal.js +115 -0
  73. package/dist/src/peer/seal.js.map +1 -0
  74. package/dist/src/peer/share.d.ts +41 -0
  75. package/dist/src/peer/share.d.ts.map +1 -0
  76. package/dist/src/peer/share.js +70 -0
  77. package/dist/src/peer/share.js.map +1 -0
  78. package/dist/src/peer/state.d.ts +68 -0
  79. package/dist/src/peer/state.d.ts.map +1 -0
  80. package/dist/src/peer/state.js +161 -0
  81. package/dist/src/peer/state.js.map +1 -0
  82. package/dist/src/peer/tail.d.ts +55 -0
  83. package/dist/src/peer/tail.d.ts.map +1 -0
  84. package/dist/src/peer/tail.js +148 -0
  85. package/dist/src/peer/tail.js.map +1 -0
  86. package/dist/src/peer/transform.d.ts +79 -0
  87. package/dist/src/peer/transform.d.ts.map +1 -0
  88. package/dist/src/peer/transform.js +237 -0
  89. package/dist/src/peer/transform.js.map +1 -0
  90. package/dist/src/peer/transport.d.ts +89 -0
  91. package/dist/src/peer/transport.d.ts.map +1 -0
  92. package/dist/src/peer/transport.js +345 -0
  93. package/dist/src/peer/transport.js.map +1 -0
  94. package/dist/src/peer/viewer-html.d.ts +10 -0
  95. package/dist/src/peer/viewer-html.d.ts.map +1 -0
  96. package/dist/src/peer/viewer-html.js +165 -0
  97. package/dist/src/peer/viewer-html.js.map +1 -0
  98. package/dist/src/peer/viewer.d.ts +58 -0
  99. package/dist/src/peer/viewer.d.ts.map +1 -0
  100. package/dist/src/peer/viewer.js +249 -0
  101. package/dist/src/peer/viewer.js.map +1 -0
  102. package/dist/src/peer/wire.d.ts +90 -0
  103. package/dist/src/peer/wire.d.ts.map +1 -0
  104. package/dist/src/peer/wire.js +10 -0
  105. package/dist/src/peer/wire.js.map +1 -0
  106. package/dist/src/peer/ws.d.ts +47 -0
  107. package/dist/src/peer/ws.d.ts.map +1 -0
  108. package/dist/src/peer/ws.js +308 -0
  109. package/dist/src/peer/ws.js.map +1 -0
  110. package/package.json +1 -1
  111. package/plugin/.codex-plugin/plugin.json +1 -1
  112. package/plugin/.mcp.json +1 -1
  113. package/plugin/mcp.json +1 -1
@@ -0,0 +1,345 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { signText } from './seal.js';
3
+ import { connectWs } from './ws.js';
4
+ import { CLOSE_REMOVED, helloSigned } from './wire.js';
5
+ import { errMessage } from './errors.js';
6
+ export class RelaySendError extends Error {
7
+ reason;
8
+ constructor(reason) {
9
+ super(reason === 'offline'
10
+ ? '받는 기기가 릴레이에 안 붙어 있어요'
11
+ : reason === 'not_connected'
12
+ ? '릴레이에 안 붙어 있어요'
13
+ : `릴레이가 전달하지 못했어요: ${reason}`);
14
+ this.reason = reason;
15
+ this.name = 'RelaySendError';
16
+ }
17
+ }
18
+ /**
19
+ * 릴레이 접속 하나. 이벤트:
20
+ * - `state`(TransportState)
21
+ * - `welcome`(roster, online) — member로 붙었다. 다시 붙을 때마다 온다
22
+ * - `roster`(SignedRoster) — 릴레이가 새 명단을 돌렸다. 받을지는 부른 쪽이 다시 확인한다
23
+ * - `presence`(key, online)
24
+ * - `join`(from, sealed) — 관리자에게 온 join 요청
25
+ * - `relay-error`(code, message)
26
+ */
27
+ export class RelayTransport extends EventEmitter {
28
+ options;
29
+ ws = null;
30
+ roster = null;
31
+ status = 'idle';
32
+ nextId = 1;
33
+ acks = new Map();
34
+ resolves = new Map();
35
+ receivers = [];
36
+ backoff;
37
+ retry = null;
38
+ announced = [];
39
+ constructor(options) {
40
+ super();
41
+ this.options = options;
42
+ this.backoff = options.minBackoffMs ?? 500;
43
+ }
44
+ get state() {
45
+ return this.status;
46
+ }
47
+ get bufferedAmount() {
48
+ return this.ws?.bufferedAmount ?? 0;
49
+ }
50
+ /** 다음 hello에 들고 갈 명단 */
51
+ setRoster(roster) {
52
+ this.roster = roster;
53
+ }
54
+ /**
55
+ * 처음 붙는다. member면 welcome, join 모드면 pending까지 기다린다. 첫 시도가 실패하면
56
+ * 던지고 reconnect가 켜져 있으면 뒤에서 계속 다시 붙는다.
57
+ */
58
+ connect(roster) {
59
+ this.roster = roster;
60
+ return this.attempt();
61
+ }
62
+ onReceive(handler) {
63
+ this.receivers.push(handler);
64
+ }
65
+ send(to, sealed) {
66
+ return new Promise((resolve, reject) => {
67
+ const ws = this.ws;
68
+ if (!ws || this.status !== 'online') {
69
+ reject(new RelaySendError('not_connected'));
70
+ return;
71
+ }
72
+ const id = this.nextId++;
73
+ const timer = setTimeout(() => {
74
+ this.acks.delete(id);
75
+ reject(new RelaySendError('timeout'));
76
+ }, this.options.ackTimeoutMs ?? 10_000);
77
+ timer.unref();
78
+ this.acks.set(id, {
79
+ timer,
80
+ resolve: (ok, error) => ok ? resolve('delivered') : reject(new RelaySendError(error ?? 'unknown')),
81
+ });
82
+ this.write({ t: 'send', id, to, data: Buffer.from(sealed).toString('base64url') });
83
+ });
84
+ }
85
+ /**
86
+ * 답을 안 기다리고 보낸다. 공유 스트림 프레임처럼 자주 가는 것에 쓴다. 릴레이도 ack를
87
+ * 안 돌려준다. 받는 쪽이 없으면 조용히 버려진다.
88
+ */
89
+ post(to, sealed) {
90
+ if (!this.ws || this.status !== 'online')
91
+ return false;
92
+ const id = this.nextId++;
93
+ return this.write({
94
+ t: 'send',
95
+ id,
96
+ to,
97
+ data: Buffer.from(sealed).toString('base64url'),
98
+ ack: false,
99
+ });
100
+ }
101
+ /** 공유 id를 연 기기를 릴레이에 묻는다 */
102
+ resolveShare(share, timeoutMs = 5_000) {
103
+ return new Promise((resolve, reject) => {
104
+ if (!this.ws || this.status !== 'online') {
105
+ reject(new RelaySendError('not_connected'));
106
+ return;
107
+ }
108
+ const id = this.nextId++;
109
+ const timer = setTimeout(() => {
110
+ this.resolves.delete(id);
111
+ reject(new RelaySendError('timeout'));
112
+ }, timeoutMs);
113
+ timer.unref();
114
+ this.resolves.set(id, (owner) => {
115
+ clearTimeout(timer);
116
+ resolve(owner);
117
+ });
118
+ this.write({ t: 'resolve', id, share });
119
+ });
120
+ }
121
+ /** 지금 연 공유 id 전부를 알린다. 다시 붙을 때도 같은 목록을 다시 보낸다 */
122
+ announce(shares) {
123
+ this.announced = [...shares];
124
+ if (this.status === 'online')
125
+ this.write({ t: 'announce', shares: this.announced });
126
+ }
127
+ publishRoster(roster) {
128
+ this.roster = roster;
129
+ if (this.status === 'online')
130
+ this.write({ t: 'roster', roster });
131
+ }
132
+ sendJoin(to, sealed) {
133
+ if (!this.ws)
134
+ return false;
135
+ return this.write({ t: 'join', to, data: Buffer.from(sealed).toString('base64url') });
136
+ }
137
+ async close() {
138
+ this.setState('closed');
139
+ if (this.retry)
140
+ clearTimeout(this.retry);
141
+ this.retry = null;
142
+ const ws = this.ws;
143
+ this.ws = null;
144
+ ws?.close(1000, 'bye');
145
+ this.failPending('not_connected');
146
+ }
147
+ attempt() {
148
+ if (this.status === 'closed' || this.status === 'removed')
149
+ return Promise.resolve();
150
+ this.setState('connecting');
151
+ return new Promise((resolve, reject) => {
152
+ let settled = false;
153
+ const settle = (err) => {
154
+ if (settled)
155
+ return;
156
+ settled = true;
157
+ if (err)
158
+ reject(err);
159
+ else
160
+ resolve();
161
+ };
162
+ connectWs(this.options.url).then((ws) => {
163
+ if (this.status === 'closed') {
164
+ ws.close(1000, 'bye');
165
+ settle();
166
+ return;
167
+ }
168
+ this.ws = ws;
169
+ ws.on('message', (text) => this.onMessage(ws, text, settle));
170
+ ws.on('close', (code, reason) => {
171
+ if (this.ws !== ws)
172
+ return;
173
+ this.ws = null;
174
+ this.failPending('not_connected');
175
+ if (code === CLOSE_REMOVED) {
176
+ this.setState('removed');
177
+ settle(new Error('명단에 없는 기기라 릴레이가 끊었어요'));
178
+ return;
179
+ }
180
+ settle(new Error(`릴레이 연결이 끊겼어요 (${code}${reason ? ` ${reason}` : ''})`));
181
+ this.lost();
182
+ });
183
+ }, (err) => {
184
+ settle(new Error(`릴레이에 못 붙었어요: ${errMessage(err)}`));
185
+ this.lost();
186
+ });
187
+ });
188
+ }
189
+ lost() {
190
+ if (this.status === 'closed' || this.status === 'removed')
191
+ return;
192
+ this.setState('offline');
193
+ const reconnect = this.options.reconnect ?? (this.options.mode ?? 'member') === 'member';
194
+ if (!reconnect || this.retry)
195
+ return;
196
+ const wait = this.backoff;
197
+ this.backoff = Math.min(this.backoff * 2, this.options.maxBackoffMs ?? 30_000);
198
+ this.retry = setTimeout(() => {
199
+ this.retry = null;
200
+ this.attempt().catch(() => {
201
+ // 실패는 close 핸들러가 다음 시도를 건다
202
+ });
203
+ }, wait);
204
+ this.retry.unref();
205
+ }
206
+ /**
207
+ * 릴레이가 보낸 프레임. 릴레이도 믿지 않는 쪽이라 꼴이 틀린 프레임은 버린다. 여기서
208
+ * 던지면 소켓 data 핸들러 밖으로 나가 데몬이 죽는다.
209
+ */
210
+ onMessage(ws, text, settle) {
211
+ let frame;
212
+ try {
213
+ frame = JSON.parse(text);
214
+ }
215
+ catch {
216
+ return;
217
+ }
218
+ if (typeof frame !== 'object' || frame === null || !wellFormed(frame))
219
+ return;
220
+ try {
221
+ this.dispatch(ws, frame, settle);
222
+ }
223
+ catch (err) {
224
+ process.stderr.write(`[peer] relay frame failed: ${errMessage(err)}\n`);
225
+ }
226
+ }
227
+ dispatch(ws, frame, settle) {
228
+ switch (frame.t) {
229
+ case 'challenge': {
230
+ const mode = this.options.mode ?? 'member';
231
+ const hello = {
232
+ t: 'hello',
233
+ team: this.options.team,
234
+ key: this.options.key.id,
235
+ mode,
236
+ sig: signText(this.options.key, helloSigned(this.options.team, frame.nonce, mode)),
237
+ ...(this.roster ? { roster: this.roster } : {}),
238
+ };
239
+ ws.send(JSON.stringify(hello));
240
+ return;
241
+ }
242
+ case 'welcome':
243
+ this.backoff = this.options.minBackoffMs ?? 500;
244
+ this.setState('online');
245
+ if (this.announced.length > 0)
246
+ this.write({ t: 'announce', shares: this.announced });
247
+ this.emit('welcome', frame.roster, frame.online);
248
+ settle();
249
+ return;
250
+ case 'pending':
251
+ this.emit('pending');
252
+ settle();
253
+ return;
254
+ case 'error':
255
+ this.emit('relay-error', frame.code, frame.message);
256
+ settle(new Error(`릴레이가 거절했어요: ${frame.message}`));
257
+ return;
258
+ case 'recv': {
259
+ const bytes = Buffer.from(frame.data, 'base64url');
260
+ for (const r of this.receivers) {
261
+ try {
262
+ r(frame.from, bytes);
263
+ }
264
+ catch (err) {
265
+ process.stderr.write(`[peer] receive handler failed: ${errMessage(err)}\n`);
266
+ }
267
+ }
268
+ return;
269
+ }
270
+ case 'ack': {
271
+ const p = this.acks.get(frame.id);
272
+ if (!p)
273
+ return;
274
+ this.acks.delete(frame.id);
275
+ clearTimeout(p.timer);
276
+ p.resolve(frame.ok, frame.error);
277
+ return;
278
+ }
279
+ case 'resolved': {
280
+ const r = this.resolves.get(frame.id);
281
+ this.resolves.delete(frame.id);
282
+ r?.(typeof frame.owner === 'string' ? frame.owner : null);
283
+ return;
284
+ }
285
+ case 'roster':
286
+ this.emit('roster', frame.roster);
287
+ return;
288
+ case 'presence':
289
+ this.emit('presence', frame.key, frame.online);
290
+ return;
291
+ case 'join':
292
+ this.emit('join', frame.from, Buffer.from(frame.data, 'base64url'));
293
+ return;
294
+ }
295
+ }
296
+ write(frame) {
297
+ return this.ws?.send(JSON.stringify(frame)) ?? false;
298
+ }
299
+ failPending(reason) {
300
+ for (const [id, p] of this.acks) {
301
+ clearTimeout(p.timer);
302
+ this.acks.delete(id);
303
+ p.resolve(false, reason);
304
+ }
305
+ this.resolves.clear();
306
+ }
307
+ setState(next) {
308
+ if (this.status === next)
309
+ return;
310
+ // closed와 removed는 되돌리지 않는다
311
+ if ((this.status === 'closed' || this.status === 'removed') && next !== 'closed')
312
+ return;
313
+ this.status = next;
314
+ this.emit('state', next);
315
+ }
316
+ }
317
+ /** 프레임 종류마다 쓰는 칸의 타입만 본다. 내용 검사는 받는 쪽이 한다 */
318
+ function wellFormed(frame) {
319
+ const f = frame;
320
+ const str = (k) => typeof f[k] === 'string';
321
+ switch (f.t) {
322
+ case 'challenge':
323
+ return str('nonce');
324
+ case 'welcome':
325
+ return typeof f.roster === 'object' && f.roster !== null && Array.isArray(f.online);
326
+ case 'pending':
327
+ return true;
328
+ case 'error':
329
+ return str('code') && str('message');
330
+ case 'recv':
331
+ case 'join':
332
+ return str('from') && str('data');
333
+ case 'ack':
334
+ return Number.isSafeInteger(f.id) && typeof f.ok === 'boolean';
335
+ case 'resolved':
336
+ return Number.isSafeInteger(f.id);
337
+ case 'roster':
338
+ return typeof f.roster === 'object' && f.roster !== null;
339
+ case 'presence':
340
+ return str('key') && typeof f.online === 'boolean';
341
+ default:
342
+ return false;
343
+ }
344
+ }
345
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../../src/peer/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare const VIEWER_WARNING = "\uBE44\uBC00\uAC12 \uD328\uD134\uC740 \uAC00\uB9AC\uC9C0\uB9CC \uC804\uBD80 \uC7A1\uC9C0\uB294 \uBABB\uD574\uC694.";
2
+ /**
3
+ * 뷰어 페이지. 바깥 자원을 하나도 안 끌어온다 — CSP가 `default-src 'none'`이라
4
+ * 인라인 스크립트와 스타일도 nonce가 맞는 것만 돈다.
5
+ *
6
+ * 대화 내용은 전부 textContent로 넣는다. 마크다운을 HTML로 그리면 남의 도구 출력에
7
+ * 든 태그가 이 페이지에서 실행될 길이 생긴다. localStorage나 IndexedDB도 안 쓴다.
8
+ */
9
+ export declare function renderViewerHtml(nonce: string, maxViewers: number): string;
10
+ //# sourceMappingURL=viewer-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer-html.d.ts","sourceRoot":"","sources":["../../../src/peer/viewer-html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,uHAA6B,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA2J1E"}
@@ -0,0 +1,165 @@
1
+ export const VIEWER_WARNING = '비밀값 패턴은 가리지만 전부 잡지는 못해요.';
2
+ /**
3
+ * 뷰어 페이지. 바깥 자원을 하나도 안 끌어온다 — CSP가 `default-src 'none'`이라
4
+ * 인라인 스크립트와 스타일도 nonce가 맞는 것만 돈다.
5
+ *
6
+ * 대화 내용은 전부 textContent로 넣는다. 마크다운을 HTML로 그리면 남의 도구 출력에
7
+ * 든 태그가 이 페이지에서 실행될 길이 생긴다. localStorage나 IndexedDB도 안 쓴다.
8
+ */
9
+ export function renderViewerHtml(nonce, maxViewers) {
10
+ return `<!doctype html>
11
+ <html lang="ko">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1">
15
+ <meta name="referrer" content="no-referrer">
16
+ <title>gestalt peer</title>
17
+ <style nonce="${nonce}">
18
+ :root { color-scheme: light dark; --muted: #888; --line: #8884; }
19
+ body { font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; }
20
+ header { position: sticky; top: 0; padding: 10px 16px; border-bottom: 1px solid var(--line);
21
+ background: Canvas; z-index: 1; }
22
+ header h1 { font-size: 15px; margin: 0 0 4px; }
23
+ .warn { color: #b45309; font-size: 13px; }
24
+ .meta { color: var(--muted); font-size: 12px; display: flex; gap: 12px; flex-wrap: wrap; }
25
+ main { padding: 12px 16px 48px; max-width: 980px; }
26
+ .ev { margin: 10px 0; }
27
+ .who { font-size: 12px; color: var(--muted); }
28
+ .text { white-space: pre-wrap; word-break: break-word; }
29
+ .prompt .text { border-left: 3px solid #2563eb; padding-left: 8px; }
30
+ details { border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; }
31
+ details pre { white-space: pre-wrap; word-break: break-word; font-size: 12px; margin: 6px 0 2px; }
32
+ summary { cursor: pointer; font-size: 13px; }
33
+ .error summary { color: #dc2626; }
34
+ .note { color: var(--muted); font-style: italic; font-size: 13px; }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <header>
39
+ <h1 id="title">gestalt peer</h1>
40
+ <div class="warn">${VIEWER_WARNING}</div>
41
+ <div class="meta">
42
+ <span id="status">연결 중</span>
43
+ <span id="unknown"></span>
44
+ </div>
45
+ </header>
46
+ <main id="log"></main>
47
+ <script nonce="${nonce}">
48
+ (() => {
49
+ const log = document.getElementById('log');
50
+ const statusEl = document.getElementById('status');
51
+ const unknownEl = document.getElementById('unknown');
52
+ const titleEl = document.getElementById('title');
53
+ const params = new URLSearchParams(location.search);
54
+ const token = params.get('t') || '';
55
+ const base = location.pathname.replace(/\\/$/, '');
56
+ const results = new Map();
57
+
58
+ const el = (tag, cls, text) => {
59
+ const n = document.createElement(tag);
60
+ if (cls) n.className = cls;
61
+ if (text !== undefined) n.textContent = text;
62
+ return n;
63
+ };
64
+ const time = (at) => (at ? new Date(at).toLocaleTimeString() : '');
65
+ const atBottom = () => window.innerHeight + window.scrollY >= document.body.scrollHeight - 40;
66
+
67
+ const STATUS = {
68
+ live: '실시간',
69
+ missing: '대화 기록 파일이 안 보여요',
70
+ truncated: '대화 기록이 줄어들어서 멈췄어요',
71
+ offline: '주인 데몬이 오프라인이에요',
72
+ };
73
+ const CLOSED = {
74
+ removed: '명단에서 빠져 공유가 끊겼어요',
75
+ owner_removed: '주인이 명단에서 빠져 공유가 끊겼어요',
76
+ expired: '공유가 만료됐어요',
77
+ };
78
+
79
+ function render(ev) {
80
+ const box = el('div', 'ev ' + ev.kind);
81
+ if (ev.kind === 'prompt' || ev.kind === 'reply') {
82
+ box.append(el('div', 'who', (ev.kind === 'prompt' ? '주인' : '모델') + ' ' + time(ev.at)));
83
+ box.append(el('div', 'text', ev.text));
84
+ } else if (ev.kind === 'tool_call') {
85
+ const d = el('details');
86
+ d.append(el('summary', '', '도구 ' + ev.name + ' ' + time(ev.at)));
87
+ if (ev.input !== undefined) d.append(el('pre', '', ev.input));
88
+ const slot = el('div');
89
+ d.append(slot);
90
+ if (ev.id) results.set(ev.id, slot);
91
+ box.append(d);
92
+ } else if (ev.kind === 'tool_result') {
93
+ const d = el('details', ev.isError ? 'error' : '');
94
+ const label = ev.hidden ? '결과 (주인이 숨김)' : ev.isError ? '결과 (에러)' : '결과';
95
+ d.append(el('summary', '', label + (ev.truncated ? ' · 앞부분만' : '')));
96
+ if (!ev.hidden) d.append(el('pre', '', ev.text || ''));
97
+ const slot = ev.toolUseId ? results.get(ev.toolUseId) : undefined;
98
+ if (slot) { slot.append(d); return; }
99
+ box.append(d);
100
+ } else if (ev.kind === 'title') {
101
+ titleEl.textContent = ev.text;
102
+ document.title = ev.text + ' · gestalt peer';
103
+ return;
104
+ } else if (ev.kind === 'sidechain') {
105
+ box.append(el('div', 'note', '서브에이전트 실행 중'));
106
+ } else if (ev.kind === 'compacted') {
107
+ box.append(el('div', 'note', '이전 대화가 압축됐어요'));
108
+ } else {
109
+ return;
110
+ }
111
+ log.append(box);
112
+ }
113
+
114
+ const eventsUrl = base + '/events?t=' + encodeURIComponent(token);
115
+ const es = new EventSource(eventsUrl);
116
+ es.addEventListener('line', (m) => {
117
+ const stick = atBottom();
118
+ render(JSON.parse(m.data));
119
+ if (stick) window.scrollTo(0, document.body.scrollHeight);
120
+ });
121
+ es.addEventListener('trimmed', (m) => {
122
+ const n = JSON.parse(m.data).dropped;
123
+ log.append(el('div', 'ev note', '메모리 상한에 걸려 앞부분 ' + n + '개를 생략했어요'));
124
+ });
125
+ es.addEventListener('stats', (m) => {
126
+ const n = JSON.parse(m.data).unknownLines;
127
+ unknownEl.textContent = n > 0 ? '모르는 줄 ' + n + '개 건너뜀' : '';
128
+ });
129
+ es.addEventListener('state', (m) => {
130
+ statusEl.textContent = STATUS[JSON.parse(m.data).status] || '';
131
+ });
132
+ es.addEventListener('closed', (m) => {
133
+ const closed = JSON.parse(m.data);
134
+ // 원격 공유의 reason은 주인 데몬이 보낸 글자다. 프로토타입 이름이 들어와도 안 걸리게 자기 키만 본다
135
+ statusEl.textContent = Object.hasOwn(CLOSED, closed.reason) ? CLOSED[closed.reason] : '공유가 종료됐어요';
136
+ const missed = closed.missed;
137
+ if (missed > 0) {
138
+ log.append(el('div', 'ev note', '공유가 종료돼 뒤쪽 ' + missed + '개는 못 받았어요'));
139
+ }
140
+ es.close();
141
+ });
142
+ // EventSource는 응답 코드를 안 알려 준다. 끊긴 이유가 뷰어 상한(429)인지 한 번 물어본다
143
+ es.onerror = () => {
144
+ if (es.readyState !== EventSource.CLOSED) {
145
+ statusEl.textContent = '다시 연결 중';
146
+ return;
147
+ }
148
+ statusEl.textContent = '연결이 끊겼어요';
149
+ const ctl = new AbortController();
150
+ fetch(eventsUrl, { signal: ctl.signal, cache: 'no-store' })
151
+ .then((r) => {
152
+ ctl.abort();
153
+ if (r.status === 429) {
154
+ statusEl.textContent = '보는 사람 수 상한(${maxViewers}명)에 걸려 못 붙었어요';
155
+ }
156
+ })
157
+ .catch(() => {});
158
+ };
159
+ })();
160
+ </script>
161
+ </body>
162
+ </html>
163
+ `;
164
+ }
165
+ //# sourceMappingURL=viewer-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer-html.js","sourceRoot":"","sources":["../../../src/peer/viewer-html.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,UAAkB;IAChE,OAAO;;;;;;;gBAOO,KAAK;;;;;;;;;;;;;;;;;;;;;;;sBAuBC,cAAc;;;;;;;iBAOnB,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+CA2GyB,UAAU;;;;;;;;;CASxD,CAAC;AACF,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { ShareFeed } from './feed.js';
2
+ export interface ViewerShare {
3
+ stream: ShareFeed;
4
+ token: string;
5
+ }
6
+ export interface ViewerHooks {
7
+ lookup(id: string): ViewerShare | undefined;
8
+ onConnect?(shareId: string): void;
9
+ onDisconnect?(shareId: string): void;
10
+ }
11
+ /**
12
+ * 공유 하나에 동시에 붙는 뷰어 상한. 넘으면 429. 뷰어 하나가 붙잡는 메모리는 소켓 버퍼로
13
+ * 묶여 있지만 연결마다 소켓과 타이머가 생기니 토큰을 아는 쪽이 끝없이 열지 못하게 한다.
14
+ * 사람이 보는 숫자로는 넉넉하다.
15
+ */
16
+ export declare const MAX_VIEWERS_PER_SHARE = 16;
17
+ /**
18
+ * 실시간 프레임을 쓰기 전에 소켓 버퍼에 이만큼 넘게 밀려 있으면 그 뷰어를 끊는다. 안 읽는
19
+ * 뷰어 하나가 데몬 메모리를 끝없이 붙잡지 못하게 한다. 이보다 큰 이벤트 하나는 들어가지만
20
+ * 소켓으로 다 빠지기 전에 다음 프레임이 오면 그때 끊는다. 앞부분은 소켓이 비는 만큼만 꺼내
21
+ * 보내서 여기에 안 쌓인다. 브라우저는 다시 붙어 Last-Event-ID부터 받는다.
22
+ */
23
+ export declare const MAX_LIVE_BACKLOG_BYTES: number;
24
+ /** 앞부분을 공유에서 한 번에 꺼내는 양. 소켓이 차면 그 자리에서 멈추므로 대략이면 된다 */
25
+ export declare const REPLAY_CHUNK_BYTES: number;
26
+ export interface ViewerServerOptions {
27
+ /** 테스트가 작게 준다 */
28
+ maxLiveBacklogBytes?: number;
29
+ }
30
+ /**
31
+ * 로컬 뷰어 HTTP 서버. `127.0.0.1`에만 붙는다.
32
+ *
33
+ * 같은 머신의 다른 프로세스나 브라우저에 열린 남의 웹페이지도 이 포트에 닿을 수 있다.
34
+ * 그래서 공유마다 토큰을 주소에 붙이고 없거나 틀리면 401이다. 모르는 공유 id도 401로
35
+ * 돌려준다 — 404와 401을 나누면 토큰 없이 공유 id가 살아 있는지 떠볼 수 있다.
36
+ *
37
+ * Routes:
38
+ * GET /s/:id?t=TOKEN → 뷰어 HTML
39
+ * GET /s/:id/events?t=TOKEN → SSE (line, trimmed, stats, state, closed). 뷰어 상한을 넘으면 429.
40
+ * closed의 missed는 따라잡는 중에 공유가 종료돼 못 보낸 이벤트 수
41
+ * * → 404
42
+ */
43
+ export declare class ViewerServer {
44
+ private readonly hooks;
45
+ private server;
46
+ private open;
47
+ private viewers;
48
+ private readonly maxLiveBacklogBytes;
49
+ constructor(hooks: ViewerHooks, options?: ViewerServerOptions);
50
+ start(port?: number): Promise<number>;
51
+ get port(): number | undefined;
52
+ stop(): Promise<void>;
53
+ private handle;
54
+ private hostAllowed;
55
+ private page;
56
+ private stream;
57
+ }
58
+ //# sourceMappingURL=viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer.d.ts","sourceRoot":"","sources":["../../../src/peer/viewer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,WAAW,CAAC;AAIvD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;IAC5C,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAQD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AAEtD,uDAAuD;AACvD,eAAO,MAAM,kBAAkB,QAAa,CAAC;AAE7C,MAAM,WAAW,mBAAmB;IAClC,iBAAiB;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;IAOrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IANxB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,IAAI,CAA6B;IACzC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;gBAG1B,KAAK,EAAE,WAAW,EACnC,OAAO,GAAE,mBAAwB;IAKnC,KAAK,CAAC,IAAI,SAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBhC,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAG7B;IAED,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBrB,OAAO,CAAC,MAAM;IAwBd,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,IAAI;IAkBZ,OAAO,CAAC,MAAM;CA4Ef"}