mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-19 21:44:02 +00:00
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:
parent
1b9b3012e3
commit
c4e67d8707
8 changed files with 521 additions and 453 deletions
|
|
@ -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
|
||||
|
|
|
|||
116
apps/raycast-extension/package-lock.json
generated
116
apps/raycast-extension/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
106
apps/raycast-extension/src/search-projects.tsx
Normal file
106
apps/raycast-extension/src/search-projects.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
apps/raycast-extension/src/withSupermemory.tsx
Normal file
48
apps/raycast-extension/src/withSupermemory.tsx
Normal 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} />;
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue