@letterstory/cli 0.177.9 → 0.177.11

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
@@ -68,6 +68,7 @@ import {
68
68
  cmdAsk,
69
69
  cmdIdeas,
70
70
  cmdPlan,
71
+ cmdMemory,
71
72
  cmdSite,
72
73
  cmdEvals,
73
74
  cmdOfferings,
@@ -87,7 +88,7 @@ import {
87
88
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
88
89
 
89
90
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
90
- export const VERSION = "0.177.9";
91
+ export const VERSION = "0.177.11";
91
92
 
92
93
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
93
94
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -100,6 +101,11 @@ export const BOOLEAN_FLAGS = new Set([
100
101
  "connect-only",
101
102
  // admin demo ideas: every live recipe, not only the Ideas that fit.
102
103
  "library",
104
+ // admin demo sweep: prepare each recipe's org for a person to start, pull requests, make orgs, skip the refit.
105
+ "prepare",
106
+ "pull-requests",
107
+ "make-orgs",
108
+ "no-refit",
103
109
  // admin targets check: check every target again, checked or not.
104
110
  "recheck",
105
111
  // build list: also move the builds' open changes along while polling.
@@ -430,6 +436,8 @@ export const BOOLEAN_FLAGS = new Set([
430
436
  "dry",
431
437
  // admin billing emails --bounced
432
438
  "bounced",
439
+ // `memory ingest --include-website` reads the org's own site too: value-less.
440
+ "include-website",
433
441
  // models add: train once the links are added. models confirm: keep the found links not named, or don't train yet.
434
442
  "train",
435
443
  "keep-rest",
@@ -1204,6 +1212,14 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
1204
1212
  admin demo check <org-id> Is it ready to show? Competitors, targets, answers, the
1205
1213
  Plan's rows, the issues open, no PR open, GitHub, the
1206
1214
  phantom's site. Exits 1 when not ready.
1215
+ admin demo ideas <org-id> [--library] [--preview] [--force] | --status [--id <run>]
1216
+ Start every Idea on a demo org for real (pull requests, or
1217
+ previews), or read how the latest run went.
1218
+ admin demo sweep [--prepare] [--pull-requests] [--recipes a,b] [--orgs a,b] [--exclude a,b] [--buy a,b] [--make-orgs] [--no-refit] | --status [--id <sweep>]
1219
+ Every live recipe end to end: each gets a test org whose Ideas
1220
+ offers it, its Start pressed (or, with --prepare, left for you
1221
+ to press in the app), and its outcome judged, with who owns a
1222
+ failure. Exits 1 when the sweep failed.
1207
1223
  admin brand-detection <org-id> [--term <t>]... [--exact <t>]... [--exclude <phrase>]... [--note "…"] | --clear [--note "…"]
1208
1224
  How a client's brand is detected in AI answers, with
1209
1225
  every earlier setting; with flags, set the exact set
@@ -1793,6 +1809,10 @@ Insights:
1793
1809
  plan later remove <queued-id> Take a queued row back off
1794
1810
  plan strategies [--site host] [--archived] The goals typed on Ideas, each a strategy
1795
1811
  plan strategy add "<words…>" [--site host] Keep a goal in your words as a strategy
1812
+ plan strategy archive <strategy-id> [--restore] Put one away (its card leaves the Plan), or bring it back
1813
+ memory ingest [--text|--text-file <path|->] [--link <url>]… [--include-website] [--mcp-url <url>] Push context to Memory: a reviewable proposal
1814
+ memory apply <ingestion-id> --changes <id>,<id> Save the proposed changes you accept
1815
+ memory add --section <id> --body <text> [--title] [--origin] File one entry in a custom memory section
1796
1816
  plan strategy archive <strategy-id> [--restore] Put one away (its goal leaves Ideas), or bring it back
1797
1817
  insights summary [--period …] [--part <block>]... Measure Overview: AI named/cited rates, share of voice, search, targets
1798
1818
  insights reads [--period …] [--site all|own|phantoms|<key>]... [--path <path>] [--engine <operator>]
@@ -2190,6 +2210,7 @@ const CLIENT_COMMANDS = {
2190
2210
  ask: cmdAsk,
2191
2211
  ideas: cmdIdeas,
2192
2212
  plan: cmdPlan,
2213
+ memory: cmdMemory,
2193
2214
  site: cmdSite,
2194
2215
  evals: cmdEvals,
2195
2216
  visits: cmdVisits,
@@ -6910,7 +6910,7 @@ const DEMO_ITEM_FLAGS = [
6910
6910
  "setup",
6911
6911
  ];
6912
6912
  const DEMO_USAGE =
6913
- "Usage: admin demo guide | list [--enrollable] | show <org-id> [--moves] | set <org-id> [--enroll] [--unenroll] [--file <state.json>] [--phantom parked|shown] [--main-blog …] [--visitors …] [--probes not_run|shown] [--cms hidden|shown] [--publish-target …] [--search-console …] [--github …] [--domain …] [--setup fresh|finished] [--domain-cap n] [--inject <key>]... [--hide <key>]... [--save-as <name>] [--baseline <owner/repo>[@sha] | --clear-baseline] [--copy-from <org-id> [--copy-scenarios a,b] [--overwrite]] [--auto-reset <scenario>|off] [--every <minutes>] [--release] | domains | new <domain> [--name <n>] [--brief <t>] [--full] [--host vercel|pages] [--no-wait] | teardown <org-id> [--keep-repo] | bootstrap <org-id>... --from <org-id> [--scenarios a,b] [--overwrite] | reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none] [--hold <minutes>] [--restore-repo revert|force] [--rebuild] [--reread-company] [--forget-company] [--forget-site] | check <org-id> | ideas <org-id> [--library] [--preview] [--force] | ideas <org-id> --status [--id <run-id>]";
6913
+ "Usage: admin demo guide | list [--enrollable] | show <org-id> [--moves] | set <org-id> [--enroll] [--unenroll] [--file <state.json>] [--phantom parked|shown] [--main-blog …] [--visitors …] [--probes not_run|shown] [--cms hidden|shown] [--publish-target …] [--search-console …] [--github …] [--domain …] [--setup fresh|finished] [--domain-cap n] [--inject <key>]... [--hide <key>]... [--save-as <name>] [--baseline <owner/repo>[@sha] | --clear-baseline] [--copy-from <org-id> [--copy-scenarios a,b] [--overwrite]] [--auto-reset <scenario>|off] [--every <minutes>] [--release] | domains | new <domain> [--name <n>] [--brief <t>] [--full] [--host vercel|pages] [--no-wait] | teardown <org-id> [--keep-repo] | bootstrap <org-id>... --from <org-id> [--scenarios a,b] [--overwrite] | reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none] [--hold <minutes>] [--restore-repo revert|force] [--rebuild] [--reread-company] [--forget-company] [--forget-site] | check <org-id> | ideas <org-id> [--library] [--preview] [--force] | ideas <org-id> --status [--id <run-id>] | sweep [--prepare] [--pull-requests] [--recipes <id>,…] [--orgs <id>,…] [--exclude <id>,…] [--buy <recipe-id>,…] [--make-orgs] [--no-refit] | sweep --status [--id <sweep-id>]";
6914
6914
 
6915
6915
  const readinessLines = (r) =>
6916
6916
  r
@@ -6936,6 +6936,49 @@ const ideasRunLines = (run) =>
6936
6936
  ].join("\n")
6937
6937
  : "No run yet.";
6938
6938
 
6939
+ const SWEEP_MARK = {
6940
+ ready: "READY",
6941
+ passed: "PASS",
6942
+ failed: "FAIL",
6943
+ waiting: "WAIT",
6944
+ held: "HELD",
6945
+ no_org: "NO ORG",
6946
+ not_derivable: "NO PROFILE",
6947
+ pending: "…",
6948
+ };
6949
+
6950
+ /** A recipe sweep's report (get_recipe_sweep), one block per recipe. */
6951
+ const recipeSweepLines = (sweep, appUrl) =>
6952
+ sweep
6953
+ ? [
6954
+ `${sweep.summary} (${sweep.state}${sweep.note && sweep.state !== "done" ? `: ${sweep.note}` : ""})`,
6955
+ `${sweep.start ? "started by the sweep" : "prepared for you to start"}, ${sweep.delivery === "build" ? "previews" : "pull requests"}, by ${sweep.started_by_name ?? "staff"} at ${sweep.created_at} sweep ${sweep.id}`,
6956
+ ...sweep.items.map((i) =>
6957
+ [
6958
+ `\n${(SWEEP_MARK[i.result] ?? i.result).padEnd(10)} ${i.name}`,
6959
+ i.org ? ` org: ${i.org.name} (${i.org.host})${i.org.made ? ", made for it" : ""}` : null,
6960
+ i.fit[0] ? ` fit: ${i.fit[0].fits}: ${i.fit[0].why}` : null,
6961
+ i.row
6962
+ ? ` row: ${i.row.title}${i.row.line ? ` / ${i.row.line}` : ""}${i.row.button ? ` [${i.row.button}]` : ""}`
6963
+ : null,
6964
+ i.did
6965
+ ? ` did: ${i.did.tools.join(", ") || "no tools"}${i.did.sandbox ? ", a sandbox" : ""}${i.did.cost_usd != null ? `, $${i.did.cost_usd}` : ""}`
6966
+ : null,
6967
+ i.verdict ? ` outcome: ${i.verdict.state}: ${i.verdict.observed}` : null,
6968
+ i.cause ? ` cause: ${i.cause.owner}: ${i.cause.why}` : null,
6969
+ i.profile_problem ? ` profile: ${i.profile_problem}` : null,
6970
+ i.profile?.unsettable?.length
6971
+ ? ` can't be made on a test org: ${i.profile.unsettable.map((u) => u.key).join(", ")}`
6972
+ : null,
6973
+ i.links?.ideas ? ` open: ${appUrl}${i.links.ideas}` : null,
6974
+ i.links?.build ? ` build: ${appUrl}${i.links.build}` : null,
6975
+ ]
6976
+ .filter(Boolean)
6977
+ .join("\n")
6978
+ ),
6979
+ ].join("\n")
6980
+ : "No sweep yet.";
6981
+
6939
6982
  async function cmdAdminDemo(ctx) {
6940
6983
  const { flags, positionals, io } = ctx;
6941
6984
  const sub = positionals[0];
@@ -7194,6 +7237,30 @@ async function cmdAdminDemo(ctx) {
7194
7237
  printResult(io, flags, result, (r) => ideasRunLines(r.run));
7195
7238
  return result.run?.state === "failed" ? 1 : 0;
7196
7239
  }
7240
+ case "sweep": {
7241
+ // Every live recipe end to end on test orgs (run_recipe_sweep), or the latest sweep's report (get_recipe_sweep).
7242
+ const path = `${DEMO}/sweeps`;
7243
+ const id = flagStr(flags.id);
7244
+ const result =
7245
+ flagBool(flags.status) || id
7246
+ ? await staffRequest(ctx, "GET", id ? `${path}?id=${encodeURIComponent(id)}` : path)
7247
+ : await staffPost(
7248
+ ctx,
7249
+ path,
7250
+ compact({
7251
+ start: flagBool(flags.prepare) ? false : undefined,
7252
+ delivery: flagBool(flags["pull-requests"]) ? "build_pr" : undefined,
7253
+ recipes: flags.recipes ? flagList(flags.recipes) : undefined,
7254
+ orgs: flags.orgs ? flagList(flags.orgs) : undefined,
7255
+ exclude: flags.exclude ? flagList(flags.exclude) : undefined,
7256
+ purchases: flags.buy ? flagList(flags.buy) : undefined,
7257
+ make_orgs: flagBool(flags["make-orgs"]) || undefined,
7258
+ refit: flagBool(flags["no-refit"]) ? false : undefined,
7259
+ })
7260
+ );
7261
+ printResult(io, flags, result, (r) => recipeSweepLines(r.sweep, ctx.client.url));
7262
+ return result.sweep?.state === "failed" ? 1 : 0;
7263
+ }
7197
7264
  default:
7198
7265
  throw new CliError(DEMO_USAGE);
7199
7266
  }
@@ -0,0 +1,88 @@
1
+ // `memory` — push context into Memory from a terminal, the way an agent does it over the MCP: thin wrappers over
2
+ // the same tools, nothing of their own.
3
+ // memory ingest [--text <t>|--text-file <path|->] [--link <url>]… [--include-website] [--mcp-url <url> [--mcp-token <t>] [--mcp-focus <words>]]
4
+ // → ingest_context (a reviewable proposal; nothing is saved yet)
5
+ // memory apply <ingestion-id> --changes <change-id>[,<change-id>…] → apply_context (saves the accepted changes)
6
+ // memory add --section <id> (--body <t>|--body-file <path|->) [--title <t>] [--origin <where it was read>]
7
+ // → add_memory_entry (one entry in a custom section)
8
+
9
+ import { CliError } from "../client.mjs";
10
+ import { argsFromFlags, flagCsv, flagStr, printResult, readTextInput, refuseUnknownFlags } from "./shared.mjs";
11
+
12
+ export const ARGS = {
13
+ ingest_context: {
14
+ text: { value: (flags) => readTextInput(flags, "text", "text-file") },
15
+ links: { flag: "link", type: "list" },
16
+ include_website: { flag: "include-website", type: "bool" },
17
+ mcp_servers: {
18
+ value: (flags) => {
19
+ const url = flagStr(flags["mcp-url"]);
20
+ const token = flagStr(flags["mcp-token"]);
21
+ const focus = flagStr(flags["mcp-focus"]);
22
+ if (!url) return undefined;
23
+ return [{ url, ...(token ? { authorization_token: token } : {}), ...(focus ? { focus } : {}) }];
24
+ },
25
+ },
26
+ },
27
+ apply_context: {
28
+ ingestion_id: { positional: 0, required: true, label: "ingestion-id" },
29
+ accepted_change_ids: { value: (flags) => flagCsv(flags.changes) },
30
+ },
31
+ add_memory_entry: {
32
+ section_id: { flag: "section", required: true },
33
+ body: { value: (flags) => readTextInput(flags, "body", "body-file") },
34
+ title: "title",
35
+ origin: "origin",
36
+ },
37
+ };
38
+
39
+ export async function cmdMemory(ctx) {
40
+ const { client, flags, io } = ctx;
41
+ const sub = ctx.positionals[0];
42
+ const rest = ctx.positionals.slice(1);
43
+ if (sub === "ingest") {
44
+ refuseUnknownFlags(ctx, ARGS.ingest_context, ["text-file", "mcp-url", "mcp-token", "mcp-focus"]);
45
+ const args = argsFromFlags(ARGS.ingest_context, flags, rest);
46
+ if (!args.text && !args.links?.length && !args.include_website && !args.mcp_servers) {
47
+ throw new CliError(
48
+ "Give it something to read: --text, --text-file, --link, --include-website or --mcp-url."
49
+ );
50
+ }
51
+ const result = await client.callTool("ingest_context", args);
52
+ printResult(io, flags, result, (r) =>
53
+ [
54
+ `Pass ${r.ingestion_id}: ${r.summary}`,
55
+ `${r.changes.length} proposed change${r.changes.length === 1 ? "" : "s"}. Nothing is saved yet.`,
56
+ ...r.changes.map((c) => ` ${c.id} [${c.area}] ${c.label}`),
57
+ r.changes.length ? `Save the ones you accept: memory apply ${r.ingestion_id} --changes <id>,<id>` : "",
58
+ ]
59
+ .filter(Boolean)
60
+ .join("\n")
61
+ );
62
+ return 0;
63
+ }
64
+ if (sub === "apply") {
65
+ refuseUnknownFlags(ctx, ARGS.apply_context, ["changes"]);
66
+ const args = argsFromFlags(ARGS.apply_context, flags, rest);
67
+ if (!args.accepted_change_ids?.length) throw new CliError("Say which changes to save: --changes <id>[,<id>…].");
68
+ const result = await client.callTool("apply_context", args);
69
+ printResult(
70
+ io,
71
+ flags,
72
+ result,
73
+ (r) => `Saved ${r.applied_change_ids.length} change${r.applied_change_ids.length === 1 ? "" : "s"}.`
74
+ );
75
+ return 0;
76
+ }
77
+ if (sub === "add") {
78
+ refuseUnknownFlags(ctx, ARGS.add_memory_entry, ["body-file"]);
79
+ const args = argsFromFlags(ARGS.add_memory_entry, flags, rest);
80
+ if (!args.body) throw new CliError("Say what to remember: --body <text> or --body-file <path|->.");
81
+ const result = await client.callTool("add_memory_entry", args);
82
+ printResult(io, flags, result, (r) => `Filed${r.entry?.id ? `: ${r.entry.id}` : ""}.`);
83
+ return 0;
84
+ }
85
+ throw new CliError(
86
+ "Usage: memory ingest [--text|--text-file|--link|--include-website|--mcp-url] | memory apply <ingestion-id> --changes <ids> | memory add --section <id> --body <text>"
87
+ );
88
+ }
package/lib/commands.mjs CHANGED
@@ -48,6 +48,7 @@ export * from "./commands/repo.mjs";
48
48
  export * from "./commands/build.mjs";
49
49
  export * from "./commands/ask.mjs";
50
50
  export * from "./commands/plan.mjs";
51
+ export * from "./commands/memory.mjs";
51
52
  export * from "./commands/site.mjs";
52
53
  export * from "./commands/evals.mjs";
53
54
  export * from "./commands/visits.mjs";
package/lib/documents.mjs CHANGED
@@ -59,6 +59,8 @@ export const DOCUMENT_FLAG_PAIRS = [
59
59
  ["body", "file"],
60
60
  ["brief", "brief-file"],
61
61
  ["text", "text-file"],
62
+ // `memory add --body-file`: one entry's prose.
63
+ ["body", "body-file"],
62
64
  // An offering's (or the company's) manifesto is prose — a PDF or a .docx is fair game.
63
65
  ["manifesto", "manifesto-file"],
64
66
  // A repo change's brief (repo.mjs `repo start`) — prose, so a .docx or PDF spec is fine.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.177.9",
3
+ "version": "0.177.11",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",