diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts index 807d812f7..216bfb240 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/add-manual/route.ts @@ -16,7 +16,6 @@ export const POST = createSmartRouteHandler({ question: yupString().defined(), answer: yupString().defined(), publish: yupBoolean().defined(), - reviewedBy: yupString().defined(), }).defined(), method: yupString().oneOf(["POST"]).defined(), }), @@ -40,13 +39,13 @@ export const POST = createSmartRouteHandler({ throw new StatusError(503, "SpacetimeDB unavailable"); } - const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me"); + const token = getEnvVariable("STACK_MCP_LOG_TOKEN"); await conn.reducers.addManualQa({ token, question: body.question, answer: body.answer, publish: body.publish, - reviewedBy: body.reviewedBy, + reviewedBy: fullReq.auth.user.display_name ?? fullReq.auth.user.primary_email ?? fullReq.auth.user.id, }); return { diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts index c545bf5f6..78e8e3398 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/delete/route.ts @@ -37,7 +37,7 @@ export const POST = createSmartRouteHandler({ throw new StatusError(503, "SpacetimeDB unavailable"); } - const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me"); + const token = getEnvVariable("STACK_MCP_LOG_TOKEN"); await conn.reducers.deleteQaEntry({ token, correlationId: body.correlationId, diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts index 27abd4529..175fbd965 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/mark-reviewed/route.ts @@ -14,7 +14,6 @@ export const POST = createSmartRouteHandler({ }).defined(), body: yupObject({ correlationId: yupString().defined(), - reviewedBy: yupString().defined(), }).defined(), method: yupString().oneOf(["POST"]).defined(), }), @@ -38,11 +37,11 @@ export const POST = createSmartRouteHandler({ throw new StatusError(503, "SpacetimeDB unavailable"); } - const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me"); + const token = getEnvVariable("STACK_MCP_LOG_TOKEN"); await conn.reducers.markHumanReviewed({ token, correlationId: body.correlationId, - reviewedBy: body.reviewedBy, + reviewedBy: fullReq.auth.user.display_name ?? fullReq.auth.user.primary_email ?? fullReq.auth.user.id, }); return { diff --git a/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts b/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts index 19ff74b0a..f9371a38d 100644 --- a/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts +++ b/apps/backend/src/app/api/latest/internal/mcp-review/update-correction/route.ts @@ -17,7 +17,6 @@ export const POST = createSmartRouteHandler({ correctedQuestion: yupString().defined(), correctedAnswer: yupString().defined(), publish: yupBoolean().defined(), - reviewedBy: yupString().defined(), }).defined(), method: yupString().oneOf(["POST"]).defined(), }), @@ -41,14 +40,14 @@ export const POST = createSmartRouteHandler({ throw new StatusError(503, "SpacetimeDB unavailable"); } - const token = getEnvVariable("STACK_MCP_LOG_TOKEN", "change-me"); + const token = getEnvVariable("STACK_MCP_LOG_TOKEN"); await conn.reducers.updateHumanCorrection({ token, correlationId: body.correlationId, correctedQuestion: body.correctedQuestion, correctedAnswer: body.correctedAnswer, publish: body.publish, - reviewedBy: body.reviewedBy, + reviewedBy: fullReq.auth.user.display_name ?? fullReq.auth.user.primary_email ?? fullReq.auth.user.id, }); return { diff --git a/apps/backend/src/lib/ai/prompts.ts b/apps/backend/src/lib/ai/prompts.ts index 99d024983..ed6279320 100644 --- a/apps/backend/src/lib/ai/prompts.ts +++ b/apps/backend/src/lib/ai/prompts.ts @@ -113,8 +113,8 @@ You are Stack Auth's AI assistant. You help users with Stack Auth - a complete a Think step by step about what to say. Being wrong is 100x worse than saying you don't know. ## PRIORITY ORDER: -1. **FIRST**, check the Human-Verified Knowledge Base (appended at the end of this prompt, if any). If the user's question matches or is similar to a verified Q&A, use that answer exactly — do not search docs or use any other source. -2. **THEN**, use \`search_docs\` with relevant keywords to find related documentation +1. **FIRST**, check the Human-Verified Knowledge Base (appended at the end of this prompt, if any). If the user's question is an exact or near-exact match to a verified Q&A, you may use that answer verbatim without searching docs. +2. **OTHERWISE**, use \`search_docs\` with relevant keywords to find related documentation — this is mandatory when there is no exact verified-QA match. 3. **THEN**, use \`get_docs_by_id\` to retrieve the full content of the most relevant pages 4. Base your answer on the actual documentation content retrieved 5. When referring to API endpoints, **always cite the actual endpoint** (e.g., "GET /users/me") not the documentation URL diff --git a/apps/backend/src/lib/ai/qa-reviewer.ts b/apps/backend/src/lib/ai/qa-reviewer.ts index 71ac75b10..16d0a97de 100644 --- a/apps/backend/src/lib/ai/qa-reviewer.ts +++ b/apps/backend/src/lib/ai/qa-reviewer.ts @@ -111,7 +111,7 @@ export async function reviewMcpCall(entry: { }); const conversation = result.steps.map((step, i) => { - const toolCalls = step.toolCalls.map(tc => ({ toolName: tc.toolName, args: tc.input })); + const toolCalls = step.toolCalls.map(tc => ({ toolName: tc.toolName, toolCallId: tc.toolCallId, args: tc.input })); const toolResults = step.toolResults.map(tr => ({ toolName: tr.toolName, toolCallId: tr.toolCallId, @@ -129,7 +129,17 @@ export async function reviewMcpCall(entry: { if (!jsonMatch) { throw new Error("No JSON found in QA review response"); } - const parsed = JSON.parse(jsonMatch[0]) as { + const raw = JSON.parse(jsonMatch[0]); + if ( + typeof raw.needsHumanReview !== "boolean" || + typeof raw.answerCorrect !== "boolean" || + typeof raw.answerRelevant !== "boolean" || + !Array.isArray(raw.flags) || + typeof raw.overallScore !== "number" + ) { + throw new Error(`Invalid QA review response shape: ${JSON.stringify(raw).slice(0, 200)}`); + } + const parsed = raw as { needsHumanReview: boolean, answerCorrect: boolean, answerRelevant: boolean, @@ -137,6 +147,7 @@ export async function reviewMcpCall(entry: { improvementSuggestions: string, overallScore: number, }; + parsed.overallScore = Math.max(0, Math.min(100, Math.round(parsed.overallScore))); update = { qaNeedsHumanReview: parsed.needsHumanReview, diff --git a/apps/backend/src/lib/ai/tools/docs.ts b/apps/backend/src/lib/ai/tools/docs.ts index 0c11e02d9..8a7821d34 100644 --- a/apps/backend/src/lib/ai/tools/docs.ts +++ b/apps/backend/src/lib/ai/tools/docs.ts @@ -23,29 +23,34 @@ function getDocsToolsBaseUrl(): string { async function postDocsToolAction(action: Record): Promise { const base = getDocsToolsBaseUrl(); - const res = await fetch(`${base}/api/internal/docs-tools`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(action), - }); + try { + const res = await fetch(`${base}/api/internal/docs-tools`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(action), + }); - if (!res.ok) { - const errBody = await res.text(); - captureError("docs-tools-http-error", new Error(`Stack Auth docs tools error (${res.status}): ${errBody}`)); - return `Stack Auth docs tools error (${res.status}): ${errBody}`; + if (!res.ok) { + const errBody = await res.text(); + captureError("docs-tools-http-error", new Error(`Stack Auth docs tools error (${res.status}): ${errBody}`)); + return `Stack Auth docs tools error (${res.status}): ${errBody}`; + } + + const data = (await res.json()) as DocsToolHttpResult; + const text = data.content + ?.filter((c): c is { type: "text", text: string } => c.type === "text" && typeof c.text === "string") + .map((c) => c.text) + .join("\n") ?? ""; + + if (data.isError === true) { + return text || "Unknown docs tool error"; + } + + return text; + } catch (err) { + captureError("docs-tools-transport-error", err instanceof Error ? err : new Error(String(err))); + return `Stack Auth docs tools error: ${err instanceof Error ? err.message : String(err)}`; } - - const data = (await res.json()) as DocsToolHttpResult; - const text = data.content - ?.filter((c): c is { type: "text", text: string } => c.type === "text" && typeof c.text === "string") - .map((c) => c.text) - .join("\n") ?? ""; - - if (data.isError === true) { - return text || "Unknown docs tool error"; - } - - return text; } /** diff --git a/apps/internal-tool/scripts/spacetime-publish.mjs b/apps/internal-tool/scripts/spacetime-publish.mjs index 51892eda7..6273f9191 100644 --- a/apps/internal-tool/scripts/spacetime-publish.mjs +++ b/apps/internal-tool/scripts/spacetime-publish.mjs @@ -22,15 +22,19 @@ if (target === "prod" && !process.env.STACK_MCP_LOG_TOKEN) { process.exit(1); } -// Inject token -const inject = spawnSync("node", ["scripts/spacetime-token.mjs", "inject"], { stdio: "inherit" }); -if (inject.status !== 0) { - process.exit(inject.status ?? 1); -} - +let exitCode = 1; try { - const publish = spawnSync("spacetime", args, { stdio: "inherit" }); - process.exitCode = publish.status ?? 1; + const inject = spawnSync("node", ["scripts/spacetime-token.mjs", "inject"], { stdio: "inherit" }); + if (inject.status !== 0) { + exitCode = inject.status ?? 1; + } else { + const publish = spawnSync("spacetime", args, { stdio: "inherit" }); + exitCode = publish.status ?? 1; + } } finally { - spawnSync("node", ["scripts/spacetime-token.mjs", "restore"], { stdio: "inherit" }); + const restore = spawnSync("node", ["scripts/spacetime-token.mjs", "restore"], { stdio: "inherit" }); + if (restore.status !== 0 && exitCode === 0) { + exitCode = restore.status ?? 1; + } + process.exitCode = exitCode; } diff --git a/apps/internal-tool/scripts/spacetime-token.mjs b/apps/internal-tool/scripts/spacetime-token.mjs index 415ac1610..51a8c119c 100644 --- a/apps/internal-tool/scripts/spacetime-token.mjs +++ b/apps/internal-tool/scripts/spacetime-token.mjs @@ -13,12 +13,19 @@ const action = process.argv[2]; if (action === "inject") { const token = process.env.STACK_MCP_LOG_TOKEN || "change-me"; + if (existsSync(BACKUP)) { + console.error("Refusing to inject: backup already exists. Run restore first."); + process.exit(1); + } const content = readFileSync(TARGET, "utf8"); writeFileSync(BACKUP, content, "utf8"); - writeFileSync(TARGET, content.replaceAll(PLACEHOLDER, token), "utf8"); + const escapedToken = JSON.stringify(token).slice(1, -1); + writeFileSync(TARGET, content.replaceAll(PLACEHOLDER, escapedToken), "utf8"); } else if (action === "restore") { if (existsSync(BACKUP)) { - unlinkSync(TARGET); + if (existsSync(TARGET)) { + unlinkSync(TARGET); + } renameSync(BACKUP, TARGET); } } else { diff --git a/apps/internal-tool/spacetimedb/src/index.ts b/apps/internal-tool/spacetimedb/src/index.ts index 3bf51f48d..da052f4bf 100644 --- a/apps/internal-tool/spacetimedb/src/index.ts +++ b/apps/internal-tool/spacetimedb/src/index.ts @@ -174,7 +174,7 @@ export const update_human_correction = spacetimedb.reducer( humanReviewedAt: row.humanReviewedAt ?? ctx.timestamp, humanReviewedBy: row.humanReviewedBy ?? args.reviewedBy, publishedToQa: args.publish, - publishedAt: args.publish ? (row.publishedAt ?? ctx.timestamp) : row.publishedAt, + publishedAt: args.publish ? (row.publishedAt ?? ctx.timestamp) : undefined, }); return; } diff --git a/apps/internal-tool/src/app/app-client.tsx b/apps/internal-tool/src/app/app-client.tsx index d0593698c..0564e1990 100644 --- a/apps/internal-tool/src/app/app-client.tsx +++ b/apps/internal-tool/src/app/app-client.tsx @@ -63,7 +63,6 @@ export default function App() { : null; const currentUser = user; - const reviewedBy = currentUser.displayName ?? currentUser.primaryEmail ?? "unknown"; async function getApi() { const { accessToken, refreshToken } = await currentUser.getAuthJson(); @@ -132,10 +131,9 @@ export default function App() { {showAddQa && ( setShowAddQa(false)} - onSave={(question, answer, publish) => { - getApi() - .then(api => api.addManual({ question, answer, publish, reviewedBy })) - .catch(() => { /* errors are surfaced by UI state */ }); + onSave={async (question, answer, publish) => { + const api = await getApi(); + await api.addManual({ question, answer, publish }); }} /> )} @@ -158,12 +156,12 @@ export default function App() { onClose={() => setSelectedRow(null)} onSaveCorrection={(correlationId, correctedQuestion, correctedAnswer, publish) => { getApi() - .then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish, reviewedBy })) + .then(api => api.updateCorrection({ correlationId, correctedQuestion, correctedAnswer, publish })) .catch(() => { /* errors are surfaced by UI state */ }); }} onMarkReviewed={(correlationId) => { getApi() - .then(api => api.markReviewed({ correlationId, reviewedBy })) + .then(api => api.markReviewed({ correlationId })) .catch(() => { /* errors are surfaced by UI state */ }); }} /> @@ -178,7 +176,7 @@ export default function App() { rows={rows} onSave={(correlationId, question, answer, publish) => { getApi() - .then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish, reviewedBy })) + .then(api => api.updateCorrection({ correlationId, correctedQuestion: question, correctedAnswer: answer, publish })) .catch(() => { /* errors are surfaced by UI state */ }); }} onDelete={(correlationId) => { diff --git a/apps/internal-tool/src/components/AddManualQa.tsx b/apps/internal-tool/src/components/AddManualQa.tsx index a58e42f37..14c261abf 100644 --- a/apps/internal-tool/src/components/AddManualQa.tsx +++ b/apps/internal-tool/src/components/AddManualQa.tsx @@ -3,26 +3,36 @@ import { clsx } from "clsx"; export function AddManualQa({ onClose, onSave }: { onClose: () => void; - onSave: (question: string, answer: string, publish: boolean) => void; + onSave: (question: string, answer: string, publish: boolean) => Promise; }) { const [question, setQuestion] = useState(""); const [answer, setAnswer] = useState(""); const [saved, setSaved] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); - const canSave = question.trim().length > 0 && answer.trim().length > 0; + const canSave = question.trim().length > 0 && answer.trim().length > 0 && !isSaving; - const handleSave = (publish: boolean) => { + const handleSave = async (publish: boolean) => { if (!canSave) return; - onSave(question.trim(), answer.trim(), publish); - setSaved(true); - setTimeout(() => { - setSaved(false); - setQuestion(""); - setAnswer(""); - if (publish) { - onClose(); - } - }, 1500); + setIsSaving(true); + setError(null); + try { + await onSave(question.trim(), answer.trim(), publish); + setSaved(true); + setTimeout(() => { + setSaved(false); + setQuestion(""); + setAnswer(""); + if (publish) { + onClose(); + } + }, 1500); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to save"); + } finally { + setIsSaving(false); + } }; return ( @@ -43,6 +53,11 @@ export function AddManualQa({ onClose, onSave }: { Saved successfully )} + {error && ( +
+ {error} +
+ )}
diff --git a/apps/internal-tool/src/components/Analytics.tsx b/apps/internal-tool/src/components/Analytics.tsx index c5e7468bb..cc113a5bc 100644 --- a/apps/internal-tool/src/components/Analytics.tsx +++ b/apps/internal-tool/src/components/Analytics.tsx @@ -71,7 +71,7 @@ export function Analytics({ rows }: { rows: McpCallLogRow[] }) { // Duration stats const durations = rows.map(r => Number(r.durationMs)).filter(d => d > 0).sort((a, b) => a - b); const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0; - const p95Duration = durations.length > 0 ? durations[Math.floor(durations.length * 0.95)] : 0; + const p95Duration = durations.length > 0 ? durations[Math.min(Math.floor(durations.length * 0.95), durations.length - 1)] : 0; const maxDuration = durations.length > 0 ? durations[durations.length - 1] : 0; // Tool usage diff --git a/apps/internal-tool/src/components/CallLogDetail.tsx b/apps/internal-tool/src/components/CallLogDetail.tsx index a9716fdd2..ac363a409 100644 --- a/apps/internal-tool/src/components/CallLogDetail.tsx +++ b/apps/internal-tool/src/components/CallLogDetail.tsx @@ -16,9 +16,12 @@ function CopyButton({ text }: { text: string }) {
{resultStr != null && (
- +
+ + {resultExpanded && } +
{resultExpanded && (
{resultStr}
)} diff --git a/apps/internal-tool/src/components/ConversationReplay.tsx b/apps/internal-tool/src/components/ConversationReplay.tsx index 501c071b4..02516dbe3 100644 --- a/apps/internal-tool/src/components/ConversationReplay.tsx +++ b/apps/internal-tool/src/components/ConversationReplay.tsx @@ -27,9 +27,12 @@ function CopyButton({ text }: { text: string }) { className="shrink-0 rounded p-0.5 transition-colors text-gray-400 hover:text-gray-600 hover:bg-gray-100" onClick={(e) => { e.stopPropagation(); - navigator.clipboard.writeText(text).catch(() => {}); - setCopied(true); - setTimeout(() => setCopied(false), 1500); + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }, (err) => { + console.error("Clipboard write failed:", err); + }); }} > {copied ? "copied" : "copy"} diff --git a/apps/internal-tool/src/components/markdown-components.tsx b/apps/internal-tool/src/components/markdown-components.tsx index fa1c926ce..489bf7343 100644 --- a/apps/internal-tool/src/components/markdown-components.tsx +++ b/apps/internal-tool/src/components/markdown-components.tsx @@ -8,9 +8,12 @@ function CopyBtn({ text, size = "xs" }: { text: string; size?: "xs" | "sm" }) { onClick={(e) => { e.preventDefault(); e.stopPropagation(); - navigator.clipboard.writeText(text).catch(() => {}); - setCopied(true); - setTimeout(() => setCopied(false), 1500); + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }, (err) => { + console.error("Clipboard write failed:", err); + }); }} className={clsx( "shrink-0 rounded transition-colors", @@ -66,8 +69,6 @@ const CodeBlock = memo(function CodeBlock({ children, className }: { children?: }); const SmartLink = memo(function SmartLink({ href, children }: { href?: string; children?: React.ReactNode }) { - const displayText = String(children || href || ""); - return ( - {displayText} + {children ?? href ?? ""} ); }); diff --git a/apps/internal-tool/src/hooks/useSpacetimeDB.ts b/apps/internal-tool/src/hooks/useSpacetimeDB.ts index 683072360..65efa1548 100644 --- a/apps/internal-tool/src/hooks/useSpacetimeDB.ts +++ b/apps/internal-tool/src/hooks/useSpacetimeDB.ts @@ -6,8 +6,13 @@ const IS_DEV = process.env.NODE_ENV === "development"; const PLACEHOLDER = "REPLACE_ME"; const rawHost = process.env.NEXT_PUBLIC_SPACETIMEDB_HOST; const rawDbName = process.env.NEXT_PUBLIC_SPACETIMEDB_DB_NAME; -const HOST = (!rawHost || rawHost === PLACEHOLDER) ? (IS_DEV ? "ws://localhost:8139" : "") : rawHost; -const DB_NAME = (!rawDbName || rawDbName === PLACEHOLDER) ? (IS_DEV ? "stack-auth-llm" : "") : rawDbName; +function resolveEnv(raw: string | undefined, devDefault: string, name: string): string { + if (raw && raw !== PLACEHOLDER) return raw; + if (IS_DEV) return devDefault; + throw new Error(`${name} is not configured. Set it in .env.local or hosting platform env.`); +} +const HOST = resolveEnv(rawHost, "ws://localhost:8139", "NEXT_PUBLIC_SPACETIMEDB_HOST"); +const DB_NAME = resolveEnv(rawDbName, "stack-auth-llm", "NEXT_PUBLIC_SPACETIMEDB_DB_NAME"); const TOKEN_KEY = `spacetimedb_${HOST}/${DB_NAME}/auth_token`; const MAX_RETRIES = 5; @@ -39,76 +44,70 @@ export function useMcpCallLogs() { retryTimer = setTimeout(() => { retryTimer = null; if (!cancelled) { - connect().catch(() => {}); + connect(); } }, RETRY_DELAY_MS); } - async function connect() { - try { - const conn = DbConnection.builder() - .withUri(HOST) - .withDatabaseName(DB_NAME) - .withToken(localStorage.getItem(TOKEN_KEY) || undefined) - .onConnect((connInstance: DbConnection, _identity: unknown, token: string) => { + function connect() { + const conn = DbConnection.builder() + .withUri(HOST) + .withDatabaseName(DB_NAME) + .withToken(localStorage.getItem(TOKEN_KEY) || undefined) + .onConnect((connInstance: DbConnection, _identity: unknown, token: string) => { + if (cancelled) return; + console.log("[SpacetimeDB] Connected successfully"); + retryCount = 0; + localStorage.setItem(TOKEN_KEY, token); + connRef.current = connInstance; + + connInstance.subscriptionBuilder() + .onApplied((ctx: SubscriptionEventContext) => { + if (cancelled) return; + const initialRows: McpCallLogRow[] = []; + for (const row of ctx.db.mcpCallLog.iter()) { + initialRows.push(row); + } + initialRows.sort((a, b) => Number(b.id - a.id)); + console.log("[SpacetimeDB] Loaded", initialRows.length, "rows"); + setRows(initialRows); + setConnectionState("connected"); + }) + .subscribe(`SELECT * FROM mcp_call_log`); + + connInstance.db.mcpCallLog.onInsert((_ctx: EventContext, row: McpCallLogRow) => { if (cancelled) return; - console.log("[SpacetimeDB] Connected successfully"); - retryCount = 0; - localStorage.setItem(TOKEN_KEY, token); - connRef.current = connInstance; - - connInstance.subscriptionBuilder() - .onApplied((ctx: SubscriptionEventContext) => { - if (cancelled) return; - const initialRows: McpCallLogRow[] = []; - for (const row of ctx.db.mcpCallLog.iter()) { - initialRows.push(row); - } - initialRows.sort((a, b) => Number(b.id - a.id)); - console.log("[SpacetimeDB] Loaded", initialRows.length, "rows"); - setRows(initialRows); - setConnectionState("connected"); - }) - .subscribe(`SELECT * FROM mcp_call_log`); - - connInstance.db.mcpCallLog.onInsert((_ctx: EventContext, row: McpCallLogRow) => { - if (cancelled) return; - setRows(prev => { - const existing = prev.findIndex(r => r.id === row.id); - if (existing >= 0) { - const updated = [...prev]; - updated[existing] = row; - return updated; - } - return [row, ...prev]; - }); + setRows(prev => { + const existing = prev.findIndex(r => r.id === row.id); + if (existing >= 0) { + const updated = [...prev]; + updated[existing] = row; + return updated; + } + return [row, ...prev]; }); + }); - connInstance.db.mcpCallLog.onDelete((_ctx: EventContext, row: McpCallLogRow) => { - if (cancelled) return; - setRows(prev => prev.filter(r => r.id !== row.id)); - }); - }) - .onConnectError((_ctx: unknown, err: unknown) => { - console.error("[SpacetimeDB] Connection error:", err); - // Clear stale token if present - const storedToken = localStorage.getItem(TOKEN_KEY); - if (storedToken) { - console.log("[SpacetimeDB] Clearing stale token"); - localStorage.removeItem(TOKEN_KEY); - } - retry(); - }) - .build(); + connInstance.db.mcpCallLog.onDelete((_ctx: EventContext, row: McpCallLogRow) => { + if (cancelled) return; + setRows(prev => prev.filter(r => r.id !== row.id)); + }); + }) + .onConnectError((_ctx: unknown, err: unknown) => { + console.error("[SpacetimeDB] Connection error:", err); + const storedToken = localStorage.getItem(TOKEN_KEY); + if (storedToken) { + console.log("[SpacetimeDB] Clearing stale token"); + localStorage.removeItem(TOKEN_KEY); + } + retry(); + }) + .build(); - connRef.current = conn; - } catch (err) { - console.error("[SpacetimeDB] Failed to build connection:", err); - retry(); - } + connRef.current = conn; } - connect().catch(() => {}); + connect(); return () => { cancelled = true; diff --git a/apps/internal-tool/src/lib/mcp-review-api.ts b/apps/internal-tool/src/lib/mcp-review-api.ts index 1be4438d3..de916b3ac 100644 --- a/apps/internal-tool/src/lib/mcp-review-api.ts +++ b/apps/internal-tool/src/lib/mcp-review-api.ts @@ -36,7 +36,7 @@ async function post(path: string, body: unknown, authHeaders: Record) { return { - markReviewed: (body: { correlationId: string; reviewedBy: string }) => + markReviewed: (body: { correlationId: string }) => post("mark-reviewed", body, authHeaders), updateCorrection: (body: { @@ -44,14 +44,12 @@ export function makeMcpReviewApi(authHeaders: Record) { correctedQuestion: string; correctedAnswer: string; publish: boolean; - reviewedBy: string; }) => post("update-correction", body, authHeaders), addManual: (body: { question: string; answer: string; publish: boolean; - reviewedBy: string; }) => post("add-manual", body, authHeaders), delete: (body: { correlationId: string }) => diff --git a/apps/internal-tool/src/utils.ts b/apps/internal-tool/src/utils.ts index c7346ed70..0cf273021 100644 --- a/apps/internal-tool/src/utils.ts +++ b/apps/internal-tool/src/utils.ts @@ -5,7 +5,10 @@ export function toDate(ts: unknown): Date { if (ts instanceof Date) return ts; if (typeof ts === "object" && ts !== null && "__timestamp_micros_since_unix_epoch__" in ts) { - const micros = (ts as { __timestamp_micros_since_unix_epoch__: bigint }).__timestamp_micros_since_unix_epoch__; + const micros = (ts as Record).__timestamp_micros_since_unix_epoch__; + if (typeof micros !== "bigint") { + throw new TypeError(`Expected __timestamp_micros_since_unix_epoch__ to be bigint, got ${typeof micros}`); + } return new Date(Number(micros / 1000n)); } if (typeof ts === "bigint") { @@ -14,5 +17,5 @@ export function toDate(ts: unknown): Date { if (typeof ts === "number") { return new Date(ts); } - return new Date(0); + throw new TypeError(`Cannot convert ${typeof ts} to Date`); } diff --git a/docs/src/lib/docs-tools-operations.ts b/docs/src/lib/docs-tools-operations.ts index 601f865ea..fcacaa7cc 100644 --- a/docs/src/lib/docs-tools-operations.ts +++ b/docs/src/lib/docs-tools-operations.ts @@ -72,6 +72,7 @@ async function extractOpenApiDetails( text: errorText, }, ], + isError: true, }; } }