PluginBench
Skill
Fail
Audit score 45

contacts

joelhooks/joelclaw

Add, enrich, and manage contacts in Joel's Vault with multi-source enrichment or quick manual creation.

What is contacts?

Manage a personal contact directory stored as markdown files in ~/Vault/Contacts/. Each contact includes structured metadata (role, organizations, social handles) and enriched context. Use this skill to add new contacts, trigger deep enrichment via Inngest (pulling from Slack, GitHub, Twitter, web search, and Roam archives), update existing contacts, or query contact information.

  • Create and store contacts as markdown files with YAML frontmatter containing name, role, organizations, social handles, and tags
  • Fire an Inngest enrichment pipeline that synthesizes data from Slack, GitHub, Twitter, web search, Roam archives, and LLM to build rich contact dossiers
  • Mark contacts as VIP for deep enrichment and ongoing monitoring across multiple channels
  • Manually create quick contacts without enrichment for simple use cases
  • Update existing contacts by re-running enrichment, which merges new data while preserving existing facts
  • Search and resolve unknown Slack user IDs to contact profiles

How to install contacts

npx skills add https://github.com/joelhooks/joelclaw --skill contacts
Prerequisites
  • A ~/Vault/Contacts/ directory (markdown files with YAML frontmatter)
  • Inngest running locally (port 8288) for enrichment pipeline (optional; manual creation works without it)
  • INNGEST_EVENT_KEY environment variable set for firing enrichment events
  • Access to Slack API token, GitHub API, Twitter/X API v2, and web search for full enrichment (optional hints)
  • Roam Research export (EDN file) at ~/Code/joelhooks/egghead-roam-research/ for Roam-based enrichment (optional)
Claude Code
Cursor
Windsurf
Cline

How to use contacts

  1. 1.Install the skill via npx skills add https://github.com/joelhooks/joelclaw --skill contacts
  2. 2.For enrichment: gather optional hints (Slack ID, GitHub username, Twitter handle, email, website) about the person
  3. 3.Fire the enrichment pipeline by sending a POST request to http://localhost:8288/e/$INNGEST_EVENT_KEY with contact/enrich.requested event (choose 'full' depth for ~60s/$0.05 or 'quick' depth for ~10s/$0.01)
  4. 4.For manual creation: write a markdown file to ~/Vault/Contacts/<Name>.md with frontmatter and sections (Contact Channels, Projects, Key Context, Recent Activity)
  5. 5.Add the contact to ~/Vault/Contacts/index.md as a wikilink
  6. 6.To update: re-run enrichment with the existing vault_path; the synthesizer merges new data with existing content
  7. 7.For VIP contacts: set vip: true in frontmatter and follow the deep enrichment playbook (web presence, interviews, collaborations, career timeline, GitHub, Twitter, relationships, content catalog, audience reach)

Use cases

Good for
  • Onboarding a new collaborator: enrich their profile with full background, career timeline, and key relationships
  • Tracking VIP contacts: mark important people for deep enrichment and set up ongoing monitoring via Google Alerts, Twitter, GitHub, and RSS feeds
  • Quick reference during meetings: look up a contact's role, recent activity, and shared projects
  • Maintaining institutional knowledge: store relationship notes, working style, and collaboration history for team members and partners
  • Building a media appearances index: extract podcast appearances and content from VIP profiles and index them for discovery
Who it's for
  • Knowledge workers and creators managing professional networks and collaborations
  • Team leads tracking key relationships and institutional knowledge
  • Podcast hosts or content creators maintaining guest and collaborator databases
  • Anyone using a personal vault system (Roam, Obsidian, or similar) for knowledge management

contacts FAQ

What's the difference between 'full' and 'quick' enrichment depth?

'Full' depth (~60s, ~$0.05) queries all 7 sources (Slack, Roam, web, GitHub, Granola, Brain-backed recall, LLM synthesis) and is best for new contacts or periodic refresh. 'Quick' depth (~10s, ~$0.01) uses only Slack and memory, good for real-time VIP detection.

Do I need to provide hints for enrichment to work?

No, hints are optional. Any known identifiers (Slack ID, GitHub, email, Twitter, website) seed the search and improve results, but enrichment will attempt discovery without them.

How does enrichment handle existing contact data?

The synthesizer merges new enrichment data with existing vault content—it won't discard existing facts unless contradicted by new information.

What is the VIP contact deep enrichment playbook?

VIPs receive a structured 9-step enrichment covering web presence, podcast/interviews, Joel collaborations, career timeline, GitHub profile, Twitter profile, key relationships, content catalog, and audience reach. Results are indexed to Typesense and monitored ongoing via Google Alerts, Twitter, GitHub, and RSS.

