@harness-fe/mcp-server 4.0.0-next.2 → 4.0.0-next.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/dist/bin.d.ts +2 -0
- package/dist/bin.js +15 -0
- package/dist/daemon.d.ts +3 -3
- package/dist/daemon.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +42 -16
- package/dist/mcpHttp.d.ts +2 -2
- package/dist/mcpHttp.js +8 -2
- package/package.json +5 -7
- package/src/bin.ts +19 -0
- package/src/daemon.ts +3 -3
- package/src/experimental.test.ts +2 -2
- package/src/index.ts +4 -4
- package/src/mcp.ts +44 -20
- package/src/mcpHttp.test.ts +3 -3
- package/src/mcpHttp.ts +10 -4
- package/src/mcpLayer.e2e.test.ts +2 -2
- package/src/newCapabilities.e2e.test.ts +3 -3
- package/dist/auth.d.ts +0 -53
- package/dist/auth.js +0 -212
- package/dist/bridge.d.ts +0 -323
- package/dist/bridge.js +0 -1618
- package/dist/cli.d.ts +0 -18
- package/dist/cli.js +0 -293
- package/dist/dashboardApi.d.ts +0 -40
- package/dist/dashboardApi.js +0 -142
- package/dist/dashboardSpa.d.ts +0 -18
- package/dist/dashboardSpa.js +0 -180
- package/dist/dashboardUrl.d.ts +0 -13
- package/dist/dashboardUrl.js +0 -18
- package/dist/eventsHandler.d.ts +0 -24
- package/dist/eventsHandler.js +0 -114
- package/dist/identity.d.ts +0 -90
- package/dist/identity.js +0 -123
- package/dist/openBrowser.d.ts +0 -33
- package/dist/openBrowser.js +0 -63
- package/dist/remoteBridge.d.ts +0 -61
- package/dist/remoteBridge.js +0 -307
- package/dist/replayCreate.d.ts +0 -36
- package/dist/replayCreate.js +0 -156
- package/dist/replayViewer.d.ts +0 -20
- package/dist/replayViewer.js +0 -168
- package/dist/sessionRouter.d.ts +0 -45
- package/dist/sessionRouter.js +0 -88
- package/dist/store/JsonMemoryStore.d.ts +0 -52
- package/dist/store/JsonMemoryStore.js +0 -119
- package/dist/store/JsonTaskStore.d.ts +0 -21
- package/dist/store/JsonTaskStore.js +0 -53
- package/dist/store/JsonlStore.d.ts +0 -128
- package/dist/store/JsonlStore.js +0 -1172
- package/dist/store/MemoryEventStore.d.ts +0 -47
- package/dist/store/MemoryEventStore.js +0 -111
- package/dist/store/WriteQueue.d.ts +0 -51
- package/dist/store/WriteQueue.js +0 -142
- package/dist/store/index.d.ts +0 -6
- package/dist/store/index.js +0 -5
- package/dist/store/types.d.ts +0 -427
- package/dist/store/types.js +0 -19
- package/dist/visitorTimeline.d.ts +0 -24
- package/dist/visitorTimeline.js +0 -68
- package/src/auth.test.ts +0 -90
- package/src/auth.ts +0 -248
- package/src/bridge-auth.test.ts +0 -196
- package/src/bridge.test.ts +0 -1708
- package/src/bridge.ts +0 -1854
- package/src/cli.ts +0 -338
- package/src/dashboardApi.test.ts +0 -235
- package/src/dashboardApi.ts +0 -184
- package/src/dashboardSpa.test.ts +0 -239
- package/src/dashboardSpa.ts +0 -195
- package/src/dashboardUrl.test.ts +0 -46
- package/src/dashboardUrl.ts +0 -28
- package/src/eventsHandler.test.ts +0 -247
- package/src/eventsHandler.ts +0 -136
- package/src/identity.test.ts +0 -109
- package/src/identity.ts +0 -137
- package/src/openBrowser.test.ts +0 -103
- package/src/openBrowser.ts +0 -81
- package/src/remoteBridge.test.ts +0 -119
- package/src/remoteBridge.ts +0 -404
- package/src/replay.test.ts +0 -271
- package/src/replayCreate.ts +0 -194
- package/src/replayViewer.ts +0 -173
- package/src/sessionRouter.ts +0 -119
- package/src/store/JsonMemoryStore.test.ts +0 -175
- package/src/store/JsonMemoryStore.ts +0 -128
- package/src/store/JsonTaskStore.test.ts +0 -212
- package/src/store/JsonTaskStore.ts +0 -59
- package/src/store/JsonlStore.test.ts +0 -1538
- package/src/store/JsonlStore.ts +0 -1325
- package/src/store/MemoryEventStore.test.ts +0 -119
- package/src/store/MemoryEventStore.ts +0 -151
- package/src/store/WriteQueue.ts +0 -165
- package/src/store/identityTagging.test.ts +0 -67
- package/src/store/index.ts +0 -29
- package/src/store/types.ts +0 -532
- package/src/visitorTimeline.test.ts +0 -197
- package/src/visitorTimeline.ts +0 -89
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import { JsonlStore } from './store/index.js';
|
|
7
|
-
import { buildVisitorTimeline } from './visitorTimeline.js';
|
|
8
|
-
|
|
9
|
-
function openSession(
|
|
10
|
-
store: JsonlStore,
|
|
11
|
-
projectId: string,
|
|
12
|
-
tabId: string,
|
|
13
|
-
sessionId = randomUUID(),
|
|
14
|
-
startedAt = Date.now(),
|
|
15
|
-
): string {
|
|
16
|
-
store.upsertTab(tabId, { connectedAt: startedAt });
|
|
17
|
-
store.upsertSession(sessionId, {
|
|
18
|
-
tabId,
|
|
19
|
-
startedAt,
|
|
20
|
-
participants: [{ projectId, joinedAt: startedAt }],
|
|
21
|
-
});
|
|
22
|
-
return sessionId;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('buildVisitorTimeline', () => {
|
|
26
|
-
let dir: string;
|
|
27
|
-
let store: JsonlStore;
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
dir = mkdtempSync(join(tmpdir(), 'visitor-timeline-'));
|
|
31
|
-
store = new JsonlStore(dir);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
rmSync(dir, { recursive: true, force: true });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns error when visitor not found', async () => {
|
|
39
|
-
await store.flush();
|
|
40
|
-
const result = buildVisitorTimeline(store, 'no-such-visitor');
|
|
41
|
-
expect(result).toEqual({ error: 'visitor not found: no-such-visitor' });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('merges events from multiple sessions ascending by ts', async () => {
|
|
45
|
-
const visitorId = 'visitor-1';
|
|
46
|
-
const projectId = 'proj-a';
|
|
47
|
-
const tabA = 'tab-a';
|
|
48
|
-
const tabB = 'tab-b';
|
|
49
|
-
|
|
50
|
-
const sessA = openSession(store, projectId, tabA);
|
|
51
|
-
const sessB = openSession(store, projectId, tabB);
|
|
52
|
-
store.upsertVisitor(visitorId, { seenAt: Date.now(), addTabId: tabA, addProjectId: projectId });
|
|
53
|
-
store.upsertVisitor(visitorId, { addTabId: tabB });
|
|
54
|
-
|
|
55
|
-
// Interleave events across the two tabs.
|
|
56
|
-
store.appendEvent(sessA, { ts: 1000, t: 'req', tab: tabA, visitorId, d: { url: '/a1' } });
|
|
57
|
-
store.appendEvent(sessB, { ts: 1500, t: 'req', tab: tabB, visitorId, d: { url: '/b1' } });
|
|
58
|
-
store.appendEvent(sessA, { ts: 2000, t: 'res', tab: tabA, visitorId, d: { status: 200 } });
|
|
59
|
-
store.appendEvent(sessB, { ts: 2500, t: 'storage', tab: tabB, visitorId, d: { op: 'remove', key: 'token' } });
|
|
60
|
-
|
|
61
|
-
await store.flush();
|
|
62
|
-
const result = buildVisitorTimeline(store, visitorId);
|
|
63
|
-
if ('error' in result) throw new Error(result.error);
|
|
64
|
-
|
|
65
|
-
expect(result.eventCount).toBe(4);
|
|
66
|
-
expect(result.sessionCount).toBe(2);
|
|
67
|
-
expect(result.events.map((e) => e.ts)).toEqual([1000, 1500, 2000, 2500]);
|
|
68
|
-
// tabId distribution proves both tabs contributed.
|
|
69
|
-
const tabs = new Set(result.events.map((e) => e.tab));
|
|
70
|
-
expect(tabs).toEqual(new Set([tabA, tabB]));
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('drops events from other visitors that landed in the same session', async () => {
|
|
74
|
-
const visitorId = 'visitor-mine';
|
|
75
|
-
const otherVisitor = 'visitor-stranger';
|
|
76
|
-
const projectId = 'proj';
|
|
77
|
-
const tabId = 'tab-shared';
|
|
78
|
-
|
|
79
|
-
const sess = openSession(store, projectId, tabId);
|
|
80
|
-
store.upsertVisitor(visitorId, { addTabId: tabId, addProjectId: projectId });
|
|
81
|
-
|
|
82
|
-
store.appendEvent(sess, { ts: 1000, t: 'req', tab: tabId, visitorId, d: { url: '/mine' } });
|
|
83
|
-
store.appendEvent(sess, { ts: 1500, t: 'req', tab: tabId, visitorId: otherVisitor, d: { url: '/other' } });
|
|
84
|
-
store.appendEvent(sess, { ts: 2000, t: 'req', tab: tabId, visitorId, d: { url: '/mine2' } });
|
|
85
|
-
|
|
86
|
-
await store.flush();
|
|
87
|
-
const result = buildVisitorTimeline(store, visitorId);
|
|
88
|
-
if ('error' in result) throw new Error(result.error);
|
|
89
|
-
|
|
90
|
-
expect(result.eventCount).toBe(2);
|
|
91
|
-
expect(result.events.every((e) => e.visitorId === visitorId)).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('honors tabIds filter at both session-discovery and row level', async () => {
|
|
95
|
-
const visitorId = 'visitor-1';
|
|
96
|
-
const projectId = 'proj';
|
|
97
|
-
const tabA = 'tab-a';
|
|
98
|
-
const tabB = 'tab-b';
|
|
99
|
-
|
|
100
|
-
const sessA = openSession(store, projectId, tabA);
|
|
101
|
-
const sessB = openSession(store, projectId, tabB);
|
|
102
|
-
store.upsertVisitor(visitorId, { addTabId: tabA, addProjectId: projectId });
|
|
103
|
-
store.upsertVisitor(visitorId, { addTabId: tabB });
|
|
104
|
-
|
|
105
|
-
store.appendEvent(sessA, { ts: 1000, t: 'req', tab: tabA, visitorId, d: {} });
|
|
106
|
-
store.appendEvent(sessB, { ts: 2000, t: 'req', tab: tabB, visitorId, d: {} });
|
|
107
|
-
|
|
108
|
-
await store.flush();
|
|
109
|
-
const result = buildVisitorTimeline(store, visitorId, { tabIds: [tabA] });
|
|
110
|
-
if ('error' in result) throw new Error(result.error);
|
|
111
|
-
|
|
112
|
-
expect(result.eventCount).toBe(1);
|
|
113
|
-
expect(result.events[0].tab).toBe(tabA);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('honors types filter via store.tail', async () => {
|
|
117
|
-
const visitorId = 'visitor-1';
|
|
118
|
-
const projectId = 'proj';
|
|
119
|
-
const tabId = 'tab-1';
|
|
120
|
-
|
|
121
|
-
const sess = openSession(store, projectId, tabId);
|
|
122
|
-
store.upsertVisitor(visitorId, { addTabId: tabId, addProjectId: projectId });
|
|
123
|
-
|
|
124
|
-
store.appendEvent(sess, { ts: 1000, t: 'log', tab: tabId, visitorId, d: {} });
|
|
125
|
-
store.appendEvent(sess, { ts: 2000, t: 'req', tab: tabId, visitorId, d: {} });
|
|
126
|
-
store.appendEvent(sess, { ts: 3000, t: 'storage', tab: tabId, visitorId, d: {} });
|
|
127
|
-
|
|
128
|
-
await store.flush();
|
|
129
|
-
const result = buildVisitorTimeline(store, visitorId, { types: ['req', 'storage'] });
|
|
130
|
-
if ('error' in result) throw new Error(result.error);
|
|
131
|
-
|
|
132
|
-
expect(result.events.map((e) => e.t)).toEqual(['req', 'storage']);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('limit takes the newest N events and reports truncated=true', async () => {
|
|
136
|
-
const visitorId = 'visitor-1';
|
|
137
|
-
const projectId = 'proj';
|
|
138
|
-
const tabId = 'tab-1';
|
|
139
|
-
|
|
140
|
-
const sess = openSession(store, projectId, tabId);
|
|
141
|
-
store.upsertVisitor(visitorId, { addTabId: tabId, addProjectId: projectId });
|
|
142
|
-
|
|
143
|
-
for (let i = 0; i < 10; i++) {
|
|
144
|
-
store.appendEvent(sess, { ts: 1000 + i, t: 'log', tab: tabId, visitorId, d: { i } });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
await store.flush();
|
|
148
|
-
const result = buildVisitorTimeline(store, visitorId, { limit: 3 });
|
|
149
|
-
if ('error' in result) throw new Error(result.error);
|
|
150
|
-
|
|
151
|
-
expect(result.eventCount).toBe(3);
|
|
152
|
-
expect(result.truncated).toBe(true);
|
|
153
|
-
expect(result.events.map((e) => e.ts)).toEqual([1007, 1008, 1009]);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('honors explicit sessionIds (skips visitor → session discovery)', async () => {
|
|
157
|
-
const visitorId = 'visitor-1';
|
|
158
|
-
const projectId = 'proj';
|
|
159
|
-
const tabA = 'tab-a';
|
|
160
|
-
const tabB = 'tab-b';
|
|
161
|
-
|
|
162
|
-
const sessA = openSession(store, projectId, tabA);
|
|
163
|
-
const sessB = openSession(store, projectId, tabB);
|
|
164
|
-
store.upsertVisitor(visitorId, { addTabId: tabA, addProjectId: projectId });
|
|
165
|
-
store.upsertVisitor(visitorId, { addTabId: tabB });
|
|
166
|
-
|
|
167
|
-
store.appendEvent(sessA, { ts: 1000, t: 'log', tab: tabA, visitorId, d: {} });
|
|
168
|
-
store.appendEvent(sessB, { ts: 2000, t: 'log', tab: tabB, visitorId, d: {} });
|
|
169
|
-
|
|
170
|
-
await store.flush();
|
|
171
|
-
const result = buildVisitorTimeline(store, visitorId, { sessionIds: [sessA] });
|
|
172
|
-
if ('error' in result) throw new Error(result.error);
|
|
173
|
-
|
|
174
|
-
expect(result.sessionCount).toBe(1);
|
|
175
|
-
expect(result.eventCount).toBe(1);
|
|
176
|
-
expect(result.events[0].tab).toBe(tabA);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('honors since/until window', async () => {
|
|
180
|
-
const visitorId = 'visitor-1';
|
|
181
|
-
const projectId = 'proj';
|
|
182
|
-
const tabId = 'tab-1';
|
|
183
|
-
|
|
184
|
-
const sess = openSession(store, projectId, tabId);
|
|
185
|
-
store.upsertVisitor(visitorId, { addTabId: tabId, addProjectId: projectId });
|
|
186
|
-
|
|
187
|
-
store.appendEvent(sess, { ts: 1000, t: 'log', tab: tabId, visitorId, d: {} });
|
|
188
|
-
store.appendEvent(sess, { ts: 2000, t: 'log', tab: tabId, visitorId, d: {} });
|
|
189
|
-
store.appendEvent(sess, { ts: 3000, t: 'log', tab: tabId, visitorId, d: {} });
|
|
190
|
-
|
|
191
|
-
await store.flush();
|
|
192
|
-
const result = buildVisitorTimeline(store, visitorId, { since: 1500, until: 2500 });
|
|
193
|
-
if ('error' in result) throw new Error(result.error);
|
|
194
|
-
|
|
195
|
-
expect(result.events.map((e) => e.ts)).toEqual([2000]);
|
|
196
|
-
});
|
|
197
|
-
});
|
package/src/visitorTimeline.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* visitor.timeline — merge event timelines across all sessions belonging to
|
|
3
|
-
* one visitor. Pulled out of mcp.ts so the merge / filter logic is unit
|
|
4
|
-
* testable without spinning up an McpServer.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { IStore, StoreEvent } from './store/index.js';
|
|
8
|
-
|
|
9
|
-
export interface VisitorTimelineOptions {
|
|
10
|
-
since?: number;
|
|
11
|
-
until?: number;
|
|
12
|
-
types?: string | string[];
|
|
13
|
-
tabIds?: string[];
|
|
14
|
-
sessionIds?: string[];
|
|
15
|
-
limit?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface VisitorTimelineResult {
|
|
19
|
-
visitorId: string;
|
|
20
|
-
sessionCount: number;
|
|
21
|
-
eventCount: number;
|
|
22
|
-
truncated: boolean;
|
|
23
|
-
events: StoreEvent[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DEFAULT_LIMIT = 200;
|
|
27
|
-
const SESSION_DISCOVERY_PAGE = 200;
|
|
28
|
-
|
|
29
|
-
export function buildVisitorTimeline(
|
|
30
|
-
store: IStore,
|
|
31
|
-
visitorId: string,
|
|
32
|
-
opts: VisitorTimelineOptions = {},
|
|
33
|
-
): VisitorTimelineResult | { error: string } {
|
|
34
|
-
const visitor = store.getVisitor(visitorId);
|
|
35
|
-
if (!visitor) return { error: `visitor not found: ${visitorId}` };
|
|
36
|
-
|
|
37
|
-
const cap = opts.limit ?? DEFAULT_LIMIT;
|
|
38
|
-
const tabFilter = opts.tabIds && opts.tabIds.length > 0 ? new Set(opts.tabIds) : undefined;
|
|
39
|
-
|
|
40
|
-
// 1. Discover candidate sessions (or honor the explicit list).
|
|
41
|
-
const candidateIds = new Set<string>();
|
|
42
|
-
if (opts.sessionIds && opts.sessionIds.length > 0) {
|
|
43
|
-
for (const id of opts.sessionIds) candidateIds.add(id);
|
|
44
|
-
} else {
|
|
45
|
-
const visitorTabs = new Set(visitor.tabIds);
|
|
46
|
-
for (const pid of visitor.projectIds) {
|
|
47
|
-
for (const sess of store.listSessions({ projectId: pid, limit: SESSION_DISCOVERY_PAGE })) {
|
|
48
|
-
if (candidateIds.has(sess.id)) continue;
|
|
49
|
-
if (sess.tabId && !visitorTabs.has(sess.tabId)) continue;
|
|
50
|
-
if (tabFilter && sess.tabId && !tabFilter.has(sess.tabId)) continue;
|
|
51
|
-
candidateIds.add(sess.id);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 2. Pull tail() from each session and merge. Over-fetch by 1 per session
|
|
57
|
-
// so we can detect single-session truncation (tail returns exactly `cap`
|
|
58
|
-
// when there are more, indistinguishable from "the session had exactly
|
|
59
|
-
// `cap` events" otherwise).
|
|
60
|
-
const merged: StoreEvent[] = [];
|
|
61
|
-
let perSessionTruncated = false;
|
|
62
|
-
for (const sid of candidateIds) {
|
|
63
|
-
const events = store.tail(sid, {
|
|
64
|
-
n: cap + 1,
|
|
65
|
-
type: opts.types,
|
|
66
|
-
since: opts.since,
|
|
67
|
-
until: opts.until,
|
|
68
|
-
});
|
|
69
|
-
if (events.length > cap) perSessionTruncated = true;
|
|
70
|
-
for (const ev of events) {
|
|
71
|
-
if (ev.visitorId && ev.visitorId !== visitorId) continue;
|
|
72
|
-
if (tabFilter && ev.tab && !tabFilter.has(ev.tab)) continue;
|
|
73
|
-
merged.push(ev);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// 3. Ascending sort, then trim to the newest `cap` events.
|
|
78
|
-
merged.sort((a, b) => a.ts - b.ts);
|
|
79
|
-
const truncated = perSessionTruncated || merged.length > cap;
|
|
80
|
-
const slice = merged.length > cap ? merged.slice(merged.length - cap) : merged;
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
visitorId,
|
|
84
|
-
sessionCount: candidateIds.size,
|
|
85
|
-
eventCount: slice.length,
|
|
86
|
-
truncated,
|
|
87
|
-
events: slice,
|
|
88
|
-
};
|
|
89
|
-
}
|