Merge branch 'dev' of github.com:rowboatlabs/rowboat into dev

This commit is contained in:
tusharmagar 2026-01-20 15:25:27 +05:30
commit cc38e8731e
10 changed files with 264 additions and 23 deletions

View file

@ -158,7 +158,7 @@ module.exports = {
// Read version from source package.json (updated by CI from git tag)
const sourcePackageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
const packageJson = {
name: '@x/main',
name: 'Rowboat',
version: sourcePackageJson.version,
main: 'dist-bundle/main.js',
};
@ -227,7 +227,7 @@ module.exports = {
// Read version from source package.json (updated by CI from git tag)
const sourcePackageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
const packageJson = {
name: '@x/main',
name: 'Rowboat',
version: sourcePackageJson.version,
main: 'dist-bundle/main.js',
// Note: No "type": "module" since we bundle as CommonJS

View file

@ -1,5 +1,5 @@
{
"name": "@x/main",
"name": "Rowboat",
"type": "module",
"version": "0.1.0",
"main": "dist/main.js",

View file

@ -210,6 +210,15 @@ function emitRunEvent(event: z.infer<typeof RunEvent>): void {
}
}
export function emitOAuthEvent(event: { provider: string; success: boolean; error?: string }): void {
const windows = BrowserWindow.getAllWindows();
for (const win of windows) {
if (!win.isDestroyed() && win.webContents) {
win.webContents.send('oauth:didConnect', event);
}
}
}
let runsWatcher: (() => void) | null = null;
export async function startRunsWatcher(): Promise<void> {
if (runsWatcher) {

View file

@ -9,6 +9,7 @@ import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js';
import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
import { triggerSync as triggerCalendarSync } from '@x/core/dist/knowledge/sync_calendar.js';
import { triggerSync as triggerFirefliesSync } from '@x/core/dist/knowledge/sync_fireflies.js';
import { emitOAuthEvent } from './ipc.js';
const REDIRECT_URI = 'http://localhost:8080/oauth/callback';
@ -160,8 +161,13 @@ export async function connectProvider(provider: string): Promise<{ success: bool
} else if (provider === 'fireflies-ai') {
triggerFirefliesSync();
}
// Emit success event to renderer
emitOAuthEvent({ provider, success: true });
} catch (error) {
console.error('OAuth token exchange failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
emitOAuthEvent({ provider, success: false, error: errorMessage });
throw error;
} finally {
// Clean up
@ -178,6 +184,7 @@ export async function connectProvider(provider: string): Promise<{ success: bool
console.log(`[OAuth] Cleaning up abandoned OAuth flow for ${provider} (timeout)`);
activeFlows.delete(state);
server.close();
emitOAuthEvent({ provider, success: false, error: 'OAuth flow timed out' });
}
}, 5 * 60 * 1000); // 5 minutes

View file

@ -127,6 +127,32 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
}
}, [open, providers, refreshAllStatuses])
// Listen for OAuth completion events
useEffect(() => {
const cleanup = window.ipc.on('oauth:didConnect', (event) => {
const { provider, success, error } = event
setProviderStates(prev => ({
...prev,
[provider]: {
isConnected: success,
isLoading: false,
isConnecting: false,
}
}))
if (success) {
toast(`Successfully connected to ${provider}`, 'success')
// Refresh status to ensure consistency
refreshAllStatuses()
} else {
toast(error || `Failed to connect to ${provider}`, 'error')
}
})
return cleanup
}, [refreshAllStatuses])
// Connect to a provider
const handleConnect = useCallback(async (provider: string) => {
setProviderStates(prev => ({
@ -138,18 +164,10 @@ export function ConnectorsPopover({ children, tooltip }: ConnectorsPopoverProps)
const result = await window.ipc.invoke('oauth:connect', { provider })
if (result.success) {
toast(`Successfully connected to ${provider}`, 'success')
// Refresh the status after successful connection
const checkResult = await window.ipc.invoke('oauth:is-connected', { provider })
setProviderStates(prev => ({
...prev,
[provider]: {
isConnected: checkResult.isConnected,
isLoading: false,
isConnecting: false,
}
}))
// OAuth flow started - keep isConnecting state, wait for event
// Event listener will handle the actual completion
} else {
// Immediate failure (e.g., couldn't start flow)
toast(result.error || `Failed to connect to ${provider}`, 'error')
setProviderStates(prev => ({
...prev,

View file

@ -9,11 +9,6 @@ export function useOAuth(provider: string) {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isConnecting, setIsConnecting] = useState<boolean>(false);
// Check connection status on mount and when provider changes
useEffect(() => {
checkConnection();
}, [provider]);
const checkConnection = useCallback(async () => {
try {
setIsLoading(true);
@ -27,23 +22,52 @@ export function useOAuth(provider: string) {
}
}, [provider]);
// Check connection status on mount and when provider changes
useEffect(() => {
checkConnection();
}, [provider, checkConnection]);
// Listen for OAuth completion events
useEffect(() => {
const cleanup = window.ipc.on('oauth:didConnect', (event) => {
if (event.provider !== provider) {
return; // Ignore events for other providers
}
setIsConnected(event.success);
setIsConnecting(false);
setIsLoading(false);
if (event.success) {
toast(`Successfully connected to ${provider}`, 'success');
// Refresh connection status to ensure consistency
checkConnection();
} else {
toast(event.error || `Failed to connect to ${provider}`, 'error');
}
});
return cleanup;
}, [provider, checkConnection]);
const connect = useCallback(async () => {
try {
setIsConnecting(true);
const result = await window.ipc.invoke('oauth:connect', { provider });
if (result.success) {
toast(`Successfully connected to ${provider}`, 'success');
await checkConnection();
// OAuth flow started - keep isConnecting state, wait for event
// Event listener will handle the actual completion
} else {
// Immediate failure (e.g., couldn't start flow)
toast(result.error || `Failed to connect to ${provider}`, 'error');
setIsConnecting(false);
}
} catch (error) {
console.error('Failed to connect:', error);
toast(`Failed to connect to ${provider}`, 'error');
} finally {
setIsConnecting(false);
}
}, [provider, checkConnection]);
}, [provider]);
const disconnect = useCallback(async () => {
try {

View file

@ -24,6 +24,8 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects,
**Email Drafting:** When users ask you to draft emails or respond to emails, load the \`draft-emails\` skill first. It provides structured guidance for processing emails, gathering context from calendar and knowledge base, and creating well-informed draft responses.
**Meeting Prep:** When users ask you to prepare for a meeting, prep for a call, or brief them on attendees, load the \`meeting-prep\` skill first. It provides structured guidance for gathering context about attendees from the knowledge base and creating useful meeting briefs.
## 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.

View file

@ -4,6 +4,7 @@ import builtinToolsSkill from "./builtin-tools/skill.js";
import deletionGuardrailsSkill from "./deletion-guardrails/skill.js";
import draftEmailsSkill from "./draft-emails/skill.js";
import mcpIntegrationSkill from "./mcp-integration/skill.js";
import meetingPrepSkill from "./meeting-prep/skill.js";
import workflowAuthoringSkill from "./workflow-authoring/skill.js";
import workflowRunOpsSkill from "./workflow-run-ops/skill.js";
@ -33,6 +34,13 @@ const definitions: SkillDefinition[] = [
summary: "Process incoming emails and create draft responses using calendar and knowledge base for context.",
content: draftEmailsSkill,
},
{
id: "meeting-prep",
title: "Meeting Prep",
folder: "meeting-prep",
summary: "Prepare for meetings by gathering context about attendees from the knowledge base.",
content: meetingPrepSkill,
},
{
id: "workflow-authoring",
title: "Workflow Authoring",

View file

@ -0,0 +1,165 @@
export const skill = String.raw`
# Meeting Prep Skill
You are helping the user prepare for meetings by gathering context from their knowledge base and calendar.
## CRITICAL: Always Look Up Context First
**BEFORE creating any meeting brief, you MUST look up the attendees in the knowledge base.**
**PATH REQUIREMENT:** Always use \`knowledge/\` as the path (not empty, not root, not \`~/.rowboat\`).
- **WRONG:** \`path: ""\` or \`path: "."\`
- **CORRECT:** \`path: "knowledge/"\`
When the user asks to prep for a meeting or mentions attendees:
1. **STOP** - Do not create a generic brief
2. **SEARCH** - Look up each attendee in the knowledge base:
\`\`\`
workspace-grep({ pattern: "Attendee Name", path: "knowledge/" })
\`\`\`
3. **READ** - Read their notes to understand who they are:
\`\`\`
workspace-readFile("knowledge/People/Attendee Name.md")
workspace-readFile("knowledge/Organizations/Their Company.md")
\`\`\`
4. **UNDERSTAND** - Extract their role, organization, relationship history, past interactions, open items
5. **THEN BRIEF** - Only now create the meeting brief, using this context
**DO NOT** skip this step. **DO NOT** provide generic briefs. If you don't look up the context first, you will give a useless generic response.
## Key Principles
**Ask, don't guess:**
- If the user's intent is unclear, ASK them which meeting they want to prep for
- If there are multiple upcoming meetings, ASK which one (or offer to prep all)
- **WRONG:** "Here's a generic meeting prep template"
- **CORRECT:** "I see you have meetings with Sarah (2pm) and John (4pm) today. Which one would you like me to prep?"
**Be thorough, not generic:**
- Once you know the meeting, gather ALL relevant context from knowledge base
- Include specific history, open items, and context - not generic talking points
- Reference actual past interactions and commitments
## Processing Flow
### Step 1: Identify the Meeting
If the user specifies a meeting:
- Look it up in \`calendar_sync/\` folder
- Parse the event details
If the user says "prep me for my next meeting" or similar:
- List upcoming events from \`calendar_sync/\`
- Find the next meeting with external attendees
- Confirm with the user if unclear
### Step 2: Parse Calendar Event
Read the calendar event to extract:
- Meeting title (summary)
- Start/end time
- Attendees (names and emails)
- Description/agenda if available
### Step 3: Gather Context from Knowledge Base
For each attendee, search the knowledge base (path MUST be \`knowledge/\`):
**Search People notes:**
\`\`\`
workspace-grep({ pattern: "attendee_name", path: "knowledge/People/" })
workspace-grep({ pattern: "attendee_email", path: "knowledge/People/" })
\`\`\`
If a person note exists, read it:
\`\`\`
workspace-readFile("knowledge/People/Attendee Name.md")
\`\`\`
Extract:
- Their role/title
- Company/organization
- Key facts about them
- Previous interactions
- Open items
**Search Organization notes:**
\`\`\`
workspace-grep({ pattern: "company_name", path: "knowledge/Organizations/" })
\`\`\`
**Search Projects:**
\`\`\`
workspace-grep({ pattern: "attendee_name", path: "knowledge/Projects/" })
workspace-grep({ pattern: "company_name", path: "knowledge/Projects/" })
\`\`\`
### Step 4: Create Meeting Brief
Create a brief with this format:
\`\`\`markdown
📋
Meeting Brief: {Attendee Name}
{Time} today · {Company}
About {First Name}
{Role at company}. {Key background - 1-2 sentences}. {What they care about or focus on}.
Your History
- {Date}: {Brief description of interaction/outcome}
- {Date}: {Brief description}
- {Date}: {Brief description}
Open Items
- {Action item} (they asked {date})
- {Action item}
Suggested Talking Points
- {Concrete suggestion based on history}
- {Reference relevant entities with [[wiki-links]]}
\`\`\`
**Example:**
\`\`\`markdown
📋
Meeting Brief: Sarah Chen
2:00 PM today · Horizon Ventures
About Sarah
Partner at Horizon Ventures. Led investments in WorkOS and Segment. Very focused on unit economics.
Your History
- Jan 15: Partner meeting positive reception
- Jan 12: Sent updated deck with cohort analysis
- Jan 8: First pitch she loved the 125% NRR
Open Items
- Send updated financial model (she asked Jan 15)
- Discuss term sheet timeline
Suggested Talking Points
- Address her question about CAC by channel
- Mention [[TechFlow]] expansion closed ($120K ARR)
\`\`\`
**Briefing Guidelines:**
- Use \`[[Name]]\` wiki-link syntax for cross-references to people, projects, orgs
- Keep "About" section concise - 2-3 sentences max
- History should be reverse chronological (most recent first)
- Limit to 3-5 most relevant history items
- Open items should be actionable and specific
- Talking points should be concrete, not generic
- If no notes exist for a person, mention that and offer to create one
## Important Notes
- Only prep for meetings with external attendees
- Skip internal calendar blocks (DND, Focus Time, Lunch, etc.)
- For meetings with multiple attendees, create sections for each key person
- Prioritize recent interactions (last 30 days) in the history section
- If an attendee has no notes, suggest what you'd want to capture about them
`;
export default skill;

View file

@ -209,6 +209,14 @@ const ipcSchemas = {
providers: z.array(z.string()),
}),
},
'oauth:didConnect': {
req: z.object({
provider: z.string(),
success: z.boolean(),
error: z.string().optional(),
}),
res: z.null(),
},
'granola:getConfig': {
req: z.null(),
res: z.object({