BASH-CMS
BASH-CMS
Governed publishing from your repo, inside VS Code: browse what's worth sharing, draft LinkedIn company-page posts, run the brand guard, approve with your name on it, and publish — with an idempotency ledger so nothing ever double-posts.
Built by BASH Consulting and used to run its own company page. The pipeline follows one rule end to end: drafts are files, approval is a human decision recorded in the file, and a script does the mechanical work. No post leaves your machine without a person deciding it should.
How it works
Content ──▶ Draft (status: pending) ──▶ Approve (status: approved) ──▶ Publish ──▶ Ledger
│ a file in a person, in the POST /rest/posts .github/
.cms/ index drafts/linkedin/ review panel as the org linkedin-log.json
or a frontmatter scanContent comes from the
.cms/contract when your repo has one (the zer0-CMS content engine's machine-readable index), and degrades honestly to a frontmatter scan when it doesn't.Drafts are markdown files with
type/status/sourcefrontmatter — the same queue format the BASH CI lane publishes from, so the extension and a GitHub Actions workflow can share one queue and one ledger without stepping on each other.The guard is mechanical: LinkedIn's 3,000-character limit, the 140-character "…see more" fold, a banned-phrase list, filler detection, and your own patterns file. Errors block a live post; a human can still be overruled only by a human.
The ledger is keyed by canonical URL. Either lane (extension or CI) posts first; the other skips. Byte-compatible with the Python lane's
json.dumpoutput, so the file never churns in git.
The four views
View | What it shows |
Drafts | The queue, by status — open the review panel, approve, publish |
Content | Distributable pages (health-ranked when |
What to write next | Catering lanes: A — distribute what exists · B — write more of what landed · C — say the quiet part · D — refresh what worked |
Published | The ledger, newest first — jump to the live post |
Getting started
Install the extension, open your content repo.
Set
bashCms.linkedin.orgUrnandbashCms.site.baseUrl(Settings → BASH-CMS).Run BASH-CMS: Set LinkedIn Credentials and paste a token with
w_organization_social+r_organization_social(from the token generator; requires Community Management API access on your LinkedIn app).Run BASH-CMS: LinkedIn: Who Am I to confirm the token, scopes, and expiry.
Draft from the Content view, review, approve — and when you're ready to go live, enable
bashCms.publish.allow.
Publishing is off by default. Until you enable it, every path — commands, the review panel, MCP — is preview and draft only.
Commands
Command | What it does |
Set / Clear LinkedIn Credentials | Token (and optional client id/secret) into VS Code SecretStorage — never settings, never a file |
LinkedIn: Who Am I | Token status, granted scopes, days to expiry |
New Article Draft / New Text Draft | Stage a |
Preview LinkedIn Payload | The exact |
Run Brand Guard on Draft | Errors, warnings, and the fold preview |
Approve Draft |
|
Publish Draft… / Publish All Approved Drafts | The live path: guard → ledger → confirm → post → record |
Open Review Panel | The approval gate, made legible (below) |
Generate Catering Worklist | Write |
Open Post on LinkedIn | Jump from the ledger to the live post |
Configure MCP Server (.vscode/mcp.json) | Fallback wiring for older VS Code |
The review panel
One panel per draft, opened beside the editor: the commentary with the fold marker where LinkedIn truncates, every guard finding, the exact outgoing JSON, and the ledger state. The Publish button stays disabled until the gates pass — and the extension re-checks every gate on its own side when you click, because a webview is UI, never the gate.
MCP server
The extension registers a bundled MCP server with VS Code (1.101+), so Copilot agent mode — or any MCP client pointed at dist/mcp-server.js — gets six tools:
Tool | Safe? |
| read-only |
| writes a pending draft for a human |
| off by default — needs |
The access token is injected into the server process only at start time, from SecretStorage — it never appears in a static definition, a setting, or a log.
Settings
Setting | Default | Purpose |
| — | The company page ( |
|
|
|
| — | Canonical site origin |
|
| Canonical URL shape (the ledger key) |
|
| Where content lives when there's no |
|
| Card thumbnail resolution |
|
| The queue |
|
| The shared ledger |
|
| The master gate |
|
| Set to |
| — | Extra banned patterns ( |
|
| Read |
Boundaries
Four rules this tool is built around, in order of importance:
Every post is approved by whoever's name goes on it. There is no unattended path to the live page.
Each account is authorized by its own owner's consent — a token the owner minted, stored in SecretStorage.
The audience you write for is declared by you — never derived from connections, followers, or anyone's profile data.
Everything read back is your own aggregate numbers. No member-level data enters the catering lanes.
Security notes
Tokens live in VS Code SecretStorage (the OS keychain). The workspace
.envis a read-only fallback for repos that already use one; the extension never writes secrets to disk.LinkedIn access tokens last 60 days and programmatic refresh is limited to LinkedIn partners — Who Am I tells you the days remaining, and the ledger records the expiry so a CI health-check can warn too.
The HTTP client never puts the token in an error message or a log line.
Development
npm install
npm run compile # type-check + lint + both bundles
npm test # unit + golden + MCP stdio + integration (downloads VS Code 1.101)
npx @vscode/vsce package # build the .vsixThe test suite includes golden cross-lane fixtures generated by the Python publishing lane (src/test/fixtures/golden/) — the ledger bytes, payload previews, and catering worklists must match that lane byte-for-byte, because both write the same files in the same repos. Regenerate with python3 src/test/fixtures/golden/generate.py <path-to-bashconsultants>.
License
MIT — © Amr Abdel-Motaleb, BASH Consulting.