@muggleai/works 5.19.0-staging.131 → 5.19.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/README.md +11 -5
- package/dist/{chunk-ODEPI25T.js → chunk-H2KMRAQZ.js} +38 -6
- package/dist/{chunk-U6ZACW6K.js → chunk-ZA5KHVX5.js} +1500 -1255
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/skills/_shared/debug-failed-run.md +5 -3
- package/dist/plugin/skills/_shared/dev-loop/action-script.md +2 -0
- package/dist/plugin/skills/_shared/dev-loop/failures.md +3 -1
- package/dist/plugin/skills/_shared/failure-mode-handling.md +5 -4
- package/dist/plugin/skills/do/e2e-repair.md +3 -1
- package/dist/plugin/skills/muggle-feedback/ops/submit.md +2 -0
- package/dist/plugin/skills/muggle-test/reference.md +1 -0
- package/dist/release-manifest.json +3 -3
- package/dist/src-RU5QOCXE.js +1 -0
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/skills/_shared/debug-failed-run.md +5 -3
- package/plugin/skills/_shared/dev-loop/action-script.md +2 -0
- package/plugin/skills/_shared/dev-loop/failures.md +3 -1
- package/plugin/skills/_shared/failure-mode-handling.md +5 -4
- package/plugin/skills/do/e2e-repair.md +3 -1
- package/plugin/skills/muggle-feedback/ops/submit.md +2 -0
- package/plugin/skills/muggle-test/reference.md +1 -0
- package/dist/src-C2IZ3MAZ.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as fs4 from 'fs';
|
|
2
2
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
-
import * as
|
|
3
|
+
import * as path13 from 'path';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import * as crypto from 'crypto';
|
|
@@ -10,7 +10,7 @@ import { platform } from 'os';
|
|
|
10
10
|
import winston from 'winston';
|
|
11
11
|
import { exec, spawn } from 'child_process';
|
|
12
12
|
import axios, { AxiosError } from 'axios';
|
|
13
|
-
import * as
|
|
13
|
+
import * as fs12 from 'fs/promises';
|
|
14
14
|
import AppInsights from 'applicationinsights';
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
|
|
@@ -29,8 +29,10 @@ var __export = (target, all) => {
|
|
|
29
29
|
// packages/mcps/src/index.ts
|
|
30
30
|
var src_exports = {};
|
|
31
31
|
__export(src_exports, {
|
|
32
|
+
ACTION_SCRIPT_FILE_NAME: () => ACTION_SCRIPT_FILE_NAME,
|
|
32
33
|
DEFAULT_PREFERENCES: () => DEFAULT_PREFERENCES,
|
|
33
34
|
ElectronAppReleaseStream: () => ElectronAppReleaseStream,
|
|
35
|
+
HTML_VIEW_OFFER_LINE: () => HTML_VIEW_OFFER_LINE,
|
|
34
36
|
ONBOARDING_BLANKET_OPTIONS: () => ONBOARDING_BLANKET_OPTIONS,
|
|
35
37
|
ONBOARDING_CHOICE_GROUPS: () => ONBOARDING_CHOICE_GROUPS,
|
|
36
38
|
ONBOARDING_MAX_OFFERS: () => ONBOARDING_MAX_OFFERS,
|
|
@@ -49,7 +51,11 @@ __export(src_exports, {
|
|
|
49
51
|
PreferenceKey: () => PreferenceKey,
|
|
50
52
|
PreferenceValue: () => PreferenceValue,
|
|
51
53
|
ProjectPreferencesReconcileOutcome: () => ProjectPreferencesReconcileOutcome,
|
|
54
|
+
RUN_SESSIONS_DIR_NAME: () => RUN_SESSIONS_DIR_NAME,
|
|
55
|
+
RunViewError: () => RunViewError,
|
|
52
56
|
RuntimeTarget: () => RuntimeTarget,
|
|
57
|
+
SCREENSHOT_DIR_RELATIVE_PATH: () => SCREENSHOT_DIR_RELATIVE_PATH,
|
|
58
|
+
SCREENSHOT_FILE_EXTENSION: () => SCREENSHOT_FILE_EXTENSION,
|
|
53
59
|
WATCHER_LIFETIME_SECONDS: () => WATCHER_LIFETIME_SECONDS,
|
|
54
60
|
WATCHER_LIFETIME_UNBOUNDED_SECONDS: () => WATCHER_LIFETIME_UNBOUNDED_SECONDS,
|
|
55
61
|
applyOnboardingAnswers: () => applyOnboardingAnswers,
|
|
@@ -91,6 +97,7 @@ __export(src_exports, {
|
|
|
91
97
|
getPlatformKey: () => getPlatformKey,
|
|
92
98
|
getQaTools: () => getQaTools,
|
|
93
99
|
getReleaseSignerIdentityUri: () => getReleaseSignerIdentityUri,
|
|
100
|
+
getRunSessionsDir: () => getRunSessionsDir,
|
|
94
101
|
getValidApiKeyData: () => getValidApiKeyData,
|
|
95
102
|
getValidCredentials: () => getValidCredentials,
|
|
96
103
|
hasApiKey: () => hasApiKey,
|
|
@@ -108,8 +115,11 @@ __export(src_exports, {
|
|
|
108
115
|
pollDeviceCode: () => pollDeviceCode,
|
|
109
116
|
qa: () => e2e_exports2,
|
|
110
117
|
readPreferencesMetadata: () => readPreferencesMetadata,
|
|
118
|
+
readRunView: () => readRunView,
|
|
111
119
|
reconcileProjectPreferences: () => reconcileProjectPreferences,
|
|
112
120
|
recordOnboardingSkip: () => recordOnboardingSkip,
|
|
121
|
+
renderRunViewHtml: () => renderRunViewHtml,
|
|
122
|
+
renderRunViewText: () => renderRunViewText,
|
|
113
123
|
resetConfig: () => resetConfig,
|
|
114
124
|
resetLogger: () => resetLogger,
|
|
115
125
|
resetPreference: () => resetPreference,
|
|
@@ -120,6 +130,7 @@ __export(src_exports, {
|
|
|
120
130
|
resolveElectronAppPathOrNull: () => resolveElectronAppPathOrNull,
|
|
121
131
|
resolveOnboardingAnswers: () => resolveOnboardingAnswers,
|
|
122
132
|
resolvePreferences: () => resolvePreferences,
|
|
133
|
+
resolveRunSessionPath: () => resolveRunSessionPath,
|
|
123
134
|
resolveRuntimeTarget: () => resolveRuntimeTarget,
|
|
124
135
|
resolveTogglePreferences: () => resolveTogglePreferences,
|
|
125
136
|
saveApiKey: () => saveApiKey,
|
|
@@ -130,7 +141,8 @@ __export(src_exports, {
|
|
|
130
141
|
validatePreference: () => validatePreference,
|
|
131
142
|
verifyFileChecksum: () => verifyFileChecksum,
|
|
132
143
|
writePreferences: () => writePreferences2,
|
|
133
|
-
writePreferencesWithMetadata: () => writePreferencesWithMetadata
|
|
144
|
+
writePreferencesWithMetadata: () => writePreferencesWithMetadata,
|
|
145
|
+
writeRunViewHtml: () => writeRunViewHtml
|
|
134
146
|
});
|
|
135
147
|
|
|
136
148
|
// packages/mcps/src/shared/runtime-target-types.ts
|
|
@@ -148,29 +160,29 @@ var ElectronAppReleaseStream = /* @__PURE__ */ ((ElectronAppReleaseStream3) => {
|
|
|
148
160
|
var DEFAULT_AUTH0_SCOPE = "openid profile email offline_access";
|
|
149
161
|
var DEFAULT_WEB_SERVICE_URL = "http://localhost:3001";
|
|
150
162
|
var RUNTIME_TARGET_ENV_VAR = "MUGGLE_MCP_PROMPT_SERVICE_TARGET";
|
|
151
|
-
var RUNTIME_TARGETS_FILE =
|
|
163
|
+
var RUNTIME_TARGETS_FILE = path13.join("config", "runtime-targets.json");
|
|
152
164
|
function getPackageRoot() {
|
|
153
|
-
const currentDir =
|
|
154
|
-
if (currentDir.includes(
|
|
155
|
-
return
|
|
165
|
+
const currentDir = path13.dirname(fileURLToPath(import.meta.url));
|
|
166
|
+
if (currentDir.includes(path13.join("dist", "shared"))) {
|
|
167
|
+
return path13.resolve(currentDir, "..", "..");
|
|
156
168
|
}
|
|
157
169
|
if (currentDir.endsWith("dist")) {
|
|
158
|
-
return
|
|
170
|
+
return path13.resolve(currentDir, "..");
|
|
159
171
|
}
|
|
160
|
-
if (currentDir.includes(
|
|
161
|
-
return
|
|
172
|
+
if (currentDir.includes(path13.join("src", "shared"))) {
|
|
173
|
+
return path13.resolve(currentDir, "..", "..", "..", "..");
|
|
162
174
|
}
|
|
163
|
-
return
|
|
175
|
+
return path13.dirname(currentDir);
|
|
164
176
|
}
|
|
165
177
|
var runtimeTargetDataCache = null;
|
|
166
178
|
function getRuntimeTargetData() {
|
|
167
179
|
if (runtimeTargetDataCache) {
|
|
168
180
|
return runtimeTargetDataCache;
|
|
169
181
|
}
|
|
170
|
-
const profilesPath =
|
|
182
|
+
const profilesPath = path13.join(getPackageRoot(), RUNTIME_TARGETS_FILE);
|
|
171
183
|
let parsedData;
|
|
172
184
|
try {
|
|
173
|
-
parsedData = JSON.parse(
|
|
185
|
+
parsedData = JSON.parse(fs4.readFileSync(profilesPath, "utf-8"));
|
|
174
186
|
} catch (error) {
|
|
175
187
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
176
188
|
throw new Error(
|
|
@@ -246,7 +258,7 @@ function assertDeviceCodeClientProvisioned(target) {
|
|
|
246
258
|
}
|
|
247
259
|
var DATA_DIR_NAME = ".muggle-ai";
|
|
248
260
|
function getDataDir() {
|
|
249
|
-
return
|
|
261
|
+
return path13.join(os4.homedir(), DATA_DIR_NAME);
|
|
250
262
|
}
|
|
251
263
|
var DEV_MANIFEST = {
|
|
252
264
|
release: "dev",
|
|
@@ -300,27 +312,27 @@ var configInstance = null;
|
|
|
300
312
|
var muggleConfigCache = null;
|
|
301
313
|
function getPackageRoot2() {
|
|
302
314
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
303
|
-
const currentDir =
|
|
304
|
-
if (currentDir.includes(
|
|
305
|
-
return
|
|
315
|
+
const currentDir = path13.dirname(currentFilePath);
|
|
316
|
+
if (currentDir.includes(path13.join("dist", "shared"))) {
|
|
317
|
+
return path13.resolve(currentDir, "..", "..");
|
|
306
318
|
}
|
|
307
319
|
if (currentDir.endsWith("dist")) {
|
|
308
|
-
return
|
|
320
|
+
return path13.resolve(currentDir, "..");
|
|
309
321
|
}
|
|
310
|
-
if (currentDir.includes(
|
|
311
|
-
return
|
|
322
|
+
if (currentDir.includes(path13.join("src", "shared"))) {
|
|
323
|
+
return path13.resolve(currentDir, "..", "..", "..", "..");
|
|
312
324
|
}
|
|
313
|
-
return
|
|
325
|
+
return path13.dirname(currentDir);
|
|
314
326
|
}
|
|
315
327
|
function getMuggleConfig() {
|
|
316
328
|
if (muggleConfigCache) {
|
|
317
329
|
return muggleConfigCache;
|
|
318
330
|
}
|
|
319
331
|
const packageRoot = getPackageRoot2();
|
|
320
|
-
const packageJsonPath =
|
|
332
|
+
const packageJsonPath = path13.join(packageRoot, "package.json");
|
|
321
333
|
let packageJson;
|
|
322
334
|
try {
|
|
323
|
-
const content =
|
|
335
|
+
const content = fs4.readFileSync(packageJsonPath, "utf-8");
|
|
324
336
|
packageJson = JSON.parse(content);
|
|
325
337
|
} catch (error) {
|
|
326
338
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -393,7 +405,7 @@ function buildOAuthSessionFileName(params) {
|
|
|
393
405
|
function getDownloadedElectronAppPath() {
|
|
394
406
|
const platformName = os4.platform();
|
|
395
407
|
const version = getElectronAppVersion();
|
|
396
|
-
const baseDir =
|
|
408
|
+
const baseDir = path13.join(
|
|
397
409
|
getDataDir2(),
|
|
398
410
|
ELECTRON_APP_DIR,
|
|
399
411
|
buildElectronAppDirName({
|
|
@@ -404,18 +416,18 @@ function getDownloadedElectronAppPath() {
|
|
|
404
416
|
let binaryPath;
|
|
405
417
|
switch (platformName) {
|
|
406
418
|
case "darwin":
|
|
407
|
-
binaryPath =
|
|
419
|
+
binaryPath = path13.join(baseDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
|
|
408
420
|
break;
|
|
409
421
|
case "win32":
|
|
410
|
-
binaryPath =
|
|
422
|
+
binaryPath = path13.join(baseDir, "MuggleAI.exe");
|
|
411
423
|
break;
|
|
412
424
|
case "linux":
|
|
413
|
-
binaryPath =
|
|
425
|
+
binaryPath = path13.join(baseDir, "MuggleAI");
|
|
414
426
|
break;
|
|
415
427
|
default:
|
|
416
428
|
return null;
|
|
417
429
|
}
|
|
418
|
-
if (
|
|
430
|
+
if (fs4.existsSync(binaryPath)) {
|
|
419
431
|
return binaryPath;
|
|
420
432
|
}
|
|
421
433
|
return null;
|
|
@@ -426,25 +438,25 @@ function getSystemElectronAppPath() {
|
|
|
426
438
|
let binaryPath;
|
|
427
439
|
switch (platformName) {
|
|
428
440
|
case "darwin":
|
|
429
|
-
binaryPath =
|
|
441
|
+
binaryPath = path13.join(homeDir, "Applications", "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
|
|
430
442
|
break;
|
|
431
443
|
case "win32":
|
|
432
|
-
binaryPath =
|
|
444
|
+
binaryPath = path13.join(homeDir, "AppData", "Local", "Programs", "MuggleAI", "MuggleAI.exe");
|
|
433
445
|
break;
|
|
434
446
|
case "linux":
|
|
435
|
-
binaryPath =
|
|
447
|
+
binaryPath = path13.join(homeDir, ".local", "share", "muggle-ai", "MuggleAI");
|
|
436
448
|
break;
|
|
437
449
|
default:
|
|
438
450
|
return null;
|
|
439
451
|
}
|
|
440
|
-
if (
|
|
452
|
+
if (fs4.existsSync(binaryPath)) {
|
|
441
453
|
return binaryPath;
|
|
442
454
|
}
|
|
443
455
|
return null;
|
|
444
456
|
}
|
|
445
457
|
function resolveElectronAppPathOrNull() {
|
|
446
458
|
const customPath = process.env.ELECTRON_APP_PATH;
|
|
447
|
-
if (customPath &&
|
|
459
|
+
if (customPath && fs4.existsSync(customPath)) {
|
|
448
460
|
return customPath;
|
|
449
461
|
}
|
|
450
462
|
const downloadedPath = getDownloadedElectronAppPath();
|
|
@@ -459,12 +471,12 @@ function resolveElectronAppPathOrNull() {
|
|
|
459
471
|
}
|
|
460
472
|
function resolveWebServicePath() {
|
|
461
473
|
const customPath = process.env.WEB_SERVICE_PATH;
|
|
462
|
-
if (customPath &&
|
|
474
|
+
if (customPath && fs4.existsSync(customPath)) {
|
|
463
475
|
return customPath;
|
|
464
476
|
}
|
|
465
477
|
const packageRoot = getPackageRoot2();
|
|
466
|
-
const siblingPath =
|
|
467
|
-
if (
|
|
478
|
+
const siblingPath = path13.resolve(packageRoot, "..", "web-service", "dist", "src", "index.js");
|
|
479
|
+
if (fs4.existsSync(siblingPath)) {
|
|
468
480
|
return siblingPath;
|
|
469
481
|
}
|
|
470
482
|
return null;
|
|
@@ -506,16 +518,16 @@ function buildLocalQaConfig() {
|
|
|
506
518
|
webServiceUrl: process.env.WEB_SERVICE_URL ?? DEFAULT_WEB_SERVICE_URL,
|
|
507
519
|
promptServiceUrl: process.env.PROMPT_SERVICE_BASE_URL ?? activeProfile.promptServiceBaseUrl,
|
|
508
520
|
dataDir,
|
|
509
|
-
sessionsDir:
|
|
510
|
-
projectsDir:
|
|
511
|
-
tempDir:
|
|
512
|
-
apiKeyFilePath:
|
|
513
|
-
oauthSessionFilePath:
|
|
521
|
+
sessionsDir: path13.join(dataDir, "sessions"),
|
|
522
|
+
projectsDir: path13.join(dataDir, "projects"),
|
|
523
|
+
tempDir: path13.join(dataDir, "temp"),
|
|
524
|
+
apiKeyFilePath: path13.join(dataDir, API_KEY_FILE),
|
|
525
|
+
oauthSessionFilePath: path13.join(
|
|
514
526
|
dataDir,
|
|
515
527
|
buildOAuthSessionFileName({ runtimeTarget: getActiveRuntimeTarget() })
|
|
516
528
|
),
|
|
517
529
|
webServicePath: resolveWebServicePath(),
|
|
518
|
-
webServicePidFile:
|
|
530
|
+
webServicePidFile: path13.join(dataDir, "web-service.pid"),
|
|
519
531
|
auth0: {
|
|
520
532
|
domain: process.env.AUTH0_DOMAIN ?? activeProfile.auth0Domain,
|
|
521
533
|
clientId: process.env.AUTH0_CLIENT_ID ?? activeProfile.auth0ClientId,
|
|
@@ -550,10 +562,10 @@ function getElectronAppVersion() {
|
|
|
550
562
|
if (envVersion && /^\d+\.\d+\.\d+$/.test(envVersion)) {
|
|
551
563
|
return envVersion;
|
|
552
564
|
}
|
|
553
|
-
const overridePath =
|
|
554
|
-
if (
|
|
565
|
+
const overridePath = path13.join(getDataDir2(), VERSION_OVERRIDE_FILE);
|
|
566
|
+
if (fs4.existsSync(overridePath)) {
|
|
555
567
|
try {
|
|
556
|
-
const content = JSON.parse(
|
|
568
|
+
const content = JSON.parse(fs4.readFileSync(overridePath, "utf-8"));
|
|
557
569
|
if (content.version && typeof content.version === "string") {
|
|
558
570
|
return content.version;
|
|
559
571
|
}
|
|
@@ -567,10 +579,10 @@ function getElectronAppVersionSource() {
|
|
|
567
579
|
if (envVersion && /^\d+\.\d+\.\d+$/.test(envVersion)) {
|
|
568
580
|
return "env";
|
|
569
581
|
}
|
|
570
|
-
const overridePath =
|
|
571
|
-
if (
|
|
582
|
+
const overridePath = path13.join(getDataDir2(), VERSION_OVERRIDE_FILE);
|
|
583
|
+
if (fs4.existsSync(overridePath)) {
|
|
572
584
|
try {
|
|
573
|
-
const content = JSON.parse(
|
|
585
|
+
const content = JSON.parse(fs4.readFileSync(overridePath, "utf-8"));
|
|
574
586
|
if (content.version && typeof content.version === "string") {
|
|
575
587
|
return "override";
|
|
576
588
|
}
|
|
@@ -614,7 +626,7 @@ function isElectronAppInstalled() {
|
|
|
614
626
|
}
|
|
615
627
|
function getElectronAppDir(version) {
|
|
616
628
|
const resolvedVersion = version ?? getElectronAppVersion();
|
|
617
|
-
return
|
|
629
|
+
return path13.join(
|
|
618
630
|
getDataDir2(),
|
|
619
631
|
ELECTRON_APP_DIR,
|
|
620
632
|
buildElectronAppDirName({
|
|
@@ -680,7 +692,7 @@ function getPlatformKey() {
|
|
|
680
692
|
async function calculateFileChecksum(filePath) {
|
|
681
693
|
return new Promise((resolve6, reject) => {
|
|
682
694
|
const hash = crypto.createHash("sha256");
|
|
683
|
-
const stream =
|
|
695
|
+
const stream = fs4.createReadStream(filePath);
|
|
684
696
|
stream.on("data", (data) => {
|
|
685
697
|
hash.update(data);
|
|
686
698
|
});
|
|
@@ -695,7 +707,7 @@ async function calculateFileChecksum(filePath) {
|
|
|
695
707
|
async function verifyFileChecksum(filePath, expectedChecksum) {
|
|
696
708
|
if (!expectedChecksum || expectedChecksum.trim() === "") {
|
|
697
709
|
logger.warn("Checksum verification skipped - no checksum provided", {
|
|
698
|
-
file:
|
|
710
|
+
file: path13.basename(filePath)
|
|
699
711
|
});
|
|
700
712
|
return {
|
|
701
713
|
valid: true,
|
|
@@ -711,13 +723,13 @@ async function verifyFileChecksum(filePath, expectedChecksum) {
|
|
|
711
723
|
const valid = normalizedExpected === normalizedActual;
|
|
712
724
|
if (!valid) {
|
|
713
725
|
logger.error("Checksum verification failed", {
|
|
714
|
-
file:
|
|
726
|
+
file: path13.basename(filePath),
|
|
715
727
|
expected: normalizedExpected,
|
|
716
728
|
actual: normalizedActual
|
|
717
729
|
});
|
|
718
730
|
} else {
|
|
719
731
|
logger.info("Checksum verified successfully", {
|
|
720
|
-
file:
|
|
732
|
+
file: path13.basename(filePath),
|
|
721
733
|
checksum: normalizedActual
|
|
722
734
|
});
|
|
723
735
|
}
|
|
@@ -730,7 +742,7 @@ async function verifyFileChecksum(filePath, expectedChecksum) {
|
|
|
730
742
|
} catch (error) {
|
|
731
743
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
732
744
|
logger.error("Checksum calculation failed", {
|
|
733
|
-
file:
|
|
745
|
+
file: path13.basename(filePath),
|
|
734
746
|
error: errorMessage
|
|
735
747
|
});
|
|
736
748
|
return {
|
|
@@ -795,107 +807,13 @@ async function openBrowserUrl(options) {
|
|
|
795
807
|
}
|
|
796
808
|
});
|
|
797
809
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
fs6.mkdirSync(dataDir, { recursive: true });
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
function loadApiKeyData() {
|
|
809
|
-
const logger6 = getLogger();
|
|
810
|
-
const apiKeyPath = getApiKeyFilePath();
|
|
811
|
-
try {
|
|
812
|
-
if (!fs6.existsSync(apiKeyPath)) {
|
|
813
|
-
logger6.debug("No API key file found", { path: apiKeyPath });
|
|
814
|
-
return null;
|
|
815
|
-
}
|
|
816
|
-
const content = fs6.readFileSync(apiKeyPath, "utf-8");
|
|
817
|
-
const data = JSON.parse(content);
|
|
818
|
-
return data;
|
|
819
|
-
} catch (error) {
|
|
820
|
-
logger6.warn("Failed to load API key data", {
|
|
821
|
-
error: error instanceof Error ? error.message : String(error)
|
|
822
|
-
});
|
|
823
|
-
return null;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
function saveApiKeyData(data) {
|
|
827
|
-
const logger6 = getLogger();
|
|
828
|
-
const apiKeyPath = getApiKeyFilePath();
|
|
829
|
-
try {
|
|
830
|
-
ensureDataDir();
|
|
831
|
-
const content = JSON.stringify(data, null, 2);
|
|
832
|
-
fs6.writeFileSync(apiKeyPath, content, { mode: 384 });
|
|
833
|
-
logger6.info("API key saved", { path: apiKeyPath });
|
|
834
|
-
} catch (error) {
|
|
835
|
-
logger6.error("Failed to save API key", {
|
|
836
|
-
error: error instanceof Error ? error.message : String(error)
|
|
837
|
-
});
|
|
838
|
-
throw error;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
function deleteApiKeyData() {
|
|
842
|
-
const logger6 = getLogger();
|
|
843
|
-
const apiKeyPath = getApiKeyFilePath();
|
|
844
|
-
try {
|
|
845
|
-
if (fs6.existsSync(apiKeyPath)) {
|
|
846
|
-
fs6.unlinkSync(apiKeyPath);
|
|
847
|
-
logger6.info("API key deleted", { path: apiKeyPath });
|
|
848
|
-
}
|
|
849
|
-
} catch (error) {
|
|
850
|
-
logger6.warn("Failed to delete API key", {
|
|
851
|
-
error: error instanceof Error ? error.message : String(error)
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
function getValidApiKeyData() {
|
|
856
|
-
const data = loadApiKeyData();
|
|
857
|
-
if (!data) {
|
|
858
|
-
return null;
|
|
859
|
-
}
|
|
860
|
-
if (data.apiKey) {
|
|
861
|
-
return data;
|
|
862
|
-
}
|
|
863
|
-
return null;
|
|
864
|
-
}
|
|
865
|
-
function hasApiKey() {
|
|
866
|
-
const data = loadApiKeyData();
|
|
867
|
-
return !!data?.apiKey;
|
|
868
|
-
}
|
|
869
|
-
function getApiKey() {
|
|
870
|
-
const data = loadApiKeyData();
|
|
871
|
-
return data?.apiKey ?? null;
|
|
872
|
-
}
|
|
873
|
-
function saveApiKey(params) {
|
|
874
|
-
const logger6 = getLogger();
|
|
875
|
-
const apiKeyPath = getApiKeyFilePath();
|
|
876
|
-
try {
|
|
877
|
-
ensureDataDir();
|
|
878
|
-
const data = {
|
|
879
|
-
accessToken: "",
|
|
880
|
-
expiresAt: "",
|
|
881
|
-
apiKey: params.apiKey,
|
|
882
|
-
apiKeyId: params.apiKeyId
|
|
883
|
-
};
|
|
884
|
-
const content = JSON.stringify(data, null, 2);
|
|
885
|
-
fs6.writeFileSync(apiKeyPath, content, { mode: 384 });
|
|
886
|
-
logger6.info("API key saved", { path: apiKeyPath });
|
|
887
|
-
} catch (error) {
|
|
888
|
-
logger6.error("Failed to save API key", {
|
|
889
|
-
error: error instanceof Error ? error.message : String(error)
|
|
890
|
-
});
|
|
891
|
-
throw error;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
var loadCredentials = loadApiKeyData;
|
|
895
|
-
var saveCredentials = saveApiKeyData;
|
|
896
|
-
var deleteCredentials = deleteApiKeyData;
|
|
897
|
-
var getValidCredentials = getValidApiKeyData;
|
|
898
|
-
var getCredentialsFilePath = getApiKeyFilePath;
|
|
810
|
+
|
|
811
|
+
// packages/mcps/src/mcp/local/services/run-view/run-view-constants.ts
|
|
812
|
+
var RUN_SESSIONS_DIR_NAME = "sessions";
|
|
813
|
+
var ACTION_SCRIPT_FILE_NAME = "action-script.json";
|
|
814
|
+
var SCREENSHOT_DIR_RELATIVE_PATH = "electron-runtime/screenshot";
|
|
815
|
+
var SCREENSHOT_FILE_EXTENSION = ".jpg";
|
|
816
|
+
var HTML_VIEW_OFFER_LINE = "See the frames: re-run with --html (add --open to open it).";
|
|
899
817
|
|
|
900
818
|
// packages/mcps/src/mcp/local/types/enums.ts
|
|
901
819
|
var DeviceCodePollStatus = /* @__PURE__ */ ((DeviceCodePollStatus2) => {
|
|
@@ -980,1214 +898,1513 @@ var DisplayResolution = /* @__PURE__ */ ((DisplayResolution2) => {
|
|
|
980
898
|
return DisplayResolution2;
|
|
981
899
|
})(DisplayResolution || {});
|
|
982
900
|
|
|
983
|
-
// packages/mcps/src/mcp/local/services/
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const payload = parts[1];
|
|
991
|
-
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
|
|
992
|
-
return JSON.parse(decoded);
|
|
993
|
-
} catch {
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
function extractJwtIssuer(token) {
|
|
998
|
-
const payload = decodeJwtPayload(token);
|
|
999
|
-
if (!payload || typeof payload.iss !== "string") {
|
|
1000
|
-
return null;
|
|
1001
|
-
}
|
|
1002
|
-
return payload.iss;
|
|
1003
|
-
}
|
|
1004
|
-
function normalizeIssuerHost(issuer) {
|
|
1005
|
-
return issuer.replace(/^https:\/\//, "").replace(/\/$/, "");
|
|
1006
|
-
}
|
|
1007
|
-
function isStoredAuthForRuntimeTarget(params) {
|
|
1008
|
-
if (params.tokenIssuer && params.expectedAuth0Domain) {
|
|
1009
|
-
const issuerHost = normalizeIssuerHost(params.tokenIssuer);
|
|
1010
|
-
const expectedHost = normalizeIssuerHost(params.expectedAuth0Domain);
|
|
1011
|
-
if (issuerHost !== expectedHost) {
|
|
1012
|
-
return false;
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
if (!params.storedRuntimeTarget) {
|
|
1016
|
-
return params.activeRuntimeTarget === "production" /* Production */;
|
|
1017
|
-
}
|
|
1018
|
-
return params.storedRuntimeTarget === params.activeRuntimeTarget;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// packages/mcps/src/mcp/local/services/auth-service.ts
|
|
1022
|
-
var DEFAULT_LOGIN_WAIT_TIMEOUT_MS = 12e4;
|
|
1023
|
-
var AuthService = class {
|
|
1024
|
-
/** Path to the OAuth session file. */
|
|
1025
|
-
oauthSessionFilePath;
|
|
1026
|
-
/** Path to the pending device code file. */
|
|
1027
|
-
pendingDeviceCodePath;
|
|
901
|
+
// packages/mcps/src/mcp/local/services/storage-service.ts
|
|
902
|
+
var DEFAULT_SESSION_MAX_AGE_DAYS = 30;
|
|
903
|
+
var StorageService = class {
|
|
904
|
+
/** Base data directory. */
|
|
905
|
+
dataDir;
|
|
906
|
+
/** Sessions directory. */
|
|
907
|
+
sessionsDir;
|
|
1028
908
|
/**
|
|
1029
|
-
* Create a new
|
|
909
|
+
* Create a new StorageService.
|
|
1030
910
|
*/
|
|
1031
911
|
constructor() {
|
|
1032
912
|
const config = getConfig();
|
|
1033
|
-
this.
|
|
1034
|
-
this.
|
|
1035
|
-
path11.dirname(config.localQa.oauthSessionFilePath),
|
|
1036
|
-
"pending-device-code.json"
|
|
1037
|
-
);
|
|
913
|
+
this.dataDir = config.localQa.dataDir;
|
|
914
|
+
this.sessionsDir = config.localQa.sessionsDir;
|
|
1038
915
|
}
|
|
1039
916
|
/**
|
|
1040
|
-
*
|
|
917
|
+
* Ensure the base directories exist.
|
|
1041
918
|
*/
|
|
1042
|
-
|
|
919
|
+
ensureDirectories() {
|
|
1043
920
|
const logger6 = getLogger();
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
logger6.
|
|
1047
|
-
|
|
921
|
+
if (!fs4.existsSync(this.dataDir)) {
|
|
922
|
+
fs4.mkdirSync(this.dataDir, { recursive: true });
|
|
923
|
+
logger6.info("Created data directory", { path: this.dataDir });
|
|
924
|
+
}
|
|
925
|
+
if (!fs4.existsSync(this.sessionsDir)) {
|
|
926
|
+
fs4.mkdirSync(this.sessionsDir, { recursive: true });
|
|
927
|
+
logger6.info("Created sessions directory", { path: this.sessionsDir });
|
|
1048
928
|
}
|
|
1049
|
-
const now = /* @__PURE__ */ new Date();
|
|
1050
|
-
const expiresAt = new Date(storedAuth.expiresAt);
|
|
1051
|
-
const isExpired = now >= expiresAt;
|
|
1052
|
-
logger6.debug("Auth status checked", {
|
|
1053
|
-
email: storedAuth.email,
|
|
1054
|
-
isExpired,
|
|
1055
|
-
expiresAt: storedAuth.expiresAt
|
|
1056
|
-
});
|
|
1057
|
-
return {
|
|
1058
|
-
authenticated: !isExpired,
|
|
1059
|
-
email: storedAuth.email,
|
|
1060
|
-
userId: storedAuth.userId,
|
|
1061
|
-
expiresAt: storedAuth.expiresAt,
|
|
1062
|
-
isExpired
|
|
1063
|
-
};
|
|
1064
929
|
}
|
|
1065
930
|
/**
|
|
1066
|
-
*
|
|
1067
|
-
* @param
|
|
931
|
+
* Create a new session directory.
|
|
932
|
+
* @param sessionId - Unique session ID.
|
|
933
|
+
* @returns Path to the session directory.
|
|
1068
934
|
*/
|
|
1069
|
-
|
|
935
|
+
createSessionDirectory(sessionId) {
|
|
1070
936
|
const logger6 = getLogger();
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
audience
|
|
1079
|
-
});
|
|
1080
|
-
const response = await fetch(url, {
|
|
1081
|
-
method: "POST",
|
|
1082
|
-
headers: {
|
|
1083
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1084
|
-
},
|
|
1085
|
-
body: body.toString()
|
|
1086
|
-
});
|
|
1087
|
-
if (!response.ok) {
|
|
1088
|
-
const errorText = await response.text();
|
|
1089
|
-
logger6.error("Device code request failed", {
|
|
1090
|
-
status: response.status,
|
|
1091
|
-
error: errorText
|
|
1092
|
-
});
|
|
1093
|
-
throw new Error(`Failed to start device code flow: ${response.status} ${errorText}`);
|
|
1094
|
-
}
|
|
1095
|
-
const deviceCodeResponse = await response.json();
|
|
1096
|
-
logger6.info("Device code flow started", {
|
|
1097
|
-
userCode: deviceCodeResponse.user_code,
|
|
1098
|
-
verificationUri: deviceCodeResponse.verification_uri,
|
|
1099
|
-
expiresIn: deviceCodeResponse.expires_in
|
|
1100
|
-
});
|
|
1101
|
-
this.storePendingDeviceCode({
|
|
1102
|
-
deviceCode: deviceCodeResponse.device_code,
|
|
1103
|
-
userCode: deviceCodeResponse.user_code,
|
|
1104
|
-
expiresAt: new Date(Date.now() + deviceCodeResponse.expires_in * 1e3).toISOString()
|
|
1105
|
-
});
|
|
1106
|
-
let browserUrl = deviceCodeResponse.verification_uri_complete;
|
|
1107
|
-
if (options?.forceNewSession) {
|
|
1108
|
-
this.logout();
|
|
1109
|
-
const logoutUrl = new URL(`https://${domain}/v2/logout`);
|
|
1110
|
-
logoutUrl.searchParams.set("client_id", clientId);
|
|
1111
|
-
logoutUrl.searchParams.set("returnTo", deviceCodeResponse.verification_uri_complete);
|
|
1112
|
-
browserUrl = logoutUrl.toString();
|
|
1113
|
-
logger6.info("Force new session: cleared local token, opening logout-redirect URL", {
|
|
1114
|
-
logoutUrl: browserUrl
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
const browserOpenResult = await openBrowserUrl({
|
|
1118
|
-
url: browserUrl
|
|
1119
|
-
});
|
|
1120
|
-
if (browserOpenResult.opened) {
|
|
1121
|
-
logger6.info("Browser opened for device code login");
|
|
1122
|
-
} else {
|
|
1123
|
-
logger6.warn("Failed to open browser for device code login", {
|
|
1124
|
-
error: browserOpenResult.error,
|
|
1125
|
-
url: browserUrl
|
|
1126
|
-
});
|
|
937
|
+
this.ensureDirectories();
|
|
938
|
+
const sessionDir = path13.join(this.sessionsDir, sessionId);
|
|
939
|
+
if (!fs4.existsSync(sessionDir)) {
|
|
940
|
+
fs4.mkdirSync(sessionDir, { recursive: true });
|
|
941
|
+
fs4.mkdirSync(path13.join(sessionDir, "screenshots"), { recursive: true });
|
|
942
|
+
fs4.mkdirSync(path13.join(sessionDir, "logs"), { recursive: true });
|
|
943
|
+
logger6.info("Created session directory", { sessionId, path: sessionDir });
|
|
1127
944
|
}
|
|
1128
|
-
return
|
|
1129
|
-
deviceCode: deviceCodeResponse.device_code,
|
|
1130
|
-
userCode: deviceCodeResponse.user_code,
|
|
1131
|
-
verificationUri: deviceCodeResponse.verification_uri,
|
|
1132
|
-
verificationUriComplete: deviceCodeResponse.verification_uri_complete,
|
|
1133
|
-
expiresIn: deviceCodeResponse.expires_in,
|
|
1134
|
-
interval: deviceCodeResponse.interval,
|
|
1135
|
-
browserOpened: browserOpenResult.opened,
|
|
1136
|
-
browserOpenError: browserOpenResult.error
|
|
1137
|
-
};
|
|
945
|
+
return sessionDir;
|
|
1138
946
|
}
|
|
1139
947
|
/**
|
|
1140
|
-
*
|
|
948
|
+
* Save session metadata.
|
|
949
|
+
* @param metadata - Session metadata to save.
|
|
1141
950
|
*/
|
|
1142
|
-
|
|
951
|
+
saveSessionMetadata(metadata) {
|
|
1143
952
|
const logger6 = getLogger();
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
fs6.writeFileSync(this.pendingDeviceCodePath, JSON.stringify(params, null, 2), {
|
|
1149
|
-
encoding: "utf-8",
|
|
1150
|
-
mode: 384
|
|
1151
|
-
});
|
|
1152
|
-
logger6.debug("Pending device code stored", { userCode: params.userCode });
|
|
953
|
+
const sessionDir = this.createSessionDirectory(metadata.sessionId);
|
|
954
|
+
const metadataPath = path13.join(sessionDir, "metadata.json");
|
|
955
|
+
fs4.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
956
|
+
logger6.debug("Saved session metadata", { sessionId: metadata.sessionId });
|
|
1153
957
|
}
|
|
1154
958
|
/**
|
|
1155
|
-
*
|
|
959
|
+
* Load session metadata.
|
|
960
|
+
* @param sessionId - Session ID to load.
|
|
961
|
+
* @returns Session metadata, or null if not found.
|
|
1156
962
|
*/
|
|
1157
|
-
|
|
963
|
+
loadSessionMetadata(sessionId) {
|
|
1158
964
|
const logger6 = getLogger();
|
|
1159
|
-
|
|
1160
|
-
|
|
965
|
+
const metadataPath = path13.join(this.sessionsDir, sessionId, "metadata.json");
|
|
966
|
+
if (!fs4.existsSync(metadataPath)) {
|
|
1161
967
|
return null;
|
|
1162
968
|
}
|
|
1163
969
|
try {
|
|
1164
|
-
const content =
|
|
1165
|
-
|
|
1166
|
-
const now = /* @__PURE__ */ new Date();
|
|
1167
|
-
const expiresAt = new Date(pendingDeviceCodeData.expiresAt);
|
|
1168
|
-
if (now >= expiresAt) {
|
|
1169
|
-
logger6.debug("Pending device code expired");
|
|
1170
|
-
this.clearPendingDeviceCode();
|
|
1171
|
-
return null;
|
|
1172
|
-
}
|
|
1173
|
-
return pendingDeviceCodeData.deviceCode;
|
|
970
|
+
const content = fs4.readFileSync(metadataPath, "utf-8");
|
|
971
|
+
return JSON.parse(content);
|
|
1174
972
|
} catch (error) {
|
|
1175
|
-
logger6.
|
|
973
|
+
logger6.error("Failed to load session metadata", {
|
|
974
|
+
sessionId,
|
|
1176
975
|
error: error instanceof Error ? error.message : String(error)
|
|
1177
976
|
});
|
|
1178
977
|
return null;
|
|
1179
978
|
}
|
|
1180
979
|
}
|
|
1181
980
|
/**
|
|
1182
|
-
*
|
|
981
|
+
* List all sessions.
|
|
982
|
+
* @returns Array of session IDs.
|
|
1183
983
|
*/
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
try {
|
|
1188
|
-
fs6.unlinkSync(this.pendingDeviceCodePath);
|
|
1189
|
-
logger6.debug("Pending device code cleared");
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
logger6.warn("Failed to clear pending device code", {
|
|
1192
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
984
|
+
listSessions() {
|
|
985
|
+
if (!fs4.existsSync(this.sessionsDir)) {
|
|
986
|
+
return [];
|
|
1195
987
|
}
|
|
988
|
+
return fs4.readdirSync(this.sessionsDir).filter((entry) => {
|
|
989
|
+
const entryPath = path13.join(this.sessionsDir, entry);
|
|
990
|
+
return fs4.statSync(entryPath).isDirectory();
|
|
991
|
+
});
|
|
1196
992
|
}
|
|
1197
993
|
/**
|
|
1198
|
-
*
|
|
994
|
+
* Get the current session ID (if any).
|
|
995
|
+
* @returns Current session ID, or null.
|
|
1199
996
|
*/
|
|
1200
|
-
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const url = `https://${domain}/oauth/token`;
|
|
1206
|
-
const body = new URLSearchParams({
|
|
1207
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1208
|
-
client_id: clientId,
|
|
1209
|
-
device_code: deviceCode
|
|
1210
|
-
});
|
|
997
|
+
getCurrentSessionId() {
|
|
998
|
+
const currentPath = path13.join(this.sessionsDir, "current");
|
|
999
|
+
if (!fs4.existsSync(currentPath)) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1211
1002
|
try {
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
},
|
|
1217
|
-
body: body.toString()
|
|
1218
|
-
});
|
|
1219
|
-
if (response.ok) {
|
|
1220
|
-
const tokenData = await response.json();
|
|
1221
|
-
const tokenResponse = {
|
|
1222
|
-
accessToken: tokenData.access_token,
|
|
1223
|
-
refreshToken: tokenData.refresh_token,
|
|
1224
|
-
tokenType: tokenData.token_type,
|
|
1225
|
-
expiresIn: tokenData.expires_in
|
|
1226
|
-
};
|
|
1227
|
-
const userInfo = await this.getUserInfo(tokenResponse.accessToken);
|
|
1228
|
-
await this.storeAuth({
|
|
1229
|
-
tokenResponse,
|
|
1230
|
-
email: userInfo.email,
|
|
1231
|
-
userId: userInfo.sub
|
|
1232
|
-
});
|
|
1233
|
-
this.clearPendingDeviceCode();
|
|
1234
|
-
logger6.info("Device code authorization complete", { email: userInfo.email });
|
|
1235
|
-
return {
|
|
1236
|
-
status: "complete" /* Complete */,
|
|
1237
|
-
message: "Authentication successful!",
|
|
1238
|
-
email: userInfo.email
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
const errorData = await response.json();
|
|
1242
|
-
if (errorData.error === "authorization_pending") {
|
|
1243
|
-
logger6.debug("Authorization pending");
|
|
1244
|
-
return {
|
|
1245
|
-
status: "pending" /* Pending */,
|
|
1246
|
-
message: "Waiting for user to complete authorization..."
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
if (errorData.error === "slow_down") {
|
|
1250
|
-
logger6.debug("Polling too fast");
|
|
1251
|
-
return {
|
|
1252
|
-
status: "pending" /* Pending */,
|
|
1253
|
-
message: "Polling too fast, slowing down..."
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
if (errorData.error === "expired_token") {
|
|
1257
|
-
logger6.warn("Device code expired");
|
|
1258
|
-
return {
|
|
1259
|
-
status: "expired" /* Expired */,
|
|
1260
|
-
message: "The authorization code has expired. Please start again."
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
if (errorData.error === "access_denied") {
|
|
1264
|
-
logger6.warn("Access denied");
|
|
1265
|
-
return {
|
|
1266
|
-
status: "error" /* Error */,
|
|
1267
|
-
message: "Access was denied by the user.",
|
|
1268
|
-
error: errorData.error_description ?? errorData.error
|
|
1269
|
-
};
|
|
1270
|
-
}
|
|
1271
|
-
logger6.error("Unexpected error during polling", { error: errorData });
|
|
1272
|
-
return {
|
|
1273
|
-
status: "error" /* Error */,
|
|
1274
|
-
message: errorData.error_description ?? errorData.error,
|
|
1275
|
-
error: errorData.error
|
|
1276
|
-
};
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1279
|
-
logger6.error("Poll request failed", { error: errorMessage });
|
|
1280
|
-
return {
|
|
1281
|
-
status: "error" /* Error */,
|
|
1282
|
-
message: `Poll request failed: ${errorMessage}`,
|
|
1283
|
-
error: errorMessage
|
|
1284
|
-
};
|
|
1003
|
+
const target = fs4.readlinkSync(currentPath);
|
|
1004
|
+
return path13.basename(target);
|
|
1005
|
+
} catch {
|
|
1006
|
+
return null;
|
|
1285
1007
|
}
|
|
1286
1008
|
}
|
|
1287
1009
|
/**
|
|
1288
|
-
*
|
|
1010
|
+
* Set the current session.
|
|
1011
|
+
* @param sessionId - Session ID to set as current.
|
|
1289
1012
|
*/
|
|
1290
|
-
|
|
1013
|
+
setCurrentSession(sessionId) {
|
|
1291
1014
|
const logger6 = getLogger();
|
|
1292
|
-
const
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
timeoutMs,
|
|
1297
|
-
pollIntervalMs
|
|
1298
|
-
});
|
|
1299
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
1300
|
-
const pollResult = await this.pollDeviceCode(params.deviceCode);
|
|
1301
|
-
if (pollResult.status !== "pending" /* Pending */) {
|
|
1302
|
-
return pollResult;
|
|
1303
|
-
}
|
|
1304
|
-
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
1305
|
-
if (remainingMs <= 0) {
|
|
1306
|
-
break;
|
|
1307
|
-
}
|
|
1308
|
-
await sleep({ durationMs: Math.min(pollIntervalMs, remainingMs) });
|
|
1015
|
+
const currentPath = path13.join(this.sessionsDir, "current");
|
|
1016
|
+
const targetPath = path13.join(this.sessionsDir, sessionId);
|
|
1017
|
+
if (fs4.existsSync(currentPath)) {
|
|
1018
|
+
fs4.unlinkSync(currentPath);
|
|
1309
1019
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
message: "Timed out waiting for browser login confirmation. Please finish login in your browser, then call `muggle_auth_poll` to continue."
|
|
1313
|
-
};
|
|
1020
|
+
fs4.symlinkSync(targetPath, currentPath);
|
|
1021
|
+
logger6.info("Set current session", { sessionId });
|
|
1314
1022
|
}
|
|
1315
1023
|
/**
|
|
1316
|
-
*
|
|
1024
|
+
* Save a screenshot to the session directory.
|
|
1025
|
+
* @param params - Screenshot save parameters.
|
|
1317
1026
|
*/
|
|
1318
|
-
|
|
1027
|
+
saveScreenshot(params) {
|
|
1028
|
+
const { sessionId, filename, data } = params;
|
|
1319
1029
|
const logger6 = getLogger();
|
|
1320
|
-
const
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
method: "GET",
|
|
1326
|
-
headers: {
|
|
1327
|
-
Authorization: `Bearer ${accessToken}`
|
|
1328
|
-
}
|
|
1329
|
-
});
|
|
1330
|
-
if (!response.ok) {
|
|
1331
|
-
logger6.warn("Failed to get user info", { status: response.status });
|
|
1332
|
-
return {};
|
|
1333
|
-
}
|
|
1334
|
-
const userInfoData = await response.json();
|
|
1335
|
-
return userInfoData;
|
|
1336
|
-
} catch (error) {
|
|
1337
|
-
logger6.warn("User info request failed", {
|
|
1338
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1339
|
-
});
|
|
1340
|
-
return {};
|
|
1341
|
-
}
|
|
1030
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
1031
|
+
const screenshotPath = path13.join(sessionDir, "screenshots", filename);
|
|
1032
|
+
fs4.writeFileSync(screenshotPath, data);
|
|
1033
|
+
logger6.debug("Saved screenshot", { sessionId, filename });
|
|
1034
|
+
return screenshotPath;
|
|
1342
1035
|
}
|
|
1343
1036
|
/**
|
|
1344
|
-
*
|
|
1037
|
+
* Append to the results markdown file.
|
|
1038
|
+
* @param params - Results append parameters.
|
|
1345
1039
|
*/
|
|
1346
|
-
|
|
1347
|
-
const {
|
|
1040
|
+
appendToResults(params) {
|
|
1041
|
+
const { sessionId, content } = params;
|
|
1348
1042
|
const logger6 = getLogger();
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
expiresAt,
|
|
1354
|
-
email,
|
|
1355
|
-
userId,
|
|
1356
|
-
runtimeTarget: getActiveRuntimeTarget()
|
|
1357
|
-
};
|
|
1358
|
-
const dir = path11.dirname(this.oauthSessionFilePath);
|
|
1359
|
-
if (!fs6.existsSync(dir)) {
|
|
1360
|
-
fs6.mkdirSync(dir, { recursive: true });
|
|
1361
|
-
}
|
|
1362
|
-
fs6.writeFileSync(this.oauthSessionFilePath, JSON.stringify(storedAuth, null, 2), {
|
|
1363
|
-
encoding: "utf-8",
|
|
1364
|
-
mode: 384
|
|
1365
|
-
});
|
|
1366
|
-
logger6.info("Auth stored successfully", { email, expiresAt });
|
|
1043
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
1044
|
+
const resultsPath = path13.join(sessionDir, "results.md");
|
|
1045
|
+
fs4.appendFileSync(resultsPath, content + "\n", "utf-8");
|
|
1046
|
+
logger6.debug("Appended to results", { sessionId });
|
|
1367
1047
|
}
|
|
1368
1048
|
/**
|
|
1369
|
-
*
|
|
1370
|
-
* @
|
|
1049
|
+
* Get the results markdown content.
|
|
1050
|
+
* @param sessionId - Session ID.
|
|
1051
|
+
* @returns Results content, or null if not found.
|
|
1371
1052
|
*/
|
|
1372
|
-
|
|
1373
|
-
const
|
|
1374
|
-
if (!
|
|
1375
|
-
return null;
|
|
1376
|
-
}
|
|
1377
|
-
try {
|
|
1378
|
-
const content = fs6.readFileSync(this.oauthSessionFilePath, "utf-8");
|
|
1379
|
-
const storedAuth = JSON.parse(content);
|
|
1380
|
-
const activeRuntimeTarget = getActiveRuntimeTarget();
|
|
1381
|
-
const config = getConfig();
|
|
1382
|
-
const expectedAuth0Domain = config.localQa.auth0.domain;
|
|
1383
|
-
const tokenIssuer = extractJwtIssuer(storedAuth.accessToken);
|
|
1384
|
-
if (!isStoredAuthForRuntimeTarget({
|
|
1385
|
-
storedRuntimeTarget: storedAuth.runtimeTarget,
|
|
1386
|
-
activeRuntimeTarget,
|
|
1387
|
-
tokenIssuer,
|
|
1388
|
-
expectedAuth0Domain
|
|
1389
|
-
})) {
|
|
1390
|
-
logger6.warn("Ignoring stored auth issued for a different runtime target", {
|
|
1391
|
-
storedRuntimeTarget: storedAuth.runtimeTarget ?? "unrecorded",
|
|
1392
|
-
activeRuntimeTarget,
|
|
1393
|
-
tokenIssuer: tokenIssuer ?? "undecodable",
|
|
1394
|
-
expectedAuth0Domain
|
|
1395
|
-
});
|
|
1396
|
-
return null;
|
|
1397
|
-
}
|
|
1398
|
-
return storedAuth;
|
|
1399
|
-
} catch (error) {
|
|
1400
|
-
logger6.error("Failed to load stored auth", {
|
|
1401
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1402
|
-
});
|
|
1053
|
+
getResults(sessionId) {
|
|
1054
|
+
const resultsPath = path13.join(this.sessionsDir, sessionId, "results.md");
|
|
1055
|
+
if (!fs4.existsSync(resultsPath)) {
|
|
1403
1056
|
return null;
|
|
1404
1057
|
}
|
|
1058
|
+
return fs4.readFileSync(resultsPath, "utf-8");
|
|
1405
1059
|
}
|
|
1406
1060
|
/**
|
|
1407
|
-
* Get the
|
|
1408
|
-
*
|
|
1061
|
+
* Get the data directory path.
|
|
1062
|
+
* @returns Data directory path.
|
|
1409
1063
|
*/
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
if (!storedAuth) {
|
|
1413
|
-
return null;
|
|
1414
|
-
}
|
|
1415
|
-
const now = /* @__PURE__ */ new Date();
|
|
1416
|
-
const expiresAt = new Date(storedAuth.expiresAt);
|
|
1417
|
-
if (now >= expiresAt) {
|
|
1418
|
-
return null;
|
|
1419
|
-
}
|
|
1420
|
-
return storedAuth.accessToken;
|
|
1064
|
+
getDataDir() {
|
|
1065
|
+
return this.dataDir;
|
|
1421
1066
|
}
|
|
1422
1067
|
/**
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1068
|
+
* Get the sessions directory path.
|
|
1069
|
+
* @returns Sessions directory path.
|
|
1425
1070
|
*/
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
if (!storedAuth) {
|
|
1429
|
-
return true;
|
|
1430
|
-
}
|
|
1431
|
-
const now = /* @__PURE__ */ new Date();
|
|
1432
|
-
const expiresAt = new Date(storedAuth.expiresAt);
|
|
1433
|
-
const bufferMs = 5 * 60 * 1e3;
|
|
1434
|
-
return now.getTime() >= expiresAt.getTime() - bufferMs;
|
|
1071
|
+
getSessionsDir() {
|
|
1072
|
+
return this.sessionsDir;
|
|
1435
1073
|
}
|
|
1436
1074
|
/**
|
|
1437
|
-
*
|
|
1438
|
-
* @
|
|
1075
|
+
* Create a new session with metadata.
|
|
1076
|
+
* @param params - Session creation parameters.
|
|
1077
|
+
* @returns Path to the session directory.
|
|
1439
1078
|
*/
|
|
1440
|
-
|
|
1079
|
+
createSession(params) {
|
|
1080
|
+
const { sessionId, targetUrl, testInstructions } = params;
|
|
1441
1081
|
const logger6 = getLogger();
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
});
|
|
1456
|
-
try {
|
|
1457
|
-
const response = await fetch(url, {
|
|
1458
|
-
method: "POST",
|
|
1459
|
-
headers: {
|
|
1460
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1461
|
-
},
|
|
1462
|
-
body: body.toString()
|
|
1463
|
-
});
|
|
1464
|
-
if (!response.ok) {
|
|
1465
|
-
const errorText = await response.text();
|
|
1466
|
-
logger6.error("Token refresh failed", {
|
|
1467
|
-
status: response.status,
|
|
1468
|
-
error: errorText
|
|
1469
|
-
});
|
|
1470
|
-
return null;
|
|
1471
|
-
}
|
|
1472
|
-
const tokenData = await response.json();
|
|
1473
|
-
const newExpiresAt = new Date(Date.now() + tokenData.expires_in * 1e3).toISOString();
|
|
1474
|
-
const updatedAuth = {
|
|
1475
|
-
accessToken: tokenData.access_token,
|
|
1476
|
-
refreshToken: tokenData.refresh_token ?? storedAuth.refreshToken,
|
|
1477
|
-
expiresAt: newExpiresAt,
|
|
1478
|
-
email: storedAuth.email,
|
|
1479
|
-
userId: storedAuth.userId,
|
|
1480
|
-
runtimeTarget: storedAuth.runtimeTarget ?? getActiveRuntimeTarget()
|
|
1481
|
-
};
|
|
1482
|
-
const dir = path11.dirname(this.oauthSessionFilePath);
|
|
1483
|
-
if (!fs6.existsSync(dir)) {
|
|
1484
|
-
fs6.mkdirSync(dir, { recursive: true });
|
|
1485
|
-
}
|
|
1486
|
-
fs6.writeFileSync(this.oauthSessionFilePath, JSON.stringify(updatedAuth, null, 2), {
|
|
1487
|
-
encoding: "utf-8",
|
|
1488
|
-
mode: 384
|
|
1489
|
-
});
|
|
1490
|
-
logger6.info("Access token refreshed", { expiresAt: newExpiresAt });
|
|
1491
|
-
return tokenData.access_token;
|
|
1492
|
-
} catch (error) {
|
|
1493
|
-
logger6.error("Token refresh request failed", {
|
|
1494
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1495
|
-
});
|
|
1496
|
-
return null;
|
|
1497
|
-
}
|
|
1082
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
1083
|
+
const metadata = {
|
|
1084
|
+
sessionId,
|
|
1085
|
+
workflowRunId: sessionId,
|
|
1086
|
+
status: "running" /* Running */,
|
|
1087
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1088
|
+
targetUrl,
|
|
1089
|
+
testInstructions
|
|
1090
|
+
};
|
|
1091
|
+
this.saveSessionMetadata(metadata);
|
|
1092
|
+
this.setCurrentSession(sessionId);
|
|
1093
|
+
logger6.info("Created session", { sessionId, targetUrl });
|
|
1094
|
+
return sessionDir;
|
|
1498
1095
|
}
|
|
1499
1096
|
/**
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
* @returns Valid access token or null if not authenticated or refresh failed.
|
|
1097
|
+
* Update the status of an existing session.
|
|
1098
|
+
* @param params - Status update parameters.
|
|
1503
1099
|
*/
|
|
1504
|
-
|
|
1100
|
+
updateSessionStatus(params) {
|
|
1101
|
+
const { sessionId, status } = params;
|
|
1505
1102
|
const logger6 = getLogger();
|
|
1506
|
-
const
|
|
1507
|
-
if (!
|
|
1508
|
-
logger6.
|
|
1509
|
-
return
|
|
1510
|
-
}
|
|
1511
|
-
const now = /* @__PURE__ */ new Date();
|
|
1512
|
-
const expiresAt = new Date(storedAuth.expiresAt);
|
|
1513
|
-
const isStrictlyExpired = now >= expiresAt;
|
|
1514
|
-
if (!isStrictlyExpired && !this.isAccessTokenExpired()) {
|
|
1515
|
-
return storedAuth.accessToken;
|
|
1516
|
-
}
|
|
1517
|
-
if (!isStrictlyExpired) {
|
|
1518
|
-
logger6.debug("Access token in buffer zone, attempting proactive refresh");
|
|
1519
|
-
} else {
|
|
1520
|
-
logger6.info("Access token expired, attempting refresh");
|
|
1521
|
-
}
|
|
1522
|
-
const refreshedToken = await this.refreshAccessToken();
|
|
1523
|
-
if (refreshedToken) {
|
|
1524
|
-
return refreshedToken;
|
|
1103
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
1104
|
+
if (!metadata) {
|
|
1105
|
+
logger6.warn("Session not found for status update", { sessionId });
|
|
1106
|
+
return;
|
|
1525
1107
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1108
|
+
metadata.status = status;
|
|
1109
|
+
if (status === "completed" /* Completed */ || status === "failed" /* Failed */) {
|
|
1110
|
+
metadata.endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1529
1111
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1112
|
+
this.saveSessionMetadata(metadata);
|
|
1113
|
+
logger6.debug("Updated session status", { sessionId, status });
|
|
1532
1114
|
}
|
|
1533
1115
|
/**
|
|
1534
|
-
*
|
|
1116
|
+
* Initialize the results.md file with a header.
|
|
1117
|
+
* @param params - Initialization parameters.
|
|
1535
1118
|
*/
|
|
1536
|
-
|
|
1119
|
+
initializeResults(params) {
|
|
1120
|
+
const { sessionId, targetUrl, testInstructions } = params;
|
|
1537
1121
|
const logger6 = getLogger();
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1122
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
1123
|
+
const resultsPath = path13.join(sessionDir, "results.md");
|
|
1124
|
+
const header = [
|
|
1125
|
+
`# Test Results: ${sessionId}`,
|
|
1126
|
+
"",
|
|
1127
|
+
`**Target URL:** ${targetUrl}`,
|
|
1128
|
+
`**Started:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1129
|
+
`**Instructions:** ${testInstructions}`,
|
|
1130
|
+
"",
|
|
1131
|
+
"---",
|
|
1132
|
+
"",
|
|
1133
|
+
"## Test Steps",
|
|
1134
|
+
""
|
|
1135
|
+
].join("\n");
|
|
1136
|
+
fs4.writeFileSync(resultsPath, header, "utf-8");
|
|
1137
|
+
logger6.debug("Initialized results.md", { sessionId });
|
|
1552
1138
|
}
|
|
1553
|
-
};
|
|
1554
|
-
var serviceInstance = null;
|
|
1555
|
-
function getAuthService() {
|
|
1556
|
-
serviceInstance ??= new AuthService();
|
|
1557
|
-
return serviceInstance;
|
|
1558
|
-
}
|
|
1559
|
-
function resetAuthService() {
|
|
1560
|
-
serviceInstance = null;
|
|
1561
|
-
}
|
|
1562
|
-
function sleep(params) {
|
|
1563
|
-
return new Promise((resolve6) => {
|
|
1564
|
-
setTimeout(() => {
|
|
1565
|
-
resolve6();
|
|
1566
|
-
}, params.durationMs);
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
var DEFAULT_SESSION_MAX_AGE_DAYS = 30;
|
|
1570
|
-
var StorageService = class {
|
|
1571
|
-
/** Base data directory. */
|
|
1572
|
-
dataDir;
|
|
1573
|
-
/** Sessions directory. */
|
|
1574
|
-
sessionsDir;
|
|
1575
1139
|
/**
|
|
1576
|
-
*
|
|
1140
|
+
* Append a test step to the results.md file.
|
|
1141
|
+
* @param params - Step parameters.
|
|
1577
1142
|
*/
|
|
1578
|
-
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1143
|
+
appendStepToResults(params) {
|
|
1144
|
+
const { sessionId, step } = params;
|
|
1145
|
+
const logger6 = getLogger();
|
|
1146
|
+
const sessionDir = this.createSessionDirectory(sessionId);
|
|
1147
|
+
const resultsPath = path13.join(sessionDir, "results.md");
|
|
1148
|
+
const statusIcon = step.success ? "\u2713" : "\u2717";
|
|
1149
|
+
const stepContent = [
|
|
1150
|
+
`### Step ${step.stepNumber}: ${step.action}`,
|
|
1151
|
+
"",
|
|
1152
|
+
step.target ? `- **Target:** ${step.target}` : "",
|
|
1153
|
+
`- **Result:** ${step.result} ${statusIcon}`,
|
|
1154
|
+
step.screenshotPath ? `- **Screenshot:** [step-${String(step.stepNumber).padStart(3, "0")}.png](screenshots/step-${String(step.stepNumber).padStart(3, "0")}.png)` : "",
|
|
1155
|
+
""
|
|
1156
|
+
].filter(Boolean).join("\n");
|
|
1157
|
+
fs4.appendFileSync(resultsPath, stepContent + "\n", "utf-8");
|
|
1158
|
+
logger6.debug("Appended step to results", { sessionId, stepNumber: step.stepNumber });
|
|
1159
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
1160
|
+
if (metadata) {
|
|
1161
|
+
metadata.stepsCount = (metadata.stepsCount ?? 0) + 1;
|
|
1162
|
+
this.saveSessionMetadata(metadata);
|
|
1163
|
+
}
|
|
1582
1164
|
}
|
|
1583
1165
|
/**
|
|
1584
|
-
*
|
|
1166
|
+
* Finalize the results.md file with a summary.
|
|
1167
|
+
* @param params - Finalization parameters.
|
|
1585
1168
|
*/
|
|
1586
|
-
|
|
1169
|
+
finalizeResults(params) {
|
|
1170
|
+
const { sessionId, status, summary } = params;
|
|
1587
1171
|
const logger6 = getLogger();
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1172
|
+
const sessionDir = path13.join(this.sessionsDir, sessionId);
|
|
1173
|
+
const resultsPath = path13.join(sessionDir, "results.md");
|
|
1174
|
+
if (!fs4.existsSync(resultsPath)) {
|
|
1175
|
+
logger6.warn("Results file not found for finalization", { sessionId });
|
|
1176
|
+
return;
|
|
1591
1177
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1178
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
1179
|
+
const endTime = /* @__PURE__ */ new Date();
|
|
1180
|
+
const startTime = metadata?.startTime ? new Date(metadata.startTime) : endTime;
|
|
1181
|
+
const durationMs = endTime.getTime() - startTime.getTime();
|
|
1182
|
+
const durationSeconds = (durationMs / 1e3).toFixed(2);
|
|
1183
|
+
const statusDisplay = status === "completed" /* Completed */ ? "\u2713 Passed" : status === "failed" /* Failed */ ? "\u2717 Failed" : "\u2014 Running";
|
|
1184
|
+
const footer = [
|
|
1185
|
+
"",
|
|
1186
|
+
"---",
|
|
1187
|
+
"",
|
|
1188
|
+
"## Summary",
|
|
1189
|
+
"",
|
|
1190
|
+
`**Status:** ${statusDisplay}`,
|
|
1191
|
+
`**Duration:** ${durationSeconds}s`,
|
|
1192
|
+
`**Steps:** ${metadata?.stepsCount ?? 0}`,
|
|
1193
|
+
`**Completed:** ${endTime.toISOString()}`,
|
|
1194
|
+
"",
|
|
1195
|
+
summary ? summary : ""
|
|
1196
|
+
].join("\n");
|
|
1197
|
+
fs4.appendFileSync(resultsPath, footer, "utf-8");
|
|
1198
|
+
logger6.debug("Finalized results.md", { sessionId, status });
|
|
1199
|
+
if (metadata) {
|
|
1200
|
+
metadata.durationMs = durationMs;
|
|
1201
|
+
metadata.endTime = endTime.toISOString();
|
|
1202
|
+
metadata.status = status;
|
|
1203
|
+
this.saveSessionMetadata(metadata);
|
|
1595
1204
|
}
|
|
1596
1205
|
}
|
|
1597
1206
|
/**
|
|
1598
|
-
*
|
|
1599
|
-
* @
|
|
1600
|
-
* @returns Path to the session directory.
|
|
1207
|
+
* List all sessions with their metadata.
|
|
1208
|
+
* @returns Array of session summaries.
|
|
1601
1209
|
*/
|
|
1602
|
-
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1210
|
+
listSessionsWithMetadata() {
|
|
1211
|
+
const sessionIds = this.listSessions();
|
|
1212
|
+
const summaries = [];
|
|
1213
|
+
for (const sessionId of sessionIds) {
|
|
1214
|
+
if (sessionId === "current") {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
1218
|
+
if (metadata) {
|
|
1219
|
+
summaries.push({
|
|
1220
|
+
sessionId: metadata.sessionId,
|
|
1221
|
+
status: metadata.status,
|
|
1222
|
+
startTime: metadata.startTime,
|
|
1223
|
+
endTime: metadata.endTime,
|
|
1224
|
+
targetUrl: metadata.targetUrl,
|
|
1225
|
+
durationMs: metadata.durationMs,
|
|
1226
|
+
stepsCount: metadata.stepsCount
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1611
1229
|
}
|
|
1612
|
-
|
|
1230
|
+
summaries.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
|
1231
|
+
return summaries;
|
|
1613
1232
|
}
|
|
1614
1233
|
/**
|
|
1615
|
-
*
|
|
1616
|
-
* @param
|
|
1234
|
+
* Cleanup old sessions beyond the specified age.
|
|
1235
|
+
* @param params - Cleanup parameters.
|
|
1236
|
+
* @returns Number of sessions deleted.
|
|
1617
1237
|
*/
|
|
1618
|
-
|
|
1238
|
+
cleanupOldSessions(params) {
|
|
1239
|
+
const maxAgeDays = params?.maxAgeDays ?? DEFAULT_SESSION_MAX_AGE_DAYS;
|
|
1619
1240
|
const logger6 = getLogger();
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1241
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
1242
|
+
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
|
|
1243
|
+
const sessionIds = this.listSessions();
|
|
1244
|
+
let deletedCount = 0;
|
|
1245
|
+
for (const sessionId of sessionIds) {
|
|
1246
|
+
if (sessionId === "current") {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
const metadata = this.loadSessionMetadata(sessionId);
|
|
1250
|
+
if (!metadata) {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const sessionDate = new Date(metadata.startTime);
|
|
1254
|
+
if (sessionDate < cutoffDate) {
|
|
1255
|
+
const sessionDir = path13.join(this.sessionsDir, sessionId);
|
|
1256
|
+
try {
|
|
1257
|
+
fs4.rmSync(sessionDir, { recursive: true, force: true });
|
|
1258
|
+
deletedCount++;
|
|
1259
|
+
logger6.info("Deleted old session", {
|
|
1260
|
+
sessionId,
|
|
1261
|
+
age: Math.floor((Date.now() - sessionDate.getTime()) / (1e3 * 60 * 60 * 24))
|
|
1262
|
+
});
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
logger6.error("Failed to delete session", {
|
|
1265
|
+
sessionId,
|
|
1266
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (deletedCount > 0) {
|
|
1272
|
+
logger6.info("Session cleanup completed", {
|
|
1273
|
+
deletedCount,
|
|
1274
|
+
maxAgeDays
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
return deletedCount;
|
|
1624
1278
|
}
|
|
1625
1279
|
/**
|
|
1626
|
-
*
|
|
1627
|
-
* @param sessionId - Session ID
|
|
1628
|
-
* @returns Session
|
|
1280
|
+
* Get a session directory path.
|
|
1281
|
+
* @param sessionId - Session ID.
|
|
1282
|
+
* @returns Session directory path.
|
|
1629
1283
|
*/
|
|
1630
|
-
|
|
1284
|
+
getSessionPath(sessionId) {
|
|
1285
|
+
return path13.join(this.sessionsDir, sessionId);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Delete a specific session.
|
|
1289
|
+
* @param sessionId - Session ID to delete.
|
|
1290
|
+
* @returns Whether deletion succeeded.
|
|
1291
|
+
*/
|
|
1292
|
+
deleteSession(sessionId) {
|
|
1631
1293
|
const logger6 = getLogger();
|
|
1632
|
-
const
|
|
1633
|
-
if (!
|
|
1634
|
-
|
|
1294
|
+
const sessionDir = path13.join(this.sessionsDir, sessionId);
|
|
1295
|
+
if (!fs4.existsSync(sessionDir)) {
|
|
1296
|
+
logger6.warn("Session not found for deletion", { sessionId });
|
|
1297
|
+
return false;
|
|
1635
1298
|
}
|
|
1636
1299
|
try {
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1300
|
+
const currentId = this.getCurrentSessionId();
|
|
1301
|
+
if (currentId === sessionId) {
|
|
1302
|
+
const currentPath = path13.join(this.sessionsDir, "current");
|
|
1303
|
+
if (fs4.existsSync(currentPath)) {
|
|
1304
|
+
fs4.unlinkSync(currentPath);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
fs4.rmSync(sessionDir, { recursive: true, force: true });
|
|
1308
|
+
logger6.info("Deleted session", { sessionId });
|
|
1309
|
+
return true;
|
|
1639
1310
|
} catch (error) {
|
|
1640
|
-
logger6.error("Failed to
|
|
1311
|
+
logger6.error("Failed to delete session", {
|
|
1641
1312
|
sessionId,
|
|
1642
1313
|
error: error instanceof Error ? error.message : String(error)
|
|
1643
1314
|
});
|
|
1644
|
-
return
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
var serviceInstance = null;
|
|
1320
|
+
function getStorageService() {
|
|
1321
|
+
serviceInstance ??= new StorageService();
|
|
1322
|
+
return serviceInstance;
|
|
1323
|
+
}
|
|
1324
|
+
function resetStorageService() {
|
|
1325
|
+
serviceInstance = null;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// packages/mcps/src/mcp/local/services/run-result-storage-service.ts
|
|
1329
|
+
var logger3 = getLogger();
|
|
1330
|
+
var RunResultStorageService = class {
|
|
1331
|
+
/** Base directory for run results. */
|
|
1332
|
+
runResultsDir;
|
|
1333
|
+
/** Base directory for test scripts. */
|
|
1334
|
+
testScriptsDir;
|
|
1335
|
+
constructor() {
|
|
1336
|
+
const storageService = getStorageService();
|
|
1337
|
+
const dataDir = storageService.getDataDir();
|
|
1338
|
+
this.runResultsDir = path13.join(dataDir, "run-results");
|
|
1339
|
+
this.testScriptsDir = path13.join(dataDir, "test-scripts");
|
|
1340
|
+
this.ensureDirectories();
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Ensure storage directories exist.
|
|
1344
|
+
*/
|
|
1345
|
+
ensureDirectories() {
|
|
1346
|
+
if (!fs4.existsSync(this.runResultsDir)) {
|
|
1347
|
+
fs4.mkdirSync(this.runResultsDir, { recursive: true });
|
|
1348
|
+
}
|
|
1349
|
+
if (!fs4.existsSync(this.testScriptsDir)) {
|
|
1350
|
+
fs4.mkdirSync(this.testScriptsDir, { recursive: true });
|
|
1645
1351
|
}
|
|
1646
1352
|
}
|
|
1353
|
+
// ========================================
|
|
1354
|
+
// Run Results
|
|
1355
|
+
// ========================================
|
|
1647
1356
|
/**
|
|
1648
|
-
* List all
|
|
1649
|
-
* @returns Array of session IDs.
|
|
1357
|
+
* List all run results.
|
|
1650
1358
|
*/
|
|
1651
|
-
|
|
1652
|
-
|
|
1359
|
+
listRunResults() {
|
|
1360
|
+
try {
|
|
1361
|
+
const files = fs4.readdirSync(this.runResultsDir).filter((f) => f.endsWith(".json"));
|
|
1362
|
+
const results = [];
|
|
1363
|
+
for (const file of files) {
|
|
1364
|
+
try {
|
|
1365
|
+
const content = fs4.readFileSync(path13.join(this.runResultsDir, file), "utf-8");
|
|
1366
|
+
results.push(JSON.parse(content));
|
|
1367
|
+
} catch {
|
|
1368
|
+
logger3.warn("Failed to read run result file", { file });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return results.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1372
|
+
} catch {
|
|
1653
1373
|
return [];
|
|
1654
1374
|
}
|
|
1655
|
-
return fs6.readdirSync(this.sessionsDir).filter((entry) => {
|
|
1656
|
-
const entryPath = path11.join(this.sessionsDir, entry);
|
|
1657
|
-
return fs6.statSync(entryPath).isDirectory();
|
|
1658
|
-
});
|
|
1659
1375
|
}
|
|
1660
1376
|
/**
|
|
1661
|
-
* Get
|
|
1662
|
-
* @returns Current session ID, or null.
|
|
1377
|
+
* Get a run result by ID.
|
|
1663
1378
|
*/
|
|
1664
|
-
|
|
1665
|
-
const
|
|
1666
|
-
if (!
|
|
1667
|
-
return
|
|
1379
|
+
getRunResult(runId) {
|
|
1380
|
+
const filePath = path13.join(this.runResultsDir, `${runId}.json`);
|
|
1381
|
+
if (!fs4.existsSync(filePath)) {
|
|
1382
|
+
return void 0;
|
|
1668
1383
|
}
|
|
1669
1384
|
try {
|
|
1670
|
-
const
|
|
1671
|
-
return
|
|
1385
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1386
|
+
return JSON.parse(content);
|
|
1672
1387
|
} catch {
|
|
1673
|
-
return
|
|
1388
|
+
return void 0;
|
|
1674
1389
|
}
|
|
1675
1390
|
}
|
|
1676
1391
|
/**
|
|
1677
|
-
*
|
|
1678
|
-
* @param sessionId - Session ID to set as current.
|
|
1392
|
+
* Save a run result.
|
|
1679
1393
|
*/
|
|
1680
|
-
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
const targetPath = path11.join(this.sessionsDir, sessionId);
|
|
1684
|
-
if (fs6.existsSync(currentPath)) {
|
|
1685
|
-
fs6.unlinkSync(currentPath);
|
|
1686
|
-
}
|
|
1687
|
-
fs6.symlinkSync(targetPath, currentPath);
|
|
1688
|
-
logger6.info("Set current session", { sessionId });
|
|
1394
|
+
saveRunResult(result) {
|
|
1395
|
+
const filePath = path13.join(this.runResultsDir, `${result.id}.json`);
|
|
1396
|
+
fs4.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
1689
1397
|
}
|
|
1690
1398
|
/**
|
|
1691
|
-
*
|
|
1692
|
-
* @param params - Screenshot save parameters.
|
|
1399
|
+
* Create a new run result.
|
|
1693
1400
|
*/
|
|
1694
|
-
|
|
1695
|
-
const
|
|
1696
|
-
const
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1401
|
+
createRunResult(params) {
|
|
1402
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1403
|
+
const id = randomUUID();
|
|
1404
|
+
const result = {
|
|
1405
|
+
id,
|
|
1406
|
+
runType: params.runType,
|
|
1407
|
+
status: "pending",
|
|
1408
|
+
cloudTestCaseId: params.cloudTestCaseId,
|
|
1409
|
+
projectId: params.projectId,
|
|
1410
|
+
useCaseId: params.useCaseId,
|
|
1411
|
+
localUrl: params.localUrl,
|
|
1412
|
+
productionUrl: params.productionUrl,
|
|
1413
|
+
localExecutionContext: params.localExecutionContext,
|
|
1414
|
+
createdAt: now,
|
|
1415
|
+
updatedAt: now
|
|
1416
|
+
};
|
|
1417
|
+
this.saveRunResult(result);
|
|
1418
|
+
return result;
|
|
1702
1419
|
}
|
|
1703
1420
|
/**
|
|
1704
|
-
*
|
|
1705
|
-
* @param params - Results append parameters.
|
|
1421
|
+
* Update a run result.
|
|
1706
1422
|
*/
|
|
1707
|
-
|
|
1708
|
-
const
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1423
|
+
updateRunResult(runId, updates) {
|
|
1424
|
+
const result = this.getRunResult(runId);
|
|
1425
|
+
if (!result) {
|
|
1426
|
+
return void 0;
|
|
1427
|
+
}
|
|
1428
|
+
const updated = {
|
|
1429
|
+
...result,
|
|
1430
|
+
...updates,
|
|
1431
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1432
|
+
};
|
|
1433
|
+
this.saveRunResult(updated);
|
|
1434
|
+
return updated;
|
|
1714
1435
|
}
|
|
1436
|
+
// ========================================
|
|
1437
|
+
// Test Scripts
|
|
1438
|
+
// ========================================
|
|
1715
1439
|
/**
|
|
1716
|
-
*
|
|
1717
|
-
* @param sessionId - Session ID.
|
|
1718
|
-
* @returns Results content, or null if not found.
|
|
1440
|
+
* List all test scripts.
|
|
1719
1441
|
*/
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1442
|
+
listTestScripts() {
|
|
1443
|
+
try {
|
|
1444
|
+
const files = fs4.readdirSync(this.testScriptsDir).filter((f) => f.endsWith(".json"));
|
|
1445
|
+
const scripts = [];
|
|
1446
|
+
for (const file of files) {
|
|
1447
|
+
try {
|
|
1448
|
+
const content = fs4.readFileSync(path13.join(this.testScriptsDir, file), "utf-8");
|
|
1449
|
+
scripts.push(JSON.parse(content));
|
|
1450
|
+
} catch {
|
|
1451
|
+
logger3.warn("Failed to read test script file", { file });
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return scripts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1455
|
+
} catch {
|
|
1456
|
+
return [];
|
|
1724
1457
|
}
|
|
1725
|
-
return fs6.readFileSync(resultsPath, "utf-8");
|
|
1726
1458
|
}
|
|
1727
1459
|
/**
|
|
1728
|
-
* Get
|
|
1729
|
-
* @returns Data directory path.
|
|
1460
|
+
* Get a test script by ID.
|
|
1730
1461
|
*/
|
|
1731
|
-
|
|
1732
|
-
|
|
1462
|
+
getTestScript(testScriptId) {
|
|
1463
|
+
const filePath = path13.join(this.testScriptsDir, `${testScriptId}.json`);
|
|
1464
|
+
if (!fs4.existsSync(filePath)) {
|
|
1465
|
+
return void 0;
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1469
|
+
return JSON.parse(content);
|
|
1470
|
+
} catch {
|
|
1471
|
+
return void 0;
|
|
1472
|
+
}
|
|
1733
1473
|
}
|
|
1734
1474
|
/**
|
|
1735
|
-
*
|
|
1736
|
-
* @returns Sessions directory path.
|
|
1475
|
+
* Save a test script.
|
|
1737
1476
|
*/
|
|
1738
|
-
|
|
1739
|
-
|
|
1477
|
+
saveTestScript(script) {
|
|
1478
|
+
const filePath = path13.join(this.testScriptsDir, `${script.id}.json`);
|
|
1479
|
+
fs4.writeFileSync(filePath, JSON.stringify(script, null, 2));
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Create a new test script.
|
|
1483
|
+
*/
|
|
1484
|
+
createTestScript(params) {
|
|
1485
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1486
|
+
const id = randomUUID();
|
|
1487
|
+
const script = {
|
|
1488
|
+
id,
|
|
1489
|
+
name: params.name,
|
|
1490
|
+
url: params.url,
|
|
1491
|
+
status: "pending",
|
|
1492
|
+
cloudTestCaseId: params.cloudTestCaseId,
|
|
1493
|
+
goal: params.goal,
|
|
1494
|
+
createdAt: now,
|
|
1495
|
+
updatedAt: now
|
|
1496
|
+
};
|
|
1497
|
+
this.saveTestScript(script);
|
|
1498
|
+
return script;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Update a test script.
|
|
1502
|
+
*/
|
|
1503
|
+
updateTestScript(testScriptId, updates) {
|
|
1504
|
+
const script = this.getTestScript(testScriptId);
|
|
1505
|
+
if (!script) {
|
|
1506
|
+
return void 0;
|
|
1507
|
+
}
|
|
1508
|
+
const updated = {
|
|
1509
|
+
...script,
|
|
1510
|
+
...updates,
|
|
1511
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1512
|
+
};
|
|
1513
|
+
this.saveTestScript(updated);
|
|
1514
|
+
return updated;
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
var instance = null;
|
|
1518
|
+
function getRunResultStorageService() {
|
|
1519
|
+
if (!instance) {
|
|
1520
|
+
instance = new RunResultStorageService();
|
|
1521
|
+
}
|
|
1522
|
+
return instance;
|
|
1523
|
+
}
|
|
1524
|
+
function resetRunResultStorageService() {
|
|
1525
|
+
instance = null;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// packages/mcps/src/mcp/local/services/run-view/read-run-view.ts
|
|
1529
|
+
function getRunSessionsDir() {
|
|
1530
|
+
return path13.join(getDataDir(), RUN_SESSIONS_DIR_NAME);
|
|
1531
|
+
}
|
|
1532
|
+
var RunViewError = class extends Error {
|
|
1533
|
+
};
|
|
1534
|
+
var listSessionDirNames = (sessionsDir) => {
|
|
1535
|
+
if (!fs4.existsSync(sessionsDir)) {
|
|
1536
|
+
throw new RunViewError(`No runs directory at ${sessionsDir}`);
|
|
1537
|
+
}
|
|
1538
|
+
return fs4.readdirSync(sessionsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1539
|
+
};
|
|
1540
|
+
function resolveRunSessionPath(runId) {
|
|
1541
|
+
const sessionsDir = getRunSessionsDir();
|
|
1542
|
+
const names = listSessionDirNames(sessionsDir);
|
|
1543
|
+
if (names.length === 0) {
|
|
1544
|
+
throw new RunViewError(`No runs found under ${sessionsDir}`);
|
|
1545
|
+
}
|
|
1546
|
+
if (!runId) {
|
|
1547
|
+
const newest = names.map((name) => ({ name, modifiedAtMs: fs4.statSync(path13.join(sessionsDir, name)).mtimeMs })).sort((left, right) => right.modifiedAtMs - left.modifiedAtMs)[0];
|
|
1548
|
+
return path13.join(sessionsDir, newest.name);
|
|
1549
|
+
}
|
|
1550
|
+
const exact = names.find((name) => name === runId);
|
|
1551
|
+
if (exact) {
|
|
1552
|
+
return path13.join(sessionsDir, exact);
|
|
1553
|
+
}
|
|
1554
|
+
const prefixed = names.filter((name) => name.startsWith(runId));
|
|
1555
|
+
if (prefixed.length === 1) {
|
|
1556
|
+
return path13.join(sessionsDir, prefixed[0]);
|
|
1557
|
+
}
|
|
1558
|
+
if (prefixed.length > 1) {
|
|
1559
|
+
throw new RunViewError(
|
|
1560
|
+
`Run id "${runId}" matches ${prefixed.length} runs (${prefixed.slice(0, 3).join(", ")}\u2026). Use more characters.`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
throw new RunViewError(`No run matching "${runId}" under ${sessionsDir}`);
|
|
1564
|
+
}
|
|
1565
|
+
var listScreenshotFileNames = (sessionPath) => {
|
|
1566
|
+
const screenshotDir = path13.join(sessionPath, ...SCREENSHOT_DIR_RELATIVE_PATH.split("/"));
|
|
1567
|
+
if (!fs4.existsSync(screenshotDir)) {
|
|
1568
|
+
return [];
|
|
1569
|
+
}
|
|
1570
|
+
return fs4.readdirSync(screenshotDir).filter((name) => name.endsWith(SCREENSHOT_FILE_EXTENSION));
|
|
1571
|
+
};
|
|
1572
|
+
var resolveScreenshotPath = (sessionPath, screenshotFileNames, recordedPath) => {
|
|
1573
|
+
if (!recordedPath) {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
const recordedName = recordedPath.split(/[\\/]/).pop();
|
|
1577
|
+
if (!recordedName) {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const match = screenshotFileNames.find((name) => name === recordedName) ?? screenshotFileNames.find((name) => name.endsWith(recordedName));
|
|
1581
|
+
return match ? path13.join(sessionPath, ...SCREENSHOT_DIR_RELATIVE_PATH.split("/"), match) : null;
|
|
1582
|
+
};
|
|
1583
|
+
function readRunView(runId) {
|
|
1584
|
+
const sessionPath = resolveRunSessionPath(runId);
|
|
1585
|
+
const actionScriptPath = path13.join(sessionPath, ACTION_SCRIPT_FILE_NAME);
|
|
1586
|
+
if (!fs4.existsSync(actionScriptPath)) {
|
|
1587
|
+
throw new RunViewError(
|
|
1588
|
+
`Run ${path13.basename(sessionPath)} has no ${ACTION_SCRIPT_FILE_NAME} \u2014 it died before writing one. Looked in ${sessionPath}`
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
let actionScript;
|
|
1592
|
+
try {
|
|
1593
|
+
actionScript = JSON.parse(fs4.readFileSync(actionScriptPath, "utf8"));
|
|
1594
|
+
} catch (parseError) {
|
|
1595
|
+
throw new RunViewError(
|
|
1596
|
+
`Could not read ${actionScriptPath}: ${parseError instanceof Error ? parseError.message : String(parseError)}`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
const resolvedRunId = path13.basename(sessionPath);
|
|
1600
|
+
const recordedOutcome = getRunResultStorageService().getRunResult(resolvedRunId)?.status ?? null;
|
|
1601
|
+
const screenshotFileNames = listScreenshotFileNames(sessionPath);
|
|
1602
|
+
const steps = (actionScript.steps ?? []).map((step, index) => ({
|
|
1603
|
+
index,
|
|
1604
|
+
action: step.operation?.action ?? "unknown",
|
|
1605
|
+
explanation: step.briefExplanation ?? "",
|
|
1606
|
+
screenshotPath: resolveScreenshotPath(sessionPath, screenshotFileNames, step.operation?.screenshot),
|
|
1607
|
+
url: step.operation?.url ?? null
|
|
1608
|
+
}));
|
|
1609
|
+
return {
|
|
1610
|
+
runId: resolvedRunId,
|
|
1611
|
+
sessionPath,
|
|
1612
|
+
title: actionScript.actionScriptName ?? null,
|
|
1613
|
+
status: recordedOutcome,
|
|
1614
|
+
verdict: actionScript.summaryStep?.briefExplanation ?? null,
|
|
1615
|
+
steps,
|
|
1616
|
+
screenshotCount: screenshotFileNames.length
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// packages/mcps/src/mcp/local/services/run-view/render-run-view-text.ts
|
|
1621
|
+
function renderRunViewText(runView, options) {
|
|
1622
|
+
const header = [
|
|
1623
|
+
`Run ${runView.runId}${runView.title ? ` \u2014 ${runView.title}` : ""}`,
|
|
1624
|
+
`${runView.status ?? "status unrecorded"} \xB7 ${runView.steps.length} steps \xB7 ${runView.screenshotCount} frames`,
|
|
1625
|
+
runView.sessionPath,
|
|
1626
|
+
""
|
|
1627
|
+
];
|
|
1628
|
+
const stepLines = runView.steps.length === 0 ? ["(no steps recorded \u2014 the run ended before writing any)"] : runView.steps.flatMap((step) => {
|
|
1629
|
+
const lines = [
|
|
1630
|
+
`${String(step.index).padStart(2, " ")} ${step.action}${step.url ? ` \u2192 ${step.url}` : ""}`
|
|
1631
|
+
];
|
|
1632
|
+
if (step.explanation) {
|
|
1633
|
+
lines.push(` ${step.explanation}`);
|
|
1634
|
+
}
|
|
1635
|
+
lines.push(` ${step.screenshotPath ?? "(no frame recorded)"}`);
|
|
1636
|
+
return lines;
|
|
1637
|
+
});
|
|
1638
|
+
const verdict = runView.verdict ? ["", `Verdict: ${runView.verdict}`] : [];
|
|
1639
|
+
const offer = options.includeHtmlOffer ? ["", HTML_VIEW_OFFER_LINE] : [];
|
|
1640
|
+
return [...header, ...stepLines, ...verdict, ...offer].join("\n");
|
|
1641
|
+
}
|
|
1642
|
+
var escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1643
|
+
var inlineFrame = (screenshotPath) => {
|
|
1644
|
+
if (!screenshotPath || !fs4.existsSync(screenshotPath)) {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
try {
|
|
1648
|
+
return `data:image/jpeg;base64,${fs4.readFileSync(screenshotPath).toString("base64")}`;
|
|
1649
|
+
} catch {
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
var renderStep = (step) => {
|
|
1654
|
+
const frame = inlineFrame(step.screenshotPath);
|
|
1655
|
+
const frameMarkup = frame ? `<a href="${frame}" target="_blank" rel="noreferrer"><img src="${frame}" alt="Step ${step.index} frame" loading="lazy"></a>` : `<p class="no-frame">no frame recorded</p>`;
|
|
1656
|
+
return `<li class="step">
|
|
1657
|
+
<div class="meta">
|
|
1658
|
+
<span class="index">${step.index}</span>
|
|
1659
|
+
<span class="action">${escapeHtml(step.action)}</span>
|
|
1660
|
+
${step.url ? `<span class="url">${escapeHtml(step.url)}</span>` : ""}
|
|
1661
|
+
</div>
|
|
1662
|
+
<p class="explanation">${escapeHtml(step.explanation || "(no explanation recorded)")}</p>
|
|
1663
|
+
${frameMarkup}
|
|
1664
|
+
</li>`;
|
|
1665
|
+
};
|
|
1666
|
+
function renderRunViewHtml(runView) {
|
|
1667
|
+
const title = `Run ${runView.runId}${runView.title ? ` \u2014 ${runView.title}` : ""}`;
|
|
1668
|
+
return `<!doctype html>
|
|
1669
|
+
<html lang="en">
|
|
1670
|
+
<head>
|
|
1671
|
+
<meta charset="utf-8">
|
|
1672
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1673
|
+
<title>${escapeHtml(title)}</title>
|
|
1674
|
+
<style>
|
|
1675
|
+
:root { color-scheme: light dark; --bg:#ffffff; --fg:#1a1a1a; --muted:#666; --line:#e2e2e2; --chip:#f1f1f4; }
|
|
1676
|
+
@media (prefers-color-scheme: dark) {
|
|
1677
|
+
:root { --bg:#16161a; --fg:#ececf0; --muted:#9a9aa5; --line:#2c2c33; --chip:#24242b; }
|
|
1678
|
+
}
|
|
1679
|
+
body { margin:0; padding:24px 16px 64px; background:var(--bg); color:var(--fg);
|
|
1680
|
+
font:14px/1.5 system-ui,-apple-system,Segoe UI,sans-serif; }
|
|
1681
|
+
.wrap { max-width: 1040px; margin: 0 auto; }
|
|
1682
|
+
h1 { font-size:20px; margin:0 0 4px; }
|
|
1683
|
+
.sub { color:var(--muted); margin:0 0 4px; }
|
|
1684
|
+
.path { color:var(--muted); font-family:ui-monospace,Menlo,Consolas,monospace; font-size:12px;
|
|
1685
|
+
word-break:break-all; margin:0 0 24px; }
|
|
1686
|
+
.verdict { border:1px solid var(--line); border-left:3px solid #c0392b; border-radius:6px;
|
|
1687
|
+
padding:12px 14px; margin:0 0 24px; background:var(--chip); }
|
|
1688
|
+
.verdict h2 { font-size:13px; text-transform:uppercase; letter-spacing:.04em; margin:0 0 6px; color:var(--muted); }
|
|
1689
|
+
ol { list-style:none; margin:0; padding:0; }
|
|
1690
|
+
.step { border-top:1px solid var(--line); padding:20px 0; }
|
|
1691
|
+
.meta { display:flex; gap:10px; align-items:baseline; flex-wrap:wrap; }
|
|
1692
|
+
.index { font-family:ui-monospace,Menlo,Consolas,monospace; color:var(--muted); }
|
|
1693
|
+
.action { font-weight:600; }
|
|
1694
|
+
.url { color:var(--muted); font-family:ui-monospace,Menlo,Consolas,monospace; font-size:12px; word-break:break-all; }
|
|
1695
|
+
.explanation { margin:6px 0 12px; }
|
|
1696
|
+
.no-frame { color:var(--muted); font-style:italic; margin:0; }
|
|
1697
|
+
img { max-width:100%; height:auto; border:1px solid var(--line); border-radius:6px; display:block; }
|
|
1698
|
+
</style>
|
|
1699
|
+
</head>
|
|
1700
|
+
<body>
|
|
1701
|
+
<div class="wrap">
|
|
1702
|
+
<h1>${escapeHtml(title)}</h1>
|
|
1703
|
+
<p class="sub">${escapeHtml(runView.status ?? "status unrecorded")} \xB7 ${runView.steps.length} steps \xB7 ${runView.screenshotCount} frames</p>
|
|
1704
|
+
<p class="path">${escapeHtml(runView.sessionPath)}</p>
|
|
1705
|
+
${runView.verdict ? `<div class="verdict"><h2>Verdict the run recorded</h2><p>${escapeHtml(runView.verdict)}</p></div>` : ""}
|
|
1706
|
+
<ol>
|
|
1707
|
+
${runView.steps.map(renderStep).join("\n")}
|
|
1708
|
+
</ol>
|
|
1709
|
+
${runView.steps.length === 0 ? '<p class="no-frame">No steps recorded \u2014 the run ended before writing any.</p>' : ""}
|
|
1710
|
+
</div>
|
|
1711
|
+
</body>
|
|
1712
|
+
</html>`;
|
|
1713
|
+
}
|
|
1714
|
+
function writeRunViewHtml(runView, outputDir) {
|
|
1715
|
+
const targetDir = outputDir ?? process.cwd();
|
|
1716
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
1717
|
+
const targetPath = path13.join(targetDir, `run-${runView.runId}.html`);
|
|
1718
|
+
fs4.writeFileSync(targetPath, renderRunViewHtml(runView), "utf8");
|
|
1719
|
+
return targetPath;
|
|
1720
|
+
}
|
|
1721
|
+
var API_KEY_FILE2 = "api-key.json";
|
|
1722
|
+
function getApiKeyFilePath() {
|
|
1723
|
+
return path13.join(getDataDir(), API_KEY_FILE2);
|
|
1724
|
+
}
|
|
1725
|
+
function ensureDataDir() {
|
|
1726
|
+
const dataDir = getDataDir();
|
|
1727
|
+
if (!fs4.existsSync(dataDir)) {
|
|
1728
|
+
fs4.mkdirSync(dataDir, { recursive: true });
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function loadApiKeyData() {
|
|
1732
|
+
const logger6 = getLogger();
|
|
1733
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
1734
|
+
try {
|
|
1735
|
+
if (!fs4.existsSync(apiKeyPath)) {
|
|
1736
|
+
logger6.debug("No API key file found", { path: apiKeyPath });
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
const content = fs4.readFileSync(apiKeyPath, "utf-8");
|
|
1740
|
+
const data = JSON.parse(content);
|
|
1741
|
+
return data;
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
logger6.warn("Failed to load API key data", {
|
|
1744
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1745
|
+
});
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
function saveApiKeyData(data) {
|
|
1750
|
+
const logger6 = getLogger();
|
|
1751
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
1752
|
+
try {
|
|
1753
|
+
ensureDataDir();
|
|
1754
|
+
const content = JSON.stringify(data, null, 2);
|
|
1755
|
+
fs4.writeFileSync(apiKeyPath, content, { mode: 384 });
|
|
1756
|
+
logger6.info("API key saved", { path: apiKeyPath });
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
logger6.error("Failed to save API key", {
|
|
1759
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1760
|
+
});
|
|
1761
|
+
throw error;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function deleteApiKeyData() {
|
|
1765
|
+
const logger6 = getLogger();
|
|
1766
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
1767
|
+
try {
|
|
1768
|
+
if (fs4.existsSync(apiKeyPath)) {
|
|
1769
|
+
fs4.unlinkSync(apiKeyPath);
|
|
1770
|
+
logger6.info("API key deleted", { path: apiKeyPath });
|
|
1771
|
+
}
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
logger6.warn("Failed to delete API key", {
|
|
1774
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
function getValidApiKeyData() {
|
|
1779
|
+
const data = loadApiKeyData();
|
|
1780
|
+
if (!data) {
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
if (data.apiKey) {
|
|
1784
|
+
return data;
|
|
1785
|
+
}
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
function hasApiKey() {
|
|
1789
|
+
const data = loadApiKeyData();
|
|
1790
|
+
return !!data?.apiKey;
|
|
1791
|
+
}
|
|
1792
|
+
function getApiKey() {
|
|
1793
|
+
const data = loadApiKeyData();
|
|
1794
|
+
return data?.apiKey ?? null;
|
|
1795
|
+
}
|
|
1796
|
+
function saveApiKey(params) {
|
|
1797
|
+
const logger6 = getLogger();
|
|
1798
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
1799
|
+
try {
|
|
1800
|
+
ensureDataDir();
|
|
1801
|
+
const data = {
|
|
1802
|
+
accessToken: "",
|
|
1803
|
+
expiresAt: "",
|
|
1804
|
+
apiKey: params.apiKey,
|
|
1805
|
+
apiKeyId: params.apiKeyId
|
|
1806
|
+
};
|
|
1807
|
+
const content = JSON.stringify(data, null, 2);
|
|
1808
|
+
fs4.writeFileSync(apiKeyPath, content, { mode: 384 });
|
|
1809
|
+
logger6.info("API key saved", { path: apiKeyPath });
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
logger6.error("Failed to save API key", {
|
|
1812
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1813
|
+
});
|
|
1814
|
+
throw error;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
var loadCredentials = loadApiKeyData;
|
|
1818
|
+
var saveCredentials = saveApiKeyData;
|
|
1819
|
+
var deleteCredentials = deleteApiKeyData;
|
|
1820
|
+
var getValidCredentials = getValidApiKeyData;
|
|
1821
|
+
var getCredentialsFilePath = getApiKeyFilePath;
|
|
1822
|
+
|
|
1823
|
+
// packages/mcps/src/mcp/local/services/stored-auth-target.ts
|
|
1824
|
+
function decodeJwtPayload(token) {
|
|
1825
|
+
try {
|
|
1826
|
+
const parts = token.split(".");
|
|
1827
|
+
if (parts.length !== 3) {
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
const payload = parts[1];
|
|
1831
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf-8");
|
|
1832
|
+
return JSON.parse(decoded);
|
|
1833
|
+
} catch {
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
function extractJwtIssuer(token) {
|
|
1838
|
+
const payload = decodeJwtPayload(token);
|
|
1839
|
+
if (!payload || typeof payload.iss !== "string") {
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
return payload.iss;
|
|
1843
|
+
}
|
|
1844
|
+
function normalizeIssuerHost(issuer) {
|
|
1845
|
+
return issuer.replace(/^https:\/\//, "").replace(/\/$/, "");
|
|
1846
|
+
}
|
|
1847
|
+
function isStoredAuthForRuntimeTarget(params) {
|
|
1848
|
+
if (params.tokenIssuer && params.expectedAuth0Domain) {
|
|
1849
|
+
const issuerHost = normalizeIssuerHost(params.tokenIssuer);
|
|
1850
|
+
const expectedHost = normalizeIssuerHost(params.expectedAuth0Domain);
|
|
1851
|
+
if (issuerHost !== expectedHost) {
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (!params.storedRuntimeTarget) {
|
|
1856
|
+
return params.activeRuntimeTarget === "production" /* Production */;
|
|
1857
|
+
}
|
|
1858
|
+
return params.storedRuntimeTarget === params.activeRuntimeTarget;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// packages/mcps/src/mcp/local/services/auth-service.ts
|
|
1862
|
+
var DEFAULT_LOGIN_WAIT_TIMEOUT_MS = 12e4;
|
|
1863
|
+
var AuthService = class {
|
|
1864
|
+
/** Path to the OAuth session file. */
|
|
1865
|
+
oauthSessionFilePath;
|
|
1866
|
+
/** Path to the pending device code file. */
|
|
1867
|
+
pendingDeviceCodePath;
|
|
1868
|
+
/**
|
|
1869
|
+
* Create a new AuthService.
|
|
1870
|
+
*/
|
|
1871
|
+
constructor() {
|
|
1872
|
+
const config = getConfig();
|
|
1873
|
+
this.oauthSessionFilePath = config.localQa.oauthSessionFilePath;
|
|
1874
|
+
this.pendingDeviceCodePath = path13.join(
|
|
1875
|
+
path13.dirname(config.localQa.oauthSessionFilePath),
|
|
1876
|
+
"pending-device-code.json"
|
|
1877
|
+
);
|
|
1740
1878
|
}
|
|
1741
1879
|
/**
|
|
1742
|
-
*
|
|
1743
|
-
* @param params - Session creation parameters.
|
|
1744
|
-
* @returns Path to the session directory.
|
|
1880
|
+
* Get current authentication status.
|
|
1745
1881
|
*/
|
|
1746
|
-
|
|
1747
|
-
const { sessionId, targetUrl, testInstructions } = params;
|
|
1882
|
+
getAuthStatus() {
|
|
1748
1883
|
const logger6 = getLogger();
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1884
|
+
const storedAuth = this.loadStoredAuth();
|
|
1885
|
+
if (!storedAuth) {
|
|
1886
|
+
logger6.debug("No stored auth found");
|
|
1887
|
+
return { authenticated: false };
|
|
1888
|
+
}
|
|
1889
|
+
const now = /* @__PURE__ */ new Date();
|
|
1890
|
+
const expiresAt = new Date(storedAuth.expiresAt);
|
|
1891
|
+
const isExpired = now >= expiresAt;
|
|
1892
|
+
logger6.debug("Auth status checked", {
|
|
1893
|
+
email: storedAuth.email,
|
|
1894
|
+
isExpired,
|
|
1895
|
+
expiresAt: storedAuth.expiresAt
|
|
1896
|
+
});
|
|
1897
|
+
return {
|
|
1898
|
+
authenticated: !isExpired,
|
|
1899
|
+
email: storedAuth.email,
|
|
1900
|
+
userId: storedAuth.userId,
|
|
1901
|
+
expiresAt: storedAuth.expiresAt,
|
|
1902
|
+
isExpired
|
|
1757
1903
|
};
|
|
1758
|
-
this.saveSessionMetadata(metadata);
|
|
1759
|
-
this.setCurrentSession(sessionId);
|
|
1760
|
-
logger6.info("Created session", { sessionId, targetUrl });
|
|
1761
|
-
return sessionDir;
|
|
1762
1904
|
}
|
|
1763
1905
|
/**
|
|
1764
|
-
*
|
|
1765
|
-
* @param
|
|
1906
|
+
* Start the device code flow.
|
|
1907
|
+
* @param options.forceNewSession - Clear existing Auth0 browser session before login to allow account switching.
|
|
1766
1908
|
*/
|
|
1767
|
-
|
|
1768
|
-
const { sessionId, status } = params;
|
|
1909
|
+
async startDeviceCodeFlow(options) {
|
|
1769
1910
|
const logger6 = getLogger();
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1911
|
+
const config = getConfig();
|
|
1912
|
+
const { domain, clientId, audience, scopes } = config.localQa.auth0;
|
|
1913
|
+
logger6.info("Starting device code flow");
|
|
1914
|
+
const url = `https://${domain}/oauth/device/code`;
|
|
1915
|
+
const body = new URLSearchParams({
|
|
1916
|
+
client_id: clientId,
|
|
1917
|
+
scope: scopes.join(" "),
|
|
1918
|
+
audience
|
|
1919
|
+
});
|
|
1920
|
+
const response = await fetch(url, {
|
|
1921
|
+
method: "POST",
|
|
1922
|
+
headers: {
|
|
1923
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1924
|
+
},
|
|
1925
|
+
body: body.toString()
|
|
1926
|
+
});
|
|
1927
|
+
if (!response.ok) {
|
|
1928
|
+
const errorText = await response.text();
|
|
1929
|
+
logger6.error("Device code request failed", {
|
|
1930
|
+
status: response.status,
|
|
1931
|
+
error: errorText
|
|
1932
|
+
});
|
|
1933
|
+
throw new Error(`Failed to start device code flow: ${response.status} ${errorText}`);
|
|
1774
1934
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1935
|
+
const deviceCodeResponse = await response.json();
|
|
1936
|
+
logger6.info("Device code flow started", {
|
|
1937
|
+
userCode: deviceCodeResponse.user_code,
|
|
1938
|
+
verificationUri: deviceCodeResponse.verification_uri,
|
|
1939
|
+
expiresIn: deviceCodeResponse.expires_in
|
|
1940
|
+
});
|
|
1941
|
+
this.storePendingDeviceCode({
|
|
1942
|
+
deviceCode: deviceCodeResponse.device_code,
|
|
1943
|
+
userCode: deviceCodeResponse.user_code,
|
|
1944
|
+
expiresAt: new Date(Date.now() + deviceCodeResponse.expires_in * 1e3).toISOString()
|
|
1945
|
+
});
|
|
1946
|
+
let browserUrl = deviceCodeResponse.verification_uri_complete;
|
|
1947
|
+
if (options?.forceNewSession) {
|
|
1948
|
+
this.logout();
|
|
1949
|
+
const logoutUrl = new URL(`https://${domain}/v2/logout`);
|
|
1950
|
+
logoutUrl.searchParams.set("client_id", clientId);
|
|
1951
|
+
logoutUrl.searchParams.set("returnTo", deviceCodeResponse.verification_uri_complete);
|
|
1952
|
+
browserUrl = logoutUrl.toString();
|
|
1953
|
+
logger6.info("Force new session: cleared local token, opening logout-redirect URL", {
|
|
1954
|
+
logoutUrl: browserUrl
|
|
1955
|
+
});
|
|
1778
1956
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1957
|
+
const browserOpenResult = await openBrowserUrl({
|
|
1958
|
+
url: browserUrl
|
|
1959
|
+
});
|
|
1960
|
+
if (browserOpenResult.opened) {
|
|
1961
|
+
logger6.info("Browser opened for device code login");
|
|
1962
|
+
} else {
|
|
1963
|
+
logger6.warn("Failed to open browser for device code login", {
|
|
1964
|
+
error: browserOpenResult.error,
|
|
1965
|
+
url: browserUrl
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
deviceCode: deviceCodeResponse.device_code,
|
|
1970
|
+
userCode: deviceCodeResponse.user_code,
|
|
1971
|
+
verificationUri: deviceCodeResponse.verification_uri,
|
|
1972
|
+
verificationUriComplete: deviceCodeResponse.verification_uri_complete,
|
|
1973
|
+
expiresIn: deviceCodeResponse.expires_in,
|
|
1974
|
+
interval: deviceCodeResponse.interval,
|
|
1975
|
+
browserOpened: browserOpenResult.opened,
|
|
1976
|
+
browserOpenError: browserOpenResult.error
|
|
1977
|
+
};
|
|
1781
1978
|
}
|
|
1782
1979
|
/**
|
|
1783
|
-
*
|
|
1784
|
-
* @param params - Initialization parameters.
|
|
1980
|
+
* Store a pending device code for later retrieval.
|
|
1785
1981
|
*/
|
|
1786
|
-
|
|
1787
|
-
const { sessionId, targetUrl, testInstructions } = params;
|
|
1982
|
+
storePendingDeviceCode(params) {
|
|
1788
1983
|
const logger6 = getLogger();
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
"---",
|
|
1799
|
-
"",
|
|
1800
|
-
"## Test Steps",
|
|
1801
|
-
""
|
|
1802
|
-
].join("\n");
|
|
1803
|
-
fs6.writeFileSync(resultsPath, header, "utf-8");
|
|
1804
|
-
logger6.debug("Initialized results.md", { sessionId });
|
|
1984
|
+
const dir = path13.dirname(this.pendingDeviceCodePath);
|
|
1985
|
+
if (!fs4.existsSync(dir)) {
|
|
1986
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1987
|
+
}
|
|
1988
|
+
fs4.writeFileSync(this.pendingDeviceCodePath, JSON.stringify(params, null, 2), {
|
|
1989
|
+
encoding: "utf-8",
|
|
1990
|
+
mode: 384
|
|
1991
|
+
});
|
|
1992
|
+
logger6.debug("Pending device code stored", { userCode: params.userCode });
|
|
1805
1993
|
}
|
|
1806
1994
|
/**
|
|
1807
|
-
*
|
|
1808
|
-
* @param params - Step parameters.
|
|
1995
|
+
* Get the pending device code if one exists and is not expired.
|
|
1809
1996
|
*/
|
|
1810
|
-
|
|
1811
|
-
const { sessionId, step } = params;
|
|
1997
|
+
getPendingDeviceCode() {
|
|
1812
1998
|
const logger6 = getLogger();
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
""
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1999
|
+
if (!fs4.existsSync(this.pendingDeviceCodePath)) {
|
|
2000
|
+
logger6.debug("No pending device code found");
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
try {
|
|
2004
|
+
const content = fs4.readFileSync(this.pendingDeviceCodePath, "utf-8");
|
|
2005
|
+
const pendingDeviceCodeData = JSON.parse(content);
|
|
2006
|
+
const now = /* @__PURE__ */ new Date();
|
|
2007
|
+
const expiresAt = new Date(pendingDeviceCodeData.expiresAt);
|
|
2008
|
+
if (now >= expiresAt) {
|
|
2009
|
+
logger6.debug("Pending device code expired");
|
|
2010
|
+
this.clearPendingDeviceCode();
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
return pendingDeviceCodeData.deviceCode;
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
logger6.warn("Failed to read pending device code", {
|
|
2016
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2017
|
+
});
|
|
2018
|
+
return null;
|
|
1830
2019
|
}
|
|
1831
2020
|
}
|
|
1832
2021
|
/**
|
|
1833
|
-
*
|
|
1834
|
-
* @param params - Finalization parameters.
|
|
2022
|
+
* Clear the pending device code file.
|
|
1835
2023
|
*/
|
|
1836
|
-
|
|
1837
|
-
const { sessionId, status, summary } = params;
|
|
2024
|
+
clearPendingDeviceCode() {
|
|
1838
2025
|
const logger6 = getLogger();
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
const durationMs = endTime.getTime() - startTime.getTime();
|
|
1849
|
-
const durationSeconds = (durationMs / 1e3).toFixed(2);
|
|
1850
|
-
const statusDisplay = status === "completed" /* Completed */ ? "\u2713 Passed" : status === "failed" /* Failed */ ? "\u2717 Failed" : "\u2014 Running";
|
|
1851
|
-
const footer = [
|
|
1852
|
-
"",
|
|
1853
|
-
"---",
|
|
1854
|
-
"",
|
|
1855
|
-
"## Summary",
|
|
1856
|
-
"",
|
|
1857
|
-
`**Status:** ${statusDisplay}`,
|
|
1858
|
-
`**Duration:** ${durationSeconds}s`,
|
|
1859
|
-
`**Steps:** ${metadata?.stepsCount ?? 0}`,
|
|
1860
|
-
`**Completed:** ${endTime.toISOString()}`,
|
|
1861
|
-
"",
|
|
1862
|
-
summary ? summary : ""
|
|
1863
|
-
].join("\n");
|
|
1864
|
-
fs6.appendFileSync(resultsPath, footer, "utf-8");
|
|
1865
|
-
logger6.debug("Finalized results.md", { sessionId, status });
|
|
1866
|
-
if (metadata) {
|
|
1867
|
-
metadata.durationMs = durationMs;
|
|
1868
|
-
metadata.endTime = endTime.toISOString();
|
|
1869
|
-
metadata.status = status;
|
|
1870
|
-
this.saveSessionMetadata(metadata);
|
|
2026
|
+
if (fs4.existsSync(this.pendingDeviceCodePath)) {
|
|
2027
|
+
try {
|
|
2028
|
+
fs4.unlinkSync(this.pendingDeviceCodePath);
|
|
2029
|
+
logger6.debug("Pending device code cleared");
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
logger6.warn("Failed to clear pending device code", {
|
|
2032
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
1871
2035
|
}
|
|
1872
2036
|
}
|
|
1873
2037
|
/**
|
|
1874
|
-
*
|
|
1875
|
-
* @returns Array of session summaries.
|
|
2038
|
+
* Poll for device code authorization completion.
|
|
1876
2039
|
*/
|
|
1877
|
-
|
|
1878
|
-
const
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2040
|
+
async pollDeviceCode(deviceCode) {
|
|
2041
|
+
const logger6 = getLogger();
|
|
2042
|
+
const config = getConfig();
|
|
2043
|
+
const { domain, clientId } = config.localQa.auth0;
|
|
2044
|
+
logger6.debug("Polling for device code authorization");
|
|
2045
|
+
const url = `https://${domain}/oauth/token`;
|
|
2046
|
+
const body = new URLSearchParams({
|
|
2047
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
2048
|
+
client_id: clientId,
|
|
2049
|
+
device_code: deviceCode
|
|
2050
|
+
});
|
|
2051
|
+
try {
|
|
2052
|
+
const response = await fetch(url, {
|
|
2053
|
+
method: "POST",
|
|
2054
|
+
headers: {
|
|
2055
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2056
|
+
},
|
|
2057
|
+
body: body.toString()
|
|
2058
|
+
});
|
|
2059
|
+
if (response.ok) {
|
|
2060
|
+
const tokenData = await response.json();
|
|
2061
|
+
const tokenResponse = {
|
|
2062
|
+
accessToken: tokenData.access_token,
|
|
2063
|
+
refreshToken: tokenData.refresh_token,
|
|
2064
|
+
tokenType: tokenData.token_type,
|
|
2065
|
+
expiresIn: tokenData.expires_in
|
|
2066
|
+
};
|
|
2067
|
+
const userInfo = await this.getUserInfo(tokenResponse.accessToken);
|
|
2068
|
+
await this.storeAuth({
|
|
2069
|
+
tokenResponse,
|
|
2070
|
+
email: userInfo.email,
|
|
2071
|
+
userId: userInfo.sub
|
|
1894
2072
|
});
|
|
2073
|
+
this.clearPendingDeviceCode();
|
|
2074
|
+
logger6.info("Device code authorization complete", { email: userInfo.email });
|
|
2075
|
+
return {
|
|
2076
|
+
status: "complete" /* Complete */,
|
|
2077
|
+
message: "Authentication successful!",
|
|
2078
|
+
email: userInfo.email
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
const errorData = await response.json();
|
|
2082
|
+
if (errorData.error === "authorization_pending") {
|
|
2083
|
+
logger6.debug("Authorization pending");
|
|
2084
|
+
return {
|
|
2085
|
+
status: "pending" /* Pending */,
|
|
2086
|
+
message: "Waiting for user to complete authorization..."
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
if (errorData.error === "slow_down") {
|
|
2090
|
+
logger6.debug("Polling too fast");
|
|
2091
|
+
return {
|
|
2092
|
+
status: "pending" /* Pending */,
|
|
2093
|
+
message: "Polling too fast, slowing down..."
|
|
2094
|
+
};
|
|
1895
2095
|
}
|
|
2096
|
+
if (errorData.error === "expired_token") {
|
|
2097
|
+
logger6.warn("Device code expired");
|
|
2098
|
+
return {
|
|
2099
|
+
status: "expired" /* Expired */,
|
|
2100
|
+
message: "The authorization code has expired. Please start again."
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
if (errorData.error === "access_denied") {
|
|
2104
|
+
logger6.warn("Access denied");
|
|
2105
|
+
return {
|
|
2106
|
+
status: "error" /* Error */,
|
|
2107
|
+
message: "Access was denied by the user.",
|
|
2108
|
+
error: errorData.error_description ?? errorData.error
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
logger6.error("Unexpected error during polling", { error: errorData });
|
|
2112
|
+
return {
|
|
2113
|
+
status: "error" /* Error */,
|
|
2114
|
+
message: errorData.error_description ?? errorData.error,
|
|
2115
|
+
error: errorData.error
|
|
2116
|
+
};
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2119
|
+
logger6.error("Poll request failed", { error: errorMessage });
|
|
2120
|
+
return {
|
|
2121
|
+
status: "error" /* Error */,
|
|
2122
|
+
message: `Poll request failed: ${errorMessage}`,
|
|
2123
|
+
error: errorMessage
|
|
2124
|
+
};
|
|
1896
2125
|
}
|
|
1897
|
-
summaries.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
|
1898
|
-
return summaries;
|
|
1899
2126
|
}
|
|
1900
2127
|
/**
|
|
1901
|
-
*
|
|
1902
|
-
* @param params - Cleanup parameters.
|
|
1903
|
-
* @returns Number of sessions deleted.
|
|
2128
|
+
* Poll for device code authorization until completion or timeout.
|
|
1904
2129
|
*/
|
|
1905
|
-
|
|
1906
|
-
const maxAgeDays = params?.maxAgeDays ?? DEFAULT_SESSION_MAX_AGE_DAYS;
|
|
2130
|
+
async waitForDeviceCodeAuthorization(params) {
|
|
1907
2131
|
const logger6 = getLogger();
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
const
|
|
1917
|
-
if (
|
|
1918
|
-
|
|
2132
|
+
const timeoutMs = params.timeoutMs ?? DEFAULT_LOGIN_WAIT_TIMEOUT_MS;
|
|
2133
|
+
const pollIntervalMs = Math.max(params.intervalSeconds, 1) * 1e3;
|
|
2134
|
+
const startedAt = Date.now();
|
|
2135
|
+
logger6.info("Waiting for device code authorization", {
|
|
2136
|
+
timeoutMs,
|
|
2137
|
+
pollIntervalMs
|
|
2138
|
+
});
|
|
2139
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
2140
|
+
const pollResult = await this.pollDeviceCode(params.deviceCode);
|
|
2141
|
+
if (pollResult.status !== "pending" /* Pending */) {
|
|
2142
|
+
return pollResult;
|
|
1919
2143
|
}
|
|
1920
|
-
const
|
|
1921
|
-
if (
|
|
1922
|
-
|
|
1923
|
-
try {
|
|
1924
|
-
fs6.rmSync(sessionDir, { recursive: true, force: true });
|
|
1925
|
-
deletedCount++;
|
|
1926
|
-
logger6.info("Deleted old session", {
|
|
1927
|
-
sessionId,
|
|
1928
|
-
age: Math.floor((Date.now() - sessionDate.getTime()) / (1e3 * 60 * 60 * 24))
|
|
1929
|
-
});
|
|
1930
|
-
} catch (error) {
|
|
1931
|
-
logger6.error("Failed to delete session", {
|
|
1932
|
-
sessionId,
|
|
1933
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
2144
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
2145
|
+
if (remainingMs <= 0) {
|
|
2146
|
+
break;
|
|
1936
2147
|
}
|
|
2148
|
+
await sleep({ durationMs: Math.min(pollIntervalMs, remainingMs) });
|
|
1937
2149
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
});
|
|
1943
|
-
}
|
|
1944
|
-
return deletedCount;
|
|
1945
|
-
}
|
|
1946
|
-
/**
|
|
1947
|
-
* Get a session directory path.
|
|
1948
|
-
* @param sessionId - Session ID.
|
|
1949
|
-
* @returns Session directory path.
|
|
1950
|
-
*/
|
|
1951
|
-
getSessionPath(sessionId) {
|
|
1952
|
-
return path11.join(this.sessionsDir, sessionId);
|
|
2150
|
+
return {
|
|
2151
|
+
status: "pending" /* Pending */,
|
|
2152
|
+
message: "Timed out waiting for browser login confirmation. Please finish login in your browser, then call `muggle_auth_poll` to continue."
|
|
2153
|
+
};
|
|
1953
2154
|
}
|
|
1954
2155
|
/**
|
|
1955
|
-
*
|
|
1956
|
-
* @param sessionId - Session ID to delete.
|
|
1957
|
-
* @returns Whether deletion succeeded.
|
|
2156
|
+
* Get user info from Auth0.
|
|
1958
2157
|
*/
|
|
1959
|
-
|
|
2158
|
+
async getUserInfo(accessToken) {
|
|
1960
2159
|
const logger6 = getLogger();
|
|
1961
|
-
const
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
return false;
|
|
1965
|
-
}
|
|
2160
|
+
const config = getConfig();
|
|
2161
|
+
const { domain } = config.localQa.auth0;
|
|
2162
|
+
const url = `https://${domain}/userinfo`;
|
|
1966
2163
|
try {
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
fs6.unlinkSync(currentPath);
|
|
2164
|
+
const response = await fetch(url, {
|
|
2165
|
+
method: "GET",
|
|
2166
|
+
headers: {
|
|
2167
|
+
Authorization: `Bearer ${accessToken}`
|
|
1972
2168
|
}
|
|
2169
|
+
});
|
|
2170
|
+
if (!response.ok) {
|
|
2171
|
+
logger6.warn("Failed to get user info", { status: response.status });
|
|
2172
|
+
return {};
|
|
1973
2173
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
return true;
|
|
2174
|
+
const userInfoData = await response.json();
|
|
2175
|
+
return userInfoData;
|
|
1977
2176
|
} catch (error) {
|
|
1978
|
-
logger6.
|
|
1979
|
-
sessionId,
|
|
2177
|
+
logger6.warn("User info request failed", {
|
|
1980
2178
|
error: error instanceof Error ? error.message : String(error)
|
|
1981
2179
|
});
|
|
1982
|
-
return
|
|
2180
|
+
return {};
|
|
1983
2181
|
}
|
|
1984
2182
|
}
|
|
1985
|
-
};
|
|
1986
|
-
var serviceInstance2 = null;
|
|
1987
|
-
function getStorageService() {
|
|
1988
|
-
serviceInstance2 ??= new StorageService();
|
|
1989
|
-
return serviceInstance2;
|
|
1990
|
-
}
|
|
1991
|
-
function resetStorageService() {
|
|
1992
|
-
serviceInstance2 = null;
|
|
1993
|
-
}
|
|
1994
|
-
var logger3 = getLogger();
|
|
1995
|
-
var RunResultStorageService = class {
|
|
1996
|
-
/** Base directory for run results. */
|
|
1997
|
-
runResultsDir;
|
|
1998
|
-
/** Base directory for test scripts. */
|
|
1999
|
-
testScriptsDir;
|
|
2000
|
-
constructor() {
|
|
2001
|
-
const storageService = getStorageService();
|
|
2002
|
-
const dataDir = storageService.getDataDir();
|
|
2003
|
-
this.runResultsDir = path11.join(dataDir, "run-results");
|
|
2004
|
-
this.testScriptsDir = path11.join(dataDir, "test-scripts");
|
|
2005
|
-
this.ensureDirectories();
|
|
2006
|
-
}
|
|
2007
2183
|
/**
|
|
2008
|
-
*
|
|
2184
|
+
* Store authentication tokens.
|
|
2009
2185
|
*/
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2186
|
+
async storeAuth(params) {
|
|
2187
|
+
const { tokenResponse, email, userId } = params;
|
|
2188
|
+
const logger6 = getLogger();
|
|
2189
|
+
const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1e3).toISOString();
|
|
2190
|
+
const storedAuth = {
|
|
2191
|
+
accessToken: tokenResponse.accessToken,
|
|
2192
|
+
refreshToken: tokenResponse.refreshToken,
|
|
2193
|
+
expiresAt,
|
|
2194
|
+
email,
|
|
2195
|
+
userId,
|
|
2196
|
+
runtimeTarget: getActiveRuntimeTarget()
|
|
2197
|
+
};
|
|
2198
|
+
const dir = path13.dirname(this.oauthSessionFilePath);
|
|
2199
|
+
if (!fs4.existsSync(dir)) {
|
|
2200
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2016
2201
|
}
|
|
2202
|
+
fs4.writeFileSync(this.oauthSessionFilePath, JSON.stringify(storedAuth, null, 2), {
|
|
2203
|
+
encoding: "utf-8",
|
|
2204
|
+
mode: 384
|
|
2205
|
+
});
|
|
2206
|
+
logger6.info("Auth stored successfully", { email, expiresAt });
|
|
2017
2207
|
}
|
|
2018
|
-
// ========================================
|
|
2019
|
-
// Run Results
|
|
2020
|
-
// ========================================
|
|
2021
2208
|
/**
|
|
2022
|
-
*
|
|
2209
|
+
* Load stored authentication for the active runtime target.
|
|
2210
|
+
* @returns Stored auth, or null when it is absent, unreadable, or was issued for another target.
|
|
2023
2211
|
*/
|
|
2024
|
-
|
|
2212
|
+
loadStoredAuth() {
|
|
2213
|
+
const logger6 = getLogger();
|
|
2214
|
+
if (!fs4.existsSync(this.oauthSessionFilePath)) {
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2025
2217
|
try {
|
|
2026
|
-
const
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2218
|
+
const content = fs4.readFileSync(this.oauthSessionFilePath, "utf-8");
|
|
2219
|
+
const storedAuth = JSON.parse(content);
|
|
2220
|
+
const activeRuntimeTarget = getActiveRuntimeTarget();
|
|
2221
|
+
const config = getConfig();
|
|
2222
|
+
const expectedAuth0Domain = config.localQa.auth0.domain;
|
|
2223
|
+
const tokenIssuer = extractJwtIssuer(storedAuth.accessToken);
|
|
2224
|
+
if (!isStoredAuthForRuntimeTarget({
|
|
2225
|
+
storedRuntimeTarget: storedAuth.runtimeTarget,
|
|
2226
|
+
activeRuntimeTarget,
|
|
2227
|
+
tokenIssuer,
|
|
2228
|
+
expectedAuth0Domain
|
|
2229
|
+
})) {
|
|
2230
|
+
logger6.warn("Ignoring stored auth issued for a different runtime target", {
|
|
2231
|
+
storedRuntimeTarget: storedAuth.runtimeTarget ?? "unrecorded",
|
|
2232
|
+
activeRuntimeTarget,
|
|
2233
|
+
tokenIssuer: tokenIssuer ?? "undecodable",
|
|
2234
|
+
expectedAuth0Domain
|
|
2235
|
+
});
|
|
2236
|
+
return null;
|
|
2035
2237
|
}
|
|
2036
|
-
return
|
|
2037
|
-
} catch {
|
|
2038
|
-
|
|
2238
|
+
return storedAuth;
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
logger6.error("Failed to load stored auth", {
|
|
2241
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2242
|
+
});
|
|
2243
|
+
return null;
|
|
2039
2244
|
}
|
|
2040
2245
|
}
|
|
2041
2246
|
/**
|
|
2042
|
-
* Get
|
|
2247
|
+
* Get the current access token (if valid).
|
|
2248
|
+
* Does not auto-refresh - use getValidAccessToken() for that.
|
|
2043
2249
|
*/
|
|
2044
|
-
|
|
2045
|
-
const
|
|
2046
|
-
if (!
|
|
2047
|
-
return
|
|
2250
|
+
getAccessToken() {
|
|
2251
|
+
const storedAuth = this.loadStoredAuth();
|
|
2252
|
+
if (!storedAuth) {
|
|
2253
|
+
return null;
|
|
2048
2254
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
return void 0;
|
|
2255
|
+
const now = /* @__PURE__ */ new Date();
|
|
2256
|
+
const expiresAt = new Date(storedAuth.expiresAt);
|
|
2257
|
+
if (now >= expiresAt) {
|
|
2258
|
+
return null;
|
|
2054
2259
|
}
|
|
2260
|
+
return storedAuth.accessToken;
|
|
2055
2261
|
}
|
|
2056
2262
|
/**
|
|
2057
|
-
*
|
|
2058
|
-
|
|
2059
|
-
saveRunResult(result) {
|
|
2060
|
-
const filePath = path11.join(this.runResultsDir, `${result.id}.json`);
|
|
2061
|
-
fs6.writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
2062
|
-
}
|
|
2063
|
-
/**
|
|
2064
|
-
* Create a new run result.
|
|
2065
|
-
*/
|
|
2066
|
-
createRunResult(params) {
|
|
2067
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2068
|
-
const id = randomUUID();
|
|
2069
|
-
const result = {
|
|
2070
|
-
id,
|
|
2071
|
-
runType: params.runType,
|
|
2072
|
-
status: "pending",
|
|
2073
|
-
cloudTestCaseId: params.cloudTestCaseId,
|
|
2074
|
-
projectId: params.projectId,
|
|
2075
|
-
useCaseId: params.useCaseId,
|
|
2076
|
-
localUrl: params.localUrl,
|
|
2077
|
-
productionUrl: params.productionUrl,
|
|
2078
|
-
localExecutionContext: params.localExecutionContext,
|
|
2079
|
-
createdAt: now,
|
|
2080
|
-
updatedAt: now
|
|
2081
|
-
};
|
|
2082
|
-
this.saveRunResult(result);
|
|
2083
|
-
return result;
|
|
2084
|
-
}
|
|
2085
|
-
/**
|
|
2086
|
-
* Update a run result.
|
|
2263
|
+
* Check if the access token is expired or about to expire.
|
|
2264
|
+
* Uses a 5-minute buffer for safety.
|
|
2087
2265
|
*/
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
if (!
|
|
2091
|
-
return
|
|
2266
|
+
isAccessTokenExpired() {
|
|
2267
|
+
const storedAuth = this.loadStoredAuth();
|
|
2268
|
+
if (!storedAuth) {
|
|
2269
|
+
return true;
|
|
2092
2270
|
}
|
|
2093
|
-
const
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
};
|
|
2098
|
-
this.saveRunResult(updated);
|
|
2099
|
-
return updated;
|
|
2271
|
+
const now = /* @__PURE__ */ new Date();
|
|
2272
|
+
const expiresAt = new Date(storedAuth.expiresAt);
|
|
2273
|
+
const bufferMs = 5 * 60 * 1e3;
|
|
2274
|
+
return now.getTime() >= expiresAt.getTime() - bufferMs;
|
|
2100
2275
|
}
|
|
2101
|
-
// ========================================
|
|
2102
|
-
// Test Scripts
|
|
2103
|
-
// ========================================
|
|
2104
2276
|
/**
|
|
2105
|
-
*
|
|
2277
|
+
* Refresh the access token using the stored refresh token.
|
|
2278
|
+
* @returns New access token or null if refresh failed.
|
|
2106
2279
|
*/
|
|
2107
|
-
|
|
2280
|
+
async refreshAccessToken() {
|
|
2281
|
+
const logger6 = getLogger();
|
|
2282
|
+
const storedAuth = this.loadStoredAuth();
|
|
2283
|
+
if (!storedAuth?.refreshToken) {
|
|
2284
|
+
logger6.debug("No refresh token available");
|
|
2285
|
+
return null;
|
|
2286
|
+
}
|
|
2287
|
+
const config = getConfig();
|
|
2288
|
+
const { domain, clientId } = config.localQa.auth0;
|
|
2289
|
+
logger6.info("Refreshing access token");
|
|
2290
|
+
const url = `https://${domain}/oauth/token`;
|
|
2291
|
+
const body = new URLSearchParams({
|
|
2292
|
+
grant_type: "refresh_token",
|
|
2293
|
+
client_id: clientId,
|
|
2294
|
+
refresh_token: storedAuth.refreshToken
|
|
2295
|
+
});
|
|
2108
2296
|
try {
|
|
2109
|
-
const
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2297
|
+
const response = await fetch(url, {
|
|
2298
|
+
method: "POST",
|
|
2299
|
+
headers: {
|
|
2300
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2301
|
+
},
|
|
2302
|
+
body: body.toString()
|
|
2303
|
+
});
|
|
2304
|
+
if (!response.ok) {
|
|
2305
|
+
const errorText = await response.text();
|
|
2306
|
+
logger6.error("Token refresh failed", {
|
|
2307
|
+
status: response.status,
|
|
2308
|
+
error: errorText
|
|
2309
|
+
});
|
|
2310
|
+
return null;
|
|
2118
2311
|
}
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2312
|
+
const tokenData = await response.json();
|
|
2313
|
+
const newExpiresAt = new Date(Date.now() + tokenData.expires_in * 1e3).toISOString();
|
|
2314
|
+
const updatedAuth = {
|
|
2315
|
+
accessToken: tokenData.access_token,
|
|
2316
|
+
refreshToken: tokenData.refresh_token ?? storedAuth.refreshToken,
|
|
2317
|
+
expiresAt: newExpiresAt,
|
|
2318
|
+
email: storedAuth.email,
|
|
2319
|
+
userId: storedAuth.userId,
|
|
2320
|
+
runtimeTarget: storedAuth.runtimeTarget ?? getActiveRuntimeTarget()
|
|
2321
|
+
};
|
|
2322
|
+
const dir = path13.dirname(this.oauthSessionFilePath);
|
|
2323
|
+
if (!fs4.existsSync(dir)) {
|
|
2324
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2325
|
+
}
|
|
2326
|
+
fs4.writeFileSync(this.oauthSessionFilePath, JSON.stringify(updatedAuth, null, 2), {
|
|
2327
|
+
encoding: "utf-8",
|
|
2328
|
+
mode: 384
|
|
2329
|
+
});
|
|
2330
|
+
logger6.info("Access token refreshed", { expiresAt: newExpiresAt });
|
|
2331
|
+
return tokenData.access_token;
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
logger6.error("Token refresh request failed", {
|
|
2334
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2335
|
+
});
|
|
2336
|
+
return null;
|
|
2122
2337
|
}
|
|
2123
2338
|
}
|
|
2124
2339
|
/**
|
|
2125
|
-
* Get a
|
|
2340
|
+
* Get a valid access token, refreshing if necessary.
|
|
2341
|
+
* This is the preferred method for getting an access token for API calls.
|
|
2342
|
+
* @returns Valid access token or null if not authenticated or refresh failed.
|
|
2126
2343
|
*/
|
|
2127
|
-
|
|
2128
|
-
const
|
|
2129
|
-
|
|
2130
|
-
|
|
2344
|
+
async getValidAccessToken() {
|
|
2345
|
+
const logger6 = getLogger();
|
|
2346
|
+
const storedAuth = this.loadStoredAuth();
|
|
2347
|
+
if (!storedAuth) {
|
|
2348
|
+
logger6.debug("No stored auth, cannot get valid token");
|
|
2349
|
+
return null;
|
|
2131
2350
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
return
|
|
2351
|
+
const now = /* @__PURE__ */ new Date();
|
|
2352
|
+
const expiresAt = new Date(storedAuth.expiresAt);
|
|
2353
|
+
const isStrictlyExpired = now >= expiresAt;
|
|
2354
|
+
if (!isStrictlyExpired && !this.isAccessTokenExpired()) {
|
|
2355
|
+
return storedAuth.accessToken;
|
|
2137
2356
|
}
|
|
2357
|
+
if (!isStrictlyExpired) {
|
|
2358
|
+
logger6.debug("Access token in buffer zone, attempting proactive refresh");
|
|
2359
|
+
} else {
|
|
2360
|
+
logger6.info("Access token expired, attempting refresh");
|
|
2361
|
+
}
|
|
2362
|
+
const refreshedToken = await this.refreshAccessToken();
|
|
2363
|
+
if (refreshedToken) {
|
|
2364
|
+
return refreshedToken;
|
|
2365
|
+
}
|
|
2366
|
+
if (!isStrictlyExpired) {
|
|
2367
|
+
logger6.warn("Token refresh failed, but token not yet expired - using existing token");
|
|
2368
|
+
return storedAuth.accessToken;
|
|
2369
|
+
}
|
|
2370
|
+
logger6.warn("Token refresh failed and token is expired, user needs to re-authenticate");
|
|
2371
|
+
return null;
|
|
2138
2372
|
}
|
|
2139
2373
|
/**
|
|
2140
|
-
*
|
|
2141
|
-
*/
|
|
2142
|
-
saveTestScript(script) {
|
|
2143
|
-
const filePath = path11.join(this.testScriptsDir, `${script.id}.json`);
|
|
2144
|
-
fs6.writeFileSync(filePath, JSON.stringify(script, null, 2));
|
|
2145
|
-
}
|
|
2146
|
-
/**
|
|
2147
|
-
* Create a new test script.
|
|
2148
|
-
*/
|
|
2149
|
-
createTestScript(params) {
|
|
2150
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2151
|
-
const id = randomUUID();
|
|
2152
|
-
const script = {
|
|
2153
|
-
id,
|
|
2154
|
-
name: params.name,
|
|
2155
|
-
url: params.url,
|
|
2156
|
-
status: "pending",
|
|
2157
|
-
cloudTestCaseId: params.cloudTestCaseId,
|
|
2158
|
-
goal: params.goal,
|
|
2159
|
-
createdAt: now,
|
|
2160
|
-
updatedAt: now
|
|
2161
|
-
};
|
|
2162
|
-
this.saveTestScript(script);
|
|
2163
|
-
return script;
|
|
2164
|
-
}
|
|
2165
|
-
/**
|
|
2166
|
-
* Update a test script.
|
|
2374
|
+
* Clear stored authentication (logout).
|
|
2167
2375
|
*/
|
|
2168
|
-
|
|
2169
|
-
const
|
|
2170
|
-
if (!
|
|
2171
|
-
|
|
2376
|
+
logout() {
|
|
2377
|
+
const logger6 = getLogger();
|
|
2378
|
+
if (!fs4.existsSync(this.oauthSessionFilePath)) {
|
|
2379
|
+
logger6.debug("No auth to clear");
|
|
2380
|
+
return false;
|
|
2381
|
+
}
|
|
2382
|
+
try {
|
|
2383
|
+
fs4.unlinkSync(this.oauthSessionFilePath);
|
|
2384
|
+
logger6.info("Auth cleared successfully");
|
|
2385
|
+
return true;
|
|
2386
|
+
} catch (error) {
|
|
2387
|
+
logger6.error("Failed to clear auth", {
|
|
2388
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2389
|
+
});
|
|
2390
|
+
return false;
|
|
2172
2391
|
}
|
|
2173
|
-
const updated = {
|
|
2174
|
-
...script,
|
|
2175
|
-
...updates,
|
|
2176
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2177
|
-
};
|
|
2178
|
-
this.saveTestScript(updated);
|
|
2179
|
-
return updated;
|
|
2180
2392
|
}
|
|
2181
2393
|
};
|
|
2182
|
-
var
|
|
2183
|
-
function
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
}
|
|
2187
|
-
return instance;
|
|
2394
|
+
var serviceInstance2 = null;
|
|
2395
|
+
function getAuthService() {
|
|
2396
|
+
serviceInstance2 ??= new AuthService();
|
|
2397
|
+
return serviceInstance2;
|
|
2188
2398
|
}
|
|
2189
|
-
function
|
|
2190
|
-
|
|
2399
|
+
function resetAuthService() {
|
|
2400
|
+
serviceInstance2 = null;
|
|
2401
|
+
}
|
|
2402
|
+
function sleep(params) {
|
|
2403
|
+
return new Promise((resolve6) => {
|
|
2404
|
+
setTimeout(() => {
|
|
2405
|
+
resolve6();
|
|
2406
|
+
}, params.durationMs);
|
|
2407
|
+
});
|
|
2191
2408
|
}
|
|
2192
2409
|
|
|
2193
2410
|
// packages/mcps/src/shared/llm-env-constants.ts
|
|
@@ -2406,13 +2623,13 @@ var PREFERENCES_SCHEMA = {
|
|
|
2406
2623
|
|
|
2407
2624
|
// packages/mcps/src/shared/llm-env-service.ts
|
|
2408
2625
|
function resolveLlmEnvOverrides(dataDirOverride2) {
|
|
2409
|
-
const filePath =
|
|
2626
|
+
const filePath = path13.join(getDataDir(), PREFERENCES_FILE_NAME);
|
|
2410
2627
|
let block;
|
|
2411
2628
|
try {
|
|
2412
|
-
if (!
|
|
2629
|
+
if (!fs4.existsSync(filePath)) {
|
|
2413
2630
|
return {};
|
|
2414
2631
|
}
|
|
2415
|
-
const raw = JSON.parse(
|
|
2632
|
+
const raw = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
2416
2633
|
block = raw[LLM_ENV_PREFERENCE_KEY];
|
|
2417
2634
|
} catch {
|
|
2418
2635
|
return {};
|
|
@@ -2647,13 +2864,13 @@ var MODIFY_LOCK_STALE_MS = 5e3;
|
|
|
2647
2864
|
|
|
2648
2865
|
// packages/mcps/src/mcp/local/services/local-execution-lock.ts
|
|
2649
2866
|
function lockFilePath() {
|
|
2650
|
-
return
|
|
2867
|
+
return path13.join(getDataDir(), LOCK_FILE_NAME);
|
|
2651
2868
|
}
|
|
2652
2869
|
function modifyLockPath() {
|
|
2653
2870
|
return lockFilePath() + MODIFY_LOCK_SUFFIX;
|
|
2654
2871
|
}
|
|
2655
2872
|
function normalizeCwd(cwd) {
|
|
2656
|
-
const resolved =
|
|
2873
|
+
const resolved = path13.resolve(cwd);
|
|
2657
2874
|
return os4.platform() === "win32" ? resolved.toLowerCase() : resolved;
|
|
2658
2875
|
}
|
|
2659
2876
|
function pidIsAlive(pid) {
|
|
@@ -2669,7 +2886,7 @@ function pidIsAlive(pid) {
|
|
|
2669
2886
|
}
|
|
2670
2887
|
async function readState() {
|
|
2671
2888
|
try {
|
|
2672
|
-
const raw = await
|
|
2889
|
+
const raw = await fs12.readFile(lockFilePath(), "utf-8");
|
|
2673
2890
|
return JSON.parse(raw);
|
|
2674
2891
|
} catch (error) {
|
|
2675
2892
|
if (error.code === "ENOENT") return null;
|
|
@@ -2677,28 +2894,28 @@ async function readState() {
|
|
|
2677
2894
|
}
|
|
2678
2895
|
}
|
|
2679
2896
|
async function writeState(state2) {
|
|
2680
|
-
await
|
|
2897
|
+
await fs12.writeFile(lockFilePath(), JSON.stringify(state2, null, 2), "utf-8");
|
|
2681
2898
|
}
|
|
2682
2899
|
async function deleteStateFile() {
|
|
2683
2900
|
try {
|
|
2684
|
-
await
|
|
2901
|
+
await fs12.unlink(lockFilePath());
|
|
2685
2902
|
} catch (error) {
|
|
2686
2903
|
if (error.code !== "ENOENT") throw error;
|
|
2687
2904
|
}
|
|
2688
2905
|
}
|
|
2689
2906
|
async function withModifyLock(fn) {
|
|
2690
|
-
await
|
|
2907
|
+
await fs12.mkdir(path13.dirname(modifyLockPath()), { recursive: true });
|
|
2691
2908
|
const modifyPath = modifyLockPath();
|
|
2692
2909
|
let fileHandle = null;
|
|
2693
2910
|
while (fileHandle === null) {
|
|
2694
2911
|
try {
|
|
2695
|
-
fileHandle = await
|
|
2912
|
+
fileHandle = await fs12.open(modifyPath, "wx");
|
|
2696
2913
|
} catch (error) {
|
|
2697
2914
|
if (error.code !== "EEXIST") throw error;
|
|
2698
2915
|
try {
|
|
2699
|
-
const stat3 = await
|
|
2916
|
+
const stat3 = await fs12.stat(modifyPath);
|
|
2700
2917
|
if (Date.now() - stat3.mtimeMs > MODIFY_LOCK_STALE_MS) {
|
|
2701
|
-
await
|
|
2918
|
+
await fs12.unlink(modifyPath).catch(() => void 0);
|
|
2702
2919
|
continue;
|
|
2703
2920
|
}
|
|
2704
2921
|
} catch {
|
|
@@ -2711,7 +2928,7 @@ async function withModifyLock(fn) {
|
|
|
2711
2928
|
return await fn();
|
|
2712
2929
|
} finally {
|
|
2713
2930
|
await fileHandle.close().catch(() => void 0);
|
|
2714
|
-
await
|
|
2931
|
+
await fs12.unlink(modifyPath).catch(() => void 0);
|
|
2715
2932
|
}
|
|
2716
2933
|
}
|
|
2717
2934
|
async function tryAcquireOnce(params) {
|
|
@@ -2850,12 +3067,12 @@ function extractStudioCloudRefs(studioReturnedResult) {
|
|
|
2850
3067
|
return refs;
|
|
2851
3068
|
}
|
|
2852
3069
|
async function readCloudRefsFile(inputFilePath) {
|
|
2853
|
-
const cloudRefsPath =
|
|
2854
|
-
|
|
2855
|
-
`cloudrefs_${
|
|
3070
|
+
const cloudRefsPath = path13.join(
|
|
3071
|
+
path13.dirname(inputFilePath),
|
|
3072
|
+
`cloudrefs_${path13.basename(inputFilePath)}`
|
|
2856
3073
|
);
|
|
2857
3074
|
try {
|
|
2858
|
-
return JSON.parse(await
|
|
3075
|
+
return JSON.parse(await fs12.readFile(cloudRefsPath, "utf-8"));
|
|
2859
3076
|
} catch {
|
|
2860
3077
|
return void 0;
|
|
2861
3078
|
}
|
|
@@ -2874,15 +3091,15 @@ function getAuthenticatedUserId() {
|
|
|
2874
3091
|
}
|
|
2875
3092
|
async function findPackageJsonAsync() {
|
|
2876
3093
|
const currentFileUrl = import.meta.url;
|
|
2877
|
-
const currentDir =
|
|
3094
|
+
const currentDir = path13.dirname(fileURLToPath(currentFileUrl));
|
|
2878
3095
|
const candidatePaths = [
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3096
|
+
path13.resolve(currentDir, "../../../package.json"),
|
|
3097
|
+
path13.resolve(currentDir, "../package.json"),
|
|
3098
|
+
path13.resolve(currentDir, "../../package.json")
|
|
2882
3099
|
];
|
|
2883
3100
|
for (const candidatePath of candidatePaths) {
|
|
2884
3101
|
try {
|
|
2885
|
-
const packageJsonRaw = await
|
|
3102
|
+
const packageJsonRaw = await fs12.readFile(candidatePath, "utf-8");
|
|
2886
3103
|
const packageJson = JSON.parse(packageJsonRaw);
|
|
2887
3104
|
if (packageJson.name === "@muggleai/works" && packageJson.version && packageJson.muggleConfig?.electronAppVersion) {
|
|
2888
3105
|
return {
|
|
@@ -2928,20 +3145,20 @@ function buildStudioAuthContent() {
|
|
|
2928
3145
|
}
|
|
2929
3146
|
async function ensureTempDir() {
|
|
2930
3147
|
const config = getConfig();
|
|
2931
|
-
const tempDir =
|
|
2932
|
-
await
|
|
3148
|
+
const tempDir = path13.join(config.localQa.dataDir, "temp");
|
|
3149
|
+
await fs12.mkdir(tempDir, { recursive: true });
|
|
2933
3150
|
return tempDir;
|
|
2934
3151
|
}
|
|
2935
3152
|
async function writeTempFile(params) {
|
|
2936
3153
|
const tempDir = await ensureTempDir();
|
|
2937
|
-
const filePath =
|
|
2938
|
-
await
|
|
3154
|
+
const filePath = path13.join(tempDir, params.filename);
|
|
3155
|
+
await fs12.writeFile(filePath, JSON.stringify(params.data, null, 2));
|
|
2939
3156
|
return filePath;
|
|
2940
3157
|
}
|
|
2941
3158
|
async function cleanupTempFiles(params) {
|
|
2942
3159
|
for (const filePath of params.filePaths) {
|
|
2943
3160
|
try {
|
|
2944
|
-
await
|
|
3161
|
+
await fs12.unlink(filePath);
|
|
2945
3162
|
} catch {
|
|
2946
3163
|
}
|
|
2947
3164
|
}
|
|
@@ -2949,12 +3166,12 @@ async function cleanupTempFiles(params) {
|
|
|
2949
3166
|
async function moveResultsToArtifacts(params) {
|
|
2950
3167
|
const storageService = getStorageService();
|
|
2951
3168
|
const sessionsDir = storageService.getSessionsDir();
|
|
2952
|
-
const artifactsDir =
|
|
2953
|
-
const actionScriptPath =
|
|
2954
|
-
await
|
|
2955
|
-
await
|
|
3169
|
+
const artifactsDir = path13.join(sessionsDir, params.runId);
|
|
3170
|
+
const actionScriptPath = path13.join(artifactsDir, "action-script.json");
|
|
3171
|
+
await fs12.mkdir(artifactsDir, { recursive: true });
|
|
3172
|
+
await fs12.copyFile(params.generatedScriptPath, actionScriptPath);
|
|
2956
3173
|
try {
|
|
2957
|
-
await
|
|
3174
|
+
await fs12.unlink(params.generatedScriptPath);
|
|
2958
3175
|
} catch {
|
|
2959
3176
|
}
|
|
2960
3177
|
return artifactsDir;
|
|
@@ -2962,13 +3179,13 @@ async function moveResultsToArtifacts(params) {
|
|
|
2962
3179
|
async function writeExecutionLogs(params) {
|
|
2963
3180
|
const storageService = getStorageService();
|
|
2964
3181
|
const sessionsDir = storageService.getSessionsDir();
|
|
2965
|
-
const artifactsDir =
|
|
2966
|
-
await
|
|
2967
|
-
const stdoutPath =
|
|
2968
|
-
const stderrPath =
|
|
3182
|
+
const artifactsDir = path13.join(sessionsDir, params.runId);
|
|
3183
|
+
await fs12.mkdir(artifactsDir, { recursive: true });
|
|
3184
|
+
const stdoutPath = path13.join(artifactsDir, "stdout.log");
|
|
3185
|
+
const stderrPath = path13.join(artifactsDir, "stderr.log");
|
|
2969
3186
|
await Promise.all([
|
|
2970
|
-
|
|
2971
|
-
|
|
3187
|
+
fs12.writeFile(stdoutPath, params.stdout, "utf-8"),
|
|
3188
|
+
fs12.writeFile(stderrPath, params.stderr, "utf-8")
|
|
2972
3189
|
]);
|
|
2973
3190
|
logger4.info("Wrote execution logs to artifacts directory", {
|
|
2974
3191
|
runId: params.runId,
|
|
@@ -2978,10 +3195,10 @@ async function writeExecutionLogs(params) {
|
|
|
2978
3195
|
async function preserveElectronRuntimeArtifacts(params) {
|
|
2979
3196
|
try {
|
|
2980
3197
|
const tempDir = await ensureTempDir();
|
|
2981
|
-
const markerPath =
|
|
3198
|
+
const markerPath = path13.join(tempDir, `${params.runId}_runtime_dir.txt`);
|
|
2982
3199
|
let runtimeDir = null;
|
|
2983
3200
|
try {
|
|
2984
|
-
const marker = await
|
|
3201
|
+
const marker = await fs12.readFile(markerPath, "utf-8");
|
|
2985
3202
|
const trimmed = marker.trim();
|
|
2986
3203
|
if (trimmed.length > 0) runtimeDir = trimmed;
|
|
2987
3204
|
} catch (markerReadError) {
|
|
@@ -3003,25 +3220,25 @@ async function preserveElectronRuntimeArtifacts(params) {
|
|
|
3003
3220
|
return;
|
|
3004
3221
|
}
|
|
3005
3222
|
try {
|
|
3006
|
-
await
|
|
3223
|
+
await fs12.access(runtimeDir);
|
|
3007
3224
|
} catch {
|
|
3008
3225
|
logger4.warn("Electron runtime dir referenced but missing on disk", {
|
|
3009
3226
|
runId: params.runId,
|
|
3010
3227
|
runtimeDir
|
|
3011
3228
|
});
|
|
3012
|
-
await
|
|
3229
|
+
await fs12.unlink(markerPath).catch(() => void 0);
|
|
3013
3230
|
return;
|
|
3014
3231
|
}
|
|
3015
3232
|
const storage = getStorageService();
|
|
3016
|
-
const destDir =
|
|
3017
|
-
await
|
|
3018
|
-
await
|
|
3233
|
+
const destDir = path13.join(storage.getSessionsDir(), params.runId, "electron-runtime");
|
|
3234
|
+
await fs12.mkdir(destDir, { recursive: true });
|
|
3235
|
+
await fs12.cp(runtimeDir, destDir, { recursive: true });
|
|
3019
3236
|
logger4.info("Preserved Electron runtime artifacts", {
|
|
3020
3237
|
runId: params.runId,
|
|
3021
3238
|
sourceDir: runtimeDir,
|
|
3022
3239
|
destDir
|
|
3023
3240
|
});
|
|
3024
|
-
await
|
|
3241
|
+
await fs12.unlink(markerPath).catch(() => void 0);
|
|
3025
3242
|
} catch (preserveError) {
|
|
3026
3243
|
logger4.warn("Failed to preserve Electron runtime artifacts", {
|
|
3027
3244
|
runId: params.runId,
|
|
@@ -3030,14 +3247,14 @@ async function preserveElectronRuntimeArtifacts(params) {
|
|
|
3030
3247
|
}
|
|
3031
3248
|
}
|
|
3032
3249
|
async function scanForRuntimeDirByMtime(params) {
|
|
3033
|
-
const runtimeRoot =
|
|
3250
|
+
const runtimeRoot = path13.join(params.electronCwd, "user_data", "runtime");
|
|
3034
3251
|
try {
|
|
3035
|
-
const entries = await
|
|
3252
|
+
const entries = await fs12.readdir(runtimeRoot, { withFileTypes: true });
|
|
3036
3253
|
let bestCandidate = null;
|
|
3037
3254
|
for (const entry of entries) {
|
|
3038
3255
|
if (!entry.isDirectory()) continue;
|
|
3039
|
-
const fullPath =
|
|
3040
|
-
const stat3 = await
|
|
3256
|
+
const fullPath = path13.join(runtimeRoot, entry.name);
|
|
3257
|
+
const stat3 = await fs12.stat(fullPath);
|
|
3041
3258
|
if (stat3.mtimeMs < params.spawnAtMs) continue;
|
|
3042
3259
|
if (bestCandidate === null || stat3.mtimeMs > bestCandidate.mtimeMs) {
|
|
3043
3260
|
bestCandidate = { path: fullPath, mtimeMs: stat3.mtimeMs };
|
|
@@ -3093,7 +3310,7 @@ async function executeElectronAppAsync(params) {
|
|
|
3093
3310
|
spawnArgs,
|
|
3094
3311
|
llmEnvOverrideNames
|
|
3095
3312
|
});
|
|
3096
|
-
const electronCwd =
|
|
3313
|
+
const electronCwd = path13.dirname(electronAppPath);
|
|
3097
3314
|
const child = spawn(electronAppPath, spawnArgs, {
|
|
3098
3315
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3099
3316
|
env: electronEnv,
|
|
@@ -3276,17 +3493,17 @@ ${executionResult.stdout}
|
|
|
3276
3493
|
STDERR:
|
|
3277
3494
|
${executionResult.stderr}`;
|
|
3278
3495
|
const sessionsDir = getStorageService().getSessionsDir();
|
|
3279
|
-
const artifactsDir2 =
|
|
3280
|
-
const generatedScriptPath2 =
|
|
3281
|
-
|
|
3282
|
-
`gen_${
|
|
3496
|
+
const artifactsDir2 = path13.join(sessionsDir, runId);
|
|
3497
|
+
const generatedScriptPath2 = path13.join(
|
|
3498
|
+
path13.dirname(inputFilePath),
|
|
3499
|
+
`gen_${path13.basename(inputFilePath)}`
|
|
3283
3500
|
);
|
|
3284
3501
|
let failedStudioResult;
|
|
3285
3502
|
try {
|
|
3286
|
-
const failedScriptRaw = await
|
|
3503
|
+
const failedScriptRaw = await fs12.readFile(generatedScriptPath2, "utf-8");
|
|
3287
3504
|
failedStudioResult = JSON.parse(failedScriptRaw);
|
|
3288
|
-
await
|
|
3289
|
-
await
|
|
3505
|
+
await fs12.copyFile(generatedScriptPath2, path13.join(artifactsDir2, "action-script.json"));
|
|
3506
|
+
await fs12.unlink(generatedScriptPath2).catch(() => {
|
|
3290
3507
|
});
|
|
3291
3508
|
} catch {
|
|
3292
3509
|
}
|
|
@@ -3315,11 +3532,11 @@ ${executionResult.stderr}`;
|
|
|
3315
3532
|
errorMessage: failureMessage
|
|
3316
3533
|
};
|
|
3317
3534
|
}
|
|
3318
|
-
const generatedScriptPath =
|
|
3319
|
-
|
|
3320
|
-
`gen_${
|
|
3535
|
+
const generatedScriptPath = path13.join(
|
|
3536
|
+
path13.dirname(inputFilePath),
|
|
3537
|
+
`gen_${path13.basename(inputFilePath)}`
|
|
3321
3538
|
);
|
|
3322
|
-
const generatedScriptRaw = await
|
|
3539
|
+
const generatedScriptRaw = await fs12.readFile(generatedScriptPath, "utf-8");
|
|
3323
3540
|
const generatedScript = JSON.parse(generatedScriptRaw);
|
|
3324
3541
|
const generatedSteps = generatedScript.steps;
|
|
3325
3542
|
if (!Array.isArray(generatedSteps)) {
|
|
@@ -3463,7 +3680,7 @@ STDOUT:
|
|
|
3463
3680
|
${executionResult.stdout}
|
|
3464
3681
|
STDERR:
|
|
3465
3682
|
${executionResult.stderr}`;
|
|
3466
|
-
const artifactsDir2 =
|
|
3683
|
+
const artifactsDir2 = path13.join(getStorageService().getSessionsDir(), runId);
|
|
3467
3684
|
storage.updateRunResult(runId, {
|
|
3468
3685
|
status: "failed",
|
|
3469
3686
|
executionTimeMs,
|
|
@@ -3481,7 +3698,7 @@ ${executionResult.stderr}`;
|
|
|
3481
3698
|
errorMessage: failureMessage
|
|
3482
3699
|
};
|
|
3483
3700
|
}
|
|
3484
|
-
const artifactsDir =
|
|
3701
|
+
const artifactsDir = path13.join(getStorageService().getSessionsDir(), runId);
|
|
3485
3702
|
const replayStudioResult = await readCloudRefsFile(inputFilePath);
|
|
3486
3703
|
const replayStudioCloudRefs = extractStudioCloudRefs(replayStudioResult);
|
|
3487
3704
|
storage.updateRunResult(runId, {
|
|
@@ -4011,7 +4228,7 @@ var PREFS_FILENAME = "preferences.json";
|
|
|
4011
4228
|
var OAUTH_SESSION_FILENAME = "oauth-session.json";
|
|
4012
4229
|
var RELEASE_MANIFEST_FILENAME = "release-manifest.json";
|
|
4013
4230
|
function getDataDir3() {
|
|
4014
|
-
return
|
|
4231
|
+
return path13.join(os4.homedir(), DATA_DIR_NAME2);
|
|
4015
4232
|
}
|
|
4016
4233
|
|
|
4017
4234
|
var cachedInstallId2;
|
|
@@ -4076,9 +4293,9 @@ function isValid3(value) {
|
|
|
4076
4293
|
function readReleaseManifest2() {
|
|
4077
4294
|
if (cachedManifest2 !== void 0)
|
|
4078
4295
|
return cachedManifest2;
|
|
4079
|
-
const
|
|
4296
|
+
const path19 = MANIFEST_PATH2;
|
|
4080
4297
|
try {
|
|
4081
|
-
const raw = readFileSync(
|
|
4298
|
+
const raw = readFileSync(path19, "utf8");
|
|
4082
4299
|
const parsed = JSON.parse(raw);
|
|
4083
4300
|
cachedManifest2 = isValid3(parsed) ? parsed : DEV_MANIFEST2;
|
|
4084
4301
|
return cachedManifest2;
|
|
@@ -4845,17 +5062,17 @@ var PromptServiceClient = class {
|
|
|
4845
5062
|
* @param path - Path to validate.
|
|
4846
5063
|
* @throws GatewayError if path is not allowed.
|
|
4847
5064
|
*/
|
|
4848
|
-
validatePath(
|
|
4849
|
-
const isAllowed = ALLOWED_UPSTREAM_PREFIXES.some((prefix) =>
|
|
5065
|
+
validatePath(path19) {
|
|
5066
|
+
const isAllowed = ALLOWED_UPSTREAM_PREFIXES.some((prefix) => path19.startsWith(prefix));
|
|
4850
5067
|
if (!isAllowed) {
|
|
4851
5068
|
const logger6 = getLogger();
|
|
4852
5069
|
logger6.error("Path not in allowlist", {
|
|
4853
|
-
path:
|
|
5070
|
+
path: path19,
|
|
4854
5071
|
allowedPrefixes: ALLOWED_UPSTREAM_PREFIXES
|
|
4855
5072
|
});
|
|
4856
5073
|
throw new GatewayError({
|
|
4857
5074
|
code: "FORBIDDEN" /* FORBIDDEN */,
|
|
4858
|
-
message: `Path '${
|
|
5075
|
+
message: `Path '${path19}' is not allowed`
|
|
4859
5076
|
});
|
|
4860
5077
|
}
|
|
4861
5078
|
}
|
|
@@ -6935,6 +7152,9 @@ var RunResultListInputSchema = z.object({
|
|
|
6935
7152
|
var RunResultGetInputSchema = z.object({
|
|
6936
7153
|
runId: MuggleEntityIdSchema.describe("Run result ID (UUID) to retrieve")
|
|
6937
7154
|
});
|
|
7155
|
+
var RunStepsGetInputSchema = z.object({
|
|
7156
|
+
runId: z.string().min(1).optional().describe("Run id, or a unique prefix of one. Omit to read the most recent run.")
|
|
7157
|
+
});
|
|
6938
7158
|
var TestScriptListInputSchema2 = z.object({
|
|
6939
7159
|
cloudTestCaseId: MuggleEntityIdSchema.optional().describe("Optional cloud test case ID (UUID) to filter by")
|
|
6940
7160
|
});
|
|
@@ -7028,11 +7248,11 @@ var TELEMETRY_DIR_NAME = "telemetry";
|
|
|
7028
7248
|
var FAILURE_EVENTS_FILE_NAME = "failure-events.jsonl";
|
|
7029
7249
|
function appendFailureEvent(record) {
|
|
7030
7250
|
try {
|
|
7031
|
-
const dir =
|
|
7032
|
-
|
|
7033
|
-
const filePath =
|
|
7251
|
+
const dir = path13.join(getDataDir(), TELEMETRY_DIR_NAME);
|
|
7252
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
7253
|
+
const filePath = path13.join(dir, FAILURE_EVENTS_FILE_NAME);
|
|
7034
7254
|
const full = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...record };
|
|
7035
|
-
|
|
7255
|
+
fs4.appendFileSync(filePath, `${JSON.stringify(full)}
|
|
7036
7256
|
`, "utf-8");
|
|
7037
7257
|
} catch (error) {
|
|
7038
7258
|
getLogger().warn("Failed to append failure event", {
|
|
@@ -7051,17 +7271,17 @@ var ProjectPreferencesReconcileOutcome = /* @__PURE__ */ ((ProjectPreferencesRec
|
|
|
7051
7271
|
|
|
7052
7272
|
// packages/mcps/src/shared/preferences-service.ts
|
|
7053
7273
|
function getGlobalPreferencesFilePath(dataDirOverride2) {
|
|
7054
|
-
return
|
|
7274
|
+
return path13.join(dataDirOverride2 ?? getDataDir(), PREFERENCES_FILE_NAME);
|
|
7055
7275
|
}
|
|
7056
7276
|
function isFirstRun(dataDirOverride2) {
|
|
7057
|
-
return !
|
|
7277
|
+
return !fs4.existsSync(getGlobalPreferencesFilePath(dataDirOverride2));
|
|
7058
7278
|
}
|
|
7059
7279
|
function readPreferencesFile(filePath) {
|
|
7060
7280
|
try {
|
|
7061
|
-
if (!
|
|
7281
|
+
if (!fs4.existsSync(filePath)) {
|
|
7062
7282
|
return {};
|
|
7063
7283
|
}
|
|
7064
|
-
const raw = JSON.parse(
|
|
7284
|
+
const raw = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
7065
7285
|
return raw.preferences ?? {};
|
|
7066
7286
|
} catch (error) {
|
|
7067
7287
|
getLogger().warn("Failed to read preferences file", {
|
|
@@ -7077,15 +7297,15 @@ function writePreferencesFile(filePath, prefs) {
|
|
|
7077
7297
|
version: PREFERENCES_VERSION,
|
|
7078
7298
|
preferences: prefs
|
|
7079
7299
|
};
|
|
7080
|
-
|
|
7300
|
+
fs4.writeFileSync(filePath, `${JSON.stringify(file, null, 2)}
|
|
7081
7301
|
`, "utf-8");
|
|
7082
7302
|
}
|
|
7083
7303
|
function readRawPreferencesFile(filePath) {
|
|
7084
7304
|
try {
|
|
7085
|
-
if (!
|
|
7305
|
+
if (!fs4.existsSync(filePath)) {
|
|
7086
7306
|
return {};
|
|
7087
7307
|
}
|
|
7088
|
-
return JSON.parse(
|
|
7308
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
7089
7309
|
} catch {
|
|
7090
7310
|
return {};
|
|
7091
7311
|
}
|
|
@@ -7096,8 +7316,8 @@ function resolvePreferences(dataDirOverride2) {
|
|
|
7096
7316
|
}
|
|
7097
7317
|
function writePreferences2(prefs, dataDirOverride2) {
|
|
7098
7318
|
const dir = dataDirOverride2 ?? getDataDir();
|
|
7099
|
-
|
|
7100
|
-
const filePath =
|
|
7319
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
7320
|
+
const filePath = path13.join(dir, PREFERENCES_FILE_NAME);
|
|
7101
7321
|
const existing = readPreferencesFile(filePath);
|
|
7102
7322
|
writePreferencesFile(filePath, { ...existing, ...prefs });
|
|
7103
7323
|
}
|
|
@@ -7110,8 +7330,8 @@ function readPreferencesMetadata(dataDirOverride2) {
|
|
|
7110
7330
|
}
|
|
7111
7331
|
function writePreferencesWithMetadata(prefs, metadata, dataDirOverride2) {
|
|
7112
7332
|
const dir = dataDirOverride2 ?? getDataDir();
|
|
7113
|
-
|
|
7114
|
-
const filePath =
|
|
7333
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
7334
|
+
const filePath = path13.join(dir, PREFERENCES_FILE_NAME);
|
|
7115
7335
|
const existing = readRawPreferencesFile(filePath);
|
|
7116
7336
|
const file = {
|
|
7117
7337
|
...existing,
|
|
@@ -7119,12 +7339,12 @@ function writePreferencesWithMetadata(prefs, metadata, dataDirOverride2) {
|
|
|
7119
7339
|
version: PREFERENCES_VERSION,
|
|
7120
7340
|
preferences: { ...existing.preferences ?? {}, ...prefs }
|
|
7121
7341
|
};
|
|
7122
|
-
|
|
7342
|
+
fs4.writeFileSync(filePath, `${JSON.stringify(file, null, 2)}
|
|
7123
7343
|
`, "utf-8");
|
|
7124
7344
|
}
|
|
7125
7345
|
function resetPreference(key, dataDirOverride2) {
|
|
7126
7346
|
const filePath = getGlobalPreferencesFilePath(dataDirOverride2);
|
|
7127
|
-
if (!
|
|
7347
|
+
if (!fs4.existsSync(filePath)) {
|
|
7128
7348
|
return;
|
|
7129
7349
|
}
|
|
7130
7350
|
if (!key) {
|
|
@@ -7136,7 +7356,7 @@ function resetPreference(key, dataDirOverride2) {
|
|
|
7136
7356
|
writePreferencesFile(filePath, existing);
|
|
7137
7357
|
}
|
|
7138
7358
|
function reconcileProjectPreferences(cwd, dataDirOverride2) {
|
|
7139
|
-
const projectFilePath =
|
|
7359
|
+
const projectFilePath = path13.join(cwd, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_FILE_NAME);
|
|
7140
7360
|
const projectPreferences = readPreferencesFile(projectFilePath);
|
|
7141
7361
|
const projectKeys = Object.keys(projectPreferences);
|
|
7142
7362
|
if (projectKeys.length === 0) {
|
|
@@ -7181,17 +7401,17 @@ var MUGGLE_HOME_DISPLAY_DIR = "~/.muggle-ai";
|
|
|
7181
7401
|
|
|
7182
7402
|
// packages/mcps/src/shared/cwd-keyed-cache.ts
|
|
7183
7403
|
function resolveHomeFilePath(cache) {
|
|
7184
|
-
return
|
|
7404
|
+
return path13.join(getDataDir(), cache.fileName);
|
|
7185
7405
|
}
|
|
7186
7406
|
function resolveLegacyFilePath(cache, cwd) {
|
|
7187
|
-
return
|
|
7407
|
+
return path13.join(cwd, LEGACY_CACHE_DIR_NAME, cache.fileName);
|
|
7188
7408
|
}
|
|
7189
7409
|
function parseJsonFile(filePath) {
|
|
7190
7410
|
try {
|
|
7191
|
-
if (!
|
|
7411
|
+
if (!fs4.existsSync(filePath)) {
|
|
7192
7412
|
return null;
|
|
7193
7413
|
}
|
|
7194
|
-
return JSON.parse(
|
|
7414
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
7195
7415
|
} catch (error) {
|
|
7196
7416
|
getLogger().warn("Failed to read cache file", {
|
|
7197
7417
|
path: filePath,
|
|
@@ -7205,12 +7425,12 @@ function readHomeEntries(cache) {
|
|
|
7205
7425
|
return parsed?.entries ?? {};
|
|
7206
7426
|
}
|
|
7207
7427
|
function writeHomeEntries(cache, entries) {
|
|
7208
|
-
|
|
7428
|
+
fs4.mkdirSync(getDataDir(), { recursive: true });
|
|
7209
7429
|
const file = {
|
|
7210
7430
|
version: CWD_KEYED_CACHE_VERSION,
|
|
7211
7431
|
entries
|
|
7212
7432
|
};
|
|
7213
|
-
|
|
7433
|
+
fs4.writeFileSync(
|
|
7214
7434
|
resolveHomeFilePath(cache),
|
|
7215
7435
|
`${JSON.stringify(file, null, 2)}
|
|
7216
7436
|
`,
|
|
@@ -7224,7 +7444,7 @@ function readLegacyEntry(cache, cwd) {
|
|
|
7224
7444
|
return parsed?.[cache.legacyEntryKey] ?? null;
|
|
7225
7445
|
}
|
|
7226
7446
|
function readCwdEntry(cache, cwd) {
|
|
7227
|
-
const cwdKey =
|
|
7447
|
+
const cwdKey = path13.resolve(cwd);
|
|
7228
7448
|
const entries = readHomeEntries(cache);
|
|
7229
7449
|
const homeEntry = entries[cwdKey];
|
|
7230
7450
|
if (homeEntry) {
|
|
@@ -7246,18 +7466,18 @@ function readCwdEntry(cache, cwd) {
|
|
|
7246
7466
|
}
|
|
7247
7467
|
function writeCwdEntry(cache, cwd, entry) {
|
|
7248
7468
|
const entries = readHomeEntries(cache);
|
|
7249
|
-
writeHomeEntries(cache, { ...entries, [
|
|
7469
|
+
writeHomeEntries(cache, { ...entries, [path13.resolve(cwd)]: entry });
|
|
7250
7470
|
}
|
|
7251
7471
|
function clearCwdEntry(cache, cwd) {
|
|
7252
7472
|
const entries = readHomeEntries(cache);
|
|
7253
|
-
const cwdKey =
|
|
7473
|
+
const cwdKey = path13.resolve(cwd);
|
|
7254
7474
|
if (cwdKey in entries) {
|
|
7255
7475
|
delete entries[cwdKey];
|
|
7256
7476
|
writeHomeEntries(cache, entries);
|
|
7257
7477
|
}
|
|
7258
7478
|
const legacyFilePath = resolveLegacyFilePath(cache, cwd);
|
|
7259
|
-
if (
|
|
7260
|
-
|
|
7479
|
+
if (fs4.existsSync(legacyFilePath)) {
|
|
7480
|
+
fs4.unlinkSync(legacyFilePath);
|
|
7261
7481
|
}
|
|
7262
7482
|
}
|
|
7263
7483
|
|
|
@@ -7390,6 +7610,29 @@ var runResultListTool = {
|
|
|
7390
7610
|
return { content, isError: false, data: { results } };
|
|
7391
7611
|
}
|
|
7392
7612
|
};
|
|
7613
|
+
var runStepsGetTool = {
|
|
7614
|
+
name: "muggle-local-run-steps-get",
|
|
7615
|
+
description: "Read a local run's steps beside the frames they produced, so the run's own verdict can be shown against its own evidence. Returns the ordered steps (action, the agent's explanation, screenshot path) plus the run's status and closing verdict. Takes a run id or a unique prefix; omit it for the most recent run. Read-only \u2014 it never modifies a session. Reach for this after a failed local run instead of parsing action-script.json by hand.",
|
|
7616
|
+
inputSchema: RunStepsGetInputSchema,
|
|
7617
|
+
execute: async (ctx) => {
|
|
7618
|
+
const logger6 = createChildLogger2(ctx.correlationId);
|
|
7619
|
+
logger6.info("Executing muggle-local-run-steps-get");
|
|
7620
|
+
const input = RunStepsGetInputSchema.parse(ctx.input);
|
|
7621
|
+
try {
|
|
7622
|
+
const runView = readRunView(input.runId);
|
|
7623
|
+
return {
|
|
7624
|
+
content: renderRunViewText(runView, { includeHtmlOffer: false }),
|
|
7625
|
+
isError: false,
|
|
7626
|
+
data: { run: runView }
|
|
7627
|
+
};
|
|
7628
|
+
} catch (error) {
|
|
7629
|
+
if (error instanceof RunViewError) {
|
|
7630
|
+
return { content: error.message, isError: true };
|
|
7631
|
+
}
|
|
7632
|
+
throw error;
|
|
7633
|
+
}
|
|
7634
|
+
}
|
|
7635
|
+
};
|
|
7393
7636
|
var runResultGetTool = {
|
|
7394
7637
|
name: "muggle-local-run-result-get",
|
|
7395
7638
|
description: "Get detailed information about a run result including screenshots and action script output.",
|
|
@@ -7422,7 +7665,7 @@ var runResultGetTool = {
|
|
|
7422
7665
|
const testScript = storage.getTestScript(storedRunResult.testScriptId);
|
|
7423
7666
|
testScriptSteps = testScript?.actionScript?.length;
|
|
7424
7667
|
}
|
|
7425
|
-
if (storedRunResult.artifactsDir &&
|
|
7668
|
+
if (storedRunResult.artifactsDir && fs4.existsSync(storedRunResult.artifactsDir)) {
|
|
7426
7669
|
contentParts.push(
|
|
7427
7670
|
"",
|
|
7428
7671
|
"### Artifacts (view action script + screenshots)",
|
|
@@ -7430,26 +7673,26 @@ var runResultGetTool = {
|
|
|
7430
7673
|
`**Location:** \`${storedRunResult.artifactsDir}\``,
|
|
7431
7674
|
""
|
|
7432
7675
|
);
|
|
7433
|
-
const actionScriptPath =
|
|
7434
|
-
const resultsMdPath =
|
|
7435
|
-
const screenshotsDir =
|
|
7436
|
-
const stdoutLogPath =
|
|
7437
|
-
const stderrLogPath =
|
|
7676
|
+
const actionScriptPath = path13.join(storedRunResult.artifactsDir, "action-script.json");
|
|
7677
|
+
const resultsMdPath = path13.join(storedRunResult.artifactsDir, "results.md");
|
|
7678
|
+
const screenshotsDir = path13.join(storedRunResult.artifactsDir, "screenshots");
|
|
7679
|
+
const stdoutLogPath = path13.join(storedRunResult.artifactsDir, "stdout.log");
|
|
7680
|
+
const stderrLogPath = path13.join(storedRunResult.artifactsDir, "stderr.log");
|
|
7438
7681
|
const artifactItems = [];
|
|
7439
|
-
if (
|
|
7682
|
+
if (fs4.existsSync(actionScriptPath)) {
|
|
7440
7683
|
artifactItems.push("- `action-script.json` \u2014 generated test steps");
|
|
7441
7684
|
}
|
|
7442
|
-
if (
|
|
7685
|
+
if (fs4.existsSync(resultsMdPath)) {
|
|
7443
7686
|
artifactItems.push("- `results.md` \u2014 step-by-step report with screenshot links");
|
|
7444
7687
|
}
|
|
7445
|
-
if (
|
|
7446
|
-
const screenshots =
|
|
7688
|
+
if (fs4.existsSync(screenshotsDir)) {
|
|
7689
|
+
const screenshots = fs4.readdirSync(screenshotsDir).filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f));
|
|
7447
7690
|
artifactItems.push(`- \`screenshots/\` \u2014 ${screenshots.length} image(s)`);
|
|
7448
7691
|
}
|
|
7449
|
-
if (
|
|
7692
|
+
if (fs4.existsSync(stdoutLogPath)) {
|
|
7450
7693
|
artifactItems.push("- `stdout.log` \u2014 electron-app stdout output");
|
|
7451
7694
|
}
|
|
7452
|
-
if (
|
|
7695
|
+
if (fs4.existsSync(stderrLogPath)) {
|
|
7453
7696
|
artifactItems.push("- `stderr.log` \u2014 electron-app stderr output");
|
|
7454
7697
|
}
|
|
7455
7698
|
if (artifactItems.length > 0) {
|
|
@@ -7798,6 +8041,7 @@ var allLocalQaTools = [
|
|
|
7798
8041
|
// Run result tools
|
|
7799
8042
|
runResultListTool,
|
|
7800
8043
|
runResultGetTool,
|
|
8044
|
+
runStepsGetTool,
|
|
7801
8045
|
// Test script tools (read-only)
|
|
7802
8046
|
testScriptListTool,
|
|
7803
8047
|
testScriptGetTool,
|
|
@@ -8032,6 +8276,7 @@ __export(local_exports2, {
|
|
|
8032
8276
|
RunResultGetInputSchema: () => RunResultGetInputSchema,
|
|
8033
8277
|
RunResultListInputSchema: () => RunResultListInputSchema,
|
|
8034
8278
|
RunResultStorageService: () => RunResultStorageService,
|
|
8279
|
+
RunStepsGetInputSchema: () => RunStepsGetInputSchema,
|
|
8035
8280
|
SessionStatus: () => SessionStatus,
|
|
8036
8281
|
SkillTelemetryEmitInputSchema: () => SkillTelemetryEmitInputSchema,
|
|
8037
8282
|
StorageService: () => StorageService,
|
|
@@ -8355,4 +8600,4 @@ function applyOnboardingAnswers(answers, dataDirOverride2, now = /* @__PURE__ */
|
|
|
8355
8600
|
return { isCompleted: true, preferences: prefs };
|
|
8356
8601
|
}
|
|
8357
8602
|
|
|
8358
|
-
export { DEFAULT_PREFERENCES, ElectronAppReleaseStream, EventName, ONBOARDING_BLANKET_OPTIONS, ONBOARDING_CHOICE_GROUPS, ONBOARDING_MAX_OFFERS, ONBOARDING_PRIMER_BULLETS, ONBOARDING_PRIMER_HEADLINE, ONBOARDING_TOGGLE_GROUPS, ONBOARDING_TOGGLE_PROMPT, ONBOARDING_UNSELECTED_TOGGLE_VALUE, OnboardingBlanketChoice, OnboardingGroupKind, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RuntimeTarget, ServiceName, Surface, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, __export, __require, applyOnboardingAnswers, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, buildOnboardingPlan, calculateFileChecksum, completeOnboarding, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports2 as e2e_exports, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir2 as getDataDir, getDisclosureCopy, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppSignedFromVersion, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getOnboardingToggleKeys, getPlatformKey, getQaTools, getReleaseSignerIdentityUri, getValidApiKeyData, getValidCredentials, hasApiKey, hasShownDisclosure, initTelemetry, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports2 as local_exports, markDisclosureShown, markOnboardingCompleted, mcp_exports, needsOnboarding, openBrowserUrl, performLogin, performLogout, pollDeviceCode, readPreferencesMetadata, reconcileProjectPreferences, recordOnboardingSkip, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveBlanketPreferences, resolveElectronAppPathOrNull, resolveOnboardingAnswers, resolvePreferences, resolveRuntimeTarget, resolveTogglePreferences, saveApiKey, saveApiKeyData, saveCredentials, src_exports, startDeviceCodeFlow, toolRequiresAuth, track, validatePreference, verifyFileChecksum, writePreferences2 as writePreferences, writePreferencesWithMetadata };
|
|
8603
|
+
export { ACTION_SCRIPT_FILE_NAME, DEFAULT_PREFERENCES, ElectronAppReleaseStream, EventName, HTML_VIEW_OFFER_LINE, ONBOARDING_BLANKET_OPTIONS, ONBOARDING_CHOICE_GROUPS, ONBOARDING_MAX_OFFERS, ONBOARDING_PRIMER_BULLETS, ONBOARDING_PRIMER_HEADLINE, ONBOARDING_TOGGLE_GROUPS, ONBOARDING_TOGGLE_PROMPT, ONBOARDING_UNSELECTED_TOGGLE_VALUE, OnboardingBlanketChoice, OnboardingGroupKind, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, ProjectPreferencesReconcileOutcome, RUN_SESSIONS_DIR_NAME, RunViewError, RuntimeTarget, SCREENSHOT_DIR_RELATIVE_PATH, SCREENSHOT_FILE_EXTENSION, ServiceName, Surface, WATCHER_LIFETIME_SECONDS, WATCHER_LIFETIME_UNBOUNDED_SECONDS, __export, __require, applyOnboardingAnswers, assertDeviceCodeClientProvisioned, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, buildOnboardingPlan, calculateFileChecksum, completeOnboarding, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports2 as e2e_exports, formatPreferencesOneLiner, getActiveElectronAppReleaseStream, getActiveRuntimeTarget, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir2 as getDataDir, getDisclosureCopy, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppReleaseTagPrefix, getElectronAppSignedFromVersion, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getOnboardingToggleKeys, getPlatformKey, getQaTools, getReleaseSignerIdentityUri, getRunSessionsDir, getValidApiKeyData, getValidCredentials, hasApiKey, hasShownDisclosure, initTelemetry, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports2 as local_exports, markDisclosureShown, markOnboardingCompleted, mcp_exports, needsOnboarding, openBrowserUrl, performLogin, performLogout, pollDeviceCode, readPreferencesMetadata, readRunView, reconcileProjectPreferences, recordOnboardingSkip, renderRunViewHtml, renderRunViewText, resetConfig, resetLogger, resetPreference, resolveActiveProfile, resolveActiveReleaseStream, resolveActiveReleaseTagPrefix, resolveBlanketPreferences, resolveElectronAppPathOrNull, resolveOnboardingAnswers, resolvePreferences, resolveRunSessionPath, resolveRuntimeTarget, resolveTogglePreferences, saveApiKey, saveApiKeyData, saveCredentials, src_exports, startDeviceCodeFlow, toolRequiresAuth, track, validatePreference, verifyFileChecksum, writePreferences2 as writePreferences, writePreferencesWithMetadata, writeRunViewHtml };
|