Get Your Agent to Help
Install the Raycast extension skill:
npx skills add johnlindquist/claude@raycast-extension
Then ask: "help me build a Raycast extension that shows my launchd job status"
Create the Extension
In Raycast, run "Create Extension" → pick "List" template. Or from the terminal:
mkdir -p ~/Code/jobs-raycast && cd ~/Code/jobs-raycast
npm init raycast-extension -- --name "AI Jobs" --type list
npm install
The Command
Replace src/index.tsx. The key: useExec calls bun run cli.ts status and parses the JSON:
import { List, ActionPanel, Action, showToast, Toast, Icon, Color } from "@raycast/api";
import { useExec } from "@raycast/utils";
// IMPORTANT: absolute paths — Raycast has minimal PATH
const BUN = `${process.env.HOME}/.bun/bin/bun`;
const CLI = "/path/to/james-long-ai-job-scheduling/src/cli.ts"; // ← update this
export default function Command() {
const { data, isLoading, revalidate } = useExec(BUN, ["run", CLI, "status"], {
parseOutput: ({ stdout }) => JSON.parse(stdout).result.jobs,
});
return (
<List isLoading={isLoading} searchBarPlaceholder="Filter jobs...">
{data?.map((job: any) => (
<List.Item
key={job.label}
title={job.name}
subtitle={job.schedule}
icon={{
source: job.loaded ? Icon.CheckCircle : Icon.Circle,
tintColor: !job.loaded ? Color.Yellow
: job.exitCode !== "0" && job.exitCode !== "-" ? Color.Red
: Color.Green,
}}
accessories={[{ text: job.scope }, { text: job.loaded ? "loaded" : "unloaded" }]}
actions={
<ActionPanel>
<Action title="Kick (Run Now)" icon={Icon.Play}
onAction={async () => {
Bun.spawnSync([BUN, "run", CLI, "kick", job.name]);
showToast({ style: Toast.Style.Success, title: `Kicked ${job.name}` });
revalidate();
}}
/>
<Action title="Sync All" icon={Icon.ArrowClockwise}
onAction={async () => {
Bun.spawnSync([BUN, "run", CLI, "sync"]);
showToast({ style: Toast.Style.Success, title: "Synced" });
revalidate();
}}
/>
</ActionPanel>
}
/>
))}
</List>
);
}
Develop
npm run dev
# Extension appears in Raycast with hot reload
Why Raycast
- ⌘+Space → "AI Jobs" → see everything instantly
- ActionPanel gives each job contextual actions (kick, sync, view logs)
useExec+ JSON CLI = no server needed- It's a native macOS experience, not a browser tab
Companion Notes
Branch: Raycast Extension
A Raycast command that shows job status, logs, and lets you kick/sync jobs from ⌘+Space. The slickest option if you already use Raycast.
Setup
# Install Raycast CLI tools
npm install -g @raycast/api
# Create the extension
cd ~/Code
npx create-raycast-extension --name ai-job-scheduler --type list
cd ai-job-scheduler
The Main Command
Replace src/index.tsx:
import { List, ActionPanel, Action, showToast, Toast, Icon, Color } from "@raycast/api";
import { useExec } from "@raycast/utils";
import { useState } from "react";
// Path to the CLI — adjust to your project
const CLI = "/path/to/james-long-ai-job-scheduling/src/cli.ts";
const BUN = "/Users/YOU/.bun/bin/bun"; // absolute path — Raycast has minimal PATH
interface Job {
name: string;
label: string;
scope: string;
schedule: string;
loaded: boolean;
pid: string;
exitCode: string;
disabled: boolean;
}
function statusIcon(job: Job) {
if (job.disabled) return { source: Icon.Circle, tintColor: Color.SecondaryText };
if (!job.loaded) return { source: Icon.Circle, tintColor: Color.Yellow };
if (job.exitCode !== "0" && job.exitCode !== "-") return { source: Icon.ExclamationMark, tintColor: Color.Red };
if (job.pid !== "-") return { source: Icon.CircleProgress, tintColor: Color.Green };
return { source: Icon.CheckCircle, tintColor: Color.Green };
}
function statusText(job: Job): string {
if (job.disabled) return "Disabled";
if (!job.loaded) return "Unloaded";
if (job.exitCode !== "0" && job.exitCode !== "-") return `Error (exit ${job.exitCode})`;
if (job.pid !== "-") return `Running (PID ${job.pid})`;
return "Idle";
}
export default function Command() {
const { data, isLoading, revalidate } = useExec(BUN, ["run", CLI, "status"], {
parseOutput: ({ stdout }) => {
const parsed = JSON.parse(stdout);
return parsed.result.jobs as Job[];
},
});
return (
<List isLoading={isLoading} searchBarPlaceholder="Filter jobs...">
{data?.map((job) => (
<List.Item
key={job.label}
title={job.name}
subtitle={job.schedule}
icon={statusIcon(job)}
accessories={[
{ text: job.scope },
{ text: statusText(job) },
]}
actions={
<ActionPanel>
<Action
title="View Logs"
icon={Icon.Terminal}
onAction={async () => {
// Open logs in a detail view — see LogDetail below
}}
/>
<Action
title="Kick (Run Now)"
icon={Icon.Play}
shortcut={{ modifiers: ["cmd"], key: "k" }}
onAction={async () => {
const proc = Bun.spawnSync([BUN, "run", CLI, "kick", job.name]);
const result = JSON.parse(new TextDecoder().decode(proc.stdout));
if (result.ok) {
showToast({ style: Toast.Style.Success, title: `Kicked ${job.name}` });
} else {
showToast({ style: Toast.Style.Failure, title: result.error.message });
}
revalidate();
}}
/>
<Action
title="Sync All Jobs"
icon={Icon.ArrowClockwise}
shortcut={{ modifiers: ["cmd"], key: "s" }}
onAction={async () => {
Bun.spawnSync([BUN, "run", CLI, "sync"]);
showToast({ style: Toast.Style.Success, title: "Synced" });
revalidate();
}}
/>
<Action
title="View Plist"
icon={Icon.Document}
onAction={async () => {
// Show plist XML in a detail view
}}
/>
<Action
title="Run Doctor"
icon={Icon.Stethoscope}
shortcut={{ modifiers: ["cmd"], key: "d" }}
onAction={async () => {
const proc = Bun.spawnSync([BUN, "run", CLI, "doctor", job.name]);
const result = JSON.parse(new TextDecoder().decode(proc.stdout));
const issues = result.result.checks[0]?.issues || [];
if (issues.length === 0) {
showToast({ style: Toast.Style.Success, title: "Healthy" });
} else {
showToast({
style: Toast.Style.Failure,
title: `${issues.length} issues`,
message: issues[0].message,
});
}
}}
/>
</ActionPanel>
}
/>
))}
</List>
);
}
Key Raycast Patterns
useExeccalls the CLI and parses JSON — the agent-first output pays offrevalidaterefreshes after mutations (kick, sync)ActionPanelgives each job contextual actions- Absolute paths — Raycast has minimal PATH, use full paths to
bunand the CLI
Development
cd ai-job-scheduler
npm run dev
# Opens in Raycast dev mode — hot reload
What to Add
- Log detail view —
Action.Pushto aDetailcomponent showingjobs logs <name>output - Plist detail — show the generated XML in a
Detailwith markdown code block - Doctor summary — a separate command showing overall health
- Menu bar command — persistent icon showing job count + health