@letterstory/cli 0.148.1 → 0.148.2

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
@@ -53,6 +53,7 @@ import {
53
53
  cmdAdmin,
54
54
  cmdFeedback,
55
55
  cmdRepo,
56
+ cmdBuild,
56
57
  cmdSite,
57
58
  cmdEvals,
58
59
  cmdOfferings,
@@ -65,12 +66,15 @@ import {
65
66
 
66
67
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
67
68
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
68
- export const VERSION = "0.148.1";
69
+ export const VERSION = "0.148.2";
69
70
 
70
71
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
71
72
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
72
73
  // `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
73
74
  export const BOOLEAN_FLAGS = new Set([
75
+ // build: repeat an open build's ask; post its progress in the org's Slack channel.
76
+ "again",
77
+ "notify-slack",
74
78
  "remap",
75
79
  // login: one-link device sign-in instead of the loopback browser flow.
76
80
  "device",
@@ -1378,6 +1382,17 @@ Insights:
1378
1382
  Page access: who's reading your phantom sites
1379
1383
  insights activity [--phantom <uuid>] [--limit <n>] What hit your phantom sites in the last 7 days, newest first
1380
1384
 
1385
+ Build (a change to your site, or launching what isn't code; the Build box's own door):
1386
+ build <words…> [--prompt <text>] [--site <host>] [--subject <host/path>]... [--action build|build_pr|build_merge|slack|linear]
1387
+ [--attach <path>]... [--attach-url <name>=<https url>]... [--attach-ref <path>]... [--attach-url-ref <name>=<https url>]...
1388
+ [--harness claude-code|letterpress-1] [--notify-slack] [--again]
1389
+ Build from your words: "add a pricing page" opens a preview or PR;
1390
+ "launch a phantom for us", "set up mailers" or "onboard my blog" is
1391
+ done by Build itself (a phantom site buys its own domain), and the
1392
+ build reads Done with no PR to merge. --again repeats an open build's ask
1393
+ build talk <words…> [--conversation <id>|--build <id>] [--site <host>] [--harness claude-code|letterpress-1]
1394
+ One turn of a conversation with Build (questions, follow-ups on a build)
1395
+
1381
1396
  The Site (one model of your whole marketing site, keyed by host and path):
1382
1397
  site list Mapped hosts, and hosts you already have elsewhere
1383
1398
  site map <host> [--sitemap <url>] [--max-pages <n>] [--no-audit] [--relation owned|watched]
@@ -1587,6 +1602,7 @@ const CLIENT_COMMANDS = {
1587
1602
  mailers: cmdMailers,
1588
1603
  feedback: cmdFeedback,
1589
1604
  repo: cmdRepo,
1605
+ build: cmdBuild,
1590
1606
  site: cmdSite,
1591
1607
  evals: cmdEvals,
1592
1608
  visits: cmdVisits,
@@ -0,0 +1,99 @@
1
+ // `build` — Build from words, the way the Build box and the letterpress terminal do it
2
+ // (src/lib/growth-builds/start-build.ts). What the words name is done: a change to the site
3
+ // becomes a preview or a pull request, and launching phantom sites or mailers, or onboarding the
4
+ // main blog, is carried out by Build itself with nothing to merge.
5
+ // build <words…> → start_build (--prompt for a long ask; --again to repeat one)
6
+ // build talk <words…> → send_build_message (--conversation <id> | --build <id> to continue)
7
+
8
+ import { CliError } from "../client.mjs";
9
+ import { argsFromFlags, flagList, flagStr, ok, printResult, refuseUnknownFlags } from "./shared.mjs";
10
+ import { attachmentUrls, uploadAttachments } from "./repo.mjs";
11
+
12
+ const ARGS = {
13
+ start_build: {
14
+ // The words after `build`, or --prompt for a longer ask.
15
+ prompt: { value: (flags, positionals) => flagStr(flags.prompt) ?? (positionals.join(" ").trim() || undefined) },
16
+ domain: "site",
17
+ // --subject host/path (repeatable): the pages it is about.
18
+ subjects: { flag: "subject", type: "list" },
19
+ // --attach <path> (uploaded first) and --attach-url <name>=<https url>, both repeatable;
20
+ // -ref ones are context the agent reads, never put in the repo.
21
+ files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
22
+ delivery: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
23
+ notify_slack: { flag: "notify-slack", type: "bool" },
24
+ harness: { flag: "harness", enum: ["claude-code", "letterpress-1"] },
25
+ again: { flag: "again", type: "bool" },
26
+ },
27
+ send_build_message: {
28
+ message: {
29
+ value: (flags, positionals) => flagStr(flags.message) ?? (positionals.join(" ").trim() || undefined),
30
+ },
31
+ conversation_id: "conversation",
32
+ build_id: "build",
33
+ domain: "site",
34
+ subjects: { flag: "subject", type: "list" },
35
+ files: { value: (_flags, _positionals, extra) => (extra.files?.length ? extra.files : undefined) },
36
+ harness: { flag: "harness", enum: ["claude-code", "letterpress-1"] },
37
+ },
38
+ };
39
+
40
+ const FILE_FLAGS = ["attach", "attach-url", "attach-ref", "attach-url-ref"];
41
+
42
+ /** The files named by --attach / --attach-url (and their -ref forms), uploaded first. */
43
+ async function attachedFiles(ctx) {
44
+ const { flags } = ctx;
45
+ const place = flagList(flags.attach);
46
+ const uploaded = await uploadAttachments(ctx, [...place, ...flagList(flags["attach-ref"])]);
47
+ return [
48
+ ...uploaded.map((f, i) => (i < place.length ? f : { ...f, reference: true })),
49
+ ...attachmentUrls(flagList(flags["attach-url"])),
50
+ ...attachmentUrls(flagList(flags["attach-url-ref"])).map((f) => ({ ...f, reference: true })),
51
+ ];
52
+ }
53
+
54
+ export async function cmdBuild(ctx) {
55
+ if (ctx.positionals[0] === "talk") return buildTalk({ ...ctx, positionals: ctx.positionals.slice(1) });
56
+ return buildStart(ctx);
57
+ }
58
+
59
+ async function buildStart(ctx) {
60
+ const { client, flags, io } = ctx;
61
+ refuseUnknownFlags(ctx, {}, [
62
+ "prompt",
63
+ "site",
64
+ "subject",
65
+ "action",
66
+ "notify-slack",
67
+ "harness",
68
+ "again",
69
+ ...FILE_FLAGS,
70
+ ]);
71
+ const files = await attachedFiles(ctx);
72
+ const args = argsFromFlags(ARGS.start_build, flags, ctx.positionals, { files });
73
+ if (!args.prompt) throw new CliError('Say what to build: letterstory build "launch a phantom for us"');
74
+ const result = await client.callTool("start_build", args);
75
+ printResult(io, flags, result, (r) => {
76
+ if (r.status !== "started") return r.message ?? r.status;
77
+ const steps = (r.build?.steps ?? []).map((s) => ` ${s.status} ${s.title}${s.note ? `: ${s.note}` : ""}`);
78
+ return [`Started: ${r.build.title} (${r.build.id})`, ...steps].join("\n");
79
+ });
80
+ if (result.status !== "started" && !flags.json) ok(ctx, "Nothing was started.");
81
+ return 0;
82
+ }
83
+
84
+ async function buildTalk(ctx) {
85
+ const { client, flags, io } = ctx;
86
+ refuseUnknownFlags(ctx, {}, ["message", "conversation", "build", "site", "subject", "harness", ...FILE_FLAGS]);
87
+ const files = await attachedFiles(ctx);
88
+ const args = argsFromFlags(ARGS.send_build_message, flags, ctx.positionals, { files });
89
+ if (!args.message) throw new CliError('Say something: letterstory build talk "make the hero darker"');
90
+ const result = await client.callTool("send_build_message", args);
91
+ printResult(io, flags, result, (r) =>
92
+ [r.reply, ...(r.questions ?? []).map((q) => `? ${q.question}`), r.build ? `Build: ${r.build.id}` : null]
93
+ .filter(Boolean)
94
+ .join("\n")
95
+ );
96
+ return 0;
97
+ }
98
+
99
+ export { ARGS };
@@ -347,7 +347,7 @@ export async function cmdRepo(ctx) {
347
347
  }
348
348
 
349
349
  /** `--attach-url name=https://…` → `{ name, url }`. */
350
- function attachmentUrls(specs) {
350
+ export function attachmentUrls(specs) {
351
351
  return specs.map((spec) => {
352
352
  const at = spec.indexOf("=");
353
353
  if (at < 1) throw new CliError(`--attach-url must be name=https://… (got ${spec})`);
@@ -356,7 +356,7 @@ function attachmentUrls(specs) {
356
356
  }
357
357
 
358
358
  /** Reserve an upload per --attach file, PUT the bytes, and return `{ name, upload_path }` for each. */
359
- async function uploadAttachments(ctx, paths) {
359
+ export async function uploadAttachments(ctx, paths) {
360
360
  if (!paths.length) return [];
361
361
  const names = paths.map((p) =>
362
362
  basename(p)
package/lib/commands.mjs CHANGED
@@ -44,6 +44,7 @@ export * from "./commands/targets.mjs";
44
44
  export * from "./commands/admin.mjs";
45
45
  export * from "./commands/feedback.mjs";
46
46
  export * from "./commands/repo.mjs";
47
+ export * from "./commands/build.mjs";
47
48
  export * from "./commands/site.mjs";
48
49
  export * from "./commands/evals.mjs";
49
50
  export * from "./commands/visits.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.148.1",
3
+ "version": "0.148.2",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",