@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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cairn-widget.js +228 -0
  3. package/dist/context-collector.d.ts +1 -0
  4. package/dist/context-collector.js +23 -0
  5. package/dist/dashboard-sqlite.d.ts +8 -0
  6. package/dist/dashboard-sqlite.js +50 -0
  7. package/dist/dashboard.d.ts +39 -0
  8. package/dist/dashboard.js +60 -0
  9. package/dist/element-ladder.d.ts +7 -0
  10. package/dist/element-ladder.js +60 -0
  11. package/dist/index.d.ts +31 -0
  12. package/dist/index.js +1069 -0
  13. package/dist/key-rotator.d.ts +7 -0
  14. package/dist/key-rotator.js +31 -0
  15. package/dist/package.json +1 -0
  16. package/dist/realtime-cli.d.ts +2 -0
  17. package/dist/realtime-cli.js +59 -0
  18. package/dist/realtime-server.d.ts +10 -0
  19. package/dist/realtime-server.js +291 -0
  20. package/dist/server.d.ts +95 -0
  21. package/dist/server.js +298 -0
  22. package/dist/speak-server.d.ts +16 -0
  23. package/dist/speak-server.js +41 -0
  24. package/dist/transcribe-server.d.ts +14 -0
  25. package/dist/transcribe-server.js +47 -0
  26. package/dist/tts-stream.d.ts +33 -0
  27. package/dist/tts-stream.js +124 -0
  28. package/dist/verb-executor.d.ts +17 -0
  29. package/dist/verb-executor.js +67 -0
  30. package/package.json +56 -0
  31. package/src/context-collector.ts +21 -0
  32. package/src/dashboard-sqlite.ts +52 -0
  33. package/src/dashboard.ts +82 -0
  34. package/src/element-ladder.ts +67 -0
  35. package/src/index.tsx +1250 -0
  36. package/src/key-rotator.ts +29 -0
  37. package/src/realtime-cli.ts +62 -0
  38. package/src/realtime-server.ts +342 -0
  39. package/src/server.ts +386 -0
  40. package/src/speak-server.ts +56 -0
  41. package/src/transcribe-server.ts +68 -0
  42. package/src/tts-stream.ts +140 -0
  43. package/src/verb-executor.ts +84 -0
  44. package/src/web-component.ts +1252 -0
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@cairnvibe/sdk",
3
+ "version": "0.1.0",
4
+ "description": "In-app AI copilot — <Copilot/> for React/Next.js, <cairn-widget> for any framework — plus the server handlers and realtime voice relay behind them.",
5
+ "license": "MIT",
6
+ "publishConfig": { "access": "public" },
7
+ "repository": { "type": "git", "url": "git+https://github.com/Vikasverma9515/cairn.git", "directory": "packages/sdk" },
8
+ "homepage": "https://github.com/Vikasverma9515/cairn#readme",
9
+ "bugs": "https://github.com/Vikasverma9515/cairn/issues",
10
+ "type": "module",
11
+ "bin": {
12
+ "cairn-realtime": "dist/realtime-cli.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "LICENSE"
20
+ ],
21
+ "exports": {
22
+ ".": "./src/index.tsx",
23
+ "./server": "./dist/server.js",
24
+ "./dashboard": "./dist/dashboard.js",
25
+ "./dashboard-sqlite": "./dist/dashboard-sqlite.js",
26
+ "./transcribe-server": "./dist/transcribe-server.js",
27
+ "./speak-server": "./dist/speak-server.js",
28
+ "./realtime-server": "./dist/realtime-server.js",
29
+ "./widget": "./dist/cairn-widget.js"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json && echo '{\"type\":\"commonjs\"}' > dist/package.json && npm run build:widget",
33
+ "build:widget": "esbuild src/web-component.ts --bundle --minify --format=iife --outfile=dist/cairn-widget.js",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "peerDependencies": {
37
+ "next": ">=13",
38
+ "react": ">=18"
39
+ },
40
+ "dependencies": {
41
+ "@anthropic-ai/sdk": "^0.32.1",
42
+ "@cairnvibe/core": "^0.1.0",
43
+ "better-sqlite3": "^13.0.3",
44
+ "groq-sdk": "^1.6.0",
45
+ "lucide-react": "^1.37.0",
46
+ "ws": "^8.21.3"
47
+ },
48
+ "devDependencies": {
49
+ "@types/better-sqlite3": "^9.6.0",
50
+ "@types/react": "^18.3.3",
51
+ "@types/ws": "^8.18.1",
52
+ "esbuild": "^0.21.5",
53
+ "typescript": "^5.5.4",
54
+ "vitest": "^2.1.9"
55
+ }
56
+ }
@@ -0,0 +1,21 @@
1
+ // Collects the minimal, privacy-conscious context the runtime sends to
2
+ // /api/copilot: the current route (passed in separately by the caller) and
3
+ // the ids of interactive elements currently visible in the viewport. Never
4
+ // sends full DOM, page text, or anything not covered by `data-ai`.
5
+
6
+ export function collectVisible(): string[] {
7
+ if (typeof document === "undefined" || typeof window === "undefined") return [];
8
+
9
+ const elements = document.querySelectorAll<HTMLElement>("[data-ai]");
10
+ const ids: string[] = [];
11
+
12
+ elements.forEach((el) => {
13
+ const rect = el.getBoundingClientRect();
14
+ const inViewport = rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth;
15
+ if (!inViewport) return;
16
+ const id = el.getAttribute("data-ai");
17
+ if (id) ids.push(id);
18
+ });
19
+
20
+ return ids;
21
+ }
@@ -0,0 +1,52 @@
1
+ // A real, durable MissesStore — same interface as the in-memory default in
2
+ // dashboard.ts, but backed by SQLite so failure-dashboard data survives
3
+ // restarts and redeploys. This is what "swap for a real one" (dashboard.ts)
4
+ // actually looks like, not a toy.
5
+
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import Database from "better-sqlite3";
9
+ import type { MissesStore, MissRecord } from "./dashboard";
10
+
11
+ // Namespaced (not just "misses") so this can safely share a Database
12
+ // connection/file with a consumer's own tables (the demo app does this).
13
+ const TABLE = "cairn_misses";
14
+
15
+ /**
16
+ * @param target Either a file path (opened/created, parent dir made if
17
+ * needed) or an already-open better-sqlite3 `Database` — pass an open
18
+ * connection to share it with your own tables instead of opening a second file.
19
+ */
20
+ export function createSqliteMissesStore(target: string | Database.Database): MissesStore {
21
+ const db = typeof target === "string" ? openFile(target) : target;
22
+
23
+ db.exec(`
24
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ attempted TEXT NOT NULL,
27
+ route TEXT NOT NULL,
28
+ at TEXT NOT NULL
29
+ )
30
+ `);
31
+
32
+ const insert = db.prepare(`INSERT INTO ${TABLE} (attempted, route, at) VALUES (?, ?, ?)`);
33
+ const selectAll = db.prepare(`SELECT attempted, route, at FROM ${TABLE} ORDER BY id ASC`);
34
+ const deleteAll = db.prepare(`DELETE FROM ${TABLE}`);
35
+
36
+ return {
37
+ report(context) {
38
+ insert.run(context.attempted, context.route, new Date().toISOString());
39
+ },
40
+ list() {
41
+ return selectAll.all() as MissRecord[];
42
+ },
43
+ clear() {
44
+ deleteAll.run();
45
+ },
46
+ };
47
+ }
48
+
49
+ function openFile(filePath: string): Database.Database {
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ return new Database(filePath);
52
+ }
@@ -0,0 +1,82 @@
1
+ // Server-side aggregation of the element-ladder misses the client already
2
+ // logs to localStorage (see element-ladder.ts). Opt-in: the Copilot widget
3
+ // only reports here if `reportMissesEndpoint` is set. Same
4
+ // build-a-handler-function shape as `createCopilotHandler` in server.ts.
5
+
6
+ export interface MissRecord {
7
+ attempted: string;
8
+ route: string;
9
+ at: string;
10
+ }
11
+
12
+ export interface MissesStore {
13
+ report(context: { attempted: string; route: string }): void;
14
+ list(): MissRecord[];
15
+ clear(): void;
16
+ }
17
+
18
+ /** In-memory store — fine for a demo or a single-instance deployment. Swap for a real one via the same interface. */
19
+ export function createMissesStore(): MissesStore {
20
+ const records: MissRecord[] = [];
21
+ return {
22
+ report(context) {
23
+ records.push({ ...context, at: new Date().toISOString() });
24
+ },
25
+ list() {
26
+ return [...records];
27
+ },
28
+ clear() {
29
+ records.length = 0;
30
+ },
31
+ };
32
+ }
33
+
34
+ export interface MissesSummary {
35
+ attempted: string;
36
+ route: string;
37
+ count: number;
38
+ lastSeen: string;
39
+ }
40
+
41
+ export function summarizeMisses(records: MissRecord[]): MissesSummary[] {
42
+ const groups = new Map<string, MissesSummary>();
43
+ for (const r of records) {
44
+ const key = `${r.route}::${r.attempted}`;
45
+ const existing = groups.get(key);
46
+ if (existing) {
47
+ existing.count += 1;
48
+ if (r.at > existing.lastSeen) existing.lastSeen = r.at;
49
+ } else {
50
+ groups.set(key, { attempted: r.attempted, route: r.route, count: 1, lastSeen: r.at });
51
+ }
52
+ }
53
+ return Array.from(groups.values()).sort((a, b) => b.count - a.count);
54
+ }
55
+
56
+ function isValidMissReport(body: unknown): body is { attempted: string; route: string } {
57
+ if (typeof body !== "object" || body === null) return false;
58
+ const candidate = body as Record<string, unknown>;
59
+ return typeof candidate.attempted === "string" && typeof candidate.route === "string";
60
+ }
61
+
62
+ export interface MissesHandler {
63
+ /** Wire to `POST` — the widget calls this on every lookup miss. */
64
+ post(body: unknown): Promise<{ status: number; body: { ok: true } | { error: string } }>;
65
+ /** Wire to `GET` — returns misses grouped by route+target with counts, most frequent first. */
66
+ get(): Promise<{ status: number; body: MissesSummary[] }>;
67
+ }
68
+
69
+ export function createMissesHandler(store: MissesStore): MissesHandler {
70
+ return {
71
+ async post(body: unknown) {
72
+ if (!isValidMissReport(body)) {
73
+ return { status: 400, body: { error: "invalid miss report" } };
74
+ }
75
+ store.report(body);
76
+ return { status: 200, body: { ok: true } };
77
+ },
78
+ async get() {
79
+ return { status: 200, body: summarizeMisses(store.list()) };
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,67 @@
1
+ // The 4-step Element Ladder (BUILD_PLAN.md invariant #3): a lookup failure
2
+ // must degrade to explain-only, never guess and click the wrong thing.
3
+ //
4
+ // 1. data-ai="..." — exact, authoritative
5
+ // 2. aria-label / role — accessible-name fallback
6
+ // 3. visible text — last resort, exact then substring match
7
+ // 4. FAIL — caller degrades to explain + logs the miss
8
+
9
+ export function findElement(target: string): HTMLElement | null {
10
+ if (typeof document === "undefined") return null;
11
+
12
+ const byDataAi = document.querySelector<HTMLElement>(`[data-ai="${cssEscape(target)}"]`);
13
+ if (byDataAi) return byDataAi;
14
+
15
+ const byAriaLabel = document.querySelector<HTMLElement>(`[aria-label="${cssEscape(target)}"]`);
16
+ if (byAriaLabel) return byAriaLabel;
17
+
18
+ const byRole = document.querySelector<HTMLElement>(`[role="${cssEscape(target)}"]`);
19
+ if (byRole) return byRole;
20
+
21
+ const candidates = document.querySelectorAll<HTMLElement>(
22
+ "button, a, [role='button'], input[type='submit'], input[type='button']",
23
+ );
24
+ const normalizedTarget = normalize(target);
25
+
26
+ for (const el of Array.from(candidates)) {
27
+ if (normalize(el.textContent ?? "") === normalizedTarget) return el;
28
+ }
29
+ for (const el of Array.from(candidates)) {
30
+ if (normalize(el.textContent ?? "").includes(normalizedTarget)) return el;
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ export function highlightElement(el: HTMLElement, glowMs = 4000): void {
37
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
38
+ el.classList.add("cairn-glow");
39
+ window.setTimeout(() => el.classList.remove("cairn-glow"), glowMs);
40
+ }
41
+
42
+ export interface MissContext {
43
+ attempted: string;
44
+ route: string;
45
+ }
46
+
47
+ const MISS_LOG_KEY = "cairn:misses";
48
+ const MISS_LOG_LIMIT = 200;
49
+
50
+ export function logMiss(context: MissContext): void {
51
+ try {
52
+ const existingRaw = window.localStorage.getItem(MISS_LOG_KEY);
53
+ const existing: (MissContext & { at: string })[] = existingRaw ? JSON.parse(existingRaw) : [];
54
+ existing.push({ ...context, at: new Date().toISOString() });
55
+ window.localStorage.setItem(MISS_LOG_KEY, JSON.stringify(existing.slice(-MISS_LOG_LIMIT)));
56
+ } catch {
57
+ // localStorage unavailable (SSR, private mode, quota) — never let logging break the UI.
58
+ }
59
+ }
60
+
61
+ function normalize(s: string): string {
62
+ return s.trim().toLowerCase().replace(/\s+/g, " ");
63
+ }
64
+
65
+ function cssEscape(s: string): string {
66
+ return s.replace(/["\\]/g, "\\$&");
67
+ }