@letterstory/cli 0.176.95 → 0.176.97

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
@@ -82,11 +82,12 @@ import {
82
82
  cmdReplicate,
83
83
  cmdDeploy,
84
84
  cmdRefs,
85
+ cmdModels,
85
86
  } from "./commands.mjs";
86
87
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
87
88
 
88
89
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
89
- export const VERSION = "0.176.95";
90
+ export const VERSION = "0.176.97";
90
91
 
91
92
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
92
93
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -101,6 +102,8 @@ export const BOOLEAN_FLAGS = new Set([
101
102
  "advance",
102
103
  // build list: only what the Plan started.
103
104
  "from-plan",
105
+ // domains plan: scores only, no model call.
106
+ "no-write",
104
107
  // build: a website or app of its own (deploy create's path). deploy create: hold until its build starts.
105
108
  "new-site",
106
109
  "wait",
@@ -413,6 +416,10 @@ export const BOOLEAN_FLAGS = new Set([
413
416
  "dry",
414
417
  // admin billing emails --bounced
415
418
  "bounced",
419
+ // models add: train once the links are added. models confirm: keep the found links not named, or don't train yet.
420
+ "train",
421
+ "keep-rest",
422
+ "no-train",
416
423
  ]);
417
424
 
418
425
  // Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
@@ -1971,10 +1978,10 @@ Domains (the domains you hold, and free ones that match what people search):
1971
1978
  Free .com/.org domains on what people search in your
1972
1979
  space (brainstormed, or the phrases given), ranked
1973
1980
  domains buy <domain> --dry-run | --yes The price and whose account pays; --yes buys it
1974
- domains plan <domain> [--phrase "<search>"] [--searches <n>]
1981
+ domains plan <domain> [--phrase "<search>"] [--searches <n>] [--version <version>] [--no-write]
1975
1982
  The most strategic site for the domain, every version
1976
1983
  scored, and whether it can launch; spends nothing
1977
- domains launch <domain> [--phrase …] [--searches …] [--words "<plan words>"] --yes
1984
+ domains launch <domain> [--phrase …] [--searches …] [--version <version>] [--words "<plan words>"] --yes
1978
1985
  Build and Launch Site: buys the domain if needed, builds
1979
1986
  the plan and puts it live
1980
1987
 
@@ -2015,6 +2022,22 @@ Refs (Visual Refs: other companies' brands to design something new after, apart
2015
2022
  Every brand's direction for it, as one file
2016
2023
  refs brief-show <brief-id> The brief, once ready
2017
2024
 
2025
+ Models (your own writing models, each trained on writing you give it so it writes in your voice):
2026
+ models [list] [--archived] Your models: where each stands, its links, its version
2027
+ models create "<name>" [--budget <usd>] [--description <d>]
2028
+ Budget: the most one training run may cost (default $100)
2029
+ models show <id> Its links, its training and what it cost
2030
+ models add <id> <url>… [--train] Links to pieces of your writing; --train starts training
2031
+ models find <id> "<the writing to find>" [--max <n>] Letter finds the links and proposes them
2032
+ models confirm <id> [<link-id>…] [--keep-rest] [--no-train]
2033
+ Confirm what was found (all, or the ids) and train on it
2034
+ models remove <id> <link-id>… Take links out of the next version
2035
+ models train <id> [--retry] Train the next version, or try a stopped one again
2036
+ models stop <id> Stop what is training; what it spent is charged
2037
+ models write <id> "<what to write>" [--words <n>] Write with its trained version
2038
+ models update <id> [--name <n>] [--budget <usd>] [--description <d>]
2039
+ models archive <id> | models restore <id>
2040
+
2018
2041
  Bench (Benches: open-source benchmarks on their own domains, kept running; @letterstory.com only):
2019
2042
  bench [list] Your benches: stage, what each waits on, domain
2020
2043
  bench create (--prompt "<what it is and how it works>"|--prompt-file <path>) [--name <n>] [--domain <d>]
@@ -2155,6 +2178,7 @@ const CLIENT_COMMANDS = {
2155
2178
  replicate: cmdReplicate,
2156
2179
  deploy: cmdDeploy,
2157
2180
  refs: cmdRefs,
2181
+ models: cmdModels,
2158
2182
  // Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
2159
2183
  admin: cmdAdmin,
2160
2184
  };
@@ -7,9 +7,10 @@
7
7
  // letterstory domains scan "nano banana pro" ["ai headshots" …] [--tld com,org,ai] [--expand none|searches|space]
8
8
  // letterstory domains buy nanobananaprofree.com --dry-run the price and whose account pays; buys nothing
9
9
  // letterstory domains buy nanobananaprofree.com --yes buys it (spends money; domains:write)
10
- // letterstory domains plan bestaidatalabs.com [--phrase "best ai data labs"] [--searches 880]
10
+ // letterstory domains plan bestaidatalabs.com [--phrase "best ai data labs"] [--searches 880] [--version comparison]
11
+ // [--no-write]
11
12
  // the most strategic site for it; spends nothing
12
- // letterstory domains launch bestaidatalabs.com [--phrase …] [--searches …] --yes
13
+ // letterstory domains launch bestaidatalabs.com [--phrase …] [--searches …] [--version …] --yes
13
14
  // Build and Launch Site: buys it if needed, builds the
14
15
  // plan and puts it live (spends money; domains:write)
15
16
 
@@ -42,11 +43,16 @@ export const ARGS = {
42
43
  domain: { value: (flags, positionals) => stripScheme(requirePositional(positionals, 0, "domain")) },
43
44
  phrase: { flag: "phrase" },
44
45
  monthly_searches: { flag: "searches", type: "num" },
46
+ // A version other than the top score (ranked_list, comparison, directory, free_tool, templates, guide).
47
+ version: { flag: "version" },
48
+ // The scores and whether it can launch, at once: the version's own plan, no model call.
49
+ write: { flag: "no-write", type: "bool", invert: true },
45
50
  },
46
51
  launch_domain_site: {
47
52
  domain: { value: (flags, positionals) => stripScheme(requirePositional(positionals, 0, "domain")) },
48
53
  phrase: { flag: "phrase" },
49
54
  monthly_searches: { flag: "searches", type: "num" },
55
+ version: { flag: "version" },
50
56
  // The words of a plan already read (`domains plan … --json`'s plan.words): that plan is built as it was.
51
57
  words: { flag: "words" },
52
58
  },
@@ -0,0 +1,219 @@
1
+ // `models` — your own writing models from a terminal (docs/architecture/writing-models.md): each post-trained
2
+ // on writing you give it, so it writes in your voice. Thin wrappers over the writing model tools
3
+ // (create_writing_model, list_writing_models, get_writing_model, update_writing_model, add_writing_model_links,
4
+ // find_writing_model_links, confirm_writing_model_links, remove_writing_model_links, train_writing_model,
5
+ // write_with_writing_model), the same lib functions the app calls.
6
+ //
7
+ // letterstory models [list] [--archived]
8
+ // letterstory models create "<name>" [--budget <usd>] [--description <d>]
9
+ // letterstory models show <id>
10
+ // letterstory models add <id> <url>… [--train]
11
+ // letterstory models find <id> "<the writing to find>" [--max <n>]
12
+ // letterstory models confirm <id> [<link-id>…] [--keep-rest] [--no-train] no link ids: all of them
13
+ // letterstory models remove <id> <link-id>…
14
+ // letterstory models train <id> [--retry]
15
+ // letterstory models stop <id> stop what is training
16
+ // letterstory models write <id> "<what to write>" [--words <n>]
17
+ // letterstory models update <id> [--name <n>] [--budget <usd>] [--description <d>]
18
+ // letterstory models archive <id> | models restore <id>
19
+
20
+ import { CliError } from "../client.mjs";
21
+ import { argsFromFlags, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
22
+
23
+ const modelId = { positional: 0, label: "model-id", required: true };
24
+ const rest = (from) => (flags, positionals) => {
25
+ const list = positionals.slice(from).filter(Boolean);
26
+ return list.length ? list : undefined;
27
+ };
28
+
29
+ export const ARGS = {
30
+ create_writing_model: {
31
+ name: { value: (flags, positionals) => flagStr(flags.name) ?? (positionals.join(" ").trim() || undefined) },
32
+ description: "description",
33
+ budget_usd: { flag: "budget", type: "num" },
34
+ },
35
+ list_writing_models: { archived: { flag: "archived", type: "bool" } },
36
+ get_writing_model: { model_id: modelId },
37
+ update_writing_model: {
38
+ model_id: modelId,
39
+ name: "name",
40
+ description: "description",
41
+ budget_usd: { flag: "budget", type: "num" },
42
+ archived: { internal: "models archive / models restore" },
43
+ },
44
+ add_writing_model_links: {
45
+ model_id: modelId,
46
+ urls: { value: rest(1) },
47
+ train: { flag: "train", type: "bool" },
48
+ },
49
+ find_writing_model_links: {
50
+ model_id: modelId,
51
+ prompt: { value: (flags, positionals) => positionals.slice(1).join(" ").trim() || undefined },
52
+ max_links: { flag: "max", type: "num" },
53
+ },
54
+ confirm_writing_model_links: {
55
+ model_id: modelId,
56
+ confirm: { value: (flags, positionals) => rest(1)(flags, positionals) ?? "all" },
57
+ decline_rest: { flag: "keep-rest", type: "bool", invert: true },
58
+ train: { flag: "no-train", type: "bool", invert: true },
59
+ },
60
+ remove_writing_model_links: { model_id: modelId, source_ids: { value: rest(1) } },
61
+ train_writing_model: { model_id: modelId, retry: { flag: "retry", type: "bool" } },
62
+ cancel_writing_model_run: { model_id: modelId },
63
+ write_with_writing_model: {
64
+ model_id: modelId,
65
+ ask: { value: (flags, positionals) => positionals.slice(1).join(" ").trim() || undefined },
66
+ max_words: { flag: "words", type: "num" },
67
+ },
68
+ };
69
+
70
+ function call(ctx, tool, extra = {}) {
71
+ return ctx.client.callTool(tool, { ...argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals), ...extra });
72
+ }
73
+
74
+ const STATE = {
75
+ empty: "no writing yet",
76
+ confirming: "links to confirm",
77
+ untrained: "not trained",
78
+ training: "training",
79
+ stopped: "needs you",
80
+ ready: "ready",
81
+ archived: "archived",
82
+ };
83
+
84
+ export async function cmdModels(ctx) {
85
+ const sub = ctx.positionals[0];
86
+ const next = { ...ctx, positionals: ctx.positionals.slice(1) };
87
+ switch (sub) {
88
+ case undefined:
89
+ case "list":
90
+ case "ls": {
91
+ refuseUnknownFlags(next, ARGS.list_writing_models, []);
92
+ const result = await call(next, "list_writing_models");
93
+ printResult(next.io, next.flags, result, (r) => {
94
+ if (!r.models?.length) return 'No models yet. Make one: models create "Our voice"';
95
+ return r.models
96
+ .map(
97
+ (m) =>
98
+ `${m.id} ${m.name.slice(0, 28).padEnd(28)} ${(STATE[m.state] ?? m.state).padEnd(17)} ${m.links.confirmed} links${m.version ? ` v${m.version.n}` : ""}`
99
+ )
100
+ .join("\n");
101
+ });
102
+ return 0;
103
+ }
104
+ case "create": {
105
+ refuseUnknownFlags(next, ARGS.create_writing_model, ["name"]);
106
+ const args = argsFromFlags(ARGS.create_writing_model, next.flags, next.positionals);
107
+ if (!args.name) throw new CliError('Name it: models create "Our voice" [--budget 100]');
108
+ return modelShow(next, await next.client.callTool("create_writing_model", args));
109
+ }
110
+ case "show":
111
+ return modelShow(next, await call(next, "get_writing_model"));
112
+ case "add": {
113
+ refuseUnknownFlags(next, ARGS.add_writing_model_links, []);
114
+ const args = argsFromFlags(ARGS.add_writing_model_links, next.flags, next.positionals);
115
+ if (!args.urls) throw new CliError("Give links: models add <id> <url> [<url>…] [--train]");
116
+ const result = await next.client.callTool("add_writing_model_links", args);
117
+ return trainingShow(next, result, `Added ${result.added} links.`);
118
+ }
119
+ case "find": {
120
+ refuseUnknownFlags(next, ARGS.find_writing_model_links, []);
121
+ const args = argsFromFlags(ARGS.find_writing_model_links, next.flags, next.positionals);
122
+ if (!args.prompt) throw new CliError('Say what to find: models find <id> "every post on our blog"');
123
+ const result = await next.client.callTool("find_writing_model_links", args);
124
+ printResult(next.io, next.flags, result, (r) =>
125
+ [
126
+ `Found ${r.proposed.length} links (of ${r.considered} pages), waiting to be confirmed:`,
127
+ ...r.proposed.map((s) => ` ${s.id} ${s.title ? `${s.title.slice(0, 50)} ` : ""}${s.url}`),
128
+ "",
129
+ `Confirm them all and train: models confirm ${args.model_id}`,
130
+ `Or only some: models confirm ${args.model_id} <link-id> [<link-id>…]`,
131
+ ].join("\n")
132
+ );
133
+ return 0;
134
+ }
135
+ case "confirm": {
136
+ refuseUnknownFlags(next, ARGS.confirm_writing_model_links, []);
137
+ const result = await call(next, "confirm_writing_model_links");
138
+ return trainingShow(
139
+ next,
140
+ result,
141
+ `Confirmed ${result.confirmed} links${result.declined ? `, declined ${result.declined}` : ""}.`
142
+ );
143
+ }
144
+ case "remove": {
145
+ const args = argsFromFlags(ARGS.remove_writing_model_links, next.flags, next.positionals);
146
+ if (!args.source_ids) throw new CliError("Give the link ids: models remove <id> <link-id> [<link-id>…]");
147
+ const result = await next.client.callTool("remove_writing_model_links", args);
148
+ printResult(next.io, next.flags, result, (r) => `Removed ${r.removed} links.`);
149
+ return 0;
150
+ }
151
+ case "train": {
152
+ refuseUnknownFlags(next, ARGS.train_writing_model, []);
153
+ const result = await call(next, "train_writing_model");
154
+ printResult(next.io, next.flags, result, (r) => runLine(r.run));
155
+ return 0;
156
+ }
157
+ case "stop": {
158
+ refuseUnknownFlags(next, ARGS.cancel_writing_model_run, []);
159
+ const result = await call(next, "cancel_writing_model_run");
160
+ printResult(next.io, next.flags, result, (r) => runLine(r.run));
161
+ return 0;
162
+ }
163
+ case "write": {
164
+ refuseUnknownFlags(next, ARGS.write_with_writing_model, []);
165
+ const args = argsFromFlags(ARGS.write_with_writing_model, next.flags, next.positionals);
166
+ if (!args.ask) throw new CliError('Say what to write: models write <id> "Write a 300-word post on…"');
167
+ const result = await next.client.callTool("write_with_writing_model", args);
168
+ printResult(next.io, next.flags, result, (r) => r.text);
169
+ return 0;
170
+ }
171
+ case "update":
172
+ refuseUnknownFlags(next, ARGS.update_writing_model, []);
173
+ return modelShow(next, await call(next, "update_writing_model"));
174
+ case "archive":
175
+ case "restore":
176
+ return modelShow(next, await call(next, "update_writing_model", { archived: sub === "archive" }));
177
+ default:
178
+ throw new CliError(
179
+ "Usage: models [list|create|show|add|find|confirm|remove|train|stop|write|update|archive|restore]"
180
+ );
181
+ }
182
+ }
183
+
184
+ function runLine(run) {
185
+ if (!run) return "Not trained yet.";
186
+ const where = run.blockedOn ? `stopped: ${run.blockedOn}` : (run.stageNote ?? run.stage);
187
+ return `Version ${run.n}: ${run.status}, ${where} ($${run.spentUsd} of up to $${run.budgetUsd})`;
188
+ }
189
+
190
+ function trainingShow(ctx, result, line) {
191
+ printResult(ctx.io, ctx.flags, result, (r) => {
192
+ const t = r.training;
193
+ const after = !t
194
+ ? ""
195
+ : t.started
196
+ ? `\n${runLine(t.run)}\nFollow it: models show ${r.model.id}`
197
+ : `\nNot training: ${t.reason}`;
198
+ return `${line}${after}`;
199
+ });
200
+ return 0;
201
+ }
202
+
203
+ function modelShow(ctx, result) {
204
+ printResult(ctx.io, ctx.flags, result, (r) => {
205
+ const m = r.model;
206
+ const lines = [
207
+ `${m.name} ${m.id}`,
208
+ ` ${STATE[m.state] ?? m.state}, built on ${m.base}, up to $${m.budgetUsd} a training run`,
209
+ ` ${m.links.confirmed} links (${m.links.read} read, ${m.links.words} words)${m.links.proposed ? `, ${m.links.proposed} to confirm` : ""}`,
210
+ ` ${runLine(m.latestRun)}`,
211
+ ];
212
+ const proposed = m.sources.filter((s) => s.status === "proposed");
213
+ if (proposed.length) lines.push("", `Confirm the links found and train: models confirm ${m.id}`);
214
+ else if (!m.links.confirmed)
215
+ lines.push("", `Give it writing: models add ${m.id} <url>… or models find ${m.id} "<where>"`);
216
+ return lines.join("\n");
217
+ });
218
+ return 0;
219
+ }
package/lib/commands.mjs CHANGED
@@ -58,3 +58,4 @@ export * from "./commands/bench.mjs";
58
58
  export * from "./commands/replicate.mjs";
59
59
  export * from "./commands/deploy.mjs";
60
60
  export * from "./commands/refs.mjs";
61
+ export * from "./commands/models.mjs";
package/lib/parked.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // parked. Which are parked is decided on the server (Super Admin → Parked), so `help` asks it
4
4
  // (GET /api/parked, `cli_sections`). This list is only what it falls back to when the server can't
5
5
  // be reached: the state when this version shipped.
6
- export const PARKED_HELP_SECTIONS = ["Seers", "Shredder", "Ledes"];
6
+ export const PARKED_HELP_SECTIONS = ["Seers", "Shredder", "Ledes", "Models"];
7
7
 
8
8
  /** The parked help sections the server names now, or null when it can't say in time. */
9
9
  export async function fetchParkedSections(url, { fetchImpl = globalThis.fetch, timeoutMs = 1500 } = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.95",
3
+ "version": "0.176.97",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",