@topy-ai/maggie 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/bin/maggie.js +152 -0
  2. package/bundled-references/ai-native-blog-contract.md +310 -0
  3. package/bundled-references/blog-data-contract.md +146 -0
  4. package/bundled-references/blog-implementation.md +46 -0
  5. package/bundled-references/blog-operations-contract.md +68 -0
  6. package/bundled-references/browser-inspection.md +39 -0
  7. package/bundled-references/provider-adapter-contract.md +68 -0
  8. package/bundled-references/seo-technical-contract.md +75 -0
  9. package/bundled-skills/README.md +16 -0
  10. package/bundled-skills/maggie-blog-bootstrap/SKILL.md +243 -0
  11. package/bundled-skills/maggie-clone/SKILL.md +213 -0
  12. package/bundled-skills/maggie-deployment/SKILL.md +61 -0
  13. package/bundled-skills/maggie-deployment/agents/openai.yaml +4 -0
  14. package/bundled-skills/maggie-deployment/references/cloudflare.md +76 -0
  15. package/bundled-skills/maggie-deployment/references/provider-contract.md +32 -0
  16. package/bundled-skills/maggie-project-context/SKILL.md +38 -0
  17. package/bundled-skills/maggie-seo-geo/SKILL.md +53 -0
  18. package/bundled-skills/maggie-social-share/SKILL.md +48 -0
  19. package/bundled-tools/clis/maggie.py +748 -0
  20. package/bundled-tools/clis/maggie_clone.py +82 -0
  21. package/bundled-tools/clis/site_audit.py +99 -0
  22. package/bundled-tools/integrations/analytics.md +34 -0
  23. package/bundled-tools/integrations/maggie-api-pull.md +72 -0
  24. package/bundled-tools/integrations/maggie-project-context.md +62 -0
  25. package/bundled-tools/integrations/maggie-seo-audit.md +16 -0
  26. package/bundled-tools/integrations/maggie-skills-api.md +76 -0
  27. package/bundled-tools/integrations/maggie-social-share.md +23 -0
  28. package/bundled-tools/integrations/maggie-visibility.md +22 -0
  29. package/package.json +29 -0
  30. package/references/ai-native-blog-contract.md +310 -0
  31. package/references/blog-data-contract.md +146 -0
  32. package/references/blog-implementation.md +46 -0
  33. package/references/blog-operations-contract.md +68 -0
  34. package/references/browser-inspection.md +39 -0
  35. package/references/provider-adapter-contract.md +68 -0
  36. package/references/seo-technical-contract.md +75 -0
