ptywright 0.2.0 → 0.3.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 CHANGED
@@ -107,7 +107,7 @@ bunx ptywright@latest pty validate tests/cassettes/codex.pty.json
107
107
  bunx ptywright@latest pty inspect tests/cassettes/codex.pty.json
108
108
  ```
109
109
 
110
- External projects do not need to depend on aitty. Use the structural
110
+ External projects do not need a ptywright-specific PTY wrapper. Use the structural
111
111
  `wrapPtyLike` API for `node-pty`/`bun-pty` style objects:
112
112
 
113
113
  ```ts
@@ -308,9 +308,10 @@ Recording artifacts are best for failure diagnosis or manual review; prefer `sna
308
308
 
309
309
  ## Browser Agent Regression
310
310
 
311
- The new destructive path is browser-first: ptywright can launch an agent through
312
- `@aitty/cli`, drive the browser-hosted wterm DOM with Playwright, and persist a
313
- replayable run artifact plus terminal/DOM snapshots.
311
+ The browser-first path is integration-agnostic: ptywright launches any command
312
+ that prints a browser URL, drives the terminal DOM with Playwright, and persists
313
+ a replayable run artifact plus terminal/DOM snapshots. The browser page must
314
+ expose the terminal root as `[data-terminal-root]`.
314
315
 
315
316
  ```bash
316
317
  # First run records snapshots, screenshots, replay metadata, and report.
@@ -379,9 +380,39 @@ Artifacts are split intentionally:
379
380
  - `tests/agent-snapshots/<name>/` contains stable terminal/DOM baselines.
380
381
  - `--update-snapshots` is the explicit update path for intentional UI changes.
381
382
 
382
- `launch.mode=aitty` runs `aitty exec --launch print -- <agent>`. By default
383
- ptywright resolves the sibling `../aitty/packages/cli/dist/cli.js`; set
384
- `PTYWRIGHT_AITTY_CLI` or `launch.aitty.command` to override it.
383
+ `launch.mode=command` is the recommended integration contract. `command` and
384
+ `args` are spawned directly, and ptywright reads the first URL printed to stdout
385
+ or stderr. Use `waitForUrlMs` to tune startup timeouts and `urlRegex` when the
386
+ URL is embedded in structured output. Set `launch.agentFlavor` explicitly when
387
+ the command is a wrapper, so mask presets still match the underlying agent.
388
+
389
+ `launch.mode=url` skips process launch and points ptywright at an already
390
+ running browser terminal.
391
+
392
+ A wrapper integration is just a normal command that prints its browser URL:
393
+
394
+ ```json
395
+ {
396
+ "name": "codex_browser_replay",
397
+ "launch": {
398
+ "mode": "command",
399
+ "agentFlavor": "codex",
400
+ "command": "node_modules/.bin/browser-terminal-launcher",
401
+ "args": [
402
+ "--replay",
403
+ "test/recordings/codex-yolo.pty.json",
404
+ "--speed",
405
+ "0",
406
+ "--print-url"
407
+ ],
408
+ "waitForUrlMs": 15000
409
+ },
410
+ "steps": [
411
+ { "type": "waitForStableDom" },
412
+ { "type": "snapshot", "name": "codex", "targets": ["terminal", "dom"] }
413
+ ]
414
+ }
415
+ ```
385
416
 
386
417
  Set `launch.agentFlavor` to `codex`, `claude`, `droid`, or `generic` to opt
387
418
  into built-in mask presets for timestamps, generated ids, model names, token
package/dist/agent.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as runAgentSpecPath, i as runAgentSpec, n as printAittyLaunchPlan, r as replayAgentRecordPath, t as defaultSpecNameForPath } from "./runner-DzZlFrt1.mjs";
2
- export { defaultSpecNameForPath, printAittyLaunchPlan, replayAgentRecordPath, runAgentSpec, runAgentSpecPath };
1
+ import { a as runAgentSpecPath, i as runAgentSpec, n as printAgentLaunchPlan, r as replayAgentRecordPath, t as defaultSpecNameForPath } from "./runner-zi0nItvB.mjs";
2
+ export { defaultSpecNameForPath, printAgentLaunchPlan, replayAgentRecordPath, runAgentSpec, runAgentSpecPath };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { t as main } from "../cli-DIUx2w6X.mjs";
2
+ import { t as main } from "../cli-CfvlbRoZ.mjs";
3
3
  //#region src/bin/ptywright.ts
