@cairnvibe/sdk 0.2.13 → 0.3.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/dist/agent-loop.d.ts +113 -0
- package/dist/agent-loop.js +128 -0
- package/dist/cairn-widget.js +2 -2
- package/dist/element-ladder.d.ts +71 -0
- package/dist/element-ladder.js +168 -0
- package/dist/index.d.ts +79 -1
- package/dist/index.js +864 -89
- package/dist/key-rotator.d.ts +28 -0
- package/dist/key-rotator.js +57 -3
- package/dist/memory-sqlite.d.ts +86 -0
- package/dist/memory-sqlite.js +230 -0
- package/dist/realtime-cli.js +22 -1
- package/dist/realtime-server.d.ts +83 -2
- package/dist/realtime-server.js +549 -120
- package/dist/server.d.ts +249 -5
- package/dist/server.js +984 -77
- package/dist/skill-store.d.ts +17 -0
- package/dist/skill-store.js +78 -0
- package/dist/tts-stream.d.ts +25 -0
- package/dist/tts-stream.js +32 -0
- package/dist/vad.d.ts +27 -0
- package/dist/vad.js +128 -0
- package/dist/verb-executor.d.ts +32 -11
- package/dist/verb-executor.js +224 -16
- package/dist/webmcp-client.d.ts +14 -1
- package/dist/webmcp-client.js +22 -1
- package/package.json +3 -1
- package/src/agent-loop.ts +222 -0
- package/src/element-ladder.ts +170 -0
- package/src/index.tsx +914 -93
- package/src/key-rotator.ts +57 -2
- package/src/memory-sqlite.ts +283 -0
- package/src/realtime-cli.ts +24 -1
- package/src/realtime-server.ts +655 -122
- package/src/server.ts +1077 -77
- package/src/skill-store.ts +88 -0
- package/src/tts-stream.ts +30 -0
- package/src/vad.ts +153 -0
- package/src/verb-executor.ts +243 -22
- package/src/web-component.ts +82 -17
- package/src/webmcp-client.ts +30 -2
package/src/element-ladder.ts
CHANGED
|
@@ -40,6 +40,94 @@ export function findElement(target: string, liveElements?: Map<string, HTMLEleme
|
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Phase 3 step 4 (see DEVELOPMENT.md/the plan file) — CODA's own point:
|
|
45
|
+
* the Executor gets real local retry latitude for a genuinely MECHANICAL
|
|
46
|
+
* miss (a re-render replaced the DOM node the frozen liveElements snapshot
|
|
47
|
+
* pointed at; an animation/async render hadn't settled yet) before a
|
|
48
|
+
* failure escalates all the way to the Critic/a replan. Deliberately NOT
|
|
49
|
+
* a second LLM call — the Executor stays opinion-free, exactly re-running
|
|
50
|
+
* the SAME real lookup (which, past the liveElements-map check, already
|
|
51
|
+
* queries the LIVE DOM directly — a stale snapshot doesn't matter to that
|
|
52
|
+
* part) after a short real wait. A target that's genuinely not on the
|
|
53
|
+
* page still fails after `attempts`, surfacing as a real miss — this
|
|
54
|
+
* never silently invents success.
|
|
55
|
+
*/
|
|
56
|
+
export async function findElementWithRetry(
|
|
57
|
+
target: string,
|
|
58
|
+
liveElements?: Map<string, HTMLElement>,
|
|
59
|
+
attempts = 2,
|
|
60
|
+
delayMs = 300,
|
|
61
|
+
): Promise<HTMLElement | null> {
|
|
62
|
+
for (let i = 0; i < attempts; i++) {
|
|
63
|
+
const el = findElement(target, liveElements);
|
|
64
|
+
if (el) return el;
|
|
65
|
+
if (i < attempts - 1) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Real, live-found bug this closes: a `fill`/`click` step reported itself
|
|
72
|
+
* "done" the instant its DOM event was dispatched — but the app's own
|
|
73
|
+
* reaction to that event (a filtered search-results grid re-rendering, a
|
|
74
|
+
* cart count updating) can be an unbounded-latency async round trip (a
|
|
75
|
+
* Next.js App Router `router.push` re-fetching a server component, for
|
|
76
|
+
* example — not a fixed debounce with a known delay to just sleep past).
|
|
77
|
+
* A `read` step immediately after saw STALE content and the agent
|
|
78
|
+
* confidently reported findings that didn't match what the page actually,
|
|
79
|
+
* eventually, showed — confirmed live: typing "book" into a search box,
|
|
80
|
+
* then reading the still-unfiltered product grid a moment later, and
|
|
81
|
+
* reporting a match ("Novel: The Long Way") the REAL, since-filtered page
|
|
82
|
+
* went on to show zero results for.
|
|
83
|
+
*
|
|
84
|
+
* Waits for real DOM mutations instead of guessing a sleep duration: if
|
|
85
|
+
* nothing starts mutating within `initialWaitMs`, resolves immediately
|
|
86
|
+
* (the action had no async effect at all — no reason to add latency to
|
|
87
|
+
* the common case); once mutations start, waits for `quietMs` of no
|
|
88
|
+
* further mutations before considering the page settled; a hard
|
|
89
|
+
* `timeoutMs` ceiling means a page that never stops mutating (an
|
|
90
|
+
* animation, a polling widget) can't stall the agent loop forever.
|
|
91
|
+
*/
|
|
92
|
+
export function waitForDomSettle(initialWaitMs = 100, quietMs = 200, timeoutMs = 1500): Promise<void> {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
// Real gap this closes: some callers stub a partial `document` (real
|
|
95
|
+
// tests in this repo do exactly that for other reasons — a fake
|
|
96
|
+
// WebMCP-tool document, for instance) without the rest of the DOM API
|
|
97
|
+
// surface — checking `document` alone isn't enough to guarantee
|
|
98
|
+
// MutationObserver (or document.body) actually exist too.
|
|
99
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined" || !document.body) {
|
|
100
|
+
resolve();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let settled = false;
|
|
104
|
+
let sawMutation = false;
|
|
105
|
+
let quietTimer: ReturnType<typeof setTimeout> | null = null;
|
|
106
|
+
|
|
107
|
+
const finish = () => {
|
|
108
|
+
if (settled) return;
|
|
109
|
+
settled = true;
|
|
110
|
+
observer.disconnect();
|
|
111
|
+
if (quietTimer) clearTimeout(quietTimer);
|
|
112
|
+
clearTimeout(hardCap);
|
|
113
|
+
resolve();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const observer = new MutationObserver(() => {
|
|
117
|
+
sawMutation = true;
|
|
118
|
+
if (quietTimer) clearTimeout(quietTimer);
|
|
119
|
+
quietTimer = setTimeout(finish, quietMs);
|
|
120
|
+
});
|
|
121
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
122
|
+
|
|
123
|
+
const hardCap = setTimeout(finish, timeoutMs);
|
|
124
|
+
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
if (!sawMutation) finish(); // the action had no async effect — nothing to wait for
|
|
127
|
+
}, initialWaitMs);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
43
131
|
export function highlightElement(el: HTMLElement, glowMs = 4000): void {
|
|
44
132
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
45
133
|
el.classList.add("cairn-glow");
|
|
@@ -87,6 +175,88 @@ export function readElement(el: HTMLElement): string {
|
|
|
87
175
|
return trimmed.length > 500 ? `${trimmed.slice(0, 499)}…` : trimmed || "(empty)";
|
|
88
176
|
}
|
|
89
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Chooses a real `<option>` by its visible text — never a raw internal
|
|
180
|
+
* `value` the model could never actually see. Native `<select>` gets the
|
|
181
|
+
* direct path (set `.value` to the matching option's own value, then fire
|
|
182
|
+
* the same input/change pair fillElement uses so React notices). A custom
|
|
183
|
+
* listbox/combobox (role="listbox"/"option" — Radix, Headless UI, etc.)
|
|
184
|
+
* has no real `<option>` to set, so the fallback clicks the matching
|
|
185
|
+
* option-shaped descendant instead, the same "do the real user gesture"
|
|
186
|
+
* principle the do/click cases already follow.
|
|
187
|
+
*/
|
|
188
|
+
export function selectOption(el: HTMLElement, visibleText: string): boolean {
|
|
189
|
+
if (el.tagName === "SELECT") {
|
|
190
|
+
const select = el as HTMLSelectElement;
|
|
191
|
+
const match = Array.from(select.options).find((o) => normalize(o.textContent ?? "") === normalize(visibleText)) ?? Array.from(select.options).find((o) => normalize(o.textContent ?? "").includes(normalize(visibleText)));
|
|
192
|
+
if (!match) return false;
|
|
193
|
+
select.value = match.value;
|
|
194
|
+
select.dispatchEvent(new Event("input", { bubbles: true }));
|
|
195
|
+
select.dispatchEvent(new Event("change", { bubbles: true }));
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const candidates = el.querySelectorAll<HTMLElement>('[role="option"], option, li, [role="menuitem"]');
|
|
200
|
+
const match = Array.from(candidates).find((c) => normalize(c.textContent ?? "") === normalize(visibleText)) ?? Array.from(candidates).find((c) => normalize(c.textContent ?? "").includes(normalize(visibleText)));
|
|
201
|
+
if (!match) return false;
|
|
202
|
+
match.click();
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A real multi-point pointer-event sequence — pointerdown on `from`'s
|
|
208
|
+
* center, several pointermove steps toward `to`'s center, pointerup on
|
|
209
|
+
* `to` — the same technique a real mouse drag produces, for canvas/kanban/
|
|
210
|
+
* sortable-list libraries (react-dnd, dnd-kit, n8n's own node canvas) that
|
|
211
|
+
* listen for pointer events rather than a single synthetic "drop". Mouse
|
|
212
|
+
* events are fired alongside (same coordinates) for the older libraries
|
|
213
|
+
* that still only listen for those. jsdom's getBoundingClientRect returns
|
|
214
|
+
* all-zero rects with no real layout engine — fine here, since what matters
|
|
215
|
+
* for a test is that the sequence fires with consistent coordinates, not
|
|
216
|
+
* that they reflect real pixels.
|
|
217
|
+
*/
|
|
218
|
+
export function dragElement(from: HTMLElement, to: HTMLElement, steps = 5): void {
|
|
219
|
+
const fromRect = from.getBoundingClientRect();
|
|
220
|
+
const toRect = to.getBoundingClientRect();
|
|
221
|
+
const fromX = fromRect.left + fromRect.width / 2;
|
|
222
|
+
const fromY = fromRect.top + fromRect.height / 2;
|
|
223
|
+
const toX = toRect.left + toRect.width / 2;
|
|
224
|
+
const toY = toRect.top + toRect.height / 2;
|
|
225
|
+
|
|
226
|
+
const fire = (target: HTMLElement, type: string, x: number, y: number) => {
|
|
227
|
+
const opts = { bubbles: true, cancelable: true, clientX: x, clientY: y, view: typeof window !== "undefined" ? window : undefined };
|
|
228
|
+
if (typeof PointerEvent !== "undefined") target.dispatchEvent(new PointerEvent(type.replace("mouse", "pointer"), opts));
|
|
229
|
+
target.dispatchEvent(new MouseEvent(type, opts));
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
fire(from, "mousedown", fromX, fromY);
|
|
233
|
+
for (let i = 1; i <= steps; i++) {
|
|
234
|
+
const x = fromX + ((toX - fromX) * i) / steps;
|
|
235
|
+
const y = fromY + ((toY - fromY) * i) / steps;
|
|
236
|
+
fire(i === steps ? to : from, "mousemove", x, y);
|
|
237
|
+
}
|
|
238
|
+
fire(to, "mouseup", toX, toY);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const KEYS_WITH_PRINTABLE_CHAR = new Set(["Enter", "Tab"]);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Presses one real key on a target element — focuses it first (a real
|
|
245
|
+
* keypress always lands on whatever's focused; a component that reacts to
|
|
246
|
+
* Escape/Enter/arrows almost always keys off document-level or its own
|
|
247
|
+
* focus-scoped listener, so focus has to be real before the event fires).
|
|
248
|
+
* Fires keydown/keyup (and keypress only for the handful of keys that
|
|
249
|
+
* still expect one — Enter/Tab — matching a real browser's own behavior,
|
|
250
|
+
* which no longer fires keypress for pure navigation keys like arrows).
|
|
251
|
+
*/
|
|
252
|
+
export function pressKey(el: HTMLElement, key: string): void {
|
|
253
|
+
if (typeof el.focus === "function") el.focus();
|
|
254
|
+
const opts = { bubbles: true, cancelable: true, key };
|
|
255
|
+
el.dispatchEvent(new KeyboardEvent("keydown", opts));
|
|
256
|
+
if (KEYS_WITH_PRINTABLE_CHAR.has(key)) el.dispatchEvent(new KeyboardEvent("keypress", opts));
|
|
257
|
+
el.dispatchEvent(new KeyboardEvent("keyup", opts));
|
|
258
|
+
}
|
|
259
|
+
|
|
90
260
|
export interface MissContext {
|
|
91
261
|
attempted: string;
|
|
92
262
|
route: string;
|