@rubytech/create-maxy-code 0.1.126 → 0.1.127
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/dist/index.js +12 -0
- package/package.json +1 -1
- package/payload/platform/config/brand.json +20 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts +33 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js +119 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/src/index.ts +172 -0
- package/payload/platform/lib/aeo-llms-txt-writer/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +73 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts +34 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js +176 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js.map +1 -0
- package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +19 -57
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts +5 -38
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts.map +1 -1
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js +7 -114
- package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js.map +1 -1
- package/payload/platform/templates/specialists/agents/content-producer.md +3 -3
- package/payload/server/public/consent.css +97 -0
- package/payload/server/public/consent.js +259 -0
- package/payload/server/public/privacy.html +68 -5
- package/payload/server/public/v.js +53 -0
- package/payload/server/server.js +229 -84
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { mkdir, readdir, rename, stat } from "node:fs/promises";
|
|
2
|
+
import { resolve as resolvePath, join, dirname } from "node:path";
|
|
3
|
+
import { writeLlmsTxt } from "../../../../../lib/aeo-llms-txt-writer/dist/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Publish an already-extracted static-site tree into the per-account
|
|
6
|
+
* static-publish surface (<accountDir>/sites/<slug>/) and emit one
|
|
7
|
+
* canonical path slug. Pure function — the MCP registration in index.ts
|
|
8
|
+
* adapts the discriminated-union result onto the MCP transport response.
|
|
9
|
+
*
|
|
10
|
+
* Invariants (each maps to one Refusal kind):
|
|
11
|
+
* - slug segments match SAFE_SEG_RE, no leading dot, no `..`
|
|
12
|
+
* - destination must be absent or empty
|
|
13
|
+
* - source tree must contain no symlinks at any depth
|
|
14
|
+
* - source must have at least one .html at the top level
|
|
15
|
+
* - if no index.html at top level, exactly one top-level .html
|
|
16
|
+
*
|
|
17
|
+
* Post-move:
|
|
18
|
+
* - canonical path is `/sites/<slug>/` if index.html at top level,
|
|
19
|
+
* otherwise `/sites/<slug>/<the-one-html>.html`
|
|
20
|
+
* - llms.txt + llms-full.txt are refreshed via writeLlmsTxt
|
|
21
|
+
* (best-effort: failure does not unwind the mv)
|
|
22
|
+
*
|
|
23
|
+
* All operator-visible log lines preserved verbatim from the prior
|
|
24
|
+
* skill-prose contract so existing grep targets keep working.
|
|
25
|
+
*/
|
|
26
|
+
const SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|
|
27
|
+
const TAG = "[publish-site]";
|
|
28
|
+
function refusal(kind, detail, logLine) {
|
|
29
|
+
process.stderr.write(`${TAG} refused reason=${kind} ${logLine}\n`);
|
|
30
|
+
return { ok: false, refusal: kind, detail };
|
|
31
|
+
}
|
|
32
|
+
function validateSlug(slug) {
|
|
33
|
+
const segments = slug.split("/").filter((s) => s.length > 0);
|
|
34
|
+
if (segments.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
refusal: refusal("unsafe-slug", { slug }, `slug=${slug}`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
for (const seg of segments) {
|
|
41
|
+
if (seg === "..") {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
refusal: refusal("unsafe-slug", { slug, segment: seg }, `slug=${slug}`),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (seg.startsWith(".")) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
refusal: refusal("unsafe-slug", { slug, segment: seg }, `slug=${slug}`),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (!SAFE_SEG_RE.test(seg)) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
refusal: refusal("unsafe-slug", { slug, segment: seg }, `slug=${slug}`),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, segments };
|
|
61
|
+
}
|
|
62
|
+
async function findSymlinkAnywhere(dir) {
|
|
63
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
64
|
+
for (const ent of entries) {
|
|
65
|
+
if (ent.isSymbolicLink())
|
|
66
|
+
return join(dir, ent.name);
|
|
67
|
+
if (ent.isDirectory()) {
|
|
68
|
+
const found = await findSymlinkAnywhere(join(dir, ent.name));
|
|
69
|
+
if (found)
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async function countFiles(dir) {
|
|
76
|
+
let n = 0;
|
|
77
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
78
|
+
for (const ent of entries) {
|
|
79
|
+
if (ent.isFile())
|
|
80
|
+
n += 1;
|
|
81
|
+
else if (ent.isDirectory())
|
|
82
|
+
n += await countFiles(join(dir, ent.name));
|
|
83
|
+
}
|
|
84
|
+
return n;
|
|
85
|
+
}
|
|
86
|
+
export async function publishSite(input, deps) {
|
|
87
|
+
const { accountId, accountDir, source, slug } = input;
|
|
88
|
+
const slugCheck = validateSlug(slug);
|
|
89
|
+
if (!slugCheck.ok)
|
|
90
|
+
return slugCheck.refusal;
|
|
91
|
+
const destination = resolvePath(accountDir, "sites", ...slugCheck.segments);
|
|
92
|
+
// Pre-flight: dest must be absent or empty.
|
|
93
|
+
try {
|
|
94
|
+
const destStat = await stat(destination);
|
|
95
|
+
if (destStat.isDirectory()) {
|
|
96
|
+
const entries = await readdir(destination);
|
|
97
|
+
if (entries.length > 0) {
|
|
98
|
+
return refusal("destination-occupied", { destination, entryCount: entries.length }, `dest=${destination}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
return refusal("destination-occupied", { destination, kind: "non-directory" }, `dest=${destination}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const code = err?.code;
|
|
107
|
+
if (code !== "ENOENT")
|
|
108
|
+
throw err;
|
|
109
|
+
// ENOENT is the happy path — destination doesn't exist yet.
|
|
110
|
+
}
|
|
111
|
+
// Pre-flight: source symlink scan (any depth).
|
|
112
|
+
const symlink = await findSymlinkAnywhere(source);
|
|
113
|
+
if (symlink) {
|
|
114
|
+
return refusal("symlink-in-source", { source, entry: symlink }, `entry=${symlink}`);
|
|
115
|
+
}
|
|
116
|
+
// Pre-flight: top-level html inventory.
|
|
117
|
+
const topLevel = await readdir(source, { withFileTypes: true });
|
|
118
|
+
const topHtml = topLevel
|
|
119
|
+
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".html"))
|
|
120
|
+
.map((e) => e.name);
|
|
121
|
+
if (topHtml.length === 0) {
|
|
122
|
+
return refusal("zero-html", { source }, `source=${source}`);
|
|
123
|
+
}
|
|
124
|
+
const hasIndex = topHtml.some((n) => n.toLowerCase() === "index.html");
|
|
125
|
+
if (!hasIndex && topHtml.length > 1) {
|
|
126
|
+
return refusal("ambiguous-html", { source, candidates: topHtml }, `candidates=${topHtml.join(",")}`);
|
|
127
|
+
}
|
|
128
|
+
const movedFiles = await countFiles(source);
|
|
129
|
+
// Move. Single rename — same filesystem assumed (accountDir-local).
|
|
130
|
+
// Ensure the parent (`<accountDir>/sites/...` up to the leaf) exists so
|
|
131
|
+
// the first publish on a fresh accountDir doesn't ENOENT. mkdir is
|
|
132
|
+
// idempotent on the parent; the leaf itself was confirmed absent above.
|
|
133
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
134
|
+
// EXDEV or EACCES propagates as an exception → MCP isError surfacing
|
|
135
|
+
// via the registration layer in index.ts.
|
|
136
|
+
await rename(source, destination);
|
|
137
|
+
process.stderr.write(`${TAG} move from=${source} to=${destination}/ files=${movedFiles}\n`);
|
|
138
|
+
const kind = hasIndex ? "index" : "file";
|
|
139
|
+
const pathSlug = hasIndex
|
|
140
|
+
? `/sites/${slugCheck.segments.join("/")}/`
|
|
141
|
+
: `/sites/${slugCheck.segments.join("/")}/${topHtml[0]}`;
|
|
142
|
+
process.stderr.write(`${TAG} url emitted=${pathSlug} kind=${kind}\n`);
|
|
143
|
+
// Post-move hook: refresh llms.txt at the site root. Best-effort.
|
|
144
|
+
// Never unwinds the mv — a stale llms.txt is strictly less bad than
|
|
145
|
+
// a failed publish.
|
|
146
|
+
let aeo;
|
|
147
|
+
try {
|
|
148
|
+
const llms = await writeLlmsTxt({
|
|
149
|
+
accountId,
|
|
150
|
+
siteName: slugCheck.segments[slugCheck.segments.length - 1],
|
|
151
|
+
siteDir: destination,
|
|
152
|
+
siteDescription: input.siteDescription,
|
|
153
|
+
}, { getSession: deps.getSession });
|
|
154
|
+
const indexPath = llms.writtenPaths?.index ?? "";
|
|
155
|
+
aeo = {
|
|
156
|
+
ok: true,
|
|
157
|
+
indexPath,
|
|
158
|
+
indexBytes: llms.sizeBytes.index,
|
|
159
|
+
};
|
|
160
|
+
process.stderr.write(`${TAG} aeo-llms-txt-ok wrote=${indexPath} indexBytes=${llms.sizeBytes.index}\n`);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
164
|
+
aeo = { ok: false, reason };
|
|
165
|
+
process.stderr.write(`${TAG} aeo-llms-txt-failed reason=${reason}\n`);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
pathSlug,
|
|
170
|
+
kind,
|
|
171
|
+
movedFiles,
|
|
172
|
+
destination,
|
|
173
|
+
aeo,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=publish-site.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-site.js","sourceRoot":"","sources":["../../src/tools/publish-site.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,75 +1,37 @@
|
|
|
1
1
|
# Publish Site
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Call `mcp__admin__publish-site` to move an already-extracted static-site tree into the per-account static-publish surface (`<accountDir>/sites/<slug>/`) and emit one canonical path slug. Pair the slug with `mcp__admin__public-hostname` to surface the full URL in the same turn.
|
|
4
4
|
|
|
5
|
-
**Invoked from `specialists:content-producer`** when the brief carries a host-website / publish-site / put-online intent.
|
|
5
|
+
**Invoked from `specialists:content-producer`** when the brief carries a host-website / publish-site / put-online intent. The slug validation, symlink scan, refusal taxonomy, `mv`, canonical-path selection, and llms.txt refresh all run inside the typed tool — this skill is the intent router, not the implementer.
|
|
6
6
|
|
|
7
|
-
## When to
|
|
7
|
+
## When to use
|
|
8
8
|
|
|
9
9
|
Activate when **both** are true:
|
|
10
10
|
|
|
11
11
|
- The current turn carries a "host this website" / "publish this site" / "put this online" / equivalent intent.
|
|
12
|
-
- A directory tree of HTML + assets is already on disk under `<accountDir>/extracted/<attachmentId>/` (
|
|
12
|
+
- A directory tree of HTML + assets is already on disk under `<accountDir>/extracted/<attachmentId>/` (typical output of [unzip-attachment](../unzip-attachment/SKILL.md)), or the conversation already names a source directory under `<accountDir>/`.
|
|
13
13
|
|
|
14
|
-
Do **not** activate for `.pdf`, `.docx`, slide decks, single-image attachments, or zips whose extracted output is not a directory tree of HTML + assets — those have their own skills (`a4-print-documents`, `deck-pages`, etc.).
|
|
14
|
+
Do **not** activate for `.pdf`, `.docx`, slide decks, single-image attachments, or zips whose extracted output is not a directory tree of HTML + assets — those have their own skills (`a4-print-documents`, `deck-pages`, etc.).
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Call
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
-
|
|
18
|
+
```
|
|
19
|
+
mcp__admin__publish-site
|
|
20
|
+
source: <absolute path to the extracted source directory>
|
|
21
|
+
slug: <operator-supplied or operator-confirmed path under sites/>
|
|
22
|
+
siteName?: <optional llms.txt header; defaults to the last slug segment>
|
|
23
|
+
siteDescription?: <optional one-line blockquote under the llms.txt header>
|
|
24
|
+
```
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
Confirm the slug with the operator before calling if it isn't already explicit in the conversation.
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
## Surface the result
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
3. **One emitted path slug.** The skill emits exactly one path string starting with `/sites/`:
|
|
28
|
-
- `/sites/<slug>/` — when `<accountDir>/sites/<slug>/index.html` exists after the move.
|
|
29
|
-
- `/sites/<slug>/<file>.html` — when no `index.html` and exactly one HTML file sits at the top level.
|
|
30
|
-
4. **Path only — no scheme, no host.** The emitted string is a path. Never emit a URL with a scheme or a host. The operator pairs the slug with their own public-host root.
|
|
31
|
-
5. **No fallback servers, no Playwright probes, no service restarts.** If the move or URL form is wrong, refuse. Reaching for `python -m http.server`, `npx http-server`, browser-automation probes, or platform restarts in place of fixing the URL contract is the prior-session failure pattern this skill exists to close.
|
|
32
|
-
6. **Source must be a real directory tree, no symlinks.** If `find <source> -type l` returns any entry, refuse.
|
|
33
|
-
|
|
34
|
-
## Refusals
|
|
35
|
-
|
|
36
|
-
Each refusal is a `[publish-site] refused` log line plus a fixed operator message. No retry, no silent substitution.
|
|
37
|
-
|
|
38
|
-
| Reason | Trigger | Log line |
|
|
39
|
-
|---|---|---|
|
|
40
|
-
| `unsafe-slug` | Any segment fails `SAFE_SEG_RE`, has a leading `.`, or equals `..` | `[publish-site] refused reason=unsafe-slug slug=<value>` |
|
|
41
|
-
| `destination-occupied` | `<accountDir>/sites/<slug>/` already exists with non-empty contents | `[publish-site] refused reason=destination-occupied dest=<path>` |
|
|
42
|
-
| `symlink-in-source` | `find <source> -type l` returns at least one entry | `[publish-site] refused reason=symlink-in-source entry=<path>` |
|
|
43
|
-
| `zero-html` | The top level of `<source>` has no `*.html` files | `[publish-site] refused reason=zero-html source=<path>` |
|
|
44
|
-
| `ambiguous-html` | Multiple top-level `*.html` files and no `index.html` | `[publish-site] refused reason=ambiguous-html candidates=<f1>,<f2>,...` |
|
|
45
|
-
|
|
46
|
-
The operator message for `ambiguous-html` names the candidate files and asks the operator to pick which file is the landing page (or to add an `index.html` to the source). No move happens until the choice is explicit.
|
|
47
|
-
|
|
48
|
-
## Flow
|
|
49
|
-
|
|
50
|
-
1. **Resolve and validate slug.** Split on `/`; reject any segment that violates the rules above.
|
|
51
|
-
2. **Pre-flight checks.** Inspect the source tree's top level: count `*.html` files; check for `index.html`; run the symlink scan; check the destination is absent or empty. Apply refusals before any disk mutation.
|
|
52
|
-
3. **Move.** Single `mv <source> <accountDir>/sites/<slug>/`. Emit one log line:
|
|
53
|
-
`[publish-site] move from=<source> to=<accountDir>/sites/<slug>/ files=<n>`
|
|
54
|
-
4. **Choose the canonical path.** `index.html` present at top level → path is `/sites/<slug>/`. Otherwise the single top-level HTML file → path is `/sites/<slug>/<file>.html`.
|
|
55
|
-
5. **Emit.** One log line:
|
|
56
|
-
`[publish-site] url emitted=<path-slug> kind=<index|file>`
|
|
57
|
-
6. **Refresh `llms.txt` at the site root.** Call `mcp__aeo__aeo-write-llms-txt` with `{ siteName: "<slug>", siteDir: "<accountDir>/sites/<slug>" }`. The AEO tool reads the account's `KnowledgeDocument` set and writes `llms.txt` + `llms-full.txt` into `<siteDir>/`, so answer-engine crawlers always see the current page set at the moment of publish. `accountId` is implicit — the AEO plugin reads it from its own env. On success, emit `[publish-site] aeo-llms-txt-ok wrote=<indexPath> indexBytes=<n>`. **Best-effort invariant:** if the call fails (tool returns `isError`, throws, or the `aeo` plugin is not loaded for this account), emit `[publish-site] aeo-llms-txt-failed reason=<msg>` and continue. **Never unwind the `mv` on AEO failure** — the publish already succeeded, the URL is live, and a stale or missing `llms.txt` is strictly less bad than a failed publish.
|
|
58
|
-
7. **Resolve the public hostname and emit the full URL.** Call `mcp__admin__public-hostname` to fetch this account's hostname; the tool reads `cloudflared/config.yml` + `alias-domains.json` — the same files the platform server trusts to route. Concatenate `https://<hostname><path-slug>` and surface that one URL to the operator. If the tool returns `reason: no-tunnel`, relay the tool's remediation message verbatim — do not improvise the URL. The route at `/sites/*` (see [server/routes/sites.ts](../../../../ui/server/routes/sites.ts)) handles the trailing-slash redirect on the dir form, so the slug is correct as-emitted whether the operator's HTML uses an `index.html` entry-point or a publisher-named landing file.
|
|
59
|
-
|
|
60
|
-
## Log lines (grep targets)
|
|
61
|
-
|
|
62
|
-
| When | Line |
|
|
63
|
-
|---|---|
|
|
64
|
-
| After move | `[publish-site] move from=<src> to=<dest> files=<n>` |
|
|
65
|
-
| Path emission | `[publish-site] url emitted=<path-slug> kind=<index\|file>` |
|
|
66
|
-
| AEO llms.txt success | `[publish-site] aeo-llms-txt-ok wrote=<indexPath> indexBytes=<n>` |
|
|
67
|
-
| AEO llms.txt failure (best-effort, publish continues) | `[publish-site] aeo-llms-txt-failed reason=<msg>` |
|
|
68
|
-
| Refusal (any reason) | `[publish-site] refused reason=<unsafe-slug\|destination-occupied\|symlink-in-source\|zero-html\|ambiguous-html> <key=value pairs>` |
|
|
30
|
+
- **On success** the tool returns `pathSlug` (e.g. `/sites/demo/` or `/sites/demo/brochure.html`). Call `mcp__admin__public-hostname` next and surface `https://<hostname><pathSlug>` to the operator. The `aeo:` line on the response tells you whether `llms.txt` refresh succeeded; if it failed, the publish still succeeded — relay the URL anyway.
|
|
31
|
+
- **On refusal** the tool returns a `refused: <kind>` body with a fixed `Operator action:` guidance line. Relay the guidance verbatim. The five refusal kinds are `unsafe-slug`, `destination-occupied`, `symlink-in-source`, `zero-html`, and `ambiguous-html`. No retry, no improvisation, no fallback server.
|
|
69
32
|
|
|
70
33
|
## Out of scope
|
|
71
34
|
|
|
72
|
-
- Cleanup of pre-existing synthetic files left by earlier sessions (e.g. a stray `index.html` produced by an earlier `cp brochure.html index.html` workaround). Refuse with `destination-occupied` and let the operator clean up explicitly.
|
|
73
35
|
- Extraction. `unzip-attachment` already owns the extract step; the source path is its output.
|
|
74
|
-
- DNS, tunnels, certificates, or any public-host configuration. The route at `/sites/*` is the wire contract
|
|
75
|
-
-
|
|
36
|
+
- DNS, tunnels, certificates, or any public-host configuration. The route at `/sites/*` is the wire contract.
|
|
37
|
+
- Cleanup of pre-existing synthetic files left by earlier sessions. A non-empty destination produces `destination-occupied` and the operator cleans up explicitly.
|
|
@@ -1,42 +1,9 @@
|
|
|
1
|
+
import { type WriteLlmsTxtInput, type WriteLlmsTxtResult } from "../../../../../lib/aeo-llms-txt-writer/dist/index.js";
|
|
2
|
+
export type { WriteLlmsTxtInput, WriteLlmsTxtResult };
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* # <Site name>
|
|
6
|
-
* > <Short description>
|
|
7
|
-
*
|
|
8
|
-
* ## <Section name>
|
|
9
|
-
* - [<Title>](<URL>): <Summary>
|
|
10
|
-
*
|
|
11
|
-
* `llms-full.txt` concatenates the full text of every listed page in
|
|
12
|
-
* the same order, separated by a blank line and the page's title as an
|
|
13
|
-
* H1.
|
|
14
|
-
*
|
|
15
|
-
* Source: `KnowledgeDocument` nodes for the account that have a `url`
|
|
16
|
-
* property. `summary` and `body` properties feed the index and the
|
|
17
|
-
* concatenated dump respectively. Pages without a `url` are skipped
|
|
18
|
-
* (they aren't reachable from a public site), and the count of skipped
|
|
19
|
-
* pages is returned so the operator can audit completeness.
|
|
4
|
+
* Thin aeo-side wrapper around the plugin-agnostic writeLlmsTxt lib.
|
|
5
|
+
* Injects this plugin's neo4j session factory so the shared lib stays
|
|
6
|
+
* decoupled from any one plugin's driver wiring.
|
|
20
7
|
*/
|
|
21
|
-
export interface WriteLlmsTxtInput {
|
|
22
|
-
accountId: string;
|
|
23
|
-
siteName: string;
|
|
24
|
-
siteDescription?: string;
|
|
25
|
-
siteDir?: string;
|
|
26
|
-
}
|
|
27
|
-
export interface WriteLlmsTxtResult {
|
|
28
|
-
llmsTxt: string;
|
|
29
|
-
llmsFullTxt: string;
|
|
30
|
-
pageCount: number;
|
|
31
|
-
skippedNoUrl: number;
|
|
32
|
-
sizeBytes: {
|
|
33
|
-
index: number;
|
|
34
|
-
full: number;
|
|
35
|
-
};
|
|
36
|
-
writtenPaths?: {
|
|
37
|
-
index: string;
|
|
38
|
-
full: string;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
8
|
export declare function writeLlmsTxt(input: WriteLlmsTxtInput): Promise<WriteLlmsTxtResult>;
|
|
42
9
|
//# sourceMappingURL=aeo-write-llms-txt.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aeo-write-llms-txt.d.ts","sourceRoot":"","sources":["../../src/tools/aeo-write-llms-txt.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aeo-write-llms-txt.d.ts","sourceRoot":"","sources":["../../src/tools/aeo-write-llms-txt.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,sDAAsD,CAAC;AAG9D,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,CAAC;AAEtD;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CAE7B"}
|
|
@@ -1,118 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve as resolvePath } from "node:path";
|
|
1
|
+
import { writeLlmsTxt as writeLlmsTxtLib, } from "../../../../../lib/aeo-llms-txt-writer/dist/index.js";
|
|
3
2
|
import { getSession } from "../lib/neo4j.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const flat = body.replace(/\s+/g, " ").trim();
|
|
10
|
-
if (flat.length <= 200)
|
|
11
|
-
return flat;
|
|
12
|
-
return flat.slice(0, 197).trimEnd() + "…";
|
|
13
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* Thin aeo-side wrapper around the plugin-agnostic writeLlmsTxt lib.
|
|
5
|
+
* Injects this plugin's neo4j session factory so the shared lib stays
|
|
6
|
+
* decoupled from any one plugin's driver wiring.
|
|
7
|
+
*/
|
|
14
8
|
export async function writeLlmsTxt(input) {
|
|
15
|
-
|
|
16
|
-
let rows = [];
|
|
17
|
-
let skippedNoUrl = 0;
|
|
18
|
-
try {
|
|
19
|
-
const r = await session.run(`MATCH (d:KnowledgeDocument {accountId: $accountId})
|
|
20
|
-
RETURN d.title AS title, d.url AS url, d.summary AS summary,
|
|
21
|
-
coalesce(d.body, d.abstract, '') AS body
|
|
22
|
-
ORDER BY d.title ASC`, { accountId: input.accountId });
|
|
23
|
-
for (const rec of r.records) {
|
|
24
|
-
const title = rec.get("title") ?? "";
|
|
25
|
-
const url = rec.get("url") ?? "";
|
|
26
|
-
const summary = rec.get("summary") ?? "";
|
|
27
|
-
const body = rec.get("body") ?? "";
|
|
28
|
-
if (!url || !title) {
|
|
29
|
-
skippedNoUrl += 1;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
rows.push({
|
|
33
|
-
title: escapeMarkdown(title),
|
|
34
|
-
url: url.trim(),
|
|
35
|
-
summary: escapeMarkdown(summary) || summariseBody(body) || "(no summary)",
|
|
36
|
-
body: body.trim(),
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
finally {
|
|
41
|
-
await session.close();
|
|
42
|
-
}
|
|
43
|
-
const indexLines = [];
|
|
44
|
-
indexLines.push(`# ${escapeMarkdown(input.siteName)}`);
|
|
45
|
-
indexLines.push("");
|
|
46
|
-
if (input.siteDescription) {
|
|
47
|
-
indexLines.push(`> ${escapeMarkdown(input.siteDescription)}`);
|
|
48
|
-
indexLines.push("");
|
|
49
|
-
}
|
|
50
|
-
indexLines.push("## Pages");
|
|
51
|
-
indexLines.push("");
|
|
52
|
-
for (const row of rows) {
|
|
53
|
-
indexLines.push(`- [${row.title}](${row.url}): ${row.summary}`);
|
|
54
|
-
}
|
|
55
|
-
indexLines.push("");
|
|
56
|
-
indexLines.push("<!-- generated by @maxy/aeo aeo-write-llms-txt; spec: https://llmstxt.org/ -->");
|
|
57
|
-
const llmsTxt = indexLines.join("\n");
|
|
58
|
-
const fullLines = [];
|
|
59
|
-
fullLines.push(`# ${escapeMarkdown(input.siteName)}`);
|
|
60
|
-
fullLines.push("");
|
|
61
|
-
if (input.siteDescription) {
|
|
62
|
-
fullLines.push(`> ${escapeMarkdown(input.siteDescription)}`);
|
|
63
|
-
fullLines.push("");
|
|
64
|
-
}
|
|
65
|
-
for (const row of rows) {
|
|
66
|
-
fullLines.push(`# ${row.title}`);
|
|
67
|
-
fullLines.push("");
|
|
68
|
-
fullLines.push(`Source: ${row.url}`);
|
|
69
|
-
fullLines.push("");
|
|
70
|
-
if (row.body) {
|
|
71
|
-
// Demote any in-body H1s to H2 so a single H1 hierarchy holds.
|
|
72
|
-
const safeBody = row.body.replace(HEADER_RX, "## ");
|
|
73
|
-
fullLines.push(safeBody);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
fullLines.push(row.summary);
|
|
77
|
-
}
|
|
78
|
-
fullLines.push("");
|
|
79
|
-
fullLines.push("---");
|
|
80
|
-
fullLines.push("");
|
|
81
|
-
}
|
|
82
|
-
const llmsFullTxt = fullLines.join("\n");
|
|
83
|
-
const sizeBytes = {
|
|
84
|
-
index: Buffer.byteLength(llmsTxt, "utf-8"),
|
|
85
|
-
full: Buffer.byteLength(llmsFullTxt, "utf-8"),
|
|
86
|
-
};
|
|
87
|
-
process.stderr.write(`[aeo-llms-txt] site=${input.siteName} pages=${rows.length} skippedNoUrl=${skippedNoUrl} indexBytes=${sizeBytes.index} fullBytes=${sizeBytes.full}\n`);
|
|
88
|
-
let writtenPaths;
|
|
89
|
-
if (input.siteDir) {
|
|
90
|
-
const dir = resolvePath(input.siteDir);
|
|
91
|
-
let dirStat;
|
|
92
|
-
try {
|
|
93
|
-
dirStat = await stat(dir);
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
const code = err?.code;
|
|
97
|
-
throw new Error(`siteDir does not exist (${code ?? "unknown"}): ${dir}`);
|
|
98
|
-
}
|
|
99
|
-
if (!dirStat.isDirectory()) {
|
|
100
|
-
throw new Error(`siteDir is not a directory: ${dir}`);
|
|
101
|
-
}
|
|
102
|
-
const indexPath = resolvePath(dir, "llms.txt");
|
|
103
|
-
const fullPath = resolvePath(dir, "llms-full.txt");
|
|
104
|
-
await writeFile(indexPath, llmsTxt, "utf-8");
|
|
105
|
-
await writeFile(fullPath, llmsFullTxt, "utf-8");
|
|
106
|
-
writtenPaths = { index: indexPath, full: fullPath };
|
|
107
|
-
process.stderr.write(`[aeo-llms-txt] wrote site=${dir} indexBytes=${sizeBytes.index} fullBytes=${sizeBytes.full}\n`);
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
llmsTxt,
|
|
111
|
-
llmsFullTxt,
|
|
112
|
-
pageCount: rows.length,
|
|
113
|
-
skippedNoUrl,
|
|
114
|
-
sizeBytes,
|
|
115
|
-
writtenPaths,
|
|
116
|
-
};
|
|
9
|
+
return writeLlmsTxtLib(input, { getSession });
|
|
117
10
|
}
|
|
118
11
|
//# sourceMappingURL=aeo-write-llms-txt.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aeo-write-llms-txt.js","sourceRoot":"","sources":["../../src/tools/aeo-write-llms-txt.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"aeo-write-llms-txt.js","sourceRoot":"","sources":["../../src/tools/aeo-write-llms-txt.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,IAAI,eAAe,GAGhC,MAAM,sDAAsD,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI7C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAwB;IAExB,OAAO,eAAe,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -3,7 +3,7 @@ name: content-producer
|
|
|
3
3
|
description: "Visual production and static-site hosting. Reads from the populated graph to produce visual artifacts (image generation, PDF rendering, component delivery) and hosts already-prepared static sites by extracting attached archives via unzip-attachment then placing the tree under <accountDir>/sites/<slug>/ via publish-site. Delegate for: generating images, saving rendered pages as PDF, or any 'host this website' / 'publish this site' / 'put this online' intent carrying an HTML+assets archive. Not document ingestion: graph ingestion of any kind routes to specialists:database-operator. Static-site zips are extracted to disk for publication, never written to the graph."
|
|
4
4
|
summary: "Produces visual output from your graph: generates images, renders pages to PDF, and hosts static websites you upload as a zip."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
|
-
tools: Bash, mcp__memory__memory-search, mcp__replicate__image-generate, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_pdf_save, mcp__admin__file-attach, mcp__admin__plugin-read, mcp__admin__public-hostname
|
|
6
|
+
tools: Bash, mcp__memory__memory-search, mcp__replicate__image-generate, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_pdf_save, mcp__admin__file-attach, mcp__admin__plugin-read, mcp__admin__public-hostname, mcp__admin__publish-site
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Content Producer
|
|
@@ -34,9 +34,9 @@ Generate the HTML, navigate the Playwright browser to it, and capture with `brow
|
|
|
34
34
|
|
|
35
35
|
## Hosting websites
|
|
36
36
|
|
|
37
|
-
When a brief carries a "host this website" / "publish this site" / "put this online" intent with a `.zip` of HTML and assets, run
|
|
37
|
+
When a brief carries a "host this website" / "publish this site" / "put this online" intent with a `.zip` of HTML and assets, run `skill-load skillName=unzip-attachment` to extract, then call `mcp__admin__publish-site` directly. The tool owns slug validation, the symlink scan, the `mv`, the refusal taxonomy, and the post-publish llms.txt refresh — load `skill-load skillName=publish-site` only if you need the intent-router context. Do not improvise a fallback server.
|
|
38
38
|
|
|
39
|
-
Confirm the slug with the operator before publishing. The slug is one or more `/`-separated segments under `<accountDir>/sites/`. Refusals are loud-fail; relay the
|
|
39
|
+
Confirm the slug with the operator before publishing. The slug is one or more `/`-separated segments under `<accountDir>/sites/`. Refusals are loud-fail; relay the tool's `Operator action:` line verbatim and stop. On success, take the `pathSlug` from the tool response, call `mcp__admin__public-hostname`, and surface `https://<hostname><pathSlug>` to the operator as one line.
|
|
40
40
|
|
|
41
41
|
## File delivery
|
|
42
42
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* PECR consent banner styles (Task 375). Brand overrides arrive as inline
|
|
2
|
+
* CSS variables on :root from /consent.js after fetching /v/brand-config. */
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--mxy-consent-bg: #FFFFFF;
|
|
6
|
+
--mxy-consent-fg: #1F1F1F;
|
|
7
|
+
--mxy-consent-accent: #1F1F1F;
|
|
8
|
+
--mxy-consent-accentFg: #FFFFFF;
|
|
9
|
+
--mxy-consent-border: #E5E5E2;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#mxy-consent {
|
|
13
|
+
position: fixed;
|
|
14
|
+
left: 0;
|
|
15
|
+
right: 0;
|
|
16
|
+
bottom: 0;
|
|
17
|
+
z-index: 2147483640;
|
|
18
|
+
background: var(--mxy-consent-bg);
|
|
19
|
+
color: var(--mxy-consent-fg);
|
|
20
|
+
border-top: 1px solid var(--mxy-consent-border);
|
|
21
|
+
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.06);
|
|
22
|
+
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
23
|
+
padding: 16px 20px env(safe-area-inset-bottom, 16px);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#mxy-consent .mxy-consent__inner {
|
|
27
|
+
max-width: 1100px;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 24px;
|
|
31
|
+
align-items: center;
|
|
32
|
+
flex-wrap: wrap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#mxy-consent .mxy-consent__text {
|
|
36
|
+
flex: 1 1 320px;
|
|
37
|
+
min-width: 240px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#mxy-consent .mxy-consent__title {
|
|
41
|
+
display: block;
|
|
42
|
+
font-size: 15px;
|
|
43
|
+
margin-bottom: 4px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#mxy-consent .mxy-consent__body {
|
|
47
|
+
margin: 0 0 6px 0;
|
|
48
|
+
color: var(--mxy-consent-fg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#mxy-consent .mxy-consent__link {
|
|
52
|
+
color: var(--mxy-consent-fg);
|
|
53
|
+
text-decoration: underline;
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#mxy-consent .mxy-consent__actions {
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: 10px;
|
|
60
|
+
flex: 0 0 auto;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Accept and reject are equal-weighted per ICO guidance — same size,
|
|
64
|
+
* same prominence. Reject is the outlined variant only because the
|
|
65
|
+
* accent fill must indicate the active surface in monochrome palettes;
|
|
66
|
+
* both buttons share padding, font-weight and minimum width. */
|
|
67
|
+
|
|
68
|
+
#mxy-consent .mxy-consent__btn {
|
|
69
|
+
appearance: none;
|
|
70
|
+
-webkit-appearance: none;
|
|
71
|
+
border-radius: 6px;
|
|
72
|
+
font: inherit;
|
|
73
|
+
font-weight: 600;
|
|
74
|
+
padding: 10px 18px;
|
|
75
|
+
min-width: 96px;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
border: 1px solid var(--mxy-consent-accent);
|
|
78
|
+
transition: opacity 120ms ease;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#mxy-consent .mxy-consent__btn:hover { opacity: 0.85; }
|
|
82
|
+
#mxy-consent .mxy-consent__btn:focus { outline: 2px solid var(--mxy-consent-accent); outline-offset: 2px; }
|
|
83
|
+
|
|
84
|
+
#mxy-consent .mxy-consent__btn--reject {
|
|
85
|
+
background: var(--mxy-consent-bg);
|
|
86
|
+
color: var(--mxy-consent-fg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#mxy-consent .mxy-consent__btn--accept {
|
|
90
|
+
background: var(--mxy-consent-accent);
|
|
91
|
+
color: var(--mxy-consent-accentFg);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (max-width: 560px) {
|
|
95
|
+
#mxy-consent .mxy-consent__actions { width: 100%; }
|
|
96
|
+
#mxy-consent .mxy-consent__btn { flex: 1 1 0; }
|
|
97
|
+
}
|