libretto 0.5.0 → 0.5.2
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 +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -3,142 +3,146 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { LoggerSink } from "./logger.js";
|
|
4
4
|
|
|
5
5
|
export function createFileLogSink({
|
|
6
|
-
|
|
6
|
+
filePath,
|
|
7
7
|
}: {
|
|
8
|
-
|
|
8
|
+
filePath: string;
|
|
9
9
|
}): LoggerSink {
|
|
10
|
-
|
|
10
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const writeStream = fs.createWriteStream(filePath, { flags: "a" });
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
return {
|
|
15
|
+
write: ({ id, scope, level, event, data, options }) => {
|
|
16
|
+
if (writeStream.destroyed || writeStream.writableEnded) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const timestamp = options?.timestamp || new Date();
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
const logEntry = {
|
|
22
|
+
timestamp: timestamp.toISOString(),
|
|
23
|
+
id,
|
|
24
|
+
level,
|
|
25
|
+
scope,
|
|
26
|
+
event,
|
|
27
|
+
data,
|
|
28
|
+
};
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const jsonLine = JSON.stringify(logEntry) + "\n";
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
32
|
+
try {
|
|
33
|
+
writeStream.write(jsonLine, (error) => {
|
|
34
|
+
if (error) {
|
|
35
|
+
console.error("Failed to write to log file:", error);
|
|
36
|
+
console[level]({ id, scope, event, data, timestamp });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("Failed to write to log file:", error);
|
|
41
|
+
console[level]({ id, scope, event, data, timestamp });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
flush: () =>
|
|
45
|
+
new Promise<void>((resolve, reject) => {
|
|
46
|
+
if (
|
|
47
|
+
!writeStream.writable ||
|
|
48
|
+
writeStream.writableEnded ||
|
|
49
|
+
writeStream.destroyed
|
|
50
|
+
) {
|
|
51
|
+
resolve();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
writeStream.write("", (error) => {
|
|
55
|
+
if (error) {
|
|
56
|
+
reject(error);
|
|
57
|
+
} else {
|
|
58
|
+
resolve();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
close: () =>
|
|
63
|
+
new Promise<void>((resolve) => {
|
|
64
|
+
if (writeStream.destroyed || writeStream.closed) {
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let settled = false;
|
|
69
|
+
const done = () => {
|
|
70
|
+
if (settled) return;
|
|
71
|
+
settled = true;
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
writeStream.once("finish", done);
|
|
75
|
+
writeStream.once("close", done);
|
|
76
|
+
writeStream.once("error", done);
|
|
77
|
+
try {
|
|
78
|
+
writeStream.end();
|
|
79
|
+
} catch {
|
|
80
|
+
done();
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
// ANSI color codes
|
|
83
87
|
const colors = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
reset: "\x1b[0m",
|
|
89
|
+
gray: "\x1b[90m",
|
|
90
|
+
red: "\x1b[31m",
|
|
91
|
+
yellow: "\x1b[33m",
|
|
92
|
+
blue: "\x1b[34m",
|
|
93
|
+
cyan: "\x1b[36m",
|
|
90
94
|
};
|
|
91
95
|
|
|
92
96
|
function formatTimestamp(date: Date): string {
|
|
93
|
-
|
|
97
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
export const prettyConsoleSink: LoggerSink = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
write: ({ scope, level, event, data, options }) => {
|
|
102
|
+
const timestamp = `${colors.gray}${formatTimestamp(options?.timestamp || new Date())}${colors.reset}`;
|
|
103
|
+
const levelColor =
|
|
104
|
+
level === "error"
|
|
105
|
+
? colors.red
|
|
106
|
+
: level === "warn"
|
|
107
|
+
? colors.yellow
|
|
108
|
+
: colors.blue;
|
|
109
|
+
const coloredScope = scope ? `${colors.cyan}[${scope}]${colors.reset}` : "";
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
const logPrefix = `${timestamp} ${levelColor}${level.toUpperCase()}${colors.reset} ${coloredScope} ${event}`;
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
113
|
+
if (level === "error" && data.error) {
|
|
114
|
+
const { error, ...otherData } = data;
|
|
115
|
+
console.error(logPrefix);
|
|
116
|
+
if (error.stack) {
|
|
117
|
+
console.error(` ${error.stack}`);
|
|
118
|
+
} else if (error.type && error.message) {
|
|
119
|
+
console.error(` ${error.type}: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
if (Object.keys(otherData).length > 0) {
|
|
122
|
+
console.error(JSON.stringify(otherData, null, 2));
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
console[level](logPrefix);
|
|
126
|
+
if (Object.keys(data).length > 0) {
|
|
127
|
+
console[level](JSON.stringify(data, null, 2));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
127
131
|
};
|
|
128
132
|
|
|
129
133
|
export const jsonlConsoleSink: LoggerSink = {
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
write: ({ id, scope, level, event, data, options }) => {
|
|
135
|
+
const timestamp = options?.timestamp || new Date();
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
const logEntry = {
|
|
138
|
+
timestamp: timestamp.toISOString(),
|
|
139
|
+
id,
|
|
140
|
+
level,
|
|
141
|
+
scope: scope || undefined,
|
|
142
|
+
event,
|
|
143
|
+
data,
|
|
144
|
+
};
|
|
141
145
|
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
console.log(JSON.stringify(logEntry));
|
|
147
|
+
},
|
|
144
148
|
};
|
|
@@ -11,99 +11,100 @@ const PAUSED_SIGNAL_SUFFIX = "paused";
|
|
|
11
11
|
const RESUME_SIGNAL_SUFFIX = "resume";
|
|
12
12
|
|
|
13
13
|
function getLibrettoRoot(cwd: string = process.cwd()): string {
|
|
14
|
-
|
|
14
|
+
return join(resolveLibrettoRepoRoot(cwd), LIBRETTO_DIRNAME);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function getLibrettoSessionsDir(cwd: string = process.cwd()): string {
|
|
18
|
-
|
|
18
|
+
return join(getLibrettoRoot(cwd), LIBRETTO_SESSIONS_DIRNAME);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function getLibrettoSessionDir(
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
sessionName: string,
|
|
23
|
+
cwd: string = process.cwd(),
|
|
24
24
|
): string {
|
|
25
|
-
|
|
25
|
+
return join(getLibrettoSessionsDir(cwd), sessionName);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function getLibrettoSessionStatePath(
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
sessionName: string,
|
|
30
|
+
cwd: string = process.cwd(),
|
|
31
31
|
): string {
|
|
32
|
-
|
|
32
|
+
return join(getLibrettoSessionDir(sessionName, cwd), SESSION_STATE_FILENAME);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function getLibrettoPauseSignalDir(
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
sessionName: string,
|
|
37
|
+
cwd: string = process.cwd(),
|
|
38
38
|
): string {
|
|
39
|
-
|
|
39
|
+
return getLibrettoSessionDir(sessionName, cwd);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function getLibrettoRunnerLogDir(
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
sessionName: string,
|
|
44
|
+
cwd: string = process.cwd(),
|
|
45
45
|
): string {
|
|
46
|
-
|
|
46
|
+
return join(getLibrettoSessionDir(sessionName, cwd), RUNNER_LOG_DIRNAME);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function getRunnerLogPathForDir(logDir: string): string {
|
|
50
|
-
|
|
50
|
+
return join(logDir, RUNNER_LOG_FILENAME);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function getPauseSignalPathForDir(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
signalDir: string,
|
|
55
|
+
sessionName: string,
|
|
56
|
+
signal: "paused" | "resume",
|
|
57
57
|
): string {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const suffix =
|
|
59
|
+
signal === "paused" ? PAUSED_SIGNAL_SUFFIX : RESUME_SIGNAL_SUFFIX;
|
|
60
|
+
return join(signalDir, `${sessionName}.${suffix}`);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function getLibrettoPausedSignalPath(
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
sessionName: string,
|
|
65
|
+
cwd: string = process.cwd(),
|
|
65
66
|
): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
return getPauseSignalPathForDir(
|
|
68
|
+
getLibrettoPauseSignalDir(sessionName, cwd),
|
|
69
|
+
sessionName,
|
|
70
|
+
"paused",
|
|
71
|
+
);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function getLibrettoResumeSignalPath(
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
sessionName: string,
|
|
76
|
+
cwd: string = process.cwd(),
|
|
76
77
|
): string {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
return getPauseSignalPathForDir(
|
|
79
|
+
getLibrettoPauseSignalDir(sessionName, cwd),
|
|
80
|
+
sessionName,
|
|
81
|
+
"resume",
|
|
82
|
+
);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export function ensureLibrettoSessionStatePath(
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
sessionName: string,
|
|
87
|
+
cwd: string = process.cwd(),
|
|
87
88
|
): string {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const filePath = getLibrettoSessionStatePath(sessionName, cwd);
|
|
90
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
91
|
+
return filePath;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export function ensureLibrettoPauseSignalDir(
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
sessionName: string,
|
|
96
|
+
cwd: string = process.cwd(),
|
|
96
97
|
): string {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const dir = getLibrettoPauseSignalDir(sessionName, cwd);
|
|
99
|
+
mkdirSync(dir, { recursive: true });
|
|
100
|
+
return dir;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
export function ensureLibrettoRunnerLogDir(
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
sessionName: string,
|
|
105
|
+
cwd: string = process.cwd(),
|
|
105
106
|
): string {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const dir = getLibrettoRunnerLogDir(sessionName, cwd);
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
109
|
+
return dir;
|
|
109
110
|
}
|
|
@@ -4,24 +4,24 @@ import { resolve } from "node:path";
|
|
|
4
4
|
const repoRootCache = new Map<string, string>();
|
|
5
5
|
|
|
6
6
|
export function resolveLibrettoRepoRoot(cwd: string = process.cwd()): string {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const override = process.env.LIBRETTO_REPO_ROOT?.trim();
|
|
8
|
+
if (override) {
|
|
9
|
+
return resolve(override);
|
|
10
|
+
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const normalizedCwd = resolve(cwd);
|
|
13
|
+
const cached = repoRootCache.get(normalizedCwd);
|
|
14
|
+
if (cached) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
19
|
+
cwd: normalizedCwd,
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
});
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const repoRoot =
|
|
24
|
+
result.status === 0 && result.stdout ? result.stdout.trim() : normalizedCwd;
|
|
25
|
+
repoRootCache.set(normalizedCwd, repoRoot);
|
|
26
|
+
return repoRoot;
|
|
27
27
|
}
|
package/src/shared/run/api.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chromium,
|
|
3
|
+
type Browser,
|
|
4
|
+
type BrowserContext,
|
|
5
|
+
type Page,
|
|
6
|
+
} from "playwright";
|
|
2
7
|
import { createServer } from "node:net";
|
|
3
8
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
9
|
import { ensureLibrettoSessionStatePath } from "../paths/paths.js";
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
SESSION_STATE_VERSION,
|
|
12
|
+
SessionStateFileSchema,
|
|
13
|
+
} from "../state/session-state.js";
|
|
14
|
+
import { readLibrettoConfig } from "../../cli/core/ai-config.js";
|
|
6
15
|
|
|
7
16
|
async function pickFreePort(): Promise<number> {
|
|
8
17
|
return await new Promise((resolve, reject) => {
|
|
@@ -36,6 +45,53 @@ export type BrowserSession = {
|
|
|
36
45
|
close: () => Promise<void>;
|
|
37
46
|
};
|
|
38
47
|
|
|
48
|
+
function resolveWindowPosition(): { x: number; y: number } | undefined {
|
|
49
|
+
return readLibrettoConfig().windowPosition;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function applyWindowPosition(
|
|
53
|
+
browser: Browser,
|
|
54
|
+
context: BrowserContext,
|
|
55
|
+
page: Page,
|
|
56
|
+
windowPosition: { x: number; y: number } | undefined,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
if (!windowPosition) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const requestedBounds = {
|
|
63
|
+
left: windowPosition.x,
|
|
64
|
+
top: windowPosition.y,
|
|
65
|
+
windowState: "normal" as const,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const pageCdp = await context.newCDPSession(page);
|
|
69
|
+
let browserCdp:
|
|
70
|
+
| Awaited<ReturnType<Browser["newBrowserCDPSession"]>>
|
|
71
|
+
| undefined;
|
|
72
|
+
try {
|
|
73
|
+
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
74
|
+
const targetId = (
|
|
75
|
+
targetInfo as { targetInfo?: { targetId?: string } }
|
|
76
|
+
).targetInfo?.targetId;
|
|
77
|
+
browserCdp = await browser.newBrowserCDPSession();
|
|
78
|
+
const windowResult = await browserCdp.send(
|
|
79
|
+
"Browser.getWindowForTarget",
|
|
80
|
+
targetId ? { targetId } : {},
|
|
81
|
+
);
|
|
82
|
+
await browserCdp.send("Browser.setWindowBounds", {
|
|
83
|
+
windowId: windowResult.windowId,
|
|
84
|
+
bounds: requestedBounds,
|
|
85
|
+
});
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
87
|
+
} catch {
|
|
88
|
+
// Best-effort: window positioning should not prevent browser launch.
|
|
89
|
+
} finally {
|
|
90
|
+
await pageCdp.detach().catch(() => {});
|
|
91
|
+
await browserCdp?.detach().catch(() => {});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
39
95
|
export async function launchBrowser({
|
|
40
96
|
sessionName,
|
|
41
97
|
headless = false,
|
|
@@ -43,12 +99,16 @@ export async function launchBrowser({
|
|
|
43
99
|
storageStatePath,
|
|
44
100
|
}: LaunchBrowserArgs): Promise<BrowserSession> {
|
|
45
101
|
const debugPort = await pickFreePort();
|
|
102
|
+
const windowPosition = headless ? undefined : resolveWindowPosition();
|
|
46
103
|
const browser = await chromium.launch({
|
|
47
104
|
headless,
|
|
48
105
|
args: [
|
|
49
106
|
"--disable-blink-features=AutomationControlled",
|
|
50
107
|
`--remote-debugging-port=${debugPort}`,
|
|
51
108
|
"--no-focus-on-check",
|
|
109
|
+
...(windowPosition
|
|
110
|
+
? [`--window-position=${windowPosition.x},${windowPosition.y}`]
|
|
111
|
+
: []),
|
|
52
112
|
],
|
|
53
113
|
});
|
|
54
114
|
|
|
@@ -57,6 +117,7 @@ export async function launchBrowser({
|
|
|
57
117
|
...(storageStatePath ? { storageState: storageStatePath } : {}),
|
|
58
118
|
});
|
|
59
119
|
const page = await context.newPage();
|
|
120
|
+
await applyWindowPosition(browser, context, page, windowPosition);
|
|
60
121
|
page.setDefaultTimeout(30_000);
|
|
61
122
|
page.setDefaultNavigationTimeout(45_000);
|
|
62
123
|
|
|
@@ -65,7 +126,8 @@ export async function launchBrowser({
|
|
|
65
126
|
? (JSON.parse(readFileSync(metadataPath, "utf-8")) as unknown)
|
|
66
127
|
: undefined;
|
|
67
128
|
|
|
68
|
-
const parsedExistingState =
|
|
129
|
+
const parsedExistingState =
|
|
130
|
+
SessionStateFileSchema.safeParse(existingStateRaw);
|
|
69
131
|
|
|
70
132
|
writeFileSync(
|
|
71
133
|
metadataPath,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
SESSION_STATE_VERSION,
|
|
3
|
+
SessionStatusSchema,
|
|
4
|
+
SessionStateFileSchema,
|
|
5
|
+
parseSessionStateData,
|
|
6
|
+
parseSessionStateContent,
|
|
7
|
+
serializeSessionState,
|
|
8
|
+
type SessionStatus,
|
|
9
|
+
type SessionState,
|
|
10
|
+
type SessionStateFile,
|
|
11
11
|
} from "./session-state.js";
|