aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/raycast-extension/CHANGELOG.md19
-rw-r--r--apps/raycast-extension/package-lock.json116
-rw-r--r--apps/raycast-extension/package.json20
-rw-r--r--apps/raycast-extension/src/add-memory.tsx194
-rw-r--r--apps/raycast-extension/src/api.ts57
-rw-r--r--apps/raycast-extension/src/search-memories.tsx432
-rw-r--r--apps/raycast-extension/src/search-projects.tsx106
-rw-r--r--apps/raycast-extension/src/withSupermemory.tsx48
8 files changed, 530 insertions, 462 deletions
diff --git a/apps/raycast-extension/CHANGELOG.md b/apps/raycast-extension/CHANGELOG.md
index 06b6fe0e..f8bdf596 100644
--- a/apps/raycast-extension/CHANGELOG.md
+++ b/apps/raycast-extension/CHANGELOG.md
@@ -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 \ No newline at end of file
+- Added project organization support for memories
diff --git a/apps/raycast-extension/package-lock.json b/apps/raycast-extension/package-lock.json
index 703ef839..323af290 100644
--- a/apps/raycast-extension/package-lock.json
+++ b/apps/raycast-extension/package-lock.json
@@ -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",
diff --git a/apps/raycast-extension/package.json b/apps/raycast-extension/package.json
index 9481601b..ed821577 100644
--- a/apps/raycast-extension/package.json
+++ b/apps/raycast-extension/package.json
@@ -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"
}
-} \ No newline at end of file
+}
diff --git a/apps/raycast-extension/src/add-memory.tsx b/apps/raycast-extension/src/add-memory.tsx
index 87fb853f..812c1c4a 100644
--- a/apps/raycast-extension/src/add-memory.tsx
+++ b/apps/raycast-extension/src/add-memory.tsx
@@ -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>
+ )
}
diff --git a/apps/raycast-extension/src/api.ts b/apps/raycast-extension/src/api.ts
index 0cb2ec20..55672a77 100644
--- a/apps/raycast-extension/src/api.ts
+++ b/apps/raycast-extension/src/api.ts
@@ -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;
}
diff --git a/apps/raycast-extension/src/search-memories.tsx b/apps/raycast-extension/src/search-memories.tsx
index 850fbe3c..dbc47ceb 100644
--- a/apps/raycast-extension/src/search-memories.tsx
+++ b/apps/raycast-extension/src/search-memories.tsx
@@ -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;
-};
-
-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>
- );
+ if (memory.metadata?.url && typeof memory.metadata.url === "string") {
+ return memory.metadata.url
+ }
+ return null
+}
+
+const truncateContent = (content: string, maxLength = 100) => {
+ if (content.length <= maxLength) return content
+ return `${content.substring(0, maxLength)}...`
+}
+
+export default withSupermemory(Command)
+function Command() {
+ const [searchText, setSearchText] = useState("")
+
+ useEffect(() => {
+ async function loadSelectedText() {
+ try {
+ const selectedText = await getSelectedText()
+ if (selectedText) {
+ setSearchText(selectedText)
+ }
+ } catch {
+ // No text selected or error getting selected text - silently fail
+ }
+ }
+
+ loadSelectedText()
+ }, [])
+
+ const { isLoading, data: searchResults = [] } = usePromise(
+ async (query: string) => {
+ const q = query.trim()
+ if (!q) return []
+
+ 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],
+ )
+
+ 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 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>
+ }
+ />
+ )
}
diff --git a/apps/raycast-extension/src/search-projects.tsx b/apps/raycast-extension/src/search-projects.tsx
new file mode 100644
index 00000000..bacd8537
--- /dev/null
+++ b/apps/raycast-extension/src/search-projects.tsx
@@ -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>
+ );
+}
diff --git a/apps/raycast-extension/src/withSupermemory.tsx b/apps/raycast-extension/src/withSupermemory.tsx
new file mode 100644
index 00000000..13c1d7b8
--- /dev/null
+++ b/apps/raycast-extension/src/withSupermemory.tsx
@@ -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} />;
+ };
+}