diff --git a/FORK_CONFIG.md b/FORK_CONFIG.md index dbe47a7..0fa3855 100644 --- a/FORK_CONFIG.md +++ b/FORK_CONFIG.md @@ -2,6 +2,8 @@ After forking this repo, update these files with your site information. Choose one of two options: +**Important**: Keep your `fork-config.json` file after configuring. The `sync:discovery` commands will use it to update discovery files (`AGENTS.md`, `CLAUDE.md`, `public/llms.txt`) with your configured values. + --- ## Option 1: Automated Script (Recommended) @@ -16,6 +18,8 @@ cp fork-config.json.example fork-config.json The file `fork-config.json` is gitignored, so your configuration stays local and is not committed. The `.example` file remains as a template. +**Keep this file**: Even after running `npm run configure`, keep the `fork-config.json` file. Future sync commands will use it to maintain your configuration. + ### Step 2: Edit fork-config.json ```json @@ -1070,7 +1074,7 @@ rightSidebar: true ## AI Chat Configuration -Configure the AI writing assistant powered by Anthropic Claude. +Configure the AI writing assistant. The Dashboard AI Agent supports multiple providers (Anthropic, OpenAI, Google) and includes image generation. ### In fork-config.json @@ -1079,6 +1083,19 @@ Configure the AI writing assistant powered by Anthropic Claude. "aiChat": { "enabledOnWritePage": false, "enabledOnContent": false + }, + "aiDashboard": { + "enableImageGeneration": true, + "defaultTextModel": "claude-sonnet-4-20250514", + "textModels": [ + { "id": "claude-sonnet-4-20250514", "name": "Claude Sonnet 4", "provider": "anthropic" }, + { "id": "gpt-4o", "name": "GPT-4o", "provider": "openai" }, + { "id": "gemini-2.0-flash", "name": "Gemini 2.0 Flash", "provider": "google" } + ], + "imageModels": [ + { "id": "gemini-2.0-flash-exp-image-generation", "name": "Nano Banana", "provider": "google" }, + { "id": "imagen-3.0-generate-002", "name": "Nano Banana Pro", "provider": "google" } + ] } } ``` @@ -1092,14 +1109,36 @@ aiChat: { enabledOnWritePage: true, // Show AI chat toggle on /write page enabledOnContent: true, // Allow AI chat on posts/pages via frontmatter }, +aiDashboard: { + enableImageGeneration: true, // Enable image generation tab in Dashboard AI Agent + defaultTextModel: "claude-sonnet-4-20250514", // Default model for chat + textModels: [ + { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" }, + { id: "gpt-4o", name: "GPT-4o", provider: "openai" }, + { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", provider: "google" }, + ], + imageModels: [ + { id: "gemini-2.0-flash-exp-image-generation", name: "Nano Banana", provider: "google" }, + { id: "imagen-3.0-generate-002", name: "Nano Banana Pro", provider: "google" }, + ], +}, ``` **Environment Variables (Convex):** -- `ANTHROPIC_API_KEY` (required): Your Anthropic API key +| Variable | Provider | Features | +| --- | --- | --- | +| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | +| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | +| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | + +**Optional system prompt variables:** + - `CLAUDE_PROMPT_STYLE`, `CLAUDE_PROMPT_COMMUNITY`, `CLAUDE_PROMPT_RULES` (optional): Split system prompts - `CLAUDE_SYSTEM_PROMPT` (optional): Single system prompt fallback +**Note:** Only configure the API keys for providers you want to use. If a key is not set, users see a helpful setup message when they try to use that model. + **Frontmatter Usage:** Enable AI chat on posts/pages: @@ -1114,6 +1153,13 @@ aiChat: true Requires `rightSidebar: true` and `siteConfig.aiChat.enabledOnContent: true`. +**Dashboard AI Agent Features:** + +- **Chat Tab:** Multi-model selector with lazy API key validation +- **Image Tab:** AI image generation with aspect ratio selection (1:1, 16:9, 9:16, 4:3, 3:4) +- Images stored in Convex storage with session tracking +- Gallery view of recent generated images + --- ## Posts Display Configuration @@ -1205,11 +1251,15 @@ Update these files: 3. Run `npm run dev` to test locally 4. Deploy to Netlify when ready +**Note**: Keep your `fork-config.json` file. When you run `npm run sync:discovery` or `npm run sync:all`, it reads from `fork-config.json` to update discovery files with your site information. + --- ## Syncing Discovery Files -Discovery files (`AGENTS.md` and `public/llms.txt`) can be automatically updated with your current app data. +Discovery files (`AGENTS.md`, `CLAUDE.md`, and `public/llms.txt`) can be automatically updated with your current app data. + +**How it works**: The sync:discovery script reads from `fork-config.json` (if it exists) to get your site name, URL, and GitHub info. This ensures your configured values are preserved when updating discovery files. ### Commands diff --git a/TASK.md b/TASK.md index 3a278eb..bb7284c 100644 --- a/TASK.md +++ b/TASK.md @@ -3,16 +3,29 @@ ## To Do - [ ] fix site confg link -- [ ] dashboard agent nanobanno - [ ] npm package - ## Current Status -v2.5.0 ready. Social footer icons can now display in header navigation. +v2.6.0 ready. Multi-model AI chat and image generation in Dashboard. ## Completed +- [x] Multi-model AI chat and image generation in Dashboard + - [x] AI Agent section with tab-based UI (Chat and Image Generation tabs) + - [x] Multi-model selector for text chat (Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash) + - [x] Lazy API key validation with friendly setup instructions per provider + - [x] Image generation with Nano Banana (gemini-2.0-flash-exp-image-generation) and Nano Banana Pro (imagen-3.0-generate-002) + - [x] Aspect ratio selector for images (1:1, 16:9, 9:16, 4:3, 3:4) + - [x] Generated images stored in Convex storage with session tracking + - [x] New `aiDashboard` configuration in siteConfig.ts + - [x] New `convex/aiImageGeneration.ts` for Gemini image generation + - [x] New `aiGeneratedImages` table in schema for tracking generated images + - [x] Updated aiChatActions.ts with multi-provider support (Anthropic, OpenAI, Google) + - [x] Updated AIChatView.tsx with selectedModel prop + - [x] CSS styles for AI Agent tabs, model selectors, and image display + - [x] Updated files.md, changelog.md, TASK.md, changelog-page.md + - [x] Social footer icons in header navigation - [x] Added `showInHeader` option to `siteConfig.socialFooter` config - [x] Exported `platformIcons` from SocialFooter.tsx for reuse diff --git a/changelog.md b/changelog.md index 82d0da7..0112f71 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,49 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [2.6.0] - 2026-01-01 + +### Added + +- Multi-model AI chat support in Dashboard + - Model dropdown selector to choose between Anthropic (Claude Sonnet 4), OpenAI (GPT-4o), and Google (Gemini 2.0 Flash) + - Lazy API key validation: errors only shown when user tries to use a specific model + - Each provider has friendly setup instructions with links to get API keys +- AI Image Generation tab in Dashboard + - Generate images using Gemini models (Nano Banana and Nano Banana Pro) + - Aspect ratio selector (1:1, 16:9, 9:16, 4:3, 3:4) + - Generated images stored in Convex storage with session tracking + - Markdown-rendered error messages with setup instructions +- New `aiDashboard` configuration in siteConfig + - `enableImageGeneration`: Toggle image generation tab + - `defaultTextModel`: Set default AI model for chat + - `textModels`: Configure available text chat models + - `imageModels`: Configure available image generation models + +### Technical + +- Updated `convex/aiChatActions.ts` to support multiple AI providers + - Added `callAnthropicApi`, `callOpenAIApi`, `callGeminiApi` helper functions + - Added `getProviderFromModel` to determine provider from model ID + - Added `getApiKeyForProvider` for lazy API key retrieval + - Added `getNotConfiguredMessage` for provider-specific setup instructions +- Updated `src/components/AIChatView.tsx` with `selectedModel` prop +- Updated `src/pages/Dashboard.tsx` with new `AIAgentSection` + - Tab-based UI for Chat and Image Generation + - Model dropdowns with provider labels + - Aspect ratio selector for image generation +- Added CSS styles for AI Agent section in `src/styles/global.css` + - `.ai-agent-tabs`, `.ai-agent-tab` for tab navigation + - `.ai-model-selector`, `.ai-model-dropdown` for model selection + - `.ai-aspect-ratio-selector` for aspect ratio options + - `.ai-generated-image`, `.ai-image-error`, `.ai-image-loading` for image display + +### Environment Variables + +- `ANTHROPIC_API_KEY`: Required for Claude models +- `OPENAI_API_KEY`: Required for GPT-4o +- `GOOGLE_AI_API_KEY`: Required for Gemini text chat and image generation + ## [2.5.0] - 2026-01-01 ### Added diff --git a/content/blog/about-this-blog.md b/content/blog/about-this-blog.md index 1c728aa..e446e87 100644 --- a/content/blog/about-this-blog.md +++ b/content/blog/about-this-blog.md @@ -15,7 +15,7 @@ authorImage: "/images/authors/markdown.png" # About This Markdown Framework -An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. +An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. ## How It Works diff --git a/content/blog/fork-configuration-guide.md b/content/blog/fork-configuration-guide.md index e3d0dcd..ba5226f 100644 --- a/content/blog/fork-configuration-guide.md +++ b/content/blog/fork-configuration-guide.md @@ -309,6 +309,8 @@ Once configuration is complete: 3. **Test locally**: Run `npm run dev` and verify your site name, footer, and metadata 4. **Push to git**: Commit all changes and push to trigger a Netlify rebuild +**Important**: Keep your `fork-config.json` file. The `sync:discovery` and `sync:all` commands read from it to update discovery files (`AGENTS.md`, `CLAUDE.md`, `public/llms.txt`) with your configured values. Without it, these files would revert to placeholder values. + ## Existing content The configuration script only updates site-level settings. It does not modify your markdown content in `content/blog/` or `content/pages/`. Your existing posts and pages remain unchanged. diff --git a/content/blog/setup-guide.md b/content/blog/setup-guide.md index 4c56bd0..30c4a83 100644 --- a/content/blog/setup-guide.md +++ b/content/blog/setup-guide.md @@ -1536,7 +1536,7 @@ Content is stored in localStorage only and not synced to the database. Refreshin ## AI Agent chat -The site includes an AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled in two places: +The site includes an AI writing assistant (Agent) that supports multiple AI providers. Agent can be enabled in three places: **1. Write page (`/write`)** @@ -1566,11 +1566,39 @@ aiChat: true # Enable Agent in right sidebar --- ``` +**3. Dashboard AI Agent (`/dashboard`)** + +The Dashboard includes a dedicated AI Agent section with a tab-based UI for Chat and Image Generation. + +**Chat Tab features:** + +- Multi-model selector: Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash +- Per-session chat history stored in Convex +- Markdown rendering for AI responses +- Copy functionality for AI responses +- Lazy API key validation (errors only shown when user tries to use a specific model) + +**Image Tab features:** + +- AI image generation with two models: + - Nano Banana (gemini-2.0-flash-exp-image-generation) - Experimental model + - Nano Banana Pro (imagen-3.0-generate-002) - Production model +- Aspect ratio selection: 1:1, 16:9, 9:16, 4:3, 3:4 +- Images stored in Convex storage with session tracking +- Gallery view of recent generated images + **Environment variables:** -Agent requires the following Convex environment variables: +Agent requires API keys for the providers you want to use. Set these in Convex environment variables: + +| Variable | Provider | Features | +| --- | --- | --- | +| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | +| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | +| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | + +**Optional system prompt variables:** -- `ANTHROPIC_API_KEY` (required): Your Anthropic API key for Claude API access - `CLAUDE_PROMPT_STYLE` (optional): First part of system prompt - `CLAUDE_PROMPT_COMMUNITY` (optional): Second part of system prompt - `CLAUDE_PROMPT_RULES` (optional): Third part of system prompt @@ -1581,7 +1609,10 @@ Agent requires the following Convex environment variables: 1. Go to [Convex Dashboard](https://dashboard.convex.dev) 2. Select your project 3. Navigate to Settings > Environment Variables -4. Add `ANTHROPIC_API_KEY` with your API key value +4. Add API keys for the providers you want to use: + - `ANTHROPIC_API_KEY` for Claude + - `OPENAI_API_KEY` for GPT-4o + - `GOOGLE_AI_API_KEY` for Gemini and image generation 5. Optionally add system prompt variables (`CLAUDE_PROMPT_STYLE`, etc.) 6. Deploy changes @@ -1592,11 +1623,11 @@ Agent requires the following Convex environment variables: - Chat history is stored per-session, per-context in Convex (aiChats table) - Page content can be provided as context for AI responses - Chat history limited to last 20 messages for efficiency -- If API key is not set, Agent displays "API key is not set" error message +- API key validation is lazy: errors only appear when you try to use a specific model **Error handling:** -If `ANTHROPIC_API_KEY` is not configured in Convex environment variables, Agent displays a user-friendly error message: "API key is not set". This helps identify when the API key is missing in production deployments. +If an API key is not configured for a provider, Agent displays a user-friendly setup message with instructions when you try to use that model. Only configure the API keys for providers you want to use. ## Dashboard diff --git a/content/pages/about.md b/content/pages/about.md index be7811a..4b61e1e 100644 --- a/content/pages/about.md +++ b/content/pages/about.md @@ -3,10 +3,10 @@ title: "About" slug: "about" published: true order: 2 -excerpt: "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.." +excerpt: "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs." --- -An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. +An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. ## What makes it a dev sync system? diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index 44a94fc..bb72a52 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -10,6 +10,58 @@ layout: "sidebar" All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v2.6.0 + +Released January 1, 2026 + +**Multi-model AI chat and image generation in Dashboard** + +- AI Agent section with tab-based UI (Chat and Image Generation tabs) +- Multi-model selector for text chat + - Claude Sonnet 4 (Anthropic) + - GPT-4o (OpenAI) + - Gemini 2.0 Flash (Google) +- Lazy API key validation: errors only shown when user tries to use a specific model +- Each provider has friendly setup instructions with links to get API keys +- AI Image Generation tab + - Generate images using Gemini models (Nano Banana and Nano Banana Pro) + - Aspect ratio selector (1:1, 16:9, 9:16, 4:3, 3:4) + - Generated images stored in Convex storage with session tracking + - Markdown-rendered error messages with setup instructions +- New `aiDashboard` configuration in siteConfig + - `enableImageGeneration`: Toggle image generation tab + - `defaultTextModel`: Set default AI model for chat + - `textModels`: Configure available text chat models + - `imageModels`: Configure available image generation models + +**Technical details:** + +- New file: `convex/aiImageGeneration.ts` for Gemini image generation action +- New table: `aiGeneratedImages` in schema for tracking generated images +- Updated `convex/aiChatActions.ts` with multi-provider support + - Added `callAnthropicApi`, `callOpenAIApi`, `callGeminiApi` helper functions + - Added `getProviderFromModel` to determine provider from model ID + - Added `getApiKeyForProvider` for lazy API key retrieval + - Added `getNotConfiguredMessage` for provider-specific setup instructions +- Updated `src/components/AIChatView.tsx` with `selectedModel` prop +- Updated `src/pages/Dashboard.tsx` with new AI Agent section + - Tab-based UI for Chat and Image Generation + - Model dropdowns with provider labels + - Aspect ratio selector for image generation +- CSS styles for AI Agent section in `src/styles/global.css` + - `.ai-agent-tabs`, `.ai-agent-tab` for tab navigation + - `.ai-model-selector`, `.ai-model-dropdown` for model selection + - `.ai-aspect-ratio-selector` for aspect ratio options + - `.ai-generated-image`, `.ai-image-error`, `.ai-image-loading` for image display + +**Environment Variables:** + +- `ANTHROPIC_API_KEY`: Required for Claude models +- `OPENAI_API_KEY`: Required for GPT-4o +- `GOOGLE_AI_API_KEY`: Required for Gemini text chat and image generation + +Updated files: `convex/aiImageGeneration.ts`, `convex/aiChatActions.ts`, `convex/aiChats.ts`, `convex/schema.ts`, `src/components/AIChatView.tsx`, `src/pages/Dashboard.tsx`, `src/config/siteConfig.ts`, `src/styles/global.css`, `files.md`, `TASK.md`, `changelog.md`, `content/pages/changelog-page.md` + ## v2.5.0 Released January 1, 2026 diff --git a/content/pages/docs.md b/content/pages/docs.md index a3ac3b8..beb71bb 100644 --- a/content/pages/docs.md +++ b/content/pages/docs.md @@ -4,7 +4,7 @@ slug: "docs" published: true order: 0 layout: "sidebar" -aiChat: false +aiChat: true rightSidebar: true showFooter: true --- @@ -1096,11 +1096,34 @@ When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` ### AI Agent -- Dedicated AI chat section separate from the Write page -- Uses Anthropic Claude API (requires `ANTHROPIC_API_KEY` in Convex environment) +The Dashboard includes a dedicated AI Agent section with tab-based UI for Chat and Image Generation. + +**Chat Tab:** + +- Multi-model selector: Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash - Per-session chat history stored in Convex - Markdown rendering for AI responses - Copy functionality for AI responses +- Lazy API key validation (errors only shown when user tries to use a specific model) + +**Image Tab:** + +- AI image generation with two models: + - Nano Banana (gemini-2.0-flash-exp-image-generation) - Experimental model + - Nano Banana Pro (imagen-3.0-generate-002) - Production model +- Aspect ratio selection: 1:1, 16:9, 9:16, 4:3, 3:4 +- Images stored in Convex storage with session tracking +- Gallery view of recent generated images + +**Environment Variables (Convex):** + +| Variable | Description | +| --- | --- | +| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 | +| `OPENAI_API_KEY` | Required for GPT-4o | +| `GOOGLE_AI_API_KEY` | Required for Gemini 2.0 Flash and image generation | + +**Note:** Only configure the API keys for models you want to use. If a key is not set, users see a helpful setup message when they try to use that model. ### Newsletter Management diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index d2f141d..2ea446d 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -10,6 +10,7 @@ import type * as aiChatActions from "../aiChatActions.js"; import type * as aiChats from "../aiChats.js"; +import type * as aiImageGeneration from "../aiImageGeneration.js"; import type * as contact from "../contact.js"; import type * as contactActions from "../contactActions.js"; import type * as crons from "../crons.js"; @@ -31,6 +32,7 @@ import type { declare const fullApi: ApiFromModules<{ aiChatActions: typeof aiChatActions; aiChats: typeof aiChats; + aiImageGeneration: typeof aiImageGeneration; contact: typeof contact; contactActions: typeof contactActions; crons: typeof crons; diff --git a/convex/aiChatActions.ts b/convex/aiChatActions.ts index 76311f7..378a345 100644 --- a/convex/aiChatActions.ts +++ b/convex/aiChatActions.ts @@ -9,8 +9,20 @@ import type { TextBlockParam, ImageBlockParam, } from "@anthropic-ai/sdk/resources/messages/messages"; +import OpenAI from "openai"; +import { GoogleGenAI, Content } from "@google/genai"; import FirecrawlApp from "@mendable/firecrawl-js"; -import type { Id } from "./_generated/dataModel"; +import type { ChatCompletionMessageParam } from "openai/resources/chat/completions"; + +// Model validator for multi-model support +const modelValidator = v.union( + v.literal("claude-sonnet-4-20250514"), + v.literal("gpt-4o"), + v.literal("gemini-2.0-flash") +); + +// Type for model selection +type AIModel = "claude-sonnet-4-20250514" | "gpt-4o" | "gemini-2.0-flash"; // Default system prompt for writing assistant const DEFAULT_SYSTEM_PROMPT = `You are a helpful writing assistant. Help users write clearly and concisely. @@ -82,14 +94,229 @@ async function scrapeUrl(url: string): Promise<{ } } +/** + * Get provider from model ID + */ +function getProviderFromModel(model: AIModel): "anthropic" | "openai" | "google" { + if (model.startsWith("claude")) return "anthropic"; + if (model.startsWith("gpt")) return "openai"; + if (model.startsWith("gemini")) return "google"; + return "anthropic"; // Default fallback +} + +/** + * Get API key for a provider, returns null if not configured + */ +function getApiKeyForProvider(provider: "anthropic" | "openai" | "google"): string | null { + switch (provider) { + case "anthropic": + return process.env.ANTHROPIC_API_KEY || null; + case "openai": + return process.env.OPENAI_API_KEY || null; + case "google": + return process.env.GOOGLE_AI_API_KEY || null; + } +} + +/** + * Get not configured message for a provider + */ +function getNotConfiguredMessage(provider: "anthropic" | "openai" | "google"): string { + const configs = { + anthropic: { + name: "Claude (Anthropic)", + envVar: "ANTHROPIC_API_KEY", + consoleUrl: "https://console.anthropic.com/", + consoleName: "Anthropic Console", + }, + openai: { + name: "GPT (OpenAI)", + envVar: "OPENAI_API_KEY", + consoleUrl: "https://platform.openai.com/api-keys", + consoleName: "OpenAI Platform", + }, + google: { + name: "Gemini (Google)", + envVar: "GOOGLE_AI_API_KEY", + consoleUrl: "https://aistudio.google.com/apikey", + consoleName: "Google AI Studio", + }, + }; + + const config = configs[provider]; + return ( + `**${config.name} is not configured.**\n\n` + + `To enable this model, add your \`${config.envVar}\` to the Convex environment variables.\n\n` + + `**Setup steps:**\n` + + `1. Get an API key from [${config.consoleName}](${config.consoleUrl})\n` + + `2. Add it to Convex: \`npx convex env set ${config.envVar} your-key-here\`\n` + + `3. For production, set it in the [Convex Dashboard](https://dashboard.convex.dev/)\n\n` + + `See the [Convex environment variables docs](https://docs.convex.dev/production/environment-variables) for more details.` + ); +} + +/** + * Call Anthropic Claude API + */ +async function callAnthropicApi( + apiKey: string, + model: string, + systemPrompt: string, + messages: Array<{ + role: "user" | "assistant"; + content: string | Array; + }> +): Promise { + const anthropic = new Anthropic({ apiKey }); + + const response = await anthropic.messages.create({ + model, + max_tokens: 2048, + system: systemPrompt, + messages, + }); + + const textContent = response.content.find((block) => block.type === "text"); + if (!textContent || textContent.type !== "text") { + throw new Error("No text content in Claude response"); + } + + return textContent.text; +} + +/** + * Call OpenAI GPT API + */ +async function callOpenAIApi( + apiKey: string, + model: string, + systemPrompt: string, + messages: Array<{ + role: "user" | "assistant"; + content: string | Array; + }> +): Promise { + const openai = new OpenAI({ apiKey }); + + // Convert messages to OpenAI format + const openaiMessages: ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ]; + + for (const msg of messages) { + if (typeof msg.content === "string") { + if (msg.role === "user") { + openaiMessages.push({ role: "user", content: msg.content }); + } else { + openaiMessages.push({ role: "assistant", content: msg.content }); + } + } else { + // Convert content blocks to OpenAI format + const content: Array<{ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }> = []; + for (const block of msg.content) { + if (block.type === "text") { + content.push({ type: "text", text: block.text }); + } else if (block.type === "image" && "source" in block && block.source.type === "url") { + content.push({ type: "image_url", image_url: { url: block.source.url } }); + } + } + if (msg.role === "user") { + openaiMessages.push({ + role: "user", + content: content.length === 1 && content[0].type === "text" ? content[0].text : content, + }); + } else { + // Assistant messages only support string content in OpenAI + const textContent = content.filter(c => c.type === "text").map(c => (c as { type: "text"; text: string }).text).join("\n"); + openaiMessages.push({ role: "assistant", content: textContent }); + } + } + } + + const response = await openai.chat.completions.create({ + model, + max_tokens: 2048, + messages: openaiMessages, + }); + + const textContent = response.choices[0]?.message?.content; + if (!textContent) { + throw new Error("No text content in OpenAI response"); + } + + return textContent; +} + +/** + * Call Google Gemini API + */ +async function callGeminiApi( + apiKey: string, + model: string, + systemPrompt: string, + messages: Array<{ + role: "user" | "assistant"; + content: string | Array; + }> +): Promise { + const ai = new GoogleGenAI({ apiKey }); + + // Convert messages to Gemini format + const geminiMessages: Content[] = []; + + for (const msg of messages) { + const role = msg.role === "assistant" ? "model" : "user"; + + if (typeof msg.content === "string") { + geminiMessages.push({ + role, + parts: [{ text: msg.content }], + }); + } else { + // Convert content blocks to Gemini format + const parts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = []; + for (const block of msg.content) { + if (block.type === "text") { + parts.push({ text: block.text }); + } + // Note: Gemini handles images differently, would need base64 encoding + // For now, skip image blocks in Gemini + } + if (parts.length > 0) { + geminiMessages.push({ role, parts }); + } + } + } + + const response = await ai.models.generateContent({ + model, + contents: geminiMessages, + config: { + systemInstruction: systemPrompt, + maxOutputTokens: 2048, + }, + }); + + const textContent = response.candidates?.[0]?.content?.parts?.find( + (part: { text?: string }) => part.text + ); + + if (!textContent || !("text" in textContent)) { + throw new Error("No text content in Gemini response"); + } + + return textContent.text as string; +} + /** * Generate AI response for a chat - * Calls Claude API and saves the response + * Supports multiple AI providers: Anthropic, OpenAI, Google */ export const generateResponse = action({ args: { chatId: v.id("aiChats"), userMessage: v.string(), + model: v.optional(modelValidator), pageContext: v.optional(v.string()), attachments: v.optional( v.array( @@ -105,17 +332,14 @@ export const generateResponse = action({ }, returns: v.string(), handler: async (ctx, args) => { - // Get API key - return friendly message if not configured - const apiKey = process.env.ANTHROPIC_API_KEY; + // Use default model if not specified + const selectedModel: AIModel = args.model || "claude-sonnet-4-20250514"; + const provider = getProviderFromModel(selectedModel); + + // Get API key for the selected provider - lazy check only when model is used + const apiKey = getApiKeyForProvider(provider); if (!apiKey) { - const notConfiguredMessage = - "**AI chat is not configured on production.**\n\n" + - "To enable AI responses, add your `ANTHROPIC_API_KEY` to the Convex environment variables.\n\n" + - "**Setup steps:**\n" + - "1. Get an API key from [Anthropic Console](https://console.anthropic.com/)\n" + - "2. Add it to Convex: `npx convex env set ANTHROPIC_API_KEY your-key-here`\n" + - "3. For production, set it in the [Convex Dashboard](https://dashboard.convex.dev/)\n\n" + - "See the [Convex environment variables docs](https://docs.convex.dev/production/environment-variables) for more details."; + const notConfiguredMessage = getNotConfiguredMessage(provider); // Save the message to chat history so it appears in the conversation await ctx.runMutation(internal.aiChats.addAssistantMessage, { @@ -172,15 +396,15 @@ export const generateResponse = action({ // Build messages array from chat history (last 20 messages) const recentMessages = chat.messages.slice(-20); - const claudeMessages: Array<{ + const formattedMessages: Array<{ role: "user" | "assistant"; content: string | Array; }> = []; - // Convert chat messages to Claude format + // Convert chat messages to provider-agnostic format for (const msg of recentMessages) { if (msg.role === "assistant") { - claudeMessages.push({ + formattedMessages.push({ role: "assistant", content: msg.content, }); @@ -230,7 +454,7 @@ export const generateResponse = action({ } } - claudeMessages.push({ + formattedMessages.push({ role: "user", content: contentParts.length === 1 && contentParts[0].type === "text" @@ -282,7 +506,7 @@ export const generateResponse = action({ } } - claudeMessages.push({ + formattedMessages.push({ role: "user", content: newMessageContent.length === 1 && newMessageContent[0].type === "text" @@ -290,27 +514,26 @@ export const generateResponse = action({ : newMessageContent, }); - // Initialize Anthropic client - const anthropic = new Anthropic({ - apiKey, - }); + // Call the appropriate AI provider + let assistantMessage: string; - // Call Claude API - const response = await anthropic.messages.create({ - model: "claude-sonnet-4-20250514", - max_tokens: 2048, - system: systemPrompt, - messages: claudeMessages, - }); - - // Extract text content from response - const textContent = response.content.find((block) => block.type === "text"); - if (!textContent || textContent.type !== "text") { - throw new Error("No text content in Claude response"); + try { + switch (provider) { + case "anthropic": + assistantMessage = await callAnthropicApi(apiKey, selectedModel, systemPrompt, formattedMessages); + break; + case "openai": + assistantMessage = await callOpenAIApi(apiKey, selectedModel, systemPrompt, formattedMessages); + break; + case "google": + assistantMessage = await callGeminiApi(apiKey, selectedModel, systemPrompt, formattedMessages); + break; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + assistantMessage = `**Error from ${provider}:** ${errorMessage}`; } - const assistantMessage = textContent.text; - // Save the assistant message to the chat await ctx.runMutation(internal.aiChats.addAssistantMessage, { chatId: args.chatId, diff --git a/convex/aiChats.ts b/convex/aiChats.ts index 0ef7f4a..7db8595 100644 --- a/convex/aiChats.ts +++ b/convex/aiChats.ts @@ -354,3 +354,60 @@ export const getChatsBySession = query({ }, }); +/** + * Save generated image metadata (internal - called from action) + */ +export const saveGeneratedImage = internalMutation({ + args: { + sessionId: v.string(), + prompt: v.string(), + model: v.string(), + storageId: v.id("_storage"), + mimeType: v.string(), + }, + returns: v.id("aiGeneratedImages"), + handler: async (ctx, args) => { + const imageId = await ctx.db.insert("aiGeneratedImages", { + sessionId: args.sessionId, + prompt: args.prompt, + model: args.model, + storageId: args.storageId, + mimeType: args.mimeType, + createdAt: Date.now(), + }); + + return imageId; + }, +}); + +/** + * Get recent generated images for a session (internal - called from action) + */ +export const getRecentImagesInternal = internalQuery({ + args: { + sessionId: v.string(), + limit: v.number(), + }, + returns: v.array( + v.object({ + _id: v.id("aiGeneratedImages"), + _creationTime: v.number(), + sessionId: v.string(), + prompt: v.string(), + model: v.string(), + storageId: v.id("_storage"), + mimeType: v.string(), + createdAt: v.number(), + }) + ), + handler: async (ctx, args) => { + const images = await ctx.db + .query("aiGeneratedImages") + .withIndex("by_session", (q) => q.eq("sessionId", args.sessionId)) + .order("desc") + .take(args.limit); + + return images; + }, +}); + diff --git a/convex/aiImageGeneration.ts b/convex/aiImageGeneration.ts new file mode 100644 index 0000000..0493822 --- /dev/null +++ b/convex/aiImageGeneration.ts @@ -0,0 +1,230 @@ +"use node"; + +import { v } from "convex/values"; +import type { Id } from "./_generated/dataModel"; +import { action } from "./_generated/server"; +import { internal } from "./_generated/api"; + +// Type for images returned from internal query +type GeneratedImageRecord = { + _id: Id<"aiGeneratedImages">; + _creationTime: number; + sessionId: string; + prompt: string; + model: string; + storageId: Id<"_storage">; + mimeType: string; + createdAt: number; +}; +import { GoogleGenAI } from "@google/genai"; + +// Image model validator +const imageModelValidator = v.union( + v.literal("gemini-2.0-flash-exp-image-generation"), + v.literal("imagen-3.0-generate-002") +); + +// Aspect ratio validator +const aspectRatioValidator = v.union( + v.literal("1:1"), + v.literal("16:9"), + v.literal("9:16"), + v.literal("4:3"), + v.literal("3:4") +); + +/** + * Generate an image using Gemini's image generation API + * Stores the result in Convex storage and returns metadata + */ +export const generateImage = action({ + args: { + sessionId: v.string(), + prompt: v.string(), + model: imageModelValidator, + aspectRatio: v.optional(aspectRatioValidator), + }, + returns: v.object({ + success: v.boolean(), + storageId: v.optional(v.id("_storage")), + url: v.optional(v.string()), + error: v.optional(v.string()), + }), + handler: async (ctx, args) => { + // Check for API key - return friendly error if not configured + const apiKey = process.env.GOOGLE_AI_API_KEY; + if (!apiKey) { + return { + success: false, + error: + "**Gemini Image Generation is not configured.**\n\n" + + "To use image generation, add your `GOOGLE_AI_API_KEY` to the Convex environment variables.\n\n" + + "**Setup steps:**\n" + + "1. Get an API key from [Google AI Studio](https://aistudio.google.com/apikey)\n" + + "2. Add it to Convex: `npx convex env set GOOGLE_AI_API_KEY your-key-here`\n" + + "3. For production, set it in the [Convex Dashboard](https://dashboard.convex.dev/)\n\n" + + "See the [Convex environment variables docs](https://docs.convex.dev/production/environment-variables) for more details.", + }; + } + + try { + const ai = new GoogleGenAI({ apiKey }); + + // Configure generation based on model + let imageBytes: Uint8Array; + let mimeType = "image/png"; + + if (args.model === "gemini-2.0-flash-exp-image-generation") { + // Gemini Flash experimental image generation + const response = await ai.models.generateContent({ + model: args.model, + contents: [{ role: "user", parts: [{ text: args.prompt }] }], + config: { + responseModalities: ["image", "text"], + }, + }); + + // Extract image from response + const parts = response.candidates?.[0]?.content?.parts; + const imagePart = parts?.find( + (part) => { + const inlineData = part.inlineData as { mimeType?: string; data?: string } | undefined; + return inlineData?.mimeType?.startsWith("image/"); + } + ); + + const inlineData = imagePart?.inlineData as { mimeType?: string; data?: string } | undefined; + if (!imagePart || !inlineData || !inlineData.mimeType || !inlineData.data) { + return { + success: false, + error: "No image was generated. Try a different prompt.", + }; + } + + mimeType = inlineData.mimeType; + imageBytes = base64ToBytes(inlineData.data); + } else { + // Imagen 3.0 model + const response = await ai.models.generateImages({ + model: args.model, + prompt: args.prompt, + config: { + numberOfImages: 1, + aspectRatio: args.aspectRatio || "1:1", + }, + }); + + const image = response.generatedImages?.[0]; + if (!image || !image.image?.imageBytes) { + return { + success: false, + error: "No image was generated. Try a different prompt.", + }; + } + + mimeType = "image/png"; + imageBytes = base64ToBytes(image.image.imageBytes); + } + + // Store the image in Convex storage + const blob = new Blob([imageBytes as BlobPart], { type: mimeType }); + const storageId = await ctx.storage.store(blob); + + // Get the URL for the stored image + const url = await ctx.storage.getUrl(storageId); + + // Save metadata to database + await ctx.runMutation(internal.aiChats.saveGeneratedImage, { + sessionId: args.sessionId, + prompt: args.prompt, + model: args.model, + storageId, + mimeType, + }); + + return { + success: true, + storageId, + url: url || undefined, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + + // Check for specific API errors + if (errorMessage.includes("quota") || errorMessage.includes("rate")) { + return { + success: false, + error: "**Rate limit exceeded.** Please try again in a few moments.", + }; + } + + if (errorMessage.includes("safety") || errorMessage.includes("blocked")) { + return { + success: false, + error: "**Image generation blocked.** The prompt may have triggered content safety filters. Try rephrasing your prompt.", + }; + } + + return { + success: false, + error: `**Image generation failed:** ${errorMessage}`, + }; + } + }, +}); + +/** + * Get recent generated images for a session + */ +export const getRecentImages = action({ + args: { + sessionId: v.string(), + limit: v.optional(v.number()), + }, + returns: v.array( + v.object({ + _id: v.id("aiGeneratedImages"), + prompt: v.string(), + model: v.string(), + url: v.union(v.string(), v.null()), + createdAt: v.number(), + }) + ), + handler: async (ctx, args): Promise; + prompt: string; + model: string; + url: string | null; + createdAt: number; + }>> => { + const images: GeneratedImageRecord[] = await ctx.runQuery(internal.aiChats.getRecentImagesInternal, { + sessionId: args.sessionId, + limit: args.limit || 10, + }); + + // Get URLs for each image + const imagesWithUrls = await Promise.all( + images.map(async (image: GeneratedImageRecord) => ({ + _id: image._id, + prompt: image.prompt, + model: image.model, + url: await ctx.storage.getUrl(image.storageId), + createdAt: image.createdAt, + })) + ); + + return imagesWithUrls; + }, +}); + +/** + * Helper to convert base64 string to Uint8Array + */ +function base64ToBytes(base64: string): Uint8Array { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; +} diff --git a/convex/http.ts b/convex/http.ts index 67a3a0b..d70f4ad 100644 --- a/convex/http.ts +++ b/convex/http.ts @@ -5,7 +5,7 @@ import { rssFeed, rssFullFeed } from "./rss"; const http = httpRouter(); -// Site configuration +// Site configuration - update these for your site (or run npm run configure) const SITE_URL = process.env.SITE_URL || "https://www.markdown.fast"; const SITE_NAME = "markdown sync framework"; @@ -100,7 +100,7 @@ http.route({ site: SITE_NAME, url: SITE_URL, description: - "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify.", + "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify.", posts: posts.map((post: { title: string; slug: string; description: string; date: string; readTime?: string; tags: string[] }) => ({ title: post.title, slug: post.slug, @@ -223,7 +223,7 @@ http.route({ site: SITE_NAME, url: SITE_URL, description: - "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify.", + "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify.", exportedAt: new Date().toISOString(), totalPosts: fullPosts.length, posts: fullPosts, diff --git a/convex/rss.ts b/convex/rss.ts index 44775d0..a3d1601 100644 --- a/convex/rss.ts +++ b/convex/rss.ts @@ -1,11 +1,11 @@ import { httpAction } from "./_generated/server"; import { api } from "./_generated/api"; -// Site configuration for RSS feed +// Site configuration for RSS feed - update these for your site (or run npm run configure) const SITE_URL = process.env.SITE_URL || "https://www.markdown.fast"; const SITE_TITLE = "markdown sync framework"; const SITE_DESCRIPTION = - "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify."; + "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify."; // Escape XML special characters function escapeXml(text: string): string { diff --git a/convex/schema.ts b/convex/schema.ts index 6a7073f..531e8a8 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -149,6 +149,18 @@ export default defineSchema({ .index("by_session_and_context", ["sessionId", "contextId"]) .index("by_session", ["sessionId"]), + // AI generated images from Gemini image generation + aiGeneratedImages: defineTable({ + sessionId: v.string(), // Anonymous session ID from localStorage + prompt: v.string(), // User's image prompt + model: v.string(), // Model used: "gemini-2.5-flash-image" or "gemini-3-pro-image-preview" + storageId: v.id("_storage"), // Convex storage ID for the generated image + mimeType: v.string(), // Image MIME type: "image/png" or "image/jpeg" + createdAt: v.number(), // Timestamp when image was generated + }) + .index("by_session", ["sessionId"]) + .index("by_createdAt", ["createdAt"]), + // Newsletter subscribers table // Stores email subscriptions with unsubscribe tokens newsletterSubscribers: defineTable({ diff --git a/files.md b/files.md index c37a255..8c7af7f 100644 --- a/files.md +++ b/files.md @@ -35,7 +35,7 @@ A brief description of each file in the codebase. | File | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, featured section with configurable title via featuredTitle, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration, footer configuration with markdown support, social footer configuration, homepage configuration, AI chat configuration, newsletter configuration with admin and notifications, contact form configuration, weekly digest configuration, stats page configuration with public/private toggle, dashboard configuration with optional WorkOS authentication via requireAuth, image lightbox configuration with enabled toggle) | +| `siteConfig.ts` | Centralized site configuration (name, logo, blog page, posts display with homepage post limit and read more link, featured section with configurable title via featuredTitle, GitHub contributions, nav order, inner page logo settings, hardcoded navigation items for React routes, GitHub repository config for AI service raw URLs, font family configuration, right sidebar configuration, footer configuration with markdown support, social footer configuration, homepage configuration, AI chat configuration, aiDashboard configuration with multi-model support for text chat and image generation, newsletter configuration with admin and notifications, contact form configuration, weekly digest configuration, stats page configuration with public/private toggle, dashboard configuration with optional WorkOS authentication via requireAuth, image lightbox configuration with enabled toggle) | ### Pages (`src/pages/`) @@ -48,7 +48,7 @@ A brief description of each file in the codebase. | `TagPage.tsx` | Tag archive page displaying posts filtered by a specific tag. Includes view mode toggle (list/cards) with localStorage persistence | | `AuthorPage.tsx` | Author archive page displaying posts by a specific author. Includes view mode toggle (list/cards) with localStorage persistence. Author name clickable in posts links to this page. | | `Write.tsx` | Three-column markdown writing page with Cursor docs-style UI, frontmatter reference with copy buttons, theme toggle, font switcher (serif/sans/monospace), localStorage persistence, and optional AI Agent mode (toggleable via siteConfig.aiChat.enabledOnWritePage). When enabled, Agent replaces the textarea with AIChatView component. Includes scroll prevention when switching to Agent mode to prevent page jump. Title changes to "Agent" when in AI chat mode. | -| `Dashboard.tsx` | Centralized dashboard at `/dashboard` for content management and site configuration. Features include: Posts and Pages list views with filtering, search, pagination, items per page selector; Post/Page editor with markdown editor, live preview, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown; Write Post/Page sections with full-screen writing interface; AI Agent section (dedicated chat separate from Write page); Newsletter management (all Newsletter Admin features integrated); Content import (Firecrawl UI); Site configuration (Config Generator UI for all siteConfig.ts settings); Index HTML editor; Analytics (real-time stats dashboard); Sync commands UI with buttons for all sync operations; Header sync buttons for quick sync; Dashboard search; Toast notifications; Command modal; Mobile responsive design. Uses Convex queries for real-time data, localStorage for preferences, ReactMarkdown for preview. Optional WorkOS authentication via siteConfig.dashboard.requireAuth. When requireAuth is false, dashboard is open access. When requireAuth is true and WorkOS is configured, dashboard requires login. Shows setup instructions if requireAuth is true but WorkOS is not configured. | +| `Dashboard.tsx` | Centralized dashboard at `/dashboard` for content management and site configuration. Features include: Posts and Pages list views with filtering, search, pagination, items per page selector; Post/Page editor with markdown editor, live preview, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown; Write Post/Page sections with full-screen writing interface; AI Agent section with tab-based UI for Chat and Image Generation, multi-model selector (Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash), image generation with Nano Banana models and aspect ratio selection; Newsletter management (all Newsletter Admin features integrated); Content import (Firecrawl UI); Site configuration (Config Generator UI for all siteConfig.ts settings); Index HTML editor; Analytics (real-time stats dashboard); Sync commands UI with buttons for all sync operations; Header sync buttons for quick sync; Dashboard search; Toast notifications; Command modal; Mobile responsive design. Uses Convex queries for real-time data, localStorage for preferences, ReactMarkdown for preview. Optional WorkOS authentication via siteConfig.dashboard.requireAuth. When requireAuth is false, dashboard is open access. When requireAuth is true and WorkOS is configured, dashboard requires login. Shows setup instructions if requireAuth is true but WorkOS is not configured. | | `Callback.tsx` | OAuth callback handler for WorkOS authentication. Handles redirect from WorkOS after user login, exchanges authorization code for user information, then redirects to dashboard. Only used when WorkOS is configured. | | `NewsletterAdmin.tsx` | Three-column newsletter admin page for managing subscribers and sending newsletters. Left sidebar with navigation and stats, main area with searchable subscriber list, right sidebar with send newsletter panel and recent sends. Access at /newsletter-admin, configurable via siteConfig.newsletterAdmin. | @@ -118,7 +118,8 @@ A brief description of each file in the codebase. | `rss.ts` | RSS feed generation (update SITE_URL/SITE_TITLE when forking, uses www.markdown.fast) | | `auth.config.ts` | Convex authentication configuration for WorkOS. Defines JWT providers for WorkOS API and user management. Requires WORKOS_CLIENT_ID environment variable in Convex. Optional - only needed if using WorkOS authentication for dashboard. | | `aiChats.ts` | Queries and mutations for AI chat history (per-session, per-context storage). Handles anonymous session IDs, per-page chat contexts, and message history management. Supports page content as context for AI responses. | -| `aiChatActions.ts` | Anthropic Claude API integration action for AI chat responses. Requires ANTHROPIC_API_KEY environment variable in Convex. Uses claude-sonnet-3-5-20240620 model. System prompt configurable via environment variables (CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES, or CLAUDE_SYSTEM_PROMPT). Includes error handling for missing API keys with user-friendly error messages. Supports page content context and chat history (last 20 messages). | +| `aiChatActions.ts` | Multi-provider AI chat action supporting Anthropic (Claude Sonnet 4), OpenAI (GPT-4o), and Google (Gemini 2.0 Flash). Requires respective API keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY. Lazy API key validation only shows errors when user attempts to use a specific model. System prompt configurable via environment variables. Supports page content context and chat history (last 20 messages). | +| `aiImageGeneration.ts` | Gemini image generation action using Google AI API. Supports gemini-2.0-flash-exp-image-generation (Nano Banana) and imagen-3.0-generate-002 (Nano Banana Pro) models. Features aspect ratio selection (1:1, 16:9, 9:16, 4:3, 3:4), Convex storage integration, and session-based image tracking. Requires GOOGLE_AI_API_KEY environment variable. | | `newsletter.ts` | Newsletter mutations and queries: subscribe, unsubscribe, getSubscriberCount, getActiveSubscribers, getAllSubscribers (admin), deleteSubscriber (admin), getNewsletterStats, getPostsForNewsletter, wasPostSent, recordPostSent, scheduleSendPostNewsletter, scheduleSendCustomNewsletter, scheduleSendStatsSummary, getStatsForSummary. | | `newsletterActions.ts` | Newsletter actions (Node.js runtime): sendPostNewsletter, sendCustomNewsletter, sendWeeklyDigest, notifyNewSubscriber, sendWeeklyStatsSummary. Uses AgentMail SDK for email delivery. Includes markdown-to-HTML conversion for custom emails. | | `contact.ts` | Contact form mutations and actions: submitContact, sendContactEmail (AgentMail API), markEmailSent. | diff --git a/fork-config.json.example b/fork-config.json.example index 5bbe596..52462bf 100644 --- a/fork-config.json.example +++ b/fork-config.json.example @@ -104,6 +104,19 @@ "enabledOnWritePage": false, "enabledOnContent": false }, + "aiDashboard": { + "enableImageGeneration": true, + "defaultTextModel": "claude-sonnet-4-20250514", + "textModels": [ + { "id": "claude-sonnet-4-20250514", "name": "Claude Sonnet 4", "provider": "anthropic" }, + { "id": "gpt-4o", "name": "GPT-4o", "provider": "openai" }, + { "id": "gemini-2.0-flash", "name": "Gemini 2.0 Flash", "provider": "google" } + ], + "imageModels": [ + { "id": "gemini-2.0-flash-exp-image-generation", "name": "Nano Banana", "provider": "google" }, + { "id": "imagen-3.0-generate-002", "name": "Nano Banana Pro", "provider": "google" } + ] + }, "newsletter": { "enabled": false, "agentmail": { diff --git a/index.html b/index.html index d460183..5b2d396 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ @@ -48,7 +48,7 @@ =20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, "node_modules/@hono/node-server": { "version": "1.19.7", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", @@ -1064,6 +1087,50 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1265,6 +1332,16 @@ "react-dom": ">= 16.8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@quansync/fs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", @@ -3466,12 +3543,21 @@ "version": "25.0.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -3882,6 +3968,18 @@ "react-dom": ">=18" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -3943,6 +4041,27 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/agentmail": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/agentmail/-/agentmail-0.1.15.tgz", @@ -4014,7 +4133,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4024,7 +4142,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4130,7 +4247,26 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -4143,6 +4279,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -4186,7 +4331,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4239,6 +4383,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4444,7 +4594,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4457,7 +4606,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -4602,6 +4750,15 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/date-fns": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", @@ -4792,6 +4949,21 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4805,6 +4977,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -5194,6 +5372,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -5414,6 +5601,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5520,6 +5730,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -5536,6 +5762,12 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -5544,6 +5776,40 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5592,6 +5858,70 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5756,6 +6086,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5812,6 +6169,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6156,6 +6526,28 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", @@ -6295,6 +6687,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6377,6 +6778,21 @@ "ws": "*" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -6426,6 +6842,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6479,6 +6904,27 @@ "node": ">=6" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7553,7 +7999,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -7565,6 +8010,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7605,6 +8059,44 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -7654,6 +8146,71 @@ "wrappy": "1" } }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7704,6 +8261,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7792,6 +8355,28 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -8740,6 +9325,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -8955,6 +9560,18 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -9011,6 +9628,71 @@ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -9029,7 +9711,19 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9225,6 +9919,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -9929,7 +10629,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "devOptional": true, "license": "MIT" }, "node_modules/unified": { @@ -10772,6 +11471,31 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10813,6 +11537,100 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 4ee581b..d6984fb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.71.2", + "@google/genai": "^1.0.1", "@convex-dev/aggregate": "^0.2.0", "@convex-dev/workos": "^0.0.1", "@mendable/firecrawl-js": "^1.21.1", diff --git a/public/raw/about.md b/public/raw/about.md index f62be4f..2e7aaae 100644 --- a/public/raw/about.md +++ b/public/raw/about.md @@ -5,7 +5,7 @@ Type: page Date: 2026-01-02 --- -An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. +An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. ## What makes it a dev sync system? diff --git a/public/raw/changelog.md b/public/raw/changelog.md index 390cc50..f8ea049 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -8,6 +8,58 @@ Date: 2026-01-02 All notable changes to this project. ![](https://img.shields.io/badge/License-MIT-yellow.svg) +## v2.6.0 + +Released January 1, 2026 + +**Multi-model AI chat and image generation in Dashboard** + +- AI Agent section with tab-based UI (Chat and Image Generation tabs) +- Multi-model selector for text chat + - Claude Sonnet 4 (Anthropic) + - GPT-4o (OpenAI) + - Gemini 2.0 Flash (Google) +- Lazy API key validation: errors only shown when user tries to use a specific model +- Each provider has friendly setup instructions with links to get API keys +- AI Image Generation tab + - Generate images using Gemini models (Nano Banana and Nano Banana Pro) + - Aspect ratio selector (1:1, 16:9, 9:16, 4:3, 3:4) + - Generated images stored in Convex storage with session tracking + - Markdown-rendered error messages with setup instructions +- New `aiDashboard` configuration in siteConfig + - `enableImageGeneration`: Toggle image generation tab + - `defaultTextModel`: Set default AI model for chat + - `textModels`: Configure available text chat models + - `imageModels`: Configure available image generation models + +**Technical details:** + +- New file: `convex/aiImageGeneration.ts` for Gemini image generation action +- New table: `aiGeneratedImages` in schema for tracking generated images +- Updated `convex/aiChatActions.ts` with multi-provider support + - Added `callAnthropicApi`, `callOpenAIApi`, `callGeminiApi` helper functions + - Added `getProviderFromModel` to determine provider from model ID + - Added `getApiKeyForProvider` for lazy API key retrieval + - Added `getNotConfiguredMessage` for provider-specific setup instructions +- Updated `src/components/AIChatView.tsx` with `selectedModel` prop +- Updated `src/pages/Dashboard.tsx` with new AI Agent section + - Tab-based UI for Chat and Image Generation + - Model dropdowns with provider labels + - Aspect ratio selector for image generation +- CSS styles for AI Agent section in `src/styles/global.css` + - `.ai-agent-tabs`, `.ai-agent-tab` for tab navigation + - `.ai-model-selector`, `.ai-model-dropdown` for model selection + - `.ai-aspect-ratio-selector` for aspect ratio options + - `.ai-generated-image`, `.ai-image-error`, `.ai-image-loading` for image display + +**Environment Variables:** + +- `ANTHROPIC_API_KEY`: Required for Claude models +- `OPENAI_API_KEY`: Required for GPT-4o +- `GOOGLE_AI_API_KEY`: Required for Gemini text chat and image generation + +Updated files: `convex/aiImageGeneration.ts`, `convex/aiChatActions.ts`, `convex/aiChats.ts`, `convex/schema.ts`, `src/components/AIChatView.tsx`, `src/pages/Dashboard.tsx`, `src/config/siteConfig.ts`, `src/styles/global.css`, `files.md`, `TASK.md`, `changelog.md`, `content/pages/changelog-page.md` + ## v2.5.0 Released January 1, 2026 diff --git a/public/raw/docs.md b/public/raw/docs.md index ec1f2d1..62fea4e 100644 --- a/public/raw/docs.md +++ b/public/raw/docs.md @@ -1092,11 +1092,34 @@ When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` ### AI Agent -- Dedicated AI chat section separate from the Write page -- Uses Anthropic Claude API (requires `ANTHROPIC_API_KEY` in Convex environment) +The Dashboard includes a dedicated AI Agent section with tab-based UI for Chat and Image Generation. + +**Chat Tab:** + +- Multi-model selector: Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash - Per-session chat history stored in Convex - Markdown rendering for AI responses - Copy functionality for AI responses +- Lazy API key validation (errors only shown when user tries to use a specific model) + +**Image Tab:** + +- AI image generation with two models: + - Nano Banana (gemini-2.0-flash-exp-image-generation) - Experimental model + - Nano Banana Pro (imagen-3.0-generate-002) - Production model +- Aspect ratio selection: 1:1, 16:9, 9:16, 4:3, 3:4 +- Images stored in Convex storage with session tracking +- Gallery view of recent generated images + +**Environment Variables (Convex):** + +| Variable | Description | +| --- | --- | +| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 | +| `OPENAI_API_KEY` | Required for GPT-4o | +| `GOOGLE_AI_API_KEY` | Required for Gemini 2.0 Flash and image generation | + +**Note:** Only configure the API keys for models you want to use. If a key is not set, users see a helpful setup message when they try to use that model. ### Newsletter Management diff --git a/public/raw/fork-configuration-guide.md b/public/raw/fork-configuration-guide.md index 319170f..f9a9ef8 100644 --- a/public/raw/fork-configuration-guide.md +++ b/public/raw/fork-configuration-guide.md @@ -303,6 +303,8 @@ Once configuration is complete: 3. **Test locally**: Run `npm run dev` and verify your site name, footer, and metadata 4. **Push to git**: Commit all changes and push to trigger a Netlify rebuild +**Important**: Keep your `fork-config.json` file. The `sync:discovery` and `sync:all` commands read from it to update discovery files (`AGENTS.md`, `CLAUDE.md`, `public/llms.txt`) with your configured values. Without it, these files would revert to placeholder values. + ## Existing content The configuration script only updates site-level settings. It does not modify your markdown content in `content/blog/` or `content/pages/`. Your existing posts and pages remain unchanged. diff --git a/public/raw/index.md b/public/raw/index.md index c845280..f7d183d 100644 --- a/public/raw/index.md +++ b/public/raw/index.md @@ -46,7 +46,7 @@ This is the homepage index of all published content. - **[Footer](/raw/footer.md)** - **[Home Intro](/raw/home-intro.md)** - **[Docs](/raw/docs.md)** -- **[About](/raw/about.md)** - An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs.. +- **[About](/raw/about.md)** - An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. - **[Projects](/raw/projects.md)** - **[Contact](/raw/contact.md)** - **[Changelog](/raw/changelog.md)** diff --git a/public/raw/setup-guide.md b/public/raw/setup-guide.md index fb90a0e..f53127c 100644 --- a/public/raw/setup-guide.md +++ b/public/raw/setup-guide.md @@ -1529,7 +1529,7 @@ Content is stored in localStorage only and not synced to the database. Refreshin ## AI Agent chat -The site includes an AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled in two places: +The site includes an AI writing assistant (Agent) that supports multiple AI providers. Agent can be enabled in three places: **1. Write page (`/write`)** @@ -1559,11 +1559,39 @@ aiChat: true # Enable Agent in right sidebar --- ``` +**3. Dashboard AI Agent (`/dashboard`)** + +The Dashboard includes a dedicated AI Agent section with a tab-based UI for Chat and Image Generation. + +**Chat Tab features:** + +- Multi-model selector: Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash +- Per-session chat history stored in Convex +- Markdown rendering for AI responses +- Copy functionality for AI responses +- Lazy API key validation (errors only shown when user tries to use a specific model) + +**Image Tab features:** + +- AI image generation with two models: + - Nano Banana (gemini-2.0-flash-exp-image-generation) - Experimental model + - Nano Banana Pro (imagen-3.0-generate-002) - Production model +- Aspect ratio selection: 1:1, 16:9, 9:16, 4:3, 3:4 +- Images stored in Convex storage with session tracking +- Gallery view of recent generated images + **Environment variables:** -Agent requires the following Convex environment variables: +Agent requires API keys for the providers you want to use. Set these in Convex environment variables: + +| Variable | Provider | Features | +| --- | --- | --- | +| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | +| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | +| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | + +**Optional system prompt variables:** -- `ANTHROPIC_API_KEY` (required): Your Anthropic API key for Claude API access - `CLAUDE_PROMPT_STYLE` (optional): First part of system prompt - `CLAUDE_PROMPT_COMMUNITY` (optional): Second part of system prompt - `CLAUDE_PROMPT_RULES` (optional): Third part of system prompt @@ -1574,7 +1602,10 @@ Agent requires the following Convex environment variables: 1. Go to [Convex Dashboard](https://dashboard.convex.dev) 2. Select your project 3. Navigate to Settings > Environment Variables -4. Add `ANTHROPIC_API_KEY` with your API key value +4. Add API keys for the providers you want to use: + - `ANTHROPIC_API_KEY` for Claude + - `OPENAI_API_KEY` for GPT-4o + - `GOOGLE_AI_API_KEY` for Gemini and image generation 5. Optionally add system prompt variables (`CLAUDE_PROMPT_STYLE`, etc.) 6. Deploy changes @@ -1585,11 +1616,11 @@ Agent requires the following Convex environment variables: - Chat history is stored per-session, per-context in Convex (aiChats table) - Page content can be provided as context for AI responses - Chat history limited to last 20 messages for efficiency -- If API key is not set, Agent displays "API key is not set" error message +- API key validation is lazy: errors only appear when you try to use a specific model **Error handling:** -If `ANTHROPIC_API_KEY` is not configured in Convex environment variables, Agent displays a user-friendly error message: "API key is not set". This helps identify when the API key is missing in production deployments. +If an API key is not configured for a provider, Agent displays a user-friendly setup message with instructions when you try to use that model. Only configure the API keys for providers you want to use. ## Dashboard diff --git a/scripts/configure-fork.ts b/scripts/configure-fork.ts index 85a70cf..ae13163 100644 --- a/scripts/configure-fork.ts +++ b/scripts/configure-fork.ts @@ -440,12 +440,14 @@ function updatePostTsx(config: ForkConfig): void { console.log("\nUpdating src/pages/Post.tsx..."); updateFile("src/pages/Post.tsx", [ + // Match any existing SITE_URL value (https://...) { - search: /const SITE_URL = "https:\/\/markdowncms\.netlify\.app";/, + search: /const SITE_URL = "https:\/\/[^"]+";/, replace: `const SITE_URL = "${config.siteUrl}";`, }, + // Match any existing SITE_NAME value { - search: /const SITE_NAME = "markdown sync framework";/, + search: /const SITE_NAME = "[^"]+";/, replace: `const SITE_NAME = "${config.siteName}";`, }, ]); @@ -456,22 +458,31 @@ function updateConvexHttp(config: ForkConfig): void { console.log("\nUpdating convex/http.ts..."); updateFile("convex/http.ts", [ + // Match any existing SITE_URL value with process.env fallback { - search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/markdowncms\.netlify\.app";/, + search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/[^"]+";/, replace: `const SITE_URL = process.env.SITE_URL || "${config.siteUrl}";`, }, + // Match any existing SITE_NAME value (line 10) { - search: /const SITE_NAME = "markdown sync framework";/, + search: /const SITE_NAME = "[^"]+";/, replace: `const SITE_NAME = "${config.siteName}";`, }, + // Match any existing siteUrl in generateMetaHtml function { - search: /const siteUrl = process\.env\.SITE_URL \|\| "https:\/\/markdowncms\.netlify\.app";/, + search: /const siteUrl = process\.env\.SITE_URL \|\| "https:\/\/[^"]+";/, replace: `const siteUrl = process.env.SITE_URL || "${config.siteUrl}";`, }, + // Match any existing siteName in generateMetaHtml function { - search: /const siteName = "markdown sync framework";/, + search: /const siteName = "[^"]+";/, replace: `const siteName = "${config.siteName}";`, }, + // Update the description in API responses + { + search: /"An open-source publishing framework[^"]*"/g, + replace: `"${config.siteDescription}"`, + }, ]); } @@ -480,14 +491,17 @@ function updateConvexRss(config: ForkConfig): void { console.log("\nUpdating convex/rss.ts..."); updateFile("convex/rss.ts", [ + // Match any existing SITE_URL value with process.env fallback { - search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/markdowncms\.netlify\.app";/, + search: /const SITE_URL = process\.env\.SITE_URL \|\| "https:\/\/[^"]+";/, replace: `const SITE_URL = process.env.SITE_URL || "${config.siteUrl}";`, }, + // Match any existing SITE_TITLE value { - search: /const SITE_TITLE = "markdown sync framework";/, + search: /const SITE_TITLE = "[^"]+";/, replace: `const SITE_TITLE = "${config.siteName}";`, }, + // Match any existing SITE_DESCRIPTION value (multiline) { search: /const SITE_DESCRIPTION =\s*"[^"]+";/, replace: `const SITE_DESCRIPTION =\n "${config.siteDescription}";`, @@ -500,89 +514,94 @@ function updateIndexHtml(config: ForkConfig): void { console.log("\nUpdating index.html..."); const replacements: Array<{ search: string | RegExp; replace: string }> = [ - // Meta description + // Meta description (match any content) { search: //, replace: ``, }, - // Meta author + // Meta author (match any content) { search: //, replace: ``, }, - // Open Graph title + // Open Graph title (match any content) { search: //, replace: ``, }, - // Open Graph description + // Open Graph description (match any content) { search: //, replace: ``, }, - // Open Graph URL + // Open Graph URL (match any https URL) { - search: //, + search: //, replace: ``, }, - // Open Graph site name + // Open Graph site name (match any content) { - search: //, + search: //, replace: ``, }, - // Open Graph image + // Open Graph site name with newline formatting { - search: //, - replace: ``, + search: //, + replace: ``, }, - // Twitter domain + // Open Graph image (match any https URL) + { + search: //, + replace: ``, + }, + // Twitter domain (match any domain) { search: //, replace: ``, }, - // Twitter URL + // Twitter URL (match any https URL) { - search: //, + search: //, replace: ``, }, - // Twitter title + // Twitter title (match any content) { search: //, replace: ``, }, - // Twitter description + // Twitter description (match any content) { search: //, replace: ``, }, - // Twitter image + // Twitter image (match any https URL) { - search: //, - replace: ``, + search: //, + replace: ``, }, - // JSON-LD name + // JSON-LD name (match any value) { - search: /"name": "markdown sync framework"/g, - replace: `"name": "${config.siteName}"`, + search: /"name": "[^"]+",\s*\n\s*"url":/g, + replace: `"name": "${config.siteName}",\n "url":`, }, - // JSON-LD URL + // JSON-LD URL (match any https URL) { - search: /"url": "https:\/\/markdowncms\.netlify\.app"/g, + search: /"url": "https:\/\/[^"]+"/g, replace: `"url": "${config.siteUrl}"`, }, - // JSON-LD description + // JSON-LD description (match any content) { - search: /"description": "An open-source publishing framework[^"]*"/, + search: /"description": "[^"]+"/, replace: `"description": "${config.siteDescription}"`, }, - // JSON-LD search target + // JSON-LD search target (match any URL) { - search: /"target": "https:\/\/markdowncms\.netlify\.app\/\?q=\{search_term_string\}"/, + search: /"target": "https:\/\/[^"]+\/\?q=\{search_term_string\}"/, replace: `"target": "${config.siteUrl}/?q={search_term_string}"`, }, - // Page title + // Page title (match any title content) { - search: /markdown "sync" framework<\/title>/, + search: /<title>[^<]+<\/title>/, replace: `<title>${config.siteTitle}`, }, ]; @@ -733,25 +752,30 @@ function updateOpenApiYaml(config: ForkConfig): void { const githubUrl = `https://github.com/${config.githubUsername}/${config.githubRepo}`; updateFile("public/openapi.yaml", [ + // Match any title ending with API { - search: /title: markdown sync framework API/, + search: /title: .+ API/, replace: `title: ${config.siteName} API`, }, + // Match any GitHub contact URL { - search: /url: https:\/\/github\.com\/waynesutton\/markdown-site/, + search: /url: https:\/\/github\.com\/[^\/]+\/[^\s]+/, replace: `url: ${githubUrl}`, }, + // Match any server URL (production server line) { - search: /- url: https:\/\/markdowncms\.netlify\.app/, - replace: `- url: ${config.siteUrl}`, + search: /- url: https:\/\/[^\s]+\n\s+description: Production server/, + replace: `- url: ${config.siteUrl}\n description: Production server`, }, + // Match any example site name { - search: /example: markdown sync framework/g, - replace: `example: ${config.siteName}`, + search: /example: .+\n\s+url:/g, + replace: `example: ${config.siteName}\n url:`, }, + // Match any example URL (for site URL) { - search: /example: https:\/\/markdowncms\.netlify\.app/g, - replace: `example: ${config.siteUrl}`, + search: /example: https:\/\/[^\s]+\n\s+posts:/, + replace: `example: ${config.siteUrl}\n posts:`, }, ]); } diff --git a/scripts/sync-discovery-files.ts b/scripts/sync-discovery-files.ts index f493c64..2ca5077 100644 --- a/scripts/sync-discovery-files.ts +++ b/scripts/sync-discovery-files.ts @@ -2,13 +2,17 @@ /** * Discovery Files Sync Script * - * Reads siteConfig.ts and Convex data to update discovery files. + * Reads fork-config.json (if available), siteConfig.ts, and Convex data to update discovery files. * Run with: npm run sync:discovery (dev) or npm run sync:discovery:prod (prod) * * This script updates: * - AGENTS.md (project overview and current status sections) * - CLAUDE.md (current status section for Claude Code) * - public/llms.txt (site info, API endpoints, GitHub links) + * + * IMPORTANT: If fork-config.json exists, it will be used as the source of truth. + * This ensures that after running `npm run configure`, subsequent sync:discovery + * commands will use your configured values. */ import fs from "fs"; @@ -33,12 +37,48 @@ const PROJECT_ROOT = process.cwd(); const PUBLIC_DIR = path.join(PROJECT_ROOT, "public"); const ROOT_DIR = PROJECT_ROOT; +// Fork config interface (matches fork-config.json structure) +interface ForkConfig { + siteName: string; + siteTitle: string; + siteDescription: string; + siteUrl: string; + siteDomain: string; + githubUsername: string; + githubRepo: string; + contactEmail?: string; + bio?: string; + gitHubRepoConfig?: { + owner: string; + repo: string; + branch: string; + contentPath: string; + }; +} + +// Load fork-config.json if it exists +function loadForkConfig(): ForkConfig | null { + try { + const configPath = path.join(PROJECT_ROOT, "fork-config.json"); + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, "utf-8"); + const config = JSON.parse(content) as ForkConfig; + console.log("Using configuration from fork-config.json"); + return config; + } + } catch (error) { + console.warn("Could not load fork-config.json, falling back to siteConfig.ts"); + } + return null; +} + // Site config data structure interface SiteConfigData { name: string; title: string; bio: string; description?: string; + siteUrl?: string; // Added to pass URL from fork-config.json gitHubRepo?: { owner: string; repo: string; @@ -47,8 +87,39 @@ interface SiteConfigData { }; } -// Load site config from siteConfig.ts using regex +// Cached fork config +let cachedForkConfig: ForkConfig | null | undefined = undefined; + +// Get fork config (cached) +function getForkConfig(): ForkConfig | null { + if (cachedForkConfig === undefined) { + cachedForkConfig = loadForkConfig(); + } + return cachedForkConfig; +} + +// Load site config - prioritizes fork-config.json over siteConfig.ts function loadSiteConfig(): SiteConfigData { + // First try fork-config.json + const forkConfig = getForkConfig(); + if (forkConfig) { + return { + name: forkConfig.siteName, + title: forkConfig.siteTitle, + bio: forkConfig.bio || forkConfig.siteDescription, + description: forkConfig.siteDescription, + siteUrl: forkConfig.siteUrl, + gitHubRepo: forkConfig.gitHubRepoConfig || { + owner: forkConfig.githubUsername, + repo: forkConfig.githubRepo, + branch: "main", + contentPath: "public/raw", + }, + }; + } + + // Fall back to siteConfig.ts + console.log("No fork-config.json found, reading from siteConfig.ts"); try { const configPath = path.join( PROJECT_ROOT, @@ -94,14 +165,14 @@ function loadSiteConfig(): SiteConfigData { : undefined; return { - name: nameMatch?.[1] || "markdown sync framework", - title: titleMatch?.[1] || "markdown sync framework", + name: nameMatch?.[1] || "Your Site Name", + title: titleMatch?.[1] || "Your Site Title", bio: bioMatch?.[1] || - "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..", + "Your site description here.", description: bioMatch?.[1] || - "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..", + "Your site description here.", gitHubRepo, }; } @@ -110,30 +181,51 @@ function loadSiteConfig(): SiteConfigData { } return { - name: "markdown sync framework", - title: "markdown sync framework", - bio: "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..", - description: - "An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs..", + name: "Your Site Name", + title: "Your Site Title", + bio: "Your site description here.", + description: "Your site description here.", }; } -// Get site URL from environment or config -function getSiteUrl(): string { - return ( - process.env.SITE_URL || process.env.VITE_SITE_URL || "https://markdown.fast" - ); +// Get site URL from fork-config.json, environment, or siteConfig +function getSiteUrl(siteConfig?: SiteConfigData): string { + // 1. Check fork-config.json (via siteConfig) + if (siteConfig?.siteUrl) { + return siteConfig.siteUrl; + } + // 2. Check fork-config.json directly + const forkConfig = getForkConfig(); + if (forkConfig?.siteUrl) { + return forkConfig.siteUrl; + } + // 3. Check environment variables + if (process.env.SITE_URL) { + return process.env.SITE_URL; + } + if (process.env.VITE_SITE_URL) { + return process.env.VITE_SITE_URL; + } + // 4. Return placeholder (user should configure) + return "https://yoursite.example.com"; } -// Build GitHub URL from repo config or fallback +// Build GitHub URL from repo config or fork-config.json function getGitHubUrl(siteConfig: SiteConfigData): string { if (siteConfig.gitHubRepo) { return `https://github.com/${siteConfig.gitHubRepo.owner}/${siteConfig.gitHubRepo.repo}`; } - return ( - process.env.GITHUB_REPO_URL || - "https://github.com/waynesutton/markdown-site" - ); + // Check fork-config.json directly + const forkConfig = getForkConfig(); + if (forkConfig) { + return `https://github.com/${forkConfig.githubUsername}/${forkConfig.githubRepo}`; + } + // Check environment variable + if (process.env.GITHUB_REPO_URL) { + return process.env.GITHUB_REPO_URL; + } + // Return placeholder + return "https://github.com/yourusername/your-repo"; } // Update CLAUDE.md with current status @@ -326,9 +418,9 @@ async function syncDiscoveryFiles() { // Initialize Convex client const client = new ConvexHttpClient(convexUrl); - // Load site configuration + // Load site configuration (uses fork-config.json if available) const siteConfig = loadSiteConfig(); - const siteUrl = getSiteUrl(); + const siteUrl = getSiteUrl(siteConfig); console.log(`Site: ${siteConfig.name}`); console.log(`Title: ${siteConfig.title}`); diff --git a/src/components/AIChatView.tsx b/src/components/AIChatView.tsx index deb5133..a8825f1 100644 --- a/src/components/AIChatView.tsx +++ b/src/components/AIChatView.tsx @@ -33,6 +33,7 @@ interface AIChatViewProps { pageContent?: string; // Optional page content for context onClose?: () => void; // Optional close handler hideAttachments?: boolean; // Hide image/link attachment buttons (for right sidebar) + selectedModel?: string; // Selected AI model ID (e.g., "claude-sonnet-4-20250514", "gpt-4o", "gemini-2.0-flash") } export default function AIChatView({ @@ -40,6 +41,7 @@ export default function AIChatView({ pageContent, onClose, hideAttachments = false, + selectedModel, }: AIChatViewProps) { // State const [inputValue, setInputValue] = useState(""); @@ -334,6 +336,7 @@ export default function AIChatView({ await generateResponse({ chatId, userMessage: message || "", + model: selectedModel as "claude-sonnet-4-20250514" | "gpt-4o" | "gemini-2.0-flash" | undefined, pageContext: hasLoadedContext ? undefined : pageContent, attachments: attachmentsToSend.length > 0 ? attachmentsToSend : undefined, diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 73ca421..c548298 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -192,25 +192,26 @@ export default function Layout({ children }: LayoutProps) { {/* Desktop search and theme (visible on desktop only) */}
{/* Social icons in header (if enabled) */} - {siteConfig.socialFooter?.enabled && siteConfig.socialFooter?.showInHeader && ( -
- {siteConfig.socialFooter.socialLinks.map((link) => { - const IconComponent = platformIcons[link.platform]; - return ( - - - - ); - })} -
- )} + {siteConfig.socialFooter?.enabled && + siteConfig.socialFooter?.showInHeader && ( +
+ {siteConfig.socialFooter.socialLinks.map((link) => { + const IconComponent = platformIcons[link.platform]; + return ( + + + + ); + })} +
+ )} {/* Search button with icon */} + {enableImageGeneration && ( + + )} +
+ + {/* Chat Tab */} + {activeTab === "chat" && ( +
+ {/* Model Selector */} +
+ Model: +
+ + {showTextModelDropdown && ( +
+ {textModels.map((model) => ( + + ))} +
+ )} +
+
+ +
+ )} + + {/* Image Generation Tab */} + {activeTab === "image" && enableImageGeneration && ( +
+ {/* Image Model Selector */} +
+ Model: +
+ + {showImageModelDropdown && ( +
+ {imageModels.map((model) => ( + + ))} +
+ )} +
+
+ + {/* Aspect Ratio Selector */} +
+ Aspect: +
+ {(["1:1", "16:9", "9:16", "4:3", "3:4"] as const).map((ratio) => ( + + ))} +
+
+ + {/* Generated Image Display */} + {generatedImage && ( +
+ {generatedImage.prompt} +

{generatedImage.prompt}

+
+ )} + + {/* Error Display */} + {imageError && ( +
+ {imageError} +
+ )} + + {/* Loading State */} + {isGeneratingImage && ( +
+ + Generating image... +
+ )} + + {/* Prompt Input */} +
+