veryfront 0.0.45 → 0.0.47

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 (61) hide show
  1. package/dist/ai/components.js +3 -3
  2. package/dist/ai/components.js.map +2 -2
  3. package/dist/ai/index.d.ts +18 -3
  4. package/dist/ai/index.js +12 -2
  5. package/dist/ai/index.js.map +2 -2
  6. package/dist/cli.js +4 -5
  7. package/dist/components.js +1 -1
  8. package/dist/components.js.map +1 -1
  9. package/dist/config.d.ts +7 -0
  10. package/dist/config.js +1 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/data.js +1 -1
  13. package/dist/data.js.map +1 -1
  14. package/dist/index.js +2 -5
  15. package/dist/index.js.map +2 -2
  16. package/dist/integrations/_base/connector.json +11 -0
  17. package/dist/integrations/_base/files/SETUP.md +132 -0
  18. package/dist/integrations/_base/files/app/api/integrations/status/route.ts +38 -0
  19. package/dist/integrations/_base/files/app/setup/page.tsx +461 -0
  20. package/dist/integrations/_base/files/lib/oauth.ts +145 -0
  21. package/dist/integrations/_base/files/lib/token-store.ts +109 -0
  22. package/dist/integrations/calendar/connector.json +77 -0
  23. package/dist/integrations/calendar/files/ai/tools/create-event.ts +83 -0
  24. package/dist/integrations/calendar/files/ai/tools/find-free-time.ts +108 -0
  25. package/dist/integrations/calendar/files/ai/tools/list-events.ts +98 -0
  26. package/dist/integrations/calendar/files/app/api/auth/calendar/callback/route.ts +114 -0
  27. package/dist/integrations/calendar/files/app/api/auth/calendar/route.ts +29 -0
  28. package/dist/integrations/calendar/files/lib/calendar-client.ts +309 -0
  29. package/dist/integrations/calendar/files/lib/oauth.ts +145 -0
  30. package/dist/integrations/calendar/files/lib/token-store.ts +109 -0
  31. package/dist/integrations/github/connector.json +84 -0
  32. package/dist/integrations/github/files/ai/tools/create-issue.ts +75 -0
  33. package/dist/integrations/github/files/ai/tools/get-pr-diff.ts +82 -0
  34. package/dist/integrations/github/files/ai/tools/list-prs.ts +93 -0
  35. package/dist/integrations/github/files/ai/tools/list-repos.ts +81 -0
  36. package/dist/integrations/github/files/app/api/auth/github/callback/route.ts +132 -0
  37. package/dist/integrations/github/files/app/api/auth/github/route.ts +29 -0
  38. package/dist/integrations/github/files/lib/github-client.ts +282 -0
  39. package/dist/integrations/github/files/lib/oauth.ts +145 -0
  40. package/dist/integrations/github/files/lib/token-store.ts +109 -0
  41. package/dist/integrations/gmail/connector.json +78 -0
  42. package/dist/integrations/gmail/files/ai/tools/list-emails.ts +92 -0
  43. package/dist/integrations/gmail/files/ai/tools/search-emails.ts +92 -0
  44. package/dist/integrations/gmail/files/ai/tools/send-email.ts +77 -0
  45. package/dist/integrations/gmail/files/app/api/auth/gmail/callback/route.ts +114 -0
  46. package/dist/integrations/gmail/files/app/api/auth/gmail/route.ts +29 -0
  47. package/dist/integrations/gmail/files/lib/gmail-client.ts +259 -0
  48. package/dist/integrations/gmail/files/lib/oauth.ts +145 -0
  49. package/dist/integrations/gmail/files/lib/token-store.ts +109 -0
  50. package/dist/integrations/slack/connector.json +74 -0
  51. package/dist/integrations/slack/files/ai/tools/get-messages.ts +65 -0
  52. package/dist/integrations/slack/files/ai/tools/list-channels.ts +63 -0
  53. package/dist/integrations/slack/files/ai/tools/send-message.ts +49 -0
  54. package/dist/integrations/slack/files/app/api/auth/slack/callback/route.ts +132 -0
  55. package/dist/integrations/slack/files/app/api/auth/slack/route.ts +29 -0
  56. package/dist/integrations/slack/files/lib/oauth.ts +145 -0
  57. package/dist/integrations/slack/files/lib/slack-client.ts +215 -0
  58. package/dist/integrations/slack/files/lib/token-store.ts +109 -0
  59. package/dist/templates/ai/app/page.tsx +4 -4
  60. package/dist/templates/ai/tsconfig.json +1 -0
  61. package/package.json +1 -1
