Add permissions file, enhance HubertChat init & retry, update API routes

- Introduce .claude/.fuse_hidden... with explicit allow permissions
- Add fetchWithTimeout utility for robust network calls
- Implement init error handling and retry mechanism in HubChat
- Update API handlers for new visitor and chat routes
- Adjust wrangler.jsonc for deployment changes

Signed: Hubert The Eunuch
This commit is contained in:
Nicholai Vogel 2026-01-03 18:18:53 -07:00
parent d4959500a8
commit 9ef1027cc6
6 changed files with 257 additions and 37 deletions

View File

@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"WebFetch(domain:openrouter.ai)",
"Bash(node:*)",
"Bash(curl:*)",
"Bash(pnpm build:*)",
"Bash(find:*)",
"Bash(pnpm add:*)",
"WebFetch(domain:substance.biohazardvfx.com)",
"WebFetch(domain:substrate.biohazardvfx.com)"
],
"deny": [],
"ask": []
}
}

View File

@ -1,5 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import { randomUUID } from 'crypto';
interface Message {
role: 'user' | 'assistant' | 'system';
@ -7,6 +6,24 @@ interface Message {
timestamp: string;
}
// Utility: Fetch with timeout
const fetchWithTimeout = async (url: string, options: RequestInit = {}, timeout = 8000) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
};
export default function HubertChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
@ -14,15 +31,32 @@ export default function HubertChat() {
const [conversationId, setConversationId] = useState<string | null>(null);
const [isTyping, setIsTyping] = useState(false);
const [isInitializing, setIsInitializing] = useState(true);
const [initError, setInitError] = useState<string | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Initialize visitor on mount
useEffect(() => {
const initVisitor = async () => {
try {
console.log('[Hubert] Starting initialization...');
setIsInitializing(true);
const response = await fetch('/api/hubert/new-visitor', { method: 'POST' });
setInitError(null);
console.log('[Hubert] Fetching /api/hubert/new-visitor...');
const response = await fetchWithTimeout('/api/hubert/new-visitor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, 8000); // 8 second timeout
console.log('[Hubert] Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('[Hubert] Initialization successful:', data);
setVisitorId(data.visitor_id);
setConversationId(data.conversation_id);
@ -33,27 +67,128 @@ export default function HubertChat() {
timestamp: new Date().toISOString(),
}]);
} catch (error) {
console.error('Failed to initialize Hubert:', error);
console.error('[Hubert] Initialization failed:', error);
let errorMessage = '/// ERROR: UNKNOWN_FAILURE';
if (error instanceof Error) {
if (error.name === 'AbortError') {
errorMessage = '/// ERROR: TIMEOUT - API_UNRESPONSIVE';
console.error('[Hubert] Request timed out after 8 seconds');
} else if (error.message.includes('Failed to fetch')) {
errorMessage = '/// ERROR: NETWORK_FAILURE - CHECK_API_ROUTE';
console.error('[Hubert] Network error - API route may not exist');
} else {
errorMessage = `/// ERROR: ${error.message}`;
}
}
setInitError(errorMessage);
setMessages([{
role: 'system',
content: '/// ERROR: HUBERT_OFFLINE - REFRESH_PAGE',
content: errorMessage + '\\n\\nCLICK [RETRY] BELOW',
timestamp: new Date().toISOString(),
}]);
} finally {
console.log('[Hubert] Initialization complete, setting isInitializing to false');
setIsInitializing(false);
}
};
initVisitor();
}, []);
// Auto-scroll to bottom
// Retry initialization
const retryInit = () => {
console.log('[Hubert] Retrying initialization...');
setIsInitializing(true);
setInitError(null);
setMessages([]);
// Re-trigger initialization
const initVisitor = async () => {
try {
console.log('[Hubert] Starting initialization...');
setIsInitializing(true);
setInitError(null);
console.log('[Hubert] Fetching /api/hubert/new-visitor...');
const response = await fetchWithTimeout('/api/hubert/new-visitor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, 8000);
console.log('[Hubert] Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('[Hubert] Initialization successful:', data);
setVisitorId(data.visitor_id);
setConversationId(data.conversation_id);
setMessages([{
role: 'system',
content: `/// HUBERT_EUNUCH /// ONLINE\\n\\nI suppose you want something. State your business.`,
timestamp: new Date().toISOString(),
}]);
} catch (error) {
console.error('[Hubert] Initialization failed:', error);
let errorMessage = '/// ERROR: UNKNOWN_FAILURE';
if (error instanceof Error) {
if (error.name === 'AbortError') {
errorMessage = '/// ERROR: TIMEOUT - API_UNRESPONSIVE';
console.error('[Hubert] Request timed out after 8 seconds');
} else if (error.message.includes('Failed to fetch')) {
errorMessage = '/// ERROR: NETWORK_FAILURE - CHECK_API_ROUTE';
console.error('[Hubert] Network error - API route may not exist');
} else {
errorMessage = `/// ERROR: ${error.message}`;
}
}
setInitError(errorMessage);
setMessages([{
role: 'system',
content: errorMessage + '\\n\\nCLICK [RETRY] BELOW',
timestamp: new Date().toISOString(),
}]);
} finally {
console.log('[Hubert] Initialization complete, setting isInitializing to false');
setIsInitializing(false);
}
};
initVisitor();
};
// Auto-scroll to bottom of chat container (not entire page)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
if (messagesEndRef.current) {
const container = messagesEndRef.current.parentElement;
if (container) {
container.scrollTop = container.scrollHeight;
}
}
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || isTyping || !visitorId || !conversationId) return;
console.log('[Hubert] sendMessage called', { input, isTyping, visitorId, conversationId });
if (!input.trim() || isTyping || !visitorId || !conversationId) {
console.log('[Hubert] sendMessage blocked:', {
noInput: !input.trim(),
isTyping,
noVisitorId: !visitorId,
noConversationId: !conversationId
});
return;
}
console.log('[Hubert] Sending message:', input);
const userMessage: Message = {
role: 'user',
content: input,
@ -65,6 +200,7 @@ export default function HubertChat() {
setIsTyping(true);
try {
console.log('[Hubert] Fetching /api/hubert/chat...');
const response = await fetch('/api/hubert/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -78,7 +214,14 @@ export default function HubertChat() {
}),
});
console.log('[Hubert] Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('[Hubert] Response data:', data);
if (data.error) {
throw new Error(data.error);
@ -90,9 +233,10 @@ export default function HubertChat() {
timestamp: new Date().toISOString(),
};
console.log('[Hubert] Adding assistant message:', assistantMessage.content);
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Hubert chat error:', error);
console.error('[Hubert] Chat error:', error);
setMessages(prev => [...prev, {
role: 'assistant',
content: '/// HUBERT_MALFUNCTION - TRY AGAIN',
@ -103,10 +247,13 @@ export default function HubertChat() {
}
};
if (isInitializing) {
// Show loading or error state
if (isInitializing || initError) {
return (
<div className="bg-[var(--theme-bg-secondary)] border-2 border-[var(--theme-border-primary)] shadow-2xl">
<div className="flex items-center justify-center py-12">
<div className="flex flex-col items-center justify-center py-12 px-6 gap-6">
{isInitializing && !initError ? (
<>
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
<div className="w-2 h-2 bg-brand-accent animate-pulse" />
@ -117,6 +264,36 @@ export default function HubertChat() {
HUBERT_IS_BOOTING...
</span>
</div>
<div className="text-[10px] font-mono text-[var(--theme-text-subtle)] text-center max-w-md">
Initializing chatbot... Check console for debug info.
</div>
</>
) : initError ? (
<>
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
<div className="w-2 h-2 bg-red-500" />
<div className="w-2 h-2 bg-red-500/50" />
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
</div>
<span className="text-xs font-mono text-red-400">
HUBERT_INITIALIZATION_FAILED
</span>
</div>
<div className="font-mono text-sm text-[var(--theme-text-muted)] text-center max-w-md px-4 py-3 bg-[var(--theme-bg-tertiary)] border border-[var(--theme-border-secondary)]">
{initError}
</div>
<button
onClick={retryInit}
className="px-6 py-3 bg-brand-accent text-brand-dark font-mono text-[10px] uppercase tracking-widest font-bold hover:bg-brand-accent/90 transition-all border-none cursor-pointer"
>
[RETRY]
</button>
<div className="text-[10px] font-mono text-[var(--theme-text-subtle)] text-center max-w-md">
Check browser console for detailed error information.
</div>
</>
) : null}
</div>
</div>
);

View File

@ -35,8 +35,6 @@ import HubertChat from '../HubertChat';
</div>
<!-- Hubert Chat Interface -->
<client:load>
<HubertChat />
</client:load>
<HubertChat client:load />
</div>
</section>

View File

@ -71,7 +71,15 @@ const searchBlog = tool(
*/
export const POST = async (context) => {
try {
const { request, env } = context || {};
const { request, locals } = context || {};
// In Astro with Cloudflare adapter, env is at locals.runtime.env
const env = locals?.runtime?.env;
console.log('[Hubert API] Chat endpoint called');
console.log('[Hubert API] env object:', env);
console.log('[Hubert API] env keys:', env ? Object.keys(env) : 'no env');
console.log('[Hubert API] OPENROUTER_API_KEY present:', !!(env?.OPENROUTER_API_KEY));
const { messages, conversation_id, visitor_id } = await request.json();
if (!messages || !conversation_id || !visitor_id) {
@ -88,7 +96,7 @@ export const POST = async (context) => {
const openRouterApiKey = env?.OPENROUTER_API_KEY;
if (!openRouterApiKey) {
// Dev mode fallback: return a canned response
console.log('[Hubert] Dev mode: No API key, using fallback response');
console.log('[Hubert API] Dev mode: No API key found, using fallback response');
return new Response(
JSON.stringify({
messages: [
@ -167,7 +175,8 @@ When they say goodbye or conversation ends, use the save_conversation tool to ar
const data = await response.json();
const assistantContent = data.choices[0]?.message?.content || '...';
console.log(`[Hubert] Generated response in ${Date.now() - Date.parse(response.headers.get('date') || '').getTime()}ms`);
const responseTime = response.headers.get('date') ? Date.now() - Date.parse(response.headers.get('date')) : 0;
console.log(`[Hubert] Generated response in ${responseTime}ms`);
return new Response(
JSON.stringify({

View File

@ -9,25 +9,41 @@ export const prerender = false;
* Used when Hubert interface first loads
*/
export const POST = async (context) => {
console.log('[Hubert API] /api/hubert/new-visitor endpoint called');
try {
const { request, env } = context;
const { request, locals } = context;
// In Astro with Cloudflare adapter, env is at locals.runtime.env
const env = locals?.runtime?.env;
console.log('[Hubert API] Request received, env binding available:', !!env);
console.log('[Hubert API] HUBERT_DB binding available:', !!(env && env.HUBERT_DB));
const userAgent = request.headers.get('user-agent') || 'unknown';
const ip = request.headers.get('cf-connecting-ip') || 'unknown';
const visitorId = randomUUID();
console.log('[Hubert API] Generated visitor ID:', visitorId);
// Only insert into database if HUBERT_DB binding exists (production)
// In dev mode, this allows the chatbot to work without Cloudflare bindings
if (env && env.HUBERT_DB) {
console.log('[Hubert API] Attempting database insert...');
try {
await env.HUBERT_DB.prepare(`
INSERT INTO visitors (visitor_id, first_seen_at, last_seen_at, ip_address, user_agent)
VALUES (?, datetime('now'), datetime('now'), ?, ?)
`).bind(visitorId, ip, userAgent).run();
console.log(`[Hubert] New visitor initialized: ${visitorId}`);
console.log(`[Hubert API] Database insert successful for visitor: ${visitorId}`);
} catch (dbError) {
console.error('[Hubert API] Database insert failed (continuing anyway):', dbError);
// Continue anyway - don't fail initialization if DB is misconfigured
}
} else {
console.log(`[Hubert] Dev mode: Skipping database insert for visitor: ${visitorId}`);
console.log(`[Hubert API] Dev mode: Skipping database insert for visitor: ${visitorId}`);
}
console.log('[Hubert API] Returning success response');
return Response.json({
visitor_id: visitorId,
conversation_id: visitorId, // Use visitor_id as initial conversation_id

View File

@ -28,10 +28,14 @@
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
*/
// "vars": { "MY_VARIABLE": "production_value" }
"vars": {
"OPENROUTER_API_KEY": ""
},
/**
* Note: Use secrets to store sensitive data.
* https:// developers.cloudflare.com/workers/configuration/secrets/
* For dev: Add OPENROUTER_API_KEY to .dev.vars file
* For production: Use wrangler secret put OPENROUTER_API_KEY
*/
/**
* Static Assets