@pcamarajr/scout 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ import { anthropic } from "@ai-sdk/anthropic";
2
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
3
+ import { createVertex } from "@ai-sdk/google-vertex";
4
+ import { openai } from "@ai-sdk/openai";
5
+ import { generateText, stepCountIs, tool, } from "ai";
6
+ import { detectAiCredentials } from "../../credentials.js";
7
+ /**
8
+ * Maps a provider + model id onto the matching AI SDK `LanguageModel`.
9
+ *
10
+ * anthropic → @ai-sdk/anthropic
11
+ * openai → @ai-sdk/openai
12
+ * google → @ai-sdk/google when a Gemini/Generative-AI API key is present;
13
+ * otherwise @ai-sdk/google-vertex (keyless, via Application Default
14
+ * Credentials). The split is driven by the same credential ladder
15
+ * `detectAiCredentials("google")` reports, so the engine and
16
+ * `scout doctor` agree on which sub-provider runs.
17
+ *
18
+ * For Vertex we pass `project` from GOOGLE_CLOUD_PROJECT when set (the provider
19
+ * otherwise reads GOOGLE_VERTEX_PROJECT) and default `location` to
20
+ * GOOGLE_VERTEX_LOCATION or "us-central1" — the Vertex provider requires a
21
+ * location eagerly at model construction, so we supply a sensible default to
22
+ * keep the keyless path zero-config. ADC itself comes from google-auth-library.
23
+ */
24
+ export function resolveModel(provider, modelId) {
25
+ switch (provider) {
26
+ case "anthropic":
27
+ return anthropic(modelId);
28
+ case "openai":
29
+ return openai(modelId);
30
+ case "google": {
31
+ const status = detectAiCredentials("google");
32
+ const usesApiKey = status.ok && status.source !== undefined && /API_KEY$/.test(status.source);
33
+ if (usesApiKey)
34
+ return createGoogleGenerativeAI()(modelId);
35
+ const project = process.env.GOOGLE_CLOUD_PROJECT?.trim();
36
+ const location = process.env.GOOGLE_VERTEX_LOCATION?.trim() || "us-central1";
37
+ return createVertex({ location, ...(project ? { project } : {}) })(modelId);
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Second engine, built on the Vercel AI SDK. It runs the SAME engine-neutral
43
+ * ScoutTool set through `generateText`'s tool-calling loop, against Anthropic,
44
+ * Google (Gemini API key or Vertex ADC) or OpenAI — selected per the spec's
45
+ * provider via {@link resolveModel}. The shared orchestrator owns prompts,
46
+ * verdict capture and the forced-verdict rescue — this class only drives the SDK
47
+ * loop and maps the SDK's `finishReason` into the QueryEndInfo vocabulary the
48
+ * rescue understands.
49
+ */
50
+ export class AiSdkEngine {
51
+ opts;
52
+ constructor(opts = {}) {
53
+ this.opts = opts;
54
+ }
55
+ async run(spec) {
56
+ const model = this.opts.model ?? resolveModel(spec.provider, spec.model);
57
+ const tools = buildAiSdkTools(spec.tools);
58
+ const transcript = [];
59
+ // Running conversation so resume() can append the rescue prompt and
60
+ // continue from where the agent left off.
61
+ const messages = [{ role: "user", content: spec.userPrompt }];
62
+ const abortController = new AbortController();
63
+ const step = async (maxTurns) => {
64
+ try {
65
+ const result = await generateText({
66
+ model,
67
+ system: spec.systemPrompt,
68
+ messages,
69
+ tools,
70
+ stopWhen: stepCountIs(maxTurns),
71
+ abortSignal: abortController.signal,
72
+ });
73
+ // Collect assistant text across every step in order.
74
+ for (const s of result.steps) {
75
+ if (s.text && s.text.trim())
76
+ transcript.push(s.text.trim());
77
+ }
78
+ // Persist the full assistant/tool exchange so the next step continues it.
79
+ messages.push(...result.response.messages);
80
+ return toEndInfo(result.finishReason, result.steps.length);
81
+ }
82
+ catch (error) {
83
+ return {
84
+ subtype: "error_during_execution",
85
+ errors: [error instanceof Error ? error.message : String(error)],
86
+ };
87
+ }
88
+ };
89
+ const end = await step(spec.maxTurns);
90
+ const resume = async (prompt, maxTurns) => {
91
+ const before = transcript.length;
92
+ messages.push({ role: "user", content: prompt });
93
+ const resumeEnd = await step(maxTurns);
94
+ return { end: resumeEnd, transcript: transcript.slice(before) };
95
+ };
96
+ return { end, transcript, resume };
97
+ }
98
+ }
99
+ /**
100
+ * Maps the AI SDK `finishReason` onto the QueryEndInfo.subtype vocabulary that
101
+ * {@link describeNoVerdict} and the forced-verdict rescue already speak.
102
+ *
103
+ * tool-calls / length → "error_max_turns" (hit the step budget mid-flight)
104
+ * stop → "success" (model ended its turn cleanly)
105
+ * error / content-filter / other → "error_during_execution"
106
+ *
107
+ * Rationale: when the loop stops on `tool-calls` it means `stopWhen` cut it off
108
+ * with a tool call still pending — i.e. it would have kept going, the budget ran
109
+ * out (exactly the Agent SDK's `error_max_turns`). `length` is the token-budget
110
+ * analog. Only a clean `stop` means the model chose to end its turn.
111
+ */
112
+ export function toEndInfo(finishReason, numTurns) {
113
+ switch (finishReason) {
114
+ case "tool-calls":
115
+ case "length":
116
+ return { subtype: "error_max_turns", numTurns };
117
+ case "stop":
118
+ return { subtype: "success", numTurns };
119
+ default:
120
+ return { subtype: "error_during_execution", numTurns, errors: [`finishReason=${finishReason}`] };
121
+ }
122
+ }
123
+ /**
124
+ * Adapts the engine-neutral ScoutTool set into AI SDK tools. `execute` returns
125
+ * the tool text; on a tool error we return the error string (prefixed "ERRO:",
126
+ * same as the handler produces) rather than throwing. Returning a string keeps
127
+ * the model in the loop with the failure visible — the same recovery affordance
128
+ * the Agent SDK gives via an isError tool result — instead of aborting the run.
129
+ */
130
+ function buildAiSdkTools(scoutTools) {
131
+ const entries = scoutTools.map((scoutTool) => [
132
+ scoutTool.name,
133
+ tool({
134
+ description: scoutTool.description,
135
+ inputSchema: scoutTool.schema,
136
+ execute: async (args) => {
137
+ const result = await scoutTool.handler(args);
138
+ return result.text;
139
+ },
140
+ }),
141
+ ]);
142
+ return Object.fromEntries(entries);
143
+ }
144
+ //# sourceMappingURL=ai-sdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-sdk.js","sourceRoot":"","sources":["../../../src/runner/engines/ai-sdk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { AgentEngine, EngineRunSpec, EngineSession } from "./types.js";
2
+ /**
3
+ * The original, trusted engine: runs the verification through the
4
+ * `@anthropic-ai/claude-agent-sdk` subprocess. Behavior here is intentionally
5
+ * IDENTICAL to the pre-refactor ai-runner: bypassPermissions, empty
6
+ * settingSources, allowedTools scoped to the in-process MCP browser server, the
7
+ * query()+drain() loop, and an abort-in-finally so no subprocess outlives a run.
8
+ */
9
+ export declare class ClaudeAgentSdkEngine implements AgentEngine {
10
+ run(spec: EngineRunSpec): Promise<EngineSession>;
11
+ }
@@ -0,0 +1,117 @@
1
+ import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
2
+ /**
3
+ * The original, trusted engine: runs the verification through the
4
+ * `@anthropic-ai/claude-agent-sdk` subprocess. Behavior here is intentionally
5
+ * IDENTICAL to the pre-refactor ai-runner: bypassPermissions, empty
6
+ * settingSources, allowedTools scoped to the in-process MCP browser server, the
7
+ * query()+drain() loop, and an abort-in-finally so no subprocess outlives a run.
8
+ */
9
+ export class ClaudeAgentSdkEngine {
10
+ async run(spec) {
11
+ const browserServer = createSdkMcpServer({
12
+ name: "browser",
13
+ version: "1.0.0",
14
+ tools: spec.tools.map(toAgentSdkTool),
15
+ });
16
+ const baseOptions = {
17
+ model: spec.model,
18
+ systemPrompt: spec.systemPrompt,
19
+ mcpServers: { browser: browserServer },
20
+ allowedTools: ["mcp__browser__*"],
21
+ tools: [],
22
+ settingSources: [],
23
+ permissionMode: "bypassPermissions",
24
+ };
25
+ const transcript = [];
26
+ let sessionId;
27
+ let endInfo;
28
+ // Drains a query, collecting transcript/session/end info. Always aborts at
29
+ // the end so no claude-agent-sdk subprocess outlives the run.
30
+ const drain = async (q, controller) => {
31
+ try {
32
+ for await (const message of q) {
33
+ if ("session_id" in message && message.session_id)
34
+ sessionId = message.session_id;
35
+ if (message.type === "assistant") {
36
+ for (const block of message.message.content) {
37
+ if (block.type === "text" && block.text.trim())
38
+ transcript.push(block.text.trim());
39
+ }
40
+ }
41
+ if (message.type === "result") {
42
+ endInfo = {
43
+ subtype: message.subtype,
44
+ numTurns: message.num_turns,
45
+ errors: "errors" in message ? message.errors : undefined,
46
+ };
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ finally {
52
+ controller.abort();
53
+ }
54
+ };
55
+ const mainController = new AbortController();
56
+ try {
57
+ await drain(query({
58
+ prompt: spec.userPrompt,
59
+ options: { ...baseOptions, maxTurns: spec.maxTurns, abortController: mainController },
60
+ }), mainController);
61
+ }
62
+ catch (error) {
63
+ endInfo = {
64
+ subtype: "sdk_error",
65
+ errors: [error instanceof Error ? error.message : String(error)],
66
+ };
67
+ }
68
+ const resume = async (prompt, maxTurns) => {
69
+ const resumeTranscript = [];
70
+ let resumeEnd;
71
+ if (!sessionId) {
72
+ return { end: { subtype: "no_session" }, transcript: resumeTranscript };
73
+ }
74
+ const rescueController = new AbortController();
75
+ // Reuse drain() so transcript/session/end semantics stay identical, then
76
+ // split off only what the rescue produced for the resume result.
77
+ const beforeLen = transcript.length;
78
+ try {
79
+ await drain(query({
80
+ prompt,
81
+ options: { ...baseOptions, maxTurns, resume: sessionId, abortController: rescueController },
82
+ }), rescueController);
83
+ resumeEnd = endInfo;
84
+ }
85
+ catch (error) {
86
+ resumeEnd = {
87
+ subtype: "sdk_error",
88
+ errors: [error instanceof Error ? error.message : String(error)],
89
+ };
90
+ }
91
+ resumeTranscript.push(...transcript.slice(beforeLen));
92
+ return { end: resumeEnd ?? { subtype: "no_session" }, transcript: resumeTranscript };
93
+ };
94
+ return {
95
+ get end() {
96
+ return endInfo ?? { subtype: "sdk_error" };
97
+ },
98
+ transcript,
99
+ resume,
100
+ };
101
+ }
102
+ }
103
+ /**
104
+ * Adapts an engine-neutral ScoutTool into the Agent SDK `tool()` shape. The
105
+ * Agent SDK's tool() wants a raw Zod shape, so we hand it `schema.shape`, and we
106
+ * map our normalized {text, isError} result back into a CallToolResult.
107
+ */
108
+ function toAgentSdkTool(scoutTool) {
109
+ return tool(scoutTool.name, scoutTool.description, scoutTool.schema.shape, async (args) => {
110
+ const result = await scoutTool.handler(args);
111
+ return {
112
+ content: [{ type: "text", text: result.text }],
113
+ ...(result.isError ? { isError: true } : {}),
114
+ };
115
+ });
116
+ }
117
+ //# sourceMappingURL=claude-agent-sdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-agent-sdk.js","sourceRoot":"","sources":["../../../src/runner/engines/claude-agent-sdk.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { AiProvider } from "../../credentials.js";
2
+ import { AiSdkEngine } from "./ai-sdk.js";
3
+ import { ClaudeAgentSdkEngine } from "./claude-agent-sdk.js";
4
+ import type { AgentEngine } from "./types.js";
5
+ /** The two engines Scout can run an AI verification through. */
6
+ export type EngineKind = "agent-sdk" | "ai-sdk";
7
+ /**
8
+ * Resolves which engine to use. Explicit preference (SCOUT_ENGINE / config
9
+ * `engine`) wins. Otherwise the default is forward-looking: the trusted Agent
10
+ * SDK for Anthropic, the AI SDK for non-Anthropic providers (which is the only
11
+ * engine that can reach them — still gated by credential detection until those
12
+ * providers are wired up).
13
+ */
14
+ export declare function resolveEngineKind(provider: AiProvider, enginePref: EngineKind | undefined): EngineKind;
15
+ /** Instantiates the engine for a provider + explicit preference. */
16
+ export declare function selectEngine(provider: AiProvider, enginePref: EngineKind | undefined): AgentEngine;
17
+ /** Parses a raw SCOUT_ENGINE / config value, ignoring unknown strings. */
18
+ export declare function parseEngineKind(value: string | undefined): EngineKind | undefined;
19
+ export { AiSdkEngine, ClaudeAgentSdkEngine };
20
+ export type { AgentEngine };
@@ -0,0 +1,28 @@
1
+ import { AiSdkEngine } from "./ai-sdk.js";
2
+ import { ClaudeAgentSdkEngine } from "./claude-agent-sdk.js";
3
+ /**
4
+ * Resolves which engine to use. Explicit preference (SCOUT_ENGINE / config
5
+ * `engine`) wins. Otherwise the default is forward-looking: the trusted Agent
6
+ * SDK for Anthropic, the AI SDK for non-Anthropic providers (which is the only
7
+ * engine that can reach them — still gated by credential detection until those
8
+ * providers are wired up).
9
+ */
10
+ export function resolveEngineKind(provider, enginePref) {
11
+ if (enginePref)
12
+ return enginePref;
13
+ return provider === "anthropic" ? "agent-sdk" : "ai-sdk";
14
+ }
15
+ /** Instantiates the engine for a provider + explicit preference. */
16
+ export function selectEngine(provider, enginePref) {
17
+ return resolveEngineKind(provider, enginePref) === "ai-sdk"
18
+ ? new AiSdkEngine()
19
+ : new ClaudeAgentSdkEngine();
20
+ }
21
+ /** Parses a raw SCOUT_ENGINE / config value, ignoring unknown strings. */
22
+ export function parseEngineKind(value) {
23
+ if (value === "agent-sdk" || value === "ai-sdk")
24
+ return value;
25
+ return undefined;
26
+ }
27
+ export { AiSdkEngine, ClaudeAgentSdkEngine };
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/runner/engines/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAM7D;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAoB,EACpB,UAAkC;IAElC,IAAI,UAAU;QAAE,OAAO,UAAU,CAAC;IAClC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3D,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAC1B,QAAoB,EACpB,UAAkC;IAElC,OAAO,iBAAiB,CAAC,QAAQ,EAAE,UAAU,CAAC,KAAK,QAAQ;QACzD,CAAC,CAAC,IAAI,WAAW,EAAE;QACnB,CAAC,CAAC,IAAI,oBAAoB,EAAE,CAAC;AACjC,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,eAAe,CAAC,KAAyB;IACvD,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { AiProvider } from "../../credentials.js";
2
+ import type { Step, Verdict } from "../../types.js";
3
+ import type { ScoutTool } from "../agent-tools.js";
4
+ export interface AiRunOutcome {
5
+ verdict: Verdict;
6
+ reason: string;
7
+ steps: Step[];
8
+ transcript: string[];
9
+ /**
10
+ * Set when the runner itself failed to produce a verdict (agent never called
11
+ * scout_verdict) — NOT a UI judgment. Callers may retry and must report it
12
+ * as an infrastructure failure, never as "the scenario is blocked by the UI".
13
+ */
14
+ runnerFailure?: string;
15
+ }
16
+ /**
17
+ * How an engine run ended, for no-verdict diagnostics. The Agent SDK fills this
18
+ * from the SDK `result` message; the AI SDK engine maps its `finishReason` into
19
+ * the same `subtype` vocabulary so {@link describeNoVerdict} works unchanged.
20
+ */
21
+ export interface QueryEndInfo {
22
+ subtype: string;
23
+ numTurns?: number;
24
+ errors?: string[];
25
+ }
26
+ /**
27
+ * Human-readable cause for a run that ended without scout_verdict.
28
+ * Distinguishes runner-infrastructure causes (turn budget, SDK errors) from
29
+ * an agent that simply stopped talking.
30
+ */
31
+ export declare function describeNoVerdict(end: QueryEndInfo | undefined, maxTurns: number): string;
32
+ /**
33
+ * Records navigation relative to the configured baseUrl so cached scripts
34
+ * survive --base-url / SCOUT_BASE_URL pointing at another server. URLs that
35
+ * merely share the prefix (http://localhost:3000x) or live on other hosts
36
+ * stay absolute.
37
+ */
38
+ export declare function relativizeUrl(url: string, baseUrl: string): string;
39
+ /** Everything an engine needs to drive one agent run. */
40
+ export interface EngineRunSpec {
41
+ /** Inferred from the model id; selects the AI SDK provider in the AI SDK engine. */
42
+ provider: AiProvider;
43
+ model: string;
44
+ systemPrompt: string;
45
+ userPrompt: string;
46
+ tools: ScoutTool[];
47
+ maxTurns: number;
48
+ }
49
+ /** The result of resuming a session (the forced-verdict rescue). */
50
+ export interface EngineResumeResult {
51
+ end: QueryEndInfo;
52
+ transcript: string[];
53
+ }
54
+ /**
55
+ * A live (or just-finished) engine session. The shared orchestrator reads
56
+ * `end`/`transcript`, and — if no verdict was captured — calls `resume()` to
57
+ * run the forced-verdict rescue with a small turn budget.
58
+ */
59
+ export interface EngineSession {
60
+ end: QueryEndInfo;
61
+ /** Assistant text collected during the run, in order. */
62
+ transcript: string[];
63
+ resume(prompt: string, maxTurns: number): Promise<EngineResumeResult>;
64
+ }
65
+ /** A pluggable agent engine. `run` performs the initial verification pass. */
66
+ export interface AgentEngine {
67
+ run(spec: EngineRunSpec): Promise<EngineSession>;
68
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Human-readable cause for a run that ended without scout_verdict.
3
+ * Distinguishes runner-infrastructure causes (turn budget, SDK errors) from
4
+ * an agent that simply stopped talking.
5
+ */
6
+ export function describeNoVerdict(end, maxTurns) {
7
+ if (!end)
8
+ return "a sessão do agente terminou sem emitir resultado (subprocesso morreu?)";
9
+ switch (end.subtype) {
10
+ case "error_max_turns":
11
+ return `o agente estourou o limite de ${maxTurns} turns sem chamar scout_verdict`;
12
+ case "error_during_execution":
13
+ return `erro durante a execução do agente${end.errors?.length ? `: ${end.errors.join("; ")}` : ""}`;
14
+ case "success":
15
+ return "o agente encerrou normalmente sem chamar scout_verdict";
16
+ default:
17
+ return `a sessão do agente terminou com "${end.subtype}"${end.errors?.length ? `: ${end.errors.join("; ")}` : ""}`;
18
+ }
19
+ }
20
+ /**
21
+ * Records navigation relative to the configured baseUrl so cached scripts
22
+ * survive --base-url / SCOUT_BASE_URL pointing at another server. URLs that
23
+ * merely share the prefix (http://localhost:3000x) or live on other hosts
24
+ * stay absolute.
25
+ */
26
+ export function relativizeUrl(url, baseUrl) {
27
+ const base = baseUrl.replace(/\/+$/, "");
28
+ if (url === base || url === `${base}/`)
29
+ return "/";
30
+ if (url.startsWith(base)) {
31
+ const rest = url.slice(base.length);
32
+ if (rest.startsWith("/") || rest.startsWith("?") || rest.startsWith("#"))
33
+ return rest;
34
+ }
35
+ return url;
36
+ }
37
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/runner/engines/types.ts"],"names":[],"mappings":"AA4BA;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAA6B,EAAE,QAAgB;IAC/E,IAAI,CAAC,GAAG;QAAE,OAAO,wEAAwE,CAAC;IAC1F,QAAQ,GAAG,CAAC,OAAO,EAAE,CAAC;QACpB,KAAK,iBAAiB;YACpB,OAAO,iCAAiC,QAAQ,iCAAiC,CAAC;QACpF,KAAK,wBAAwB;YAC3B,OAAO,oCAAoC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACtG,KAAK,SAAS;YACZ,OAAO,wDAAwD,CAAC;QAClE;YACE,OAAO,oCAAoC,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACvH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,OAAe;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACzC,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IACnD,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;IACxF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pcamarajr/scout",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Self-healing browser QA: natural-language scenarios verified by an AI agent, replayed deterministically in CI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,12 +43,18 @@
43
43
  "dev": "tsx src/cli.ts",
44
44
  "typecheck": "tsc --noEmit",
45
45
  "test": "node --import tsx --test tests/*.test.ts",
46
+ "smoke": "node scripts/smoke.mjs",
46
47
  "prepublishOnly": "npm run build",
47
48
  "prepare": "husky"
48
49
  },
49
50
  "dependencies": {
51
+ "@ai-sdk/anthropic": "^3.0.85",
52
+ "@ai-sdk/google": "^3.0.83",
53
+ "@ai-sdk/google-vertex": "^4.0.147",
54
+ "@ai-sdk/openai": "^3.0.73",
50
55
  "@anthropic-ai/claude-agent-sdk": "^0.3.0",
51
56
  "@modelcontextprotocol/sdk": "^1.12.0",
57
+ "ai": "^6.0.208",
52
58
  "commander": "^13.1.0",
53
59
  "gray-matter": "^4.0.3",
54
60
  "playwright": "^1.52.0",