aboutsummaryrefslogtreecommitdiff
path: root/apps/browser-extension/utils
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-09-18 20:16:57 -0700
committerDhravya Shah <[email protected]>2025-09-18 20:30:04 -0700
commitdd3122a4831eac3507b7feb8ba2f1816be5eb3cf (patch)
tree4c3eda087974eaaea0b91c85f839eaa8650522e8 /apps/browser-extension/utils
parentnewish get started page (diff)
downloadsupermemory-09-18-format_browser_extension.tar.xz
supermemory-09-18-format_browser_extension.zip
format browser extension09-18-format_browser_extension
Diffstat (limited to 'apps/browser-extension/utils')
-rw-r--r--apps/browser-extension/utils/api.ts68
-rw-r--r--apps/browser-extension/utils/constants.ts18
-rw-r--r--apps/browser-extension/utils/memory-popup.ts48
-rw-r--r--apps/browser-extension/utils/posthog.ts50
-rw-r--r--apps/browser-extension/utils/query-client.ts8
-rw-r--r--apps/browser-extension/utils/query-hooks.ts22
-rw-r--r--apps/browser-extension/utils/route-detection.ts85
-rw-r--r--apps/browser-extension/utils/twitter-auth.ts54
-rw-r--r--apps/browser-extension/utils/twitter-import.ts126
-rw-r--r--apps/browser-extension/utils/twitter-utils.ts298
-rw-r--r--apps/browser-extension/utils/types.ts134
-rw-r--r--apps/browser-extension/utils/ui-components.ts268
12 files changed, 595 insertions, 584 deletions
diff --git a/apps/browser-extension/utils/api.ts b/apps/browser-extension/utils/api.ts
index 2a4c838b..273f9152 100644
--- a/apps/browser-extension/utils/api.ts
+++ b/apps/browser-extension/utils/api.ts
@@ -1,27 +1,27 @@
/**
* API service for supermemory browser extension
*/
-import { API_ENDPOINTS, STORAGE_KEYS } from "./constants"
+import { API_ENDPOINTS, STORAGE_KEYS } from "./constants";
import {
AuthenticationError,
type MemoryPayload,
type Project,
type ProjectsResponse,
SupermemoryAPIError,
-} from "./types"
+} from "./types";
/**
* Get bearer token from storage
*/
async function getBearerToken(): Promise<string> {
- const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN])
- const token = result[STORAGE_KEYS.BEARER_TOKEN]
+ const result = await chrome.storage.local.get([STORAGE_KEYS.BEARER_TOKEN]);
+ const token = result[STORAGE_KEYS.BEARER_TOKEN];
if (!token) {
- throw new AuthenticationError("Bearer token not found")
+ throw new AuthenticationError("Bearer token not found");
}
- return token
+ return token;
}
/**
@@ -31,7 +31,7 @@ async function makeAuthenticatedRequest<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
- const token = await getBearerToken()
+ const token = await getBearerToken();
const response = await fetch(`${API_ENDPOINTS.SUPERMEMORY_API}${endpoint}`, {
...options,
@@ -41,19 +41,19 @@ async function makeAuthenticatedRequest<T>(
"Content-Type": "application/json",
...options.headers,
},
- })
+ });
if (!response.ok) {
if (response.status === 401) {
- throw new AuthenticationError("Invalid or expired token")
+ throw new AuthenticationError("Invalid or expired token");
}
throw new SupermemoryAPIError(
`API request failed: ${response.statusText}`,
response.status,
- )
+ );
}
- return response.json()
+ return response.json();
}
/**
@@ -62,11 +62,11 @@ async function makeAuthenticatedRequest<T>(
export async function fetchProjects(): Promise<Project[]> {
try {
const response =
- await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects")
- return response.projects
+ await makeAuthenticatedRequest<ProjectsResponse>("/v3/projects");
+ return response.projects;
} catch (error) {
- console.error("Failed to fetch projects:", error)
- throw error
+ console.error("Failed to fetch projects:", error);
+ throw error;
}
}
@@ -77,11 +77,11 @@ export async function getDefaultProject(): Promise<Project | null> {
try {
const result = await chrome.storage.local.get([
STORAGE_KEYS.DEFAULT_PROJECT,
- ])
- return result[STORAGE_KEYS.DEFAULT_PROJECT] || null
+ ]);
+ return result[STORAGE_KEYS.DEFAULT_PROJECT] || null;
} catch (error) {
- console.error("Failed to get default project:", error)
- return null
+ console.error("Failed to get default project:", error);
+ return null;
}
}
@@ -92,10 +92,10 @@ export async function setDefaultProject(project: Project): Promise<void> {
try {
await chrome.storage.local.set({
[STORAGE_KEYS.DEFAULT_PROJECT]: project,
- })
+ });
} catch (error) {
- console.error("Failed to set default project:", error)
- throw error
+ console.error("Failed to set default project:", error);
+ throw error;
}
}
@@ -107,11 +107,11 @@ export async function saveMemory(payload: MemoryPayload): Promise<unknown> {
const response = await makeAuthenticatedRequest<unknown>("/v3/documents", {
method: "POST",
body: JSON.stringify(payload),
- })
- return response
+ });
+ return response;
} catch (error) {
- console.error("Failed to save memory:", error)
- throw error
+ console.error("Failed to save memory:", error);
+ throw error;
}
}
@@ -123,11 +123,11 @@ export async function searchMemories(query: string): Promise<unknown> {
const response = await makeAuthenticatedRequest<unknown>("/v4/search", {
method: "POST",
body: JSON.stringify({ q: query, include: { relatedMemories: true } }),
- })
- return response
+ });
+ return response;
} catch (error) {
- console.error("Failed to search memories:", error)
- throw error
+ console.error("Failed to search memories:", error);
+ throw error;
}
}
@@ -150,13 +150,13 @@ export async function saveAllTweets(
},
}),
},
- )
- return response
+ );
+ return response;
} catch (error) {
if (error instanceof SupermemoryAPIError && error.statusCode === 409) {
// Skip if already exists (409 Conflict)
- return
+ return;
}
- throw error
+ throw error;
}
}
diff --git a/apps/browser-extension/utils/constants.ts b/apps/browser-extension/utils/constants.ts
index ac286717..39ea54a7 100644
--- a/apps/browser-extension/utils/constants.ts
+++ b/apps/browser-extension/utils/constants.ts
@@ -8,7 +8,7 @@ export const API_ENDPOINTS = {
SUPERMEMORY_WEB: import.meta.env.PROD
? "https://app.supermemory.ai"
: "http://localhost:3000",
-} as const
+} as const;
/**
* Storage Keys
@@ -22,7 +22,7 @@ export const STORAGE_KEYS = {
TWITTER_AUTH_TOKEN: "twitter-auth-token",
DEFAULT_PROJECT: "sm-default-project",
AUTO_SEARCH_ENABLED: "sm-auto-search-enabled",
-} as const
+} as const;
/**
* DOM Element IDs
@@ -35,7 +35,7 @@ export const ELEMENT_IDS = {
CHATGPT_INPUT_BAR_ELEMENT: "sm-chatgpt-input-bar-element",
CLAUDE_INPUT_BAR_ELEMENT: "sm-claude-input-bar-element",
T3_INPUT_BAR_ELEMENT: "sm-t3-input-bar-element",
-} as const
+} as const;
/**
* UI Configuration
@@ -49,7 +49,7 @@ export const UI_CONFIG = {
OBSERVER_THROTTLE_DELAY: 300, // milliseconds between observer callback executions
ROUTE_CHECK_INTERVAL: 2000, // milliseconds between route change checks
API_REQUEST_TIMEOUT: 10000, // milliseconds for API request timeout
-} as const
+} as const;
/**
* Supported Domains
@@ -60,7 +60,7 @@ export const DOMAINS = {
CLAUDE: ["claude.ai"],
T3: ["t3.chat"],
SUPERMEMORY: ["localhost", "supermemory.ai", "app.supermemory.ai"],
-} as const
+} as const;
/**
* Container Tags
@@ -68,7 +68,7 @@ export const DOMAINS = {
export const CONTAINER_TAGS = {
TWITTER_BOOKMARKS: "sm_project_twitter_bookmarks",
DEFAULT_PROJECT: "sm_project_default",
-} as const
+} as const;
/**
* Message Types for extension communication
@@ -81,11 +81,11 @@ export const MESSAGE_TYPES = {
IMPORT_DONE: "sm-import-done",
GET_RELATED_MEMORIES: "sm-get-related-memories",
CAPTURE_PROMPT: "sm-capture-prompt",
-} as const
+} as const;
export const CONTEXT_MENU_IDS = {
SAVE_TO_SUPERMEMORY: "sm-save-to-supermemory",
-} as const
+} as const;
export const POSTHOG_EVENT_KEY = {
TWITTER_IMPORT_STARTED: "twitter_import_started",
@@ -98,4 +98,4 @@ export const POSTHOG_EVENT_KEY = {
CLAUDE_CHAT_MEMORIES_AUTO_SEARCHED: "claude_chat_memories_auto_searched",
CHATGPT_CHAT_MEMORIES_SEARCHED: "chatgpt_chat_memories_searched",
CHATGPT_CHAT_MEMORIES_AUTO_SEARCHED: "chatgpt_chat_memories_auto_searched",
-} as const
+} as const;
diff --git a/apps/browser-extension/utils/memory-popup.ts b/apps/browser-extension/utils/memory-popup.ts
index ba2d2a1c..b10d814b 100644
--- a/apps/browser-extension/utils/memory-popup.ts
+++ b/apps/browser-extension/utils/memory-popup.ts
@@ -4,13 +4,13 @@
*/
export interface MemoryPopupConfig {
- memoriesData: string
- onClose: () => void
- onRemove?: () => void
+ memoriesData: string;
+ onClose: () => void;
+ onRemove?: () => void;
}
export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
- const popup = document.createElement("div")
+ const popup = document.createElement("div");
popup.style.cssText = `
position: fixed;
bottom: 80px;
@@ -28,9 +28,9 @@ export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
z-index: 999999;
display: none;
overflow: hidden;
- `
+ `;
- const header = document.createElement("div")
+ const header = document.createElement("div");
header.style.cssText = `
display: flex;
justify-content: space-between;
@@ -38,56 +38,56 @@ export function createMemoryPopup(config: MemoryPopupConfig): HTMLElement {
padding: 8px;
border-bottom: 1px solid #333;
opacity: 0.8;
- `
+ `;
header.innerHTML = `
<span style="font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">INCLUDED MEMORIES</span>
<div style="display: flex; gap: 4px;">
${config.onRemove ? '<button id="remove-memories-btn" style="background: none; border: none; color: #ff4444; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;" title="Remove memories">✕</button>' : ""}
<button id="close-popup-btn" style="background: none; border: none; color: white; cursor: pointer; font-size: 14px; padding: 2px; border-radius: 2px;">✕</button>
</div>
- `
+ `;
- const content = document.createElement("div")
+ const content = document.createElement("div");
content.style.cssText = `
padding: 8px;
max-height: 300px;
overflow-y: auto;
line-height: 1.4;
- `
- content.textContent = config.memoriesData
+ `;
+ content.textContent = config.memoriesData;
- const closeBtn = header.querySelector("#close-popup-btn")
- closeBtn?.addEventListener("click", config.onClose)
+ const closeBtn = header.querySelector("#close-popup-btn");
+ closeBtn?.addEventListener("click", config.onClose);
- const removeBtn = header.querySelector("#remove-memories-btn")
+ const removeBtn = header.querySelector("#remove-memories-btn");
if (removeBtn && config.onRemove) {
- removeBtn.addEventListener("click", config.onRemove)
+ removeBtn.addEventListener("click", config.onRemove);
}
- popup.appendChild(header)
- popup.appendChild(content)
+ popup.appendChild(header);
+ popup.appendChild(content);
- return popup
+ return popup;
}
export function showMemoryPopup(popup: HTMLElement): void {
- popup.style.display = "block"
+ popup.style.display = "block";
setTimeout(() => {
if (popup.style.display === "block") {
- hideMemoryPopup(popup)
+ hideMemoryPopup(popup);
}
- }, 10000)
+ }, 10000);
}
export function hideMemoryPopup(popup: HTMLElement): void {
- popup.style.display = "none"
+ popup.style.display = "none";
}
export function toggleMemoryPopup(popup: HTMLElement): void {
if (popup.style.display === "none" || popup.style.display === "") {
- showMemoryPopup(popup)
+ showMemoryPopup(popup);
} else {
- hideMemoryPopup(popup)
+ hideMemoryPopup(popup);
}
}
diff --git a/apps/browser-extension/utils/posthog.ts b/apps/browser-extension/utils/posthog.ts
index cdcdbc4e..8cf0b557 100644
--- a/apps/browser-extension/utils/posthog.ts
+++ b/apps/browser-extension/utils/posthog.ts
@@ -1,21 +1,21 @@
-import { PostHog } from "posthog-js/dist/module.no-external"
-import { STORAGE_KEYS } from "./constants"
+import { PostHog } from "posthog-js/dist/module.no-external";
+import { STORAGE_KEYS } from "./constants";
export async function identifyUser(posthog: PostHog): Promise<void> {
- const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA])
- const userData = stored[STORAGE_KEYS.USER_DATA]
+ const stored = await chrome.storage.local.get([STORAGE_KEYS.USER_DATA]);
+ const userData = stored[STORAGE_KEYS.USER_DATA];
if (userData?.userId) {
posthog.identify(userData.userId, {
email: userData.email,
name: userData.name,
userId: userData.userId,
- })
+ });
}
}
-let posthogInstance: PostHog | null = null
-let initializationPromise: Promise<PostHog> | null = null
+let posthogInstance: PostHog | null = null;
+let initializationPromise: Promise<PostHog> | null = null;
export const POSTHOG_CONFIG = {
api_host: "https://api.supermemory.ai/orange",
@@ -24,40 +24,40 @@ export const POSTHOG_CONFIG = {
persistence: "localStorage",
capture_pageview: false,
autocapture: false,
-} as const
+} as const;
export async function getPostHogInstance(): Promise<PostHog> {
if (posthogInstance) {
- return posthogInstance
+ return posthogInstance;
}
if (initializationPromise) {
- return initializationPromise
+ return initializationPromise;
}
- initializationPromise = initializePostHog()
- return initializationPromise
+ initializationPromise = initializePostHog();
+ return initializationPromise;
}
async function initializePostHog(): Promise<PostHog> {
try {
- const posthog = new PostHog()
+ const posthog = new PostHog();
if (!import.meta.env.WXT_POSTHOG_API_KEY) {
- console.error("PostHog API key not configured")
- throw new Error("PostHog API key not configured")
+ console.error("PostHog API key not configured");
+ throw new Error("PostHog API key not configured");
}
- posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG)
+ posthog.init(import.meta.env.WXT_POSTHOG_API_KEY || "", POSTHOG_CONFIG);
- await identifyUser(posthog)
+ await identifyUser(posthog);
- posthogInstance = posthog
- return posthog
+ posthogInstance = posthog;
+ return posthog;
} catch (error) {
- console.error("Failed to initialize PostHog:", error)
- initializationPromise = null
- throw error
+ console.error("Failed to initialize PostHog:", error);
+ initializationPromise = null;
+ throw error;
}
}
@@ -66,9 +66,9 @@ export async function trackEvent(
properties?: Record<string, unknown>,
): Promise<void> {
try {
- const posthog = await getPostHogInstance()
- posthog.capture(eventName, properties)
+ const posthog = await getPostHogInstance();
+ posthog.capture(eventName, properties);
} catch (error) {
- console.error(`Failed to track event ${eventName}:`, error)
+ console.error(`Failed to track event ${eventName}:`, error);
}
}
diff --git a/apps/browser-extension/utils/query-client.ts b/apps/browser-extension/utils/query-client.ts
index c1839691..dac7b639 100644
--- a/apps/browser-extension/utils/query-client.ts
+++ b/apps/browser-extension/utils/query-client.ts
@@ -1,7 +1,7 @@
/**
* React Query configuration for supermemory browser extension
*/
-import { QueryClient } from "@tanstack/react-query"
+import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
@@ -11,9 +11,9 @@ export const queryClient = new QueryClient({
retry: (failureCount, error) => {
// Don't retry on authentication errors
if (error?.constructor?.name === "AuthenticationError") {
- return false
+ return false;
}
- return failureCount < 3
+ return failureCount < 3;
},
refetchOnWindowFocus: false,
},
@@ -21,4 +21,4 @@ export const queryClient = new QueryClient({
retry: 1,
},
},
-})
+});
diff --git a/apps/browser-extension/utils/query-hooks.ts b/apps/browser-extension/utils/query-hooks.ts
index 721a68ad..f1b683e7 100644
--- a/apps/browser-extension/utils/query-hooks.ts
+++ b/apps/browser-extension/utils/query-hooks.ts
@@ -1,21 +1,21 @@
/**
* React Query hooks for supermemory API
*/
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
fetchProjects,
getDefaultProject,
saveMemory,
searchMemories,
setDefaultProject,
-} from "./api"
-import type { MemoryPayload } from "./types"
+} from "./api";
+import type { MemoryPayload } from "./types";
// Query Keys
export const queryKeys = {
projects: ["projects"] as const,
defaultProject: ["defaultProject"] as const,
-}
+};
// Projects Query
export function useProjects(options?: { enabled?: boolean }) {
@@ -24,7 +24,7 @@ export function useProjects(options?: { enabled?: boolean }) {
queryFn: fetchProjects,
staleTime: 5 * 60 * 1000, // 5 minutes
enabled: options?.enabled ?? true,
- })
+ });
}
// Default Project Query
@@ -34,31 +34,31 @@ export function useDefaultProject(options?: { enabled?: boolean }) {
queryFn: getDefaultProject,
staleTime: 2 * 60 * 1000, // 2 minutes
enabled: options?.enabled ?? true,
- })
+ });
}
// Set Default Project Mutation
export function useSetDefaultProject() {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: setDefaultProject,
onSuccess: (_, project) => {
- queryClient.setQueryData(queryKeys.defaultProject, project)
+ queryClient.setQueryData(queryKeys.defaultProject, project);
},
- })
+ });
}
// Save Memory Mutation
export function useSaveMemory() {
return useMutation({
mutationFn: (payload: MemoryPayload) => saveMemory(payload),
- })
+ });
}
// Search Memories Mutation
export function useSearchMemories() {
return useMutation({
mutationFn: (query: string) => searchMemories(query),
- })
+ });
}
diff --git a/apps/browser-extension/utils/route-detection.ts b/apps/browser-extension/utils/route-detection.ts
index a8a4714f..26787800 100644
--- a/apps/browser-extension/utils/route-detection.ts
+++ b/apps/browser-extension/utils/route-detection.ts
@@ -3,20 +3,20 @@
* Shared logic for detecting route changes across different AI chat platforms
*/
-import { UI_CONFIG } from "./constants"
+import { UI_CONFIG } from "./constants";
export interface RouteDetectionConfig {
- platform: string
- selectors: string[]
- reinitCallback: () => void
- checkInterval?: number
- observerThrottleDelay?: number
+ platform: string;
+ selectors: string[];
+ reinitCallback: () => void;
+ checkInterval?: number;
+ observerThrottleDelay?: number;
}
export interface RouteDetectionCleanup {
- observer: MutationObserver | null
- urlCheckInterval: NodeJS.Timeout | null
- observerThrottle: NodeJS.Timeout | null
+ observer: MutationObserver | null;
+ urlCheckInterval: NodeJS.Timeout | null;
+ observerThrottle: NodeJS.Timeout | null;
}
export function createRouteDetection(
@@ -24,94 +24,97 @@ export function createRouteDetection(
cleanup: RouteDetectionCleanup,
): void {
if (cleanup.observer) {
- cleanup.observer.disconnect()
+ cleanup.observer.disconnect();
}
if (cleanup.urlCheckInterval) {
- clearInterval(cleanup.urlCheckInterval)
+ clearInterval(cleanup.urlCheckInterval);
}
if (cleanup.observerThrottle) {
- clearTimeout(cleanup.observerThrottle)
- cleanup.observerThrottle = null
+ clearTimeout(cleanup.observerThrottle);
+ cleanup.observerThrottle = null;
}
- let currentUrl = window.location.href
+ let currentUrl = window.location.href;
const checkForRouteChange = () => {
if (window.location.href !== currentUrl) {
- currentUrl = window.location.href
- console.log(`${config.platform} route changed, re-initializing`)
- setTimeout(config.reinitCallback, 1000)
+ currentUrl = window.location.href;
+ console.log(`${config.platform} route changed, re-initializing`);
+ setTimeout(config.reinitCallback, 1000);
}
- }
+ };
cleanup.urlCheckInterval = setInterval(
checkForRouteChange,
config.checkInterval || UI_CONFIG.ROUTE_CHECK_INTERVAL,
- )
+ );
cleanup.observer = new MutationObserver((mutations) => {
if (cleanup.observerThrottle) {
- return
+ return;
}
- let shouldRecheck = false
+ let shouldRecheck = false;
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
- const element = node as Element
+ const element = node as Element;
for (const selector of config.selectors) {
if (
element.querySelector?.(selector) ||
element.matches?.(selector)
) {
- shouldRecheck = true
- break
+ shouldRecheck = true;
+ break;
}
}
}
- })
+ });
}
- })
+ });
if (shouldRecheck) {
cleanup.observerThrottle = setTimeout(() => {
try {
- cleanup.observerThrottle = null
- config.reinitCallback()
+ cleanup.observerThrottle = null;
+ config.reinitCallback();
} catch (error) {
- console.error(`Error in ${config.platform} observer callback:`, error)
+ console.error(
+ `Error in ${config.platform} observer callback:`,
+ error,
+ );
}
- }, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY)
+ }, config.observerThrottleDelay || UI_CONFIG.OBSERVER_THROTTLE_DELAY);
}
- })
+ });
try {
cleanup.observer.observe(document.body, {
childList: true,
subtree: true,
- })
+ });
} catch (error) {
- console.error(`Failed to set up ${config.platform} route observer:`, error)
+ console.error(`Failed to set up ${config.platform} route observer:`, error);
if (cleanup.urlCheckInterval) {
- clearInterval(cleanup.urlCheckInterval)
+ clearInterval(cleanup.urlCheckInterval);
}
- cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000)
+ cleanup.urlCheckInterval = setInterval(checkForRouteChange, 1000);
}
}
export function cleanupRouteDetection(cleanup: RouteDetectionCleanup): void {
if (cleanup.observer) {
- cleanup.observer.disconnect()
- cleanup.observer = null
+ cleanup.observer.disconnect();
+ cleanup.observer = null;
}
if (cleanup.urlCheckInterval) {
- clearInterval(cleanup.urlCheckInterval)
- cleanup.urlCheckInterval = null
+ clearInterval(cleanup.urlCheckInterval);
+ cleanup.urlCheckInterval = null;
}
if (cleanup.observerThrottle) {
- clearTimeout(cleanup.observerThrottle)
- cleanup.observerThrottle = null
+ clearTimeout(cleanup.observerThrottle);
+ cleanup.observerThrottle = null;
}
}
diff --git a/apps/browser-extension/utils/twitter-auth.ts b/apps/browser-extension/utils/twitter-auth.ts
index 3dfc50f6..17cbe464 100644
--- a/apps/browser-extension/utils/twitter-auth.ts
+++ b/apps/browser-extension/utils/twitter-auth.ts
@@ -2,12 +2,12 @@
* Twitter Authentication Module
* Handles token capture and storage for Twitter API access
*/
-import { STORAGE_KEYS } from "./constants"
+import { STORAGE_KEYS } from "./constants";
export interface TwitterAuthTokens {
- cookie: string
- csrf: string
- auth: string
+ cookie: string;
+ csrf: string;
+ auth: string;
}
/**
@@ -17,41 +17,41 @@ export interface TwitterAuthTokens {
*/
export function captureTwitterTokens(
details: chrome.webRequest.WebRequestDetails & {
- requestHeaders?: chrome.webRequest.HttpHeader[]
+ requestHeaders?: chrome.webRequest.HttpHeader[];
},
): boolean {
if (!(details.url.includes("x.com") || details.url.includes("twitter.com"))) {
- return false
+ return false;
}
const authHeader = details.requestHeaders?.find(
(header) => header.name.toLowerCase() === "authorization",
- )
+ );
const cookieHeader = details.requestHeaders?.find(
(header) => header.name.toLowerCase() === "cookie",
- )
+ );
const csrfHeader = details.requestHeaders?.find(
(header) => header.name.toLowerCase() === "x-csrf-token",
- )
+ );
if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) {
chrome.storage.session.get([STORAGE_KEYS.TOKENS_LOGGED], (result) => {
if (!result[STORAGE_KEYS.TOKENS_LOGGED]) {
- console.log("Twitter auth tokens captured successfully")
- chrome.storage.session.set({ [STORAGE_KEYS.TOKENS_LOGGED]: true })
+ console.log("Twitter auth tokens captured successfully");
+ chrome.storage.session.set({ [STORAGE_KEYS.TOKENS_LOGGED]: true });
}
- })
+ });
chrome.storage.session.set({
[STORAGE_KEYS.TWITTER_COOKIE]: cookieHeader.value,
[STORAGE_KEYS.TWITTER_CSRF]: csrfHeader.value,
[STORAGE_KEYS.TWITTER_AUTH_TOKEN]: authHeader.value,
- })
+ });
- return true
+ return true;
}
- return false
+ return false;
}
/**
@@ -63,21 +63,21 @@ export async function getTwitterTokens(): Promise<TwitterAuthTokens | null> {
STORAGE_KEYS.TWITTER_COOKIE,
STORAGE_KEYS.TWITTER_CSRF,
STORAGE_KEYS.TWITTER_AUTH_TOKEN,
- ])
+ ]);
if (
!result[STORAGE_KEYS.TWITTER_COOKIE] ||
!result[STORAGE_KEYS.TWITTER_CSRF] ||
!result[STORAGE_KEYS.TWITTER_AUTH_TOKEN]
) {
- return null
+ return null;
}
return {
cookie: result[STORAGE_KEYS.TWITTER_COOKIE],
csrf: result[STORAGE_KEYS.TWITTER_CSRF],
auth: result[STORAGE_KEYS.TWITTER_AUTH_TOKEN],
- }
+ };
}
/**
@@ -86,16 +86,16 @@ export async function getTwitterTokens(): Promise<TwitterAuthTokens | null> {
* @returns Headers object ready for fetch requests
*/
export function createTwitterAPIHeaders(tokens: TwitterAuthTokens): Headers {
- const headers = new Headers()
- headers.append("Cookie", tokens.cookie)
- headers.append("X-Csrf-Token", tokens.csrf)
- headers.append("Authorization", tokens.auth)
- headers.append("Content-Type", "application/json")
+ const headers = new Headers();
+ headers.append("Cookie", tokens.cookie);
+ headers.append("X-Csrf-Token", tokens.csrf);
+ headers.append("Authorization", tokens.auth);
+ headers.append("Content-Type", "application/json");
headers.append(
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
- )
- headers.append("Accept", "*/*")
- headers.append("Accept-Language", "en-US,en;q=0.9")
- return headers
+ );
+ headers.append("Accept", "*/*");
+ headers.append("Accept-Language", "en-US,en;q=0.9");
+ return headers;
}
diff --git a/apps/browser-extension/utils/twitter-import.ts b/apps/browser-extension/utils/twitter-import.ts
index e68d6dbf..bfd3a046 100644
--- a/apps/browser-extension/utils/twitter-import.ts
+++ b/apps/browser-extension/utils/twitter-import.ts
@@ -3,45 +3,45 @@
* Handles the import process for Twitter bookmarks
*/
-import { saveAllTweets } from "./api"
-import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth"
+import { saveAllTweets } from "./api";
+import { createTwitterAPIHeaders, getTwitterTokens } from "./twitter-auth";
import {
BOOKMARKS_URL,
buildRequestVariables,
extractNextCursor,
getAllTweets,
type TwitterAPIResponse,
-} from "./twitter-utils"
+} from "./twitter-utils";
-export type ImportProgressCallback = (message: string) => Promise<void>
+export type ImportProgressCallback = (message: string) => Promise<void>;
-export type ImportCompleteCallback = (totalImported: number) => Promise<void>
+export type ImportCompleteCallback = (totalImported: number) => Promise<void>;
export interface TwitterImportConfig {
- onProgress: ImportProgressCallback
- onComplete: ImportCompleteCallback
- onError: (error: Error) => Promise<void>
+ onProgress: ImportProgressCallback;
+ onComplete: ImportCompleteCallback;
+ onError: (error: Error) => Promise<void>;
}
/**
* Rate limiting configuration
*/
class RateLimiter {
- private waitTime = 60000 // Start with 1 minute
+ private waitTime = 60000; // Start with 1 minute
async handleRateLimit(onProgress: ImportProgressCallback): Promise<void> {
- const waitTimeInSeconds = this.waitTime / 1000
+ const waitTimeInSeconds = this.waitTime / 1000;
await onProgress(
`Rate limit reached. Waiting for ${waitTimeInSeconds} seconds before retrying...`,
- )
+ );
- await new Promise((resolve) => setTimeout(resolve, this.waitTime))
- this.waitTime *= 2 // Exponential backoff
+ await new Promise((resolve) => setTimeout(resolve, this.waitTime));
+ this.waitTime *= 2; // Exponential backoff
}
reset(): void {
- this.waitTime = 60000
+ this.waitTime = 60000;
}
}
@@ -49,8 +49,8 @@ class RateLimiter {
* Main class for handling Twitter bookmarks import
*/
export class TwitterImporter {
- private importInProgress = false
- private rateLimiter = new RateLimiter()
+ private importInProgress = false;
+ private rateLimiter = new RateLimiter();
constructor(private config: TwitterImportConfig) {}
@@ -60,19 +60,19 @@ export class TwitterImporter {
*/
async startImport(): Promise<void> {
if (this.importInProgress) {
- throw new Error("Import already in progress")
+ throw new Error("Import already in progress");
}
- this.importInProgress = true
- const uniqueGroupId = crypto.randomUUID()
+ this.importInProgress = true;
+ const uniqueGroupId = crypto.randomUUID();
try {
- await this.batchImportAll("", 0, uniqueGroupId)
- this.rateLimiter.reset()
+ await this.batchImportAll("", 0, uniqueGroupId);
+ this.rateLimiter.reset();
} catch (error) {
- await this.config.onError(error as Error)
+ await this.config.onError(error as Error);
} finally {
- this.importInProgress = false
+ this.importInProgress = false;
}
}
@@ -81,52 +81,56 @@ export class TwitterImporter {
* @param cursor - Pagination cursor for Twitter API
* @param totalImported - Number of tweets imported so far
*/
- private async batchImportAll(cursor = "", totalImported = 0, uniqueGroupId = "twitter_bookmarks"): Promise<void> {
+ private async batchImportAll(
+ cursor = "",
+ totalImported = 0,
+ uniqueGroupId = "twitter_bookmarks",
+ ): Promise<void> {
try {
// Use a local variable to track imported count
- let importedCount = totalImported
+ let importedCount = totalImported;
// Get authentication tokens
- const tokens = await getTwitterTokens()
+ const tokens = await getTwitterTokens();
if (!tokens) {
await this.config.onProgress(
"Please visit Twitter/X first to capture authentication tokens",
- )
- return
+ );
+ return;
}
// Create headers for API request
- const headers = createTwitterAPIHeaders(tokens)
+ const headers = createTwitterAPIHeaders(tokens);
// Build API request with pagination
- const variables = buildRequestVariables(cursor)
+ const variables = buildRequestVariables(cursor);
const urlWithCursor = cursor
? `${BOOKMARKS_URL}&variables=${encodeURIComponent(JSON.stringify(variables))}`
- : BOOKMARKS_URL
+ : BOOKMARKS_URL;
const response = await fetch(urlWithCursor, {
method: "GET",
headers,
redirect: "follow",
- })
+ });
if (!response.ok) {
- const errorText = await response.text()
- console.error(`Twitter API Error ${response.status}:`, errorText)
+ const errorText = await response.text();
+ console.error(`Twitter API Error ${response.status}:`, errorText);
if (response.status === 429) {
- await this.rateLimiter.handleRateLimit(this.config.onProgress)
- return this.batchImportAll(cursor, totalImported, uniqueGroupId)
+ await this.rateLimiter.handleRateLimit(this.config.onProgress);
+ return this.batchImportAll(cursor, totalImported, uniqueGroupId);
}
throw new Error(
`Failed to fetch data: ${response.status} - ${errorText}`,
- )
+ );
}
- const data: TwitterAPIResponse = await response.json()
- const tweets = getAllTweets(data)
+ const data: TwitterAPIResponse = await response.json();
+ const tweets = getAllTweets(data);
- const documents: MemoryPayload[] = []
+ const documents: MemoryPayload[] = [];
// Convert tweets to MemoryPayload
for (const tweet of tweets) {
@@ -139,49 +143,51 @@ export class TwitterImporter {
likes: tweet.favorite_count,
retweets: tweet.retweet_count || 0,
sm_internal_group_id: uniqueGroupId,
- }
+ };
documents.push({
containerTags: ["sm_project_twitter_bookmarks"],
content: `https://x.com/${tweet.user.screen_name}/status/${tweet.id_str}`,
metadata,
customId: tweet.id_str,
- })
- importedCount++
- await this.config.onProgress(`Imported ${importedCount} tweets, so far...`)
+ });
+ importedCount++;
+ await this.config.onProgress(
+ `Imported ${importedCount} tweets, so far...`,
+ );
} catch (error) {
- console.error("Error importing tweet:", error)
+ console.error("Error importing tweet:", error);
}
}
try {
if (documents.length > 0) {
- await saveAllTweets(documents)
+ await saveAllTweets(documents);
}
- console.log("Tweets saved")
- console.log("Documents:", documents)
+ console.log("Tweets saved");
+ console.log("Documents:", documents);
} catch (error) {
- console.error("Error saving tweets batch:", error)
- await this.config.onError(error as Error)
- return
+ console.error("Error saving tweets batch:", error);
+ await this.config.onError(error as Error);
+ return;
}
// Handle pagination
const instructions =
- data.data?.bookmark_timeline_v2?.timeline?.instructions
- const nextCursor = extractNextCursor(instructions || [])
+ data.data?.bookmark_timeline_v2?.timeline?.instructions;
+ const nextCursor = extractNextCursor(instructions || []);
- console.log("Next cursor:", nextCursor)
- console.log("Tweets length:", tweets.length)
+ console.log("Next cursor:", nextCursor);
+ console.log("Tweets length:", tweets.length);
if (nextCursor && tweets.length > 0) {
- await new Promise((resolve) => setTimeout(resolve, 1000)) // Rate limiting
- await this.batchImportAll(nextCursor, importedCount, uniqueGroupId)
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Rate limiting
+ await this.batchImportAll(nextCursor, importedCount, uniqueGroupId);
} else {
- await this.config.onComplete(importedCount)
+ await this.config.onComplete(importedCount);
}
} catch (error) {
- console.error("Batch import error:", error)
- await this.config.onError(error as Error)
+ console.error("Batch import error:", error);
+ await this.config.onError(error as Error);
}
}
}
diff --git a/apps/browser-extension/utils/twitter-utils.ts b/apps/browser-extension/utils/twitter-utils.ts
index 7a7b86db..89d774bd 100644
--- a/apps/browser-extension/utils/twitter-utils.ts
+++ b/apps/browser-extension/utils/twitter-utils.ts
@@ -1,120 +1,120 @@
// Twitter API data structures and transformation utilities
interface TwitterAPITweet {
- __typename?: string
+ __typename?: string;
legacy: {
- lang?: string
- favorite_count: number
- created_at: string
- display_text_range?: [number, number]
+ lang?: string;
+ favorite_count: number;
+ created_at: string;
+ display_text_range?: [number, number];
entities?: {
- hashtags?: Array<{ indices: [number, number]; text: string }>
+ hashtags?: Array<{ indices: [number, number]; text: string }>;
urls?: Array<{
- display_url: string
- expanded_url: string
- indices: [number, number]
- url: string
- }>
+ display_url: string;
+ expanded_url: string;
+ indices: [number, number];
+ url: string;
+ }>;
user_mentions?: Array<{
- id_str: string
- indices: [number, number]
- name: string
- screen_name: string
- }>
- symbols?: Array<{ indices: [number, number]; text: string }>
- media?: MediaEntity[]
- }
- id_str: string
- full_text: string
- reply_count?: number
- retweet_count?: number
- quote_count?: number
- }
+ id_str: string;
+ indices: [number, number];
+ name: string;
+ screen_name: string;
+ }>;
+ symbols?: Array<{ indices: [number, number]; text: string }>;
+ media?: MediaEntity[];
+ };
+ id_str: string;
+ full_text: string;
+ reply_count?: number;
+ retweet_count?: number;
+ quote_count?: number;
+ };
core?: {
user_results?: {
result?: {
legacy?: {
- id_str: string
- name: string
- profile_image_url_https: string
- screen_name: string
- verified: boolean
- }
- is_blue_verified?: boolean
- }
- }
- }
+ id_str: string;
+ name: string;
+ profile_image_url_https: string;
+ screen_name: string;
+ verified: boolean;
+ };
+ is_blue_verified?: boolean;
+ };
+ };
+ };
}
interface MediaEntity {
- type: string
- media_url_https: string
+ type: string;
+ media_url_https: string;
sizes?: {
large?: {
- w: number
- h: number
- }
- }
+ w: number;
+ h: number;
+ };
+ };
video_info?: {
variants?: Array<{
- url: string
- }>
- duration_millis?: number
- }
+ url: string;
+ }>;
+ duration_millis?: number;
+ };
}
export interface Tweet {
- __typename?: string
- lang?: string
- favorite_count: number
- created_at: string
- display_text_range?: [number, number]
+ __typename?: string;
+ lang?: string;
+ favorite_count: number;
+ created_at: string;
+ display_text_range?: [number, number];
entities: {
hashtags: Array<{
- indices: [number, number]
- text: string
- }>
+ indices: [number, number];
+ text: string;
+ }>;
urls?: Array<{
- display_url: string
- expanded_url: string
- indices: [number, number]
- url: string
- }>
+ display_url: string;
+ expanded_url: string;
+ indices: [number, number];
+ url: string;
+ }>;
user_mentions: Array<{
- id_str: string
- indices: [number, number]
- name: string
- screen_name: string
- }>
+ id_str: string;
+ indices: [number, number];
+ name: string;
+ screen_name: string;
+ }>;
symbols: Array<{
- indices: [number, number]
- text: string
- }>
- }
- id_str: string
- text: string
+ indices: [number, number];
+ text: string;
+ }>;
+ };
+ id_str: string;
+ text: string;
user: {
- id_str: string
- name: string
- profile_image_url_https: string
- screen_name: string
- verified: boolean
- is_blue_verified?: boolean
- }
- conversation_count: number
+ id_str: string;
+ name: string;
+ profile_image_url_https: string;
+ screen_name: string;
+ verified: boolean;
+ is_blue_verified?: boolean;
+ };
+ conversation_count: number;
photos?: Array<{
- url: string
- width: number
- height: number
- }>
+ url: string;
+ width: number;
+ height: number;
+ }>;
videos?: Array<{
- url: string
- thumbnail_url: string
- duration: number
- }>
- retweet_count?: number
- quote_count?: number
- reply_count?: number
+ url: string;
+ thumbnail_url: string;
+ duration: number;
+ }>;
+ retweet_count?: number;
+ quote_count?: number;
+ reply_count?: number;
}
export interface TwitterAPIResponse {
@@ -122,16 +122,16 @@ export interface TwitterAPIResponse {
bookmark_timeline_v2: {
timeline: {
instructions: Array<{
- type: string
+ type: string;
entries?: Array<{
- entryId: string
- sortIndex: string
- content: Record<string, unknown>
- }>
- }>
- }
- }
- }
+ entryId: string;
+ sortIndex: string;
+ content: Record<string, unknown>;
+ }>;
+ }>;
+ };
+ };
+ };
}
// Twitter API features configuration
@@ -165,9 +165,9 @@ export const TWITTER_API_FEATURES = {
articles_preview_enabled: true,
rweb_video_timestamps_enabled: true,
verified_phone_label_enabled: true,
-}
+};
-export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`
+export const BOOKMARKS_URL = `https://x.com/i/api/graphql/xLjCVTqYWz8CGSprLU349w/Bookmarks?features=${encodeURIComponent(JSON.stringify(TWITTER_API_FEATURES))}`;
/**
* Transform raw Twitter API response data into standardized Tweet format
@@ -177,29 +177,29 @@ export function transformTweetData(
): Tweet | null {
try {
const content = input.content as {
- itemContent?: { tweet_results?: { result?: unknown } }
- }
- const tweetData = content?.itemContent?.tweet_results?.result
+ itemContent?: { tweet_results?: { result?: unknown } };
+ };
+ const tweetData = content?.itemContent?.tweet_results?.result;
if (!tweetData) {
- return null
+ return null;
}
- const tweet = tweetData as TwitterAPITweet
+ const tweet = tweetData as TwitterAPITweet;
if (!tweet.legacy) {
- return null
+ return null;
}
// Handle media entities
- const media = (tweet.legacy.entities?.media as MediaEntity[]) || []
+ const media = (tweet.legacy.entities?.media as MediaEntity[]) || [];
const photos = media
.filter((m) => m.type === "photo")
.map((m) => ({
url: m.media_url_https,
width: m.sizes?.large?.w || 0,
height: m.sizes?.large?.h || 0,
- }))
+ }));
const videos = media
.filter((m) => m.type === "video")
@@ -207,7 +207,7 @@ export function transformTweetData(
url: m.video_info?.variants?.[0]?.url || "",
thumbnail_url: m.media_url_https,
duration: m.video_info?.duration_millis || 0,
- }))
+ }));
const transformed: Tweet = {
__typename: tweet.__typename,
@@ -239,20 +239,20 @@ export function transformTweetData(
retweet_count: tweet.legacy.retweet_count || 0,
quote_count: tweet.legacy.quote_count || 0,
reply_count: tweet.legacy.reply_count || 0,
- }
+ };
if (photos.length > 0) {
- transformed.photos = photos
+ transformed.photos = photos;
}
if (videos.length > 0) {
- transformed.videos = videos
+ transformed.videos = videos;
}
- return transformed
+ return transformed;
} catch (error) {
- console.error("Error transforming tweet data:", error)
- return null
+ console.error("Error transforming tweet data:", error);
+ return null;
}
}
@@ -260,29 +260,29 @@ export function transformTweetData(
* Extract all tweets from Twitter API response
*/
export function getAllTweets(data: TwitterAPIResponse): Tweet[] {
- const tweets: Tweet[] = []
+ const tweets: Tweet[] = [];
try {
const instructions =
- data.data?.bookmark_timeline_v2?.timeline?.instructions || []
+ data.data?.bookmark_timeline_v2?.timeline?.instructions || [];
for (const instruction of instructions) {
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
for (const entry of instruction.entries) {
if (entry.entryId.startsWith("tweet-")) {
- const tweet = transformTweetData(entry)
+ const tweet = transformTweetData(entry);
if (tweet) {
- tweets.push(tweet)
+ tweets.push(tweet);
}
}
}
}
}
} catch (error) {
- console.error("Error extracting tweets:", error)
+ console.error("Error extracting tweets:", error);
}
- return tweets
+ return tweets;
}
/**
@@ -295,69 +295,69 @@ export function extractNextCursor(
for (const instruction of instructions) {
if (instruction.type === "TimelineAddEntries" && instruction.entries) {
const entries = instruction.entries as Array<{
- entryId: string
- content?: { value?: string }
- }>
+ entryId: string;
+ content?: { value?: string };
+ }>;
for (const entry of entries) {
if (entry.entryId.startsWith("cursor-bottom-")) {
- return entry.content?.value || null
+ return entry.content?.value || null;
}
}
}
}
} catch (error) {
- console.error("Error extracting cursor:", error)
+ console.error("Error extracting cursor:", error);
}
- return null
+ return null;
}
/**
* Convert Tweet object to markdown format for storage
*/
export function tweetToMarkdown(tweet: Tweet): string {
- const username = tweet.user?.screen_name || "unknown"
- const displayName = tweet.user?.name || "Unknown User"
- const date = new Date(tweet.created_at).toLocaleDateString()
- const time = new Date(tweet.created_at).toLocaleTimeString()
+ const username = tweet.user?.screen_name || "unknown";
+ const displayName = tweet.user?.name || "Unknown User";
+ const date = new Date(tweet.created_at).toLocaleDateString();
+ const time = new Date(tweet.created_at).toLocaleTimeString();
- let markdown = `# Tweet by @${username} (${displayName})\n\n`
- markdown += `**Date:** ${date} ${time}\n`
- markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`
+ let markdown = `# Tweet by @${username} (${displayName})\n\n`;
+ markdown += `**Date:** ${date} ${time}\n`;
+ markdown += `**Likes:** ${tweet.favorite_count} | **Retweets:** ${tweet.retweet_count || 0} | **Replies:** ${tweet.reply_count || 0}\n\n`;
// Add tweet text
- markdown += `${tweet.text}\n\n`
+ markdown += `${tweet.text}\n\n`;
// Add media if present
if (tweet.photos && tweet.photos.length > 0) {
- markdown += "**Images:**\n"
+ markdown += "**Images:**\n";
tweet.photos.forEach((photo, index) => {
- markdown += `![Image ${index + 1}](${photo.url})\n`
- })
- markdown += "\n"
+ markdown += `![Image ${index + 1}](${photo.url})\n`;
+ });
+ markdown += "\n";
}
if (tweet.videos && tweet.videos.length > 0) {
- markdown += "**Videos:**\n"
+ markdown += "**Videos:**\n";
tweet.videos.forEach((video, index) => {
- markdown += `[Video ${index + 1}](${video.url})\n`
- })
- markdown += "\n"
+ markdown += `[Video ${index + 1}](${video.url})\n`;
+ });
+ markdown += "\n";
}
// Add hashtags and mentions
if (tweet.entities.hashtags.length > 0) {
- markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`
+ markdown += `**Hashtags:** ${tweet.entities.hashtags.map((h) => `#${h.text}`).join(", ")}\n`;
}
if (tweet.entities.user_mentions.length > 0) {
- markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`
+ markdown += `**Mentions:** ${tweet.entities.user_mentions.map((m) => `@${m.screen_name}`).join(", ")}\n`;
}
// Add raw data for reference
- markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`
+ markdown += `\n---\n<details>\n<summary>Raw Tweet Data</summary>\n\n\`\`\`json\n${JSON.stringify(tweet, null, 2)}\n\`\`\`\n</details>`;
- return markdown
+ return markdown;
}
/**
@@ -367,11 +367,11 @@ export function buildRequestVariables(cursor?: string, count = 100) {
const variables = {
count,
includePromotedContent: false,
- }
+ };
if (cursor) {
- ;(variables as Record<string, unknown>).cursor = cursor
+ (variables as Record<string, unknown>).cursor = cursor;
}
- return variables
+ return variables;
}
diff --git a/apps/browser-extension/utils/types.ts b/apps/browser-extension/utils/types.ts
index 06a4ae72..d5049e19 100644
--- a/apps/browser-extension/utils/types.ts
+++ b/apps/browser-extension/utils/types.ts
@@ -5,102 +5,102 @@
/**
* Toast states for UI feedback
*/
-export type ToastState = "loading" | "success" | "error"
+export type ToastState = "loading" | "success" | "error";
/**
* Message types for extension communication
*/
export interface ExtensionMessage {
- action?: string
- type?: string
- data?: unknown
- state?: ToastState
- importedMessage?: string
- totalImported?: number
- actionSource?: string
+ action?: string;
+ type?: string;
+ data?: unknown;
+ state?: ToastState;
+ importedMessage?: string;
+ totalImported?: number;
+ actionSource?: string;
}
/**
* Memory data structure for saving content
*/
export interface MemoryData {
- html?: string
- content?: string
- highlightedText?: string
- url?: string
+ html?: string;
+ content?: string;
+ highlightedText?: string;
+ url?: string;
}
/**
* Supermemory API payload for storing memories
*/
export interface MemoryPayload {
- containerTags?: string[]
- content: string
+ containerTags?: string[];
+ content: string;
metadata: {
- sm_source: string
- [key: string]: unknown
- }
- customId?: string
+ sm_source: string;
+ [key: string]: unknown;
+ };
+ customId?: string;
}
/**
* Twitter-specific memory metadata
*/
export interface TwitterMemoryMetadata {
- sm_source: "twitter_bookmarks"
- tweet_id: string
- author: string
- created_at: string
- likes: number
- retweets: number
+ sm_source: "twitter_bookmarks";
+ tweet_id: string;
+ author: string;
+ created_at: string;
+ likes: number;
+ retweets: number;
}
/**
* Storage data structure for Chrome storage
*/
export interface StorageData {
- bearerToken?: string
+ bearerToken?: string;
twitterAuth?: {
- cookie: string
- csrf: string
- auth: string
- }
- tokens_logged?: boolean
- cookie?: string
- csrf?: string
- auth?: string
- defaultProject?: Project
+ cookie: string;
+ csrf: string;
+ auth: string;
+ };
+ tokens_logged?: boolean;
+ cookie?: string;
+ csrf?: string;
+ auth?: string;
+ defaultProject?: Project;
projectsCache?: {
- projects: Project[]
- timestamp: number
- }
+ projects: Project[];
+ timestamp: number;
+ };
}
/**
* Context menu click info
*/
export interface ContextMenuClickInfo {
- menuItemId: string | number
- editable?: boolean
- frameId?: number
- frameUrl?: string
- linkUrl?: string
- mediaType?: string
- pageUrl?: string
- parentMenuItemId?: string | number
- selectionText?: string
- srcUrl?: string
- targetElementId?: number
- wasChecked?: boolean
+ menuItemId: string | number;
+ editable?: boolean;
+ frameId?: number;
+ frameUrl?: string;
+ linkUrl?: string;
+ mediaType?: string;
+ pageUrl?: string;
+ parentMenuItemId?: string | number;
+ selectionText?: string;
+ srcUrl?: string;
+ targetElementId?: number;
+ wasChecked?: boolean;
}
/**
* API Response types
*/
export interface APIResponse<T = unknown> {
- success: boolean
- data?: T
- error?: string
+ success: boolean;
+ data?: T;
+ error?: string;
}
/**
@@ -112,41 +112,41 @@ export class ExtensionError extends Error {
public code?: string,
public statusCode?: number,
) {
- super(message)
- this.name = "ExtensionError"
+ super(message);
+ this.name = "ExtensionError";
}
}
export class TwitterAPIError extends ExtensionError {
constructor(message: string, statusCode?: number) {
- super(message, "TWITTER_API_ERROR", statusCode)
- this.name = "TwitterAPIError"
+ super(message, "TWITTER_API_ERROR", statusCode);
+ this.name = "TwitterAPIError";
}
}
export class SupermemoryAPIError extends ExtensionError {
constructor(message: string, statusCode?: number) {
- super(message, "SUPERMEMORY_API_ERROR", statusCode)
- this.name = "SupermemoryAPIError"
+ super(message, "SUPERMEMORY_API_ERROR", statusCode);
+ this.name = "SupermemoryAPIError";
}
}
export class AuthenticationError extends ExtensionError {
constructor(message = "Authentication required") {
- super(message, "AUTH_ERROR")
- this.name = "AuthenticationError"
+ super(message, "AUTH_ERROR");
+ this.name = "AuthenticationError";
}
}
export interface Project {
- id: string
- name: string
- containerTag: string
- createdAt: string
- updatedAt: string
- documentCount: number
+ id: string;
+ name: string;
+ containerTag: string;
+ createdAt: string;
+ updatedAt: string;
+ documentCount: number;
}
export interface ProjectsResponse {
- projects: Project[]
+ projects: Project[];
}
diff --git a/apps/browser-extension/utils/ui-components.ts b/apps/browser-extension/utils/ui-components.ts
index 8a56ea5a..ed14faff 100644
--- a/apps/browser-extension/utils/ui-components.ts
+++ b/apps/browser-extension/utils/ui-components.ts
@@ -3,8 +3,8 @@
* Reusable UI components for the browser extension
*/
-import { ELEMENT_IDS, UI_CONFIG } from "./constants"
-import type { ToastState } from "./types"
+import { ELEMENT_IDS, UI_CONFIG } from "./constants";
+import type { ToastState } from "./types";
/**
* Creates a toast notification element
@@ -12,8 +12,8 @@ import type { ToastState } from "./types"
* @returns HTMLElement - The toast element
*/
export function createToast(state: ToastState): HTMLElement {
- const toast = document.createElement("div")
- toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST
+ const toast = document.createElement("div");
+ toast.id = ELEMENT_IDS.SUPERMEMORY_TOAST;
toast.style.cssText = `
position: fixed;
@@ -33,12 +33,12 @@ export function createToast(state: ToastState): HTMLElement {
max-width: 300px;
animation: slideIn 0.3s ease-out;
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.18), 0 1.5px 6px 0 rgba(0,0,0,0.12);
- `
+ `;
// Add keyframe animations and fonts if not already present
if (!document.getElementById("supermemory-toast-styles")) {
- const style = document.createElement("style")
- style.id = "supermemory-toast-styles"
+ const style = document.createElement("style");
+ style.id = "supermemory-toast-styles";
style.textContent = `
@font-face {
font-family: 'Space Grotesk';
@@ -87,15 +87,15 @@ export function createToast(state: ToastState): HTMLElement {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
- `
- document.head.appendChild(style)
+ `;
+ document.head.appendChild(style);
}
- const icon = document.createElement("div")
- icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;"
+ const icon = document.createElement("div");
+ icon.style.cssText = "width: 20px; height: 20px; flex-shrink: 0;";
- const text = document.createElement("span")
- text.style.fontWeight = "500"
+ const text = document.createElement("span");
+ text.style.fontWeight = "500";
// Configure toast based on state
switch (state) {
@@ -111,16 +111,16 @@ export function createToast(state: ToastState): HTMLElement {
<path d="M20.49 15.49L18.36 17.62" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.9"/>
<path d="M5.64 6.36L3.51 8.49" stroke="#6366f1" stroke-width="2" stroke-linecap="round" opacity="0.6"/>
</svg>
- `
- icon.style.animation = "spin 1s linear infinite"
- text.textContent = "Adding to Memory..."
- break
+ `;
+ icon.style.animation = "spin 1s linear infinite";
+ text.textContent = "Adding to Memory...";
+ break;
case "success": {
- const iconUrl = browser.runtime.getURL("/icon-16.png")
- icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
- text.textContent = "Added to Memory"
- break
+ const iconUrl = browser.runtime.getURL("/icon-16.png");
+ icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`;
+ text.textContent = "Added to Memory";
+ break;
}
case "error":
@@ -130,15 +130,15 @@ export function createToast(state: ToastState): HTMLElement {
<path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
- `
- text.textContent = "Failed to save memory / Make sure you are logged in"
- break
+ `;
+ text.textContent = "Failed to save memory / Make sure you are logged in";
+ break;
}
- toast.appendChild(icon)
- toast.appendChild(text)
+ toast.appendChild(icon);
+ toast.appendChild(text);
- return toast
+ return toast;
}
/**
@@ -147,8 +147,8 @@ export function createToast(state: ToastState): HTMLElement {
* @returns HTMLElement - The button element
*/
export function createTwitterImportButton(onClick: () => void): HTMLElement {
- const button = document.createElement("div")
- button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON
+ const button = document.createElement("div");
+ button.id = ELEMENT_IDS.TWITTER_IMPORT_BUTTON;
button.style.cssText = `
position: fixed;
top: 10px;
@@ -164,27 +164,27 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
align-items: center;
gap: 8px;
transition: all 0.2s ease;
- `
+ `;
- const iconUrl = browser.runtime.getURL("/icon-16.png")
+ const iconUrl = browser.runtime.getURL("/icon-16.png");
button.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
<span style="font-weight: 500; font-size: 12px;">Import Bookmarks</span>
- `
+ `;
button.addEventListener("mouseenter", () => {
- button.style.opacity = "0.8"
- button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)"
- })
+ button.style.opacity = "0.8";
+ button.style.boxShadow = "0 4px 12px rgba(29, 155, 240, 0.4)";
+ });
button.addEventListener("mouseleave", () => {
- button.style.opacity = "1"
- button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)"
- })
+ button.style.opacity = "1";
+ button.style.boxShadow = "0 2px 8px rgba(29, 155, 240, 0.3)";
+ });
- button.addEventListener("click", onClick)
+ button.addEventListener("click", onClick);
- return button
+ return button;
}
/**
@@ -193,7 +193,7 @@ export function createTwitterImportButton(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element
*/
export function createSaveTweetElement(onClick: () => void): HTMLElement {
- const iconButton = document.createElement("div")
+ const iconButton = document.createElement("div");
iconButton.style.cssText = `
display: inline-flex;
align-items: flex-end;
@@ -206,29 +206,29 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
margin-right: 10px;
margin-bottom: 2px;
z-index: 1000;
- `
+ `;
- const iconFileName = "/icon-16.png"
- const iconUrl = browser.runtime.getURL(iconFileName)
+ const iconFileName = "/icon-16.png";
+ const iconUrl = browser.runtime.getURL(iconFileName);
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 4px;" />
- `
+ `;
iconButton.addEventListener("mouseenter", () => {
- iconButton.style.opacity = "1"
- })
+ iconButton.style.opacity = "1";
+ });
iconButton.addEventListener("mouseleave", () => {
- iconButton.style.opacity = "0.7"
- })
+ iconButton.style.opacity = "0.7";
+ });
iconButton.addEventListener("click", (event) => {
- event.stopPropagation()
- event.preventDefault()
- onClick()
- })
+ event.stopPropagation();
+ event.preventDefault();
+ onClick();
+ });
- return iconButton
+ return iconButton;
}
/**
@@ -237,7 +237,7 @@ export function createSaveTweetElement(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element
*/
export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
- const iconButton = document.createElement("div")
+ const iconButton = document.createElement("div");
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
@@ -247,30 +247,30 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
cursor: pointer;
transition: opacity 0.2s ease;
border-radius: 50%;
- `
+ `;
// Use appropriate icon based on theme
- const iconFileName = "/icon-16.png"
- const iconUrl = browser.runtime.getURL(iconFileName)
+ const iconFileName = "/icon-16.png";
+ const iconUrl = browser.runtime.getURL(iconFileName);
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Save to Memory" style="border-radius: 50%;" />
- `
+ `;
iconButton.addEventListener("mouseenter", () => {
- iconButton.style.opacity = "0.8"
- })
+ iconButton.style.opacity = "0.8";
+ });
iconButton.addEventListener("mouseleave", () => {
- iconButton.style.opacity = "1"
- })
+ iconButton.style.opacity = "1";
+ });
iconButton.addEventListener("click", (event) => {
- event.stopPropagation()
- event.preventDefault()
- onClick()
- })
+ event.stopPropagation();
+ event.preventDefault();
+ onClick();
+ });
- return iconButton
+ return iconButton;
}
/**
@@ -279,7 +279,7 @@ export function createChatGPTInputBarElement(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element
*/
export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
- const iconButton = document.createElement("div")
+ const iconButton = document.createElement("div");
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
@@ -290,31 +290,31 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
- `
+ `;
- const iconFileName = "/icon-16.png"
- const iconUrl = browser.runtime.getURL(iconFileName)
+ const iconFileName = "/icon-16.png";
+ const iconUrl = browser.runtime.getURL(iconFileName);
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
- `
+ `;
iconButton.addEventListener("mouseenter", () => {
- iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"
- iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"
- })
+ iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
+ iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)";
+ });
iconButton.addEventListener("mouseleave", () => {
- iconButton.style.backgroundColor = "transparent"
- iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"
- })
+ iconButton.style.backgroundColor = "transparent";
+ iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)";
+ });
iconButton.addEventListener("click", (event) => {
- event.stopPropagation()
- event.preventDefault()
- onClick()
- })
+ event.stopPropagation();
+ event.preventDefault();
+ onClick();
+ });
- return iconButton
+ return iconButton;
}
/**
@@ -323,7 +323,7 @@ export function createClaudeInputBarElement(onClick: () => void): HTMLElement {
* @returns HTMLElement - The save button element
*/
export function createT3InputBarElement(onClick: () => void): HTMLElement {
- const iconButton = document.createElement("div")
+ const iconButton = document.createElement("div");
iconButton.style.cssText = `
display: inline-flex;
align-items: center;
@@ -334,31 +334,31 @@ export function createT3InputBarElement(onClick: () => void): HTMLElement {
transition: all 0.2s ease;
border-radius: 6px;
background: transparent;
- `
+ `;
- const iconFileName = "/icon-16.png"
- const iconUrl = browser.runtime.getURL(iconFileName)
+ const iconFileName = "/icon-16.png";
+ const iconUrl = browser.runtime.getURL(iconFileName);
iconButton.innerHTML = `
<img src="${iconUrl}" width="20" height="20" alt="Get Related Memories from supermemory" style="border-radius: 4px;" />
- `
+ `;
iconButton.addEventListener("mouseenter", () => {
- iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)"
- iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)"
- })
+ iconButton.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
+ iconButton.style.borderColor = "rgba(0, 0, 0, 0.2)";
+ });
iconButton.addEventListener("mouseleave", () => {
- iconButton.style.backgroundColor = "transparent"
- iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)"
- })
+ iconButton.style.backgroundColor = "transparent";
+ iconButton.style.borderColor = "rgba(0, 0, 0, 0.1)";
+ });
iconButton.addEventListener("click", (event) => {
- event.stopPropagation()
- event.preventDefault()
- onClick()
- })
+ event.stopPropagation();
+ event.preventDefault();
+ onClick();
+ });
- return iconButton
+ return iconButton;
}
/**
@@ -371,7 +371,7 @@ export const DOMUtils = {
* @returns boolean
*/
isOnDomain(domains: readonly string[]): boolean {
- return domains.includes(window.location.hostname)
+ return domains.includes(window.location.hostname);
},
/**
@@ -379,9 +379,9 @@ export const DOMUtils = {
* @returns boolean - true if dark mode, false if light mode
*/
isDarkMode(): boolean {
- const htmlElement = document.documentElement
- const style = htmlElement.getAttribute("style")
- return style?.includes("color-scheme: dark") || false
+ const htmlElement = document.documentElement;
+ const style = htmlElement.getAttribute("style");
+ return style?.includes("color-scheme: dark") || false;
},
/**
@@ -390,7 +390,7 @@ export const DOMUtils = {
* @returns boolean
*/
elementExists(id: string): boolean {
- return !!document.getElementById(id)
+ return !!document.getElementById(id);
},
/**
@@ -398,8 +398,8 @@ export const DOMUtils = {
* @param id - Element ID to remove
*/
removeElement(id: string): void {
- const element = document.getElementById(id)
- element?.remove()
+ const element = document.getElementById(id);
+ element?.remove();
},
/**
@@ -412,18 +412,20 @@ export const DOMUtils = {
state: ToastState,
duration: number = UI_CONFIG.TOAST_DURATION,
): HTMLElement {
- const existingToast = document.getElementById(ELEMENT_IDS.SUPERMEMORY_TOAST)
+ const existingToast = document.getElementById(
+ ELEMENT_IDS.SUPERMEMORY_TOAST,
+ );
if ((state === "success" || state === "error") && existingToast) {
- const icon = existingToast.querySelector("div")
- const text = existingToast.querySelector("span")
+ const icon = existingToast.querySelector("div");
+ const text = existingToast.querySelector("span");
if (icon && text) {
if (state === "success") {
- const iconUrl = browser.runtime.getURL("/icon-16.png")
- icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`
- icon.style.animation = ""
- text.textContent = "Added to Memory"
+ const iconUrl = browser.runtime.getURL("/icon-16.png");
+ icon.innerHTML = `<img src="${iconUrl}" width="20" height="20" alt="Success" style="border-radius: 2px;" />`;
+ icon.style.animation = "";
+ text.textContent = "Added to Memory";
} else if (state === "error") {
icon.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -431,52 +433,52 @@ export const DOMUtils = {
<path d="M15 9L9 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9L15 15" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
- `
- icon.style.animation = ""
+ `;
+ icon.style.animation = "";
text.textContent =
- "Failed to save memory / Make sure you are logged in"
+ "Failed to save memory / Make sure you are logged in";
}
// Auto-dismiss
setTimeout(() => {
if (document.body.contains(existingToast)) {
- existingToast.style.animation = "fadeOut 0.3s ease-out"
+ existingToast.style.animation = "fadeOut 0.3s ease-out";
setTimeout(() => {
if (document.body.contains(existingToast)) {
- existingToast.remove()
+ existingToast.remove();
}
- }, 300)
+ }, 300);
}
- }, duration)
+ }, duration);
- return existingToast
+ return existingToast;
}
}
const existingToasts = document.querySelectorAll(
`#${ELEMENT_IDS.SUPERMEMORY_TOAST}`,
- )
+ );
existingToasts.forEach((toast) => {
- toast.remove()
- })
+ toast.remove();
+ });
- const toast = createToast(state)
- document.body.appendChild(toast)
+ const toast = createToast(state);
+ document.body.appendChild(toast);
// Auto-dismiss for success and error states
if (state === "success" || state === "error") {
setTimeout(() => {
if (document.body.contains(toast)) {
- toast.style.animation = "fadeOut 0.3s ease-out"
+ toast.style.animation = "fadeOut 0.3s ease-out";
setTimeout(() => {
if (document.body.contains(toast)) {
- toast.remove()
+ toast.remove();
}
- }, 300)
+ }, 300);
}
- }, duration)
+ }, duration);
}
- return toast
+ return toast;
},
-}
+};