mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-28 19:05:31 +02:00
Merge branch 'dev' of github.com:rowboatlabs/rowboat into dev
This commit is contained in:
commit
cc38e8731e
10 changed files with 264 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@x/main",
|
||||
"name": "Rowboat",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/main.js",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue