@itmetsam/samurai-framework 0.22.2 → 0.22.4

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 (76) hide show
  1. package/AGENT-GUIDE.md +50 -0
  2. package/bin/samurai.mjs +2 -0
  3. package/dist/agent/index.d.ts +11 -0
  4. package/dist/agent/index.js +12 -0
  5. package/dist/agent/instructions.d.ts +13 -0
  6. package/dist/agent/instructions.js +44 -0
  7. package/dist/agent/limits.d.ts +12 -0
  8. package/dist/agent/limits.js +18 -0
  9. package/dist/agent/loop.d.ts +32 -0
  10. package/dist/agent/loop.js +46 -0
  11. package/dist/agent/model.d.ts +9 -0
  12. package/dist/agent/model.js +14 -0
  13. package/dist/agent/prompt.d.ts +15 -0
  14. package/dist/agent/prompt.js +46 -0
  15. package/dist/agent/providers.d.ts +19 -0
  16. package/dist/agent/providers.js +95 -0
  17. package/dist/agent/recording.d.ts +41 -0
  18. package/dist/agent/recording.js +68 -0
  19. package/dist/agent/run.d.ts +11 -0
  20. package/dist/agent/run.js +209 -0
  21. package/dist/agent/sdk-tools.d.ts +15 -0
  22. package/dist/agent/sdk-tools.js +44 -0
  23. package/dist/agent/session.d.ts +62 -0
  24. package/dist/agent/session.js +196 -0
  25. package/dist/agent/snapshot-script.d.ts +18 -0
  26. package/dist/agent/snapshot-script.js +163 -0
  27. package/dist/agent/snapshot.d.ts +54 -0
  28. package/dist/agent/snapshot.js +95 -0
  29. package/dist/agent/take-over.d.ts +19 -0
  30. package/dist/agent/take-over.js +49 -0
  31. package/dist/agent/tools/act.d.ts +2 -0
  32. package/dist/agent/tools/act.js +124 -0
  33. package/dist/agent/tools/ask.d.ts +1 -0
  34. package/dist/agent/tools/ask.js +19 -0
  35. package/dist/agent/tools/context.d.ts +63 -0
  36. package/dist/agent/tools/context.js +42 -0
  37. package/dist/agent/tools/expect.d.ts +1 -0
  38. package/dist/agent/tools/expect.js +152 -0
  39. package/dist/agent/tools/finish.d.ts +1 -0
  40. package/dist/agent/tools/finish.js +12 -0
  41. package/dist/agent/tools/index.d.ts +3 -0
  42. package/dist/agent/tools/index.js +21 -0
  43. package/dist/agent/tools/navigate.d.ts +1 -0
  44. package/dist/agent/tools/navigate.js +53 -0
  45. package/dist/agent/tools/origin-guard.d.ts +6 -0
  46. package/dist/agent/tools/origin-guard.js +19 -0
  47. package/dist/agent/tools/origins.d.ts +11 -0
  48. package/dist/agent/tools/origins.js +22 -0
  49. package/dist/agent/tools/screenshot.d.ts +1 -0
  50. package/dist/agent/tools/screenshot.js +39 -0
  51. package/dist/agent/tools/snapshot.d.ts +1 -0
  52. package/dist/agent/tools/snapshot.js +12 -0
  53. package/dist/agent/tools/use-secret.d.ts +1 -0
  54. package/dist/agent/tools/use-secret.js +41 -0
  55. package/dist/agent/types.d.ts +104 -0
  56. package/dist/agent/types.js +1 -0
  57. package/dist/capabilities.d.ts +2 -0
  58. package/dist/capabilities.js +2 -0
  59. package/dist/cli/args.d.ts +5 -0
  60. package/dist/cli/args.js +5 -0
  61. package/dist/cli/generate.d.ts +10 -0
  62. package/dist/cli/generate.js +139 -0
  63. package/dist/cli/io.d.ts +2 -0
  64. package/dist/cli/main.js +3 -0
  65. package/dist/cli/prompt.d.ts +2 -0
  66. package/dist/cli/prompt.js +16 -0
  67. package/dist/cli/record.js +3 -1
  68. package/dist/cli/usage.d.ts +1 -1
  69. package/dist/cli/usage.js +19 -2
  70. package/dist/index.js +2 -0
  71. package/dist/recorder/normalise.d.ts +6 -0
  72. package/dist/recorder/normalise.js +2 -0
  73. package/dist/recorder/session.d.ts +1 -1
  74. package/dist/recorder/session.js +1 -1
  75. package/dist/types/config.d.ts +5 -0
  76. package/package.json +19 -4
