@ucsandman/legcli 0.9.0 → 0.11.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 (52) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +110 -11
  3. package/bin/leg.mjs +78 -15
  4. package/docs/ERRORS.md +187 -0
  5. package/docs/README.md +3 -1
  6. package/docs/ROADMAP-v2.md +24 -11
  7. package/docs/VOCABULARY.md +1 -0
  8. package/docs/adapters.md +93 -11
  9. package/docs/board-guide.md +20 -1
  10. package/docs/cli-contracts.md +50 -17
  11. package/docs/configuration.md +56 -5
  12. package/docs/history.md +172 -0
  13. package/docs/runtime-tap.md +156 -0
  14. package/fixtures/limits/grok/grok-balance-exhausted.json +11 -0
  15. package/fixtures/live/grok/cmd.txt +1 -1
  16. package/fixtures/live/grok/parsed.json +6 -3
  17. package/fixtures/live/grok/run.json +22 -10
  18. package/fixtures/verified.json +8 -1
  19. package/package.json +1 -1
  20. package/scripts/build-docs-site.mjs +11 -4
  21. package/scripts/probe.mjs +2 -1
  22. package/src/accounts.mjs +5 -2
  23. package/src/adapters/cli.mjs +130 -0
  24. package/src/adapters/custom.mjs +271 -0
  25. package/src/adapters/grok.mjs +51 -10
  26. package/src/adapters/index.mjs +34 -7
  27. package/src/attach.mjs +85 -13
  28. package/src/audit.mjs +118 -0
  29. package/src/board/audit.js +123 -0
  30. package/src/board/board.css +38 -1
  31. package/src/board/board.js +14 -2
  32. package/src/board/history.js +377 -0
  33. package/src/board/index.html +55 -0
  34. package/src/board/sessions.js +49 -7
  35. package/src/history/cli.mjs +159 -0
  36. package/src/history/common.mjs +119 -0
  37. package/src/history/index.mjs +429 -0
  38. package/src/history/providers/agy.mjs +91 -0
  39. package/src/history/providers/claude.mjs +161 -0
  40. package/src/history/providers/codex.mjs +133 -0
  41. package/src/history/providers/copilot.mjs +94 -0
  42. package/src/history/providers/grok.mjs +138 -0
  43. package/src/history/worktrees.mjs +116 -0
  44. package/src/redact.mjs +23 -5
  45. package/src/server.mjs +272 -28
  46. package/src/sessions.mjs +9 -0
  47. package/src/share.mjs +66 -6
  48. package/src/taps/claude.mjs +11 -4
  49. package/src/taps/grok.mjs +4 -0
  50. package/src/taps/mod.mjs +340 -0
  51. package/src/usage.mjs +21 -5
  52. package/src/worktree.mjs +1 -1
