Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns: ["*"]
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Run offline regression tests
run: python -m unittest discover -s tests -v
- name: Check Python syntax
run: python -m compileall -q scripts tests
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.claude/worktrees/
.DS_Store

__pycache__/
*.pyc
28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The machine never writes for you. It interviews you, then rearranges what you sa

Three files make it yours: a profile, a voice guide built from studying your ten best posts, and a lessons file that accumulates every correction you make. The machine gets more like you with every piece.

See the [fictional founder example](example/personal/profile.md), its [interview transcript](example/personal/transcripts/2026-07-16-alert-theory.md), and the [resulting draft](example/personal/drafts/2026-07-16-alert-theory.md).

## The pipeline

```
Expand All @@ -30,21 +32,17 @@ Three files make it yours: a profile, a voice guide built from studying your ten

## Install

**As a Claude Code plugin:**

```
/plugin install derekcedarbaum2/content-machine
```

**Or clone and use directly:**
Clone the plugin and load it explicitly in Claude Code:

```bash
git clone https://github.com/derekcedarbaum2/content-machine.git
cd content-machine
claude # skills are picked up from skills/
claude --plugin-dir .
```

Then run `/content-setup`. It builds your personal folder (default `~/.content-machine/`, relocatable), interviews you for your profile, and studies your top posts to write your voice guide. Nothing personal lives in this repo — the machine is shared, your voice is not.
Run `/content-machine:content-setup`. Plugin commands use the `content-machine:` namespace, so `/draft` in the workflow above becomes `/content-machine:draft`.

It builds your personal folder (default `~/.content-machine/`, relocatable), interviews you for your profile, and studies your top posts to write your voice guide. Nothing personal lives in this repo — the machine is shared, your voice is not.

## What's where

Expand All @@ -67,3 +65,15 @@ The Oracle reads whatever your Claude Code environment can reach — MCP servers
## License

MIT. The architecture is Alex Lieberman's, described publicly on the podcast; this implementation and the rubrics are original work.

## Support and validation

Claude Code with local plugin support. The workflow is Markdown; other agents can read it, but plugin loading is verified only for the documented Claude Code layout.

This is an independently maintained project. Report reproducible bugs through Issues; security reports follow [SECURITY.md](SECURITY.md). The latest release and default branch receive fixes, with no response-time guarantee.

Run the local checks with:

```bash
python3 -m unittest discover -s tests -v
```
7 changes: 7 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Security policy

Report suspected vulnerabilities through this repository's Security tab using “Report a vulnerability.” Include the affected version, reproduction steps, and likely impact. Use fictional data and redact credentials before submitting a report.

Please keep exploit details and personal information out of public issues. If private reporting is unavailable, open an issue asking for a private reporting channel without disclosing the vulnerability.

Security fixes target the latest release and the default branch. Older versions have no separate support commitment. This is an independently maintained project, so response times are not guaranteed.
45 changes: 45 additions & 0 deletions scripts/validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Check the distributable plugin without calling a model or external service."""
import json
from pathlib import Path
import re
import sys
from urllib.parse import unquote
SKILLS = {"content-machine", "content-setup", "draft", "interview", "lessons", "oracle", "repurpose"}

def validate(root):
root = Path(root)
errors = []
try:
manifest = json.loads((root / ".claude-plugin/plugin.json").read_text())
if manifest.get("name") != "content-machine" or not re.fullmatch(r"\d+\.\d+\.\d+", manifest.get("version", "")):
errors.append("Plugin name or version is invalid")
except (OSError, ValueError) as error:
errors.append(f"Cannot read plugin manifest: {error}")
for name in sorted(SKILLS):
p = root / "skills" / name / "SKILL.md"
if not p.exists():
errors.append(f"Missing skill: {name}")
continue
text = p.read_text()
if not text.startswith("---\n") or "description:" not in text.split("---", 2)[1]:
errors.append(f"Missing skill frontmatter: {name}")
for name in ("profile.md", "voice-guide.md", "sources.md", "content-lessons.md"):
if not (root / "templates/personal" / name).is_file():
errors.append(f"Missing onboarding template: {name}")
for p in root.rglob("*.md"):
if ".git" in p.parts:
continue
for url in re.findall(r"\[[^\]]*\]\(([^\s)]+)\)", p.read_text()):
if re.match(r"[a-zA-Z][a-zA-Z0-9+.-]*:", url) or url.startswith(("#", "/", "~")):
continue
target = unquote(url.split("#")[0])
if target and not (p.parent / target).exists():
errors.append(f"Broken link in {p.relative_to(root)}: {target}")
return errors

if __name__ == "__main__":
errors = validate(Path(__file__).resolve().parents[1])
for error in errors:
print(error, file=sys.stderr)
print(f"Plugin validation: {len(errors)} error(s)")
raise SystemExit(bool(errors))
24 changes: 24 additions & 0 deletions tests/test_distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import importlib.util
from pathlib import Path
import shutil
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("validate", ROOT / "scripts/validate.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

class DistributionTests(unittest.TestCase):
def test_shipped_plugin(self):
self.assertEqual(module.validate(ROOT), [])

def test_missing_skill_and_broken_link_are_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
for folder in ("skills", "templates", ".claude-plugin"):
shutil.copytree(ROOT / folder, root / folder)
(root / "skills/draft/SKILL.md").unlink()
(root / "README.md").write_text("[missing](missing.md)\n")
errors = module.validate(root)
self.assertTrue(any("Missing skill: draft" in e for e in errors))
self.assertTrue(any("missing.md" in e for e in errors))
Loading