@letterstory/cli 0.176.44 → 0.176.46

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
@@ -84,7 +84,7 @@ import {
84
84
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
85
85
 
86
86
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
87
- export const VERSION = "0.176.44";
87
+ export const VERSION = "0.176.46";
88
88
 
89
89
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
90
90
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -1853,6 +1853,7 @@ Visits (Visitors: AI agents reading your site, and AI answers sending people to
1853
1853
  Pages one agent read that the other didn't
1854
1854
  visits export [--since 30d] [--format json|csv] [--table agent_visits|citations]
1855
1855
  visits sites Your Visitors sites and their install checks
1856
+ visits domain <domain> [--site <id>] [--name <n>] Move a site to a new domain, keeping its key
1856
1857
  visits pages [--since 28d] [--path '/docs/*'] [--agent <id>] [--limit <n>] [--site <id>]
1857
1858
  People and AI agents per page
1858
1859
  visits health --site <id> Staff: how far to trust a site's numbers, each check and its fix
@@ -5,7 +5,7 @@
5
5
  // Same flag/positional/io conventions as posts.mjs — no commander, no color.
6
6
 
7
7
  import { CliError } from "../client.mjs";
8
- import { printResult, ok, argsFromFlags, flagList, refuseUnknownFlags } from "./shared.mjs";
8
+ import { printResult, ok, argsFromFlags, flagList, flagStr, refuseUnknownFlags } from "./shared.mjs";
9
9
 
10
10
  const TRIGGERS = ["competitor_page", "visitor_identified", "issue_found", "build_suggestion", "plan_update", "custom"];
11
11
  const TEMPLATES = [
@@ -74,6 +74,13 @@ function factsOf(flags) {
74
74
  }
75
75
 
76
76
  /** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
77
+ const destination = {
78
+ value: (flags) => {
79
+ const v = flagStr(flags.destination);
80
+ return v === undefined ? undefined : v === "none" ? null : { collection_id: v === "waiting" ? null : v };
81
+ },
82
+ };
83
+
77
84
  export const ARGS = {
78
85
  describe_automation_options: {},
79
86
  list_event_automations: {},
@@ -81,6 +88,7 @@ export const ARGS = {
81
88
  automation_id: { positional: 0, label: "automation-id", required: true },
82
89
  },
83
90
  create_event_automation: {
91
+ destination,
84
92
  template: { flag: "template", enum: TEMPLATES },
85
93
  name: { flag: "name" },
86
94
  trigger: { flag: "trigger", enum: TRIGGERS },
@@ -92,6 +100,8 @@ export const ARGS = {
92
100
  backfill,
93
101
  },
94
102
  update_event_automation: {
103
+ destination,
104
+ expected_updated_at: "expected-updated-at",
95
105
  automation_id: { positional: 0, label: "automation-id", required: true },
96
106
  name: { flag: "name" },
97
107
  trigger_params: triggerParams,
@@ -86,6 +86,8 @@ export const ARGS = {
86
86
  },
87
87
  },
88
88
  update_automation: {
89
+ expected_updated_at: "expected-updated-at",
90
+ expected_source: { value: () => undefined },
89
91
  collection_id: { flag: "collection", required: true },
90
92
  automation_id: { positional: 0, label: "automation-id", required: true },
91
93
  pace_per_week: { flag: "pace", type: "num" },
@@ -116,6 +118,7 @@ export const ARGS = {
116
118
  reason: { flag: "reason" },
117
119
  },
118
120
  delete_automation: {
121
+ expected_source: { value: () => undefined },
119
122
  collection_id: { flag: "collection", required: true },
120
123
  automation_id: { positional: 0, label: "automation-id", required: true },
121
124
  },
@@ -163,7 +166,15 @@ export async function cmdAutomations(ctx) {
163
166
 
164
167
  async function run(ctx, tool, said) {
165
168
  const { client, flags, io } = ctx;
166
- const result = await client.callTool(tool, argsFromFlags(ARGS[tool], flags, ctx.positionals));
169
+ const args = argsFromFlags(ARGS[tool], flags, ctx.positionals);
170
+ if ((tool === "update_automation" || tool === "delete_automation") && args.automation_id?.startsWith("derived:")) {
171
+ const { automation: a } = await client.callTool("get_automation", {
172
+ collection_id: args.collection_id,
173
+ automation_id: args.automation_id,
174
+ });
175
+ args.expected_source = { kind: a.kind, cluster_id: a.cluster_id, target_id: a.target_id, seer_id: a.seer_id };
176
+ }
177
+ const result = await client.callTool(tool, args);
167
178
  if (said) ok(ctx, said);
168
179
  printResult(io, flags, result);
169
180
  return 0;
@@ -21,6 +21,16 @@ import { attachmentUrls, uploadAttachments } from "./repo.mjs";
21
21
 
22
22
  const ARGS = {
23
23
  start_build: {
24
+ purpose: {
25
+ value: (flags) => {
26
+ if (typeof flags.purpose !== "string") return undefined;
27
+ try {
28
+ return JSON.parse(flags.purpose);
29
+ } catch {
30
+ throw new CliError("--purpose takes JSON.");
31
+ }
32
+ },
33
+ },
24
34
  // The words after `build`, or --prompt for a longer ask.
25
35
  prompt: { value: (flags, positionals) => flagStr(flags.prompt) ?? (positionals.join(" ").trim() || undefined) },
26
36
  domain: "site",
@@ -59,6 +69,16 @@ const ARGS = {
59
69
  },
60
70
  },
61
71
  send_build_message: {
72
+ work_context: {
73
+ value: (flags) => {
74
+ if (typeof flags["work-context"] !== "string") return undefined;
75
+ try {
76
+ return JSON.parse(flags["work-context"]);
77
+ } catch {
78
+ throw new CliError("--work-context takes JSON.");
79
+ }
80
+ },
81
+ },
62
82
  message: {
63
83
  value: (flags, positionals) => flagStr(flags.message) ?? (positionals.join(" ").trim() || undefined),
64
84
  },
@@ -17,6 +17,17 @@
17
17
  import { CliError } from "../client.mjs";
18
18
  import { argsFromFlags, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
19
19
 
20
+ const jsonFlag = (name) => ({
21
+ value: (flags) => {
22
+ const v = flagStr(flags[name]);
23
+ if (v === undefined) return undefined;
24
+ try {
25
+ return JSON.parse(v);
26
+ } catch {
27
+ throw new CliError("--" + name + " takes JSON.");
28
+ }
29
+ },
30
+ });
20
31
  const onOff = (name) => (flags) => {
21
32
  const v = flagStr(flags[name]);
22
33
  if (v === undefined) return undefined;
@@ -26,6 +37,19 @@ const onOff = (name) => (flags) => {
26
37
  };
27
38
 
28
39
  export const ARGS = {
40
+ list_plan_work_purposes: { resource: jsonFlag("resource"), strategy_id: "strategy" },
41
+ set_plan_work_purpose: {
42
+ resource: jsonFlag("resource"),
43
+ purpose: jsonFlag("purpose"),
44
+ attached: { value: (flags) => !flags.remove },
45
+ },
46
+ get_plan_work: {
47
+ purpose: jsonFlag("purpose"),
48
+ section: { flag: "section", enum: ["writing", "watching"] },
49
+ resource_id: "resource",
50
+ offset: { flag: "offset", type: "num" },
51
+ limit: { flag: "limit", type: "num" },
52
+ },
29
53
  get_auto_plan: { domain: "site" },
30
54
  set_auto_plan: {
31
55
  domain: "site",
@@ -99,6 +123,38 @@ export async function cmdPlan(ctx) {
99
123
  const { client, flags, io } = ctx;
100
124
  const sub = ctx.positionals[0];
101
125
  const rest = ctx.positionals.slice(1);
126
+ if (sub === "purposes" || sub === "purpose") {
127
+ const tool = sub === "purposes" ? "list_plan_work_purposes" : "set_plan_work_purpose";
128
+ refuseUnknownFlags(ctx, ARGS[tool]);
129
+ const r = await client.callTool(tool, argsFromFlags(ARGS[tool], flags, rest));
130
+ printResult(
131
+ io,
132
+ flags,
133
+ r,
134
+ (v) =>
135
+ v.purposes.map((p) => p.title + " · " + p.resource.type + " " + p.resource.id).join("\n") ||
136
+ "No goal associations."
137
+ );
138
+ return 0;
139
+ }
140
+ if (sub === "work") {
141
+ refuseUnknownFlags(ctx, ARGS.get_plan_work);
142
+ const result = await client.callTool("get_plan_work", argsFromFlags(ARGS.get_plan_work, flags, rest));
143
+ printResult(
144
+ io,
145
+ flags,
146
+ result,
147
+ (r) =>
148
+ [
149
+ ...r.items.map(
150
+ (w) => w.title + " · " + w.status_label + "\n " + (w.attention || w.summary) + "\n " + w.href
151
+ ),
152
+ ...r.unavailable.map((u) => u.reason),
153
+ ...(r.next_offset !== null ? ["More: plan work --offset " + r.next_offset] : []),
154
+ ].join("\n") || "No ongoing work."
155
+ );
156
+ return 0;
157
+ }
102
158
  if (sub === "auto") {
103
159
  refuseUnknownFlags(ctx, {}, ["site", ...SETTING_FLAGS]);
104
160
  if (rest[0] && !["activate", "disable"].includes(rest[0]))
@@ -210,6 +266,6 @@ export async function cmdPlan(ctx) {
210
266
  throw new CliError('Usage: plan strategy add "<words…>" | plan strategy archive <strategy-id> [--restore]');
211
267
  }
212
268
  throw new CliError(
213
- "Usage: plan auto [settings…] | plan pending | plan veto <proposal-id> | plan start <proposal-id> | plan preview | plan later <action-id> | plan later remove <id> | plan strategies | plan strategy add|archive"
269
+ "Usage: plan work [--section writing|watching] | plan auto [settings…] | plan pending | plan veto <proposal-id> | plan start <proposal-id> | plan preview | plan later <action-id> | plan later remove <id> | plan strategies | plan strategy add|archive"
214
270
  );
215
271
  }
@@ -11,6 +11,7 @@
11
11
  // letterstory visits people --since 30d people who read the site, and how many were identified
12
12
  // letterstory visits export --since 30d --format csv
13
13
  // letterstory visits sites
14
+ // letterstory visits domain composal.com [--site <id>] move the site to a new domain, keeping its key
14
15
  // letterstory visits pages --since 28d [--path '/docs/*'] [--agent openai-crawler]
15
16
  // letterstory visits health --site <id> staff: how far to trust a site's numbers, and what to fix
16
17
  // letterstory visits connect vercel|cloudflare send the host's request logs (no code to deploy)
@@ -26,6 +27,11 @@ const days = { value: (flags) => sinceDays(flags.since) };
26
27
 
27
28
  export const ARGS = {
28
29
  visitors_list_sites: {},
30
+ visitors_update_site: {
31
+ site_id: "site",
32
+ domain: { positional: 0, label: "new domain (e.g. acme.com)", required: true },
33
+ name: "name",
34
+ },
29
35
  visitors_get_agent_visits: {
30
36
  site_id: "site",
31
37
  days,
@@ -99,6 +105,8 @@ export async function cmdVisits(ctx) {
99
105
  return visitsExport(rest);
100
106
  case "sites":
101
107
  return visitsSites(rest);
108
+ case "domain":
109
+ return visitsDomain(rest);
102
110
  case "pages":
103
111
  return visitsPages(rest);
104
112
  case "health":
@@ -111,7 +119,7 @@ export async function cmdVisits(ctx) {
111
119
  return visitsDisconnect(rest);
112
120
  default:
113
121
  throw new CliError(
114
- "Usage: visits [list|citations|people|compare|export|sites|pages|health|connect|connectors|disconnect]"
122
+ "Usage: visits [list|citations|people|compare|export|sites|domain|pages|health|connect|connectors|disconnect]"
115
123
  );
116
124
  }
117
125
  }
@@ -359,6 +367,25 @@ async function visitsPages(ctx) {
359
367
  return 0;
360
368
  }
361
369
 
370
+ async function visitsDomain(ctx) {
371
+ const { flags, io } = ctx;
372
+ const result = await call(ctx, "visitors_update_site");
373
+ printResult(io, flags, result, (r) => {
374
+ const domain = r.site?.domain ?? "";
375
+ if (!r.changed) return `Already on ${domain}.`;
376
+ const check = r.verification;
377
+ return [
378
+ `Moved ${r.previous_domain ?? "the site"} to ${domain}; the key stays ${r.site?.key ?? ""}.`,
379
+ check
380
+ ? check.found
381
+ ? `Proven: the script was found on ${check.checked_url ?? domain}.`
382
+ : `Not proven yet: ${check.reason}`
383
+ : "Same site: its proof carried over.",
384
+ ].join("\n");
385
+ });
386
+ return 0;
387
+ }
388
+
362
389
  async function visitsSites(ctx) {
363
390
  const { flags, io } = ctx;
364
391
  const result = await call(ctx, "visitors_list_sites");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.44",
3
+ "version": "0.176.46",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",