libretto 0.6.9 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +2 -0
- package/dist/cli/commands/auth.js +535 -0
- package/dist/cli/commands/billing.js +74 -0
- package/dist/cli/commands/browser.js +8 -3
- package/dist/cli/commands/deploy.js +2 -7
- package/dist/cli/commands/execution.js +99 -136
- package/dist/cli/commands/snapshot.js +38 -126
- package/dist/cli/core/ai-model.js +0 -3
- package/dist/cli/core/auth-fetch.js +195 -0
- package/dist/cli/core/auth-storage.js +52 -0
- package/dist/cli/core/browser.js +128 -202
- package/dist/cli/core/daemon/config.js +6 -0
- package/dist/cli/core/daemon/daemon.js +298 -0
- package/dist/cli/core/daemon/exec.js +86 -0
- package/dist/cli/core/daemon/index.js +16 -0
- package/dist/cli/core/daemon/ipc.js +171 -0
- package/dist/cli/core/daemon/pages.js +15 -0
- package/dist/cli/core/daemon/snapshot.js +86 -0
- package/dist/cli/core/daemon/spawn.js +90 -0
- package/dist/cli/core/exec-compiler.js +111 -0
- package/dist/cli/core/prompt.js +72 -0
- package/dist/cli/core/providers/libretto-cloud.js +2 -6
- package/dist/cli/core/readonly-exec.js +1 -1
- package/dist/cli/router.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +0 -5
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +2 -1
- package/docs/browser-automation-approaches.md +435 -0
- package/docs/releasing.md +117 -0
- package/package.json +4 -3
- package/skills/libretto/SKILL.md +14 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +2 -0
- package/src/cli/commands/auth.ts +787 -0
- package/src/cli/commands/billing.ts +133 -0
- package/src/cli/commands/browser.ts +8 -2
- package/src/cli/commands/deploy.ts +2 -7
- package/src/cli/commands/execution.ts +126 -186
- package/src/cli/commands/snapshot.ts +46 -143
- package/src/cli/core/ai-model.ts +4 -5
- package/src/cli/core/auth-fetch.ts +283 -0
- package/src/cli/core/auth-storage.ts +102 -0
- package/src/cli/core/browser.ts +159 -242
- package/src/cli/core/daemon/config.ts +46 -0
- package/src/cli/core/daemon/daemon.ts +429 -0
- package/src/cli/core/daemon/exec.ts +128 -0
- package/src/cli/core/daemon/index.ts +24 -0
- package/src/cli/core/daemon/ipc.ts +294 -0
- package/src/cli/core/daemon/pages.ts +21 -0
- package/src/cli/core/daemon/snapshot.ts +114 -0
- package/src/cli/core/daemon/spawn.ts +171 -0
- package/src/cli/core/exec-compiler.ts +169 -0
- package/src/cli/core/prompt.ts +94 -0
- package/src/cli/core/providers/libretto-cloud.ts +2 -6
- package/src/cli/core/readonly-exec.ts +2 -1
- package/src/cli/router.ts +4 -0
- package/src/cli/workers/run-integration-runtime.ts +0 -6
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/browser-daemon.js +0 -122
- package/src/cli/core/browser-daemon.ts +0 -198
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { openSync, closeSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { getDaemonSocketPath } from "./ipc.js";
|
|
6
|
+
import { DaemonClient } from "./ipc.js";
|
|
7
|
+
const DEFAULT_IPC_TIMEOUT_MS = 1e4;
|
|
8
|
+
const IPC_POLL_INTERVAL_MS = 250;
|
|
9
|
+
async function spawnSessionDaemon(options) {
|
|
10
|
+
const {
|
|
11
|
+
config,
|
|
12
|
+
session,
|
|
13
|
+
logger,
|
|
14
|
+
logPath,
|
|
15
|
+
ipcTimeoutMs = DEFAULT_IPC_TIMEOUT_MS,
|
|
16
|
+
onFailure
|
|
17
|
+
} = options;
|
|
18
|
+
const daemonEntryPath = fileURLToPath(
|
|
19
|
+
new URL("./daemon.js", import.meta.url)
|
|
20
|
+
);
|
|
21
|
+
const require2 = createRequire(import.meta.url);
|
|
22
|
+
const tsxImportPath = pathToFileURL(require2.resolve("tsx/esm")).href;
|
|
23
|
+
const childStderrFd = openSync(logPath, "a");
|
|
24
|
+
const child = spawn(
|
|
25
|
+
process.execPath,
|
|
26
|
+
["--import", tsxImportPath, daemonEntryPath, JSON.stringify(config)],
|
|
27
|
+
{
|
|
28
|
+
detached: true,
|
|
29
|
+
stdio: ["ignore", "ignore", childStderrFd]
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
child.unref();
|
|
33
|
+
closeSync(childStderrFd);
|
|
34
|
+
const pid = child.pid;
|
|
35
|
+
logger.info("daemon-spawned", { pid, session });
|
|
36
|
+
let childSpawnError = null;
|
|
37
|
+
let childEarlyExit = null;
|
|
38
|
+
child.on("error", (err) => {
|
|
39
|
+
childSpawnError = err;
|
|
40
|
+
logger.error("daemon-spawn-error", { error: err, session });
|
|
41
|
+
});
|
|
42
|
+
child.on("exit", (code, signal) => {
|
|
43
|
+
childEarlyExit = { code, signal };
|
|
44
|
+
logger.warn("daemon-early-exit", { code, signal, session, pid });
|
|
45
|
+
});
|
|
46
|
+
const socketPath = getDaemonSocketPath(session);
|
|
47
|
+
const client = new DaemonClient(socketPath);
|
|
48
|
+
const maxAttempts = Math.ceil(ipcTimeoutMs / IPC_POLL_INTERVAL_MS);
|
|
49
|
+
let ipcReady = false;
|
|
50
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
51
|
+
const spawnError = childSpawnError;
|
|
52
|
+
if (spawnError !== null) {
|
|
53
|
+
await onFailure?.();
|
|
54
|
+
const errWithCode = spawnError;
|
|
55
|
+
const hint = errWithCode.code === "ENOENT" ? " Ensure Node.js is available in PATH for child processes." : "";
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Failed to spawn daemon: ${spawnError.message}.${hint} Check logs: ${logPath}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const earlyExit = childEarlyExit;
|
|
61
|
+
if (earlyExit !== null) {
|
|
62
|
+
await onFailure?.();
|
|
63
|
+
const status = earlyExit.code ?? earlyExit.signal ?? "unknown";
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Daemon exited before startup (status: ${status}). Check logs: ${logPath}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
await new Promise((r) => setTimeout(r, IPC_POLL_INTERVAL_MS));
|
|
69
|
+
ipcReady = await client.ping();
|
|
70
|
+
if (ipcReady) break;
|
|
71
|
+
if (i > 0 && i % 10 === 0) {
|
|
72
|
+
logger.info("daemon-waiting-for-ipc", { attempt: i, session });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!ipcReady) {
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, "SIGTERM");
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
await onFailure?.();
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Daemon failed to start within ${Math.ceil(ipcTimeoutMs / 1e3)}s. Check logs: ${logPath}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
logger.info("daemon-ipc-ready", { session, socketPath });
|
|
86
|
+
return { pid, socketPath, client };
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
spawnSessionDaemon
|
|
90
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as moduleBuiltin from "node:module";
|
|
2
|
+
const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
|
|
3
|
+
function withSuppressedStripTypeScriptWarning(action) {
|
|
4
|
+
const mutableProcess = process;
|
|
5
|
+
const originalEmitWarning = mutableProcess.emitWarning;
|
|
6
|
+
mutableProcess.emitWarning = (...args) => {
|
|
7
|
+
const warning = args[0];
|
|
8
|
+
const typeOrOptions = args[1];
|
|
9
|
+
const warningMessage = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
|
|
10
|
+
const warningType = typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type ?? "" : "";
|
|
11
|
+
if (warningType === "ExperimentalWarning" && warningMessage.includes("stripTypeScriptTypes")) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
originalEmitWarning(...args);
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
return action();
|
|
18
|
+
} finally {
|
|
19
|
+
mutableProcess.emitWarning = originalEmitWarning;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function compileTypeScriptExecFunction(code, helperNames) {
|
|
23
|
+
if (!stripTypeScriptTypes) return null;
|
|
24
|
+
const wrappedSource = `(async function __librettoExec(${helperNames.join(", ")}) {
|
|
25
|
+
${code}
|
|
26
|
+
})`;
|
|
27
|
+
const jsSource = withSuppressedStripTypeScriptWarning(
|
|
28
|
+
() => stripTypeScriptTypes(wrappedSource, { mode: "strip" })
|
|
29
|
+
);
|
|
30
|
+
const createFunction = new Function(
|
|
31
|
+
`return ${jsSource}`
|
|
32
|
+
);
|
|
33
|
+
return createFunction();
|
|
34
|
+
}
|
|
35
|
+
function compileExecFunction(code, helperNames) {
|
|
36
|
+
const typeStripped = compileTypeScriptExecFunction(code, helperNames);
|
|
37
|
+
if (typeStripped) return typeStripped;
|
|
38
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
39
|
+
}).constructor;
|
|
40
|
+
return new AsyncFunction(...helperNames, code);
|
|
41
|
+
}
|
|
42
|
+
function stripEmptyCatchHandlers(code) {
|
|
43
|
+
const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
|
|
44
|
+
let strippedCount = 0;
|
|
45
|
+
let result = "";
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < code.length) {
|
|
48
|
+
if (code[i] === "/" && code[i + 1] === "/") {
|
|
49
|
+
const end = code.indexOf("\n", i);
|
|
50
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
|
|
51
|
+
result += slice;
|
|
52
|
+
i += slice.length;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (code[i] === "/" && code[i + 1] === "*") {
|
|
56
|
+
const end = code.indexOf("*/", i + 2);
|
|
57
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
|
|
58
|
+
result += slice;
|
|
59
|
+
i += slice.length;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
63
|
+
const quote = code[i];
|
|
64
|
+
let j = i + 1;
|
|
65
|
+
while (j < code.length) {
|
|
66
|
+
if (code[j] === "\\" && quote !== "`") {
|
|
67
|
+
j += 2;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (code[j] === "\\" && quote === "`") {
|
|
71
|
+
j += 2;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (code[j] === quote) {
|
|
75
|
+
j++;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
|
|
79
|
+
let depth = 1;
|
|
80
|
+
j += 2;
|
|
81
|
+
while (j < code.length && depth > 0) {
|
|
82
|
+
if (code[j] === "{") depth++;
|
|
83
|
+
else if (code[j] === "}") depth--;
|
|
84
|
+
j++;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
j++;
|
|
89
|
+
}
|
|
90
|
+
result += code.slice(i, j);
|
|
91
|
+
i = j;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
catchRe.lastIndex = i;
|
|
95
|
+
const match = catchRe.exec(code);
|
|
96
|
+
if (match && match.index === i) {
|
|
97
|
+
strippedCount++;
|
|
98
|
+
i += match[0].length;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
result += code[i];
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
return { cleaned: result, strippedCount };
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
compileExecFunction,
|
|
108
|
+
compileTypeScriptExecFunction,
|
|
109
|
+
stripEmptyCatchHandlers,
|
|
110
|
+
withSuppressedStripTypeScriptWarning
|
|
111
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
async function prompt(question, opts = {}) {
|
|
4
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
5
|
+
try {
|
|
6
|
+
const display = opts.defaultValue ? `${question} (${opts.defaultValue}) ` : `${question} `;
|
|
7
|
+
const answer = (await rl.question(display)).trim();
|
|
8
|
+
if (answer.length === 0 && opts.defaultValue !== void 0) {
|
|
9
|
+
return opts.defaultValue;
|
|
10
|
+
}
|
|
11
|
+
return answer;
|
|
12
|
+
} finally {
|
|
13
|
+
rl.close();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const CTRL_C = "";
|
|
17
|
+
const CR = "\r";
|
|
18
|
+
const LF = "\n";
|
|
19
|
+
const BACKSPACE = "\b";
|
|
20
|
+
const DELETE = "\x7F";
|
|
21
|
+
async function promptPassword(question) {
|
|
22
|
+
if (!stdin.isTTY) {
|
|
23
|
+
return prompt(question);
|
|
24
|
+
}
|
|
25
|
+
process.stdout.write(`${question} `);
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let buffer = "";
|
|
28
|
+
const wasRaw = stdin.isRaw;
|
|
29
|
+
stdin.setRawMode(true);
|
|
30
|
+
stdin.resume();
|
|
31
|
+
stdin.setEncoding("utf8");
|
|
32
|
+
const cleanup = () => {
|
|
33
|
+
stdin.setRawMode(wasRaw);
|
|
34
|
+
stdin.pause();
|
|
35
|
+
stdin.removeListener("data", onData);
|
|
36
|
+
process.stdout.write("\n");
|
|
37
|
+
};
|
|
38
|
+
const onData = (chunk) => {
|
|
39
|
+
for (const ch of chunk) {
|
|
40
|
+
if (ch === LF || ch === CR) {
|
|
41
|
+
cleanup();
|
|
42
|
+
resolve(buffer);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (ch === CTRL_C) {
|
|
46
|
+
cleanup();
|
|
47
|
+
reject(new Error("Aborted."));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (ch === BACKSPACE || ch === DELETE) {
|
|
51
|
+
if (buffer.length > 0) {
|
|
52
|
+
buffer = buffer.slice(0, -1);
|
|
53
|
+
process.stdout.write("\b \b");
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch.charCodeAt(0) < 32) continue;
|
|
58
|
+
buffer += ch;
|
|
59
|
+
process.stdout.write("*");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
stdin.on("data", onData);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function slugify(name) {
|
|
66
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
prompt,
|
|
70
|
+
promptPassword,
|
|
71
|
+
slugify
|
|
72
|
+
};
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
+
import { HOSTED_API_URL } from "../auth-fetch.js";
|
|
1
2
|
function createLibrettoCloudProvider() {
|
|
2
3
|
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
3
4
|
if (!apiKey)
|
|
4
5
|
throw new Error(
|
|
5
6
|
"LIBRETTO_API_KEY is required for the Libretto Cloud provider."
|
|
6
7
|
);
|
|
7
|
-
const
|
|
8
|
-
if (!apiUrl)
|
|
9
|
-
throw new Error(
|
|
10
|
-
"LIBRETTO_API_URL is required for the Libretto Cloud provider."
|
|
11
|
-
);
|
|
12
|
-
const endpoint = apiUrl.replace(/\/$/, "");
|
|
8
|
+
const endpoint = HOSTED_API_URL;
|
|
13
9
|
return {
|
|
14
10
|
async createSession() {
|
|
15
11
|
const timeoutSeconds = Number(
|
package/dist/cli/router.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { aiCommands } from "./commands/ai.js";
|
|
2
|
+
import { authCommands } from "./commands/auth.js";
|
|
3
|
+
import { billingCommands } from "./commands/billing.js";
|
|
2
4
|
import { browserCommands } from "./commands/browser.js";
|
|
3
5
|
import { deployCommand } from "./commands/deploy.js";
|
|
4
6
|
import { executionCommands } from "./commands/execution.js";
|
|
@@ -11,6 +13,8 @@ const cliRoutes = {
|
|
|
11
13
|
deploy: deployCommand,
|
|
12
14
|
...executionCommands,
|
|
13
15
|
ai: aiCommands,
|
|
16
|
+
auth: authCommands,
|
|
17
|
+
billing: billingCommands,
|
|
14
18
|
setup: setupCommand,
|
|
15
19
|
status: statusCommand,
|
|
16
20
|
snapshot: snapshotCommand
|
|
@@ -3,7 +3,6 @@ import { writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { cwd } from "node:process";
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { loadEnv } from "../../shared/env/load-env.js";
|
|
7
6
|
import {
|
|
8
7
|
getDefaultWorkflowFromModuleExports,
|
|
9
8
|
getWorkflowsFromModuleExports,
|
|
@@ -127,10 +126,6 @@ async function installHeadedWorkflowVisualization(args) {
|
|
|
127
126
|
async function runIntegrationInternal(args, options) {
|
|
128
127
|
const { logger } = options;
|
|
129
128
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
130
|
-
const envPath = loadEnv();
|
|
131
|
-
if (envPath) {
|
|
132
|
-
logger.info("loaded-env", { path: envPath });
|
|
133
|
-
}
|
|
134
129
|
const workflow = await loadDefaultWorkflow(absolutePath);
|
|
135
130
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
136
131
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
@@ -48,6 +48,7 @@ declare const SessionStateFileSchema: z.ZodObject<{
|
|
|
48
48
|
name: z.ZodString;
|
|
49
49
|
sessionId: z.ZodString;
|
|
50
50
|
}, z.core.$strip>>;
|
|
51
|
+
daemonSocketPath: z.ZodOptional<z.ZodString>;
|
|
51
52
|
}, z.core.$strip>;
|
|
52
53
|
type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
53
54
|
type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
|
|
@@ -27,7 +27,8 @@ const SessionStateFileSchema = z.object({
|
|
|
27
27
|
status: SessionStatusSchema.optional(),
|
|
28
28
|
mode: SessionAccessModeSchema.default("write-access"),
|
|
29
29
|
viewport: SessionViewportSchema.optional(),
|
|
30
|
-
provider: ProviderStateSchema.optional()
|
|
30
|
+
provider: ProviderStateSchema.optional(),
|
|
31
|
+
daemonSocketPath: z.string().optional()
|
|
31
32
|
});
|
|
32
33
|
function formatIssues(error) {
|
|
33
34
|
return error.issues.map((issue) => {
|