@letterstory/cli 0.176.43 → 0.176.45
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/lib/cli.mjs +15 -1
- package/lib/commands/replicate.mjs +138 -0
- package/lib/commands/visits.mjs +28 -1
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -78,12 +78,13 @@ import {
|
|
|
78
78
|
cmdVary,
|
|
79
79
|
cmdDomains,
|
|
80
80
|
cmdBench,
|
|
81
|
+
cmdReplicate,
|
|
81
82
|
cmdDeploy,
|
|
82
83
|
} from "./commands.mjs";
|
|
83
84
|
import { fetchParkedSections, withoutParked } from "./parked.mjs";
|
|
84
85
|
|
|
85
86
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
86
|
-
export const VERSION = "0.176.
|
|
87
|
+
export const VERSION = "0.176.45";
|
|
87
88
|
|
|
88
89
|
/** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
|
|
89
90
|
export const EXIT_PAYMENT_REQUIRED = 3;
|
|
@@ -109,6 +110,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
109
110
|
"spec-changed",
|
|
110
111
|
// bench update: remove the README's disclosure.
|
|
111
112
|
"no-disclosure",
|
|
113
|
+
// replicate show: list only the pages below the bar.
|
|
114
|
+
"failing",
|
|
112
115
|
// sim create / resample / sample-key: keep the customer's sample key, or delete it now.
|
|
113
116
|
"keep-key",
|
|
114
117
|
"delete-key",
|
|
@@ -1850,6 +1853,7 @@ Visits (Visitors: AI agents reading your site, and AI answers sending people to
|
|
|
1850
1853
|
Pages one agent read that the other didn't
|
|
1851
1854
|
visits export [--since 30d] [--format json|csv] [--table agent_visits|citations]
|
|
1852
1855
|
visits sites Your Visitors sites and their install checks
|
|
1856
|
+
visits domain <domain> [--site <id>] [--name <n>] Move a site to a new domain, keeping its key
|
|
1853
1857
|
visits pages [--since 28d] [--path '/docs/*'] [--agent <id>] [--limit <n>] [--site <id>]
|
|
1854
1858
|
People and AI agents per page
|
|
1855
1859
|
visits health --site <id> Staff: how far to trust a site's numbers, each check and its fix
|
|
@@ -1915,6 +1919,15 @@ Bench (Benches: open-source benchmarks on their own domains, kept running; @lett
|
|
|
1915
1919
|
bench cite <id> The reference line a post cites
|
|
1916
1920
|
bench replicate <id> REPLICATE.md: how an agent reproduces the results
|
|
1917
1921
|
|
|
1922
|
+
Replicate (the Replicator: a website rebuilt as a native Next.js repo, page for page; @letterstory.com only):
|
|
1923
|
+
replicate <https://site> [--max-pages <n>] [--repo-name <r>] [--repo-owner <o>]
|
|
1924
|
+
Capture every page, rebuild it, compare it pixel by pixel
|
|
1925
|
+
replicate [list] Your replications: stage, how closely each matches
|
|
1926
|
+
replicate show <id> [--failing] Pages with their routes and pixel match; collections; events
|
|
1927
|
+
replicate attach <id> <repo-name> [--repo-owner <o>] The repo to commit it into (yours; Letter never creates it)
|
|
1928
|
+
replicate advance <id> [--retry] Move it on now; --retry once blocked_on is done
|
|
1929
|
+
replicate archive <id> Stop it; the project stays in storage and in the repo
|
|
1930
|
+
|
|
1918
1931
|
Evals (how the pipeline is doing on your posts, and the regression cases captured from them):
|
|
1919
1932
|
evals scoreboard [--collection <id>] [--days N] Every measure by stage: posts, failing, rate
|
|
1920
1933
|
evals cases [--status open|guarding|proposed|retired|rejected] [--stage research|brief|links|mentions|accuracy|pipeline|shred]
|
|
@@ -2021,6 +2034,7 @@ const CLIENT_COMMANDS = {
|
|
|
2021
2034
|
vary: cmdVary,
|
|
2022
2035
|
domains: cmdDomains,
|
|
2023
2036
|
bench: cmdBench,
|
|
2037
|
+
replicate: cmdReplicate,
|
|
2024
2038
|
deploy: cmdDeploy,
|
|
2025
2039
|
// Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
|
|
2026
2040
|
admin: cmdAdmin,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// `replicate` — the Replicator from a terminal (docs/architecture/replicator.md): a website rebuilt
|
|
2
|
+
// as a native Next.js repo, page for page, and committed to a repo on your organization's GitHub.
|
|
3
|
+
// Staff only: the tools answer only a key an @letterstory.com person made. Thin wrappers over the
|
|
4
|
+
// replicator tools (create_replication, list_replications, get_replication,
|
|
5
|
+
// attach_replication_repo, advance_replication, archive_replication), the same lib functions the
|
|
6
|
+
// app calls.
|
|
7
|
+
//
|
|
8
|
+
// letterstory replicate https://example.com [--max-pages 300] [--repo-name example-site] [--repo-owner <account>]
|
|
9
|
+
// letterstory replicate list
|
|
10
|
+
// letterstory replicate show <id> [--failing]
|
|
11
|
+
// letterstory replicate attach <id> <repo-name> [--repo-owner <account>]
|
|
12
|
+
// letterstory replicate advance <id> [--retry]
|
|
13
|
+
// letterstory replicate archive <id>
|
|
14
|
+
|
|
15
|
+
import { CliError } from "../client.mjs";
|
|
16
|
+
import { argsFromFlags, printResult, refuseUnknownFlags } from "./shared.mjs";
|
|
17
|
+
|
|
18
|
+
const replicationId = { positional: 0, label: "replication-id", required: true };
|
|
19
|
+
|
|
20
|
+
export const ARGS = {
|
|
21
|
+
create_replication: {
|
|
22
|
+
url: { positional: 0, label: "url", required: true },
|
|
23
|
+
max_pages: { flag: "max-pages", type: "num" },
|
|
24
|
+
repo_name: "repo-name",
|
|
25
|
+
repo_owner: "repo-owner",
|
|
26
|
+
},
|
|
27
|
+
list_replications: {},
|
|
28
|
+
get_replication: {
|
|
29
|
+
replication_id: replicationId,
|
|
30
|
+
pages: { value: (flags) => (flags.failing === true ? "failing" : undefined) },
|
|
31
|
+
},
|
|
32
|
+
attach_replication_repo: {
|
|
33
|
+
replication_id: replicationId,
|
|
34
|
+
repo_name: { positional: 1, label: "repo-name", required: true },
|
|
35
|
+
repo_owner: "repo-owner",
|
|
36
|
+
},
|
|
37
|
+
advance_replication: { replication_id: replicationId, retry: { flag: "retry", type: "bool" } },
|
|
38
|
+
archive_replication: { replication_id: replicationId },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function call(ctx, tool) {
|
|
42
|
+
return ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pct = (x) => `${(x * 100).toFixed(2)}%`;
|
|
46
|
+
|
|
47
|
+
export async function cmdReplicate(ctx) {
|
|
48
|
+
const sub = ctx.positionals[0];
|
|
49
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
50
|
+
if (sub && /^https?:\/\//i.test(sub)) {
|
|
51
|
+
refuseUnknownFlags(ctx, ARGS.create_replication, []);
|
|
52
|
+
return replicationShow(ctx, await call(ctx, "create_replication"));
|
|
53
|
+
}
|
|
54
|
+
switch (sub) {
|
|
55
|
+
case undefined:
|
|
56
|
+
case "list":
|
|
57
|
+
case "ls":
|
|
58
|
+
return replicationList(rest);
|
|
59
|
+
case "show":
|
|
60
|
+
refuseUnknownFlags(rest, ARGS.get_replication, ["failing"]);
|
|
61
|
+
return replicationDetail(rest, await call(rest, "get_replication"));
|
|
62
|
+
case "attach":
|
|
63
|
+
refuseUnknownFlags(rest, ARGS.attach_replication_repo, []);
|
|
64
|
+
return replicationShow(rest, await call(rest, "attach_replication_repo"));
|
|
65
|
+
case "advance":
|
|
66
|
+
refuseUnknownFlags(rest, ARGS.advance_replication, []);
|
|
67
|
+
return replicationShow(rest, await call(rest, "advance_replication"));
|
|
68
|
+
case "archive":
|
|
69
|
+
return replicationShow(rest, await call(rest, "archive_replication"));
|
|
70
|
+
default:
|
|
71
|
+
throw new CliError("Usage: replicate <https://site> | replicate [list|show|attach|advance|archive]");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function replicationList(ctx) {
|
|
76
|
+
const result = await call(ctx, "list_replications");
|
|
77
|
+
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
78
|
+
if (!r.replications?.length) return "No replications yet. Start one: replicate https://example.com";
|
|
79
|
+
return r.replications
|
|
80
|
+
.map(
|
|
81
|
+
(x) =>
|
|
82
|
+
`${x.id} ${x.host.padEnd(28)} ${(x.blocked_on ? "blocked" : x.status).padEnd(10)} ${x.stage.padEnd(10)} ${
|
|
83
|
+
x.summary
|
|
84
|
+
? `${x.summary.passing}/${x.summary.replicated} pages match, mean ${pct(x.summary.meanPixel)}`
|
|
85
|
+
: ""
|
|
86
|
+
}`
|
|
87
|
+
)
|
|
88
|
+
.join("\n");
|
|
89
|
+
});
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function headline(x) {
|
|
94
|
+
return [
|
|
95
|
+
`${x.host} — ${x.status}, stage ${x.stage}`,
|
|
96
|
+
...(x.blocked_on ? [` Blocked: ${x.blocked_on}`, ` Then: replicate advance ${x.id} --retry`] : []),
|
|
97
|
+
...(x.stage_note && !x.blocked_on ? [` ${x.stage_note}`] : []),
|
|
98
|
+
...(x.summary
|
|
99
|
+
? [
|
|
100
|
+
` ${x.summary.replicated} of ${x.summary.pages} pages replicated; ${x.summary.passing} match the original at every width (mean ${pct(x.summary.meanPixel)})`,
|
|
101
|
+
]
|
|
102
|
+
: []),
|
|
103
|
+
...x.collections.map(
|
|
104
|
+
(c) =>
|
|
105
|
+
` Collection ${c.pattern}: ${c.items} items (${c.markdown_bodies} as Markdown) in ${c.content_path_template}`
|
|
106
|
+
),
|
|
107
|
+
...(x.repo_url
|
|
108
|
+
? [` Repo ${x.repo_url}${x.commit_sha ? ` @ ${x.commit_sha.slice(0, 7)}` : ""}`]
|
|
109
|
+
: x.repo_name
|
|
110
|
+
? [` Repo ${x.repo_owner ? `${x.repo_owner}/` : ""}${x.repo_name} (attached)`]
|
|
111
|
+
: []),
|
|
112
|
+
...(x.stage === "ready" && !x.repo_name ? [` Ready: replicate attach ${x.id} <repo-name> to commit it`] : []),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function replicationShow(ctx, result) {
|
|
117
|
+
printResult(ctx.io, ctx.flags, result, (r) => headline(r.replication).join("\n"));
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function replicationDetail(ctx, result) {
|
|
122
|
+
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
123
|
+
const lines = headline(r.replication);
|
|
124
|
+
if (r.pages?.length) {
|
|
125
|
+
lines.push("", "Pages:");
|
|
126
|
+
for (const p of r.pages)
|
|
127
|
+
lines.push(
|
|
128
|
+
` ${p.passing ? "✓" : "✗"} ${p.path.padEnd(48)} ${p.scores.map((s) => `${s.viewport} ${pct(s.pixel)}`).join(" ")}${p.error ? ` (${p.error})` : ""}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (r.dropped?.length)
|
|
132
|
+
lines.push("", `The site's own code left out (needs its builder's runtime): ${r.dropped.length}`);
|
|
133
|
+
if (r.events?.length)
|
|
134
|
+
lines.push("", "Recent:", ...r.events.slice(0, 8).map((e) => ` ${e.created_at} ${e.message}`));
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
});
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
package/lib/commands/visits.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// letterstory visits people --since 30d people who read the site, and how many were identified
|
|
12
12
|
// letterstory visits export --since 30d --format csv
|
|
13
13
|
// letterstory visits sites
|
|
14
|
+
// letterstory visits domain composal.com [--site <id>] move the site to a new domain, keeping its key
|
|
14
15
|
// letterstory visits pages --since 28d [--path '/docs/*'] [--agent openai-crawler]
|
|
15
16
|
// letterstory visits health --site <id> staff: how far to trust a site's numbers, and what to fix
|
|
16
17
|
// letterstory visits connect vercel|cloudflare send the host's request logs (no code to deploy)
|
|
@@ -26,6 +27,11 @@ const days = { value: (flags) => sinceDays(flags.since) };
|
|
|
26
27
|
|
|
27
28
|
export const ARGS = {
|
|
28
29
|
visitors_list_sites: {},
|
|
30
|
+
visitors_update_site: {
|
|
31
|
+
site_id: "site",
|
|
32
|
+
domain: { positional: 0, label: "new domain (e.g. acme.com)", required: true },
|
|
33
|
+
name: "name",
|
|
34
|
+
},
|
|
29
35
|
visitors_get_agent_visits: {
|
|
30
36
|
site_id: "site",
|
|
31
37
|
days,
|
|
@@ -99,6 +105,8 @@ export async function cmdVisits(ctx) {
|
|
|
99
105
|
return visitsExport(rest);
|
|
100
106
|
case "sites":
|
|
101
107
|
return visitsSites(rest);
|
|
108
|
+
case "domain":
|
|
109
|
+
return visitsDomain(rest);
|
|
102
110
|
case "pages":
|
|
103
111
|
return visitsPages(rest);
|
|
104
112
|
case "health":
|
|
@@ -111,7 +119,7 @@ export async function cmdVisits(ctx) {
|
|
|
111
119
|
return visitsDisconnect(rest);
|
|
112
120
|
default:
|
|
113
121
|
throw new CliError(
|
|
114
|
-
"Usage: visits [list|citations|people|compare|export|sites|pages|health|connect|connectors|disconnect]"
|
|
122
|
+
"Usage: visits [list|citations|people|compare|export|sites|domain|pages|health|connect|connectors|disconnect]"
|
|
115
123
|
);
|
|
116
124
|
}
|
|
117
125
|
}
|
|
@@ -359,6 +367,25 @@ async function visitsPages(ctx) {
|
|
|
359
367
|
return 0;
|
|
360
368
|
}
|
|
361
369
|
|
|
370
|
+
async function visitsDomain(ctx) {
|
|
371
|
+
const { flags, io } = ctx;
|
|
372
|
+
const result = await call(ctx, "visitors_update_site");
|
|
373
|
+
printResult(io, flags, result, (r) => {
|
|
374
|
+
const domain = r.site?.domain ?? "";
|
|
375
|
+
if (!r.changed) return `Already on ${domain}.`;
|
|
376
|
+
const check = r.verification;
|
|
377
|
+
return [
|
|
378
|
+
`Moved ${r.previous_domain ?? "the site"} to ${domain}; the key stays ${r.site?.key ?? ""}.`,
|
|
379
|
+
check
|
|
380
|
+
? check.found
|
|
381
|
+
? `Proven: the script was found on ${check.checked_url ?? domain}.`
|
|
382
|
+
: `Not proven yet: ${check.reason}`
|
|
383
|
+
: "Same site: its proof carried over.",
|
|
384
|
+
].join("\n");
|
|
385
|
+
});
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
362
389
|
async function visitsSites(ctx) {
|
|
363
390
|
const { flags, io } = ctx;
|
|
364
391
|
const result = await call(ctx, "visitors_list_sites");
|
package/lib/commands.mjs
CHANGED