@letterstory/cli 0.176.87 → 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 +18 -10
- package/lib/commands/ask.mjs +96 -22
- package/lib/commands/plan.mjs +4 -2
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
package/lib/commands/ask.mjs
CHANGED
|
@@ -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
|
|
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|… →
|
|
5
|
-
// ask --strategy <id> → a
|
|
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
|
|
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
|
|
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
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
package/lib/commands/plan.mjs
CHANGED
|
@@ -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 ?
|
|
200
|
-
:
|
|
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")
|