package/src/audit.mjs ADDED
@@ -0,0 +1,118 @@
1
+ // audit — one list of who did what on this board, across every terminal and
2
+ // every card, newest first.
3
+ //
4
+ // The ledger already names an actor on every event; until now you could only
5
+ // read that one session or one card at a time, which is no use when the
6
+ // question is "who landed that?" or "who handed my terminal off last night".
7
+ // Nothing new is recorded here: this reads what is already on disk.
8
+ //
9
+ // Owner only (src/server.mjs): the trail names repositories and people.
10
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+ import { home } from './store.mjs'
13
+ import { listSessions, readEvents as readSessionEvents } from './sessions.mjs'
14
+ import { readEvents as readCardEvents } from './ledger.mjs'
15
+
16
+ // The types worth a line in an audit: something a person or an agent DID, not
17
+ // the running commentary. A `status` line is commentary; a hand-off is not.
18
+ export const AUDITED = [
19
+ 'handoff', 'handoff_requested', 'handed_off', 'landed', 'land', 'bounced', 'killed',
20
+ 'approved', 'approval_needed', 'reassigned', 'paused', 'resumed', 'ended', 'done',
21
+ 'failed', 'trust', 'harness', 'worktree', 'station_done', 'leg_started', 'rerun',
22
+ ]
23
+
24
+ export const ACTOR_KINDS = ['human', 'agent', 'leg']
25
+
26
+ function actorOf(ev) {
27
+ // a card event carries a structured actor; a session event carries `by`
28
+ if (ev.actor && typeof ev.actor === 'object') {
29
+ if (ev.actor.type === 'human') return { kind: 'human', name: String(ev.actor.id ?? 'unknown') }
30
+ if (ev.actor.type === 'agent') return { kind: 'agent', name: String(ev.actor.adapter ?? 'agent') }
31
+ return { kind: 'leg', name: 'leg' }
32
+ }
33
+ if (ev.by) return { kind: 'human', name: String(ev.by) }
34
+ return { kind: 'leg', name: 'leg' }
35
+ }
36
+
37
+ function cardIds() {
38
+ const root = join(home(), 'cards')
39
+ if (!existsSync(root)) return []
40
+ try { return readdirSync(root).filter((d) => existsSync(join(root, d, 'card.json'))) } catch { return [] }
41
+ }
42
+
43
+ function cardMeta(id) {
44
+ try { return JSON.parse(readFileSync(join(home(), 'cards', id, 'card.json'), 'utf8')) } catch { return null }
45
+ }
46
+
47
+ // → { entries: [...], truncated, scanned: { sessions, cards, events } }
48
+ // `scanned` is on the record deliberately: an empty audit from a board that
49
+ // looked at nothing reads exactly like a quiet week, and the two are not the
50
+ // same thing.
51
+ export function auditTrail({ limit = 200, since = null, who = null, kind = null, types = null } = {}) {
52
+ const sinceMs = since ? Date.parse(since) : null
53
+ const wanted = Array.isArray(types) && types.length ? new Set(types) : new Set(AUDITED)
54
+ const rows = []
55
+ let events = 0
56
+
57
+ const sessions = listSessions()
58
+ for (const s of sessions) {
59
+ for (const ev of readSessionEvents(s.session_id)) {
60
+ events++
61
+ if (!wanted.has(ev.type)) continue
62
+ const at = Date.parse(ev.ts)
63
+ if (sinceMs && Number.isFinite(at) && at < sinceMs) continue
64
+ const actor = actorOf(ev)
65
+ rows.push({
66
+ at: ev.ts,
67
+ who: actor.name,
68
+ kind: actor.kind,
69
+ what: ev.type,
70
+ summary: String(ev.summary ?? ''),
71
+ where: 'terminal',
72
+ id: s.session_id,
73
+ agent: s.agent ?? null,
74
+ repo: s.repo ?? null,
75
+ branch: s.branch ?? null,
76
+ })
77
+ }
78
+ }
79
+
80
+ const ids = cardIds()
81
+ for (const id of ids) {
82
+ const card = cardMeta(id)
83
+ for (const ev of readCardEvents(id)) {
84
+ events++
85
+ if (!wanted.has(ev.type)) continue
86
+ const at = Date.parse(ev.ts)
87
+ if (sinceMs && Number.isFinite(at) && at < sinceMs) continue
88
+ const actor = actorOf(ev)
89
+ rows.push({
90
+ at: ev.ts,
91
+ who: actor.name,
92
+ kind: actor.kind,
93
+ what: ev.type,
94
+ summary: String(ev.summary ?? ''),
95
+ where: 'card',
96
+ id,
97
+ agent: ev.actor?.type === 'agent' ? ev.actor.adapter : null,
98
+ repo: card?.repo ?? null,
99
+ branch: card?.branch ?? null,
100
+ })
101
+ }
102
+ }
103
+
104
+ let filtered = rows
105
+ if (who) filtered = filtered.filter((r) => r.who.toLowerCase() === String(who).toLowerCase())
106
+ if (kind) filtered = filtered.filter((r) => r.kind === kind)
107
+ filtered.sort((a, b) => (Date.parse(b.at) || 0) - (Date.parse(a.at) || 0))
108
+ const capped = filtered.slice(0, Math.max(1, Math.min(limit, 1000)))
109
+
110
+ return {
111
+ entries: capped,
112
+ truncated: filtered.length > capped.length,
113
+ matched: filtered.length,
114
+ // L2: a verdict carries the volume it processed
115
+ scanned: { sessions: sessions.length, cards: ids.length, events },
116
+ people: [...new Set(rows.filter((r) => r.kind === 'human').map((r) => r.who))].sort(),
117
+ }
118
+ }
@@ -0,0 +1,123 @@
1
+ // Audit trail: who did what, across every terminal and every card on this
2
+ // board. Data: /api/audit, which is owner-only — the trail names repositories
3
+ // and the people on the roster.
4
+ //
5
+ // It loads when the reader asks, never on a board render: the trail reads every
6
+ // session's events and every card's ledger, and that is not work to do on a
7
+ // timer behind a panel nobody has opened. Like the history cell, nothing here
8
+ // arrives over SSE (DESIGN.md rule 3: the live terminals lane is the only live
9
+ // region).
10
+ //
11
+ // `el`, `api`, `getToken`, `ago` and `whenAgo` are copied from sessions.js,
12
+ // which cannot export from its IIFE. The time grammar is sessions.js's: a
13
+ // change to ago() there is a change here in the same commit.
14
+ (function () {
15
+ 'use strict'
16
+
17
+ function getToken() { return localStorage.getItem('legToken') || localStorage.getItem('batonToken') || '' }
18
+ async function api(path, opts = {}) {
19
+ const headers = { 'Content-Type': 'application/json' }
20
+ const token = getToken()
21
+ if (token) headers.Authorization = `Bearer ${token}`
22
+ const r = await fetch(path, { method: opts.method || 'GET', headers, body: opts.body ? JSON.stringify(opts.body) : undefined })
23
+ const data = await r.json().catch(() => ({}))
24
+ if (!r.ok) throw new Error(data.error || `${r.status} ${r.statusText}`)
25
+ return data
26
+ }
27
+ function el(tag, attrs, children) {
28
+ const n = document.createElement(tag)
29
+ for (const [k, v] of Object.entries(attrs || {})) { if (v === null || v === undefined) continue; if (k === 'class') n.className = v; else n.setAttribute(k, v) }
30
+ for (const c of children || []) { if (c === null || c === undefined) continue; n.appendChild(typeof c === 'string' ? document.createTextNode(c) : c) }
31
+ return n
32
+ }
33
+ function ago(ms) {
34
+ const s = Math.max(0, Math.floor(ms / 1000))
35
+ if (s < 60) return `${s}s`
36
+ const m = Math.floor(s / 60)
37
+ if (m < 60) return `${m}m`
38
+ const h = Math.floor(m / 60)
39
+ if (h < 48) return `${h}h${m % 60 ? ` ${m % 60}m` : ''}`
40
+ const d = Math.floor(h / 24)
41
+ return `${d}d${h % 24 ? ` ${h % 24}h` : ''}`
42
+ }
43
+ function whenAgo(ts) {
44
+ const t = Date.parse(ts)
45
+ return Number.isFinite(t) ? `${ago(Date.now() - t)} ago` : ''
46
+ }
47
+ function clockAt(ms) {
48
+ const d = new Date(ms)
49
+ if (!Number.isFinite(d.getTime())) return ''
50
+ return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })
51
+ }
52
+
53
+ const root = document.getElementById('audit')
54
+ if (!root) return
55
+ const whoSel = document.getElementById('audit-who')
56
+ const kindSel = document.getElementById('audit-kind')
57
+ const loadBtn = document.getElementById('audit-load')
58
+ const meta = document.getElementById('audit-meta')
59
+ const list = document.getElementById('audit-list')
60
+ let knownPeople = null
61
+
62
+ // A guest or an operator never sees this panel; the fieldset stays hidden
63
+ // until /api/audit answers, so a 403 leaves nothing on the page to click.
64
+ async function reveal() {
65
+ try {
66
+ const r = await api('/api/audit?limit=1')
67
+ root.hidden = false
68
+ knownPeople = r.people || []
69
+ fillPeople()
70
+ } catch { root.hidden = true }
71
+ }
72
+
73
+ function fillPeople() {
74
+ if (!knownPeople) return
75
+ const current = whoSel.value
76
+ while (whoSel.options.length > 1) whoSel.remove(1)
77
+ for (const p of knownPeople) whoSel.appendChild(el('option', { value: p }, [p]))
78
+ whoSel.value = current
79
+ }
80
+
81
+ function row(e) {
82
+ const line = el('div', { class: 'audit-row' })
83
+ line.appendChild(el('span', { class: 'audit-when', title: e.at }, [clockAt(Date.parse(e.at))]))
84
+ line.appendChild(el('span', { class: `chip chip-id-${e.kind === 'human' ? 'human' : (e.agent || 'leg')}` }, [e.who]))
85
+ line.appendChild(el('span', { class: 'audit-what' }, [e.what.replace(/_/g, ' ')]))
86
+ line.appendChild(el('span', { class: 'audit-summary' }, [e.summary || '']))
87
+ const where = e.where === 'card' ? `card ${e.id}` : `terminal ${String(e.id).split('-').pop()}`
88
+ line.appendChild(el('span', { class: 'chip' }, [where]))
89
+ if (e.repo) line.appendChild(el('span', { class: 'chip' }, [String(e.repo).split(/[\\/]/).pop() + (e.branch ? `@${e.branch}` : '')]))
90
+ line.appendChild(el('span', { class: 'audit-ago' }, [whenAgo(e.at)]))
91
+ return line
92
+ }
93
+
94
+ async function load() {
95
+ loadBtn.disabled = true
96
+ meta.textContent = 'reading every terminal and every card…'
97
+ list.replaceChildren()
98
+ try {
99
+ const q = new URLSearchParams({ limit: '200' })
100
+ if (whoSel.value) q.set('who', whoSel.value)
101
+ if (kindSel.value) q.set('kind', kindSel.value)
102
+ const r = await api(`/api/audit?${q}`)
103
+ knownPeople = r.people || knownPeople
104
+ fillPeople()
105
+ // L2: the verdict carries the volume it processed, so an empty trail from
106
+ // a board that looked at nothing does not read like a quiet week.
107
+ const scanned = `${r.scanned.sessions} terminal${r.scanned.sessions === 1 ? '' : 's'} and ${r.scanned.cards} card${r.scanned.cards === 1 ? '' : 's'}, ${r.scanned.events} events read`
108
+ meta.textContent = r.entries.length
109
+ ? `${r.matched} action${r.matched === 1 ? '' : 's'}${r.truncated ? `, newest ${r.entries.length} shown` : ''} — ${scanned}`
110
+ : `nothing matched — ${scanned}`
111
+ for (const e of r.entries) list.appendChild(row(e))
112
+ } catch (err) {
113
+ meta.textContent = err.message
114
+ } finally {
115
+ loadBtn.disabled = false
116
+ }
117
+ }
118
+
119
+ loadBtn.addEventListener('click', load)
120
+ whoSel.addEventListener('change', load)
121
+ kindSel.addEventListener('change', load)
122
+ reveal()
123
+ })()
@@ -130,6 +130,7 @@
130
130
  --id-codex: #69DBBA; /* oklch(0.815 0.115 172) cool green */
131
131
  --id-agy: #CC97F3; /* oklch(0.760 0.140 310) violet, 46 degrees clear of the accent */
132
132
  --id-grok: #70B8FF; /* oklch(0.750 0.130 240) azure blue */
133
+ --id-copilot: #F0A3B8; /* oklch(0.790 0.110 5) rose; lists and reads, never runs */
133
134
  --id-fake: #A0A6AE; /* oklch(0.720 0.010 258) the scripted adapter, neutral in hue */
134
135
 
135
136
  /* type scale. Fixed rem, range 13 to 52. The old board ran 13 to 21, which
@@ -222,6 +223,7 @@ code { font-family: var(--mono); font-size: 0.94em; }
222
223
  .dot.id-codex { background: var(--id-codex); }
223
224
  .dot.id-agy { background: var(--id-agy); }
224
225
  .dot.id-grok { background: var(--id-grok); }
226
+ .dot.id-copilot { background: var(--id-copilot); }
225
227
 
226
228
  /* ---- logins ------------------------------------------------------------- */
227
229
  .logins { display: grid; gap: 20px; }
@@ -323,6 +325,7 @@ code { font-family: var(--mono); font-size: 0.94em; }
323
325
  .chip-id-codex { color: var(--id-codex); }
324
326
  .chip-id-agy { color: var(--id-agy); }
325
327
  .chip-id-grok { color: var(--id-grok); }
328
+ .chip-id-copilot { color: var(--id-copilot); }
326
329
  .chip-id-fake { color: var(--id-fake); }
327
330
  .chip-state-ok { color: var(--ok-text); }
328
331
  .chip-state-warn { color: var(--warn-text); }
@@ -370,7 +373,7 @@ code { font-family: var(--mono); font-size: 0.94em; }
370
373
  .drawer-msg-when { font-size: var(--t--1); color: var(--text-3); font-family: var(--mono); }
371
374
 
372
375
  /* ---- ledger: on the ground, unpanelled --------------------------------- */
373
- .ledger { margin-top: 56px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px; padding-top: 32px; border-top: 1px solid var(--edge); }
376
+ .ledger { margin-top: 56px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 40px; padding-top: 32px; border-top: 1px solid var(--edge); }
374
377
  .ledger-cell h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
375
378
  .ledger-cell .region-meta { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); }
376
379
  .ledger-actions { margin-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
@@ -384,6 +387,38 @@ code { font-family: var(--mono); font-size: 0.94em; }
384
387
  .line-main { color: var(--text); }
385
388
  .line-meta { color: var(--text-2); }
386
389
  .line-when { color: var(--text-3); text-align: right; }
390
+ /* history: the conversations every agent keeps, one row each, opening in place */
391
+ .history-filters { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px 14px; align-items: center; margin: 4px 0 18px; max-width: 64ch; }
392
+ .history-filters label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
393
+ .history-filters label:has(input[type="checkbox"]) { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; font-weight: var(--w-text); }
394
+ /* Audit trail (Settings). Same grammar as the history rows: a flat list with
395
+ hairline separators, the time on the left, the actor as a chip. */
396
+ .audit { border: 0; padding: 0; margin: 22px 0 0; }
397
+ .audit legend { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); padding: 0; }
398
+ .audit-filters { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; margin: 10px 0 14px; }
399
+ .audit-filters label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
400
+ .audit-list { display: block; }
401
+ .audit-row { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: baseline; padding: 10px 0; font-size: var(--t-0); }
402
+ .audit-row + .audit-row { border-top: 1px solid var(--line); }
403
+ .audit-when { color: var(--text-3); font-variant-numeric: tabular-nums; min-width: 12ch; }
404
+ .audit-what { font-weight: var(--w-head); }
405
+ .audit-summary { color: var(--text-2); flex: 1 1 46ch; min-width: 0; overflow-wrap: anywhere; }
406
+ .audit-ago { color: var(--text-3); margin-left: auto; }
407
+ @media (max-width: 640px) { .audit-ago { margin-left: 0; } }
408
+
409
+ .history-list { display: block; }
410
+ .history-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 20px; align-items: baseline; padding: 12px 0; font-size: var(--t-0); }
411
+ .history-row + .history-row, .history-row + .history-detail, .history-detail + .history-row { border-top: 1px solid var(--line); }
412
+ .history-title { display: block; width: 100%; text-align: left; background: none; border: 0; padding: 0; font: inherit; color: var(--text); cursor: pointer; line-height: 1.4; }
413
+ .history-title:hover { color: var(--accent-hi); }
414
+ .history-title:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-control); }
415
+ .history-register { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
416
+ .history-when { color: var(--text-3); white-space: nowrap; font-size: var(--t--1); }
417
+ .history-detail { padding: 14px 0 20px; }
418
+ .history-detail .kv { margin-bottom: 14px; }
419
+ .history-command { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 12px; font-size: var(--t-0); }
420
+ .history-empty { font-size: var(--t-0); color: var(--text-3); padding: 12px 0; }
421
+
387
422
  .group-head { display: flex; align-items: baseline; gap: 10px; margin-top: 24px; margin-bottom: 4px; font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
388
423
  .group-head:first-child { margin-top: 0; }
389
424
  .group-head span { font-weight: var(--w-text); color: var(--text-3); }
@@ -556,6 +591,8 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
556
591
  .kv { grid-template-columns: 1fr; gap: 4px 0; }
557
592
  .kv-val + .kv-key { margin-top: 12px; }
558
593
  .ledger { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
594
+ .history-row { grid-template-columns: 1fr; }
595
+ .history-filters { grid-template-columns: 1fr; }
559
596
  .line { grid-template-columns: 1fr auto; gap: 6px 14px; }
560
597
  .line-main { grid-column: 1 / -1; }
561
598
  .line-meta { grid-column: 1; }
@@ -292,6 +292,15 @@
292
292
  : `${s === 'reconnecting' ? 'Reconnecting' : 'Connecting'} to Leg on ${state.bind}.`
293
293
  }
294
294
 
295
+ // sessions.js owns the terminals region and listens for `leg:sessions`.
296
+ // `baton:sessions` is the old name, still emitted for anything outside this
297
+ // page that listens for it; nothing in the board may listen for both, because
298
+ // each listener rebuilds the whole grid.
299
+ function publishSessions(detail) {
300
+ window.dispatchEvent(new CustomEvent('leg:sessions', { detail }))
301
+ window.dispatchEvent(new CustomEvent('baton:sessions', { detail }))
302
+ }
303
+
295
304
  function connectSse() {
296
305
  if (state.es) { try { state.es.close() } catch { /* ignore */ } }
297
306
  const request = ++state.sseRequest
@@ -312,9 +321,12 @@
312
321
  // nothing between the drop and this hello was replayed: an open detail
313
322
  // region is as old as the gap
314
323
  scheduleDrawerRefresh()
315
- if (data.sessions) window.dispatchEvent(new CustomEvent('leg:sessions', { detail: data.sessions })); window.dispatchEvent(new CustomEvent('baton:sessions', { detail: data.sessions }));
324
+ if (data.sessions) publishSessions(data.sessions)
316
325
  })
317
- es.addEventListener('sessions', (e) => { if (state.es === es && request === state.sseRequest) window.dispatchEvent(new CustomEvent('leg:sessions', { detail: JSON.parse(e.data) })); window.dispatchEvent(new CustomEvent('baton:sessions', { detail: JSON.parse(e.data) })); })
326
+ // one parse, one publish, and both inside the staleness guard: the missing
327
+ // braces meant a superseded EventSource still drove a full rebuild, and the
328
+ // payload (a quarter of a megabyte) was parsed twice to do it
329
+ es.addEventListener('sessions', (e) => { if (state.es === es && request === state.sseRequest) publishSessions(JSON.parse(e.data)) })
318
330
  es.addEventListener('card', (e) => { if (state.es === es && request === state.sseRequest) upsertCard(JSON.parse(e.data)) })
319
331
  es.addEventListener('removed', (e) => { if (state.es === es && request === state.sseRequest) dropCard(JSON.parse(e.data).card_id) })
320
332
  es.addEventListener('event', (e) => { if (state.es === es && request === state.sseRequest) onLedgerEvent(JSON.parse(e.data)) })