@@ -0,0 +1,139 @@
1
+ import { parseRunnerFlags, parseRunOverrides, } from "./args.js";
2
+ const USAGE_LINE = 'Usage: samurai generate "<goal>" --file <spec> [--new <title>] [--test <name|index>] [--url <url>] [--model <provider:name>] [--max-steps <n>] [--yes-origin <origin>] [--secret <NAME>] [--json]';
3
+ async function loadAgent() {
4
+ try {
5
+ return await import("../agent/index.js");
6
+ }
7
+ catch (error) {
8
+ if (error.code === "ERR_MODULE_NOT_FOUND")
9
+ throw new Error("samurai generate needs the packages ai and zod, and a provider package for your model; run: npm install ai zod @ai-sdk/anthropic (or @ai-sdk/openai, @ai-sdk/openai-compatible)");
10
+ throw error;
11
+ }
12
+ }
13
+ function origin(raw) {
14
+ try {
15
+ return new URL(raw).origin;
16
+ }
17
+ catch {
18
+ throw new Error(`--yes-origin: "${raw}" is not a URL such as https://shop.example`);
19
+ }
20
+ }
21
+ /** The model the run uses: `--model`, else `SAMURAI_AI_MODEL`, else the config's `ai.model` */
22
+ async function chosenModel(agent, flag, env) {
23
+ const spec = flag ?? (await agent.defaultModelSpec(env));
24
+ if (!spec)
25
+ throw new Error("No model: pass --model provider:name (anthropic, openai or openai-compatible), set SAMURAI_AI_MODEL, or set ai.model in samurai.config.ts");
26
+ return {
27
+ spec,
28
+ model: await agent.modelFromSpec(spec, { projectDir: process.cwd(), env }),
29
+ };
30
+ }
31
+ /** `samurai generate "<goal>"`: lets the AI agent write a test and prints what it does. Returns the exit code */
32
+ export async function generateCommand({ values, positionals }, io, deps = {}) {
33
+ const goal = positionals[0];
34
+ if (!goal)
35
+ throw new Error(`Which goal? ${USAGE_LINE}`);
36
+ const files = values.file ?? [];
37
+ if (files.length !== 1)
38
+ throw new Error(`Give exactly one --file <spec>. ${USAGE_LINE}`);
39
+ const maxSteps = values["max-steps"] === undefined ? undefined : Number(values["max-steps"]);
40
+ if (maxSteps !== undefined && (!Number.isInteger(maxSteps) || maxSteps < 1))
41
+ throw new Error(`--max-steps must be a whole number from 1, got "${values["max-steps"]}"`);
42
+ const flags = parseRunnerFlags(values);
43
+ if (flags.browsers && flags.browsers.length > 1)
44
+ throw new Error("--browser: the agent uses one browser");
45
+ const test = values.test === undefined
46
+ ? undefined
47
+ : /^\d+$/.test(values.test)
48
+ ? Number(values.test)
49
+ : values.test;
50
+ const allowedOrigins = (values["yes-origin"] ?? []).map(origin);
51
+ const agent = await (deps.loadAgent ?? loadAgent)();
52
+ const { spec, model } = await chosenModel(agent, values.model, io.env);
53
+ const json = values.json === true;
54
+ if (!json)
55
+ io.out(`Model ${spec.slice(0, spec.indexOf(":"))}: the pages the agent reads are sent to that provider.\n`);
56
+ const session = agent.runAgent({
57
+ ...parseRunOverrides(values, io.env),
58
+ model,
59
+ goal,
60
+ file: files[0],
61
+ ...(test !== undefined && { test }),
62
+ ...(values.new !== undefined && { create: values.new }),
63
+ ...(values.url !== undefined && { url: values.url }),
64
+ ...(flags.browsers && { browser: flags.browsers[0] }),
65
+ ...(flags.port !== undefined && { port: flags.port }),
66
+ ...(flags.headless !== undefined && { headless: flags.headless }),
67
+ ...(allowedOrigins.length > 0 && { allowedOrigins }),
68
+ ...(values.secret && { secrets: values.secret }),
69
+ ...(values["secret-field"] && { secretFields: values["secret-field"] }),
70
+ ...(maxSteps !== undefined && { limits: { maxSteps } }),
71
+ ...(io.signal && { signal: io.signal }),
72
+ });
73
+ let unanswerable;
74
+ for await (const event of session.events) {
75
+ if (json)
76
+ io.out(`${JSON.stringify(event)}\n`);
77
+ switch (event.type) {
78
+ case "plan":
79
+ if (!json)
80
+ io.out(`${event.text}\n`);
81
+ break;
82
+ case "tool-call":
83
+ if (!json)
84
+ io.out(` · ${event.tool} ${summaryOf(event.args)}\n`);
85
+ break;
86
+ case "step":
87
+ if (!json && event.code !== undefined)
88
+ io.out(` ${event.event.op === "insert" ? "+" : "~"} ${event.code}\n`);
89
+ break;
90
+ case "question": {
91
+ const originQuestion = event.options?.join("/") === "allow/deny";
92
+ if (io.prompt && !json) {
93
+ io.out(`? ${event.question}${event.options ? ` (${event.options.join("/")})` : ""}\n`);
94
+ void io
95
+ .prompt("> ")
96
+ .then((answer) => session.answer(event.id, answer))
97
+ .catch(() => undefined); // the run ended while the question was open
98
+ }
99
+ else if (originQuestion) {
100
+ session.answer(event.id, "deny");
101
+ }
102
+ else {
103
+ unanswerable = event.question;
104
+ session.stop();
105
+ }
106
+ break;
107
+ }
108
+ case "ended":
109
+ if (!json)
110
+ summarise(io, event.result);
111
+ break;
112
+ default:
113
+ break;
114
+ }
115
+ }
116
+ if (unanswerable !== undefined)
117
+ io.err(`The agent asked "${unanswerable}" and nobody can answer: stdin is not a terminal${json ? " (or --json is set)" : ""}. Run it in a terminal, or put the answer in the goal.\n`);
118
+ return (await session.result).reason === "finished" ? 0 : 1;
119
+ }
120
+ function summaryOf(args) {
121
+ const text = JSON.stringify(args ?? {});
122
+ return text.length > 100 ? `${text.slice(0, 99)}…` : text;
123
+ }
124
+ const ENDINGS = {
125
+ stopped: "Stopped.",
126
+ "step-limit": "Stopped at the step limit (--max-steps).",
127
+ "token-limit": "Stopped at the token limit.",
128
+ "time-limit": "Stopped at the time limit.",
129
+ };
130
+ function summarise(io, result) {
131
+ const base = `"${result.test}" has ${result.steps} ${result.steps === 1 ? "step" : "steps"} in ${result.file}.`;
132
+ if (result.reason === "error")
133
+ io.err(`\nThe run failed: ${result.error ?? "unknown error"}\n${base}\n`);
134
+ else
135
+ io.out(`\n${result.reason === "finished" ? "Done" : (ENDINGS[result.reason] ?? result.reason)} ${base}\n${result.summary ? `${result.summary}\n` : ""}`);
136
+ if (result.secrets.length > 0)
137
+ io.out(`Set ${result.secrets.map((name) => `SAMURAI_SECRET_${name}`).join(", ")} (environment variable or .env.<environment>) before running it.\n`);
138
+ io.out(`${result.tokens} tokens used. Review the steps before you run the test.\n`);
139
+ }
package/dist/cli/io.d.ts CHANGED
@@ -7,4 +7,6 @@ export interface Io {
7
7
  env: Partial<NodeJS.ProcessEnv>;
8
8
  /** Aborts a recording (Ctrl+C) */
9
9
  signal?: AbortSignal;
10
+ /** Asks the person at the terminal; set when stdin is a terminal, so a command may ask questions */
11
+ prompt?: (question: string) => Promise<string>;
10
12
  }
