fellow-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/database.d.ts +76 -0
- package/dist/database.js +244 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1117 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Itai Liba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Fellow MCP Server
|
|
2
|
+
|
|
3
|
+
A local MCP (Model Context Protocol) server that wraps the Fellow.ai API, providing tools to access meeting data, transcripts, summaries, action items, and participants.
|
|
4
|
+
|
|
5
|
+
**Features:**
|
|
6
|
+
- Local SQLite database for caching meeting data
|
|
7
|
+
- Automatic incremental sync to keep action items fresh
|
|
8
|
+
- Full-text search across cached notes
|
|
9
|
+
- Find meetings by participant
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### Option 1: Install from npm (Recommended)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g fellow-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Option 2: Install from source
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/liba2k/fellow-mcp.git
|
|
23
|
+
cd fellow-mcp
|
|
24
|
+
npm install
|
|
25
|
+
npm run build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### 1. Get your Fellow API credentials
|
|
31
|
+
|
|
32
|
+
1. Log into your Fellow account
|
|
33
|
+
2. Navigate to Developer API settings in your User settings
|
|
34
|
+
3. Generate a new API key
|
|
35
|
+
4. Note your workspace subdomain (the part before `.fellow.app` in your URL)
|
|
36
|
+
|
|
37
|
+
### 2. Configure your MCP client
|
|
38
|
+
|
|
39
|
+
#### If installed via npm:
|
|
40
|
+
|
|
41
|
+
Add the following to your MCP client configuration (e.g., `~/.config/opencode/opencode.json`):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcp": {
|
|
46
|
+
"fellow": {
|
|
47
|
+
"type": "local",
|
|
48
|
+
"command": ["fellow-mcp"],
|
|
49
|
+
"env": {
|
|
50
|
+
"FELLOW_API_KEY": "your-api-key-here",
|
|
51
|
+
"FELLOW_SUBDOMAIN": "your-subdomain"
|
|
52
|
+
},
|
|
53
|
+
"enabled": true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### If installed from source:
|
|
60
|
+
|
|
61
|
+
Option A: Use the run.sh script with .env file:
|
|
62
|
+
|
|
63
|
+
1. Copy the example env file and fill in your credentials:
|
|
64
|
+
```bash
|
|
65
|
+
cp .env.example .env
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. Edit `.env` with your credentials:
|
|
69
|
+
```
|
|
70
|
+
FELLOW_API_KEY=your-api-key-here
|
|
71
|
+
FELLOW_SUBDOMAIN=your-subdomain
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
3. Configure your MCP client:
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcp": {
|
|
78
|
+
"fellow": {
|
|
79
|
+
"type": "local",
|
|
80
|
+
"command": ["/path/to/fellow-mcp/run.sh"],
|
|
81
|
+
"enabled": true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Option B: Pass environment variables directly:
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcp": {
|
|
91
|
+
"fellow": {
|
|
92
|
+
"type": "local",
|
|
93
|
+
"command": ["node", "/path/to/fellow-mcp/dist/index.js"],
|
|
94
|
+
"env": {
|
|
95
|
+
"FELLOW_API_KEY": "your-api-key-here",
|
|
96
|
+
"FELLOW_SUBDOMAIN": "your-subdomain"
|
|
97
|
+
},
|
|
98
|
+
"enabled": true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Available Tools
|
|
105
|
+
|
|
106
|
+
### API Tools (Direct Fellow API calls)
|
|
107
|
+
|
|
108
|
+
#### `search_meetings`
|
|
109
|
+
Search for meetings/recordings in Fellow.
|
|
110
|
+
|
|
111
|
+
**Parameters:**
|
|
112
|
+
- `title` (optional): Filter by meeting title (case-insensitive partial match)
|
|
113
|
+
- `created_at_start` (optional): Filter meetings created after this date (ISO format)
|
|
114
|
+
- `created_at_end` (optional): Filter meetings created before this date (ISO format)
|
|
115
|
+
- `limit` (optional): Maximum number of results (1-50, default 20)
|
|
116
|
+
|
|
117
|
+
#### `get_meeting_transcript`
|
|
118
|
+
Get the full transcript of a meeting recording with speaker labels and timestamps.
|
|
119
|
+
|
|
120
|
+
**Parameters:**
|
|
121
|
+
- `recording_id` (optional): The ID of the recording
|
|
122
|
+
- `meeting_title` (optional): Search by meeting title
|
|
123
|
+
|
|
124
|
+
#### `get_meeting_summary`
|
|
125
|
+
Get the meeting summary/notes content including agenda items, discussion topics, and decisions.
|
|
126
|
+
|
|
127
|
+
**Parameters:**
|
|
128
|
+
- `note_id` (optional): The ID of the note
|
|
129
|
+
- `recording_id` (optional): Get the summary for a recording's associated note
|
|
130
|
+
- `meeting_title` (optional): Search by meeting title
|
|
131
|
+
|
|
132
|
+
#### `get_action_items`
|
|
133
|
+
Extract action items from a single meeting's notes.
|
|
134
|
+
|
|
135
|
+
**Parameters:**
|
|
136
|
+
- `note_id` (optional): The ID of the note
|
|
137
|
+
- `meeting_title` (optional): Search by meeting title
|
|
138
|
+
|
|
139
|
+
#### `get_meeting_participants`
|
|
140
|
+
Get the list of participants/attendees for a meeting.
|
|
141
|
+
|
|
142
|
+
**Parameters:**
|
|
143
|
+
- `note_id` (optional): The ID of the note
|
|
144
|
+
- `meeting_title` (optional): Search by meeting title
|
|
145
|
+
|
|
146
|
+
### Database Tools (Local SQLite cache)
|
|
147
|
+
|
|
148
|
+
#### `sync_meetings`
|
|
149
|
+
Sync meetings from Fellow API to local database.
|
|
150
|
+
|
|
151
|
+
**Parameters:**
|
|
152
|
+
- `force` (optional, default: false): If true, performs full re-sync. Otherwise does incremental sync (only new/updated since last sync)
|
|
153
|
+
- `include_transcripts` (optional, default: false): If true, also fetches and stores transcripts (slower)
|
|
154
|
+
|
|
155
|
+
#### `get_all_action_items`
|
|
156
|
+
Get all action items from the local database. **Automatically performs incremental sync first** to ensure data is fresh.
|
|
157
|
+
|
|
158
|
+
**Parameters:**
|
|
159
|
+
- `assignee` (optional): Filter by assignee name (partial match)
|
|
160
|
+
- `show_completed` (optional, default: false): If true, includes completed action items
|
|
161
|
+
- `since` (optional): Only return action items from meetings on or after this date (ISO format: YYYY-MM-DD)
|
|
162
|
+
|
|
163
|
+
#### `get_meetings_by_participants`
|
|
164
|
+
Find meetings that included specific participants.
|
|
165
|
+
|
|
166
|
+
**Parameters:**
|
|
167
|
+
- `emails` (required): List of email addresses to search for
|
|
168
|
+
- `require_all` (optional, default: false): If true, only return meetings where ALL specified participants attended
|
|
169
|
+
|
|
170
|
+
#### `search_cached_notes`
|
|
171
|
+
Full-text search across all cached meeting notes (titles and content).
|
|
172
|
+
|
|
173
|
+
**Parameters:**
|
|
174
|
+
- `query` (required): Search query
|
|
175
|
+
|
|
176
|
+
#### `get_sync_status`
|
|
177
|
+
Get the current sync status and database statistics.
|
|
178
|
+
|
|
179
|
+
## Local Database
|
|
180
|
+
|
|
181
|
+
Meeting data is cached in a local SQLite database at `~/.fellow-mcp/fellow.db`. This enables:
|
|
182
|
+
|
|
183
|
+
- Fast local searches
|
|
184
|
+
- Querying across all action items
|
|
185
|
+
- Finding meetings by participant
|
|
186
|
+
- Offline access to cached data
|
|
187
|
+
|
|
188
|
+
The database stores:
|
|
189
|
+
- Notes (meeting summaries, agendas, content)
|
|
190
|
+
- Recordings (with optional transcripts)
|
|
191
|
+
- Action items (parsed from notes with assignee/due date extraction)
|
|
192
|
+
- Participants (email addresses)
|
|
193
|
+
|
|
194
|
+
## Environment Variables
|
|
195
|
+
|
|
196
|
+
| Variable | Required | Description |
|
|
197
|
+
|----------|----------|-------------|
|
|
198
|
+
| `FELLOW_API_KEY` | Yes | Your Fellow API key |
|
|
199
|
+
| `FELLOW_SUBDOMAIN` | Yes | Your Fellow workspace subdomain |
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Watch mode for development
|
|
205
|
+
npm run dev
|
|
206
|
+
|
|
207
|
+
# Build
|
|
208
|
+
npm run build
|
|
209
|
+
|
|
210
|
+
# Test the server
|
|
211
|
+
node dist/index.js --test
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Requirements
|
|
215
|
+
|
|
216
|
+
- Node.js >= 18.0.0
|
|
217
|
+
- A Fellow.ai account with API access
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT
|
|
222
|
+
|
|
223
|
+
## API Reference
|
|
224
|
+
|
|
225
|
+
This MCP server wraps the [Fellow Developer API](https://developers.fellow.ai/reference/introduction). The API uses:
|
|
226
|
+
- `X-API-KEY` header for authentication
|
|
227
|
+
- POST requests for list operations (with JSON body for filters/pagination)
|
|
228
|
+
- GET requests for retrieving individual resources
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface StoredNote {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
created_at: string;
|
|
5
|
+
updated_at: string;
|
|
6
|
+
event_start: string | null;
|
|
7
|
+
event_end: string | null;
|
|
8
|
+
event_guid: string | null;
|
|
9
|
+
call_url: string | null;
|
|
10
|
+
content_markdown: string | null;
|
|
11
|
+
synced_at: string;
|
|
12
|
+
}
|
|
13
|
+
export interface StoredRecording {
|
|
14
|
+
id: string;
|
|
15
|
+
note_id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
created_at: string;
|
|
18
|
+
updated_at: string;
|
|
19
|
+
event_start: string | null;
|
|
20
|
+
event_end: string | null;
|
|
21
|
+
recording_start: string | null;
|
|
22
|
+
recording_end: string | null;
|
|
23
|
+
event_guid: string | null;
|
|
24
|
+
call_url: string | null;
|
|
25
|
+
transcript_json: string | null;
|
|
26
|
+
synced_at: string;
|
|
27
|
+
}
|
|
28
|
+
export interface StoredActionItem {
|
|
29
|
+
id: number;
|
|
30
|
+
note_id: string;
|
|
31
|
+
content: string;
|
|
32
|
+
assignee: string | null;
|
|
33
|
+
due_date: string | null;
|
|
34
|
+
is_completed: boolean;
|
|
35
|
+
created_at: string;
|
|
36
|
+
}
|
|
37
|
+
export interface StoredParticipant {
|
|
38
|
+
id: number;
|
|
39
|
+
note_id: string;
|
|
40
|
+
email: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class FellowDatabase {
|
|
43
|
+
private db;
|
|
44
|
+
constructor(dbPath?: string);
|
|
45
|
+
private initSchema;
|
|
46
|
+
upsertNote(note: Omit<StoredNote, "synced_at">): void;
|
|
47
|
+
getNote(id: string): StoredNote | null;
|
|
48
|
+
getAllNotes(): StoredNote[];
|
|
49
|
+
searchNotes(query: string): StoredNote[];
|
|
50
|
+
upsertRecording(recording: Omit<StoredRecording, "synced_at">): void;
|
|
51
|
+
getRecording(id: string): StoredRecording | null;
|
|
52
|
+
clearActionItemsForNote(noteId: string): void;
|
|
53
|
+
insertActionItem(item: Omit<StoredActionItem, "id">): void;
|
|
54
|
+
getAllActionItems(filters?: {
|
|
55
|
+
assignee?: string;
|
|
56
|
+
is_completed?: boolean;
|
|
57
|
+
since?: string;
|
|
58
|
+
}): (StoredActionItem & {
|
|
59
|
+
note_title: string;
|
|
60
|
+
event_start: string | null;
|
|
61
|
+
})[];
|
|
62
|
+
clearParticipantsForNote(noteId: string): void;
|
|
63
|
+
insertParticipant(noteId: string, email: string): void;
|
|
64
|
+
getMeetingsByParticipants(emails: string[]): StoredNote[];
|
|
65
|
+
getMeetingsWithAllParticipants(emails: string[]): StoredNote[];
|
|
66
|
+
getParticipantsForNote(noteId: string): string[];
|
|
67
|
+
getLastSyncTime(): string | null;
|
|
68
|
+
setLastSyncTime(time: string): void;
|
|
69
|
+
getStats(): {
|
|
70
|
+
notes: number;
|
|
71
|
+
recordings: number;
|
|
72
|
+
action_items: number;
|
|
73
|
+
participants: number;
|
|
74
|
+
};
|
|
75
|
+
close(): void;
|
|
76
|
+
}
|
package/dist/database.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
export class FellowDatabase {
|
|
6
|
+
db;
|
|
7
|
+
constructor(dbPath) {
|
|
8
|
+
const defaultPath = path.join(os.homedir(), ".fellow-mcp", "fellow.db");
|
|
9
|
+
const finalPath = dbPath ?? defaultPath;
|
|
10
|
+
// Ensure directory exists
|
|
11
|
+
const dir = path.dirname(finalPath);
|
|
12
|
+
if (!fs.existsSync(dir)) {
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
this.db = new Database(finalPath);
|
|
16
|
+
this.db.pragma("journal_mode = WAL");
|
|
17
|
+
this.initSchema();
|
|
18
|
+
}
|
|
19
|
+
initSchema() {
|
|
20
|
+
this.db.exec(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
title TEXT NOT NULL,
|
|
24
|
+
created_at TEXT NOT NULL,
|
|
25
|
+
updated_at TEXT NOT NULL,
|
|
26
|
+
event_start TEXT,
|
|
27
|
+
event_end TEXT,
|
|
28
|
+
event_guid TEXT,
|
|
29
|
+
call_url TEXT,
|
|
30
|
+
content_markdown TEXT,
|
|
31
|
+
synced_at TEXT NOT NULL
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS recordings (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
note_id TEXT,
|
|
37
|
+
title TEXT NOT NULL,
|
|
38
|
+
created_at TEXT NOT NULL,
|
|
39
|
+
updated_at TEXT NOT NULL,
|
|
40
|
+
event_start TEXT,
|
|
41
|
+
event_end TEXT,
|
|
42
|
+
recording_start TEXT,
|
|
43
|
+
recording_end TEXT,
|
|
44
|
+
event_guid TEXT,
|
|
45
|
+
call_url TEXT,
|
|
46
|
+
transcript_json TEXT,
|
|
47
|
+
synced_at TEXT NOT NULL,
|
|
48
|
+
FOREIGN KEY (note_id) REFERENCES notes(id)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS action_items (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
note_id TEXT NOT NULL,
|
|
54
|
+
content TEXT NOT NULL,
|
|
55
|
+
assignee TEXT,
|
|
56
|
+
due_date TEXT,
|
|
57
|
+
is_completed INTEGER DEFAULT 0,
|
|
58
|
+
created_at TEXT NOT NULL,
|
|
59
|
+
FOREIGN KEY (note_id) REFERENCES notes(id)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS participants (
|
|
63
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
note_id TEXT NOT NULL,
|
|
65
|
+
email TEXT NOT NULL,
|
|
66
|
+
FOREIGN KEY (note_id) REFERENCES notes(id),
|
|
67
|
+
UNIQUE(note_id, email)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS sync_status (
|
|
71
|
+
key TEXT PRIMARY KEY,
|
|
72
|
+
value TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_notes_event_start ON notes(event_start);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_notes_title ON notes(title);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_recordings_note_id ON recordings(note_id);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_action_items_note_id ON action_items(note_id);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_action_items_assignee ON action_items(assignee);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_participants_note_id ON participants(note_id);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_participants_email ON participants(email);
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
// Notes
|
|
85
|
+
upsertNote(note) {
|
|
86
|
+
const stmt = this.db.prepare(`
|
|
87
|
+
INSERT INTO notes (id, title, created_at, updated_at, event_start, event_end, event_guid, call_url, content_markdown, synced_at)
|
|
88
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
89
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
90
|
+
title = excluded.title,
|
|
91
|
+
updated_at = excluded.updated_at,
|
|
92
|
+
event_start = excluded.event_start,
|
|
93
|
+
event_end = excluded.event_end,
|
|
94
|
+
event_guid = excluded.event_guid,
|
|
95
|
+
call_url = excluded.call_url,
|
|
96
|
+
content_markdown = COALESCE(excluded.content_markdown, content_markdown),
|
|
97
|
+
synced_at = excluded.synced_at
|
|
98
|
+
`);
|
|
99
|
+
stmt.run(note.id, note.title, note.created_at, note.updated_at, note.event_start, note.event_end, note.event_guid, note.call_url, note.content_markdown, new Date().toISOString());
|
|
100
|
+
}
|
|
101
|
+
getNote(id) {
|
|
102
|
+
const stmt = this.db.prepare("SELECT * FROM notes WHERE id = ?");
|
|
103
|
+
return stmt.get(id);
|
|
104
|
+
}
|
|
105
|
+
getAllNotes() {
|
|
106
|
+
const stmt = this.db.prepare("SELECT * FROM notes ORDER BY event_start DESC");
|
|
107
|
+
return stmt.all();
|
|
108
|
+
}
|
|
109
|
+
searchNotes(query) {
|
|
110
|
+
const stmt = this.db.prepare(`
|
|
111
|
+
SELECT * FROM notes
|
|
112
|
+
WHERE title LIKE ? OR content_markdown LIKE ?
|
|
113
|
+
ORDER BY event_start DESC
|
|
114
|
+
`);
|
|
115
|
+
const pattern = `%${query}%`;
|
|
116
|
+
return stmt.all(pattern, pattern);
|
|
117
|
+
}
|
|
118
|
+
// Recordings
|
|
119
|
+
upsertRecording(recording) {
|
|
120
|
+
const stmt = this.db.prepare(`
|
|
121
|
+
INSERT INTO recordings (id, note_id, title, created_at, updated_at, event_start, event_end, recording_start, recording_end, event_guid, call_url, transcript_json, synced_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
123
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
124
|
+
note_id = excluded.note_id,
|
|
125
|
+
title = excluded.title,
|
|
126
|
+
updated_at = excluded.updated_at,
|
|
127
|
+
event_start = excluded.event_start,
|
|
128
|
+
event_end = excluded.event_end,
|
|
129
|
+
recording_start = excluded.recording_start,
|
|
130
|
+
recording_end = excluded.recording_end,
|
|
131
|
+
event_guid = excluded.event_guid,
|
|
132
|
+
call_url = excluded.call_url,
|
|
133
|
+
transcript_json = COALESCE(excluded.transcript_json, transcript_json),
|
|
134
|
+
synced_at = excluded.synced_at
|
|
135
|
+
`);
|
|
136
|
+
stmt.run(recording.id, recording.note_id, recording.title, recording.created_at, recording.updated_at, recording.event_start, recording.event_end, recording.recording_start, recording.recording_end, recording.event_guid, recording.call_url, recording.transcript_json, new Date().toISOString());
|
|
137
|
+
}
|
|
138
|
+
getRecording(id) {
|
|
139
|
+
const stmt = this.db.prepare("SELECT * FROM recordings WHERE id = ?");
|
|
140
|
+
return stmt.get(id);
|
|
141
|
+
}
|
|
142
|
+
// Action Items
|
|
143
|
+
clearActionItemsForNote(noteId) {
|
|
144
|
+
const stmt = this.db.prepare("DELETE FROM action_items WHERE note_id = ?");
|
|
145
|
+
stmt.run(noteId);
|
|
146
|
+
}
|
|
147
|
+
insertActionItem(item) {
|
|
148
|
+
const stmt = this.db.prepare(`
|
|
149
|
+
INSERT INTO action_items (note_id, content, assignee, due_date, is_completed, created_at)
|
|
150
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
151
|
+
`);
|
|
152
|
+
stmt.run(item.note_id, item.content, item.assignee, item.due_date, item.is_completed ? 1 : 0, item.created_at);
|
|
153
|
+
}
|
|
154
|
+
getAllActionItems(filters) {
|
|
155
|
+
let query = `
|
|
156
|
+
SELECT a.*, n.title as note_title, n.event_start
|
|
157
|
+
FROM action_items a
|
|
158
|
+
JOIN notes n ON a.note_id = n.id
|
|
159
|
+
WHERE 1=1
|
|
160
|
+
`;
|
|
161
|
+
const params = [];
|
|
162
|
+
if (filters?.assignee) {
|
|
163
|
+
query += " AND a.assignee LIKE ?";
|
|
164
|
+
params.push(`%${filters.assignee}%`);
|
|
165
|
+
}
|
|
166
|
+
if (filters?.is_completed !== undefined) {
|
|
167
|
+
query += " AND a.is_completed = ?";
|
|
168
|
+
params.push(filters.is_completed ? 1 : 0);
|
|
169
|
+
}
|
|
170
|
+
if (filters?.since) {
|
|
171
|
+
query += " AND n.event_start >= ?";
|
|
172
|
+
params.push(filters.since);
|
|
173
|
+
}
|
|
174
|
+
query += " ORDER BY n.event_start DESC";
|
|
175
|
+
const stmt = this.db.prepare(query);
|
|
176
|
+
return stmt.all(...params);
|
|
177
|
+
}
|
|
178
|
+
// Participants
|
|
179
|
+
clearParticipantsForNote(noteId) {
|
|
180
|
+
const stmt = this.db.prepare("DELETE FROM participants WHERE note_id = ?");
|
|
181
|
+
stmt.run(noteId);
|
|
182
|
+
}
|
|
183
|
+
insertParticipant(noteId, email) {
|
|
184
|
+
const stmt = this.db.prepare(`
|
|
185
|
+
INSERT OR IGNORE INTO participants (note_id, email) VALUES (?, ?)
|
|
186
|
+
`);
|
|
187
|
+
stmt.run(noteId, email);
|
|
188
|
+
}
|
|
189
|
+
getMeetingsByParticipants(emails) {
|
|
190
|
+
if (emails.length === 0)
|
|
191
|
+
return [];
|
|
192
|
+
const placeholders = emails.map(() => "?").join(",");
|
|
193
|
+
const stmt = this.db.prepare(`
|
|
194
|
+
SELECT DISTINCT n.* FROM notes n
|
|
195
|
+
JOIN participants p ON n.id = p.note_id
|
|
196
|
+
WHERE p.email IN (${placeholders})
|
|
197
|
+
ORDER BY n.event_start DESC
|
|
198
|
+
`);
|
|
199
|
+
return stmt.all(...emails);
|
|
200
|
+
}
|
|
201
|
+
getMeetingsWithAllParticipants(emails) {
|
|
202
|
+
if (emails.length === 0)
|
|
203
|
+
return [];
|
|
204
|
+
const placeholders = emails.map(() => "?").join(",");
|
|
205
|
+
const stmt = this.db.prepare(`
|
|
206
|
+
SELECT n.* FROM notes n
|
|
207
|
+
WHERE (
|
|
208
|
+
SELECT COUNT(DISTINCT p.email) FROM participants p
|
|
209
|
+
WHERE p.note_id = n.id AND p.email IN (${placeholders})
|
|
210
|
+
) = ?
|
|
211
|
+
ORDER BY n.event_start DESC
|
|
212
|
+
`);
|
|
213
|
+
return stmt.all(...emails, emails.length);
|
|
214
|
+
}
|
|
215
|
+
getParticipantsForNote(noteId) {
|
|
216
|
+
const stmt = this.db.prepare("SELECT email FROM participants WHERE note_id = ?");
|
|
217
|
+
const rows = stmt.all(noteId);
|
|
218
|
+
return rows.map((r) => r.email);
|
|
219
|
+
}
|
|
220
|
+
// Sync status
|
|
221
|
+
getLastSyncTime() {
|
|
222
|
+
const stmt = this.db.prepare("SELECT value FROM sync_status WHERE key = 'last_sync'");
|
|
223
|
+
const row = stmt.get();
|
|
224
|
+
return row?.value ?? null;
|
|
225
|
+
}
|
|
226
|
+
setLastSyncTime(time) {
|
|
227
|
+
const stmt = this.db.prepare(`
|
|
228
|
+
INSERT INTO sync_status (key, value) VALUES ('last_sync', ?)
|
|
229
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
230
|
+
`);
|
|
231
|
+
stmt.run(time);
|
|
232
|
+
}
|
|
233
|
+
// Stats
|
|
234
|
+
getStats() {
|
|
235
|
+
const notes = this.db.prepare("SELECT COUNT(*) as count FROM notes").get().count;
|
|
236
|
+
const recordings = this.db.prepare("SELECT COUNT(*) as count FROM recordings").get().count;
|
|
237
|
+
const action_items = this.db.prepare("SELECT COUNT(*) as count FROM action_items").get().count;
|
|
238
|
+
const participants = this.db.prepare("SELECT COUNT(DISTINCT email) as count FROM participants").get().count;
|
|
239
|
+
return { notes, recordings, action_items, participants };
|
|
240
|
+
}
|
|
241
|
+
close() {
|
|
242
|
+
this.db.close();
|
|
243
|
+
}
|
|
244
|
+
}
|
package/dist/index.d.ts
ADDED