@letterstory/cli 0.176.86 → 0.176.88

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
@@ -66,6 +66,7 @@ import {
66
66
  cmdRepo,
67
67
  cmdBuild,
68
68
  cmdAsk,
69
+ cmdIdeas,
69
70
  cmdPlan,
70
71
  cmdSite,
71
72
  cmdEvals,
@@ -85,7 +86,7 @@ import {
85
86
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
86
87
 
87
88
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
88
- export const VERSION = "0.176.86";
89
+ export const VERSION = "0.176.88";
89
90
 
90
91
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
91
92
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -1736,26 +1737,32 @@ Mail:
1736
1737
  (any phone format; country as a name or a 2-letter code)
1737
1738
 
1738
1739
  Insights:
1739
- ask [<words…>] [--outcome geo|seo|…] [--more] [--lens fix|build] [--site host]
1740
- Ask Letterpress what to do next: the plan for an outcome (the
1741
- Plan tab's box), what's broken (--lens fix, most pressing
1742
- first), what to build (--lens build), or your own words. Nothing asked:
1743
- each list's head. The same answer as the console and Slack.
1740
+ ideas [--goal all|geo|seo|agent_conviction|inbound_conversion|<strategy-id>] [--count 3|5|all] [--site host]
1741
+ Ideas, as the app lists them: every idea that fits (filtered to a
1742
+ goal, or a goal you typed), the top 3 by default; each with its
1743
+ state (ready, needs you, waiting, PR open, done), what it waits
1744
+ on, why, its steps, and what starting it does.
1745
+ ask [<words…>] [--outcome geo|seo|…] [--strategy <id>] [--more] [--lens fix|build] [--site host]
1746
+ Ask Letterpress what to do next: Ideas for a goal (--outcome, or
1747
+ --strategy for a goal you typed; --more for every idea that fits),
1748
+ what's broken (--lens fix, most pressing first), what to build
1749
+ (--lens build), or your own words. Nothing asked: each list's head.
1750
+ The same answer as the app and Slack.
1744
1751
  plan auto [activate|disable] [--site host] [--veto-hours <1-168>|off] [--pace-hours <1-168>]
1745
1752
  [--delivery build|build_pr|build_merge]
1746
1753
  [--aggression low|medium|high|xhigh] [--strategies geo,seo,agent_conviction,inbound_conversion|any]
1747
1754
  [--slack-channel <C…>|none]
1748
1755
  Auto-Plan on a site: Letterpress picks its strategies and builds
1749
- them under the site's strategies (--strategies, any by default; the Plan reads the same set); each pick waits --veto-hours for a veto. Settings shown when none given.
1756
+ them under the site's strategies (--strategies, any by default); each pick waits --veto-hours for a veto. Settings shown when none given.
1750
1757
  plan pending [--site host] What Auto-Plan picked and is waiting to build
1751
1758
  plan veto <proposal-id> Veto one: it is never built
1752
1759
  plan start <proposal-id> Start one now (a Needs you pick, or one waiting out its review window)
1753
1760
  plan preview [--site host] What Auto-Plan would do first, before it is on
1754
- plan later <action-id> [--outcome geo|…] [--site host] Queue a Plan row for later: Auto-Plan starts it once it's ready
1761
+ plan later <action-id> [--outcome geo|…] [--site host] Queue an idea for later: Auto-Plan starts it once it's ready
1755
1762
  plan later remove <queued-id> Take a queued row back off
1756
- plan strategies [--site host] [--archived] The goals typed on the Plan, each a strategy
1763
+ plan strategies [--site host] [--archived] The goals typed on Ideas, each a strategy
1757
1764
  plan strategy add "<words…>" [--site host] Keep a goal in your words as a strategy
1758
- plan strategy archive <strategy-id> [--restore] Put one away (its card leaves the Plan), or bring it back
1765
+ plan strategy archive <strategy-id> [--restore] Put one away (its goal leaves Ideas), or bring it back
1759
1766
  insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
1760
1767
  insights reads [--period …] [--site all|own|phantoms|<key>]... [--path <path>] [--engine <operator>]
1761
1768
  Measure → Reads: AI engines and people reading your sites, counted one way
@@ -2125,6 +2132,7 @@ const CLIENT_COMMANDS = {
2125
2132
  repo: cmdRepo,
2126
2133
  build: cmdBuild,
2127
2134
  ask: cmdAsk,
2135
+ ideas: cmdIdeas,
2128
2136
  plan: cmdPlan,
2129
2137
  site: cmdSite,
2130
2138
  evals: cmdEvals,
@@ -1,11 +1,20 @@
1
1
  // `ask` — ask Letterpress, the one door (docs/architecture/one-recipe-engine.md §9b): what should
2
- // this account do next. The same answer as the console's Plan, Build and Fix tabs and Slack.
2
+ // this account do next. The same answer as Ideas, Build and Fix and Slack.
3
3
  // ask → each list's head: what's broken, what to build
4
- // ask --outcome geo|seo|… → the plan for that outcome (Ideas filtered to it); --more: every row that fits
5
- // ask --strategy <id> → a typed strategy's next actions (plan strategies)
4
+ // ask --outcome geo|seo|… → Ideas for that goal; --more: every idea that fits
5
+ // ask --strategy <id> → a goal you typed (plan strategies), the same door as its words
6
6
  // ask --lens fix | --lens build → what's broken (most pressing first) | what to build
7
7
  // ask "<words…>" → in your own words; what nothing does comes back as unmet
8
8
  // ask "<words…>" --found "<…>" → with what you read outside the account for them (a case study)
9
+ //
10
+ // `ideas` — Ideas as the app shows it (docs/architecture/plan-strategies.md, "Ideas: one flat list"): every idea that
11
+ // fits, read through the same door with `more`, a goal filter and a count. A projection of ask_letterpress, never a
12
+ // list of its own:
13
+ // ideas [--goal all|geo|seo|agent_conviction|inbound_conversion|<strategy-id>] [--count 3|5|all] [--site host]
14
+ //
15
+ // Each row is printed as Ideas shows it: its title and state in words (the door's `badge`: ready, needs you,
16
+ // waiting, PR open, done), what it waits on (`waiting`), why, why now, its steps (`detail`), what is queued on it
17
+ // (`queued`) and what starting it does (`start`); never a raw status or a tool's name.
9
18
 
10
19
  import { CliError } from "../client.mjs";
11
20
  import { argsFromFlags, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
@@ -13,20 +22,52 @@ import { argsFromFlags, flagStr, printResult, refuseUnknownFlags } from "./share
13
22
  export const ARGS = {
14
23
  ask_letterpress: {
15
24
  outcome: "outcome",
16
- // --strategy <id>: a typed strategy's next actions (plan strategies), the same door as its words.
25
+ // --strategy <id>: a goal you typed (plan strategies), the same door as its words.
17
26
  strategy_id: "strategy",
18
27
  lens: "lens",
19
28
  words: { value: (flags, positionals) => flagStr(flags.words) ?? (positionals.join(" ").trim() || undefined) },
20
29
  // --found: what was read outside the account for the words (a case study, a competitor), as findings.
21
30
  found: "found",
22
- // --refresh: decide every strategy again from the facts as they are now (the standing fit is read otherwise).
31
+ // --refresh: decide every idea again from the facts as they are now (the standing fit is read otherwise).
23
32
  refresh: { flag: "refresh", type: "bool" },
24
- // --more: every row that fits, not only the top ones (the Ideas list's Show all); alone, Ideas under All.
33
+ // --more: every idea that fits, not only the top ones (Ideas' All); alone, Ideas under All.
25
34
  more: { flag: "more", type: "bool" },
26
35
  domain: "site",
27
36
  },
28
37
  };
29
38
 
39
+ /** A row's state in words, as its badge says it; an older server's raw status said the same way. */
40
+ const STATUS_WORDS = { ready: "ready", blocked: "waiting", running: "running", done: "done" };
41
+
42
+ /** Pure: the lines one item prints as, numbered `n` (1-based), indented under its title. */
43
+ export function ideaLines(item, n) {
44
+ const pad = " ";
45
+ const state = item.badge?.label ?? STATUS_WORDS[item.status] ?? item.status;
46
+ const lines = [`${String(n).padStart(3)}. ${item.title} [${state}]`];
47
+ const waiting = item.waiting ?? (item.blocked_by?.length ? item.blocked_by.join("; ") : null);
48
+ if (waiting) lines.push(`${pad}Waiting on: ${waiting}`);
49
+ if (item.why) lines.push(`${pad}${item.why}`);
50
+ if (item.why_now && item.why_now !== item.why) lines.push(`${pad}Why now: ${item.why_now}`);
51
+ // What the row does and its steps ("Next: …", "Turns on … in Automations"), as Ideas shows them.
52
+ if (item.detail && item.detail !== item.why) lines.push(`${pad}${item.detail}`);
53
+ if (item.queued?.line) lines.push(`${pad}${item.queued.line}`);
54
+ if (item.start) lines.push(`${pad}${item.start.label}: ${item.start.does}`);
55
+ else if (item.action && !item.action.callable && item.action.href && item.status === "ready")
56
+ lines.push(`${pad}Done in the app: ${item.action.href}`);
57
+ return lines;
58
+ }
59
+
60
+ /** Pure: an answer's footer: what nothing does, and a first read still under way. */
61
+ function tailLines(r) {
62
+ const lines = [];
63
+ if (r.unmet) lines.push(`Not something we do yet: ${r.unmet}`);
64
+ if (r.status === "pending")
65
+ lines.push(
66
+ `Still reading${r.waiting_on?.length ? `: ${r.waiting_on.join("; ")}` : " the site"}. Ask again in a few seconds.`
67
+ );
68
+ return lines;
69
+ }
70
+
30
71
  export async function cmdAsk(ctx) {
31
72
  const { client, flags, io } = ctx;
32
73
  refuseUnknownFlags(ctx, {}, ["outcome", "strategy", "lens", "words", "found", "site", "refresh", "more"]);
@@ -34,23 +75,56 @@ export async function cmdAsk(ctx) {
34
75
  if ([args.outcome, args.strategy_id, args.lens, args.words].filter(Boolean).length > 1)
35
76
  throw new CliError("One ask at a time: --outcome, --strategy <id>, --lens fix|build, or your words.");
36
77
  const result = await client.callTool("ask_letterpress", args);
78
+ printResult(io, flags, result, (r) =>
79
+ [r.message, ...r.items.flatMap((i, at) => ideaLines(i, at + 1)), ...tailLines(r)].join("\n")
80
+ );
81
+ return 0;
82
+ }
83
+
84
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
85
+
86
+ /**
87
+ * Pure: the ask behind Ideas' goal filter (AccountPlanView `askOfIdeasFilter`): All is the outcome `other`, a goal its
88
+ * outcome, a typed goal its strategy id; always every idea that fits (`more`), the count is the terminal's to apply.
89
+ * Which goals exist is the door's to say: a name it doesn't take comes back as its error.
90
+ */
91
+ export function askOfIdeasGoal(goal) {
92
+ const g = (goal ?? "all").trim();
93
+ if (!g || g.toLowerCase() === "all") return { outcome: "other", more: true };
94
+ if (UUID.test(g)) return { strategy_id: g, more: true };
95
+ return { outcome: g.toLowerCase().replace(/[\s-]+/g, "_"), more: true };
96
+ }
97
+
98
+ /** Pure: how many ideas --count shows: 3 (the default), 5, or every one (null). */
99
+ export function ideasCount(count) {
100
+ const c = flagStr(count)?.trim().toLowerCase();
101
+ if (c === undefined || c === "" || c === "3") return 3;
102
+ if (c === "5") return 5;
103
+ if (c === "all") return null;
104
+ throw new CliError("--count takes 3, 5 or all.");
105
+ }
106
+
107
+ export async function cmdIdeas(ctx) {
108
+ const { client, flags, io } = ctx;
109
+ refuseUnknownFlags(ctx, {}, ["goal", "count", "site"]);
110
+ if (ctx.positionals.length)
111
+ throw new CliError(
112
+ 'ideas takes no words: --goal all|geo|seo|agent_conviction|inbound_conversion|<strategy-id>, --count 3|5|all. For your own words, `ask "<words…>"`.'
113
+ );
114
+ const count = ideasCount(flags.count);
115
+ const site = flagStr(flags.site);
116
+ const result = await client.callTool("ask_letterpress", {
117
+ ...askOfIdeasGoal(flagStr(flags.goal)),
118
+ ...(site ? { domain: site } : {}),
119
+ });
37
120
  printResult(io, flags, result, (r) => {
38
- const lines = [r.message];
39
- for (const i of r.items) {
40
- const waits = i.blocked_by?.length ? ` (waits on: ${i.blocked_by.join("; ")})` : "";
41
- const how = i.action
42
- ? i.action.callable
43
- ? ` → ${i.action.tool}`
44
- : i.action.href
45
- ? ` → ${i.action.href}`
46
- : ""
47
- : "";
48
- lines.push(` ${i.status.padEnd(7)} ${i.title}${how}${waits}`, ` ${i.why}`);
49
- // What the row does and its steps ("Next: …", "Turns on … in Automations"), as Ideas shows them.
50
- if (i.detail && i.detail !== i.why) lines.push(` ${i.detail}`);
51
- }
52
- if (r.unmet) lines.push(`Not something we do yet: ${r.unmet}`);
53
- if (r.status === "pending") lines.push("(reading the site; ask again in a few seconds)");
121
+ const shown = count === null ? r.items : r.items.slice(0, count);
122
+ if (!r.items.length)
123
+ return [r.status === "pending" ? "Coming up with your ideas…" : r.message, ...tailLines(r)].join("\n");
124
+ const lines = [`Ideas${site ? ` for ${site}` : ""}: ${shown.length} of ${r.items.length}.`];
125
+ shown.forEach((i, at) => lines.push(...ideaLines(i, at + 1)));
126
+ if (shown.length < r.items.length) lines.push(`More with --count ${count === 3 ? "5 or --count all" : "all"}.`);
127
+ lines.push(...tailLines(r));
54
128
  return lines.join("\n");
55
129
  });
56
130
  return 0;
@@ -196,8 +196,10 @@ export async function cmdPlan(ctx) {
196
196
  (p) =>
197
197
  ` ${p.status.padEnd(8)} ${p.title} ${
198
198
  p.queued_by
199
- ? `(later${p.waits_on ? `, waits on ${p.waits_on}` : ""})`
200
- : `(builds ${p.due_at})`
199
+ ? `(later${p.waits_on ? `: ${p.waits_on}` : ""})`
200
+ : p.waits_on
201
+ ? "(yours to start)"
202
+ : `(builds ${p.due_at})`
201
203
  } ${p.id}`
202
204
  )
203
205
  .join("\n")
@@ -180,6 +180,13 @@ function replicationShow(ctx, result) {
180
180
  function replicationDetail(ctx, result) {
181
181
  printResult(ctx.io, ctx.flags, result, (r) => {
182
182
  const lines = headline(r.replication);
183
+ // Waiting for its domain to be verified: the tag for the homepage (then advance --retry checks it).
184
+ if (r.domain_verification)
185
+ lines.push(
186
+ "",
187
+ `Add this tag to the <head> of ${r.domain_verification.host}'s homepage:`,
188
+ r.domain_verification.tag
189
+ );
183
190
  if (r.pages?.length) {
184
191
  lines.push("", "Pages:");
185
192
  for (const p of r.pages) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.86",
3
+ "version": "0.176.88",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",