@oimdb/mcp 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "MCP server and CDP inspector for OIMDB — connects AI assistants to live browser state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
package/ui/app.css CHANGED
@@ -13,6 +13,7 @@
13
13
  --accent: #4fc3f7;
14
14
  --green: #4caf50;
15
15
  --yellow: #ffb74d;
16
+ --red: #ef5350;
16
17
  --gray: #616161;
17
18
  --tag-bg: #2d3f4a;
18
19
  --tag-warn: #4a2d2d;
@@ -58,9 +59,7 @@ html, body {
58
59
 
59
60
  #conn-label { font-size: 12px; color: var(--text-dim); }
60
61
 
61
- .toolbar-actions {
62
- display: flex; align-items: center; gap: 10px; margin-left: auto;
63
- }
62
+ .toolbar-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
64
63
  #last-updated { font-size: 11px; color: var(--text-dim); }
65
64
 
66
65
  #btn-refresh {
@@ -89,9 +88,7 @@ html, body {
89
88
  background: var(--bg-panel); border: 1px solid var(--border);
90
89
  border-radius: 8px; text-align: center;
91
90
  }
92
- .waiting-title {
93
- font-size: 18px; font-weight: 600; color: var(--accent); margin-bottom: 14px;
94
- }
91
+ .waiting-title { font-size: 18px; font-weight: 600; color: var(--accent); margin-bottom: 14px; }
95
92
  .waiting-card p { color: var(--text-dim); margin-bottom: 10px; line-height: 1.6; }
96
93
  .waiting-card code {
97
94
  font-family: var(--mono); background: var(--bg-input);
@@ -119,6 +116,20 @@ html, body {
119
116
  }
120
117
  .col:last-child { border-right: none; }
121
118
 
119
+ /* Right column split into computeds + history */
120
+ #col-right { flex-direction: column; }
121
+
122
+ .col-section {
123
+ display: flex; flex-direction: column;
124
+ flex: 1; min-height: 0; overflow: hidden;
125
+ border-bottom: 1px solid var(--border);
126
+ }
127
+ .col-section:last-child { border-bottom: none; }
128
+
129
+ .col-section-history { flex: 0 0 220px; }
130
+
131
+ .col-body-flex { flex: 1; min-height: 0; overflow-y: auto; }
132
+
122
133
  .col-header {
123
134
  display: flex; align-items: center; gap: 7px;
124
135
  height: 32px; padding: 0 14px; flex-shrink: 0;
@@ -160,7 +171,7 @@ html, body {
160
171
  .c-count { font-size: 11px; color: var(--text-dim); }
161
172
 
162
173
  .c-detail {
163
- display: none; padding: 8px 14px 10px 32px;
174
+ display: none; padding: 8px 14px 0 32px;
164
175
  background: var(--bg); border-top: 1px solid var(--border);
165
176
  font-size: 11px; font-family: var(--mono);
166
177
  }
@@ -180,6 +191,56 @@ html, body {
180
191
  .tag-warn { background: var(--tag-warn); color: #ffaa80; }
181
192
  .tag-rel-arrow { color: var(--accent); margin: 0 3px; }
182
193
 
194
+ /* ── Entity table ─────────────────────────────── */
195
+
196
+ .entity-wrap {
197
+ margin: 8px 0 0 -18px;
198
+ border-top: 1px solid var(--border);
199
+ padding: 8px 0 10px 0;
200
+ }
201
+
202
+ .entity-table-wrap {
203
+ overflow-x: auto;
204
+ max-height: 280px;
205
+ overflow-y: auto;
206
+ }
207
+
208
+ .entity-table {
209
+ width: 100%;
210
+ border-collapse: collapse;
211
+ font-size: 11px;
212
+ font-family: var(--mono);
213
+ }
214
+
215
+ .entity-table th {
216
+ position: sticky; top: 0;
217
+ background: var(--bg-panel);
218
+ color: var(--text-dim);
219
+ font-weight: 600;
220
+ text-align: left;
221
+ padding: 4px 10px;
222
+ border-bottom: 1px solid var(--border);
223
+ white-space: nowrap;
224
+ }
225
+
226
+ .entity-table td {
227
+ padding: 3px 10px;
228
+ border-bottom: 1px solid rgba(255,255,255,0.04);
229
+ color: var(--text);
230
+ white-space: nowrap;
231
+ max-width: 200px;
232
+ overflow: hidden;
233
+ text-overflow: ellipsis;
234
+ }
235
+
236
+ .entity-table tbody tr:hover td { background: var(--bg-hover); }
237
+
238
+ .entity-note {
239
+ padding: 4px 10px;
240
+ font-size: 10px;
241
+ color: var(--text-dim);
242
+ }
243
+
183
244
  /* ── Computed rows ────────────────────────────── */
184
245
 
185
246
  .comp-row {
@@ -190,9 +251,7 @@ html, body {
190
251
 
191
252
  .comp-top { display: flex; align-items: center; gap: 8px; }
192
253
 
193
- .status-dot {
194
- width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
195
- }
254
+ .status-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
196
255
  .status-dot.fresh { background: var(--green); }
197
256
  .status-dot.stale { background: var(--yellow); }
198
257
  .status-dot.pending { background: var(--gray); }
@@ -223,9 +282,31 @@ html, body {
223
282
  }
224
283
  .comp-deps-label { font-size: 11px; color: var(--text-dim); margin-right: 4px; }
225
284
 
285
+ /* ── Flush history ────────────────────────────── */
286
+
287
+ .flush-row {
288
+ display: flex; align-items: center; gap: 8px;
289
+ padding: 4px 14px; border-bottom: 1px solid rgba(255,255,255,0.04);
290
+ font-size: 11px; font-family: var(--mono);
291
+ }
292
+ .flush-row:hover { background: var(--bg-hover); }
293
+
294
+ .flush-time { color: var(--text-dim); flex-shrink: 0; }
295
+
296
+ .flush-tags { display: flex; flex-wrap: wrap; gap: 4px; }
297
+
298
+ .flush-tag {
299
+ padding: 1px 6px; border-radius: 3px;
300
+ background: var(--bg-input); color: var(--text-dim);
301
+ font-size: 10px;
302
+ }
303
+ .flush-add { background: #1a3a1c; color: #81c784; }
304
+ .flush-del { background: #3a1a1a; color: #ef9a9a; }
305
+ .flush-no-change { color: var(--text-dim); background: none; }
306
+
226
307
  /* ── Scrollbar ────────────────────────────────── */
227
308
 
228
- ::-webkit-scrollbar { width: 6px; }
309
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
229
310
  ::-webkit-scrollbar-track { background: var(--bg); }
230
311
  ::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
231
312
  ::-webkit-scrollbar-thumb:hover { background: #555; }
@@ -234,5 +315,9 @@ html, body {
234
315
 
235
316
  .empty {
236
317
  padding: 20px 16px; color: var(--text-dim);
237
- font-size: 12px; text-align: center;
318
+ font-size: 12px; text-align: center; line-height: 1.7;
319
+ }
320
+ .empty code {
321
+ font-family: var(--mono); background: var(--bg-input);
322
+ padding: 1px 5px; border-radius: 3px; color: var(--accent);
238
323
  }
package/ui/app.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // ── State ──────────────────────────────────────────────────────
4
4
 
5
- let autoTimer = null;
6
- let expanded = new Set();
5
+ let autoTimer = null;
6
+ let expanded = new Set();
7
7
 
8
8
  // ── Elements ───────────────────────────────────────────────────
9
9
 
@@ -16,6 +16,7 @@ const badgeCollections = document.getElementById('badge-collections');
16
16
  const badgeComputeds = document.getElementById('badge-computeds');
17
17
  const listCollections = document.getElementById('list-collections');
18
18
  const listComputeds = document.getElementById('list-computeds');
19
+ const listHistory = document.getElementById('list-history');
19
20
  const btnRefresh = document.getElementById('btn-refresh');
20
21
  const chkAuto = document.getElementById('chk-auto');
21
22
 
@@ -23,7 +24,8 @@ const chkAuto = document.getElementById('chk-auto');
23
24
 
24
25
  async function poll() {
25
26
  try {
26
- const res = await fetch('/api/inspect');
27
+ const base = location.origin !== 'null' ? location.origin : '';
28
+ const res = await fetch(base + '/api/inspect');
27
29
  const json = await res.json();
28
30
 
29
31
  if (!json.connected || !json.data) {
@@ -43,7 +45,7 @@ async function poll() {
43
45
  // ── Layout ──────────────────────────────────────────────────────
44
46
 
45
47
  function showWaiting() {
46
- connDot.className = 'conn-dot disconnected';
48
+ connDot.className = 'conn-dot disconnected';
47
49
  connLabel.textContent = 'Waiting for browser…';
48
50
  screenWaiting.classList.remove('hidden');
49
51
  screenMain.classList.add('hidden');
@@ -56,7 +58,7 @@ function showMain() {
56
58
 
57
59
  function setConnected(tab) {
58
60
  connDot.className = 'conn-dot connected';
59
- connLabel.textContent = tab ? `${tab.title}` : 'Connected';
61
+ connLabel.textContent = tab ? tab.title : 'Connected';
60
62
  }
61
63
 
62
64
  // ── Render ──────────────────────────────────────────────────────
@@ -64,12 +66,14 @@ function setConnected(tab) {
64
66
  function render(state) {
65
67
  const collections = state.collections || {};
66
68
  const computeds = state.computeds || {};
69
+ const history = state.history || [];
67
70
 
68
71
  badgeCollections.textContent = Object.keys(collections).length;
69
72
  badgeComputeds.textContent = Object.keys(computeds).length;
70
73
 
71
74
  renderCollections(collections);
72
75
  renderComputeds(computeds);
76
+ renderHistory(history);
73
77
  }
74
78
 
75
79
  // ── Collections ─────────────────────────────────────────────────
@@ -109,7 +113,11 @@ function buildCollRow(name, info) {
109
113
 
110
114
  const detail = document.createElement('div');
111
115
  detail.className = 'c-detail' + (isOpen ? ' open' : '');
112
- detail.innerHTML = buildCollDetail(info);
116
+ detail.innerHTML = buildCollMeta(info);
117
+
118
+ // Entity table
119
+ const table = buildEntityTable(info);
120
+ if (table) detail.appendChild(table);
113
121
 
114
122
  summary.addEventListener('click', () => {
115
123
  const opening = detail.classList.toggle('open');
@@ -122,7 +130,7 @@ function buildCollRow(name, info) {
122
130
  return row;
123
131
  }
124
132
 
125
- function buildCollDetail(info) {
133
+ function buildCollMeta(info) {
126
134
  const parts = [];
127
135
 
128
136
  if (info.sampleEntity && typeof info.sampleEntity === 'object') {
@@ -152,7 +160,79 @@ function buildCollDetail(info) {
152
160
  parts.push(drow('desc', `<span style="color:var(--text-dim)">${esc(info.description)}</span>`));
153
161
  }
154
162
 
155
- return parts.join('') || '<div class="d-row"><span style="color:var(--text-dim);font-size:11px">No details</span></div>';
163
+ return parts.join('') || '';
164
+ }
165
+
166
+ // ── Entity table ────────────────────────────────────────────────
167
+
168
+ const MAX_ENTITY_ROWS = 200;
169
+
170
+ function buildEntityTable(info) {
171
+ const entities = info.entities;
172
+ if (!Array.isArray(entities) || entities.length === 0) return null;
173
+
174
+ const first = entities[0];
175
+ if (!first || typeof first !== 'object') return null;
176
+ const cols = Object.keys(first);
177
+ if (cols.length === 0) return null;
178
+
179
+ const shown = entities.slice(0, MAX_ENTITY_ROWS);
180
+
181
+ const wrap = document.createElement('div');
182
+ wrap.className = 'entity-wrap';
183
+
184
+ const tableWrap = document.createElement('div');
185
+ tableWrap.className = 'entity-table-wrap';
186
+
187
+ const table = document.createElement('table');
188
+ table.className = 'entity-table';
189
+
190
+ // Head
191
+ const thead = document.createElement('thead');
192
+ const headRow = document.createElement('tr');
193
+ cols.forEach(col => {
194
+ const th = document.createElement('th');
195
+ th.textContent = col;
196
+ headRow.appendChild(th);
197
+ });
198
+ thead.appendChild(headRow);
199
+ table.appendChild(thead);
200
+
201
+ // Body
202
+ const tbody = document.createElement('tbody');
203
+ shown.forEach(entity => {
204
+ const tr = document.createElement('tr');
205
+ cols.forEach(col => {
206
+ const td = document.createElement('td');
207
+ const val = (entity)[col];
208
+ td.textContent = fmtCell(val);
209
+ td.title = JSON.stringify(val);
210
+ tr.appendChild(td);
211
+ });
212
+ tbody.appendChild(tr);
213
+ });
214
+ table.appendChild(tbody);
215
+ tableWrap.appendChild(table);
216
+ wrap.appendChild(tableWrap);
217
+
218
+ if (entities.length > MAX_ENTITY_ROWS) {
219
+ const note = document.createElement('div');
220
+ note.className = 'entity-note';
221
+ note.textContent = `showing ${MAX_ENTITY_ROWS} of ${entities.length}`;
222
+ wrap.appendChild(note);
223
+ }
224
+
225
+ return wrap;
226
+ }
227
+
228
+ function fmtCell(v) {
229
+ if (v === null || v === undefined) return '—';
230
+ if (typeof v === 'object') {
231
+ const s = JSON.stringify(v);
232
+ return s.length > 40 ? s.slice(0, 40) + '…' : s;
233
+ }
234
+ const s = String(v);
235
+ return s.length > 60 ? s.slice(0, 60) + '…' : s;
156
236
  }
157
237
 
158
238
  function drow(label, content) {
@@ -240,6 +320,62 @@ function buildCompRow(name, info) {
240
320
  return row;
241
321
  }
242
322
 
323
+ // ── Flush History ───────────────────────────────────────────────
324
+
325
+ function renderHistory(history) {
326
+ if (!history.length) {
327
+ listHistory.innerHTML = '<div class="empty">No flushes recorded yet.<br>Call <code>registry.trackFlushes(...)</code> in debug.ts.</div>';
328
+ return;
329
+ }
330
+ listHistory.innerHTML = '';
331
+ history.forEach((record, i) => {
332
+ const prev = history[i + 1];
333
+ listHistory.appendChild(buildFlushRow(record, prev));
334
+ });
335
+ }
336
+
337
+ function buildFlushRow(record, prev) {
338
+ const row = document.createElement('div');
339
+ row.className = 'flush-row';
340
+
341
+ const time = document.createElement('span');
342
+ time.className = 'flush-time';
343
+ time.textContent = new Date(record.time).toLocaleTimeString('en', {
344
+ hour12: false,
345
+ hour: '2-digit',
346
+ minute: '2-digit',
347
+ second: '2-digit',
348
+ fractionalSecondDigits: 3,
349
+ });
350
+ row.appendChild(time);
351
+
352
+ const tags = document.createElement('span');
353
+ tags.className = 'flush-tags';
354
+
355
+ for (const [name, count] of Object.entries(record.counts)) {
356
+ const prevCount = prev ? (prev.counts[name] ?? 0) : 0;
357
+ const diff = count - prevCount;
358
+ const tag = document.createElement('span');
359
+ tag.className = 'flush-tag';
360
+
361
+ if (diff === 0 && i !== 0) continue; // skip unchanged (unless first flush)
362
+ const sign = diff > 0 ? '+' : '';
363
+ tag.textContent = diff !== 0 ? `${name} ${sign}${diff}` : `${name} ${count}`;
364
+ tag.className = 'flush-tag' + (diff > 0 ? ' flush-add' : diff < 0 ? ' flush-del' : '');
365
+ tags.appendChild(tag);
366
+ }
367
+
368
+ if (!tags.children.length) {
369
+ const tag = document.createElement('span');
370
+ tag.className = 'flush-tag flush-no-change';
371
+ tag.textContent = 'no changes';
372
+ tags.appendChild(tag);
373
+ }
374
+
375
+ row.appendChild(tags);
376
+ return row;
377
+ }
378
+
243
379
  // ── Helpers ─────────────────────────────────────────────────────
244
380
 
245
381
  function esc(s) {
@@ -261,8 +397,8 @@ function fmt(v) {
261
397
 
262
398
  // ── Auto-refresh ─────────────────────────────────────────────────
263
399
 
264
- function startAuto() { stopAuto(); autoTimer = setInterval(poll, 1000); }
265
- function stopAuto() { if (autoTimer !== null) { clearInterval(autoTimer); autoTimer = null; } }
400
+ function startAuto() { stopAuto(); autoTimer = setInterval(poll, 1000); }
401
+ function stopAuto() { if (autoTimer !== null) { clearInterval(autoTimer); autoTimer = null; } }
266
402
 
267
403
  // ── Boot ────────────────────────────────────────────────────────
268
404
 
package/ui/index.html CHANGED
@@ -44,11 +44,17 @@
44
44
  <div id="list-collections" class="col-body"></div>
45
45
  </div>
46
46
  <div id="col-right" class="col">
47
- <div class="col-header">
48
- Computeds
49
- <span class="badge" id="badge-computeds">0</span>
47
+ <div class="col-section">
48
+ <div class="col-header">
49
+ Computeds
50
+ <span class="badge" id="badge-computeds">0</span>
51
+ </div>
52
+ <div id="list-computeds" class="col-body col-body-flex"></div>
53
+ </div>
54
+ <div class="col-section col-section-history">
55
+ <div class="col-header">Flushes</div>
56
+ <div id="list-history" class="col-body col-body-flex"></div>
50
57
  </div>
51
- <div id="list-computeds" class="col-body"></div>
52
58
  </div>
53
59
  </div>
54
60