feat(raycast-extension): initial version of supermemory extension for raycast (#440)

feat(raycast-extension): initial version of supermemory extension for raycast

chore: update the metadata and ui for app to get api key

![supermemory-1.png](https://app.graphite.dev/user-attachments/assets/631a865e-8d7b-43df-8753-480f6b80a6d8.png)

![supermemory-2.png](https://app.graphite.dev/user-attachments/assets/956fff54-5447-4feb-a88b-8b465d4cda68.png)
This commit is contained in:
MaheshtheDev 2025-10-02 16:22:56 +00:00
parent e778b6d494
commit 163c9daca7
16 changed files with 4199 additions and 5 deletions

14
apps/raycast-extension/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# Raycast specific files
raycast-env.d.ts
.raycast-swift-build
.swiftpm
compiled_raycast_swift
compiled_raycast_rust
# misc
.DS_Store

View file

@ -0,0 +1,6 @@
# supermemory-raycast Changelog
## [Initial Version] - {2025-09-27}
- Added Supermemory integration with Add Memory and Search Memories commands
- Added project organization support for memories

View file

@ -0,0 +1,35 @@
# Raycast Extension for Supermemory
A Raycast extension that lets you add memories and search through your Supermemory collection directly from Raycast.
## Setup
1. Install the extension in Raycast
2. Get your API key from [app.supermemory.ai](https://app.supermemory.ai)
3. Open the extension preferences and enter your API key
## Features
### Add Memory
- Add new memories to your Supermemory collection
- Organize memories by project
- Add optional titles and URLs
- Keyboard shortcut: Cmd+Enter to submit
### Search Memories
- Search through your entire Supermemory collection
- Real-time search with debouncing
- View detailed memory information
- Copy content or open related URLs
- Shows relevance scores and creation dates
## Commands
- `Add Memory` - Add a new memory to your collection
- `Search Memories` - Search through your existing memories
## Authentication
This extension requires a Supermemory API key. You can get your API key from [supermemory.link/raycast](https://supermemory.link/raycast).
The API key is stored securely in Raycast preferences and is required for all operations.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,4 @@
const { defineConfig } = require("eslint/config")
const raycastConfig = require("@raycast/eslint-config")
module.exports = defineConfig([...raycastConfig])

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

3233
apps/raycast-extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "supermemory",
"title": "Supermemory",
"description": "Add and search memories with your personal AI-powered knowledge base",
"icon": "extension-icon.png",
"author": "supermemory",
"platforms": [
"macOS",
"Windows"
],
"categories": [
"Productivity",
"Web"
],
"license": "MIT",
"commands": [
{
"name": "add-memory",
"title": "Add Memory",
"subtitle": "add memory to your supermemory app",
"description": "Add text, URLs, or documents to your supermemory knowledge base",
"mode": "view"
},
{
"name": "search-memories",
"title": "Search Memories",
"description": "Search through your saved memories and find relevant information",
"mode": "view"
}
],
"preferences": [
{
"name": "apiKey",
"type": "password",
"required": true,
"title": "API Key",
"description": "Your Supermemory API Key. Get it from https://supermemory.link/raycast",
"placeholder": "Enter your Supermemory API Key"
}
],
"dependencies": {
"@raycast/api": "^1.103.2",
"@raycast/utils": "^1.17.0"
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
"@types/node": "22.13.10",
"@types/react": "19.0.10",
"eslint": "^9.22.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2"
},
"scripts": {
"build": "ray build",
"dev": "ray develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish"
}
}

View file

@ -0,0 +1,110 @@
import {
Form,
ActionPanel,
Action,
showToast,
Toast,
useNavigation,
} from "@raycast/api";
import { useEffect, useState } from "react";
import {
addMemory,
fetchProjects,
checkApiConnection,
type Project,
} from "./api";
interface FormValues {
content: string;
project: string;
}
export default function Command() {
const [projects, setProjects] = useState<Project[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const { pop } = useNavigation();
useEffect(() => {
async function loadProjects() {
try {
setIsLoading(true);
const isConnected = await checkApiConnection();
if (!isConnected) {
return;
}
const fetchedProjects = await fetchProjects();
setProjects(fetchedProjects);
} catch (error) {
console.error("Failed to load projects:", error);
} finally {
setIsLoading(false);
}
}
loadProjects();
}, []);
async function handleSubmit(values: FormValues) {
if (!values.content.trim()) {
await showToast({
style: Toast.Style.Failure,
title: "Content Required",
message: "Please enter some content for the memory",
});
return;
}
try {
setIsSubmitting(true);
const containerTags = values.project ? [values.project] : undefined;
await addMemory({
content: values.content.trim(),
containerTags,
});
pop();
} catch (error) {
console.error("Failed to add memory:", error);
} finally {
setIsSubmitting(false);
}
}
return (
<Form
isLoading={isLoading || isSubmitting}
actions={
<ActionPanel>
<Action.SubmitForm title="Add Memory" onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.TextArea
id="content"
title="Content"
placeholder="Enter the memory content..."
info="The main content of your memory. This is required."
/>
<Form.Separator />
<Form.Dropdown
id="project"
title="Project"
info="Select a project to organize this memory"
storeValue
>
<Form.Dropdown.Item value="" title="No Project" />
{projects.map((project) => (
<Form.Dropdown.Item
key={project.id}
value={project.containerTag}
title={project.name}
/>
))}
</Form.Dropdown>
</Form>
);
}

View file

@ -0,0 +1,227 @@
import { getPreferenceValues, showToast, Toast } from "@raycast/api";
export interface Project {
id: string;
name: string;
containerTag: string;
description?: string;
}
export interface Memory {
id: string;
content: string;
title?: string;
url?: string;
containerTag?: string;
createdAt: string;
}
export interface SearchResult {
documentId: string;
chunks: unknown[];
title?: string;
metadata: Record<string, unknown>;
score?: number;
createdAt: string;
updatedAt: string;
type: string;
}
export interface AddMemoryRequest {
content: string;
containerTags?: string[];
title?: string;
url?: string;
metadata?: Record<string, unknown>;
}
export interface SearchRequest {
q: string;
containerTags?: string[];
limit?: number;
}
export interface SearchResponse {
results: SearchResult[];
timing: number;
total: number;
}
const API_BASE_URL = "https://api.supermemory.ai";
class SupermemoryAPIError extends Error {
constructor(
message: string,
public status?: number,
) {
super(message);
this.name = "SupermemoryAPIError";
}
}
class AuthenticationError extends Error {
constructor(message: string) {
super(message);
this.name = "AuthenticationError";
}
}
async function getApiKey(): Promise<string> {
try {
const preferences = getPreferenceValues<{ apiKey: string }>();
const apiKey = preferences.apiKey?.trim();
if (!apiKey) {
throw new AuthenticationError(
"API key is required. Please add your Supermemory API key in preferences.",
);
}
return apiKey;
} catch {
throw new AuthenticationError("Failed to get API key from preferences.");
}
}
async function makeAuthenticatedRequest<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const apiKey = await getApiKey();
const url = `${API_BASE_URL}${endpoint}`;
try {
const response = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
if (response.status === 401) {
throw new AuthenticationError(
"Invalid API key. Please check your API key in preferences. Get a new one from https://supermemory.link/raycast",
);
}
let errorMessage = `API request failed: ${response.statusText}`;
try {
const errorBody = (await response.json()) as { message?: string };
if (errorBody.message) {
errorMessage = errorBody.message;
}
} catch {
// Ignore JSON parsing errors, use default message
}
throw new SupermemoryAPIError(errorMessage, response.status);
}
if (!response.headers.get("content-type")?.includes("application/json")) {
throw new SupermemoryAPIError("Invalid response format from API");
}
const data = (await response.json()) as T;
return data;
} catch (err) {
if (
err instanceof AuthenticationError ||
err instanceof SupermemoryAPIError
) {
throw err;
}
// Handle network errors or other fetch errors
throw new SupermemoryAPIError(
`Network error: ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
}
export async function fetchProjects(): Promise<Project[]> {
try {
const response = await makeAuthenticatedRequest<{ projects: Project[] }>(
"/v3/projects",
);
return response.projects || [];
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to fetch projects",
message:
error instanceof Error ? error.message : "Unknown error occurred",
});
throw error;
}
}
export async function addMemory(request: AddMemoryRequest): Promise<Memory> {
try {
const response = await makeAuthenticatedRequest<Memory>("/v3/documents", {
method: "POST",
body: JSON.stringify(request),
});
await showToast({
style: Toast.Style.Success,
title: "Memory Added",
message: "Successfully added memory to Supermemory",
});
return response;
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to add memory",
message:
error instanceof Error ? error.message : "Unknown error occurred",
});
throw error;
}
}
export async function searchMemories(
request: SearchRequest,
): Promise<SearchResult[]> {
try {
const response = await makeAuthenticatedRequest<SearchResponse>(
"/v3/search",
{
method: "POST",
body: JSON.stringify(request),
},
);
return response.results || [];
} catch (error) {
await showToast({
style: Toast.Style.Failure,
title: "Failed to search memories",
message:
error instanceof Error ? error.message : "Unknown error occurred",
});
throw error;
}
}
// Helper function to check if API key is configured and valid
export async function checkApiConnection(): Promise<boolean> {
try {
await fetchProjects();
return true;
} catch (error) {
if (error instanceof AuthenticationError) {
await showToast({
style: Toast.Style.Failure,
title: "API Key Required",
message:
"Please configure your Supermemory API key in preferences. Get it from https://supermemory.link/raycast",
});
}
return false;
}
}

View file

@ -0,0 +1,253 @@
import {
ActionPanel,
Detail,
List,
Action,
Icon,
showToast,
Toast,
Clipboard,
openExtensionPreferences,
} from "@raycast/api";
import { useState, useEffect, useCallback } from "react";
import { searchMemories, checkApiConnection, type SearchResult } from "./api";
const extractContent = (memory: SearchResult) => {
if (memory.chunks && memory.chunks.length > 0) {
return memory.chunks
.map((chunk: unknown) => {
if (typeof chunk === "string") return chunk;
if (
chunk &&
typeof chunk === "object" &&
"content" in chunk &&
typeof chunk.content === "string"
)
return chunk.content;
if (
chunk &&
typeof chunk === "object" &&
"text" in chunk &&
typeof chunk.text === "string"
)
return chunk.text;
return "";
})
.filter(Boolean)
.join(" ");
}
return "No content available";
};
const extractUrl = (memory: SearchResult) => {
if (memory.metadata?.url && typeof memory.metadata.url === "string") {
return memory.metadata.url;
}
return null;
};
export default function Command() {
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [searchText, setSearchText] = useState("");
const [hasSearched, setHasSearched] = useState(false);
const [isConnected, setIsConnected] = useState<boolean | null>(null);
useEffect(() => {
async function checkConnection() {
const connected = await checkApiConnection();
setIsConnected(connected);
}
checkConnection();
}, []);
const performSearch = useCallback(
async (query: string) => {
if (!query.trim() || !isConnected) return;
try {
setIsLoading(true);
setHasSearched(true);
const results = await searchMemories({
q: query.trim(),
limit: 50,
});
setSearchResults(results);
if (results.length === 0) {
await showToast({
style: Toast.Style.Success,
title: "Search Complete",
message: "No memories found for your query",
});
}
} catch (error) {
console.error("Search failed:", error);
setSearchResults([]);
} finally {
setIsLoading(false);
}
},
[isConnected],
);
useEffect(() => {
if (!searchText.trim()) {
setSearchResults([]);
setHasSearched(false);
return;
}
const debounceTimer = setTimeout(() => {
performSearch(searchText);
}, 500);
return () => clearTimeout(debounceTimer);
}, [searchText, performSearch]);
const formatDate = (dateString: string) => {
try {
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return "Unknown date";
}
};
const truncateContent = (content: string, maxLength = 100) => {
if (content.length <= maxLength) return content;
return `${content.substring(0, maxLength)}...`;
};
if (isConnected === false) {
return (
<List>
<List.EmptyView
icon={Icon.ExclamationMark}
title="API Key Required"
description="Please configure your Supermemory API key to search memories"
actions={
<ActionPanel>
<Action
title="Open Extension Preferences"
onAction={() => openExtensionPreferences()}
icon={Icon.Gear}
/>
</ActionPanel>
}
/>
</List>
);
}
return (
<List
isLoading={isLoading}
onSearchTextChange={setSearchText}
searchBarPlaceholder="Search your memories..."
throttle
>
{!hasSearched && !searchText.trim() ? (
<List.EmptyView
icon={Icon.MagnifyingGlass}
title="Search Your Memories"
description="Type to search through your Supermemory collection"
/>
) : hasSearched && searchResults.length === 0 ? (
<List.EmptyView
icon={Icon.Document}
title="No Memories Found"
description={`No memories found for "${searchText}"`}
/>
) : (
searchResults.map((memory) => {
const content = extractContent(memory);
const url = extractUrl(memory);
return (
<List.Item
key={memory.documentId}
icon={url ? Icon.Link : Icon.Document}
title={memory.title || "Untitled Memory"}
subtitle={truncateContent(content)}
accessories={[
{ text: formatDate(memory.createdAt) },
...(memory.score
? [{ text: `${Math.round(memory.score * 100)}%` }]
: []),
]}
actions={
<ActionPanel>
<Action.Push
title="View Details"
target={<MemoryDetail memory={memory} />}
icon={Icon.Eye}
/>
<Action
title="Copy Content"
onAction={() => Clipboard.copy(content)}
icon={Icon.Clipboard}
shortcut={{ modifiers: ["cmd"], key: "c" }}
/>
{url && (
<Action.OpenInBrowser
title="Open URL"
url={url}
shortcut={{ modifiers: ["cmd"], key: "o" }}
/>
)}
</ActionPanel>
}
/>
);
})
)}
</List>
);
}
function MemoryDetail({ memory }: { memory: SearchResult }) {
const content = extractContent(memory);
const url = extractUrl(memory);
const markdown = `
# ${memory.title || "Untitled Memory"}
${content}
---
**Created:** ${new Date(memory.createdAt).toLocaleString()}
${url ? `**URL:** ${url}` : ""}
${memory.score ? `**Relevance:** ${Math.round(memory.score * 100)}%` : ""}
`;
return (
<Detail
markdown={markdown}
actions={
<ActionPanel>
<Action
title="Copy Content"
onAction={() => Clipboard.copy(content)}
icon={Icon.Clipboard}
shortcut={{ modifiers: ["cmd"], key: "c" }}
/>
{url && (
<Action.OpenInBrowser
title="Open URL"
url={url}
shortcut={{ modifiers: ["cmd"], key: "o" }}
/>
)}
</ActionPanel>
}
/>
);
}

View file

@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["src/**/*", "raycast-env.d.ts"],
"compilerOptions": {
"lib": ["ES2023"],
"module": "commonjs",
"target": "ES2023",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"resolveJsonModule": true
}
}

View file

@ -4,6 +4,7 @@ import { useAuth } from "@lib/auth-context"
import { generateId } from "@lib/generate-id"
import {
ADD_MEMORY_SHORTCUT_URL,
RAYCAST_EXTENSION_URL,
SEARCH_MEMORY_SHORTCUT_URL,
} from "@repo/lib/constants"
import { fetchConnectionsFeature } from "@repo/lib/queries"
@ -20,9 +21,17 @@ import type { ConnectionResponseSchema } from "@repo/validation/api"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { Check, Copy, Smartphone, Trash2 } from "lucide-react"
import {
Check,
Copy,
DownloadIcon,
KeyIcon,
Smartphone,
Trash2,
} from "lucide-react"
import { motion } from "motion/react"
import Image from "next/image"
import { useSearchParams } from "next/navigation"
import { useEffect, useId, useState } from "react"
import { toast } from "sonner"
import type { z } from "zod"
@ -87,6 +96,7 @@ export function IntegrationsView() {
const queryClient = useQueryClient()
const { selectedProject } = useProject()
const autumn = useCustomer()
const searchParams = useSearchParams()
const [showApiKeyModal, setShowApiKeyModal] = useState(false)
const [apiKey, setApiKey] = useState<string>("")
const [copied, setCopied] = useState(false)
@ -94,7 +104,12 @@ export function IntegrationsView() {
const [selectedShortcutType, setSelectedShortcutType] = useState<
"add" | "search" | null
>(null)
const [showRaycastApiKeyModal, setShowRaycastApiKeyModal] = useState(false)
const [raycastApiKey, setRaycastApiKey] = useState<string>("")
const [raycastCopied, setRaycastCopied] = useState(false)
const [hasTriggeredRaycast, setHasTriggeredRaycast] = useState(false)
const apiKeyId = useId()
const raycastApiKeyId = useId()
const handleUpgrade = async () => {
try {
@ -233,7 +248,7 @@ export function IntegrationsView() {
setApiKey(apiKey)
setShowApiKeyModal(true)
setCopied(false)
handleCopyApiKey()
handleCopyApiKey(apiKey)
},
onError: (error) => {
toast.error("Failed to create API key", {
@ -242,12 +257,49 @@ export function IntegrationsView() {
},
})
const createRaycastApiKeyMutation = useMutation({
mutationFn: async () => {
const res = await authClient.apiKey.create({
metadata: {
organizationId: org?.id,
type: "raycast-extension",
},
name: `raycast-${generateId().slice(0, 8)}`,
prefix: `sm_${org?.id}_`,
})
return res.key
},
onSuccess: (apiKey) => {
setRaycastApiKey(apiKey)
setShowRaycastApiKeyModal(true)
setRaycastCopied(false)
handleCopyApiKey(apiKey)
},
onError: (error) => {
toast.error("Failed to create Raycast API key", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
})
useEffect(() => {
const qParam = searchParams.get("q")
if (
qParam === "raycast" &&
!hasTriggeredRaycast &&
!createRaycastApiKeyMutation.isPending
) {
setHasTriggeredRaycast(true)
createRaycastApiKeyMutation.mutate()
}
}, [searchParams, hasTriggeredRaycast, createRaycastApiKeyMutation])
const handleShortcutClick = (shortcutType: "add" | "search") => {
setSelectedShortcutType(shortcutType)
createApiKeyMutation.mutate()
}
const handleCopyApiKey = async () => {
const handleCopyApiKey = async (apiKey: string) => {
try {
await navigator.clipboard.writeText(apiKey)
setCopied(true)
@ -281,6 +333,18 @@ export function IntegrationsView() {
}
}
const handleRaycastDialogClose = (open: boolean) => {
setShowRaycastApiKeyModal(open)
if (!open) {
setRaycastApiKey("")
setRaycastCopied(false)
}
}
const handleRaycastClick = () => {
createRaycastApiKeyMutation.mutate()
}
return (
<div className="space-y-4 sm:space-y-4 custom-scrollbar">
{/* iOS Shortcuts */}
@ -336,6 +400,64 @@ export function IntegrationsView() {
</div>
</div>
{/* Raycast Extension */}
<div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3 mb-3">
<div className="p-2 bg-purple-500/10 rounded-lg flex-shrink-0">
<svg
width="24"
height="24"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Raycast Icon</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7 18.079V21L0 14L1.46 12.54L7 18.081V18.079ZM9.921 21H7L14 28L15.46 26.54L9.921 21ZM26.535 15.462L27.996 14L13.996 0L12.538 1.466L18.077 7.004H14.73L10.864 3.146L9.404 4.606L11.809 7.01H10.129V17.876H20.994V16.196L23.399 18.6L24.859 17.14L20.994 13.274V9.927L26.535 15.462ZM7.73 6.276L6.265 7.738L7.833 9.304L9.294 7.844L7.73 6.276ZM20.162 18.708L18.702 20.17L20.268 21.738L21.73 20.276L20.162 18.708ZM4.596 9.41L3.134 10.872L7 14.738V11.815L4.596 9.41ZM16.192 21.006H13.268L17.134 24.872L18.596 23.41L16.192 21.006Z"
fill="#FF6363"
/>
</svg>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-card-foreground font-semibold text-base mb-1">
Raycast Extension
</h3>
<p className="text-muted-foreground text-sm leading-relaxed">
Add and search memories directly from Raycast on Mac and
Windows.
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<Button
variant="secondary"
className="flex-1"
onClick={handleRaycastClick}
disabled={createRaycastApiKeyMutation.isPending}
>
<KeyIcon className="h-4 w-4" />
{createRaycastApiKeyMutation.isPending
? "Generating..."
: "Get API Key"}
</Button>
<Button
variant="secondary"
className="flex-1"
onClick={() => {
window.open(RAYCAST_EXTENSION_URL, "_blank")
analytics.extensionInstallClicked()
}}
>
<DownloadIcon className="h-4 w-4" />
Install Extension
</Button>
</div>
</div>
</div>
{/* Chrome Extension */}
<div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
@ -630,8 +752,8 @@ export function IntegrationsView() {
<Button
size="sm"
variant="ghost"
onClick={handleCopyApiKey}
className="hover:bg-accent"
onClick={() => handleCopyApiKey(apiKey)}
className="text-white/70 hover:text-white hover:bg-white/10"
>
{copied ? (
<Check className="h-4 w-4 text-chart-2" />
@ -696,6 +818,115 @@ export function IntegrationsView() {
</DialogContent>
</DialogPortal>
</Dialog>
<Dialog
open={showRaycastApiKeyModal}
onOpenChange={handleRaycastDialogClose}
>
<DialogPortal>
<DialogContent className="bg-card border-border text-card-foreground md:max-w-md z-[100]">
<DialogHeader>
<DialogTitle className="text-card-foreground text-lg font-semibold">
Setup Raycast Extension
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* API Key Section */}
<div className="space-y-2">
<label
htmlFor={raycastApiKeyId}
className="text-sm font-medium text-muted-foreground"
>
Your Raycast API Key
</label>
<div className="flex items-center gap-2">
<input
id={raycastApiKeyId}
type="text"
value={raycastApiKey}
readOnly
className="flex-1 bg-input border border-border rounded-lg px-3 py-2 text-sm text-foreground font-mono"
/>
<Button
size="sm"
variant="ghost"
onClick={() => handleCopyApiKey(raycastApiKey)}
className="text-muted-foreground hover:text-foreground hover:bg-accent"
>
{raycastCopied ? (
<Check className="h-4 w-4 text-chart-2" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
{/* Steps */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-muted-foreground">
Follow these steps:
</h4>
<div className="space-y-2">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
1
</div>
<p className="text-sm text-muted-foreground">
Install the Raycast extension from the Raycast Store
</p>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
2
</div>
<p className="text-sm text-muted-foreground">
Open Raycast preferences and paste your API key
</p>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
3
</div>
<p className="text-sm text-muted-foreground">
Use "Add Memory" or "Search Memories" commands!
</p>
</div>
</div>
</div>
<div className="flex gap-2 pt-2">
<Button
onClick={() => {
window.open(RAYCAST_EXTENSION_URL, "_blank")
analytics.extensionInstallClicked()
}}
className="flex-1"
variant="default"
>
<svg
width="24"
height="24"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Raycast Icon</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7 18.079V21L0 14L1.46 12.54L7 18.081V18.079ZM9.921 21H7L14 28L15.46 26.54L9.921 21ZM26.535 15.462L27.996 14L13.996 0L12.538 1.466L18.077 7.004H14.73L10.864 3.146L9.404 4.606L11.809 7.01H10.129V17.876H20.994V16.196L23.399 18.6L24.859 17.14L20.994 13.274V9.927L26.535 15.462ZM7.73 6.276L6.265 7.738L7.833 9.304L9.294 7.844L7.73 6.276ZM20.162 18.708L18.702 20.17L20.268 21.738L21.73 20.276L20.162 18.708ZM4.596 9.41L3.134 10.872L7 14.738V11.815L4.596 9.41ZM16.192 21.006H13.268L17.134 24.872L18.596 23.41L16.192 21.006Z"
fill="#FF6363"
/>
</svg>
Install Extension
</Button>
</div>
</div>
</DialogContent>
</DialogPortal>
</Dialog>
</div>
)
}

View file

@ -15,6 +15,7 @@
"packageManager": "bun@1.2.17",
"workspaces": [
"apps/*",
"!apps/raycast-extension",
"packages/*"
],
"dependencies": {

View file

@ -4,10 +4,12 @@ const SEARCH_MEMORY_SHORTCUT_URL =
"https://www.icloud.com/shortcuts/f2b5c544372844a38ab4c6900e2a88de"
const ADD_MEMORY_SHORTCUT_URL =
"https://www.icloud.com/shortcuts/0fd3e855be444845b457f94c78c2c8d9"
const RAYCAST_EXTENSION_URL = "https://www.raycast.com/supermemory/supermemory"
export {
BIG_DIMENSIONS_NEW,
DEFAULT_PROJECT_ID,
SEARCH_MEMORY_SHORTCUT_URL,
ADD_MEMORY_SHORTCUT_URL,
RAYCAST_EXTENSION_URL,
}