@letterstory/cli 0.176.48 → 0.176.50

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
@@ -80,11 +80,12 @@ import {
80
80
  cmdBench,
81
81
  cmdReplicate,
82
82
  cmdDeploy,
83
+ cmdRefs,
83
84
  } from "./commands.mjs";
84
85
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
85
86
 
86
87
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
87
- export const VERSION = "0.176.48";
88
+ export const VERSION = "0.176.50";
88
89
 
89
90
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
90
91
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -1676,7 +1677,8 @@ Insights:
1676
1677
  Plan tab's box), what's broken (--lens fix, most pressing
1677
1678
  first), what to build (--lens build), or your own words. Nothing asked:
1678
1679
  each list's head. The same answer as the console and Slack.
1679
- plan auto [activate|disable] [--site host] [--veto-hours <1-168>|off] [--pace-hours <1-168>] [--auto-merge on|off]
1680
+ plan auto [activate|disable] [--site host] [--veto-hours <1-168>|off] [--pace-hours <1-168>]
1681
+ [--delivery build|build_pr|build_merge]
1680
1682
  [--aggression low|medium|high|xhigh] [--goal geo|seo|agent_conviction|inbound_conversion|any]
1681
1683
  [--slack-channel <C…>|none]
1682
1684
  Auto-Plan on a site: Letterpress picks its strategies and builds
