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 CHANGED
@@ -1,10 +1,10 @@
1
- # psst
1
+ # PSST
2
2
 
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.
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 psst dashboard (Settings › Connections).
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 psst skill.* `init` copies the skills into `.claude/skills/` and `.agents/skills/`: `psst` (the plumbing) and `psst-tasks` (below).
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. psst never asks for a key, token or deploy access.
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 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).
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 psst tasks
37
+ ## Let your agent do your PSST tasks
38
38
 
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.
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 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.
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 psst's own paths.
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** psst or the site unreachable from here.
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(["chatgpt", "perplexity", "gemini", "copilot", "claude", "other", "none"]);
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: EXIT.failed },
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.1.0";
12574
- var PSST_NEXT_VERSION = "0.2.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 psst site id: it looks like site_ followed by 16 letters and digits`);
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
- `psst is unreachable from here (${edge}). Allow network access for this command, or ask the person to run it`
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", `psst knows no site ${site}. Copy the site id from the dashboard, Settings \u203A Connections`);
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", `psst answered ${res.status} (busy or down for a moment). Run the same command again in a few minutes`);
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", `psst answered ${res.status} for the site manifest`);
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", "psst answered with something that is not JSON for the site manifest");
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 psst (npx saypsst@latest)");
12639
- if (parsed.data.site.publicId !== site) throw new CliError("P06", "psst answered with another site's manifest");
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 psst install here (no psst.config.ts, no psst rewrite in vercel.json): pass --site site_\u2026 or run in the site's folder"
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") ?? "?"}: psst's paths must answer directly on the site's domain`, "P39");
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 psst stylesheet is served same-origin") : check2("style", "failed", `${url2} answers ${type || "no content type"}, not text/css`, "P44");
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 psst page`, "P44");
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}, psst published ${version2} (as ${probe.name}); caches refresh within 10 minutes`, "P32");
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: psst pages must be identical for people and crawlers`, "P35");
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 psst's pages" : `${url2} could not be reached`)];
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 psst sitemap" : `robots.txt does not list ${prefix}/sitemap.xml (recommended)`, listed ? void 0 : "P21")
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", `psst knows ${site} (${m.site.domain}, ${m.site.mode}, ${prefix})`)];
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
- 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.
13222
- Before touching psst files or running \`npx saypsst\`, read \`.claude/skills/psst/SKILL.md\` (also in \`.agents/skills/psst/\`).
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), "psst note");
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 psst is installed?" If yes, run npx saypsst init --agents-md',
13246
- message("P28", "AGENTS.md has no psst note")
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, "psst note");
13252
- else changes.replace(file2, `${current}${current.endsWith("\n") ? "\n" : "\n\n"}${note}`, "psst note appended");
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
- steps.push(
13327
- `${steps.filter((l) => /^\d\./.test(l)).length + 1}. Keep \`config.matcher\` a literal and make sure it also matches "${prefix}/:path*" (psst counts AI crawlers there).`
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 psst dashboard`,
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 psst dashboard, Settings \u203A Connections)");
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/). 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"
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}. psst never overwrites it: if this repository moves to the new site, change \`site\` there by hand, then run init again`
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 psst, ${domain2}. Change it in the dashboard, or drop --domain`);
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 psst, ${prefix}. Change the prefix in the dashboard (Settings \u203A Connections) first, then run init again`
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", `psst needs Next.js 15 or later (this app has ${project.nextMajor}). Upgrade Next, or use --mode proxy`);
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
- throw new CliError(
13718
- "P08",
13719
- "this app turns on Cache Components (cacheComponents in next.config), which psst's route files do not support yet. Use --mode proxy, or turn it off"
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 psst skill shows (Cloudflare Worker, nginx, CloudFront)`
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; psst's routes take precedence under ${prefix}`);
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}); psst leaves it alone. Ask psst to release the claim in the dashboard`,
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: "psst pages" });
13838
+ routeFiles.push({ ...page, content: catchAllPageFile(page.config), module: "@saypsst/next/page", what: "PSST pages" });
13806
13839
  const handlers = [
13807
- ["sitemap", ["sitemap.xml"], "psst sitemap"],
13808
- ["style", ["%5Fpsst", "style.css"], "psst stylesheet"],
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 psst)" });
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 psst)" });
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 (current === null) changes.create(r.abs, r.content, r.what);
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 psst's route file; init left it alone`)
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
- if (hasPsstProxy(content)) changes.unchanged(changes.rel(existingProxy), "withPsst() in place");
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 psst",
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), "psst sitemap listed");
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 psst sitemap",
13966
+ "reference the PSST sitemap",
13919
13967
  how,
