@cairnvibe/sdk 0.1.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/LICENSE +21 -0
- package/dist/cairn-widget.js +228 -0
- package/dist/context-collector.d.ts +1 -0
- package/dist/context-collector.js +23 -0
- package/dist/dashboard-sqlite.d.ts +8 -0
- package/dist/dashboard-sqlite.js +50 -0
- package/dist/dashboard.d.ts +39 -0
- package/dist/dashboard.js +60 -0
- package/dist/element-ladder.d.ts +7 -0
- package/dist/element-ladder.js +60 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +1069 -0
- package/dist/key-rotator.d.ts +7 -0
- package/dist/key-rotator.js +31 -0
- package/dist/package.json +1 -0
- package/dist/realtime-cli.d.ts +2 -0
- package/dist/realtime-cli.js +59 -0
- package/dist/realtime-server.d.ts +10 -0
- package/dist/realtime-server.js +291 -0
- package/dist/server.d.ts +95 -0
- package/dist/server.js +298 -0
- package/dist/speak-server.d.ts +16 -0
- package/dist/speak-server.js +41 -0
- package/dist/transcribe-server.d.ts +14 -0
- package/dist/transcribe-server.js +47 -0
- package/dist/tts-stream.d.ts +33 -0
- package/dist/tts-stream.js +124 -0
- package/dist/verb-executor.d.ts +17 -0
- package/dist/verb-executor.js +67 -0
- package/package.json +56 -0
- package/src/context-collector.ts +21 -0
- package/src/dashboard-sqlite.ts +52 -0
- package/src/dashboard.ts +82 -0
- package/src/element-ladder.ts +67 -0
- package/src/index.tsx +1250 -0
- package/src/key-rotator.ts +29 -0
- package/src/realtime-cli.ts +62 -0
- package/src/realtime-server.ts +342 -0
- package/src/server.ts +386 -0
- package/src/speak-server.ts +56 -0
- package/src/transcribe-server.ts +68 -0
- package/src/tts-stream.ts +140 -0
- package/src/verb-executor.ts +84 -0
- package/src/web-component.ts +1252 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Server-side aggregation of the element-ladder misses the client already
|
|
3
|
+
// logs to localStorage (see element-ladder.ts). Opt-in: the Copilot widget
|
|
4
|
+
// only reports here if `reportMissesEndpoint` is set. Same
|
|
5
|
+
// build-a-handler-function shape as `createCopilotHandler` in server.ts.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.createMissesStore = createMissesStore;
|
|
8
|
+
exports.summarizeMisses = summarizeMisses;
|
|
9
|
+
exports.createMissesHandler = createMissesHandler;
|
|
10
|
+
/** In-memory store — fine for a demo or a single-instance deployment. Swap for a real one via the same interface. */
|
|
11
|
+
function createMissesStore() {
|
|
12
|
+
const records = [];
|
|
13
|
+
return {
|
|
14
|
+
report(context) {
|
|
15
|
+
records.push({ ...context, at: new Date().toISOString() });
|
|
16
|
+
},
|
|
17
|
+
list() {
|
|
18
|
+
return [...records];
|
|
19
|
+
},
|
|
20
|
+
clear() {
|
|
21
|
+
records.length = 0;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function summarizeMisses(records) {
|
|
26
|
+
const groups = new Map();
|
|
27
|
+
for (const r of records) {
|
|
28
|
+
const key = `${r.route}::${r.attempted}`;
|
|
29
|
+
const existing = groups.get(key);
|
|
30
|
+
if (existing) {
|
|
31
|
+
existing.count += 1;
|
|
32
|
+
if (r.at > existing.lastSeen)
|
|
33
|
+
existing.lastSeen = r.at;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
groups.set(key, { attempted: r.attempted, route: r.route, count: 1, lastSeen: r.at });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Array.from(groups.values()).sort((a, b) => b.count - a.count);
|
|
40
|
+
}
|
|
41
|
+
function isValidMissReport(body) {
|
|
42
|
+
if (typeof body !== "object" || body === null)
|
|
43
|
+
return false;
|
|
44
|
+
const candidate = body;
|
|
45
|
+
return typeof candidate.attempted === "string" && typeof candidate.route === "string";
|
|
46
|
+
}
|
|
47
|
+
function createMissesHandler(store) {
|
|
48
|
+
return {
|
|
49
|
+
async post(body) {
|
|
50
|
+
if (!isValidMissReport(body)) {
|
|
51
|
+
return { status: 400, body: { error: "invalid miss report" } };
|
|
52
|
+
}
|
|
53
|
+
store.report(body);
|
|
54
|
+
return { status: 200, body: { ok: true } };
|
|
55
|
+
},
|
|
56
|
+
async get() {
|
|
57
|
+
return { status: 200, body: summarizeMisses(store.list()) };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function findElement(target: string): HTMLElement | null;
|
|
2
|
+
export declare function highlightElement(el: HTMLElement, glowMs?: number): void;
|
|
3
|
+
export interface MissContext {
|
|
4
|
+
attempted: string;
|
|
5
|
+
route: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function logMiss(context: MissContext): void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The 4-step Element Ladder (BUILD_PLAN.md invariant #3): a lookup failure
|
|
3
|
+
// must degrade to explain-only, never guess and click the wrong thing.
|
|
4
|
+
//
|
|
5
|
+
// 1. data-ai="..." — exact, authoritative
|
|
6
|
+
// 2. aria-label / role — accessible-name fallback
|
|
7
|
+
// 3. visible text — last resort, exact then substring match
|
|
8
|
+
// 4. FAIL — caller degrades to explain + logs the miss
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.findElement = findElement;
|
|
11
|
+
exports.highlightElement = highlightElement;
|
|
12
|
+
exports.logMiss = logMiss;
|
|
13
|
+
function findElement(target) {
|
|
14
|
+
if (typeof document === "undefined")
|
|
15
|
+
return null;
|
|
16
|
+
const byDataAi = document.querySelector(`[data-ai="${cssEscape(target)}"]`);
|
|
17
|
+
if (byDataAi)
|
|
18
|
+
return byDataAi;
|
|
19
|
+
const byAriaLabel = document.querySelector(`[aria-label="${cssEscape(target)}"]`);
|
|
20
|
+
if (byAriaLabel)
|
|
21
|
+
return byAriaLabel;
|
|
22
|
+
const byRole = document.querySelector(`[role="${cssEscape(target)}"]`);
|
|
23
|
+
if (byRole)
|
|
24
|
+
return byRole;
|
|
25
|
+
const candidates = document.querySelectorAll("button, a, [role='button'], input[type='submit'], input[type='button']");
|
|
26
|
+
const normalizedTarget = normalize(target);
|
|
27
|
+
for (const el of Array.from(candidates)) {
|
|
28
|
+
if (normalize(el.textContent ?? "") === normalizedTarget)
|
|
29
|
+
return el;
|
|
30
|
+
}
|
|
31
|
+
for (const el of Array.from(candidates)) {
|
|
32
|
+
if (normalize(el.textContent ?? "").includes(normalizedTarget))
|
|
33
|
+
return el;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function highlightElement(el, glowMs = 4000) {
|
|
38
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
39
|
+
el.classList.add("cairn-glow");
|
|
40
|
+
window.setTimeout(() => el.classList.remove("cairn-glow"), glowMs);
|
|
41
|
+
}
|
|
42
|
+
const MISS_LOG_KEY = "cairn:misses";
|
|
43
|
+
const MISS_LOG_LIMIT = 200;
|
|
44
|
+
function logMiss(context) {
|
|
45
|
+
try {
|
|
46
|
+
const existingRaw = window.localStorage.getItem(MISS_LOG_KEY);
|
|
47
|
+
const existing = existingRaw ? JSON.parse(existingRaw) : [];
|
|
48
|
+
existing.push({ ...context, at: new Date().toISOString() });
|
|
49
|
+
window.localStorage.setItem(MISS_LOG_KEY, JSON.stringify(existing.slice(-MISS_LOG_LIMIT)));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// localStorage unavailable (SSR, private mode, quota) — never let logging break the UI.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function normalize(s) {
|
|
56
|
+
return s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
57
|
+
}
|
|
58
|
+
function cssEscape(s) {
|
|
59
|
+
return s.replace(/["\\]/g, "\\$&");
|
|
60
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface CopilotProps {
|
|
2
|
+
/** Reserved for a future client-side manifest fetch. Not required — the server handler owns the manifest. */
|
|
3
|
+
manifest?: string;
|
|
4
|
+
/** Where the widget posts questions. Defaults to "/api/copilot". */
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
/** Action ids this deployment actually wired up for the "do" verb. */
|
|
7
|
+
registeredActions?: string[];
|
|
8
|
+
/** Called when the model returns a valid "do" verb for a registered action. */
|
|
9
|
+
onDo?: (action: string, target?: string) => void;
|
|
10
|
+
/** If set, a lookup miss is also POSTed here so failures can be aggregated server-side. */
|
|
11
|
+
reportMissesEndpoint?: string;
|
|
12
|
+
/**
|
|
13
|
+
* If set, shows a mic button that records audio and POSTs it here for
|
|
14
|
+
* transcription — re-sent every ~2s while recording so the field fills in
|
|
15
|
+
* progressively. Hidden automatically if the browser has no mic access.
|
|
16
|
+
*/
|
|
17
|
+
transcribeEndpoint?: string;
|
|
18
|
+
/** If set, the widget speaks each explain/highlight answer aloud (Deepgram TTS via `@cairnvibe/sdk/speak-server`). */
|
|
19
|
+
speakEndpoint?: string;
|
|
20
|
+
/**
|
|
21
|
+
* If set, shows a "start conversation" control that opens a live
|
|
22
|
+
* WebSocket to a `@cairnvibe/sdk/realtime-server` relay (run via
|
|
23
|
+
* `cairn-realtime`) for a real-time voice conversation: streaming
|
|
24
|
+
* transcription, verbs executed as soon as they're resolved, and the
|
|
25
|
+
* answer spoken back — all without you touching the keyboard.
|
|
26
|
+
*/
|
|
27
|
+
realtimeUrl?: string;
|
|
28
|
+
/** Display name for the agent, shown in the widget's header and button labels. Defaults to "Cairn". */
|
|
29
|
+
persona?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function Copilot({ endpoint, registeredActions, onDo, reportMissesEndpoint, transcribeEndpoint, speakEndpoint, realtimeUrl, persona, }: CopilotProps): import("react").JSX.Element;
|