@rubytech/create-realagent 1.0.834 → 1.0.836

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent",
3
- "version": "1.0.834",
3
+ "version": "1.0.836",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent": "./dist/index.js"
@@ -0,0 +1,71 @@
1
+ # Publish Site
2
+
3
+ Move an already-extracted static-site tree into the per-account static-publish surface (`<accountDir>/sites/<slug>/`) and emit exactly one canonical path slug for the operator to share. The operator pastes the slug after their own public-host root — the public hostname is whatever their tunnel terminates at, and is not knowable from inside this skill.
4
+
5
+ ## When to Use
6
+
7
+ Activate when **both** are true:
8
+
9
+ - The current turn carries a "host this website" / "publish this site" / "put this online" / equivalent intent.
10
+ - A directory tree of HTML + assets is already on disk under `<accountDir>/extracted/<attachmentId>/` (the typical output of [unzip-attachment](../unzip-attachment/SKILL.md)), or the conversation already names a source directory under `<accountDir>/`.
11
+
12
+ 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.). The intent + tree shape is the gate.
13
+
14
+ ## Inputs
15
+
16
+ - `source` — absolute path to the extracted directory tree (already produced by `unzip-attachment`).
17
+ - `slug` — operator-supplied or operator-confirmed path under `<accountDir>/sites/`. One or more `/`-separated segments; each segment matches the platform's `SAFE_SEG_RE` (`/^[a-z0-9_][a-z0-9_.-]{0,99}$/i`); no segment starts with `.`; no segment equals `..`. Confirm with the operator before proceeding when the slug is not already explicit.
18
+
19
+ ## Invariants
20
+
21
+ These are non-negotiable. Each is mechanically check-able and each maps to a fixed refusal log line.
22
+
23
+ 1. **Move, never copy.** The source tree is `mv`'d into `<accountDir>/sites/<slug>/`. No re-extraction, no `cp`. After success the source path no longer exists.
24
+ 2. **No filename invention.** Never `cp <any>.html index.html` to satisfy a URL form. If the moved tree's top level has no `index.html`, the canonical path slug points at the actual landing HTML by name.
25
+ 3. **One emitted path slug.** The skill emits exactly one path string starting with `/sites/`:
26
+ - `/sites/<slug>/` — when `<accountDir>/sites/<slug>/index.html` exists after the move.
27
+ - `/sites/<slug>/<file>.html` — when no `index.html` and exactly one HTML file sits at the top level.
28
+ 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.
29
+ 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.
30
+ 6. **Source must be a real directory tree, no symlinks.** If `find <source> -type l` returns any entry, refuse.
31
+
32
+ ## Refusals
33
+
34
+ Each refusal is a `[publish-site] refused` log line plus a fixed operator message. No retry, no silent substitution.
35
+
36
+ | Reason | Trigger | Log line |
37
+ |---|---|---|
38
+ | `unsafe-slug` | Any segment fails `SAFE_SEG_RE`, has a leading `.`, or equals `..` | `[publish-site] refused reason=unsafe-slug slug=<value>` |
39
+ | `destination-occupied` | `<accountDir>/sites/<slug>/` already exists with non-empty contents | `[publish-site] refused reason=destination-occupied dest=<path>` |
40
+ | `symlink-in-source` | `find <source> -type l` returns at least one entry | `[publish-site] refused reason=symlink-in-source entry=<path>` |
41
+ | `zero-html` | The top level of `<source>` has no `*.html` files | `[publish-site] refused reason=zero-html source=<path>` |
42
+ | `ambiguous-html` | Multiple top-level `*.html` files and no `index.html` | `[publish-site] refused reason=ambiguous-html candidates=<f1>,<f2>,...` |
43
+
44
+ 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.
45
+
46
+ ## Flow
47
+
48
+ 1. **Resolve and validate slug.** Split on `/`; reject any segment that violates the rules above.
49
+ 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.
50
+ 3. **Move.** Single `mv <source> <accountDir>/sites/<slug>/`. Emit one log line:
51
+ `[publish-site] move from=<source> to=<accountDir>/sites/<slug>/ files=<n>`
52
+ 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`.
53
+ 5. **Emit.** One log line:
54
+ `[publish-site] url emitted=<path-slug> kind=<index|file>`
55
+ 6. **Operator-facing chat output.** One line: the path slug, framed as "paste this after your public-host root" — no scheme, no host, no decoration. The route at `/sites/*` (see [server/routes/sites.ts](../../../../ui/server/routes/sites.ts)) takes care of 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.
56
+
57
+ ## Log lines (grep targets)
58
+
59
+ | When | Line |
60
+ |---|---|
61
+ | After move | `[publish-site] move from=<src> to=<dest> files=<n>` |
62
+ | Path emission | `[publish-site] url emitted=<path-slug> kind=<index\|file>` |
63
+ | Refusal (any reason) | `[publish-site] refused reason=<unsafe-slug\|destination-occupied\|symlink-in-source\|zero-html\|ambiguous-html> <key=value pairs>` |
64
+
65
+ ## Out of scope
66
+
67
+ - Discovering or emitting the operator's public hostname. The hostname is whatever their tunnel terminates at — not knowable from inside this skill, and not the platform's concern. The operator pairs the slug with their own host root.
68
+ - 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.
69
+ - Extraction. `unzip-attachment` already owns the extract step; the source path is its output.
70
+ - DNS, tunnels, certificates, or any public-host configuration. The route at `/sites/*` is the wire contract; this skill is a placement + URL-shape skill only.
71
+ - Per-directory default-filename config (e.g. a `.publish.json` that names a default landing file). The publisher names the landing file; no inference layer.
@@ -56,3 +56,7 @@ One `start` pairs with exactly one of `{done, oversize, zip-slip-blocked, symlin
56
56
  - Nested-archive recursion — if an extracted file is itself a zip, the operator may manually re-invoke the skill on that file; no auto-recursion.
57
57
  - Password-protected archives — refused, never prompted.
58
58
  - Public-chat zip uploads — this skill is admin-only; the public agent never sees attachments of this class.
59
+
60
+ ## See also
61
+
62
+ - [publish-site](../publish-site/SKILL.md) — when the extracted tree is a static website (HTML + assets), hand off to `publish-site` to move it under `<accountDir>/sites/<slug>/` and emit the canonical path slug.
@@ -322,7 +322,9 @@ Each row in the Conversations modal exposes a `View logs` row-action that opens
322
322
 
323
323
  ### Static publish surface — `/sites/*`
324
324
 
325
- {{productName}} hosts a generic per-account static-tree publish surface at `https://public.<brand>/sites/<...>/<file>`. The route serves files from `<accountDir>/sites/<...>` with URL=disk mirroring — operator drops the tree on disk, no upload API. Extended MIME covers HTML/CSS/JS/woff2/fonts on top of images. Trailing-`/` or extension-less requests fall back to `index.html`. Path traversal (`..`, encoded `..`, segments failing `SAFE_SEG_RE`) returns 403; symlinks escaping the sites root are rejected via a `realpathSync` re-check. `.html` responses carry `Content-Security-Policy: default-src 'self' https: data:; script-src 'none'` and `Cache-Control: no-cache`; assets are cached for an hour; every response carries `X-Content-Type-Options: nosniff`. Per-account isolation comes from `resolveAccount` — Maxy and Real Agent installs each see only their own tree. Drop a brochure at `~/.realagent/data/sites/properties/<id>/brochure/output/` and it serves at `https://public.realagent.bot/sites/properties/<id>/brochure/output/brochure.html`. See `.docs/web-chat.md` `/sites/*` route entry for the wire contract and `[sites]` log lines (`serve|not-found|path-traversal-rejected|symlink-escape-rejected|no-account`).
325
+ {{productName}} hosts a generic per-account static-tree publish surface at `https://public.<brand>/sites/<...>/<file>`. The route serves files from `<accountDir>/sites/<...>` with URL=disk mirroring — operator drops the tree on disk, no upload API. Extended MIME covers HTML/CSS/JS/woff2/fonts on top of images. Path traversal (`..`, encoded `..`, segments failing `SAFE_SEG_RE`) returns 403; symlinks escaping the sites root are rejected via a `realpathSync` re-check. `.html` responses carry `Content-Security-Policy: default-src 'self' https: data:; script-src 'none'` and `Cache-Control: no-cache`; assets are cached for an hour; every response carries `X-Content-Type-Options: nosniff`. Per-account isolation comes from `resolveAccount` — every brand's install sees only its own tree.
326
+
327
+ **Directory canonicalisation.** A request whose disk target is a directory is `301`'d to the trailing-slash form (query string preserved) before any body is served — RFC 3986 §5.3 base resolution requires the trailing slash so relative refs in the served HTML resolve under the directory, not its parent. After the redirect the route serves `<dir>/index.html` if it exists on disk; otherwise `404`. There is **no** implicit-`index.html` invention for missing paths — the publisher owns canonical URLs. A brochure shipped without `index.html` is reached at `/sites/<slug>/<file>.html`, and the admin skill `publish-site` is the sanctioned surface that moves the extracted tree under `<accountDir>/sites/<slug>/` and emits the canonical path slug. Operator-side: drop a brochure at `<accountDir>/sites/properties/<id>/brochure/output/` and it serves at `<public-host>/sites/properties/<id>/brochure/output/brochure.html` (or `<public-host>/sites/properties/<id>/brochure/output/` if that directory contains an `index.html`). See `.docs/web-chat.md` `/sites/*` route entry for the wire contract and `[sites]` log lines (`serve|redirect-trailing-slash|not-found|path-traversal-rejected|symlink-escape-rejected|no-account`).
326
328
 
327
329
  ### Cross-tab session rotation
328
330