@rubytech/create-maxy-code 0.1.125 → 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 +23 -1
- package/package.json +2 -2
- 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/lib/graph-style/dist/index.d.ts +78 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-style/dist/index.js +296 -0
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +114 -0
- package/payload/platform/lib/graph-style/src/index.ts +307 -0
- package/payload/platform/lib/graph-style/tsconfig.json +9 -0
- package/payload/platform/lib/graph-style/vitest.config.ts +9 -0
- package/payload/platform/package-lock.json +318 -0
- package/payload/platform/package.json +3 -2
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- 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/plugins/docs/references/visitor-graph.md +4 -3
- package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/graph-viewer/PLUGIN.md +56 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts +7 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +74 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts +5 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js +43 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts +28 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js +73 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts +40 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js +77 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts +45 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +170 -0
- package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -0
- package/payload/platform/plugins/graph-viewer/mcp/package.json +25 -0
- package/payload/platform/plugins/graph-viewer/mcp/vitest.config.ts +8 -0
- package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +35 -0
- package/payload/platform/scripts/check-plugin-tools-mcp-consistency.mjs +136 -0
- package/payload/platform/templates/specialists/agents/content-producer.md +3 -3
- package/payload/server/public/assets/{admin-D6IfAzYY.js → admin-FcRHAL-3.js} +7 -7
- package/payload/server/public/assets/{data-BGbQyufe.js → data-Ds37mflX.js} +1 -1
- package/payload/server/public/assets/{graph-D-1lRTeL.js → graph-CmWRhaiS.js} +1 -1
- package/payload/server/public/assets/graph-labels-Ch2r00Gt.js +1 -0
- package/payload/server/public/assets/page-BOtNny_4.js +51 -0
- package/payload/server/public/assets/page-BcHhJXUt.js +1 -0
- package/payload/server/public/consent.css +97 -0
- package/payload/server/public/consent.js +259 -0
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/public/privacy.html +68 -5
- package/payload/server/public/v.js +53 -0
- package/payload/server/server.js +465 -176
- package/payload/server/public/assets/graph-labels-BRXJHNYE.js +0 -1
- package/payload/server/public/assets/page-B4oirCvn.js +0 -1
- package/payload/server/public/assets/page-FmJ7PIYx.js +0 -51
|
@@ -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"}
|
|
@@ -50,10 +50,11 @@ All under the `real-agent-buyers` plugin, admin-side only:
|
|
|
50
50
|
| `visitor-recent-by-person` | Recent sessions for a known `:Person` (morning round, 1:1 prep). |
|
|
51
51
|
| `visitor-recent-by-page` | Recent visitors of a given listing slug or URL. |
|
|
52
52
|
| `visitor-engagement-score` | Engagement-ranked `:Person` list (nurture queue). |
|
|
53
|
-
| `visitor-recommendation-ctr` | Graph-backed CTR over a window
|
|
53
|
+
| `visitor-recommendation-ctr` | Graph-backed CTR over a window, joined from `:Recommendation` and `:Click` nodes. |
|
|
54
54
|
| `visitor-session-detail` | Full event timeline for one `:Session`. |
|
|
55
55
|
| `visitor-event-ingest` | Admin companion to `POST /v/event` for test harness work. |
|
|
56
|
-
| `visitor-backfill-from-logs` | One-shot importer: parses `[property-recommended]` and `[property-card-click]` log lines
|
|
56
|
+
| `visitor-backfill-from-logs` | One-shot importer: parses `[property-recommended]` and `[property-card-click]` log lines into the graph; used to recover late-arriving sessions and for ad-hoc forensics. |
|
|
57
|
+
| `mint-visitor-token` | Mints a signed token bound to a `:Person` for outbound URLs in `morning-round`, `lead-nurturing`, `vendor-updates`. Returns `{ token, expiryMs }`; the agent appends `&v=<token>` to `/listings/<slug>/click?session=<sk>`. Same secret file as the UI server; both processes share it via the wx-create pattern. (Task 362) |
|
|
57
58
|
|
|
58
59
|
## Privacy
|
|
59
60
|
|
|
@@ -70,7 +71,7 @@ Quick checks the operator can run after deployment:
|
|
|
70
71
|
1. Load a published listing page; grep `[visitor-event] type=pageview` in `server.log` within 1s.
|
|
71
72
|
2. Scroll past 50%; grep `[visitor-event] type=scroll depth=50`.
|
|
72
73
|
3. Click an element marked `data-track="floorplan"`; grep `[visitor-event] type=click label=floorplan`.
|
|
73
|
-
4. Run `visitor-backfill-from-logs` over
|
|
74
|
+
4. Run `visitor-backfill-from-logs` over a log window where live writes were lost (process restart, etc.); the response reports `recWritten` and `clickWritten` counts. Subsequent runs over the same window are idempotent for `:Recommendation` and append-only for `:Click`.
|
|
74
75
|
|
|
75
76
|
## Failure signals
|
|
76
77
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "graph-viewer",
|
|
3
|
+
"description": "Inline Neo4j subgraph renderer. Provides graph-render — a Node-side PNG renderer that walks N hops from a starting node, applies the same palette and display strings the live /graph page uses, and returns the image as an MCP image content block. Removes the device-switch otherwise required for the agent to show the operator what a subgraph looks like. Account-scoped: the tool refuses to render nodes outside the caller's accountId. Layout is d3-force with a deterministic seed; drawing is @napi-rs/canvas with a system-font fallback for labels. The fidelity contract is palette and display-string parity — node positions will differ from the live page's vis-network layout. Ships one skill: render-graph.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Rubytech LLC"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graph-viewer
|
|
3
|
+
mcp:
|
|
4
|
+
command: node
|
|
5
|
+
args:
|
|
6
|
+
- ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
|
|
7
|
+
- ${PLATFORM_ROOT}/plugins/graph-viewer/mcp/dist/index.js
|
|
8
|
+
env:
|
|
9
|
+
MCP_SPAWN_TEE_NAME: graph-viewer
|
|
10
|
+
LOG_DIR: ${LOG_DIR}
|
|
11
|
+
PLATFORM_ROOT: ${PLATFORM_ROOT}
|
|
12
|
+
ACCOUNT_ID: ${ACCOUNT_ID}
|
|
13
|
+
SESSION_ID: ${SESSION_ID}
|
|
14
|
+
mcp-manifest: skip
|
|
15
|
+
description: "Inline Neo4j subgraph renderer. Provides graph-render — a Node-side PNG renderer that walks N hops from a starting node, applies the same palette and display strings the live /graph page uses, and returns the image as an MCP image content block. Removes the device-switch otherwise required for the agent to show the operator what a subgraph looks like. Account-scoped: the tool refuses to render nodes outside the caller's accountId. Layout is d3-force with a deterministic seed; drawing is @napi-rs/canvas with a system-font fallback for labels. The fidelity contract is palette and display-string parity — node positions will differ from the live page's vis-network layout. Ships one skill: render-graph."
|
|
16
|
+
tools:
|
|
17
|
+
- name: graph-render
|
|
18
|
+
publicAllowlist: false
|
|
19
|
+
adminAllowlist: true
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# graph-viewer
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
A single MCP tool, `graph-render`, takes a Neo4j node ID and a hop depth and
|
|
27
|
+
returns an inline PNG of the subgraph rooted at that node. The agent
|
|
28
|
+
attaches the PNG to its reply so the operator can see the graph without
|
|
29
|
+
opening the admin UI on a different device.
|
|
30
|
+
|
|
31
|
+
## Why it exists
|
|
32
|
+
|
|
33
|
+
Before this plugin, the agent had no way to ship the operator a picture
|
|
34
|
+
of a subgraph from inside chat. Mermaid and ASCII substitutes were
|
|
35
|
+
illegible and bore no resemblance to the live `/graph` page. Operators
|
|
36
|
+
otherwise had to leave the chat surface, open the admin UI on whatever
|
|
37
|
+
device hosts it, and re-find the same subgraph by hand.
|
|
38
|
+
|
|
39
|
+
## Surface boundaries
|
|
40
|
+
|
|
41
|
+
- `graph-render` only accepts a `nodeId + hopDepth` query shape. Arbitrary
|
|
42
|
+
Cypher is not supported and is filed as a follow-up task.
|
|
43
|
+
- Pixel-equivalence with `/graph` is not a goal. The layout engine is
|
|
44
|
+
d3-force, not the live page's vis-network port; node positions will
|
|
45
|
+
differ. The colour palette, label precedence, and short-label strings
|
|
46
|
+
are shared with `/graph` via `platform/lib/graph-style/`.
|
|
47
|
+
- Subgraphs above 200 nodes return a truncation envelope alongside the
|
|
48
|
+
capped render.
|
|
49
|
+
- `:Trashed` nodes are filtered out, matching `memory-search`'s contract.
|
|
50
|
+
|
|
51
|
+
## Enabling
|
|
52
|
+
|
|
53
|
+
The plugin is auto-discovered from `platform/plugins/`. Each operator
|
|
54
|
+
account must list `graph-viewer` in its `enabledPlugins` (use the admin
|
|
55
|
+
`plugin-toggle-enabled` MCP tool) before `graph-render` appears in the
|
|
56
|
+
agent's tool surface.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server entrypoint for the graph-viewer plugin. Registers one tool,
|
|
3
|
+
* `graph-render`, on the standard stdio transport. Account-scoping is
|
|
4
|
+
* enforced inside the tool handler against the caller-supplied accountId.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|