4
4
  await main();
5
5
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { c as createDefaultPtyAdapter, l as resolvePtyBackend } from "./runner-zApMYWZx.mjs";
2
- import { a as readScriptManifestPath, c as resolveScriptManifestPath, d as resolveScriptRunSummaryPath, f as runScriptPath, i as findScriptSummaryManifest, l as validateScriptManifest, n as runAllScripts, o as relocateScriptManifestCommands, s as resolveManifestPrimaryPath$1, t as createPtywrightServer, u as readScriptRunSummaryPath } from "./server-VHuEWWj_.mjs";
3
- import { C as isAgentManifestLike, E as writeAgentManifestPath, S as agentManifestPath, T as validateAgentManifestFiles, _ as normalizeAgentFlowSpec, a as runAgentSpecPath, b as launchAittyBrowserSession, c as agentRunModeSchema, d as readAgentRunRecordPath, f as writeAgentRunRecordPath, g as readAgentCassettePath, h as isAgentCassetteLike, l as formatAgentArgv, m as createAgentTemplateSpec, o as loadAgentSpec, p as formatArgv, r as replayAgentRecordPath, s as AGENT_RUN_RECORD_SCHEMA_URL, u as isAgentRunRecordLike, v as sanitizeArtifactName, w as readAgentManifestPath, x as AGENT_MANIFEST_FILE_NAME, y as launchAgentBrowser } from "./runner-DzZlFrt1.mjs";
2
+ import { a as readScriptManifestPath, c as resolveScriptManifestPath, d as resolveScriptRunSummaryPath, f as runScriptPath, i as findScriptSummaryManifest, l as validateScriptManifest, n as runAllScripts, o as relocateScriptManifestCommands, s as resolveManifestPrimaryPath$1, t as createPtywrightServer, u as readScriptRunSummaryPath } from "./server-BC3yo-dq.mjs";
3
+ import { C as isAgentManifestLike, E as writeAgentManifestPath, S as agentManifestPath, T as validateAgentManifestFiles, _ as readAgentCassettePath, a as runAgentSpecPath, b as launchAgentBrowser, c as agentRunModeSchema, d as readAgentRunRecordPath, f as writeAgentRunRecordPath, g as isAgentCassetteLike, h as resolveAgentLaunchTarget, l as formatAgentArgv, m as createAgentTemplateSpec, o as loadAgentSpec, p as formatArgv, r as replayAgentRecordPath, s as AGENT_RUN_RECORD_SCHEMA_URL, u as isAgentRunRecordLike, v as normalizeAgentFlowSpec, w as readAgentManifestPath, x as AGENT_MANIFEST_FILE_NAME, y as sanitizeArtifactName } from "./runner-zi0nItvB.mjs";
4
4
  import { c as createPtyCassetteReplay, i as formatPtyCassetteInspectLines, l as readPtyCassettePath, o as inspectPtyCassettePath, r as createPtyCassetteRecorder, t as wrapPtyLike, v as validatePtyCassette } from "./pty_like-Cpkh_O9B.mjs";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
@@ -2135,13 +2135,11 @@ async function recordAgentSpecPath(specPath, options) {
2135
2135
  async function recordAgentSpec(input, options) {
2136
2136
  const spec = normalizeAgentFlowSpec(input);
2137
2137
  const rootDir = options.rootDir ? resolve(process.cwd(), options.rootDir) : process.cwd();
2138
- const launchMode = spec.launch.mode ?? (spec.launch.url ? "url" : "aitty");
2139
2138
  const outPath = isAbsolute(options.outPath) ? options.outPath : resolve(process.cwd(), options.outPath);
2140
2139
  const durationMs = options.durationMs ?? 3e4;
2141
2140
  const steps = [];
2142
2141
  let browser = null;
2143
- const session = launchMode === "aitty" ? await launchAittyBrowserSession(spec.launch, { rootDir }) : null;
2144
- const url = launchMode === "url" ? spec.launch.url : session.url;
2142
+ const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
2145
2143
  try {
2146
2144
  browser = await launchAgentBrowser({ headless: options.headless ?? false });
2147
2145
  const viewport = spec.viewports?.[0] ?? {
@@ -2160,7 +2158,7 @@ async function recordAgentSpec(input, options) {
2160
2158
  });
2161
2159
  const page = await context.newPage();
2162
2160
  await installRecorderHooks(page);
2163
- await page.goto(url, {
2161
+ await page.goto(launchTarget.url, {
2164
2162
  waitUntil: "domcontentloaded",
2165
2163
  timeout: spec.defaults?.timeoutMs ?? 3e4
2166
2164
  });
@@ -2198,19 +2196,19 @@ async function recordAgentSpec(input, options) {
2198
2196
  ok: true,
2199
2197
  outPath,
2200
2198
  stepCount: recorded.steps.length,
2201
- url
2199
+ url: launchTarget.url
2202
2200
  };
2203
2201
  } catch (error) {
2204
2202
  return {
2205
2203
  ok: false,
2206
2204
  outPath,
2207
2205
  stepCount: steps.length,
2208
- url,
2206
+ url: launchTarget.url,
2209
2207
  error: error instanceof Error ? error.message : String(error)
2210
2208
  };
2211
2209
  } finally {
2212
2210
  await browser?.close();
2213
- await session?.close();
2211
+ await launchTarget.session?.close();
2214
2212
  }
2215
2213
  }
2216
2214
  async function installRecorderHooks(page) {
package/dist/cli.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as main } from "./cli-DIUx2w6X.mjs";
1
+ import { t as main } from "./cli-CfvlbRoZ.mjs";
2
2
  export { main };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createPtywrightServer } from "./server-VHuEWWj_.mjs";
1
+ import { t as createPtywrightServer } from "./server-BC3yo-dq.mjs";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  //#region src/index.ts
4
4
  const { server, sessions } = createPtywrightServer();
package/dist/mcp.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createPtywrightServer } from "./server-VHuEWWj_.mjs";
1
+ import { t as createPtywrightServer } from "./server-BC3yo-dq.mjs";
2
2
  export { createPtywrightServer };
@@ -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 { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
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: z.enum(["aitty", "url"]).optional(),
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
- aitty: z.object({
408
- command: z.string().min(1).optional(),
409
- args: z.array(z.string()).optional(),
410
- project: z.string().min(1).optional(),
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 === "aitty" && !value.command) ctx.addIssue({
293
+ if (mode === "command" && !value.command) ctx.addIssue({
432
294
  code: z.ZodIssueCode.custom,
433
- message: "launch.command is required when launch.mode is 'aitty'"
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,127 @@ function escapeHtml$1(input) {
759
624
  return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
760
625
  }
761
626
  //#endregion
627
+ //#region src/agent/command_launch.ts
628
+ const DEFAULT_URL_REGEX = /https?:\/\/[^\s"'<>]+/;
629
+ function buildCommandLaunchCommand(launch, options = {}) {
630
+ if (!launch.command) throw new Error("launch.command is required when launch.mode is 'command'");
631
+ const rootDir = options.rootDir ?? process.cwd();
632
+ const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
633
+ return {
634
+ file: launch.command,
635
+ args: launch.args ?? [],
636
+ cwd,
637
+ env: {
638
+ ...options.env ?? process.env,
639
+ ...launch.env
640
+ },
641
+ label: launch.command,
642
+ urlRegex: launch.urlRegex,
643
+ waitForUrlMs: launch.waitForUrlMs
644
+ };
645
+ }
646
+ async function launchBrowserSessionFromCommand(command) {
647
+ const timeoutMs = command.waitForUrlMs ?? 15e3;
648
+ const child = spawn(command.file, command.args, {
649
+ cwd: command.cwd,
650
+ env: command.env,
651
+ stdio: [
652
+ "ignore",
653
+ "pipe",
654
+ "pipe"
655
+ ]
656
+ });
657
+ const stdoutChunks = [];
658
+ const stderrChunks = [];
659
+ return {
660
+ url: await new Promise((resolveUrl, reject) => {
661
+ let settled = false;
662
+ const timer = setTimeout(() => {
663
+ 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()}`));
664
+ }, timeoutMs);
665
+ const finish = (result) => {
666
+ if (settled) return;
667
+ settled = true;
668
+ clearTimeout(timer);
669
+ child.stdout.off("data", onStdout);
670
+ child.stderr.off("data", onStderr);
671
+ child.off("error", onError);
672
+ child.off("exit", onExit);
673
+ if (result instanceof Error) reject(result);
674
+ else resolveUrl(result);
675
+ };
676
+ const readUrl = () => {
677
+ const found = extractUrlFromOutput(`${stdoutChunks.join("")}\n${stderrChunks.join("")}`, command.urlRegex);
678
+ if (found) finish(found);
679
+ };
680
+ const onStdout = (chunk) => {
681
+ stdoutChunks.push(chunk.toString("utf8"));
682
+ readUrl();
683
+ };
684
+ const onStderr = (chunk) => {
685
+ stderrChunks.push(chunk.toString("utf8"));
686
+ readUrl();
687
+ };
688
+ const onError = (error) => finish(error);
689
+ const onExit = (code, signal) => {
690
+ 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()}`));
691
+ };
692
+ child.stdout.on("data", onStdout);
693
+ child.stderr.on("data", onStderr);
694
+ child.once("error", onError);
695
+ child.once("exit", onExit);
696
+ }),
697
+ process: child,
698
+ close: () => closeChild(child)
699
+ };
700
+ }
701
+ function extractUrlFromOutput(output, regexSource) {
702
+ if (!regexSource) return output.match(DEFAULT_URL_REGEX)?.[0] ?? null;
703
+ const match = output.match(new RegExp(regexSource, "m"));
704
+ return match?.[1] ?? match?.[0] ?? null;
705
+ }
706
+ function formatBrowserLaunchCommand(command) {
707
+ return [command.file, ...command.args].join(" ");
708
+ }
709
+ async function closeChild(child) {
710
+ if (child.exitCode !== null || child.signalCode !== null) return;
711
+ await new Promise((resolveClose) => {
712
+ const timer = setTimeout(() => {
713
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
714
+ resolveClose();
715
+ }, 2e3);
716
+ child.once("exit", () => {
717
+ clearTimeout(timer);
718
+ resolveClose();
719
+ });
720
+ child.kill("SIGTERM");
721
+ });
722
+ }
723
+ //#endregion
724
+ //#region src/agent/launch.ts
725
+ function buildAgentLaunchCommand(launch, options = {}) {
726
+ if (resolveAgentLaunchMode(launch) === "url") return null;
727
+ return buildCommandLaunchCommand(launch, options);
728
+ }
729
+ async function resolveAgentLaunchTarget(launch, options = {}) {
730
+ const mode = resolveAgentLaunchMode(launch);
731
+ if (mode === "url") return {
732
+ mode,
733
+ url: launch.url,
734
+ session: null
735
+ };
736
+ const session = await launchBrowserSessionFromCommand(buildCommandLaunchCommand(launch, options));
737
+ return {
738
+ mode,
739
+ url: session.url,
740
+ session
741
+ };
742
+ }
743
+ function formatAgentLaunchCommand(launch) {
744
+ const command = buildAgentLaunchCommand(launch);
745
+ return command ? formatBrowserLaunchCommand(command) : "launch.mode=url";
746
+ }
747
+ //#endregion
762
748
  //#region src/agent/presets.ts
763
749
  const COMMON_AGENT_MASKS = [
764
750
  {
@@ -849,19 +835,15 @@ function createAgentTemplateSpec(flavor) {
849
835
  artifactsDir: `.tmp/agent/${name}`,
850
836
  snapshotDir: `tests/agent-snapshots/${name}`,
851
837
  launch: {
852
- mode: "aitty",
838
+ mode: "command",
853
839
  agentFlavor: flavor,
854
- command,
855
- args: [],
856
- aitty: {
857
- project: "ptywright",
858
- label: command,
859
- title: `${command} browser smoke`,
860
- subtitle: "browser-hosted terminal agent regression",
861
- theme: "light",
862
- fontSize: 14,
863
- waitForUrlMs: 15e3
864
- }
840
+ command: "your-browser-terminal-launcher",
841
+ args: [
842
+ "--agent",
843
+ command,
844
+ "--print-url"
845
+ ],
846
+ waitForUrlMs: 15e3
865
847
  },
866
848
  viewports: DEFAULT_VIEWPORTS.map((viewport) => ({ ...viewport })),
867
849
  defaults: {
@@ -1412,9 +1394,7 @@ async function runAgentSpec(input, options = {}) {
1412
1394
  }
1413
1395
  async function runViewport(args) {
1414
1396
  const { browser, spec, viewport, rootDir, artifactsDir, snapshotDir, updateSnapshots, recordCassette, cassette, result } = args;
1415
- const launchMode = spec.launch.mode ?? (spec.launch.url ? "url" : "aitty");
1416
- const session = launchMode === "aitty" ? await launchAittyBrowserSession(spec.launch, { rootDir }) : null;
1417
- const url = launchMode === "url" ? spec.launch.url : session.url;
1397
+ const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
1418
1398
  const context = await browser.newContext({
1419
1399
  viewport: {
1420
1400
  width: viewport.width,
@@ -1440,7 +1420,7 @@ async function runViewport(args) {
1440
1420
  nextReplayPhase: 0
1441
1421
  };
1442
1422
  try {
1443
- await page.goto(url, {
1423
+ await page.goto(launchTarget.url, {
1444
1424
  waitUntil: "domcontentloaded",
1445
1425
  timeout: spec.defaults?.timeoutMs ?? 3e4
1446
1426
  });
@@ -1482,7 +1462,7 @@ async function runViewport(args) {
1482
1462
  }
1483
1463
  } finally {
1484
1464
  await withTimeout(context.close().catch(() => void 0), 5e3);
1485
- await session?.close();
1465
+ await launchTarget.session?.close();
1486
1466
  }
1487
1467
  }
1488
1468
  async function replayAgentCassette(cassette, cassettePath, options) {
@@ -1884,14 +1864,11 @@ function formatStepLabel(step) {
1884
1864
  function envTruthy(value) {
1885
1865
  return value === "1" || value === "true" || value === "yes";
1886
1866
  }
1887
- function printAittyLaunchPlan(input) {
1888
- const spec = normalizeAgentFlowSpec(input);
1889
- if ((spec.launch.mode ?? "aitty") !== "aitty") return "launch.mode=url";
1890
- const command = buildAittyExecCommand(spec.launch);
1891
- return [command.file, ...command.args].join(" ");
1867
+ function printAgentLaunchPlan(input) {
1868
+ return formatAgentLaunchCommand(normalizeAgentFlowSpec(input).launch);
1892
1869
  }
1893
1870
  function defaultSpecNameForPath(path) {
1894
1871
  return sanitizeArtifactName(basename(path, extname(path)));
1895
1872
  }
1896
1873
  //#endregion
1897
- export { isAgentManifestLike as C, writeAgentManifestPath as E, agentManifestPath as S, validateAgentManifestFiles as T, normalizeAgentFlowSpec as _, runAgentSpecPath as a, launchAittyBrowserSession as b, agentRunModeSchema as c, readAgentRunRecordPath as d, writeAgentRunRecordPath as f, readAgentCassettePath as g, isAgentCassetteLike as h, runAgentSpec as i, formatAgentArgv as l, createAgentTemplateSpec as m, printAittyLaunchPlan as n, loadAgentSpec as o, formatArgv as p, replayAgentRecordPath as r, AGENT_RUN_RECORD_SCHEMA_URL as s, defaultSpecNameForPath as t, isAgentRunRecordLike as u, sanitizeArtifactName as v, readAgentManifestPath as w, AGENT_MANIFEST_FILE_NAME as x, launchAgentBrowser as y };
1874
+ export { isAgentManifestLike as C, writeAgentManifestPath as E, agentManifestPath as S, validateAgentManifestFiles as T, readAgentCassettePath as _, runAgentSpecPath as a, launchAgentBrowser as b, agentRunModeSchema as c, readAgentRunRecordPath as d, writeAgentRunRecordPath as f, isAgentCassetteLike 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, normalizeAgentFlowSpec as v, readAgentManifestPath as w, AGENT_MANIFEST_FILE_NAME as x, sanitizeArtifactName as y };
@@ -2081,7 +2081,7 @@ function joinPosix(a, b) {
2081
2081
  }
2082
2082
  //#endregion
2083
2083
  //#region package.json
2084
- var version = "0.2.0";
2084
+ var version = "0.3.0";
2085
2085
  //#endregion
2086
2086
  //#region src/mcp/server.ts
2087
2087
  const textMaskRuleSchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ptywright",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal/TUI automation driver over PTY + xterm, exposed as MCP tools",
5
5
  "keywords": [
6
6
  "agent",
@@ -14,31 +14,15 @@
14
14
  "type": "object",
15
15
  "additionalProperties": false,
16
16
  "properties": {
17
- "mode": { "type": "string", "enum": ["aitty", "url"] },
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
- "aitty": {
25
- "type": "object",
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": {