raycast-extension

from johnlindquist/claude

No description

12 stars2 forksUpdated Dec 19, 2025
npx skills add https://github.com/johnlindquist/claude --skill raycast-extension

SKILL.md

Raycast Extension Development

Quick Start

  1. Create project structure
  2. Write package.json with extension config
  3. Implement command in src/
  4. Run npm install && npm run dev

Project Structure

my-extension/
├── package.json          # Extension manifest + dependencies
├── tsconfig.json         # TypeScript config
├── .eslintrc.json        # ESLint config
├── raycast-env.d.ts      # Type definitions (auto-generated)
├── assets/
│   └── extension-icon.png  # 512x512 PNG icon
└── src/
    └── command-name.tsx    # Command implementation

package.json Template

{
  "name": "extension-name",
  "title": "Extension Title",
  "description": "What this extension does",
  "icon": "extension-icon.png",
  "author": "author-name",
  "categories": ["Productivity", "Developer Tools"],
  "license": "MIT",
  "commands": [
    {
      "name": "command-name",
      "title": "Command Title",
      "description": "What this command does",
      "mode": "view",
      "keywords": ["keyword1", "keyword2"]
    }
  ],
  "dependencies": {
    "@raycast/api": "^1.83.1",
    "@raycast/utils": "^1.17.0"
  },
  "devDependencies": {
    "@raycast/eslint-config": "^1.0.11",
    "@types/node": "22.5.4",
    "@types/react": "18.3.3",
    "eslint": "^8.57.0",
    "prettier": "^3.3.3",
    "typescript": "^5.5.4"
  },
  "scripts": {
    "build": "ray build --skip-types -e dist -o dist",
    "dev": "ray develop",
    "fix-lint": "ray lint --fix",
    "lint": "ray lint"
  }
}

Command Modes

ModeUse Case
viewShow UI with Detail, List, Form, Grid
no-viewBackground task, clipboard, notifications only
menu-barMenu bar icon with dropdown

Hotkey Configuration

Add to command in package.json:

"hotkey": {
  "modifiers": ["opt"],
  "key": "m"
}

Modifiers: cmd, opt, ctrl, shift

Note: Hotkeys in package.json are suggestions. Users set them in Raycast Preferences → Extensions.

tsconfig.json

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["ES2022"],
    "module": "ES2022",
    "moduleResolution": "bundler",
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022"
  },
  "include": ["src/**/*", "raycast-env.d.ts"]
}

.eslintrc.json

{
  "root": true,
  "extends": ["@raycast"]
}

Command Patterns

No-View Command (Background Task)

import { showHUD, Clipboard, showToast, Toast } from "@raycast/api";

export default async function Command() {
  const toast = await showToast({
    style: Toast.Style.Animated,
    title: "Working...",
  });

  try {
    // Do work
    const result = await doSomething();

    await Clipboard.copy(result);
    await showHUD("✅ Done!");
  } catch (error) {
    toast.style = Toast.Style.Failure;
    toast.title = "Failed";
    toast.message = error instanceof Error ? error.message : "Unknown error";
  }
}

View Command (List)

import { List, ActionPanel, Action } from "@raycast/api";

export default function Command() {
  return (
    <List>
      <List.Item
        title="Item"
        actions={
          <ActionPanel>
            <Action.CopyToClipboard content="text" />
          </ActionPanel>
        }
      />
    </List>
  );
}

View Command (Detail)

import { Detail } from "@raycast/api";

export default function Command() {
  const markdown = `# Hello World`;
  return <Detail markdown={markdown} />;
}

Performance & Caching

Instant Load Pattern (No Empty Flash)

Use synchronous cache read + async refresh for instant perceived load:

import { List, Cache } from "@raycast/api";
import { useCachedPromise, withCache } from "@raycast/utils";

const cache = new Cache();
const CACHE_KEY = "myData";

// Read cache synchronously at module load (before React renders)
function getInitialData(): MyData[] {
  const cached = cache.get(CACHE_KEY);
  if (cached) {
    try {
      return JSON.parse(cached);
    } catch {
      return [];
    }
  }
  return [];
}

// Expensive async operation wrapped with withCache (5 min TTL)
const fetchExpensiveData = withCache(
  async () => {
    // Your expensive operation here
    return await someSlowOperation();
  },
  { maxAge: 5 * 60 * 1000 }
);

async function fetchAllData(): Promise<MyData[]> {
  const data = await fetchExpensiveData();
  // Update cache for next launch
  cache.set(CACHE_KEY, JSON.stringify(data));
  return data;
}

export default function Command() {
  const { data, isLoading } = useCachedPromise(fetchAllData, [], {
    initialData: getInitialData(), // Sync read - instant render!
    keepPreviousData: true,
  });

  return (
    <List isLoading={isLoading && !data?.lengt

...
Read full content

Repository Stats

Stars12
Forks2