@letterstory/cli 0.112.0 → 0.113.1
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 +10 -7
- package/lib/commands/repo.mjs +11 -4
- package/lib/commands/site.mjs +94 -9
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./commands.mjs";
|
|
61
61
|
|
|
62
62
|
// Keep in sync with cli/package.json.
|
|
63
|
-
export const VERSION = "0.
|
|
63
|
+
export const VERSION = "0.113.1";
|
|
64
64
|
|
|
65
65
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
66
66
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -72,6 +72,8 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
72
72
|
"subtree",
|
|
73
73
|
// site map: do not start an audit for a host that has none.
|
|
74
74
|
"no-audit",
|
|
75
|
+
// site metrics: skip the previous window (no deltas, no revamp indicator).
|
|
76
|
+
"no-compare",
|
|
75
77
|
// site refresh / observe: the loop's switches.
|
|
76
78
|
"force-map",
|
|
77
79
|
"force-audit",
|
|
@@ -1174,15 +1176,16 @@ The Site (one model of your whole marketing site, keyed by host and path):
|
|
|
1174
1176
|
site propose <host> [path] --tool <tool> --input '<json>'|--input-file <p> --label <text> [--why <text>]
|
|
1175
1177
|
[--observation <id>]... Propose a registry tool call; input is validated now
|
|
1176
1178
|
site decide <proposal-id> --decision accept|dismiss [--reason <why>]
|
|
1177
|
-
site competitors [--since <iso>] [--limit <n>]
|
|
1178
|
-
site watch-competitors [domain]
|
|
1179
|
+
site competitors [--since <iso> | --days <n>] [--limit <n>] Tracked competitors' sites: new and removed pages, by folder
|
|
1180
|
+
site watch-competitors [domain] [--limit <n>] Map competitors' sitemaps (the next few unmapped, or one now)
|
|
1181
|
+
site highlight <domain> [--off] Highlight a competitor; its changes lead the top changes
|
|
1179
1182
|
|
|
1180
1183
|
Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub App opens a PR):
|
|
1181
1184
|
repo repos Repos the Letterstory GitHub App can reach
|
|
1182
1185
|
repo branches --repo owner/name Branches a change can start from (default first)
|
|
1183
1186
|
repo prs [--repo owner/name] Every open PR (whoever opened it), longest-idle first
|
|
1184
1187
|
repo suggestions Fixes found and not acted on yet, and how to start each
|
|
1185
|
-
repo settings [--action build|build_pr|slack] [--branch <name>|none] [--slack-agent <name|U…>|none]
|
|
1188
|
+
repo settings [--action build|build_pr|build_merge|slack] [--branch <name>|none] [--slack-agent <name|U…>|none]
|
|
1186
1189
|
[--slack-channel <C…>|none] [--slack-reports off|as_found|daily] [--reports-channel <C…>|none] [--autopilot-channel <C…>|none] [--listen <C…,C…>|none] [--autopilot on|off] [--autopilot-weekly <1-20>]
|
|
1187
1190
|
[--autopilot-severity critical|warning]
|
|
1188
1191
|
The org's defaults for every fix: build (preview first),
|
|
@@ -1192,13 +1195,13 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1192
1195
|
repo slack-test Post a test into the Send to Slack channel
|
|
1193
1196
|
repo start --repo <owner/name> --instructions <text>|--instructions-file <path|->
|
|
1194
1197
|
[--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]...
|
|
1195
|
-
[--action build|build_pr|slack] [--wait]
|
|
1198
|
+
[--action build|build_pr|build_merge|slack] [--wait]
|
|
1196
1199
|
Start a change (attached files go to the sandbox);
|
|
1197
1200
|
--wait follows it to the PR
|
|
1198
|
-
repo fix-audit --issue <issue-type>... [--audit <id>] [--branch <base>] [--action build|build_pr|slack] [--wait]
|
|
1201
|
+
repo fix-audit --issue <issue-type>... [--audit <id>] [--branch <base>] [--action build|build_pr|build_merge|slack] [--wait]
|
|
1199
1202
|
One PR fixing site-audit issues (repeat --issue to bundle) on every affected page,
|
|
1200
1203
|
in the repo verified to build the site
|
|
1201
|
-
repo install-letterprove --repo <owner/name> [--branch <base>] [--action build|build_pr|slack] [--wait]
|
|
1204
|
+
repo install-letterprove --repo <owner/name> [--branch <base>] [--action build|build_pr|build_merge|slack] [--wait]
|
|
1202
1205
|
One PR adding Letterprove's script and identify calls
|
|
1203
1206
|
(pick the repo where users sign in)
|
|
1204
1207
|
repo sites [--detect [--force]] Which website each repo builds, and the evidence
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -49,7 +49,7 @@ const ARGS = {
|
|
|
49
49
|
instructions: { value: (flags) => readTextInput(flags, "instructions", "instructions-file") },
|
|
50
50
|
// --attach <path> (uploaded first) and --attach-url <name>=<https url>, both repeatable.
|
|
51
51
|
files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
|
|
52
|
-
action: { flag: "action", enum: ["build", "build_pr", "slack"] },
|
|
52
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
53
53
|
preview: { flag: "preview", type: "bool" },
|
|
54
54
|
},
|
|
55
55
|
create_repo_change_upload: { file_names: { internal: "the basenames of the --attach files" } },
|
|
@@ -74,21 +74,21 @@ const ARGS = {
|
|
|
74
74
|
issue_types: { value: (flags) => (issueList(flags).length > 1 ? issueList(flags) : undefined) },
|
|
75
75
|
audit_id: "audit",
|
|
76
76
|
base_branch: "branch",
|
|
77
|
-
action: { flag: "action", enum: ["build", "build_pr", "slack"] },
|
|
77
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
78
78
|
preview: { flag: "preview", type: "bool" },
|
|
79
79
|
},
|
|
80
80
|
letterprove_open_install_pr: {
|
|
81
81
|
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
82
82
|
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
83
83
|
base_branch: "branch",
|
|
84
|
-
action: { flag: "action", enum: ["build", "build_pr", "slack"] },
|
|
84
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
85
85
|
},
|
|
86
86
|
get_repo_change_settings: {},
|
|
87
87
|
list_suggested_fixes: {},
|
|
88
88
|
list_slack_fix_targets: {},
|
|
89
89
|
send_slack_test_message: {},
|
|
90
90
|
set_repo_change_settings: {
|
|
91
|
-
default_action: { flag: "action", enum: ["build", "build_pr", "slack"] },
|
|
91
|
+
default_action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
92
92
|
// --branch <name>, or --branch none for each repo's own default.
|
|
93
93
|
default_branch: { value: (flags) => (flags.branch === "none" ? null : flagStr(flags.branch)) },
|
|
94
94
|
// --slack-agent Devin (or a U… member id), or --slack-agent none to tag nobody.
|
|
@@ -115,6 +115,13 @@ const ARGS = {
|
|
|
115
115
|
.map((c) => c.trim())
|
|
116
116
|
.filter(Boolean),
|
|
117
117
|
},
|
|
118
|
+
// --listen-all on|off: answer messages meant for Letter in every channel it's in
|
|
119
|
+
slack_listen_all: {
|
|
120
|
+
value: (flags) =>
|
|
121
|
+
flags["listen-all"] === undefined
|
|
122
|
+
? undefined
|
|
123
|
+
: ["on", "true", "1"].includes(String(flags["listen-all"])),
|
|
124
|
+
},
|
|
118
125
|
autopilot_channel: {
|
|
119
126
|
value: (flags) => (flags["autopilot-channel"] === "none" ? null : flagStr(flags["autopilot-channel"])),
|
|
120
127
|
},
|
package/lib/commands/site.mjs
CHANGED
|
@@ -2,15 +2,26 @@
|
|
|
2
2
|
// (src/lib/site). One subcommand per tool:
|
|
3
3
|
// site list → list_sites site map <host> → map_site
|
|
4
4
|
// site tree <host> [path] → get_site_tree site page <host> <path> → get_site_page
|
|
5
|
+
// site metrics <host> [path] → get_site_metrics (--window 7d|28d|90d, --page <path>…, --no-compare)
|
|
5
6
|
// site observations <host> → list_site_observations site observe <host> → observe_site
|
|
6
7
|
// site dismiss <host> <id> → dismiss_site_observation
|
|
7
8
|
// site families <host> → list_site_families site decide-family <id> → decide_site_family
|
|
8
9
|
// site proposals <host> → list_site_proposals site propose <host> → create_site_proposal
|
|
9
10
|
// site decide <proposal> → decide_site_proposal site fix <host> <id>… → fix_site_observations
|
|
10
11
|
// site competitors → list_competitor_sites site watch-competitors [d] → watch_competitor_sites
|
|
12
|
+
// site highlight <domain> [--off] → set_competitor_highlight
|
|
11
13
|
|
|
12
14
|
import { CliError } from "../client.mjs";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
argsFromFlags,
|
|
17
|
+
flagList,
|
|
18
|
+
flagNum,
|
|
19
|
+
flagStr,
|
|
20
|
+
ok,
|
|
21
|
+
printResult,
|
|
22
|
+
readTextInput,
|
|
23
|
+
refuseUnknownFlags,
|
|
24
|
+
} from "./shared.mjs";
|
|
14
25
|
|
|
15
26
|
/** --include <path>… / --exclude <path>… (+ --reason) → decide_site_family members. */
|
|
16
27
|
function familyMembers(flags) {
|
|
@@ -67,11 +78,34 @@ const ARGS = {
|
|
|
67
78
|
},
|
|
68
79
|
},
|
|
69
80
|
},
|
|
81
|
+
get_site_metrics: {
|
|
82
|
+
host: { positional: 0, required: true, label: "host" },
|
|
83
|
+
path: { positional: 1, label: "path" },
|
|
84
|
+
paths: { flag: "page", type: "list" },
|
|
85
|
+
window: { flag: "window", enum: ["7d", "28d", "90d"] },
|
|
86
|
+
compare: { flag: "no-compare", type: "bool", invert: true },
|
|
87
|
+
},
|
|
70
88
|
list_competitor_sites: {
|
|
71
|
-
since
|
|
89
|
+
// --since <iso>, or --days <n> as shorthand for "the last n days" (--days 1 is the last 24 hours).
|
|
90
|
+
since: {
|
|
91
|
+
value: (flags) => {
|
|
92
|
+
const since = flagStr(flags.since);
|
|
93
|
+
if (since !== undefined) return since;
|
|
94
|
+
const days = flagNum(flags.days);
|
|
95
|
+
return days === undefined ? undefined : new Date(Date.now() - days * 86_400_000).toISOString();
|
|
96
|
+
},
|
|
97
|
+
},
|
|
72
98
|
limit: { flag: "limit", type: "num" },
|
|
73
99
|
},
|
|
74
|
-
watch_competitor_sites: {
|
|
100
|
+
watch_competitor_sites: {
|
|
101
|
+
domain: { positional: 0, label: "domain" },
|
|
102
|
+
limit: { flag: "limit", type: "num" },
|
|
103
|
+
},
|
|
104
|
+
set_competitor_highlight: {
|
|
105
|
+
domain: { positional: 0, required: true, label: "domain" },
|
|
106
|
+
// Highlighted unless --off.
|
|
107
|
+
highlighted: { value: (flags) => flags.off !== true },
|
|
108
|
+
},
|
|
75
109
|
get_site_page: {
|
|
76
110
|
host: { positional: 0, required: true, label: "host" },
|
|
77
111
|
path: { positional: 1, required: true, label: "path" },
|
|
@@ -143,7 +177,7 @@ const ARGS = {
|
|
|
143
177
|
},
|
|
144
178
|
base_branch: { flag: "base-branch" },
|
|
145
179
|
preview: { flag: "preview", type: "bool" },
|
|
146
|
-
action: { flag: "action", enum: ["build", "build_pr", "slack"] },
|
|
180
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack"] },
|
|
147
181
|
},
|
|
148
182
|
};
|
|
149
183
|
|
|
@@ -161,6 +195,8 @@ export async function cmdSite(ctx) {
|
|
|
161
195
|
return siteUnmap(rest);
|
|
162
196
|
case "tree":
|
|
163
197
|
return siteTree(rest);
|
|
198
|
+
case "metrics":
|
|
199
|
+
return siteMetrics(rest);
|
|
164
200
|
case "page":
|
|
165
201
|
return sitePage(rest);
|
|
166
202
|
case "observations":
|
|
@@ -185,9 +221,11 @@ export async function cmdSite(ctx) {
|
|
|
185
221
|
return siteCompetitors(rest);
|
|
186
222
|
case "watch-competitors":
|
|
187
223
|
return siteWatchCompetitors(rest);
|
|
224
|
+
case "highlight":
|
|
225
|
+
return siteHighlight(rest);
|
|
188
226
|
default:
|
|
189
227
|
throw new CliError(
|
|
190
|
-
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix, competitors, watch-competitors`
|
|
228
|
+
`Unknown site subcommand: ${sub ?? "(none)"}. Try: list, map, refresh, unmap, tree, metrics, page, observations, observe, dismiss, families, decide-family, proposals, propose, decide, fix, competitors, watch-competitors, highlight`
|
|
191
229
|
);
|
|
192
230
|
}
|
|
193
231
|
}
|
|
@@ -212,11 +250,12 @@ async function siteList(ctx) {
|
|
|
212
250
|
printResult(io, flags, result, (r) => {
|
|
213
251
|
const lines = r.sites.map(
|
|
214
252
|
(s) =>
|
|
215
|
-
`${s.host} ${s.mapStatus} ${s.pagesTotal} pages${s.mapTruncated ? " (truncated)" : ""}${s.mappedAt ? ` mapped ${s.mappedAt.slice(0, 10)}` : ""}`
|
|
253
|
+
`${s.host}${s.kind && s.kind !== "site" ? ` [${s.kind}]` : ""} ${s.mapStatus} ${s.pagesTotal} pages${s.mapTruncated ? " (truncated)" : ""}${s.mappedAt ? ` mapped ${s.mappedAt.slice(0, 10)}` : ""}`
|
|
216
254
|
);
|
|
217
255
|
if (r.suggestions.length) {
|
|
218
256
|
lines.push("Not yet mapped:");
|
|
219
|
-
for (const s of r.suggestions)
|
|
257
|
+
for (const s of r.suggestions)
|
|
258
|
+
lines.push(` ${s.host}${s.kind && s.kind !== "site" ? ` [${s.kind}]` : ""} (${s.from.join(", ")})`);
|
|
220
259
|
}
|
|
221
260
|
return lines.join("\n") || "No sites yet. Map one with: letterstory site map <host>";
|
|
222
261
|
});
|
|
@@ -233,16 +272,24 @@ async function siteCompetitors(ctx) {
|
|
|
233
272
|
printResult(io, flags, result, (r) => {
|
|
234
273
|
const lines = [`Since ${r.since.slice(0, 10)}:`];
|
|
235
274
|
for (const c of r.competitors) {
|
|
275
|
+
const star = c.highlighted ? "* " : " ";
|
|
236
276
|
if (!c.changes) {
|
|
237
|
-
lines.push(
|
|
277
|
+
lines.push(
|
|
278
|
+
`${star}${c.name} ${c.domain} not mapped (letterstory site watch-competitors ${c.domain})`
|
|
279
|
+
);
|
|
238
280
|
continue;
|
|
239
281
|
}
|
|
240
282
|
const folders = c.changes.folders.map((f) => `${f.path} +${f.added}${f.removed ? ` -${f.removed}` : ""}`);
|
|
241
283
|
lines.push(
|
|
242
|
-
|
|
284
|
+
`${star}${c.name} ${c.host} ${c.changes.pages} pages +${c.changes.added} -${c.changes.removed}${folders.length ? ` ${folders.join(", ")}` : ""}`
|
|
243
285
|
);
|
|
244
286
|
for (const p of c.changes.addedPages.slice(0, 5)) lines.push(` + ${p.path} ${p.at.slice(0, 10)}`);
|
|
245
287
|
}
|
|
288
|
+
const top = r.topChanges ?? [];
|
|
289
|
+
if (top.length) {
|
|
290
|
+
lines.push(r.competitors.some((c) => c.highlighted) ? "Top changes (highlighted):" : "Top changes:");
|
|
291
|
+
for (const t of top) lines.push(` ${t.summary} ${t.at.slice(0, 10)}`);
|
|
292
|
+
}
|
|
246
293
|
return r.competitors.length ? lines.join("\n") : "No competitors tracked yet.";
|
|
247
294
|
});
|
|
248
295
|
return 0;
|
|
@@ -260,6 +307,18 @@ async function siteWatchCompetitors(ctx) {
|
|
|
260
307
|
return 0;
|
|
261
308
|
}
|
|
262
309
|
|
|
310
|
+
async function siteHighlight(ctx) {
|
|
311
|
+
const { client, flags, io } = ctx;
|
|
312
|
+
refuseUnknownFlags(ctx, ARGS.set_competitor_highlight, ["off"]);
|
|
313
|
+
const result = await client.callTool(
|
|
314
|
+
"set_competitor_highlight",
|
|
315
|
+
argsFromFlags(ARGS.set_competitor_highlight, flags, ctx.positionals)
|
|
316
|
+
);
|
|
317
|
+
ok(ctx, result.message);
|
|
318
|
+
printResult(io, flags, result, (r) => (r.highlighted.length ? `Highlighted: ${r.highlighted.join(", ")}` : ""));
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
263
322
|
async function siteMap(ctx) {
|
|
264
323
|
const { client, flags, io } = ctx;
|
|
265
324
|
refuseUnknownFlags(ctx, ARGS.map_site);
|
|
@@ -303,6 +362,32 @@ async function siteTree(ctx) {
|
|
|
303
362
|
return 0;
|
|
304
363
|
}
|
|
305
364
|
|
|
365
|
+
async function siteMetrics(ctx) {
|
|
366
|
+
const { client, flags, io } = ctx;
|
|
367
|
+
refuseUnknownFlags(ctx, ARGS.get_site_metrics);
|
|
368
|
+
const result = await client.callTool(
|
|
369
|
+
"get_site_metrics",
|
|
370
|
+
argsFromFlags(ARGS.get_site_metrics, flags, ctx.positionals)
|
|
371
|
+
);
|
|
372
|
+
printResult(io, flags, result, (r) => {
|
|
373
|
+
const lines = [`${r.host} ${r.window} (${r.range.start} to ${r.range.end}) source: ${r.source}`];
|
|
374
|
+
if (r.source !== "lettervisit") lines.push(` ${r.message}`);
|
|
375
|
+
for (const p of r.pages) {
|
|
376
|
+
const value = r.source === "lettervisit" ? p.accesses?.total : p.clicks;
|
|
377
|
+
const delta = r.source === "lettervisit" ? p.accesses_delta_pct : p.clicks_delta_pct;
|
|
378
|
+
const split = p.accesses
|
|
379
|
+
? ` (AI ${p.accesses.ai_agents}, crawlers ${p.accesses.search_crawlers}, people ${p.accesses.humans})`
|
|
380
|
+
: "";
|
|
381
|
+
const marks = p.indicators.map((i) => i.label).join(", ");
|
|
382
|
+
lines.push(
|
|
383
|
+
` ${p.path} ${value ?? "-"} ${r.unit ?? ""}${delta !== null ? ` ${delta > 0 ? "+" : ""}${delta}%` : ""}${split}${marks ? ` [${marks}]` : ""}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return lines.join("\n");
|
|
387
|
+
});
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
306
391
|
async function sitePage(ctx) {
|
|
307
392
|
const { client, flags, io } = ctx;
|
|
308
393
|
refuseUnknownFlags(ctx, ARGS.get_site_page);
|