Skip to content

Commit 64b0132

Browse files
authored
Merge pull request #30 from willwearing/feat/cli-agent-platform
feat: CLI, MCP server, Stripe Connect — agent-first course creation platform
2 parents 1ffb252 + 3b8a0d4 commit 64b0132

63 files changed

Lines changed: 6799 additions & 361 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
name: Publish Packages to npm
2+
3+
on:
4+
# Manual trigger with package selection
5+
workflow_dispatch:
6+
inputs:
7+
package:
8+
description: 'Package to publish (all, shared, cli, mcp)'
9+
required: true
10+
default: 'all'
11+
type: choice
12+
options:
13+
- all
14+
- shared
15+
- cli
16+
- mcp
17+
dry_run:
18+
description: 'Dry run (no actual publish)'
19+
required: false
20+
default: false
21+
type: boolean
22+
23+
# Auto-publish on version tags
24+
push:
25+
tags:
26+
- 'v*' # Publish all packages on v* tags
27+
- 'cli@*' # Publish CLI only
28+
- 'mcp@*' # Publish MCP only
29+
- 'shared@*' # Publish shared only
30+
31+
jobs:
32+
publish:
33+
runs-on: ubuntu-latest
34+
permissions:
35+
contents: read
36+
id-token: write
37+
38+
steps:
39+
- name: Checkout
40+
uses: actions/checkout@v4
41+
42+
- name: Setup bun
43+
uses: oven-sh/setup-bun@v2
44+
with:
45+
bun-version: latest
46+
47+
- name: Install dependencies
48+
run: bun install
49+
50+
- name: Determine which packages to publish
51+
id: packages
52+
run: |
53+
TAG="${GITHUB_REF_NAME:-}"
54+
INPUT="${{ github.event.inputs.package }}"
55+
56+
# Default to input if workflow_dispatch, otherwise parse tag
57+
if [ -n "$INPUT" ]; then
58+
echo "target=$INPUT" >> "$GITHUB_OUTPUT"
59+
elif [[ "$TAG" == v* ]]; then
60+
echo "target=all" >> "$GITHUB_OUTPUT"
61+
elif [[ "$TAG" == cli@* ]]; then
62+
echo "target=cli" >> "$GITHUB_OUTPUT"
63+
elif [[ "$TAG" == mcp@* ]]; then
64+
echo "target=mcp" >> "$GITHUB_OUTPUT"
65+
elif [[ "$TAG" == shared@* ]]; then
66+
echo "target=shared" >> "$GITHUB_OUTPUT"
67+
else
68+
echo "target=all" >> "$GITHUB_OUTPUT"
69+
fi
70+
71+
- name: Build shared package
72+
if: steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'shared' || steps.packages.outputs.target == 'cli' || steps.packages.outputs.target == 'mcp'
73+
run: cd packages/shared && bun run build
74+
75+
- name: Build CLI package
76+
if: steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'cli'
77+
run: cd packages/cli && bun run build
78+
79+
- name: Build MCP package
80+
if: steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'mcp'
81+
run: cd packages/mcp && bun run build
82+
83+
# Publish shared first (cli and mcp depend on it)
84+
- name: Publish @graspful/shared
85+
if: (steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'shared') && github.event.inputs.dry_run != 'true'
86+
run: cd packages/shared && bun publish --access public
87+
env:
88+
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
89+
90+
- name: Publish @graspful/cli
91+
if: (steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'cli') && github.event.inputs.dry_run != 'true'
92+
run: cd packages/cli && bun publish --access public
93+
env:
94+
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
95+
96+
- name: Publish @graspful/mcp
97+
if: (steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'mcp') && github.event.inputs.dry_run != 'true'
98+
run: cd packages/mcp && bun publish --access public
99+
env:
100+
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
101+
102+
# Dry run mode — show what would be published
103+
- name: Dry run — @graspful/shared
104+
if: (steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'shared') && github.event.inputs.dry_run == 'true'
105+
run: cd packages/shared && bun publish --access public --dry-run
106+
107+
- name: Dry run — @graspful/cli
108+
if: (steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'cli') && github.event.inputs.dry_run == 'true'
109+
run: cd packages/cli && bun publish --access public --dry-run
110+
111+
- name: Dry run — @graspful/mcp
112+
if: (steps.packages.outputs.target == 'all' || steps.packages.outputs.target == 'mcp') && github.event.inputs.dry_run == 'true'
113+
run: cd packages/mcp && bun publish --access public --dry-run

README.md

Lines changed: 117 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,138 @@
1-
# Niche Audio Prep
1+
# Graspful
22

3-
White-labeled adaptive learning platform for professional certification exams. Audio instruction + mastery-based progression, powered by a knowledge graph.
3+
> Create adaptive learning courses with AI agents. Launch in minutes.
44
5-
## What This Is
5+
## What Is Graspful?
66

7-
Professionals in trades and regulated industries (firefighters, pilots, electricians, CDL drivers, real estate agents) need to memorize dense regulatory content to pass certification exams. They're on jobsites, in trucks, on shift -- they can't sit down and read a textbook.
7+
Graspful is an agent-first course creation platform. Courses are defined as YAML files with knowledge graphs, validated by schema, and imported via CLI or MCP server -- no UI clicking required. Agents (or humans) scaffold a course, fill in content concept by concept, run quality checks, and publish. The platform handles the rest: adaptive diagnostics, mastery-based progression, spaced repetition, white-label landing pages, and Stripe billing.
88

9-
This platform delivers that content via audio, then verifies understanding through active practice. One codebase serves every niche. Adding a new certification = content + config, not code.
9+
## How It Works
1010

11-
## Guiding Principles
11+
1. **Scaffold** -- `graspful create course --topic "CKA Exam"` generates the knowledge graph skeleton
12+
2. **Fill** -- `graspful fill concept course.yaml networking` adds KPs and practice problems
13+
3. **Review** -- `graspful review course.yaml` runs 10 quality checks
14+
4. **Import** -- `graspful import course.yaml --org k8s-cert --publish` goes live
15+
5. **Brand** -- `graspful create brand --niche "Kubernetes"` generates the landing page
1216

13-
### 1. Identify what the student already knows
14-
An adaptive diagnostic maps existing knowledge in 20-60 questions. No wasting time on material already mastered.
17+
Two YAMLs (course + brand) produce one live product with adaptive learning, spaced repetition, and Stripe billing.
1518

16-
### 2. Build a personal knowledge profile on a knowledge graph
17-
Every course is a directed graph of concepts connected by prerequisite and encompassing edges. The student's diagnostic results overlay onto this graph, producing a personal map of what's known, partially known, and unknown.
19+
## Quick Start
1820

19-
### 3. Teach only at the knowledge frontier
20-
The frontier is the boundary between known and unknown. The system teaches only concepts whose prerequisites are fully mastered. Every minute of study time is spent on exactly the right thing.
21+
```bash
22+
npx @graspful/cli init
23+
```
2124

22-
### 4. Minimum effective dose of instruction + active practice
23-
Each lesson cycles: short audio explanation, worked example, 2-3 practice problems. Students spend most of their time solving problems, not passively listening.
25+
Or install globally:
2426

25-
### 5. Enforce mastery relentlessly
26-
Can't consistently solve problems? You don't advance. The system routes you to parallel learning paths, identifies the specific weak prerequisite, remediates it, then brings you back.
27+
```bash
28+
bun add -g @graspful/cli
29+
graspful login
30+
graspful create course --scaffold-only --topic "Your Topic" -o course.yaml
31+
```
2732

28-
### 6. Spaced repetition + broad-coverage quizzes
29-
Previously learned material decays. The system schedules reviews on an exponential spacing schedule and runs periodic timed quizzes across recent material.
33+
## Tech Stack
3034

31-
### 7. Review old stuff by learning new stuff
32-
The key efficiency innovation. Advanced concepts implicitly practice their prerequisites as subskills. The system tracks this implicit repetition and credits it against the review schedule -- dramatically reducing explicit review burden. You advance by reviewing, not by going backwards.
35+
- **Backend:** NestJS, Prisma, PostgreSQL (Supabase-hosted)
36+
- **Frontend:** Next.js (App Router), React, Tailwind CSS, shadcn/ui
37+
- **CLI:** `@graspful/cli` (commander.js)
38+
- **MCP Server:** `@graspful/mcp` for AI agent integration
39+
- **Auth:** Supabase Auth (JWT) + API keys for agents
40+
- **Billing:** Stripe + Stripe Connect (70/30 revenue share)
41+
- **Monorepo:** Turborepo, bun
3342