@@ -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,215 @@
1
+ /**
2
+ * Slack API Client
3
+ *
4
+ * Provides a type-safe interface to Slack 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 SLACK_API_BASE = "https://slack.com/api";
25
+
26
+ export interface SlackChannel {
27
+ id: string;
28
+ name: string;
29
+ is_channel: boolean;
30
+ is_private: boolean;
31
+ is_member: boolean;
32
+ topic?: { value: string };
33
+ purpose?: { value: string };
34
+ }
35
+
36
+ export interface SlackMessage {
37
+ type: string;
38
+ user?: string;
39
+ text: string;
40
+ ts: string;
41
+ thread_ts?: string;
42
+ reply_count?: number;
43
+ reactions?: Array<{ name: string; count: number }>;
44
+ }
45
+
46
+ export interface SlackUser {
47
+ id: string;
48
+ name: string;
49
+ real_name: string;
50
+ profile: {
51
+ display_name: string;
52
+ email?: string;
53
+ image_48?: string;
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Slack OAuth provider configuration
59
+ */
60
+ export const slackOAuthProvider = {
61
+ name: "slack",
62
+ authorizationUrl: "https://slack.com/oauth/v2/authorize",
63
+ tokenUrl: "https://slack.com/api/oauth.v2.access",
64
+ clientId: getEnv("SLACK_CLIENT_ID") || "",
65
+ clientSecret: getEnv("SLACK_CLIENT_SECRET") || "",
66
+ scopes: [
67
+ "channels:history",
68
+ "channels:read",
69
+ "chat:write",
70
+ "users:read",
71
+ "im:history",
72
+ "im:read",
73
+ ],
74
+ callbackPath: "/api/auth/slack/callback",
75
+ };
76
+
77
+ /**
78
+ * Create a Slack client for a specific user
79
+ */
80
+ export function createSlackClient(userId: string) {
81
+ async function getAccessToken(): Promise<string> {
82
+ const token = await getValidToken(slackOAuthProvider, userId, "slack");
83
+ if (!token) {
84
+ throw new Error("Slack not connected. Please connect your Slack account first.");
85
+ }
86
+ return token;
87
+ }
88
+
89
+ async function apiRequest<T>(
90
+ method: string,
91
+ params: Record<string, unknown> = {},
92
+ ): Promise<T> {
93
+ const accessToken = await getAccessToken();
94
+
95
+ const response = await fetch(`${SLACK_API_BASE}/${method}`, {
96
+ method: "POST",
97
+ headers: {
98
+ Authorization: `Bearer ${accessToken}`,
99
+ "Content-Type": "application/json; charset=utf-8",
100
+ },
101
+ body: JSON.stringify(params),
102
+ });
103
+
104
+ const data = await response.json();
105
+
106
+ if (!data.ok) {
107
+ throw new Error(`Slack API error: ${data.error}`);
108
+ }
109
+
110
+ return data as T;
111
+ }
112
+
113
+ return {
114
+ /**
115
+ * List channels the user is a member of
116
+ */
117
+ async listChannels(options: {
118
+ limit?: number;
119
+ excludeArchived?: boolean;
120
+ } = {}): Promise<SlackChannel[]> {
121
+ const result = await apiRequest<{ channels: SlackChannel[] }>(
122
+ "conversations.list",
123
+ {
124
+ limit: options.limit || 100,
125
+ exclude_archived: options.excludeArchived ?? true,
126
+ types: "public_channel,private_channel",
127
+ },
128
+ );
129
+ return result.channels;
130
+ },
131
+
132
+ /**
133
+ * Get messages from a channel
134
+ */
135
+ async getMessages(
136
+ channelId: string,
137
+ options: { limit?: number; oldest?: string } = {},
138
+ ): Promise<SlackMessage[]> {
139
+ const result = await apiRequest<{ messages: SlackMessage[] }>(
140
+ "conversations.history",
141
+ {
142
+ channel: channelId,
143
+ limit: options.limit || 20,
144
+ oldest: options.oldest,
145
+ },
146
+ );
147
+ return result.messages;
148
+ },
149
+
150
+ /**
151
+ * Send a message to a channel
152
+ */
153
+ async sendMessage(
154
+ channelId: string,
155
+ text: string,
156
+ options: { threadTs?: string; unfurlLinks?: boolean } = {},
157
+ ): Promise<{ ts: string; channel: string }> {
158
+ const result = await apiRequest<{ ts: string; channel: string }>(
159
+ "chat.postMessage",
160
+ {
161
+ channel: channelId,
162
+ text,
163
+ thread_ts: options.threadTs,
164
+ unfurl_links: options.unfurlLinks ?? true,
165
+ },
166
+ );
167
+ return result;
168
+ },
169
+
170
+ /**
171
+ * Get user info
172
+ */
173
+ async getUser(userId: string): Promise<SlackUser> {
174
+ const result = await apiRequest<{ user: SlackUser }>("users.info", {
175
+ user: userId,
176
+ });
177
+ return result.user;
178
+ },
179
+
180
+ /**
181
+ * Get thread replies
182
+ */
183
+ async getThread(
184
+ channelId: string,
185
+ threadTs: string,
186
+ ): Promise<SlackMessage[]> {
187
+ const result = await apiRequest<{ messages: SlackMessage[] }>(
188
+ "conversations.replies",
189
+ {
190
+ channel: channelId,
191
+ ts: threadTs,
192
+ },
193
+ );
194
+ return result.messages;
195
+ },
196
+
197
+ /**
198
+ * Search messages
199
+ */
200
+ async searchMessages(
201
+ query: string,
202
+ options: { count?: number } = {},
203
+ ): Promise<SlackMessage[]> {
204
+ const result = await apiRequest<{
205
+ messages: { matches: SlackMessage[] };
206
+ }>("search.messages", {
207
+ query,
208
+ count: options.count || 20,
209
+ });
210
+ return result.messages.matches;
211
+ },
212
+ };
213
+ }
214
+
215
+ export type SlackClient = ReturnType<typeof createSlackClient>;
@@ -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
+ }
@@ -8,15 +8,15 @@ export default function ChatPage() {
8
8
 
9
9
  return (
10
10
  <div className="flex flex-col h-screen bg-white dark:bg-neutral-900">
11
- {/* Header - minimal */}
12
- <header className="flex-shrink-0 border-b border-neutral-200 dark:border-neutral-800">
11
+ {/* Header - sticky at top */}
12
+ <header className="sticky top-0 z-10 flex-shrink-0 border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
13
13
  <div className="max-w-2xl mx-auto px-4 py-3 flex items-center justify-center">
14
14
  <h1 className="font-medium text-neutral-900 dark:text-white">AI Assistant</h1>
15
15
  </div>
16
16
  </header>
17
17
 
18
- {/* Chat */}
19
- <Chat {...chat} className="flex-1" placeholder="Message" />
18
+ {/* Chat - fills remaining space with scrollable content */}
19
+ <Chat {...chat} className="flex-1 min-h-0" placeholder="Message" />
20
20
  </div>
21
21
  )
22
22
  }
@@ -11,6 +11,7 @@
11
11
  "resolveJsonModule": true,
12
12
  "isolatedModules": true,
13
13
  "noEmit": true,
14
+ "allowImportingTsExtensions": true,
14
15
  "lib": ["DOM", "DOM.Iterable", "ES2020"]
15
16
  },
16
17
  "include": ["**/*.ts", "**/*.tsx"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "Zero-config React meta-framework for building agentic AI applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",