@harness-fe/mcp-server 4.0.0-next.2 → 4.0.0-next.4
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 +65 -19
- package/dist/mcpHttp.d.ts +2 -2
- package/dist/mcpHttp.js +88 -18
- 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 +67 -23
- package/src/mcpHttp.test.ts +52 -3
- package/src/mcpHttp.ts +102 -23
- 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
package/dist/replayViewer.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Replay viewer — HTTP routes for serving exported rrweb recordings.
|
|
3
|
-
*
|
|
4
|
-
* Routes (all served on the same port as the WS bridge):
|
|
5
|
-
* GET /replay/:id HTML viewer page (rrweb-player UI)
|
|
6
|
-
* GET /replay/:id.json Raw events array for the player to fetch
|
|
7
|
-
* GET /replay/static/player.js Bundled rrweb-player (UMD)
|
|
8
|
-
* GET /replay/static/player.css Bundled rrweb-player styles
|
|
9
|
-
*
|
|
10
|
-
* The HTML page loads the static assets relatively and then calls
|
|
11
|
-
* /replay/:id.json to hydrate the player. This keeps the viewer self-contained
|
|
12
|
-
* and offline-capable — no CDN dependencies.
|
|
13
|
-
*/
|
|
14
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
15
|
-
import { createRequire } from 'node:module';
|
|
16
|
-
import { dirname, join } from 'node:path';
|
|
17
|
-
const require = createRequire(import.meta.url);
|
|
18
|
-
/** Locate the bundled rrweb-player dist directory. */
|
|
19
|
-
function resolvePlayerDist() {
|
|
20
|
-
// rrweb-player exposes package.json; use it to find the dist folder.
|
|
21
|
-
const pkgPath = require.resolve('rrweb-player/package.json');
|
|
22
|
-
return join(dirname(pkgPath), 'dist');
|
|
23
|
-
}
|
|
24
|
-
const PLAYER_DIST = (() => {
|
|
25
|
-
try {
|
|
26
|
-
return resolvePlayerDist();
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
})();
|
|
32
|
-
const VIEWER_HTML = (exportId, meta) => `<!doctype html>
|
|
33
|
-
<html lang="en">
|
|
34
|
-
<head>
|
|
35
|
-
<meta charset="utf-8" />
|
|
36
|
-
<title>Harness replay · ${escapeHtml(exportId)}</title>
|
|
37
|
-
<link rel="stylesheet" href="/replay/static/player.css" />
|
|
38
|
-
<style>
|
|
39
|
-
html, body { margin: 0; padding: 0; background: #1a1a1a; color: #eee; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
40
|
-
.meta { padding: 12px 16px; font-size: 13px; color: #aaa; border-bottom: 1px solid #333; }
|
|
41
|
-
.meta code { color: #6cf; }
|
|
42
|
-
.stage { padding: 16px; display: flex; justify-content: center; }
|
|
43
|
-
.error { padding: 24px; color: #f66; }
|
|
44
|
-
</style>
|
|
45
|
-
</head>
|
|
46
|
-
<body>
|
|
47
|
-
<div class="meta">
|
|
48
|
-
Replay <code>${escapeHtml(exportId)}</code> · session <code>${escapeHtml(meta.sessionId)}</code>${meta.tabId ? ` · tab <code>${escapeHtml(meta.tabId)}</code>` : ''} · ${meta.eventCount} events · ${Math.round(meta.durationMs / 100) / 10}s
|
|
49
|
-
</div>
|
|
50
|
-
<div class="stage" id="stage"></div>
|
|
51
|
-
<script src="/replay/static/player.js"></script>
|
|
52
|
-
<script>
|
|
53
|
-
(async function() {
|
|
54
|
-
const stage = document.getElementById('stage');
|
|
55
|
-
try {
|
|
56
|
-
const resp = await fetch(${JSON.stringify(`/replay/${exportId}.json`)});
|
|
57
|
-
if (!resp.ok) throw new Error('failed to fetch events: ' + resp.status);
|
|
58
|
-
const events = await resp.json();
|
|
59
|
-
if (!Array.isArray(events) || events.length < 2) {
|
|
60
|
-
throw new Error('export has fewer than 2 events — cannot play back');
|
|
61
|
-
}
|
|
62
|
-
// rrwebPlayer is the UMD global.
|
|
63
|
-
new rrwebPlayer({
|
|
64
|
-
target: stage,
|
|
65
|
-
props: {
|
|
66
|
-
events,
|
|
67
|
-
autoPlay: true,
|
|
68
|
-
showController: true,
|
|
69
|
-
width: Math.min(1280, window.innerWidth - 64),
|
|
70
|
-
height: Math.min(720, window.innerHeight - 120),
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
} catch (err) {
|
|
74
|
-
stage.innerHTML = '<div class="error">Replay failed: ' + (err && err.message ? String(err.message).replace(/[<>&]/g, '') : 'unknown error') + '</div>';
|
|
75
|
-
console.error('[harness replay]', err);
|
|
76
|
-
}
|
|
77
|
-
})();
|
|
78
|
-
</script>
|
|
79
|
-
</body>
|
|
80
|
-
</html>`;
|
|
81
|
-
function escapeHtml(s) {
|
|
82
|
-
return s.replace(/[&<>"']/g, (ch) => {
|
|
83
|
-
switch (ch) {
|
|
84
|
-
case '&': return '&';
|
|
85
|
-
case '<': return '<';
|
|
86
|
-
case '>': return '>';
|
|
87
|
-
case '"': return '"';
|
|
88
|
-
default: return ''';
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
function send(res, status, contentType, body) {
|
|
93
|
-
res.statusCode = status;
|
|
94
|
-
res.setHeader('content-type', contentType);
|
|
95
|
-
res.setHeader('cache-control', 'no-store');
|
|
96
|
-
res.end(body);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Build a handler that dispatches /replay/* requests. Returns undefined for
|
|
100
|
-
* non-replay paths so the caller can chain or 404.
|
|
101
|
-
*/
|
|
102
|
-
export function createReplayHandler(store) {
|
|
103
|
-
return (req, res) => {
|
|
104
|
-
if (!req.url)
|
|
105
|
-
return false;
|
|
106
|
-
const url = new URL(req.url, 'http://localhost');
|
|
107
|
-
const path = url.pathname;
|
|
108
|
-
if (!path.startsWith('/replay/'))
|
|
109
|
-
return false;
|
|
110
|
-
// Static assets
|
|
111
|
-
if (path === '/replay/static/player.js') {
|
|
112
|
-
if (!PLAYER_DIST)
|
|
113
|
-
return reply500(res, 'rrweb-player not installed');
|
|
114
|
-
const file = join(PLAYER_DIST, 'index.js');
|
|
115
|
-
if (!existsSync(file))
|
|
116
|
-
return reply500(res, 'rrweb-player bundle missing');
|
|
117
|
-
send(res, 200, 'application/javascript; charset=utf-8', readFileSync(file));
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (path === '/replay/static/player.css') {
|
|
121
|
-
if (!PLAYER_DIST)
|
|
122
|
-
return reply500(res, 'rrweb-player not installed');
|
|
123
|
-
const file = join(PLAYER_DIST, 'style.css');
|
|
124
|
-
if (!existsSync(file))
|
|
125
|
-
return reply500(res, 'rrweb-player styles missing');
|
|
126
|
-
send(res, 200, 'text/css; charset=utf-8', readFileSync(file));
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
// /replay/:id or /replay/:id.json
|
|
130
|
-
const tail = path.slice('/replay/'.length);
|
|
131
|
-
if (!tail || tail.includes('/')) {
|
|
132
|
-
send(res, 404, 'text/plain; charset=utf-8', 'Not Found');
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
const isJson = tail.endsWith('.json');
|
|
136
|
-
const exportId = isJson ? tail.slice(0, -'.json'.length) : tail;
|
|
137
|
-
if (!/^[A-Za-z0-9_-]+$/.test(exportId)) {
|
|
138
|
-
send(res, 400, 'text/plain; charset=utf-8', 'Invalid export id');
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
const meta = store.getExport(exportId);
|
|
142
|
-
if (!meta) {
|
|
143
|
-
send(res, 404, 'text/plain; charset=utf-8', `Unknown export: ${exportId}`);
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
if (isJson) {
|
|
147
|
-
const events = store.readExportEvents(exportId);
|
|
148
|
-
if (!events) {
|
|
149
|
-
send(res, 404, 'application/json; charset=utf-8', '{"error":"export events missing"}');
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
send(res, 200, 'application/json; charset=utf-8', JSON.stringify(events));
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
const html = VIEWER_HTML(exportId, {
|
|
156
|
-
sessionId: meta.sessionId,
|
|
157
|
-
tabId: meta.tabId,
|
|
158
|
-
durationMs: Math.max(0, meta.endTs - meta.startTs),
|
|
159
|
-
eventCount: meta.eventCount,
|
|
160
|
-
});
|
|
161
|
-
send(res, 200, 'text/html; charset=utf-8', html);
|
|
162
|
-
return true;
|
|
163
|
-
};
|
|
164
|
-
function reply500(res, msg) {
|
|
165
|
-
send(res, 500, 'text/plain; charset=utf-8', msg);
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
}
|
package/dist/sessionRouter.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SessionRouter — registry of connected peers (vite-plugin + runtime-client).
|
|
3
|
-
*
|
|
4
|
-
* - Project: 1 vite-plugin per project (uniqued by projectId)
|
|
5
|
-
* - Tab : N runtime-clients per project (each browser tab is one)
|
|
6
|
-
*
|
|
7
|
-
* Active tab heuristic: most recently active (last command or last event).
|
|
8
|
-
* Caller can override via explicit tabId on every command.
|
|
9
|
-
*/
|
|
10
|
-
import type { PeerRole, TabInfo } from '@harness-fe/protocol';
|
|
11
|
-
import type { Principal } from './identity.js';
|
|
12
|
-
export interface PeerSession {
|
|
13
|
-
role: PeerRole;
|
|
14
|
-
projectId: string;
|
|
15
|
-
tabId?: string;
|
|
16
|
-
/** Caller identity behind this connection (4.0 · P1). Defaults to `local`. */
|
|
17
|
-
principal?: Principal;
|
|
18
|
-
/** Runtime-client only: identifies the page load (sessionId) this connection belongs to. */
|
|
19
|
-
sessionId?: string;
|
|
20
|
-
/** Runtime-client only: stable per-browser visitor identifier. */
|
|
21
|
-
visitorId?: string;
|
|
22
|
-
/** App-supplied user identifier propagated from HarnessScript userId prop. */
|
|
23
|
-
userId?: string;
|
|
24
|
-
/** Opaque identifier for the underlying connection. */
|
|
25
|
-
connectionId: string;
|
|
26
|
-
lastActive: number;
|
|
27
|
-
page?: {
|
|
28
|
-
url?: string;
|
|
29
|
-
title?: string;
|
|
30
|
-
userAgent?: string;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
export declare class SessionRouter {
|
|
34
|
-
private peers;
|
|
35
|
-
private mostRecentTabId?;
|
|
36
|
-
register(session: Omit<PeerSession, 'lastActive'>): PeerSession;
|
|
37
|
-
unregister(connectionId: string): void;
|
|
38
|
-
touch(connectionId: string): void;
|
|
39
|
-
getByConnectionId(connectionId: string): PeerSession | undefined;
|
|
40
|
-
findVitePlugin(projectId?: string): PeerSession | undefined;
|
|
41
|
-
findTab(tabId?: string): PeerSession | undefined;
|
|
42
|
-
private findFallbackTab;
|
|
43
|
-
listTabs(): TabInfo[];
|
|
44
|
-
listProjects(): string[];
|
|
45
|
-
}
|
package/dist/sessionRouter.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SessionRouter — registry of connected peers (vite-plugin + runtime-client).
|
|
3
|
-
*
|
|
4
|
-
* - Project: 1 vite-plugin per project (uniqued by projectId)
|
|
5
|
-
* - Tab : N runtime-clients per project (each browser tab is one)
|
|
6
|
-
*
|
|
7
|
-
* Active tab heuristic: most recently active (last command or last event).
|
|
8
|
-
* Caller can override via explicit tabId on every command.
|
|
9
|
-
*/
|
|
10
|
-
export class SessionRouter {
|
|
11
|
-
peers = new Map(); // key = connectionId
|
|
12
|
-
mostRecentTabId;
|
|
13
|
-
register(session) {
|
|
14
|
-
const stored = { ...session, lastActive: Date.now() };
|
|
15
|
-
this.peers.set(session.connectionId, stored);
|
|
16
|
-
if (session.role === 'runtime-client' && session.tabId) {
|
|
17
|
-
this.mostRecentTabId = session.tabId;
|
|
18
|
-
}
|
|
19
|
-
return stored;
|
|
20
|
-
}
|
|
21
|
-
unregister(connectionId) {
|
|
22
|
-
const peer = this.peers.get(connectionId);
|
|
23
|
-
this.peers.delete(connectionId);
|
|
24
|
-
if (peer?.tabId && this.mostRecentTabId === peer.tabId) {
|
|
25
|
-
this.mostRecentTabId = this.findFallbackTab()?.tabId;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
touch(connectionId) {
|
|
29
|
-
const peer = this.peers.get(connectionId);
|
|
30
|
-
if (!peer)
|
|
31
|
-
return;
|
|
32
|
-
peer.lastActive = Date.now();
|
|
33
|
-
if (peer.role === 'runtime-client' && peer.tabId) {
|
|
34
|
-
this.mostRecentTabId = peer.tabId;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
getByConnectionId(connectionId) {
|
|
38
|
-
return this.peers.get(connectionId);
|
|
39
|
-
}
|
|
40
|
-
findVitePlugin(projectId) {
|
|
41
|
-
const candidates = [...this.peers.values()].filter((p) => p.role === 'vite-plugin' || p.role === 'webpack-plugin');
|
|
42
|
-
if (!candidates.length)
|
|
43
|
-
return undefined;
|
|
44
|
-
if (projectId)
|
|
45
|
-
return candidates.find((c) => c.projectId === projectId);
|
|
46
|
-
// No projectId filter — return the most recent.
|
|
47
|
-
return candidates.sort((a, b) => b.lastActive - a.lastActive)[0];
|
|
48
|
-
}
|
|
49
|
-
findTab(tabId) {
|
|
50
|
-
if (tabId) {
|
|
51
|
-
for (const p of this.peers.values()) {
|
|
52
|
-
if (p.role === 'runtime-client' && p.tabId === tabId)
|
|
53
|
-
return p;
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
if (this.mostRecentTabId) {
|
|
58
|
-
return this.findTab(this.mostRecentTabId);
|
|
59
|
-
}
|
|
60
|
-
return this.findFallbackTab();
|
|
61
|
-
}
|
|
62
|
-
findFallbackTab() {
|
|
63
|
-
const tabs = [...this.peers.values()]
|
|
64
|
-
.filter((p) => p.role === 'runtime-client' && p.tabId)
|
|
65
|
-
.sort((a, b) => b.lastActive - a.lastActive);
|
|
66
|
-
return tabs[0];
|
|
67
|
-
}
|
|
68
|
-
listTabs() {
|
|
69
|
-
return [...this.peers.values()]
|
|
70
|
-
.filter((p) => p.role === 'runtime-client' && !!p.tabId)
|
|
71
|
-
.map((p) => ({
|
|
72
|
-
tabId: p.tabId,
|
|
73
|
-
projectId: p.projectId,
|
|
74
|
-
url: p.page?.url,
|
|
75
|
-
title: p.page?.title,
|
|
76
|
-
userAgent: p.page?.userAgent,
|
|
77
|
-
connectedAt: p.lastActive,
|
|
78
|
-
}));
|
|
79
|
-
}
|
|
80
|
-
listProjects() {
|
|
81
|
-
const ids = new Set();
|
|
82
|
-
for (const p of this.peers.values()) {
|
|
83
|
-
if (p.role === 'vite-plugin' || p.role === 'webpack-plugin')
|
|
84
|
-
ids.add(p.projectId);
|
|
85
|
-
}
|
|
86
|
-
return [...ids];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonMemoryStore — JSON-based persistence for agent memory (key-value store per project).
|
|
3
|
-
*
|
|
4
|
-
* File layout:
|
|
5
|
-
* {dataDir}/{projectId}/memory.json
|
|
6
|
-
*
|
|
7
|
-
* File format:
|
|
8
|
-
* {
|
|
9
|
-
* "key1": { "key": "key1", "value": "...", "updatedAt": 1700000000000 },
|
|
10
|
-
* "key2": { "key": "key2", "value": "...", "updatedAt": 1700000001000 }
|
|
11
|
-
* }
|
|
12
|
-
*
|
|
13
|
-
* All mutations use atomic write-then-rename for durability.
|
|
14
|
-
*/
|
|
15
|
-
import type { IMemoryStore, MemoryEntry } from './types.js';
|
|
16
|
-
export declare class JsonMemoryStore implements IMemoryStore {
|
|
17
|
-
private readonly dataDir;
|
|
18
|
-
constructor(dataDir: string);
|
|
19
|
-
private memoryPath;
|
|
20
|
-
/**
|
|
21
|
-
* Read and parse memory.json for a project.
|
|
22
|
-
* Returns a null-prototype object on missing or corrupt file (never throws).
|
|
23
|
-
* Using Object.create(null) prevents prototype pollution from keys like __proto__.
|
|
24
|
-
*/
|
|
25
|
-
private load;
|
|
26
|
-
/**
|
|
27
|
-
* Atomically write memory data to disk using tmp + rename strategy.
|
|
28
|
-
* Logs error on failure without throwing.
|
|
29
|
-
*/
|
|
30
|
-
private save;
|
|
31
|
-
/**
|
|
32
|
-
* Get a memory entry by key.
|
|
33
|
-
* Returns undefined if the key does not exist or memory.json is missing.
|
|
34
|
-
*/
|
|
35
|
-
get(projectId: string, key: string): MemoryEntry | undefined;
|
|
36
|
-
/**
|
|
37
|
-
* Write or update a memory entry.
|
|
38
|
-
* Sets updatedAt to the current Unix ms timestamp.
|
|
39
|
-
* Returns the new/updated MemoryEntry.
|
|
40
|
-
*/
|
|
41
|
-
set(projectId: string, key: string, value: string): MemoryEntry;
|
|
42
|
-
/**
|
|
43
|
-
* Delete a memory entry by key.
|
|
44
|
-
* Returns true if the key existed and was removed, false otherwise.
|
|
45
|
-
*/
|
|
46
|
-
delete(projectId: string, key: string): boolean;
|
|
47
|
-
/**
|
|
48
|
-
* List all memory entries for a project, sorted by updatedAt descending.
|
|
49
|
-
* Returns an empty array if memory.json does not exist.
|
|
50
|
-
*/
|
|
51
|
-
list(projectId: string): MemoryEntry[];
|
|
52
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonMemoryStore — JSON-based persistence for agent memory (key-value store per project).
|
|
3
|
-
*
|
|
4
|
-
* File layout:
|
|
5
|
-
* {dataDir}/{projectId}/memory.json
|
|
6
|
-
*
|
|
7
|
-
* File format:
|
|
8
|
-
* {
|
|
9
|
-
* "key1": { "key": "key1", "value": "...", "updatedAt": 1700000000000 },
|
|
10
|
-
* "key2": { "key": "key2", "value": "...", "updatedAt": 1700000001000 }
|
|
11
|
-
* }
|
|
12
|
-
*
|
|
13
|
-
* All mutations use atomic write-then-rename for durability.
|
|
14
|
-
*/
|
|
15
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
import { sanitizeId } from './JsonlStore.js';
|
|
18
|
-
export class JsonMemoryStore {
|
|
19
|
-
dataDir;
|
|
20
|
-
constructor(dataDir) {
|
|
21
|
-
this.dataDir = dataDir;
|
|
22
|
-
}
|
|
23
|
-
// ── Path helpers ──────────────────────────────────────────────────────
|
|
24
|
-
memoryPath(projectId) {
|
|
25
|
-
return join(this.dataDir, sanitizeId(projectId), 'memory.json');
|
|
26
|
-
}
|
|
27
|
-
// ── Private I/O ───────────────────────────────────────────────────────
|
|
28
|
-
/**
|
|
29
|
-
* Read and parse memory.json for a project.
|
|
30
|
-
* Returns a null-prototype object on missing or corrupt file (never throws).
|
|
31
|
-
* Using Object.create(null) prevents prototype pollution from keys like __proto__.
|
|
32
|
-
*/
|
|
33
|
-
load(projectId) {
|
|
34
|
-
const path = this.memoryPath(projectId);
|
|
35
|
-
try {
|
|
36
|
-
const raw = readFileSync(path, 'utf-8');
|
|
37
|
-
const parsed = JSON.parse(raw);
|
|
38
|
-
// Copy into a null-prototype object to prevent prototype pollution
|
|
39
|
-
const safe = Object.create(null);
|
|
40
|
-
for (const key of Object.keys(parsed)) {
|
|
41
|
-
safe[key] = parsed[key];
|
|
42
|
-
}
|
|
43
|
-
return safe;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return Object.create(null);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Atomically write memory data to disk using tmp + rename strategy.
|
|
51
|
-
* Logs error on failure without throwing.
|
|
52
|
-
*/
|
|
53
|
-
save(projectId, data) {
|
|
54
|
-
const path = this.memoryPath(projectId);
|
|
55
|
-
const tmpPath = `${path}.tmp`;
|
|
56
|
-
// Ensure the project directory exists
|
|
57
|
-
const dir = join(this.dataDir, sanitizeId(projectId));
|
|
58
|
-
try {
|
|
59
|
-
mkdirSync(dir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
console.error(`[JsonMemoryStore] failed to create directory ${dir}:`, err);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
67
|
-
renameSync(tmpPath, path);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
console.error(`[JsonMemoryStore] failed to write ${path}:`, err);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// ── IMemoryStore implementation ───────────────────────────────────────
|
|
74
|
-
/**
|
|
75
|
-
* Get a memory entry by key.
|
|
76
|
-
* Returns undefined if the key does not exist or memory.json is missing.
|
|
77
|
-
*/
|
|
78
|
-
get(projectId, key) {
|
|
79
|
-
const data = this.load(projectId);
|
|
80
|
-
return data[key];
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Write or update a memory entry.
|
|
84
|
-
* Sets updatedAt to the current Unix ms timestamp.
|
|
85
|
-
* Returns the new/updated MemoryEntry.
|
|
86
|
-
*/
|
|
87
|
-
set(projectId, key, value) {
|
|
88
|
-
const data = this.load(projectId);
|
|
89
|
-
const entry = {
|
|
90
|
-
key,
|
|
91
|
-
value,
|
|
92
|
-
updatedAt: Date.now(),
|
|
93
|
-
};
|
|
94
|
-
data[key] = entry;
|
|
95
|
-
this.save(projectId, data);
|
|
96
|
-
return entry;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Delete a memory entry by key.
|
|
100
|
-
* Returns true if the key existed and was removed, false otherwise.
|
|
101
|
-
*/
|
|
102
|
-
delete(projectId, key) {
|
|
103
|
-
const data = this.load(projectId);
|
|
104
|
-
if (!(key in data)) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
delete data[key];
|
|
108
|
-
this.save(projectId, data);
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* List all memory entries for a project, sorted by updatedAt descending.
|
|
113
|
-
* Returns an empty array if memory.json does not exist.
|
|
114
|
-
*/
|
|
115
|
-
list(projectId) {
|
|
116
|
-
const data = this.load(projectId);
|
|
117
|
-
return Object.values(data).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonTaskStore — JSON-based persistence for annotation tasks.
|
|
3
|
-
*
|
|
4
|
-
* File format: {dataDir}/{sanitizeId(projectId)}/tasks.json
|
|
5
|
-
* ```json
|
|
6
|
-
* { "version": 1, "tasks": Task[] }
|
|
7
|
-
* ```
|
|
8
|
-
*
|
|
9
|
-
* Writes are atomic: write to a .tmp file then rename to the final path.
|
|
10
|
-
* On read failure (missing or corrupt file), returns an empty array.
|
|
11
|
-
* On write failure, logs the error without throwing.
|
|
12
|
-
*/
|
|
13
|
-
import type { Task } from '@harness-fe/protocol';
|
|
14
|
-
import type { ITaskStore } from './types.js';
|
|
15
|
-
export declare class JsonTaskStore implements ITaskStore {
|
|
16
|
-
private readonly dataDir;
|
|
17
|
-
constructor(dataDir: string);
|
|
18
|
-
private tasksPath;
|
|
19
|
-
loadTasks(projectId: string): Task[];
|
|
20
|
-
saveTasks(projectId: string, tasks: Task[]): void;
|
|
21
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonTaskStore — JSON-based persistence for annotation tasks.
|
|
3
|
-
*
|
|
4
|
-
* File format: {dataDir}/{sanitizeId(projectId)}/tasks.json
|
|
5
|
-
* ```json
|
|
6
|
-
* { "version": 1, "tasks": Task[] }
|
|
7
|
-
* ```
|
|
8
|
-
*
|
|
9
|
-
* Writes are atomic: write to a .tmp file then rename to the final path.
|
|
10
|
-
* On read failure (missing or corrupt file), returns an empty array.
|
|
11
|
-
* On write failure, logs the error without throwing.
|
|
12
|
-
*/
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { sanitizeId } from './JsonlStore.js';
|
|
16
|
-
export class JsonTaskStore {
|
|
17
|
-
dataDir;
|
|
18
|
-
constructor(dataDir) {
|
|
19
|
-
this.dataDir = dataDir;
|
|
20
|
-
}
|
|
21
|
-
tasksPath(projectId) {
|
|
22
|
-
return join(this.dataDir, sanitizeId(projectId), 'tasks.json');
|
|
23
|
-
}
|
|
24
|
-
loadTasks(projectId) {
|
|
25
|
-
const path = this.tasksPath(projectId);
|
|
26
|
-
if (!existsSync(path))
|
|
27
|
-
return [];
|
|
28
|
-
try {
|
|
29
|
-
const raw = readFileSync(path, 'utf-8');
|
|
30
|
-
const parsed = JSON.parse(raw);
|
|
31
|
-
if (!Array.isArray(parsed?.tasks))
|
|
32
|
-
return [];
|
|
33
|
-
return parsed.tasks;
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
saveTasks(projectId, tasks) {
|
|
40
|
-
const path = this.tasksPath(projectId);
|
|
41
|
-
const dir = join(this.dataDir, sanitizeId(projectId));
|
|
42
|
-
try {
|
|
43
|
-
mkdirSync(dir, { recursive: true });
|
|
44
|
-
const tmp = `${path}.tmp`;
|
|
45
|
-
const data = { version: 1, tasks };
|
|
46
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
47
|
-
renameSync(tmp, path);
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
console.error(`[JsonTaskStore] saveTasks failed for project "${projectId}":`, err);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* JsonlStore — JSONL-based persistence layer (v0.4.0 layout).
|
|
3
|
-
*
|
|
4
|
-
* New layout (v0.4.0):
|
|
5
|
-
* {dataDir}/projects/{projectId}/meta.json
|
|
6
|
-
* {dataDir}/projects/{projectId}/notes.jsonl
|
|
7
|
-
* {dataDir}/projects/{projectId}/builds/{buildId}/meta.json
|
|
8
|
-
* {dataDir}/tabs/{tabId}/meta.json
|
|
9
|
-
* {dataDir}/sessions/{sessionId}/meta.json
|
|
10
|
-
* {dataDir}/sessions/{sessionId}/timeline.jsonl
|
|
11
|
-
* {dataDir}/sessions/{sessionId}/recording.jsonl
|
|
12
|
-
* {dataDir}/exports/index.jsonl
|
|
13
|
-
* {dataDir}/exports/{exportId}.rrweb.json
|
|
14
|
-
*
|
|
15
|
-
* Legacy layout (v0.3.x, read-only fallback):
|
|
16
|
-
* {dataDir}/{projectId}/sessions/{buildId}/tabs/{tabId}/...
|
|
17
|
-
* On startup, if legacy dirs are detected a warning is emitted pointing
|
|
18
|
-
* users to `rm -rf ~/.harness/data`.
|
|
19
|
-
*/
|
|
20
|
-
import type { BuildMeta, IStore, ProjectMeta, ProjectTreeNode, PurgeResult, RecordingChunk, RecordingChunkSummary, ReplayExportMeta, RetentionPolicy, SearchOptions, SessionMeta, SessionSummary, StoreEvent, TabMeta, TailOptions, VisitorMeta } from './types.js';
|
|
21
|
-
import type { VisitorEnv } from '@harness-fe/protocol';
|
|
22
|
-
export declare class JsonlStore implements IStore {
|
|
23
|
-
private readonly dataDir;
|
|
24
|
-
private readonly writeQueue;
|
|
25
|
-
/**
|
|
26
|
-
* In-memory index: sessionId → SessionMeta (rebuilt on startup, kept in sync).
|
|
27
|
-
* Enables O(1) session lookup without disk reads.
|
|
28
|
-
*/
|
|
29
|
-
private sessionIndex;
|
|
30
|
-
/**
|
|
31
|
-
* In-memory index: buildId → projectId (from openBuild / upsertBuild).
|
|
32
|
-
* Enables resolving project from buildId for legacy bridge compat.
|
|
33
|
-
*/
|
|
34
|
-
private buildIndex;
|
|
35
|
-
constructor(dataDir?: string);
|
|
36
|
-
/** Scan disk to rebuild in-memory indexes. Mark orphaned sessions (no endedAt). */
|
|
37
|
-
private _rebuildIndexes;
|
|
38
|
-
private projectsDir;
|
|
39
|
-
private projectDir;
|
|
40
|
-
private buildDir;
|
|
41
|
-
private visitorsDir;
|
|
42
|
-
private visitorDir;
|
|
43
|
-
private tabsDir;
|
|
44
|
-
private tabDir;
|
|
45
|
-
private sessionsDir;
|
|
46
|
-
private sessionDir;
|
|
47
|
-
private sessionTimeline;
|
|
48
|
-
private sessionRecording;
|
|
49
|
-
private exportsDir;
|
|
50
|
-
private exportIndex;
|
|
51
|
-
private exportEventsPath;
|
|
52
|
-
openBuild(projectId: string, patch?: Partial<Omit<BuildMeta, 'id' | 'projectId' | 'builtAt'>>): string;
|
|
53
|
-
closeBuild(buildId: string, closedAt?: number): void;
|
|
54
|
-
upsertTab(tabId: string, patch: Partial<Omit<TabMeta, 'id'>>): TabMeta;
|
|
55
|
-
getTab(tabId: string): TabMeta | undefined;
|
|
56
|
-
closeTab(tabId: string, disconnectedAt?: number): void;
|
|
57
|
-
upsertSession(sessionId: string, meta: Partial<Omit<SessionMeta, 'id'>> & {
|
|
58
|
-
tabId: string;
|
|
59
|
-
startedAt: number;
|
|
60
|
-
}): SessionMeta;
|
|
61
|
-
closeSession(sessionId: string, endedAt?: number): void;
|
|
62
|
-
getSession(sessionId: string): SessionMeta | undefined;
|
|
63
|
-
listSessions(opts?: {
|
|
64
|
-
tabId?: string;
|
|
65
|
-
projectId?: string;
|
|
66
|
-
buildId?: string;
|
|
67
|
-
limit?: number;
|
|
68
|
-
}): SessionMeta[];
|
|
69
|
-
appendEvent(sessionId: string, event: StoreEvent): void;
|
|
70
|
-
appendEventBatch(sessionId: string, events: StoreEvent[]): void;
|
|
71
|
-
appendRecording(sessionId: string, chunk: unknown): void;
|
|
72
|
-
writeNote(projectId: string, key: string, value: string): void;
|
|
73
|
-
upsertProject(projectId: string, patch: Partial<Omit<ProjectMeta, 'id' | 'createdAt'>>): ProjectMeta;
|
|
74
|
-
getProject(projectId: string): ProjectMeta | undefined;
|
|
75
|
-
listProjects(): ProjectMeta[];
|
|
76
|
-
upsertVisitor(visitorId: string, patch: {
|
|
77
|
-
userId?: string;
|
|
78
|
-
seenAt?: number;
|
|
79
|
-
incrementSession?: boolean;
|
|
80
|
-
addTabId?: string;
|
|
81
|
-
addProjectId?: string;
|
|
82
|
-
lastEnv?: VisitorEnv;
|
|
83
|
-
}): VisitorMeta;
|
|
84
|
-
getVisitor(visitorId: string): VisitorMeta | undefined;
|
|
85
|
-
listVisitors(opts?: {
|
|
86
|
-
projectId?: string;
|
|
87
|
-
limit?: number;
|
|
88
|
-
}): VisitorMeta[];
|
|
89
|
-
upsertBuild(projectId: string, buildId: string, patch: Partial<Omit<BuildMeta, 'id' | 'projectId'>>): BuildMeta;
|
|
90
|
-
getBuild(projectId: string, buildId: string): BuildMeta | undefined;
|
|
91
|
-
listBuilds(projectId: string, limit?: number): BuildMeta[];
|
|
92
|
-
getProjectTree(rootId?: string): ProjectTreeNode[];
|
|
93
|
-
tail(sessionId: string, opts?: TailOptions): StoreEvent[];
|
|
94
|
-
search(sessionId: string, query: string, opts?: SearchOptions): StoreEvent[];
|
|
95
|
-
listRecordings(sessionId: string): RecordingChunkSummary[];
|
|
96
|
-
sliceRecordings(sessionId: string, since: number, until: number): RecordingChunk[];
|
|
97
|
-
writeExport(input: {
|
|
98
|
-
sessionId: string;
|
|
99
|
-
tabId?: string;
|
|
100
|
-
since: number;
|
|
101
|
-
until: number;
|
|
102
|
-
label?: string;
|
|
103
|
-
events: unknown[];
|
|
104
|
-
startTs: number;
|
|
105
|
-
endTs: number;
|
|
106
|
-
chunkCount: number;
|
|
107
|
-
}): ReplayExportMeta;
|
|
108
|
-
getExport(exportId: string): ReplayExportMeta | undefined;
|
|
109
|
-
readExportEvents(exportId: string): unknown[] | undefined;
|
|
110
|
-
listExports(projectId: string, limit?: number): ReplayExportMeta[];
|
|
111
|
-
summary(sessionId: string): SessionSummary;
|
|
112
|
-
listNotes(projectId: string): Array<{
|
|
113
|
-
key: string;
|
|
114
|
-
value: string;
|
|
115
|
-
ts: number;
|
|
116
|
-
}>;
|
|
117
|
-
purge(policy?: RetentionPolicy): PurgeResult;
|
|
118
|
-
private pruneExports;
|
|
119
|
-
/**
|
|
120
|
-
* Flush all pending WriteQueue entries to disk. Used in tests.
|
|
121
|
-
*/
|
|
122
|
-
flush(): Promise<void>;
|
|
123
|
-
close(): Promise<void>;
|
|
124
|
-
private pruneRecordingFile;
|
|
125
|
-
private readMarkerTimestamps;
|
|
126
|
-
}
|
|
127
|
-
/** Sanitize a string for use as a directory name. */
|
|
128
|
-
export declare function sanitizeId(id: string): string;
|