Can I search the Roam Research archive for contact information?

Yes. Use Python regex to search the Roam EDN export file at ~/Code/joelhooks/egghead-roam-research/ by block content or page title. People are tagged with prefixes like [[collaborator/Name]], [[client/Name]], or [[staff/Name]].

Full instructions (SKILL.md)

Source of truth, from joelhooks/joelclaw.


name: contacts displayName: Contacts description: "Add, enrich, and manage contacts in Joel's Vault. Fire the Inngest enrichment pipeline for full multi-source dossiers, or create quick contacts manually. Use when: 'add a contact', 'enrich this person', 'who is X', 'VIP contact', 'update contact', or any task involving the Vault/Contacts directory." version: 1.0.0 author: joel tags: [joelclaw, contacts, vault, enrichment, people] disable-model-invocation: true

Contacts

Manage contacts in ~/Vault/Contacts/. Each contact is a markdown file with YAML frontmatter.

Contact File Location

~/Vault/Contacts/<Name>.md

Index file: ~/Vault/Contacts/index.md — wikilink list of all contacts.

Frontmatter Schema

---
name: Full Name
aliases: [nickname, handle]
role: Current Role / Title
organizations: [Org1, Org2]
vip: true  # or false
slack_user_id: U0XXXXXXX
slack_dm_channel: D0XXXXXXX  # null if unknown
website: https://example.com
github: username
twitter: handle
email: user@example.com
tags: [vip, instructor, creator, family, employee]
---

Sections

# Name

## Contact Channels
- Slack, email, social handles, website

## Projects
- Active projects, courses, collaborations

## Key Context
- Relationship notes, working style, history

## Recent Activity
- YYYY-MM-DD | channel | summary

See ~/Vault/Contacts/Matt Pocock.md for a fully enriched example.

Adding a Contact

Option 1: Fire the Enrichment Pipeline (preferred)

Send an Inngest event. The contact-enrich function fans out across Slack, Roam, web/GitHub, Granola, Brain-backed recall, and other indexed sources, synthesizes with LLM, and writes the Vault file.

# Via curl (CLI has OTEL import bug under Bun v1.3.9)
curl -s -X POST "http://localhost:8288/e/$INNGEST_EVENT_KEY" \
  -H "Content-Type: application/json" \
  -d '[{
    "name": "contact/enrich.requested",
    "data": {
      "name": "Person Name",
      "depth": "full",
      "hints": {
        "slack_user_id": "U0XXXXXXX",
        "github": "username",
        "twitter": "handle",
        "email": "user@example.com",
        "website": "https://example.com"
      }
    },
    "ts": EPOCH_MS
  }]'

Depth modes:

  • full (~60s, ~$0.05): All 7 sources + LLM synthesis. Use for new contacts or periodic refresh.
  • quick (~10s, ~$0.01): Slack + memory only. Good for real-time VIP detection.

Hints are optional but help: Any known identifiers (Slack ID, GitHub, email, Twitter, website) seed the search and improve results.

Option 2: Quick Manual Create

For simple contacts where enrichment is overkill:

---
name: Person Name
aliases: []
role: Role
organizations: [Org]
vip: false
slack_user_id: null
website: null
github: null
twitter: null
email: null
tags: [tag1]
---

# Person Name

## Contact Channels
- ...

## Key Context
- ...

Write to ~/Vault/Contacts/Person Name.md and add [[Person Name]] to index.md.

Updating Contacts

Re-run enrichment with the existing vault path:

{
  "name": "contact/enrich.requested",
  "data": {
    "name": "Person Name",
    "vault_path": "Contacts/Person Name.md",
    "depth": "full"
  }
}

The synthesizer merges new data with existing content — it won't discard existing facts unless contradicted.

VIP Contacts (ADR-0151)

Mark vip: true in frontmatter. VIPs get deep enrichment + ongoing monitoring.

Deep Enrichment Playbook (one-time)

Every VIP gets the full treatment. This is what we did for Kent C. Dodds (Feb 26, 2026):

StepSourceWhat to Capture
1. Web presenceWeb search {name} + {org}Bio, role, location, personal details
2. Podcast/interviewsWeb search {name} podcast interviewAppearance list, own podcasts, audiences
3. Joel collaborationsTheir website, appearances pagesJoint podcasts, co-organized events, shared projects
4. Career timelineDefuddle 2-3 key interview transcriptsOrigin story, career arc, key decisions, values
5. GitHub profileGitHub API or webRepos, followers, orgs, contribution patterns
6. X/Twitter profileX API v2 (use x-api skill)Bio, followers, recent tweets, engagement
7. Key relationshipsCross-reference transcripts + contactsWho they work with, who they mention, who we know in common
8. Content catalogWebsite crawl (defuddle)Courses, blog posts, open source projects
9. Audience reachPodcast counts, social followersConference circuit, community presence

