From career
Two-way sync between markdown CRM (crm/companies.md, crm/opportunities.md, crm/outreach.md) and Airtable. Reads CRM_ADAPTER=airtable. Markdown stays the source of truth — Airtable is a mirror with a diff back. Conflicts surfaced for user resolution, never auto-merged.
npx claudepluginhub danielrosehill/claude-code-plugins --plugin careerThis skill is limited to using the following tools:
Markdown CRM works fine until you want a sortable view, mobile access, or a teammate. This adapter mirrors the markdown rows into Airtable and pulls back changes the user made there.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Markdown CRM works fine until you want a sortable view, mobile access, or a teammate. This adapter mirrors the markdown rows into Airtable and pulls back changes the user made there.
Markdown remains the source of truth. Airtable is a working surface.
$ARGUMENTS:
--direction=<push|pull|two-way>.--table=<companies|opportunities|outreach|all> — default all.--dry-run — print the diff plan, no writes.CRM_ADAPTER=airtableAIRTABLE_BASE_IDAIRTABLE_TABLE_COMPANIES, AIRTABLE_TABLE_OPPORTUNITIES, AIRTABLE_TABLE_OUTREACHIf config missing, bail with the keys to set.
crm/companies.md rows ↔ Airtable Companies:
| markdown column | airtable field |
|---|---|
| slug (id) | Slug (primary) |
| name | Name |
| domain | Domain |
| status | Status (single select) |
| size | Size |
| hq | HQ |
| notes | Notes (long text) |
| last-touch | Last Touch (date) |
crm/opportunities.md ↔ Opportunities: slug, company-slug (linked record), role/engagement, stage, amount, opened, closed, notes.
crm/outreach.md ↔ Outreach: id, company-slug (linked), recipient, channel, sent-date, status, follow-up-due, draft-path, notes.
Linked records (company-slug → Companies) resolved by slug.
For each row, classify:
local-only — in markdown, not in Airtable.remote-only — in Airtable, not in markdown.match — same in both.conflict — same slug, divergent values (and both modified since last sync).Use ${WORKING_FOLDER}/.airtable-sync.json to track last-sync timestamps per row, so a one-sided edit is not a conflict.
--direction=push: write local-only and updated-local rows to Airtable. Don't touch remote-only or conflicts.--direction=pull: write remote-only and updated-remote rows to markdown. Don't touch local-only or conflicts.--direction=two-way: push local, pull remote, surface conflicts.For markdown writes: edit the table cells, preserving file structure. Append a **Last synced:** YYYY-MM-DDTHH:MM line under the table header.
For Airtable writes: use MCP create/update by Slug.
For each conflict, write a row in ${WORKING_FOLDER}/crm/.conflicts-<date>.md:
## <slug> — <table>
- field: <field>
- markdown: <value> (modified <when>)
- airtable: <value> (modified <when>)
resolve via: edit the markdown value and re-run sync, or change Airtable and re-run.
Never auto-resolve.
Rewrite .airtable-sync.json.
direction: <push|pull|two-way>
pushed: <n> pulled: <n> unchanged: <n> conflicts: <n>
manifest: .airtable-sync.json
conflicts file (if any): crm/.conflicts-<date>.md
--dry-run is the right move on the first run after a long gap.