package/dist/cli/main.js CHANGED
@@ -3,6 +3,7 @@ import { listTests, runTests } from "../runner/run.js";
3
3
  import { ConsoleReporter } from "./console-reporter.js";
4
4
  import { browsersCommand } from "./browsers.js";
5
5
  import { flowCommand } from "./flow.js";
6
+ import { generateCommand } from "./generate.js";
6
7
  import { initProject } from "./init.js";
7
8
  import { recordCommand } from "./record.js";
8
9
  import { USAGE } from "./usage.js";
@@ -36,6 +37,8 @@ export async function main(argv, io) {
36
37
  io.out("\nNext: install the dependencies, then `samurai browsers install firefox`, then `samurai run`.\n");
37
38
  return 0;
38
39
  }
40
+ if (command === "generate")
41
+ return await generateCommand({ values, positionals }, io);
39
42
  if (command === "record")
40
43
  return await recordCommand({ values, positionals }, io);
41
44
  if (command !== "run" && command !== "list") {
@@ -0,0 +1,2 @@
1
+ /** Asks on the terminal when stdin is one; undefined otherwise (a pipe, CI), so a command knows it cannot ask */
2
+ export declare function terminalPrompt(): ((question: string) => Promise<string>) | undefined;
@@ -0,0 +1,16 @@
1
+ import { createInterface } from "node:readline";
2
+ /** Asks on the terminal when stdin is one; undefined otherwise (a pipe, CI), so a command knows it cannot ask */
3
+ export function terminalPrompt() {
4
+ if (!process.stdin.isTTY)
5
+ return undefined;
6
+ return (question) => new Promise((resolve) => {
7
+ const lines = createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ });
11
+ lines.question(question, (answer) => {
12
+ lines.close();
13
+ resolve(answer);
14
+ });
15
+ });
16
+ }
@@ -66,6 +66,8 @@ export async function recordCommand({ values, positionals }, io) {
66
66
  if (at !== undefined && (!Number.isInteger(at) || at < 0))
67
67
  throw new Error(`--at must be a step number, got "${values.at}"`);
68
68
  const flags = parseRunnerFlags(values);
69
+ if (flags.headless === true)
70
+ throw new Error("--headless: a recording needs a window; the recorder never runs without one");
69
71
  if (flags.browsers && flags.browsers.length > 1)
70
72
  throw new Error("--browser: a recording uses one browser");
71
73
  const test = values.test === undefined
@@ -81,7 +83,7 @@ export async function recordCommand({ values, positionals }, io) {
81
83
  ...(values.new !== undefined && { create: values.new }),
82
84
  ...(at !== undefined && { at }),
83
85
  ...(values.url !== undefined && { url: values.url }),
84
- ...(flags.headless !== undefined && { headless: flags.headless }),
86
+ headless: false,
85
87
  ...(flags.port !== undefined && { port: flags.port }),
86
88
  ...(flags.browsers && { browser: flags.browsers[0] }),
87
89
  ...(io.signal && { signal: io.signal }),
@@ -1 +1 @@
1
- export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups; repeat it for several\n --browser <name> Run in this browser (firefox or chrome) instead of the config's\n browsers; repeat it for more than one\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n --workers <n|n%> Tests that run at the same time, each in a worker process: a number\n from 1, or a share of the CPUs like 50%. Default: the config's\n workers (or SAMURAI_WORKERS), else half the CPUs\n --retries <n> Run a failing test again up to n times, each with a fresh browser\n (or SAMURAI_RETRIES; default 0)\n --shard <current/total> Run one slice of the selected tests, to split a run over machines:\n --shard=2/4 runs the second of four. Whole files (and serial\n blocks) go to one shard; an empty shard succeeds\n\nOptions for record (also --env, --port, --json, --headless, --no-headless):\n --browser <name> Record in this browser (default: the first of the config's browsers)\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
1
+ export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai generate \"<goal>\" Let an AI agent write a test into a spec file (needs ai, zod and a provider)\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups; repeat it for several\n --browser <name> Run in this browser (firefox or chrome) instead of the config's\n browsers; repeat it for more than one\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n --workers <n|n%> Tests that run at the same time, each in a worker process: a number\n from 1, or a share of the CPUs like 50%. Default: the config's\n workers (or SAMURAI_WORKERS), else half the CPUs\n --retries <n> Run a failing test again up to n times, each with a fresh browser\n (or SAMURAI_RETRIES; default 0)\n --shard <current/total> Run one slice of the selected tests, to split a run over machines:\n --shard=2/4 runs the second of four. Whole files (and serial\n blocks) go to one shard; an empty shard succeeds\n\nOptions for record (also --env, --port, --json; always with a window):\n --browser <name> Record in this browser (default: the first of the config's browsers)\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\nOptions for generate (also --env, --browser, --port, --headless, --json):\n --file <spec> The spec file to write into (required, once)\n --new <title> Add a test with this title (creates the file if needed)\n --test <name|index> Write into this test; not needed when the file has one\n --url <url> Page to start on (default: the environment's baseURL)\n --model <provider:name> anthropic, openai or openai-compatible, for example\n anthropic:claude-sonnet-4-5. Default: SAMURAI_AI_MODEL, then the\n config's ai.model. Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY,\n SAMURAI_AI_API_KEY (+ SAMURAI_AI_BASE_URL for openai-compatible)\n --max-steps <n> Most model calls in the run (default 40)\n --yes-origin <origin> A site the agent may open without asking; repeatable\n --secret <NAME> A secret the agent may type (it never sees the value); repeatable\n --secret-field <css> A field whose value is never shown to the model; repeatable\n Questions are asked in the terminal; without one (or with --json) an unknown site is refused\n and any other question ends the run.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed (generate: the agent finished), 1 when a test failed (generate: it ended at a limit, was stopped or failed), 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
package/dist/cli/usage.js CHANGED
@@ -5,6 +5,7 @@ Usage:
5
5
  samurai run [options] Run the tests (the default command)
6
6
  samurai list [options] List the tests without running them
7
7
  samurai record <spec> Open a browser and record what you do into a spec file
8
+ samurai generate "<goal>" Let an AI agent write a test into a spec file (needs ai, zod and a provider)
8
9
  samurai init [folder] Scaffold a project: config, an example test, .env.example
9
10
  samurai flow <command> List, check and run the flows in ./flows (see below)
10
11
  samurai browsers [command] List, install and remove the browsers SAMURAI launches
@@ -33,7 +34,7 @@ Options for run:
33
34
  --shard=2/4 runs the second of four. Whole files (and serial
34
35
  blocks) go to one shard; an empty shard succeeds
35
36
 
36
- Options for record (also --env, --port, --json, --headless, --no-headless):
37
+ Options for record (also --env, --port, --json; always with a window):
37
38
  --browser <name> Record in this browser (default: the first of the config's browsers)
38
39
  --new <title> Add a test with this title (creates the spec file if needed)
39
40
  --test <name|index> Record into this test; not needed when the file has one
@@ -41,10 +42,26 @@ Options for record (also --env, --port, --json, --headless, --no-headless):
41
42
  --url <url> Page to start on (default: the environment's baseURL)
42
43
  Stop with Ctrl+C or by closing the browser window.
43
44
 
45
+ Options for generate (also --env, --browser, --port, --headless, --json):
46
+ --file <spec> The spec file to write into (required, once)
47
+ --new <title> Add a test with this title (creates the file if needed)
48
+ --test <name|index> Write into this test; not needed when the file has one
49
+ --url <url> Page to start on (default: the environment's baseURL)
50
+ --model <provider:name> anthropic, openai or openai-compatible, for example
51
+ anthropic:claude-sonnet-4-5. Default: SAMURAI_AI_MODEL, then the
52
+ config's ai.model. Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY,
53
+ SAMURAI_AI_API_KEY (+ SAMURAI_AI_BASE_URL for openai-compatible)
54
+ --max-steps <n> Most model calls in the run (default 40)
55
+ --yes-origin <origin> A site the agent may open without asking; repeatable
56
+ --secret <NAME> A secret the agent may type (it never sees the value); repeatable
57
+ --secret-field <css> A field whose value is never shown to the model; repeatable
58
+ Questions are asked in the terminal; without one (or with --json) an unknown site is refused
59
+ and any other question ends the run.
60
+
44
61
  -h, --help Show this help
45
62
  -v, --version Show the version
46
63
 
47
- Exit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.
64
+ Exit code: 0 when every test passed (generate: the agent finished), 1 when a test failed (generate: it ended at a limit, was stopped or failed), 2 for a usage or configuration error.
48
65
 
49
66
  samurai flow:
50
67
  list The flows in ./flows
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { main } from "./cli/main.js";
2
+ import { terminalPrompt } from "./cli/prompt.js";
2
3
  // Development entry (`npm run dev`, which runs `tsx src/index.ts`): the same as `samurai run`, straight from the sources
3
4
  const interrupt = new AbortController();
4
5
  process.on("SIGINT", () => {
@@ -12,4 +13,5 @@ process.exit(await main(process.argv.slice(2), {
12
13
  color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
13
14
  env: process.env,
14
15
  signal: interrupt.signal,
16
+ ...(terminalPrompt() && { prompt: terminalPrompt() }),
15
17
  }));
@@ -64,6 +64,12 @@ export type Observation = {
64
64
  flags: string;
65
65
  };
66
66
  }
67
+ /** An action with no argument that a page event does not tell apart: the AI agent's hover, double click and clear */
68
+ | {
69
+ type: "action";
70
+ kind: "hover" | "dblclick" | "clear";
71
+ locator: LocatorSpec;
72
+ }
67
73
  /** The toolbar's Page panel: the page's URL or title */
68
74
  | {
69
75
  type: "expectPage";
@@ -71,6 +71,8 @@ export class Normaliser {
71
71
  locator: observation.locator,
72
72
  value: observation.value,
73
73
  };
74
+ case "action":
75
+ return { kind: observation.kind, locator: observation.locator };
74
76
  case "wait-idle":
75
77
  return { kind: "waitForNetworkIdle" };
76
78
  case "wait-time":
@@ -24,7 +24,7 @@ export interface RecordSpecOptions extends ProjectOptions {
24
24
  browser?: SupportedBrowser;
25
25
  /** The browser's debugging port. @default DEFAULT_PORT (9223) */
26
26
  port?: number;
27
- /** Run the browser without a window. A person can't use it then; for tests and machines without a display. @default the config's `use.headless`, else headless on CI and a window otherwise */
27
+ /** Run the browser without a window. A person can't use it then, so this is only for tests of the recorder itself. Never read from the config, the environment or `CI`: a recording always opens a window. @default false */
28
28
  headless?: boolean;
29
29
  /** Aborting ends the recording, like closing the browser window */
30
30
  signal?: AbortSignal;
@@ -28,7 +28,7 @@ export async function recordSpec(options) {
28
28
  const writer = new SpecWriter(spec.source, file, testIndex);
29
29
  const { browser, page } = await launchRecordingBrowser(browserName, {
30
30
  ...options,
31
- headless: options.headless ?? settings.headless,
31
+ headless: options.headless ?? false,
32
32
  });
33
33
  const loaded = await loadRawConfig();
34
34
  const folder = filesFolder(projectDir(), loaded.filesDir);
@@ -204,6 +204,11 @@ export interface SamuraiTestConfig {
204
204
  * @default "files"
205
205
  */
206
206
  filesDir?: string;
207
+ /**
208
+ * The AI agent (`samurai generate`). `model` is a `provider:name` pair, such as `anthropic:claude-sonnet-4-5`;
209
+ * never a key (keys come from the environment). The `--model` flag and `SAMURAI_AI_MODEL` win over it
210
+ */
211
+ ai?: { model?: string };
207
212
  /** How pages are opened, and what the browser pretends: the options of `test.use()` apply to every test */
208
213
  use?: UseOptions &
209
214
  FixtureUse & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itmetsam/samurai-framework",
3
- "version": "0.22.2",
3
+ "version": "0.22.4",
4
4
  "description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
5
5
  "main": "dist/api.js",
6
6
  "exports": {
@@ -52,6 +52,10 @@
52
52
  "types": "./dist/capabilities.d.ts",
53
53
  "default": "./dist/capabilities.js"
54
54
  },
55
+ "./agent": {
56
+ "types": "./dist/agent/index.d.ts",
57
+ "default": "./dist/agent/index.js"
58
+ },
55
59
  "./package.json": "./package.json"
56
60
  },
57
61
  "scripts": {
@@ -80,10 +84,12 @@
80
84
  "type": "module",
81
85
  "devDependencies": {
82
86
  "@types/node": "^25.6.0",
87
+ "ai": "^7.0.0",
83
88
  "esbuild": "^0.28.2",
84
89
  "knip": "^6.42.1",
85
90
  "madge": "^8.0.0",
86
- "tsx": "^4.23.15"
91
+ "tsx": "^4.23.15",
92
+ "zod": "^4.1.8"
87
93
  },
88
94
  "dependencies": {
89
95
  "@puppeteer/browsers": "^3.2.4",
@@ -93,17 +99,26 @@
93
99
  "types": "dist/api.d.ts",
94
100
  "files": [
95
101
  "dist",
96
- "bin"
102
+ "bin",
103
+ "AGENT-GUIDE.md"
97
104
  ],
98
105
  "bin": {
99
106
  "samurai": "bin/samurai.mjs"
100
107
  },
101
108
  "peerDependencies": {
102
- "tsx": "^4.21.0"
109
+ "ai": "^7.0.0",
110
+ "tsx": "^4.21.0",
111
+ "zod": "^3.25.76 || ^4.1.8"
103
112
  },
104
113
  "peerDependenciesMeta": {
114
+ "ai": {
115
+ "optional": true
116
+ },
105
117
  "tsx": {
106
118
  "optional": true
119
+ },
120
+ "zod": {
121
+ "optional": true
107
122
  }
108
123
  },
109
124
  "repository": {