@showrun/core 0.1.0 → 0.1.1-b

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.
Files changed (56) hide show
  1. package/dist/__tests__/config.test.d.ts +2 -0
  2. package/dist/__tests__/config.test.d.ts.map +1 -0
  3. package/dist/__tests__/config.test.js +164 -0
  4. package/dist/__tests__/httpReplay.test.d.ts +2 -0
  5. package/dist/__tests__/httpReplay.test.d.ts.map +1 -0
  6. package/dist/__tests__/httpReplay.test.js +306 -0
  7. package/dist/__tests__/requestSnapshot.test.d.ts +2 -0
  8. package/dist/__tests__/requestSnapshot.test.d.ts.map +1 -0
  9. package/dist/__tests__/requestSnapshot.test.js +323 -0
  10. package/dist/browserLauncher.d.ts.map +1 -1
  11. package/dist/browserLauncher.js +7 -1
  12. package/dist/config.d.ts +96 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +268 -0
  15. package/dist/dsl/interpreter.d.ts +9 -0
  16. package/dist/dsl/interpreter.d.ts.map +1 -1
  17. package/dist/dsl/interpreter.js +24 -5
  18. package/dist/dsl/stepHandlers.d.ts +7 -0
  19. package/dist/dsl/stepHandlers.d.ts.map +1 -1
  20. package/dist/dsl/stepHandlers.js +141 -5
  21. package/dist/dsl/templating.d.ts.map +1 -1
  22. package/dist/dsl/templating.js +11 -0
  23. package/dist/dsl/types.d.ts +16 -4
  24. package/dist/dsl/types.d.ts.map +1 -1
  25. package/dist/dsl/validation.d.ts.map +1 -1
  26. package/dist/dsl/validation.js +29 -14
  27. package/dist/httpReplay.d.ts +43 -0
  28. package/dist/httpReplay.d.ts.map +1 -0
  29. package/dist/httpReplay.js +102 -0
  30. package/dist/index.d.ts +4 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +5 -0
  33. package/dist/jsonPackValidator.d.ts.map +1 -1
  34. package/dist/jsonPackValidator.js +12 -3
  35. package/dist/loader.d.ts.map +1 -1
  36. package/dist/loader.js +4 -0
  37. package/dist/networkCapture.d.ts +10 -4
  38. package/dist/networkCapture.d.ts.map +1 -1
  39. package/dist/networkCapture.js +20 -12
  40. package/dist/requestSnapshot.d.ts +91 -0
  41. package/dist/requestSnapshot.d.ts.map +1 -0
  42. package/dist/requestSnapshot.js +200 -0
  43. package/dist/runner.d.ts.map +1 -1
  44. package/dist/runner.js +209 -13
  45. package/dist/storage/index.d.ts +3 -0
  46. package/dist/storage/index.d.ts.map +1 -0
  47. package/dist/storage/index.js +2 -0
  48. package/dist/storage/keys.d.ts +12 -0
  49. package/dist/storage/keys.d.ts.map +1 -0
  50. package/dist/storage/keys.js +39 -0
  51. package/dist/storage/types.d.ts +112 -0
  52. package/dist/storage/types.d.ts.map +1 -0
  53. package/dist/storage/types.js +7 -0
  54. package/dist/types.d.ts +5 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +2 -2