3443
## Architecture
3544

36-
- **Frontend:** Next.js 15, shadcn/ui, Tailwind -- single Vercel deployment, custom domains per niche, mobile-responsive
37-
- **Backend:** NestJS (TypeScript), Prisma, Supabase -- DDD bounded contexts
38-
- **Audio:** Kokoro TTS on Modal -- batch pre-generation
39-
- **Adaptive Learning:** Knowledge graph + FIRe spaced repetition + mastery enforcement
40-
- **Billing:** Stripe
41-
- **Mobile:** Deferred (see [mobile-plan.md](docs/mobile-plan.md))
45+
```
46+
graspful/
47+
├── apps/web/ # Next.js frontend
48+
├── backend/ # NestJS API
49+
├── packages/
50+
│ ├── shared/ # Zod schemas, types, quality gate
51+
│ ├── cli/ # @graspful/cli
52+
│ └── mcp/ # @graspful/mcp server
53+
├── content/
54+
│ ├── courses/ # Course YAML files
55+
│ ├── brands/ # Brand YAML files
56+
│ └── academies/ # Multi-course academy manifests
57+
└── docs/ # Documentation
58+
```
4259

43-
## Docs
60+
## CLI Commands
4461

45-
| Document | What It Covers |
46-
|----------|---------------|
47-
| [PLAN.md](docs/PLAN.md) | Master plan, architecture overview, 13-phase execution roadmap |
48-
| [adaptive-learning-architecture.md](docs/adaptive-learning-architecture.md) | Full adaptive learning system design -- knowledge graph, FIRe algorithm, diagnostic, mastery, DDD contexts, data model, API |
49-
| [backend-plan.md](docs/backend-plan.md) | NestJS backend architecture, Prisma schema, API design, content pipeline |
50-
| [frontend-plan.md](docs/frontend-plan.md) | Next.js frontend, white-label theming, audio player, 25 tasks |
51-
| [white-label-architecture-research.md](docs/white-label-architecture-research.md) | 4 approaches to white-labeling, recommendation |
52-
| [market-research.md](market-research.md) | 18+ niche markets analyzed, tier rankings |
53-
| [mobile-plan.md](docs/mobile-plan.md) | React Native (Expo), offline audio, background playback, 25 tasks (Deferred) |
62+
| Command | Description |
63+
|---------|-------------|
64+
| `graspful create course` | Generate course YAML skeleton |
65+
| `graspful create brand` | Generate brand YAML with theme presets |
66+
| `graspful fill concept` | Add KPs and problems to a concept |
67+
| `graspful validate` | Offline schema + DAG validation |
68+
| `graspful review` | 10 mechanical quality checks |
69+
| `graspful describe` | Course statistics |
70+
| `graspful import` | Push YAML to Graspful instance |
71+
| `graspful publish` | Publish a draft course |
72+
| `graspful login` | Authenticate |
5473

55-
## Development Workflow
74+
## MCP Server
5675

57-
- Each phase is executed by a separate Claude Code agent
58-
- Workflow: `/writing-plans` to create detailed specs, then `/subagent-driven-development` or `/executing-plans` to build
59-
- All phases use TDD with two-stage review
60-
- Status tracked per phase in [PLAN.md](docs/PLAN.md)
76+
For AI agent integration (Claude Code, Cursor, Codex):
6177

62-
## Inspired By
78+
```bash
79+
npx @graspful/cli init # Auto-configures MCP for your editor
80+
```
6381

