chore: update new contributions for raycast extension (#561)

chore: update new contributions for raycast extension

add quick add from selection with extension
This commit is contained in:
MaheshtheDev 2025-12-04 18:58:30 +00:00
parent 1b9b3012e3
commit c4e67d8707
8 changed files with 521 additions and 453 deletions

View file

@ -1,6 +1,19 @@
# supermemory-raycast Changelog
# Supermemory Changelog
## [Initial Version] - {2025-09-27}
## [Quick Add from Selection] - 2025-11-05
- Select text anywhere and quickly add it as a memory - the content field is automatically filled
- Select text and search memories instantly - the search field is automatically filled
- Save time by selecting text before opening commands
## [Search Projects + Enhancements] - 2025-10-13
- Added command to search and add projects
- Removed `useEffect`
- Simplified `getApiKey` since `Preferences` will be enforced for presence and trim automatically
- Moved API Key check into its own HoC
## [Initial Version] - 2025-10-02
- Added Supermemory integration with Add Memory and Search Memories commands
- Added project organization support for memories
- Added project organization support for memories

View file

@ -7,8 +7,8 @@
"name": "supermemory",
"license": "MIT",
"dependencies": {
"@raycast/api": "^1.103.2",
"@raycast/utils": "^1.17.0"
"@raycast/api": "^1.103.3",
"@raycast/utils": "^2.2.1"
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
@ -1134,18 +1134,18 @@
}
},
"node_modules/@raycast/api": {
"version": "1.103.2",
"resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.103.2.tgz",
"integrity": "sha512-5QTKMRv86t0cUFH9a07ne8a+ry8bZ2xtOd7trx3yiMmJREr5edYXsqcGDsOyeJLjrjH7WhoRlESIW/CHgNwg8w==",
"version": "1.103.3",
"resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.103.3.tgz",
"integrity": "sha512-+lS8yOrqSP7++ZyomZnMkN0pzEZto/XOqag8MXt9SmTuJBZvfMV74f9wzXmJkRmynYxQHnpLqvs9otWrDd3Bvw==",
"license": "MIT",
"dependencies": {
"@oclif/core": "^4.4.1",
"@oclif/plugin-autocomplete": "^3.2.31",
"@oclif/plugin-help": "^6.2.29",
"@oclif/plugin-not-found": "^3.2.57",
"@oclif/core": "^4.5.4",
"@oclif/plugin-autocomplete": "^3.2.35",
"@oclif/plugin-help": "^6.2.33",
"@oclif/plugin-not-found": "^3.2.68",
"@types/node": "22.13.10",
"@types/react": "19.0.10",
"esbuild": "^0.25.5",
"esbuild": "^0.25.10",
"react": "19.0.0"
},
"bin": {
@ -1204,20 +1204,21 @@
}
},
"node_modules/@raycast/utils": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-1.19.1.tgz",
"integrity": "sha512-/udUGcTZCgZZwzesmjBkqG5naQZTD/ZLHbqRwkWcF+W97vf9tr9raxKyQjKsdZ17OVllw2T3sHBQsVUdEmCm2g==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-2.2.1.tgz",
"integrity": "sha512-MBOD3eccHTu1HVQstoqoJJsA5PubWv18EUq8Dxwg1PEJE+xVjEuslXpsNgR/2RtpTGeKgGiXePxUiVp5mILN5w==",
"license": "MIT",
"dependencies": {
"cross-fetch": "^3.1.6",
"dequal": "^2.0.3",
"object-hash": "^3.0.0",
"signal-exit": "^4.0.2",
"stream-chain": "^2.2.5",
"stream-json": "^1.8.0"
"dequal": "^2.0.3"
},
"peerDependencies": {
"@raycast/api": ">=1.69.0"
"@raycast/api": ">=1.99.4",
"react": ">=19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
},
"node_modules/@types/estree": {
@ -1733,15 +1734,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/cross-fetch": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.7.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2608,35 +2600,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -2910,21 +2873,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -3037,12 +2985,6 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@ -3127,22 +3069,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -5,6 +5,10 @@
"description": "Add and search memories with your personal AI-powered knowledge base",
"icon": "extension-icon.png",
"author": "supermemory",
"contributors": [
"xmok",
"maheshthedev"
],
"platforms": [
"macOS",
"Windows"
@ -18,15 +22,23 @@
{
"name": "add-memory",
"title": "Add Memory",
"subtitle": "add memory to your supermemory app",
"subtitle": "Supermemory",
"description": "Add text, URLs, or documents to your supermemory knowledge base",
"mode": "view"
},
{
"name": "search-memories",
"title": "Search Memories",
"subtitle": "Supermemory",
"description": "Search through your saved memories and find relevant information",
"mode": "view"
},
{
"name": "search-projects",
"title": "Search Projects",
"subtitle": "Supermemory",
"description": "Search through your saved projects and find relevant information",
"mode": "view"
}
],
"preferences": [
@ -40,8 +52,8 @@
}
],
"dependencies": {
"@raycast/api": "^1.103.2",
"@raycast/utils": "^1.17.0"
"@raycast/api": "^1.103.3",
"@raycast/utils": "^2.2.1"
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
@ -59,4 +71,4 @@
"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

@ -1,110 +1,114 @@
import {
Form,
ActionPanel,
Action,
showToast,
Toast,
useNavigation,
} from "@raycast/api";
import { useEffect, useState } from "react";
import {
addMemory,
fetchProjects,
checkApiConnection,
type Project,
} from "./api";
Form,
ActionPanel,
Action,
showToast,
Toast,
useNavigation,
Icon,
getSelectedText,
} from "@raycast/api"
import { useState, useEffect } from "react"
import { addMemory, fetchProjects } from "./api"
import { usePromise } from "@raycast/utils"
import { withSupermemory } from "./withSupermemory"
interface FormValues {
content: string;
project: string;
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();
export default withSupermemory(Command)
function Command() {
const [isSubmitting, setIsSubmitting] = useState(false)
const [initialContent, setInitialContent] = useState("")
const { pop } = useNavigation()
useEffect(() => {
async function loadProjects() {
try {
setIsLoading(true);
const isConnected = await checkApiConnection();
if (!isConnected) {
return;
}
const { isLoading, data: projects = [] } = usePromise(fetchProjects)
const fetchedProjects = await fetchProjects();
setProjects(fetchedProjects);
} catch (error) {
console.error("Failed to load projects:", error);
} finally {
setIsLoading(false);
}
}
useEffect(() => {
async function loadSelectedText() {
try {
const selectedText = await getSelectedText()
if (selectedText) {
setInitialContent(selectedText)
}
} catch {
// No text selected or error getting selected text - silently fail
// User can still manually enter content
}
}
loadProjects();
}, []);
loadSelectedText()
}, [])
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;
}
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);
try {
setIsSubmitting(true)
const containerTags = values.project ? [values.project] : undefined;
const containerTags = values.project ? [values.project] : undefined
await addMemory({
content: values.content.trim(),
containerTags,
});
await addMemory({
content: values.content.trim(),
containerTags,
})
pop();
} catch (error) {
console.error("Failed to add memory:", error);
} finally {
setIsSubmitting(false);
}
}
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>
);
return (
<Form
isLoading={isLoading || isSubmitting}
actions={
!isLoading && (
<ActionPanel>
<Action.SubmitForm
icon={Icon.Plus}
title="Add Memory"
onSubmit={handleSubmit}
/>
</ActionPanel>
)
}
>
<Form.TextArea
id="content"
title="Content"
value={initialContent}
placeholder="Enter the memory content..."
info="The main content of your memory. This is required."
onChange={(value) => setInitialContent(value)}
/>
<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

@ -35,6 +35,10 @@ export interface AddMemoryRequest {
metadata?: Record<string, unknown>;
}
interface AddProjectRequest {
name: string;
}
export interface SearchRequest {
q: string;
containerTags?: string[];
@ -66,28 +70,16 @@ class AuthenticationError extends Error {
}
}
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.");
}
function getApiKey(): string {
const { apiKey } = getPreferenceValues<Preferences>();
return apiKey;
}
async function makeAuthenticatedRequest<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const apiKey = await getApiKey();
const apiKey = getApiKey();
const url = `${API_BASE_URL}${endpoint}`;
@ -159,6 +151,21 @@ export async function fetchProjects(): Promise<Project[]> {
}
}
export async function addProject(request: AddProjectRequest): Promise<Project> {
const response = await makeAuthenticatedRequest<Project>("/v3/projects", {
method: "POST",
body: JSON.stringify(request),
});
await showToast({
style: Toast.Style.Success,
title: "Project Added",
message: "Successfully added project to Supermemory",
});
return response;
}
export async function addMemory(request: AddMemoryRequest): Promise<Memory> {
try {
const response = await makeAuthenticatedRequest<Memory>("/v3/documents", {
@ -209,19 +216,7 @@ export async function searchMemories(
}
// 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;
}
export async function fetchSettings(): Promise<object> {
const response = await makeAuthenticatedRequest<object>("/v3/settings");
return response;
}

View file

@ -1,222 +1,187 @@
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";
ActionPanel,
Detail,
List,
Action,
Icon,
showToast,
Toast,
getSelectedText,
} from "@raycast/api"
import { useState, useEffect } from "react"
import { searchMemories, type SearchResult } from "./api"
import { usePromise } from "@raycast/utils"
import { withSupermemory } from "./withSupermemory"
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";
};
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;
};
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);
const truncateContent = (content: string, maxLength = 100) => {
if (content.length <= maxLength) return content
return `${content.substring(0, maxLength)}...`
}
useEffect(() => {
async function checkConnection() {
const connected = await checkApiConnection();
setIsConnected(connected);
}
checkConnection();
}, []);
export default withSupermemory(Command)
function Command() {
const [searchText, setSearchText] = useState("")
const performSearch = useCallback(
async (query: string) => {
if (!query.trim() || !isConnected) return;
useEffect(() => {
async function loadSelectedText() {
try {
const selectedText = await getSelectedText()
if (selectedText) {
setSearchText(selectedText)
}
} catch {
// No text selected or error getting selected text - silently fail
}
}
try {
setIsLoading(true);
setHasSearched(true);
loadSelectedText()
}, [])
const results = await searchMemories({
q: query.trim(),
limit: 50,
});
const { isLoading, data: searchResults = [] } = usePromise(
async (query: string) => {
const q = query.trim()
if (!q) return []
setSearchResults(results);
const results = await searchMemories({
q,
limit: 50,
})
if (!results.length) {
await showToast({
style: Toast.Style.Success,
title: "Search Complete",
message: "No memories found for your query",
})
}
return results
},
[searchText],
)
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],
);
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"
}
}
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>
);
const hasSearched = !isLoading && !searchResults.length
return (
<List
isLoading={isLoading}
onSearchTextChange={setSearchText}
searchText={searchText}
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 ? (
<List.EmptyView
icon={Icon.Document}
title="No Memories Found"
description={`No memories found for "${searchText}"`}
/>
) : isLoading && searchText.trim() ? (
<List.EmptyView
icon={Icon.MagnifyingGlass}
title="Searching Your Memories"
/>
) : (
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={{ value: truncateContent(content), tooltip: 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.CopyToClipboard
title="Copy Content"
shortcut={{ modifiers: ["cmd"], key: "c" }}
content={content}
/>
{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 content = extractContent(memory)
const url = extractUrl(memory)
const markdown = `
const markdown = `
# ${memory.title || "Untitled Memory"}
${content}
@ -226,28 +191,27 @@ ${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>
}
/>
);
return (
<Detail
markdown={markdown}
actions={
<ActionPanel>
<Action.CopyToClipboard
title="Copy Content"
shortcut={{ modifiers: ["cmd"], key: "c" }}
content={content}
/>
{url && (
<Action.OpenInBrowser
title="Open URL"
url={url}
shortcut={{ modifiers: ["cmd"], key: "o" }}
/>
)}
</ActionPanel>
}
/>
)
}

View file

@ -0,0 +1,106 @@
import {
ActionPanel,
List,
Action,
Icon,
Form,
useNavigation,
} from "@raycast/api";
import { useState } from "react";
import { fetchProjects, addProject } from "./api";
import {
FormValidation,
showFailureToast,
useCachedPromise,
useForm,
} from "@raycast/utils";
import { withSupermemory } from "./withSupermemory";
export default withSupermemory(Command);
function Command() {
const { isLoading, data: projects, mutate } = useCachedPromise(fetchProjects);
return (
<List isLoading={isLoading} searchBarPlaceholder="Search your projects">
{!isLoading && !projects?.length ? (
<List.EmptyView
title="No Projects Found"
actions={
<ActionPanel>
<Action.Push
icon={Icon.Plus}
title="Create Project"
target={<CreateProject />}
onPop={mutate}
/>
</ActionPanel>
}
/>
) : (
projects?.map((project) => (
<List.Item
key={project.id}
icon={Icon.Folder}
title={project.name}
subtitle={project.description}
accessories={[{ tag: project.containerTag }]}
actions={
<ActionPanel>
<Action.Push
icon={Icon.Plus}
title="Create Project"
target={<CreateProject />}
onPop={mutate}
/>
</ActionPanel>
}
/>
))
)}
</List>
);
}
function CreateProject() {
const { pop } = useNavigation();
const [isLoading, setIsLoading] = useState(false);
const { handleSubmit, itemProps } = useForm<{ name: string }>({
async onSubmit(values) {
setIsLoading(true);
try {
await addProject(values);
pop();
} catch (error) {
await showFailureToast(error, { title: "Failed to add project" });
} finally {
setIsLoading(false);
}
},
validation: {
name: FormValidation.Required,
},
});
return (
<Form
navigationTitle="Search Projects / Add"
isLoading={isLoading}
actions={
<ActionPanel>
<Action.SubmitForm
icon={Icon.Plus}
title="Create Project"
onSubmit={handleSubmit}
/>
</ActionPanel>
}
>
<Form.TextField
title="Name"
placeholder="My Awesome Project"
info="This will help you organize your memories"
{...itemProps.name}
/>
</Form>
);
}

View file

@ -0,0 +1,48 @@
import { usePromise } from "@raycast/utils";
import { fetchSettings } from "./api";
import {
Action,
ActionPanel,
Detail,
Icon,
List,
openExtensionPreferences,
} from "@raycast/api";
import { ComponentType } from "react";
export function withSupermemory<P extends object>(Component: ComponentType<P>) {
return function SupermemoryWrappedComponent(props: P) {
const { isLoading, data } = usePromise(fetchSettings, [], {
failureToastOptions: {
title: "Invalid API Key",
message:
"Invalid API key. Please check your API key in preferences. Get a new one from https://supermemory.link/raycast",
},
});
if (!data) {
return isLoading ? (
<Detail isLoading />
) : (
<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 <Component {...props} />;
};
}