veryfront 0.0.45 → 0.0.46

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.
Files changed (57) hide show
  1. package/dist/ai/index.d.ts +11 -1
  2. package/dist/ai/index.js +2 -2
  3. package/dist/ai/index.js.map +2 -2
  4. package/dist/cli.js +6 -4
  5. package/dist/components.js +1 -1
  6. package/dist/components.js.map +1 -1
  7. package/dist/config.d.ts +7 -0
  8. package/dist/config.js +1 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/data.js +1 -1
  11. package/dist/data.js.map +1 -1
  12. package/dist/index.js +32 -33
  13. package/dist/index.js.map +2 -2
  14. package/dist/integrations/_base/connector.json +11 -0
  15. package/dist/integrations/_base/files/SETUP.md +132 -0
  16. package/dist/integrations/_base/files/app/api/integrations/status/route.ts +38 -0
  17. package/dist/integrations/_base/files/app/setup/page.tsx +461 -0
  18. package/dist/integrations/_base/files/lib/oauth.ts +145 -0
  19. package/dist/integrations/_base/files/lib/token-store.ts +109 -0
  20. package/dist/integrations/calendar/connector.json +77 -0
  21. package/dist/integrations/calendar/files/ai/tools/create-event.ts +83 -0
  22. package/dist/integrations/calendar/files/ai/tools/find-free-time.ts +108 -0
  23. package/dist/integrations/calendar/files/ai/tools/list-events.ts +98 -0
  24. package/dist/integrations/calendar/files/app/api/auth/calendar/callback/route.ts +114 -0
  25. package/dist/integrations/calendar/files/app/api/auth/calendar/route.ts +29 -0
  26. package/dist/integrations/calendar/files/lib/calendar-client.ts +309 -0
  27. package/dist/integrations/calendar/files/lib/oauth.ts +145 -0
  28. package/dist/integrations/calendar/files/lib/token-store.ts +109 -0
  29. package/dist/integrations/github/connector.json +84 -0
  30. package/dist/integrations/github/files/ai/tools/create-issue.ts +75 -0
  31. package/dist/integrations/github/files/ai/tools/get-pr-diff.ts +82 -0
  32. package/dist/integrations/github/files/ai/tools/list-prs.ts +93 -0
  33. package/dist/integrations/github/files/ai/tools/list-repos.ts +81 -0
  34. package/dist/integrations/github/files/app/api/auth/github/callback/route.ts +132 -0
  35. package/dist/integrations/github/files/app/api/auth/github/route.ts +29 -0
  36. package/dist/integrations/github/files/lib/github-client.ts +282 -0
  37. package/dist/integrations/github/files/lib/oauth.ts +145 -0
  38. package/dist/integrations/github/files/lib/token-store.ts +109 -0
  39. package/dist/integrations/gmail/connector.json +78 -0
  40. package/dist/integrations/gmail/files/ai/tools/list-emails.ts +92 -0
  41. package/dist/integrations/gmail/files/ai/tools/search-emails.ts +92 -0
  42. package/dist/integrations/gmail/files/ai/tools/send-email.ts +77 -0
  43. package/dist/integrations/gmail/files/app/api/auth/gmail/callback/route.ts +114 -0
  44. package/dist/integrations/gmail/files/app/api/auth/gmail/route.ts +29 -0
  45. package/dist/integrations/gmail/files/lib/gmail-client.ts +259 -0
  46. package/dist/integrations/gmail/files/lib/oauth.ts +145 -0
  47. package/dist/integrations/gmail/files/lib/token-store.ts +109 -0
  48. package/dist/integrations/slack/connector.json +74 -0
  49. package/dist/integrations/slack/files/ai/tools/get-messages.ts +65 -0
  50. package/dist/integrations/slack/files/ai/tools/list-channels.ts +63 -0
  51. package/dist/integrations/slack/files/ai/tools/send-message.ts +49 -0
  52. package/dist/integrations/slack/files/app/api/auth/slack/callback/route.ts +132 -0
  53. package/dist/integrations/slack/files/app/api/auth/slack/route.ts +29 -0
  54. package/dist/integrations/slack/files/lib/oauth.ts +145 -0
  55. package/dist/integrations/slack/files/lib/slack-client.ts +215 -0
  56. package/dist/integrations/slack/files/lib/token-store.ts +109 -0
  57. package/package.json +1 -1
@@ -0,0 +1,282 @@
1
+ /**
2
+ * GitHub API Client
3
+ *
4
+ * Provides a type-safe interface to GitHub API operations.
5
+ */
6
+
7
+ import { tokenStore as _tokenStore } from "./token-store.ts";
8
+ import { getValidToken } from "./oauth.ts";
9
+
10
+ // Helper for Cross-Platform environment access
11
+ function getEnv(key: string): string | undefined {
12
+ // @ts-ignore - Deno global
13
+ if (typeof Deno !== "undefined") {
14
+ // @ts-ignore - Deno global
15
+ return Deno.env.get(key);
16
+ } // @ts-ignore - process global
17
+ else if (typeof process !== "undefined" && process.env) {
18
+ // @ts-ignore - process global
19
+ return process.env[key];
20
+ }
21
+ return undefined;
22
+ }
23
+
24
+ const GITHUB_API_BASE = "https://api.github.com";
25
+
26
+ export interface GitHubRepo {
27
+ id: number;
28
+ name: string;
29
+ full_name: string;
30
+ description: string | null;
31
+ private: boolean;
32
+ html_url: string;
33
+ default_branch: string;
34
+ language: string | null;
35
+ stargazers_count: number;
36
+ forks_count: number;
37
+ open_issues_count: number;
38
+ updated_at: string;
39
+ }
40
+
41
+ export interface GitHubPullRequest {
42
+ id: number;
43
+ number: number;
44
+ title: string;
45
+ body: string | null;
46
+ state: "open" | "closed";
47
+ html_url: string;
48
+ user: { login: string; avatar_url: string };
49
+ created_at: string;
50
+ updated_at: string;
51
+ head: { ref: string; sha: string };
52
+ base: { ref: string };
53
+ mergeable: boolean | null;
54
+ additions: number;
55
+ deletions: number;
56
+ changed_files: number;
57
+ draft: boolean;
58
+ labels: Array<{ name: string; color: string }>;
59
+ }
60
+
61
+ export interface GitHubIssue {
62
+ id: number;
63
+ number: number;
64
+ title: string;
65
+ body: string | null;
66
+ state: "open" | "closed";
67
+ html_url: string;
68
+ user: { login: string };
69
+ created_at: string;
70
+ updated_at: string;
71
+ labels: Array<{ name: string; color: string }>;
72
+ assignees: Array<{ login: string }>;
73
+ }
74
+
75
+ export interface GitHubCommit {
76
+ sha: string;
77
+ commit: {
78
+ message: string;
79
+ author: { name: string; date: string };
80
+ };
81
+ html_url: string;
82
+ author: { login: string; avatar_url: string } | null;
83
+ }
84
+
85
+ /**
86
+ * GitHub OAuth provider configuration
87
+ */
88
+ export const githubOAuthProvider = {
89
+ name: "github",
90
+ authorizationUrl: "https://github.com/login/oauth/authorize",
91
+ tokenUrl: "https://github.com/login/oauth/access_token",
92
+ clientId: getEnv("GITHUB_CLIENT_ID") || "",
93
+ clientSecret: getEnv("GITHUB_CLIENT_SECRET") || "",
94
+ scopes: ["repo", "read:user", "read:org"],
95
+ callbackPath: "/api/auth/github/callback",
96
+ };
97
+
98
+ /**
99
+ * Create a GitHub client for a specific user
100
+ */
101
+ export function createGitHubClient(userId: string) {
102
+ async function getAccessToken(): Promise<string> {
103
+ const token = await getValidToken(githubOAuthProvider, userId, "github");
104
+ if (!token) {
105
+ throw new Error("GitHub not connected. Please connect your GitHub account first.");
106
+ }
107
+ return token;
108
+ }
109
+
110
+ async function apiRequest<T>(
111
+ endpoint: string,
112
+ options: RequestInit = {},
113
+ ): Promise<T> {
114
+ const accessToken = await getAccessToken();
115
+
116
+ const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
117
+ ...options,
118
+ headers: {
119
+ Authorization: `Bearer ${accessToken}`,
120
+ Accept: "application/vnd.github+json",
121
+ "X-GitHub-Api-Version": "2022-11-28",
122
+ ...options.headers,
123
+ },
124
+ });
125
+
126
+ if (!response.ok) {
127
+ const error = await response.text();
128
+ throw new Error(`GitHub API error: ${response.status} - ${error}`);
129
+ }
130
+
131
+ return response.json();
132
+ }
133
+
134
+ return {
135
+ /**
136
+ * List user's repositories
137
+ */
138
+ listRepos(options: {
139
+ sort?: "created" | "updated" | "pushed" | "full_name";
140
+ perPage?: number;
141
+ type?: "all" | "owner" | "public" | "private" | "member";
142
+ } = {}): Promise<GitHubRepo[]> {
143
+ const params = new URLSearchParams();
144
+ if (options.sort) params.set("sort", options.sort);
145
+ if (options.perPage) params.set("per_page", String(options.perPage));
146
+ if (options.type) params.set("type", options.type);
147
+
148
+ const query = params.toString();
149
+ return apiRequest<GitHubRepo[]>(`/user/repos${query ? `?${query}` : ""}`);
150
+ },
151
+
152
+ /**
153
+ * List pull requests for a repository
154
+ */
155
+ listPullRequests(
156
+ owner: string,
157
+ repo: string,
158
+ options: {
159
+ state?: "open" | "closed" | "all";
160
+ perPage?: number;
161
+ } = {},
162
+ ): Promise<GitHubPullRequest[]> {
163
+ const params = new URLSearchParams();
164
+ params.set("state", options.state || "open");
165
+ if (options.perPage) params.set("per_page", String(options.perPage));
166
+
167
+ return apiRequest<GitHubPullRequest[]>(
168
+ `/repos/${owner}/${repo}/pulls?${params.toString()}`,
169
+ );
170
+ },
171
+
172
+ /**
173
+ * Get a single pull request
174
+ */
175
+ getPullRequest(
176
+ owner: string,
177
+ repo: string,
178
+ pullNumber: number,
179
+ ): Promise<GitHubPullRequest> {
180
+ return apiRequest<GitHubPullRequest>(
181
+ `/repos/${owner}/${repo}/pulls/${pullNumber}`,
182
+ );
183
+ },
184
+
185
+ /**
186
+ * Get pull request diff
187
+ */
188
+ async getPullRequestDiff(
189
+ owner: string,
190
+ repo: string,
191
+ pullNumber: number,
192
+ ): Promise<string> {
193
+ const accessToken = await getAccessToken();
194
+
195
+ const response = await fetch(
196
+ `${GITHUB_API_BASE}/repos/${owner}/${repo}/pulls/${pullNumber}`,
197
+ {
198
+ headers: {
199
+ Authorization: `Bearer ${accessToken}`,
200
+ Accept: "application/vnd.github.diff",
201
+ "X-GitHub-Api-Version": "2022-11-28",
202
+ },
203
+ },
204
+ );
205
+
206
+ if (!response.ok) {
207
+ throw new Error(`GitHub API error: ${response.status}`);
208
+ }
209
+
210
+ return response.text();
211
+ },
212
+
213
+ /**
214
+ * Create an issue
215
+ */
216
+ createIssue(
217
+ owner: string,
218
+ repo: string,
219
+ options: {
220
+ title: string;
221
+ body?: string;
222
+ labels?: string[];
223
+ assignees?: string[];
224
+ },
225
+ ): Promise<GitHubIssue> {
226
+ return apiRequest<GitHubIssue>(`/repos/${owner}/${repo}/issues`, {
227
+ method: "POST",
228
+ body: JSON.stringify(options),
229
+ });
230
+ },
231
+
232
+ /**
233
+ * List issues for a repository
234
+ */
235
+ listIssues(
236
+ owner: string,
237
+ repo: string,
238
+ options: {
239
+ state?: "open" | "closed" | "all";
240
+ perPage?: number;
241
+ } = {},
242
+ ): Promise<GitHubIssue[]> {
243
+ const params = new URLSearchParams();
244
+ params.set("state", options.state || "open");
245
+ if (options.perPage) params.set("per_page", String(options.perPage));
246
+
247
+ return apiRequest<GitHubIssue[]>(
248
+ `/repos/${owner}/${repo}/issues?${params.toString()}`,
249
+ );
250
+ },
251
+
252
+ /**
253
+ * List commits for a repository
254
+ */
255
+ listCommits(
256
+ owner: string,
257
+ repo: string,
258
+ options: {
259
+ sha?: string;
260
+ perPage?: number;
261
+ } = {},
262
+ ): Promise<GitHubCommit[]> {
263
+ const params = new URLSearchParams();
264
+ if (options.sha) params.set("sha", options.sha);
265
+ if (options.perPage) params.set("per_page", String(options.perPage));
266
+
267
+ const query = params.toString();
268
+ return apiRequest<GitHubCommit[]>(
269
+ `/repos/${owner}/${repo}/commits${query ? `?${query}` : ""}`,
270
+ );
271
+ },
272
+
273
+ /**
274
+ * Get authenticated user
275
+ */
276
+ getUser(): Promise<{ login: string; name: string; email: string }> {
277
+ return apiRequest("/user");
278
+ },
279
+ };
280
+ }
281
+
282
+ export type GitHubClient = ReturnType<typeof createGitHubClient>;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * OAuth Helper Functions
3
+ *
4
+ * Provides utilities for OAuth 2.0 authorization flows.
5
+ */
6
+
7
+ import { type OAuthToken, tokenStore } from "./token-store.ts";
8
+
9
+ export interface OAuthProvider {
10
+ name: string;
11
+ authorizationUrl: string;
12
+ tokenUrl: string;
13
+ clientId: string;
14
+ clientSecret: string;
15
+ scopes: string[];
16
+ callbackPath: string;
17
+ }
18
+
19
+ /**
20
+ * Generate OAuth authorization URL
21
+ */
22
+ export function getAuthorizationUrl(
23
+ provider: OAuthProvider,
24
+ state: string,
25
+ redirectUri: string,
26
+ ): string {
27
+ const params = new URLSearchParams({
28
+ client_id: provider.clientId,
29
+ redirect_uri: redirectUri,
30
+ response_type: "code",
31
+ scope: provider.scopes.join(" "),
32
+ state,
33
+ access_type: "offline",
34
+ prompt: "consent",
35
+ });
36
+
37
+ return `${provider.authorizationUrl}?${params.toString()}`;
38
+ }
39
+
40
+ /**
41
+ * Exchange authorization code for tokens
42
+ */
43
+ export async function exchangeCodeForTokens(
44
+ provider: OAuthProvider,
45
+ code: string,
46
+ redirectUri: string,
47
+ ): Promise<OAuthToken> {
48
+ const response = await fetch(provider.tokenUrl, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/x-www-form-urlencoded",
52
+ },
53
+ body: new URLSearchParams({
54
+ client_id: provider.clientId,
55
+ client_secret: provider.clientSecret,
56
+ code,
57
+ grant_type: "authorization_code",
58
+ redirect_uri: redirectUri,
59
+ }),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const error = await response.text();
64
+ throw new Error(`Token exchange failed: ${response.status} - ${error}`);
65
+ }
66
+
67
+ const data = await response.json();
68
+
69
+ return {
70
+ accessToken: data.access_token,
71
+ refreshToken: data.refresh_token,
72
+ expiresAt: data.expires_in ? Date.now() + (data.expires_in * 1000) : undefined,
73
+ tokenType: data.token_type || "Bearer",
74
+ scope: data.scope,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Refresh an expired access token
80
+ */
81
+ export async function refreshAccessToken(
82
+ provider: OAuthProvider,
83
+ refreshToken: string,
84
+ ): Promise<OAuthToken> {
85
+ const response = await fetch(provider.tokenUrl, {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/x-www-form-urlencoded",
89
+ },
90
+ body: new URLSearchParams({
91
+ client_id: provider.clientId,
92
+ client_secret: provider.clientSecret,
93
+ refresh_token: refreshToken,
94
+ grant_type: "refresh_token",
95
+ }),
96
+ });
97
+
98
+ if (!response.ok) {
99
+ const error = await response.text();
100
+ throw new Error(`Token refresh failed: ${response.status} - ${error}`);
101
+ }
102
+
103
+ const data = await response.json();
104
+
105
+ return {
106
+ accessToken: data.access_token,
107
+ refreshToken: data.refresh_token || refreshToken,
108
+ expiresAt: data.expires_in ? Date.now() + (data.expires_in * 1000) : undefined,
109
+ tokenType: data.token_type || "Bearer",
110
+ scope: data.scope,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Get a valid access token, refreshing if necessary
116
+ */
117
+ export async function getValidToken(
118
+ provider: OAuthProvider,
119
+ userId: string,
120
+ service: string,
121
+ ): Promise<string | null> {
122
+ const token = await tokenStore.getToken(userId, service);
123
+
124
+ if (!token) {
125
+ return null;
126
+ }
127
+
128
+ // Check if token is expired (with 5 minute buffer)
129
+ // If no expiresAt, token doesn't expire (e.g., GitHub)
130
+ const isExpired = token.expiresAt ? token.expiresAt < Date.now() + 5 * 60 * 1000 : false;
131
+
132
+ if (isExpired && token.refreshToken) {
133
+ try {
134
+ const newToken = await refreshAccessToken(provider, token.refreshToken);
135
+ await tokenStore.setToken(userId, service, newToken);
136
+ return newToken.accessToken;
137
+ } catch {
138
+ // Refresh failed, user needs to re-authorize
139
+ await tokenStore.revokeToken(userId, service);
140
+ return null;
141
+ }
142
+ }
143
+
144
+ return token.accessToken;
145
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * OAuth Token Store
3
+ *
4
+ * Simple in-memory token store for development.
5
+ * Replace with a database or KV store for production.
6
+ */
7
+
8
+ export interface OAuthToken {
9
+ accessToken: string;
10
+ refreshToken?: string;
11
+ expiresAt?: number;
12
+ tokenType?: string;
13
+ scope?: string;
14
+ }
15
+
16
+ export interface TokenStore {
17
+ getToken(userId: string, service: string): Promise<OAuthToken | null>;
18
+ setToken(userId: string, service: string, token: OAuthToken): Promise<void>;
19
+ revokeToken(userId: string, service: string): Promise<void>;
20
+ isConnected(userId: string, service: string): Promise<boolean>;
21
+ }
22
+
23
+ // In-memory storage for development
24
+ const tokens = new Map<string, OAuthToken>();
25
+
26
+ function getKey(userId: string, service: string): string {
27
+ return `${userId}:${service}`;
28
+ }
29
+
30
+ /**
31
+ * Simple in-memory token store
32
+ *
33
+ * NOTE: This is for development only. In production, use:
34
+ * - Database (Postgres, SQLite, etc.)
35
+ * - KV store (Cloudflare Workers KV, Vercel KV, etc.)
36
+ * - Encrypted file storage
37
+ */
38
+ export const tokenStore: TokenStore = {
39
+ getToken(userId: string, service: string): Promise<OAuthToken | null> {
40
+ const key = getKey(userId, service);
41
+ return Promise.resolve(tokens.get(key) || null);
42
+ },
43
+
44
+ setToken(
45
+ userId: string,
46
+ service: string,
47
+ token: OAuthToken,
48
+ ): Promise<void> {
49
+ const key = getKey(userId, service);
50
+ tokens.set(key, token);
51
+ return Promise.resolve();
52
+ },
53
+
54
+ revokeToken(userId: string, service: string): Promise<void> {
55
+ const key = getKey(userId, service);
56
+ tokens.delete(key);
57
+ return Promise.resolve();
58
+ },
59
+
60
+ async isConnected(userId: string, service: string): Promise<boolean> {
61
+ const token = await this.getToken(userId, service);
62
+ if (!token) return false;
63
+ // Check if token is not expired (if no expiry, token doesn't expire)
64
+ return !token.expiresAt || token.expiresAt > Date.now();
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Factory function to create a custom token store
70
+ */
71
+ export function createTokenStore(options: {
72
+ get: (key: string) => Promise<string | null>;
73
+ set: (key: string, value: string) => Promise<void>;
74
+ delete: (key: string) => Promise<void>;
75
+ }): TokenStore {
76
+ return {
77
+ async getToken(userId: string, service: string): Promise<OAuthToken | null> {
78
+ const key = getKey(userId, service);
79
+ const data = await options.get(key);
80
+ if (!data) return null;
81
+ try {
82
+ return JSON.parse(data) as OAuthToken;
83
+ } catch {
84
+ return null;
85
+ }
86
+ },
87
+
88
+ async setToken(
89
+ userId: string,
90
+ service: string,
91
+ token: OAuthToken,
92
+ ): Promise<void> {
93
+ const key = getKey(userId, service);
94
+ await options.set(key, JSON.stringify(token));
95
+ },
96
+
97
+ async revokeToken(userId: string, service: string): Promise<void> {
98
+ const key = getKey(userId, service);
99
+ await options.delete(key);
100
+ },
101
+
102
+ async isConnected(userId: string, service: string): Promise<boolean> {
103
+ const token = await this.getToken(userId, service);
104
+ if (!token) return false;
105
+ // Check if token is not expired (if no expiry, token doesn't expire)
106
+ return !token.expiresAt || token.expiresAt > Date.now();
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "gmail",
3
+ "displayName": "Gmail",
4
+ "icon": "gmail.svg",
5
+ "description": "Read and send emails via Gmail API",
6
+ "auth": {
7
+ "type": "oauth2",
8
+ "provider": "google",
9
+ "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
10
+ "tokenUrl": "https://oauth2.googleapis.com/token",
11
+ "scopes": [
12
+ "https://www.googleapis.com/auth/gmail.readonly",
13
+ "https://www.googleapis.com/auth/gmail.send",
14
+ "https://www.googleapis.com/auth/gmail.modify"
15
+ ],
16
+ "callbackPath": "/api/auth/gmail/callback"
17
+ },
18
+ "envVars": [
19
+ {
20
+ "name": "GOOGLE_CLIENT_ID",
21
+ "description": "Google OAuth Client ID",
22
+ "required": true,
23
+ "sensitive": false,
24
+ "docsUrl": "https://console.cloud.google.com/apis/credentials"
25
+ },
26
+ {
27
+ "name": "GOOGLE_CLIENT_SECRET",
28
+ "description": "Google OAuth Client Secret",
29
+ "required": true,
30
+ "sensitive": true,
31
+ "docsUrl": "https://console.cloud.google.com/apis/credentials"
32
+ }
33
+ ],
34
+ "tools": [
35
+ {
36
+ "id": "list-emails",
37
+ "name": "List Emails",
38
+ "description": "Get recent emails from inbox",
39
+ "requiresWrite": false
40
+ },
41
+ {
42
+ "id": "send-email",
43
+ "name": "Send Email",
44
+ "description": "Send an email to recipients",
45
+ "requiresWrite": true
46
+ },
47
+ {
48
+ "id": "search-emails",
49
+ "name": "Search Emails",
50
+ "description": "Search emails by query",
51
+ "requiresWrite": false
52
+ }
53
+ ],
54
+ "prompts": [
55
+ {
56
+ "id": "summarize-emails",
57
+ "title": "Summarize today's emails",
58
+ "prompt": "Summarize my unread emails from today. Group them by priority and highlight any that need immediate attention.",
59
+ "category": "productivity",
60
+ "icon": "mail"
61
+ },
62
+ {
63
+ "id": "draft-reply",
64
+ "title": "Draft a quick reply",
65
+ "prompt": "Help me draft a reply to my most recent email. Keep it professional and concise.",
66
+ "category": "productivity",
67
+ "icon": "reply"
68
+ },
69
+ {
70
+ "id": "find-emails",
71
+ "title": "Find important emails",
72
+ "prompt": "Search my emails for important messages from the past week that I might have missed.",
73
+ "category": "productivity",
74
+ "icon": "search"
75
+ }
76
+ ],
77
+ "suggestedWith": ["calendar", "slack"]
78
+ }
@@ -0,0 +1,92 @@
1
+ import { tool } from "veryfront/ai";
2
+ import { z } from "zod";
3
+ import { createGmailClient, parseEmailHeaders } from "../../lib/gmail-client.ts";
4
+
5
+ export default tool({
6
+ id: "list-emails",
7
+ description:
8
+ "List recent emails from Gmail inbox. Returns email subjects, senders, and snippets.",
9
+ inputSchema: z.object({
10
+ maxResults: z
11
+ .number()
12
+ .min(1)
13
+ .max(50)
14
+ .default(10)
15
+ .describe("Maximum number of emails to return"),
16
+ unreadOnly: z
17
+ .boolean()
18
+ .default(false)
19
+ .describe("Only return unread emails"),
20
+ label: z
21
+ .string()
22
+ .optional()
23
+ .describe("Filter by Gmail label (e.g., 'INBOX', 'IMPORTANT', 'STARRED')"),
24
+ }),
25
+ execute: async ({ maxResults, unreadOnly, label }, context) => {
26
+ const userId = context?.userId as string | undefined;
27
+ if (!userId) {
28
+ return {
29
+ error: "User not authenticated. Please log in first.",
30
+ };
31
+ }
32
+
33
+ try {
34
+ const gmail = createGmailClient(userId);
35
+
36
+ let query = "";
37
+ if (unreadOnly) {
38
+ query = "is:unread";
39
+ }
40
+
41
+ const labelIds = label ? [label] : undefined;
42
+
43
+ const list = await gmail.listMessages({
44
+ maxResults,
45
+ query: query || undefined,
46
+ labelIds,
47
+ });
48
+
49
+ if (!list.messages || list.messages.length === 0) {
50
+ return {
51
+ emails: [],
52
+ message: "No emails found matching your criteria.",
53
+ };
54
+ }
55
+
56
+ // Fetch metadata for each email
57
+ const emails = await Promise.all(
58
+ list.messages.map(async (m: { id: string }) => {
59
+ const message = await gmail.getMessage(m.id, "metadata");
60
+ const headers = parseEmailHeaders(message.payload?.headers || []);
61
+
62
+ return {
63
+ id: message.id,
64
+ threadId: message.threadId,
65
+ from: headers.from,
66
+ to: headers.to,
67
+ subject: headers.subject,
68
+ date: headers.date,
69
+ snippet: message.snippet,
70
+ isUnread: message.labelIds?.includes("UNREAD") || false,
71
+ isStarred: message.labelIds?.includes("STARRED") || false,
72
+ isImportant: message.labelIds?.includes("IMPORTANT") || false,
73
+ };
74
+ }),
75
+ );
76
+
77
+ return {
78
+ emails,
79
+ count: emails.length,
80
+ message: `Found ${emails.length} email(s).`,
81
+ };
82
+ } catch (error) {
83
+ if (error instanceof Error && error.message.includes("not connected")) {
84
+ return {
85
+ error: "Gmail not connected. Please connect your Gmail account.",
86
+ connectUrl: "/api/auth/gmail",
87
+ };
88
+ }
89
+ throw error;
90
+ }
91
+ },
92
+ });