@@ -1723,6 +1725,7 @@ Build (a change to your site, or launching what isn't code; the Build box's own
1723
1725
  done by Build itself (a phantom site buys its own domain), and the
1724
1726
  build reads Done with no PR to merge. --again repeats an open build's ask.
1725
1727
  --new-site: a website of its own, built and hosted (deploy create)
1728
+ --look-like a.com,b.com (with --new-site): sites to model its look after
1726
1729
  build talk <words…> [--conversation <id>|--build <id>] [--site <host>] [--harness letterpress-1|claude-code|codex]
1727
1730
  [--repo owner/name] [--action build|build_pr|build_merge]
1728
1731
  One turn of a conversation with Build (questions, follow-ups on a build)
@@ -1891,9 +1894,10 @@ Vary (A/B tests of a page's words and images on your own site):
1891
1894
 
1892
1895
  Deploy (websites and apps Letter builds from your words and hosts on their own domains):
1893
1896
  deploy [list] [--kind app|phantom|bench] Everything deployed: apps, phantoms (and benches, for staff)
1894
- deploy create "<what it is and who it is for>" [--name <n>] [--domain <d>] [--wait]
1897
+ deploy create "<what it is and who it is for>" [--name <n>] [--domain <d>] [--ref <visual-ref-id>] [--wait]
1895
1898
  Named, given a domain (bought under $30 unless you name one),
1896
- built by Letterpress-1 with Visitors in it, and put live
1899
+ built by Letterpress-1 with Visitors in it, and put live;
1900
+ --ref designs it after a Visual Ref (refs create)
1897
1901
  deploy show <id> Where it stands, its domain, the records to set, recent events
1898
1902
  deploy update <id> [--name <n>] [--domain <d>] The domain only before it is built
1899
1903
  deploy pause <id> | deploy resume <id> | deploy archive <id>
@@ -1902,6 +1906,17 @@ Deploy (websites and apps Letter builds from your words and hosts on their own d
1902
1906
  deploy connect <id> <owner/name> Move its source to your GitHub repo (empty); it's
1903
1907
  pushed once the Letter GitHub App can reach it
1904
1908
 
1909
+ Refs (Visual Refs: other companies' brands to design something new after, apart from your own brand):
1910
+ refs [list] [--archived] Your Visual Refs and their brands
1911
+ refs create "<name>" --brand <host[=what to take]>… [--notes <n>]
1912
+ Each brand is learned from its site (a few minutes)
1913
+ refs show <id> Its brands, whether each is learned, their type
1914
+ refs update <id> [--name <n>] [--notes <n>] [--brand <host[=note]>]… [--remove <host>]…
1915
+ refs archive <id> | refs restore <id>
1916
+ refs brief <id> "<what is being made, for whom>" [--wait]
1917
+ Every brand's direction for it, as one file
1918
+ refs brief-show <brief-id> The brief, once ready
1919
+
1905
1920
  Bench (Benches: open-source benchmarks on their own domains, kept running; @letterstory.com only):
1906
1921
  bench [list] Your benches: stage, what each waits on, domain
1907
1922
  bench create (--prompt "<what it is and how it works>"|--prompt-file <path>) [--name <n>] [--domain <d>]
@@ -2036,6 +2051,7 @@ const CLIENT_COMMANDS = {
2036
2051
  bench: cmdBench,
2037
2052
  replicate: cmdReplicate,
2038
2053
  deploy: cmdDeploy,
2054
+ refs: cmdRefs,
2039
2055
  // Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
2040
2056
  admin: cmdAdmin,
2041
2057
  };
@@ -16,7 +16,16 @@
16
16
 
17
17
  import { readFileSync } from "node:fs";
18
18
  import { CliError } from "../client.mjs";
19
- import { argsFromFlags, flagList, flagStr, followJob, ok, printResult, refuseUnknownFlags } from "./shared.mjs";
19
+ import {
20
+ argsFromFlags,
21
+ flagCsv,
22
+ flagList,
23
+ flagStr,
24
+ followJob,
25
+ ok,
26
+ printResult,
27
+ refuseUnknownFlags,
28
+ } from "./shared.mjs";
20
29
  import { attachmentUrls, uploadAttachments } from "./repo.mjs";
21
30
 
22
31
  const ARGS = {
@@ -54,6 +63,8 @@ const ARGS = {
54
63
  recipe: "recipe",
55
64
  // --new-site: a website or app of its own, built and hosted on its own domain (deploy create's path).
56
65
  new_site: { flag: "new-site", type: "bool" },
66
+ // --new-site --look-like linear.app,supabase.com: other companies' sites to model its look after.
67
+ look_like: { value: (flags) => (flagCsv(flags["look-like"]).length ? flagCsv(flags["look-like"]) : undefined) },
57
68
  // --plan-only: answer what it would do and start nothing (--json for the plan to send back).
58
69
  plan_only: { flag: "plan-only", type: "bool" },
59
70
  // --plan-only always plans what it's given, as /plan does: it was asked for by name.
@@ -3,7 +3,7 @@
3
3
  // Thin wrappers over the deployment tools (create_deployment, list_deployments, get_deployment,
4
4
  // update_deployment, advance_deployment), the same lib functions the app calls.
5
5
  //
6
- // letterstory deploy create "Build me a website for HVAC contractors to collect leads" [--name <n>] [--domain <d>] [--wait]
6
+ // letterstory deploy create "Build me a website for HVAC contractors to collect leads" [--name <n>] [--domain <d>] [--ref <visual-ref-id>] [--wait]
7
7
  // letterstory deploy list [--kind app|phantom|bench]
8
8
  // letterstory deploy show <id>
9
9
  // letterstory deploy update <id> [--name <n>] [--domain <d>]
@@ -29,6 +29,7 @@ export const ARGS = {
29
29
  prompt: { value: (flags, positionals) => flagStr(flags.prompt) ?? (positionals.join(" ").trim() || undefined) },
30
30
  name: "name",
31
31
  domain: "domain",
32
+ visual_ref_id: { flag: "ref" },
32
33
  wait: { flag: "wait", type: "bool" },
33
34
  },
34
35
  list_deployments: { kind: { flag: "kind", enum: ["app", "phantom", "bench"] } },
@@ -1,7 +1,7 @@
1
1
  // `plan` — Auto-Plan on one site (Plan → Auto-Plan; Plan is scoped to one website, --site, else the
2
2
  // organization's own domain). The same calls as the Plan's modal and its sidebar's Pending rows.
3
3
  // plan auto → get_auto_plan
4
- // plan auto [activate|disable] [--veto-hours <1-168>|off] [--pace-hours <1-168>] [--auto-merge on|off]
4
+ // plan auto [activate|disable] [--veto-hours <1-168>|off] [--pace-hours <1-168>] [--delivery build|build_pr|build_merge]
5
5
  // [--aggression low|medium|high|xhigh] [--goal geo|seo|agent_conviction|inbound_conversion|any]
6
6
  // [--slack-channel <C…>|none] → set_auto_plan
7
7
  // plan pending → list_auto_plan_proposals (status pending)
@@ -28,13 +28,6 @@ const jsonFlag = (name) => ({
28
28
  }
29
29
  },
30
30
  });
31
- const onOff = (name) => (flags) => {
32
- const v = flagStr(flags[name]);
33
- if (v === undefined) return undefined;
34
- if (["on", "true", "1"].includes(v)) return true;
35
- if (["off", "false", "0"].includes(v)) return false;
36
- throw new CliError(`--${name} takes on or off.`);
37
- };
38
31
 
39
32
  export const ARGS = {
40
33
  list_plan_work_purposes: { resource: jsonFlag("resource"), strategy_id: "strategy" },
@@ -71,7 +64,8 @@ export const ARGS = {
71
64
  },
72
65
  // --pace-hours <1-168> how often it picks the next action (1 every hour, 24 every day, 168 every week).
73
66
  pace_hours: { flag: "pace-hours", type: "num" },
74
- merge: { value: onOff("auto-merge") },
67
+ // How far each pick goes: a preview, a pull request, or one merged once ready.
68
+ delivery: { flag: "delivery", enum: ["build", "build_pr", "build_merge"] },
75
69
  aggression: { flag: "aggression", enum: ["low", "medium", "high", "xhigh"] },
76
70
  // --goal geo picks strategies for that goal only; --goal any (the default) reads every strategy.
77
71
  goal: {
@@ -117,7 +111,7 @@ export const ARGS = {
117
111
  },
118
112
  };
119
113
 
120
- const SETTING_FLAGS = ["veto-hours", "pace-hours", "auto-merge", "aggression", "goal", "slack-channel"];
114
+ const SETTING_FLAGS = ["veto-hours", "pace-hours", "delivery", "aggression", "goal", "slack-channel"];
121
115
 
122
116
  export async function cmdPlan(ctx) {
123
117
  const { client, flags, io } = ctx;
@@ -0,0 +1,131 @@
1
+ // `refs` — Visual Refs from a terminal (docs/architecture/visual-refs.md): sets of other companies'
2
+ // brands to design something new after, kept apart from your own brand. Thin wrappers over the
3
+ // Visual Ref tools (create_visual_ref, list_visual_refs, get_visual_ref, update_visual_ref,
4
+ // brief_visual_ref, get_visual_ref_brief), the same lib functions the app calls.
5
+ //
6
+ // letterstory refs list [--archived]
7
+ // letterstory refs create "Linear × Supabase" --brand linear.app --brand "supabase.com=the dark palette" [--notes <n>]
8
+ // letterstory refs show <id>
9
+ // letterstory refs update <id> [--name <n>] [--notes <n>] [--brand <host[=note]>]… [--remove <host>]…
10
+ // letterstory refs archive <id> | refs restore <id>
11
+ // letterstory refs brief <id> "<what is being made, for whom>" [--wait]
12
+ // letterstory refs brief-show <brief-id>
13
+ // letterstory deploy create "<the site>" --ref <id> a new site designed after the ref
14
+
15
+ import { CliError } from "../client.mjs";
16
+ import { argsFromFlags, flagList, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
17
+
18
+ const refId = { positional: 0, label: "ref-id", required: true };
19
+
20
+ /** `--brand host` or `--brand "host=what to take from it"`, repeatable. */
21
+ function brandsOf(flags) {
22
+ const brands = flagList(flags.brand).map((raw) => {
23
+ const at = raw.indexOf("=");
24
+ const host = (at < 0 ? raw : raw.slice(0, at)).trim();
25
+ const note = at < 0 ? "" : raw.slice(at + 1).trim();
26
+ return note ? { host, note } : { host };
27
+ });
28
+ return brands.length ? brands : undefined;
29
+ }
30
+
31
+ export const ARGS = {
32
+ create_visual_ref: {
33
+ name: { value: (flags, positionals) => flagStr(flags.name) ?? (positionals.join(" ").trim() || undefined) },
34
+ notes: "notes",
35
+ brands: { value: (flags) => brandsOf(flags) },
36
+ },
37
+ list_visual_refs: { archived: { flag: "archived", type: "bool" } },
38
+ get_visual_ref: { ref_id: refId },
39
+ update_visual_ref: {
40
+ ref_id: refId,
41
+ name: "name",
42
+ notes: "notes",
43
+ add_brands: { value: (flags) => brandsOf(flags) },
44
+ remove_hosts: { value: (flags) => (flagList(flags.remove).length ? flagList(flags.remove) : undefined) },
45
+ archived: { internal: "refs archive / refs restore" },
46
+ },
47
+ brief_visual_ref: {
48
+ ref_id: refId,
49
+ task: {
50
+ value: (flags, positionals) => flagStr(flags.task) ?? (positionals.slice(1).join(" ").trim() || undefined),
51
+ },
52
+ wait: { flag: "wait", type: "bool" },
53
+ },
54
+ get_visual_ref_brief: { brief_id: { positional: 0, label: "brief-id", required: true } },
55
+ };
56
+
57
+ function call(ctx, tool, extra = {}) {
58
+ return ctx.client.callTool(tool, { ...argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals), ...extra });
59
+ }
60
+
61
+ export async function cmdRefs(ctx) {
62
+ const sub = ctx.positionals[0];
63
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
64
+ switch (sub) {
65
+ case undefined:
66
+ case "list":
67
+ case "ls": {
68
+ refuseUnknownFlags(rest, ARGS.list_visual_refs, []);
69
+ const result = await call(rest, "list_visual_refs");
70
+ printResult(rest.io, rest.flags, result, (r) => {
71
+ if (!r.refs?.length)
72
+ return 'No Visual Refs yet. Make one: refs create "Linear × Supabase" --brand linear.app --brand supabase.com';
73
+ return r.refs
74
+ .map((v) => `${v.id} ${v.name.slice(0, 28).padEnd(28)} ${v.brands.map((b) => b.host).join(", ")}`)
75
+ .join("\n");
76
+ });
77
+ return 0;
78
+ }
79
+ case "create": {
80
+ refuseUnknownFlags(rest, ARGS.create_visual_ref, ["brand", "name"]);
81
+ const args = argsFromFlags(ARGS.create_visual_ref, rest.flags, rest.positionals);
82
+ if (!args.name || !args.brands)
83
+ throw new CliError('Name it and give its brands: refs create "Linear × Supabase" --brand linear.app');
84
+ return refShow(rest, await rest.client.callTool("create_visual_ref", args));
85
+ }
86
+ case "show":
87
+ return refShow(rest, await call(rest, "get_visual_ref"));
88
+ case "update":
89
+ refuseUnknownFlags(rest, ARGS.update_visual_ref, ["brand", "remove"]);
90
+ return refShow(rest, await call(rest, "update_visual_ref"));
91
+ case "archive":
92
+ case "restore":
93
+ return refShow(rest, await call(rest, "update_visual_ref", { archived: sub === "archive" }));
94
+ case "brief": {
95
+ refuseUnknownFlags(rest, ARGS.brief_visual_ref, ["task"]);
96
+ return briefShow(rest, await call(rest, "brief_visual_ref"));
97
+ }
98
+ case "brief-show":
99
+ return briefShow(rest, await call(rest, "get_visual_ref_brief"));
100
+ default:
101
+ throw new CliError("Usage: refs [list|create|show|update|archive|restore|brief|brief-show]");
102
+ }
103
+ }
104
+
105
+ function refShow(ctx, result) {
106
+ printResult(ctx.io, ctx.flags, result, (r) => {
107
+ const v = r.ref;
108
+ const lines = [`${v.name}${v.archivedAt ? " (archived)" : ""} ${v.id}`, ...(v.notes ? [` ${v.notes}`] : [])];
109
+ for (const b of v.brands) {
110
+ const state = b.brand ? b.brand.state : "";
111
+ lines.push(` ${b.host.padEnd(28)} ${state.padEnd(12)} ${b.note ?? ""}`.trimEnd());
112
+ const type = b.brand?.profile?.type?.map((t) => t.family).filter(Boolean);
113
+ if (type?.length) lines.push(` type: ${[...new Set(type)].join(", ")}`);
114
+ }
115
+ lines.push("", `Brief a task on it: refs brief ${v.id} "<what is being made, for whom>"`);
116
+ return lines.join("\n");
117
+ });
118
+ return 0;
119
+ }
120
+
121
+ function briefShow(ctx, result) {
122
+ printResult(ctx.io, ctx.flags, result, (r) => {
123
+ const b = r.brief;
124
+ if (b.status === "pending") {
125
+ const parts = b.parts.map((p) => ` ${p.host.padEnd(28)} ${p.status}`);
126
+ return [`Brief ${b.id}: pending`, ...parts, "", `Check again: refs brief-show ${b.id}`].join("\n");
127
+ }
128
+ return b.markdown ?? `Brief ${b.id}: ${b.status}`;
129
+ });
130
+ return 0;
131
+ }
package/lib/commands.mjs CHANGED
@@ -57,3 +57,4 @@ export * from "./commands/domains.mjs";
57
57
  export * from "./commands/bench.mjs";
58
58
  export * from "./commands/replicate.mjs";
59
59
  export * from "./commands/deploy.mjs";
60
+ export * from "./commands/refs.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.48",
3
+ "version": "0.176.50",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",