@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 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.112.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>] Tracked competitors' sites: new and removed pages, by folder
1178
- site watch-competitors [domain] Map competitors' sitemaps (every unmapped one, or one now)
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
@@ -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
  },
@@ -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 { argsFromFlags, flagList, flagStr, ok, printResult, readTextInput, refuseUnknownFlags } from "./shared.mjs";
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: { flag: "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: { domain: { positional: 0, label: "domain" } },
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) lines.push(` ${s.host} (${s.from.join(", ")})`);
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(` ${c.name} ${c.domain} not mapped (letterstory site watch-competitors ${c.domain})`);
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
- ` ${c.name} ${c.host} ${c.changes.pages} pages +${c.changes.added} -${c.changes.removed}${folders.length ? ` ${folders.join(", ")}` : ""}`
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.112.0",
3
+ "version": "0.113.1",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",