ptywright 0.2.0 → 0.4.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 +76 -7
- package/dist/agent.mjs +2 -2
- package/dist/bin/ptywright.mjs +1 -1
- package/dist/{cli-DIUx2w6X.mjs → cli-C40H_ElC.mjs} +60 -28
- package/dist/cli.mjs +1 -1
- package/dist/config-B0r-JCFI.mjs +52 -0
- package/dist/config.mjs +2 -0
- package/dist/index.mjs +1 -1
- package/dist/mcp.mjs +1 -1
- package/dist/pty-cassette.mjs +1 -1
- package/dist/{runner-DzZlFrt1.mjs → runner-CembqDgJ.mjs} +211 -185
- package/dist/{server-VHuEWWj_.mjs → server-h--2U0Ic.mjs} +1 -1
- package/package.json +2 -1
- package/schemas/ptywright-agent.schema.json +3 -19
- package/skills/ptywright-testing/SKILL.md +113 -79
- package/skills/ptywright-testing/agents/openai.yaml +4 -0
- package/skills/ptywright-testing/references/agent-regression.md +132 -0
- package/skills/ptywright-testing/references/ci-and-debugging.md +95 -0
- package/skills/ptywright-testing/references/mcp-tools.md +91 -0
- package/skills/ptywright-testing/references/raw-pty-cassettes.md +82 -0
- package/skills/ptywright-testing/references/script-runner.md +80 -0
- /package/dist/{pty_like-Cpkh_O9B.mjs → pty_like-DqCo7XdB.mjs} +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
6
|
import { chromium } from "playwright";
|
|
8
7
|
import { createServer } from "node:http";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
9
|
//#region src/agent/manifest.ts
|
|
10
10
|
const AGENT_MANIFEST_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json";
|
|
11
11
|
const AGENT_MANIFEST_FILE_NAME = "ptywright-agent.manifest.json";
|
|
@@ -171,134 +171,6 @@ function formatZodIssues$1(error) {
|
|
|
171
171
|
}).join("; ");
|
|
172
172
|
}
|
|
173
173
|
//#endregion
|
|
174
|
-
//#region src/agent/aitty.ts
|
|
175
|
-
function buildAittyExecCommand(launch, options = {}) {
|
|
176
|
-
if (!launch.command) throw new Error("launch.command is required for aitty mode");
|
|
177
|
-
const rootDir = options.rootDir ?? process.cwd();
|
|
178
|
-
const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
|
|
179
|
-
const aitty = launch.aitty ?? {};
|
|
180
|
-
const cli = resolveAittyCliCommand(aitty.command, rootDir, options.env ?? process.env);
|
|
181
|
-
const args = [
|
|
182
|
-
...cli.args,
|
|
183
|
-
"exec",
|
|
184
|
-
"--launch",
|
|
185
|
-
"print"
|
|
186
|
-
];
|
|
187
|
-
pushOption(args, "--cwd", cwd);
|
|
188
|
-
pushOption(args, "--host", aitty.host);
|
|
189
|
-
pushOption(args, "--port", aitty.port);
|
|
190
|
-
pushOption(args, "--project", aitty.project);
|
|
191
|
-
pushOption(args, "--label", aitty.label);
|
|
192
|
-
pushOption(args, "--title", aitty.title);
|
|
193
|
-
pushOption(args, "--subtitle", aitty.subtitle);
|
|
194
|
-
pushOption(args, "--theme", aitty.theme && aitty.theme !== "auto" ? aitty.theme : void 0);
|
|
195
|
-
pushOption(args, "--font-size", aitty.fontSize);
|
|
196
|
-
pushOption(args, "--experimental-screen-mode", aitty.screenMode);
|
|
197
|
-
args.push("--", launch.command, ...launch.args ?? []);
|
|
198
|
-
return {
|
|
199
|
-
file: cli.file,
|
|
200
|
-
args,
|
|
201
|
-
cwd,
|
|
202
|
-
env: {
|
|
203
|
-
...options.env ?? process.env,
|
|
204
|
-
...launch.env
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
async function launchAittyBrowserSession(launch, options = {}) {
|
|
209
|
-
const command = buildAittyExecCommand(launch, options);
|
|
210
|
-
const timeoutMs = launch.aitty?.waitForUrlMs ?? 15e3;
|
|
211
|
-
const child = spawn(command.file, command.args, {
|
|
212
|
-
cwd: command.cwd,
|
|
213
|
-
env: command.env,
|
|
214
|
-
stdio: [
|
|
215
|
-
"ignore",
|
|
216
|
-
"pipe",
|
|
217
|
-
"pipe"
|
|
218
|
-
]
|
|
219
|
-
});
|
|
220
|
-
const chunks = [];
|
|
221
|
-
const stderrChunks = [];
|
|
222
|
-
return {
|
|
223
|
-
url: await new Promise((resolveUrl, reject) => {
|
|
224
|
-
let settled = false;
|
|
225
|
-
const timer = setTimeout(() => {
|
|
226
|
-
finish(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for aitty session URL\nstderr=${stderrChunks.join("").trim()}`));
|
|
227
|
-
}, timeoutMs);
|
|
228
|
-
const finish = (result) => {
|
|
229
|
-
if (settled) return;
|
|
230
|
-
settled = true;
|
|
231
|
-
clearTimeout(timer);
|
|
232
|
-
child.stdout.off("data", onStdout);
|
|
233
|
-
child.stderr.off("data", onStderr);
|
|
234
|
-
child.off("error", onError);
|
|
235
|
-
child.off("exit", onExit);
|
|
236
|
-
if (result instanceof Error) reject(result);
|
|
237
|
-
else resolveUrl(result);
|
|
238
|
-
};
|
|
239
|
-
const onStdout = (chunk) => {
|
|
240
|
-
const text = chunk.toString("utf8");
|
|
241
|
-
chunks.push(text);
|
|
242
|
-
const found = extractAittyUrlFromOutput(chunks.join(""));
|
|
243
|
-
if (found) finish(found);
|
|
244
|
-
};
|
|
245
|
-
const onStderr = (chunk) => {
|
|
246
|
-
stderrChunks.push(chunk.toString("utf8"));
|
|
247
|
-
};
|
|
248
|
-
const onError = (error) => finish(error);
|
|
249
|
-
const onExit = (code, signal) => {
|
|
250
|
-
finish(/* @__PURE__ */ new Error(`aitty exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${chunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
251
|
-
};
|
|
252
|
-
child.stdout.on("data", onStdout);
|
|
253
|
-
child.stderr.on("data", onStderr);
|
|
254
|
-
child.once("error", onError);
|
|
255
|
-
child.once("exit", onExit);
|
|
256
|
-
}),
|
|
257
|
-
process: child,
|
|
258
|
-
close: () => closeChild(child)
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
function extractAittyUrlFromOutput(output) {
|
|
262
|
-
return output.match(/https?:\/\/[^\s"'<>]+/)?.[0] ?? null;
|
|
263
|
-
}
|
|
264
|
-
function resolveAittyCliCommand(explicitCommand, rootDir, env) {
|
|
265
|
-
if (explicitCommand) return {
|
|
266
|
-
file: explicitCommand,
|
|
267
|
-
args: []
|
|
268
|
-
};
|
|
269
|
-
if (env.PTYWRIGHT_AITTY_CLI) return {
|
|
270
|
-
file: env.PTYWRIGHT_AITTY_CLI,
|
|
271
|
-
args: []
|
|
272
|
-
};
|
|
273
|
-
const siblingDist = resolve(rootDir, "../aitty/packages/cli/dist/cli.js");
|
|
274
|
-
if (existsSync(siblingDist)) return {
|
|
275
|
-
file: "node",
|
|
276
|
-
args: [siblingDist]
|
|
277
|
-
};
|
|
278
|
-
return {
|
|
279
|
-
file: "aitty",
|
|
280
|
-
args: []
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function pushOption(args, name, value) {
|
|
284
|
-
if (value === void 0 || value === "") return;
|
|
285
|
-
args.push(name, String(value));
|
|
286
|
-
}
|
|
287
|
-
async function closeChild(child) {
|
|
288
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
289
|
-
await new Promise((resolveClose) => {
|
|
290
|
-
const timer = setTimeout(() => {
|
|
291
|
-
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
292
|
-
resolveClose();
|
|
293
|
-
}, 2e3);
|
|
294
|
-
child.once("exit", () => {
|
|
295
|
-
clearTimeout(timer);
|
|
296
|
-
resolveClose();
|
|
297
|
-
});
|
|
298
|
-
child.kill("SIGTERM");
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
//#endregion
|
|
302
174
|
//#region src/agent/browser.ts
|
|
303
175
|
async function launchAgentBrowser(args) {
|
|
304
176
|
let lastError;
|
|
@@ -391,8 +263,14 @@ const agentViewportSchema = z.object({
|
|
|
391
263
|
isMobile: z.boolean().optional(),
|
|
392
264
|
hasTouch: z.boolean().optional()
|
|
393
265
|
});
|
|
266
|
+
const agentLaunchModeSchema = z.enum(["command", "url"]);
|
|
267
|
+
function inferAgentLaunchMode(value) {
|
|
268
|
+
if (value.mode) return value.mode;
|
|
269
|
+
if (value.url) return "url";
|
|
270
|
+
return "command";
|
|
271
|
+
}
|
|
394
272
|
const agentLaunchSchema = z.object({
|
|
395
|
-
mode:
|
|
273
|
+
mode: agentLaunchModeSchema.optional(),
|
|
396
274
|
agentFlavor: z.enum([
|
|
397
275
|
"codex",
|
|
398
276
|
"claude",
|
|
@@ -404,33 +282,17 @@ const agentLaunchSchema = z.object({
|
|
|
404
282
|
cwd: z.string().optional(),
|
|
405
283
|
env: z.record(z.string()).optional(),
|
|
406
284
|
url: z.string().url().optional(),
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
label: z.string().min(1).optional(),
|
|
412
|
-
title: z.string().min(1).optional(),
|
|
413
|
-
subtitle: z.string().min(1).optional(),
|
|
414
|
-
theme: z.enum([
|
|
415
|
-
"dark",
|
|
416
|
-
"light",
|
|
417
|
-
"auto"
|
|
418
|
-
]).optional(),
|
|
419
|
-
fontSize: z.number().int().min(11).max(24).optional(),
|
|
420
|
-
screenMode: z.enum(["termvision"]).optional(),
|
|
421
|
-
port: z.number().int().min(0).max(65535).optional(),
|
|
422
|
-
host: z.string().min(1).optional(),
|
|
423
|
-
waitForUrlMs: z.number().int().positive().optional()
|
|
424
|
-
}).optional()
|
|
425
|
-
}).superRefine((value, ctx) => {
|
|
426
|
-
const mode = value.mode ?? (value.url ? "url" : "aitty");
|
|
285
|
+
urlRegex: z.string().min(1).optional(),
|
|
286
|
+
waitForUrlMs: z.number().int().positive().optional()
|
|
287
|
+
}).strict().superRefine((value, ctx) => {
|
|
288
|
+
const mode = inferAgentLaunchMode(value);
|
|
427
289
|
if (mode === "url" && !value.url) ctx.addIssue({
|
|
428
290
|
code: z.ZodIssueCode.custom,
|
|
429
291
|
message: "launch.url is required when launch.mode is 'url'"
|
|
430
292
|
});
|
|
431
|
-
if (mode === "
|
|
293
|
+
if (mode === "command" && !value.command) ctx.addIssue({
|
|
432
294
|
code: z.ZodIssueCode.custom,
|
|
433
|
-
message: "launch.command is required when launch.mode is '
|
|
295
|
+
message: "launch.command is required when launch.mode is 'command'"
|
|
434
296
|
});
|
|
435
297
|
});
|
|
436
298
|
const waitForTextStepSchema = z.object({
|
|
@@ -528,6 +390,9 @@ function normalizeAgentFlowSpec(input) {
|
|
|
528
390
|
viewports: spec.viewports?.length ? spec.viewports : [...DEFAULT_AGENT_VIEWPORTS]
|
|
529
391
|
};
|
|
530
392
|
}
|
|
393
|
+
function resolveAgentLaunchMode(launch) {
|
|
394
|
+
return inferAgentLaunchMode(launch);
|
|
395
|
+
}
|
|
531
396
|
//#endregion
|
|
532
397
|
//#region src/agent/cassette.ts
|
|
533
398
|
const AGENT_CASSETTE_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json";
|
|
@@ -759,6 +624,164 @@ function escapeHtml$1(input) {
|
|
|
759
624
|
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
760
625
|
}
|
|
761
626
|
//#endregion
|
|
627
|
+
//#region src/agent/config_defaults.ts
|
|
628
|
+
function normalizeAgentFlowSpecWithConfig(input, config) {
|
|
629
|
+
return normalizeAgentFlowSpec(applyAgentConfigDefaults(agentFlowSpecSchema.parse(input), config));
|
|
630
|
+
}
|
|
631
|
+
function applyAgentConfigDefaults(input, config) {
|
|
632
|
+
const agent = config?.agent;
|
|
633
|
+
if (!agent) return input;
|
|
634
|
+
const name = sanitizeArtifactName(input.name ?? "agent-flow");
|
|
635
|
+
const configDefaults = agent.defaults ?? {};
|
|
636
|
+
const specDefaults = input.defaults ?? {};
|
|
637
|
+
const viewports = input.viewports ? void 0 : cloneViewports(configDefaults.viewports);
|
|
638
|
+
return {
|
|
639
|
+
...input,
|
|
640
|
+
artifactsDir: input.artifactsDir ?? resolveNamedDir(agent.artifactsRoot, name, config.rootDir),
|
|
641
|
+
snapshotDir: input.snapshotDir ?? resolveNamedDir(agent.snapshotDir, name, config.rootDir),
|
|
642
|
+
viewports: viewports ?? input.viewports,
|
|
643
|
+
defaults: {
|
|
644
|
+
...specDefaults,
|
|
645
|
+
timeoutMs: specDefaults.timeoutMs ?? configDefaults.timeoutMs,
|
|
646
|
+
screenshot: specDefaults.screenshot ?? configDefaults.screenshot,
|
|
647
|
+
mask: mergeMaskRules(configDefaults.mask, specDefaults.mask)
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function resolveNamedDir(root, name, configRoot) {
|
|
652
|
+
if (!root) return void 0;
|
|
653
|
+
const namedDir = join(root, name);
|
|
654
|
+
return isAbsolute(namedDir) ? namedDir : resolve(configRoot, namedDir);
|
|
655
|
+
}
|
|
656
|
+
function cloneViewports(viewports) {
|
|
657
|
+
return Array.isArray(viewports) && viewports.length > 0 ? viewports.map((viewport) => ({ ...viewport })) : void 0;
|
|
658
|
+
}
|
|
659
|
+
function mergeMaskRules(configMask, specMask) {
|
|
660
|
+
const merged = [...configMask ?? [], ...specMask ?? []];
|
|
661
|
+
return merged.length > 0 ? merged : void 0;
|
|
662
|
+
}
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/agent/command_launch.ts
|
|
665
|
+
const DEFAULT_URL_REGEX = /https?:\/\/[^\s"'<>]+/;
|
|
666
|
+
function buildCommandLaunchCommand(launch, options = {}) {
|
|
667
|
+
if (!launch.command) throw new Error("launch.command is required when launch.mode is 'command'");
|
|
668
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
669
|
+
const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
|
|
670
|
+
return {
|
|
671
|
+
file: launch.command,
|
|
672
|
+
args: launch.args ?? [],
|
|
673
|
+
cwd,
|
|
674
|
+
env: {
|
|
675
|
+
...options.env ?? process.env,
|
|
676
|
+
...launch.env
|
|
677
|
+
},
|
|
678
|
+
label: launch.command,
|
|
679
|
+
urlRegex: launch.urlRegex,
|
|
680
|
+
waitForUrlMs: launch.waitForUrlMs
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
async function launchBrowserSessionFromCommand(command) {
|
|
684
|
+
const timeoutMs = command.waitForUrlMs ?? 15e3;
|
|
685
|
+
const child = spawn(command.file, command.args, {
|
|
686
|
+
cwd: command.cwd,
|
|
687
|
+
env: command.env,
|
|
688
|
+
stdio: [
|
|
689
|
+
"ignore",
|
|
690
|
+
"pipe",
|
|
691
|
+
"pipe"
|
|
692
|
+
]
|
|
693
|
+
});
|
|
694
|
+
const stdoutChunks = [];
|
|
695
|
+
const stderrChunks = [];
|
|
696
|
+
return {
|
|
697
|
+
url: await new Promise((resolveUrl, reject) => {
|
|
698
|
+
let settled = false;
|
|
699
|
+
const timer = setTimeout(() => {
|
|
700
|
+
finish(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for ${command.label ?? command.file} session URL\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
701
|
+
}, timeoutMs);
|
|
702
|
+
const finish = (result) => {
|
|
703
|
+
if (settled) return;
|
|
704
|
+
settled = true;
|
|
705
|
+
clearTimeout(timer);
|
|
706
|
+
child.stdout.off("data", onStdout);
|
|
707
|
+
child.stderr.off("data", onStderr);
|
|
708
|
+
child.off("error", onError);
|
|
709
|
+
child.off("exit", onExit);
|
|
710
|
+
if (result instanceof Error) reject(result);
|
|
711
|
+
else resolveUrl(result);
|
|
712
|
+
};
|
|
713
|
+
const readUrl = () => {
|
|
714
|
+
const found = extractUrlFromOutput(`${stdoutChunks.join("")}\n${stderrChunks.join("")}`, command.urlRegex);
|
|
715
|
+
if (found) finish(found);
|
|
716
|
+
};
|
|
717
|
+
const onStdout = (chunk) => {
|
|
718
|
+
stdoutChunks.push(chunk.toString("utf8"));
|
|
719
|
+
readUrl();
|
|
720
|
+
};
|
|
721
|
+
const onStderr = (chunk) => {
|
|
722
|
+
stderrChunks.push(chunk.toString("utf8"));
|
|
723
|
+
readUrl();
|
|
724
|
+
};
|
|
725
|
+
const onError = (error) => finish(error);
|
|
726
|
+
const onExit = (code, signal) => {
|
|
727
|
+
finish(/* @__PURE__ */ new Error(`${command.label ?? command.file} exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
728
|
+
};
|
|
729
|
+
child.stdout.on("data", onStdout);
|
|
730
|
+
child.stderr.on("data", onStderr);
|
|
731
|
+
child.once("error", onError);
|
|
732
|
+
child.once("exit", onExit);
|
|
733
|
+
}),
|
|
734
|
+
process: child,
|
|
735
|
+
close: () => closeChild(child)
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function extractUrlFromOutput(output, regexSource) {
|
|
739
|
+
if (!regexSource) return output.match(DEFAULT_URL_REGEX)?.[0] ?? null;
|
|
740
|
+
const match = output.match(new RegExp(regexSource, "m"));
|
|
741
|
+
return match?.[1] ?? match?.[0] ?? null;
|
|
742
|
+
}
|
|
743
|
+
function formatBrowserLaunchCommand(command) {
|
|
744
|
+
return [command.file, ...command.args].join(" ");
|
|
745
|
+
}
|
|
746
|
+
async function closeChild(child) {
|
|
747
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
748
|
+
await new Promise((resolveClose) => {
|
|
749
|
+
const timer = setTimeout(() => {
|
|
750
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
751
|
+
resolveClose();
|
|
752
|
+
}, 2e3);
|
|
753
|
+
child.once("exit", () => {
|
|
754
|
+
clearTimeout(timer);
|
|
755
|
+
resolveClose();
|
|
756
|
+
});
|
|
757
|
+
child.kill("SIGTERM");
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
//#endregion
|
|
761
|
+
//#region src/agent/launch.ts
|
|
762
|
+
function buildAgentLaunchCommand(launch, options = {}) {
|
|
763
|
+
if (resolveAgentLaunchMode(launch) === "url") return null;
|
|
764
|
+
return buildCommandLaunchCommand(launch, options);
|
|
765
|
+
}
|
|
766
|
+
async function resolveAgentLaunchTarget(launch, options = {}) {
|
|
767
|
+
const mode = resolveAgentLaunchMode(launch);
|
|
768
|
+
if (mode === "url") return {
|
|
769
|
+
mode,
|
|
770
|
+
url: launch.url,
|
|
771
|
+
session: null
|
|
772
|
+
};
|
|
773
|
+
const session = await launchBrowserSessionFromCommand(buildCommandLaunchCommand(launch, options));
|
|
774
|
+
return {
|
|
775
|
+
mode,
|
|
776
|
+
url: session.url,
|
|
777
|
+
session
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function formatAgentLaunchCommand(launch) {
|
|
781
|
+
const command = buildAgentLaunchCommand(launch);
|
|
782
|
+
return command ? formatBrowserLaunchCommand(command) : "launch.mode=url";
|
|
783
|
+
}
|
|
784
|
+
//#endregion
|
|
762
785
|
//#region src/agent/presets.ts
|
|
763
786
|
const COMMON_AGENT_MASKS = [
|
|
764
787
|
{
|
|
@@ -849,19 +872,15 @@ function createAgentTemplateSpec(flavor) {
|
|
|
849
872
|
artifactsDir: `.tmp/agent/${name}`,
|
|
850
873
|
snapshotDir: `tests/agent-snapshots/${name}`,
|
|
851
874
|
launch: {
|
|
852
|
-
mode: "
|
|
875
|
+
mode: "command",
|
|
853
876
|
agentFlavor: flavor,
|
|
854
|
-
command,
|
|
855
|
-
args: [
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
theme: "light",
|
|
862
|
-
fontSize: 14,
|
|
863
|
-
waitForUrlMs: 15e3
|
|
864
|
-
}
|
|
877
|
+
command: "your-browser-terminal-launcher",
|
|
878
|
+
args: [
|
|
879
|
+
"--agent",
|
|
880
|
+
command,
|
|
881
|
+
"--print-url"
|
|
882
|
+
],
|
|
883
|
+
waitForUrlMs: 15e3
|
|
865
884
|
},
|
|
866
885
|
viewports: DEFAULT_VIEWPORTS.map((viewport) => ({ ...viewport })),
|
|
867
886
|
defaults: {
|
|
@@ -1303,20 +1322,26 @@ function escapeAttribute(input) {
|
|
|
1303
1322
|
//#region src/agent/spec_loader.ts
|
|
1304
1323
|
async function loadAgentSpec(specPath) {
|
|
1305
1324
|
const resolved = resolve(process.cwd(), specPath);
|
|
1306
|
-
if (resolved.endsWith(".json"))
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1325
|
+
if (resolved.endsWith(".json")) {
|
|
1326
|
+
const raw = JSON.parse(readFileSync(resolved, "utf8"));
|
|
1327
|
+
return {
|
|
1328
|
+
spec: normalizeAgentFlowSpec(raw),
|
|
1329
|
+
raw,
|
|
1330
|
+
path: resolved
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1310
1333
|
const mod = await import(`${pathToFileURL(resolved).href}?t=${Date.now()}`);
|
|
1334
|
+
const raw = mod.default ?? mod.spec;
|
|
1311
1335
|
return {
|
|
1312
|
-
spec: normalizeAgentFlowSpec(
|
|
1336
|
+
spec: normalizeAgentFlowSpec(raw),
|
|
1337
|
+
raw,
|
|
1313
1338
|
path: resolved
|
|
1314
1339
|
};
|
|
1315
1340
|
}
|
|
1316
1341
|
//#endregion
|
|
1317
1342
|
//#region src/agent/runner.ts
|
|
1318
1343
|
async function runAgentSpecPath(specPath, options = {}) {
|
|
1319
|
-
return runAgentSpec((await loadAgentSpec(specPath)).
|
|
1344
|
+
return runAgentSpec((await loadAgentSpec(specPath)).raw, options);
|
|
1320
1345
|
}
|
|
1321
1346
|
async function replayAgentRecordPath(recordPath, options = {}) {
|
|
1322
1347
|
const raw = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
@@ -1329,14 +1354,20 @@ async function replayAgentRecordPath(recordPath, options = {}) {
|
|
|
1329
1354
|
artifactsDir: options.artifactsDir ?? join(dirname(recordPath), "replay")
|
|
1330
1355
|
});
|
|
1331
1356
|
}
|
|
1332
|
-
if (record.spec) return runAgentSpec(record.spec,
|
|
1357
|
+
if (record.spec) return runAgentSpec(record.spec, {
|
|
1358
|
+
...options,
|
|
1359
|
+
config: void 0
|
|
1360
|
+
});
|
|
1333
1361
|
if (!record.flowPath) throw new Error(`invalid agent run record: missing replay source in ${recordPath}`);
|
|
1334
|
-
return runAgentSpecPath(isAbsolute(record.flowPath) ? record.flowPath : resolve(dirname(recordPath), record.flowPath),
|
|
1362
|
+
return runAgentSpecPath(isAbsolute(record.flowPath) ? record.flowPath : resolve(dirname(recordPath), record.flowPath), {
|
|
1363
|
+
...options,
|
|
1364
|
+
config: void 0
|
|
1365
|
+
});
|
|
1335
1366
|
}
|
|
1336
1367
|
async function runAgentSpec(input, options = {}) {
|
|
1337
1368
|
const startedAt = Date.now();
|
|
1338
1369
|
const rootDir = options.rootDir ? resolve(process.cwd(), options.rootDir) : process.cwd();
|
|
1339
|
-
const spec =
|
|
1370
|
+
const spec = normalizeAgentFlowSpecWithConfig(input, options.replayCassette ? void 0 : options.config);
|
|
1340
1371
|
const name = sanitizeArtifactName(spec.name ?? "agent-flow");
|
|
1341
1372
|
const artifactsDir = resolve(rootDir, options.artifactsDir ?? spec.artifactsDir ?? join(".tmp", "agent", name));
|
|
1342
1373
|
const snapshotDir = resolve(rootDir, spec.snapshotDir ?? join("snapshots", name));
|
|
@@ -1412,9 +1443,7 @@ async function runAgentSpec(input, options = {}) {
|
|
|
1412
1443
|
}
|
|
1413
1444
|
async function runViewport(args) {
|
|
1414
1445
|
const { browser, spec, viewport, rootDir, artifactsDir, snapshotDir, updateSnapshots, recordCassette, cassette, result } = args;
|
|
1415
|
-
const
|
|
1416
|
-
const session = launchMode === "aitty" ? await launchAittyBrowserSession(spec.launch, { rootDir }) : null;
|
|
1417
|
-
const url = launchMode === "url" ? spec.launch.url : session.url;
|
|
1446
|
+
const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
|
|
1418
1447
|
const context = await browser.newContext({
|
|
1419
1448
|
viewport: {
|
|
1420
1449
|
width: viewport.width,
|
|
@@ -1440,7 +1469,7 @@ async function runViewport(args) {
|
|
|
1440
1469
|
nextReplayPhase: 0
|
|
1441
1470
|
};
|
|
1442
1471
|
try {
|
|
1443
|
-
await page.goto(url, {
|
|
1472
|
+
await page.goto(launchTarget.url, {
|
|
1444
1473
|
waitUntil: "domcontentloaded",
|
|
1445
1474
|
timeout: spec.defaults?.timeoutMs ?? 3e4
|
|
1446
1475
|
});
|
|
@@ -1482,7 +1511,7 @@ async function runViewport(args) {
|
|
|
1482
1511
|
}
|
|
1483
1512
|
} finally {
|
|
1484
1513
|
await withTimeout(context.close().catch(() => void 0), 5e3);
|
|
1485
|
-
await session?.close();
|
|
1514
|
+
await launchTarget.session?.close();
|
|
1486
1515
|
}
|
|
1487
1516
|
}
|
|
1488
1517
|
async function replayAgentCassette(cassette, cassettePath, options) {
|
|
@@ -1884,14 +1913,11 @@ function formatStepLabel(step) {
|
|
|
1884
1913
|
function envTruthy(value) {
|
|
1885
1914
|
return value === "1" || value === "true" || value === "yes";
|
|
1886
1915
|
}
|
|
1887
|
-
function
|
|
1888
|
-
|
|
1889
|
-
if ((spec.launch.mode ?? "aitty") !== "aitty") return "launch.mode=url";
|
|
1890
|
-
const command = buildAittyExecCommand(spec.launch);
|
|
1891
|
-
return [command.file, ...command.args].join(" ");
|
|
1916
|
+
function printAgentLaunchPlan(input) {
|
|
1917
|
+
return formatAgentLaunchCommand(normalizeAgentFlowSpec(input).launch);
|
|
1892
1918
|
}
|
|
1893
1919
|
function defaultSpecNameForPath(path) {
|
|
1894
1920
|
return sanitizeArtifactName(basename(path, extname(path)));
|
|
1895
1921
|
}
|
|
1896
1922
|
//#endregion
|
|
1897
|
-
export {
|
|
1923
|
+
export { agentManifestPath as C, writeAgentManifestPath as D, validateAgentManifestFiles as E, AGENT_MANIFEST_FILE_NAME as S, readAgentManifestPath as T, isAgentCassetteLike as _, runAgentSpecPath as a, sanitizeArtifactName as b, agentRunModeSchema as c, readAgentRunRecordPath as d, writeAgentRunRecordPath as f, normalizeAgentFlowSpecWithConfig as g, resolveAgentLaunchTarget as h, runAgentSpec as i, formatAgentArgv as l, createAgentTemplateSpec as m, printAgentLaunchPlan as n, loadAgentSpec as o, formatArgv as p, replayAgentRecordPath as r, AGENT_RUN_RECORD_SCHEMA_URL as s, defaultSpecNameForPath as t, isAgentRunRecordLike as u, readAgentCassettePath as v, isAgentManifestLike as w, launchAgentBrowser as x, normalizeAgentFlowSpec as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ptywright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Terminal/TUI automation driver over PTY + xterm, exposed as MCP tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"exports": {
|
|
38
38
|
".": "./dist/cli.mjs",
|
|
39
39
|
"./agent": "./dist/agent.mjs",
|
|
40
|
+
"./config": "./dist/config.mjs",
|
|
40
41
|
"./mcp": "./dist/mcp.mjs",
|
|
41
42
|
"./pty-cassette": "./dist/pty-cassette.mjs",
|
|
42
43
|
"./session": "./dist/session.mjs",
|
|
@@ -14,31 +14,15 @@
|
|
|
14
14
|
"type": "object",
|
|
15
15
|
"additionalProperties": false,
|
|
16
16
|
"properties": {
|
|
17
|
-
"mode": { "type": "string", "enum": ["
|
|
17
|
+
"mode": { "type": "string", "enum": ["command", "url"] },
|
|
18
18
|
"agentFlavor": { "type": "string", "enum": ["codex", "claude", "droid", "generic"] },
|
|
19
19
|
"command": { "type": "string", "minLength": 1 },
|
|
20
20
|
"args": { "type": "array", "items": { "type": "string" } },
|
|
21
21
|
"cwd": { "type": "string" },
|
|
22
22
|
"env": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
23
23
|
"url": { "type": "string" },
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"additionalProperties": false,
|
|
27
|
-
"properties": {
|
|
28
|
-
"command": { "type": "string", "minLength": 1 },
|
|
29
|
-
"args": { "type": "array", "items": { "type": "string" } },
|
|
30
|
-
"project": { "type": "string", "minLength": 1 },
|
|
31
|
-
"label": { "type": "string", "minLength": 1 },
|
|
32
|
-
"title": { "type": "string", "minLength": 1 },
|
|
33
|
-
"subtitle": { "type": "string", "minLength": 1 },
|
|
34
|
-
"theme": { "type": "string", "enum": ["dark", "light", "auto"] },
|
|
35
|
-
"fontSize": { "type": "integer", "minimum": 11, "maximum": 24 },
|
|
36
|
-
"screenMode": { "type": "string", "enum": ["termvision"] },
|
|
37
|
-
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
|
|
38
|
-
"host": { "type": "string", "minLength": 1 },
|
|
39
|
-
"waitForUrlMs": { "type": "integer", "minimum": 1 }
|
|
40
|
-
}
|
|
41
|
-
}
|
|
24
|
+
"urlRegex": { "type": "string", "minLength": 1 },
|
|
25
|
+
"waitForUrlMs": { "type": "integer", "minimum": 1 }
|
|
42
26
|
}
|
|
43
27
|
},
|
|
44
28
|
"viewports": {
|