13920
- message("P21", `add the psst sitemap (https://${domain2}${prefix}/sitemap.xml) to robots`)
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), "psst sitemap listed");
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}); psst leaves it alone`, message("P25", `${p} is already served by the site`), "skipped");
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 psst edge`);
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 psst edge (other keys untouched)`, "updated");
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 psst edge does not expose crawler hits yet; the dashboard shows them"
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 `psst: ${m.message} (${m.id})
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 psst on a site and check it in production
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 psst to this site (Next.js App Router, or a static site on Vercel; other hosts get
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 psst note to AGENTS.md
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 psst path.
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 psst or the site unreachable.
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 psst skill has each message id.",
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(`psst: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
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.1.0",
4
- "description": "Install psst on your site (npx saypsst init) and check it in production (npx saypsst doctor).",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "format": "psst.schemas/1",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "contract": "1.0",
5
5
  "commands": {
6
6
  "init": "init.schema.json",
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: psst
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.
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
- # psst
6
+ # PSST
7
7
 
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.
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 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.
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: psst is routed by a CDN, a Worker or a reverse proxy | `proxy.md` |
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. 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).
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 psst path. Its probes carry `X-Psst-Doctor: 1` and are never counted as crawler hits.
31
- - `status`: what psst has published, with versions.
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 psst updates it: do not edit it.
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; 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.
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 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.
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 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).
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 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.
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":"…"}}`.
@@ -1,6 +1,6 @@
1
- # psst on Next.js (App Router)
1
+ # PSST on Next.js (App Router)
2
2
 
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`.
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 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 |
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
- 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:
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 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.
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 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`) is not supported yet (P08): the segment config these files need is removed there. Use `proxy.md` instead.
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 psst config: …` (PsstConfigError) | A value in `psst.config.ts` is wrong; the message names it. `npx saypsst init` prints the expected keys (P23). |
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 psst route file | Exclude `app/(psst)/**` from that rule rather than editing the file. |
80
+ | A lint rule flags a PSST route file | Exclude `app/(psst)/**` from that rule rather than editing the file. |
80
81
 
81
- ## Remove psst
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.
@@ -1,6 +1,6 @@
1
- # psst behind a proxy, a CDN or a Worker
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. 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.
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` (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`.
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 psst's 410 and 503 into the site's page).
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). 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`.
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): 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.
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`: psst answered something unexpected. If it says "contract", update the CLI (`npx saypsst@latest init`); otherwise retry later.
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 psst's files need. Use `proxy.md` (route the prefix at the host) until psst supports it.
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 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.
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 psst sitemap URL where robots is produced. Never change its allow/disallow policy for psst.
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 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.
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 psst does not take it. The person can release the claim in the dashboard.
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 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).
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 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.
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 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.
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 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.
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 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.
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 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.
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
- # psst on a static site on Vercel
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). 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.
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 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.
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 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.
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 psst
40
+ ## Remove PSST
41
41
 
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.
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 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`).
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
- # psst tasks
6
+ # PSST tasks
7
7
 
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.
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. psst never deploys anything here.
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 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.
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 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.
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 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.
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 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.
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 psst updates it: do not edit it.
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 psst routine
1
+ # Nightly PSST routine
2
2
 
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.
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 psst connector is connected in Claude Code (see `SKILL.md` › Before you start).
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 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.
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 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.
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