Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .changeset/production-hardening.md

This file was deleted.

1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
- name: Run live E2E suite
env:
LIVE_LLM: '1'
SKIP_LIVE_VOICE: '1'
GUIDEKIT_SECRET: guidekit-example-e2e-secret-32-chars
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
SKIP_NPM_DRY_RUN: '1'
Expand Down
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "@guidekit/core (ESM)",
"path": "packages/core/dist/index.js",
"limit": "85 KB",
"limit": "92 KB",
"gzip": true
},
{
Expand Down
16 changes: 8 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,27 @@ e2e/
└── env.ts # .env.local + LIVE_LLM detection
```

Voice E2E always mocks the browser Web Speech API — no Deepgram/ElevenLabs in Playwright.
Voice E2E: contract tier mocks Web Speech in Playwright; live tier uses real Web Speech via Chromium fake-audio-capture (no Deepgram/ElevenLabs).

### E2E coverage matrix (user-facing flows)

| Flow | Contract | Live |
|------|:--------:|:----:|
| Widget UI / a11y | yes | |
| Widget UI / a11y | yes | yes |
| Proxy health / token / LLM | yes | yes |
| Text chat + streaming | mocked | yes |
| Multi-turn memory | — | yes |
| Agent tools (scroll, highlight, navigate, tour, clickElement) | yes | yes |
| Platform Mode (RAG, plugin, cognitive page) | yes | yes |
| Session recovery 401 | yes | yes |
| Voice (Web Speech mock → LLM) | yes | yes |
| Custom actions / form / readPage / dismiss | yes | partial |
| Voice (Web Speech → LLM) | mocked STT | real STT |
| Custom actions / form / readPage / dismiss | yes | yes |
| STT/TTS proxy key minting | yes | — |
| Hallucination guard bus event | yes | |
| Vanilla IIFE widget | yes | |
| Headless custom UI | yes | |
| Hallucination guard bus event | yes | yes |
| Vanilla IIFE widget | yes | yes |
| Headless custom UI | yes | yes |

Commands: `pnpm test:e2e:contract` (CI), `pnpm test:e2e:live` (local), `pnpm test:e2e:live:full` (publish gate).
Commands: `pnpm test:e2e:contract` (CI), `pnpm test:e2e:live` (local), `pnpm test:e2e:live:full` (publish gate). Set `SKIP_LIVE_VOICE=1` to skip the headed real-Web-Speech live voice spec when STT is unavailable.

Before release, run `pnpm check:release` (runs live suite twice for the flake budget). Publish workflow uploads Playwright artifacts on failure.

Expand Down
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

| Version | Supported |
| ------- | --------- |
| 0.1.x | Yes |
| 1.x | Yes |
| 0.3.x | Security fixes only |
| 0.1.x | No |

## Reporting a Vulnerability

Expand Down
58 changes: 58 additions & 0 deletions apps/docs/app/docs/getting-started/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,64 @@ For quick prototyping without server routes, pass API keys directly:
| Create React App | Full |
| Non-React (vanilla JS) | Via `@guidekit/vanilla` |

## SPA navigation (Next.js App Router)

GuideKit detects URL changes via the Navigation API, `popstate`, and polling. For client-side route changes that bypass full page loads, pass your App Router instance so `navigate()` and post-nav rescans stay reliable:

```tsx
'use client';

import { useRouter } from 'next/navigation';
import { GuideKitProvider } from '@guidekit/react';

export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter();

return (
<GuideKitProvider
tokenEndpoint="/api/guidekit/token"
proxy={{ llm: '/api/guidekit/llm', health: '/api/guidekit/health' }}
llm={{ provider: 'gemini', model: 'gemini-2.5-flash-lite' }}
navigation={{ router: { push: (href) => router.push(href) } }}
>
{children}
</GuideKitProvider>
);
}
```

On each route change, GuideKit clears **PageMemory**, rescans the DOM, and emits `dom:route-change` plus `context:memory-cleared`.

For **in-page DOM swaps** (tabs, modals, virtual lists) without a URL change, call `core.rescanPage()` after updating the UI so the assistant sees fresh sections:

```tsx
'use client';

import { useEffect, useState } from 'react';
import { useGuideKitCore } from '@guidekit/react';

