cairn-engine 2.1.0 → 2.2.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.
package/README.md CHANGED
@@ -2,72 +2,179 @@
2
2
 
3
3
  ![cairn banner](https://raw.githubusercontent.com/team-poem/cairn/main/banner.svg)
4
4
 
5
- The engine behind [cairn](https://github.com/team-poem/cairn) — an AI walks an unfamiliar
6
- app **once** to discover a browser test and **freezes it into a marker**; from then on it
7
- replays that path **deterministically, with no LLM in the loop**. When a step breaks or lands
8
- in the wrong state, the LLM returns to **heal** just that step, then re-freezes. **Discovery is
9
- paid once; every replay is free.** Model- and browser-agnostic — embed it, or drive it from the `cairn` CLI.
5
+ [![npm](https://img.shields.io/npm/v/cairn-engine.svg)](https://www.npmjs.com/package/cairn-engine)
6
+ [![CI](https://github.com/team-poem/cairn/actions/workflows/ci.yml/badge.svg)](https://github.com/team-poem/cairn/actions/workflows/ci.yml)
7
+ [![types](https://img.shields.io/npm/types/cairn-engine.svg)](https://www.npmjs.com/package/cairn-engine)
8
+ [![license](https://img.shields.io/npm/l/cairn-engine.svg)](https://github.com/team-poem/cairn/blob/main/LICENSE)
9
+
10
+ **Browser tests you write in plain language — that then run with zero AI in the loop.**
11
+
12
+ An AI walks your app **once** to discover the flow and **freezes** it. From then on it replays
13
+ **deterministically — no LLM, no hand-written selectors.** When the UI changes and a step breaks,
14
+ the AI returns to **heal just that step**, then re-freezes. A third thing, between two tools you
15
+ already reach for:
16
+
17
+ - **Scripted (Playwright/Cypress)** — deterministic, but you hand-write selectors that break every redesign.
18
+ - **LLM agents** — plain language, but a slow, costly, flaky model in _every_ run.
19
+ - **cairn** — plain-language authoring **and** deterministic, free, self-healing replay.
20
+
21
+ ## Use it
10
22
 
11
23
  ```sh
12
24
  npm install cairn-engine
13
25
  ```
14
26
 
15
- ```sh
16
- # discover an LLM walks the app once and writes a scenario
17
- cairn discover "follow the link to learn more" --url https://example.com --freeze t.json
18
- # replay deterministic, no LLM; non-zero exit on failure (CI gate)
19
- cairn replay t.json
20
- # heal repair a broken step via the LLM and re-freeze
21
- cairn replay t.json --heal --freeze t.json
27
+ **Author once** — an AI discovers the flow; you freeze it to a file:
28
+
29
+ ```ts
30
+ import { discover, ChromeDevToolsDriver, createLlmClient } from "cairn-engine";
31
+ import { writeFileSync } from "node:fs";
32
+
33
+ const scenario = await discover(
34
+ "log in, add the first product, open the cart",
35
+ {
36
+ driver: new ChromeDevToolsDriver(),
37
+ llm: createLlmClient(), // Claude Code if installed, else ANTHROPIC_API_KEY
38
+ baseUrl: "https://shop.example",
39
+ },
40
+ );
41
+ writeFileSync("cart.skill.json", JSON.stringify(scenario, null, 2));
22
42
  ```
23
43
 
24
- Embed itevery stage is an injected port:
44
+ **Replay forever**deterministic, no LLM. When the UI drifts, `heal` repairs the step and you
45
+ re-freeze the fixed path:
25
46
 
26
47
  ```ts
27
- import { runScenario } from "cairn-engine";
48
+ import { runScenario, loadSkillFile } from "cairn-engine";
49
+ import { writeFileSync } from "node:fs";
50
+
51
+ const { result, healedScenario } = await runScenario(
52
+ loadSkillFile("cart.skill.json"),
53
+ {
54
+ heal: true, // repair a broken step with the LLM instead of going red
55
+ },
56
+ );
57
+
58
+ if (healedScenario) {
59
+ // the UI changed and cairn adapted — write the repaired path back
60
+ writeFileSync("cart.skill.json", JSON.stringify(healedScenario, null, 2));
61
+ }
62
+ if (!result.verdict.passed) process.exit(1); // a deterministic gate for CI
63
+ ```
64
+
65
+ Prefer a one-off from the terminal? The same steps are CLI commands —
66
+ `cairn discover … --freeze cart.skill.json` · `cairn replay cart.skill.json` · `… --heal`.
67
+
68
+ **Models** — set a key and cairn picks the backend: **Anthropic** (`ANTHROPIC_API_KEY`, or a local
69
+ **Claude Code** install with no key), **OpenAI** (`OPENAI_API_KEY`), or **Gemini**
70
+ (`GEMINI_API_KEY`). Force one with `createLlmClient({ backend: "openai" })`, or implement the
71
+ `LlmClient` port for any other model.
72
+
73
+ ## How the loop works
74
+
75
+ ```
76
+ intent ─► discover (LLM, once) ─► cart.skill.json ─► replay (no LLM, forever)
77
+ │ a step breaks
78
+
79
+ self-heal (LLM, just that step)
80
+ ```
28
81
 
29
- const { result } = await runScenario(scenario, { heal: true });
30
- if (!result.verdict.passed) process.exit(1);
82
+ - **discover** _(LLM · once)_ observes the live page, picks one action, acts, and repeats until your intent is met. Out comes a `Scenario`.
83
+ - **freeze** — that scenario is plain JSON (`*.skill.json`): a flat list of steps + assertions, each target carrying several locators. No model, no LLM — just data.
84
+ - **replay** _(no LLM)_ — runs the steps through a `Driver`, auto-waiting for the page to settle; a `Critic` rules on three layers of evidence — _did it act_ · _what it looked like_ · _the requests & console_. Same input, same verdict.
85
+ - **heal** _(LLM · only on a break)_ — when a target stops resolving or the outcome diverges, the LLM maps your original step `intent` onto the new page, repairs that one step, retries, and returns a scenario to re-freeze. A green replay never calls it.
86
+
87
+ Discovery is paid once; regression is free. A frozen scenario is data you can read, diff, and edit
88
+ by hand:
89
+
90
+ ```json
91
+ {
92
+ "name": "cart",
93
+ "steps": [
94
+ { "kind": "goto", "url": "https://shop.example" },
95
+ {
96
+ "kind": "type",
97
+ "target": { "text": "Email" },
98
+ "text": "you@shop.example"
99
+ },
100
+ {
101
+ "kind": "click",
102
+ "target": { "text": "Log in" },
103
+ "intent": "submit the login form",
104
+ "expect": { "requestStatus": { "urlIncludes": "/auth", "status": 200 } }
105
+ },
106
+ { "kind": "click", "target": { "text": "Add to cart" } },
107
+ { "kind": "click", "target": { "text": "Cart", "role": "link" } },
108
+ { "kind": "waitFor", "until": { "url": "/cart" } }
109
+ ],
110
+ "assertions": [
111
+ { "kind": "navigated", "to": "/cart" },
112
+ { "kind": "no-failed-requests" }
113
+ ]
114
+ }
31
115
  ```
32
116
 
33
- Building a UI on top? The engine exposes the seams; you bring the UI:
117
+ Each `target` keeps several locators `text` (accessible name) first, `role` + `index` as a
118
+ rename-resilient fallback, `selector` as a CSS escape hatch — which is what lets replay survive a
119
+ redesign without falling back to the LLM. The `expect` on a step is its post-condition: replay
120
+ checks it deterministically and only heals if it diverges.
121
+
122
+ **Measured, not claimed** — a real multi-step checkout, via cairn's `bench/` harness:
123
+
124
+ - **4/4 deterministic** replays · **0 LLM calls** on replay
125
+ - discovery **~$0.50 once** → every replay after is **$0** (a full LLM agent runs **~$15–30 _per run_**)
126
+ - a renamed button broke hand-written selectors; cairn **healed it and stayed green**
127
+
128
+ ## Build on it
129
+
130
+ cairn is the machinery — discover · freeze · replay · heal — behind a handful of ports, **general
131
+ in mechanism, specific in meaning.** It's made to be **built on**, not scattered across your
132
+ service as test code. A few things it powers:
133
+
134
+ - **A QA tool** — non-developers write flows in plain language, then watch them replay & self-heal
135
+ - **A CI regression gate** — frozen flows run on every PR; drift heals instead of going red
136
+ - **A synthetic monitor** — replay critical paths against production, alert only when one truly breaks
137
+ - **A visual-replay app** — the engine streams per-step progress + screenshots; you draw the UI
138
+
139
+ You _can_ call `runScenario` straight from a test file — nothing stops you. But that isn't the
140
+ point: cairn is **not a Jest or Playwright you write service tests in** — it's the engine those
141
+ kinds of tools are built _from_. Reach for it to **build** testing tooling, not to author a test
142
+ suite by hand.
143
+
144
+ ## Extend it
145
+
146
+ The core knows no app — **you** supply what "success" means and how to drive the browser. Every
147
+ stage is a replaceable port — your own `Driver` (e.g. Playwright), `Critic`, `Reporter`,
148
+ `ContextProvider` (auth/fixtures), `LlmClient` (any model). Too much for a full port? `custom`
149
+ assertions/actions define success inline:
34
150
 
35
151
  ```ts
36
- const controller = new AbortController();
37
152
  await runScenario(scenario, {
38
- signal: controller.signal, // a Stop button
39
- screenshots: true, // capture a PNG per step
40
- onStep: (e) => render(e.index, e.step, e.ok, e.screenshot), // live timeline
153
+ custom: {
154
+ "cart-has": (p, ev) =>
155
+ ev.logic.requests.some((r) => r.url.includes(p.path) && r.status === 200),
156
+ },
41
157
  });
42
158
  ```
43
159
 
44
- Make it yours the engine ships defaults, your product defines the specifics:
160
+ Building a UI on top? The engine streams exactly what a screen needs — wire it up and draw:
45
161
 
46
162
  ```ts
163
+ const controller = new AbortController();
47
164
  await runScenario(scenario, {
48
- // success is whatever your product says it is
49
- custom: { "cart-has": (p, ev) => ev.logic.requests.some((r) => r.url.includes(p.path) && r.status === 200) },
50
- // product-specific interactions, beyond click/type/hover/select/scroll
51
- actions: { "drag-slider": async (driver, p) => { /* … */ } },
165
+ signal: controller.signal, // a Stop button
166
+ screenshots: true, // a PNG per step
167
+ onStep: (s) => render(s.index, s.step, s.ok, s.screenshot), // a live timeline
52
168
  });
53
169
  ```
54
170
 
55
- Every layer is replaceable: bring your own `Driver` (e.g. Playwright), `Critic`, `Reporter`,
56
- `ContextProvider` (auth / fixtures), or `LlmClient` (any model) and use `custom`
57
- assertions / `actions` for what doesn't fit the built-ins. Nothing forces your product
58
- through only what we decided.
59
-
60
- **Browser or extension (no Node)?** `runScenario` and the default Chrome DevTools MCP driver
61
- need Node. Import the browser-safe core from `cairn-engine/browser` and compose `runHarness`
62
- with your own `Driver` (e.g. one over `chrome.debugger`) plus a fetch-based `LlmClient`:
63
-
64
- ```ts
65
- import { runHarness, StaticPlanner, AssertionCritic, AnthropicLlmClient } from "cairn-engine/browser";
66
- ```
171
+ **Browser / extension (no Node)?** Import the browser-safe core from `cairn-engine/browser` and
172
+ compose `runHarness` with your own `Driver` (e.g. one over `chrome.debugger`) plus a fetch-based
173
+ `LlmClient`.
67
174
 
68
- No API key needed if you have **Claude Code** installed (cairn shells out to it); set
69
- `ANTHROPIC_API_KEY` to use the Anthropic API instead.
175
+ ## Conventions
70
176
 
71
- **Full docs, design, and the loop diagram:** https://github.com/team-poem/cairn
177
+ Name embedded files `*.agentic.ts` + frozen `*.skill.json` — distinct from `*.test.ts` /
178
+ `*.spec.ts`, stable glob `**/*.agentic.ts`.
72
179
 
73
- MIT
180
+ **Full docs · design · the loop:** https://github.com/team-poem/cairn · MIT
@@ -1,4 +1,4 @@
1
- const delay = (ms) => new Promise((r) => setTimeout(r, ms));
1
+ import { postJsonWithRetry } from "./http.js";
2
2
  export class AnthropicLlmClient {
3
3
  id;
4
4
  apiKey;
@@ -18,58 +18,39 @@ export class AnthropicLlmClient {
18
18
  this.id = `anthropic:${this.model}`;
19
19
  }
20
20
  async complete(prompt, opts = {}) {
21
- const body = JSON.stringify({
22
- model: this.model,
23
- max_tokens: opts.maxTokens ?? 1024,
24
- // #15 — cache the (constant) system prompt so repeated discover steps don't re-bill it.
25
- ...(opts.system
26
- ? { system: [{ type: "text", text: opts.system, cache_control: { type: "ephemeral" } }] }
27
- : {}),
28
- messages: [{ role: "user", content: prompt }],
21
+ const data = await postJsonWithRetry(`${this.baseUrl}/v1/messages`, {
22
+ headers: {
23
+ "content-type": "application/json",
24
+ "x-api-key": this.apiKey,
25
+ "anthropic-version": "2023-06-01",
26
+ },
27
+ body: JSON.stringify({
28
+ model: this.model,
29
+ max_tokens: opts.maxTokens ?? 1024,
30
+ // #15 — cache the (constant) system prompt so repeated discover steps don't re-bill it.
31
+ ...(opts.system
32
+ ? {
33
+ system: [
34
+ {
35
+ type: "text",
36
+ text: opts.system,
37
+ cache_control: { type: "ephemeral" },
38
+ },
39
+ ],
40
+ }
41
+ : {}),
42
+ messages: [{ role: "user", content: prompt }],
43
+ }),
44
+ }, {
45
+ timeoutMs: this.timeoutMs,
46
+ maxRetries: this.maxRetries,
47
+ label: "Anthropic",
29
48
  });
30
- for (let attempt = 0;; attempt++) {
31
- const controller = new AbortController();
32
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
33
- try {
34
- const res = await fetch(`${this.baseUrl}/v1/messages`, {
35
- method: "POST",
36
- headers: {
37
- "content-type": "application/json",
38
- "x-api-key": this.apiKey,
39
- "anthropic-version": "2023-06-01",
40
- },
41
- body,
42
- signal: controller.signal,
43
- });
44
- if (res.ok) {
45
- const data = (await res.json());
46
- return (data.content ?? [])
47
- .filter((c) => c.type === "text" && typeof c.text === "string")
48
- .map((c) => c.text)
49
- .join("")
50
- .trim();
51
- }
52
- // Back off on transient errors (rate limit / overloaded / server), else fail.
53
- if ((res.status === 429 || res.status >= 500) && attempt < this.maxRetries) {
54
- await delay(500 * 2 ** attempt);
55
- continue;
56
- }
57
- throw new Error(`Anthropic API ${res.status}: ${await res.text()}`);
58
- }
59
- catch (err) {
60
- const aborted = err instanceof Error && err.name === "AbortError";
61
- if (aborted && attempt >= this.maxRetries)
62
- throw new Error(`Anthropic request timed out after ${this.timeoutMs}ms`);
63
- if (aborted) {
64
- await delay(500 * 2 ** attempt);
65
- continue;
66
- }
67
- throw err;
68
- }
69
- finally {
70
- clearTimeout(timer);
71
- }
72
- }
49
+ return (data.content ?? [])
50
+ .filter((c) => c.type === "text" && typeof c.text === "string")
51
+ .map((c) => c.text)
52
+ .join("")
53
+ .trim();
73
54
  }
74
55
  }
75
56
  //# sourceMappingURL=anthropic.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/adapters/llm/anthropic.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/adapters/llm/anthropic.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAgB9C,MAAM,OAAO,kBAAkB;IACpB,EAAE,CAAS;IACH,MAAM,CAAS;IACf,KAAK,CAAS;IACd,OAAO,CAAS;IAChB,SAAS,CAAS;IAClB,UAAU,CAAS;IAEpC,YAAY,OAAyB,EAAE;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAC5D,IAAI,CAAC,MAAM;YACT,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,mBAAmB,CAAC;QAC/C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,2BAA2B,CAAC;QAC3D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC;QAC1C,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,EAAE,GAAG,aAAa,IAAI,CAAC,KAAK,EAAE,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,OAAwB,EAAE;QACvD,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAClC,GAAG,IAAI,CAAC,OAAO,cAAc,EAC7B;YACE,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,WAAW,EAAE,IAAI,CAAC,MAAM;gBACxB,mBAAmB,EAAE,YAAY;aAClC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;gBAClC,wFAAwF;gBACxF,GAAG,CAAC,IAAI,CAAC,MAAM;oBACb,CAAC,CAAC;wBACE,MAAM,EAAE;4BACN;gCACE,IAAI,EAAE,MAAM;gCACZ,IAAI,EAAE,IAAI,CAAC,MAAM;gCACjB,aAAa,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;6BACrC;yBACF;qBACF;oBACH,CAAC,CAAC,EAAE,CAAC;gBACP,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;aAC9C,CAAC;SACH,EACD;YACE,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,KAAK,EAAE,WAAW;SACnB,CACF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;aACxB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;aAC9D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,IAAI,CAAC,EAAE,CAAC;aACR,IAAI,EAAE,CAAC;IACZ,CAAC;CACF"}
@@ -1,8 +1,12 @@
1
- /** Picks the LLM backend (invariant #5): the API if ANTHROPIC_API_KEY is set, else local Claude Code. */
1
+ /**
2
+ * Picks the LLM backend (invariant #5). An explicit `backend` wins; otherwise the first provider
3
+ * whose API key is present, in order Anthropic → OpenAI → Gemini, falling back to local Claude Code.
4
+ */
2
5
  import type { LlmClient } from "../../core/ports.js";
6
+ export type LlmBackend = "anthropic" | "openai" | "gemini" | "claude-code";
3
7
  export interface LlmFactoryOptions {
4
8
  /** Force a backend regardless of environment. */
5
- backend?: "anthropic" | "claude-code";
9
+ backend?: LlmBackend;
6
10
  model?: string;
7
11
  }
8
12
  export declare function createLlmClient(opts?: LlmFactoryOptions): LlmClient;
@@ -1,9 +1,27 @@
1
1
  import { AnthropicLlmClient } from "./anthropic.js";
2
2
  import { ClaudeCodeLlmClient } from "./claude-code.js";
3
+ import { GeminiLlmClient } from "./gemini.js";
4
+ import { OpenAILlmClient } from "./openai.js";
5
+ function detectBackend() {
6
+ if (process.env.ANTHROPIC_API_KEY)
7
+ return "anthropic";
8
+ if (process.env.OPENAI_API_KEY)
9
+ return "openai";
10
+ if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY)
11
+ return "gemini";
12
+ return "claude-code";
13
+ }
3
14
  export function createLlmClient(opts = {}) {
4
- const backend = opts.backend ?? (process.env.ANTHROPIC_API_KEY ? "anthropic" : "claude-code");
5
- return backend === "anthropic"
6
- ? new AnthropicLlmClient({ model: opts.model })
7
- : new ClaudeCodeLlmClient({ model: opts.model });
15
+ const backend = opts.backend ?? detectBackend();
16
+ switch (backend) {
17
+ case "openai":
18
+ return new OpenAILlmClient({ model: opts.model });
19
+ case "gemini":
20
+ return new GeminiLlmClient({ model: opts.model });
21
+ case "claude-code":
22
+ return new ClaudeCodeLlmClient({ model: opts.model });
23
+ default:
24
+ return new AnthropicLlmClient({ model: opts.model });
25
+ }
8
26
  }
9
27
  //# sourceMappingURL=factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/adapters/llm/factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAQvD,MAAM,UAAU,eAAe,CAAC,OAA0B,EAAE;IAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC9F,OAAO,OAAO,KAAK,WAAW;QAC5B,CAAC,CAAC,IAAI,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;QAC/C,CAAC,CAAC,IAAI,mBAAmB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/adapters/llm/factory.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAU9C,SAAS,aAAa;IACpB,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAAE,OAAO,WAAW,CAAC;IACtD,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,QAAQ,CAAC;IAChD,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,OAAO,QAAQ,CAAC;IAC9E,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAA0B,EAAE;IAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,aAAa,EAAE,CAAC;IAChD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,QAAQ;YACX,OAAO,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACpD,KAAK,QAAQ;YACX,OAAO,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACpD,KAAK,aAAa;YAChB,OAAO,IAAI,mBAAmB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACxD;YACE,OAAO,IAAI,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACzD,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * LlmClient backed by the Google Gemini API (GEMINI_API_KEY / GOOGLE_API_KEY). Uses fetch — no SDK.
3
+ */
4
+ import type { CompleteOptions, LlmClient } from "../../core/ports.js";
5
+ export interface GeminiOptions {
6
+ apiKey?: string;
7
+ model?: string;
8
+ baseUrl?: string;
9
+ timeoutMs?: number;
10
+ maxRetries?: number;
11
+ }
12
+ export declare class GeminiLlmClient implements LlmClient {
13
+ readonly id: string;
14
+ private readonly apiKey;
15
+ private readonly model;
16
+ private readonly baseUrl;
17
+ private readonly timeoutMs?;
18
+ private readonly maxRetries?;
19
+ constructor(opts?: GeminiOptions);
20
+ complete(prompt: string, opts?: CompleteOptions): Promise<string>;
21
+ }
@@ -0,0 +1,44 @@
1
+ import { postJsonWithRetry } from "./http.js";
2
+ export class GeminiLlmClient {
3
+ id;
4
+ apiKey;
5
+ model;
6
+ baseUrl;
7
+ timeoutMs;
8
+ maxRetries;
9
+ constructor(opts = {}) {
10
+ const apiKey = opts.apiKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
11
+ if (!apiKey)
12
+ throw new Error("GeminiLlmClient requires GEMINI_API_KEY");
13
+ this.apiKey = apiKey;
14
+ this.model = opts.model ?? "gemini-2.0-flash";
15
+ this.baseUrl = opts.baseUrl ?? "https://generativelanguage.googleapis.com";
16
+ this.timeoutMs = opts.timeoutMs;
17
+ this.maxRetries = opts.maxRetries;
18
+ this.id = `gemini:${this.model}`;
19
+ }
20
+ async complete(prompt, opts = {}) {
21
+ const data = await postJsonWithRetry(`${this.baseUrl}/v1beta/models/${this.model}:generateContent`, {
22
+ headers: {
23
+ "content-type": "application/json",
24
+ "x-goog-api-key": this.apiKey,
25
+ },
26
+ body: JSON.stringify({
27
+ ...(opts.system
28
+ ? { systemInstruction: { parts: [{ text: opts.system }] } }
29
+ : {}),
30
+ contents: [{ role: "user", parts: [{ text: prompt }] }],
31
+ generationConfig: { maxOutputTokens: opts.maxTokens ?? 1024 },
32
+ }),
33
+ }, {
34
+ timeoutMs: this.timeoutMs,
35
+ maxRetries: this.maxRetries,
36
+ label: "Gemini",
37
+ });
38
+ return (data.candidates?.[0]?.content?.parts ?? [])
39
+ .map((p) => p.text ?? "")
40
+ .join("")
41
+ .trim();
42
+ }
43
+ }
44
+ //# sourceMappingURL=gemini.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../../src/adapters/llm/gemini.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAc9C,MAAM,OAAO,eAAe;IACjB,EAAE,CAAS;IACH,MAAM,CAAS;IACf,KAAK,CAAS;IACd,OAAO,CAAS;IAChB,SAAS,CAAU;IACnB,UAAU,CAAU;IAErC,YAAY,OAAsB,EAAE;QAClC,MAAM,MAAM,GACV,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;QAC1E,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,kBAAkB,CAAC;QAC9C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,2CAA2C,CAAC;QAC3E,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,EAAE,GAAG,UAAU,IAAI,CAAC,KAAK,EAAE,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,OAAwB,EAAE;QACvD,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAClC,GAAG,IAAI,CAAC,OAAO,kBAAkB,IAAI,CAAC,KAAK,kBAAkB,EAC7D;YACE,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,gBAAgB,EAAE,IAAI,CAAC,MAAM;aAC9B;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,GAAG,CAAC,IAAI,CAAC,MAAM;oBACb,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;oBAC3D,CAAC,CAAC,EAAE,CAAC;gBACP,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;gBACvD,gBAAgB,EAAE,EAAE,eAAe,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,EAAE;aAC9D,CAAC;SACH,EACD;YACE,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,KAAK,EAAE,QAAQ;SAChB,CACF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC;aAChD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;aACxB,IAAI,CAAC,EAAE,CAAC;aACR,IAAI,EAAE,CAAC;IACZ,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ export interface HttpRetryOptions {
2
+ /** Per-request timeout (ms). Default 60s — a stalled connection rejects instead of hanging. */
3
+ timeoutMs?: number;
4
+ /** Retries on transient errors (429 / 5xx / timeout). Default 2. */
5
+ maxRetries?: number;
6
+ /** Provider name for error messages, e.g. "Anthropic". */
7
+ label?: string;
8
+ }
9
+ export declare function postJsonWithRetry<T>(url: string, init: {
10
+ headers: Record<string, string>;
11
+ body: string;
12
+ }, opts?: HttpRetryOptions): Promise<T>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared POST-JSON transport for the HTTP-based LlmClient adapters (Anthropic, OpenAI, Gemini):
3
+ * a per-request timeout plus exponential back-off on transient failures (429 / 5xx / timeout).
4
+ * Each adapter builds its own request body and parses its own response shape.
5
+ */
6
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
7
+ export async function postJsonWithRetry(url, init, opts = {}) {
8
+ const timeoutMs = opts.timeoutMs ?? 60_000;
9
+ const maxRetries = opts.maxRetries ?? 2;
10
+ const label = opts.label ?? "LLM";
11
+ for (let attempt = 0;; attempt++) {
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
14
+ try {
15
+ const res = await fetch(url, {
16
+ method: "POST",
17
+ headers: init.headers,
18
+ body: init.body,
19
+ signal: controller.signal,
20
+ });
21
+ if (res.ok)
22
+ return (await res.json());
23
+ // Back off on transient errors (rate limit / overloaded / server), else fail.
24
+ if ((res.status === 429 || res.status >= 500) && attempt < maxRetries) {
25
+ await delay(500 * 2 ** attempt);
26
+ continue;
27
+ }
28
+ throw new Error(`${label} API ${res.status}: ${await res.text()}`);
29
+ }
30
+ catch (err) {
31
+ const aborted = err instanceof Error && err.name === "AbortError";
32
+ if (aborted && attempt >= maxRetries)
33
+ throw new Error(`${label} request timed out after ${timeoutMs}ms`);
34
+ if (aborted) {
35
+ await delay(500 * 2 ** attempt);
36
+ continue;
37
+ }
38
+ throw err;
39
+ }
40
+ finally {
41
+ clearTimeout(timer);
42
+ }
43
+ }
44
+ }
45
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/adapters/llm/http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAC1C,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAWxC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,IAAuD,EACvD,OAAyB,EAAE;IAE3B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC;IAElC,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,EAAE,EAAE,CAAC;QAClC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;QAC9D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,IAAI,GAAG,CAAC,EAAE;gBAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;YAC3C,8EAA8E;YAC9E,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;gBACtE,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;gBAChC,SAAS;YACX,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,GAAG,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACrE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,CAAC;YAClE,IAAI,OAAO,IAAI,OAAO,IAAI,UAAU;gBAClC,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,4BAA4B,SAAS,IAAI,CAAC,CAAC;YACrE,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;gBAChC,SAAS;YACX,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * LlmClient backed by the OpenAI Chat Completions API (OPENAI_API_KEY). Uses fetch — no SDK.
3
+ */
4
+ import type { CompleteOptions, LlmClient } from "../../core/ports.js";
5
+ export interface OpenAIOptions {
6
+ apiKey?: string;
7
+ model?: string;
8
+ baseUrl?: string;
9
+ timeoutMs?: number;
10
+ maxRetries?: number;
11
+ }
12
+ export declare class OpenAILlmClient implements LlmClient {
13
+ readonly id: string;
14
+ private readonly apiKey;
15
+ private readonly model;
16
+ private readonly baseUrl;
17
+ private readonly timeoutMs?;
18
+ private readonly maxRetries?;
19
+ constructor(opts?: OpenAIOptions);
20
+ complete(prompt: string, opts?: CompleteOptions): Promise<string>;
21
+ }
@@ -0,0 +1,43 @@
1
+ import { postJsonWithRetry } from "./http.js";
2
+ export class OpenAILlmClient {
3
+ id;
4
+ apiKey;
5
+ model;
6
+ baseUrl;
7
+ timeoutMs;
8
+ maxRetries;
9
+ constructor(opts = {}) {
10
+ const apiKey = opts.apiKey ?? process.env.OPENAI_API_KEY;
11
+ if (!apiKey)
12
+ throw new Error("OpenAILlmClient requires OPENAI_API_KEY");
13
+ this.apiKey = apiKey;
14
+ this.model = opts.model ?? "gpt-4o";
15
+ this.baseUrl = opts.baseUrl ?? "https://api.openai.com";
16
+ this.timeoutMs = opts.timeoutMs;
17
+ this.maxRetries = opts.maxRetries;
18
+ this.id = `openai:${this.model}`;
19
+ }
20
+ async complete(prompt, opts = {}) {
21
+ const messages = [];
22
+ if (opts.system)
23
+ messages.push({ role: "system", content: opts.system });
24
+ messages.push({ role: "user", content: prompt });
25
+ const data = await postJsonWithRetry(`${this.baseUrl}/v1/chat/completions`, {
26
+ headers: {
27
+ "content-type": "application/json",
28
+ authorization: `Bearer ${this.apiKey}`,
29
+ },
30
+ body: JSON.stringify({
31
+ model: this.model,
32
+ max_tokens: opts.maxTokens ?? 1024,
33
+ messages,
34
+ }),
35
+ }, {
36
+ timeoutMs: this.timeoutMs,
37
+ maxRetries: this.maxRetries,
38
+ label: "OpenAI",
39
+ });
40
+ return (data.choices?.[0]?.message?.content ?? "").trim();
41
+ }
42
+ }
43
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.js","sourceRoot":"","sources":["../../../src/adapters/llm/openai.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAc9C,MAAM,OAAO,eAAe;IACjB,EAAE,CAAS;IACH,MAAM,CAAS;IACf,KAAK,CAAS;IACd,OAAO,CAAS;IAChB,SAAS,CAAU;IACnB,UAAU,CAAU;IAErC,YAAY,OAAsB,EAAE;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;QACzD,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QACxE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAC;QACpC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,wBAAwB,CAAC;QACxD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,EAAE,GAAG,UAAU,IAAI,CAAC,KAAK,EAAE,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,OAAwB,EAAE;QACvD,MAAM,QAAQ,GAA6C,EAAE,CAAC;QAC9D,IAAI,IAAI,CAAC,MAAM;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAEjD,MAAM,IAAI,GAAG,MAAM,iBAAiB,CAClC,GAAG,IAAI,CAAC,OAAO,sBAAsB,EACrC;YACE,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;aACvC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,UAAU,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;gBAClC,QAAQ;aACT,CAAC;SACH,EACD;YACE,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,KAAK,EAAE,QAAQ;SAChB,CACF,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5D,CAAC;CACF"}
package/dist/browser.d.ts CHANGED
@@ -22,6 +22,8 @@ export { FakeDriver } from "./adapters/drivers/fake.js";
22
22
  export { SelfHealingDriver, parseHealChoice } from "./adapters/drivers/self-heal.js";
23
23
  export type { Heal, SelfHealOptions } from "./adapters/drivers/self-heal.js";
24
24
  export { AnthropicLlmClient } from "./adapters/llm/anthropic.js";
25
+ export { OpenAILlmClient } from "./adapters/llm/openai.js";
26
+ export { GeminiLlmClient } from "./adapters/llm/gemini.js";
25
27
  export { discover, parseDecision } from "./core/discover.js";
26
28
  export type { DiscoverOptions, Decision } from "./core/discover.js";
27
29
  export { LlmStepHealer } from "./core/step-heal.js";
package/dist/browser.js CHANGED
@@ -19,6 +19,8 @@ export { ConsoleReporter } from "./adapters/reporters/console.js";
19
19
  export { FakeDriver } from "./adapters/drivers/fake.js";
20
20
  export { SelfHealingDriver, parseHealChoice } from "./adapters/drivers/self-heal.js";
21
21
  export { AnthropicLlmClient } from "./adapters/llm/anthropic.js";
22
+ export { OpenAILlmClient } from "./adapters/llm/openai.js";
23
+ export { GeminiLlmClient } from "./adapters/llm/gemini.js";
22
24
  export { discover, parseDecision } from "./core/discover.js";
23
25
  export { LlmStepHealer } from "./core/step-heal.js";
24
26
  export { scoreTarget, scoreScenario, weakTargets } from "./core/freeze.js";
@@ -1 +1 @@
1
- {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE7F,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACjG,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAGrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE7F,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACjG,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAGrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE3D,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -20,7 +20,10 @@ export { SelfHealingDriver, parseHealChoice } from "./adapters/drivers/self-heal
20
20
  export type { Heal, SelfHealOptions } from "./adapters/drivers/self-heal.js";
21
21
  export { ClaudeCodeLlmClient } from "./adapters/llm/claude-code.js";
22
22
  export { AnthropicLlmClient } from "./adapters/llm/anthropic.js";
23
+ export { OpenAILlmClient } from "./adapters/llm/openai.js";
24
+ export { GeminiLlmClient } from "./adapters/llm/gemini.js";
23
25
  export { createLlmClient } from "./adapters/llm/factory.js";
26
+ export type { LlmBackend, LlmFactoryOptions } from "./adapters/llm/factory.js";
24
27
  export { FileSkillStore, loadSkillFile } from "./adapters/skills/file-store.js";
25
28
  export { discover, parseDecision } from "./core/discover.js";
26
29
  export type { DiscoverOptions, Decision } from "./core/discover.js";
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ export { ChromeDevToolsDriver } from "./adapters/drivers/chrome.js";
16
16
  export { SelfHealingDriver, parseHealChoice } from "./adapters/drivers/self-heal.js";
17
17
  export { ClaudeCodeLlmClient } from "./adapters/llm/claude-code.js";
18
18
  export { AnthropicLlmClient } from "./adapters/llm/anthropic.js";
19
+ export { OpenAILlmClient } from "./adapters/llm/openai.js";
20
+ export { GeminiLlmClient } from "./adapters/llm/gemini.js";
19
21
  export { createLlmClient } from "./adapters/llm/factory.js";
20
22
  export { FileSkillStore, loadSkillFile } from "./adapters/skills/file-store.js";
21
23
  export { discover, parseDecision } from "./core/discover.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEnF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACjG,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAGrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC7F,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEnF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EACL,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACjG,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAGrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE7D,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cairn-engine",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "An engine for self-healing E2E browser tests — discovered once by an AI, replayed deterministically.",
5
5
  "keywords": [
6
6
  "e2e",