@seleniumbox/sbox-mcp 1.0.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/PUBLISHING.md +115 -0
- package/README.md +79 -0
- package/dist/adapters/auth.adapter.d.ts +46 -0
- package/dist/adapters/auth.adapter.js +54 -0
- package/dist/adapters/browser.adapter.d.ts +43 -0
- package/dist/adapters/browser.adapter.js +39 -0
- package/dist/adapters/device.adapter.d.ts +60 -0
- package/dist/adapters/device.adapter.js +40 -0
- package/dist/adapters/diagnostics.adapter.d.ts +73 -0
- package/dist/adapters/diagnostics.adapter.js +89 -0
- package/dist/adapters/index.d.ts +16 -0
- package/dist/adapters/index.js +20 -0
- package/dist/adapters/project.adapter.d.ts +38 -0
- package/dist/adapters/project.adapter.js +39 -0
- package/dist/adapters/sbox-api.client.d.ts +31 -0
- package/dist/adapters/sbox-api.client.js +166 -0
- package/dist/adapters/session.adapter.d.ts +77 -0
- package/dist/adapters/session.adapter.js +95 -0
- package/dist/adapters/stats.adapter.d.ts +72 -0
- package/dist/adapters/stats.adapter.js +108 -0
- package/dist/adapters/upload.adapter.d.ts +16 -0
- package/dist/adapters/upload.adapter.js +25 -0
- package/dist/adapters/user.adapter.d.ts +24 -0
- package/dist/adapters/user.adapter.js +25 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +16 -0
- package/dist/config/env.d.ts +23 -0
- package/dist/config/env.js +64 -0
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +8 -0
- package/dist/controllers/analytics.controller.d.ts +10 -0
- package/dist/controllers/analytics.controller.js +127 -0
- package/dist/controllers/auth-device.controller.d.ts +7 -0
- package/dist/controllers/auth-device.controller.js +60 -0
- package/dist/controllers/auth.controller.d.ts +5 -0
- package/dist/controllers/auth.controller.js +20 -0
- package/dist/controllers/browser.controller.d.ts +5 -0
- package/dist/controllers/browser.controller.js +23 -0
- package/dist/controllers/device.controller.d.ts +5 -0
- package/dist/controllers/device.controller.js +23 -0
- package/dist/controllers/index.d.ts +6 -0
- package/dist/controllers/index.js +28 -0
- package/dist/controllers/project-stats.controller.d.ts +5 -0
- package/dist/controllers/project-stats.controller.js +29 -0
- package/dist/controllers/session.controller.d.ts +9 -0
- package/dist/controllers/session.controller.js +120 -0
- package/dist/controllers/upload.controller.d.ts +5 -0
- package/dist/controllers/upload.controller.js +44 -0
- package/dist/controllers/user.controller.d.ts +5 -0
- package/dist/controllers/user.controller.js +29 -0
- package/dist/device-flow.store.d.ts +7 -0
- package/dist/device-flow.store.js +23 -0
- package/dist/dto/auth.dto.d.ts +26 -0
- package/dist/dto/auth.dto.js +9 -0
- package/dist/dto/browser.dto.d.ts +13 -0
- package/dist/dto/browser.dto.js +2 -0
- package/dist/dto/device.dto.d.ts +15 -0
- package/dist/dto/device.dto.js +2 -0
- package/dist/dto/index.d.ts +6 -0
- package/dist/dto/index.js +22 -0
- package/dist/dto/project-stats.dto.d.ts +8 -0
- package/dist/dto/project-stats.dto.js +2 -0
- package/dist/dto/session.dto.d.ts +50 -0
- package/dist/dto/session.dto.js +9 -0
- package/dist/dto/user.dto.d.ts +6 -0
- package/dist/dto/user.dto.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +35 -0
- package/dist/mcp/tools/auth-tools.d.ts +5 -0
- package/dist/mcp/tools/auth-tools.js +132 -0
- package/dist/mcp/tools/helpers.d.ts +26 -0
- package/dist/mcp/tools/helpers.js +53 -0
- package/dist/mcp/tools/index.d.ts +5 -0
- package/dist/mcp/tools/index.js +12 -0
- package/dist/mcp/tools/rest-tools.d.ts +5 -0
- package/dist/mcp/tools/rest-tools.js +545 -0
- package/dist/mcp-server.d.ts +6 -0
- package/dist/mcp-server.js +28 -0
- package/dist/middleware/auth.middleware.d.ts +10 -0
- package/dist/middleware/auth.middleware.js +23 -0
- package/dist/middleware/debug-log.middleware.d.ts +6 -0
- package/dist/middleware/debug-log.middleware.js +84 -0
- package/dist/middleware/error.middleware.d.ts +8 -0
- package/dist/middleware/error.middleware.js +24 -0
- package/dist/middleware/validate.middleware.d.ts +7 -0
- package/dist/middleware/validate.middleware.js +42 -0
- package/dist/routes/analytics.routes.d.ts +1 -0
- package/dist/routes/analytics.routes.js +15 -0
- package/dist/routes/auth.routes.d.ts +1 -0
- package/dist/routes/auth.routes.js +15 -0
- package/dist/routes/browser.routes.d.ts +1 -0
- package/dist/routes/browser.routes.js +11 -0
- package/dist/routes/device.routes.d.ts +1 -0
- package/dist/routes/device.routes.js +11 -0
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +27 -0
- package/dist/routes/mcp.routes.d.ts +6 -0
- package/dist/routes/mcp.routes.js +70 -0
- package/dist/routes/project-stats.routes.d.ts +1 -0
- package/dist/routes/project-stats.routes.js +11 -0
- package/dist/routes/session.routes.d.ts +1 -0
- package/dist/routes/session.routes.js +23 -0
- package/dist/routes/upload.routes.d.ts +1 -0
- package/dist/routes/upload.routes.js +20 -0
- package/dist/routes/user.routes.d.ts +1 -0
- package/dist/routes/user.routes.js +11 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +17 -0
- package/dist/services/analytics.service.d.ts +88 -0
- package/dist/services/analytics.service.js +98 -0
- package/dist/services/auth.service.d.ts +27 -0
- package/dist/services/auth.service.js +68 -0
- package/dist/services/browser.service.d.ts +25 -0
- package/dist/services/browser.service.js +43 -0
- package/dist/services/device.service.d.ts +18 -0
- package/dist/services/device.service.js +58 -0
- package/dist/services/diagnostics.service.d.ts +87 -0
- package/dist/services/diagnostics.service.js +92 -0
- package/dist/services/enrichment.service.d.ts +9 -0
- package/dist/services/enrichment.service.js +112 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.js +31 -0
- package/dist/services/project.service.d.ts +22 -0
- package/dist/services/project.service.js +31 -0
- package/dist/services/session.service.d.ts +62 -0
- package/dist/services/session.service.js +104 -0
- package/dist/services/sessions-per-project.service.d.ts +18 -0
- package/dist/services/sessions-per-project.service.js +57 -0
- package/dist/services/upload.service.d.ts +20 -0
- package/dist/services/upload.service.js +29 -0
- package/dist/services/user.service.d.ts +17 -0
- package/dist/services/user.service.js +39 -0
- package/dist/token-cache.d.ts +13 -0
- package/dist/token-cache.js +114 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +31 -0
- package/dist/utils/open-browser.d.ts +6 -0
- package/dist/utils/open-browser.js +23 -0
- package/dist/utils/time-range.d.ts +11 -0
- package/dist/utils/time-range.js +16 -0
- package/package.json +42 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.projectService = exports.diagnosticsService = exports.analyticsService = exports.uploadService = exports.sessionsPerProjectService = exports.userService = exports.deviceService = exports.browserService = exports.sessionService = exports.authService = void 0;
|
|
4
|
+
const auth_adapter_1 = require("../adapters/auth.adapter");
|
|
5
|
+
const session_adapter_1 = require("../adapters/session.adapter");
|
|
6
|
+
const browser_adapter_1 = require("../adapters/browser.adapter");
|
|
7
|
+
const device_adapter_1 = require("../adapters/device.adapter");
|
|
8
|
+
const project_adapter_1 = require("../adapters/project.adapter");
|
|
9
|
+
const upload_adapter_1 = require("../adapters/upload.adapter");
|
|
10
|
+
const stats_adapter_1 = require("../adapters/stats.adapter");
|
|
11
|
+
const diagnostics_adapter_1 = require("../adapters/diagnostics.adapter");
|
|
12
|
+
const auth_service_1 = require("./auth.service");
|
|
13
|
+
const session_service_1 = require("./session.service");
|
|
14
|
+
const browser_service_1 = require("./browser.service");
|
|
15
|
+
const device_service_1 = require("./device.service");
|
|
16
|
+
const user_service_1 = require("./user.service");
|
|
17
|
+
const sessions_per_project_service_1 = require("./sessions-per-project.service");
|
|
18
|
+
const upload_service_1 = require("./upload.service");
|
|
19
|
+
const analytics_service_1 = require("./analytics.service");
|
|
20
|
+
const diagnostics_service_1 = require("./diagnostics.service");
|
|
21
|
+
const project_service_1 = require("./project.service");
|
|
22
|
+
exports.authService = (0, auth_service_1.createAuthService)(auth_adapter_1.authAdapter);
|
|
23
|
+
exports.sessionService = (0, session_service_1.createSessionService)(session_adapter_1.sessionAdapter);
|
|
24
|
+
exports.browserService = (0, browser_service_1.createBrowserService)(browser_adapter_1.browserAdapter);
|
|
25
|
+
exports.deviceService = (0, device_service_1.createDeviceService)(device_adapter_1.deviceAdapter);
|
|
26
|
+
exports.userService = (0, user_service_1.createUserService)(session_adapter_1.sessionAdapter);
|
|
27
|
+
exports.sessionsPerProjectService = (0, sessions_per_project_service_1.createSessionsPerProjectService)(project_adapter_1.projectAdapter, session_adapter_1.sessionAdapter);
|
|
28
|
+
exports.uploadService = (0, upload_service_1.createUploadService)(upload_adapter_1.uploadAdapter, project_adapter_1.projectAdapter);
|
|
29
|
+
exports.analyticsService = (0, analytics_service_1.createAnalyticsService)(stats_adapter_1.statsAdapter);
|
|
30
|
+
exports.diagnosticsService = (0, diagnostics_service_1.createDiagnosticsService)(diagnostics_adapter_1.diagnosticsAdapter);
|
|
31
|
+
exports.projectService = (0, project_service_1.createProjectService)(project_adapter_1.projectAdapter);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project service: list projects (all or "my projects" for the current user).
|
|
3
|
+
*/
|
|
4
|
+
import type { ProjectAdapter } from "../adapters/project.adapter";
|
|
5
|
+
export interface ProjectDto {
|
|
6
|
+
id: number | string;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
hasToken: boolean;
|
|
10
|
+
tokenPreview?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProjectService {
|
|
13
|
+
listMyProjects(token: string): Promise<{
|
|
14
|
+
success: true;
|
|
15
|
+
data: ProjectDto[];
|
|
16
|
+
} | {
|
|
17
|
+
success: false;
|
|
18
|
+
status: number;
|
|
19
|
+
message: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export declare function createProjectService(adapter: ProjectAdapter): ProjectService;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Project service: list projects (all or "my projects" for the current user).
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createProjectService = createProjectService;
|
|
7
|
+
function toProjectDto(raw) {
|
|
8
|
+
const id = raw.id ?? 0;
|
|
9
|
+
const name = raw.name ?? "";
|
|
10
|
+
const description = raw.description ?? "";
|
|
11
|
+
const tokenObj = raw.token;
|
|
12
|
+
const hasToken = Boolean(tokenObj?.token);
|
|
13
|
+
const tokenPreview = tokenObj?.token
|
|
14
|
+
? tokenObj.token.length > 12
|
|
15
|
+
? tokenObj.token.slice(0, 6) + "…" + tokenObj.token.slice(-6)
|
|
16
|
+
: "***"
|
|
17
|
+
: undefined;
|
|
18
|
+
return { id, name, description, hasToken, tokenPreview };
|
|
19
|
+
}
|
|
20
|
+
function createProjectService(adapter) {
|
|
21
|
+
return {
|
|
22
|
+
async listMyProjects(token) {
|
|
23
|
+
const res = await adapter.listMyProjects(token);
|
|
24
|
+
if (res.status !== 200) {
|
|
25
|
+
return { success: false, status: res.status, message: res.error ?? "Failed to list my projects" };
|
|
26
|
+
}
|
|
27
|
+
const data = res.projects.map(toProjectDto);
|
|
28
|
+
return { success: true, data };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session service: get session details, update status, delete session, start manual.
|
|
3
|
+
* Uses adapters and enrichment; never exposes internal IDs.
|
|
4
|
+
*/
|
|
5
|
+
import type { SessionAdapter } from "../adapters/session.adapter";
|
|
6
|
+
import type { SessionDetailDto, SessionListResultDto, UpdateSessionStatusBodyDto, DeleteSessionResponseDto, StartManualSessionBodyDto } from "../dto/session.dto";
|
|
7
|
+
export interface ListSessionsParams {
|
|
8
|
+
limit?: number;
|
|
9
|
+
offset?: number;
|
|
10
|
+
projectName?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
browser?: string;
|
|
13
|
+
build?: string;
|
|
14
|
+
sortBy?: string;
|
|
15
|
+
sortDirection?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SessionService {
|
|
18
|
+
getSessionDetail(ekey: string, token: string): Promise<{
|
|
19
|
+
success: true;
|
|
20
|
+
data: SessionDetailDto;
|
|
21
|
+
} | {
|
|
22
|
+
success: false;
|
|
23
|
+
status: number;
|
|
24
|
+
message: string;
|
|
25
|
+
}>;
|
|
26
|
+
listSessions(params: ListSessionsParams, token: string): Promise<{
|
|
27
|
+
success: true;
|
|
28
|
+
data: SessionListResultDto;
|
|
29
|
+
} | {
|
|
30
|
+
success: false;
|
|
31
|
+
status: number;
|
|
32
|
+
message: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Update a single session's result (passed/failed) via POST /e34/api/test-data?sessionId=&passed= */
|
|
35
|
+
updateSessionStatusBySessionId(sessionId: string, passed: boolean, token: string): Promise<{
|
|
36
|
+
success: boolean;
|
|
37
|
+
status: number;
|
|
38
|
+
message?: string;
|
|
39
|
+
}>;
|
|
40
|
+
updateSessionStatus(body: UpdateSessionStatusBodyDto, token: string): Promise<{
|
|
41
|
+
success: boolean;
|
|
42
|
+
status: number;
|
|
43
|
+
message?: string;
|
|
44
|
+
}>;
|
|
45
|
+
deleteSession(sessionId: string, token: string): Promise<{
|
|
46
|
+
success: true;
|
|
47
|
+
data: DeleteSessionResponseDto;
|
|
48
|
+
} | {
|
|
49
|
+
success: false;
|
|
50
|
+
status: number;
|
|
51
|
+
message: string;
|
|
52
|
+
}>;
|
|
53
|
+
startManualSession(body: StartManualSessionBodyDto, token: string): Promise<{
|
|
54
|
+
success: true;
|
|
55
|
+
data: SessionDetailDto;
|
|
56
|
+
} | {
|
|
57
|
+
success: false;
|
|
58
|
+
status: number;
|
|
59
|
+
message: string;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
export declare function createSessionService(adapter: SessionAdapter): SessionService;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Session service: get session details, update status, delete session, start manual.
|
|
4
|
+
* Uses adapters and enrichment; never exposes internal IDs.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.createSessionService = createSessionService;
|
|
8
|
+
const enrichment_service_1 = require("./enrichment.service");
|
|
9
|
+
function createSessionService(adapter) {
|
|
10
|
+
return {
|
|
11
|
+
async getSessionDetail(ekey, token) {
|
|
12
|
+
const res = await adapter.getByEkey(ekey, token);
|
|
13
|
+
if (res.status !== 200 || !res.data) {
|
|
14
|
+
return { success: false, status: res.status, message: res.error ?? "Session not found" };
|
|
15
|
+
}
|
|
16
|
+
const videoUrl = (0, enrichment_service_1.buildVideoUrl)(res.data.ekey ?? ekey);
|
|
17
|
+
const dto = (0, enrichment_service_1.enrichSessionDetail)(res.data, videoUrl);
|
|
18
|
+
return { success: true, data: dto };
|
|
19
|
+
},
|
|
20
|
+
async listSessions(params, token) {
|
|
21
|
+
const size = Math.min(Math.max(params.limit ?? 25, 1), 100);
|
|
22
|
+
const offset = Math.max(params.offset ?? 0, 0);
|
|
23
|
+
const res = await adapter.listPaginated({
|
|
24
|
+
projectId: params.projectName,
|
|
25
|
+
size,
|
|
26
|
+
offset,
|
|
27
|
+
status: params.status,
|
|
28
|
+
browser: params.browser,
|
|
29
|
+
build: params.build,
|
|
30
|
+
sortBy: params.sortBy ?? "arrival",
|
|
31
|
+
sortDirection: params.sortDirection ?? "desc",
|
|
32
|
+
}, token);
|
|
33
|
+
if (res.status !== 200) {
|
|
34
|
+
return { success: false, status: res.status, message: res.error ?? "List sessions failed" };
|
|
35
|
+
}
|
|
36
|
+
const content = res.content.map((raw) => {
|
|
37
|
+
const videoUrl = raw.ekey ? (0, enrichment_service_1.buildVideoUrl)(raw.ekey) : null;
|
|
38
|
+
return (0, enrichment_service_1.enrichSessionDetail)(raw, videoUrl);
|
|
39
|
+
});
|
|
40
|
+
return { success: true, data: { content, totalElements: res.totalElements } };
|
|
41
|
+
},
|
|
42
|
+
async updateSessionStatusBySessionId(sessionId, passed, token) {
|
|
43
|
+
const res = await adapter.updateSessionStatus(sessionId, passed, token);
|
|
44
|
+
if (res.status >= 200 && res.status < 300) {
|
|
45
|
+
return { success: true, status: res.status };
|
|
46
|
+
}
|
|
47
|
+
return { success: false, status: res.status, message: res.error };
|
|
48
|
+
},
|
|
49
|
+
async updateSessionStatus(body, token) {
|
|
50
|
+
const res = await adapter.updateStatusByBuild({ buildName: body.buildName, projectId: body.projectName, status: body.status }, token);
|
|
51
|
+
if (res.status >= 200 && res.status < 300) {
|
|
52
|
+
return { success: true, status: res.status };
|
|
53
|
+
}
|
|
54
|
+
return { success: false, status: res.status, message: res.error };
|
|
55
|
+
},
|
|
56
|
+
async deleteSession(sessionId, token) {
|
|
57
|
+
const res = await adapter.deleteManualSession(sessionId, token);
|
|
58
|
+
if (res.status >= 200 && res.status < 300) {
|
|
59
|
+
return { success: true, data: { success: true, message: "Session deleted" } };
|
|
60
|
+
}
|
|
61
|
+
return { success: false, status: res.status, message: res.error ?? "Delete failed" };
|
|
62
|
+
},
|
|
63
|
+
async startManualSession(body, token) {
|
|
64
|
+
const cap = {
|
|
65
|
+
"e34:projectId": body.projectName,
|
|
66
|
+
...(body.browserName && { browserName: body.browserName }),
|
|
67
|
+
...(body.browserVersion && { version: body.browserVersion }),
|
|
68
|
+
...(body.url && { url: body.url }),
|
|
69
|
+
};
|
|
70
|
+
Object.keys(body).forEach((k) => {
|
|
71
|
+
if (!["projectName", "browserName", "browserVersion", "url"].includes(k))
|
|
72
|
+
cap[k] = body[k];
|
|
73
|
+
});
|
|
74
|
+
const res = await adapter.startManualSession(cap, token);
|
|
75
|
+
if (res.status !== 200 || !res.sessionId) {
|
|
76
|
+
return { success: false, status: res.status, message: res.error ?? "Start manual session failed" };
|
|
77
|
+
}
|
|
78
|
+
const getRes = await adapter.getByEkey(res.sessionId, token);
|
|
79
|
+
const raw = getRes.data;
|
|
80
|
+
const videoUrl = (0, enrichment_service_1.buildVideoUrl)(res.sessionId);
|
|
81
|
+
const dto = raw ? (0, enrichment_service_1.enrichSessionDetail)(raw, videoUrl) : {
|
|
82
|
+
sessionId: res.sessionId,
|
|
83
|
+
projectName: body.projectName,
|
|
84
|
+
browserName: body.browserName ?? "",
|
|
85
|
+
browserVersion: body.browserVersion ?? "",
|
|
86
|
+
deviceName: "",
|
|
87
|
+
userName: "",
|
|
88
|
+
status: "running",
|
|
89
|
+
framework: "selenium",
|
|
90
|
+
startTime: new Date().toISOString(),
|
|
91
|
+
endTime: null,
|
|
92
|
+
durationMs: null,
|
|
93
|
+
videoUrl,
|
|
94
|
+
capabilities: {
|
|
95
|
+
browser: body.browserName,
|
|
96
|
+
version: body.browserVersion,
|
|
97
|
+
"e34:projectId": body.projectName,
|
|
98
|
+
...(body.url && { url: body.url }),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
return { success: true, data: dto };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions per project (last X days): totalSessions, passed, failed, running, avgDuration.
|
|
3
|
+
* Uses project and session adapters; aggregates in MCP (backend has no single endpoint).
|
|
4
|
+
*/
|
|
5
|
+
import type { ProjectAdapter } from "../adapters/project.adapter";
|
|
6
|
+
import type { SessionAdapter } from "../adapters/session.adapter";
|
|
7
|
+
import type { SessionsPerProjectDto } from "../dto/project-stats.dto";
|
|
8
|
+
export interface SessionsPerProjectService {
|
|
9
|
+
getSessionsPerProject(token: string, days: number): Promise<{
|
|
10
|
+
success: true;
|
|
11
|
+
data: SessionsPerProjectDto[];
|
|
12
|
+
} | {
|
|
13
|
+
success: false;
|
|
14
|
+
status: number;
|
|
15
|
+
message: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createSessionsPerProjectService(projectAdapter: ProjectAdapter, sessionAdapter: SessionAdapter): SessionsPerProjectService;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sessions per project (last X days): totalSessions, passed, failed, running, avgDuration.
|
|
4
|
+
* Uses project and session adapters; aggregates in MCP (backend has no single endpoint).
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.createSessionsPerProjectService = createSessionsPerProjectService;
|
|
8
|
+
const PAGE_SIZE = 500;
|
|
9
|
+
function createSessionsPerProjectService(projectAdapter, sessionAdapter) {
|
|
10
|
+
return {
|
|
11
|
+
async getSessionsPerProject(token, days) {
|
|
12
|
+
const countRes = await projectAdapter.getCountPerProject(days, token);
|
|
13
|
+
if (countRes.status !== 200) {
|
|
14
|
+
return { success: false, status: countRes.status, message: countRes.error ?? "Failed to get project counts" };
|
|
15
|
+
}
|
|
16
|
+
const projects = countRes.items.map((p) => p.project).filter(Boolean);
|
|
17
|
+
const results = [];
|
|
18
|
+
const maxAgeDays = Math.max(0, days);
|
|
19
|
+
for (const projectName of projects) {
|
|
20
|
+
const listRes = await sessionAdapter.listPaginated({ projectId: projectName, size: PAGE_SIZE, offset: 0, maxAgeDays }, token);
|
|
21
|
+
if (listRes.status !== 200)
|
|
22
|
+
continue;
|
|
23
|
+
const content = listRes.content;
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
let running = 0;
|
|
27
|
+
let totalDuration = 0;
|
|
28
|
+
let countWithDuration = 0;
|
|
29
|
+
for (const s of content) {
|
|
30
|
+
const r = (s.result ?? "").toLowerCase();
|
|
31
|
+
if (r === "passed")
|
|
32
|
+
passed++;
|
|
33
|
+
else if (r === "failed" || r === "timeout")
|
|
34
|
+
failed++;
|
|
35
|
+
else
|
|
36
|
+
running++;
|
|
37
|
+
const d = s.duration ?? s.duration;
|
|
38
|
+
if (typeof d === "number" && d >= 0) {
|
|
39
|
+
totalDuration += d;
|
|
40
|
+
countWithDuration++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const totalSessions = content.length;
|
|
44
|
+
const avgDuration = countWithDuration > 0 ? Math.round(totalDuration / countWithDuration) : 0;
|
|
45
|
+
results.push({
|
|
46
|
+
projectName,
|
|
47
|
+
totalSessions,
|
|
48
|
+
passed,
|
|
49
|
+
failed,
|
|
50
|
+
running,
|
|
51
|
+
avgDuration,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { success: true, data: results };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload service: upload mobile apps (APK/IPA) to SBOX. Resolves project name to id; no business logic beyond delegation.
|
|
3
|
+
*/
|
|
4
|
+
import type { UploadAdapter } from "../adapters/upload.adapter";
|
|
5
|
+
import type { ProjectAdapter } from "../adapters/project.adapter";
|
|
6
|
+
export interface UploadResultDto {
|
|
7
|
+
uploadedAppIdentifier: string;
|
|
8
|
+
projectName: string;
|
|
9
|
+
}
|
|
10
|
+
export interface UploadService {
|
|
11
|
+
uploadMobileApp(projectName: string, fileBuffer: Buffer, filename: string, token: string): Promise<{
|
|
12
|
+
success: true;
|
|
13
|
+
data: UploadResultDto;
|
|
14
|
+
} | {
|
|
15
|
+
success: false;
|
|
16
|
+
status: number;
|
|
17
|
+
message: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export declare function createUploadService(uploadAdapter: UploadAdapter, projectAdapter: ProjectAdapter): UploadService;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Upload service: upload mobile apps (APK/IPA) to SBOX. Resolves project name to id; no business logic beyond delegation.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createUploadService = createUploadService;
|
|
7
|
+
function createUploadService(uploadAdapter, projectAdapter) {
|
|
8
|
+
return {
|
|
9
|
+
async uploadMobileApp(projectName, fileBuffer, filename, token) {
|
|
10
|
+
const listRes = await projectAdapter.listProjects(token);
|
|
11
|
+
if (listRes.status !== 200) {
|
|
12
|
+
return { success: false, status: listRes.status, message: listRes.error ?? "Failed to list projects" };
|
|
13
|
+
}
|
|
14
|
+
const project = listRes.projects.find((p) => (p.name ?? "").toLowerCase() === projectName.toLowerCase());
|
|
15
|
+
if (!project || project.id == null) {
|
|
16
|
+
return { success: false, status: 404, message: `Project not found: ${projectName}` };
|
|
17
|
+
}
|
|
18
|
+
const projectId = String(project.id);
|
|
19
|
+
const res = await uploadAdapter.uploadMobileApp(projectId, fileBuffer, filename, token);
|
|
20
|
+
if (res.status !== 200 || !res.uploadedAppIdentifier) {
|
|
21
|
+
return { success: false, status: res.status, message: res.error ?? "Upload failed" };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
success: true,
|
|
25
|
+
data: { uploadedAppIdentifier: res.uploadedAppIdentifier, projectName: project.name ?? projectName },
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active users (top N) with session counts and project names.
|
|
3
|
+
* Uses user adapter and session list; returns business-friendly DTOs.
|
|
4
|
+
*/
|
|
5
|
+
import type { SessionAdapter } from "../adapters/session.adapter";
|
|
6
|
+
import type { ActiveUserDto } from "../dto/user.dto";
|
|
7
|
+
export interface UserService {
|
|
8
|
+
getActiveUsersTopN(token: string, limit: number): Promise<{
|
|
9
|
+
success: true;
|
|
10
|
+
data: ActiveUserDto[];
|
|
11
|
+
} | {
|
|
12
|
+
success: false;
|
|
13
|
+
status: number;
|
|
14
|
+
message: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createUserService(sessionAdapter: SessionAdapter): UserService;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Active users (top N) with session counts and project names.
|
|
4
|
+
* Uses user adapter and session list; returns business-friendly DTOs.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.createUserService = createUserService;
|
|
8
|
+
function createUserService(sessionAdapter) {
|
|
9
|
+
return {
|
|
10
|
+
async getActiveUsersTopN(token, limit) {
|
|
11
|
+
const sessionsRes = await sessionAdapter.listPaginated({ size: 500, offset: 0, sortBy: "arrival", sortDirection: "desc" }, token);
|
|
12
|
+
if (sessionsRes.status !== 200) {
|
|
13
|
+
return { success: false, status: sessionsRes.status, message: sessionsRes.error ?? "Failed to list sessions" };
|
|
14
|
+
}
|
|
15
|
+
const byUser = new Map();
|
|
16
|
+
for (const s of sessionsRes.content) {
|
|
17
|
+
const uid = (s.extras ?? {})["e34:userId"];
|
|
18
|
+
const project = (s.extras ?? {})["e34:projectId"];
|
|
19
|
+
const userKey = uid ?? "unknown";
|
|
20
|
+
if (!byUser.has(userKey))
|
|
21
|
+
byUser.set(userKey, { count: 0, projects: new Set() });
|
|
22
|
+
const entry = byUser.get(userKey);
|
|
23
|
+
entry.count += 1;
|
|
24
|
+
if (project)
|
|
25
|
+
entry.projects.add(project);
|
|
26
|
+
}
|
|
27
|
+
const sorted = [...byUser.entries()]
|
|
28
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
29
|
+
.slice(0, limit);
|
|
30
|
+
const dtos = sorted.map(([userName, { count, projects }]) => ({
|
|
31
|
+
userName,
|
|
32
|
+
email: null,
|
|
33
|
+
activeSessionCount: count,
|
|
34
|
+
projectNames: [...projects],
|
|
35
|
+
}));
|
|
36
|
+
return { success: true, data: dtos };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory and file-backed cache for the SBOX session token. Set on successful sbox_open_login,
|
|
3
|
+
* used by other tools when token is not passed. Cleared on 401/invalid token.
|
|
4
|
+
* File persistence ensures the token survives MCP process restarts (e.g. new process per IDE prompt).
|
|
5
|
+
* lastPollId is set when we return device-flow (browser didn't open) so sbox_complete_login can use it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getCachedToken(): string | null;
|
|
8
|
+
export declare function setCachedToken(token: string): void;
|
|
9
|
+
export declare function clearCachedToken(): void;
|
|
10
|
+
export declare function setLastPollId(pollId: string): void;
|
|
11
|
+
export declare function getLastPollId(): string | null;
|
|
12
|
+
/** Resolve token: prefer provided token, else cached. Returns null if neither. */
|
|
13
|
+
export declare function resolveToken(provided: string | undefined | null): string | null;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory and file-backed cache for the SBOX session token. Set on successful sbox_open_login,
|
|
4
|
+
* used by other tools when token is not passed. Cleared on 401/invalid token.
|
|
5
|
+
* File persistence ensures the token survives MCP process restarts (e.g. new process per IDE prompt).
|
|
6
|
+
* lastPollId is set when we return device-flow (browser didn't open) so sbox_complete_login can use it.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.getCachedToken = getCachedToken;
|
|
13
|
+
exports.setCachedToken = setCachedToken;
|
|
14
|
+
exports.clearCachedToken = clearCachedToken;
|
|
15
|
+
exports.setLastPollId = setLastPollId;
|
|
16
|
+
exports.getLastPollId = getLastPollId;
|
|
17
|
+
exports.resolveToken = resolveToken;
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const os_1 = __importDefault(require("os"));
|
|
21
|
+
let cachedToken = null;
|
|
22
|
+
let lastPollId = null;
|
|
23
|
+
let memoryLoadedFromFile = false;
|
|
24
|
+
function getTokenFilePath() {
|
|
25
|
+
if (process.env.SBOX_MCP_TOKEN_FILE)
|
|
26
|
+
return process.env.SBOX_MCP_TOKEN_FILE;
|
|
27
|
+
const dir = path_1.default.join(os_1.default.homedir(), ".sbox-mcp");
|
|
28
|
+
return path_1.default.join(dir, "token");
|
|
29
|
+
}
|
|
30
|
+
function loadTokenFromFile() {
|
|
31
|
+
if (memoryLoadedFromFile)
|
|
32
|
+
return;
|
|
33
|
+
memoryLoadedFromFile = true;
|
|
34
|
+
try {
|
|
35
|
+
const filePath = getTokenFilePath();
|
|
36
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
37
|
+
const raw = fs_1.default.readFileSync(filePath, "utf-8").trim();
|
|
38
|
+
if (raw)
|
|
39
|
+
cachedToken = raw;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignore read errors (e.g. no file, permissions)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function saveTokenToFile(token) {
|
|
47
|
+
try {
|
|
48
|
+
const filePath = getTokenFilePath();
|
|
49
|
+
if (token) {
|
|
50
|
+
const dir = path_1.default.dirname(filePath);
|
|
51
|
+
if (!fs_1.default.existsSync(dir))
|
|
52
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
53
|
+
fs_1.default.writeFileSync(filePath, token, { mode: 0o600, encoding: "utf-8" });
|
|
54
|
+
}
|
|
55
|
+
else if (fs_1.default.existsSync(filePath)) {
|
|
56
|
+
fs_1.default.unlinkSync(filePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// ignore write errors
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function getCachedToken() {
|
|
64
|
+
loadTokenFromFile();
|
|
65
|
+
return cachedToken;
|
|
66
|
+
}
|
|
67
|
+
function setCachedToken(token) {
|
|
68
|
+
cachedToken = token;
|
|
69
|
+
saveTokenToFile(token);
|
|
70
|
+
}
|
|
71
|
+
function clearCachedToken() {
|
|
72
|
+
cachedToken = null;
|
|
73
|
+
saveTokenToFile(null);
|
|
74
|
+
}
|
|
75
|
+
const LAST_POLL_ID_FILE = "last_poll_id";
|
|
76
|
+
function getCacheDir() {
|
|
77
|
+
if (process.env.SBOX_MCP_TOKEN_FILE)
|
|
78
|
+
return path_1.default.dirname(process.env.SBOX_MCP_TOKEN_FILE);
|
|
79
|
+
return path_1.default.join(os_1.default.homedir(), ".sbox-mcp");
|
|
80
|
+
}
|
|
81
|
+
function setLastPollId(pollId) {
|
|
82
|
+
lastPollId = pollId;
|
|
83
|
+
try {
|
|
84
|
+
const dir = getCacheDir();
|
|
85
|
+
if (!fs_1.default.existsSync(dir))
|
|
86
|
+
fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, LAST_POLL_ID_FILE), pollId, { mode: 0o600, encoding: "utf-8" });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function getLastPollId() {
|
|
94
|
+
if (lastPollId)
|
|
95
|
+
return lastPollId;
|
|
96
|
+
try {
|
|
97
|
+
const filePath = path_1.default.join(getCacheDir(), LAST_POLL_ID_FILE);
|
|
98
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
99
|
+
lastPollId = fs_1.default.readFileSync(filePath, "utf-8").trim() || null;
|
|
100
|
+
return lastPollId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/** Resolve token: prefer provided token, else cached. Returns null if neither. */
|
|
109
|
+
function resolveToken(provided) {
|
|
110
|
+
const t = (provided ?? "").trim();
|
|
111
|
+
if (t)
|
|
112
|
+
return t;
|
|
113
|
+
return getCachedToken();
|
|
114
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple logger for MCP module. All output goes to stderr so that when running over
|
|
3
|
+
* stdio (MCP transport), stdout is reserved for JSON-RPC and is not corrupted by logs.
|
|
4
|
+
*/
|
|
5
|
+
export interface Logger {
|
|
6
|
+
info(msg: string, meta?: Record<string, unknown>): void;
|
|
7
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
8
|
+
error(msg: string, meta?: Record<string, unknown>): void;
|
|
9
|
+
debug(msg: string, meta?: Record<string, unknown>): void;
|
|
10
|
+
}
|
|
11
|
+
export declare const logger: Logger;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Simple logger for MCP module. All output goes to stderr so that when running over
|
|
4
|
+
* stdio (MCP transport), stdout is reserved for JSON-RPC and is not corrupted by logs.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.logger = void 0;
|
|
8
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
9
|
+
function format(level, msg, meta) {
|
|
10
|
+
const ts = new Date().toISOString();
|
|
11
|
+
const m = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
|
|
12
|
+
return `${ts} [${level}] ${msg}${m}\n`;
|
|
13
|
+
}
|
|
14
|
+
function writeStderr(line) {
|
|
15
|
+
process.stderr.write(line);
|
|
16
|
+
}
|
|
17
|
+
exports.logger = {
|
|
18
|
+
info(msg, meta) {
|
|
19
|
+
writeStderr(format("INFO", msg, meta));
|
|
20
|
+
},
|
|
21
|
+
warn(msg, meta) {
|
|
22
|
+
writeStderr(format("WARN", msg, meta));
|
|
23
|
+
},
|
|
24
|
+
error(msg, meta) {
|
|
25
|
+
writeStderr(format("ERROR", msg, meta));
|
|
26
|
+
},
|
|
27
|
+
debug(msg, meta) {
|
|
28
|
+
if (!isProd)
|
|
29
|
+
writeStderr(format("DEBUG", msg, meta));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open a URL in the system default browser. Cross-platform via spawn (no shell).
|
|
3
|
+
* Returns true if open succeeded, false otherwise (e.g. headless environment).
|
|
4
|
+
* Never throws; callers can continue (e.g. return auth_url for IDE to open).
|
|
5
|
+
*/
|
|
6
|
+
export declare function openUrlInBrowser(url: string): Promise<boolean>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Open a URL in the system default browser. Cross-platform via spawn (no shell).
|
|
4
|
+
* Returns true if open succeeded, false otherwise (e.g. headless environment).
|
|
5
|
+
* Never throws; callers can continue (e.g. return auth_url for IDE to open).
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.openUrlInBrowser = openUrlInBrowser;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const os_1 = require("os");
|
|
11
|
+
function openUrlInBrowser(url) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const plat = (0, os_1.platform)();
|
|
14
|
+
const [cmd, args] = plat === "win32"
|
|
15
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
16
|
+
: plat === "darwin"
|
|
17
|
+
? ["open", [url]]
|
|
18
|
+
: ["xdg-open", [url]];
|
|
19
|
+
const child = (0, child_process_1.spawn)(cmd, args, { stdio: "ignore" });
|
|
20
|
+
child.on("error", () => resolve(false));
|
|
21
|
+
child.on("close", (code) => resolve(code === 0));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds query string for SBOX time-range params. Backend accepts either days or unit+value.
|
|
3
|
+
*/
|
|
4
|
+
export type TimeRange = {
|
|
5
|
+
days: number;
|
|
6
|
+
} | {
|
|
7
|
+
unit: "hour" | "minute" | "day";
|
|
8
|
+
value: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function buildTimeQuery(timeRange: TimeRange): string;
|
|
11
|
+
export declare function parseTimeToRange(value: number, unit: "hour" | "minute" | "day"): TimeRange;
|