@letterstory/cli 0.176.87 → 0.176.89
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/automate.mjs +7 -2
- 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.89";
|
|
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `automate` — Letterpress → Automations (docs/architecture/event-automations.md): an event, read
|
|
2
2
|
// against your filter (--instruction), then done by Letterpress as your prompt (--prompt) says.
|
|
3
|
-
// options / list / show / add / set / try / runs / approve / reject / send / findings / act / dismiss / remove, through the
|
|
3
|
+
// options / list / show / add / set / try / trigger / runs / approve / reject / send / findings / act / dismiss / remove, through the
|
|
4
4
|
// same tools the app uses. (A letterbase's writing automations are made and paced with `automations`.) `list` also
|
|
5
5
|
// shows the jobs Letter runs for the account (source "letter": a letterbase's internal links, its add-your-name
|
|
6
6
|
// suggestions, Fix autopilot, Auto-Plan, and each letterbase's writing automations), each with its scope: changed with
|
|
@@ -165,6 +165,9 @@ export const ARGS = {
|
|
|
165
165
|
slack_channel: slackChannel,
|
|
166
166
|
limit: { flag: "limit", type: "num" },
|
|
167
167
|
},
|
|
168
|
+
trigger_event_automation: {
|
|
169
|
+
automation_id: { positional: 0, label: "automation-id", required: true },
|
|
170
|
+
},
|
|
168
171
|
list_automation_runs: {
|
|
169
172
|
automation_id: { flag: "automation" },
|
|
170
173
|
status: { flag: "status", enum: STATUSES },
|
|
@@ -234,6 +237,8 @@ export async function cmdAutomate(ctx) {
|
|
|
234
237
|
return run(rest, "update_event_automation", "Changed.");
|
|
235
238
|
case "try":
|
|
236
239
|
return run(rest, "try_event_automation");
|
|
240
|
+
case "trigger":
|
|
241
|
+
return run(rest, "trigger_event_automation");
|
|
237
242
|
case "runs":
|
|
238
243
|
return run(rest, "list_automation_runs");
|
|
239
244
|
case "approve":
|
|
@@ -253,7 +258,7 @@ export async function cmdAutomate(ctx) {
|
|
|
253
258
|
return run(rest, "delete_event_automation", "Removed.");
|
|
254
259
|
default:
|
|
255
260
|
throw new CliError(
|
|
256
|
-
`Unknown automate subcommand: ${sub ?? "(none)"}. Try: options, list, show, add, set, try, runs, approve, reject, send, findings, act, dismiss, remove`
|
|
261
|
+
`Unknown automate subcommand: ${sub ?? "(none)"}. Try: options, list, show, add, set, try, trigger, runs, approve, reject, send, findings, act, dismiss, remove`
|
|
257
262
|
);
|
|
258
263
|
}
|
|
259
264
|
}
|
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")
|