libretto 0.6.11 → 0.6.13
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 +7 -8
- package/README.template.md +7 -8
- package/dist/cli/cli.js +0 -22
- package/dist/cli/commands/browser.js +18 -24
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +3 -310
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +46 -64
- package/dist/cli/commands/status.js +1 -40
- package/dist/cli/core/browser.js +303 -124
- package/dist/cli/core/config.js +5 -6
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +497 -90
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +48 -9
- package/dist/cli/core/experiments.js +39 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/index.js +0 -2
- package/dist/cli/router.js +4 -3
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/instrumentation/instrument.js +4 -4
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/docs/releasing.md +8 -6
- package/package.json +5 -2
- package/skills/libretto/SKILL.md +19 -19
- package/skills/libretto/references/configuration-file-reference.md +6 -12
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto-readonly/SKILL.md +2 -9
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +0 -23
- package/src/cli/commands/browser.ts +14 -18
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +3 -400
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +54 -94
- package/src/cli/commands/status.ts +1 -48
- package/src/cli/core/browser.ts +372 -150
- package/src/cli/core/config.ts +4 -5
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +645 -107
- package/src/cli/core/daemon/ipc.ts +319 -214
- package/src/cli/core/daemon/snapshot.ts +71 -15
- package/src/cli/core/experiments.ts +56 -0
- package/src/cli/core/resolve-model.ts +5 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +86 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/router.ts +4 -3
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/instrumentation/instrument.ts +4 -4
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/commands/ai.js +0 -109
- package/dist/cli/core/ai-model.js +0 -192
- package/dist/cli/core/api-snapshot-analyzer.js +0 -86
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/core/snapshot-analyzer.js +0 -666
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/scripts/summarize-evals.mjs +0 -135
- package/src/cli/commands/ai.ts +0 -143
- package/src/cli/core/ai-model.ts +0 -298
- package/src/cli/core/api-snapshot-analyzer.ts +0 -110
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/core/snapshot-analyzer.ts +0 -855
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { openSync, closeSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createIpcPeer } from "../../../shared/ipc/ipc.js";
|
|
7
|
+
import { connectToIpcSocket } from "../../../shared/ipc/socket-transport.js";
|
|
4
8
|
import { REPO_ROOT } from "../context.js";
|
|
9
|
+
function createNoopDaemonToCliHandlers() {
|
|
10
|
+
return {
|
|
11
|
+
workflowOutput: () => {
|
|
12
|
+
},
|
|
13
|
+
workflowPaused: () => {
|
|
14
|
+
},
|
|
15
|
+
workflowFinished: () => {
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
5
19
|
class DaemonClientError extends Error {
|
|
6
20
|
constructor(message, output) {
|
|
7
21
|
super(message);
|
|
@@ -9,163 +23,190 @@ class DaemonClientError extends Error {
|
|
|
9
23
|
this.name = "DaemonClientError";
|
|
10
24
|
}
|
|
11
25
|
}
|
|
26
|
+
function isDaemonReadyMessage(message) {
|
|
27
|
+
if (typeof message !== "object" || message === null) return false;
|
|
28
|
+
const candidate = message;
|
|
29
|
+
return candidate.type === "ready" && typeof candidate.socketPath === "string";
|
|
30
|
+
}
|
|
31
|
+
function isDaemonStartupErrorMessage(message) {
|
|
32
|
+
if (typeof message !== "object" || message === null) return false;
|
|
33
|
+
const candidate = message;
|
|
34
|
+
return candidate.type === "startup-error" && typeof candidate.message === "string";
|
|
35
|
+
}
|
|
12
36
|
function getDaemonSocketPath(session) {
|
|
13
37
|
const hash = createHash("sha256").update(`${REPO_ROOT}:${session}`).digest("hex").slice(0, 12);
|
|
14
38
|
return `/tmp/libretto-${process.getuid()}-${hash}.sock`;
|
|
15
39
|
}
|
|
16
|
-
class
|
|
17
|
-
constructor(
|
|
18
|
-
this.
|
|
19
|
-
this.handler = handler;
|
|
40
|
+
class DaemonClient {
|
|
41
|
+
constructor(ipc) {
|
|
42
|
+
this.ipc = ipc;
|
|
20
43
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (err.code !== "ENOENT") throw err;
|
|
27
|
-
}
|
|
28
|
-
const server = createServer((socket) => {
|
|
29
|
-
let buffer = "";
|
|
30
|
-
socket.on("data", (chunk) => {
|
|
31
|
-
buffer += chunk.toString();
|
|
32
|
-
const newlineIndex = buffer.indexOf("\n");
|
|
33
|
-
if (newlineIndex === -1) return;
|
|
34
|
-
const line = buffer.slice(0, newlineIndex);
|
|
35
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
36
|
-
void (async () => {
|
|
37
|
-
let response;
|
|
38
|
-
try {
|
|
39
|
-
const request = JSON.parse(line);
|
|
40
|
-
const data = await this.handler(request);
|
|
41
|
-
response = { id: request.id, type: "result", data };
|
|
42
|
-
} catch (err) {
|
|
43
|
-
const id = (() => {
|
|
44
|
-
try {
|
|
45
|
-
return JSON.parse(line).id ?? "unknown";
|
|
46
|
-
} catch {
|
|
47
|
-
return "unknown";
|
|
48
|
-
}
|
|
49
|
-
})();
|
|
50
|
-
response = {
|
|
51
|
-
id,
|
|
52
|
-
type: "error",
|
|
53
|
-
message: err instanceof Error ? err.message : String(err),
|
|
54
|
-
output: err instanceof Error ? err.output : void 0
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
socket.end(JSON.stringify(response) + "\n");
|
|
58
|
-
})();
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
this.server = server;
|
|
62
|
-
await new Promise((resolve, reject) => {
|
|
63
|
-
server.on("error", reject);
|
|
64
|
-
server.listen(this.socketPath, () => resolve());
|
|
65
|
-
});
|
|
44
|
+
static async connect(socketPath, handlers = createNoopDaemonToCliHandlers()) {
|
|
45
|
+
const transport = await connectToIpcSocket(socketPath);
|
|
46
|
+
return new DaemonClient(
|
|
47
|
+
createIpcPeer(transport, handlers)
|
|
48
|
+
);
|
|
66
49
|
}
|
|
67
|
-
async
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
50
|
+
static async spawn(options) {
|
|
51
|
+
const { config, logger, logPath, startupTimeoutMs, handlers, onFailure } = options;
|
|
52
|
+
const { session } = config;
|
|
53
|
+
const daemonEntryPath = fileURLToPath(
|
|
54
|
+
new URL("./daemon.js", import.meta.url)
|
|
55
|
+
);
|
|
56
|
+
const require2 = createRequire(import.meta.url);
|
|
57
|
+
const tsxCliPath = require2.resolve("tsx/cli");
|
|
58
|
+
const childStderrFd = openSync(logPath, "a");
|
|
59
|
+
const child = spawn(
|
|
60
|
+
process.execPath,
|
|
61
|
+
[
|
|
62
|
+
tsxCliPath,
|
|
63
|
+
...config.workflow?.tsconfigPath ? ["--tsconfig", config.workflow.tsconfigPath] : [],
|
|
64
|
+
daemonEntryPath,
|
|
65
|
+
JSON.stringify(config)
|
|
66
|
+
],
|
|
67
|
+
{
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: ["ignore", "ignore", childStderrFd, "ipc"]
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
closeSync(childStderrFd);
|
|
73
|
+
const pid = child.pid;
|
|
74
|
+
logger.info("daemon-spawned", { pid, session });
|
|
75
|
+
const readyMessage = await DaemonClient.waitForReadyMessage({
|
|
76
|
+
child,
|
|
77
|
+
timeoutMs: startupTimeoutMs,
|
|
78
|
+
formatTimeoutError: () => new Error(
|
|
79
|
+
`Daemon failed to start within ${Math.ceil(startupTimeoutMs / 1e3)}s. Check logs: ${logPath}`
|
|
80
|
+
),
|
|
81
|
+
formatSpawnError: (error) => {
|
|
82
|
+
const errWithCode = error;
|
|
83
|
+
const hint = errWithCode.code === "ENOENT" ? " Ensure Node.js is available in PATH for child processes." : "";
|
|
84
|
+
return new Error(
|
|
85
|
+
`Failed to spawn daemon: ${error.message}.${hint} Check logs: ${logPath}`
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
formatExitError: (code, signal) => {
|
|
89
|
+
const status = code ?? signal ?? "unknown";
|
|
90
|
+
return new Error(
|
|
91
|
+
`Daemon exited before startup (status: ${status}). Check logs: ${logPath}`
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
onReady: (message) => {
|
|
95
|
+
logger.info("daemon-ready", {
|
|
96
|
+
session,
|
|
97
|
+
socketPath: message.socketPath,
|
|
98
|
+
pid
|
|
99
|
+
});
|
|
100
|
+
child.disconnect();
|
|
101
|
+
child.unref();
|
|
102
|
+
},
|
|
103
|
+
onSpawnError: (error) => {
|
|
104
|
+
logger.error("daemon-spawn-error", { error, session });
|
|
105
|
+
},
|
|
106
|
+
onExit: (code, signal, ready) => {
|
|
107
|
+
logger.warn("daemon-exit", { code, signal, session, pid, ready });
|
|
108
|
+
}
|
|
109
|
+
}).catch(async (error) => {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, "SIGTERM");
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
await onFailure?.();
|
|
115
|
+
throw error;
|
|
73
116
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
class DaemonClient {
|
|
82
|
-
constructor(socketPath) {
|
|
83
|
-
this.socketPath = socketPath;
|
|
117
|
+
const client = await DaemonClient.connect(
|
|
118
|
+
readyMessage.socketPath,
|
|
119
|
+
handlers
|
|
120
|
+
);
|
|
121
|
+
const socketPath = readyMessage.socketPath;
|
|
122
|
+
logger.info("daemon-ipc-ready", { session, socketPath });
|
|
123
|
+
return { pid, socketPath, provider: readyMessage.provider, client };
|
|
84
124
|
}
|
|
85
|
-
async
|
|
125
|
+
static async waitForReadyMessage(args) {
|
|
126
|
+
const {
|
|
127
|
+
child,
|
|
128
|
+
timeoutMs,
|
|
129
|
+
formatTimeoutError,
|
|
130
|
+
formatSpawnError,
|
|
131
|
+
formatExitError,
|
|
132
|
+
onReady,
|
|
133
|
+
onSpawnError,
|
|
134
|
+
onExit
|
|
135
|
+
} = args;
|
|
86
136
|
return new Promise((resolve, reject) => {
|
|
87
|
-
|
|
88
|
-
let
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
137
|
+
let ready = false;
|
|
138
|
+
let timeout;
|
|
139
|
+
const cleanup = () => {
|
|
140
|
+
clearTimeout(timeout);
|
|
141
|
+
child.off("message", onMessage);
|
|
142
|
+
child.off("error", onError);
|
|
143
|
+
child.off("exit", onChildExit);
|
|
144
|
+
};
|
|
145
|
+
const fail = (error) => {
|
|
146
|
+
cleanup();
|
|
147
|
+
reject(error);
|
|
148
|
+
};
|
|
149
|
+
timeout = setTimeout(() => fail(formatTimeoutError()), timeoutMs);
|
|
150
|
+
const onMessage = (message) => {
|
|
151
|
+
if (isDaemonStartupErrorMessage(message)) {
|
|
152
|
+
fail(new Error(message.message));
|
|
153
|
+
return;
|
|
105
154
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
generateId() {
|
|
113
|
-
return Math.random().toString(36).slice(2, 10);
|
|
114
|
-
}
|
|
115
|
-
async sendOrThrow(request) {
|
|
116
|
-
const response = await this.send(request);
|
|
117
|
-
if (response.type === "error") {
|
|
118
|
-
throw new DaemonClientError(response.message, response.output);
|
|
119
|
-
}
|
|
120
|
-
return response.data;
|
|
121
|
-
}
|
|
122
|
-
async sendResult(request) {
|
|
123
|
-
const response = await this.send(request);
|
|
124
|
-
if (response.type === "error") {
|
|
125
|
-
return {
|
|
126
|
-
ok: false,
|
|
127
|
-
message: response.message,
|
|
128
|
-
output: response.output
|
|
155
|
+
if (!isDaemonReadyMessage(message)) return;
|
|
156
|
+
ready = true;
|
|
157
|
+
cleanup();
|
|
158
|
+
onReady?.(message);
|
|
159
|
+
resolve(message);
|
|
129
160
|
};
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
const onError = (error) => {
|
|
162
|
+
onSpawnError?.(error);
|
|
163
|
+
fail(formatSpawnError(error));
|
|
164
|
+
};
|
|
165
|
+
const onChildExit = (code, signal) => {
|
|
166
|
+
onExit?.(code, signal, ready);
|
|
167
|
+
if (ready) return;
|
|
168
|
+
fail(formatExitError(code, signal));
|
|
169
|
+
};
|
|
170
|
+
child.on("message", onMessage);
|
|
171
|
+
child.on("error", onError);
|
|
172
|
+
child.on("exit", onChildExit);
|
|
173
|
+
});
|
|
132
174
|
}
|
|
133
175
|
async ping() {
|
|
134
176
|
try {
|
|
135
|
-
await this.
|
|
177
|
+
await this.ipc.call.ping();
|
|
136
178
|
return true;
|
|
137
179
|
} catch {
|
|
138
180
|
return false;
|
|
139
181
|
}
|
|
140
182
|
}
|
|
141
183
|
async pages() {
|
|
142
|
-
return this.
|
|
184
|
+
return this.ipc.call.pages();
|
|
143
185
|
}
|
|
144
186
|
async exec(args) {
|
|
145
|
-
return this.
|
|
146
|
-
id: this.generateId(),
|
|
147
|
-
command: "exec",
|
|
148
|
-
...args
|
|
149
|
-
});
|
|
187
|
+
return this.ipc.call.exec(args);
|
|
150
188
|
}
|
|
151
189
|
async readonlyExec(args) {
|
|
152
|
-
return this.
|
|
153
|
-
id: this.generateId(),
|
|
154
|
-
command: "readonly-exec",
|
|
155
|
-
...args
|
|
156
|
-
});
|
|
190
|
+
return this.ipc.call.readonlyExec(args);
|
|
157
191
|
}
|
|
158
192
|
async snapshot(args = {}) {
|
|
159
|
-
return this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
193
|
+
return this.ipc.call.snapshot(args);
|
|
194
|
+
}
|
|
195
|
+
async getWorkflowStatus() {
|
|
196
|
+
return this.ipc.call.getWorkflowStatus();
|
|
197
|
+
}
|
|
198
|
+
async resumeWorkflow() {
|
|
199
|
+
await this.ipc.call.resumeWorkflow();
|
|
200
|
+
}
|
|
201
|
+
async close() {
|
|
202
|
+
return this.ipc.call.close();
|
|
203
|
+
}
|
|
204
|
+
destroy() {
|
|
205
|
+
this.ipc.destroy();
|
|
164
206
|
}
|
|
165
207
|
}
|
|
166
208
|
export {
|
|
167
209
|
DaemonClient,
|
|
168
210
|
DaemonClientError,
|
|
169
|
-
DaemonServer,
|
|
170
211
|
getDaemonSocketPath
|
|
171
212
|
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { mkdirSync
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { getSessionSnapshotRunDir } from "../context.js";
|
|
3
|
+
import {
|
|
4
|
+
snapshot
|
|
5
|
+
} from "../../../shared/snapshot/capture-snapshot.js";
|
|
6
|
+
import { waitForPageStable } from "../../../shared/snapshot/wait-for-page-stable.js";
|
|
7
|
+
import { librettoCommand } from "../../../shared/package-manager.js";
|
|
3
8
|
import {
|
|
4
9
|
resolveSnapshotViewport,
|
|
5
10
|
readSnapshotViewportMetrics,
|
|
@@ -8,7 +13,46 @@ import {
|
|
|
8
13
|
forceSnapshotViewport
|
|
9
14
|
} from "../../commands/snapshot.js";
|
|
10
15
|
const RENDER_SETTLE_TIMEOUT_MS = 1e4;
|
|
11
|
-
async function
|
|
16
|
+
async function handleCompactSnapshot(targetPage, session, logger, options = {}) {
|
|
17
|
+
if (options.useCachedSnapshot) {
|
|
18
|
+
if (!options.cachedSnapshot) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`No compact snapshot is cached for session "${session}". Run ${librettoCommand(`snapshot --session ${session}`)} first.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const screenshot2 = await captureSnapshotScreenshot(
|
|
24
|
+
targetPage,
|
|
25
|
+
session,
|
|
26
|
+
logger,
|
|
27
|
+
options.pageId
|
|
28
|
+
);
|
|
29
|
+
return {
|
|
30
|
+
mode: "compact",
|
|
31
|
+
pngPath: screenshot2.pngPath,
|
|
32
|
+
snapshot: options.cachedSnapshot
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const waitResult = await waitForPageStable(targetPage);
|
|
36
|
+
if (!waitResult.ok) {
|
|
37
|
+
logger.warn("compact-snapshot-stability-wait-incomplete", {
|
|
38
|
+
session,
|
|
39
|
+
pageId: options.pageId,
|
|
40
|
+
diagnostics: waitResult.diagnostics
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const screenshot = await captureSnapshotScreenshot(
|
|
44
|
+
targetPage,
|
|
45
|
+
session,
|
|
46
|
+
logger,
|
|
47
|
+
options.pageId
|
|
48
|
+
);
|
|
49
|
+
return {
|
|
50
|
+
mode: "compact",
|
|
51
|
+
pngPath: screenshot.pngPath,
|
|
52
|
+
snapshot: await snapshot(targetPage)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function captureSnapshotScreenshot(targetPage, session, logger, pageId) {
|
|
12
56
|
const snapshotRunId = `snapshot-${Date.now()}`;
|
|
13
57
|
const snapshotRunDir = getSessionSnapshotRunDir(session, snapshotRunId);
|
|
14
58
|
mkdirSync(snapshotRunDir, { recursive: true });
|
|
@@ -25,7 +69,6 @@ async function handleSnapshot(targetPage, session, logger, pageId) {
|
|
|
25
69
|
logger.warn("screenshot-url-read-failed", { session, pageId, error });
|
|
26
70
|
}
|
|
27
71
|
const pngPath = `${snapshotRunDir}/page.png`;
|
|
28
|
-
const htmlPath = `${snapshotRunDir}/page.html`;
|
|
29
72
|
await Promise.race([
|
|
30
73
|
targetPage.waitForLoadState("networkidle").catch(() => {
|
|
31
74
|
}),
|
|
@@ -63,24 +106,20 @@ async function handleSnapshot(targetPage, session, logger, pageId) {
|
|
|
63
106
|
);
|
|
64
107
|
await targetPage.screenshot({ path: pngPath });
|
|
65
108
|
}
|
|
66
|
-
|
|
67
|
-
writeFileSync(htmlPath, htmlContent);
|
|
68
|
-
logger.info("screenshot-success", {
|
|
109
|
+
logger.info("screenshot-captured", {
|
|
69
110
|
session,
|
|
70
111
|
pageUrl,
|
|
71
112
|
title,
|
|
72
113
|
pngPath,
|
|
73
|
-
htmlPath,
|
|
74
114
|
snapshotRunId
|
|
75
115
|
});
|
|
76
116
|
return {
|
|
77
117
|
pngPath,
|
|
78
|
-
htmlPath,
|
|
79
118
|
snapshotRunId,
|
|
80
119
|
pageUrl: pageUrl ?? "",
|
|
81
120
|
title: title ?? ""
|
|
82
121
|
};
|
|
83
122
|
}
|
|
84
123
|
export {
|
|
85
|
-
|
|
124
|
+
handleCompactSnapshot
|
|
86
125
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readLibrettoConfig,
|
|
3
|
+
writeLibrettoConfig
|
|
4
|
+
} from "./config.js";
|
|
5
|
+
const EXPERIMENTS = {};
|
|
6
|
+
function isExperimentName(name) {
|
|
7
|
+
return Object.hasOwn(EXPERIMENTS, name);
|
|
8
|
+
}
|
|
9
|
+
function resolveExperiments(config = readLibrettoConfig()) {
|
|
10
|
+
return Object.fromEntries(
|
|
11
|
+
Object.entries(EXPERIMENTS).map(([name, metadata]) => [
|
|
12
|
+
name,
|
|
13
|
+
config.experiments?.[name] ?? metadata.defaultValue
|
|
14
|
+
])
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function setExperimentEnabled(name, enabled, configPath) {
|
|
18
|
+
if (!isExperimentName(name)) {
|
|
19
|
+
throw new Error(`Unknown experiment "${name}".`);
|
|
20
|
+
}
|
|
21
|
+
const config = readLibrettoConfig(configPath);
|
|
22
|
+
const writtenConfig = writeLibrettoConfig(
|
|
23
|
+
{
|
|
24
|
+
...config,
|
|
25
|
+
experiments: {
|
|
26
|
+
...config.experiments,
|
|
27
|
+
[name]: enabled
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
configPath
|
|
31
|
+
);
|
|
32
|
+
return resolveExperiments(writtenConfig);
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
EXPERIMENTS,
|
|
36
|
+
isExperimentName,
|
|
37
|
+
resolveExperiments,
|
|
38
|
+
setExperimentEnabled
|
|
39
|
+
};
|
package/dist/cli/core/session.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
parseSessionStateContent,
|
|
19
19
|
serializeSessionState
|
|
20
20
|
} from "../../shared/state/index.js";
|
|
21
|
+
import { librettoCommand } from "../../shared/package-manager.js";
|
|
21
22
|
const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
22
23
|
const SESSION_DEV_SERVER = "dev-server";
|
|
23
24
|
const SESSION_BROWSER_AGENT = "browser-agent";
|
|
@@ -119,7 +120,7 @@ function throwSessionNotFoundError(session) {
|
|
|
119
120
|
}
|
|
120
121
|
lines.push("");
|
|
121
122
|
lines.push("Start one with:");
|
|
122
|
-
lines.push(`
|
|
123
|
+
lines.push(` ${librettoCommand(`open <url> --session ${session}`)}`);
|
|
123
124
|
throw new Error(lines.join("\n"));
|
|
124
125
|
}
|
|
125
126
|
function assertSessionStateExistsOrThrow(session) {
|
|
@@ -176,7 +177,7 @@ function assertSessionAllowsCommand(state, commandName, allowedModes) {
|
|
|
176
177
|
}
|
|
177
178
|
const supportedModes = [...allowedModes].join(", ");
|
|
178
179
|
throw new Error(
|
|
179
|
-
`Command "${commandName}" is blocked for session "${state.session}" because it is in ${mode} mode. Allowed modes for this command: ${supportedModes}. Run
|
|
180
|
+
`Command "${commandName}" is blocked for session "${state.session}" because it is in ${mode} mode. Allowed modes for this command: ${supportedModes}. Run \`${librettoCommand(`session-mode write-access --session ${state.session}`)}\` to unlock the session.`
|
|
180
181
|
);
|
|
181
182
|
}
|
|
182
183
|
function clearSessionState(session, logger) {
|
|
@@ -213,7 +214,7 @@ function assertSessionAvailableForStart(session, logger) {
|
|
|
213
214
|
if (!existingState) return;
|
|
214
215
|
if (existingState.provider && existingState.cdpEndpoint) {
|
|
215
216
|
throw new Error(
|
|
216
|
-
`Session "${session}" is already open via ${existingState.provider.name} provider. Close it first with:
|
|
217
|
+
`Session "${session}" is already open via ${existingState.provider.name} provider. Close it first with: ${librettoCommand(`close --session ${session}`)}`
|
|
217
218
|
);
|
|
218
219
|
}
|
|
219
220
|
if (existingState.pid == null || !isPidRunning(existingState.pid)) {
|
|
@@ -222,7 +223,7 @@ function assertSessionAvailableForStart(session, logger) {
|
|
|
222
223
|
}
|
|
223
224
|
const endpoint = `http://127.0.0.1:${existingState.port}`;
|
|
224
225
|
throw new Error(
|
|
225
|
-
`Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with:
|
|
226
|
+
`Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: ${librettoCommand(`close --session ${session}`)}`
|
|
226
227
|
);
|
|
227
228
|
}
|
|
228
229
|
export {
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { REPO_ROOT } from "./context.js";
|
|
5
|
+
import { librettoCommand } from "../../shared/package-manager.js";
|
|
5
6
|
const INSTALLED_SKILL_PATHS = [
|
|
6
7
|
[".agents", "skills", "libretto", "SKILL.md"],
|
|
7
8
|
[".claude", "skills", "libretto", "SKILL.md"]
|
|
@@ -63,7 +64,7 @@ function warnIfInstalledSkillOutOfDate() {
|
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
console.error(
|
|
66
|
-
`Warning: Your agent skill (${mismatch.installedVersion}) is out of date with your Libretto CLI (${mismatch.cliVersion}). Please run
|
|
67
|
+
`Warning: Your agent skill (${mismatch.installedVersion}) is out of date with your Libretto CLI (${mismatch.cliVersion}). Please run \`${librettoCommand("setup")}\` to update your skills to the correct version.`
|
|
67
68
|
);
|
|
68
69
|
} catch {
|
|
69
70
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { installPauseHandler } from "../../../shared/debug/pause-handler.js";
|
|
2
|
+
import {
|
|
3
|
+
getAbsoluteIntegrationPath,
|
|
4
|
+
installHeadedWorkflowVisualization,
|
|
5
|
+
loadDefaultWorkflow
|
|
6
|
+
} from "../workflow-runtime.js";
|
|
7
|
+
class WorkflowController {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
status = { state: "idle" };
|
|
12
|
+
pendingPause;
|
|
13
|
+
started = false;
|
|
14
|
+
start(workflowConfig) {
|
|
15
|
+
if (this.started) {
|
|
16
|
+
throw new Error("Workflow controller has already started.");
|
|
17
|
+
}
|
|
18
|
+
this.started = true;
|
|
19
|
+
this.status = { state: "running" };
|
|
20
|
+
void this.run(workflowConfig);
|
|
21
|
+
}
|
|
22
|
+
pause(args) {
|
|
23
|
+
if (this.pendingPause) {
|
|
24
|
+
throw new Error("Workflow is already paused.");
|
|
25
|
+
}
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
this.pendingPause = { resolve };
|
|
28
|
+
this.status = { state: "paused", ...args };
|
|
29
|
+
this.config.onOutcome?.(this.status);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
resume() {
|
|
33
|
+
if (!this.pendingPause) {
|
|
34
|
+
throw new Error("Workflow is not paused.");
|
|
35
|
+
}
|
|
36
|
+
const pendingPause = this.pendingPause;
|
|
37
|
+
this.pendingPause = void 0;
|
|
38
|
+
this.status = { state: "running" };
|
|
39
|
+
pendingPause.resolve();
|
|
40
|
+
}
|
|
41
|
+
getStatus() {
|
|
42
|
+
return this.status;
|
|
43
|
+
}
|
|
44
|
+
async run(workflowConfig) {
|
|
45
|
+
const restoreOutput = this.captureProcessOutput();
|
|
46
|
+
try {
|
|
47
|
+
const absolutePath = getAbsoluteIntegrationPath(
|
|
48
|
+
workflowConfig.integrationPath
|
|
49
|
+
);
|
|
50
|
+
const workflow = workflowConfig.loadedWorkflow ?? await loadDefaultWorkflow(absolutePath);
|
|
51
|
+
const workflowLogger = this.config.logger.withScope("integration-run", {
|
|
52
|
+
integrationPath: absolutePath,
|
|
53
|
+
workflowName: workflow.name,
|
|
54
|
+
session: this.config.session
|
|
55
|
+
});
|
|
56
|
+
console.log(
|
|
57
|
+
`Running workflow "${workflow.name}" from ${absolutePath} (${this.config.headed ? "headed" : "headless"})...`
|
|
58
|
+
);
|
|
59
|
+
if (this.config.headed && workflowConfig.visualize !== false) {
|
|
60
|
+
await installHeadedWorkflowVisualization({
|
|
61
|
+
context: this.config.context,
|
|
62
|
+
logger: workflowLogger
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
await this.config.context.addInitScript(() => {
|
|
66
|
+
globalThis.__name = (target, value) => Object.defineProperty(target, "name", {
|
|
67
|
+
value,
|
|
68
|
+
configurable: true
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
const workflowContext = {
|
|
72
|
+
session: this.config.session,
|
|
73
|
+
page: this.config.page
|
|
74
|
+
};
|
|
75
|
+
const uninstallPauseHandler = installPauseHandler(
|
|
76
|
+
(pauseArgs) => this.pause({
|
|
77
|
+
...pauseArgs,
|
|
78
|
+
url: this.config.page.isClosed() ? void 0 : this.config.page.url()
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
try {
|
|
82
|
+
await workflow.run(workflowContext, workflowConfig.params ?? {});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.emitOutcome({
|
|
85
|
+
state: "finished",
|
|
86
|
+
result: "failed",
|
|
87
|
+
message: error instanceof Error ? error.message : String(error),
|
|
88
|
+
phase: "workflow"
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
} finally {
|
|
92
|
+
uninstallPauseHandler();
|
|
93
|
+
}
|
|
94
|
+
this.emitOutcome({
|
|
95
|
+
state: "finished",
|
|
96
|
+
result: "completed",
|
|
97
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.emitOutcome({
|
|
101
|
+
state: "finished",
|
|
102
|
+
result: "failed",
|
|
103
|
+
message: error instanceof Error ? error.message : String(error),
|
|
104
|
+
phase: "setup"
|
|
105
|
+
});
|
|
106
|
+
} finally {
|
|
107
|
+
restoreOutput();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
emitOutcome(outcome) {
|
|
111
|
+
this.resolvePendingPause();
|
|
112
|
+
this.status = outcome;
|
|
113
|
+
this.config.onOutcome?.(outcome);
|
|
114
|
+
}
|
|
115
|
+
resolvePendingPause() {
|
|
116
|
+
const pendingPause = this.pendingPause;
|
|
117
|
+
if (!pendingPause) return;
|
|
118
|
+
this.pendingPause = void 0;
|
|
119
|
+
pendingPause.resolve();
|
|
120
|
+
}
|
|
121
|
+
captureProcessOutput() {
|
|
122
|
+
const stdout = process.stdout;
|
|
123
|
+
const stderr = process.stderr;
|
|
124
|
+
const originalStdoutWrite = stdout.write;
|
|
125
|
+
const originalStderrWrite = stderr.write;
|
|
126
|
+
stdout.write = ((...writeArgs) => {
|
|
127
|
+
const [chunk] = writeArgs;
|
|
128
|
+
this.config.onLog?.({ stream: "stdout", text: chunkToString(chunk) });
|
|
129
|
+
return Reflect.apply(originalStdoutWrite, stdout, writeArgs);
|
|
130
|
+
});
|
|
131
|
+
stderr.write = ((...writeArgs) => {
|
|
132
|
+
const [chunk] = writeArgs;
|
|
133
|
+
this.config.onLog?.({ stream: "stderr", text: chunkToString(chunk) });
|
|
134
|
+
return Reflect.apply(originalStderrWrite, stderr, writeArgs);
|
|
135
|
+
});
|
|
136
|
+
return () => {
|
|
137
|
+
stdout.write = originalStdoutWrite;
|
|
138
|
+
stderr.write = originalStderrWrite;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function chunkToString(chunk) {
|
|
143
|
+
return Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
WorkflowController
|
|
147
|
+
};
|