package/dist/config.js ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * System-wide configuration for ShowRun.
3
+ *
4
+ * Layered config discovery:
5
+ * Built-in defaults < global config.json < project config.json < .env < real env vars
6
+ *
7
+ * Config directory search order (lowest → highest priority):
8
+ * Linux/macOS: $XDG_CONFIG_HOME/showrun/ → ~/.showrun/ → ancestor .showrun/ → cwd/.showrun/
9
+ * Windows: %APPDATA%\showrun\ → ancestor .showrun\ → cwd\.showrun\
10
+ */
11
+ import { existsSync, copyFileSync } from 'fs';
12
+ import { resolve, join, parse as parsePath } from 'path';
13
+ import { platform, homedir } from 'os';
14
+ import { cwd } from 'process';
15
+ import { ensureDir, readJsonFile } from './packUtils.js';
16
+ // ── Deep merge helper ──────────────────────────────────────────────────────
17
+ /**
18
+ * Recursively merge `override` into `base`, returning a new object.
19
+ * - Primitives and arrays in override replace base values.
20
+ * - Null/undefined values in override are skipped.
21
+ * - Nested plain objects are merged recursively.
22
+ */
23
+ export function deepMerge(base, override) {
24
+ if (typeof base !== 'object' || base === null ||
25
+ typeof override !== 'object' || override === null ||
26
+ Array.isArray(base) || Array.isArray(override)) {
27
+ return override;
28
+ }
29
+ const result = { ...base };
30
+ const src = override;
31
+ for (const key of Object.keys(src)) {
32
+ const overrideVal = src[key];
33
+ if (overrideVal === null || overrideVal === undefined)
34
+ continue;
35
+ const baseVal = result[key];
36
+ if (typeof baseVal === 'object' && baseVal !== null && !Array.isArray(baseVal) &&
37
+ typeof overrideVal === 'object' && overrideVal !== null && !Array.isArray(overrideVal)) {
38
+ result[key] = deepMerge(baseVal, overrideVal);
39
+ }
40
+ else {
41
+ result[key] = overrideVal;
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ // ── Directory discovery ────────────────────────────────────────────────────
47
+ /**
48
+ * Walk from `startDir` up toward the filesystem root, collecting every
49
+ * `.showrun/` directory encountered. Returns in bottom-up order (closest
50
+ * ancestor first) which is the *highest* priority order.
51
+ */
52
+ function walkUpForShowrunDirs(startDir) {
53
+ const dirs = [];
54
+ let dir = resolve(startDir);
55
+ const root = parsePath(dir).root;
56
+ // Skip cwd itself — handled separately
57
+ dir = resolve(dir, '..');
58
+ while (dir !== root && dir.length > root.length) {
59
+ const candidate = join(dir, '.showrun');
60
+ if (existsSync(candidate)) {
61
+ dirs.push(candidate);
62
+ }
63
+ dir = resolve(dir, '..');
64
+ }
65
+ // Reverse so that farthest ancestor is first (lowest priority)
66
+ return dirs.reverse();
67
+ }
68
+ /**
69
+ * Returns an ordered list of config directories to search, lowest priority first.
70
+ */
71
+ export function discoverConfigDirs() {
72
+ const dirs = [];
73
+ const os = platform();
74
+ const home = homedir();
75
+ const currentDir = cwd();
76
+ if (os === 'win32') {
77
+ // Windows: %APPDATA%\showrun
78
+ const appData = process.env.APPDATA;
79
+ if (appData) {
80
+ dirs.push(join(appData, 'showrun'));
81
+ }
82
+ }
83
+ else {
84
+ // Linux/macOS: XDG_CONFIG_HOME/showrun (default ~/.config/showrun)
85
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(home, '.config');
86
+ dirs.push(join(xdgConfig, 'showrun'));
87
+ // ~/.showrun
88
+ dirs.push(join(home, '.showrun'));
89
+ }
90
+ // Ancestor .showrun/ directories (farthest ancestor first = lowest priority)
91
+ dirs.push(...walkUpForShowrunDirs(currentDir));
92
+ // cwd/.showrun (highest priority)
93
+ dirs.push(join(currentDir, '.showrun'));
94
+ return dirs;
95
+ }
96
+ // ── Config loading ─────────────────────────────────────────────────────────
97
+ const DEFAULT_CONFIG = {};
98
+ /**
99
+ * Load all `config.json` files from discovered directories, merge them, and
100
+ * return the result along with metadata about which files were loaded.
101
+ */
102
+ export function loadConfig() {
103
+ const searchedDirs = discoverConfigDirs();
104
+ let merged = { ...DEFAULT_CONFIG };
105
+ const loadedFiles = [];
106
+ for (const dir of searchedDirs) {
107
+ const configPath = join(dir, 'config.json');
108
+ if (existsSync(configPath)) {
109
+ try {
110
+ const fileConfig = readJsonFile(configPath);
111
+ merged = deepMerge(merged, fileConfig);
112
+ loadedFiles.push(configPath);
113
+ }
114
+ catch (err) {
115
+ console.warn(`[Config] Failed to load ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ }
118
+ }
119
+ return { config: merged, loadedFiles, searchedDirs };
120
+ }
121
+ // ── Env var mapping ────────────────────────────────────────────────────────
122
+ /** Map of config path → env var name */
123
+ const CONFIG_TO_ENV = [
124
+ { path: ['llm', 'provider'], envVar: 'LLM_PROVIDER' },
125
+ { path: ['llm', 'anthropic', 'apiKey'], envVar: 'ANTHROPIC_API_KEY' },
126
+ { path: ['llm', 'anthropic', 'model'], envVar: 'ANTHROPIC_MODEL' },
127
+ { path: ['llm', 'anthropic', 'baseUrl'], envVar: 'ANTHROPIC_BASE_URL' },
128
+ { path: ['llm', 'openai', 'apiKey'], envVar: 'OPENAI_API_KEY' },
129
+ { path: ['llm', 'openai', 'model'], envVar: 'OPENAI_MODEL' },
130
+ { path: ['llm', 'openai', 'baseUrl'], envVar: 'OPENAI_BASE_URL' },
131
+ { path: ['agent', 'maxBrowserRounds'], envVar: 'MAX_BROWSER_ROUNDS' },
132
+ { path: ['agent', 'debug'], envVar: 'SHOWRUN_DEBUG' },
133
+ { path: ['agent', 'transcriptLogging'], envVar: 'SHOWRUN_TRANSCRIPT_LOGGING' },
134
+ { path: ['prompts', 'teachChatSystemPrompt'], envVar: 'TEACH_CHAT_SYSTEM_PROMPT' },
135
+ { path: ['prompts', 'explorationAgentPromptPath'], envVar: 'EXPLORATION_AGENT_PROMPT_PATH' },
136
+ { path: ['techniques', 'vectorStore', 'provider'], envVar: 'VECTORSTORE_PROVIDER' },
137
+ { path: ['techniques', 'vectorStore', 'url'], envVar: 'WEAVIATE_URL' },
138
+ { path: ['techniques', 'vectorStore', 'apiKey'], envVar: 'WEAVIATE_API_KEY' },
139
+ { path: ['techniques', 'vectorStore', 'vectorizer'], envVar: 'WEAVIATE_VECTORIZER' },
140
+ { path: ['techniques', 'embedding', 'apiKey'], envVar: 'EMBEDDING_API_KEY' },
141
+ { path: ['techniques', 'embedding', 'model'], envVar: 'EMBEDDING_MODEL' },
142
+ { path: ['techniques', 'embedding', 'baseUrl'], envVar: 'EMBEDDING_BASE_URL' },
143
+ { path: ['techniques', 'collectionName'], envVar: 'TECHNIQUES_COLLECTION' },
144
+ ];
145
+ function getNestedValue(obj, path) {
146
+ let current = obj;
147
+ for (const key of path) {
148
+ if (current === null || current === undefined || typeof current !== 'object')
149
+ return undefined;
150
+ current = current[key];
151
+ }
152
+ return current;
153
+ }
154
+ /**
155
+ * Apply config values to `process.env`, only setting vars that are not already present.
156
+ */
157
+ export function applyConfigToEnv(config) {
158
+ for (const { path, envVar } of CONFIG_TO_ENV) {
159
+ if (process.env[envVar])
160
+ continue; // real env / dotenv takes precedence
161
+ const value = getNestedValue(config, path);
162
+ if (value !== undefined && value !== null) {
163
+ process.env[envVar] = String(value);
164
+ }
165
+ }
166
+ }
167
+ // ── File resolution ────────────────────────────────────────────────────────
168
+ /**
169
+ * Resolve a filename by searching local paths first (cwd, then ancestors),
170
+ * then config directories (highest priority first).
171
+ * Local files always win over config dir copies.
172
+ * Returns the first existing path, or null.
173
+ */
174
+ export function resolveFilePath(filename) {
175
+ // 1. Search cwd first — local files take priority
176
+ const cwdPath = resolve(cwd(), filename);
177
+ if (existsSync(cwdPath))
178
+ return cwdPath;
179
+ // 2. Walk up from cwd looking for the file directly (not inside .showrun)
180
+ let dir = resolve(cwd(), '..');
181
+ const root = parsePath(dir).root;
182
+ while (dir !== root && dir.length > root.length) {
183
+ const candidate = resolve(dir, filename);
184
+ if (existsSync(candidate))
185
+ return candidate;
186
+ dir = resolve(dir, '..');
187
+ }
188
+ // 3. Fall back to config dirs from highest priority (last) to lowest (first)
189
+ const configDirs = discoverConfigDirs();
190
+ for (let i = configDirs.length - 1; i >= 0; i--) {
191
+ const candidate = join(configDirs[i], filename);
192
+ if (existsSync(candidate))
193
+ return candidate;
194
+ }
195
+ return null;
196
+ }
197
+ // ── System prompt helper ───────────────────────────────────────────────────
198
+ /**
199
+ * Get the global config directory path for the current platform.
200
+ */
201
+ export function getGlobalConfigDir() {
202
+ const os = platform();
203
+ if (os === 'win32') {
204
+ const appData = process.env.APPDATA;
205
+ if (appData)
206
+ return join(appData, 'showrun');
207
+ return join(homedir(), 'AppData', 'Roaming', 'showrun');
208
+ }
209
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
210
+ return join(xdgConfig, 'showrun');
211
+ }
212
+ /**
213
+ * Ensure a system prompt file exists in a config directory.
214
+ * If the prompt was found outside config dirs (e.g. repo root), copy it
215
+ * into the global config dir so it's available when running from any directory.
216
+ */
217
+ export function ensureSystemPromptInConfigDir(filename, sourcePath) {
218
+ const configDirs = discoverConfigDirs();
219
+ // Check if the file already exists in any config dir
220
+ for (const dir of configDirs) {
221
+ const candidate = join(dir, filename);
222
+ if (existsSync(candidate))
223
+ return candidate;
224
+ }
225
+ // Not in any config dir — copy it to the global config dir
226
+ const globalDir = getGlobalConfigDir();
227
+ const destPath = join(globalDir, filename);
228
+ ensureDir(globalDir);
229
+ copyFileSync(sourcePath, destPath);
230
+ console.log(`[Config] Created config directory at ${globalDir}`);
231
+ console.log(`[Config] Copied ${filename} to ${destPath}`);
232
+ return destPath;
233
+ }
234
+ // ── Main entry point ───────────────────────────────────────────────────────
235
+ /**
236
+ * Load config, merge, and apply to process.env.
237
+ * This is the single call sites should use (e.g. CLI bootstrap).
238
+ */
239
+ export function initConfig() {
240
+ const result = loadConfig();
241
+ applyConfigToEnv(result.config);
242
+ if (result.loadedFiles.length > 0) {
243
+ console.log(`[Config] Loaded: ${result.loadedFiles.join(', ')}`);
244
+ }
245
+ return result;
246
+ }
247
+ // ── Default config template ────────────────────────────────────────────────
248
+ export const DEFAULT_CONFIG_TEMPLATE = {
249
+ llm: {
250
+ provider: 'anthropic',
251
+ anthropic: { apiKey: '', model: '', baseUrl: '' },
252
+ openai: { apiKey: '', model: '', baseUrl: '' },
253
+ },
254
+ agent: {
255
+ maxBrowserRounds: 0,
256
+ debug: false,
257
+ transcriptLogging: false,
258
+ },
259
+ prompts: {
260
+ teachChatSystemPrompt: '',
261
+ explorationAgentPromptPath: '',
262
+ },
263
+ techniques: {
264
+ vectorStore: { provider: 'weaviate', url: '', apiKey: '', vectorizer: '' },
265
+ embedding: { apiKey: '', model: '', baseUrl: '' },
266
+ collectionName: '',
267
+ },
268
+ };
@@ -1,5 +1,6 @@
1
1
  import type { RunContext, AuthConfig } from '../types.js';
2
2
  import type { DslStep, RunFlowOptions, RunFlowResult } from './types.js';
3
+ import type { SnapshotFile } from '../requestSnapshot.js';
3
4
  /**
4
5
  * Extended options for running a flow with auth resilience
5
6
  */
@@ -16,6 +17,14 @@ export interface RunFlowOptionsWithAuth extends RunFlowOptions {
16
17
  * Secrets for template resolution ({{secret.NAME}})
17
18
  */
18
19
  secrets?: Record<string, string>;
20
+ /**
21
+ * If true, run in HTTP-only mode (skip browser steps, replay from snapshots)
22
+ */
23
+ httpMode?: boolean;
24
+ /**
25
+ * Request snapshots for HTTP-first execution
26
+ */
27
+ snapshots?: SnapshotFile;
19
28
  }
20
29
  /**
21
30
  * Runs a flow of DSL steps sequentially with auth resilience support
@@ -1 +1 @@
1
- {"version":3,"file":"interpreter.d.ts","sourceRoot":"","sources":["../../src/dsl/interpreter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAa,UAAU,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA8DzE;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAyBD;;GAEG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,OAAO,EAAE,EAChB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,aAAa,CAAC,CAybxB"}
1
+ {"version":3,"file":"interpreter.d.ts","sourceRoot":"","sources":["../../src/dsl/interpreter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAa,UAAU,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAgBzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA+C1D;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAyBD;;GAEG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,OAAO,EAAE,EAChB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,aAAa,CAAC,CAycxB"}
@@ -72,6 +72,8 @@ export async function runFlow(ctx, steps, options) {
72
72
  const profileId = options?.profileId;
73
73
  const cacheDir = options?.cacheDir;
74
74
  const secrets = options?.secrets ?? {};
75
+ const httpMode = options?.httpMode ?? false;
76
+ const snapshots = options?.snapshots;
75
77
  // Get secret values for redaction (only values >= 3 chars)
76
78
  const secretValues = Object.values(secrets).filter((v) => v && v.length >= 3);
77
79
  // Validate flow before execution
@@ -98,6 +100,9 @@ export async function runFlow(ctx, steps, options) {
98
100
  inputs,
99
101
  networkCapture: ctx.networkCapture,
100
102
  authMonitor: authMonitor ?? undefined,
103
+ httpMode,
104
+ snapshots: snapshots ?? undefined,
105
+ secrets,
101
106
  };
102
107
  if (authConfig?.authPolicy) {
103
108
  authMonitor = new AuthFailureMonitor(authConfig.authPolicy);
@@ -174,8 +179,13 @@ export async function runFlow(ctx, steps, options) {
174
179
  }
175
180
  }
176
181
  catch (conditionError) {
177
- // Log condition evaluation error but continue with step execution
178
- console.warn(`[interpreter] Error evaluating skip_if for step ${step.id}:`, conditionError);
182
+ const msg = `skip_if evaluation error for step "${step.id}": ${conditionError instanceof Error ? conditionError.message : String(conditionError)}`;
183
+ console.warn(`[interpreter] ${msg}`);
184
+ ctx.logger.log({ type: 'error', data: { error: msg, stepId: step.id, type: step.type } });
185
+ // Push to hints so the agent can see the error
186
+ if (!vars['__jmespath_hints'])
187
+ vars['__jmespath_hints'] = [];
188
+ vars['__jmespath_hints'].push(msg);
179
189
  }
180
190
  }
181
191
  // Resolve templates in step params before execution
@@ -384,8 +394,8 @@ export async function runFlow(ctx, steps, options) {
384
394
  stepsExecuted++;
385
395
  continue;
386
396
  }
387
- // Capture artifacts on error if available
388
- if (ctx.artifacts) {
397
+ // Capture artifacts on error if available (skip in HTTP mode — no browser)
398
+ if (ctx.artifacts && !httpMode) {
389
399
  try {
390
400
  await ctx.artifacts.saveScreenshot(`error-${step.id}`);
391
401
  const html = await ctx.page.content();
@@ -396,12 +406,20 @@ export async function runFlow(ctx, steps, options) {
396
406
  console.error('Failed to save artifacts:', artifactError);
397
407
  }
398
408
  }
409
+ // Attach partial results to the error for upstream consumers
410
+ if (error instanceof Error) {
411
+ error.partialResult = {
412
+ collectibles: { ...collectibles },
413
+ stepsExecuted,
414
+ failedStepId: step.id,
415
+ };
416
+ }
399
417
  // Stop on error (default behavior)
400
418
  throw error;
401
419
  }
402
420
  }
403
421
  const durationMs = Date.now() - startTime;
404
- const finalUrl = ctx.page.url();
422
+ const finalUrl = httpMode ? undefined : ctx.page.url();
405
423
  // Collect JMESPath hints from vars (stored by step handlers)
406
424
  const jmespathHints = vars['__jmespath_hints'] || [];
407
425
  const singleHint = vars['__jmespath_hint'];
@@ -418,6 +436,7 @@ export async function runFlow(ctx, steps, options) {
418
436
  stepsExecuted,
419
437
  stepsTotal: steps.length,
420
438
  },
439
+ _vars: { ...vars },
421
440
  };
422
441
  // Only include _hints if there are any
423
442
  if (uniqueHints.length > 0) {
@@ -2,6 +2,7 @@ import type { Page, BrowserContext, Frame } from 'playwright';
2
2
  import type { DslStep } from './types.js';
3
3
  import type { NetworkCaptureApi } from '../networkCapture.js';
4
4
  import type { AuthFailureMonitor } from '../authResilience.js';
5
+ import type { SnapshotFile } from '../requestSnapshot.js';
5
6
  /**
6
7
  * Step execution context
7
8
  */
@@ -24,6 +25,12 @@ export interface StepContext {
24
25
  previousTabIndex?: number;
25
26
  /** Task pack directory path (for resolving relative file paths) */
26
27
  packDir?: string;
28
+ /** If true, running in HTTP-only mode (no browser) */
29
+ httpMode?: boolean;
30
+ /** Request snapshots for HTTP-first execution */
31
+ snapshots?: SnapshotFile;
32
+ /** Secrets for template resolution in HTTP mode */
33
+ secrets?: Record<string, string>;
27
34
  }
28
35
  /**
29
36
  * Executes a single DSL step
@@ -1 +1 @@
1
- {"version":3,"file":"stepHandlers.d.ts","sourceRoot":"","sources":["../../src/dsl/stepHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EACV,OAAO,EAqBR,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAA4C,MAAM,sBAAsB,CAAC;AAGxG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,IAAI,CAAC;IACX,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,mDAAmD;IACnD,cAAc,CAAC,EAAE,iBAAiB,CAAC;IACnC,kFAAkF;IAClF,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,oDAAoD;IACpD,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAw5BD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC,CAgEf"}
1
+ {"version":3,"file":"stepHandlers.d.ts","sourceRoot":"","sources":["../../src/dsl/stepHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EACV,OAAO,EAqBR,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAA4C,MAAM,sBAAsB,CAAC;AAGxG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,KAAK,EAAE,YAAY,EAAmB,MAAM,uBAAuB,CAAC;AAI3E;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,IAAI,CAAC;IACX,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,mDAAmD;IACnD,cAAc,CAAC,EAAE,iBAAiB,CAAC;IACnC,kFAAkF;IAClF,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,oDAAoD;IACpD,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAmiCD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC,CA4Ef"}
@@ -1,6 +1,8 @@
1
1
  import { resolveTemplate } from './templating.js';
2
2
  import { resolveTargetWithFallback, selectorToTarget } from './target.js';
3
3
  import { search as jmesSearch } from '@jmespath-community/jmespath';
4
+ import { replayFromSnapshot } from '../httpReplay.js';
5
+ import { validateResponse } from '../requestSnapshot.js';
4
6
  /**
5
7
  * Executes a navigate step
6
8
  */
@@ -284,18 +286,24 @@ function getByPath(obj, path) {
284
286
  }
285
287
  try {
286
288
  const result = jmesSearch(obj, trimmed);
287
- // Check for null/undefined results and provide diagnostic hint
289
+ // Check for null/undefined results and provide diagnostic hint with data structure
288
290
  if (result === null || result === undefined) {
291
+ const topKeys = typeof obj === 'object' && obj !== null
292
+ ? Object.keys(obj).slice(0, 10).join(', ')
293
+ : typeof obj;
289
294
  return {
290
295
  value: result,
291
- hint: `JMESPath '${trimmed}' matched nothing (returned ${result}). Verify the path exists. Try a simpler path like 'data' or 'keys(@)' to inspect the structure.`,
296
+ hint: `JMESPath '${trimmed}' matched nothing (returned ${result}). Actual top-level keys: [${topKeys}]. Try a simpler path like 'data' or 'keys(@)' to inspect the structure.`,
292
297
  };
293
298
  }
294
299
  // Check for empty array results
295
300
  if (Array.isArray(result) && result.length === 0) {
301
+ const topKeys = typeof obj === 'object' && obj !== null
302
+ ? Object.keys(obj).slice(0, 10).join(', ')
303
+ : typeof obj;
296
304
  return {
297
305
  value: result,
298
- hint: `JMESPath '${trimmed}' returned an empty array. The path may be correct but no items matched.`,
306
+ hint: `JMESPath '${trimmed}' returned an empty array. Actual top-level keys: [${topKeys}]. The path may be correct but no items matched.`,
299
307
  };
300
308
  }
301
309
  return { value: result };
@@ -399,8 +407,13 @@ async function executeNetworkReplay(ctx, step) {
399
407
  setQuery: step.params.overrides.setQuery,
400
408
  setHeaders: step.params.overrides.setHeaders,
401
409
  body: step.params.overrides.body,
402
- urlReplace: step.params.overrides.urlReplace,
403
- bodyReplace: step.params.overrides.bodyReplace,
410
+ // Normalize urlReplace/bodyReplace to array (DSL type accepts single or array)
411
+ urlReplace: step.params.overrides.urlReplace
412
+ ? (Array.isArray(step.params.overrides.urlReplace) ? step.params.overrides.urlReplace : [step.params.overrides.urlReplace])
413
+ : undefined,
414
+ bodyReplace: step.params.overrides.bodyReplace
415
+ ? (Array.isArray(step.params.overrides.bodyReplace) ? step.params.overrides.bodyReplace : [step.params.overrides.bodyReplace])
416
+ : undefined,
404
417
  }
405
418
  : undefined;
406
419
  let result;
@@ -717,10 +730,133 @@ async function executeSwitchTab(ctx, step) {
717
730
  ctx.vars['__newPage'] = targetPage;
718
731
  await targetPage.bringToFront();
719
732
  }
733
+ /** Step types that are skipped silently in HTTP mode (setup/trigger steps). */
734
+ const HTTP_MODE_SKIP_STEPS = new Set([
735
+ 'navigate', 'click', 'fill', 'select_option', 'press_key',
736
+ 'upload_file', 'wait_for', 'assert', 'frame', 'new_tab',
737
+ 'switch_tab', 'network_find',
738
+ ]);
739
+ /**
740
+ * Merge flow-level step overrides with snapshot-level overrides.
741
+ * Step overrides take precedence for scalar fields (url, body).
742
+ * Array fields (bodyReplace, urlReplace) are concatenated: snapshot first, then step.
743
+ * Object fields (setQuery, setHeaders) are merged: step values override snapshot values.
744
+ */
745
+ function mergeStepOverridesIntoSnapshot(snapshot, stepOverrides) {
746
+ if (!stepOverrides)
747
+ return snapshot;
748
+ // Normalize step overrides arrays
749
+ const stepBodyReplace = stepOverrides.bodyReplace
750
+ ? (Array.isArray(stepOverrides.bodyReplace) ? stepOverrides.bodyReplace : [stepOverrides.bodyReplace])
751
+ : [];
752
+ const stepUrlReplace = stepOverrides.urlReplace
753
+ ? (Array.isArray(stepOverrides.urlReplace) ? stepOverrides.urlReplace : [stepOverrides.urlReplace])
754
+ : [];
755
+ const snapshotOv = snapshot.overrides;
756
+ const bodyReplaceArr = [...(snapshotOv?.bodyReplace ?? []), ...stepBodyReplace];
757
+ const urlReplaceArr = [...(snapshotOv?.urlReplace ?? []), ...stepUrlReplace];
758
+ // Coerce step setQuery values to strings (step type allows string | number, snapshot expects string)
759
+ const stepSetQuery = stepOverrides.setQuery
760
+ ? Object.fromEntries(Object.entries(stepOverrides.setQuery).map(([k, v]) => [k, String(v)]))
761
+ : undefined;
762
+ const setQueryMerged = (snapshotOv?.setQuery || stepSetQuery)
763
+ ? { ...snapshotOv?.setQuery, ...stepSetQuery }
764
+ : undefined;
765
+ const setHeadersMerged = (snapshotOv?.setHeaders || stepOverrides.setHeaders)
766
+ ? { ...snapshotOv?.setHeaders, ...stepOverrides.setHeaders }
767
+ : undefined;
768
+ const merged = {
769
+ // Scalar overrides: step takes precedence
770
+ url: stepOverrides.url ?? snapshotOv?.url,
771
+ body: stepOverrides.body ?? snapshotOv?.body,
772
+ setQuery: setQueryMerged && Object.keys(setQueryMerged).length > 0 ? setQueryMerged : undefined,
773
+ setHeaders: setHeadersMerged && Object.keys(setHeadersMerged).length > 0 ? setHeadersMerged : undefined,
774
+ bodyReplace: bodyReplaceArr.length > 0 ? bodyReplaceArr : undefined,
775
+ urlReplace: urlReplaceArr.length > 0 ? urlReplaceArr : undefined,
776
+ };
777
+ const hasOverrides = merged.url || merged.body || merged.setQuery || merged.setHeaders || merged.bodyReplace || merged.urlReplace;
778
+ return { ...snapshot, overrides: hasOverrides ? merged : undefined };
779
+ }
780
+ /**
781
+ * Execute a network_replay step in HTTP-only mode using snapshot data.
782
+ */
783
+ async function executeNetworkReplayHttp(ctx, step) {
784
+ if (!ctx.snapshots) {
785
+ throw new Error('network_replay in HTTP mode requires snapshots');
786
+ }
787
+ const snapshot = ctx.snapshots.snapshots[step.id];
788
+ if (!snapshot) {
789
+ throw new Error(`No snapshot found for step "${step.id}"`);
790
+ }
791
+ // Merge flow-level overrides (from step.params) with snapshot-level overrides.
792
+ // This ensures bodyReplace/urlReplace from the flow are applied in HTTP-only mode.
793
+ const mergedSnapshot = mergeStepOverridesIntoSnapshot(snapshot, step.params.overrides);
794
+ const result = await replayFromSnapshot(mergedSnapshot, ctx.inputs, ctx.vars, {
795
+ secrets: ctx.secrets,
796
+ });
797
+ // Validate the response — throw to trigger browser fallback if stale
798
+ const validation = validateResponse(snapshot, result);
799
+ if (!validation.valid) {
800
+ throw new Error(`Snapshot stale for step "${step.id}": ${validation.reason}`);
801
+ }
802
+ if (step.params.saveAs) {
803
+ ctx.vars[step.params.saveAs] = {
804
+ status: result.status,
805
+ contentType: result.contentType,
806
+ body: result.body,
807
+ bodySize: result.bodySize,
808
+ };
809
+ }
810
+ // Use 'path' with fallback to deprecated 'jsonPath'
811
+ const pathExpr = step.params.response.path || step.params.response.jsonPath;
812
+ let outValue;
813
+ if (step.params.response.as === 'json') {
814
+ try {
815
+ outValue = JSON.parse(result.body);
816
+ }
817
+ catch {
818
+ throw new Error(`network_replay (HTTP mode): response body is not valid JSON (status ${result.status})`);
819
+ }
820
+ if (pathExpr) {
821
+ const pathResult = getByPath(outValue, pathExpr);
822
+ outValue = pathResult.value;
823
+ if (pathResult.hint) {
824
+ ctx.vars['__jmespath_hint'] = pathResult.hint;
825
+ }
826
+ }
827
+ }
828
+ else {
829
+ if (pathExpr) {
830
+ const pathResult = getByPath(JSON.parse(result.body), pathExpr);
831
+ outValue = pathResult.value;
832
+ if (pathResult.hint) {
833
+ ctx.vars['__jmespath_hint'] = pathResult.hint;
834
+ }
835
+ }
836
+ else {
837
+ outValue = result.body;
838
+ }
839
+ if (typeof outValue === 'object' && outValue !== null) {
840
+ outValue = JSON.stringify(outValue);
841
+ }
842
+ }
843
+ ctx.collectibles[step.params.out] = outValue;
844
+ }
720
845
  /**
721
846
  * Executes a single DSL step
722
847
  */
723
848
  export async function executeStep(ctx, step) {
849
+ // In HTTP mode, skip DOM/setup steps and use snapshot replay for network_replay
850
+ if (ctx.httpMode) {
851
+ if (HTTP_MODE_SKIP_STEPS.has(step.type)) {
852
+ return; // silently skip
853
+ }
854
+ if (step.type === 'network_replay') {
855
+ await executeNetworkReplayHttp(ctx, step);
856
+ return;
857
+ }
858
+ // network_extract, set_var, sleep execute normally below
859
+ }
724
860
  switch (step.type) {
725
861
  case 'navigate':
726
862
  await executeNavigate(ctx, step);
@@ -1 +1 @@
1
- {"version":3,"file":"templating.d.ts","sourceRoot":"","sources":["../../src/dsl/templating.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA0BlD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAWlF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,eAAe,GAAG,CAAC,CAkBvE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhD"}
1
+ {"version":3,"file":"templating.d.ts","sourceRoot":"","sources":["../../src/dsl/templating.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAuClD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAWlF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,eAAe,GAAG,CAAC,CAkBvE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhD"}
@@ -10,6 +10,17 @@ const env = new nunjucks.Environment(null, {
10
10
  autoescape: false,
11
11
  throwOnUndefined: false, // Allow undefined values to render as empty string
12
12
  });
13
+ /**
14
+ * pctEncode filter: like urlencode but also encodes characters that
15
+ * encodeURIComponent leaves unescaped (parentheses, !, ', *, ~).
16
+ * Useful in URLs where those chars have structural meaning (e.g. LinkedIn Sales Navigator query syntax).
17
+ * Usage: {{inputs.company_name | pctEncode}}
18
+ */
19
+ env.addFilter('pctEncode', (val) => {
20
+ if (val == null)
21
+ return '';
22
+ return encodeURIComponent(String(val)).replace(/[!'()*~]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
23
+ });
13
24
  /**
14
25
  * TOTP filter: generates a 6-digit TOTP code from a base32 secret.
15
26
  * Usage: {{secret.TOTP_KEY | totp}}
@@ -433,16 +433,22 @@ export interface NetworkReplayStep extends BaseDslStep {
433
433
  setQuery?: Record<string, string | number>;
434
434
  setHeaders?: Record<string, string>;
435
435
  body?: string;
436
- /** Regex find/replace on captured URL; replace can use $1, $2. Supports {{vars.xxx}}/{{inputs.xxx}} (resolved before replace). */
436
+ /** Regex find/replace on captured URL; replace can use $1, $2. Supports {{vars.xxx}}/{{inputs.xxx}} (resolved before replace). Accepts single object or array. */
437
437
  urlReplace?: {
438
438
  find: string;
439
439
  replace: string;
440
- };
441
- /** Regex find/replace on captured body; replace can use $1, $2. Supports {{vars.xxx}}/{{inputs.xxx}} (resolved before replace). */
440
+ } | Array<{
441
+ find: string;
442
+ replace: string;
443
+ }>;
444
+ /** Regex find/replace on captured body; replace can use $1, $2. Supports {{vars.xxx}}/{{inputs.xxx}} (resolved before replace). Accepts single object or array. */
442
445
  bodyReplace?: {
443
446
  find: string;
444
447
  replace: string;
445
- };
448
+ } | Array<{
449
+ find: string;
450
+ replace: string;
451
+ }>;
446
452
  };
447
453
  auth: 'browser_context';
448
454
  out: string;
@@ -691,5 +697,11 @@ export interface RunFlowResult {
691
697
  * These help AI agents understand why data extraction may have failed.
692
698
  */
693
699
  _hints?: string[];
700
+ /**
701
+ * Internal: resolved vars after flow execution. Used by snapshot capture
702
+ * to look up network entries by their runtime-resolved request IDs.
703
+ * Not part of the public API.
704
+ */
705
+ _vars?: Record<string, unknown>;
694
706
  }
695
707
  //# sourceMappingURL=types.d.ts.map