saypsst 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/dist/bin.mjs +123 -75
- package/package.json +2 -2
- package/schemas/index.json +1 -1
- package/skills/psst/SKILL.md +20 -20
- package/skills/psst/nextjs.md +15 -14
- package/skills/psst/proxy.md +4 -4
- package/skills/psst/troubleshooting.md +21 -21
- package/skills/psst/vercel-static.md +7 -7
- package/skills/psst-tasks/SKILL.md +11 -11
- package/skills/psst-tasks/routine.md +7 -7
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PSST
|
|
2
2
|
|
|
3
|
-
Install [
|
|
3
|
+
Install [PSST](https://saypsst.com) on your site, and check it in production. PSST publishes pages that make your brand show up in AI answers, on your own domain, under a path prefix it owns (default `/resources`), with no deploy after this one-time install.
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
|
-
1. Copy your site id from the
|
|
7
|
+
1. Copy your site id from the PSST dashboard (Settings › Connections).
|
|
8
8
|
2. In your site's folder: `npx saypsst init --site <site id>`. It checks before it writes, and prints any edit it will not make for you.
|
|
9
9
|
3. Commit and deploy. Then check it in production: `npx saypsst doctor --wait`.
|
|
10
10
|
|
|
@@ -16,9 +16,9 @@ npx saypsst doctor --wait
|
|
|
16
16
|
|
|
17
17
|
Briefs for Orchestrapay and Enrow (the exact prompt, the PR contents, host notes): `features/publish-integration-2026-10-28/install-briefs/`.
|
|
18
18
|
|
|
19
|
-
Or paste this to your coding agent: *Run `npx saypsst init --site site_xxxxxxxxxxxxxxxx` and follow the
|
|
19
|
+
Or paste this to your coding agent: *Run `npx saypsst init --site site_xxxxxxxxxxxxxxxx` and follow the PSST skill.* `init` copies the skills into `.claude/skills/` and `.agents/skills/`: `psst` (the plumbing) and `psst-tasks` (below).
|
|
20
20
|
|
|
21
|
-
- **No secrets.** The site id is public.
|
|
21
|
+
- **No secrets.** The site id is public. PSST never asks for a key, token or deploy access.
|
|
22
22
|
- **Zero runtime dependencies, no install scripts.** One bundled file, Node 20 or later.
|
|
23
23
|
- **Idempotent.** `init` marks with ✓ what is in place, does only what is missing, never overwrites a file you edited, and prints the exact edit for a file too unusual to change.
|
|
24
24
|
|
|
@@ -30,20 +30,20 @@ Or paste this to your coding agent: *Run `npx saypsst init --site site_xxxxxxxxx
|
|
|
30
30
|
| Static site on Vercel | `vercel.json` without Next (or `--mode vercel_static`) | Rewrites `{prefix}/:path*` → `https://edge.saypsst.com/s/{site}{prefix}/:path*` (and claimed root paths) inserted first in `vercel.json`, every other byte kept; the sitemap line in `robots.txt`. |
|
|
31
31
|
| Anything else | | Nothing but the skill; prints how to route the prefix (Cloudflare Worker, nginx, CloudFront, Next Pages Router rewrites). |
|
|
32
32
|
|
|
33
|
-
All modes: the two skills, and with `--agents-md` a two-line note in `AGENTS.md`. Before writing anything, `init` checks the site id with
|
|
33
|
+
All modes: the two skills, and with `--agents-md` a two-line note in `AGENTS.md`. Before writing anything, `init` checks the site id with PSST, that the prefix is free in the repository and on the live site, and that the app is supported (App Router, Next ≥ 15, no Cache Components).
|
|
34
34
|
|
|
35
35
|
`init` options: `--site`, `--site-name` (Next.js: overrides the brand name for Open Graph and JSON-LD; defaults to the dashboard's, then `psst.config.ts`), `--prefix` / `--domain` (must equal the dashboard's; default to it), `--mode next|vercel_static|proxy`, `--agents-md`, `--no-install`, `--dry-run`, `--dir <path>`. For local testing: `--edge <origin>` (or `PSST_EDGE`; `http:` only on localhost), `--site-url <origin>` (the live site to check the prefix on, default `https://{domain}`), `--psst-next <spec>` (dependency spec for `@saypsst/next`, for example a tarball path).
|
|
36
36
|
|
|
37
|
-
## Let your agent do your
|
|
37
|
+
## Let your agent do your PSST tasks
|
|
38
38
|
|
|
39
|
-
Every
|
|
39
|
+
Every PSST brief says who does it once you approve it: **PSST** (mentions, listings, community, pages on PSST's own paths) or **your agent** (changes to your existing pages and stack). The `psst-tasks` skill is how your agent does its share.
|
|
40
40
|
|
|
41
|
-
1. Approve briefs in the
|
|
42
|
-
2. Connect the
|
|
43
|
-
3. Tell your agent: *Do my
|
|
44
|
-
4. Optional: a nightly run ("Anything to implement from
|
|
41
|
+
1. Approve briefs in the PSST dashboard (Overview ✓). The ones marked *Done by <Brand>'s agent* are now your agent's.
|
|
42
|
+
2. Connect the PSST connector (`https://app.saypsst.com/api/mcp`) in Claude Code or Codex (setup: https://app.saypsst.com/connectors).
|
|
43
|
+
3. Tell your agent: *Do my PSST tasks. Don't stop until they're done.* It lists its approved briefs, makes each change in this repository, opens a pull request the way your repository works, and marks a brief done only once the change is live on production.
|
|
44
|
+
4. Optional: a nightly run ("Anything to implement from PSST?"). `skills/psst-tasks/routine.md` has the prompt.
|
|
45
45
|
|
|
46
|
-
Your agent never approves or declines briefs, never acts off your site and never touches
|
|
46
|
+
Your agent never approves or declines briefs, never acts off your site and never touches PSST's own paths.
|
|
47
47
|
|
|
48
48
|
## `doctor`
|
|
49
49
|
|
|
@@ -55,4 +55,4 @@ The site's mode, prefix, claimed root paths, published pages with their 12-chara
|
|
|
55
55
|
|
|
56
56
|
## Output and exit codes
|
|
57
57
|
|
|
58
|
-
`--json` prints one object per command; the JSON Schemas are in `schemas/` (`index.json` maps them). Exit codes: **0** done · **1** failed (message id `P01`…`P44`, explained in the skill's `troubleshooting.md`) · **2** usage · **3** waiting, run the same command again · **4**
|
|
58
|
+
`--json` prints one object per command; the JSON Schemas are in `schemas/` (`index.json` maps them). Exit codes: **0** done · **1** failed (message id `P01`…`P44`, explained in the skill's `troubleshooting.md`) · **2** usage · **3** waiting, run the same command again · **4** PSST or the site unreachable from here.
|
package/dist/bin.mjs
CHANGED
|
@@ -12421,7 +12421,20 @@ var publicIdSchema = external_exports.string().regex(/^site_[a-z2-7]{16}$/);
|
|
|
12421
12421
|
var sitePathSchema = external_exports.string().max(512).regex(SITE_PATH).refine((p) => !p.startsWith("//") && !p.includes(".."), "invalid path");
|
|
12422
12422
|
var siteModeSchema = external_exports.enum(["next", "vercel_static", "proxy"]);
|
|
12423
12423
|
var agentClassSchema = external_exports.enum(["training", "search", "user_fetch", "other"]);
|
|
12424
|
-
var referrerClassSchema = external_exports.enum([
|
|
12424
|
+
var referrerClassSchema = external_exports.enum([
|
|
12425
|
+
"chatgpt",
|
|
12426
|
+
"perplexity",
|
|
12427
|
+
"gemini",
|
|
12428
|
+
"copilot",
|
|
12429
|
+
"claude",
|
|
12430
|
+
"grok",
|
|
12431
|
+
"mistral",
|
|
12432
|
+
"deepseek",
|
|
12433
|
+
"meta",
|
|
12434
|
+
"you",
|
|
12435
|
+
"other",
|
|
12436
|
+
"none"
|
|
12437
|
+
]);
|
|
12425
12438
|
var hitSourceSchema = external_exports.enum(["edge", "proxy"]);
|
|
12426
12439
|
var publishStateSchema = external_exports.enum(["published", "verified"]);
|
|
12427
12440
|
var hashSchema = external_exports.string().regex(/^[0-9a-f]{64}$/);
|
|
@@ -12492,7 +12505,17 @@ var AI_AGENT_RULES = [
|
|
|
12492
12505
|
{ agent: "Meta-ExternalAgent", agentClass: "training", pattern: /meta-externalagent/i },
|
|
12493
12506
|
{ agent: "Bytespider", agentClass: "training", pattern: /bytespider/i },
|
|
12494
12507
|
{ agent: "CCBot", agentClass: "training", pattern: /ccbot/i },
|
|
12495
|
-
{ agent: "Amazonbot", agentClass: "training", pattern: /amazonbot/i }
|
|
12508
|
+
{ agent: "Amazonbot", agentClass: "training", pattern: /amazonbot/i },
|
|
12509
|
+
{ agent: "MistralAI-User", agentClass: "user_fetch", pattern: /mistralai-user/i },
|
|
12510
|
+
{ agent: "MistralAI-Index", agentClass: "search", pattern: /mistralai-index/i },
|
|
12511
|
+
{ agent: "DuckAssistBot", agentClass: "search", pattern: /duckassistbot/i },
|
|
12512
|
+
{ agent: "Google-Agent", agentClass: "user_fetch", pattern: /google-agent/i },
|
|
12513
|
+
{ agent: "Meta-ExternalFetcher", agentClass: "user_fetch", pattern: /meta-externalfetcher/i },
|
|
12514
|
+
{ agent: "meta-webindexer", agentClass: "search", pattern: /meta-webindexer/i },
|
|
12515
|
+
{ agent: "YouBot", agentClass: "search", pattern: /youbot/i },
|
|
12516
|
+
{ agent: "Amzn-SearchBot", agentClass: "search", pattern: /amzn-searchbot/i },
|
|
12517
|
+
{ agent: "Amzn-User", agentClass: "user_fetch", pattern: /amzn-user/i },
|
|
12518
|
+
{ agent: "Claude-Agent", agentClass: "user_fetch", pattern: /claude-agent/i }
|
|
12496
12519
|
];
|
|
12497
12520
|
new Set(AI_AGENT_RULES.map((r) => r.agent));
|
|
12498
12521
|
|
|
@@ -12517,7 +12540,7 @@ var MESSAGES = {
|
|
|
12517
12540
|
P05: { code: "site_unknown", exit: EXIT.failed },
|
|
12518
12541
|
P06: { code: "edge_error", exit: EXIT.failed },
|
|
12519
12542
|
P07: { code: "site_name_required", exit: EXIT.usage },
|
|
12520
|
-
P08: { code: "cache_components", exit:
|
|
12543
|
+
P08: { code: "cache_components", exit: null },
|
|
12521
12544
|
P09: { code: "config_other_site", exit: EXIT.failed },
|
|
12522
12545
|
P10: { code: "prefix_mismatch", exit: EXIT.usage },
|
|
12523
12546
|
P11: { code: "domain_mismatch", exit: EXIT.usage },
|
|
@@ -12570,8 +12593,8 @@ var CliError = class extends Error {
|
|
|
12570
12593
|
};
|
|
12571
12594
|
|
|
12572
12595
|
// src/version.ts
|
|
12573
|
-
var CLI_VERSION = "0.
|
|
12574
|
-
var PSST_NEXT_VERSION = "0.
|
|
12596
|
+
var CLI_VERSION = "0.2.0";
|
|
12597
|
+
var PSST_NEXT_VERSION = "0.3.0";
|
|
12575
12598
|
var JSON_CONTRACT = "1.0";
|
|
12576
12599
|
var DEFAULT_EDGE = "https://edge.saypsst.com";
|
|
12577
12600
|
var USER_AGENT = `psst-cli/${CLI_VERSION} (+https://saypsst.com)`;
|
|
@@ -12595,7 +12618,7 @@ function parseEdgeOrigin(value) {
|
|
|
12595
12618
|
}
|
|
12596
12619
|
function assertSiteId(value) {
|
|
12597
12620
|
if (!publicIdSchema.safeParse(value).success) {
|
|
12598
|
-
throw new CliError("P13", `"${value}" is not a
|
|
12621
|
+
throw new CliError("P13", `"${value}" is not a PSST site id: it looks like site_ followed by 16 letters and digits`);
|
|
12599
12622
|
}
|
|
12600
12623
|
return value;
|
|
12601
12624
|
}
|
|
@@ -12618,25 +12641,25 @@ async function fetchManifest(edge, site) {
|
|
|
12618
12641
|
} catch {
|
|
12619
12642
|
throw new CliError(
|
|
12620
12643
|
"P04",
|
|
12621
|
-
`
|
|
12644
|
+
`PSST is unreachable from here (${edge}). Allow network access for this command, or ask the person to run it`
|
|
12622
12645
|
);
|
|
12623
12646
|
}
|
|
12624
12647
|
if (res.status === 404) {
|
|
12625
|
-
throw new CliError("P05", `
|
|
12648
|
+
throw new CliError("P05", `PSST knows no site ${site}. Copy the site id from the dashboard, Settings \u203A Connections`);
|
|
12626
12649
|
}
|
|
12627
12650
|
if (res.status === 503 || res.status === 429) {
|
|
12628
|
-
throw new CliError("P16", `
|
|
12651
|
+
throw new CliError("P16", `PSST answered ${res.status} (busy or down for a moment). Run the same command again in a few minutes`);
|
|
12629
12652
|
}
|
|
12630
|
-
if (res.status !== 200) throw new CliError("P06", `
|
|
12653
|
+
if (res.status !== 200) throw new CliError("P06", `PSST answered ${res.status} for the site manifest`);
|
|
12631
12654
|
let raw;
|
|
12632
12655
|
try {
|
|
12633
12656
|
raw = JSON.parse(res.body);
|
|
12634
12657
|
} catch {
|
|
12635
|
-
throw new CliError("P06", "
|
|
12658
|
+
throw new CliError("P06", "PSST answered with something that is not JSON for the site manifest");
|
|
12636
12659
|
}
|
|
12637
12660
|
const parsed = manifestResponseSchema.safeParse(raw);
|
|
12638
|
-
if (!parsed.success) throw new CliError("P06", "the site manifest does not match this CLI's contract: update
|
|
12639
|
-
if (parsed.data.site.publicId !== site) throw new CliError("P06", "
|
|
12661
|
+
if (!parsed.success) throw new CliError("P06", "the site manifest does not match this CLI's contract: update PSST (npx saypsst@latest)");
|
|
12662
|
+
if (parsed.data.site.publicId !== site) throw new CliError("P06", "PSST answered with another site's manifest");
|
|
12640
12663
|
return parsed.data;
|
|
12641
12664
|
}
|
|
12642
12665
|
async function fetchEdgeVerify(edge, site, prefix) {
|
|
@@ -12803,7 +12826,8 @@ function parsePsstConfig(file2, text) {
|
|
|
12803
12826
|
siteName: stringValue(text, "siteName"),
|
|
12804
12827
|
prefix: stringValue(text, "prefix"),
|
|
12805
12828
|
rootPaths: roots?.[1] !== void 0 ? [...roots[1].matchAll(/["']([^"']+)["']/g)].map((m) => m[1] ?? "") : null,
|
|
12806
|
-
edge: stringValue(text, "edge")
|
|
12829
|
+
edge: stringValue(text, "edge"),
|
|
12830
|
+
serveFromEdge: /\bserveFromEdge\s*:\s*true\b/.test(text)
|
|
12807
12831
|
};
|
|
12808
12832
|
}
|
|
12809
12833
|
function findPsstConfig(dir) {
|
|
@@ -12911,7 +12935,7 @@ function resolveSite(dir, site, edge) {
|
|
|
12911
12935
|
if (!id) {
|
|
12912
12936
|
throw new CliError(
|
|
12913
12937
|
"P42",
|
|
12914
|
-
"no
|
|
12938
|
+
"no PSST install here (no psst.config.ts, no PSST rewrite in vercel.json): pass --site site_\u2026 or run in the site's folder"
|
|
12915
12939
|
);
|
|
12916
12940
|
}
|
|
12917
12941
|
return {
|
|
@@ -12936,7 +12960,7 @@ function notOk(name, url2, res) {
|
|
|
12936
12960
|
if (res === null) return check2(name, "failed", `${url2} could not be reached`, "P40");
|
|
12937
12961
|
if (res.status === 404) return check2(name, "waiting", `${url2} answers 404: deploy the install, then run doctor again`, "P30");
|
|
12938
12962
|
if (res.status >= 300 && res.status < 400) {
|
|
12939
|
-
return check2(name, "failed", `${url2} redirects (${res.status}) to ${res.headers.get("location") ?? "?"}:
|
|
12963
|
+
return check2(name, "failed", `${url2} redirects (${res.status}) to ${res.headers.get("location") ?? "?"}: PSST's paths must answer directly on the site's domain`, "P39");
|
|
12940
12964
|
}
|
|
12941
12965
|
return check2(name, "failed", `${url2} answers ${res.status}`, "P44");
|
|
12942
12966
|
}
|
|
@@ -12955,7 +12979,7 @@ async function checkStyle(base, prefix) {
|
|
|
12955
12979
|
const res = await get(url2);
|
|
12956
12980
|
if (!res || res.status !== 200) return notOk("style", url2, res);
|
|
12957
12981
|
const type = res.headers.get("content-type") ?? "";
|
|
12958
|
-
return type.startsWith("text/css") ? check2("style", "passed", "the
|
|
12982
|
+
return type.startsWith("text/css") ? check2("style", "passed", "the PSST stylesheet is served same-origin") : check2("style", "failed", `${url2} answers ${type || "no content type"}, not text/css`, "P44");
|
|
12959
12983
|
}
|
|
12960
12984
|
async function checkSitemap(base, m, pages) {
|
|
12961
12985
|
const url2 = `${base}${m.site.prefix}/sitemap.xml`;
|
|
@@ -12988,9 +13012,9 @@ async function checkPage(base, domain2, page) {
|
|
|
12988
13012
|
}
|
|
12989
13013
|
const html = res.body;
|
|
12990
13014
|
const served = metaContent(html, "psst-version");
|
|
12991
|
-
if (served === null) return check2(name, "failed", `${url2} has no psst-version meta (as ${probe.name}): it is not a
|
|
13015
|
+
if (served === null) return check2(name, "failed", `${url2} has no psst-version meta (as ${probe.name}): it is not a PSST page`, "P44");
|
|
12992
13016
|
if (served !== version2) {
|
|
12993
|
-
return check2(name, "waiting", `${url2} serves version ${served},
|
|
13017
|
+
return check2(name, "waiting", `${url2} serves version ${served}, PSST published ${version2} (as ${probe.name}); caches refresh within 10 minutes`, "P32");
|
|
12994
13018
|
}
|
|
12995
13019
|
const robotsMeta = (metaContent(html, "robots") ?? "").toLowerCase();
|
|
12996
13020
|
const robotsHeader = (res.headers.get("x-robots-tag") ?? "").toLowerCase();
|
|
@@ -13008,7 +13032,7 @@ async function checkPage(base, domain2, page) {
|
|
|
13008
13032
|
const text = articleText(html);
|
|
13009
13033
|
if (browserText === null) browserText = text;
|
|
13010
13034
|
else if (text !== browserText) {
|
|
13011
|
-
return check2(name, "failed", `${url2} serves ${probe.name} other content than a browser:
|
|
13035
|
+
return check2(name, "failed", `${url2} serves ${probe.name} other content than a browser: PSST pages must be identical for people and crawlers`, "P35");
|
|
13012
13036
|
}
|
|
13013
13037
|
}
|
|
13014
13038
|
return check2(name, "passed", `200, version ${version2}, canonical, indexable, full text without JavaScript, same for ${PROBES.map((p) => p.name).join(", ")}`);
|
|
@@ -13017,7 +13041,7 @@ async function checkRobots(base, prefix) {
|
|
|
13017
13041
|
const url2 = `${base}/robots.txt`;
|
|
13018
13042
|
const res = await get(url2);
|
|
13019
13043
|
if (!res || res.status === 404 || res.status === 410) {
|
|
13020
|
-
return [check2("robots", "information", res ? "no robots.txt: every crawler may fetch
|
|
13044
|
+
return [check2("robots", "information", res ? "no robots.txt: every crawler may fetch PSST's pages" : `${url2} could not be reached`)];
|
|
13021
13045
|
}
|
|
13022
13046
|
if (res.status !== 200) return [check2("robots", "information", `${url2} answers ${res.status}`)];
|
|
13023
13047
|
const robots = parseRobots(res.body);
|
|
@@ -13039,7 +13063,7 @@ async function checkRobots(base, prefix) {
|
|
|
13039
13063
|
);
|
|
13040
13064
|
const listed = robots.sitemaps.some((s) => s.endsWith(`${prefix}/sitemap.xml`));
|
|
13041
13065
|
out.push(
|
|
13042
|
-
check2("robots:sitemap", "information", listed ? "robots.txt lists the
|
|
13066
|
+
check2("robots:sitemap", "information", listed ? "robots.txt lists the PSST sitemap" : `robots.txt does not list ${prefix}/sitemap.xml (recommended)`, listed ? void 0 : "P21")
|
|
13043
13067
|
);
|
|
13044
13068
|
return out;
|
|
13045
13069
|
}
|
|
@@ -13047,7 +13071,7 @@ async function doctorRound(base, edge, site) {
|
|
|
13047
13071
|
const m = await fetchManifest(edge, site);
|
|
13048
13072
|
const prefix = m.site.prefix;
|
|
13049
13073
|
const pages = m.pages.filter((p) => p.kind !== "llms_txt").sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1).slice(0, MAX_PAGES);
|
|
13050
|
-
const checks = [check2("manifest", "passed", `
|
|
13074
|
+
const checks = [check2("manifest", "passed", `PSST knows ${site} (${m.site.domain}, ${m.site.mode}, ${prefix})`)];
|
|
13051
13075
|
const [verify, style, sitemap] = await Promise.all([checkVerify(base, edge, site, prefix), checkStyle(base, prefix), checkSitemap(base, m, pages)]);
|
|
13052
13076
|
checks.push(verify, style, sitemap);
|
|
13053
13077
|
if (m.site.claimedRootPaths.includes("/llms.txt")) checks.push(await checkLlms(base));
|
|
@@ -13218,8 +13242,8 @@ function installSkill(changes, repo) {
|
|
|
13218
13242
|
var AGENTS_NOTE_MARKER = "<!-- psst -->";
|
|
13219
13243
|
function agentsNote(prefix) {
|
|
13220
13244
|
return `${AGENTS_NOTE_MARKER}
|
|
13221
|
-
|
|
13222
|
-
Before touching
|
|
13245
|
+
PSST serves AI-search pages on this site under \`${prefix}\` (\`psst.config.ts\`, \`app/(psst)/\` or the \`vercel.json\` rewrites); its content is never edited here.
|
|
13246
|
+
Before touching PSST files or running \`npx saypsst\`, read \`.claude/skills/psst/SKILL.md\` (also in \`.agents/skills/psst/\`).
|
|
13223
13247
|
`;
|
|
13224
13248
|
}
|
|
13225
13249
|
function agentsMdCandidates(repo, dir) {
|
|
@@ -13234,7 +13258,7 @@ function agentsMd(changes, repo, dir, prefix, requested) {
|
|
|
13234
13258
|
const candidates = agentsMdCandidates(repo, dir);
|
|
13235
13259
|
const noted = candidates.find((f) => readText(f)?.includes(AGENTS_NOTE_MARKER));
|
|
13236
13260
|
if (noted) {
|
|
13237
|
-
changes.unchanged(changes.rel(noted), "
|
|
13261
|
+
changes.unchanged(changes.rel(noted), "PSST note");
|
|
13238
13262
|
return;
|
|
13239
13263
|
}
|
|
13240
13264
|
const file2 = candidates.find((f) => readText(f) !== null) ?? join(repo, "AGENTS.md");
|
|
@@ -13242,14 +13266,14 @@ function agentsMd(changes, repo, dir, prefix, requested) {
|
|
|
13242
13266
|
if (!requested) {
|
|
13243
13267
|
changes.info(
|
|
13244
13268
|
changes.rel(file2),
|
|
13245
|
-
'Ask the person: "Add a two-line note to AGENTS.md so every coding agent knows
|
|
13246
|
-
message("P28", "AGENTS.md has no
|
|
13269
|
+
'Ask the person: "Add a two-line note to AGENTS.md so every coding agent knows PSST is installed?" If yes, run npx saypsst init --agents-md',
|
|
13270
|
+
message("P28", "AGENTS.md has no PSST note")
|
|
13247
13271
|
);
|
|
13248
13272
|
return;
|
|
13249
13273
|
}
|
|
13250
13274
|
const note = agentsNote(prefix);
|
|
13251
|
-
if (current === null) changes.create(file2, note, "
|
|
13252
|
-
else changes.replace(file2, `${current}${current.endsWith("\n") ? "\n" : "\n\n"}${note}`, "
|
|
13275
|
+
if (current === null) changes.create(file2, note, "PSST note");
|
|
13276
|
+
else changes.replace(file2, `${current}${current.endsWith("\n") ? "\n" : "\n\n"}${note}`, "PSST note appended");
|
|
13253
13277
|
}
|
|
13254
13278
|
function addDependency(changes, dir, name, spec) {
|
|
13255
13279
|
const file2 = join(dir, "package.json");
|
|
@@ -13282,6 +13306,13 @@ function addDependency(changes, dir, name, spec) {
|
|
|
13282
13306
|
}
|
|
13283
13307
|
|
|
13284
13308
|
// src/edit/proxy.ts
|
|
13309
|
+
function edgeMatcherPaths(prefix, rootPaths) {
|
|
13310
|
+
return [`${prefix}/:path*`, ...rootPaths];
|
|
13311
|
+
}
|
|
13312
|
+
function missingMatcherPaths(content, paths) {
|
|
13313
|
+
if (!/\bmatcher\b/.test(content) || content.includes("(?!")) return [];
|
|
13314
|
+
return paths.filter((p) => !content.includes(`"${p}"`) && !content.includes(`'${p}'`));
|
|
13315
|
+
}
|
|
13285
13316
|
function hasPsstProxy(content) {
|
|
13286
13317
|
return /\bwithPsst\s*\(/.test(content) && content.includes("@saypsst/next/proxy");
|
|
13287
13318
|
}
|
|
@@ -13323,9 +13354,8 @@ import psst from "${configImport}";`;
|
|
|
13323
13354
|
indentLines(`export const proxy = withPsst(psst, siteProxy);`, 3)
|
|
13324
13355
|
);
|
|
13325
13356
|
}
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
);
|
|
13357
|
+
const matcher = input2.edgeRootPaths ? `${edgeMatcherPaths(prefix, input2.edgeRootPaths).map((p) => `"${p}"`).join(", ")} (PSST serves these paths from the proxy)` : `"${prefix}/:path*" (PSST counts AI crawlers there)`;
|
|
13358
|
+
steps.push(`${steps.filter((l) => /^\d\./.test(l)).length + 1}. Keep \`config.matcher\` a literal and make sure it also matches ${matcher}.`);
|
|
13329
13359
|
return steps.join("\n");
|
|
13330
13360
|
}
|
|
13331
13361
|
function indentLines(text, n) {
|
|
@@ -13340,12 +13370,13 @@ function psstConfigFile(v) {
|
|
|
13340
13370
|
`import { definePsst } from "@saypsst/next";`,
|
|
13341
13371
|
``,
|
|
13342
13372
|
`export default definePsst({`,
|
|
13343
|
-
` site: ${q(v.site)}, // public site id from the
|
|
13373
|
+
` site: ${q(v.site)}, // public site id from the PSST dashboard`,
|
|
13344
13374
|
` domain: ${q(v.domain)}, // production host, no scheme`,
|
|
13345
13375
|
` siteName: ${q(v.siteName)},`,
|
|
13346
13376
|
` prefix: ${q(v.prefix)},`,
|
|
13347
13377
|
` rootPaths: ${rootPaths}, // add "/about" and/or "/llms.txt" only when the dashboard claimed them`,
|
|
13348
13378
|
...v.edge ? [` edge: ${q(v.edge)},`] : [],
|
|
13379
|
+
...v.serveFromEdge ? [` serveFromEdge: true, // Cache Components: proxy.ts serves PSST pages from the edge`] : [],
|
|
13349
13380
|
`});`,
|
|
13350
13381
|
``
|
|
13351
13382
|
];
|
|
@@ -13658,14 +13689,14 @@ async function runInit(opts) {
|
|
|
13658
13689
|
const vercelInstall = findVercelInstall(opts.dir);
|
|
13659
13690
|
const siteArg = opts.site ?? existing?.site ?? vercelInstall?.site;
|
|
13660
13691
|
if (!siteArg) {
|
|
13661
|
-
throw new CliError("P13", "init needs --site site_\u2026 (the site id from the
|
|
13692
|
+
throw new CliError("P13", "init needs --site site_\u2026 (the site id from the PSST dashboard, Settings \u203A Connections)");
|
|
13662
13693
|
}
|
|
13663
13694
|
const site = assertSiteId(siteArg);
|
|
13664
13695
|
const project = detectProject(opts.dir, opts.mode);
|
|
13665
13696
|
if (project.mode === "pages_router") {
|
|
13666
13697
|
throw new CliError(
|
|
13667
13698
|
"P01",
|
|
13668
|
-
"this Next.js app has no App Router folder (app/ or src/app/).
|
|
13699
|
+
"this Next.js app has no App Router folder (app/ or src/app/). PSST's Next.js routes need the App Router; serve PSST through a proxy instead: see proxy.md in the PSST skill, or run init --mode proxy"
|
|
13669
13700
|
);
|
|
13670
13701
|
}
|
|
13671
13702
|
if (project.mode === "proxy" && project.candidates.length > 0) {
|
|
@@ -13678,19 +13709,19 @@ async function runInit(opts) {
|
|
|
13678
13709
|
if (existing?.site && existing.site !== site) {
|
|
13679
13710
|
throw new CliError(
|
|
13680
13711
|
"P09",
|
|
13681
|
-
`${existing.file.slice(opts.dir.length + 1)} is set up for ${existing.site}, not ${site}.
|
|
13712
|
+
`${existing.file.slice(opts.dir.length + 1)} is set up for ${existing.site}, not ${site}. PSST never overwrites it: if this repository moves to the new site, change \`site\` there by hand, then run init again`
|
|
13682
13713
|
);
|
|
13683
13714
|
}
|
|
13684
13715
|
const manifest = await fetchManifest(edge, site);
|
|
13685
13716
|
const domain2 = manifest.site.domain;
|
|
13686
13717
|
if (opts.domain && opts.domain !== domain2) {
|
|
13687
|
-
throw new CliError("P11", `--domain ${opts.domain} differs from the site's domain in
|
|
13718
|
+
throw new CliError("P11", `--domain ${opts.domain} differs from the site's domain in PSST, ${domain2}. Change it in the dashboard, or drop --domain`);
|
|
13688
13719
|
}
|
|
13689
13720
|
const prefix = manifest.site.prefix;
|
|
13690
13721
|
if (opts.prefix && opts.prefix !== prefix) {
|
|
13691
13722
|
throw new CliError(
|
|
13692
13723
|
"P10",
|
|
13693
|
-
`--prefix ${opts.prefix} differs from the site's prefix in
|
|
13724
|
+
`--prefix ${opts.prefix} differs from the site's prefix in PSST, ${prefix}. Change the prefix in the dashboard (Settings \u203A Connections) first, then run init again`
|
|
13694
13725
|
);
|
|
13695
13726
|
}
|
|
13696
13727
|
const changes = new Changes(opts.dir, opts.dryRun, repoRoot(opts.dir));
|
|
@@ -13711,12 +13742,13 @@ async function runInit(opts) {
|
|
|
13711
13742
|
);
|
|
13712
13743
|
}
|
|
13713
13744
|
if (project.nextMajor !== null && project.nextMajor < 15) {
|
|
13714
|
-
throw new CliError("P14", `
|
|
13745
|
+
throw new CliError("P14", `PSST needs Next.js 15 or later (this app has ${project.nextMajor}). Upgrade Next, or use --mode proxy`);
|
|
13715
13746
|
}
|
|
13716
13747
|
if (project.cacheComponents) {
|
|
13717
|
-
|
|
13718
|
-
"
|
|
13719
|
-
"this app turns on Cache Components
|
|
13748
|
+
changes.info(
|
|
13749
|
+
"cacheComponents",
|
|
13750
|
+
"this app turns on Cache Components, where PSST's route files do not build: proxy.ts serves PSST pages from the edge, with PSST's own header and footer",
|
|
13751
|
+
message("P08", "Cache Components is on: PSST pages come from the edge, not this app's layout")
|
|
13720
13752
|
);
|
|
13721
13753
|
}
|
|
13722
13754
|
const conflicts = appRoutesUnder(join(opts.dir, project.appDir), prefix);
|
|
@@ -13724,7 +13756,7 @@ async function runInit(opts) {
|
|
|
13724
13756
|
throw new CliError("P03", `the app already has routes under ${prefix}: ${conflicts.map((c) => `${project.appDir}/${c}`).join(", ")}. Pick a free prefix in the dashboard, then run init again`);
|
|
13725
13757
|
}
|
|
13726
13758
|
await checkLive(changes, opts, site, domain2, prefix);
|
|
13727
|
-
await setUpNext(changes, opts, project, manifest, { site, domain: domain2, siteName, prefix, edge }, existing, next);
|
|
13759
|
+
await setUpNext(changes, opts, project, manifest, { site, domain: domain2, siteName, prefix, edge, serveFromEdge: project.cacheComponents }, existing, next);
|
|
13728
13760
|
} else if (project.mode === "vercel_static") {
|
|
13729
13761
|
const conflict = staticPrefixConflict(opts.dir, prefix, site);
|
|
13730
13762
|
if (conflict) {
|
|
@@ -13739,7 +13771,7 @@ async function runInit(opts) {
|
|
|
13739
13771
|
`nothing to write: route ${prefix}/* (and any claimed root path) on ${domain2} to ${edge}/s/${site}${prefix}/* at your host or CDN`
|
|
13740
13772
|
);
|
|
13741
13773
|
next.push(
|
|
13742
|
-
`Route ${prefix}/* to ${edge}/s/${site}${prefix}/* in front of the site, as proxy.md in the
|
|
13774
|
+
`Route ${prefix}/* to ${edge}/s/${site}${prefix}/* in front of the site, as proxy.md in the PSST skill shows (Cloudflare Worker, nginx, CloudFront)`
|
|
13743
13775
|
);
|
|
13744
13776
|
for (const p of manifest.site.claimedRootPaths) next.push(`Route ${p} to ${edge}/s/${site}${p}`);
|
|
13745
13777
|
next.push(`Add "Sitemap: https://${domain2}${prefix}/sitemap.xml" to robots.txt`);
|
|
@@ -13765,7 +13797,7 @@ async function checkLive(changes, opts, site, domain2, prefix) {
|
|
|
13765
13797
|
if (live.kind === "skipped") {
|
|
13766
13798
|
changes.info(prefix, `live check skipped: ${live.reason}`, message("P29", "could not check that the prefix is free"));
|
|
13767
13799
|
} else if (live.kind === "catch_all") {
|
|
13768
|
-
changes.info(prefix, `the live site answers every path;
|
|
13800
|
+
changes.info(prefix, `the live site answers every path; PSST's routes take precedence under ${prefix}`);
|
|
13769
13801
|
}
|
|
13770
13802
|
}
|
|
13771
13803
|
async function setUpNext(changes, opts, project, manifest, v, existing, next) {
|
|
@@ -13782,7 +13814,7 @@ async function setUpNext(changes, opts, project, manifest, v, existing, next) {
|
|
|
13782
13814
|
if (served) {
|
|
13783
13815
|
changes.info(
|
|
13784
13816
|
p,
|
|
13785
|
-
`the site serves ${p} itself (${served});
|
|
13817
|
+
`the site serves ${p} itself (${served}); PSST leaves it alone. Ask PSST to release the claim in the dashboard`,
|
|
13786
13818
|
message("P25", `${p} is already served by the site`),
|
|
13787
13819
|
"skipped"
|
|
13788
13820
|
);
|
|
@@ -13794,7 +13826,8 @@ async function setUpNext(changes, opts, project, manifest, v, existing, next) {
|
|
|
13794
13826
|
siteName: v.siteName,
|
|
13795
13827
|
prefix: v.prefix,
|
|
13796
13828
|
rootPaths,
|
|
13797
|
-
...v.edge !== DEFAULT_EDGE ? { edge: v.edge } : {}
|
|
13829
|
+
...v.edge !== DEFAULT_EDGE ? { edge: v.edge } : {},
|
|
13830
|
+
...v.serveFromEdge ? { serveFromEdge: true } : {}
|
|
13798
13831
|
};
|
|
13799
13832
|
writeConfig(changes, configFile, values, existing);
|
|
13800
13833
|
const app = join(dir, project.appDir);
|
|
@@ -13802,10 +13835,10 @@ async function setUpNext(changes, opts, project, manifest, v, existing, next) {
|
|
|
13802
13835
|
const file2 = (abs) => ({ abs, config: importPath(abs, configNoExt) });
|
|
13803
13836
|
const routeFiles = [];
|
|
13804
13837
|
const page = file2(join(group, "[...slug]", `page.${ts ? "tsx" : "js"}`));
|
|
13805
|
-
routeFiles.push({ ...page, content: catchAllPageFile(page.config), module: "@saypsst/next/page", what: "
|
|
13838
|
+
routeFiles.push({ ...page, content: catchAllPageFile(page.config), module: "@saypsst/next/page", what: "PSST pages" });
|
|
13806
13839
|
const handlers = [
|
|
13807
|
-
["sitemap", ["sitemap.xml"], "
|
|
13808
|
-
["style", ["%5Fpsst", "style.css"], "
|
|
13840
|
+
["sitemap", ["sitemap.xml"], "PSST sitemap"],
|
|
13841
|
+
["style", ["%5Fpsst", "style.css"], "PSST stylesheet"],
|
|
13809
13842
|
["verify", ["%5Fpsst", "verify"], "domain proof"]
|
|
13810
13843
|
];
|
|
13811
13844
|
for (const [kind, segs, what] of handlers) {
|
|
@@ -13814,15 +13847,19 @@ async function setUpNext(changes, opts, project, manifest, v, existing, next) {
|
|
|
13814
13847
|
}
|
|
13815
13848
|
if (rootPaths.includes("/about")) {
|
|
13816
13849
|
const f = file2(join(app, "about", `page.${ts ? "tsx" : "js"}`));
|
|
13817
|
-
routeFiles.push({ ...f, content: aboutPageFile(f.config), module: "@saypsst/next/page", what: "/about (claimed by
|
|
13850
|
+
routeFiles.push({ ...f, content: aboutPageFile(f.config), module: "@saypsst/next/page", what: "/about (claimed by PSST)" });
|
|
13818
13851
|
}
|
|
13819
13852
|
if (rootPaths.includes("/llms.txt")) {
|
|
13820
13853
|
const f = file2(join(app, "llms.txt", `route.${ext}`));
|
|
13821
|
-
routeFiles.push({ ...f, content: handlerFile("llms", f.config), module: "@saypsst/next/llms", what: "/llms.txt (claimed by
|
|
13854
|
+
routeFiles.push({ ...f, content: handlerFile("llms", f.config), module: "@saypsst/next/llms", what: "/llms.txt (claimed by PSST)" });
|
|
13822
13855
|
}
|
|
13823
13856
|
for (const r of routeFiles) {
|
|
13824
13857
|
const current = readText(r.abs);
|
|
13825
|
-
if (
|
|
13858
|
+
if (v.serveFromEdge) {
|
|
13859
|
+
if (current !== null && isOurs(current, r.module)) {
|
|
13860
|
+
changes.edit(changes.rel(r.abs), r.what, "Delete this file: it does not build with Cache Components, and proxy.ts serves this path now", message("P08", `${changes.rel(r.abs)} does not build with Cache Components`));
|
|
13861
|
+
}
|
|
13862
|
+
} else if (current === null) changes.create(r.abs, r.content, r.what);
|
|
13826
13863
|
else if (current === r.content || isOurs(current, r.module)) changes.unchanged(changes.rel(r.abs), r.what);
|
|
13827
13864
|
else {
|
|
13828
13865
|
changes.edit(
|
|
@@ -13830,7 +13867,7 @@ async function setUpNext(changes, opts, project, manifest, v, existing, next) {
|
|
|
13830
13867
|
r.what,
|
|
13831
13868
|
`Replace the file with:
|
|
13832
13869
|
${indent(r.content.trimEnd())}`,
|
|
13833
|
-
message("P24", `${changes.rel(r.abs)} is not
|
|
13870
|
+
message("P24", `${changes.rel(r.abs)} is not PSST's route file; init left it alone`)
|
|
13834
13871
|
);
|
|
13835
13872
|
}
|
|
13836
13873
|
}
|
|
@@ -13839,15 +13876,23 @@ ${indent(r.content.trimEnd())}`,
|
|
|
13839
13876
|
const existingProxy = firstExisting(base, proxyNames);
|
|
13840
13877
|
if (existingProxy === null) {
|
|
13841
13878
|
const abs = join(base, `${exportName}.${ext}`);
|
|
13842
|
-
changes.create(abs, proxyFile(exportName, importPath(abs, configNoExt)), "AI crawler and referral hits, 410 for withdrawn pages");
|
|
13879
|
+
changes.create(abs, proxyFile(exportName, importPath(abs, configNoExt)), v.serveFromEdge ? "PSST pages from the edge, AI crawler and referral hits" : "AI crawler and referral hits, 410 for withdrawn pages");
|
|
13843
13880
|
} else {
|
|
13844
13881
|
const content = readText(existingProxy) ?? "";
|
|
13845
|
-
|
|
13882
|
+
const missing = v.serveFromEdge ? missingMatcherPaths(content, edgeMatcherPaths(v.prefix, rootPaths)) : [];
|
|
13883
|
+
if (hasPsstProxy(content) && missing.length > 0) {
|
|
13884
|
+
changes.edit(
|
|
13885
|
+
changes.rel(existingProxy),
|
|
13886
|
+
"PSST paths are served from this proxy: its matcher must reach them",
|
|
13887
|
+
`Add to config.matcher: ${missing.map((p) => JSON.stringify(p)).join(", ")}`,
|
|
13888
|
+
message("P20", `${changes.rel(existingProxy)} is the site's own code; init prints the edit`)
|
|
13889
|
+
);
|
|
13890
|
+
} else if (hasPsstProxy(content)) changes.unchanged(changes.rel(existingProxy), "withPsst() in place");
|
|
13846
13891
|
else {
|
|
13847
13892
|
changes.edit(
|
|
13848
13893
|
changes.rel(existingProxy),
|
|
13849
13894
|
"the site has its own proxy: wrap it with withPsst()",
|
|
13850
|
-
proxyEdit({ file: changes.rel(existingProxy), content, configImport: importPath(existingProxy, configNoExt), prefix: v.prefix }),
|
|
13895
|
+
proxyEdit({ file: changes.rel(existingProxy), content, configImport: importPath(existingProxy, configNoExt), prefix: v.prefix, ...v.serveFromEdge ? { edgeRootPaths: rootPaths } : {} }),
|
|
13851
13896
|
message("P20", `${changes.rel(existingProxy)} is the site's own code; init prints the edit`)
|
|
13852
13897
|
);
|
|
13853
13898
|
}
|
|
@@ -13891,11 +13936,14 @@ function writeConfig(changes, file2, values, existing) {
|
|
|
13891
13936
|
if ((existing.edge ?? DEFAULT_EDGE) !== (values.edge ?? DEFAULT_EDGE)) {
|
|
13892
13937
|
diffs.push(values.edge ? `edge: ${JSON.stringify(values.edge)}` : `(remove the edge key)`);
|
|
13893
13938
|
}
|
|
13939
|
+
if (existing.serveFromEdge !== (values.serveFromEdge ?? false)) {
|
|
13940
|
+
diffs.push(values.serveFromEdge ? "serveFromEdge: true" : "(remove the serveFromEdge key)");
|
|
13941
|
+
}
|
|
13894
13942
|
if (diffs.length === 0) changes.unchanged(changes.rel(file2), `site ${values.site} (with your additions)`);
|
|
13895
13943
|
else {
|
|
13896
13944
|
changes.edit(
|
|
13897
13945
|
changes.rel(file2),
|
|
13898
|
-
"psst.config differs from the site's settings in
|
|
13946
|
+
"psst.config differs from the site's settings in PSST",
|
|
13899
13947
|
`Set these keys in definePsst({ \u2026 }):
|
|
13900
13948
|
${indent(diffs.join("\n"))}`,
|
|
13901
13949
|
message("P23", `${changes.rel(file2)} differs from the dashboard; init never rewrites it`)
|
|
@@ -13908,16 +13956,16 @@ function sitemapLine(domain2, prefix) {
|
|
|
13908
13956
|
function appendRobots(changes, file2, domain2, prefix) {
|
|
13909
13957
|
const current = readText(file2) ?? "";
|
|
13910
13958
|
const line = sitemapLine(domain2, prefix);
|
|
13911
|
-
if (current.includes(`${prefix}/sitemap.xml`)) changes.unchanged(changes.rel(file2), "
|
|
13959
|
+
if (current.includes(`${prefix}/sitemap.xml`)) changes.unchanged(changes.rel(file2), "PSST sitemap listed");
|
|
13912
13960
|
else changes.replace(file2, `${current}${current === "" || current.endsWith("\n") ? "" : "\n"}${line}
|
|
13913
13961
|
`, `added ${line}`);
|
|
13914
13962
|
}
|
|
13915
13963
|
function printRobots(changes, item, domain2, prefix, how) {
|
|
13916
13964
|
changes.edit(
|
|
13917
13965
|
item,
|
|
13918
|
-
"reference the
|
|
13966
|
+
"reference the PSST sitemap",
|
|
13919
13967
|
how,
|
|
13920
|
-
message("P21", `add the
|
|
13968
|
+
message("P21", `add the PSST sitemap (https://${domain2}${prefix}/sitemap.xml) to robots`)
|
|
13921
13969
|
);
|
|
13922
13970
|
}
|
|
13923
13971
|
function robotsNext(changes, dir, project, domain2, prefix) {
|
|
@@ -13925,7 +13973,7 @@ function robotsNext(changes, dir, project, domain2, prefix) {
|
|
|
13925
13973
|
const robotsCode = firstExisting(app, ["robots.ts", "robots.js"]);
|
|
13926
13974
|
if (robotsCode) {
|
|
13927
13975
|
const content = readText(robotsCode) ?? "";
|
|
13928
|
-
if (content.includes(`${prefix}/sitemap.xml`)) changes.unchanged(changes.rel(robotsCode), "
|
|
13976
|
+
if (content.includes(`${prefix}/sitemap.xml`)) changes.unchanged(changes.rel(robotsCode), "PSST sitemap listed");
|
|
13929
13977
|
else {
|
|
13930
13978
|
printRobots(
|
|
13931
13979
|
changes,
|
|
@@ -13980,12 +14028,12 @@ function setUpVercelStatic(changes, opts, manifest, edge, hasVercelJson) {
|
|
|
13980
14028
|
for (const p of manifest.site.claimedRootPaths) {
|
|
13981
14029
|
const served = staticServesRootPath(opts.dir, p);
|
|
13982
14030
|
if (served) {
|
|
13983
|
-
changes.info(p, `the site serves ${p} itself (${served});
|
|
14031
|
+
changes.info(p, `the site serves ${p} itself (${served}); PSST leaves it alone`, message("P25", `${p} is already served by the site`), "skipped");
|
|
13984
14032
|
} else wanted.push({ source: p, destination: `${edge}/s/${publicId}${p}` });
|
|
13985
14033
|
}
|
|
13986
14034
|
const file2 = join(opts.dir, "vercel.json");
|
|
13987
14035
|
if (!hasVercelJson) {
|
|
13988
|
-
changes.create(file2, newVercelJson(wanted), `rewrites ${wanted.map((w) => w.source).join(", ")} to the
|
|
14036
|
+
changes.create(file2, newVercelJson(wanted), `rewrites ${wanted.map((w) => w.source).join(", ")} to the PSST edge`);
|
|
13989
14037
|
return;
|
|
13990
14038
|
}
|
|
13991
14039
|
const text = readText(file2) ?? "";
|
|
@@ -14010,7 +14058,7 @@ ${indent(c.wanted.destination)}`,
|
|
|
14010
14058
|
);
|
|
14011
14059
|
}
|
|
14012
14060
|
if (plan.added.length > 0) {
|
|
14013
|
-
changes.replace(file2, plan.text, `rewrites ${plan.added.map((w) => w.source).join(", ")} to the
|
|
14061
|
+
changes.replace(file2, plan.text, `rewrites ${plan.added.map((w) => w.source).join(", ")} to the PSST edge (other keys untouched)`, "updated");
|
|
14014
14062
|
} else if (plan.conflicts.length === 0) {
|
|
14015
14063
|
changes.unchanged("vercel.json", `rewrites ${plan.present.map((w) => w.source).join(", ")} in place`);
|
|
14016
14064
|
}
|
|
@@ -14037,7 +14085,7 @@ async function runStatus(opts) {
|
|
|
14037
14085
|
withdrawn: [...m.withdrawn],
|
|
14038
14086
|
crawlerHits: {
|
|
14039
14087
|
status: "not_available",
|
|
14040
|
-
reason: "the
|
|
14088
|
+
reason: "the PSST edge does not expose crawler hits yet; the dashboard shows them"
|
|
14041
14089
|
}
|
|
14042
14090
|
};
|
|
14043
14091
|
}
|
|
@@ -14120,19 +14168,19 @@ var processIo = {
|
|
|
14120
14168
|
err: (text) => process.stderr.write(text)
|
|
14121
14169
|
};
|
|
14122
14170
|
function failureLine(m) {
|
|
14123
|
-
return `
|
|
14171
|
+
return `PSST: ${m.message} (${m.id})
|
|
14124
14172
|
`;
|
|
14125
14173
|
}
|
|
14126
14174
|
|
|
14127
14175
|
// src/cli.ts
|
|
14128
|
-
var HELP = `saypsst ${CLI_VERSION}: install
|
|
14176
|
+
var HELP = `saypsst ${CLI_VERSION}: install PSST on a site and check it in production
|
|
14129
14177
|
|
|
14130
14178
|
Usage:
|
|
14131
14179
|
npx saypsst init --site <site_id> [--site-name "Brand"] [--agents-md]
|
|
14132
14180
|
npx saypsst doctor [--wait] [--url <origin>]
|
|
14133
14181
|
npx saypsst status
|
|
14134
14182
|
|
|
14135
|
-
init Adds
|
|
14183
|
+
init Adds PSST to this site (Next.js App Router, or a static site on Vercel; other hosts get
|
|
14136
14184
|
printed proxy instructions). Idempotent: run it again any time, it only does what is missing
|
|
14137
14185
|
and never overwrites your edits.
|
|
14138
14186
|
--site <id> public site id from the dashboard (Settings \u203A Connections)
|
|
@@ -14140,10 +14188,10 @@ init Adds psst to this site (Next.js App Router, or a static site on Vercel
|
|
|
14140
14188
|
--prefix <path> must equal the dashboard's prefix (default: the dashboard's)
|
|
14141
14189
|
--domain <host> must equal the dashboard's domain (default: the dashboard's)
|
|
14142
14190
|
--mode <mode> next | vercel_static | proxy (default: detected)
|
|
14143
|
-
--agents-md append a two-line
|
|
14191
|
+
--agents-md append a two-line PSST note to AGENTS.md
|
|
14144
14192
|
--no-install do not run the package manager
|
|
14145
14193
|
--dry-run print what would change, write nothing
|
|
14146
|
-
doctor Fetches the production site as a browser and as AI crawlers and checks every
|
|
14194
|
+
doctor Fetches the production site as a browser and as AI crawlers and checks every PSST path.
|
|
14147
14195
|
--wait poll up to --timeout seconds (default 300) while a check waits on a deploy
|
|
14148
14196
|
--url <origin> probe this origin instead of https://{domain} (local or preview build)
|
|
14149
14197
|
status The site's published pages and versions.
|
|
@@ -14151,7 +14199,7 @@ status The site's published pages and versions.
|
|
|
14151
14199
|
All commands: --json (one JSON object, schemas in node_modules/saypsst/schemas), --dir <path>,
|
|
14152
14200
|
--site <id>, --edge <origin> (default https://edge.saypsst.com).
|
|
14153
14201
|
|
|
14154
|
-
Exit codes: 0 done \xB7 1 failed (message id P\u2026) \xB7 2 usage \xB7 3 waiting, run again \xB7 4
|
|
14202
|
+
Exit codes: 0 done \xB7 1 failed (message id P\u2026) \xB7 2 usage \xB7 3 waiting, run again \xB7 4 PSST or the site unreachable.
|
|
14155
14203
|
`;
|
|
14156
14204
|
var OPTIONS = {
|
|
14157
14205
|
site: { type: "string" },
|
|
@@ -14298,7 +14346,7 @@ async function main(argv, env = {}) {
|
|
|
14298
14346
|
for (const c of r2.checks) lines.push(`${CHECK_SYMBOL[c.status]} ${c.check} ${c.detail}${c.message ? ` (${c.message.id})` : ""}`);
|
|
14299
14347
|
lines.push(
|
|
14300
14348
|
"",
|
|
14301
|
-
r2.exit === EXIT.ok ? "All checks passed." : r2.exit === EXIT.waiting ? "Waiting: deploy (or let caches refresh), then run npx saypsst doctor --wait again." : r2.exit === EXIT.network ? "The site could not be reached from here: allow network access, or ask the person to run doctor." : "Fix the failed checks: troubleshooting.md in the
|
|
14349
|
+
r2.exit === EXIT.ok ? "All checks passed." : r2.exit === EXIT.waiting ? "Waiting: deploy (or let caches refresh), then run npx saypsst doctor --wait again." : r2.exit === EXIT.network ? "The site could not be reached from here: allow network access, or ask the person to run doctor." : "Fix the failed checks: troubleshooting.md in the PSST skill has each message id.",
|
|
14302
14350
|
""
|
|
14303
14351
|
);
|
|
14304
14352
|
io.out(lines.join("\n"));
|
|
@@ -14337,7 +14385,7 @@ main(process.argv.slice(2)).then(
|
|
|
14337
14385
|
process.exitCode = code;
|
|
14338
14386
|
},
|
|
14339
14387
|
(err) => {
|
|
14340
|
-
process.stderr.write(`
|
|
14388
|
+
process.stderr.write(`PSST: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
14341
14389
|
`);
|
|
14342
14390
|
process.exitCode = 1;
|
|
14343
14391
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saypsst",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Install
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Install PSST on your site (npx saypsst init) and check it in production (npx saypsst doctor).",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/schemas/index.json
CHANGED
package/skills/psst/SKILL.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: psst
|
|
3
|
-
description:
|
|
3
|
+
description: PSST publishes pages that make this brand show up in AI answers (ChatGPT, Claude, Gemini, Perplexity, Google AI Overviews) on this site's own domain, under a path prefix PSST owns (default /resources), plus optionally /about and /llms.txt. Use it for finishing setup after `npx saypsst init`; any change near psst.config.ts, app/(psst)/, the PSST rewrites in vercel.json, or the proxy's withPsst(); checking that PSST pages are live and visible to AI crawlers (`npx saypsst doctor`); reading what PSST has published (`npx saypsst status`); and any PSST message id (P01…P44), exit code or build problem.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# PSST
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
PSST helps a brand show up in AI answers. Its agents find the most useful page the site is missing (an `/about`, an `/llms.txt`, a guide under the prefix), draft it from the site's own facts, and, once an admin approves it in the PSST dashboard, publish it **on this site's domain** with no deploy. Every page is plain HTML (no JavaScript), the same for people and crawlers, with a canonical URL, JSON-LD and a `psst-version` meta that proves which approved version is live.
|
|
9
9
|
|
|
10
|
-
The person installed
|
|
10
|
+
The person installed PSST once, with one pull request. From then on PSST writes content, and the site serves it. You keep the plumbing working; you never write PSST content.
|
|
11
11
|
|
|
12
12
|
**First:**
|
|
13
13
|
- **Read the site's own file before any change:**
|
|
@@ -16,21 +16,21 @@ The person installed psst once, with one pull request. From then on psst writes
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| `psst.config.ts` (or `.js`) imports from `@saypsst/next` | `nextjs.md` |
|
|
18
18
|
| `vercel.json` rewrites a path to `https://edge.saypsst.com/s/site_…` | `vercel-static.md` |
|
|
19
|
-
| Neither:
|
|
19
|
+
| Neither: PSST is routed by a CDN, a Worker or a reverse proxy | `proxy.md` |
|
|
20
20
|
|
|
21
21
|
- **When a command, doctor, a build log or a warning names a problem** (`P01` to `P44`, or an exit code), read `troubleshooting.md`.
|
|
22
22
|
|
|
23
23
|
## Where things are
|
|
24
24
|
|
|
25
|
-
- **The site id** (`site_` + 16 characters) is public, not a secret. It is in `psst.config.ts` or in the `vercel.json` rewrite destination.
|
|
26
|
-
- **The prefix** (`/resources` by default) belongs to
|
|
27
|
-
- **The content lives in
|
|
25
|
+
- **The site id** (`site_` + 16 characters) is public, not a secret. It is in `psst.config.ts` or in the `vercel.json` rewrite destination. PSST needs no key, token or secret anywhere: if something asks for one, it is not PSST.
|
|
26
|
+
- **The prefix** (`/resources` by default) belongs to PSST. The site must not serve anything else there. Root paths (`/about`, `/llms.txt`) are PSST's only when the dashboard claimed them and the site did not already serve them.
|
|
27
|
+
- **The content lives in PSST**, never in the repository. Pages change when an admin approves a new version in the dashboard, and appear within about 10 minutes (300 s ISR plus 300 s CDN cache).
|
|
28
28
|
- **`npx saypsst <command>`:**
|
|
29
29
|
- `init --site <id>`: sets up or repairs the plumbing. Idempotent: it marks with ✓ what is in place, does only what is missing, never overwrites an edit, and prints the exact edit for a file too unusual to change.
|
|
30
|
-
- `doctor [--wait] [--url <origin>]`: fetches the production site (or `--url`) as a browser and as GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and Googlebot, and checks every
|
|
31
|
-
- `status`: what
|
|
30
|
+
- `doctor [--wait] [--url <origin>]`: fetches the production site (or `--url`) as a browser and as GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and Googlebot, and checks every PSST path. Its probes carry `X-Psst-Doctor: 1` and are never counted as crawler hits.
|
|
31
|
+
- `status`: what PSST has published, with versions.
|
|
32
32
|
- Every command takes `--json`; the shapes are in `node_modules/saypsst/schemas/` (or the CLI package's `schemas/`).
|
|
33
|
-
- **This skill's other files:** the site's own (above), and `troubleshooting.md`. `init` copies this folder into `.claude/skills/psst/` and `.agents/skills/psst/`, and replaces it on the next `init` when
|
|
33
|
+
- **This skill's other files:** the site's own (above), and `troubleshooting.md`. `init` copies this folder into `.claude/skills/psst/` and `.agents/skills/psst/`, and replaces it on the next `init` when PSST updates it: do not edit it.
|
|
34
34
|
|
|
35
35
|
## Setup after `npx saypsst init`
|
|
36
36
|
|
|
@@ -45,22 +45,22 @@ The person installed psst once, with one pull request. From then on psst writes
|
|
|
45
45
|
| Exit 1 or 4 | Read `troubleshooting.md` at the message id. Nothing was half-written: init checks before it writes. |
|
|
46
46
|
| `Run pnpm install …` in `Next:` | Run it (init skipped it because of `--no-install` or `--dry-run`). |
|
|
47
47
|
|
|
48
|
-
2. **Check the change like any other:** the repository's own typecheck, lint and build. For Next.js, `next build` must pass (the route files are plain re-exports;
|
|
49
|
-
3. **Show the person `git diff`** in two sentences:
|
|
50
|
-
4. **After the deploy**, run `npx saypsst doctor --wait` (give the Bash call a 360000 ms timeout). Exit 0 means the site serves
|
|
51
|
-
5. **Leave the person oriented:** the first page appears after they approve it in the dashboard (Publishing), then
|
|
48
|
+
2. **Check the change like any other:** the repository's own typecheck, lint and build. For Next.js, `next build` must pass (the route files are plain re-exports; PSST being unreachable never fails a build).
|
|
49
|
+
3. **Show the person `git diff`** in two sentences: PSST's routes under the prefix, the config with the public site id, the proxy wrapper, the robots line, this skill. Nothing changes for visitors until a page is published, and only under the prefix.
|
|
50
|
+
4. **After the deploy**, run `npx saypsst doctor --wait` (give the Bash call a 360000 ms timeout). Exit 0 means the site serves PSST; exit 3 means it is still waiting on the deploy or caches: run it again. Before any page is published, doctor checks the domain proof, the stylesheet and the sitemap, and says "nothing is published yet" (P41): that is expected.
|
|
51
|
+
5. **Leave the person oriented:** the first page appears after they approve it in the dashboard (Publishing), then PSST verifies it in production by itself.
|
|
52
52
|
|
|
53
53
|
## Rules
|
|
54
54
|
|
|
55
|
-
- **Never edit
|
|
56
|
-
- **Never add
|
|
55
|
+
- **Never edit PSST's route files by hand** (`app/(psst)/**`, a claimed `app/about/page.tsx` or `app/llms.txt/route.ts`), except to pass a `Shell` to `psstPage` (see `nextjs.md`). `export const revalidate = 300` must stay a literal in each file: Next.js reads it statically.
|
|
56
|
+
- **Never add PSST content locally**: no MDX, no pages, no copy under the prefix. Content changes go through the dashboard, where an admin approves each version.
|
|
57
57
|
- **Never put anything else under the prefix**, and never block it in the proxy, middleware, auth or a CDN rule.
|
|
58
|
-
- **Never change robots.txt policy for
|
|
59
|
-
- **Never cache
|
|
58
|
+
- **Never change robots.txt policy for PSST** without the person. Doctor reports training, search and user-fetch crawlers separately; only blocked search or user-fetch agents fail it.
|
|
59
|
+
- **Never cache PSST paths longer than the origin says**, and never cache a 404 or 5xx there (a CDN rule that does breaks updates and outages).
|
|
60
60
|
- **Verify with doctor, not by reading code.** Done means `npx saypsst doctor` exits 0 against production.
|
|
61
61
|
|
|
62
62
|
## Exit codes
|
|
63
63
|
|
|
64
|
-
- **0** done · **1** failed, with a message id · **2** the command or its arguments are wrong · **3** waiting on a deploy, caches or
|
|
64
|
+
- **0** done · **1** failed, with a message id · **2** the command or its arguments are wrong · **3** waiting on a deploy, caches or PSST: run the same command again · **4** PSST or the site is unreachable from here (a sandbox without network): allow network access for the command, or ask the person to run it.
|
|
65
65
|
|
|
66
66
|
With `--json`, a failure reads `{"contract":"1.0","command":"init","site":…,"exit":1,"error":{"id":"P02","code":"prefix_served","message":"…"}}`.
|
package/skills/psst/nextjs.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PSST on Next.js (App Router)
|
|
2
2
|
|
|
3
|
-
This site serves
|
|
3
|
+
This site serves PSST through `@saypsst/next`, route modules inside its own Next.js app (15 or 16, App Router, any host). `SKILL.md` holds what every site shares; this file holds the code. The package's own reference is `node_modules/@saypsst/next/README.md`.
|
|
4
4
|
|
|
5
5
|
## What `init` set up
|
|
6
6
|
|
|
@@ -10,10 +10,10 @@ Paths assume `app/` and the prefix `/resources`. With `src/app/`, `psst.config.t
|
|
|
10
10
|
|---|---|
|
|
11
11
|
| `package.json` | `@saypsst/next` at an exact version (its only dependency is `zod`; no install scripts) |
|
|
12
12
|
| `psst.config.ts` | `definePsst({ site, domain, siteName, prefix, rootPaths })`: the public site id, the production host, the brand name. `definePsst` validates it and fails the build on a typo. |
|
|
13
|
-
| `app/(psst)/resources/[...slug]/page.tsx` | Every
|
|
14
|
-
| `app/(psst)/resources/sitemap.xml/route.ts` | The
|
|
15
|
-
| `app/(psst)/resources/%5Fpsst/style.css/route.ts` | The
|
|
16
|
-
| `app/(psst)/resources/%5Fpsst/verify/route.ts` | The domain proof
|
|
13
|
+
| `app/(psst)/resources/[...slug]/page.tsx` | Every PSST page under the prefix. Time-based ISR (`revalidate = 300`), `dynamicParams = true`. |
|
|
14
|
+
| `app/(psst)/resources/sitemap.xml/route.ts` | The PSST sitemap |
|
|
15
|
+
| `app/(psst)/resources/%5Fpsst/style.css/route.ts` | The PSST stylesheet, same-origin (no inline style, so strict CSPs pass) |
|
|
16
|
+
| `app/(psst)/resources/%5Fpsst/verify/route.ts` | The domain proof PSST checks before it publishes |
|
|
17
17
|
| `app/about/page.tsx`, `app/llms.txt/route.ts` | Only when the dashboard claimed the path and the app did not serve it |
|
|
18
18
|
| `proxy.ts` (Next 16) or `middleware.ts` (Next 15) | `withPsst(psst)`: counts AI-crawler fetches and AI referrals (classes only: no IP, no cookie, no query, no UA string), answers 410 for withdrawn pages. Never awaited, never throws. |
|
|
19
19
|
| `public/robots.txt` | One added line: `Sitemap: https://{domain}/resources/sitemap.xml` |
|
|
@@ -24,7 +24,7 @@ Two Next.js rules shape these files:
|
|
|
24
24
|
|
|
25
25
|
## The site's header and footer
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
PSST pages render inside the root layout. When that layout has no header or footer (common: they live in a `(marketing)` group layout), pass a `Shell` built from the site's own components. It is the one hand edit PSST allows in its route files, and `init` keeps it:
|
|
28
28
|
|
|
29
29
|
```tsx
|
|
30
30
|
// app/(psst)/resources/[...slug]/page.tsx
|
|
@@ -51,7 +51,7 @@ export const generateMetadata = page.generateMetadata;
|
|
|
51
51
|
export default page.default;
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
`children` is
|
|
54
|
+
`children` is PSST's own `<main>`: do not wrap it in another `<main>`. The Shell must be a server component (no `"use client"` at its top; client components inside it are fine). Without a Shell, `psst.config.ts` can take `shell: { nav: [{ label, href }], footer }` for a plain built-in header and footer.
|
|
55
55
|
|
|
56
56
|
## An existing proxy or middleware
|
|
57
57
|
|
|
@@ -63,9 +63,10 @@ A proxy that rewrites, redirects or authenticates every path must let the prefix
|
|
|
63
63
|
|
|
64
64
|
## Caching and failures
|
|
65
65
|
|
|
66
|
-
- Pages and handlers revalidate every 300 s. When
|
|
67
|
-
- `next build` never fails because of
|
|
68
|
-
- **Cache Components** (`cacheComponents: true
|
|
66
|
+
- Pages and handlers revalidate every 300 s. When PSST is unreachable, the last good render keeps being served; a page never rendered answers 500 uncached (never a cached 404), and the next request retries.
|
|
67
|
+
- `next build` never fails because of PSST: unreachable at build, the pages render on first request.
|
|
68
|
+
- **Cache Components** (`cacheComponents: true`, P08): the segment config the route files need is removed there, so init writes no route files. `serveFromEdge: true` in `psst.config` makes `withPsst()` rewrite PSST paths to the edge, which renders them with its own header and footer, not the app's layout. To use the app's layout, turn the flag off and run init again.
|
|
69
|
+
- **`trailingSlash: true`**: Next redirects PSST's paths before any PSST code runs, so `doctor` and the domain proof report P39. Pages still answer after the redirect. With `serveFromEdge`, adding `skipTrailingSlashRedirect: true` to `next.config` clears it, but that changes the redirects of the whole site: ask the person first.
|
|
69
70
|
- **Self-hosted with several instances** (for example behind CloudFront): each instance keeps its own ISR cache, so updates land per instance within 300 s. The CDN in front must respect the origin's `Cache-Control` on the prefix.
|
|
70
71
|
|
|
71
72
|
## Build or type errors
|
|
@@ -73,11 +74,11 @@ A proxy that rewrites, redirects or authenticates every path must let the prefix
|
|
|
73
74
|
| Error | Fix |
|
|
74
75
|
|---|---|
|
|
75
76
|
| `Cannot find module '@saypsst/next/page'` | Install: run the package manager (`pnpm install`, `npm install`). |
|
|
76
|
-
| `invalid
|
|
77
|
+
| `invalid PSST config: …` (PsstConfigError) | A value in `psst.config.ts` is wrong; the message names it. `npx saypsst init` prints the expected keys (P23). |
|
|
77
78
|
| `Cannot find module '../../../../psst.config'` | The config moved. Put it back next to `app/` (or in `src/` with `src/app/`), or run `npx saypsst init`, which prints each route file's expected content (P24). |
|
|
78
79
|
| Type error in the Shell | Type its props as `PsstShellProps` from `@saypsst/next/page`. |
|
|
79
|
-
| A lint rule flags a
|
|
80
|
+
| A lint rule flags a PSST route file | Exclude `app/(psst)/**` from that rule rather than editing the file. |
|
|
80
81
|
|
|
81
|
-
## Remove
|
|
82
|
+
## Remove PSST
|
|
82
83
|
|
|
83
84
|
Delete `app/(psst)/`, the claimed root-path files, `psst.config.ts`, the `withPsst` wrapper (or the proxy file init created), the robots line, `@saypsst/next` from `package.json`, and `.claude/skills/psst/`. Ask the person to withdraw the pages in the dashboard first, so crawlers get 410s rather than 404s.
|
package/skills/psst/proxy.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PSST behind a proxy, a CDN or a Worker
|
|
2
2
|
|
|
3
|
-
For any site that is neither a Next.js App Router app nor a static site on Vercel: a Pages Router app, Nuxt, Astro SSR, WordPress, Shopify, or a Next.js app behind CloudFront whose infra owner prefers routing.
|
|
3
|
+
For any site that is neither a Next.js App Router app nor a static site on Vercel: a Pages Router app, Nuxt, Astro SSR, WordPress, Shopify, or a Next.js app behind CloudFront whose infra owner prefers routing. PSST's edge renders full pages; the site's host or CDN routes PSST's paths to it. `init --mode proxy` writes nothing but the skill; this file has the snippets. `SKILL.md` holds what every site shares.
|
|
4
4
|
|
|
5
5
|
Throughout: `SITE` is the site id (`site_…`), `PREFIX` the prefix (`/resources`), `EDGE` is `https://edge.saypsst.com`. The edge path is always `EDGE/s/SITE` followed by the path on the site, unchanged.
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ Throughout: `SITE` is the site id (`site_…`), `PREFIX` the prefix (`/resources
|
|
|
11
11
|
| `PREFIX` and everything under it | `EDGE/s/SITE/PREFIX/…` (same path) |
|
|
12
12
|
| `/about`, `/llms.txt`, only if the dashboard claimed them | `EDGE/s/SITE/about`, `EDGE/s/SITE/llms.txt` |
|
|
13
13
|
|
|
14
|
-
Forward the method, the `User-Agent` and the `Referer` (
|
|
14
|
+
Forward the method, the `User-Agent` and the `Referer` (PSST classifies AI crawlers and referrals from them; it stores no IP, cookie or UA string). Do not forward cookies or `Authorization`. Keep the response status and the `Cache-Control`, `Content-Type`, `Content-Security-Policy` and `X-Psst-Version` headers. Never cache a 404 or 5xx from the edge, and never cache longer than its `s-maxage`.
|
|
15
15
|
|
|
16
16
|
## Cloudflare Worker (route `example.com/resources*`)
|
|
17
17
|
|
|
@@ -55,7 +55,7 @@ A `proxy_pass` with a variable needs a `resolver` (for example `resolver 1.1.1.1
|
|
|
55
55
|
1. Origin: `edge.saypsst.com`, HTTPS only, origin path `/s/site_xxxxxxxxxxxxxxxx`.
|
|
56
56
|
2. Behavior: path pattern `/resources*` (above the default behavior), that origin, cache policy honouring origin headers (`CachingOptimized` with min TTL 0, or a policy with "Use origin cache headers"), origin request policy forwarding `User-Agent` and `Referer`, no cookies, no query strings.
|
|
57
57
|
3. One behavior per claimed root path (`/about`, `/llms.txt`), same origin.
|
|
58
|
-
4. Error pages: none for this behavior (a custom error response would turn
|
|
58
|
+
4. Error pages: none for this behavior (a custom error response would turn PSST's 410 and 503 into the site's page).
|
|
59
59
|
|
|
60
60
|
## Next.js Pages Router (P01)
|
|
61
61
|
|
|
@@ -6,37 +6,37 @@
|
|
|
6
6
|
|
|
7
7
|
## init stops (nothing was written)
|
|
8
8
|
|
|
9
|
-
- **P01** `no_app_router`: a Next.js app without `app/` or `src/app/` (Pages Router only).
|
|
9
|
+
- **P01** `no_app_router`: a Next.js app without `app/` or `src/app/` (Pages Router only). PSST's Next.js routes need the App Router. Route PSST at the host instead: `proxy.md`, "Next.js Pages Router" (`beforeFiles` rewrites), then `npx saypsst init --mode proxy`.
|
|
10
10
|
- **P02** `prefix_served`: the live site already answers 200 under the prefix. Another prefix is needed: the person changes it in the dashboard (Settings › Connections), then run init again. Never delete the site's pages to make room.
|
|
11
11
|
- **P03** `prefix_route_exists`: the repository already has routes or files under the prefix (`app/resources/`, `public/resources/`, a rewrite or redirect). Same fix as P02.
|
|
12
|
-
- **P04** `edge_unreachable` (exit 4):
|
|
12
|
+
- **P04** `edge_unreachable` (exit 4): PSST's edge cannot be reached from here, as in a sandbox without network. Allow network access for the command, or ask the person to run it.
|
|
13
13
|
- **P05** `site_unknown`: no site with that id. Copy the id again from the dashboard (Settings › Connections); it looks like `site_` + 16 letters and digits 2–7.
|
|
14
|
-
- **P06** `edge_error`:
|
|
14
|
+
- **P06** `edge_error`: PSST answered something unexpected. If it says "contract", update the CLI (`npx saypsst@latest init`); otherwise retry later.
|
|
15
15
|
- **P07** `site_name_required` (exit 2): pass `--site-name "Brand"`, the brand as the site names itself. Normally not needed: init takes the brand name from the dashboard, then from `psst.config.ts`. Only an edge without it asks.
|
|
16
|
-
- **P08** `cache_components`: `cacheComponents: true` in `next.config` removes the route segment config
|
|
16
|
+
- **P08** `cache_components`: not a failure. `cacheComponents: true` in `next.config` removes the route segment config PSST's route files need, so init sets `serveFromEdge: true` and writes only `proxy.ts`; the edge serves PSST pages with its own header and footer. If init prints a route file to delete, delete it: it comes from an install made before the flag was on.
|
|
17
17
|
- **P09** `config_other_site`: `psst.config.ts` names another site. init never rewrites it. If the repository really moves to the new site, change `site` there by hand (ask the person), then run init.
|
|
18
18
|
- **P10** `prefix_mismatch` (exit 2): `--prefix` must equal the dashboard's prefix. Change it in the dashboard first, or drop `--prefix`.
|
|
19
19
|
- **P11** `domain_mismatch` (exit 2): `--domain` must equal the dashboard's domain. Fix the domain in the dashboard, or drop `--domain`.
|
|
20
20
|
- **P12** `install_failed`: the package manager failed to install `@saypsst/next`. Everything else is in place: fix the install (network, registry auth, peer conflicts), run it, then run init again.
|
|
21
21
|
- **P13** `usage` (exit 2): a wrong command, option or value; the message says which. `npx saypsst --help`.
|
|
22
|
-
- **P14** `next_unsupported`: Next.js older than 15. Upgrade (it changes the app beyond
|
|
23
|
-
- **P15** `monorepo_root`: this folder holds several apps. Run init in the site's folder (`cd apps/web`), the one Vercel builds; or pass `--mode proxy` if
|
|
24
|
-
- **P16** `edge_busy` (exit 3):
|
|
22
|
+
- **P14** `next_unsupported`: Next.js older than 15. Upgrade (it changes the app beyond PSST: ask the person), or use `proxy.md`.
|
|
23
|
+
- **P15** `monorepo_root`: this folder holds several apps. Run init in the site's folder (`cd apps/web`), the one Vercel builds; or pass `--mode proxy` if PSST is routed elsewhere.
|
|
24
|
+
- **P16** `edge_busy` (exit 3): PSST answered 503 or 429 for a moment. Run the same command again in a few minutes.
|
|
25
25
|
|
|
26
26
|
## init printed an edit (`!`)
|
|
27
27
|
|
|
28
28
|
Make the edit exactly as printed, then run `npx saypsst init` again: the line turns ✓.
|
|
29
29
|
|
|
30
30
|
- **P20** `proxy_edit`: the site has its own `proxy.ts` / `middleware.ts`. Wrap it with `withPsst()` as printed; keep its matcher a literal that also matches the prefix (`nextjs.md`).
|
|
31
|
-
- **P21** `robots_edit`: robots is generated by code (`app/robots.ts`) or missing. Add the
|
|
31
|
+
- **P21** `robots_edit`: robots is generated by code (`app/robots.ts`) or missing. Add the PSST sitemap URL where robots is produced. Never change its allow/disallow policy for PSST.
|
|
32
32
|
- **P22** `vercel_json_edit`: `vercel.json` has comments, invalid JSON or legacy `routes`. Add the printed rewrites first in `rewrites` (or the route before `{ "handle": "filesystem" }`).
|
|
33
|
-
- **P23** `config_edit`: `psst.config.ts` differs from the site's settings in
|
|
34
|
-
- **P24** `route_file_edited`: a file at a
|
|
35
|
-
- **P26** `rewrite_conflict`: `vercel.json` already rewrites a
|
|
33
|
+
- **P23** `config_edit`: `psst.config.ts` differs from the site's settings in PSST (domain, brand name, prefix, root paths). Set the printed keys; keep anything you added (`shell`).
|
|
34
|
+
- **P24** `route_file_edited`: a file at a PSST route path is not PSST's (or lost `export const revalidate = 300`). Replace it with the printed content; a `Shell` is the only change PSST allows.
|
|
35
|
+
- **P26** `rewrite_conflict`: `vercel.json` already rewrites a PSST path elsewhere. Point it at the printed destination, if the person agrees the path is PSST's.
|
|
36
36
|
|
|
37
37
|
## Information (nothing to fix)
|
|
38
38
|
|
|
39
|
-
- **P25** `root_path_served`: the site already serves `/about` or `/llms.txt`, so
|
|
39
|
+
- **P25** `root_path_served`: the site already serves `/about` or `/llms.txt`, so PSST does not take it. The person can release the claim in the dashboard.
|
|
40
40
|
- **P27** `mode_mismatch`: the dashboard's mode differs from what init detected; init sets up what it found. Tell the person so the dashboard can be updated.
|
|
41
41
|
- **P28** `agents_md_ask`: ask the person whether to add the two-line note to AGENTS.md; if yes, `npx saypsst init --agents-md`.
|
|
42
42
|
- **P29** `live_check_skipped`: init could not fetch the live site (not deployed yet, or no network), so it could not confirm the prefix is free there. Check it after the deploy with `npx saypsst doctor`.
|
|
@@ -45,18 +45,18 @@ Make the edit exactly as printed, then run `npx saypsst init` again: the line tu
|
|
|
45
45
|
|
|
46
46
|
## doctor
|
|
47
47
|
|
|
48
|
-
Doctor fetches each
|
|
48
|
+
Doctor fetches each PSST path on the production domain (or `--url`) as a browser and as GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and Googlebot, without following redirects. `--wait` repeats while a check is waiting, up to 5 minutes (under Claude Code, give that Bash call a 360000 ms timeout).
|
|
49
49
|
|
|
50
|
-
- **P30** `not_deployed` (waiting): a
|
|
50
|
+
- **P30** `not_deployed` (waiting): a PSST path answers 404. Deploy the install; for a just-published page, wait a minute and run again. Still 404 after a deploy: in Next.js, check that `app/(psst)/` is deployed and the proxy does not rewrite the prefix; on Vercel static, that no file or redirect shadows it; behind a proxy, that the route exists.
|
|
51
51
|
- **P31** `verify_mismatch`: `{prefix}/_psst/verify` does not serve this site's proof. The site's config names another site, or something else answers that path.
|
|
52
|
-
- **P32** `version_stale` (waiting): the page serves an older version than
|
|
52
|
+
- **P32** `version_stale` (waiting): the page serves an older version than PSST published. Caches refresh within about 10 minutes. Still stale after 15 minutes: a CDN caches the prefix longer than the origin's `s-maxage` (fix its cache rule), or a self-hosted app runs several instances with their own caches.
|
|
53
53
|
- **P33** `noindex`: the page is noindex, from the layout's metadata, a header rule (`X-Robots-Tag`), or the host's preview protection. Remove it for the prefix.
|
|
54
|
-
- **P34** `canonical_wrong`: the canonical is not `https://{domain}{path}`. The layout overrides
|
|
55
|
-
- **P35** `bot_content_differs`: crawlers get other content than people: a bot rule, a WAF challenge, or A/B testing on the prefix.
|
|
56
|
-
- **P36** `text_missing`: the page's text is not in the HTML without JavaScript. Something renders the prefix client-side or replaces
|
|
54
|
+
- **P34** `canonical_wrong`: the canonical is not `https://{domain}{path}`. The layout overrides PSST's metadata, or `domain` in `psst.config.ts` is wrong (P23).
|
|
55
|
+
- **P35** `bot_content_differs`: crawlers get other content than people: a bot rule, a WAF challenge, or A/B testing on the prefix. PSST pages must be identical for everyone; exclude the prefix from it.
|
|
56
|
+
- **P36** `text_missing`: the page's text is not in the HTML without JavaScript. Something renders the prefix client-side or replaces PSST's page; check the proxy and layouts.
|
|
57
57
|
- **P37** `robots_blocks`: robots.txt blocks AI search or user-fetch agents from the prefix, so AI answers cannot cite these pages. The policy is the site's: show the person and let them decide. Blocking training crawlers is reported as information only.
|
|
58
|
-
- **P38** `sitemap_bad`: `{prefix}/sitemap.xml` is not
|
|
59
|
-
- **P39** `redirected`: a
|
|
58
|
+
- **P38** `sitemap_bad`: `{prefix}/sitemap.xml` is not PSST's sitemap, or does not list a published page yet (waiting, like P32).
|
|
59
|
+
- **P39** `redirected`: a PSST path redirects (to `www.`, to https, to a login). PSST's paths must answer directly on the configured domain: fix the redirect, or the domain in the dashboard.
|
|
60
60
|
- **P40** `site_unreachable` (exit 4 when it is the only failure): the site could not be fetched from here: no network in the sandbox, DNS not pointed yet, or a firewall.
|
|
61
61
|
- **P42** `no_site_id` (exit 2): nothing in this folder names the site. Run doctor in the site's folder, or pass `--site site_…`.
|
|
62
|
-
- **P44** `bad_status`: a
|
|
62
|
+
- **P44** `bad_status`: a PSST path answers an unexpected status or type (500, 410, an HTML error page, no `psst-version`). A 500 that persists: the app cannot reach PSST at runtime (outbound network blocked); a 410: the page was withdrawn in the dashboard.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PSST on a static site on Vercel
|
|
2
2
|
|
|
3
|
-
This site deploys on Vercel without Next.js (plain HTML, Astro static, a custom build).
|
|
3
|
+
This site deploys on Vercel without Next.js (plain HTML, Astro static, a custom build). PSST pages are served by the PSST edge and reach visitors through **rewrites in `vercel.json`**: Vercel fetches them from the edge and serves them on the site's own domain. There is no PSST code in the repository, no dependency and no secret. `SKILL.md` holds what every site shares.
|
|
4
4
|
|
|
5
5
|
## What `init` set up
|
|
6
6
|
|
|
@@ -21,10 +21,10 @@ This site deploys on Vercel without Next.js (plain HTML, Astro static, a custom
|
|
|
21
21
|
|
|
22
22
|
## Rules for this site
|
|
23
23
|
|
|
24
|
-
- **Vercel serves files before rewrites.** A file or folder under the prefix in the build output (`public/resources/…`, `resources/index.html`, `about.html`) hides
|
|
25
|
-
- **Redirects run before rewrites.** A redirect whose source covers the prefix (`/resources/:path*`, `/:path*`) sends visitors away from
|
|
24
|
+
- **Vercel serves files before rewrites.** A file or folder under the prefix in the build output (`public/resources/…`, `resources/index.html`, `about.html`) hides PSST's page: remove it, or ask for another prefix in the dashboard (P03).
|
|
25
|
+
- **Redirects run before rewrites.** A redirect whose source covers the prefix (`/resources/:path*`, `/:path*`) sends visitors away from PSST: exclude the prefix from it.
|
|
26
26
|
- **`cleanUrls` and `trailingSlash`** are fine: the rewrite matches both forms.
|
|
27
|
-
- **Headers:** a `Content-Security-Policy` in `vercel.json` also applies to
|
|
27
|
+
- **Headers:** a `Content-Security-Policy` in `vercel.json` also applies to PSST pages, and they need nothing beyond `'self'` for the stylesheet: no inline script or style, and JSON-LD is a data block. A strict `script-src` with hashes passes.
|
|
28
28
|
- **Legacy `routes`:** a `vercel.json` with `routes` cannot use `rewrites`. init prints the edit (P22); add a route before `{ "handle": "filesystem" }`: `{ "src": "/resources/(.*)", "dest": "https://edge.saypsst.com/s/site_…/resources/$1" }`.
|
|
29
29
|
- **`vercel.json` with comments** (or invalid JSON): init prints the rewrites to add (P22).
|
|
30
30
|
- **Monorepo:** run init in the folder that holds this site's `vercel.json` (the Vercel project's root directory), for example `cd apps/web` (P15).
|
|
@@ -37,6 +37,6 @@ The edge answers `Cache-Control: public, max-age=0, s-maxage=300, stale-while-re
|
|
|
37
37
|
|
|
38
38
|
After the deploy: `npx saypsst doctor --wait`. A preview deployment can be checked with `npx saypsst doctor --url https://<preview>.vercel.app` (canonical URLs still point at the production domain, as they should).
|
|
39
39
|
|
|
40
|
-
## Remove
|
|
40
|
+
## Remove PSST
|
|
41
41
|
|
|
42
|
-
Delete the
|
|
42
|
+
Delete the PSST rewrites from `vercel.json`, the robots line and `.claude/skills/psst/`. Ask the person to withdraw the pages in the dashboard first.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: psst-tasks
|
|
3
|
-
description: Do the
|
|
3
|
+
description: Do the PSST tasks assigned to this repository's coding agent. PSST (saypsst.com) finds what keeps this brand out of AI answers and writes evidence-backed task briefs; the person approves them in the PSST dashboard, and the ones marked "Done by <Brand>'s agent" change this site's existing pages or stack (copy, sections, FAQ blocks, schema, meta, llms.txt, robots, sitemap). Use it when the person says "do my PSST tasks", "anything to implement from PSST?", "work through the PSST briefs" or runs the nightly PSST routine (`routine.md`).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# PSST tasks
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
PSST splits approved work between two executors. **PSST** does everything off this site (mentions, listings, community threads, third-party pages) and the new pages it publishes under its own prefix. **This repository's agent** (you) does the rest: changes to the site's existing pages and stack. Each brief says which. You only ever do briefs marked for the agent.
|
|
9
9
|
|
|
10
|
-
The person said go when they approved the brief. You still work the way this repository works: its branches, its review rules, its checks.
|
|
10
|
+
The person said go when they approved the brief. You still work the way this repository works: its branches, its review rules, its checks. PSST never deploys anything here.
|
|
11
11
|
|
|
12
12
|
## Before you start
|
|
13
13
|
|
|
14
|
-
- **The
|
|
14
|
+
- **The PSST connector** must be connected in this session (an MCP server at `https://app.saypsst.com/api/mcp`, signed in as someone in the brand's PSST organization). If its tools (`list_projects`, `list_actions`, `get_action`, `report_progress`, `complete_action`) are missing, stop and say: "Connect the PSST connector first: https://app.saypsst.com/connectors." Do not scrape the dashboard.
|
|
15
15
|
- **Pick the project**: `list_projects`. One project: use it. Several: use the one whose brand owns this repository's site; if that is unclear, ask once.
|
|
16
16
|
- **Read this repository's rules** (AGENTS.md, CLAUDE.md, CONTRIBUTING) before the first edit. They win over this skill.
|
|
17
17
|
|
|
@@ -20,7 +20,7 @@ The person said go when they approved the brief. You still work the way this rep
|
|
|
20
20
|
1. **List your briefs**: `list_actions` with `status: "in_progress"` and `executor: "agent"`, every page. Approved briefs are `in_progress`; `new` and `planned` ones still wait for the person, so leave them alone.
|
|
21
21
|
2. **Order**: by `priority` (P0 first), then rank. One brief at a time, start to finish.
|
|
22
22
|
3. **Already shipped?** If the brief's `notes` (from `get_action`: your earlier `report_progress` lines) or your last run's summary name a PR or commit for it, do not redo it: if that PR is still open, report it as waiting; if it shipped, go straight to step 9.
|
|
23
|
-
4. **Read the whole brief**: `get_action`. Use `content.target.url` (the page to change; `null` when the brief creates a new page, such as a free tool, whose path is in `content.definitionOfDone`, e.g. `/tools/<slug>`), `content.checklist` (the steps), `content.draft` (wording
|
|
23
|
+
4. **Read the whole brief**: `get_action`. Use `content.target.url` (the page to change; `null` when the brief creates a new page, such as a free tool, whose path is in `content.definitionOfDone`, e.g. `/tools/<slug>`), `content.checklist` (the steps), `content.draft` (wording PSST proposes), `content.definitionOfDone`, `content.evidence` (why). The brief is a proposal grounded in evidence: keep its intent; adapt the wording to the site's voice and facts. Never invent claims, numbers, reviews or quotes. If a fact is unknown, leave it out and say so.
|
|
24
24
|
5. **Find the code** that renders the target page (route, template, CMS entry, content file). If the page is not built from this repository (a CMS, another repo, a hosted help center), do not guess: report it as blocked with where it lives.
|
|
25
25
|
6. **Make the change**, the smallest one that meets the checklist. Run the repository's own checks (typecheck, lint, tests, build) and fix what you broke.
|
|
26
26
|
7. **Ship it the repository's way**: a branch and a pull request, or a commit if that is how this repository works. Never push to a protected branch, skip review or force anything. Put the brief's title and id in the PR description.
|
|
@@ -35,11 +35,11 @@ The person said go when they approved the brief. You still work the way this rep
|
|
|
35
35
|
|
|
36
36
|
## Hard rules
|
|
37
37
|
|
|
38
|
-
- **Never touch
|
|
39
|
-
- **Never act off this site**: no emails, posts, reviews, outreach, third-party edits. Briefs marked `psst` are
|
|
38
|
+
- **Never touch PSST's own paths**: the prefix PSST owns (default `/resources`), its route files, `psst.config.ts`, the PSST rewrites. Those are PSST's (see the `psst` skill). A brief that targets them is PSST's, not yours: skip it and say so.
|
|
39
|
+
- **Never act off this site**: no emails, posts, reviews, outreach, third-party edits. Briefs marked `psst` are PSST's.
|
|
40
40
|
- **Never change a brief's status any other way**: no approving, declining or reopening briefs. The person decides; you report.
|
|
41
|
-
- **Connector errors are answers, not obstacles.** `consent_required`: tell the person to revoke the
|
|
42
|
-
- **Secrets stay where they are.**
|
|
41
|
+
- **Connector errors are answers, not obstacles.** `consent_required`: tell the person to revoke the PSST connection at https://app.saypsst.com/connectors/access and connect again. `not_agent_task` or `conflict`: the brief is no longer yours or no longer in progress; skip it and say so. `notes_full`: ask the person to tidy the brief's notes in the dashboard.
|
|
42
|
+
- **Secrets stay where they are.** PSST needs no key in this repository.
|
|
43
43
|
|
|
44
44
|
## When you finish
|
|
45
45
|
|
|
@@ -53,4 +53,4 @@ Reply with one line per brief:
|
|
|
53
53
|
|
|
54
54
|
Then the count: "3 live, 1 waiting for deploy, 1 blocked." Nothing else.
|
|
55
55
|
|
|
56
|
-
**This skill's other file:** `routine.md` (a nightly run). `npx saypsst init` copies this folder into `.claude/skills/psst-tasks/` and `.agents/skills/psst-tasks/` and replaces it when
|
|
56
|
+
**This skill's other file:** `routine.md` (a nightly run). `npx saypsst init` copies this folder into `.claude/skills/psst-tasks/` and `.agents/skills/psst-tasks/` and replaces it when PSST updates it: do not edit it.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# Nightly
|
|
1
|
+
# Nightly PSST routine
|
|
2
2
|
|
|
3
|
-
Run this to set up a nightly check: "Anything to implement from
|
|
3
|
+
Run this to set up a nightly check: "Anything to implement from PSST?" Every night the agent picks up newly approved briefs, ships them as pull requests, and completes the ones that went live since the last run.
|
|
4
4
|
|
|
5
5
|
## Set it up (once, with the person)
|
|
6
6
|
|
|
7
|
-
1. The
|
|
7
|
+
1. The PSST connector is connected in Claude Code (see `SKILL.md` › Before you start).
|
|
8
8
|
2. Create a scheduled task that runs in this repository every night. In Claude Code: ask "schedule a nightly task at 02:00 in this repo with the prompt below" (desktop scheduled task or `/schedule`). In Codex or Cursor, use their scheduled or background agents with the same prompt.
|
|
9
9
|
3. Give it permission to create branches and open pull requests in this repository, and nothing more.
|
|
10
10
|
|
|
11
11
|
## The prompt
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
Anything to implement from
|
|
15
|
-
1. Re-check every
|
|
14
|
+
Anything to implement from PSST? Use the psst-tasks skill.
|
|
15
|
+
1. Re-check every PSST brief you left "waiting for deploy": if the change is live on production, complete it.
|
|
16
16
|
2. Then do every other approved agent brief (never one that already has a PR), one at a time, until each is live, waiting for deploy with a PR, or blocked.
|
|
17
17
|
Open pull requests; never merge or deploy on your own unless this repository's rules say the agent may.
|
|
18
18
|
End with the one-line-per-brief summary.
|
|
@@ -21,8 +21,8 @@ End with the one-line-per-brief summary.
|
|
|
21
21
|
## What the person sees in the morning
|
|
22
22
|
|
|
23
23
|
- Pull requests to review, one per brief, each naming the brief.
|
|
24
|
-
- In the
|
|
25
|
-
- Nothing to do? The run ends with "Nothing to implement from
|
|
24
|
+
- In the PSST dashboard: briefs that went live moved to Done, the rest show their PR in the notes.
|
|
25
|
+
- Nothing to do? The run ends with "Nothing to implement from PSST." and opens nothing.
|
|
26
26
|
|
|
27
27
|
## Rules for the scheduled run
|
|
28
28
|
|