From d191c00e4d9a8f2bfc49f09fce23bf4473e90ecd Mon Sep 17 00:00:00 2001 From: arkml <6592213+arkml@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:30:02 +0530 Subject: [PATCH 01/97] Memory2 (#444) Agent Notes: background memory system that learns from emails, chats, and explicit saves - Background service (agent_notes.ts) runs periodically, collecting user-sent emails, copilot conversation history, and save-to-memory inbox entries - Agent (agent_notes_agent.ts) processes all sources with workspace tools, deciding what to update: user.md (timestamped facts), preferences.md (general rules), style/email.md (writing patterns), and topic-specific files as needed - save-to-memory builtin tool lets the copilot proactively note preferences during conversations - user.md and preferences.md injected into copilot system prompt on every turn; other files listed for on-demand access - Agent manages timestamp freshness on user.md: refreshes confirmed facts, removes stale transient ones --- apps/x/apps/main/src/main.ts | 4 + apps/x/packages/core/src/agents/runtime.ts | 89 ++++- .../src/application/assistant/instructions.ts | 25 ++ .../core/src/application/lib/builtin-tools.ts | 21 ++ .../core/src/knowledge/agent_notes.ts | 333 ++++++++++++++++++ .../core/src/knowledge/agent_notes_agent.ts | 90 +++++ .../core/src/knowledge/agent_notes_state.ts | 62 ++++ apps/x/packages/shared/src/service-events.ts | 1 + 8 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 apps/x/packages/core/src/knowledge/agent_notes.ts create mode 100644 apps/x/packages/core/src/knowledge/agent_notes_agent.ts create mode 100644 apps/x/packages/core/src/knowledge/agent_notes_state.ts diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 9b89f033..3b8b70c8 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -21,6 +21,7 @@ import { init as initEmailLabeling } from "@x/core/dist/knowledge/label_emails.j import { init as initNoteTagging } from "@x/core/dist/knowledge/tag_notes.js"; import { init as initInlineTasks } from "@x/core/dist/knowledge/inline_tasks.js"; import { init as initAgentRunner } from "@x/core/dist/agent-schedule/runner.js"; +import { init as initAgentNotes } from "@x/core/dist/knowledge/agent_notes.js"; import { initConfigs } from "@x/core/dist/config/initConfigs.js"; import started from "electron-squirrel-startup"; import { execSync } from "node:child_process"; @@ -230,6 +231,9 @@ app.whenReady().then(async () => { // start background agent runner (scheduled agents) initAgentRunner(); + // start agent notes learning service + initAgentNotes(); + app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index f752543f..baf23c3e 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -30,6 +30,61 @@ import { getRaw as getNoteCreationRaw } from "../knowledge/note_creation.js"; import { getRaw as getLabelingAgentRaw } from "../knowledge/labeling_agent.js"; import { getRaw as getNoteTaggingAgentRaw } from "../knowledge/note_tagging_agent.js"; import { getRaw as getInlineTaskAgentRaw } from "../knowledge/inline_task_agent.js"; +import { getRaw as getAgentNotesAgentRaw } from "../knowledge/agent_notes_agent.js"; + +const AGENT_NOTES_DIR = path.join(WorkDir, 'knowledge', 'Agent Notes'); + +function loadAgentNotesContext(): string | null { + const sections: string[] = []; + + const userFile = path.join(AGENT_NOTES_DIR, 'user.md'); + const prefsFile = path.join(AGENT_NOTES_DIR, 'preferences.md'); + + try { + if (fs.existsSync(userFile)) { + const content = fs.readFileSync(userFile, 'utf-8').trim(); + if (content) { + sections.push(`## About the User\nThese are notes you took about the user in previous chats.\n\n${content}`); + } + } + } catch { /* ignore */ } + + try { + if (fs.existsSync(prefsFile)) { + const content = fs.readFileSync(prefsFile, 'utf-8').trim(); + if (content) { + sections.push(`## User Preferences\nThese are notes you took on their general preferences.\n\n${content}`); + } + } + } catch { /* ignore */ } + + // List other Agent Notes files for on-demand access + const otherFiles: string[] = []; + const skipFiles = new Set(['user.md', 'preferences.md', 'inbox.md']); + try { + if (fs.existsSync(AGENT_NOTES_DIR)) { + function listMdFiles(dir: string, prefix: string) { + for (const entry of fs.readdirSync(dir)) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + listMdFiles(fullPath, `${prefix}${entry}/`); + } else if (entry.endsWith('.md') && !skipFiles.has(`${prefix}${entry}`)) { + otherFiles.push(`${prefix}${entry}`); + } + } + } + listMdFiles(AGENT_NOTES_DIR, ''); + } + } catch { /* ignore */ } + + if (otherFiles.length > 0) { + sections.push(`## More Specific Preferences\nFor more specific preferences, you can read these files using workspace-readFile. Only read them when relevant to the current task.\n\n${otherFiles.map(f => `- knowledge/Agent Notes/${f}`).join('\n')}`); + } + + if (sections.length === 0) return null; + return `# Agent Memory\n\n${sections.join('\n\n')}`; +} export interface IAgentRuntime { trigger(runId: string): Promise; @@ -418,6 +473,31 @@ export async function loadAgent(id: string): Promise> { return agent; } + if (id === 'agent_notes_agent') { + const agentNotesAgentRaw = getAgentNotesAgentRaw(); + let agent: z.infer = { + name: id, + instructions: agentNotesAgentRaw, + }; + + if (agentNotesAgentRaw.startsWith("---")) { + const end = agentNotesAgentRaw.indexOf("\n---", 3); + if (end !== -1) { + const fm = agentNotesAgentRaw.slice(3, end).trim(); + const content = agentNotesAgentRaw.slice(end + 4).trim(); + const yaml = parse(fm); + const parsed = Agent.omit({ name: true, instructions: true }).parse(yaml); + agent = { + ...agent, + ...parsed, + instructions: content, + }; + } + } + + return agent; + } + const repo = container.resolve('agentsRepo'); return await repo.fetch(id); } @@ -773,7 +853,7 @@ export async function* streamAgent({ const provider = await isSignedIn() ? await getGatewayProvider() : createProvider(modelConfig.provider); - const knowledgeGraphAgents = ["note_creation", "email-draft", "meeting-prep", "labeling_agent", "note_tagging_agent"]; + const knowledgeGraphAgents = ["note_creation", "email-draft", "meeting-prep", "labeling_agent", "note_tagging_agent", "agent_notes_agent"]; const modelId = (knowledgeGraphAgents.includes(state.agentName!) && modelConfig.knowledgeGraphModel) ? modelConfig.knowledgeGraphModel : modelConfig.model; @@ -951,6 +1031,13 @@ export async function* streamAgent({ timeZoneName: 'short' }); let instructionsWithDateTime = `Current date and time: ${currentDateTime}\n\n${agent.instructions}`; + // Inject Agent Notes context for copilot + if (state.agentName === 'copilot' || state.agentName === 'rowboatx') { + const agentNotesContext = loadAgentNotesContext(); + if (agentNotesContext) { + instructionsWithDateTime += `\n\n${agentNotesContext}`; + } + } if (voiceInput) { loopLogger.log('voice input enabled, injecting voice input prompt'); instructionsWithDateTime += `\n\n# Voice Input\nThe user's message was transcribed from speech. Be aware that:\n- There may be transcription errors. Silently correct obvious ones (e.g. homophones, misheard words). If an error is genuinely ambiguous, briefly mention your interpretation (e.g. "I'm assuming you meant X").\n- Spoken messages are often long-winded. The user may ramble, repeat themselves, or correct something they said earlier in the same message. Focus on their final intent, not every word verbatim.`; diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts index 978e6c40..d39a0d63 100644 --- a/apps/x/packages/core/src/application/assistant/instructions.ts +++ b/apps/x/packages/core/src/application/assistant/instructions.ts @@ -37,6 +37,30 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects, **Slack:** When users ask about Slack messages, want to send messages to teammates, check channel conversations, or find someone on Slack, load the \`slack\` skill. You can send messages, view channel history, search conversations, and find users. Always show message drafts to the user before sending. +## Learning About the User (save-to-memory) + +Use the \`save-to-memory\` tool to note things worth remembering about the user. This builds a persistent profile that helps you serve them better over time. Call it proactively — don't ask permission. + +**When to save:** +- User states a preference: "I prefer bullet points" +- User corrects your style: "too formal, keep it casual" +- You learn about their relationships: "Monica is my co-founder" +- You notice workflow patterns: "no meetings before 11am" +- User gives explicit instructions: "never use em-dashes" +- User has preferences for specific tasks: "pitch decks should be minimal, max 12 slides" + +**Capture context, not blanket rules:** +- BAD: "User prefers casual tone" — this loses important context +- GOOD: "User prefers casual tone with internal team (Ramnique, Monica) but formal/polished with investors (Brad, Dalton)" +- BAD: "User likes short emails" — too vague +- GOOD: "User sends very terse 1-2 line emails to co-founder Ramnique, but writes structured 2-3 paragraph emails to investors with proper greetings" +- Always note WHO or WHAT CONTEXT a preference applies to. Most preferences are situational, not universal. + +**When NOT to save:** +- Ephemeral task details ("draft an email about X") +- Things already in the knowledge graph +- Information you can derive from reading their notes + ## Memory That Compounds Unlike other AI assistants that start cold every session, you have access to a live knowledge graph that updates itself from Gmail, calendar, and meeting notes (Google Meet, Granola, Fireflies). This isn't just summaries - it's structured extraction of decisions, commitments, open questions, and context, routed to long-lived notes for each person, project, and topic. @@ -187,6 +211,7 @@ ${runtimeContextPrompt} - \`slack-checkConnection\`, \`slack-listAvailableTools\`, \`slack-executeAction\` - Slack integration (requires Slack to be connected via Composio). Use \`slack-listAvailableTools\` first to discover available tool slugs, then \`slack-executeAction\` to execute them. - \`web-search\` and \`research-search\` - Web and research search tools (available when configured). **You MUST load the \`web-search\` skill before using either of these tools.** It tells you which tool to pick and how many searches to do. - \`app-navigation\` - Control the app UI: open notes, switch views, filter/search the knowledge base, manage saved views. **Load the \`app-navigation\` skill before using this tool.** +- \`save-to-memory\` - Save observations about the user to the agent memory system. Use this proactively during conversations. **Prefer these tools whenever possible** — they work instantly with zero friction. For file operations inside \`~/.rowboat/\`, always use these instead of \`executeCommand\`. diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 92742668..5275aaa9 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -1260,4 +1260,25 @@ export const BuiltinTools: z.infer = { } }, }, + 'save-to-memory': { + description: "Save a note about the user to the agent memory inbox. Use this when you observe something worth remembering — their preferences, communication patterns, relationship context, scheduling habits, or explicit instructions about how they want things done.", + inputSchema: z.object({ + note: z.string().describe("The observation or preference to remember. Be specific and concise."), + }), + execute: async ({ note }: { note: string }) => { + const inboxPath = path.join(WorkDir, 'knowledge', 'Agent Notes', 'inbox.md'); + const dir = path.dirname(inboxPath); + await fs.mkdir(dir, { recursive: true }); + + const timestamp = new Date().toISOString(); + const entry = `\n- [${timestamp}] ${note}\n`; + + await fs.appendFile(inboxPath, entry, 'utf-8'); + + return { + success: true, + message: `Saved to memory: ${note}`, + }; + }, + }, }; diff --git a/apps/x/packages/core/src/knowledge/agent_notes.ts b/apps/x/packages/core/src/knowledge/agent_notes.ts new file mode 100644 index 00000000..b9f10f38 --- /dev/null +++ b/apps/x/packages/core/src/knowledge/agent_notes.ts @@ -0,0 +1,333 @@ +import fs from 'fs'; +import path from 'path'; +import { WorkDir } from '../config/config.js'; +import { createRun, createMessage } from '../runs/runs.js'; +import { bus } from '../runs/bus.js'; +import { serviceLogger } from '../services/service_logger.js'; +import { loadUserConfig } from '../pre_built/config.js'; +import { + loadAgentNotesState, + saveAgentNotesState, + markEmailProcessed, + markRunProcessed, + type AgentNotesState, +} from './agent_notes_state.js'; + +const SYNC_INTERVAL_MS = 10 * 1000; // 10 seconds (for testing) +const EMAIL_BATCH_SIZE = 5; +const RUNS_BATCH_SIZE = 5; +const GMAIL_SYNC_DIR = path.join(WorkDir, 'gmail_sync'); +const RUNS_DIR = path.join(WorkDir, 'runs'); +const AGENT_NOTES_DIR = path.join(WorkDir, 'knowledge', 'Agent Notes'); +const INBOX_FILE = path.join(AGENT_NOTES_DIR, 'inbox.md'); +const AGENT_ID = 'agent_notes_agent'; + +// --- File helpers --- + +function ensureAgentNotesDir(): void { + if (!fs.existsSync(AGENT_NOTES_DIR)) { + fs.mkdirSync(AGENT_NOTES_DIR, { recursive: true }); + } +} + +// --- Email scanning --- + +function findUserSentEmails( + state: AgentNotesState, + userEmail: string, + limit: number, +): string[] { + if (!fs.existsSync(GMAIL_SYNC_DIR)) { + return []; + } + + const results: { path: string; mtime: number }[] = []; + const userEmailLower = userEmail.toLowerCase(); + + function traverse(dir: string) { + const entries = fs.readdirSync(dir); + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + if (entry !== 'attachments') { + traverse(fullPath); + } + } else if (stat.isFile() && entry.endsWith('.md')) { + if (state.processedEmails[fullPath]) { + continue; + } + + try { + const content = fs.readFileSync(fullPath, 'utf-8'); + const fromLines = content.match(/^### From:.*$/gm); + if (fromLines?.some(line => line.toLowerCase().includes(userEmailLower))) { + results.push({ path: fullPath, mtime: stat.mtimeMs }); + } + } catch { + continue; + } + } + } + } + + traverse(GMAIL_SYNC_DIR); + + results.sort((a, b) => b.mtime - a.mtime); + return results.slice(0, limit).map(r => r.path); +} + +function extractUserPartsFromEmail(content: string, userEmail: string): string | null { + const userEmailLower = userEmail.toLowerCase(); + const sections = content.split(/^---$/m); + const userSections: string[] = []; + + for (const section of sections) { + const fromMatch = section.match(/^### From:.*$/m); + if (fromMatch && fromMatch[0].toLowerCase().includes(userEmailLower)) { + userSections.push(section.trim()); + } + } + + return userSections.length > 0 ? userSections.join('\n\n---\n\n') : null; +} + +// --- Inbox reading --- + +function readInbox(): string[] { + if (!fs.existsSync(INBOX_FILE)) { + return []; + } + const content = fs.readFileSync(INBOX_FILE, 'utf-8').trim(); + if (!content) { + return []; + } + return content.split('\n').filter(l => l.trim()); +} + +function clearInbox(): void { + if (fs.existsSync(INBOX_FILE)) { + fs.writeFileSync(INBOX_FILE, ''); + } +} + +// --- Copilot run scanning --- + +function findNewCopilotRuns(state: AgentNotesState): string[] { + if (!fs.existsSync(RUNS_DIR)) { + return []; + } + + const results: string[] = []; + const files = fs.readdirSync(RUNS_DIR).filter(f => f.endsWith('.jsonl')); + + for (const file of files) { + if (state.processedRuns[file]) { + continue; + } + + try { + const fullPath = path.join(RUNS_DIR, file); + const fd = fs.openSync(fullPath, 'r'); + const buf = Buffer.alloc(512); + const bytesRead = fs.readSync(fd, buf, 0, 512, 0); + fs.closeSync(fd); + + const firstLine = buf.subarray(0, bytesRead).toString('utf-8').split('\n')[0]; + const event = JSON.parse(firstLine); + if (event.agentName === 'copilot') { + results.push(file); + } + } catch { + continue; + } + } + + results.sort(); + return results; +} + +function extractConversationMessages(runFilePath: string): { role: string; text: string }[] { + const messages: { role: string; text: string }[] = []; + try { + const content = fs.readFileSync(runFilePath, 'utf-8'); + const lines = content.split('\n').filter(l => l.trim()); + + for (const line of lines) { + try { + const event = JSON.parse(line); + if (event.type !== 'message') continue; + + const msg = event.message; + if (!msg || (msg.role !== 'user' && msg.role !== 'assistant')) continue; + + let text = ''; + if (typeof msg.content === 'string') { + text = msg.content.trim(); + } else if (Array.isArray(msg.content)) { + text = msg.content + .filter((p: { type: string }) => p.type === 'text') + .map((p: { text: string }) => p.text) + .join('\n') + .trim(); + } + + if (text) { + messages.push({ role: msg.role, text }); + } + } catch { + continue; + } + } + } catch { + // ignore + } + return messages; +} + +// --- Wait for agent run completion --- + +async function waitForRunCompletion(runId: string): Promise { + return new Promise(async (resolve) => { + const unsubscribe = await bus.subscribe('*', async (event) => { + if (event.type === 'run-processing-end' && event.runId === runId) { + unsubscribe(); + resolve(); + } + }); + }); +} + +// --- Main processing --- + +async function processAgentNotes(): Promise { + const userConfig = loadUserConfig(); + if (!userConfig) { + console.log('[AgentNotes] No user config found, skipping'); + return; + } + + ensureAgentNotesDir(); + const state = loadAgentNotesState(); + + // Collect all source material + const messageParts: string[] = []; + + // 1. Emails + const emailPaths = findUserSentEmails(state, userConfig.email, EMAIL_BATCH_SIZE); + if (emailPaths.length > 0) { + messageParts.push(`## Emails sent by ${userConfig.name}\n`); + for (const p of emailPaths) { + const content = fs.readFileSync(p, 'utf-8'); + const userParts = extractUserPartsFromEmail(content, userConfig.email); + if (userParts) { + messageParts.push(`---\n${userParts}\n---\n`); + } + } + } + + // 2. Inbox entries + const inboxEntries = readInbox(); + if (inboxEntries.length > 0) { + messageParts.push(`## Notes from the assistant (save-to-memory inbox)\n`); + messageParts.push(inboxEntries.join('\n')); + } + + // 3. Copilot conversations + const newRuns = findNewCopilotRuns(state); + const runsToProcess = newRuns.slice(-RUNS_BATCH_SIZE); + if (runsToProcess.length > 0) { + let conversationText = ''; + for (const runFile of runsToProcess) { + const messages = extractConversationMessages(path.join(RUNS_DIR, runFile)); + if (messages.length === 0) continue; + conversationText += `\n--- Conversation ---\n`; + for (const msg of messages) { + conversationText += `${msg.role}: ${msg.text}\n\n`; + } + } + if (conversationText.trim()) { + messageParts.push(`## Recent copilot conversations\n${conversationText}`); + } + } + + // Nothing to process + if (messageParts.length === 0) { + return; + } + + const serviceRun = await serviceLogger.startRun({ + service: 'agent_notes', + message: 'Processing agent notes', + trigger: 'timer', + }); + + try { + const timestamp = new Date().toISOString(); + const message = `Current timestamp: ${timestamp}\n\nProcess the following source material and update the Agent Notes folder accordingly.\n\n${messageParts.join('\n\n')}`; + + const agentRun = await createRun({ agentId: AGENT_ID }); + await createMessage(agentRun.id, message); + await waitForRunCompletion(agentRun.id); + + // Mark everything as processed + for (const p of emailPaths) { + markEmailProcessed(p, state); + } + for (const r of newRuns) { + markRunProcessed(r, state); + } + if (inboxEntries.length > 0) { + clearInbox(); + } + + state.lastRunTime = new Date().toISOString(); + saveAgentNotesState(state); + + await serviceLogger.log({ + type: 'run_complete', + service: serviceRun.service, + runId: serviceRun.runId, + level: 'info', + message: 'Agent notes processing complete', + durationMs: Date.now() - serviceRun.startedAt, + outcome: 'ok', + summary: { + emails: emailPaths.length, + inboxEntries: inboxEntries.length, + copilotRuns: runsToProcess.length, + }, + }); + } catch (error) { + console.error('[AgentNotes] Error processing:', error); + await serviceLogger.log({ + type: 'error', + service: serviceRun.service, + runId: serviceRun.runId, + level: 'error', + message: 'Error processing agent notes', + error: error instanceof Error ? error.message : String(error), + }); + } +} + +// --- Entry point --- + +export async function init() { + console.log('[AgentNotes] Starting Agent Notes Service...'); + console.log(`[AgentNotes] Will process every ${SYNC_INTERVAL_MS / 1000} seconds`); + + // Initial run + await processAgentNotes(); + + // Periodic polling + while (true) { + await new Promise(resolve => setTimeout(resolve, SYNC_INTERVAL_MS)); + try { + await processAgentNotes(); + } catch (error) { + console.error('[AgentNotes] Error in main loop:', error); + } + } +} diff --git a/apps/x/packages/core/src/knowledge/agent_notes_agent.ts b/apps/x/packages/core/src/knowledge/agent_notes_agent.ts new file mode 100644 index 00000000..58aa22a7 --- /dev/null +++ b/apps/x/packages/core/src/knowledge/agent_notes_agent.ts @@ -0,0 +1,90 @@ +export function getRaw(): string { + return `--- +tools: + workspace-writeFile: + type: builtin + name: workspace-writeFile + workspace-readFile: + type: builtin + name: workspace-readFile + workspace-edit: + type: builtin + name: workspace-edit + workspace-readdir: + type: builtin + name: workspace-readdir + workspace-mkdir: + type: builtin + name: workspace-mkdir +--- +# Agent Notes + +You are the Agent Notes agent. You maintain a set of notes about the user in the \`knowledge/Agent Notes/\` folder. Your job is to process new source material and update the notes accordingly. + +## Folder Structure + +The Agent Notes folder contains markdown files that capture what you've learned about the user: + +- **user.md** — Facts about who the user IS: their identity, role, company, team, projects, relationships, life context. NOT how they write or what they prefer. Each fact is a timestamped bullet point. +- **preferences.md** — General preferences and explicit rules (e.g., "don't use em-dashes", "no meetings before 11am"). These are injected into the assistant's system prompt on every chat. +- **style/email.md** — Email writing style patterns, bucketed by recipient context, with examples from actual emails. +- Other files as needed — If you notice preferences specific to a topic (e.g., presentations, meeting prep), create a dedicated file for them (e.g., \`presentations.md\`, \`meeting-prep.md\`). + +## How to Process Source Material + +You will receive a message containing some combination of: +1. **Emails sent by the user** — Analyze their writing style and update \`style/email.md\`. Do NOT put style observations in \`user.md\`. +2. **Inbox entries** — Notes the assistant saved during conversations via save-to-memory. Route each to the appropriate file. General preferences go to \`preferences.md\`. Topic-specific preferences get their own file. +3. **Copilot conversations** — User and assistant messages from recent chats. Extract lasting facts about the user and append timestamped entries to \`user.md\`. + +## What Goes Where — Be Strict + +### user.md — ONLY identity and context facts +Good examples: +- Co-founded Rowboat Labs with Ramnique +- Team of 4 people +- Previously worked at Twitter +- Planning to fundraise after Product Hunt launch +- Based in Bangalore, travels to SF periodically + +Bad examples (do NOT put these in user.md): +- "Uses concise, friendly scheduling replies" → this is style, goes in style/email.md +- "Frequently replies with short confirmations" → this is style, goes in style/email.md +- "Uses the abbreviation PFA" → this is style, goes in style/email.md +- "Requested a children's story about a scientist grandmother" → this is an ephemeral task, skip entirely +- "Prefers 30-minute meeting slots" → this is a preference, goes in preferences.md + +### style/email.md — Writing patterns from emails +Organize by recipient context. Include concrete examples quoted from actual emails. +- Close team (very terse, no greeting/sign-off) +- External/investors (casual but structured) +- Formal/cold (concise, complete sentences) + +### preferences.md — Explicit rules and preferences +Things the user has stated they want or don't want. + +### Other files — Topic-specific persistent preferences ONLY +Create a new file ONLY for recurring preference themes where the user has expressed multiple lasting preferences about a specific skill or task type. Examples: \`presentations.md\` (if the user has stated preferences about slide design, deck structure, etc.), \`meeting-prep.md\` (if they have preferences about how meetings are prepared). + +Do NOT create files for: +- One-off facts or transient situations (e.g., "looking for housing in SF" — that's a user.md fact, not a preference file) +- Topics with only a single observation +- Things that are better captured in user.md or preferences.md + +## Rules + +- Always read a file before updating it so you know what's already there. +- For \`user.md\`: Format is \`- [ISO_TIMESTAMP] The fact\`. The timestamp indicates when the fact was last confirmed. + - **Add** new facts with the current timestamp. + - **Refresh** existing facts: if you would add a fact that's already there, update its timestamp to the current one so it stays fresh. + - **Remove** facts that are likely outdated. Use your judgment: time-bound facts (e.g., "planning to launch next week", "has a meeting with X on Friday") go stale quickly. Stable facts (e.g., "co-founded Rowboat with Ramnique", "previously worked at Twitter") persist. If a fact's timestamp is old and it describes something transient, remove it. +- For \`preferences.md\` and other preference files: you may reorganize and deduplicate, but preserve all existing preferences that are still relevant. +- **Deduplicate strictly.** Before adding anything, check if the same fact is already captured — even if worded differently. Do NOT add a near-duplicate. +- **Skip ephemeral tasks.** If the user asked the assistant to do a one-off thing (draft an email, write a story, search for something), that is NOT a fact about the user. Skip it entirely. +- Be concise — bullet points, not paragraphs. +- Capture context, not blanket rules. BAD: "User prefers casual tone". GOOD: "User prefers casual tone with internal team but formal with investors." +- **If there's nothing new to add to a file, do NOT touch it.** Do not create placeholder content, do not write "no preferences recorded", do not add explanatory notes about what the file is for. Leave it empty or leave it as-is. +- **Do NOT create files unless you have actual content for them.** An empty or boilerplate file is worse than no file. +- Create the \`style/\` directory if it doesn't exist yet and you have style content to write. +`; +} diff --git a/apps/x/packages/core/src/knowledge/agent_notes_state.ts b/apps/x/packages/core/src/knowledge/agent_notes_state.ts new file mode 100644 index 00000000..d500e155 --- /dev/null +++ b/apps/x/packages/core/src/knowledge/agent_notes_state.ts @@ -0,0 +1,62 @@ +import fs from 'fs'; +import path from 'path'; +import { WorkDir } from '../config/config.js'; + +const STATE_FILE = path.join(WorkDir, 'agent_notes_state.json'); + +export interface AgentNotesState { + processedEmails: Record; + processedRuns: Record; + lastRunTime: string; +} + +export function loadAgentNotesState(): AgentNotesState { + if (fs.existsSync(STATE_FILE)) { + try { + const parsed = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')); + // Handle migration from older state without processedRuns + if (!parsed.processedRuns) { + parsed.processedRuns = {}; + } + return parsed; + } catch (error) { + console.error('Error loading agent notes state:', error); + } + } + + return { + processedEmails: {}, + processedRuns: {}, + lastRunTime: new Date(0).toISOString(), + }; +} + +export function saveAgentNotesState(state: AgentNotesState): void { + try { + fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); + } catch (error) { + console.error('Error saving agent notes state:', error); + throw error; + } +} + +export function markEmailProcessed(filePath: string, state: AgentNotesState): void { + state.processedEmails[filePath] = { + processedAt: new Date().toISOString(), + }; +} + +export function markRunProcessed(runFile: string, state: AgentNotesState): void { + state.processedRuns[runFile] = { + processedAt: new Date().toISOString(), + }; +} + +export function resetAgentNotesState(): void { + const emptyState: AgentNotesState = { + processedEmails: {}, + processedRuns: {}, + lastRunTime: new Date().toISOString(), + }; + saveAgentNotesState(emptyState); +} diff --git a/apps/x/packages/shared/src/service-events.ts b/apps/x/packages/shared/src/service-events.ts index 807bc063..b7a7c579 100644 --- a/apps/x/packages/shared/src/service-events.ts +++ b/apps/x/packages/shared/src/service-events.ts @@ -9,6 +9,7 @@ export const ServiceName = z.enum([ 'voice_memo', 'email_labeling', 'note_tagging', + 'agent_notes', ]); const ServiceEventBase = z.object({ From 8151769891af667d49c050cef012b8cf27f236fc Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:47:53 +0530 Subject: [PATCH 02/97] fix import --- apps/x/apps/renderer/src/extensions/calendar-block.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/x/apps/renderer/src/extensions/calendar-block.tsx b/apps/x/apps/renderer/src/extensions/calendar-block.tsx index 9be8c44f..f72dc5d4 100644 --- a/apps/x/apps/renderer/src/extensions/calendar-block.tsx +++ b/apps/x/apps/renderer/src/extensions/calendar-block.tsx @@ -1,4 +1,4 @@ -import { mergeAttributes, Node } from '@tiptap/react' +import { mergeAttributes, Node as TiptapNode } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' import { X, Calendar, Video, ChevronDown, Mic } from 'lucide-react' import { blocks } from '@x/shared' @@ -74,7 +74,8 @@ function JoinMeetingSplitButton({ onJoinAndNotes, onNotesOnly }: { useEffect(() => { if (!open) return const handler = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) + const target = e.target + if (ref.current && target instanceof globalThis.Node && !ref.current.contains(target)) setOpen(false) } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) @@ -322,7 +323,7 @@ function CalendarBlockView({ node, deleteNode }: { node: { attrs: Record Date: Tue, 24 Mar 2026 11:50:22 +0530 Subject: [PATCH 03/97] use deepgram ws proxy, simplify env vars --- apps/x/apps/main/src/ipc.ts | 24 +++++-- apps/x/apps/main/src/oauth-handler.ts | 4 +- .../src/hooks/useMeetingTranscription.ts | 24 ++++--- .../renderer/src/hooks/useRowboatAccount.ts | 65 +++++++++++++++++++ .../x/apps/renderer/src/hooks/useVoiceMode.ts | 19 ++++-- .../renderer/src/lib/deepgram-listen-url.ts | 10 +++ apps/x/packages/core/src/auth/providers.ts | 20 +++--- apps/x/packages/core/src/auth/tokens.ts | 2 +- apps/x/packages/core/src/config/env.ts | 5 +- apps/x/packages/core/src/config/rowboat.ts | 15 +++++ .../src/knowledge/fireflies-client-factory.ts | 2 +- .../src/knowledge/google-client-factory.ts | 2 +- apps/x/packages/core/src/voice/voice.ts | 17 ----- apps/x/packages/shared/src/ipc.ts | 15 +++-- apps/x/packages/shared/src/rowboat-account.ts | 7 ++ 15 files changed, 170 insertions(+), 61 deletions(-) create mode 100644 apps/x/apps/renderer/src/hooks/useRowboatAccount.ts create mode 100644 apps/x/apps/renderer/src/lib/deepgram-listen-url.ts create mode 100644 apps/x/packages/core/src/config/rowboat.ts create mode 100644 apps/x/packages/shared/src/rowboat-account.ts diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index df4abcec..b92e3fe9 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -42,6 +42,8 @@ import { versionHistory, voice } from '@x/core'; import { classifySchedule, processRowboatInstruction } from '@x/core/dist/knowledge/inline_tasks.js'; import { getBillingInfo } from '@x/core/dist/billing/billing.js'; import { summarizeMeeting } from '@x/core/dist/knowledge/summarize_meeting.js'; +import { getAccessToken } from '@x/core/dist/auth/tokens.js'; +import { getRowboatConfig } from '@x/core/dist/config/rowboat.js'; /** * Convert markdown to a styled HTML document for PDF/DOCX export. @@ -144,10 +146,10 @@ export function registerIpcHandlers(handlers: InvokeHandlers) { ipcMain.handle(channel, async (event, rawArgs) => { // Validate request payload const args = ipc.validateRequest(channel, rawArgs); - + // Call handler const result = await handler(event, args); - + // Validate response payload return ipc.validateResponse(channel, result); }); @@ -471,6 +473,21 @@ export function setupIpcHandlers() { const config = await repo.getClientFacingConfig(); return { config }; }, + 'account:getRowboat': async () => { + const signedIn = await isSignedIn(); + if (!signedIn) { + return { signedIn: false, accessToken: null, config: null }; + } + + const config = await getRowboatConfig(); + + try { + const accessToken = await getAccessToken(); + return { signedIn: true, accessToken, config }; + } catch { + return { signedIn: true, accessToken: null, config }; + } + }, 'granola:getConfig': async () => { const repo = container.resolve('granolaConfigRepo'); const config = await repo.getConfig(); @@ -719,9 +736,6 @@ export function setupIpcHandlers() { 'voice:synthesize': async (_event, args) => { return voice.synthesizeSpeech(args.text); }, - 'voice:getDeepgramToken': async () => { - return voice.getDeepgramToken(); - }, // Billing handler 'billing:getInfo': async () => { return await getBillingInfo(); diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index b38a42d2..99865d8e 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -75,7 +75,7 @@ function getClientRegistrationRepo(): IClientRegistrationRepo { * Get or create OAuth configuration for a provider */ async function getProviderConfiguration(provider: string, clientIdOverride?: string): Promise { - const config = getProviderConfig(provider); + const config = await getProviderConfig(provider); const resolveClientId = async (): Promise => { if (config.client.mode === 'static' && config.client.clientId) { return config.client.clientId; @@ -156,7 +156,7 @@ export async function connectProvider(provider: string, clientId?: string): Prom cancelActiveFlow('new_flow_started'); const oauthRepo = getOAuthRepo(); - const providerConfig = getProviderConfig(provider); + const providerConfig = await getProviderConfig(provider); if (provider === 'google') { if (!clientId) { diff --git a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts index 12743fbf..35a0a703 100644 --- a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts +++ b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts @@ -1,4 +1,6 @@ import { useCallback, useRef, useState } from 'react'; +import { buildDeepgramListenUrl } from '@/lib/deepgram-listen-url'; +import { useRowboatAccount } from '@/hooks/useRowboatAccount'; export type MeetingTranscriptionState = 'idle' | 'connecting' | 'recording' | 'stopping'; @@ -101,6 +103,7 @@ function formatTranscript(entries: TranscriptEntry[], date: string, calendarEven // Hook // --------------------------------------------------------------------------- export function useMeetingTranscription(onAutoStop?: () => void) { + const { refresh: refreshRowboatAccount } = useRowboatAccount(); const [state, setState] = useState('idle'); const wsRef = useRef(null); const micStreamRef = useRef(null); @@ -188,13 +191,18 @@ export function useMeetingTranscription(onAutoStop?: () => void) { const usingHeadphones = await detectHeadphones(); console.log(`[meeting] Audio output mode: ${usingHeadphones ? 'headphones' : 'speakers'}`); - // Get Deepgram token + // Rowboat WebSocket + bearer token when signed in; else local Deepgram API key let ws: WebSocket; try { - const result = await window.ipc.invoke('voice:getDeepgramToken', null); - if (result) { - console.log('[meeting] Using proxy token'); - ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['bearer', result.token]); + const account = await refreshRowboatAccount(); + if ( + account?.signedIn && + account.accessToken && + account.config?.websocketApiUrl + ) { + const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS); + console.log('[meeting] Using Rowboat WebSocket'); + ws = new WebSocket(listenUrl, ['bearer', account.accessToken]); } else { const config = await window.ipc.invoke('voice:getConfig', null); if (!config?.deepgram) { @@ -202,11 +210,11 @@ export function useMeetingTranscription(onAutoStop?: () => void) { setState('idle'); return null; } - console.log('[meeting] Using API key'); + console.log('[meeting] Using Deepgram API key'); ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['token', config.deepgram.apiKey]); } } catch (err) { - console.error('[meeting] Failed to get Deepgram token:', err); + console.error('[meeting] Failed to connect Deepgram:', err); setState('idle'); return null; } @@ -389,7 +397,7 @@ export function useMeetingTranscription(onAutoStop?: () => void) { setState('recording'); return notePath; - }, [state, cleanup, scheduleDebouncedWrite]); + }, [state, cleanup, scheduleDebouncedWrite, refreshRowboatAccount]); const stop = useCallback(async () => { if (state !== 'recording') return; diff --git a/apps/x/apps/renderer/src/hooks/useRowboatAccount.ts b/apps/x/apps/renderer/src/hooks/useRowboatAccount.ts new file mode 100644 index 00000000..1cab2414 --- /dev/null +++ b/apps/x/apps/renderer/src/hooks/useRowboatAccount.ts @@ -0,0 +1,65 @@ +import { z } from 'zod'; +import { useCallback, useEffect, useState } from 'react'; +import { RowboatApiConfig } from '@x/shared/dist/rowboat-account.js'; + + +interface RowboatAccountState { + signedIn: boolean; + accessToken: string | null; + config: z.infer | null; +} + +export type RowboatAccountSnapshot = RowboatAccountState; + +const DEFAULT_STATE: RowboatAccountState = { + signedIn: false, + accessToken: null, + config: null, +}; + +export function useRowboatAccount() { + const [state, setState] = useState(DEFAULT_STATE); + const [isLoading, setIsLoading] = useState(true); + + const refresh = useCallback(async (): Promise => { + try { + setIsLoading(true); + const result = await window.ipc.invoke('account:getRowboat', null); + const next: RowboatAccountSnapshot = { + signedIn: result.signedIn, + accessToken: result.accessToken, + config: result.config, + }; + setState(next); + return next; + } catch (error) { + console.error('Failed to load Rowboat account state:', error); + setState(DEFAULT_STATE); + return null; + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + refresh(); + }, [refresh]); + + useEffect(() => { + const cleanup = window.ipc.on('oauth:didConnect', (event) => { + if (event.provider !== 'rowboat') { + return; + } + refresh(); + }); + return cleanup; + }, [refresh]); + + return { + signedIn: state.signedIn, + accessToken: state.accessToken, + config: state.config, + isLoading, + refresh, + }; +} diff --git a/apps/x/apps/renderer/src/hooks/useVoiceMode.ts b/apps/x/apps/renderer/src/hooks/useVoiceMode.ts index 439aec3b..854ac9ea 100644 --- a/apps/x/apps/renderer/src/hooks/useVoiceMode.ts +++ b/apps/x/apps/renderer/src/hooks/useVoiceMode.ts @@ -1,4 +1,6 @@ import { useCallback, useRef, useState } from 'react'; +import { buildDeepgramListenUrl } from '@/lib/deepgram-listen-url'; +import { useRowboatAccount } from '@/hooks/useRowboatAccount'; export type VoiceState = 'idle' | 'connecting' | 'listening'; @@ -15,6 +17,7 @@ const DEEPGRAM_PARAMS = new URLSearchParams({ const DEEPGRAM_LISTEN_URL = `wss://api.deepgram.com/v1/listen?${DEEPGRAM_PARAMS.toString()}`; export function useVoiceMode() { + const { refresh: refreshRowboatAccount } = useRowboatAccount(); const [state, setState] = useState('idle'); const [interimText, setInterimText] = useState(''); const wsRef = useRef(null); @@ -25,16 +28,20 @@ export function useVoiceMode() { const interimRef = useRef(''); // Connect (or reconnect) the Deepgram WebSocket. - // Fetches a fresh token on each connect — temp tokens have short TTL. + // Refreshes Rowboat account before connect so access token is current. const connectWs = useCallback(async () => { if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) return; let ws: WebSocket; - // Try signed-in proxy token first (passed as query param for JWTs) - const result = await window.ipc.invoke('voice:getDeepgramToken', null); - if (result) { - ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['bearer', result.token]); + const account = await refreshRowboatAccount(); + if ( + account?.signedIn && + account.accessToken && + account.config?.websocketApiUrl + ) { + const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS); + ws = new WebSocket(listenUrl, ['bearer', account.accessToken]); } else { // Fall back to local API key (passed as subprotocol) const config = await window.ipc.invoke('voice:getConfig', null); @@ -72,7 +79,7 @@ export function useVoiceMode() { console.log('[voice] WebSocket closed'); wsRef.current = null; }; - }, []); + }, [refreshRowboatAccount]); // Stop audio capture and close WS const stopAudioCapture = useCallback(() => { diff --git a/apps/x/apps/renderer/src/lib/deepgram-listen-url.ts b/apps/x/apps/renderer/src/lib/deepgram-listen-url.ts new file mode 100644 index 00000000..fc286c19 --- /dev/null +++ b/apps/x/apps/renderer/src/lib/deepgram-listen-url.ts @@ -0,0 +1,10 @@ +/** + * Merge Deepgram query params onto a Rowboat WebSocket base URL from account config. + */ +export function buildDeepgramListenUrl(baseWsUrl: string, params: URLSearchParams): string { + const url = new URL("/deepgram/v1/listen", baseWsUrl); + for (const [key, value] of params) { + url.searchParams.set(key, value); + } + return url.toString(); +} diff --git a/apps/x/packages/core/src/auth/providers.ts b/apps/x/packages/core/src/auth/providers.ts index 2fc644d9..52bd0ab5 100644 --- a/apps/x/packages/core/src/auth/providers.ts +++ b/apps/x/packages/core/src/auth/providers.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { SUPABASE_PROJECT_URL } from '../config/env.js'; +import { getRowboatConfig } from '../config/rowboat.js'; /** * Discovery configuration - how to get OAuth endpoints @@ -55,7 +55,7 @@ const providerConfigs: ProviderConfig = { rowboat: { discovery: { mode: 'issuer', - issuer: `${SUPABASE_PROJECT_URL}/auth/v1/.well-known/oauth-authorization-server`, + issuer: "TBD", }, client: { mode: 'dcr', @@ -98,21 +98,21 @@ const providerConfigs: ProviderConfig = { /** * Get provider configuration by name */ -export function getProviderConfig(providerName: string): ProviderConfigEntry { +export async function getProviderConfig(providerName: string): Promise { const config = providerConfigs[providerName]; if (!config) { throw new Error(`Unknown OAuth provider: ${providerName}`); } + if (providerName === 'rowboat') { + const rowboatConfig = await getRowboatConfig(); + config.discovery = { + mode: 'issuer', + issuer: `${rowboatConfig.supabaseUrl}/auth/v1/.well-known/oauth-authorization-server`, + } + } return config; } -/** - * Get all provider configurations - */ -export function getAllProviderConfigs(): ProviderConfig { - return providerConfigs; -} - /** * Get list of all configured OAuth providers */ diff --git a/apps/x/packages/core/src/auth/tokens.ts b/apps/x/packages/core/src/auth/tokens.ts index e89c911d..8a30bf9f 100644 --- a/apps/x/packages/core/src/auth/tokens.ts +++ b/apps/x/packages/core/src/auth/tokens.ts @@ -19,7 +19,7 @@ export async function getAccessToken(): Promise { throw new Error('Rowboat token expired and no refresh token available. Please sign in again.'); } - const providerConfig = getProviderConfig('rowboat'); + const providerConfig = await getProviderConfig('rowboat'); if (providerConfig.discovery.mode !== 'issuer') { throw new Error('Rowboat provider requires issuer discovery mode'); } diff --git a/apps/x/packages/core/src/config/env.ts b/apps/x/packages/core/src/config/env.ts index b8c0e3d7..4c638986 100644 --- a/apps/x/packages/core/src/config/env.ts +++ b/apps/x/packages/core/src/config/env.ts @@ -1,5 +1,2 @@ export const API_URL = - process.env.API_URL || 'https://api.x.rowboatlabs.com'; - -export const SUPABASE_PROJECT_URL = - process.env.SUPABASE_PROJECT_URL || 'https://jpxoiuhlshgwixajvsbu.supabase.co'; + process.env.API_URL || 'https://api.x.rowboatlabs.com'; \ No newline at end of file diff --git a/apps/x/packages/core/src/config/rowboat.ts b/apps/x/packages/core/src/config/rowboat.ts new file mode 100644 index 00000000..3fa282e6 --- /dev/null +++ b/apps/x/packages/core/src/config/rowboat.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; +import { RowboatApiConfig } from "@x/shared/dist/rowboat-account.js"; +import { API_URL } from "./env.js"; + +let cached: z.infer | null = null; + +export async function getRowboatConfig(): Promise> { + if (cached) { + return cached; + } + const response = await fetch(`${API_URL}/v1/config`); + const data = RowboatApiConfig.parse(await response.json()); + cached = data; + return data; +} \ No newline at end of file diff --git a/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts b/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts index 1ee865e9..03bbff5a 100644 --- a/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts +++ b/apps/x/packages/core/src/knowledge/fireflies-client-factory.ts @@ -135,7 +135,7 @@ export class FirefliesClientFactory { } console.log(`[Fireflies] Initializing OAuth configuration...`); - const providerConfig = getProviderConfig(this.PROVIDER_NAME); + const providerConfig = await getProviderConfig(this.PROVIDER_NAME); if (providerConfig.discovery.mode === 'issuer') { if (providerConfig.client.mode === 'static') { diff --git a/apps/x/packages/core/src/knowledge/google-client-factory.ts b/apps/x/packages/core/src/knowledge/google-client-factory.ts index 3cb83cd1..b3bcfe7a 100644 --- a/apps/x/packages/core/src/knowledge/google-client-factory.ts +++ b/apps/x/packages/core/src/knowledge/google-client-factory.ts @@ -155,7 +155,7 @@ export class GoogleClientFactory { } console.log(`[OAuth] Initializing Google OAuth configuration...`); - const providerConfig = getProviderConfig(this.PROVIDER_NAME); + const providerConfig = await getProviderConfig(this.PROVIDER_NAME); if (providerConfig.discovery.mode === 'issuer') { if (providerConfig.client.mode === 'static') { diff --git a/apps/x/packages/core/src/voice/voice.ts b/apps/x/packages/core/src/voice/voice.ts index 4c698a83..895c81b9 100644 --- a/apps/x/packages/core/src/voice/voice.ts +++ b/apps/x/packages/core/src/voice/voice.ts @@ -33,23 +33,6 @@ export async function getVoiceConfig(): Promise { }; } -export async function getDeepgramToken(): Promise<{ token: string } | null> { - const signedIn = await isSignedIn(); - if (!signedIn) return null; - - const accessToken = await getAccessToken(); - const response = await fetch(`${API_URL}/v1/voice/deepgram-token`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${accessToken}` }, - }); - if (!response.ok) { - console.error('[voice] Deepgram token error:', response.status); - return null; - } - const data = await response.json(); - return { token: data.token }; -} - export async function synthesizeSpeech(text: string): Promise<{ audioBase64: string; mimeType: string }> { const config = await getVoiceConfig(); const signedIn = await isSignedIn(); diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 82dc5275..5f4988f4 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -7,6 +7,7 @@ import { AgentScheduleConfig, AgentScheduleEntry } from './agent-schedule.js'; import { AgentScheduleState } from './agent-schedule-state.js'; import { ServiceEvent } from './service-events.js'; import { UserMessageContent } from './message.js'; +import { RowboatApiConfig } from './rowboat-account.js'; // ============================================================================ // Runtime Validation Schemas (Single Source of Truth) @@ -252,6 +253,14 @@ const ipcSchemas = { })), }), }, + 'account:getRowboat': { + req: z.null(), + res: z.object({ + signedIn: z.boolean(), + accessToken: z.string().nullable(), + config: RowboatApiConfig.nullable(), + }), + }, 'oauth:didConnect': { req: z.object({ provider: z.string(), @@ -492,12 +501,6 @@ const ipcSchemas = { mimeType: z.string(), }), }, - 'voice:getDeepgramToken': { - req: z.null(), - res: z.object({ - token: z.string(), - }).nullable(), - }, 'meeting:summarize': { req: z.object({ transcript: z.string(), diff --git a/apps/x/packages/shared/src/rowboat-account.ts b/apps/x/packages/shared/src/rowboat-account.ts new file mode 100644 index 00000000..c6b7cff4 --- /dev/null +++ b/apps/x/packages/shared/src/rowboat-account.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const RowboatApiConfig = z.object({ + appUrl: z.string(), + websocketApiUrl: z.string(), + supabaseUrl: z.string(), +}); From e0e4f7165c9f4e6bcbea10a0998482fce6462a24 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:24:32 +0530 Subject: [PATCH 04/97] populate user.json with the email --- .../x/packages/core/src/config/user_config.ts | 44 +++++++++++++++++ .../core/src/knowledge/agent_notes.ts | 48 ++++++++++++++----- 2 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 apps/x/packages/core/src/config/user_config.ts diff --git a/apps/x/packages/core/src/config/user_config.ts b/apps/x/packages/core/src/config/user_config.ts new file mode 100644 index 00000000..75034946 --- /dev/null +++ b/apps/x/packages/core/src/config/user_config.ts @@ -0,0 +1,44 @@ +import fs from 'fs'; +import path from 'path'; +import { z } from 'zod'; +import { WorkDir } from './config.js'; + +const USER_CONFIG_PATH = path.join(WorkDir, 'config', 'user.json'); + +export const UserConfig = z.object({ + name: z.string().optional(), + email: z.string().email(), + domain: z.string().optional(), +}); + +export type UserConfig = z.infer; + +export function loadUserConfig(): UserConfig | null { + try { + if (fs.existsSync(USER_CONFIG_PATH)) { + const content = fs.readFileSync(USER_CONFIG_PATH, 'utf-8'); + const parsed = JSON.parse(content); + return UserConfig.parse(parsed); + } + } catch (error) { + console.error('[UserConfig] Error loading user config:', error); + } + return null; +} + +export function saveUserConfig(config: UserConfig): void { + const dir = path.dirname(USER_CONFIG_PATH); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + const validated = UserConfig.parse(config); + fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(validated, null, 2)); +} + +export function updateUserEmail(email: string): void { + const existing = loadUserConfig(); + const config = existing + ? { ...existing, email } + : { email }; + saveUserConfig(config); +} diff --git a/apps/x/packages/core/src/knowledge/agent_notes.ts b/apps/x/packages/core/src/knowledge/agent_notes.ts index b9f10f38..433423ac 100644 --- a/apps/x/packages/core/src/knowledge/agent_notes.ts +++ b/apps/x/packages/core/src/knowledge/agent_notes.ts @@ -1,10 +1,12 @@ import fs from 'fs'; import path from 'path'; +import { google } from 'googleapis'; import { WorkDir } from '../config/config.js'; import { createRun, createMessage } from '../runs/runs.js'; import { bus } from '../runs/bus.js'; import { serviceLogger } from '../services/service_logger.js'; -import { loadUserConfig } from '../pre_built/config.js'; +import { loadUserConfig, updateUserEmail } from '../config/user_config.js'; +import { GoogleClientFactory } from './google-client-factory.js'; import { loadAgentNotesState, saveAgentNotesState, @@ -199,28 +201,52 @@ async function waitForRunCompletion(runId: string): Promise { }); } +// --- User email resolution --- + +async function ensureUserEmail(): Promise { + const existing = loadUserConfig(); + if (existing?.email) { + return existing.email; + } + + // Try to get email from Gmail profile + try { + const auth = await GoogleClientFactory.getClient(); + if (auth) { + const gmail = google.gmail({ version: 'v1', auth }); + const profile = await gmail.users.getProfile({ userId: 'me' }); + if (profile.data.emailAddress) { + updateUserEmail(profile.data.emailAddress); + console.log(`[AgentNotes] Auto-populated user email: ${profile.data.emailAddress}`); + return profile.data.emailAddress; + } + } + } catch (error) { + console.log('[AgentNotes] Could not fetch Gmail profile for user email:', error instanceof Error ? error.message : error); + } + + return null; +} + // --- Main processing --- async function processAgentNotes(): Promise { - const userConfig = loadUserConfig(); - if (!userConfig) { - console.log('[AgentNotes] No user config found, skipping'); - return; - } - ensureAgentNotesDir(); const state = loadAgentNotesState(); + const userEmail = await ensureUserEmail(); // Collect all source material const messageParts: string[] = []; - // 1. Emails - const emailPaths = findUserSentEmails(state, userConfig.email, EMAIL_BATCH_SIZE); + // 1. Emails (only if we have user email) + const emailPaths = userEmail + ? findUserSentEmails(state, userEmail, EMAIL_BATCH_SIZE) + : []; if (emailPaths.length > 0) { - messageParts.push(`## Emails sent by ${userConfig.name}\n`); + messageParts.push(`## Emails sent by the user\n`); for (const p of emailPaths) { const content = fs.readFileSync(p, 'utf-8'); - const userParts = extractUserPartsFromEmail(content, userConfig.email); + const userParts = extractUserPartsFromEmail(content, userEmail!); if (userParts) { messageParts.push(`---\n${userParts}\n---\n`); } From 0094cfa397d6e473a8e15d5eca11bfc3aa0f4c12 Mon Sep 17 00:00:00 2001 From: arkml <6592213+arkml@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:32:02 +0530 Subject: [PATCH 05/97] Fix2 (#445) * fix V8 memory issue --- apps/x/apps/main/entitlements.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/x/apps/main/entitlements.plist b/apps/x/apps/main/entitlements.plist index db2dbd7e..c0899b9d 100644 --- a/apps/x/apps/main/entitlements.plist +++ b/apps/x/apps/main/entitlements.plist @@ -2,6 +2,8 @@ + com.apple.security.cs.allow-jit + com.apple.security.device.audio-input com.apple.security.device.screen-capture From db7c843d3322e3f1bb3c8021098fb8bd8cb5c026 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:53:21 +0530 Subject: [PATCH 06/97] move notes to 15sec --- apps/x/packages/core/src/knowledge/build_graph.ts | 2 +- apps/x/packages/core/src/knowledge/label_emails.ts | 2 +- apps/x/packages/core/src/knowledge/tag_notes.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/x/packages/core/src/knowledge/build_graph.ts b/apps/x/packages/core/src/knowledge/build_graph.ts index 7ca70894..ab76b5e4 100644 --- a/apps/x/packages/core/src/knowledge/build_graph.ts +++ b/apps/x/packages/core/src/knowledge/build_graph.ts @@ -25,7 +25,7 @@ const NOTES_OUTPUT_DIR = path.join(WorkDir, 'knowledge'); const NOTE_CREATION_AGENT = 'note_creation'; // Configuration for the graph builder service -const SYNC_INTERVAL_MS = 30 * 1000; // Check every 30 seconds +const SYNC_INTERVAL_MS = 15 * 1000; // 15 seconds const SOURCE_FOLDERS = [ 'gmail_sync', path.join('knowledge', 'Meetings', 'fireflies'), diff --git a/apps/x/packages/core/src/knowledge/label_emails.ts b/apps/x/packages/core/src/knowledge/label_emails.ts index a62f674a..a72aa8f5 100644 --- a/apps/x/packages/core/src/knowledge/label_emails.ts +++ b/apps/x/packages/core/src/knowledge/label_emails.ts @@ -12,7 +12,7 @@ import { type LabelingState, } from './labeling_state.js'; -const SYNC_INTERVAL_MS = 3 * 60 * 1000; // 3 minutes +const SYNC_INTERVAL_MS = 15 * 1000; // 15 seconds const BATCH_SIZE = 15; const LABELING_AGENT = 'labeling_agent'; const GMAIL_SYNC_DIR = path.join(WorkDir, 'gmail_sync'); diff --git a/apps/x/packages/core/src/knowledge/tag_notes.ts b/apps/x/packages/core/src/knowledge/tag_notes.ts index 086a3bb5..89c616e6 100644 --- a/apps/x/packages/core/src/knowledge/tag_notes.ts +++ b/apps/x/packages/core/src/knowledge/tag_notes.ts @@ -13,7 +13,7 @@ import { } from './note_tagging_state.js'; import { getNoteTypeDefinitions } from './note_system.js'; -const SYNC_INTERVAL_MS = 30 * 1000; // 30 seconds +const SYNC_INTERVAL_MS = 15 * 1000; // 15 seconds const BATCH_SIZE = 15; const NOTE_TAGGING_AGENT = 'note_tagging_agent'; const KNOWLEDGE_DIR = path.join(WorkDir, 'knowledge'); From 1686c8f878b26bbf40543e595f417a6e32380116 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:27:48 +0530 Subject: [PATCH 07/97] configure per-service model defaults for signed-in users When signed in, default assistant to gpt-5.4, knowledge graph agents to gpt-5.4-nano, inline task agent to gpt-5.4-mini, and meeting notes to gpt-5.4. Add meetingNotesModel config field. Fix summarize_meeting to use gateway provider when signed in. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/x/packages/core/src/agents/runtime.ts | 16 ++++++++++++---- .../core/src/knowledge/summarize_meeting.ts | 11 +++++++++-- apps/x/packages/core/src/models/repo.ts | 1 + apps/x/packages/shared/src/models.ts | 1 + 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index baf23c3e..09347145 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -850,13 +850,21 @@ export async function* streamAgent({ const tools = await buildTools(agent); // set up provider + model - const provider = await isSignedIn() + const signedIn = await isSignedIn(); + const provider = signedIn ? await getGatewayProvider() : createProvider(modelConfig.provider); const knowledgeGraphAgents = ["note_creation", "email-draft", "meeting-prep", "labeling_agent", "note_tagging_agent", "agent_notes_agent"]; - const modelId = (knowledgeGraphAgents.includes(state.agentName!) && modelConfig.knowledgeGraphModel) - ? modelConfig.knowledgeGraphModel - : modelConfig.model; + const isKgAgent = knowledgeGraphAgents.includes(state.agentName!); + const isInlineTaskAgent = state.agentName === "inline_task_agent"; + const defaultModel = signedIn ? "gpt-5.4" : modelConfig.model; + const defaultKgModel = signedIn ? "gpt-5.4-nano" : defaultModel; + const defaultInlineTaskModel = signedIn ? "gpt-5.4-mini" : defaultModel; + const modelId = isInlineTaskAgent + ? defaultInlineTaskModel + : (isKgAgent && modelConfig.knowledgeGraphModel) + ? modelConfig.knowledgeGraphModel + : isKgAgent ? defaultKgModel : defaultModel; const model = provider.languageModel(modelId); logger.log(`using model: ${modelId}`); diff --git a/apps/x/packages/core/src/knowledge/summarize_meeting.ts b/apps/x/packages/core/src/knowledge/summarize_meeting.ts index d3d09248..534b6655 100644 --- a/apps/x/packages/core/src/knowledge/summarize_meeting.ts +++ b/apps/x/packages/core/src/knowledge/summarize_meeting.ts @@ -4,6 +4,8 @@ import { generateText } from 'ai'; import container from '../di/container.js'; import type { IModelConfigRepo } from '../models/repo.js'; import { createProvider } from '../models/models.js'; +import { isSignedIn } from '../account/account.js'; +import { getGatewayProvider } from '../models/gateway.js'; import { WorkDir } from '../config/config.js'; const CALENDAR_SYNC_DIR = path.join(WorkDir, 'calendar_sync'); @@ -137,8 +139,13 @@ function loadCalendarEventContext(calendarEventJson: string): string { export async function summarizeMeeting(transcript: string, meetingStartTime?: string, calendarEventJson?: string): Promise { const repo = container.resolve('modelConfigRepo'); const config = await repo.getConfig(); - const provider = createProvider(config.provider); - const model = provider.languageModel(config.model); + const signedIn = await isSignedIn(); + const provider = signedIn + ? await getGatewayProvider() + : createProvider(config.provider); + const modelId = config.meetingNotesModel + || (signedIn ? "gpt-5.4" : config.model); + const model = provider.languageModel(modelId); // If a specific calendar event was linked, use it directly. // Otherwise fall back to scanning events within ±3 hours. diff --git a/apps/x/packages/core/src/models/repo.ts b/apps/x/packages/core/src/models/repo.ts index d941336c..4643951e 100644 --- a/apps/x/packages/core/src/models/repo.ts +++ b/apps/x/packages/core/src/models/repo.ts @@ -51,6 +51,7 @@ export class FSModelConfigRepo implements IModelConfigRepo { model: config.model, models: config.models, knowledgeGraphModel: config.knowledgeGraphModel, + meetingNotesModel: config.meetingNotesModel, }; const toWrite = { ...config, providers: existingProviders }; diff --git a/apps/x/packages/shared/src/models.ts b/apps/x/packages/shared/src/models.ts index 30403f94..2c1588e8 100644 --- a/apps/x/packages/shared/src/models.ts +++ b/apps/x/packages/shared/src/models.ts @@ -12,4 +12,5 @@ export const LlmModelConfig = z.object({ model: z.string(), models: z.array(z.string()).optional(), knowledgeGraphModel: z.string().optional(), + meetingNotesModel: z.string().optional(), }); From 29ee4b593022aac80f68841ea8a1e7dc333b82f5 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:32:56 +0530 Subject: [PATCH 08/97] hide Models tab in settings when signed in Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/x/apps/renderer/src/components/settings-dialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/x/apps/renderer/src/components/settings-dialog.tsx b/apps/x/apps/renderer/src/components/settings-dialog.tsx index f7df586b..1e38f720 100644 --- a/apps/x/apps/renderer/src/components/settings-dialog.tsx +++ b/apps/x/apps/renderer/src/components/settings-dialog.tsx @@ -1240,7 +1240,7 @@ function NoteTaggingSettings({ dialogOpen }: { dialogOpen: boolean }) { export function SettingsDialog({ children }: SettingsDialogProps) { const [open, setOpen] = useState(false) - const [activeTab, setActiveTab] = useState("models") + const [activeTab, setActiveTab] = useState("account") const [content, setContent] = useState("") const [originalContent, setOriginalContent] = useState("") const [loading, setLoading] = useState(false) @@ -1259,7 +1259,7 @@ export function SettingsDialog({ children }: SettingsDialogProps) { }) }, [open]) - const visibleTabs = useMemo(() => tabs, []) + const visibleTabs = useMemo(() => rowboatConnected ? tabs.filter(t => t.id !== "models") : tabs, [rowboatConnected]) const activeTabConfig = visibleTabs.find((t) => t.id === activeTab) ?? visibleTabs[0] const isJsonTab = activeTab === "mcp" || activeTab === "security" From 333ccb096bde947a42aaf6fec60c930b5dc69bb4 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:37:50 +0530 Subject: [PATCH 09/97] inject current date/time and chronological processing context into note creation agent Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/x/packages/core/src/knowledge/note_creation.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/x/packages/core/src/knowledge/note_creation.ts b/apps/x/packages/core/src/knowledge/note_creation.ts index adc1eafc..478ced81 100644 --- a/apps/x/packages/core/src/knowledge/note_creation.ts +++ b/apps/x/packages/core/src/knowledge/note_creation.ts @@ -27,6 +27,15 @@ tools: type: builtin name: workspace-glob --- +# Context + +**Current date and time:** ${new Date().toISOString()} + +Sources (emails, meetings, voice memos) are processed in roughly chronological order. This means: +- Earlier sources may reference events that have since occurred — later sources will provide updates. +- If a source mentions a future meeting or deadline, it may already be in the past by now. Use the current date above to reason about what is past vs. upcoming. +- Don't treat old commitments as still "open" if later sources or the current date suggest they've likely been resolved. + # Task You are a memory agent. Given a single source file (email, meeting transcript, or voice memo), you will: From 2d62824030f4b22815a1f9b47ebb6eefbe612b18 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:57:28 +0530 Subject: [PATCH 10/97] fix agent notes not populating user email when Gmail connected via Composio ensureUserEmail() only tried direct Google OAuth, which doesn't work when Gmail is connected through Composio. Now tries Composio first. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../core/src/knowledge/agent_notes.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/x/packages/core/src/knowledge/agent_notes.ts b/apps/x/packages/core/src/knowledge/agent_notes.ts index 433423ac..5ec3e801 100644 --- a/apps/x/packages/core/src/knowledge/agent_notes.ts +++ b/apps/x/packages/core/src/knowledge/agent_notes.ts @@ -7,6 +7,8 @@ import { bus } from '../runs/bus.js'; import { serviceLogger } from '../services/service_logger.js'; import { loadUserConfig, updateUserEmail } from '../config/user_config.js'; import { GoogleClientFactory } from './google-client-factory.js'; +import { useComposioForGoogle, executeAction } from '../composio/client.js'; +import { composioAccountsRepo } from '../composio/repo.js'; import { loadAgentNotesState, saveAgentNotesState, @@ -209,7 +211,30 @@ async function ensureUserEmail(): Promise { return existing.email; } - // Try to get email from Gmail profile + // Try Composio (used when signed in or composio configured) + try { + if (await useComposioForGoogle()) { + const account = composioAccountsRepo.getAccount('gmail'); + if (account && account.status === 'ACTIVE') { + const result = await executeAction('GMAIL_GET_PROFILE', { + connected_account_id: account.id, + user_id: 'rowboat-user', + version: 'latest', + arguments: { user_id: 'me' }, + }); + const email = (result.data as Record)?.emailAddress as string | undefined; + if (email) { + updateUserEmail(email); + console.log(`[AgentNotes] Auto-populated user email via Composio: ${email}`); + return email; + } + } + } + } catch (error) { + console.log('[AgentNotes] Could not fetch email via Composio:', error instanceof Error ? error.message : error); + } + + // Try direct Google OAuth try { const auth = await GoogleClientFactory.getClient(); if (auth) { From fa07a7535846c0ae1cc8f89acafdf28626768c3f Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:04:22 +0530 Subject: [PATCH 11/97] draft email options --- apps/x/apps/renderer/src/App.tsx | 14 +++ .../renderer/src/extensions/email-block.tsx | 92 +++++++++++++++++-- apps/x/apps/renderer/src/styles/editor.css | 92 +++++++++++++++++++ apps/x/packages/shared/src/blocks.ts | 1 + 4 files changed, 190 insertions(+), 9 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 20fa5435..591ef21e 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3490,6 +3490,20 @@ function App() { return () => window.removeEventListener('calendar-block:join-meeting', handler) }, []) + // Email block: draft with assistant + useEffect(() => { + const handler = () => { + const pending = window.__pendingEmailDraft + if (pending) { + setPresetMessage(pending.prompt) + setIsChatSidebarOpen(true) + window.__pendingEmailDraft = undefined + } + } + window.addEventListener('email-block:draft-with-assistant', handler) + return () => window.removeEventListener('email-block:draft-with-assistant', handler) + }, []) + const ensureWikiFile = useCallback(async (wikiPath: string) => { const resolvedPath = toKnowledgePath(wikiPath) if (!resolvedPath) return null diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx index 084f3ba2..c17625f3 100644 --- a/apps/x/apps/renderer/src/extensions/email-block.tsx +++ b/apps/x/apps/renderer/src/extensions/email-block.tsx @@ -1,6 +1,6 @@ import { mergeAttributes, Node } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' -import { X, Mail, ChevronDown, ExternalLink, Copy, Check, Sparkles, Loader2 } from 'lucide-react' +import { X, Mail, ChevronDown, ExternalLink, Copy, Check, Sparkles, Loader2, MessageSquare } from 'lucide-react' import { blocks } from '@x/shared' import { useState, useEffect, useRef, useCallback } from 'react' @@ -21,6 +21,12 @@ function getInitials(name: string): string { return name.split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase() } +declare global { + interface Window { + __pendingEmailDraft?: { prompt: string } + } +} + // --- Email Block --- function EmailBlockView({ node, deleteNode, updateAttributes }: { @@ -39,14 +45,27 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { const hasDraft = !!config?.draft_response const hasPastSummary = !!config?.past_summary + const responseMode = config?.response_mode || 'both' // Local draft state for editing const [draftBody, setDraftBody] = useState(config?.draft_response || '') const [contextExpanded, setContextExpanded] = useState(false) const [copied, setCopied] = useState(false) const [generating, setGenerating] = useState(false) + const [responseSplitOpen, setResponseSplitOpen] = useState(false) + const responseSplitRef = useRef(null) const bodyRef = useRef(null) + // Close split dropdown on outside click + useEffect(() => { + if (!responseSplitOpen) return + const handler = (e: MouseEvent) => { + if (responseSplitRef.current && !responseSplitRef.current.contains(e.target as Node)) setResponseSplitOpen(false) + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, [responseSplitOpen]) + // Sync draft from external changes useEffect(() => { try { @@ -105,6 +124,19 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { } }, [config, generating, raw, updateAttributes]) + const draftWithAssistant = useCallback(() => { + if (!config) return + let prompt = `Help me draft a response to this email` + if (config.threadId) { + prompt += `. Read the full thread at gmail_sync/${config.threadId}.md for context` + } + prompt += `.\n\n` + prompt += `**From:** ${config.from || 'Unknown'}\n` + prompt += `**Subject:** ${config.subject || 'No subject'}\n` + window.__pendingEmailDraft = { prompt } + window.dispatchEvent(new Event('email-block:draft-with-assistant')) + }, [config]) + if (!config) { return ( @@ -250,14 +282,56 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { {contextExpanded ? 'Hide' : 'Show'} context )} - + {responseMode === 'inline' && ( + + )} + {responseMode === 'assistant' && ( + + )} + {responseMode === 'both' && ( +
+ + + {responseSplitOpen && ( +
+ +
+ )} +
+ )} {gmailUrl && ( + + + + + + + +
+
Settings
+
+ + +
+
+ +
+ - +
+ + + + diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.js b/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.js new file mode 100644 index 00000000..6a3fc0b1 --- /dev/null +++ b/apps/x/packages/core/src/knowledge/chrome-extension/extension/popup.js @@ -0,0 +1,258 @@ +const SERVER_URL = 'http://localhost:3001'; + + +let currentDomain = null; +let currentStatus = null; +let currentConfig = null; + +async function getCurrentTab() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + return tab; +} + +function extractDomain(url) { + try { + const parsed = new URL(url); + return parsed.hostname; + } catch { + return null; + } +} + +function updateStatusBadge(status, serverReachable) { + const badge = document.getElementById('statusBadge'); + const statusText = document.getElementById('statusText'); + + badge.classList.remove('capturing', 'not-capturing', 'awaiting', 'error'); + + if (!serverReachable) { + badge.classList.add('error'); + statusText.textContent = 'Error'; + return; + } + + switch (status) { + case 'whitelisted': + case 'capturing': + badge.classList.add('capturing'); + statusText.textContent = 'Indexing'; + break; + case 'blacklisted': + badge.classList.add('not-capturing'); + statusText.textContent = 'Not indexing'; + break; + case 'unknown': + badge.classList.add('awaiting'); + statusText.textContent = 'Awaiting'; + break; + default: + badge.classList.add('not-capturing'); + statusText.textContent = 'Unknown'; + } +} + +function showApprovalSection(show) { + document.getElementById('approvalSection').classList.toggle('hidden', !show); +} + +function showToggleSection(show, isCapturing) { + const section = document.getElementById('toggleSection'); + const label = document.getElementById('toggleLabel'); + const btn = document.getElementById('toggleBtn'); + + section.classList.toggle('hidden', !show); + + if (isCapturing) { + label.textContent = 'Capturing this site'; + btn.textContent = 'Stop'; + btn.onclick = () => removeDomain('whitelist'); + } else { + label.textContent = 'Not capturing this site'; + btn.textContent = 'Start'; + btn.onclick = () => removeDomain('blacklist'); + } +} + +function showError(show) { + document.getElementById('errorMessage').classList.toggle('hidden', !show); +} + +// Settings section +function getSelectedMode(config) { + return config.mode === 'all' ? 'work' : 'ask'; +} + +function initSettings(config) { + currentConfig = config; + const mode = getSelectedMode(config); + + const radio = document.querySelector(`input[name="captureMode"][value="${mode}"]`); + if (radio) radio.checked = true; +} + +async function saveSettingsFromUI() { + const selectedRadio = document.querySelector('input[name="captureMode"]:checked'); + const mode = selectedRadio ? selectedRadio.value : 'ask'; + + let config; + if (mode === 'work') { + config = { + mode: 'all', + whitelist: currentConfig ? currentConfig.whitelist : [], + blacklist: currentConfig ? currentConfig.blacklist : [], + enabled: true + }; + } else { + config = { + mode: 'ask', + whitelist: currentConfig ? currentConfig.whitelist : [], + blacklist: currentConfig ? currentConfig.blacklist : [], + enabled: true + }; + } + + try { + await chrome.runtime.sendMessage({ type: 'SAVE_CONFIG', config }); + currentConfig = config; + await loadStatus(); + } catch (error) { + console.error('Failed to save settings:', error); + } +} + +// Domain status +async function loadStatus() { + const tab = await getCurrentTab(); + if (!tab || !tab.url) { + document.getElementById('domainDisplay').textContent = 'No page'; + return; + } + + currentDomain = extractDomain(tab.url); + if (!currentDomain) { + document.getElementById('domainDisplay').textContent = 'Invalid URL'; + return; + } + + document.getElementById('domainDisplay').textContent = currentDomain; + + try { + const response = await chrome.runtime.sendMessage({ + type: 'GET_DOMAIN_STATUS', + url: tab.url + }); + + currentStatus = response.status; + const serverReachable = response.serverReachable; + + updateStatusBadge(currentStatus, serverReachable); + showError(!serverReachable); + + if (!serverReachable) { + showApprovalSection(false); + showToggleSection(false, false); + return; + } + + if (currentStatus === 'unknown') { + showApprovalSection(true); + showToggleSection(false, false); + } else if (currentStatus === 'whitelisted' || currentStatus === 'capturing') { + showApprovalSection(false); + showToggleSection(true, true); + } else if (currentStatus === 'blacklisted') { + showApprovalSection(false); + showToggleSection(true, false); + } else { + showApprovalSection(false); + showToggleSection(false, false); + } + } catch (error) { + console.error('Failed to get status:', error); + showError(true); + } +} + +async function loadStats() { + try { + const response = await fetch(`${SERVER_URL}/status`); + if (response.ok) { + const data = await response.json(); + document.getElementById('statsCount').textContent = `${data.count} pages indexed locally`; + } + } catch (error) { + console.log('Failed to load stats:', error); + } +} + +async function approveDomain() { + if (!currentDomain) return; + try { + await chrome.runtime.sendMessage({ type: 'APPROVE_DOMAIN', domain: currentDomain }); + // Reload config to reflect the new whitelist in settings + const resp = await chrome.runtime.sendMessage({ type: 'GET_CONFIG' }); + if (resp && resp.config) initSettings(resp.config); + await loadStatus(); + } catch (error) { + console.error('Failed to approve domain:', error); + } +} + +async function rejectDomain() { + if (!currentDomain) return; + try { + await chrome.runtime.sendMessage({ type: 'REJECT_DOMAIN', domain: currentDomain }); + await loadStatus(); + } catch (error) { + console.error('Failed to reject domain:', error); + } +} + +async function captureOnce() { + try { + const response = await chrome.runtime.sendMessage({ type: 'CAPTURE_ONCE' }); + if (response.success) { + window.close(); + } + } catch (error) { + console.error('Failed to capture:', error); + } +} + +async function removeDomain(list) { + if (!currentDomain) return; + try { + const messageType = list === 'whitelist' ? 'REMOVE_FROM_WHITELIST' : 'REMOVE_FROM_BLACKLIST'; + await chrome.runtime.sendMessage({ type: messageType, domain: currentDomain }); + // Reload config to reflect changes in settings + const resp = await chrome.runtime.sendMessage({ type: 'GET_CONFIG' }); + if (resp && resp.config) initSettings(resp.config); + await loadStatus(); + } catch (error) { + console.error('Failed to remove domain:', error); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + // Load config and init settings + try { + const resp = await chrome.runtime.sendMessage({ type: 'GET_CONFIG' }); + if (resp && resp.config) { + initSettings(resp.config); + } + } catch (error) { + console.error('Failed to load config:', error); + } + + // Radio change listeners + document.querySelectorAll('input[name="captureMode"]').forEach(radio => { + radio.addEventListener('change', () => saveSettingsFromUI()); + }); + + loadStatus(); + loadStats(); + + document.getElementById('approveBtn').addEventListener('click', approveDomain); + document.getElementById('rejectBtn').addEventListener('click', rejectDomain); + document.getElementById('captureOnceBtn').addEventListener('click', captureOnce); +}); diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/extension/styles.css b/apps/x/packages/core/src/knowledge/chrome-extension/extension/styles.css new file mode 100644 index 00000000..399c473b --- /dev/null +++ b/apps/x/packages/core/src/knowledge/chrome-extension/extension/styles.css @@ -0,0 +1,279 @@ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f9fafb; + --bg-tertiary: #f3f4f6; + --text-primary: #111827; + --text-secondary: #6b7280; + --text-muted: #9ca3af; + --border-color: #e5e7eb; + --accent-color: #3b82f6; + --accent-hover: #2563eb; + --success-color: #10b981; + --warning-color: #f59e0b; + --error-color: #ef4444; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #1f2937; + --bg-secondary: #111827; + --bg-tertiary: #374151; + --text-primary: #f9fafb; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --border-color: #374151; + --accent-color: #60a5fa; + --accent-hover: #3b82f6; + --success-color: #34d399; + --warning-color: #fbbf24; + --error-color: #f87171; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.3); + } +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--text-primary); + background-color: var(--bg-primary); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + font-size: 14px; + font-weight: 500; + border-radius: 6px; + border: none; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background-color: var(--accent-color); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background-color: var(--accent-hover); +} + +.btn-secondary { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover:not(:disabled) { + background-color: var(--border-color); +} + +.btn-ghost { + background-color: transparent; + color: var(--text-secondary); +} + +.btn-ghost:hover:not(:disabled) { + background-color: var(--bg-tertiary); + color: var(--text-primary); +} + +.btn-sm { + padding: 4px 8px; + font-size: 12px; +} + +.btn-block { + width: 100%; +} + +/* Status badges */ +.status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 9999px; + font-size: 12px; + font-weight: 500; +} + +.status-badge.capturing { + background-color: rgba(16, 185, 129, 0.1); + color: var(--success-color); +} + +.status-badge.not-capturing { + background-color: rgba(107, 114, 128, 0.1); + color: var(--text-secondary); +} + +.status-badge.awaiting { + background-color: rgba(245, 158, 11, 0.1); + color: var(--warning-color); +} + +.status-badge.error { + background-color: rgba(239, 68, 68, 0.1); + color: var(--error-color); +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: currentColor; +} + +/* Cards */ +.card { + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; +} + +/* Form elements */ +.radio-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +.radio-option { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease; +} + +.radio-option:hover { + border-color: var(--accent-color); + background-color: var(--bg-secondary); +} + +.radio-option.selected { + border-color: var(--accent-color); + background-color: rgba(59, 130, 246, 0.05); +} + +.radio-option input[type="radio"] { + margin-top: 2px; + accent-color: var(--accent-color); +} + +.radio-option-content { + flex: 1; +} + +.radio-option-title { + font-weight: 500; + color: var(--text-primary); +} + +.radio-option-desc { + font-size: 13px; + color: var(--text-secondary); + margin-top: 2px; +} + +/* Toggle/Checkbox */ +.toggle-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 12px; + padding-left: 24px; +} + +.toggle-item { + display: flex; + align-items: center; + gap: 8px; +} + +.toggle-item input[type="checkbox"] { + accent-color: var(--accent-color); +} + +.toggle-item label { + font-size: 13px; + color: var(--text-secondary); + cursor: pointer; +} + +/* Divider */ +.divider { + height: 1px; + background-color: var(--border-color); + margin: 12px 0; +} + +/* Link */ +.link { + color: var(--accent-color); + text-decoration: none; + font-size: 13px; +} + +.link:hover { + text-decoration: underline; +} + +/* Text utilities */ +.text-sm { + font-size: 12px; +} + +.text-muted { + color: var(--text-muted); +} + +.text-secondary { + color: var(--text-secondary); +} + +.text-center { + text-align: center; +} + +/* Spacing utilities */ +.mt-1 { margin-top: 4px; } +.mt-2 { margin-top: 8px; } +.mt-3 { margin-top: 12px; } +.mt-4 { margin-top: 16px; } +.mb-1 { margin-bottom: 4px; } +.mb-2 { margin-bottom: 8px; } +.mb-3 { margin-bottom: 12px; } +.mb-4 { margin-bottom: 16px; } + +/* Flex utilities */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-1 { gap: 4px; } +.gap-2 { gap: 8px; } +.gap-3 { gap: 12px; } diff --git a/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts b/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts new file mode 100644 index 00000000..0cb127b9 --- /dev/null +++ b/apps/x/packages/core/src/knowledge/chrome-extension/server/server.ts @@ -0,0 +1,281 @@ +import express from 'express'; +import cors from 'cors'; +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import { WorkDir } from '../../../config/config.js'; + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: '10mb' })); + +const CAPTURED_PAGES_DIR = path.join(WorkDir, 'chrome_sync'); +const CONFIG_DIR = path.join(WorkDir, 'config'); +const CONFIG_FILE = path.join(CONFIG_DIR, 'chrome-plugin.json'); + +interface Config { + mode: 'all' | 'ask'; + whitelist: string[]; + blacklist: string[]; + enabled: boolean; +} + +const DEFAULT_CONFIG: Config = { + mode: 'ask', + whitelist: [], + blacklist: [], + enabled: true +}; + +const contentHashes = new Map(); + +function extractDomain(url: string): string { + try { + const parsed = new URL(url); + return parsed.host || 'unknown'; + } catch { + return 'unknown'; + } +} + +function pathToSlug(url: string): string { + try { + const parsed = new URL(url); + const p = parsed.pathname + (parsed.search || ''); + if (!p || p === '/') return 'index'; + let slug = p.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, ''); + return slug.substring(0, 80) || 'index'; + } catch { + return 'index'; + } +} + +function hashContent(content: string): string { + return crypto.createHash('sha256').update(content, 'utf-8').digest('hex'); +} + +function findExistingFile(domainDir: string, pathSlug: string): string | null { + if (!fs.existsSync(domainDir)) return null; + const files = fs.readdirSync(domainDir); + for (const filename of files) { + if (filename.endsWith(`_${pathSlug}.md`)) { + return path.join(domainDir, filename); + } + } + return null; +} + +// POST /capture +app.post('/capture', (req, res) => { + const data = req.body; + if (!data) { + return res.status(400).json({ error: 'No JSON data provided' }); + } + + const { url, content = '', timestamp, title = 'Untitled' } = data; + + if (!url || !timestamp) { + return res.status(400).json({ error: 'Missing required fields: url, timestamp' }); + } + + const domain = extractDomain(url); + const pathSlug = pathToSlug(url); + const contentHash = hashContent(content); + const cacheKey = `${domain}/${pathSlug}`; + + const dt = new Date(timestamp); + const year = dt.getFullYear(); + const month = String(dt.getMonth() + 1).padStart(2, '0'); + const day = String(dt.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}`; + const hours = String(dt.getHours()).padStart(2, '0'); + const minutes = String(dt.getMinutes()).padStart(2, '0'); + const seconds = String(dt.getSeconds()).padStart(2, '0'); + const timeStr = `${hours}-${minutes}`; + const timeDisplay = `${hours}:${minutes}:${seconds}`; + const tzOffset = -dt.getTimezoneOffset(); + const tzSign = tzOffset >= 0 ? '+' : '-'; + const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, '0'); + const tzMins = String(Math.abs(tzOffset) % 60).padStart(2, '0'); + const isoTimestamp = `${dateStr}T${hours}:${minutes}:${seconds}${tzSign}${tzHours}:${tzMins}`; + + // date/domain directory structure + const domainDir = path.join(CAPTURED_PAGES_DIR, dateStr, domain); + fs.mkdirSync(domainDir, { recursive: true }); + + const existingFile = findExistingFile(domainDir, pathSlug); + if (existingFile && contentHashes.get(cacheKey) === contentHash) { + return res.json({ status: 'skipped', reason: 'duplicate content' }); + } + + contentHashes.set(cacheKey, contentHash); + + // If file exists, append with scroll separator + if (existingFile) { + const scrollSeparator = `\n\n---\n📜 Scroll captured at ${timeDisplay}\n---\n\n`; + fs.appendFileSync(existingFile, scrollSeparator + content, 'utf-8'); + const rel = `${dateStr}/${domain}/${path.basename(existingFile)}`; + return res.json({ status: 'appended', filename: rel }); + } + + // New file - create with frontmatter + const filename = `${timeStr}_${pathSlug}.md`; + const filepath = path.join(domainDir, filename); + + const markdownContent = `--- +url: ${url} +title: ${title} +captured_at: ${isoTimestamp} +--- + +${content} +`; + + fs.writeFileSync(filepath, markdownContent, 'utf-8'); + return res.status(201).json({ status: 'captured', filename: `${dateStr}/${domain}/${filename}` }); +}); + +// GET /status +app.get('/status', (_req, res) => { + let count = 0; + const domains: Record = {}; + + if (!fs.existsSync(CAPTURED_PAGES_DIR)) { + return res.json({ count: 0, domains: [] }); + } + + for (const dateEntry of fs.readdirSync(CAPTURED_PAGES_DIR)) { + const datePath = path.join(CAPTURED_PAGES_DIR, dateEntry); + if (!fs.statSync(datePath).isDirectory()) continue; + + for (const domainEntry of fs.readdirSync(datePath)) { + const domainPath = path.join(datePath, domainEntry); + if (!fs.statSync(domainPath).isDirectory()) continue; + + const domainCount = fs.readdirSync(domainPath).filter(f => f.endsWith('.md')).length; + count += domainCount; + if (domainCount > 0) { + domains[domainEntry] = (domains[domainEntry] || 0) + domainCount; + } + } + } + + const domainList = Object.entries(domains) + .map(([domain, c]) => ({ domain, count: c })) + .sort((a, b) => b.count - a.count); + + return res.json({ count, domains: domainList }); +}); + +// Config helpers +function loadConfig(): Config { + if (fs.existsSync(CONFIG_FILE)) { + try { + const raw = fs.readFileSync(CONFIG_FILE, 'utf-8'); + return JSON.parse(raw); + } catch { + // fall through + } + } + return { ...DEFAULT_CONFIG }; +} + +function saveConfig(config: Config): void { + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8'); +} + +function validateConfig(data: any): data is Config { + if (typeof data !== 'object' || data === null) return false; + if (data.mode !== 'all' && data.mode !== 'ask') return false; + if (!Array.isArray(data.whitelist)) return false; + if (!Array.isArray(data.blacklist)) return false; + if (typeof data.enabled !== 'boolean') return false; + return true; +} + +// GET /browse/config +app.get('/browse/config', (_req, res) => { + const config = loadConfig(); + return res.json(config); +}); + +// POST /browse/config +app.post('/browse/config', (req, res) => { + const data = req.body; + if (!data) { + return res.status(400).json({ error: 'No JSON data provided' }); + } + + if (!validateConfig(data)) { + return res.status(400).json({ error: 'Invalid config shape' }); + } + + saveConfig(data); + return res.json({ status: 'saved', config: data }); +}); + +const PORT = 3001; +const RETENTION_DAYS = 7; +const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour + +function cleanUpOldFiles(): void { + if (!fs.existsSync(CAPTURED_PAGES_DIR)) return; + + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - RETENTION_DAYS); + const cutoffStr = cutoff.toISOString().slice(0, 10); // YYYY-MM-DD + + for (const dateEntry of fs.readdirSync(CAPTURED_PAGES_DIR)) { + // only process date-formatted directories + if (!/^\d{4}-\d{2}-\d{2}$/.test(dateEntry)) continue; + if (dateEntry >= cutoffStr) continue; + + const datePath = path.join(CAPTURED_PAGES_DIR, dateEntry); + if (!fs.statSync(datePath).isDirectory()) continue; + + fs.rmSync(datePath, { recursive: true, force: true }); + console.log(`[ChromeSync] Cleaned up old captures: ${dateEntry}`); + } +} + +function isServerEnabled(): boolean { + if (!fs.existsSync(CONFIG_FILE)) return false; + try { + const raw = fs.readFileSync(CONFIG_FILE, 'utf-8'); + const config = JSON.parse(raw); + return config.serverEnabled === true; + } catch { + return false; + } +} + +function startServer(): void { + fs.mkdirSync(CAPTURED_PAGES_DIR, { recursive: true }); + + cleanUpOldFiles(); + setInterval(cleanUpOldFiles, CLEANUP_INTERVAL_MS); + + app.listen(PORT, 'localhost', () => { + console.log('[ChromeSync] Server starting.'); + console.log(` Captured pages: ${CAPTURED_PAGES_DIR}`); + console.log(` Config: ${CONFIG_FILE}`); + console.log(` Listening on http://localhost:${PORT}`); + }); +} + +export async function init(): Promise { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + + if (isServerEnabled()) { + startServer(); + return; + } + + console.log('[ChromeSync] Server disabled, watching config for changes...'); + fs.watch(CONFIG_DIR, (_, filename) => { + if (filename === 'chrome-plugin.json' && isServerEnabled()) { + console.log('[ChromeSync] serverEnabled set to true, starting server...'); + startServer(); + } + }); +} diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index c4e701a5..e59ce990 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -53,9 +53,6 @@ importers: electron-squirrel-startup: specifier: ^1.0.1 version: 1.0.1 - html-to-docx: - specifier: ^1.8.0 - version: 1.8.0(encoding@0.1.13) mammoth: specifier: ^1.11.0 version: 1.11.0 @@ -238,9 +235,6 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.3(react@19.2.3) - recharts: - specifier: ^3.8.0 - version: 3.8.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -310,16 +304,16 @@ importers: dependencies: '@ai-sdk/anthropic': specifier: ^2.0.63 - version: 2.0.63(zod@4.2.1) + version: 2.0.70(zod@4.2.1) '@ai-sdk/google': specifier: ^2.0.53 - version: 2.0.53(zod@4.2.1) + version: 2.0.61(zod@4.2.1) '@ai-sdk/openai': specifier: ^2.0.91 - version: 2.0.91(zod@4.2.1) + version: 2.0.99(zod@4.2.1) '@ai-sdk/openai-compatible': specifier: ^1.0.33 - version: 1.0.33(zod@4.2.1) + version: 1.0.34(zod@4.2.1) '@ai-sdk/provider': specifier: ^2.0.1 version: 2.0.1 @@ -334,7 +328,7 @@ importers: version: 1.25.1(hono@4.11.3)(zod@4.2.1) '@openrouter/ai-sdk-provider': specifier: ^1.2.6 - version: 1.5.4(ai@5.0.133(zod@4.2.1))(zod@4.2.1) + version: 1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1) '@react-pdf/renderer': specifier: ^4.3.2 version: 4.3.2(react@19.2.3) @@ -346,16 +340,22 @@ importers: version: link:../shared ai: specifier: ^5.0.133 - version: 5.0.133(zod@4.2.1) + version: 5.0.151(zod@4.2.1) awilix: specifier: ^12.0.5 version: 12.0.5 chokidar: specifier: ^4.0.3 version: 4.0.3 + cors: + specifier: ^2.8.6 + version: 2.8.6 cron-parser: specifier: ^5.5.0 version: 5.5.0 + express: + specifier: ^5.2.1 + version: 5.2.1 glob: specifier: ^13.0.0 version: 13.0.0 @@ -399,6 +399,12 @@ importers: specifier: ^4.2.1 version: 4.2.1 devDependencies: + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 '@types/node': specifier: ^25.0.3 version: 25.0.3 @@ -417,8 +423,8 @@ importers: packages: - '@ai-sdk/anthropic@2.0.63': - resolution: {integrity: sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA==} + '@ai-sdk/anthropic@2.0.70': + resolution: {integrity: sha512-W3WjQlb0Ho+CVAQUvb8Rtk3hGS3Jlgy79ihY2H0yj2k4yU8XuxpQw0Oz+7JQsB47j+jlHhk7nUXtxhAeRg3S3Q==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -429,26 +435,26 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@2.0.39': - resolution: {integrity: sha512-ULnefGmRHG0/tRrf+dtDwgQYAttGi/TR0FmASAzTs1dtpeZp4Xoh1VyWrX3Z1bM3WDs9RM3ZeSE77kQT/jbfjw==} + '@ai-sdk/gateway@2.0.56': + resolution: {integrity: sha512-omvb2Bwpgqg8PKqOpYdIaW+fdEIWcfm2B/j3dx37DxzOIt6fr57VVcfw7pu/EaACcY0O+wsg50iFCPGcsI2Cbg==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/google@2.0.53': - resolution: {integrity: sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ==} + '@ai-sdk/google@2.0.61': + resolution: {integrity: sha512-hIs7UvL8X5MBG3uxdciSotD4I27UcMa4/we9Qf98fM/RgMTwyk9zXcr7GM6k5yLBZ5S0QeZWkfqKwtdiDnUEEQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai-compatible@1.0.33': - resolution: {integrity: sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ==} + '@ai-sdk/openai-compatible@1.0.34': + resolution: {integrity: sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@2.0.91': - resolution: {integrity: sha512-lozfRHfSTHg5/UliQjTDcOtISYGbEpt4FS/6QM5PcLmhdT0HmROllaBmG7+JaK+uqFtDXZGgMIpz3bqB9nzqCQ==} + '@ai-sdk/openai@2.0.99': + resolution: {integrity: sha512-wwa1/DuO9XThaA+sAi0d3+xfkbEx9nRhZ1USV6kktndmEs8aQRR0DJK/Iec+mwNu06IhfDGd5vMscR1U1q155g==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -459,8 +465,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@3.0.21': - resolution: {integrity: sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q==} + '@ai-sdk/provider-utils@3.0.22': + resolution: {integrity: sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -1572,46 +1578,6 @@ packages: '@octokit/types@6.41.0': resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} - '@oozcitak/dom@1.15.5': - resolution: {integrity: sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==} - engines: {node: '>=8.0'} - - '@oozcitak/dom@1.15.6': - resolution: {integrity: sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==} - engines: {node: '>=8.0'} - - '@oozcitak/infra@1.0.3': - resolution: {integrity: sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==} - engines: {node: '>=6.0'} - - '@oozcitak/infra@1.0.5': - resolution: {integrity: sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==} - engines: {node: '>=6.0'} - - '@oozcitak/url@1.0.0': - resolution: {integrity: sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==} - engines: {node: '>=8.0'} - - '@oozcitak/util@1.0.1': - resolution: {integrity: sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==} - engines: {node: '>=6.0'} - - '@oozcitak/util@1.0.2': - resolution: {integrity: sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==} - engines: {node: '>=6.0'} - - '@oozcitak/util@8.0.0': - resolution: {integrity: sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==} - engines: {node: '>=6.0'} - - '@oozcitak/util@8.3.3': - resolution: {integrity: sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==} - engines: {node: '>=6.0'} - - '@oozcitak/util@8.3.4': - resolution: {integrity: sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==} - engines: {node: '>=8.0'} - '@openrouter/ai-sdk-provider@1.5.4': resolution: {integrity: sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw==} engines: {node: '>=18'} @@ -2550,17 +2516,6 @@ packages: '@react-pdf/types@2.9.2': resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==} - '@reduxjs/toolkit@2.11.2': - resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -2921,9 +2876,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -3221,9 +3173,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -3335,6 +3296,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -3347,6 +3314,9 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3398,6 +3368,12 @@ packages: '@types/pdf-parse@1.1.5': resolution: {integrity: sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==} + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3409,6 +3385,12 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -3617,8 +3599,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - ai@5.0.133: - resolution: {integrity: sha512-N6KnwSWKcXEWPnAri3anRuzRvcrvtDz1W1JG9CvMrQ0Xdp8Vu8ZToNW/eHt63CmrbmzTwVw/HaCtJuO+MYtS7A==} + ai@5.0.151: + resolution: {integrity: sha512-tsLIv+QN9wJ/xl/fnYgjRYoGSThdnOfU4d6+7QEUKX3EcwviWMEaL1gOE+zfdkcay/Tbc02ZBtTRHoulS6DYvQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -3793,9 +3775,6 @@ packages: brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} - browser-split@0.0.1: - resolution: {integrity: sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==} - browserify-zlib@0.2.0: resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} @@ -3857,9 +3836,6 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - camelize@1.0.1: - resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001761: resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} @@ -4057,8 +4033,8 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} cose-base@1.0.3: @@ -4287,9 +4263,6 @@ packages: supports-color: optional: true - decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -4358,24 +4331,12 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} - dom-serializer@0.2.2: - resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} - dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dom-walk@0.1.2: - resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} - - domelementtype@1.3.1: - resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} - domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - domhandler@2.4.2: - resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} - domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -4383,9 +4344,6 @@ packages: dompurify@3.3.1: resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} - domutils@1.7.0: - resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} - domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -4470,16 +4428,6 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} - ent@2.2.2: - resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==} - engines: {node: '>= 0.4'} - - entities@1.1.2: - resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} - - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -4498,9 +4446,6 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - error@4.4.0: - resolution: {integrity: sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -4520,9 +4465,6 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-toolkit@1.45.1: - resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} - es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -4623,9 +4565,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - ev-store@7.0.0: - resolution: {integrity: sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -4945,7 +4884,6 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.0: @@ -4954,7 +4892,7 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -4969,9 +4907,6 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} - global@4.4.0: - resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -5115,24 +5050,12 @@ packages: hsl-to-rgb-for-reals@1.1.1: resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==} - html-entities@2.6.0: - resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} - - html-to-docx@1.8.0: - resolution: {integrity: sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==} - - html-to-vdom@0.7.0: - resolution: {integrity: sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==} - html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlparser2@3.10.1: - resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} - http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -5190,23 +5113,9 @@ packages: engines: {node: '>=6.9.0'} hasBin: true - image-size@1.2.1: - resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} - engines: {node: '>=16.x'} - hasBin: true - - image-to-base64@2.2.0: - resolution: {integrity: sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==} - immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - - immer@11.1.4: - resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -5223,9 +5132,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - individual@3.0.0: - resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} - infer-owner@1.0.4: resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} @@ -5326,9 +5232,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-object@1.0.2: - resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -5339,10 +5242,6 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -5937,9 +5836,6 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - min-document@2.19.2: - resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} - minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -6069,9 +5965,6 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next-tick@0.2.2: - resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==} - nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -6532,9 +6425,6 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} - punycode@1.4.1: - resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6594,18 +6484,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-redux@9.2.0: - resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} - peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -6671,26 +6549,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - recharts@3.8.0: - resolution: {integrity: sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==} - engines: {node: '>=18'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -6763,9 +6625,6 @@ packages: resolution: {integrity: sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==} engines: {node: '>=14', npm: '>=7'} - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -6854,10 +6713,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -7053,9 +6908,6 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 - string-template@0.2.1: - resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -7141,7 +6993,7 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me temp@0.9.4: resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} @@ -7174,9 +7026,6 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -7306,8 +7155,8 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - underscore@1.13.7: - resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -7448,12 +7297,6 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - - virtual-dom@2.1.1: - resolution: {integrity: sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==} - vite-compatible-readable-stream@3.6.1: resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} engines: {node: '>= 6'} @@ -7603,21 +7446,11 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - x-is-array@0.1.0: - resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==} - - x-is-string@0.1.0: - resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==} - xlsx@0.18.5: resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} engines: {node: '>=0.8'} hasBin: true - xmlbuilder2@2.1.2: - resolution: {integrity: sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==} - engines: {node: '>=8.0'} - xmlbuilder@10.1.1: resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} engines: {node: '>=4.0'} @@ -7694,10 +7527,10 @@ packages: snapshots: - '@ai-sdk/anthropic@2.0.63(zod@4.2.1)': + '@ai-sdk/anthropic@2.0.70(zod@4.2.1)': dependencies: '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1) zod: 4.2.1 '@ai-sdk/gateway@2.0.24(zod@4.2.1)': @@ -7707,29 +7540,29 @@ snapshots: '@vercel/oidc': 3.0.5 zod: 4.2.1 - '@ai-sdk/gateway@2.0.39(zod@4.2.1)': + '@ai-sdk/gateway@2.0.56(zod@4.2.1)': dependencies: '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1) '@vercel/oidc': 3.1.0 zod: 4.2.1 - '@ai-sdk/google@2.0.53(zod@4.2.1)': + '@ai-sdk/google@2.0.61(zod@4.2.1)': dependencies: '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1) zod: 4.2.1 - '@ai-sdk/openai-compatible@1.0.33(zod@4.2.1)': + '@ai-sdk/openai-compatible@1.0.34(zod@4.2.1)': dependencies: '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1) zod: 4.2.1 - '@ai-sdk/openai@2.0.91(zod@4.2.1)': + '@ai-sdk/openai@2.0.99(zod@4.2.1)': dependencies: '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1) zod: 4.2.1 '@ai-sdk/provider-utils@3.0.20(zod@4.2.1)': @@ -7739,7 +7572,7 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.2.1 - '@ai-sdk/provider-utils@3.0.21(zod@4.2.1)': + '@ai-sdk/provider-utils@3.0.22(zod@4.2.1)': dependencies: '@ai-sdk/provider': 2.0.1 '@standard-schema/spec': 1.1.0 @@ -9240,7 +9073,7 @@ snapshots: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) content-type: 1.0.5 - cors: 2.8.5 + cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.6 @@ -9397,45 +9230,10 @@ snapshots: dependencies: '@octokit/openapi-types': 12.11.0 - '@oozcitak/dom@1.15.5': - dependencies: - '@oozcitak/infra': 1.0.5 - '@oozcitak/url': 1.0.0 - '@oozcitak/util': 8.0.0 - - '@oozcitak/dom@1.15.6': - dependencies: - '@oozcitak/infra': 1.0.5 - '@oozcitak/url': 1.0.0 - '@oozcitak/util': 8.3.4 - - '@oozcitak/infra@1.0.3': - dependencies: - '@oozcitak/util': 1.0.1 - - '@oozcitak/infra@1.0.5': - dependencies: - '@oozcitak/util': 8.0.0 - - '@oozcitak/url@1.0.0': - dependencies: - '@oozcitak/infra': 1.0.3 - '@oozcitak/util': 1.0.2 - - '@oozcitak/util@1.0.1': {} - - '@oozcitak/util@1.0.2': {} - - '@oozcitak/util@8.0.0': {} - - '@oozcitak/util@8.3.3': {} - - '@oozcitak/util@8.3.4': {} - - '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.133(zod@4.2.1))(zod@4.2.1)': + '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)': dependencies: '@openrouter/sdk': 0.1.27 - ai: 5.0.133(zod@4.2.1) + ai: 5.0.151(zod@4.2.1) zod: 4.2.1 '@openrouter/sdk@0.1.27': @@ -10461,18 +10259,6 @@ snapshots: '@react-pdf/primitives': 4.1.1 '@react-pdf/stylesheet': 6.1.2 - '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': - dependencies: - '@standard-schema/spec': 1.1.0 - '@standard-schema/utils': 0.3.0 - immer: 11.1.4 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 - optionalDependencies: - react: 19.2.3 - react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) - '@remirror/core-constants@3.0.0': {} '@rolldown/pluginutils@1.0.0-beta.53': {} @@ -10918,8 +10704,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@standard-schema/utils@0.3.0': {} - '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -11232,6 +11016,11 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.0.3 + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.4 @@ -11239,6 +11028,14 @@ snapshots: '@types/node': 25.0.3 '@types/responselike': 1.0.3 + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.0.3 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 25.0.3 + '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': @@ -11378,6 +11175,19 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 25.0.3 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + '@types/fs-extra@9.0.13': dependencies: '@types/node': 25.0.3 @@ -11391,6 +11201,8 @@ snapshots: '@types/http-cache-semantics@4.0.4': {} + '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} '@types/katex@0.16.7': {} @@ -11447,6 +11259,10 @@ snapshots: dependencies: '@types/node': 25.0.3 + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -11459,6 +11275,15 @@ snapshots: dependencies: '@types/node': 25.0.3 + '@types/send@1.2.1': + dependencies: + '@types/node': 25.0.3 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.0.3 + '@types/trusted-types@2.0.7': optional: true @@ -11718,11 +11543,11 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.2.1 - ai@5.0.133(zod@4.2.1): + ai@5.0.151(zod@4.2.1): dependencies: - '@ai-sdk/gateway': 2.0.39(zod@4.2.1) + '@ai-sdk/gateway': 2.0.56(zod@4.2.1) '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.21(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.22(zod@4.2.1) '@opentelemetry/api': 1.9.0 zod: 4.2.1 @@ -11904,8 +11729,6 @@ snapshots: dependencies: base64-js: 1.5.1 - browser-split@0.0.1: {} - browserify-zlib@0.2.0: dependencies: pako: 1.0.11 @@ -12000,8 +11823,6 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 - camelize@1.0.1: {} - caniuse-lite@1.0.30001761: {} ccount@2.0.1: {} @@ -12173,7 +11994,7 @@ snapshots: core-util-is@1.0.3: {} - cors@2.8.5: + cors@2.8.6: dependencies: object-assign: 4.1.1 vary: 1.1.2 @@ -12422,8 +12243,6 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js-light@2.5.1: {} - decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -12487,27 +12306,14 @@ snapshots: minimatch: 3.1.2 p-limit: 3.1.0 - dom-serializer@0.2.2: - dependencies: - domelementtype: 2.3.0 - entities: 2.2.0 - dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 - dom-walk@0.1.2: {} - - domelementtype@1.3.1: {} - domelementtype@2.3.0: {} - domhandler@2.4.2: - dependencies: - domelementtype: 1.3.1 - domhandler@5.0.3: dependencies: domelementtype: 2.3.0 @@ -12516,11 +12322,6 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - domutils@1.7.0: - dependencies: - dom-serializer: 0.2.2 - domelementtype: 1.3.1 - domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -12536,7 +12337,7 @@ snapshots: duck@0.1.12: dependencies: - underscore: 1.13.7 + underscore: 1.13.8 dunder-proto@1.0.1: dependencies: @@ -12661,17 +12462,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 - ent@2.2.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - punycode: 1.4.1 - safe-regex-test: 1.1.0 - - entities@1.1.2: {} - - entities@2.2.0: {} - entities@4.5.0: {} entities@6.0.1: {} @@ -12684,12 +12474,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - error@4.4.0: - dependencies: - camelize: 1.0.1 - string-template: 0.2.1 - xtend: 4.0.2 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -12707,8 +12491,6 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-toolkit@1.45.1: {} - es6-error@4.1.1: optional: true @@ -12873,10 +12655,6 @@ snapshots: etag@1.8.1: {} - ev-store@7.0.0: - dependencies: - individual: 3.0.0 - event-target-shim@5.0.1: {} eventemitter3@5.0.1: {} @@ -13320,11 +13098,6 @@ snapshots: dependencies: ini: 2.0.0 - global@4.4.0: - dependencies: - min-document: 2.19.2 - process: 0.11.10 - globals@14.0.0: {} globals@16.5.0: {} @@ -13571,44 +13344,10 @@ snapshots: hsl-to-rgb-for-reals@1.1.1: {} - html-entities@2.6.0: {} - - html-to-docx@1.8.0(encoding@0.1.13): - dependencies: - '@oozcitak/dom': 1.15.6 - '@oozcitak/util': 8.3.4 - color-name: 1.1.4 - html-entities: 2.6.0 - html-to-vdom: 0.7.0 - image-size: 1.2.1 - image-to-base64: 2.2.0(encoding@0.1.13) - jszip: 3.10.1 - lodash: 4.17.21 - mime-types: 2.1.35 - nanoid: 3.3.11 - virtual-dom: 2.1.1 - xmlbuilder2: 2.1.2 - transitivePeerDependencies: - - encoding - - html-to-vdom@0.7.0: - dependencies: - ent: 2.2.2 - htmlparser2: 3.10.1 - html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} - htmlparser2@3.10.1: - dependencies: - domelementtype: 1.3.1 - domhandler: 2.4.2 - domutils: 1.7.0 - entities: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - http-cache-semantics@4.2.0: {} http-errors@2.0.1: @@ -13673,22 +13412,8 @@ snapshots: image-size@0.7.5: optional: true - image-size@1.2.1: - dependencies: - queue: 6.0.2 - - image-to-base64@2.2.0(encoding@0.1.13): - dependencies: - node-fetch: 2.7.0(encoding@0.1.13) - transitivePeerDependencies: - - encoding - immediate@3.0.6: {} - immer@10.2.0: {} - - immer@11.1.4: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -13701,8 +13426,6 @@ snapshots: indent-string@4.0.0: {} - individual@3.0.0: {} - infer-owner@1.0.4: {} inflight@1.0.6: @@ -13777,8 +13500,6 @@ snapshots: is-number@7.0.0: {} - is-object@1.0.2: {} - is-plain-obj@4.1.0: {} is-promise@4.0.0: {} @@ -13786,13 +13507,6 @@ snapshots: is-property@1.0.2: optional: true - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - is-stream@1.1.0: {} is-stream@2.0.1: {} @@ -14078,7 +13792,7 @@ snapshots: dependencies: duck: 0.1.12 option: 0.2.4 - underscore: 1.13.7 + underscore: 1.13.8 lower-case@2.0.2: dependencies: @@ -14147,7 +13861,7 @@ snapshots: jszip: 3.10.1 lop: 0.4.2 path-is-absolute: 1.0.1 - underscore: 1.13.7 + underscore: 1.13.8 xmlbuilder: 10.1.1 map-age-cleaner@0.1.3: @@ -14638,10 +14352,6 @@ snapshots: mimic-response@3.1.0: {} - min-document@2.19.2: - dependencies: - dom-walk: 0.1.2 - minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -14757,8 +14467,6 @@ snapshots: neo-async@2.6.2: {} - next-tick@0.2.2: {} - nice-try@1.0.5: {} no-case@3.0.4: @@ -15247,8 +14955,6 @@ snapshots: punycode.js@2.3.1: {} - punycode@1.4.1: {} - punycode@2.3.1: {} pusher-js@8.4.0: @@ -15358,15 +15064,6 @@ snapshots: react-is@16.13.1: {} - react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1): - dependencies: - '@types/use-sync-external-store': 0.0.6 - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) - optionalDependencies: - '@types/react': 19.2.7 - redux: 5.0.1 - react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3): @@ -15441,36 +15138,10 @@ snapshots: readdirp@4.1.2: {} - recharts@3.8.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1): - dependencies: - '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3) - clsx: 2.1.1 - decimal.js-light: 2.5.1 - es-toolkit: 1.45.1 - eventemitter3: 5.0.1 - immer: 10.2.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-is: 16.13.1 - react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) - reselect: 5.1.1 - tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.3) - victory-vendor: 37.3.6 - transitivePeerDependencies: - - '@types/react' - - redux - rechoir@0.8.0: dependencies: resolve: 1.22.11 - redux-thunk@3.1.0(redux@5.0.1): - dependencies: - redux: 5.0.1 - - redux@5.0.1: {} - regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -15577,8 +15248,6 @@ snapshots: dependencies: pe-library: 1.0.1 - reselect@5.1.1: {} - resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -15697,12 +15366,6 @@ snapshots: safe-buffer@5.2.1: {} - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - safer-buffer@2.1.2: {} scheduler@0.25.0-rc-603e6108-20241029: {} @@ -15953,8 +15616,6 @@ snapshots: - micromark-util-types - supports-color - string-template@0.2.1: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -16072,8 +15733,6 @@ snapshots: tiny-inflate@1.0.3: {} - tiny-invariant@1.3.3: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -16194,7 +15853,7 @@ snapshots: ufo@1.6.1: {} - underscore@1.13.7: {} + underscore@1.13.8: {} undici-types@6.21.0: {} @@ -16345,34 +16004,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - victory-vendor@37.3.6: - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.7 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - - virtual-dom@2.1.1: - dependencies: - browser-split: 0.0.1 - error: 4.4.0 - ev-store: 7.0.0 - global: 4.4.0 - is-object: 1.0.2 - next-tick: 0.2.2 - x-is-array: 0.1.0 - x-is-string: 0.1.0 - vite-compatible-readable-stream@3.6.1: dependencies: inherits: 2.0.4 @@ -16524,10 +16155,6 @@ snapshots: wrappy@1.0.2: {} - x-is-array@0.1.0: {} - - x-is-string@0.1.0: {} - xlsx@0.18.5: dependencies: adler-32: 1.3.1 @@ -16538,17 +16165,12 @@ snapshots: wmf: 1.0.2 word: 0.3.0 - xmlbuilder2@2.1.2: - dependencies: - '@oozcitak/dom': 1.15.5 - '@oozcitak/infra': 1.0.5 - '@oozcitak/util': 8.3.3 - xmlbuilder@10.1.1: {} xmlbuilder@15.1.1: {} - xtend@4.0.2: {} + xtend@4.0.2: + optional: true y18n@5.0.8: {} From 86cc2aaf7340afaad5ca8cf3a5d695c8291a6b01 Mon Sep 17 00:00:00 2001 From: arkml <6592213+arkml@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:31:49 +0530 Subject: [PATCH 19/97] Meeting notes2 (#454) Improve meeting transcription: screen recording permissions, collapsible transcript block --- apps/x/apps/main/forge.config.cjs | 3 + apps/x/apps/main/src/ipc.ts | 20 +- apps/x/apps/renderer/src/App.tsx | 76 +-- .../src/components/markdown-editor.tsx | 4 + .../src/extensions/transcript-block.tsx | 177 +++++++ .../src/hooks/useMeetingTranscription.ts | 161 +++--- apps/x/apps/renderer/src/styles/editor.css | 77 ++- .../core/src/knowledge/summarize_meeting.ts | 3 +- apps/x/packages/shared/src/blocks.ts | 6 + apps/x/packages/shared/src/ipc.ts | 10 + apps/x/pnpm-lock.yaml | 464 +++++++++++++++++- 11 files changed, 885 insertions(+), 116 deletions(-) create mode 100644 apps/x/apps/renderer/src/extensions/transcript-block.tsx diff --git a/apps/x/apps/main/forge.config.cjs b/apps/x/apps/main/forge.config.cjs index c79a8c43..178cb7e1 100644 --- a/apps/x/apps/main/forge.config.cjs +++ b/apps/x/apps/main/forge.config.cjs @@ -11,6 +11,9 @@ module.exports = { icon: './icons/icon', // .icns extension added automatically appBundleId: 'com.rowboat.app', appCategoryType: 'public.app-category.productivity', + extendInfo: { + NSAudioCaptureUsageDescription: 'Rowboat needs access to system audio to transcribe meetings from other apps (Zoom, Meet, etc.)', + }, osxSign: { batchCodesignCalls: true, optionsForFile: () => ({ diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index b92e3fe9..0fa0de79 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -1,4 +1,4 @@ -import { ipcMain, BrowserWindow, shell, dialog } from 'electron'; +import { ipcMain, BrowserWindow, shell, dialog, systemPreferences, desktopCapturer } from 'electron'; import { ipc } from '@x/shared'; import path from 'node:path'; import os from 'node:os'; @@ -719,6 +719,24 @@ export function setupIpcHandlers() { return { success: false, error: 'Unknown format' }; }, + 'meeting:checkScreenPermission': async () => { + if (process.platform !== 'darwin') return { granted: true }; + const status = systemPreferences.getMediaAccessStatus('screen'); + console.log('[meeting] Screen recording permission status:', status); + if (status === 'granted') return { granted: true }; + // Not granted — call desktopCapturer.getSources() to register the app + // in the macOS Screen Recording list. On first call this shows the + // native permission prompt (signed apps are remembered across restarts). + try { await desktopCapturer.getSources({ types: ['screen'] }); } catch { /* ignore */ } + // Re-check after the native prompt was dismissed + const statusAfter = systemPreferences.getMediaAccessStatus('screen'); + console.log('[meeting] Screen recording permission status after prompt:', statusAfter); + return { granted: statusAfter === 'granted' }; + }, + 'meeting:openScreenRecordingSettings': async () => { + await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); + return { success: true }; + }, 'meeting:summarize': async (_event, args) => { const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson); return { notes }; diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f83ea5cb..b2bc9d7f 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -484,7 +484,7 @@ function FixedSidebarToggle({ )} style={{ marginLeft: TITLEBAR_BUTTON_GAP_PX }} > - {meetingSummarizing ? ( + {meetingSummarizing || meetingState === 'connecting' ? ( ) : meetingState === 'recording' ? ( @@ -494,7 +494,7 @@ function FixedSidebarToggle({ - {meetingSummarizing ? 'Generating meeting notes...' : meetingState === 'recording' ? 'Stop meeting notes' : 'Take new meeting notes'} + {meetingSummarizing ? 'Generating meeting notes...' : meetingState === 'connecting' ? 'Starting transcription...' : meetingState === 'recording' ? 'Stop meeting notes' : 'Take new meeting notes'} )} @@ -3417,9 +3417,9 @@ function App() { const [meetingSummarizing, setMeetingSummarizing] = useState(false) const [showMeetingPermissions, setShowMeetingPermissions] = useState(false) - const startMeetingAfterPermissions = useCallback(async () => { - setShowMeetingPermissions(false) - localStorage.setItem('meeting-permissions-acknowledged', '1') + const [checkingPermission, setCheckingPermission] = useState(false) + + const startMeetingNow = useCallback(async () => { const calEvent = pendingCalendarEventRef.current pendingCalendarEventRef.current = undefined const notePath = await meetingTranscription.start(calEvent) @@ -3429,6 +3429,23 @@ function App() { } }, [meetingTranscription, handleVoiceNoteCreated]) + const handleCheckPermissionAndRetry = useCallback(async () => { + setCheckingPermission(true) + try { + const { granted } = await window.ipc.invoke('meeting:checkScreenPermission', null) + if (granted) { + setShowMeetingPermissions(false) + await startMeetingNow() + } + } finally { + setCheckingPermission(false) + } + }, [startMeetingNow]) + + const handleOpenScreenRecordingSettings = useCallback(async () => { + await window.ipc.invoke('meeting:openScreenRecordingSettings', null) + }, []) + const handleToggleMeeting = useCallback(async () => { if (meetingTranscription.state === 'recording') { await meetingTranscription.stop() @@ -3450,16 +3467,15 @@ function App() { const calendarEventJson = calEventMatch?.[1]?.replace(/''/g, "'") const { notes } = await window.ipc.invoke('meeting:summarize', { transcript: fileContent, meetingStartTime, calendarEventJson }) if (notes) { - // Prepend meeting notes below the title but above the transcript - const { raw: fm, body: transcriptBody } = splitFrontmatter(fileContent) - // Use frontmatter title as the heading (set from calendar event summary) + // Prepend meeting notes above the existing transcript block + const { raw: fm, body } = splitFrontmatter(fileContent) const fmTitleMatch = fileContent.match(/^title:\s*(.+)$/m) - const noteTitle = fmTitleMatch?.[1]?.trim() || 'Meeting note' - // Strip any existing top-level heading from body - const bodyWithoutTitle = transcriptBody.replace(/^#\s+.+\s*\n*/, '') - // Also strip any title/heading the LLM may have generated + const noteTitle = fmTitleMatch?.[1]?.trim() || 'Meeting Notes' const cleanedNotes = notes.replace(/^#{1,2}\s+.+\n+/, '') - const newBody = `# ${noteTitle}\n\n` + cleanedNotes + '\n\n---\n\n## Raw transcript\n\n' + bodyWithoutTitle + // Extract the existing transcript block and preserve it as-is + const transcriptBlockMatch = body.match(/(```transcript\n[\s\S]*?\n```)/) + const transcriptBlock = transcriptBlockMatch?.[1] || '' + const newBody = `# ${noteTitle}\n\n` + cleanedNotes + (transcriptBlock ? '\n\n' + transcriptBlock : '') const newContent = fm ? `${fm}\n${newBody}` : newBody await window.ipc.invoke('workspace:writeFile', { path: notePath, @@ -3477,20 +3493,18 @@ function App() { meetingNotePathRef.current = null } } else if (meetingTranscription.state === 'idle') { - // Show permissions modal on first use (macOS only — Windows works out of the box) - if (isMac && !localStorage.getItem('meeting-permissions-acknowledged')) { - setShowMeetingPermissions(true) - return - } - const calEvent = pendingCalendarEventRef.current - pendingCalendarEventRef.current = undefined - const notePath = await meetingTranscription.start(calEvent) - if (notePath) { - meetingNotePathRef.current = notePath - await handleVoiceNoteCreated(notePath) + // On macOS, check screen recording permission before starting + if (isMac) { + const result = await window.ipc.invoke('meeting:checkScreenPermission', null) + console.log('[meeting] Permission check result:', result) + if (!result.granted) { + setShowMeetingPermissions(true) + return + } } + await startMeetingNow() } - }, [meetingTranscription, handleVoiceNoteCreated]) + }, [meetingTranscription, handleVoiceNoteCreated, startMeetingNow]) handleToggleMeetingRef.current = handleToggleMeeting // Listen for calendar block "join meeting & take notes" events @@ -4421,23 +4435,25 @@ function App() { - Meeting transcription setup + Screen recording permission required - Rowboat needs Screen Recording permission to capture meeting audio from other apps (Zoom, Meet, etc.). + Rowboat needs Screen Recording permission to capture meeting audio from other apps (Zoom, Meet, etc.). This feature won't work without it.

To enable this:

    -
  1. Open System SettingsPrivacy & Security
  2. -
  3. Click Screen Recording
  4. +
  5. Open System SettingsPrivacy & SecurityScreen Recording
  6. Toggle on Rowboat
  7. You may need to restart the app after granting permission
- + +
diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx index 2592dec3..f3ccba2d 100644 --- a/apps/x/apps/renderer/src/components/markdown-editor.tsx +++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx @@ -15,6 +15,7 @@ import { ChartBlockExtension } from '@/extensions/chart-block' import { TableBlockExtension } from '@/extensions/table-block' import { CalendarBlockExtension } from '@/extensions/calendar-block' import { EmailBlockExtension } from '@/extensions/email-block' +import { TranscriptBlockExtension } from '@/extensions/transcript-block' import { Markdown } from 'tiptap-markdown' import { useEffect, useCallback, useMemo, useRef, useState } from 'react' import { Calendar, ChevronDown, ExternalLink } from 'lucide-react' @@ -155,6 +156,8 @@ function getMarkdownWithBlankLines(editor: Editor): string { blocks.push('```calendar\n' + (node.attrs?.data as string || '{}') + '\n```') } else if (node.type === 'emailBlock') { blocks.push('```email\n' + (node.attrs?.data as string || '{}') + '\n```') + } else if (node.type === 'transcriptBlock') { + blocks.push('```transcript\n' + (node.attrs?.data as string || '{}') + '\n```') } else if (node.type === 'codeBlock') { const lang = (node.attrs?.language as string) || '' blocks.push('```' + lang + '\n' + nodeToText(node) + '\n```') @@ -567,6 +570,7 @@ export function MarkdownEditor({ TableBlockExtension, CalendarBlockExtension, EmailBlockExtension, + TranscriptBlockExtension, WikiLink.configure({ onCreate: wikiLinks?.onCreate ? (path) => { diff --git a/apps/x/apps/renderer/src/extensions/transcript-block.tsx b/apps/x/apps/renderer/src/extensions/transcript-block.tsx new file mode 100644 index 00000000..9b76f568 --- /dev/null +++ b/apps/x/apps/renderer/src/extensions/transcript-block.tsx @@ -0,0 +1,177 @@ +import { mergeAttributes, Node } from '@tiptap/react' +import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' +import { ChevronDown, FileText } from 'lucide-react' +import { blocks } from '@x/shared' +import { useState, useMemo } from 'react' + +interface TranscriptEntry { + speaker: string + text: string +} + +function parseTranscript(raw: string): TranscriptEntry[] { + const entries: TranscriptEntry[] = [] + const lines = raw.split('\n') + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + // Match **Speaker Name:** text or **You:** text + const match = trimmed.match(/^\*\*(.+?):\*\*\s*(.*)$/) + if (match) { + entries.push({ speaker: match[1], text: match[2] }) + } else if (entries.length > 0) { + // Continuation line — append to last entry + entries[entries.length - 1].text += ' ' + trimmed + } + } + return entries +} + +function speakerColor(speaker: string): string { + // Simple hash to pick a consistent color per speaker + let hash = 0 + for (let i = 0; i < speaker.length; i++) { + hash = speaker.charCodeAt(i) + ((hash << 5) - hash) + } + const colors = [ + '#3b82f6', // blue + '#06b6d4', // cyan + '#6366f1', // indigo + '#8b5cf6', // purple + '#0ea5e9', // sky + '#2563eb', // blue darker + '#7c3aed', // violet + ] + return colors[Math.abs(hash) % colors.length] +} + +function TranscriptBlockView({ node, getPos, editor }: { + node: { attrs: Record } + getPos: () => number | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any + editor: any +}) { + const raw = node.attrs.data as string + let config: blocks.TranscriptBlock | null = null + + try { + config = blocks.TranscriptBlockSchema.parse(JSON.parse(raw)) + } catch { + // fallback below + } + + // Auto-detect: expand if this is the first real block (live recording), + // collapse if there's other content above (notes have been generated) + const isFirstBlock = useMemo(() => { + try { + const pos = getPos() + if (pos === undefined) return false + const firstChild = editor?.state?.doc?.firstChild + if (!firstChild) return true + // If the transcript block is right after the first node (heading), it's the main content + return pos <= (firstChild.nodeSize ?? 0) + 1 + } catch { + return false + } + }, [getPos, editor]) + + const [expanded, setExpanded] = useState(isFirstBlock) + + const entries = useMemo(() => { + if (!config) return [] + return parseTranscript(config.transcript) + }, [config]) + + if (!config) { + return ( + +
+ + Invalid transcript block +
+
+ ) + } + + return ( + +
e.stopPropagation()}> + + {expanded && ( +
+ {entries.length > 0 ? ( + entries.map((entry, i) => ( +
+ + {entry.speaker} + + {entry.text} +
+ )) + ) : ( +
{config.transcript}
+ )} +
+ )} +
+
+ ) +} + +export const TranscriptBlockExtension = Node.create({ + name: 'transcriptBlock', + group: 'block', + atom: true, + selectable: true, + draggable: false, + + addAttributes() { + return { + data: { default: '{}' }, + } + }, + + parseHTML() { + return [{ + tag: 'pre', + priority: 60, + getAttrs(element) { + const code = element.querySelector('code') + if (!code) return false + const cls = code.className || '' + if (cls.includes('language-transcript')) { + return { data: code.textContent || '{}' } + } + return false + }, + }] + }, + + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) { + return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'transcript-block' })] + }, + + addNodeView() { + return ReactNodeViewRenderer(TranscriptBlockView) + }, + + addStorage() { + return { + markdown: { + serialize(state: { write: (text: string) => void; closeBlock: (node: unknown) => void }, node: { attrs: { data: string } }) { + state.write('```transcript\n' + node.attrs.data + '\n```') + state.closeBlock(node) + }, + parse: {}, + }, + } + }, +}) diff --git a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts index 35a0a703..50d89a57 100644 --- a/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts +++ b/apps/x/apps/renderer/src/hooks/useMeetingTranscription.ts @@ -60,7 +60,7 @@ export interface CalendarEventMeta { } function formatTranscript(entries: TranscriptEntry[], date: string, calendarEvent?: CalendarEventMeta): string { - const noteTitle = calendarEvent?.summary || 'Meeting note'; + const noteTitle = calendarEvent?.summary || 'Meeting Notes'; const lines = [ '---', 'type: meeting', @@ -89,13 +89,18 @@ function formatTranscript(entries: TranscriptEntry[], date: string, calendarEven `# ${noteTitle}`, '', ); + // Build the raw transcript text + const transcriptLines: string[] = []; for (let i = 0; i < entries.length; i++) { if (i > 0 && entries[i].speaker !== entries[i - 1].speaker) { - lines.push(''); + transcriptLines.push(''); } - lines.push(`**${entries[i].speaker}:** ${entries[i].text}`); - lines.push(''); + transcriptLines.push(`**${entries[i].speaker}:** ${entries[i].text}`); + transcriptLines.push(''); } + const transcriptText = transcriptLines.join('\n').trim(); + const transcriptData = JSON.stringify({ transcript: transcriptText }); + lines.push('```transcript', transcriptData, '```'); return lines.join('\n'); } @@ -187,52 +192,83 @@ export function useMeetingTranscription(onAutoStop?: () => void) { if (state !== 'idle') return null; setState('connecting'); - // Detect headphones vs speakers - const usingHeadphones = await detectHeadphones(); - console.log(`[meeting] Audio output mode: ${usingHeadphones ? 'headphones' : 'speakers'}`); - - // Rowboat WebSocket + bearer token when signed in; else local Deepgram API key - let ws: WebSocket; - try { - const account = await refreshRowboatAccount(); - if ( - account?.signedIn && - account.accessToken && - account.config?.websocketApiUrl - ) { - const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS); - console.log('[meeting] Using Rowboat WebSocket'); - ws = new WebSocket(listenUrl, ['bearer', account.accessToken]); - } else { - const config = await window.ipc.invoke('voice:getConfig', null); - if (!config?.deepgram) { - console.error('[meeting] No Deepgram config available'); - setState('idle'); - return null; + // Run independent setup steps in parallel for faster startup + const [headphoneResult, wsResult, micResult, systemResult] = await Promise.allSettled([ + // 1. Detect headphones vs speakers + detectHeadphones(), + // 2. Set up Deepgram WebSocket (account refresh + connect + wait for open) + (async () => { + const account = await refreshRowboatAccount(); + let ws: WebSocket; + if ( + account?.signedIn && + account.accessToken && + account.config?.websocketApiUrl + ) { + const listenUrl = buildDeepgramListenUrl(account.config.websocketApiUrl, DEEPGRAM_PARAMS); + console.log('[meeting] Using Rowboat WebSocket'); + ws = new WebSocket(listenUrl, ['bearer', account.accessToken]); + } else { + const config = await window.ipc.invoke('voice:getConfig', null); + if (!config?.deepgram) { + throw new Error('No Deepgram config available'); + } + console.log('[meeting] Using Deepgram API key'); + ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['token', config.deepgram.apiKey]); } - console.log('[meeting] Using Deepgram API key'); - ws = new WebSocket(DEEPGRAM_LISTEN_URL, ['token', config.deepgram.apiKey]); - } - } catch (err) { - console.error('[meeting] Failed to connect Deepgram:', err); - setState('idle'); - return null; - } - wsRef.current = ws; + const ok = await new Promise((resolve) => { + ws.onopen = () => resolve(true); + ws.onerror = () => resolve(false); + setTimeout(() => resolve(false), 5000); + }); + if (!ok) throw new Error('WebSocket failed to connect'); + console.log('[meeting] WebSocket connected'); + return ws; + })(), + // 3. Get mic stream + navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }, + }), + // 4. Get system audio via getDisplayMedia (loopback) + (async () => { + const stream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true }); + stream.getVideoTracks().forEach(t => t.stop()); + if (stream.getAudioTracks().length === 0) { + stream.getTracks().forEach(t => t.stop()); + throw new Error('No audio track from getDisplayMedia'); + } + console.log('[meeting] System audio captured'); + return stream; + })(), + ]); - // Wait for WS open - const wsOk = await new Promise((resolve) => { - ws.onopen = () => resolve(true); - ws.onerror = () => resolve(false); - setTimeout(() => resolve(false), 5000); - }); - if (!wsOk) { - console.error('[meeting] WebSocket failed to connect'); + // Check for failures — clean up any successful resources if something failed + const failed = wsResult.status === 'rejected' + || micResult.status === 'rejected' + || systemResult.status === 'rejected'; + + if (failed) { + if (wsResult.status === 'rejected') console.error('[meeting] WebSocket setup failed:', wsResult.reason); + if (micResult.status === 'rejected') console.error('[meeting] Microphone access denied:', micResult.reason); + if (systemResult.status === 'rejected') console.error('[meeting] System audio access denied:', systemResult.reason); + // Clean up any resources that did succeed + if (wsResult.status === 'fulfilled') { wsResult.value.close(); } + if (micResult.status === 'fulfilled') { micResult.value.getTracks().forEach(t => t.stop()); } + if (systemResult.status === 'fulfilled') { systemResult.value.getTracks().forEach(t => t.stop()); } cleanup(); setState('idle'); return null; } - console.log('[meeting] WebSocket connected'); + + const usingHeadphones = headphoneResult.status === 'fulfilled' ? headphoneResult.value : false; + console.log(`[meeting] Audio output mode: ${usingHeadphones ? 'headphones' : 'speakers'}`); + + const ws = wsResult.value; + wsRef.current = ws; // Set up WS message handler transcriptRef.current = []; @@ -283,43 +319,10 @@ export function useMeetingTranscription(onAutoStop?: () => void) { wsRef.current = null; }; - // Get mic stream - let micStream: MediaStream; - try { - micStream = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - }, - }); - } catch (err) { - console.error('[meeting] Microphone access denied:', err); - cleanup(); - setState('idle'); - return null; - } + const micStream = micResult.value; micStreamRef.current = micStream; - // Get system audio via getDisplayMedia (loopback) - let systemStream: MediaStream; - try { - systemStream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: true }); - systemStream.getVideoTracks().forEach(t => t.stop()); - } catch (err) { - console.error('[meeting] System audio access denied:', err); - cleanup(); - setState('idle'); - return null; - } - if (systemStream.getAudioTracks().length === 0) { - console.error('[meeting] No audio track from getDisplayMedia'); - systemStream.getTracks().forEach(t => t.stop()); - cleanup(); - setState('idle'); - return null; - } - console.log('[meeting] System audio captured'); + const systemStream = systemResult.value; systemStreamRef.current = systemStream; // ----- Audio pipeline ----- diff --git a/apps/x/apps/renderer/src/styles/editor.css b/apps/x/apps/renderer/src/styles/editor.css index 8701099b..efa481c1 100644 --- a/apps/x/apps/renderer/src/styles/editor.css +++ b/apps/x/apps/renderer/src/styles/editor.css @@ -618,7 +618,8 @@ .tiptap-editor .ProseMirror .chart-block-wrapper, .tiptap-editor .ProseMirror .table-block-wrapper, .tiptap-editor .ProseMirror .calendar-block-wrapper, -.tiptap-editor .ProseMirror .email-block-wrapper { +.tiptap-editor .ProseMirror .email-block-wrapper, +.tiptap-editor .ProseMirror .transcript-block-wrapper { margin: 8px 0; } @@ -628,7 +629,8 @@ .tiptap-editor .ProseMirror .table-block-card, .tiptap-editor .ProseMirror .calendar-block-card, .tiptap-editor .ProseMirror .email-block-card, -.tiptap-editor .ProseMirror .email-draft-block-card { +.tiptap-editor .ProseMirror .email-draft-block-card, +.tiptap-editor .ProseMirror .transcript-block-card { position: relative; padding: 12px 14px; border: 1px solid var(--border); @@ -644,7 +646,8 @@ .tiptap-editor .ProseMirror .table-block-card:hover, .tiptap-editor .ProseMirror .calendar-block-card:hover, .tiptap-editor .ProseMirror .email-block-card:hover, -.tiptap-editor .ProseMirror .email-draft-block-card:hover { +.tiptap-editor .ProseMirror .email-draft-block-card:hover, +.tiptap-editor .ProseMirror .transcript-block-card:hover { background-color: color-mix(in srgb, var(--muted) 70%, transparent); } @@ -1488,6 +1491,74 @@ margin-left: 4px; } +/* Transcript block */ +.tiptap-editor .ProseMirror .transcript-block-toggle { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 0; + font-size: 13px; + font-weight: 500; + color: color-mix(in srgb, var(--foreground) 60%, transparent); + background: none; + border: none; + cursor: pointer; + transition: color 0.12s ease; +} + +.tiptap-editor .ProseMirror .transcript-block-toggle:hover { + color: var(--foreground); +} + +.tiptap-editor .ProseMirror .transcript-block-chevron { + transition: transform 0.15s ease; + flex-shrink: 0; +} + +.tiptap-editor .ProseMirror .transcript-block-chevron-open { + transform: rotate(180deg); +} + +.tiptap-editor .ProseMirror .transcript-block-content { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); + display: flex; + flex-direction: column; + gap: 6px; +} + +.tiptap-editor .ProseMirror .transcript-entry { + font-size: 13px; + line-height: 1.5; +} + +.tiptap-editor .ProseMirror .transcript-speaker { + font-weight: 600; + margin-right: 6px; +} + +.tiptap-editor .ProseMirror .transcript-text { + color: color-mix(in srgb, var(--foreground) 75%, transparent); +} + +.tiptap-editor .ProseMirror .transcript-raw { + font-size: 13px; + line-height: 1.6; + color: color-mix(in srgb, var(--foreground) 70%, transparent); + white-space: pre-wrap; + word-break: break-word; +} + +.tiptap-editor .ProseMirror .transcript-block-error { + display: flex; + align-items: center; + gap: 6px; + color: color-mix(in srgb, var(--foreground) 55%, transparent); + font-size: 13px; +} + /* Meeting event banner */ .meeting-event-banner { position: relative; diff --git a/apps/x/packages/core/src/knowledge/summarize_meeting.ts b/apps/x/packages/core/src/knowledge/summarize_meeting.ts index 534b6655..30e3c5d4 100644 --- a/apps/x/packages/core/src/knowledge/summarize_meeting.ts +++ b/apps/x/packages/core/src/knowledge/summarize_meeting.ts @@ -15,7 +15,8 @@ const SYSTEM_PROMPT = `You are a meeting notes assistant. Given a raw meeting tr ## Calendar matching You will be given the transcript (with a timestamp of when recording started) and recent calendar events with their titles, times, and attendees. If a calendar event clearly matches this meeting (overlapping time + content aligns), then: - Do NOT output a title or heading — the title is already set by the caller. -- Replace generic speaker labels ("Speaker 0", "Speaker 1", "System audio") with actual attendee names, but ONLY if you have HIGH CONFIDENCE about which speaker is which based on the discussion content. If unsure, use "They" instead of "Speaker 0" etc. +- ONLY use names from the calendar event attendee list. Do NOT introduce names that are not in the attendee list — any unrecognized names in the transcript are transcription errors. +- Replace generic speaker labels ("Speaker 0", "Speaker 1", "System audio") with actual attendee names from the list, but ONLY if you have HIGH CONFIDENCE about which speaker is which based on the discussion content. If unsure, use "They" instead of "Speaker 0" etc. - "You" in the transcript is the local user — if the calendar event has an organizer or you can identify who "You" is from context, use their name. If no calendar event matches with high confidence, or if no calendar events are provided, use "They" for all non-"You" speakers. diff --git a/apps/x/packages/shared/src/blocks.ts b/apps/x/packages/shared/src/blocks.ts index 55d1cd3e..68209051 100644 --- a/apps/x/packages/shared/src/blocks.ts +++ b/apps/x/packages/shared/src/blocks.ts @@ -74,3 +74,9 @@ export const EmailBlockSchema = z.object({ }); export type EmailBlock = z.infer; + +export const TranscriptBlockSchema = z.object({ + transcript: z.string(), +}); + +export type TranscriptBlock = z.infer; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 5f4988f4..28718db3 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -501,6 +501,16 @@ const ipcSchemas = { mimeType: z.string(), }), }, + 'meeting:checkScreenPermission': { + req: z.null(), + res: z.object({ + granted: z.boolean(), + }), + }, + 'meeting:openScreenRecordingSettings': { + req: z.null(), + res: z.object({ success: z.boolean() }), + }, 'meeting:summarize': { req: z.object({ transcript: z.string(), diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index e59ce990..01a9240f 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: electron-squirrel-startup: specifier: ^1.0.1 version: 1.0.1 + html-to-docx: + specifier: ^1.8.0 + version: 1.8.0(encoding@0.1.13) mammoth: specifier: ^1.11.0 version: 1.11.0 @@ -235,6 +238,9 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.3(react@19.2.3) + recharts: + specifier: ^3.8.0 + version: 3.8.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1578,6 +1584,46 @@ packages: '@octokit/types@6.41.0': resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} + '@oozcitak/dom@1.15.5': + resolution: {integrity: sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==} + engines: {node: '>=8.0'} + + '@oozcitak/dom@1.15.6': + resolution: {integrity: sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==} + engines: {node: '>=8.0'} + + '@oozcitak/infra@1.0.3': + resolution: {integrity: sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==} + engines: {node: '>=6.0'} + + '@oozcitak/infra@1.0.5': + resolution: {integrity: sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==} + engines: {node: '>=6.0'} + + '@oozcitak/url@1.0.0': + resolution: {integrity: sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==} + engines: {node: '>=8.0'} + + '@oozcitak/util@1.0.1': + resolution: {integrity: sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==} + engines: {node: '>=6.0'} + + '@oozcitak/util@1.0.2': + resolution: {integrity: sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==} + engines: {node: '>=6.0'} + + '@oozcitak/util@8.0.0': + resolution: {integrity: sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==} + engines: {node: '>=6.0'} + + '@oozcitak/util@8.3.3': + resolution: {integrity: sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==} + engines: {node: '>=6.0'} + + '@oozcitak/util@8.3.4': + resolution: {integrity: sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==} + engines: {node: '>=8.0'} + '@openrouter/ai-sdk-provider@1.5.4': resolution: {integrity: sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw==} engines: {node: '>=18'} @@ -2516,6 +2562,17 @@ packages: '@react-pdf/types@2.9.2': resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==} + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@remirror/core-constants@3.0.0': resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} @@ -2876,6 +2933,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/helpers@0.5.18': resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} @@ -3775,6 +3835,9 @@ packages: brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browser-split@0.0.1: + resolution: {integrity: sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==} + browserify-zlib@0.2.0: resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} @@ -3836,6 +3899,9 @@ packages: camel-case@4.1.2: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001761: resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} @@ -4263,6 +4329,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -4331,12 +4400,24 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + dom-serializer@0.2.2: + resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domhandler@2.4.2: + resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} + domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -4344,6 +4425,9 @@ packages: dompurify@3.3.1: resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -4428,6 +4512,16 @@ packages: resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} engines: {node: '>=10.13.0'} + ent@2.2.2: + resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==} + engines: {node: '>= 0.4'} + + entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -4446,6 +4540,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error@4.4.0: + resolution: {integrity: sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -4465,6 +4562,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -4565,6 +4665,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + ev-store@7.0.0: + resolution: {integrity: sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -4907,6 +5010,9 @@ packages: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -5050,12 +5156,24 @@ packages: hsl-to-rgb-for-reals@1.1.1: resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-to-docx@1.8.0: + resolution: {integrity: sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==} + + html-to-vdom@0.7.0: + resolution: {integrity: sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@3.10.1: + resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -5113,9 +5231,23 @@ packages: engines: {node: '>=6.9.0'} hasBin: true + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + + image-to-base64@2.2.0: + resolution: {integrity: sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==} + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -5132,6 +5264,9 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + individual@3.0.0: + resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} + infer-owner@1.0.4: resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} @@ -5232,6 +5367,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-object@1.0.2: + resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -5242,6 +5380,10 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -5836,6 +5978,9 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -5965,6 +6110,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-tick@0.2.2: + resolution: {integrity: sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==} + nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -6425,6 +6573,9 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6484,6 +6635,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -6549,10 +6712,26 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -6625,6 +6804,9 @@ packages: resolution: {integrity: sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==} engines: {node: '>=14', npm: '>=7'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -6713,6 +6895,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -6908,6 +7094,9 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + string-template@0.2.1: + resolution: {integrity: sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -7026,6 +7215,9 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -7297,6 +7489,12 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + virtual-dom@2.1.1: + resolution: {integrity: sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==} + vite-compatible-readable-stream@3.6.1: resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} engines: {node: '>= 6'} @@ -7446,11 +7644,21 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + x-is-array@0.1.0: + resolution: {integrity: sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==} + + x-is-string@0.1.0: + resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==} + xlsx@0.18.5: resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} engines: {node: '>=0.8'} hasBin: true + xmlbuilder2@2.1.2: + resolution: {integrity: sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==} + engines: {node: '>=8.0'} + xmlbuilder@10.1.1: resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} engines: {node: '>=4.0'} @@ -9230,6 +9438,41 @@ snapshots: dependencies: '@octokit/openapi-types': 12.11.0 + '@oozcitak/dom@1.15.5': + dependencies: + '@oozcitak/infra': 1.0.5 + '@oozcitak/url': 1.0.0 + '@oozcitak/util': 8.0.0 + + '@oozcitak/dom@1.15.6': + dependencies: + '@oozcitak/infra': 1.0.5 + '@oozcitak/url': 1.0.0 + '@oozcitak/util': 8.3.4 + + '@oozcitak/infra@1.0.3': + dependencies: + '@oozcitak/util': 1.0.1 + + '@oozcitak/infra@1.0.5': + dependencies: + '@oozcitak/util': 8.0.0 + + '@oozcitak/url@1.0.0': + dependencies: + '@oozcitak/infra': 1.0.3 + '@oozcitak/util': 1.0.2 + + '@oozcitak/util@1.0.1': {} + + '@oozcitak/util@1.0.2': {} + + '@oozcitak/util@8.0.0': {} + + '@oozcitak/util@8.3.3': {} + + '@oozcitak/util@8.3.4': {} + '@openrouter/ai-sdk-provider@1.5.4(ai@5.0.151(zod@4.2.1))(zod@4.2.1)': dependencies: '@openrouter/sdk': 0.1.27 @@ -10259,6 +10502,18 @@ snapshots: '@react-pdf/primitives': 4.1.1 '@react-pdf/stylesheet': 6.1.2 + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.3 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) + '@remirror/core-constants@3.0.0': {} '@rolldown/pluginutils@1.0.0-beta.53': {} @@ -10704,6 +10959,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@swc/helpers@0.5.18': dependencies: tslib: 2.8.1 @@ -11729,6 +11986,8 @@ snapshots: dependencies: base64-js: 1.5.1 + browser-split@0.0.1: {} + browserify-zlib@0.2.0: dependencies: pako: 1.0.11 @@ -11823,6 +12082,8 @@ snapshots: pascal-case: 3.1.2 tslib: 2.8.1 + camelize@1.0.1: {} + caniuse-lite@1.0.30001761: {} ccount@2.0.1: {} @@ -12243,6 +12504,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -12306,14 +12569,27 @@ snapshots: minimatch: 3.1.2 p-limit: 3.1.0 + dom-serializer@0.2.2: + dependencies: + domelementtype: 2.3.0 + entities: 2.2.0 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 + dom-walk@0.1.2: {} + + domelementtype@1.3.1: {} + domelementtype@2.3.0: {} + domhandler@2.4.2: + dependencies: + domelementtype: 1.3.1 + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 @@ -12322,6 +12598,11 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + domutils@1.7.0: + dependencies: + dom-serializer: 0.2.2 + domelementtype: 1.3.1 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -12462,6 +12743,17 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + ent@2.2.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + punycode: 1.4.1 + safe-regex-test: 1.1.0 + + entities@1.1.2: {} + + entities@2.2.0: {} + entities@4.5.0: {} entities@6.0.1: {} @@ -12474,6 +12766,12 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error@4.4.0: + dependencies: + camelize: 1.0.1 + string-template: 0.2.1 + xtend: 4.0.2 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -12491,6 +12789,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-toolkit@1.45.1: {} + es6-error@4.1.1: optional: true @@ -12655,6 +12955,10 @@ snapshots: etag@1.8.1: {} + ev-store@7.0.0: + dependencies: + individual: 3.0.0 + event-target-shim@5.0.1: {} eventemitter3@5.0.1: {} @@ -13098,6 +13402,11 @@ snapshots: dependencies: ini: 2.0.0 + global@4.4.0: + dependencies: + min-document: 2.19.2 + process: 0.11.10 + globals@14.0.0: {} globals@16.5.0: {} @@ -13344,10 +13653,44 @@ snapshots: hsl-to-rgb-for-reals@1.1.1: {} + html-entities@2.6.0: {} + + html-to-docx@1.8.0(encoding@0.1.13): + dependencies: + '@oozcitak/dom': 1.15.6 + '@oozcitak/util': 8.3.4 + color-name: 1.1.4 + html-entities: 2.6.0 + html-to-vdom: 0.7.0 + image-size: 1.2.1 + image-to-base64: 2.2.0(encoding@0.1.13) + jszip: 3.10.1 + lodash: 4.17.21 + mime-types: 2.1.35 + nanoid: 3.3.11 + virtual-dom: 2.1.1 + xmlbuilder2: 2.1.2 + transitivePeerDependencies: + - encoding + + html-to-vdom@0.7.0: + dependencies: + ent: 2.2.2 + htmlparser2: 3.10.1 + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} + htmlparser2@3.10.1: + dependencies: + domelementtype: 1.3.1 + domhandler: 2.4.2 + domutils: 1.7.0 + entities: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + http-cache-semantics@4.2.0: {} http-errors@2.0.1: @@ -13412,8 +13755,22 @@ snapshots: image-size@0.7.5: optional: true + image-size@1.2.1: + dependencies: + queue: 6.0.2 + + image-to-base64@2.2.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + immediate@3.0.6: {} + immer@10.2.0: {} + + immer@11.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -13426,6 +13783,8 @@ snapshots: indent-string@4.0.0: {} + individual@3.0.0: {} + infer-owner@1.0.4: {} inflight@1.0.6: @@ -13500,6 +13859,8 @@ snapshots: is-number@7.0.0: {} + is-object@1.0.2: {} + is-plain-obj@4.1.0: {} is-promise@4.0.0: {} @@ -13507,6 +13868,13 @@ snapshots: is-property@1.0.2: optional: true + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + is-stream@1.1.0: {} is-stream@2.0.1: {} @@ -14352,6 +14720,10 @@ snapshots: mimic-response@3.1.0: {} + min-document@2.19.2: + dependencies: + dom-walk: 0.1.2 + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -14467,6 +14839,8 @@ snapshots: neo-async@2.6.2: {} + next-tick@0.2.2: {} + nice-try@1.0.5: {} no-case@3.0.4: @@ -14955,6 +15329,8 @@ snapshots: punycode.js@2.3.1: {} + punycode@1.4.1: {} + punycode@2.3.1: {} pusher-js@8.4.0: @@ -15064,6 +15440,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.7 + redux: 5.0.1 + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3): @@ -15138,10 +15523,36 @@ snapshots: readdirp@4.1.2: {} + recharts@3.8.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1))(react@19.2.3) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.45.1 + eventemitter3: 5.0.1 + immer: 10.2.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.3)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.3) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + rechoir@0.8.0: dependencies: resolve: 1.22.11 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -15248,6 +15659,8 @@ snapshots: dependencies: pe-library: 1.0.1 + reselect@5.1.1: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -15366,6 +15779,12 @@ snapshots: safe-buffer@5.2.1: {} + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + safer-buffer@2.1.2: {} scheduler@0.25.0-rc-603e6108-20241029: {} @@ -15616,6 +16035,8 @@ snapshots: - micromark-util-types - supports-color + string-template@0.2.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -15733,6 +16154,8 @@ snapshots: tiny-inflate@1.0.3: {} + tiny-invariant@1.3.3: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -16004,6 +16427,34 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + virtual-dom@2.1.1: + dependencies: + browser-split: 0.0.1 + error: 4.4.0 + ev-store: 7.0.0 + global: 4.4.0 + is-object: 1.0.2 + next-tick: 0.2.2 + x-is-array: 0.1.0 + x-is-string: 0.1.0 + vite-compatible-readable-stream@3.6.1: dependencies: inherits: 2.0.4 @@ -16155,6 +16606,10 @@ snapshots: wrappy@1.0.2: {} + x-is-array@0.1.0: {} + + x-is-string@0.1.0: {} + xlsx@0.18.5: dependencies: adler-32: 1.3.1 @@ -16165,12 +16620,17 @@ snapshots: wmf: 1.0.2 word: 0.3.0 + xmlbuilder2@2.1.2: + dependencies: + '@oozcitak/dom': 1.15.5 + '@oozcitak/infra': 1.0.5 + '@oozcitak/util': 8.3.3 + xmlbuilder@10.1.1: {} xmlbuilder@15.1.1: {} - xtend@4.0.2: - optional: true + xtend@4.0.2: {} y18n@5.0.8: {} From 61e92783b2f7b2d8b2d551a2dfc9471d56d52632 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:43:48 +0530 Subject: [PATCH 20/97] fix nested lists save --- .../src/components/markdown-editor.tsx | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/apps/x/apps/renderer/src/components/markdown-editor.tsx b/apps/x/apps/renderer/src/components/markdown-editor.tsx index f3ccba2d..d7920b8b 100644 --- a/apps/x/apps/renderer/src/components/markdown-editor.tsx +++ b/apps/x/apps/renderer/src/components/markdown-editor.tsx @@ -109,39 +109,44 @@ function getMarkdownWithBlankLines(editor: Editor): string { const level = (node.attrs?.level as number) || 1 const text = nodeToText(node) blocks.push('#'.repeat(level) + ' ' + text) - } else if (node.type === 'bulletList' || node.type === 'orderedList') { - // Handle lists - all items are part of one block - const listLines: string[] = [] - const listItems = (node.content || []) as Array<{ content?: Array; attrs?: Record }> - listItems.forEach((item, index) => { - const prefix = node.type === 'orderedList' ? `${index + 1}. ` : '- ' - const itemContent = (item.content || []) as Array<{ type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }> - itemContent.forEach((para: { type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }, paraIndex: number) => { - const text = nodeToText(para) - if (paraIndex === 0) { - listLines.push(prefix + text) + } else if (node.type === 'bulletList' || node.type === 'orderedList' || node.type === 'taskList') { + // Recursively serialize lists to handle nested bullets + const serializeList = ( + listNode: { type?: string; content?: Array>; attrs?: Record }, + indent: number + ): string[] => { + const lines: string[] = [] + const items = (listNode.content || []) as Array<{ content?: Array>; attrs?: Record }> + items.forEach((item, index) => { + const indentStr = ' '.repeat(indent) + let prefix: string + if (listNode.type === 'taskList') { + const checked = item.attrs?.checked ? 'x' : ' ' + prefix = `- [${checked}] ` + } else if (listNode.type === 'orderedList') { + prefix = `${index + 1}. ` } else { - listLines.push(' ' + text) + prefix = '- ' } + const itemContent = (item.content || []) as Array<{ type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }> + let firstPara = true + itemContent.forEach(child => { + if (child.type === 'bulletList' || child.type === 'orderedList' || child.type === 'taskList') { + lines.push(...serializeList(child, indent + 1)) + } else { + const text = nodeToText(child) + if (firstPara) { + lines.push(indentStr + prefix + text) + firstPara = false + } else { + lines.push(indentStr + ' ' + text) + } + } + }) }) - }) - blocks.push(listLines.join('\n')) - } else if (node.type === 'taskList') { - const listLines: string[] = [] - const listItems = (node.content || []) as Array<{ content?: Array; attrs?: Record }> - listItems.forEach(item => { - const checked = item.attrs?.checked ? 'x' : ' ' - const itemContent = (item.content || []) as Array<{ type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }> - itemContent.forEach((para: { type?: string; content?: Array<{ type?: string; text?: string; marks?: Array<{ type: string; attrs?: Record }> }>; attrs?: Record }, paraIndex: number) => { - const text = nodeToText(para) - if (paraIndex === 0) { - listLines.push(`- [${checked}] ${text}`) - } else { - listLines.push(' ' + text) - } - }) - }) - blocks.push(listLines.join('\n')) + return lines + } + blocks.push(serializeList(node, 0).join('\n')) } else if (node.type === 'taskBlock') { blocks.push('```task\n' + (node.attrs?.data as string || '{}') + '\n```') } else if (node.type === 'imageBlock') { From 1c5e5afda8a6a8198562dc0bba0cbbf4617e98e8 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:50:23 +0530 Subject: [PATCH 21/97] fix oauth callback params propagation --- apps/x/apps/main/src/auth-server.ts | 4 ++-- apps/x/apps/main/src/composio-handler.ts | 2 +- apps/x/apps/main/src/oauth-handler.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/x/apps/main/src/auth-server.ts b/apps/x/apps/main/src/auth-server.ts index b0b890c0..78e519d0 100644 --- a/apps/x/apps/main/src/auth-server.ts +++ b/apps/x/apps/main/src/auth-server.ts @@ -25,7 +25,7 @@ export interface AuthServerResult { */ export function createAuthServer( port: number = DEFAULT_PORT, - onCallback: (code: string, state: string) => void | Promise + onCallback: (params: Record) => void | Promise ): Promise { return new Promise((resolve, reject) => { const server = createServer((req, res) => { @@ -67,7 +67,7 @@ export function createAuthServer( // Handle callback - either traditional OAuth with code/state or Composio-style notification // Composio callbacks may not have code/state, just a notification that the flow completed - onCallback(code || '', state || ''); + onCallback(Object.fromEntries(url.searchParams.entries())); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts index e5b25d1a..452a76e3 100644 --- a/apps/x/apps/main/src/composio-handler.ts +++ b/apps/x/apps/main/src/composio-handler.ts @@ -143,7 +143,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ // Set up callback server let cleanupTimeout: NodeJS.Timeout; - const { server } = await createAuthServer(8081, async (_code, _state) => { + const { server } = await createAuthServer(8081, async () => { // OAuth callback received - sync the account status try { const accountStatus = await composioClient.getConnectedAccount(connectedAccountId); diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index 2efc77c2..bf9c77ff 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -186,9 +186,9 @@ export async function connectProvider(provider: string, clientId?: string): Prom }); // Create callback server - const { server } = await createAuthServer(8080, async (code, receivedState) => { + const { server } = await createAuthServer(8080, async (params: Record) => { // Validate state - if (receivedState !== state) { + if (params.state !== state) { throw new Error('Invalid state parameter - possible CSRF attack'); } @@ -199,7 +199,7 @@ export async function connectProvider(provider: string, clientId?: string): Prom try { // Build callback URL for token exchange - const callbackUrl = new URL(`${REDIRECT_URI}?code=${code}&state=${receivedState}`); + const callbackUrl = new URL(`${REDIRECT_URI}?${new URLSearchParams(params).toString()}`); // Exchange code for tokens console.log(`[OAuth] Exchanging authorization code for tokens (${provider})...`); From 903fecc5f538ddf0cd2771201711e1d4807e124c Mon Sep 17 00:00:00 2001 From: arkml <6592213+arkml@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:07:41 +0530 Subject: [PATCH 22/97] Daily5 (#457) * better cal and email block design * modified email block and draft with copilot --- .../src/extensions/calendar-block.tsx | 26 +- .../renderer/src/extensions/email-block.tsx | 345 ++++------ apps/x/apps/renderer/src/styles/editor.css | 628 +++++++----------- apps/x/packages/shared/src/blocks.ts | 1 + 4 files changed, 366 insertions(+), 634 deletions(-) diff --git a/apps/x/apps/renderer/src/extensions/calendar-block.tsx b/apps/x/apps/renderer/src/extensions/calendar-block.tsx index f72dc5d4..9f0eec02 100644 --- a/apps/x/apps/renderer/src/extensions/calendar-block.tsx +++ b/apps/x/apps/renderer/src/extensions/calendar-block.tsx @@ -9,12 +9,15 @@ function formatTime(dateStr: string): string { return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) } -function getDateParts(dateStr: string): { day: number; month: string; weekday: string } { +function getDateParts(dateStr: string): { day: number; month: string; weekday: string; isToday: boolean } { const d = new Date(dateStr) + const now = new Date() + const isToday = d.getDate() === now.getDate() && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear() return { day: d.getDate(), - month: d.toLocaleDateString([], { month: 'long' }), - weekday: d.toLocaleDateString([], { weekday: 'short' }), + month: d.toLocaleDateString([], { month: 'short' }).toUpperCase(), + weekday: d.toLocaleDateString([], { weekday: 'short' }).toUpperCase(), + isToday, } } @@ -62,7 +65,8 @@ interface ResolvedEvent { conferenceLink?: string } -const EVENT_BAR_COLOR = '#7ec8c8' +const GCAL_EVENT_COLOR = '#039be5' +const GCAL_TODAY_COLOR = '#1a73e8' function JoinMeetingSplitButton({ onJoinAndNotes, onNotesOnly }: { onJoinAndNotes: () => void @@ -273,11 +277,8 @@ function CalendarBlockView({ node, deleteNode }: { node: { attrs: Record {parts ? ( <> - {parts.day} -
- {parts.month} - {parts.weekday} -
+ {parts.weekday} + {parts.day} ) : ( ? @@ -288,16 +289,13 @@ function CalendarBlockView({ node, deleteNode }: { node: { attrs: Record e.stopPropagation()} onClick={(e) => { e.stopPropagation(); handleEventClick(event) }} > -
- {event.summary || 'Untitled event'} + {event.summary || '(No title)'}
{getTimeRange(event)} diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx index 9be8c72c..7356c94c 100644 --- a/apps/x/apps/renderer/src/extensions/email-block.tsx +++ b/apps/x/apps/renderer/src/extensions/email-block.tsx @@ -1,8 +1,9 @@ import { mergeAttributes, Node } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' -import { X, Mail, ChevronDown, ExternalLink, Copy, Check, Sparkles, Loader2, MessageSquare } from 'lucide-react' +import { X, Mail, ChevronDown, ExternalLink, Copy, Check, MessageSquare } from 'lucide-react' import { blocks } from '@x/shared' import { useState, useEffect, useRef, useCallback } from 'react' +import { useTheme } from '@/contexts/theme-context' // --- Helpers --- @@ -17,8 +18,10 @@ function formatEmailDate(dateStr: string): string { } } -function getInitials(name: string): string { - return name.split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase() +/** Extract just the name part from "Name " format */ +function senderFirstName(from: string): string { + const name = from.replace(/<.*>/, '').trim() + return name.split(/\s+/)[0] || name } declare global { @@ -45,27 +48,15 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { const hasDraft = !!config?.draft_response const hasPastSummary = !!config?.past_summary - const responseMode = config?.response_mode || 'both' + + const { resolvedTheme } = useTheme() // Local draft state for editing const [draftBody, setDraftBody] = useState(config?.draft_response || '') - const [contextExpanded, setContextExpanded] = useState(false) + const [emailExpanded, setEmailExpanded] = useState(false) const [copied, setCopied] = useState(false) - const [generating, setGenerating] = useState(false) - const [responseSplitOpen, setResponseSplitOpen] = useState(false) - const responseSplitRef = useRef(null) const bodyRef = useRef(null) - // Close split dropdown on outside click - useEffect(() => { - if (!responseSplitOpen) return - const handler = (e: MouseEvent) => { - if (responseSplitRef.current && !responseSplitRef.current.contains(e.target as globalThis.Node)) setResponseSplitOpen(false) - } - document.addEventListener('mousedown', handler) - return () => document.removeEventListener('mousedown', handler) - }, [responseSplitOpen]) - // Sync draft from external changes useEffect(() => { try { @@ -89,53 +80,23 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { } catch { /* ignore */ } }, [raw, updateAttributes]) - const generateResponse = useCallback(async () => { - if (!config || generating) return - setGenerating(true) - try { - const ipc = (window as unknown as { ipc: { invoke: (channel: string, args: Record) => Promise<{ response?: string }> } }).ipc - // Build context for the agent - let noteContent = `# Email: ${config.subject || 'No subject'}\n\n` - noteContent += `**From:** ${config.from || 'Unknown'}\n` - noteContent += `**Date:** ${config.date || 'Unknown'}\n\n` - noteContent += `## Latest email\n\n${config.latest_email}\n\n` - if (config.past_summary) { - noteContent += `## Earlier conversation summary\n\n${config.past_summary}\n\n` - } - - const result = await ipc.invoke('inline-task:process', { - instruction: `Draft a concise, professional response to this email. Return only the email body text, no subject line or headers.`, - noteContent, - notePath: '', - }) - - if (result.response) { - // Clean up the response — strip any markdown headers the agent may add - const cleaned = result.response.replace(/^#+\s+.*\n*/gm, '').trim() - setDraftBody(cleaned) - // Update the block data to include the draft - const current = JSON.parse(raw) as Record - updateAttributes({ data: JSON.stringify({ ...current, draft_response: cleaned }) }) - } - } catch (err) { - console.error('[email-block] Failed to generate response:', err) - } finally { - setGenerating(false) - } - }, [config, generating, raw, updateAttributes]) - const draftWithAssistant = useCallback(() => { if (!config) return - let prompt = `Help me draft a response to this email` + let prompt = draftBody + ? `Help me refine this draft response to an email` + : `Help me draft a response to this email` if (config.threadId) { prompt += `. Read the full thread at gmail_sync/${config.threadId}.md for context` } prompt += `.\n\n` prompt += `**From:** ${config.from || 'Unknown'}\n` prompt += `**Subject:** ${config.subject || 'No subject'}\n` + if (draftBody) { + prompt += `\n**Current draft:**\n${draftBody}\n` + } window.__pendingEmailDraft = { prompt } window.dispatchEvent(new Event('email-block:draft-with-assistant')) - }, [config]) + }, [config, draftBody]) if (!config) { return ( @@ -152,185 +113,112 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { ? `https://mail.google.com/mail/u/0/#all/${config.threadId}` : null - // --- Render: Draft mode (draft_response present) --- - if (hasDraft) { - return ( - -
e.stopPropagation()}> - - {/* Draft header */} - {config.to && ( -
-
- To - {config.to} -
- {config.subject && ( -
- Subject - {config.subject} + // Build summary: use explicit summary, or auto-generate from sender + subject + const summary = config.summary + || (config.from && config.subject + ? `${senderFirstName(config.from)} reached out about ${config.subject}` + : config.subject || 'New email') + + return ( + +
e.stopPropagation()}> + + + {/* Header: Email badge */} +
+ + Email +
+ + {/* Summary */} +
{summary}
+ + {/* Expandable email details */} + + + {emailExpanded && ( +
+
+
+
+
+
{config.from || 'Unknown'}
+ {config.date &&
{formatEmailDate(config.date)}
} +
+ {config.subject &&
Subject: {config.subject}
}
- )} +
+
{config.latest_email}
- )} - {/* Editable draft body */} -