@itmetsam/samurai-framework 0.22.3 → 0.22.5
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/AGENT-GUIDE.md +50 -0
- package/bin/samurai.mjs +2 -0
- package/dist/agent/generate-kind.d.ts +5 -0
- package/dist/agent/generate-kind.js +40 -0
- package/dist/agent/index.d.ts +19 -0
- package/dist/agent/index.js +23 -0
- package/dist/agent/instructions.d.ts +13 -0
- package/dist/agent/instructions.js +44 -0
- package/dist/agent/limits.d.ts +12 -0
- package/dist/agent/limits.js +18 -0
- package/dist/agent/loop.d.ts +32 -0
- package/dist/agent/loop.js +46 -0
- package/dist/agent/model.d.ts +9 -0
- package/dist/agent/model.js +14 -0
- package/dist/agent/plan/guard.d.ts +7 -0
- package/dist/agent/plan/guard.js +32 -0
- package/dist/agent/plan/plan-file.d.ts +59 -0
- package/dist/agent/plan/plan-file.js +201 -0
- package/dist/agent/plan/plan-kind.d.ts +5 -0
- package/dist/agent/plan/plan-kind.js +28 -0
- package/dist/agent/plan/prompt.d.ts +3 -0
- package/dist/agent/plan/prompt.js +37 -0
- package/dist/agent/plan/read-plan.d.ts +5 -0
- package/dist/agent/plan/read-plan.js +28 -0
- package/dist/agent/plan/recorder.d.ts +30 -0
- package/dist/agent/plan/recorder.js +60 -0
- package/dist/agent/plan/run.d.ts +8 -0
- package/dist/agent/plan/run.js +10 -0
- package/dist/agent/plan/tools.d.ts +4 -0
- package/dist/agent/plan/tools.js +95 -0
- package/dist/agent/prompt.d.ts +21 -0
- package/dist/agent/prompt.js +60 -0
- package/dist/agent/providers.d.ts +19 -0
- package/dist/agent/providers.js +95 -0
- package/dist/agent/recording.d.ts +45 -0
- package/dist/agent/recording.js +78 -0
- package/dist/agent/run-kind.d.ts +48 -0
- package/dist/agent/run-kind.js +1 -0
- package/dist/agent/run.d.ts +15 -0
- package/dist/agent/run.js +201 -0
- package/dist/agent/sdk-tools.d.ts +15 -0
- package/dist/agent/sdk-tools.js +52 -0
- package/dist/agent/session.d.ts +62 -0
- package/dist/agent/session.js +196 -0
- package/dist/agent/snapshot-script.d.ts +18 -0
- package/dist/agent/snapshot-script.js +163 -0
- package/dist/agent/snapshot.d.ts +54 -0
- package/dist/agent/snapshot.js +95 -0
- package/dist/agent/take-over.d.ts +19 -0
- package/dist/agent/take-over.js +49 -0
- package/dist/agent/tools/act.d.ts +2 -0
- package/dist/agent/tools/act.js +124 -0
- package/dist/agent/tools/ask.d.ts +1 -0
- package/dist/agent/tools/ask.js +19 -0
- package/dist/agent/tools/context.d.ts +70 -0
- package/dist/agent/tools/context.js +42 -0
- package/dist/agent/tools/expect.d.ts +1 -0
- package/dist/agent/tools/expect.js +152 -0
- package/dist/agent/tools/finish.d.ts +1 -0
- package/dist/agent/tools/finish.js +12 -0
- package/dist/agent/tools/index.d.ts +3 -0
- package/dist/agent/tools/index.js +23 -0
- package/dist/agent/tools/navigate.d.ts +1 -0
- package/dist/agent/tools/navigate.js +53 -0
- package/dist/agent/tools/origin-guard.d.ts +6 -0
- package/dist/agent/tools/origin-guard.js +19 -0
- package/dist/agent/tools/origins.d.ts +11 -0
- package/dist/agent/tools/origins.js +22 -0
- package/dist/agent/tools/screenshot.d.ts +1 -0
- package/dist/agent/tools/screenshot.js +39 -0
- package/dist/agent/tools/snapshot.d.ts +1 -0
- package/dist/agent/tools/snapshot.js +12 -0
- package/dist/agent/tools/undo.d.ts +1 -0
- package/dist/agent/tools/undo.js +23 -0
- package/dist/agent/tools/use-secret.d.ts +1 -0
- package/dist/agent/tools/use-secret.js +41 -0
- package/dist/agent/types.d.ts +128 -0
- package/dist/agent/types.js +1 -0
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +2 -0
- package/dist/cli/agent-run.d.ts +43 -0
- package/dist/cli/agent-run.js +132 -0
- package/dist/cli/args.d.ts +8 -0
- package/dist/cli/args.js +8 -0
- package/dist/cli/generate.d.ts +6 -0
- package/dist/cli/generate.js +48 -0
- package/dist/cli/io.d.ts +2 -0
- package/dist/cli/main.js +6 -0
- package/dist/cli/plan.d.ts +6 -0
- package/dist/cli/plan.js +28 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.js +16 -0
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +26 -1
- package/dist/index.js +2 -0
- package/dist/recorder/normalise.d.ts +6 -0
- package/dist/recorder/normalise.js +2 -0
- package/dist/types/config.d.ts +5 -0
- package/package.json +19 -4
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { parseRunnerFlags, parseRunOverrides, } from "./args.js";
|
|
2
|
+
async function loadAgentEntry(command) {
|
|
3
|
+
try {
|
|
4
|
+
return await import("../agent/index.js");
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
if (error.code === "ERR_MODULE_NOT_FOUND")
|
|
8
|
+
throw new Error(`samurai ${command} 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)`);
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function origin(raw) {
|
|
13
|
+
try {
|
|
14
|
+
return new URL(raw).origin;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`--yes-origin: "${raw}" is not a URL such as https://shop.example`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The model the run uses: `--model`, else `SAMURAI_AI_MODEL`, else the config's `ai.model` */
|
|
21
|
+
async function chosenModel(agent, flag, env) {
|
|
22
|
+
const spec = flag ?? (await agent.defaultModelSpec(env));
|
|
23
|
+
if (!spec)
|
|
24
|
+
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");
|
|
25
|
+
return {
|
|
26
|
+
spec,
|
|
27
|
+
model: await agent.modelFromSpec(spec, { projectDir: process.cwd(), env }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What `generate` and `plan` share: the flags both read (browser, origins, secrets, limits, ...), the model and
|
|
32
|
+
* the notice of which provider receives the pages. Throws a usage error for a bad flag.
|
|
33
|
+
*/
|
|
34
|
+
export async function prepareAgentRun({ values }, io, deps, command) {
|
|
35
|
+
const maxSteps = values["max-steps"] === undefined ? undefined : Number(values["max-steps"]);
|
|
36
|
+
if (maxSteps !== undefined && (!Number.isInteger(maxSteps) || maxSteps < 1))
|
|
37
|
+
throw new Error(`--max-steps must be a whole number from 1, got "${values["max-steps"]}"`);
|
|
38
|
+
const flags = parseRunnerFlags(values);
|
|
39
|
+
if (flags.browsers && flags.browsers.length > 1)
|
|
40
|
+
throw new Error("--browser: the agent uses one browser");
|
|
41
|
+
const allowedOrigins = (values["yes-origin"] ?? []).map(origin);
|
|
42
|
+
const agent = await (deps.loadAgent ?? (() => loadAgentEntry(command)))();
|
|
43
|
+
const { spec, model } = await chosenModel(agent, values.model, io.env);
|
|
44
|
+
if (values.json !== true)
|
|
45
|
+
io.out(`Model ${spec.slice(0, spec.indexOf(":"))}: the pages the agent reads are sent to that provider.\n`);
|
|
46
|
+
return {
|
|
47
|
+
agent,
|
|
48
|
+
model,
|
|
49
|
+
options: {
|
|
50
|
+
...parseRunOverrides(values, io.env),
|
|
51
|
+
...(values.url !== undefined && { url: values.url }),
|
|
52
|
+
...(flags.browsers && { browser: flags.browsers[0] }),
|
|
53
|
+
...(flags.port !== undefined && { port: flags.port }),
|
|
54
|
+
...(flags.headless !== undefined && { headless: flags.headless }),
|
|
55
|
+
...(allowedOrigins.length > 0 && { allowedOrigins }),
|
|
56
|
+
...(values.secret && { secrets: values.secret }),
|
|
57
|
+
...(values["secret-field"] && { secretFields: values["secret-field"] }),
|
|
58
|
+
...(maxSteps !== undefined && { limits: { maxSteps } }),
|
|
59
|
+
...(io.signal && { signal: io.signal }),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function summaryOf(args) {
|
|
64
|
+
const text = JSON.stringify(args ?? {});
|
|
65
|
+
return text.length > 100 ? `${text.slice(0, 99)}…` : text;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Prints a run's events (or each as a JSON line) and answers its questions in the terminal. Returns the exit code:
|
|
69
|
+
* 0 for a run that finished, 1 for any other end. `summarise` prints the closing text of the command.
|
|
70
|
+
*/
|
|
71
|
+
export async function followRun(session, io, json, summarise) {
|
|
72
|
+
let unanswerable;
|
|
73
|
+
for await (const event of session.events) {
|
|
74
|
+
if (json)
|
|
75
|
+
io.out(`${JSON.stringify(event)}\n`);
|
|
76
|
+
switch (event.type) {
|
|
77
|
+
case "plan":
|
|
78
|
+
if (!json)
|
|
79
|
+
io.out(`${event.text}\n`);
|
|
80
|
+
break;
|
|
81
|
+
case "tool-call":
|
|
82
|
+
if (!json)
|
|
83
|
+
io.out(` · ${event.tool} ${summaryOf(event.args)}\n`);
|
|
84
|
+
break;
|
|
85
|
+
case "step":
|
|
86
|
+
if (!json && event.code !== undefined)
|
|
87
|
+
io.out(` ${event.event.op === "insert" ? "+" : "~"} ${event.code}\n`);
|
|
88
|
+
break;
|
|
89
|
+
case "plan-step":
|
|
90
|
+
if (!json)
|
|
91
|
+
io.out(` + ${event.n}. ${event.text}\n`);
|
|
92
|
+
break;
|
|
93
|
+
case "question": {
|
|
94
|
+
const originQuestion = event.options?.join("/") === "allow/deny";
|
|
95
|
+
if (io.prompt && !json) {
|
|
96
|
+
io.out(`? ${event.question}${event.options ? ` (${event.options.join("/")})` : ""}\n`);
|
|
97
|
+
void io
|
|
98
|
+
.prompt("> ")
|
|
99
|
+
.then((answer) => session.answer(event.id, answer))
|
|
100
|
+
.catch(() => undefined); // the run ended while the question was open
|
|
101
|
+
}
|
|
102
|
+
else if (originQuestion) {
|
|
103
|
+
session.answer(event.id, "deny");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
unanswerable = event.question;
|
|
107
|
+
session.stop();
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case "ended":
|
|
112
|
+
if (!json)
|
|
113
|
+
summarise(event.result);
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (unanswerable !== undefined)
|
|
120
|
+
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`);
|
|
121
|
+
return (await session.result).reason === "finished" ? 0 : 1;
|
|
122
|
+
}
|
|
123
|
+
const ENDINGS = {
|
|
124
|
+
stopped: "Stopped.",
|
|
125
|
+
"step-limit": "Stopped at the step limit (--max-steps).",
|
|
126
|
+
"token-limit": "Stopped at the token limit.",
|
|
127
|
+
"time-limit": "Stopped at the time limit.",
|
|
128
|
+
};
|
|
129
|
+
/** The opening words of the closing text: "Done" or how the run stopped */
|
|
130
|
+
export function endingOf(reason) {
|
|
131
|
+
return reason === "finished" ? "Done" : (ENDINGS[reason] ?? reason);
|
|
132
|
+
}
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -24,7 +24,15 @@ export declare function parseCommandLine(argv: string[]): {
|
|
|
24
24
|
test?: string | undefined;
|
|
25
25
|
at?: string | undefined;
|
|
26
26
|
new?: string | undefined;
|
|
27
|
+
plan?: string | undefined;
|
|
28
|
+
out?: string | undefined;
|
|
29
|
+
force?: boolean | undefined;
|
|
27
30
|
url?: string | undefined;
|
|
31
|
+
model?: string | undefined;
|
|
32
|
+
"max-steps"?: string | undefined;
|
|
33
|
+
"yes-origin"?: string[] | undefined;
|
|
34
|
+
secret?: string[] | undefined;
|
|
35
|
+
"secret-field"?: string[] | undefined;
|
|
28
36
|
build?: string | undefined;
|
|
29
37
|
unused?: boolean | undefined;
|
|
30
38
|
help?: boolean | undefined;
|
package/dist/cli/args.js
CHANGED
|
@@ -19,7 +19,15 @@ const FLAGS = {
|
|
|
19
19
|
test: { type: "string" },
|
|
20
20
|
at: { type: "string" },
|
|
21
21
|
new: { type: "string" },
|
|
22
|
+
plan: { type: "string" },
|
|
23
|
+
out: { type: "string" },
|
|
24
|
+
force: { type: "boolean" },
|
|
22
25
|
url: { type: "string" },
|
|
26
|
+
model: { type: "string" },
|
|
27
|
+
"max-steps": { type: "string" },
|
|
28
|
+
"yes-origin": { type: "string", multiple: true },
|
|
29
|
+
secret: { type: "string", multiple: true },
|
|
30
|
+
"secret-field": { type: "string", multiple: true },
|
|
23
31
|
build: { type: "string" },
|
|
24
32
|
unused: { type: "boolean" },
|
|
25
33
|
help: { type: "boolean", short: "h" },
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type AgentCommandDeps } from "./agent-run.js";
|
|
2
|
+
import type { ParsedCommandLine } from "./args.js";
|
|
3
|
+
import type { Io } from "./io.js";
|
|
4
|
+
export type GenerateDeps = AgentCommandDeps;
|
|
5
|
+
/** `samurai generate "<goal>"`: lets the AI agent write a test and prints what it does. Returns the exit code */
|
|
6
|
+
export declare function generateCommand(parsed: ParsedCommandLine, io: Io, deps?: GenerateDeps): Promise<number>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { endingOf, followRun, prepareAgentRun, } from "./agent-run.js";
|
|
2
|
+
const USAGE_LINE = 'Usage: samurai generate "<goal>" --file <spec> [--new <title>] [--test <name|index>] [--plan <plan.md>] [--url <url>] [--model <provider:name>] [--max-steps <n>] [--yes-origin <origin>] [--secret <NAME>] [--json]';
|
|
3
|
+
/** `samurai generate "<goal>"`: lets the AI agent write a test and prints what it does. Returns the exit code */
|
|
4
|
+
export async function generateCommand(parsed, io, deps = {}) {
|
|
5
|
+
const { values, positionals } = parsed;
|
|
6
|
+
const goal = positionals[0];
|
|
7
|
+
if (!goal)
|
|
8
|
+
throw new Error(`Which goal? ${USAGE_LINE}`);
|
|
9
|
+
const files = values.file ?? [];
|
|
10
|
+
if (files.length !== 1)
|
|
11
|
+
throw new Error(`Give exactly one --file <spec>. ${USAGE_LINE}`);
|
|
12
|
+
const test = values.test === undefined
|
|
13
|
+
? undefined
|
|
14
|
+
: /^\d+$/.test(values.test)
|
|
15
|
+
? Number(values.test)
|
|
16
|
+
: values.test;
|
|
17
|
+
const { agent, model, options } = await prepareAgentRun(parsed, io, deps, "generate");
|
|
18
|
+
const plan = values.plan === undefined ? undefined : readPlanFlag(agent, values.plan);
|
|
19
|
+
const session = agent.runAgent({
|
|
20
|
+
...options,
|
|
21
|
+
model,
|
|
22
|
+
goal,
|
|
23
|
+
file: files[0],
|
|
24
|
+
...(test !== undefined && { test }),
|
|
25
|
+
...(values.new !== undefined && { create: values.new }),
|
|
26
|
+
...(plan !== undefined && { plan }),
|
|
27
|
+
});
|
|
28
|
+
return followRun(session, io, values.json === true, (result) => summarise(io, result));
|
|
29
|
+
}
|
|
30
|
+
/** The text of `--plan <file>`; a file that cannot be used is a usage error that names it */
|
|
31
|
+
function readPlanFlag(agent, file) {
|
|
32
|
+
try {
|
|
33
|
+
return agent.readPlan(file);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`--plan: ${error.message.replace(/^readPlan\(\): /, "")}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function summarise(io, result) {
|
|
40
|
+
const base = `"${result.test}" has ${result.steps} ${result.steps === 1 ? "step" : "steps"} in ${result.file}.`;
|
|
41
|
+
if (result.reason === "error")
|
|
42
|
+
io.err(`\nThe run failed: ${result.error ?? "unknown error"}\n${base}\n`);
|
|
43
|
+
else
|
|
44
|
+
io.out(`\n${endingOf(result.reason)} ${base}\n${result.summary ? `${result.summary}\n` : ""}`);
|
|
45
|
+
if (result.secrets.length > 0)
|
|
46
|
+
io.out(`Set ${result.secrets.map((name) => `SAMURAI_SECRET_${name}`).join(", ")} (environment variable or .env.<environment>) before running it.\n`);
|
|
47
|
+
io.out(`${result.tokens} tokens used. Review the steps before you run the test.\n`);
|
|
48
|
+
}
|
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,7 +3,9 @@ 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";
|
|
8
|
+
import { planCommand } from "./plan.js";
|
|
7
9
|
import { recordCommand } from "./record.js";
|
|
8
10
|
import { USAGE } from "./usage.js";
|
|
9
11
|
import { frameworkVersion } from "./version.js";
|
|
@@ -36,6 +38,10 @@ export async function main(argv, io) {
|
|
|
36
38
|
io.out("\nNext: install the dependencies, then `samurai browsers install firefox`, then `samurai run`.\n");
|
|
37
39
|
return 0;
|
|
38
40
|
}
|
|
41
|
+
if (command === "generate")
|
|
42
|
+
return await generateCommand({ values, positionals }, io);
|
|
43
|
+
if (command === "plan")
|
|
44
|
+
return await planCommand({ values, positionals }, io);
|
|
39
45
|
if (command === "record")
|
|
40
46
|
return await recordCommand({ values, positionals }, io);
|
|
41
47
|
if (command !== "run" && command !== "list") {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type AgentCommandDeps } from "./agent-run.js";
|
|
2
|
+
import type { ParsedCommandLine } from "./args.js";
|
|
3
|
+
import type { Io } from "./io.js";
|
|
4
|
+
export type PlanDeps = AgentCommandDeps;
|
|
5
|
+
/** `samurai plan "<goal>"`: lets the AI agent explore the page and write a test plan, without submitting anything */
|
|
6
|
+
export declare function planCommand(parsed: ParsedCommandLine, io: Io, deps?: PlanDeps): Promise<number>;
|
package/dist/cli/plan.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { endingOf, followRun, prepareAgentRun, } from "./agent-run.js";
|
|
2
|
+
const USAGE_LINE = 'Usage: samurai plan "<goal>" --out <plan.md> [--force] [--url <url>] [--model <provider:name>] [--max-steps <n>] [--yes-origin <origin>] [--secret <NAME>] [--json]';
|
|
3
|
+
/** `samurai plan "<goal>"`: lets the AI agent explore the page and write a test plan, without submitting anything */
|
|
4
|
+
export async function planCommand(parsed, io, deps = {}) {
|
|
5
|
+
const { values, positionals } = parsed;
|
|
6
|
+
const goal = positionals[0];
|
|
7
|
+
if (!goal)
|
|
8
|
+
throw new Error(`Which goal? ${USAGE_LINE}`);
|
|
9
|
+
if (values.out === undefined || values.out === "")
|
|
10
|
+
throw new Error(`Give the plan file with --out <plan.md>. ${USAGE_LINE}`);
|
|
11
|
+
const { agent, model, options } = await prepareAgentRun(parsed, io, deps, "plan");
|
|
12
|
+
const session = agent.runPlan({
|
|
13
|
+
...options,
|
|
14
|
+
model,
|
|
15
|
+
goal,
|
|
16
|
+
out: values.out,
|
|
17
|
+
...(values.force === true && { force: true }),
|
|
18
|
+
});
|
|
19
|
+
return followRun(session, io, values.json === true, (result) => summarise(io, result));
|
|
20
|
+
}
|
|
21
|
+
function summarise(io, result) {
|
|
22
|
+
const base = `The plan in ${result.file} has ${result.steps} ${result.steps === 1 ? "step" : "steps"}.`;
|
|
23
|
+
if (result.reason === "error")
|
|
24
|
+
io.err(`\nThe run failed: ${result.error ?? "unknown error"}\n${base}\n`);
|
|
25
|
+
else
|
|
26
|
+
io.out(`\n${endingOf(result.reason)} ${base}\n${result.summary ? `${result.summary}\n` : ""}`);
|
|
27
|
+
io.out(`${result.tokens} tokens used. Read the plan, fix what is wrong, then: samurai generate "<goal>" --plan ${result.file} --file <spec>\n`);
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/cli/usage.d.ts
CHANGED
|
@@ -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; 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\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 plan \"<goal>\" Let an AI agent explore a page and write a test plan (Markdown); it submits nothing\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 plan (also --env, --browser, --port, --headless, --json and the model, limit and\norigin options of generate below):\n --out <plan.md> The plan file to write (required); written after every step\n --force Replace --out when it exists\n The page is read-only while planning: every request that is not a GET is blocked.\n\nOptions for generate (also --env, --browser, --port, --headless, --json):\n --file <spec> The spec file to write into (required, once)\n --plan <plan.md> A plan from samurai plan: the agent follows it and checks each step\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, plan: the agent finished), 1 when a test failed (generate, plan: 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,8 @@ 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 plan "<goal>" Let an AI agent explore a page and write a test plan (Markdown); it submits nothing
|
|
9
|
+
samurai generate "<goal>" Let an AI agent write a test into a spec file (needs ai, zod and a provider)
|
|
8
10
|
samurai init [folder] Scaffold a project: config, an example test, .env.example
|
|
9
11
|
samurai flow <command> List, check and run the flows in ./flows (see below)
|
|
10
12
|
samurai browsers [command] List, install and remove the browsers SAMURAI launches
|
|
@@ -41,10 +43,33 @@ Options for record (also --env, --port, --json; always with a window):
|
|
|
41
43
|
--url <url> Page to start on (default: the environment's baseURL)
|
|
42
44
|
Stop with Ctrl+C or by closing the browser window.
|
|
43
45
|
|
|
46
|
+
Options for plan (also --env, --browser, --port, --headless, --json and the model, limit and
|
|
47
|
+
origin options of generate below):
|
|
48
|
+
--out <plan.md> The plan file to write (required); written after every step
|
|
49
|
+
--force Replace --out when it exists
|
|
50
|
+
The page is read-only while planning: every request that is not a GET is blocked.
|
|
51
|
+
|
|
52
|
+
Options for generate (also --env, --browser, --port, --headless, --json):
|
|
53
|
+
--file <spec> The spec file to write into (required, once)
|
|
54
|
+
--plan <plan.md> A plan from samurai plan: the agent follows it and checks each step
|
|
55
|
+
--new <title> Add a test with this title (creates the file if needed)
|
|
56
|
+
--test <name|index> Write into this test; not needed when the file has one
|
|
57
|
+
--url <url> Page to start on (default: the environment's baseURL)
|
|
58
|
+
--model <provider:name> anthropic, openai or openai-compatible, for example
|
|
59
|
+
anthropic:claude-sonnet-4-5. Default: SAMURAI_AI_MODEL, then the
|
|
60
|
+
config's ai.model. Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY,
|
|
61
|
+
SAMURAI_AI_API_KEY (+ SAMURAI_AI_BASE_URL for openai-compatible)
|
|
62
|
+
--max-steps <n> Most model calls in the run (default 40)
|
|
63
|
+
--yes-origin <origin> A site the agent may open without asking; repeatable
|
|
64
|
+
--secret <NAME> A secret the agent may type (it never sees the value); repeatable
|
|
65
|
+
--secret-field <css> A field whose value is never shown to the model; repeatable
|
|
66
|
+
Questions are asked in the terminal; without one (or with --json) an unknown site is refused
|
|
67
|
+
and any other question ends the run.
|
|
68
|
+
|
|
44
69
|
-h, --help Show this help
|
|
45
70
|
-v, --version Show the version
|
|
46
71
|
|
|
47
|
-
Exit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.
|
|
72
|
+
Exit code: 0 when every test passed (generate, plan: the agent finished), 1 when a test failed (generate, plan: it ended at a limit, was stopped or failed), 2 for a usage or configuration error.
|
|
48
73
|
|
|
49
74
|
samurai flow:
|
|
50
75
|
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":
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.22.5",
|
|
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
|
-
"
|
|
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": {
|