curl -fsSL https://claude.ai/install.sh | bash, then claude. That's the native installer the setup docs recommend. It is not on the Free plan.@reference.jpg.Here's a reference image of the dashboard vibe I want (attached). Build it as ONE self-contained dashboard.html file (Canvas plus CSS, no external libraries). Near-black navy background, a glowing central core with orbital rings and rotating dashed HUD arcs, and a heads-up layout: top-left a JARVIS title, subtitle and today's date; top-right a live ticking clock and a connector status list (green dot online, amber blinking offline); a streaming activity log down the left that reveals one line at a time; a stats panel on the right with thin glowing bars; a calendar and a weather panel; big figures across the bottom; and a bottom bar with a glowing "BRIEF ME" button.
Read ALL the numbers and text from a separate jarvis_data.js that sets window.JARVIS_DATA, so I only edit that file day to day. Save brief.mp3 alongside dashboard.html, add a hidden audio element for it, and make clicking BRIEF ME play it and pulse the core while it speaks.
Make it look expensive and responsive. Then take a screenshot of what you built, compare it to my reference image, list the differences, and fix them. Repeat until it matches.
jarvis_data.js. It's beautiful and it knows nothing. Layer two is what makes the numbers real.This single file is the difference between your Jarvis and a generic bot. It's why yours knows a slipped deadline on project two matters more than a full inbox. In your project folder, ask Claude Code to create a CLAUDE.md, then make it specific. Make a priorities note while you're here, even five ranked bullets, because the brief ranks your day from it and can't invent one.
# Who I am
I'm [name], I [what you do]. I care most about [your #1 goal right now].
# What I'm working on this month
- [project 1], [project 2], [project 3]
# My priorities, ranked
1. [top priority] 2. [next] 3. [next]
# My tone
[e.g. warm, direct, no corporate filler]
# Hard rules
- Always draft, never send. Never take irreversible actions without asking.
- Only use the apps I've connected.
Don't read this one, just copy the whole block. It's the architecture behind my Jarvis, and it saves itself as a skill you can trigger by phrase.
You are my personal assistant, my own Jarvis. First, read my CLAUDE.md so you know who I am, my projects, my priorities and my tone. From now on, act as a proactive chief of staff for me, not a generic chatbot.
Keep this skill fully self-contained: list every source to pull and exactly how to format the brief, because when it runs on a schedule it has no memory of our chats.
Build me a "morning brief" routine, and save it as a reusable skill I can trigger by phrase (like "run my morning brief", "brief me", or "what's on my plate"). When I trigger it, do this:
1. CALENDAR: read today's events from my Google Calendar. Flag anything that needs prep, and any conflicts or gaps.
2. INBOX: scan my Gmail from the last 24 hours. Group it into needs-a-reply / FYI / ignore. For the needs-a-reply group, draft short replies in my tone and leave them in Drafts. Never send.
3. PRIORITIES + TASKS: read my priorities and task list (from my Notion or my priorities note), and pick the 3 things that actually matter most today, ranked, with a one-line why for each.
4. ONE THING TO KNOW: surface a single important signal, a deadline, a number that moved, a follow-up I'm forgetting.
Then write it as 6 to 8 short spoken lines, in my voice, warm and direct, no filler. END with the single most important question: "What should I handle first?"
Write the brief in [your language]. Save it to brief.txt and to a Notion page. Then rewrite jarvis_data.js so the dashboard shows today's real numbers and a one-line activity log per source you read, and mark each connector online or offline based on whether the call actually succeeded. Then read the brief out loud through my Fish Audio voice.
If a source is empty or a connector is down, say so plainly in the brief and mark it offline on the dashboard. Never fill a gap with a plausible guess.
Rules, always: you draft, I approve. Never send email, post, delete, or take any irreversible action on your own. Only pull from the apps I've connected.
So you're not pasting blind, this is the shape of what comes back:
The skill saves itself to ~/.claude/skills/<name>/SKILL.md. Ask Claude Code for more the same way, and it reaches for them automatically when a request matches:
A connector is an authorised link between the brain and a service you already use. It's the difference between Claude talking about your calendar and Claude reading it. Nobody can do this step for you. The prompt above is portable; your authorisations are yours, and until you grant them your beautiful dashboard is a screensaver.
/mcp to see them. Ones you've never used hide behind a "Show unused connectors" row.claude mcp add will fail with "is Anthropic-hosted and doesn't support local OAuth". Connect those on the web and let the CLI inherit them. claude mcp add --transport http NAME https://the-mcp-url/mcp then claude mcp login NAME is for third-party servers that publish their own MCP URL.ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN or apiKeyHelper is active, when a third-party provider like Bedrock is in play, or when CLAUDE_CODE_OAUTH_TOKEN holds a long-lived token. Run /status to see which auth is actually live. An ANTHROPIC_API_KEY left in your shell profile from an old experiment will hide every connector you own, with no error.What each one can and cannot do. Worth knowing before you design a routine around it:
~/.claude/mcp-needs-auth-cache.json, keyed by connector display name, and a successful web re-login doesn't always clear the entry. Back the file up, remove that connector's key (or delete the file, it rebuilds), then fully quit and reopen Claude Code, Cmd+Q rather than closing the window. This is internal client state, not a documented API, so re-check the filename if it moves. The durable lesson is the symptom: when the web says connected and the client says needs auth, suspect a stale cache before you suspect your account.This is what makes it feel like Jarvis. Sign up at Fish Audio, then:
fish.audio/m/<id>. That's what tells the API which voice to speak in..env in your project with FISH_API_KEY=your-key, and add .env to your .gitignore. This keeps the key out of your code so it can't leak when you share the folder.[calm] Your 3pm is confirmed. [excited] The launch numbers are in.Write me a speak script that turns text into audio and plays it, with three tiers so it never goes silent:
1. Fish Audio. POST https://api.fish.audio/v1/tts, model sent as an HTTP header (model: s2.1-pro-free), reference_id and text in the body, API key read from FISH_API_KEY in my .env file. Generate the speech in [your language].
2. If that fails, fall back to edge-tts with the voice en-GB-RyanNeural at --rate=-4%.
3. If that fails, fall back to the built-in system voice (say -v Daniel on macOS).
Two things that matter: after the API call, check the response is ACTUALLY audio before playing it (check the bytes with `file`, not just the HTTP status), and check the exit code and that the output file exists before moving on, so a failure falls through to the next tier instead of silently producing nothing.
Print which tier spoke, like "voice: fish-audio" or "voice: edge-tts fallback", so I can see when my main voice quietly stopped working.
Save the audio as brief.mp3 in the same folder as dashboard.html so the BRIEF ME button finds it.
python3 is not one program. Homebrew's /opt/homebrew/bin/python3 usually comes first on your PATH and may not have edge_tts installed, while /usr/bin/python3 does. So a script calling bare python3 -m edge_tts fails in the script and works when you type it in the terminal. If the call is wrapped without a return-code check, it fails completely silently: no audio, no error, no clue. Find the right interpreter with for p in $(which -a python3); do echo -n "$p: "; $p -c "import edge_tts; print('ok')" 2>&1 | head -1; done, then hardcode that absolute path, or make a dedicated virtualenv and remove the ambiguity permanently.s2.1-pro-free is $0.00 per million UTF-8 bytes under Fish's fair-use policy, with no hard character cap. If that changes, the paid rate is $15 per million UTF-8 bytes, roughly 180,000 English words or 12 hours of speech, and there are no subscription fees or monthly minimums for API access. Four things worth knowing: a typo in the model header silently falls back to the paid s2.1-pro, so check that string; free-tier requests may be used to improve model quality; if your product is over $1M ARR Fish asks you to contact them first; and because billing is per UTF-8 byte, a Chinese character costs about three times an English one. It speaks 83 languages, so your Jarvis can brief you in any of them. Voice Design itself costs $0.01 per request and the web app gates it behind the Plus tier.Right now you have a front end reading a static file, and a skill that can write that file. Connecting them is one instruction, and then one decision about how you open the page.
Every time my morning brief skill runs, have it rewrite jarvis_data.js from what it actually read: today's date, the event count and next event, the unread and needs-a-reply counts, my top 3 priorities, and one activity-log line per source. Set each connector's status to online only if that call actually returned data, otherwise offline. Never write a number the run didn't actually retrieve. Then serve the folder at http://localhost:8787 with a tiny local server, and give me a one-line script that starts the server and opens the page.
file:// page, fetch('/ask') resolves to file:///ask, which isn't a route, it's a path on disk that doesn't exist. You can't fix it with an absolute URL either, because file: URLs get an opaque origin, and the browser sends Origin: null or refuses the request outright. No header on your server can fix that. Common myth worth ignoring: people say the mic fails because file:// isn't a secure context. Not true, the spec explicitly treats file: as potentially trustworthy. The fetch is the blocker, not the mic. And http://localhost is treated as secure, so everything works there.play() without a user gesture, and a blocked call rejects rather than throwing. Your BRIEF ME button satisfies the gesture, which is why it works. Handle the rejection anyway, because a scheduled brief has no gesture behind it. Check the server is alive with curl -s -o /dev/null -w "%{http_code}" http://localhost:8787/, expect 200.The choice hinges on one question: does your brief need local files, or does it need to speak?
/schedule daily brief at 9am, or set it at claude.ai/code/routines. It gets a fresh clone with no access to your local files and it cannot play audio, so the brief lands as text in Notion. Runs on Pro and up, with a daily cap per account.~/Scripts/. And read the first week of runs before you trust any of it.Your Jarvis will not be right on day one, and that's the point rather than a problem. The whole thing is built in plain English, so you fix it by telling Claude Code what's wrong:
/status for a stray API key. Voice silent with no error: the python3 path problem, or a response that wasn't actually audio. Dashboard numbers frozen: the skill isn't rewriting jarvis_data.js, or you're looking at a cached page. No sound in the browser: autoplay needs a click.the waitlist is now open
A 30-day cohort to build real AI systems with me, step by step. Join the waitlist and you hear first, and lock the founding price.