@@ -0,0 +1,310 @@
1
+ # Maggie AI-Native Blog Application Contract
2
+
3
+ This contract describes the complete application surface for a Maggie-powered
4
+ blog. It fixes the content, operations, and SEO semantics while leaving visual
5
+ design and component composition to the implementing agent.
6
+
7
+ ## Public information architecture
8
+
9
+ The default route map is:
10
+
11
+ ```text
12
+ /blog published post index
13
+ /blog/[slug] post detail
14
+ /topics topic index
15
+ /topics/[topic-slug] topic landing page and post grid
16
+ /authors/[author-slug] authorship page when author pages are enabled
17
+ /about optional organisation/EEAT page
18
+ /sitemap.xml sitemap index or single sitemap
19
+ /sitemap-posts-[part].xml post sitemap parts when required
20
+ /robots.txt crawl rules and sitemap reference
21
+ ```
22
+
23
+ ### Blog index
24
+
25
+ The blog index should contain, in an accessible and crawlable order:
26
+
27
+ 1. page title and concise description;
28
+ 2. optional featured or latest post block;
29
+ 3. hot-topic navigation using real topic URLs;
30
+ 4. blog grid/list with title, excerpt, date, author, image alt text, and
31
+ normal anchor links;
32
+ 5. stable pagination with self-canonical pages and no indexable duplicate
33
+ query URLs;
34
+ 6. useful FAQ section only when questions and answers are maintained in the
35
+ content model;
36
+ 7. a configured CTA with explicit attribution.
37
+
38
+ ### Post detail
39
+
40
+ Each published post must render:
41
+
42
+ - title, excerpt, canonical URL, language, and visible publication date;
43
+ - author name, author profile link, credentials or organisation context when
44
+ available, and source attribution;
45
+ - modified date only when it represents a real content revision;
46
+ - readable content with one `h1`, logical `h2`/`h3` headings, table of contents
47
+ when useful, internal links, related topics, and related posts;
48
+ - cover image with dimensions and meaningful alt text;
49
+ - Article/BlogPosting JSON-LD matching the visible page;
50
+ - FAQ section only when the FAQ items are visible and maintained;
51
+ - CTA selected by policy, with UTM attribution generated from known values only;
52
+ - no draft, queue, internal note, or private project context.
53
+
54
+ ### Topic landing page
55
+
56
+ Each indexable topic page must have:
57
+
58
+ - unique topic title and useful topic description;
59
+ - a stable topic slug and canonical URL;
60
+ - a crawlable post grid filtered by `published` status;
61
+ - pagination when the list exceeds the configured page size;
62
+ - optional topic FAQ with visible answers and FAQ schema only when eligible;
63
+ - related topics and one configured CTA where relevant;
64
+ - no thin page created solely because a tag exists.
65
+
66
+ Do not create indexable topic pages until the topic has a description and a
67
+ minimum configured number of published posts, unless the user explicitly
68
+ approves an exception.
69
+
70
+ ## Stable domain model
71
+
72
+ ```ts
73
+ type PublishStatus = "draft" | "review" | "approved" | "published" | "archived";
74
+
75
+ type Author = {
76
+ id: string;
77
+ slug: string;
78
+ name: string;
79
+ jobTitle?: string;
80
+ organisation?: string;
81
+ bio?: string;
82
+ profileUrl?: string;
83
+ sameAs?: string[];
84
+ avatarUrl?: string;
85
+ credentials?: string[];
86
+ };
87
+
88
+ type Topic = {
89
+ id: string;
90
+ slug: string;
91
+ title: string;
92
+ description: string;
93
+ status: "draft" | "published" | "archived";
94
+ seoTitle?: string;
95
+ seoDescription?: string;
96
+ faqIds: string[];
97
+ minimumPublishedPosts: number;
98
+ };
99
+
100
+ type FaqItem = {
101
+ id: string;
102
+ question: string;
103
+ answerMarkdown: string;
104
+ sortOrder: number;
105
+ status: "draft" | "published";
106
+ };
107
+
108
+ type Cta = {
109
+ id: string;
110
+ label: string;
111
+ url: string;
112
+ placement: "blog_index" | "post" | "topic" | "footer";
113
+ status: "draft" | "active" | "archived";
114
+ trackingKey?: string;
115
+ };
116
+
117
+ type SeoSnapshot = {
118
+ title: string;
119
+ description: string;
120
+ canonicalUrl: string;
121
+ robots: "index,follow" | "noindex,follow" | "noindex,nofollow";
122
+ ogType: "website" | "article";
123
+ ogImageUrl?: string;
124
+ twitterCard: "summary" | "summary_large_image";
125
+ jsonLdType: "Article" | "BlogPosting" | "CollectionPage" | "FAQPage";
126
+ };
127
+
128
+ type BlogReport = {
129
+ id: string;
130
+ runType: "technical_seo" | "content_quality" | "visibility" | "analytics";
131
+ status: "queued" | "running" | "completed" | "failed";
132
+ startedAt?: string;
133
+ completedAt?: string;
134
+ summary: Record<string, unknown>;
135
+ findings: Array<{
136
+ severity: "error" | "warning" | "info";
137
+ code: string;
138
+ url?: string;
139
+ message: string;
140
+ }>;
141
+ };
142
+ ```
143
+
144
+ Extend the [canonical post model](blog-data-contract.md) with `authorIds`,
145
+ `topicIds`, `faqIds`, `ctaId`, `seo`, and `contentQuality` rather than putting
146
+ these fields into an untyped metadata blob.
147
+
148
+ ## Maggie Ops dashboard
149
+
150
+ The operations dashboard is private, authenticated, and never included in the
151
+ public sitemap or robots allowlist:
152
+
153
+ ```text
154
+ /ops health summary and pending actions
155
+ /ops/posts filterable post inventory
156
+ /ops/posts/new create draft
157
+ /ops/posts/[id]/edit edit metadata/content with validation
158
+ /ops/posts/[id]/preview preview draft without indexing
159
+ /ops/topics topic and FAQ management
160
+ /ops/sitemap sitemap source, matching history, and eligibility
161
+ /ops/reports technical SEO, content, visibility, analytics reports
162
+ /ops/settings/site origin, locale, timezone, author defaults, CTA defaults
163
+ /ops/settings/integrations AI CMO key status, API Pull, GSC, and GA4 settings
164
+ ```
165
+
166
+ Dashboard actions must be explicit and auditable:
167
+
168
+ | Action | Required state | External write |
169
+ |---|---|---:|
170
+ | Save draft | authenticated editor | no |
171
+ | Approve post | review-ready post and editor approval | no |
172
+ | Publish post | approved post and publishing permission | yes/public |
173
+ | Match sitemap | completed bootstrap and configured source | AI CMO API |
174
+ | Queue rewrite | matched asset and rewrite policy | AI CMO API |
175
+ | Sync API Pull | server key and source mapping | AI CMO API |
176
+ | Run report | configured target/property | provider/API varies |
177
+ | Change integration | owner/admin permission | yes |
178
+
179
+ Every mutation records actor, timestamp, previous state, new state, reason,
180
+ and correlation/idempotency key. The dashboard should show a dry-run preview
181
+ before queueing, publishing, changing sitemap sources, or changing integration
182
+ settings.
183
+
184
+ ### Ops API boundary
185
+
186
+ Keep the browser UI thin. All reads and mutations go through authenticated
187
+ server-side routes with typed request/response objects:
188
+
189
+ ```text
190
+ GET /api/ops/summary
191
+ GET /api/ops/posts?status=&topic=&page=
192
+ POST /api/ops/posts
193
+ GET /api/ops/posts/[id]
194
+ PATCH /api/ops/posts/[id]
195
+ POST /api/ops/posts/[id]/preview
196
+ POST /api/ops/posts/[id]/approve
197
+ POST /api/ops/posts/[id]/publish
198
+ GET /api/ops/topics
199
+ POST /api/ops/topics
200
+ PATCH /api/ops/topics/[id]
201
+ GET /api/ops/sitemap/runs
202
+ POST /api/ops/sitemap/match
203
+ POST /api/ops/sitemap/auto-detect
204
+ GET /api/ops/reports?type=&status=
205
+ POST /api/ops/reports
206
+ GET /api/ops/settings
207
+ PATCH /api/ops/settings/site
208
+ PATCH /api/ops/settings/integrations
209
+ ```
210
+
211
+ Every route must perform session authentication, role authorization, input
212
+ schema validation, and state-transition validation. Do not expose raw database
213
+ queries or the AI CMO key to the browser. Return a correlation ID with errors
214
+ so an Ops user can reconcile a failed action without seeing secret values.
215
+
216
+ The post editor must validate title, slug, excerpt, language, author, topics,
217
+ publication status, canonical URL, cover image/alt text, and SEO fields before
218
+ save. A preview uses `noindex` and the same renderer as the public post; it is
219
+ not a second content implementation.
220
+
221
+ ### Ops dashboard summary
222
+
223
+ `GET /api/ops/summary` should expose only bounded operational facts:
224
+
225
+ ```ts
226
+ type OpsSummary = {
227
+ bootstrap: "complete" | "incomplete";
228
+ content: { published: number; drafts: number; review: number; failedSyncs: number };
229
+ sitemap: { lastRunAt?: string; matched: number; eligible: number; unmatched: number };
230
+ reports: { openErrors: number; lastTechnicalSeoRunAt?: string };
231
+ integrations: { aiCmo: "disabled" | "configured" | "verified" | "error"; gsc: string; ga4: string };
232
+ };
233
+ ```
234
+
235
+ Counts must be calculated from the same repositories used by the public
236
+ routes. Do not use stale client counters to decide whether a post is eligible
237
+ for publication or rewrite.
238
+
239
+ ## Configuration boundary
240
+
241
+ Keep non-secret configuration typed and reviewable:
242
+
243
+ ```ts
244
+ type BlogConfig = {
245
+ site: {
246
+ name: string;
247
+ baseUrl: string;
248
+ locale: string;
249
+ timezone: string;
250
+ blogPath: string;
251
+ postsPerPage: number;
252
+ topicMinimumPosts: number;
253
+ };
254
+ defaults: {
255
+ authorId?: string;
256
+ ctaId?: string;
257
+ language: string;
258
+ robotsPolicy: "index,follow" | "noindex,follow";
259
+ };
260
+ integrations: {
261
+ aiCmo: { enabled: boolean; baseUrl: string; apiKeyEnv: string; syncMode: "manual" | "scheduled" };
262
+ gsc: { enabled: boolean; verificationMode?: "html" | "dns" };
263
+ ga4: { enabled: boolean; measurementIdEnv?: string; consentRequired: boolean };
264
+ };
265
+ publishing: {
266
+ requireApproval: boolean;
267
+ allowScheduledPublish: boolean;
268
+ allowAutoRewritePublish: boolean;
269
+ };
270
+ };
271
+ ```
272
+
273
+ Secret values stay in runtime environment variables. The dashboard may show
274
+ configured/missing/verified status, but never the key or token value.
275
+
276
+ ## Backend and SEO invariants
277
+
278
+ - Public reads select `status = published` and valid `publishedAt` in one
279
+ repository function; templates must not implement their own filtering.
280
+ - Post, topic, FAQ, author, CTA, and report IDs are stable and never derived
281
+ from mutable display text after publication.
282
+ - Every public URL has one canonical owner. Slug changes create redirects and
283
+ never silently create a second page.
284
+ - Preview, draft, review, archived, ops, and report URLs are `noindex` and
285
+ excluded from sitemap output.
286
+ - The sitemap is generated from the same publication query used by the public
287
+ list, not from a separate hand-maintained URL array.
288
+ - Metadata and JSON-LD are generated from validated domain objects, not from
289
+ free-form agent prose.
290
+ - Build and report failures preserve the last known-good public output.
291
+ - API Pull, GSC, and GA4 integrations are optional; missing credentials must
292
+ produce a visible disabled/unverified state, not a broken build.
293
+
294
+ ## Implementation order
295
+
296
+ Generate and verify the application in this order:
297
+
298
+ 1. typed config and environment validation;
299
+ 2. database migrations and repository functions;
300
+ 3. post/topic/author/FAQ/CTA schemas and seed fixtures;
301
+ 4. public rendering and metadata helpers;
302
+ 5. sitemap/robots generation from the publication query;
303
+ 6. authenticated Ops API and dashboard screens;
304
+ 7. API Pull adapter, sitemap matching, and state reporting;
305
+ 8. reports, GSC/GA4 readback, and scheduled jobs;
306
+ 9. integration, route, metadata, and migration tests.
307
+
308
+ Do not build the dashboard against mock objects after step 2. Fixtures are for
309
+ tests and empty-state UI only; production reads must use the validated
310
+ repository boundary.
@@ -0,0 +1,146 @@
1
+ # Maggie Blog Data Contract
2
+
3
+ This contract is the stable backend boundary for every Maggie framework
4
+ template. UI components may differ, but adapters should normalize all content
5
+ to this model before rendering.
6
+
7
+ ## Canonical post model
8
+
9
+ ```ts
10
+ type PostStatus = "draft" | "review" | "approved" | "scheduled" | "published" | "archived";
11
+
12
+ type BlogPost = {
13
+ id: string; // local stable id
14
+ source: "local" | "api-pull" | "cms" | "manual";
15
+ sourceId?: string; // remote content_id or provider id
16
+ slug: string; // unique public identity
17
+ title: string;
18
+ excerpt: string;
19
+ contentMarkdown?: string;
20
+ contentHtml?: string;
21
+ canonicalUrl: string;
22
+ coverImageUrl?: string;
23
+ coverImageAlt?: string;
24
+ authorName?: string;
25
+ authorUrl?: string;
26
+ language: string;
27
+ tags: string[];
28
+ status: PostStatus;
29
+ publishedAt?: string; // ISO-8601 UTC
30
+ updatedAt?: string; // ISO-8601 UTC
31
+ createdAt: string; // ISO-8601 UTC
32
+ contentHash?: string;
33
+ };
34
+ ```
35
+
36
+ Required invariants:
37
+
38
+ - `slug` is unique and does not change after publication without a redirect.
39
+ - `status = published` and a valid `publishedAt` are both required for a
40
+ public route, list item, sitemap entry, or Article JSON-LD record.
41
+ - `canonicalUrl` is absolute, uses the configured site origin, and has no
42
+ tracking query parameters.
43
+ - Store Markdown and/or sanitized HTML, never unsanitized remote HTML.
44
+ - Keep remote `sourceId` separate from local `id`; neither may be inferred from
45
+ a mutable title.
46
+ - Dates are serialized as ISO-8601 UTC and displayed using the chosen locale.
47
+
48
+ ## SQLite reference schema
49
+
50
+ The Astro starter additionally includes migration tables for external CMS
51
+ imports: `wp_migrations`, `wp_import_items`, `wp_terms`, `media_assets`,
52
+ `post_media`, and `redirects`. A WordPress source identity is stored as
53
+ `source = 'cms'` and `source_id = 'wordpress:<id>'` so it remains compatible
54
+ with the canonical post source enum while remaining unique and rerunnable.
55
+
56
+ This is the default for a new, small single-instance project. Use the same
57
+ logical fields with Postgres or another engine when the deployment requires it.
58
+
59
+ ```sql
60
+ CREATE TABLE posts (
61
+ id TEXT PRIMARY KEY,
62
+ source TEXT NOT NULL CHECK (source IN ('local', 'api-pull', 'cms', 'manual')),
63
+ source_id TEXT,
64
+ slug TEXT NOT NULL UNIQUE,
65
+ title TEXT NOT NULL,
66
+ excerpt TEXT NOT NULL DEFAULT '',
67
+ content_markdown TEXT,
68
+ content_html TEXT,
69
+ canonical_url TEXT NOT NULL UNIQUE,
70
+ cover_image_url TEXT,
71
+ cover_image_alt TEXT,
72
+ author_name TEXT,
73
+ author_url TEXT,
74
+ language TEXT NOT NULL DEFAULT 'en',
75
+ status TEXT NOT NULL DEFAULT 'draft'
76
+ CHECK (status IN ('draft', 'review', 'approved', 'scheduled', 'published', 'archived')),
77
+ published_at TEXT,
78
+ updated_at TEXT,
79
+ created_at TEXT NOT NULL,
80
+ content_hash TEXT,
81
+ CHECK (status <> 'published' OR published_at IS NOT NULL)
82
+ );
83
+
84
+ CREATE UNIQUE INDEX posts_source_identity
85
+ ON posts (source, source_id)
86
+ WHERE source_id IS NOT NULL;
87
+ CREATE INDEX posts_public_order
88
+ ON posts (status, published_at DESC, updated_at DESC);
89
+
90
+ CREATE TABLE post_tags (
91
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
92
+ tag TEXT NOT NULL,
93
+ PRIMARY KEY (post_id, tag)
94
+ );
95
+
96
+ CREATE TABLE content_sync_state (
97
+ source TEXT PRIMARY KEY,
98
+ cursor TEXT,
99
+ etag TEXT,
100
+ last_success_at TEXT,
101
+ last_error TEXT,
102
+ updated_at TEXT NOT NULL
103
+ );
104
+
105
+ CREATE TABLE content_delivery_state (
106
+ post_id TEXT PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE,
107
+ remote_content_id TEXT,
108
+ remote_version TEXT,
109
+ delivery_status TEXT NOT NULL DEFAULT 'not_delivered'
110
+ CHECK (delivery_status IN ('not_delivered', 'received', 'stored', 'published', 'failed')),
111
+ last_reported_url TEXT,
112
+ last_reported_hash TEXT,
113
+ last_error TEXT,
114
+ updated_at TEXT NOT NULL
115
+ );
116
+
117
+ CREATE TABLE post_redirects (
118
+ old_slug TEXT PRIMARY KEY,
119
+ post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
120
+ created_at TEXT NOT NULL
121
+ );
122
+ ```
123
+
124
+ For a multi-instance or serverless production deployment, confirm the hosted
125
+ database and migration strategy before using SQLite. Never silently create a
126
+ second database beside an existing ORM or CMS database.
127
+
128
+ ## API Pull normalization
129
+
130
+ Map the remote response at one adapter boundary:
131
+
132
+ ```text
133
+ remote content_id -> posts.source_id and content_delivery_state.remote_content_id
134
+ remote id -> posts.source_id when content_id is unavailable
135
+ remote title -> posts.title
136
+ remote excerpt -> posts.excerpt
137
+ remote content_markdown -> posts.content_markdown
138
+ remote content_html -> sanitize, then posts.content_html
139
+ remote canonical_url -> posts.canonical_url
140
+ remote slug -> posts.slug, never overwrite a published local slug
141
+ remote generated_at -> posts.updated_at
142
+ ```
143
+
144
+ The adapter must validate required fields, preserve the original response for
145
+ debugging outside public output, upsert by source identity, and only mark a
146
+ delivery successful after local storage succeeds.
@@ -0,0 +1,46 @@
1
+ # Framework-Neutral Blog Implementation Contract
2
+
3
+ This is the minimum complete blog surface. Framework templates may map these
4
+ routes differently, but must preserve their behaviour.
5
+
6
+ ## Public surface
7
+
8
+ | Surface | Requirement |
9
+ |---|---|
10
+ | Posts index | Crawlable links, title, excerpt, published date, pagination or bounded feed |
11
+ | Post detail | Stable slug, 404 for missing/unpublished content, canonical URL |
12
+ | Robots | Allow public posts and reference the sitemap |
13
+ | Sitemap | Published posts only, absolute canonical URLs, `lastmod` when known |
14
+ | Metadata | Title, description, canonical, OG, Twitter, Article JSON-LD |
15
+ | Analytics | GA4 only when configured and consent policy permits |
16
+ | GSC | Verification path or documented DNS verification |
17
+
18
+ ## Data rules
19
+
20
+ - `slug` is unique and immutable after publication unless a redirect is created.
21
+ - `publishedAt` controls sitemap/index inclusion.
22
+ - `updatedAt` is not a substitute for publication state.
23
+ - `canonicalUrl` wins over inferred request host when configured.
24
+ - Untrusted HTML is sanitized before rendering.
25
+
26
+ ## Technical SEO acceptance
27
+
28
+ - one canonical per post;
29
+ - no indexable duplicate query URLs;
30
+ - server output contains the post title and primary content;
31
+ - one `h1` per post;
32
+ - images have dimensions or stable layout reservation and alt text;
33
+ - internal links use normal anchors;
34
+ - sitemap and robots return 200 with the correct content type.
35
+
36
+ ## GEO/AI discoverability acceptance
37
+
38
+ The implementation should make the same facts available to humans and crawlers:
39
+
40
+ - clear author, organisation, date, and source attribution;
41
+ - concise answer-first opening paragraphs;
42
+ - descriptive headings and stable URLs;
43
+ - Article JSON-LD matching visible content;
44
+ - no robots rule that accidentally blocks major search crawlers;
45
+ - no claim that crawler access guarantees AI citations or rankings.
46
+
@@ -0,0 +1,68 @@
1
+ # Maggie Blog Operations Contract
2
+
3
+ The starter keeps blog reading, content operations, migration, and agency
4
+ administration on explicit server-side contracts. Agents may customize UI/UX,
5
+ but must preserve these invariants:
6
+
7
+ ## Content operations
8
+
9
+ - Every scheduled publication is stored in `content_calendar` and is promoted
10
+ only from an approved/review state by `POST /api/ops/calendar/publish-due`.
11
+ - Every revision is a complete immutable post snapshot in `post_revisions`.
12
+ - Every bulk mutation starts as `bulk-preview`; apply operations are limited to
13
+ an allowlist of fields and produce an `audit_events` record.
14
+ - Redirects are unique by source path and accept only 301/308.
15
+ - Media is a first-class `media_assets` record with usage counts, metadata and
16
+ optional folders; remote media is never silently assumed to be local.
17
+
18
+ ## Migration
19
+
20
+ `POST /api/ops/migrations/import` accepts JSON, CSV, WXR, sitemap, or
21
+ `media-archive` input in preview/apply mode. Imported content uses
22
+ `source + source_id` as its identity and a SHA-256 checksum for incremental
23
+ reruns. `syncMode` supports `all`, `new`, and `modified`; `detectDeleted` archives
24
+ source records absent from a full snapshot, and `conflict` supports
25
+ `source-wins`, `target-wins`, or `manual`. Redirect mappings support both JSON
26
+ and CSV import/export. Persisted migrations can resume with
27
+ `POST /api/ops/migrations/:id/resume`; source identity and checksums keep the
28
+ resumed import idempotent. WordPress REST remains the richer adapter for terms,
29
+ authors, featured media and old-path redirects.
30
+
31
+ ## Agency
32
+
33
+ `workspaces`, `clients`, `sites`, and `workspace_members` isolate client data
34
+ and credentials. Workspace-scoped reads and writes use `/api/ops/agency` and
35
+ reject cross-workspace client/site/project relations. Roles are fixed to the
36
+ eight supported agency roles; secrets remain in server
37
+ environment/configuration and are never returned by browser APIs.
38
+ `approval_links` provides expiring, branded-review-compatible handoff tokens
39
+ without storing raw tokens.
40
+
41
+ Entity profiles and `entity_relations` form a small explicit entity graph;
42
+ insights also return refresh recommendations derived from persisted content
43
+ quality signals rather than silently mutating published content.
44
+
45
+ ## Vibe-coding safety
46
+
47
+ Bootstrap confirmation writes the project, decisions, schema, routes,
48
+ integrations, and migration manifests under `.maggie/`. `maggie doctor` and
49
+ `npm run schema:check` are required gates before generated code is considered
50
+ ready. Fixtures are deterministic and must be used for local provider tests.
51
+
52
+ ## API Pull lifecycle
53
+
54
+ The adapter exposes project context, post/update pulls, sitemap matching
55
+ history, rewrite queue/history, and publication-state reporting as separate
56
+ server-only operations. A local write and approval decision must complete
57
+ before `report-state` is called. Batch reporting is bounded to 500 records per
58
+ request, and API credentials are read from server configuration only.
59
+
60
+ ## External providers
61
+
62
+ Email and media are provider-neutral contracts. Resend is the default email
63
+ adapter and Cloudinary is the default media storage/delivery adapter; local or
64
+ webhook adapters can be selected without changing post, report, or media
65
+ records. Provider adapters must expose configuration health, normalize errors,
66
+ and never return credentials through Ops APIs. A new provider should implement
67
+ the relevant interface and register its ID rather than adding provider fields
68
+ to content templates.
@@ -0,0 +1,39 @@
1
+ # Maggie Clone Browser Inspection Contract
2
+
3
+ `maggie-clone` requires an available browser automation capability. The skill
4
+ is intentionally provider-neutral: use the configured Chrome, browser,
5
+ Playwright, Puppeteer, or equivalent MCP tools rather than adding a fake local
6
+ MCP server to the host project.
7
+
8
+ The browser capability must support:
9
+
10
+ - navigation to a target URL and reading the final URL after redirects;
11
+ - desktop, tablet, and mobile viewport sizes;
12
+ - full-page and viewport screenshots saved to the planned artifact directory;
13
+ - DOM/text/attribute queries and JavaScript evaluation;
14
+ - slow scrolling, click, hover, keyboard focus, and back/forward navigation;
15
+ - reading computed CSS, media sources, links, and visible accessibility labels.
16
+
17
+ The minimum extraction result for a target is:
18
+
19
+ ```text
20
+ final URL and redirect chain
21
+ viewport screenshots: 1440, 768, 390
22
+ page sections and destination route
23
+ visible copy and links
24
+ computed design tokens
25
+ asset URLs and media metadata
26
+ interaction states and triggers
27
+ responsive differences
28
+ ```
29
+
30
+ If a target requires login, a bot challenge, a consent interaction, or a
31
+ private browser profile, stop at that boundary and ask the user to provide
32
+ authorized access. Do not bypass access controls or record cookies in project
33
+ artifacts.
34
+
35
+ Use browser evaluation to measure values; do not infer a Tailwind class from
36
+ appearance. For every stateful component capture state A and state B and
37
+ record the trigger and transition. Keep screenshots and extracted data under
38
+ the target's namespaced research root so multiple URLs cannot overwrite one
39
+ another.
@@ -0,0 +1,68 @@
1
+ # Maggie Provider Adapter Contract
2
+
3
+ Third-party services are implementation details behind stable interfaces. Blog
4
+ routes, content records, reports, and Ops screens must depend on capabilities,
5
+ not on a vendor SDK or vendor-specific fields.
6
+
7
+ ## Defaults
8
+
9
+ | Capability | Default | Local/development alternative |
10
+ |---|---|---|
11
+ | Email delivery | `resend` | `webhook` or `disabled` |
12
+ | Media storage and delivery | `cloudinary` | `local` |
13
+
14
+ The default can be changed through environment configuration. Missing
15
+ credentials fail closed: email is queued or reports a provider error, and
16
+ media upload is marked failed. A typo or unknown provider never silently falls
17
+ back to a different external service.
18
+
19
+ ## Email interface
20
+
21
+ An email adapter implements `EmailProvider`:
22
+
23
+ ```ts
24
+ type EmailProvider = {
25
+ id: string;
26
+ send(message: EmailMessage): Promise<EmailResult>;
27
+ health(): { configured: boolean; capabilities: string[]; reason?: string };
28
+ };
29
+ ```
30
+
31
+ `EmailMessage` contains only normalized `from`, `to`, `subject`, `html`,
32
+ optional `text`, and optional `replyTo`. The Resend adapter uses the server-only
33
+ `RESEND_API_KEY` and `EMAIL_FROM`; it never accepts credentials from a request.
34
+ The webhook adapter preserves compatibility with existing mail gateways.
35
+
36
+ ## Media interface
37
+
38
+ An image/video/file adapter implements `MediaStorageProvider`:
39
+
40
+ ```ts
41
+ type MediaStorageProvider = {
42
+ id: string;
43
+ upload(input: MediaUpload): Promise<MediaUploadResult>;
44
+ health(): { configured: boolean; capabilities: string[]; reason?: string };
45
+ };
46
+ ```
47
+
48
+ The normalized result stores provider ID, storage key, delivery URL, byte
49
+ count, and MIME type. Local adapters may additionally return a filesystem path;
50
+ public delivery URL and local path must not be conflated. Cloudinary uploads
51
+ are signed server-side and expose only the resulting delivery URL to public
52
+ rendering.
53
+
54
+ ## Adding a provider
55
+
56
+ 1. Implement the relevant interface in `src/lib/providers/`.
57
+ 2. Normalize provider errors into `sent`, `queued`, `failed`, or upload failure
58
+ states; include a bounded, non-secret error message.
59
+ 3. Implement `health()` with capabilities and required configuration names.
60
+ 4. Register the adapter with `registerEmailProvider()` or
61
+ `registerMediaStorageProvider()`.
62
+ 5. Add env names to `.env.example`, but never add values or credentials to
63
+ generated files, logs, database settings, or browser bundles.
64
+ 6. Add a fixture/mock and a contract test for missing credentials, success,
65
+ retryable failure, and idempotent repeated calls.
66
+
67
+ Provider-specific options belong in provider configuration, not in the
68
+ canonical post or report schema.