export function TabPanel({ activeTab }: { activeTab: string }) {
const core = useGuideKitCore();

useEffect(() => {
if (core?.isReady) core.rescanPage();
}, [activeTab, core]);

return <div>{/* tab content */}</div>;
}
```

## Universal site checklist

1. Proxy mode: `tokenEndpoint` + `/api/guidekit/llm` (never ship LLM keys to the browser)
2. Optional `contentMap` for product facts the DOM cannot infer
3. Optional `data-guidekit-target` on critical CTAs (improves accuracy, not required)
4. `clickableSelectors.allow/deny` for production click boundaries
5. Redis session store for multi-instance deployments
6. `hallucinationGuard={true}` + `intelligence={true}` for Platform Mode on public sites

See the example app routes `/plain`, `/spa-rescan`, and `/iframe-test` for unannotated page demos used in contract E2E.

## Next Steps

- [Provider Setup](/docs/provider) — Configuration options in depth
Expand Down
21 changes: 21 additions & 0 deletions apps/docs/app/docs/observability/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ GuideKit guarantees these attribute keys when applicable:

Additional attributes may be added over time, but existing keys are stable.

## Reliability scorecard (Universal Assistant targets)

Track these signals in contract and live E2E suites when rolling out to production:

| Metric | Contract target | Live target |
|--------|-----------------|-------------|
| Claim grounding (hallucination guard clean) | ≥ 90% | ≥ 85% |
| Highlight accuracy on plain pages | ≥ 85% | ≥ 85% |
| Tool success (`highlight`, `scroll`, `readPageContent`) | ≥ 95% | ≥ 90% |
| Prompt tokens per turn (after PageMemory) | −40% vs full prompt | −40% |

**Bus events for context and grounding:**

- `context:memory-rebuild` — PageMemory rebuilt after route or hash change
- `context:delta` — incremental TurnDelta sent on subsequent turns
- `element:resolve` — semantic ref resolved to selector (`confidence`, `reason`)
- `action:confirmation-required` — dangerous click blocked pending user confirmation
- `validation:corrected` — high-severity hallucination issues appended to response

Use DevTools **Events** or `window.__guidekitTest` in the example app to capture these during E2E debugging.

## Debugging workflow

1. Reproduce the slow or failed message in dev with `options={{ debug: true }}`.
Expand Down
12 changes: 12 additions & 0 deletions apps/example-nextjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

### Patch Changes

- Updated dependencies [e504e76]
- @guidekit/core@1.1.0
- @guidekit/react@1.2.0
- @guidekit/server@1.0.2
- @guidekit/intelligence@2.0.0
- @guidekit/knowledge@2.0.0
- @guidekit/plugins@2.0.0

## 0.0.4

### Patch Changes

- Updated dependencies [2b44662]
- @guidekit/core@1.0.0
- @guidekit/react@1.0.0
Expand Down
27 changes: 27 additions & 0 deletions apps/example-nextjs/app/(main)/iframe-test/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default function IframeTestPage() {
return (
<main style={{ maxWidth: '800px', margin: '0 auto', padding: '32px 16px' }}>
<h1>Iframe Grounding Demo</h1>
<p>Same-origin iframe content is readable; cross-origin iframes are listed as limitations.</p>

<section id="embedded-same-origin" style={{ marginTop: '24px' }}>
<h2>Same-origin embed</h2>
<iframe
title="Same origin panel"
src="/plain"
style={{ width: '100%', height: '220px', border: '1px solid #cbd5e1', borderRadius: '8px' }}
/>
</section>

<section id="embedded-cross-origin" style={{ marginTop: '24px' }}>
<h2>Cross-origin embed</h2>
<iframe
title="External ads frame"
src="https://example.com"
sandbox=""
style={{ width: '100%', height: '120px', border: '1px solid #cbd5e1', borderRadius: '8px' }}
/>
</section>
</main>
);
}
34 changes: 34 additions & 0 deletions apps/example-nextjs/app/(main)/plain/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default function PlainPage() {
return (
<main style={{ maxWidth: '720px', margin: '0 auto', padding: '32px 16px' }}>
<h1>Plain Website Demo</h1>
<p>No <code>data-guidekit-target</code> annotations — heuristic DOM scan only.</p>

<section id="intro" aria-labelledby="intro-heading" style={{ marginTop: '24px' }}>
<h2 id="intro-heading">Introduction</h2>
<p>
GuideKit infers sections from headings and landmarks on unannotated pages like this one.
</p>
</section>

<section id="features-plain" style={{ marginTop: '24px' }}>
<h2>Product Features</h2>
<ul>
<li>Runtime DOM grounding</li>
<li>Incremental page memory</li>
<li>Safe click boundaries</li>
</ul>
</section>

<section id="account-actions" style={{ marginTop: '24px' }}>
<h2>Account</h2>
<button type="button" id="safe-action">
View profile
</button>
<button type="button" id="danger-delete" data-testid="danger-delete">
Delete account
</button>
</section>
</main>
);
}
39 changes: 39 additions & 0 deletions apps/example-nextjs/app/(main)/spa-rescan/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { useEffect, useState } from 'react';
import { useGuideKitCore } from '@guidekit/react';

export default function SpaRescanPage() {
const core = useGuideKitCore();
const [variant, setVariant] = useState<'a' | 'b'>('a');

useEffect(() => {
if (!core?.isReady || typeof core.rescanPage !== 'function') return;
core.rescanPage();
}, [variant, core?.isReady, core]);

return (
<main style={{ maxWidth: '720px', margin: '0 auto', padding: '32px 16px' }}>
<h1>SPA DOM Swap Demo</h1>
<p>Replace page content without navigation to test rescan + page memory invalidation.</p>

<button type="button" id="swap-content" onClick={() => setVariant((v) => (v === 'a' ? 'b' : 'a'))}>
Swap content
</button>

<div id="dynamic-root" style={{ marginTop: '24px' }}>
{variant === 'a' ? (
<section id="panel-alpha">
<h2>Panel Alpha</h2>
<p>Initial content visible after load.</p>
</section>
) : (
<section id="panel-beta">
<h2>Panel Beta</h2>
<p>Replacement content after DOM swap — hash should change on rescan.</p>
</section>
)}
</div>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { clearSessionKeys, validateSessionToken } from '@guidekit/server';
import { NextResponse } from 'next/server';
import { guidekitSessionStore } from '../../../../../lib/guidekit-session-store';

/**
* Dev/E2E only — clears server-side session keys so the next LLM proxy call
* returns 401 and the client can exercise real session recovery.
*/
export async function POST(request: Request): Promise<NextResponse> {
if (process.env.NODE_ENV !== 'development') {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}

const signingSecret = process.env.GUIDEKIT_SECRET;
if (!signingSecret) {
return NextResponse.json({ error: 'GUIDEKIT_SECRET not configured' }, { status: 500 });
}

let token: string | undefined;
const auth = request.headers.get('Authorization');
if (auth?.startsWith('Bearer ')) {
token = auth.slice(7).trim();
} else {
try {
const body = (await request.json()) as { token?: string };
token = typeof body.token === 'string' ? body.token.trim() : undefined;
} catch {
token = undefined;
}
}

if (!token) {
return NextResponse.json({ error: 'Missing session token' }, { status: 400 });
}

const validation = await validateSessionToken(token, signingSecret);
if (!validation.valid || !validation.payload) {
return NextResponse.json({ error: validation.error ?? 'Invalid token' }, { status: 401 });
}

const deleted = await clearSessionKeys(validation.payload.sessionId, guidekitSessionStore);
return NextResponse.json({ ok: true, deleted });
}
7 changes: 7 additions & 0 deletions apps/example-nextjs/app/guidekit-test-bridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ declare global {
events: BusEvent[];
waitForEvent: (name: string, timeoutMs?: number) => Promise<BusEvent>;
waitForReady: (timeoutMs?: number) => Promise<void>;
getPageModel: () => unknown;
addKnowledgeDocument: (doc: KnowledgeDocument) => void;
removeKnowledgeDocument: (documentId: string) => void;
clear: () => void;
Expand Down Expand Up @@ -53,6 +54,10 @@ export function GuideKitTestBridge() {
const unsubLlmEnd = core.bus.on('llm:response-end', (data) => {
push('llm:response-end', data);
});
const unsubAny = core.bus.onAny((data, name) => {
if (name === 'validation:complete' || name === 'llm:response-end') return;
push(name, data);
});

window.__guidekitTest = {
events,
Expand Down Expand Up @@ -90,6 +95,7 @@ export function GuideKitTestBridge() {
}
}, 50);
}),
getPageModel: () => core.pageModel,
addKnowledgeDocument: (doc) => {
core.addKnowledgeDocument(doc);
},
Expand All @@ -104,6 +110,7 @@ export function GuideKitTestBridge() {
return () => {
unsubValidation();
unsubLlmEnd();
unsubAny();
delete window.__guidekitTest;
};
}, [core]);
Expand Down
2 changes: 1 addition & 1 deletion apps/example-nextjs/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function Providers({ children }: { children: ReactNode }) {
debug: process.env.NODE_ENV === 'development',
mode: voiceEnabled ? 'voice' : 'text',
clickableSelectors: {
allow: ['#name', '#email', '#message', 'input', 'textarea'],
deny: ['[type="submit"]', '[data-guidekit-no-click]'],
},
}}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/example-nextjs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@guidekit/example-nextjs",
"version": "0.0.3",
"version": "0.0.4",
"private": true,
"scripts": {
"dev": "next dev -p 3099",
Expand Down
Loading
Loading