@flrande/bak-extension 0.6.11 → 0.6.12

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.
@@ -1,5 +1,499 @@
1
1
  "use strict";
2
2
  (() => {
3
+ // src/dynamic-data-tools.ts
4
+ var DATA_PATTERN = /\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|freshness|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\b/i;
5
+ var CONTRACT_PATTERN = /\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\b/i;
6
+ var EVENT_PATTERN = /\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\b/i;
7
+ var ROW_CANDIDATE_KEYS = ["data", "rows", "results", "items", "records", "entries"];
8
+ function normalizeColumnName(value) {
9
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
10
+ }
11
+ function normalizedComparableValue(value) {
12
+ if (value === null || value === void 0) {
13
+ return null;
14
+ }
15
+ if (typeof value === "object") {
16
+ return null;
17
+ }
18
+ const text = String(value).trim().toLowerCase();
19
+ return text.length > 0 ? text : null;
20
+ }
21
+ function compareNumbers(left, right) {
22
+ return left - right;
23
+ }
24
+ function latestTimestamp(timestamps) {
25
+ const values = timestamps.map((timestamp) => Date.parse(timestamp.value)).filter((value) => Number.isFinite(value));
26
+ return values.length > 0 ? Math.max(...values) : null;
27
+ }
28
+ function sampleValue(value, depth = 0) {
29
+ if (depth >= 2 || value === null || value === void 0 || typeof value !== "object") {
30
+ if (typeof value === "string") {
31
+ return value.length > 160 ? value.slice(0, 160) : value;
32
+ }
33
+ if (typeof value === "function") {
34
+ return "[Function]";
35
+ }
36
+ return value;
37
+ }
38
+ if (Array.isArray(value)) {
39
+ return value.slice(0, 3).map((item) => sampleValue(item, depth + 1));
40
+ }
41
+ const sampled = {};
42
+ for (const key of Object.keys(value).slice(0, 8)) {
43
+ try {
44
+ sampled[key] = sampleValue(value[key], depth + 1);
45
+ } catch {
46
+ sampled[key] = "[Unreadable]";
47
+ }
48
+ }
49
+ return sampled;
50
+ }
51
+ function estimateSampleSize(value) {
52
+ if (Array.isArray(value)) {
53
+ return value.length;
54
+ }
55
+ if (value && typeof value === "object") {
56
+ return Object.keys(value).length;
57
+ }
58
+ return null;
59
+ }
60
+ function classifyTimestamp(path, value, now = Date.now()) {
61
+ const normalized = path.toLowerCase();
62
+ if (DATA_PATTERN.test(normalized)) {
63
+ return "data";
64
+ }
65
+ if (CONTRACT_PATTERN.test(normalized)) {
66
+ return "contract";
67
+ }
68
+ if (EVENT_PATTERN.test(normalized)) {
69
+ return "event";
70
+ }
71
+ const parsed = Date.parse(value.trim());
72
+ return Number.isFinite(parsed) && parsed > now + 36 * 60 * 60 * 1e3 ? "contract" : "unknown";
73
+ }
74
+ function collectTimestampProbes(value, path, options = {}) {
75
+ const collected = [];
76
+ const now = typeof options.now === "number" ? options.now : Date.now();
77
+ const limit = typeof options.limit === "number" ? Math.max(1, Math.floor(options.limit)) : 16;
78
+ const visit = (candidate, candidatePath, depth) => {
79
+ if (collected.length >= limit) {
80
+ return;
81
+ }
82
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
83
+ const parsed = Date.parse(candidate.trim());
84
+ if (Number.isFinite(parsed)) {
85
+ collected.push({
86
+ path: candidatePath,
87
+ value: candidate,
88
+ category: classifyTimestamp(candidatePath, candidate, now)
89
+ });
90
+ }
91
+ return;
92
+ }
93
+ if (depth >= 3 || candidate === null || candidate === void 0) {
94
+ return;
95
+ }
96
+ if (Array.isArray(candidate)) {
97
+ candidate.slice(0, 3).forEach((entry, index) => visit(entry, `${candidatePath}[${index}]`, depth + 1));
98
+ return;
99
+ }
100
+ if (typeof candidate === "object") {
101
+ Object.keys(candidate).slice(0, 8).forEach((key) => {
102
+ try {
103
+ visit(candidate[key], candidatePath ? `${candidatePath}.${key}` : key, depth + 1);
104
+ } catch {
105
+ }
106
+ });
107
+ }
108
+ };
109
+ visit(value, path, 0);
110
+ return collected;
111
+ }
112
+ function inferSchemaHint(value) {
113
+ const rowsCandidate = extractStructuredRows(value);
114
+ if (rowsCandidate) {
115
+ if (rowsCandidate.rowType === "object") {
116
+ const firstRecord = rowsCandidate.rows.find(
117
+ (row) => typeof row === "object" && row !== null && !Array.isArray(row)
118
+ );
119
+ return {
120
+ kind: "rows-object",
121
+ columns: firstRecord ? Object.keys(firstRecord).slice(0, 12) : []
122
+ };
123
+ }
124
+ if (rowsCandidate.rowType === "array") {
125
+ const firstRow = rowsCandidate.rows.find((row) => Array.isArray(row));
126
+ return {
127
+ kind: "rows-array",
128
+ columns: firstRow ? firstRow.map((_, index) => `Column ${index + 1}`) : []
129
+ };
130
+ }
131
+ }
132
+ if (Array.isArray(value)) {
133
+ return { kind: "array" };
134
+ }
135
+ if (value && typeof value === "object") {
136
+ return {
137
+ kind: "object",
138
+ columns: Object.keys(value).slice(0, 12)
139
+ };
140
+ }
141
+ if (value === null || value === void 0) {
142
+ return null;
143
+ }
144
+ return { kind: "scalar" };
145
+ }
146
+ function extractStructuredRows(value, path = "$") {
147
+ if (Array.isArray(value)) {
148
+ if (value.length === 0) {
149
+ return { rows: value, path, rowType: "object" };
150
+ }
151
+ const first = value.find((item) => item !== null && item !== void 0);
152
+ if (Array.isArray(first)) {
153
+ return { rows: value, path, rowType: "array" };
154
+ }
155
+ if (first && typeof first === "object") {
156
+ return { rows: value, path, rowType: "object" };
157
+ }
158
+ return { rows: value, path, rowType: "scalar" };
159
+ }
160
+ if (!value || typeof value !== "object") {
161
+ return null;
162
+ }
163
+ const record = value;
164
+ for (const key of ROW_CANDIDATE_KEYS) {
165
+ if (Array.isArray(record[key])) {
166
+ return extractStructuredRows(record[key], `${path}.${key}`);
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+ function toObjectRow(row, fallbackColumns = []) {
172
+ if (row && typeof row === "object" && !Array.isArray(row)) {
173
+ return row;
174
+ }
175
+ if (Array.isArray(row)) {
176
+ const mapped = {};
177
+ row.forEach((value, index) => {
178
+ mapped[fallbackColumns[index] ?? `Column ${index + 1}`] = value;
179
+ });
180
+ return mapped;
181
+ }
182
+ if (row === null || row === void 0) {
183
+ return null;
184
+ }
185
+ return { Value: row };
186
+ }
187
+ function sampleRowsFromValue(value, limit = 5) {
188
+ const rowsCandidate = extractStructuredRows(value);
189
+ if (!rowsCandidate) {
190
+ const singleRow = toObjectRow(value);
191
+ return singleRow ? [singleRow] : [];
192
+ }
193
+ const fallbackColumns = rowsCandidate.rowType === "array" ? Array.from({ length: Array.isArray(rowsCandidate.rows[0]) ? rowsCandidate.rows[0].length : 0 }, (_, index) => `Column ${index + 1}`) : [];
194
+ return rowsCandidate.rows.slice(0, limit).map((row) => toObjectRow(row, fallbackColumns)).filter((row) => row !== null);
195
+ }
196
+ function collectSampleValues(rows) {
197
+ const values = /* @__PURE__ */ new Set();
198
+ for (const row of rows) {
199
+ for (const value of Object.values(row)) {
200
+ const comparable = normalizedComparableValue(value);
201
+ if (comparable) {
202
+ values.add(comparable);
203
+ }
204
+ if (values.size >= 24) {
205
+ return values;
206
+ }
207
+ }
208
+ }
209
+ return values;
210
+ }
211
+ function buildSourceAnalysis(source, sample) {
212
+ const sampleRows = sampleRowsFromValue(sample);
213
+ return {
214
+ source,
215
+ sampleRows,
216
+ sampleValues: collectSampleValues(sampleRows),
217
+ schemaColumns: source.schemaHint?.columns?.map(normalizeColumnName).filter(Boolean) ?? []
218
+ };
219
+ }
220
+ function parseNetworkBody(entry) {
221
+ const preview = typeof entry.responseBodyPreview === "string" ? entry.responseBodyPreview.trim() : "";
222
+ if (!preview || entry.responseBodyTruncated === true || entry.binary === true) {
223
+ return null;
224
+ }
225
+ const contentType = typeof entry.contentType === "string" ? entry.contentType.toLowerCase() : "";
226
+ if (!contentType.includes("json") && !preview.startsWith("{") && !preview.startsWith("[")) {
227
+ return null;
228
+ }
229
+ try {
230
+ return JSON.parse(preview);
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ function buildWindowSources(candidates) {
236
+ return candidates.map((candidate) => {
237
+ const source = {
238
+ sourceId: `windowGlobal:${candidate.name}`,
239
+ type: "windowGlobal",
240
+ label: candidate.name,
241
+ path: candidate.name,
242
+ sampleSize: candidate.sampleSize,
243
+ schemaHint: candidate.schemaHint,
244
+ lastObservedAt: candidate.lastObservedAt
245
+ };
246
+ return buildSourceAnalysis(source, candidate.sample);
247
+ });
248
+ }
249
+ function buildInlineJsonAnalyses(sources) {
250
+ return sources.map((sourceItem, index) => {
251
+ const source = {
252
+ sourceId: `inlineJson:${index + 1}:${sourceItem.path}`,
253
+ type: "inlineJson",
254
+ label: sourceItem.label,
255
+ path: sourceItem.path,
256
+ sampleSize: sourceItem.sampleSize,
257
+ schemaHint: sourceItem.schemaHint,
258
+ lastObservedAt: sourceItem.lastObservedAt
259
+ };
260
+ return buildSourceAnalysis(source, sourceItem.sample);
261
+ });
262
+ }
263
+ function buildNetworkAnalyses(entries) {
264
+ const analyses = [];
265
+ for (const entry of entries) {
266
+ const parsed = parseNetworkBody(entry);
267
+ if (parsed === null) {
268
+ continue;
269
+ }
270
+ const rowsCandidate = extractStructuredRows(parsed);
271
+ const schemaHint = inferSchemaHint(parsed);
272
+ const url = new URL(entry.url, "http://127.0.0.1");
273
+ const source = {
274
+ sourceId: `networkResponse:${entry.id}`,
275
+ type: "networkResponse",
276
+ label: `${entry.method} ${url.pathname}`,
277
+ path: rowsCandidate?.path ?? url.pathname,
278
+ sampleSize: estimateSampleSize(rowsCandidate?.rows ?? parsed),
279
+ schemaHint,
280
+ lastObservedAt: entry.ts
281
+ };
282
+ analyses.push(buildSourceAnalysis(source, rowsCandidate?.rows ?? parsed));
283
+ }
284
+ return analyses;
285
+ }
286
+ function scoreSourceMapping(table, source, now) {
287
+ const tableColumns = table.schema.columns.map((column) => column.label);
288
+ const normalizedTableColumns = new Map(tableColumns.map((label) => [normalizeColumnName(label), label]));
289
+ const matchedColumns = [...new Set(source.schemaColumns.filter((column) => normalizedTableColumns.has(column)).map((column) => normalizedTableColumns.get(column)))];
290
+ const basis = [];
291
+ if (matchedColumns.length > 0) {
292
+ basis.push({
293
+ type: "columnOverlap",
294
+ detail: `Column overlap on ${matchedColumns.join(", ")}`
295
+ });
296
+ }
297
+ const overlappingValues = [...table.sampleRows.flatMap((row) => Object.values(row))].map((value) => normalizedComparableValue(value)).filter((value) => value !== null).filter((value) => source.sampleValues.has(value));
298
+ const distinctOverlappingValues = [...new Set(overlappingValues)].slice(0, 5);
299
+ if (distinctOverlappingValues.length > 0) {
300
+ basis.push({
301
+ type: "sampleValueOverlap",
302
+ detail: `Shared sample values: ${distinctOverlappingValues.join(", ")}`
303
+ });
304
+ }
305
+ const explicitReferenceHit = table.table.name.toLowerCase().includes(source.source.label.toLowerCase()) || (table.table.selector ?? "").toLowerCase().includes(source.source.label.toLowerCase()) || source.source.label.toLowerCase().includes(table.table.name.toLowerCase());
306
+ if (explicitReferenceHit) {
307
+ basis.push({
308
+ type: "explicitReference",
309
+ detail: `Table label and source label both mention ${source.source.label}`
310
+ });
311
+ }
312
+ if (source.source.type === "networkResponse" && typeof source.source.lastObservedAt === "number" && Math.max(0, now - source.source.lastObservedAt) <= 9e4) {
313
+ basis.push({
314
+ type: "timeProximity",
315
+ detail: "Recent network response observed within the last 90 seconds"
316
+ });
317
+ }
318
+ if (basis.length === 0) {
319
+ return null;
320
+ }
321
+ const confidence = matchedColumns.length >= Math.max(2, Math.min(tableColumns.length, 3)) || matchedColumns.length > 0 && distinctOverlappingValues.length > 0 ? "high" : matchedColumns.length > 0 || distinctOverlappingValues.length > 0 ? "medium" : "low";
322
+ return {
323
+ tableId: table.table.id,
324
+ sourceId: source.source.sourceId,
325
+ confidence,
326
+ basis,
327
+ matchedColumns
328
+ };
329
+ }
330
+ function buildRecommendedNextActions(tables, mappings, sourceAnalyses) {
331
+ const recommendations = [];
332
+ const pushRecommendation = (item) => {
333
+ if (recommendations.some((existing) => existing.command === item.command)) {
334
+ return;
335
+ }
336
+ recommendations.push(item);
337
+ };
338
+ for (const table of tables) {
339
+ if (table.table.intelligence?.preferredExtractionMode === "scroll") {
340
+ pushRecommendation({
341
+ title: `Read all rows from ${table.table.id}`,
342
+ command: `bak table rows --table ${table.table.id} --all --max-rows 10000`,
343
+ note: "The table looks virtualized or lazy-loaded, so a scroll pass is preferred."
344
+ });
345
+ }
346
+ }
347
+ for (const mapping of mappings.filter((item) => item.confidence !== "low")) {
348
+ const source = sourceAnalyses.find((analysis) => analysis.source.sourceId === mapping.sourceId);
349
+ if (!source) {
350
+ continue;
351
+ }
352
+ if (source.source.type === "windowGlobal") {
353
+ pushRecommendation({
354
+ title: `Read ${source.source.label} directly from page data`,
355
+ command: `bak page extract --path "${source.source.path}" --resolver auto`,
356
+ note: `Mapped to ${mapping.tableId} with ${mapping.confidence} confidence.`
357
+ });
358
+ continue;
359
+ }
360
+ if (source.source.type === "networkResponse") {
361
+ const requestId = source.source.sourceId.replace(/^networkResponse:/, "");
362
+ pushRecommendation({
363
+ title: `Replay ${requestId} with table schema`,
364
+ command: `bak network replay --request-id ${requestId} --mode json --with-schema auto`,
365
+ note: `Recent response mapped to ${mapping.tableId} with ${mapping.confidence} confidence.`
366
+ });
367
+ continue;
368
+ }
369
+ pushRecommendation({
370
+ title: `Inspect ${source.source.label} inline JSON`,
371
+ command: "bak page freshness",
372
+ note: `Inline JSON source mapped to ${mapping.tableId}; use freshness or capture commands to inspect it further.`
373
+ });
374
+ }
375
+ if (recommendations.length === 0) {
376
+ pushRecommendation({
377
+ title: "Check data freshness",
378
+ command: "bak page freshness",
379
+ note: "No strong data-source mapping was found yet."
380
+ });
381
+ }
382
+ return recommendations.slice(0, 6);
383
+ }
384
+ function buildSourceMappingReport(input) {
385
+ const now = typeof input.now === "number" ? input.now : Date.now();
386
+ const windowAnalyses = buildWindowSources(input.windowSources);
387
+ const inlineAnalyses = buildInlineJsonAnalyses(input.inlineJsonSources);
388
+ const networkAnalyses = buildNetworkAnalyses(input.recentNetwork);
389
+ const sourceAnalyses = [...windowAnalyses, ...inlineAnalyses, ...networkAnalyses];
390
+ const sourceMappings = input.tables.flatMap((table) => sourceAnalyses.map((source) => scoreSourceMapping(table, source, now))).filter((mapping) => mapping !== null).sort((left, right) => {
391
+ const confidenceRank = { high: 0, medium: 1, low: 2 };
392
+ return confidenceRank[left.confidence] - confidenceRank[right.confidence] || left.tableId.localeCompare(right.tableId) || left.sourceId.localeCompare(right.sourceId);
393
+ });
394
+ return {
395
+ dataSources: sourceAnalyses.map((analysis) => analysis.source),
396
+ sourceMappings,
397
+ recommendedNextActions: buildRecommendedNextActions(input.tables, sourceMappings, sourceAnalyses),
398
+ sourceAnalyses
399
+ };
400
+ }
401
+ function mapObjectRowToSchema(row, schema) {
402
+ const normalizedKeys = new Map(Object.keys(row).map((key) => [normalizeColumnName(key), key]));
403
+ const mapped = {};
404
+ for (const column of schema.columns) {
405
+ const normalized = normalizeColumnName(column.label);
406
+ const sourceKey = normalizedKeys.get(normalized);
407
+ if (sourceKey) {
408
+ mapped[column.label] = row[sourceKey];
409
+ }
410
+ }
411
+ if (Object.keys(mapped).length > 0) {
412
+ return mapped;
413
+ }
414
+ return { ...row };
415
+ }
416
+ function selectReplaySchemaMatch(responseJson, tables, options = {}) {
417
+ const candidate = extractStructuredRows(responseJson);
418
+ if (!candidate || candidate.rows.length === 0 || tables.length === 0) {
419
+ return null;
420
+ }
421
+ const preferredTableId = options.preferredSourceId && options.mappings ? options.mappings.find((mapping) => mapping.sourceId === options.preferredSourceId && mapping.confidence !== "low")?.tableId : void 0;
422
+ const orderedTables = preferredTableId ? tables.slice().sort((left, right) => {
423
+ if (left.table.id === preferredTableId) {
424
+ return -1;
425
+ }
426
+ if (right.table.id === preferredTableId) {
427
+ return 1;
428
+ }
429
+ return left.table.id.localeCompare(right.table.id);
430
+ }) : tables;
431
+ const firstRow = candidate.rows[0];
432
+ if (Array.isArray(firstRow)) {
433
+ const matchingTable = orderedTables.find((table) => table.schema.columns.length === firstRow.length) ?? orderedTables.find((table) => table.schema.columns.length > 0) ?? null;
434
+ if (!matchingTable) {
435
+ return null;
436
+ }
437
+ return {
438
+ table: matchingTable.table,
439
+ schema: matchingTable.schema,
440
+ mappedRows: candidate.rows.filter((row) => Array.isArray(row)).map((row) => {
441
+ const mapped = {};
442
+ matchingTable.schema.columns.forEach((column, index) => {
443
+ mapped[column.label] = row[index];
444
+ });
445
+ return mapped;
446
+ }),
447
+ mappingSource: candidate.path
448
+ };
449
+ }
450
+ if (firstRow && typeof firstRow === "object") {
451
+ const rowObject = firstRow;
452
+ const rowKeys = new Set(Object.keys(rowObject).map(normalizeColumnName));
453
+ const matchingEntry = orderedTables.map((table) => ({
454
+ table,
455
+ score: table.schema.columns.filter((column) => rowKeys.has(normalizeColumnName(column.label))).length
456
+ })).sort((left, right) => compareNumbers(right.score, left.score))[0] ?? null;
457
+ if (!matchingEntry || matchingEntry.score <= 0) {
458
+ return null;
459
+ }
460
+ const matchingTable = matchingEntry.table;
461
+ return {
462
+ table: matchingTable.table,
463
+ schema: matchingTable.schema,
464
+ mappedRows: candidate.rows.filter((row) => typeof row === "object" && row !== null && !Array.isArray(row)).map((row) => mapObjectRowToSchema(row, matchingTable.schema)),
465
+ mappingSource: candidate.path
466
+ };
467
+ }
468
+ return null;
469
+ }
470
+ function buildInspectPageDataResult(input) {
471
+ const report = buildSourceMappingReport({
472
+ tables: input.tableAnalyses,
473
+ windowSources: input.pageDataCandidates,
474
+ inlineJsonSources: input.inlineJsonSources,
475
+ recentNetwork: input.recentNetwork,
476
+ now: input.now
477
+ });
478
+ return {
479
+ dataSources: report.dataSources,
480
+ sourceMappings: report.sourceMappings,
481
+ recommendedNextActions: report.recommendedNextActions
482
+ };
483
+ }
484
+ function buildPageDataProbe(name, resolver, sample) {
485
+ const timestamps = collectTimestampProbes(sample, name);
486
+ return {
487
+ name,
488
+ resolver,
489
+ sample: sampleValue(sample),
490
+ sampleSize: estimateSampleSize(sample),
491
+ schemaHint: inferSchemaHint(sample),
492
+ lastObservedAt: latestTimestamp(timestamps),
493
+ timestamps
494
+ };
495
+ }
496
+
3
497
  // src/privacy.ts
4
498
  var HIGH_ENTROPY_TOKEN_PATTERN = /^(?=.*\d)(?=.*[a-zA-Z])[A-Za-z0-9~!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`]{16,}$/;
5
499
  var TRANSPORT_SECRET_KEY_SOURCE = "(?:api[_-]?key|authorization|auth|cookie|csrf(?:token)?|nonce|password|passwd|secret|session(?:id)?|token|xsrf(?:token)?)";
@@ -62,7 +556,7 @@
62
556
  // package.json
63
557
  var package_default = {
64
558
  name: "@flrande/bak-extension",
65
- version: "0.6.11",
559
+ version: "0.6.12",
66
560
  type: "module",
67
561
  scripts: {
68
562
  build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
@@ -1414,6 +1908,7 @@
1414
1908
  var preserveHumanFocusDepth = 0;
1415
1909
  var lastBindingUpdateAt = null;
1416
1910
  var lastBindingUpdateReason = null;
1911
+ var bindingUpdateMetadata = /* @__PURE__ */ new Map();
1417
1912
  async function getConfig() {
1418
1913
  const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
1419
1914
  return {
@@ -1546,19 +2041,27 @@
1546
2041
  async function listSessionBindingStates() {
1547
2042
  return Object.values(await loadSessionBindingStateMap());
1548
2043
  }
1549
- function summarizeSessionBindings(states) {
1550
- const items = states.map((state) => {
1551
- const detached = state.windowId === null || state.tabIds.length === 0;
1552
- return {
1553
- id: state.id,
1554
- label: state.label,
1555
- tabCount: state.tabIds.length,
1556
- activeTabId: state.activeTabId,
1557
- windowId: state.windowId,
1558
- groupId: state.groupId,
1559
- detached
1560
- };
1561
- });
2044
+ async function summarizeSessionBindings(states) {
2045
+ const items = await Promise.all(
2046
+ states.map(async (state) => {
2047
+ const detached = state.windowId === null || state.tabIds.length === 0;
2048
+ const activeTab = typeof state.activeTabId === "number" ? await sessionBindingBrowser.getTab(state.activeTabId) : null;
2049
+ const bindingUpdate = bindingUpdateMetadata.get(state.id);
2050
+ return {
2051
+ id: state.id,
2052
+ label: state.label,
2053
+ tabCount: state.tabIds.length,
2054
+ activeTabId: state.activeTabId,
2055
+ activeTabTitle: activeTab?.title ?? null,
2056
+ activeTabUrl: activeTab?.url ?? null,
2057
+ windowId: state.windowId,
2058
+ groupId: state.groupId,
2059
+ detached,
2060
+ lastBindingUpdateAt: bindingUpdate?.at ?? null,
2061
+ lastBindingUpdateReason: bindingUpdate?.reason ?? null
2062
+ };
2063
+ })
2064
+ );
1562
2065
  return {
1563
2066
  count: items.length,
1564
2067
  attachedCount: items.filter((item) => !item.detached).length,
@@ -1569,7 +2072,7 @@
1569
2072
  }
1570
2073
  async function buildPopupState() {
1571
2074
  const config = await getConfig();
1572
- const sessionBindings = summarizeSessionBindings(await listSessionBindingStates());
2075
+ const sessionBindings = await summarizeSessionBindings(await listSessionBindingStates());
1573
2076
  const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
1574
2077
  let connectionState;
1575
2078
  if (!config.token) {
@@ -1630,6 +2133,10 @@
1630
2133
  function emitSessionBindingUpdated(bindingId, reason, state, extras = {}) {
1631
2134
  lastBindingUpdateAt = Date.now();
1632
2135
  lastBindingUpdateReason = reason;
2136
+ bindingUpdateMetadata.set(bindingId, {
2137
+ at: lastBindingUpdateAt,
2138
+ reason
2139
+ });
1633
2140
  sendEvent("sessionBinding.updated", {
1634
2141
  bindingId,
1635
2142
  reason,
@@ -2482,15 +2989,6 @@
2482
2989
  const dataPattern = /\\b(updated|update|updatedat|asof|timestamp|generated|generatedat|refresh|latest|last|quote|trade|price|flow|market|time|snapshot|signal)\\b/i;
2483
2990
  const contractPattern = /\\b(expiry|expiration|expires|option|contract|strike|maturity|dte|call|put|exercise)\\b/i;
2484
2991
  const eventPattern = /\\b(earnings|event|report|dividend|split|meeting|fomc|release|filing)\\b/i;
2485
- const isTimestampString = (value) => typeof value === 'string' && value.trim().length > 0 && !Number.isNaN(Date.parse(value.trim()));
2486
- const classify = (path, value) => {
2487
- const normalized = String(path || '').toLowerCase();
2488
- if (dataPattern.test(normalized)) return 'data';
2489
- if (contractPattern.test(normalized)) return 'contract';
2490
- if (eventPattern.test(normalized)) return 'event';
2491
- const parsed = Date.parse(String(value || '').trim());
2492
- return Number.isFinite(parsed) && parsed > Date.now() + 36 * 60 * 60 * 1000 ? 'contract' : 'unknown';
2493
- };
2494
2992
  const sampleValue = (value, depth = 0) => {
2495
2993
  if (depth >= 2 || value == null || typeof value !== 'object') {
2496
2994
  if (typeof value === 'string') {
@@ -2514,29 +3012,6 @@
2514
3012
  }
2515
3013
  return sampled;
2516
3014
  };
2517
- const collectTimestamps = (value, path, depth, collected) => {
2518
- if (collected.length >= 16) return;
2519
- if (isTimestampString(value)) {
2520
- collected.push({ path, value: String(value), category: classify(path, value) });
2521
- return;
2522
- }
2523
- if (depth >= 3) return;
2524
- if (Array.isArray(value)) {
2525
- value.slice(0, 3).forEach((item, index) => collectTimestamps(item, path + '[' + index + ']', depth + 1, collected));
2526
- return;
2527
- }
2528
- if (value && typeof value === 'object') {
2529
- Object.keys(value)
2530
- .slice(0, 8)
2531
- .forEach((key) => {
2532
- try {
2533
- collectTimestamps(value[key], path ? path + '.' + key : key, depth + 1, collected);
2534
- } catch {
2535
- // Ignore unreadable nested properties.
2536
- }
2537
- });
2538
- }
2539
- };
2540
3015
  const readCandidate = (name) => {
2541
3016
  if (name in globalThis) {
2542
3017
  return { resolver: 'globalThis', value: globalThis[name] };
@@ -2547,13 +3022,10 @@
2547
3022
  for (const name of candidates) {
2548
3023
  try {
2549
3024
  const resolved = readCandidate(name);
2550
- const timestamps = [];
2551
- collectTimestamps(resolved.value, name, 0, timestamps);
2552
3025
  results.push({
2553
3026
  name,
2554
3027
  resolver: resolved.resolver,
2555
- sample: sampleValue(resolved.value),
2556
- timestamps
3028
+ sample: sampleValue(resolved.value)
2557
3029
  });
2558
3030
  } catch {
2559
3031
  // Ignore inaccessible candidates.
@@ -2568,7 +3040,12 @@
2568
3040
  maxBytes: 64 * 1024
2569
3041
  });
2570
3042
  const frameResult = evaluated.result ?? evaluated.results?.find((candidate) => candidate.value || candidate.error);
2571
- return Array.isArray(frameResult?.value) ? frameResult.value : [];
3043
+ if (!Array.isArray(frameResult?.value)) {
3044
+ return [];
3045
+ }
3046
+ return frameResult.value.filter(
3047
+ (candidate) => typeof candidate === "object" && candidate !== null && typeof candidate.name === "string"
3048
+ ).map((candidate) => buildPageDataProbe(candidate.name, candidate.resolver, candidate.sample));
2572
3049
  } catch {
2573
3050
  return [];
2574
3051
  }
@@ -2692,52 +3169,59 @@
2692
3169
  }
2693
3170
  return null;
2694
3171
  }
2695
- async function enrichReplayWithSchema(tabId, response) {
2696
- const candidate = extractReplayRowsCandidate(response.json);
2697
- if (!candidate || candidate.rows.length === 0) {
2698
- return response;
2699
- }
2700
- const firstRow = candidate.rows[0];
3172
+ async function collectTableAnalyses(tabId) {
2701
3173
  const tablesResult = await forwardContentRpc(tabId, "table.list", {});
2702
3174
  const tables = Array.isArray(tablesResult.tables) ? tablesResult.tables : [];
2703
- if (tables.length === 0) {
2704
- return response;
2705
- }
2706
- const schemas = [];
3175
+ const analyses = [];
2707
3176
  for (const table of tables) {
2708
3177
  const schemaResult = await forwardContentRpc(tabId, "table.schema", { table: table.id });
3178
+ const rowsResult = await forwardContentRpc(tabId, "table.rows", {
3179
+ table: table.id,
3180
+ limit: 8
3181
+ });
2709
3182
  if (schemaResult.schema && Array.isArray(schemaResult.schema.columns)) {
2710
- schemas.push({ table: schemaResult.table ?? table, schema: schemaResult.schema });
3183
+ analyses.push({
3184
+ table: schemaResult.table ?? rowsResult.table ?? table,
3185
+ schema: schemaResult.schema,
3186
+ sampleRows: Array.isArray(rowsResult.rows) ? rowsResult.rows.slice(0, 8) : []
3187
+ });
2711
3188
  }
2712
3189
  }
2713
- if (schemas.length === 0) {
3190
+ return analyses;
3191
+ }
3192
+ async function enrichReplayWithSchema(tabId, requestId, response) {
3193
+ const candidate = extractReplayRowsCandidate(response.json);
3194
+ if (!candidate || candidate.rows.length === 0) {
2714
3195
  return response;
2715
3196
  }
2716
- if (Array.isArray(firstRow)) {
2717
- const matchingSchema = schemas.find(({ schema }) => schema.columns.length === firstRow.length) ?? schemas[0];
2718
- if (!matchingSchema) {
2719
- return response;
2720
- }
2721
- const mappedRows = candidate.rows.filter((row) => Array.isArray(row)).map((row) => {
2722
- const mapped = {};
2723
- matchingSchema.schema.columns.forEach((column, index) => {
2724
- mapped[column.label] = row[index];
2725
- });
2726
- return mapped;
2727
- });
2728
- return {
2729
- ...response,
2730
- table: matchingSchema.table,
2731
- schema: matchingSchema.schema,
2732
- mappedRows,
2733
- mappingSource: candidate.source
2734
- };
3197
+ const tables = await collectTableAnalyses(tabId);
3198
+ if (tables.length === 0) {
3199
+ return response;
2735
3200
  }
2736
- if (typeof firstRow === "object" && firstRow !== null) {
3201
+ const inspection = await collectPageInspection(tabId, {});
3202
+ const pageDataCandidates = await probePageDataCandidatesForTab(tabId, inspection);
3203
+ const recentNetwork = listNetworkEntries(tabId, { limit: 25 });
3204
+ const pageDataReport = buildInspectPageDataResult({
3205
+ suspiciousGlobals: inspection.suspiciousGlobals ?? [],
3206
+ tables: inspection.tables ?? [],
3207
+ visibleTimestamps: inspection.visibleTimestamps ?? [],
3208
+ inlineTimestamps: inspection.inlineTimestamps ?? [],
3209
+ pageDataCandidates,
3210
+ recentNetwork,
3211
+ tableAnalyses: tables,
3212
+ inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
3213
+ });
3214
+ const matched = selectReplaySchemaMatch(response.json, tables, {
3215
+ preferredSourceId: `networkResponse:${requestId}`,
3216
+ mappings: pageDataReport.sourceMappings
3217
+ });
3218
+ if (matched) {
2737
3219
  return {
2738
3220
  ...response,
2739
- mappedRows: candidate.rows.filter((row) => typeof row === "object" && row !== null),
2740
- mappingSource: candidate.source
3221
+ table: matched.table,
3222
+ schema: matched.schema,
3223
+ mappedRows: matched.mappedRows,
3224
+ mappingSource: matched.mappingSource
2741
3225
  };
2742
3226
  }
2743
3227
  return response;
@@ -3255,7 +3739,7 @@
3255
3739
  if (!first) {
3256
3740
  throw toError("E_EXECUTION", "network replay returned no response payload");
3257
3741
  }
3258
- return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, first) : first;
3742
+ return params.withSchema === "auto" && params.mode === "json" ? await enrichReplayWithSchema(tab.id, String(params.id ?? ""), first) : first;
3259
3743
  });
3260
3744
  }
3261
3745
  case "page.freshness": {
@@ -3333,6 +3817,18 @@
3333
3817
  const inspection = await collectPageInspection(tab.id, params);
3334
3818
  const pageDataCandidates = await probePageDataCandidatesForTab(tab.id, inspection);
3335
3819
  const network = listNetworkEntries(tab.id, { limit: 10 });
3820
+ const tableAnalyses = await collectTableAnalyses(tab.id);
3821
+ const enriched = buildInspectPageDataResult({
3822
+ suspiciousGlobals: inspection.suspiciousGlobals ?? [],
3823
+ tables: inspection.tables ?? [],
3824
+ visibleTimestamps: inspection.visibleTimestamps ?? [],
3825
+ inlineTimestamps: inspection.inlineTimestamps ?? [],
3826
+ pageDataCandidates,
3827
+ recentNetwork: network,
3828
+ tableAnalyses,
3829
+ inlineJsonSources: Array.isArray(inspection.inlineJsonSources) ? inspection.inlineJsonSources : []
3830
+ });
3831
+ const recommendedNextSteps = enriched.recommendedNextActions.map((action) => action.command);
3336
3832
  return {
3337
3833
  suspiciousGlobals: inspection.suspiciousGlobals ?? [],
3338
3834
  tables: inspection.tables ?? [],
@@ -3340,10 +3836,12 @@
3340
3836
  inlineTimestamps: inspection.inlineTimestamps ?? [],
3341
3837
  pageDataCandidates,
3342
3838
  recentNetwork: network,
3839
+ dataSources: enriched.dataSources,
3840
+ sourceMappings: enriched.sourceMappings,
3841
+ recommendedNextActions: enriched.recommendedNextActions,
3343
3842
  recommendedNextSteps: [
3344
- "bak page extract --path table_data --resolver auto",
3345
- "bak network search --pattern table_data",
3346
- "bak page freshness"
3843
+ ...recommendedNextSteps,
3844
+ ...["bak page freshness"].filter((command) => !recommendedNextSteps.includes(command))
3347
3845
  ]
3348
3846
  };
3349
3847
  });