@topy-ai/maggie 0.7.0 → 0.7.1

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 (31) hide show
  1. package/README.md +22 -7
  2. package/bin/maggie.js +6 -6
  3. package/bundled-references/maggiedash-dashboard-ui.md +28 -0
  4. package/bundled-skills/maggie-blog/SKILL.md +12 -0
  5. package/bundled-skills/maggie-content-localization/SKILL.md +9 -0
  6. package/bundled-skills/maggie-dash/SKILL.md +21 -0
  7. package/bundled-skills/maggie-deployment/SKILL.md +17 -0
  8. package/bundled-skills/maggie-ops/SKILL.md +27 -0
  9. package/bundled-skills/maggie-seo-geo/SKILL.md +21 -1
  10. package/bundled-skills/maggie-service-booking/SKILL.md +6 -0
  11. package/bundled-templates/maggiedash/README.md +4 -0
  12. package/bundled-templates/maggiedash/dashboard-ui-contract.json +31 -0
  13. package/bundled-tools/clis/maggie_analytics.py +14 -1
  14. package/bundled-tools/clis/maggie_blog.py +6 -0
  15. package/bundled-tools/clis/maggie_dash.py +54 -0
  16. package/bundled-tools/clis/maggie_deployment.py +43 -0
  17. package/bundled-tools/clis/maggie_feedback.py +16 -1
  18. package/bundled-tools/clis/maggie_ops.py +21 -1
  19. package/bundled-tools/clis/maggie_service_booking.py +6 -3
  20. package/bundled-tools/clis/maggie_sitemap.py +16 -2
  21. package/bundled-tools/runtime/analytics_traffic.py +30 -0
  22. package/bundled-tools/runtime/content_localization.py +63 -1
  23. package/bundled-tools/runtime/dependency_lock.py +43 -0
  24. package/bundled-tools/runtime/integration_state.py +17 -0
  25. package/bundled-tools/runtime/maggie_dash_store.py +150 -6
  26. package/bundled-tools/runtime/maggie_dash_ui.py +60 -0
  27. package/bundled-tools/runtime/maggie_sitemap.py +50 -4
  28. package/bundled-tools/runtime/route_imports.py +51 -0
  29. package/bundled-tools/runtime/seed_evidence.py +25 -0
  30. package/package.json +1 -1
  31. package/references/maggiedash-dashboard-ui.md +28 -0
