mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
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  
This commit is contained in:
parent
e778b6d494
commit
163c9daca7
16 changed files with 4199 additions and 5 deletions
14
apps/raycast-extension/.gitignore
vendored
Normal file
14
apps/raycast-extension/.gitignore
vendored
Normal 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
|
||||
6
apps/raycast-extension/CHANGELOG.md
Normal file
6
apps/raycast-extension/CHANGELOG.md
Normal 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
|
||||
35
apps/raycast-extension/README.md
Normal file
35
apps/raycast-extension/README.md
Normal 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.
|
||||
BIN
apps/raycast-extension/assets/extension-icon.png
Normal file
BIN
apps/raycast-extension/assets/extension-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
4
apps/raycast-extension/eslint.config.js
Normal file
4
apps/raycast-extension/eslint.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const { defineConfig } = require("eslint/config")
|
||||
const raycastConfig = require("@raycast/eslint-config")
|
||||
|
||||
module.exports = defineConfig([...raycastConfig])
|
||||
BIN
apps/raycast-extension/metadata/supermemory-1.png
Normal file
BIN
apps/raycast-extension/metadata/supermemory-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
apps/raycast-extension/metadata/supermemory-2.png
Normal file
BIN
apps/raycast-extension/metadata/supermemory-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
3233
apps/raycast-extension/package-lock.json
generated
Normal file
3233
apps/raycast-extension/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
62
apps/raycast-extension/package.json
Normal file
62
apps/raycast-extension/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
110
apps/raycast-extension/src/add-memory.tsx
Normal file
110
apps/raycast-extension/src/add-memory.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
227
apps/raycast-extension/src/api.ts
Normal file
227
apps/raycast-extension/src/api.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
253
apps/raycast-extension/src/search-memories.tsx
Normal file
253
apps/raycast-extension/src/search-memories.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
apps/raycast-extension/tsconfig.json
Normal file
16
apps/raycast-extension/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"packageManager": "bun@1.2.17",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"!apps/raycast-extension",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue