@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.
- package/bin/maggie.js +152 -0
- package/bundled-references/ai-native-blog-contract.md +310 -0
- package/bundled-references/blog-data-contract.md +146 -0
- package/bundled-references/blog-implementation.md +46 -0
- package/bundled-references/blog-operations-contract.md +68 -0
- package/bundled-references/browser-inspection.md +39 -0
- package/bundled-references/provider-adapter-contract.md +68 -0
- package/bundled-references/seo-technical-contract.md +75 -0
- package/bundled-skills/README.md +16 -0
- package/bundled-skills/maggie-blog-bootstrap/SKILL.md +243 -0
- package/bundled-skills/maggie-clone/SKILL.md +213 -0
- package/bundled-skills/maggie-deployment/SKILL.md +61 -0
- package/bundled-skills/maggie-deployment/agents/openai.yaml +4 -0
- package/bundled-skills/maggie-deployment/references/cloudflare.md +76 -0
- package/bundled-skills/maggie-deployment/references/provider-contract.md +32 -0
- package/bundled-skills/maggie-project-context/SKILL.md +38 -0
- package/bundled-skills/maggie-seo-geo/SKILL.md +53 -0
- package/bundled-skills/maggie-social-share/SKILL.md +48 -0
- package/bundled-tools/clis/maggie.py +748 -0
- package/bundled-tools/clis/maggie_clone.py +82 -0
- package/bundled-tools/clis/site_audit.py +99 -0
- package/bundled-tools/integrations/analytics.md +34 -0
- package/bundled-tools/integrations/maggie-api-pull.md +72 -0
- package/bundled-tools/integrations/maggie-project-context.md +62 -0
- package/bundled-tools/integrations/maggie-seo-audit.md +16 -0
- package/bundled-tools/integrations/maggie-skills-api.md +76 -0
- package/bundled-tools/integrations/maggie-social-share.md +23 -0
- package/bundled-tools/integrations/maggie-visibility.md +22 -0
- package/package.json +29 -0
- package/references/ai-native-blog-contract.md +310 -0
- package/references/blog-data-contract.md +146 -0
- package/references/blog-implementation.md +46 -0
- package/references/blog-operations-contract.md +68 -0
- package/references/browser-inspection.md +39 -0
- package/references/provider-adapter-contract.md +68 -0
- package/references/seo-technical-contract.md +75 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Plan collision-safe Maggie clone namespaces without touching a project."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalized(url: str) -> str:
|
|
15
|
+
value = urlsplit(url.strip())
|
|
16
|
+
if value.scheme not in {"http", "https"} or not value.netloc:
|
|
17
|
+
raise ValueError(f"invalid URL: {url}")
|
|
18
|
+
host = value.hostname.lower() if value.hostname else ""
|
|
19
|
+
port = value.port
|
|
20
|
+
default = (value.scheme == "http" and port == 80) or (value.scheme == "https" and port == 443)
|
|
21
|
+
authority = host if not port or default else f"{host}:{port}"
|
|
22
|
+
path = value.path or "/"
|
|
23
|
+
if path != "/":
|
|
24
|
+
path = "/" + "/".join(part for part in path.split("/") if part)
|
|
25
|
+
query = urlencode(sorted(parse_qsl(value.query, keep_blank_values=True)))
|
|
26
|
+
return urlunsplit((value.scheme, authority, path, query, value.fragment))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def key_part(value: str, fallback: str) -> str:
|
|
30
|
+
cleaned = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
|
|
31
|
+
return cleaned[:48] or fallback
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def plan(urls: list[str], project: Path) -> list[dict[str, str]]:
|
|
35
|
+
result = []
|
|
36
|
+
seen: set[tuple[str, str]] = set()
|
|
37
|
+
for raw in urls:
|
|
38
|
+
url = normalized(raw)
|
|
39
|
+
parts = urlsplit(url)
|
|
40
|
+
origin = f"{parts.scheme}://{parts.netloc}"
|
|
41
|
+
path_state = parts.path + (f"?{parts.query}" if parts.query else "") + (f"#{parts.fragment}" if parts.fragment else "")
|
|
42
|
+
site_hash = hashlib.sha256(origin.encode()).hexdigest()[:8]
|
|
43
|
+
page_hash = hashlib.sha256(path_state.encode()).hexdigest()[:8]
|
|
44
|
+
identity = (origin, path_state)
|
|
45
|
+
if identity in seen:
|
|
46
|
+
raise ValueError(f"duplicate target after normalization: {raw}")
|
|
47
|
+
seen.add(identity)
|
|
48
|
+
site_key = f"{key_part(parts.netloc, 'site')}-{site_hash}"
|
|
49
|
+
segments = [part for part in parts.path.split("/") if part]
|
|
50
|
+
page_slug = "-".join(key_part(part, "page") for part in segments) if segments else "root"
|
|
51
|
+
page_key = f"{page_slug[:48]}-{page_hash}"
|
|
52
|
+
route = parts.path or "/"
|
|
53
|
+
result.append({
|
|
54
|
+
"source_url": url,
|
|
55
|
+
"origin": origin,
|
|
56
|
+
"destination_route": route,
|
|
57
|
+
"site_key": site_key,
|
|
58
|
+
"page_key": page_key,
|
|
59
|
+
"artifact_root": str(project / "docs/research" / site_key / page_key),
|
|
60
|
+
"screenshot_root": str(project / "docs/design-references" / site_key / page_key),
|
|
61
|
+
"component_root": str(project / "src/components/sites" / site_key / page_key),
|
|
62
|
+
"asset_root": str(project / "public/sites" / site_key / page_key),
|
|
63
|
+
})
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main() -> int:
|
|
68
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
69
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
70
|
+
command = sub.add_parser("plan", help="emit a read-only clone output plan")
|
|
71
|
+
command.add_argument("urls", nargs="+", help="authorized target URLs")
|
|
72
|
+
command.add_argument("--project", type=Path, default=Path.cwd())
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
try:
|
|
75
|
+
print(json.dumps({"project_root": str(args.project.resolve()), "targets": plan(args.urls, args.project.resolve())}, indent=2))
|
|
76
|
+
except ValueError as error:
|
|
77
|
+
parser.error(str(error))
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Small dependency-free SEO/GEO smoke audit for a public site."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from html.parser import HTMLParser
|
|
11
|
+
from urllib.parse import urljoin, urlparse
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PageParser(HTMLParser):
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.title = ""
|
|
19
|
+
self.h1 = 0
|
|
20
|
+
self.canonical = ""
|
|
21
|
+
self.jsonld = 0
|
|
22
|
+
self.anchors = 0
|
|
23
|
+
self.in_title = False
|
|
24
|
+
|
|
25
|
+
def handle_starttag(self, tag, attrs):
|
|
26
|
+
data = dict(attrs)
|
|
27
|
+
if tag == "title":
|
|
28
|
+
self.in_title = True
|
|
29
|
+
elif tag == "h1":
|
|
30
|
+
self.h1 += 1
|
|
31
|
+
elif tag == "link" and data.get("rel", "").lower() == "canonical":
|
|
32
|
+
self.canonical = data.get("href", "")
|
|
33
|
+
elif tag == "script" and data.get("type") == "application/ld+json":
|
|
34
|
+
self.jsonld += 1
|
|
35
|
+
elif tag == "a" and data.get("href"):
|
|
36
|
+
self.anchors += 1
|
|
37
|
+
|
|
38
|
+
def handle_endtag(self, tag):
|
|
39
|
+
if tag == "title":
|
|
40
|
+
self.in_title = False
|
|
41
|
+
|
|
42
|
+
def handle_data(self, data):
|
|
43
|
+
if self.in_title:
|
|
44
|
+
self.title += data.strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fetch(url: str) -> tuple[int, str, str]:
|
|
48
|
+
request = Request(url, headers={"User-Agent": "AI-CMO-Skills-Audit/0.1"})
|
|
49
|
+
with urlopen(request, timeout=15) as response:
|
|
50
|
+
body = response.read(2_000_000).decode("utf-8", "replace")
|
|
51
|
+
return response.status, response.headers.get_content_type(), body
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main() -> int:
|
|
55
|
+
parser = argparse.ArgumentParser()
|
|
56
|
+
parser.add_argument("url")
|
|
57
|
+
parser.add_argument("--json", action="store_true", dest="as_json")
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
base = args.url.rstrip("/")
|
|
60
|
+
checks = {}
|
|
61
|
+
try:
|
|
62
|
+
status, content_type, html = fetch(base)
|
|
63
|
+
page = PageParser()
|
|
64
|
+
page.feed(html)
|
|
65
|
+
checks["homepage"] = {"status": status, "content_type": content_type}
|
|
66
|
+
checks["title"] = {"ok": bool(page.title), "value": page.title}
|
|
67
|
+
checks["h1"] = {"ok": page.h1 == 1, "count": page.h1}
|
|
68
|
+
checks["canonical"] = {"ok": bool(page.canonical), "value": page.canonical}
|
|
69
|
+
checks["article_jsonld"] = {"ok": page.jsonld > 0, "count": page.jsonld}
|
|
70
|
+
checks["crawlable_links"] = {"ok": page.anchors > 0, "count": page.anchors}
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
checks["homepage"] = {"ok": False, "error": type(exc).__name__}
|
|
73
|
+
|
|
74
|
+
for name, path in (("robots", "/robots.txt"), ("sitemap", "/sitemap.xml")):
|
|
75
|
+
try:
|
|
76
|
+
status, content_type, body = fetch(urljoin(base + "/", path.lstrip("/")))
|
|
77
|
+
checks[name] = {
|
|
78
|
+
"ok": status == 200,
|
|
79
|
+
"status": status,
|
|
80
|
+
"content_type": content_type,
|
|
81
|
+
"mentions_sitemap": "sitemap" in body.lower() if name == "robots" else None,
|
|
82
|
+
"url_count": len(re.findall(r"<loc>.*?</loc>", body, re.I | re.S)) if name == "sitemap" else None,
|
|
83
|
+
}
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
checks[name] = {"ok": False, "error": type(exc).__name__}
|
|
86
|
+
|
|
87
|
+
result = {"url": base, "checks": checks, "passed": all(v.get("ok", False) for v in checks.values())}
|
|
88
|
+
if args.as_json:
|
|
89
|
+
print(json.dumps(result, indent=2))
|
|
90
|
+
else:
|
|
91
|
+
for name, value in checks.items():
|
|
92
|
+
print(f"{'PASS' if value.get('ok') else 'FAIL'} {name}: {value}")
|
|
93
|
+
print("RESULT:", "PASS" if result["passed"] else "FAIL")
|
|
94
|
+
return 0 if result["passed"] else 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
sys.exit(main())
|
|
99
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# GA4 and Google Search Console
|
|
2
|
+
|
|
3
|
+
## GA4
|
|
4
|
+
|
|
5
|
+
Use the host framework's supported Google tag integration. The minimum
|
|
6
|
+
configuration is:
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
NEXT_PUBLIC_GA4_MEASUREMENT_ID=G-...
|
|
10
|
+
GA4_ENABLED=true
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For another framework, use equivalent names in `.env.example`. The integration
|
|
14
|
+
must be disabled when the measurement ID is absent or consent is not granted.
|
|
15
|
+
Do not send post body, API keys, or unnecessary personal data as event params.
|
|
16
|
+
|
|
17
|
+
Recommended events: `page_view` (automatic), `blog_post_view`, and an explicit
|
|
18
|
+
conversion event only when the host project defines one.
|
|
19
|
+
|
|
20
|
+
## GSC
|
|
21
|
+
|
|
22
|
+
Prefer DNS verification for production. If HTML verification is needed, use a
|
|
23
|
+
public token environment variable and render the exact verification tag. OAuth
|
|
24
|
+
refresh tokens and service-account JSON stay server-side and out of the repo.
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
GSC_SITE_URL=https://example.com
|
|
28
|
+
GSC_VERIFICATION_TOKEN=replace-me
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
GSC data is read-only in this starter. Any query, page, or indexing report must
|
|
32
|
+
show its date window and property so an agent does not confuse an empty result
|
|
33
|
+
with a failed connection.
|
|
34
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Maggie API-Pull Integration
|
|
2
|
+
|
|
3
|
+
The public edge is `https://api.cmo.so`. The API key is sent as:
|
|
4
|
+
|
|
5
|
+
```http
|
|
6
|
+
Authorization: Bearer aicmo_...
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Never expose this key in browser JavaScript, client bundles, logs, screenshots,
|
|
10
|
+
or public repositories. Keep polling server-side or in a protected job.
|
|
11
|
+
|
|
12
|
+
## Lifecycle
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
GET /whoami
|
|
16
|
+
-> POST /sitemap/match or /sitemap/auto-detect
|
|
17
|
+
-> GET /sitemap/matching-history
|
|
18
|
+
-> POST /rewrite/queue (optional explicit rewrite)
|
|
19
|
+
-> GET /rewrite/queue
|
|
20
|
+
-> GET /posts and GET /updates on a schedule
|
|
21
|
+
-> upsert local content and publish according to local approval policy
|
|
22
|
+
-> POST /content-tracking/report-state
|
|
23
|
+
-> GET /rewrite/history/{content_id}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Project context synchronization
|
|
27
|
+
|
|
28
|
+
`GET /project-context` returns the sanitized, read-only context for the API
|
|
29
|
+
key's AI CMO project. The key is the scope; clients must not accept a project
|
|
30
|
+
id from a browser or public request.
|
|
31
|
+
|
|
32
|
+
The response includes brand, product/service positioning, audience, region,
|
|
33
|
+
brand voice, site URL, and the structured project CTA. It deliberately omits
|
|
34
|
+
team data, uploads, testimonials, market research, and internal workflow state.
|
|
35
|
+
Use the response `ETag` with `If-None-Match` for scheduled syncs, and write the
|
|
36
|
+
result to a local generated file rather than modifying source-of-truth files by
|
|
37
|
+
hand. Map `project.cta.default_url` and `project.cta.other_urls` into
|
|
38
|
+
`conversion.ctas.default` and `conversion.ctas.additional` while preserving
|
|
39
|
+
the original payload for forwards compatibility.
|
|
40
|
+
|
|
41
|
+
## Endpoints
|
|
42
|
+
|
|
43
|
+
| Endpoint | Use | Key |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `GET /whoami` | Resolve domain and capabilities | secret or publishable |
|
|
46
|
+
| `GET /project-context` | Sync sanitized project, brand voice and CTA context | secret |
|
|
47
|
+
| `GET /posts` | Pull one new post | secret |
|
|
48
|
+
| `GET /updates` | Pull one rewrite | secret |
|
|
49
|
+
| `GET /history` | Recover delivered posts | secret |
|
|
50
|
+
| `GET /rewrite-policy` | Read rewrite policy | secret |
|
|
51
|
+
| `POST /rewrite-policy` | Set auto/manual policy | secret |
|
|
52
|
+
| `POST /sitemap/match` | Match supplied sitemap | secret |
|
|
53
|
+
| `POST /sitemap/auto-detect` | Discover and match sitemap | secret |
|
|
54
|
+
| `GET /sitemap/matching-history` | Inspect prior runs | publishable |
|
|
55
|
+
| `POST /rewrite/queue` | Generate one rewrite for a matched asset | secret |
|
|
56
|
+
| `GET /rewrite/queue` | Inspect cached picks and pending delivery | publishable |
|
|
57
|
+
| `GET /rewrite/history/{content_id}` | Inspect rewrite delivery attempts | publishable |
|
|
58
|
+
| `POST /content-tracking/report-state` | Report local publication state | secret |
|
|
59
|
+
| `POST /content-tracking/report-state/batch` | Report bounded inventory batch | secret |
|
|
60
|
+
|
|
61
|
+
## Adapter requirements
|
|
62
|
+
|
|
63
|
+
- Store remote `content_id`, `version`, `slug`, and `canonical_url` locally.
|
|
64
|
+
- Upsert new posts idempotently. A retry must not create a duplicate slug.
|
|
65
|
+
- On rewrite, update the existing record at the same canonical URL. Do not
|
|
66
|
+
replace the slug unless the API contract explicitly changes.
|
|
67
|
+
- Treat an empty `/posts` or `/updates` response as normal, not an error.
|
|
68
|
+
- Back off on 429/5xx and include an idempotency key where supported.
|
|
69
|
+
- Do not ack a local update until the new body is stored and the local publish
|
|
70
|
+
result is known.
|
|
71
|
+
- Use `report-state` for observed URL/status/hash so the tracking record can be
|
|
72
|
+
reconciled later.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Maggie Project Context Adapter
|
|
2
|
+
|
|
3
|
+
This adapter connects a host project to the AI CMO source of truth without
|
|
4
|
+
requiring the host project to know the AI CMO database schema.
|
|
5
|
+
|
|
6
|
+
## Request
|
|
7
|
+
|
|
8
|
+
```http
|
|
9
|
+
GET https://api.cmo.so/project-context
|
|
10
|
+
Authorization: Bearer aicmo_...
|
|
11
|
+
If-None-Match: "previous-etag"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The API key scopes the request to one domain, AI CMO workspace and Project. The
|
|
15
|
+
request must run server-side. A `304` means the generated local context remains
|
|
16
|
+
current.
|
|
17
|
+
|
|
18
|
+
## Safe response usage
|
|
19
|
+
|
|
20
|
+
Use these response fields to generate or refresh local files:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
project.brand/title/description
|
|
24
|
+
project.products_services/usp
|
|
25
|
+
project.target_problem/target_audience/target_region
|
|
26
|
+
project.keywords/industries
|
|
27
|
+
project.cta
|
|
28
|
+
brand_voice
|
|
29
|
+
site.domain/base_url
|
|
30
|
+
sync.updated_at
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Keep `context` intact for forward compatibility, but do not expose it directly
|
|
34
|
+
to browsers unless the host project has explicitly reviewed every field.
|
|
35
|
+
|
|
36
|
+
## CTA normalization
|
|
37
|
+
|
|
38
|
+
The source CTA can contain `default_url` and `other_urls`, where each item may
|
|
39
|
+
contain `cta_url`, `url`, `href`, text, target keyword, priority, and additional
|
|
40
|
+
metadata. Preserve all metadata and normalize only the local view:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"conversion": {
|
|
45
|
+
"ctas": {
|
|
46
|
+
"default": { "cta_url": "https://example.com/start" },
|
|
47
|
+
"additional": []
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Never invent a fallback CTA when the Project has no approved CTA. Ask for an
|
|
54
|
+
explicit destination or keep the content in preview mode.
|
|
55
|
+
|
|
56
|
+
## Refresh policy
|
|
57
|
+
|
|
58
|
+
- manual sync before bootstrap or a content strategy change;
|
|
59
|
+
- scheduled sync with ETag revalidation after that;
|
|
60
|
+
- show changed fields before updating public copy or links;
|
|
61
|
+
- record `project_id`, `ai_cmo_id`, `updated_at`, ETag, timestamp and result;
|
|
62
|
+
- never commit `generated/ai-cmo-context.json` if it contains private context.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Maggie SEO Audit Integration
|
|
2
|
+
|
|
3
|
+
Use the deterministic local audit first, then the paid/server-side audit when
|
|
4
|
+
the user has access to the full AI CMO feature.
|
|
5
|
+
|
|
6
|
+
## Audit layers
|
|
7
|
+
|
|
8
|
+
1. Local: HTTP status, robots, sitemap, canonical, metadata, headings, links,
|
|
9
|
+
JSON-LD, image alt text and server-rendered post paths.
|
|
10
|
+
2. Provider: performance, accessibility, best practices and SEO diagnostics.
|
|
11
|
+
3. AI readiness: extractability, author/freshness/evidence signals, crawler
|
|
12
|
+
access and public machine-readable context.
|
|
13
|
+
|
|
14
|
+
Provider runs should be asynchronous, cached, bounded by entitlement, and
|
|
15
|
+
reported with `job_id`, `audit_id`, `data_through`, and issue severity. A
|
|
16
|
+
provider outage must not be presented as a site failure.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Maggie Skills API (versioned)
|
|
2
|
+
|
|
3
|
+
This is the additive API for the Skills workflow. It does not replace the
|
|
4
|
+
legacy API Pull contract in [maggie-api-pull.md](./maggie-api-pull.md).
|
|
5
|
+
|
|
6
|
+
The legacy guide remains authoritative for the existing root endpoints. New
|
|
7
|
+
Skills integrations should use this namespace for lifecycle operations; do not
|
|
8
|
+
rewrite existing `/posts`, `/updates`, or `/rewrite/queue` adapters.
|
|
9
|
+
|
|
10
|
+
Base URL:
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
https://api.cmo.so/api/v1/maggie
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Authentication is server-side only:
|
|
17
|
+
|
|
18
|
+
```http
|
|
19
|
+
Authorization: Bearer aicmo_...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The edge is only an API-key proxy. Data and mutations are handled by TOPY and
|
|
23
|
+
the shared MySQL database. Never put the key in browser code or a public build.
|
|
24
|
+
|
|
25
|
+
## Contract discovery
|
|
26
|
+
|
|
27
|
+
Use `GET /api-version` to identify the additive contract and
|
|
28
|
+
`GET /capabilities` to feature-detect optional capabilities. The current
|
|
29
|
+
published OpenAPI contract is the source of truth for method details. Read-only
|
|
30
|
+
operations do not consume AI quota. Operations that may consume quota return
|
|
31
|
+
the `X-Quota-*` headers; callers must preserve and report them.
|
|
32
|
+
|
|
33
|
+
## Endpoint map
|
|
34
|
+
|
|
35
|
+
| Area | Endpoints |
|
|
36
|
+
|---|---|
|
|
37
|
+
| Version and usage | `GET /api-version`, `GET /capabilities`, `GET /entitlements`, `GET /usage`, `GET /usage/events`, `POST /usage/events` |
|
|
38
|
+
| Pull runs | `GET/POST /pull/runs`, `GET /pull/runs/{record_id}`, `GET /pull/runs/{record_id}/items` |
|
|
39
|
+
| Content assets | `GET /content-assets`, `GET/POST /content-assets/backfill`, `GET /content-assets/{record_id}`, `GET/PATCH /content-assets/{record_id}/identity`, `GET /content-assets/{record_id}/versions` |
|
|
40
|
+
| Sitemap sources | `GET/POST /sitemap/sources`, `GET/PATCH/DELETE /sitemap/sources/{record_id}`, plus the same `GET/POST` and `GET/PATCH/DELETE` plural `/sitemaps/sources` routes |
|
|
41
|
+
| Sitemap runs | `GET/POST /sitemap/runs`, `GET /sitemap/runs/{record_id}`, `GET /sitemap/runs/{record_id}/cancel`, plus the same `GET/POST`, `GET`, and `GET` cancel plural `/sitemaps/runs` routes |
|
|
42
|
+
| Sitemap schedules | `GET/POST /sitemap/schedules`, `GET/POST /sitemaps/schedules` (the current contract has no item route for schedules) |
|
|
43
|
+
| Rewrite jobs | `GET/POST /rewrite/jobs`, `GET/POST /rewrite/jobs/batch`, `GET /rewrite/jobs/{record_id}`, `GET /rewrite/jobs/{record_id}/preview|approve|reject|cancel|retry`, `GET /rewrite/jobs/{record_id}/versions` |
|
|
44
|
+
| SEO | `GET/POST /seo/audits`, `GET /seo/audits/{record_id}`, `GET/POST /seo/insights` |
|
|
45
|
+
| Visibility | `GET/POST /visibility/runs`, `GET /visibility/runs/{record_id}`, `GET/POST /visibility/prompts`, `GET/POST /visibility/competitors` |
|
|
46
|
+
| Projects and sites | `GET /projects`, `GET /projects/{record_id}`, `GET /projects/{record_id}/context|ctas`, `GET /sites`, `GET /sites/{record_id}`, `GET /sites/{record_id}/context|settings`, `PATCH /sites/{record_id}/settings` |
|
|
47
|
+
| Reports | `GET/POST /reports`, `GET /reports/{record_id}`, `GET/POST /reports/{record_id}/deliver`, `GET /reports/{record_id}/deliveries` |
|
|
48
|
+
| Jobs | `GET/POST /scheduled-jobs`, `GET/POST /jobs`, `GET /jobs/{record_id}`, `GET/POST /jobs/{record_id}/run` |
|
|
49
|
+
| Social | `GET/POST /social/drafts`, `GET /social/drafts/{record_id}`, `GET /social/drafts/{record_id}/approve|reject|schedule|publish`, `GET/POST /social/accounts`, `GET/POST /social/calendars`, `GET/POST /social/schedules`, `GET/POST /social/metrics`, `GET/POST /social/publish`, `GET /social/publish/{record_id}` |
|
|
50
|
+
| Deployments | `GET /deployment/providers`, `GET/POST /deployment/validate`, `GET/POST /deployment/registrations`, `GET /deployment/registrations/{record_id}`, `GET /deployment/registrations/{record_id}/report`; plural routes: `GET /deployments/providers`, `GET/POST /deployments/validate`, `GET/POST /deployments`, `GET /deployments/{record_id}/report` |
|
|
51
|
+
|
|
52
|
+
`{record_id}` is the resource identifier returned by the API. Treat status and
|
|
53
|
+
response fields as forward-compatible; unknown fields must be preserved.
|
|
54
|
+
|
|
55
|
+
## Workflow
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
api-version/capabilities
|
|
59
|
+
-> pull run or content-assets backfill
|
|
60
|
+
-> sitemap source/run
|
|
61
|
+
-> rewrite job preview
|
|
62
|
+
-> approve/reject/cancel
|
|
63
|
+
-> local publish and report-state (legacy endpoint)
|
|
64
|
+
-> usage/history/readback
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use `Idempotency-Key` for every mutation that can be retried. Do not treat a
|
|
68
|
+
queued response as delivered or published. Keep the legacy `/posts`,
|
|
69
|
+
`/updates`, `/project-context`, `/sitemap/match`, `/rewrite/queue`, and
|
|
70
|
+
`/content-tracking/report-state` calls unchanged when using the established
|
|
71
|
+
API Pull adapter.
|
|
72
|
+
|
|
73
|
+
For legacy adapter discovery, `GET https://api.cmo.so/whoami` remains available
|
|
74
|
+
with the original response contract. It is intentionally separate from the
|
|
75
|
+
versioned Skills namespace; new clients should discover the versioned contract
|
|
76
|
+
with `/api-version` and `/capabilities`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Maggie Social Share Integration
|
|
2
|
+
|
|
3
|
+
Social Share consumes approved AI CMO content assets and produces channel
|
|
4
|
+
derivatives. It must retain `source_content_id`, canonical URL, Project CTA,
|
|
5
|
+
voice/tone, approval state, provider post ID, and publication attempts.
|
|
6
|
+
|
|
7
|
+
## Provider contract
|
|
8
|
+
|
|
9
|
+
Each channel adapter declares OAuth scopes, supported formats, max lengths,
|
|
10
|
+
media requirements, publish/delete/republish support, retry classification and
|
|
11
|
+
idempotency behavior. Start with LinkedIn and Facebook; do not claim support for
|
|
12
|
+
a provider until its adapter and production credentials are verified.
|
|
13
|
+
|
|
14
|
+
## State machine
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
draft -> approved -> scheduled -> publishing -> published
|
|
18
|
+
\-> cancelled
|
|
19
|
+
publishing -> failed -> retrying -> published | failed
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Publishing requires a paid entitlement, active connection and explicit
|
|
23
|
+
approval. A generated social draft is never proof that a provider post exists.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Maggie Visibility Integration
|
|
2
|
+
|
|
3
|
+
This is the contract for AI SEO/GEO visibility work. The exact public route
|
|
4
|
+
availability is versioned in AI CMO's OpenAPI document; do not hard-code private
|
|
5
|
+
Geo service credentials in a host project.
|
|
6
|
+
|
|
7
|
+
## Required behavior
|
|
8
|
+
|
|
9
|
+
- scope every request to the authenticated domain/project;
|
|
10
|
+
- distinguish quick prompt checks from recurring monitoring;
|
|
11
|
+
- enforce server-side feature quota and subscription entitlement;
|
|
12
|
+
- record query set, provider, run time, result status, citations and source URLs;
|
|
13
|
+
- report observed retrieved/cited/mentioned/recommended states separately;
|
|
14
|
+
- treat provider failures as retryable operational errors, not zero visibility;
|
|
15
|
+
- never claim ranking, citation or traffic improvement without measured data.
|
|
16
|
+
|
|
17
|
+
## Host output
|
|
18
|
+
|
|
19
|
+
The skill should produce a preview/report containing query coverage, competitor
|
|
20
|
+
presence, citation/source gaps, technical blockers, and the next content action.
|
|
21
|
+
It should not directly rewrite or publish content until the content workflow's
|
|
22
|
+
approval state allows it.
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@topy-ai/maggie",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install and manage Maggie Skills for AI coding agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"maggie": "bin/maggie.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"references",
|
|
13
|
+
"bundled-skills",
|
|
14
|
+
"bundled-references",
|
|
15
|
+
"bundled-tools"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepack": "node ../../scripts/package-cli.mjs"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/TOPY-AI-LTD/ai-cmo-skills.git",
|
|
26
|
+
"directory": "packages/maggie-cli"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/TOPY-AI-LTD/ai-cmo-skills"
|
|
29
|
+
}
|