@@ -55,6 +55,12 @@ def filter_routes(routes: list[dict[str, str]], origin: str, content_types: set[
55
55
  reason = "content type not enabled"
56
56
  elif not absolute_url(url, origin):
57
57
  reason = "URL must be an absolute HTTP(S) URL on the approved origin"
58
+ elif route.get("indexable") is False:
59
+ reason = "route is explicitly non-indexable"
60
+ elif route.get("canonicalUrl") and route.get("canonicalUrl") != url:
61
+ reason = "route canonical URL does not self-canonicalize"
62
+ elif route.get("searchable") is False:
63
+ reason = "route is explicitly not searchable"
58
64
  elif EXCLUDED_PATHS.search(parsed.path) or any(key in parse_qs(parsed.query) for key in ("q", "search", "filter", "page")):
59
65
  reason = "search, filter, or non-canonical route"
60
66
  elif route.get("type", "").lower() in {"rss", "atom", "feed"}:
@@ -86,12 +92,12 @@ def build_plan(routes: list[dict[str, str]], origin: str, content_types: set[str
86
92
  for content_type in sorted(content_types):
87
93
  chunks = []
88
94
  entries = groups.get(content_type, []) or []
89
- for index in range(0, max(len(entries), 1), chunk_target):
95
+ for index in range(0, len(entries), chunk_target):
90
96
  chunk_entries = entries[index:index + chunk_target]
91
97
  filename = f"sitemap-{content_type}-{index // chunk_target + 1}.xml"
92
98
  xml = xml_file(chunk_entries)
93
99
  url = origin.rstrip("/") + "/" + filename
94
- chunks.append({"filename": filename, "url": url, "entries": len(chunk_entries), "bytes": len(xml.encode()), "sha256": hashlib.sha256(xml.encode()).hexdigest(), "xml": xml})
100
+ chunks.append({"filename": filename, "url": url, "entries": len(chunk_entries), "routes": chunk_entries, "bytes": len(xml.encode()), "sha256": hashlib.sha256(xml.encode()).hexdigest(), "xml": xml})
95
101
  sitemap_urls.append(url)
96
102
  group_plans.append({"contentType": content_type, "chunks": chunks})
97
103
  index_xml = '<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + "".join(f"<sitemap><loc>{escape(url)}</loc></sitemap>" for url in sitemap_urls) + "</sitemapindex>\n"
@@ -103,7 +109,7 @@ def build_plan(routes: list[dict[str, str]], origin: str, content_types: set[str
103
109
  return plan
104
110
 
105
111
 
106
- def validate_plan_data(plan: dict) -> dict:
112
+ def validate_plan_data(plan: dict, strict_semantic: bool = False) -> dict:
107
113
  errors = []
108
114
  warnings = []
109
115
  origin = plan.get("origin", "")
@@ -117,6 +123,21 @@ def validate_plan_data(plan: dict) -> dict:
117
123
  errors.append(f"chunk exceeds URL limit: {chunk.get('filename')}")
118
124
  if chunk.get("bytes", 0) > HARD_BYTES_LIMIT:
119
125
  errors.append(f"chunk exceeds byte limit: {chunk.get('filename')}")
126
+ for route in chunk.get("routes", []):
127
+ if route.get("indexable") is False or route.get("searchable") is False:
128
+ errors.append(f"non-indexable/searchable route was emitted: {route.get('url')}")
129
+ if route.get("canonicalUrl") and route.get("canonicalUrl") != route.get("url"):
130
+ errors.append(f"non-self-canonical route was emitted: {route.get('url')}")
131
+ if "contentWords" in route and int(route.get("contentWords") or 0) < 1:
132
+ errors.append(f"route has no searchable content evidence: {route.get('url')}")
133
+ if "title" in route and not str(route.get("title") or "").strip():
134
+ errors.append(f"route has no title evidence: {route.get('url')}")
135
+ if route.get("lastmod") and route.get("lastmodSource") in {"updated_at", "sync", "pull", "deploy"}:
136
+ errors.append(f"lastmod is derived from an operational timestamp: {route.get('url')}")
137
+ if route.get("lastmod") and route.get("lastmodSource") not in {None, "content-change", "source-revision"}:
138
+ errors.append(f"lastmod provenance is not a content change: {route.get('url')}")
139
+ if route.get("lastmod") and strict_semantic and route.get("lastmodSource") not in {"content-change", "source-revision"}:
140
+ errors.append(f"lastmod evidence is required for strict semantic validation: {route.get('url')}")
120
141
  try:
121
142
  root = ElementTree.fromstring(chunk.get("xml", ""))
122
143
  locs = [element.text or "" for element in root.iter() if element.tag.endswith("loc")]
@@ -140,5 +161,30 @@ def validate_plan_data(plan: dict) -> dict:
140
161
  except ElementTree.ParseError:
141
162
  errors.append("sitemap index is not valid XML")
142
163
  if not expected_urls:
143
- warnings.append("no sitemap chunks generated")
164
+ warnings.append("no sitemap chunks generated; empty content types are not advertised")
165
+ if strict_semantic:
166
+ for group in plan.get("groups", []):
167
+ for chunk in group.get("chunks", []):
168
+ for route in chunk.get("routes", []):
169
+ if not str(route.get("title") or "").strip(): warnings.append(f"semantic title evidence missing: {route.get('url')}")
170
+ if "contentWords" not in route: warnings.append(f"semantic content evidence missing: {route.get('url')}")
171
+ if warnings:
172
+ errors.extend(warnings)
144
173
  return {"status": "pass" if not errors else "fail", "errors": errors, "warnings": warnings}
174
+
175
+
176
+ def agent_files(routes: list[dict[str, str]], origin: str, locale: str = "en") -> dict[str, str]:
177
+ """Generate compact, locale-aware agent discovery documents."""
178
+ visible = [route for route in routes if route.get("indexable") is not False and route.get("searchable") is not False and absolute_url(route.get("url", ""), origin)]
179
+ visible.sort(key=lambda route: route.get("url", ""))
180
+ lines = [f"# {origin} ({locale})", "", "This file lists indexable public routes for agent discovery.", "", "## Routes"]
181
+ for route in visible:
182
+ label = route.get("title") or route.get("url", "").rstrip("/").rsplit("/", 1)[-1] or "home"
183
+ lines.append(f"- [{label}]({route['url']})")
184
+ sitemap = [f"# Sitemap ({locale})", "", f"- XML sitemap: {origin.rstrip('/')}/sitemap.xml", "", "## Public routes"]
185
+ sitemap.extend(f"- {route['url']}" for route in visible)
186
+ insights = [f"# Content insights ({locale})", "", f"- Indexable routes: {len(visible)}", f"- Content types: {', '.join(sorted({str(route.get('type', 'unknown')) for route in visible})) or 'none'}", "", "## Freshness"]
187
+ for route in visible:
188
+ if route.get("lastmod"):
189
+ insights.append(f"- {route['url']}: last content change {route['lastmod']}")
190
+ return {"llms.txt": "\n".join(lines) + "\n", "sitemap.md": "\n".join(sitemap) + "\n", "insights.md": "\n".join(insights) + "\n"}
@@ -0,0 +1,51 @@
1
+ """Resolve route components from source imports, never from sibling basenames."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import re
7
+ from pathlib import Path
8
+
9
+
10
+ IMPORT_RE = re.compile(r"(?:import\s+(?:[^;\n]*?\s+from\s+)?|export\s+[^;\n]*?\s+from\s+)[\"']([^\"']+)[\"']")
11
+ EXTENSIONS = ("", ".astro", ".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html")
12
+
13
+
14
+ def resolve_import(route: Path, specifier: str) -> Path | None:
15
+ if not specifier.startswith("."):
16
+ return None
17
+ base = (route.parent / specifier).resolve()
18
+ candidates = [base]
19
+ candidates.extend(Path(str(base) + extension) for extension in EXTENSIONS[1:])
20
+ candidates.extend(base / f"index{extension}" for extension in EXTENSIONS)
21
+ for candidate in candidates:
22
+ if candidate.is_file():
23
+ return candidate
24
+ return None
25
+
26
+
27
+ def imported_components(route: Path) -> list[dict[str, str | None]]:
28
+ """Return only source imports that resolve inside the project.
29
+
30
+ Bare package imports are intentionally recorded without a local path; they
31
+ are dependencies, not route-local components. The route source remains
32
+ the authority, so a same-named sibling file is never selected implicitly.
33
+ """
34
+ try:
35
+ source = route.read_text(encoding="utf-8", errors="replace")
36
+ except OSError:
37
+ return []
38
+ result = []
39
+ seen = set()
40
+ for specifier in IMPORT_RE.findall(source):
41
+ resolved = resolve_import(route, specifier)
42
+ key = (specifier, str(resolved) if resolved else None)
43
+ if key in seen:
44
+ continue
45
+ seen.add(key)
46
+ result.append({
47
+ "specifier": specifier,
48
+ "path": str(resolved) if resolved else None,
49
+ "fingerprint": hashlib.sha256(resolved.read_bytes()).hexdigest() if resolved else None,
50
+ })
51
+ return result
@@ -0,0 +1,25 @@
1
+ """Validation for explicit, sanitized development fixture evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ SECRET = re.compile(r"(?:secret|token|password|api[_-]?key|database_url)", re.I)
9
+
10
+
11
+ def validate_manifest(value: object) -> dict:
12
+ errors = []
13
+ if not isinstance(value, dict):
14
+ return {"schemaVersion": "maggie-seed-manifest.v1", "passed": False, "errors": ["manifest must be an object"]}
15
+ if value.get("schemaVersion") != "maggie-seed-manifest.v1": errors.append("unsupported schemaVersion")
16
+ if value.get("optIn") is not True: errors.append("optIn must be true")
17
+ if value.get("sanitized") is not True: errors.append("sanitized must be true")
18
+ fixtures = value.get("fixtures")
19
+ if not isinstance(fixtures, list) or not fixtures: errors.append("fixtures must be a non-empty list")
20
+ for item in fixtures if isinstance(fixtures, list) else []:
21
+ if not isinstance(item, dict) or not item.get("name") or not item.get("rows"):
22
+ errors.append("each fixture requires name and non-empty rows")
23
+ if SECRET.search(str(item)):
24
+ errors.append("fixture contains a secret-like field")
25
+ return {"schemaVersion": "maggie-seed-manifest.v1", "passed": not errors, "errors": errors, "fixtureCount": len(fixtures) if isinstance(fixtures, list) else 0}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topy-ai/maggie",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Install and manage Maggie Skills for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,28 @@
1
+ # MaggieDash dashboard UI contract
2
+
3
+ MaggieDash workspaces use the lightweight contract in
4
+ `templates/maggiedash/dashboard-ui-contract.json`. It is provider-neutral and
5
+ describes the dashboard chrome, not a framework-specific component library.
6
+
7
+ The reference is the users workspace: one `WorkspaceBar`, one sidebar/content
8
+ navigation relationship, and one `ContentTabs` row. A route must not render a
9
+ second horizontal navigation, duplicate its sidebar, or hide duplicate markup
10
+ with CSS. `WorkspaceCard` owns card framing; page routes own content.
11
+
12
+ `ContentTabs` accepts only the props it renders (`tabs`, `active`, `actions`,
13
+ and `children`). It does not accept a page `title` or `description`. If a page
14
+ needs a heading or description, render a dedicated page header or use
15
+ `WorkspaceBar` explicitly. This keeps component APIs honest and prevents
16
+ dead-copy drift.
17
+
18
+ Validate a host implementation with:
19
+
20
+ ```bash
21
+ maggie dash ui validate --contract templates/maggiedash/dashboard-ui-contract.json \
22
+ --source src/components/WorkspaceBar.tsx \
23
+ --source src/components/WorkspaceCard.tsx \
24
+ --source src/components/ContentTabs.tsx
25
+ ```
26
+
27
+ The check is static evidence. It does not replace an authenticated browser
28
+ review of the rendered dashboard at desktop and mobile sizes.