browserzap 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/bridge-server/executor.js +148 -148
- package/bridge-server/limits.js +10 -0
- package/bridge-server/mcp-server.js +22 -4
- package/bridge-server/probe.js +123 -114
- package/bridge-server/screenshots.js +130 -130
- package/bridge-server/secret.js +127 -127
- package/bridge-server/server.js +44 -7
- package/bridge-server/session-log.js +188 -188
- package/bridge-server/tools.js +15 -1
- package/extension/background.js +1 -1
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BrowserZap — session log: what every session did, for transparency and debugging.
|
|
3
|
-
*
|
|
4
|
-
* One JSON line per tool call or workflow step, appended to
|
|
5
|
-
* <config dir>/logs/YYYY-MM-DD.jsonl (0600, kept 14 days)
|
|
6
|
-
* by the MCP server (agent calls, agent-run workflows) and the bridge's panel
|
|
7
|
-
* runner (workflows run from the side panel).
|
|
8
|
-
*
|
|
9
|
-
* Recorded: time, session and client, tool, the target as it was named, the
|
|
10
|
-
* outcome (ok, state, error code), timing, and evidence useful for debugging
|
|
11
|
-
* (waited-for-target time, click path, risk, navigation, where a workflow
|
|
12
|
-
* stopped), plus the page as origin + path.
|
|
13
|
-
*
|
|
14
|
-
* Never recorded: typed values (only their length), parameter values (only
|
|
15
|
-
* their names), URL query strings or fragments, page text, approval ids,
|
|
16
|
-
* screenshots. Result summaries quote page content, so they are added only
|
|
17
|
-
* with BROWSERZAP_LOG=verbose. BROWSERZAP_LOG=off disables the file.
|
|
18
|
-
*
|
|
19
|
-
* Read it with `npm run logs` (bridge-server/index.js --logs).
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
'use strict';
|
|
23
|
-
|
|
24
|
-
const fs = require('fs');
|
|
25
|
-
const path = require('path');
|
|
26
|
-
|
|
27
|
-
const { configDir } = require('./secret');
|
|
28
|
-
|
|
29
|
-
const KEEP_DAYS = 14;
|
|
30
|
-
const MAX_DAY_BYTES = 20 * 1024 * 1024;
|
|
31
|
-
const MAX_TEXT = 80;
|
|
32
|
-
const EVIDENCE_KEYS = [
|
|
33
|
-
'waitedForTargetMs', 'clickVia', 'input', 'risk', 'navigated', 'urlChanged', 'domChanged', 'noObservableChange',
|
|
34
|
-
'expectationMet', 'verified', 'redirected', 'crossOrigin', 'reResolvedStaleRef', 'elapsedMs',
|
|
35
|
-
'workflow', 'version', 'stoppedAt', 'completedSteps', 'totalSteps', 'completedRuns',
|
|
36
|
-
// slow-server evidence (docs/AUDIT.md AU-02, AU-06, AU-07, AU-05)
|
|
37
|
-
'loaded', 'httpStatus', 'retryable', 'ttfbMs', 'domContentLoadedMs', 'loadMs', 'waitingFor', 'pageBusyMs', 'waitedForPersonMs',
|
|
38
|
-
// why a step was slow or failed (AU-12); slowestRequest is origin + path only. busyIndicator is page text: verbose only.
|
|
39
|
-
'likelyCause', 'slowestRequest', 'pendingRequests', 'readyState', 'slowSteps',
|
|
40
|
-
];
|
|
41
|
-
const PLAIN_ARGS = [
|
|
42
|
-
'key', 'direction', 'amount', 'timeoutMs', 'maxElements', 'includeText', 'maxTextChars', 'within', 'tabId',
|
|
43
|
-
'name', 'startAt', 'version', 'action', 'restoreVersion', 'limit', 'includeTab', 'waitMs', 'overwrite', 'keepValues',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const mode = () => (process.env.BROWSERZAP_LOG || 'standard').toLowerCase();
|
|
47
|
-
const logDir = () => path.join(configDir(), 'logs');
|
|
48
|
-
const dayFile = (date = new Date()) => path.join(logDir(), `${date.toISOString().slice(0, 10)}.jsonl`);
|
|
49
|
-
const clip = (v) => (typeof v === 'string' && v.length > MAX_TEXT ? `${v.slice(0, MAX_TEXT)}…` : v);
|
|
50
|
-
|
|
51
|
-
/** origin + path only: query strings and fragments often carry tokens or personal data. */
|
|
52
|
-
function safeUrl(raw) {
|
|
53
|
-
if (typeof raw !== 'string' || !raw) return undefined;
|
|
54
|
-
if (raw.includes('{{')) return clip(raw.split(/[?#]/)[0]);
|
|
55
|
-
try {
|
|
56
|
-
const u = new URL(raw);
|
|
57
|
-
return `${u.origin}${u.pathname}`;
|
|
58
|
-
} catch {
|
|
59
|
-
return '(invalid url)';
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function targetOf(target) {
|
|
64
|
-
if (!target || typeof target !== 'object') return undefined;
|
|
65
|
-
const out = {};
|
|
66
|
-
for (const key of ['ref', 'role', 'name', 'label', 'text', 'within']) if (typeof target[key] === 'string') out[key] = clip(target[key]);
|
|
67
|
-
if (typeof target.href === 'string') out.href = safeUrl(target.href) ?? clip(target.href);
|
|
68
|
-
return out;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** What a call asked for, without values. */
|
|
72
|
-
function describeArgs(args = {}) {
|
|
73
|
-
if (!args || typeof args !== 'object') return {};
|
|
74
|
-
const out = {};
|
|
75
|
-
if (args.target) out.target = targetOf(args.target);
|
|
76
|
-
if (args.expect && typeof args.expect === 'object') out.expect = Object.keys(args.expect);
|
|
77
|
-
if (args.url !== undefined) out.url = safeUrl(args.url);
|
|
78
|
-
if (args.value !== undefined) out.valueLength = String(args.value).length;
|
|
79
|
-
if (args.option && typeof args.option === 'object') out.option = Object.keys(args.option);
|
|
80
|
-
for (const key of PLAIN_ARGS) if (args[key] !== undefined && typeof args[key] !== 'object') out[key] = clip(args[key]);
|
|
81
|
-
if (args.params && typeof args.params === 'object') out.params = Object.keys(args.params);
|
|
82
|
-
if (Array.isArray(args.paramSets)) out.paramSets = args.paramSets.length;
|
|
83
|
-
if (Array.isArray(args.steps)) out.steps = args.steps.length;
|
|
84
|
-
if (args.approvalId) out.approval = true;
|
|
85
|
-
if (typeof args.message === 'string') out.messageLength = args.message.length;
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** What came back, without page text unless verbose. */
|
|
90
|
-
function describeResult(env = {}) {
|
|
91
|
-
const out = { ok: env.ok === true, state: env.state };
|
|
92
|
-
if (env.error?.code) out.code = env.error.code;
|
|
93
|
-
const page = safeUrl(env.page?.url);
|
|
94
|
-
if (page) out.page = page;
|
|
95
|
-
const evidence = {};
|
|
96
|
-
for (const key of EVIDENCE_KEYS) if (env.evidence?.[key] !== undefined) evidence[key] = env.evidence[key];
|
|
97
|
-
if (Object.keys(evidence).length) out.evidence = evidence;
|
|
98
|
-
if (Array.isArray(env.candidates)) out.candidates = env.candidates.length;
|
|
99
|
-
if (mode() === 'verbose') {
|
|
100
|
-
if (env.evidence?.busyIndicator) (out.evidence ??= {}).busyIndicator = env.evidence.busyIndicator;
|
|
101
|
-
out.summary = env.summary;
|
|
102
|
-
if (env.nextAction) out.nextAction = env.nextAction;
|
|
103
|
-
}
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let pruned = false;
|
|
108
|
-
function prune() {
|
|
109
|
-
if (pruned) return;
|
|
110
|
-
pruned = true;
|
|
111
|
-
const cutoff = Date.now() - KEEP_DAYS * 86_400_000;
|
|
112
|
-
try {
|
|
113
|
-
for (const file of fs.readdirSync(logDir())) {
|
|
114
|
-
const day = Date.parse(file.slice(0, 10));
|
|
115
|
-
if (/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(file) && day < cutoff) fs.unlinkSync(path.join(logDir(), file));
|
|
116
|
-
}
|
|
117
|
-
} catch { /* nothing to prune */ }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* @param {{ session: string, client: () => string }} who
|
|
122
|
-
* @returns {{ write(entry: object): void, file(): string }}
|
|
123
|
-
*/
|
|
124
|
-
/**
|
|
125
|
-
* One file call per entry (audit AU-21): the folder is created once per
|
|
126
|
-
* process and per folder, and the day file's size is read once per day file
|
|
127
|
-
* and then tracked in memory.
|
|
128
|
-
*/
|
|
129
|
-
let madeDir = null;
|
|
130
|
-
function createSessionLog({ session, client }) {
|
|
131
|
-
let full = false;
|
|
132
|
-
let current = { file: null, size: 0 };
|
|
133
|
-
function write(entry) {
|
|
134
|
-
if (mode() === 'off') return;
|
|
135
|
-
try {
|
|
136
|
-
const dir = logDir();
|
|
137
|
-
if (madeDir !== dir) {
|
|
138
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
139
|
-
madeDir = dir;
|
|
140
|
-
}
|
|
141
|
-
prune();
|
|
142
|
-
const file = dayFile();
|
|
143
|
-
if (current.file !== file) {
|
|
144
|
-
let size = 0;
|
|
145
|
-
try {
|
|
146
|
-
size = fs.statSync(file).size;
|
|
147
|
-
} catch { /* a new day */ }
|
|
148
|
-
current = { file, size };
|
|
149
|
-
full = false;
|
|
150
|
-
}
|
|
151
|
-
if (current.size > MAX_DAY_BYTES) {
|
|
152
|
-
if (!full) fs.appendFileSync(file, `${JSON.stringify({ ts: new Date().toISOString(), session, kind: 'log_full', note: 'Daily log limit reached; further entries today are dropped.' })}\n`);
|
|
153
|
-
full = true;
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const line = `${JSON.stringify({ ts: new Date().toISOString(), session, client: client(), ...entry })}\n`;
|
|
157
|
-
fs.appendFileSync(file, line, { mode: 0o600 });
|
|
158
|
-
current.size += Buffer.byteLength(line);
|
|
159
|
-
} catch {
|
|
160
|
-
// logging must never break a call; look at the folder again next time
|
|
161
|
-
madeDir = null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return { write, file: () => dayFile() };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** The last `limit` entries from the newest `days` daily files, optionally for one session. */
|
|
168
|
-
function readLog({ days = 2, limit = 50, session } = {}) {
|
|
169
|
-
const out = [];
|
|
170
|
-
for (let i = days - 1; i >= 0; i--) {
|
|
171
|
-
const file = dayFile(new Date(Date.now() - i * 86_400_000));
|
|
172
|
-
let lines = [];
|
|
173
|
-
try {
|
|
174
|
-
lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
175
|
-
} catch {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
for (const line of lines) {
|
|
179
|
-
try {
|
|
180
|
-
const entry = JSON.parse(line);
|
|
181
|
-
if (!session || entry.session === session) out.push(entry);
|
|
182
|
-
} catch { /* skip a torn line */ }
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return out.slice(-limit);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
module.exports = { createSessionLog, readLog, describeArgs, describeResult, safeUrl, logDir, dayFile, KEEP_DAYS };
|
|
1
|
+
/**
|
|
2
|
+
* BrowserZap — session log: what every session did, for transparency and debugging.
|
|
3
|
+
*
|
|
4
|
+
* One JSON line per tool call or workflow step, appended to
|
|
5
|
+
* <config dir>/logs/YYYY-MM-DD.jsonl (0600, kept 14 days)
|
|
6
|
+
* by the MCP server (agent calls, agent-run workflows) and the bridge's panel
|
|
7
|
+
* runner (workflows run from the side panel).
|
|
8
|
+
*
|
|
9
|
+
* Recorded: time, session and client, tool, the target as it was named, the
|
|
10
|
+
* outcome (ok, state, error code), timing, and evidence useful for debugging
|
|
11
|
+
* (waited-for-target time, click path, risk, navigation, where a workflow
|
|
12
|
+
* stopped), plus the page as origin + path.
|
|
13
|
+
*
|
|
14
|
+
* Never recorded: typed values (only their length), parameter values (only
|
|
15
|
+
* their names), URL query strings or fragments, page text, approval ids,
|
|
16
|
+
* screenshots. Result summaries quote page content, so they are added only
|
|
17
|
+
* with BROWSERZAP_LOG=verbose. BROWSERZAP_LOG=off disables the file.
|
|
18
|
+
*
|
|
19
|
+
* Read it with `npm run logs` (bridge-server/index.js --logs).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const { configDir } = require('./secret');
|
|
28
|
+
|
|
29
|
+
const KEEP_DAYS = 14;
|
|
30
|
+
const MAX_DAY_BYTES = 20 * 1024 * 1024;
|
|
31
|
+
const MAX_TEXT = 80;
|
|
32
|
+
const EVIDENCE_KEYS = [
|
|
33
|
+
'waitedForTargetMs', 'clickVia', 'input', 'risk', 'navigated', 'urlChanged', 'domChanged', 'noObservableChange',
|
|
34
|
+
'expectationMet', 'verified', 'redirected', 'crossOrigin', 'reResolvedStaleRef', 'elapsedMs',
|
|
35
|
+
'workflow', 'version', 'stoppedAt', 'completedSteps', 'totalSteps', 'completedRuns',
|
|
36
|
+
// slow-server evidence (docs/AUDIT.md AU-02, AU-06, AU-07, AU-05)
|
|
37
|
+
'loaded', 'httpStatus', 'retryable', 'ttfbMs', 'domContentLoadedMs', 'loadMs', 'waitingFor', 'pageBusyMs', 'waitedForPersonMs',
|
|
38
|
+
// why a step was slow or failed (AU-12); slowestRequest is origin + path only. busyIndicator is page text: verbose only.
|
|
39
|
+
'likelyCause', 'slowestRequest', 'pendingRequests', 'readyState', 'slowSteps',
|
|
40
|
+
];
|
|
41
|
+
const PLAIN_ARGS = [
|
|
42
|
+
'key', 'direction', 'amount', 'timeoutMs', 'maxElements', 'includeText', 'maxTextChars', 'within', 'tabId',
|
|
43
|
+
'name', 'startAt', 'version', 'action', 'restoreVersion', 'limit', 'includeTab', 'waitMs', 'overwrite', 'keepValues',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const mode = () => (process.env.BROWSERZAP_LOG || 'standard').toLowerCase();
|
|
47
|
+
const logDir = () => path.join(configDir(), 'logs');
|
|
48
|
+
const dayFile = (date = new Date()) => path.join(logDir(), `${date.toISOString().slice(0, 10)}.jsonl`);
|
|
49
|
+
const clip = (v) => (typeof v === 'string' && v.length > MAX_TEXT ? `${v.slice(0, MAX_TEXT)}…` : v);
|
|
50
|
+
|
|
51
|
+
/** origin + path only: query strings and fragments often carry tokens or personal data. */
|
|
52
|
+
function safeUrl(raw) {
|
|
53
|
+
if (typeof raw !== 'string' || !raw) return undefined;
|
|
54
|
+
if (raw.includes('{{')) return clip(raw.split(/[?#]/)[0]);
|
|
55
|
+
try {
|
|
56
|
+
const u = new URL(raw);
|
|
57
|
+
return `${u.origin}${u.pathname}`;
|
|
58
|
+
} catch {
|
|
59
|
+
return '(invalid url)';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function targetOf(target) {
|
|
64
|
+
if (!target || typeof target !== 'object') return undefined;
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const key of ['ref', 'role', 'name', 'label', 'text', 'within']) if (typeof target[key] === 'string') out[key] = clip(target[key]);
|
|
67
|
+
if (typeof target.href === 'string') out.href = safeUrl(target.href) ?? clip(target.href);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What a call asked for, without values. */
|
|
72
|
+
function describeArgs(args = {}) {
|
|
73
|
+
if (!args || typeof args !== 'object') return {};
|
|
74
|
+
const out = {};
|
|
75
|
+
if (args.target) out.target = targetOf(args.target);
|
|
76
|
+
if (args.expect && typeof args.expect === 'object') out.expect = Object.keys(args.expect);
|
|
77
|
+
if (args.url !== undefined) out.url = safeUrl(args.url);
|
|
78
|
+
if (args.value !== undefined) out.valueLength = String(args.value).length;
|
|
79
|
+
if (args.option && typeof args.option === 'object') out.option = Object.keys(args.option);
|
|
80
|
+
for (const key of PLAIN_ARGS) if (args[key] !== undefined && typeof args[key] !== 'object') out[key] = clip(args[key]);
|
|
81
|
+
if (args.params && typeof args.params === 'object') out.params = Object.keys(args.params);
|
|
82
|
+
if (Array.isArray(args.paramSets)) out.paramSets = args.paramSets.length;
|
|
83
|
+
if (Array.isArray(args.steps)) out.steps = args.steps.length;
|
|
84
|
+
if (args.approvalId) out.approval = true;
|
|
85
|
+
if (typeof args.message === 'string') out.messageLength = args.message.length;
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What came back, without page text unless verbose. */
|
|
90
|
+
function describeResult(env = {}) {
|
|
91
|
+
const out = { ok: env.ok === true, state: env.state };
|
|
92
|
+
if (env.error?.code) out.code = env.error.code;
|
|
93
|
+
const page = safeUrl(env.page?.url);
|
|
94
|
+
if (page) out.page = page;
|
|
95
|
+
const evidence = {};
|
|
96
|
+
for (const key of EVIDENCE_KEYS) if (env.evidence?.[key] !== undefined) evidence[key] = env.evidence[key];
|
|
97
|
+
if (Object.keys(evidence).length) out.evidence = evidence;
|
|
98
|
+
if (Array.isArray(env.candidates)) out.candidates = env.candidates.length;
|
|
99
|
+
if (mode() === 'verbose') {
|
|
100
|
+
if (env.evidence?.busyIndicator) (out.evidence ??= {}).busyIndicator = env.evidence.busyIndicator;
|
|
101
|
+
out.summary = env.summary;
|
|
102
|
+
if (env.nextAction) out.nextAction = env.nextAction;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let pruned = false;
|
|
108
|
+
function prune() {
|
|
109
|
+
if (pruned) return;
|
|
110
|
+
pruned = true;
|
|
111
|
+
const cutoff = Date.now() - KEEP_DAYS * 86_400_000;
|
|
112
|
+
try {
|
|
113
|
+
for (const file of fs.readdirSync(logDir())) {
|
|
114
|
+
const day = Date.parse(file.slice(0, 10));
|
|
115
|
+
if (/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(file) && day < cutoff) fs.unlinkSync(path.join(logDir(), file));
|
|
116
|
+
}
|
|
117
|
+
} catch { /* nothing to prune */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{ session: string, client: () => string }} who
|
|
122
|
+
* @returns {{ write(entry: object): void, file(): string }}
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* One file call per entry (audit AU-21): the folder is created once per
|
|
126
|
+
* process and per folder, and the day file's size is read once per day file
|
|
127
|
+
* and then tracked in memory.
|
|
128
|
+
*/
|
|
129
|
+
let madeDir = null;
|
|
130
|
+
function createSessionLog({ session, client }) {
|
|
131
|
+
let full = false;
|
|
132
|
+
let current = { file: null, size: 0 };
|
|
133
|
+
function write(entry) {
|
|
134
|
+
if (mode() === 'off') return;
|
|
135
|
+
try {
|
|
136
|
+
const dir = logDir();
|
|
137
|
+
if (madeDir !== dir) {
|
|
138
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
139
|
+
madeDir = dir;
|
|
140
|
+
}
|
|
141
|
+
prune();
|
|
142
|
+
const file = dayFile();
|
|
143
|
+
if (current.file !== file) {
|
|
144
|
+
let size = 0;
|
|
145
|
+
try {
|
|
146
|
+
size = fs.statSync(file).size;
|
|
147
|
+
} catch { /* a new day */ }
|
|
148
|
+
current = { file, size };
|
|
149
|
+
full = false;
|
|
150
|
+
}
|
|
151
|
+
if (current.size > MAX_DAY_BYTES) {
|
|
152
|
+
if (!full) fs.appendFileSync(file, `${JSON.stringify({ ts: new Date().toISOString(), session, kind: 'log_full', note: 'Daily log limit reached; further entries today are dropped.' })}\n`);
|
|
153
|
+
full = true;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const line = `${JSON.stringify({ ts: new Date().toISOString(), session, client: client(), ...entry })}\n`;
|
|
157
|
+
fs.appendFileSync(file, line, { mode: 0o600 });
|
|
158
|
+
current.size += Buffer.byteLength(line);
|
|
159
|
+
} catch {
|
|
160
|
+
// logging must never break a call; look at the folder again next time
|
|
161
|
+
madeDir = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { write, file: () => dayFile() };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The last `limit` entries from the newest `days` daily files, optionally for one session. */
|
|
168
|
+
function readLog({ days = 2, limit = 50, session } = {}) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
171
|
+
const file = dayFile(new Date(Date.now() - i * 86_400_000));
|
|
172
|
+
let lines = [];
|
|
173
|
+
try {
|
|
174
|
+
lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
try {
|
|
180
|
+
const entry = JSON.parse(line);
|
|
181
|
+
if (!session || entry.session === session) out.push(entry);
|
|
182
|
+
} catch { /* skip a torn line */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out.slice(-limit);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { createSessionLog, readLog, describeArgs, describeResult, safeUrl, logDir, dayFile, KEEP_DAYS };
|
package/bridge-server/tools.js
CHANGED
|
@@ -289,7 +289,9 @@ const MCP_TOOLS = [
|
|
|
289
289
|
},
|
|
290
290
|
{
|
|
291
291
|
name: 'browser_new_tab',
|
|
292
|
-
description: 'Open a
|
|
292
|
+
description: 'Open a helper tab in this session\'s tab group and make it the session tab. Only for a side trip ' +
|
|
293
|
+
'(research, a comparison): for a new task, navigate the main tab instead. Close the helper with browser_close_tab ' +
|
|
294
|
+
'when done. At 5 tabs, the least recently used helper is closed to make room.',
|
|
293
295
|
inputSchema: {
|
|
294
296
|
type: 'object',
|
|
295
297
|
additionalProperties: false,
|
|
@@ -311,6 +313,16 @@ const MCP_TOOLS = [
|
|
|
311
313
|
properties: { tabId: { type: 'integer', minimum: 0 } },
|
|
312
314
|
},
|
|
313
315
|
},
|
|
316
|
+
{
|
|
317
|
+
name: 'browser_close_tab',
|
|
318
|
+
description: 'Close a helper tab of this session (the current tab when tabId is omitted) and go back to the main ' +
|
|
319
|
+
'tab. The main tab cannot be closed: navigate it for the next task.',
|
|
320
|
+
inputSchema: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
additionalProperties: false,
|
|
323
|
+
properties: { tabId: { type: 'integer', minimum: 0 } },
|
|
324
|
+
},
|
|
325
|
+
},
|
|
314
326
|
{
|
|
315
327
|
name: 'browser_set_session_name',
|
|
316
328
|
description: 'Name this session; the name appears on the Chrome tab group so the user knows what it is doing.',
|
|
@@ -534,6 +546,7 @@ const ANNOTATIONS = {
|
|
|
534
546
|
browser_new_tab: ['New tab', { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }],
|
|
535
547
|
browser_list_tabs: ['List tabs', LOCAL_READ],
|
|
536
548
|
browser_switch_tab: ['Switch tab', { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }],
|
|
549
|
+
browser_close_tab: ['Close helper tab', { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }],
|
|
537
550
|
browser_set_session_name: ['Name session', { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }],
|
|
538
551
|
browser_list_profiles: ['List Chrome profiles', LOCAL_READ],
|
|
539
552
|
browser_select_profile: ['Choose Chrome profile', { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }],
|
|
@@ -577,6 +590,7 @@ const TOOL_TO_ACTION = {
|
|
|
577
590
|
browser_new_tab: 'new_tab',
|
|
578
591
|
browser_list_tabs: 'list_tabs',
|
|
579
592
|
browser_switch_tab: 'switch_tab',
|
|
593
|
+
browser_close_tab: 'close_tab',
|
|
580
594
|
};
|
|
581
595
|
|
|
582
596
|
/**
|