64-
The adaptive learning system is heavily inspired by [Math Academy](https://mathacademy.com) and Justin Skycak's published research on knowledge graphs, mastery-based learning, and the Fractional Implicit Repetition (FIRe) algorithm. See the [adaptive learning doc](docs/adaptive-learning-architecture.md#16-key-sources--further-reading) for all sources.
82+
Or manually add to your MCP config:
83+
84+
```json
85+
{
86+
"mcpServers": {
87+
"graspful": {
88+
"command": "npx",
89+
"args": ["@graspful/mcp"]
90+
}
91+
}
92+
}
93+
```
94+
95+
## Development
96+
97+
```bash
98+
# Install dependencies
99+
bun install
100+
101+
# Start development servers
102+
bun run dev
103+
104+
# Backend (port 3000)
105+
cd backend && bun run dev
106+
107+
# Frontend (port 3001)
108+
cd apps/web && bun run dev
109+
110+
# Build
111+
bun run build
112+
113+
# Test
114+
bun run test
115+
116+
# E2E tests
117+
cd apps/web && npx playwright test
118+
```
119+
120+
## Revenue Model
121+
122+
70/30 revenue share (Apple model):
123+
124+
- Free to create and publish courses
125+
- When learners subscribe, Graspful collects payment
126+
- Graspful keeps 30%, creator receives 70% via Stripe Connect
127+
- No upfront cost, no monthly platform fee
128+
129+
## Documentation
130+
131+
- [Adding a Course](docs/adding-a-course.md) -- Step-by-step course creation guide
132+
- [Course Review Gate](docs/course-review-gate.md) -- Quality checks and review specification
133+
- [CLI Agent Strategy](docs/cli-agent-strategy.md) -- Full platform strategy and architecture
134+
- [Content Guide](content/README.md) -- YAML schema reference and authoring guidelines
135+
136+
## License
137+
138+
Private

apps/web/e2e/agents-page.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test.describe("Agents Page", () => {
4+
test("agents page loads with hero heading", async ({ page }) => {
5+
await page.goto("/agents");
6+
const heading = page.getByRole("heading", { level: 1 });
7+
await expect(heading).toBeVisible({ timeout: 10_000 });
8+
await expect(heading).toContainText("agent", { ignoreCase: true });
9+
});
10+
11+
test("agents page has MCP tools section", async ({ page }) => {
12+
await page.goto("/agents");
13+
await expect(page.getByText("MCP Tools")).toBeVisible();
14+
15+
// Verify specific tool names are rendered
16+
await expect(page.getByText("create_course")).toBeVisible();
17+
await expect(page.getByText("fill_concept")).toBeVisible();
18+
await expect(page.getByText("review_course")).toBeVisible();
19+
await expect(page.getByText("validate_course")).toBeVisible();
20+
await expect(page.getByText("import_course")).toBeVisible();
21+
await expect(page.getByText("create_brand")).toBeVisible();
22+
});
23+
24+
test("agents page shows supported agents", async ({ page }) => {
25+
await page.goto("/agents");
26+
await expect(page.getByText("Works with your tools")).toBeVisible();
27+
await expect(page.getByText("Claude Code")).toBeVisible();
28+
await expect(page.getByText("Cursor")).toBeVisible();
29+
});
30+
31+
test("agents page has workflow section", async ({ page }) => {
32+
await page.goto("/agents");
33+
await expect(page.getByText("Two YAMLs. One product.")).toBeVisible();
34+
await expect(page.getByText("Course YAML")).toBeVisible();
35+
await expect(page.getByText("Brand YAML")).toBeVisible();
36+
await expect(page.getByText("Import & Launch")).toBeVisible();
37+
});
38+
39+
test("agents page has pricing section", async ({ page }) => {
40+
await page.goto("/agents");
41+
await expect(page.getByText("70 / 30")).toBeVisible();
42+
await expect(
43+
page.getByText("Revenue share when learners pay", { exact: false })
44+
).toBeVisible();
45+
});
46+
47+
test("agents page has CTA with CLI init command", async ({ page }) => {
48+
await page.goto("/agents");
49+
const cliCommand = page.getByText("npx @graspful/cli init");
50+
// Should appear in both hero and bottom CTA
51+
const count = await cliCommand.count();
52+
expect(count).toBeGreaterThanOrEqual(2);
53+
});
54+
55+
test("agents page has sign-up links", async ({ page }) => {
56+
await page.goto("/agents");
57+
const signUpLinks = page.getByRole("link", { name: /get started/i });
58+
await expect(signUpLinks.first()).toBeVisible();
59+
await expect(signUpLinks.first()).toHaveAttribute("href", "/sign-up");
60+
});
61+
});

0 commit comments

Comments
 (0)