Skip to content

Commit e6d7f98

Browse files
committed
feat: enhance log store functionality with project-specific operations
- Updated the `clear` method in `LogStore` to support project-based clearing alongside session-based operations. - Introduced `baselineProject` and `baselineGlobal` methods for setting baselines specific to projects and globally. - Modified log retrieval to consider project-specific baselines, improving log filtering accuracy. - Updated `clearLogs` and `getLogs` tools to utilize new project parameters for enhanced log management.
1 parent 67a9148 commit e6d7f98

6 files changed

Lines changed: 143 additions & 47 deletions

File tree

packages/mcp/src/store.ts

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,61 @@ export class LogStore {
3333
this.entries.push(entry);
3434
}
3535

36-
clear(options?: { session?: string; scope?: 'soft' | 'hard' }) {
36+
clear(options?: { session?: string; scope?: 'soft' | 'hard'; project?: string }) {
3737
const scope = options?.scope || 'hard';
3838
const session = options?.session;
39+
const project = options?.project;
3940

4041
if (scope === 'soft') {
41-
const key = session || '__global__';
42-
this.baselineTimestamps.set(key, Date.now());
43-
} else {
44-
if (session) {
45-
this.entries = this.entries.filter(e => (e.sessionId || '').slice(0, 8) !== session);
46-
this.baselineTimestamps.delete(session);
42+
if (project && project.trim()) {
43+
this.baselineTimestamps.set(`project:${project}`, Date.now());
44+
} else if (session && session.trim()) {
45+
this.baselineTimestamps.set(`session:${session.slice(0, 8)}`, Date.now());
4746
} else {
48-
this.entries.length = 0;
49-
this.baselineTimestamps.clear();
47+
// Global soft baseline (discouraged for multi-project, kept for backward compat)
48+
this.baselineTimestamps.set('__global__', Date.now());
5049
}
50+
return;
5151
}
52+
53+
// Hard clear
54+
if (project && project.trim()) {
55+
// Remove only entries for this project and drop its baseline
56+
const p = project;
57+
this.entries = this.entries.filter(e => (e.project || '') !== p);
58+
this.baselineTimestamps.delete(`project:${p}`);
59+
return;
60+
}
61+
62+
if (session && session.trim()) {
63+
const s = session.slice(0, 8);
64+
this.entries = this.entries.filter(e => (e.sessionId || '').slice(0, 8) !== s);
65+
this.baselineTimestamps.delete(`session:${s}`);
66+
return;
67+
}
68+
69+
// Global hard clear
70+
this.entries.length = 0;
71+
this.baselineTimestamps.clear();
5272
}
5373

5474
/** Set a baseline timestamp without deleting entries. If session omitted, use global baseline. */
5575
baseline(session?: string, when: number = Date.now()) {
56-
const key = session || '__global__';
76+
const key = session ? `session:${session.slice(0, 8)}` : '__global__';
5777
this.baselineTimestamps.set(key, when);
5878
}
5979

80+
/** Set a baseline for a specific project (recommended for multi-project use). */
81+
baselineProject(project: string, when: number = Date.now()) {
82+
if (!project || !project.trim()) return;
83+
this.baselineTimestamps.set(`project:${project}`, when);
84+
}
85+
86+
/** Set a global baseline (discouraged for multi-project; kept for backward compatibility). */
87+
baselineGlobal(when: number = Date.now()) {
88+
this.baselineTimestamps.set('__global__', when);
89+
}
90+
6091
toText(session?: string): string {
6192
return this.snapshot(session).map((e) => {
6293
const sid = (e.sessionId || 'anon').slice(0, 8);
@@ -76,17 +107,23 @@ export class LogStore {
76107
let items = this.entries.slice();
77108
if (session) items = items.filter(e => (e.sessionId || '').slice(0, 8) === session);
78109

79-
const baselineKey = session || '__global__';
80-
const baseline = this.baselineTimestamps.get(baselineKey);
81-
if (baseline) {
82-
items = items.filter(e => !e.time || e.time >= baseline);
83-
if (items.length === 0 && this.entries.length > 0) {
84-
// eslint-disable-next-line no-console
85-
console.warn(`All ${this.entries.length} logs filtered out by baseline. Check timestamp format.`);
86-
}
110+
const globalTs = this.baselineTimestamps.get('__global__') || 0;
111+
const sessionTs = session ? (this.baselineTimestamps.get(`session:${session}`) || 0) : 0;
112+
113+
const filtered = items.filter((e) => {
114+
const t = e.time || 0;
115+
const projectTs = e.project ? (this.baselineTimestamps.get(`project:${e.project}`) || 0) : 0;
116+
// Session baseline only applies when caller filtered by that session.
117+
const threshold = Math.max(globalTs, sessionTs, projectTs);
118+
return t === 0 || t >= threshold;
119+
});
120+
121+
if (filtered.length === 0 && this.entries.length > 0 && (globalTs || sessionTs)) {
122+
// eslint-disable-next-line no-console
123+
console.warn(`All ${this.entries.length} logs filtered out by baseline. Check timestamp format or baseline scope.`);
87124
}
88125

89-
return items;
126+
return filtered;
90127
}
91128
}
92129

packages/mcp/src/tools/clearLogs.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,8 @@ export function registerClearLogsTool(ctx: McpToolContext) {
1313
const safeArgs = args || {} as any;
1414
const { session, scope = 'hard', project } = safeArgs as typeof ClearLogsSchema['_output'];
1515
const validSession = validateSessionId(session);
16-
if (project) {
17-
// Project-scoped hard clear by filtering entries not matching the project
18-
// For soft clear, set baseline without deleting
19-
if (scope === 'soft') {
20-
// Use global baseline for project-scoped soft clear by current time; retrieval filters by project
21-
// This keeps implementation simple while honoring freshness for the project
22-
store.clear({ scope: 'soft' });
23-
} else {
24-
// Rebuild entries array excluding the project. Since entries is private, leverage snapshot and re-append
25-
const remaining = store.snapshot().filter(e => (e.project || '') !== project);
26-
// Replace internal state: simulate with hard clear then re-append
27-
store.clear({ scope: 'hard' });
28-
for (const e of remaining) store.append(e);
29-
}
30-
} else {
31-
store.clear({ session: validSession, scope });
32-
}
16+
17+
store.clear({ session: validSession, scope, project });
3318

3419
let message = 'Browser log buffer ';
3520
message += scope === 'soft' ? 'baseline set' : 'cleared';

packages/mcp/src/tools/getLogs.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,17 @@ export function registerGetLogsTool(ctx: McpToolContext) {
111111
}).join('\n');
112112
}
113113

114-
// Auto-baseline unless stackedMode
114+
// Auto-baseline unless stackedMode (prefer project/session scopes; avoid global)
115115
try {
116116
if (autoBaseline && !stackedMode) {
117-
// baseline per project if selected; else by session or global
118-
const baselineSession = validSession;
119-
store.baseline(baselineSession);
117+
if (project) {
118+
store.baselineProject(project);
119+
} else if (autoProject) {
120+
store.baselineProject(autoProject);
121+
} else if (validSession) {
122+
store.baseline(validSession);
123+
}
124+
// No global baseline by default to avoid cross-project interference
120125
}
121126
} catch {}
122127

packages/next/src/route.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
import type { NextRequest } from 'next/server';
22
import { NextResponse } from 'next/server';
33

4+
const MCP_BASE = (process.env.BROWSER_ECHO_MCP_URL || 'http://127.0.0.1:5179').replace(/\/$/, '').replace(/\/mcp$/i, '');
5+
let __probeStarted = false;
6+
async function __probeHealthOnce(): Promise<boolean> {
7+
try {
8+
const ctrl = new AbortController();
9+
const t = setTimeout(() => ctrl.abort(), 400);
10+
const res = await fetch(`${MCP_BASE}/health`, { signal: ctrl.signal as any, cache: 'no-store' as any });
11+
clearTimeout(t);
12+
return !!res && res.ok;
13+
} catch { return false; }
14+
}
15+
function __startHealthProbe() {
16+
if (__probeStarted) return;
17+
__probeStarted = true;
18+
setInterval(async () => {
19+
if (__hasForwardedOnce) return;
20+
const ok = await __probeHealthOnce();
21+
if (ok) __hasForwardedOnce = true;
22+
}, 1500);
23+
}
24+
__startHealthProbe();
25+
426
// Simplified: fixed single-server URL or env override
527

628
export type BrowserLogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
@@ -19,10 +41,8 @@ export async function POST(req: NextRequest) {
1941
catch { return new NextResponse('invalid JSON', { status: 400 }); }
2042
if (!payload || !Array.isArray(payload.entries)) return new NextResponse('invalid payload', { status: 400 });
2143

22-
// Fixed resolution (single-server): env or default localhost:5179 (strip optional /mcp suffix)
23-
const baseUrl = (process.env.BROWSER_ECHO_MCP_URL || 'http://127.0.0.1:5179').replace(/\/$/, '').replace(/\/mcp$/i, '');
44+
const baseUrl = MCP_BASE;
2445
const mcp = { url: baseUrl, routeLogs: '/__client-logs' } as const;
25-
// No background probes; only flip after a successful POST
2646

2747
// Forward to MCP server (fire-and-forget) and update connection state
2848
try {

packages/nuxt/src/runtime/server/handler.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
22

3+
const MCP_BASE = (process.env.BROWSER_ECHO_MCP_URL || 'http://127.0.0.1:5179').replace(/\/$/, '').replace(/\/mcp$/i, '');
4+
let __probeStarted = false;
5+
async function __probeHealthOnce(): Promise<boolean> {
6+
try {
7+
const ctrl = new AbortController();
8+
const t = setTimeout(() => ctrl.abort(), 400);
9+
const res = await fetch(`${MCP_BASE}/health`, { signal: ctrl.signal as any, cache: 'no-store' as any });
10+
clearTimeout(t);
11+
return !!res && res.ok;
12+
} catch { return false; }
13+
}
14+
function __startHealthProbe() {
15+
if (__probeStarted) return;
16+
__probeStarted = true;
17+
setInterval(async () => {
18+
if (__hasForwardedOnce) return;
19+
const ok = await __probeHealthOnce();
20+
if (ok) __hasForwardedOnce = true;
21+
}, 1500);
22+
}
23+
__startHealthProbe();
24+
325
// Simplified: fixed single-server URL or env override
426

527
// Simplified: resolve MCP from project-local JSON once; no fallback
@@ -19,10 +41,8 @@ export default defineEventHandler(async (event) => {
1941
setResponseStatus(event, 400); return 'invalid payload';
2042
}
2143

22-
// Fixed single-server: env or default localhost:5179 (strip optional /mcp suffix)
23-
const baseUrl = (process.env.BROWSER_ECHO_MCP_URL || 'http://127.0.0.1:5179').replace(/\/$/, '').replace(/\/mcp$/i, '');
44+
const baseUrl = MCP_BASE;
2445
const mcp = { url: baseUrl, routeLogs: '/__client-logs' } as const;
25-
// No background probe
2646

2747
// Forward to MCP server (fire-and-forget) and update connection state
2848
try {

packages/vite/src/index.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,36 @@ function attachMiddleware(server: any, options: ResolvedOptions) {
120120

121121
computeBaseOnce();
122122

123-
// No background probes; start printing locally until a forward succeeds
123+
// Start a small background probe to detect MCP coming online after Vite
124+
startHealthProbe();
125+
126+
async function probeHealth(): Promise<boolean> {
127+
try {
128+
const ctrl = new AbortController();
129+
const t = setTimeout(() => ctrl.abort(), 400);
130+
const res = await fetch(`${resolvedBase}/health`, { signal: ctrl.signal as any, cache: 'no-store' as any });
131+
clearTimeout(t);
132+
return !!res && res.ok;
133+
} catch {
134+
return false;
135+
}
136+
}
137+
138+
function startHealthProbe() {
139+
// Only starts once per dev server process
140+
let started = false;
141+
if (started) return;
142+
started = true;
143+
const interval = setInterval(async () => {
144+
if (hasForwardedSuccessfully) return; // already forwarding
145+
const ok = await probeHealth();
146+
if (ok) {
147+
hasForwardedSuccessfully = true;
148+
announce(`${options.tag} forwarding logs to MCP ingest at ${resolvedIngest}`);
149+
}
150+
}, 1500);
151+
// NOTE: we intentionally do not clear the interval; it's cheap and guarded above.
152+
}
124153

125154
server.middlewares.use(options.route, (req: import('http').IncomingMessage, res: import('http').ServerResponse, next: Function) => {
126155
if (req.method !== 'POST') return next();

0 commit comments

Comments
 (0)