Index to Typesense after enrichment:

  • Batch-import appearances/content to discoveries collection (NDJSON, action=upsert)
  • Tag all docs with person's name slug (e.g. kent-c-dodds) for filtering
  • Fields: id, title, url, summary, tags[], timestamp
  • Write a Vault/Resources/{name}-media-appearances.md reference doc linking back to contact

Output sections in the vault note:

  • Background & Story (origin, career timeline)
  • Teaching/Work Philosophy (or equivalent for non-educators)
  • Key Relationships (cross-linked [[wikilinks]] to other contacts)
  • Audience & Reach
  • Content/Products
  • Podcast/Collaboration History with Joel
  • Recent Activity (timestamped)

Ongoing Monitoring (Phase 2-4 of ADR-0151)

ChannelToolSignal
Google Alertsjoelclawbot Google accountName mentions in news, blogs, press
X/Twitter listjoelclaw X accountTweets, engagement
GitHub activityGitHub API (polling)New repos, releases
Podcast RSSFeed monitoringNew episodes
Website changesPeriodic defuddle + diffBlog posts, launches, bio changes

High-signal (immediate): course launches, role changes, mentions of Joel/egghead/Skill, fundraising. Low-signal (daily/weekly digest): regular tweets, blog posts, OSS activity.

Current VIPs

  • Get notified to Joel via gateway after enrichment
  • Are refreshed weekly via scheduled cron
  • Have priority in channel intelligence pipeline (ADR-0131, ADR-0132)
  • Get ongoing monitoring when ADR-0151 Phase 2+ is implemented

Roam Research Enrichment

Joel's Roam archive (~/Code/joelhooks/egghead-roam-research/) contains the full egghead-era graph (2019-2024). Many contacts have extensive history there.

Quick Search (Python regex)

cd ~/Code/joelhooks/egghead-roam-research
python3 -c "
import re
with open('egghead-2026-01-19-13-09-38.edn', 'r') as f:
    content = f.read()
pattern = r':block/string\s+\"([^\"]*?)\"'
matches = []
for m in re.findall(pattern, content):
    if '[[SEARCH_TAG]]' in m.lower():
        matches.append(m)
print(f'Found {len(matches)} blocks')
for m in matches[:30]:
    print(f'  - {m[:200]}')
"

People Taxonomy

People are tagged with relationship prefixes in Roam:

  • [[collaborator/Name]] — Strategic partners (Ian Jones, Alex Hillman)
  • [[client/Name]] — egghead instructors (Matt Pocock, Jacob Paris)
  • [[staff/Name]] — egghead team (Will Johnson, Daniel Miller, Maggie Appleton)
  • [[name]] (no prefix) — Informal references (Zac is [[zac]])

Page Title Search

python3 -c "
import re
with open('egghead-2026-01-19-13-09-38.edn', 'r') as f:
    content = f.read()
pattern = r':node/title\s+\"([^\"]*?SEARCH_TERM[^\"]*?)\"'
for m in re.findall(pattern, content):
    print(f'  page: {m}')
"

Adding to Contacts

When extracting person data from Roam, add roam_tag to frontmatter:

roam_tag: "[[collaborator/Ian Jones]]"

This enables future re-queries and cross-referencing.

Datalog Queries (advanced)

The EDN file is Datomic-style. Clojure scripts exist at scripts/ for structured analysis. See the roam-research skill for full Datalog patterns.

Resolving Unknown People

When you encounter a Slack user ID (<@U0XXXXXXX>):

# Lease token and look up profile
SLACK_USER=$(secrets lease slack_user_token --ttl 5m)
curl -s "https://slack.com/api/users.info?user=U0XXXXXXX" \
  -H "Authorization: Bearer $SLACK_USER" | jq '.user.real_name, .user.profile.email'
# Revoke only the exact lease ids acquired by this task; never all sessions’ leases.

Then fire enrichment with the resolved name and hints.

Inngest Function

  • Function: contact-enrich (packages/system-bus/src/inngest/functions/contact-enrich.ts)
  • Event: contact/enrich.requested
  • ADR: ~/Vault/docs/decisions/0133-contact-enrichment-pipeline.md
  • Concurrency: 3 max
  • Sources: Slack, Slack Connect, Roam archive, GitHub/web, Granola meetings, Brain-backed recall, and other indexed sources

Privacy

  • Contact files are in Vault (private, not in public repos)
  • Slack data stays private — never surface in public content
  • Email/phone are stored for Joel's reference only