studioflow 0.1.0 → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 StudioFlow contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -7513,29 +7513,37 @@ async function assertVisible(page, selector, timeoutMs = 5e3) {
7513
7513
  function wait(ms) {
7514
7514
  return new Promise((resolve) => setTimeout(resolve, ms));
7515
7515
  }
7516
- function boolEnv(name, fallback) {
7517
- const raw = process.env[name];
7516
+ function boolEnv(name, fallback, env = process.env) {
7517
+ const raw = env[name];
7518
7518
  if (!raw) return fallback;
7519
7519
  const normalized = raw.trim().toLowerCase();
7520
7520
  if (["1", "true", "yes", "on"].includes(normalized)) return true;
7521
7521
  if (["0", "false", "no", "off"].includes(normalized)) return false;
7522
7522
  return fallback;
7523
7523
  }
7524
- function intEnv(name, fallback) {
7525
- const raw = process.env[name];
7524
+ function intEnv(name, fallback, env = process.env) {
7525
+ const raw = env[name];
7526
7526
  if (!raw) return fallback;
7527
7527
  const parsed = Number(raw);
7528
7528
  return Number.isFinite(parsed) ? parsed : fallback;
7529
7529
  }
7530
- var renderCursorOverlay = boolEnv("STUDIOFLOW_RENDER_CURSOR", true);
7531
- var defaultCursorMoveMs = intEnv("STUDIOFLOW_CURSOR_MOVE_MS", 320);
7532
- var defaultCursorHighlightMs = intEnv("STUDIOFLOW_CURSOR_HIGHLIGHT_MS", 120);
7533
- var realisticTyping = boolEnv("STUDIOFLOW_REALISTIC_TYPING", true);
7534
- var typingDelayMs = Math.max(0, intEnv("STUDIOFLOW_TYPING_DELAY_MS", 35));
7535
- var pacingAdjustmentEnabled = boolEnv("STUDIOFLOW_PACING_ADJUSTMENT", true);
7536
- var pacingJitterEnabled = boolEnv("STUDIOFLOW_PACING_JITTER", true);
7537
7530
  var maxDelayMs = 6e4;
7538
- var cursorOverlaySetupScript = `
7531
+ function resolveRuntimePacingDefaults(env = process.env) {
7532
+ return {
7533
+ renderCursorOverlay: boolEnv("STUDIOFLOW_RENDER_CURSOR", true, env),
7534
+ cursorMoveMs: Math.max(0, intEnv("STUDIOFLOW_CURSOR_MOVE_MS", 430, env)),
7535
+ cursorHighlightMs: Math.max(0, intEnv("STUDIOFLOW_CURSOR_HIGHLIGHT_MS", 170, env)),
7536
+ realisticTyping: boolEnv("STUDIOFLOW_REALISTIC_TYPING", true, env),
7537
+ typingDelayMs: Math.max(0, intEnv("STUDIOFLOW_TYPING_DELAY_MS", 55, env)),
7538
+ clickPulseMs: Math.max(0, intEnv("STUDIOFLOW_CLICK_PULSE_MS", 220, env)),
7539
+ stepPreDelayMs: Math.max(0, intEnv("STUDIOFLOW_STEP_PRE_DELAY_MS", 90, env)),
7540
+ stepPostDelayMs: Math.max(0, intEnv("STUDIOFLOW_STEP_POST_DELAY_MS", 130, env)),
7541
+ stepDwellMs: Math.max(0, intEnv("STUDIOFLOW_STEP_DWELL_MS", 180, env)),
7542
+ pacingAdjustmentEnabled: boolEnv("STUDIOFLOW_PACING_ADJUSTMENT", true, env),
7543
+ pacingJitterEnabled: boolEnv("STUDIOFLOW_PACING_JITTER", true, env)
7544
+ };
7545
+ }
7546
+ var cursorOverlaySetupScript = (clickPulseMs) => `
7539
7547
  (() => {
7540
7548
  const win = window;
7541
7549
  if (win.__studioflowCursor) return;
@@ -7548,29 +7556,32 @@ var cursorOverlaySetupScript = `
7548
7556
  position: fixed;
7549
7557
  top: 0;
7550
7558
  left: 0;
7551
- width: 16px;
7552
- height: 16px;
7553
- border-radius: 999px;
7554
- border: 2px solid rgba(2, 6, 23, 0.9);
7555
- background: rgba(255, 255, 255, 0.78);
7556
- box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.95), 0 4px 14px rgba(2, 6, 23, 0.28);
7559
+ width: 22px;
7560
+ height: 30px;
7561
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='30' viewBox='0 0 22 30'%3E%3Cpath d='M1.5 1.5V22.2L7.4 17.4L11 28.5L14.4 27L10.8 15.9H19.6L1.5 1.5Z' fill='white' stroke='%23000' stroke-width='1.5' stroke-linejoin='round'/%3E%3C/svg%3E");
7562
+ background-repeat: no-repeat;
7563
+ background-size: 22px 30px;
7557
7564
  pointer-events: none;
7558
7565
  z-index: 2147483647;
7559
- transform: translate3d(8px, 8px, 0);
7566
+ filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.55));
7567
+ transform: translate3d(16px, 16px, 0);
7560
7568
  transition-property: transform;
7561
7569
  transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
7562
7570
  }
7563
7571
  #studioflow-cursor::after {
7564
7572
  content: "";
7565
7573
  position: absolute;
7566
- inset: -8px;
7574
+ left: -8px;
7575
+ top: -8px;
7576
+ width: 20px;
7577
+ height: 20px;
7567
7578
  border-radius: 999px;
7568
7579
  border: 2px solid rgba(59, 130, 246, 0.72);
7569
7580
  opacity: 0;
7570
7581
  transform: scale(0.65);
7571
7582
  }
7572
7583
  #studioflow-cursor.studioflow-cursor-pulse::after {
7573
- animation: studioflow-cursor-pulse 300ms ease-out;
7584
+ animation: studioflow-cursor-pulse ${Math.max(0, clickPulseMs)}ms ease-out;
7574
7585
  }
7575
7586
  @keyframes studioflow-cursor-pulse {
7576
7587
  0% { opacity: 0.9; transform: scale(0.55); }
@@ -7591,7 +7602,7 @@ var cursorOverlaySetupScript = `
7591
7602
  x = nextX;
7592
7603
  y = nextY;
7593
7604
  cursor.style.transitionDuration = \`\${Math.max(0, durationMs)}ms\`;
7594
- cursor.style.transform = \`translate3d(\${x - 8}px, \${y - 8}px, 0)\`;
7605
+ cursor.style.transform = \`translate3d(\${x}px, \${y}px, 0)\`;
7595
7606
  };
7596
7607
 
7597
7608
  win.__studioflowCursor = {
@@ -7631,40 +7642,40 @@ function jitterFactor(seed) {
7631
7642
  const normalized = hash / 4294967295;
7632
7643
  return normalized * 0.24 - 0.12;
7633
7644
  }
7634
- function resolvePacedDelay(rawMs, context, channel, allowJitter = true) {
7645
+ function resolvePacedDelay(rawMs, context, runtime, channel, allowJitter = true) {
7635
7646
  if (!rawMs || rawMs <= 0) return 0;
7636
7647
  let value = rawMs;
7637
- const multiplier = pacingAdjustmentEnabled ? context.pacingMultiplier ?? 1 : 1;
7648
+ const multiplier = runtime.pacingAdjustmentEnabled ? context.pacingMultiplier ?? 1 : 1;
7638
7649
  value *= multiplier;
7639
- const shouldApplyJitter = allowJitter && pacingJitterEnabled && !context.strictPacing && Boolean(context.jitterSeed);
7650
+ const shouldApplyJitter = allowJitter && runtime.pacingJitterEnabled && !context.strictPacing && Boolean(context.jitterSeed);
7640
7651
  if (shouldApplyJitter) {
7641
7652
  value *= 1 + jitterFactor(`${context.jitterSeed}:${channel}`);
7642
7653
  }
7643
7654
  return Math.max(0, Math.min(maxDelayMs, Math.round(value)));
7644
7655
  }
7645
- async function ensureCursorOverlay(page) {
7646
- if (!renderCursorOverlay) return;
7647
- await page.evaluate(cursorOverlaySetupScript);
7656
+ async function ensureCursorOverlay(page, runtime) {
7657
+ if (!runtime.renderCursorOverlay) return;
7658
+ await page.evaluate(cursorOverlaySetupScript(runtime.clickPulseMs));
7648
7659
  }
7649
7660
  async function getTargetCenter(page, target) {
7650
7661
  const box = await page.locator(target).first().boundingBox();
7651
7662
  if (!box) return null;
7652
7663
  return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
7653
7664
  }
7654
- async function moveCursor(page, point, durationMs) {
7665
+ async function moveCursor(page, point, durationMs, runtime) {
7655
7666
  await page.mouse.move(point.x, point.y, { steps: Math.max(8, Math.floor(durationMs / 16)) });
7656
- if (renderCursorOverlay) {
7657
- await ensureCursorOverlay(page);
7667
+ if (runtime.renderCursorOverlay) {
7668
+ await ensureCursorOverlay(page, runtime);
7658
7669
  await page.evaluate(cursorMoveScript(point, durationMs));
7659
7670
  }
7660
7671
  }
7661
- async function clickPulse(page) {
7662
- if (!renderCursorOverlay) return;
7663
- await ensureCursorOverlay(page);
7672
+ async function clickPulse(page, runtime) {
7673
+ if (!runtime.renderCursorOverlay) return;
7674
+ await ensureCursorOverlay(page, runtime);
7664
7675
  await page.evaluate(cursorClickPulseScript);
7665
7676
  }
7666
- async function applyPreStepPacing(page, step, context) {
7667
- const preDelay = resolvePacedDelay(step.preDelayMs, context, "pre");
7677
+ async function applyPreStepPacing(page, step, context, runtime) {
7678
+ const preDelay = resolvePacedDelay(step.preDelayMs ?? runtime.stepPreDelayMs, context, runtime, "pre");
7668
7679
  if (preDelay > 0) {
7669
7680
  await wait(preDelay);
7670
7681
  }
@@ -7672,66 +7683,67 @@ async function applyPreStepPacing(page, step, context) {
7672
7683
  if (!shouldMoveCursor || !step.target) return;
7673
7684
  const center = await getTargetCenter(page, step.target);
7674
7685
  if (!center) return;
7675
- const moveMs = resolvePacedDelay(step.mouseMoveMs ?? defaultCursorMoveMs, context, "move");
7686
+ const moveMs = resolvePacedDelay(step.mouseMoveMs ?? runtime.cursorMoveMs, context, runtime, "move");
7676
7687
  if (moveMs > 0) {
7677
- await moveCursor(page, center, moveMs);
7688
+ await moveCursor(page, center, moveMs, runtime);
7678
7689
  }
7679
- const highlightMs = resolvePacedDelay(step.highlightMs ?? defaultCursorHighlightMs, context, "highlight");
7690
+ const highlightMs = resolvePacedDelay(step.highlightMs ?? runtime.cursorHighlightMs, context, runtime, "highlight");
7680
7691
  if (highlightMs > 0) {
7681
7692
  await wait(highlightMs);
7682
7693
  }
7683
7694
  }
7684
- async function applyPostStepPacing(step, context) {
7685
- const postDelay = resolvePacedDelay(step.postDelayMs, context, "post");
7695
+ async function applyPostStepPacing(step, context, runtime) {
7696
+ const postDelay = resolvePacedDelay(step.postDelayMs ?? runtime.stepPostDelayMs, context, runtime, "post");
7686
7697
  if (postDelay > 0) {
7687
7698
  await wait(postDelay);
7688
7699
  }
7689
- const dwellDelay = resolvePacedDelay(step.dwellMs, context, "dwell");
7700
+ const dwellDelay = resolvePacedDelay(step.dwellMs ?? runtime.stepDwellMs, context, runtime, "dwell");
7690
7701
  if (dwellDelay > 0) {
7691
7702
  await wait(dwellDelay);
7692
7703
  }
7693
7704
  }
7694
7705
  async function executeStep(page, step, runDir, baseUrl, context = {}) {
7695
7706
  const timeout = step.timeoutMs ?? 6e3;
7696
- await applyPreStepPacing(page, step, context);
7707
+ const runtime = resolveRuntimePacingDefaults();
7708
+ await applyPreStepPacing(page, step, context, runtime);
7697
7709
  if (step.action === "goto") {
7698
7710
  const target = step.value ?? "/";
7699
7711
  const isAbsolute = /^https?:\/\//.test(target);
7700
7712
  await page.goto(isAbsolute ? target : new URL(target, baseUrl).toString(), { timeout });
7701
- await applyPostStepPacing(step, context);
7713
+ await applyPostStepPacing(step, context, runtime);
7702
7714
  return;
7703
7715
  }
7704
7716
  if (step.action === "click") {
7705
7717
  if (!step.target) throw new Error(`Step ${step.id} missing target`);
7706
7718
  await page.locator(step.target).first().click({ timeout });
7707
- await clickPulse(page);
7708
- await applyPostStepPacing(step, context);
7719
+ await clickPulse(page, runtime);
7720
+ await applyPostStepPacing(step, context, runtime);
7709
7721
  return;
7710
7722
  }
7711
7723
  if (step.action === "type") {
7712
7724
  if (!step.target) throw new Error(`Step ${step.id} missing target`);
7713
7725
  const locator = page.locator(step.target).first();
7714
- if (realisticTyping) {
7726
+ if (runtime.realisticTyping) {
7715
7727
  await locator.click({ timeout });
7716
- await clickPulse(page);
7728
+ await clickPulse(page, runtime);
7717
7729
  await locator.fill("", { timeout });
7718
- const effectiveTypingDelay = resolvePacedDelay(typingDelayMs, context, "typing", false);
7730
+ const effectiveTypingDelay = resolvePacedDelay(runtime.typingDelayMs, context, runtime, "typing", false);
7719
7731
  await page.keyboard.type(step.value ?? "", { delay: effectiveTypingDelay });
7720
7732
  } else {
7721
7733
  await locator.fill(step.value ?? "", { timeout });
7722
7734
  }
7723
- await applyPostStepPacing(step, context);
7735
+ await applyPostStepPacing(step, context, runtime);
7724
7736
  return;
7725
7737
  }
7726
7738
  if (step.action === "wait_for") {
7727
7739
  if (step.target) {
7728
7740
  await page.locator(step.target).first().waitFor({ state: "visible", timeout });
7729
- await applyPostStepPacing(step, context);
7741
+ await applyPostStepPacing(step, context, runtime);
7730
7742
  return;
7731
7743
  }
7732
7744
  if (step.value) {
7733
7745
  await page.getByText(step.value).first().waitFor({ timeout });
7734
- await applyPostStepPacing(step, context);
7746
+ await applyPostStepPacing(step, context, runtime);
7735
7747
  return;
7736
7748
  }
7737
7749
  throw new Error(`Step ${step.id} requires target or value for wait_for`);
@@ -7739,23 +7751,23 @@ async function executeStep(page, step, runDir, baseUrl, context = {}) {
7739
7751
  if (step.action === "assert_text") {
7740
7752
  if (!step.value) throw new Error(`Step ${step.id} missing value`);
7741
7753
  await assertText(page, step.value, timeout);
7742
- await applyPostStepPacing(step, context);
7754
+ await applyPostStepPacing(step, context, runtime);
7743
7755
  return;
7744
7756
  }
7745
7757
  if (step.action === "assert_visible") {
7746
7758
  if (!step.target) throw new Error(`Step ${step.id} missing target`);
7747
7759
  await assertVisible(page, step.target, timeout);
7748
- await applyPostStepPacing(step, context);
7760
+ await applyPostStepPacing(step, context, runtime);
7749
7761
  return;
7750
7762
  }
7751
7763
  if (step.action === "screenshot") {
7752
7764
  const name = step.value ?? `${step.id}.png`;
7753
7765
  await page.screenshot({ path: `${runDir}/screenshots/${name}.png`, fullPage: true });
7754
- await applyPostStepPacing(step, context);
7766
+ await applyPostStepPacing(step, context, runtime);
7755
7767
  return;
7756
7768
  }
7757
7769
  if (["recorder_start", "recorder_stop", "recorder_export"].includes(step.action)) {
7758
- await applyPostStepPacing(step, context);
7770
+ await applyPostStepPacing(step, context, runtime);
7759
7771
  return;
7760
7772
  }
7761
7773
  throw new Error(`Unsupported action: ${step.action}`);
@@ -11982,9 +11994,12 @@ import fs6 from "node:fs/promises";
11982
11994
  import path4 from "node:path";
11983
11995
 
11984
11996
  // ../../packages/artifacts/src/paths.ts
11997
+ import os2 from "node:os";
11985
11998
  import path2 from "node:path";
11986
11999
  function getRunsRoot() {
11987
- const configured = process.env.STUDIOFLOW_RUNS_DIR || ".runs";
12000
+ const dataDir = process.env.STUDIOFLOW_DATA_DIR ?? process.env.STUDIOFLOW_HOME;
12001
+ const defaultRunsDir = dataDir && dataDir.trim() ? path2.join(path2.resolve(dataDir), "runs") : path2.join(os2.homedir(), ".studioflow", "runs");
12002
+ const configured = process.env.STUDIOFLOW_RUNS_DIR || defaultRunsDir;
11988
12003
  if (path2.isAbsolute(configured)) {
11989
12004
  return configured;
11990
12005
  }
@@ -12224,6 +12239,7 @@ async function withRetry(fn, opts = {}) {
12224
12239
  async function runEngine(input) {
12225
12240
  const run2 = await createRunContext();
12226
12241
  let state = "INIT";
12242
+ const shouldExport = input.flows.some((flow) => flow.steps.some((step) => step.action === "recorder_export"));
12227
12243
  const files = {
12228
12244
  events: run2.eventsFile
12229
12245
  };
@@ -12273,10 +12289,12 @@ async function runEngine(input) {
12273
12289
  await emit("recorder.stop.begin");
12274
12290
  await stopRecording();
12275
12291
  await emit("recorder.stop.done");
12276
- state = "EXPORT";
12277
- await emit("recorder.export.begin");
12278
- await exportRecording();
12279
- await emit("recorder.export.done");
12292
+ if (shouldExport) {
12293
+ state = "EXPORT";
12294
+ await emit("recorder.export.begin");
12295
+ await exportRecording();
12296
+ await emit("recorder.export.done");
12297
+ }
12280
12298
  state = "VERIFY_ARTIFACTS";
12281
12299
  const planPath = path4.join(run2.runDir, "plan.json");
12282
12300
  await writeJsonFile(planPath, {
@@ -12354,18 +12372,18 @@ function resolveFromWorkspace(inputPath) {
12354
12372
  }
12355
12373
 
12356
12374
  // src/commands/runtime-paths.ts
12357
- import os2 from "node:os";
12375
+ import os3 from "node:os";
12358
12376
  import path6 from "node:path";
12359
12377
  function getStudioflowDataDir() {
12360
12378
  const configured = process.env.STUDIOFLOW_DATA_DIR ?? process.env.STUDIOFLOW_HOME;
12361
12379
  if (configured && configured.trim()) {
12362
12380
  return path6.resolve(configured);
12363
12381
  }
12364
- return path6.join(os2.homedir(), ".studioflow");
12382
+ return path6.join(os3.homedir(), ".studioflow");
12365
12383
  }
12366
12384
  function resolveAgentHome(envVar, defaultDir) {
12367
12385
  const configured = process.env[envVar];
12368
- return configured && configured.trim() ? path6.resolve(configured) : path6.join(os2.homedir(), defaultDir);
12386
+ return configured && configured.trim() ? path6.resolve(configured) : path6.join(os3.homedir(), defaultDir);
12369
12387
  }
12370
12388
  function getCodexSkillsDir() {
12371
12389
  return path6.join(resolveAgentHome("CODEX_HOME", ".codex"), "skills");
@@ -12511,7 +12529,7 @@ async function resolveRuntimeConfig(overrides = {}) {
12511
12529
  { value: project.config.runsDir, source: "project-config" },
12512
12530
  { value: user.config.runsDir, source: "user-config" }
12513
12531
  ],
12514
- ".runs"
12532
+ path7.join(getStudioflowDataDir(), "runs")
12515
12533
  );
12516
12534
  validateBaseUrl(baseUrl.value);
12517
12535
  return {