@letterstory/cli 0.121.4 → 0.121.6
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/README.md +11 -0
- package/lib/cli.mjs +42 -2
- package/lib/commands/automations.mjs +68 -0
- package/lib/commands/autopen.mjs +3 -0
- package/lib/commands/posts.mjs +203 -3
- package/lib/commands/reviewers.mjs +51 -2
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,6 +146,17 @@ letterstory posts list [--limit <n>] [--collection <uuid>]
|
|
|
146
146
|
letterstory posts show <article-id>
|
|
147
147
|
letterstory posts new --title "…" --collection <uuid> --body "…" [--article-id <uuid>] # or --file <path|->; --article-id updates that article instead of creating one
|
|
148
148
|
letterstory posts new --title "…" --collection <uuid> --body "…" --context "Internal data from 120 accounts: disputes fell 38%." # your own data: stated in the post with your attribution
|
|
149
|
+
letterstory posts update <article-id> --title "…" --summary "…" --tag seo --tag ai # only what you pass changes; --body/--file for the HTML body
|
|
150
|
+
letterstory posts edit <article-id> --instruction "Tighten the intro" # AI rewrite, saved as your edit
|
|
151
|
+
letterstory posts mentions <article-id> # re-apply the brand-mention & competitor policy
|
|
152
|
+
letterstory posts history <article-id>
|
|
153
|
+
letterstory posts restore <article-id> <version> # comes back as the newest version
|
|
154
|
+
letterstory posts export <article-id> --format pdf --out post.pdf # or --format md (prints the Markdown)
|
|
155
|
+
letterstory posts comments <article-id>
|
|
156
|
+
letterstory posts comments add <article-id> --body "Source?" --quote "grew 40%"
|
|
157
|
+
letterstory posts comments update <article-id> <comment-id> --resolve # or --reopen, --body "…"
|
|
158
|
+
letterstory posts comments resolve-ai <article-id> <comment-id> # AI makes the change the comment asks for
|
|
159
|
+
letterstory reviewers invite <article-id> --email editor@client.com --name "Sam" # a guest reviewer: link + PIN by email
|
|
149
160
|
letterstory posts publish <article-id>
|
|
150
161
|
letterstory posts unpublish <article-id>
|
|
151
162
|
letterstory posts delete --id <uuid> [--id <uuid> ...] # permanent; content, history, and any published copy are all removed
|
package/lib/cli.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
cmdPosts,
|
|
20
20
|
cmdPublished,
|
|
21
21
|
cmdReviewers,
|
|
22
|
+
cmdAutomations,
|
|
22
23
|
cmdCollections,
|
|
23
24
|
cmdAuthors,
|
|
24
25
|
cmdFlows,
|
|
@@ -60,7 +61,7 @@ import {
|
|
|
60
61
|
} from "./commands.mjs";
|
|
61
62
|
|
|
62
63
|
// Keep in sync with cli/package.json.
|
|
63
|
-
export const VERSION = "0.121.
|
|
64
|
+
export const VERSION = "0.121.6";
|
|
64
65
|
|
|
65
66
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
66
67
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -136,6 +137,9 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
136
137
|
"stock-only",
|
|
137
138
|
"gate",
|
|
138
139
|
"no-verify",
|
|
140
|
+
// `posts comments update <article-id> <comment-id> --resolve | --reopen`.
|
|
141
|
+
"resolve",
|
|
142
|
+
"reopen",
|
|
139
143
|
"skip",
|
|
140
144
|
// `phantom-onboard domain` / `admin create-org --onboard` switches.
|
|
141
145
|
"watch",
|
|
@@ -185,6 +189,11 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
185
189
|
"retired",
|
|
186
190
|
"retire",
|
|
187
191
|
"restore",
|
|
192
|
+
// `posts list --mine`: only your posts.
|
|
193
|
+
"mine",
|
|
194
|
+
// `automations set --pause | --resume`.
|
|
195
|
+
"pause",
|
|
196
|
+
"resume",
|
|
188
197
|
// `collection add "<headline>" --collection <id> --queue`.
|
|
189
198
|
"queue",
|
|
190
199
|
// Bool flags from command ARGS maps that were missing here.
|
|
@@ -376,9 +385,29 @@ Phantomstory (a collection Letterstory hosts as an independent publication):
|
|
|
376
385
|
phantom delete <phantom-id> --yes
|
|
377
386
|
|
|
378
387
|
Content:
|
|
379
|
-
posts list [--limit <n>] [--collection <uuid>]
|
|
388
|
+
posts list [--limit <n>] [--offset <n>] [--collection <uuid>] [--search <words>] [--mine]
|
|
389
|
+
List your articles
|
|
380
390
|
posts show <article-id> Show one article
|
|
381
391
|
posts new --title <t> --collection <uuid> (--body <text> | --file <path|->) [--article-id <uuid>] [--context <text>]
|
|
392
|
+
posts update <article-id> [--title <t>] [--body <html> | --file <path|->] [--status draft|published|archived]
|
|
393
|
+
[--summary <dek>] [--tag <t> ...] [--owner <user-id> ...] [--due <YYYY-MM-DD>] [--author <name>]
|
|
394
|
+
[--author-id <id>] [--slug <s>] [--meta-title <t>] [--meta-description <d>] [--published-url <url>]
|
|
395
|
+
[--template <key|none|inherit>] [--mention off|subtle|woven|inherit] [--direction-gate on|off|inherit]
|
|
396
|
+
[--paper-trail '<json>'] [--metadata-json '<json>']
|
|
397
|
+
Edit a post; only what you pass changes
|
|
398
|
+
posts edit <article-id> --instruction "<what to change>"
|
|
399
|
+
Rewrite the post with AI from an instruction
|
|
400
|
+
posts mentions <article-id> Re-apply the brand-mention & competitor policy
|
|
401
|
+
posts history <article-id> The post's versions, newest first
|
|
402
|
+
posts restore <article-id> <version> Bring a version back as the latest
|
|
403
|
+
posts export <article-id> --format md|pdf [--out <path>]
|
|
404
|
+
The draft as Markdown or PDF
|
|
405
|
+
posts comments <article-id> The post's comments and replies
|
|
406
|
+
posts comments add <article-id> --body <text> [--quote <text> [--prefix <t>] [--suffix <t>]]
|
|
407
|
+
[--kind note|ai_esque] [--scope sentence|document] [--reply-to <comment-id>] [--mention <user-id> ...]
|
|
408
|
+
posts comments update <article-id> <comment-id> [--body <text>] [--resolve | --reopen] [--mention <user-id> ...]
|
|
409
|
+
posts comments delete <article-id> <comment-id>
|
|
410
|
+
posts comments resolve-ai <article-id> <comment-id> Make the change the comment asks for, with AI
|
|
382
411
|
posts publish <article-id> Publish an article
|
|
383
412
|
posts unpublish <article-id> Unpublish an article
|
|
384
413
|
posts delete --id <uuid> [--id <uuid> ...] Permanently delete article(s)
|
|
@@ -391,6 +420,16 @@ Reviewers:
|
|
|
391
420
|
guest reviewer on every future
|
|
392
421
|
finished draft (no email sent now)
|
|
393
422
|
reviewers standing remove <reviewer-id> Stop future auto-invites
|
|
423
|
+
reviewers assigned Posts waiting on your review, across letterbases
|
|
424
|
+
automations list --collection <id> What a letterbase writes, how often, and why
|
|
425
|
+
automations show <automation-id> --collection <id> What it covers, its queued posts, its next ideas
|
|
426
|
+
automations add topic|series|signal|best --collection <id> [--topic <id>] [--goal <target-id>]
|
|
427
|
+
[--seer <id>] [--pace <n>] Add one (refused if it says the same as another)
|
|
428
|
+
automations set <automation-id> --collection <id> [--pace <n>] [--pause | --resume] [--goal <id>]
|
|
429
|
+
automations remove <automation-id> --collection <id>
|
|
430
|
+
reviewers invite <article-id> --email <e> [--name <n>]
|
|
431
|
+
Invite a guest to review one post
|
|
432
|
+
(emails them a link and a PIN)
|
|
394
433
|
|
|
395
434
|
Collections:
|
|
396
435
|
collections list List your collections
|
|
@@ -1343,6 +1382,7 @@ const CLIENT_COMMANDS = {
|
|
|
1343
1382
|
posts: cmdPosts,
|
|
1344
1383
|
published: cmdPublished,
|
|
1345
1384
|
reviewers: cmdReviewers,
|
|
1385
|
+
automations: cmdAutomations,
|
|
1346
1386
|
collections: cmdCollections,
|
|
1347
1387
|
authors: cmdAuthors,
|
|
1348
1388
|
flows: cmdFlows,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// `automations` — a letterbase's automations (docs/architecture/automations.md): what it writes,
|
|
2
|
+
// how often, and why. list / show / add / set / remove, through the same tools the app uses.
|
|
3
|
+
// Same flag/positional/io conventions as posts.mjs — no commander, no color.
|
|
4
|
+
|
|
5
|
+
import { CliError } from "../client.mjs";
|
|
6
|
+
import { printResult, ok, argsFromFlags, refuseUnknownFlags } from "./shared.mjs";
|
|
7
|
+
|
|
8
|
+
/** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
|
|
9
|
+
export const ARGS = {
|
|
10
|
+
list_automations: {
|
|
11
|
+
collection_id: { flag: "collection", required: true },
|
|
12
|
+
},
|
|
13
|
+
get_automation: {
|
|
14
|
+
collection_id: { flag: "collection", required: true },
|
|
15
|
+
automation_id: { positional: 0, label: "automation-id", required: true },
|
|
16
|
+
},
|
|
17
|
+
create_automation: {
|
|
18
|
+
collection_id: { flag: "collection", required: true },
|
|
19
|
+
kind: { positional: 0, label: "kind", required: true, enum: ["topic", "series", "signal", "best"] },
|
|
20
|
+
cluster_id: { flag: "topic" },
|
|
21
|
+
target_id: { flag: "goal" },
|
|
22
|
+
seer_id: { flag: "seer" },
|
|
23
|
+
pace_per_week: { flag: "pace", type: "num" },
|
|
24
|
+
},
|
|
25
|
+
update_automation: {
|
|
26
|
+
collection_id: { flag: "collection", required: true },
|
|
27
|
+
automation_id: { positional: 0, label: "automation-id", required: true },
|
|
28
|
+
pace_per_week: { flag: "pace", type: "num" },
|
|
29
|
+
paused: {
|
|
30
|
+
value: (flags) => (flags.pause === true ? true : flags.resume === true ? false : undefined),
|
|
31
|
+
},
|
|
32
|
+
target_id: { flag: "goal" },
|
|
33
|
+
},
|
|
34
|
+
delete_automation: {
|
|
35
|
+
collection_id: { flag: "collection", required: true },
|
|
36
|
+
automation_id: { positional: 0, label: "automation-id", required: true },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export async function cmdAutomations(ctx) {
|
|
41
|
+
const sub = ctx.positionals[0];
|
|
42
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
43
|
+
switch (sub) {
|
|
44
|
+
case "list":
|
|
45
|
+
case "ls":
|
|
46
|
+
return run(rest, "list_automations");
|
|
47
|
+
case "show":
|
|
48
|
+
return run(rest, "get_automation");
|
|
49
|
+
case "add":
|
|
50
|
+
return run(rest, "create_automation", "Added.");
|
|
51
|
+
case "set":
|
|
52
|
+
refuseUnknownFlags(rest, ARGS.update_automation, ["pause", "resume"]);
|
|
53
|
+
return run(rest, "update_automation", "Changed.");
|
|
54
|
+
case "remove":
|
|
55
|
+
case "rm":
|
|
56
|
+
return run(rest, "delete_automation", "Removed.");
|
|
57
|
+
default:
|
|
58
|
+
throw new CliError(`Unknown automations subcommand: ${sub ?? "(none)"}. Try: list, show, add, set, remove`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function run(ctx, tool, said) {
|
|
63
|
+
const { client, flags, io } = ctx;
|
|
64
|
+
const result = await client.callTool(tool, argsFromFlags(ARGS[tool], flags, ctx.positionals));
|
|
65
|
+
if (said) ok(ctx, said);
|
|
66
|
+
printResult(io, flags, result);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
package/lib/commands/autopen.mjs
CHANGED
|
@@ -20,6 +20,9 @@ export const ARGS = {
|
|
|
20
20
|
from_scratch: { flag: "from-scratch", type: "bool" },
|
|
21
21
|
// --skip finish,metadata: leave those steps out of this run only.
|
|
22
22
|
skip_steps: { flag: "skip", type: "list" },
|
|
23
|
+
// --source <url> … / --must-include <t> …: what this run's research reads first and covers.
|
|
24
|
+
source_urls: { flag: "source", type: "list" },
|
|
25
|
+
must_include: { flag: "must-include", type: "list" },
|
|
23
26
|
},
|
|
24
27
|
};
|
|
25
28
|
|
package/lib/commands/posts.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Ported from phantomstory-cli's posts.ts — same tool calls, same shape, adapted to
|
|
3
3
|
// this CLI's flag/positional/io conventions (no commander, no color).
|
|
4
4
|
|
|
5
|
+
import { writeFileSync } from "node:fs";
|
|
5
6
|
import { CliError } from "../client.mjs";
|
|
6
7
|
import {
|
|
7
8
|
flagStr,
|
|
@@ -13,16 +14,130 @@ import {
|
|
|
13
14
|
ok,
|
|
14
15
|
readBodyInput,
|
|
15
16
|
argsFromFlags,
|
|
17
|
+
refuseUnknownFlags,
|
|
16
18
|
} from "./shared.mjs";
|
|
17
19
|
|
|
20
|
+
const ARTICLE = { positional: 0, label: "article-id", required: true };
|
|
21
|
+
const COMMENT = { positional: 1, label: "comment-id", required: true };
|
|
22
|
+
|
|
23
|
+
/** `--<name> '<json>'` → a JSON object, or undefined when the flag is not set. */
|
|
24
|
+
function jsonObjectFlag(flags, name) {
|
|
25
|
+
const raw = flagStr(flags[name]);
|
|
26
|
+
if (raw === undefined) return undefined;
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new CliError(`--${name} is not valid JSON: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
34
|
+
throw new CliError(`--${name} must be a JSON object`);
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A per-post override: a value, or `inherit` (null: back to the collection's). */
|
|
39
|
+
function overrideFlag(flags, name, allowed) {
|
|
40
|
+
const raw = flagStr(flags[name]);
|
|
41
|
+
if (raw === undefined) return undefined;
|
|
42
|
+
if (raw === "inherit") return null;
|
|
43
|
+
if (allowed && !allowed.includes(raw))
|
|
44
|
+
throw new CliError(`--${name} must be one of: ${[...allowed, "inherit"].join(", ")}`);
|
|
45
|
+
return raw;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `--direction-gate on|off|inherit`. */
|
|
49
|
+
function directionGateFlag(flags) {
|
|
50
|
+
const raw = overrideFlag(flags, "direction-gate", ["on", "off"]);
|
|
51
|
+
return raw === undefined || raw === null ? raw : raw === "on";
|
|
52
|
+
}
|
|
53
|
+
|
|
18
54
|
/** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
|
|
19
55
|
export const ARGS = {
|
|
20
56
|
publish_article: {
|
|
21
|
-
article_id:
|
|
57
|
+
article_id: ARTICLE,
|
|
22
58
|
},
|
|
23
59
|
delete_article: {
|
|
24
60
|
article_ids: { flag: "id", type: "list", required: true, label: "id" },
|
|
25
61
|
},
|
|
62
|
+
update_article: {
|
|
63
|
+
article_id: ARTICLE,
|
|
64
|
+
title: "title",
|
|
65
|
+
content: { value: (flags) => readBodyInput(flags) },
|
|
66
|
+
status: { flag: "status", enum: ["draft", "published", "archived"] },
|
|
67
|
+
metadata: { value: (flags) => jsonObjectFlag(flags, "metadata-json") },
|
|
68
|
+
metadataPatch: {
|
|
69
|
+
object: {
|
|
70
|
+
summary: "summary",
|
|
71
|
+
tags: { flag: "tag", type: "list" },
|
|
72
|
+
owners: { flag: "owner", type: "list" },
|
|
73
|
+
due_date: "due",
|
|
74
|
+
author: "author",
|
|
75
|
+
author_id: "author-id",
|
|
76
|
+
slug: "slug",
|
|
77
|
+
meta_title: "meta-title",
|
|
78
|
+
meta_description: "meta-description",
|
|
79
|
+
published_url: "published-url",
|
|
80
|
+
template_key: { value: (flags) => overrideFlag(flags, "template") },
|
|
81
|
+
mention_degree: { value: (flags) => overrideFlag(flags, "mention", ["off", "subtle", "woven"]) },
|
|
82
|
+
direction_gate: { value: (flags) => directionGateFlag(flags) },
|
|
83
|
+
paper_trail: { value: (flags) => jsonObjectFlag(flags, "paper-trail") },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
edit_article_with_prompt: {
|
|
88
|
+
article_id: ARTICLE,
|
|
89
|
+
instruction: { flag: "instruction", required: true },
|
|
90
|
+
},
|
|
91
|
+
run_mention_pass: {
|
|
92
|
+
article_id: ARTICLE,
|
|
93
|
+
},
|
|
94
|
+
get_article_history: {
|
|
95
|
+
article_id: ARTICLE,
|
|
96
|
+
},
|
|
97
|
+
restore_article_version: {
|
|
98
|
+
article_id: ARTICLE,
|
|
99
|
+
// Positional, not `--version`: that is the global "print the CLI version" switch.
|
|
100
|
+
version: {
|
|
101
|
+
value: (_flags, positionals) => {
|
|
102
|
+
const n = flagNum(positionals[1]);
|
|
103
|
+
if (n === undefined) throw new CliError("Missing required <version>");
|
|
104
|
+
return n;
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
export_article: {
|
|
109
|
+
article_id: ARTICLE,
|
|
110
|
+
format: { flag: "format", required: true, enum: ["md", "pdf"] },
|
|
111
|
+
},
|
|
112
|
+
list_article_comments: {
|
|
113
|
+
article_id: ARTICLE,
|
|
114
|
+
},
|
|
115
|
+
add_article_comment: {
|
|
116
|
+
article_id: ARTICLE,
|
|
117
|
+
body: { flag: "body", required: true },
|
|
118
|
+
kind: { flag: "kind", enum: ["note", "ai_esque"] },
|
|
119
|
+
scope: { flag: "scope", enum: ["sentence", "document"] },
|
|
120
|
+
parent_id: "reply-to",
|
|
121
|
+
quote: "quote",
|
|
122
|
+
quote_prefix: "prefix",
|
|
123
|
+
quote_suffix: "suffix",
|
|
124
|
+
mentioned_user_ids: { flag: "mention", type: "list" },
|
|
125
|
+
},
|
|
126
|
+
update_article_comment: {
|
|
127
|
+
article_id: ARTICLE,
|
|
128
|
+
comment_id: COMMENT,
|
|
129
|
+
body: "body",
|
|
130
|
+
resolved: { flag: "resolve", type: "bool", negate: "reopen" },
|
|
131
|
+
mentioned_user_ids: { flag: "mention", type: "list" },
|
|
132
|
+
},
|
|
133
|
+
delete_article_comment: {
|
|
134
|
+
article_id: ARTICLE,
|
|
135
|
+
comment_id: COMMENT,
|
|
136
|
+
},
|
|
137
|
+
resolve_comment_with_ai: {
|
|
138
|
+
article_id: ARTICLE,
|
|
139
|
+
comment_id: COMMENT,
|
|
140
|
+
},
|
|
26
141
|
};
|
|
27
142
|
|
|
28
143
|
export async function cmdPosts(ctx) {
|
|
@@ -36,6 +151,20 @@ export async function cmdPosts(ctx) {
|
|
|
36
151
|
return postsShow(rest);
|
|
37
152
|
case "new":
|
|
38
153
|
return postsNew(rest);
|
|
154
|
+
case "update":
|
|
155
|
+
return postsWrite(rest, "update_article", "Updated.");
|
|
156
|
+
case "edit":
|
|
157
|
+
return postsWrite(rest, "edit_article_with_prompt", "Edited.");
|
|
158
|
+
case "mentions":
|
|
159
|
+
return postsWrite(rest, "run_mention_pass", "Mention pass done.");
|
|
160
|
+
case "history":
|
|
161
|
+
return postsRead(rest, "get_article_history");
|
|
162
|
+
case "restore":
|
|
163
|
+
return postsWrite(rest, "restore_article_version", "Restored as the latest version.");
|
|
164
|
+
case "export":
|
|
165
|
+
return postsExport(rest);
|
|
166
|
+
case "comments":
|
|
167
|
+
return postsComments(rest);
|
|
39
168
|
case "publish":
|
|
40
169
|
return postsPublish(rest);
|
|
41
170
|
case "unpublish":
|
|
@@ -45,14 +174,85 @@ export async function cmdPosts(ctx) {
|
|
|
45
174
|
return postsDelete(rest);
|
|
46
175
|
default:
|
|
47
176
|
throw new CliError(
|
|
48
|
-
`Unknown posts subcommand: ${sub ?? "(none)"}. Try: list, show, new, publish, unpublish, delete`
|
|
177
|
+
`Unknown posts subcommand: ${sub ?? "(none)"}. Try: list, show, new, update, edit, mentions, history, restore, export, comments, publish, unpublish, delete`
|
|
49
178
|
);
|
|
50
179
|
}
|
|
51
180
|
}
|
|
52
181
|
|
|
182
|
+
// Each tool named once, literally, so scripts/parity-check.ts sees which command calls it.
|
|
183
|
+
const CALL = {
|
|
184
|
+
update_article: (client, args) => client.callTool("update_article", args),
|
|
185
|
+
edit_article_with_prompt: (client, args) => client.callTool("edit_article_with_prompt", args),
|
|
186
|
+
run_mention_pass: (client, args) => client.callTool("run_mention_pass", args),
|
|
187
|
+
get_article_history: (client, args) => client.callTool("get_article_history", args),
|
|
188
|
+
restore_article_version: (client, args) => client.callTool("restore_article_version", args),
|
|
189
|
+
list_article_comments: (client, args) => client.callTool("list_article_comments", args),
|
|
190
|
+
add_article_comment: (client, args) => client.callTool("add_article_comment", args),
|
|
191
|
+
update_article_comment: (client, args) => client.callTool("update_article_comment", args),
|
|
192
|
+
delete_article_comment: (client, args) => client.callTool("delete_article_comment", args),
|
|
193
|
+
resolve_comment_with_ai: (client, args) => client.callTool("resolve_comment_with_ai", args),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** A write: refuse a flag its ARGS map does not declare, then call the tool. */
|
|
197
|
+
async function postsWrite(ctx, tool, done) {
|
|
198
|
+
const { client, flags, io, positionals } = ctx;
|
|
199
|
+
refuseUnknownFlags(ctx, ARGS[tool]);
|
|
200
|
+
const result = await CALL[tool](client, argsFromFlags(ARGS[tool], flags, positionals));
|
|
201
|
+
ok(ctx, done);
|
|
202
|
+
printResult(io, flags, result);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function postsRead(ctx, tool) {
|
|
207
|
+
const { client, flags, io, positionals } = ctx;
|
|
208
|
+
const result = await CALL[tool](client, argsFromFlags(ARGS[tool], flags, positionals));
|
|
209
|
+
printResult(io, flags, result);
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// `--out <path>` writes the file (a PDF's bytes decoded); without it the result prints.
|
|
214
|
+
async function postsExport(ctx) {
|
|
215
|
+
const { client, flags, io, positionals } = ctx;
|
|
216
|
+
const result = await client.callTool("export_article", argsFromFlags(ARGS.export_article, flags, positionals));
|
|
217
|
+
const out = flagStr(flags.out);
|
|
218
|
+
if (out === undefined) {
|
|
219
|
+
if (!flags.json && result?.encoding === "utf8") io.log(result.content);
|
|
220
|
+
else printResult(io, flags, result);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
const bytes = result.encoding === "base64" ? Buffer.from(result.content, "base64") : result.content;
|
|
224
|
+
try {
|
|
225
|
+
writeFileSync(out, bytes);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
throw new CliError(`Could not write --out ${out}: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
ok(ctx, `Wrote ${out}.`);
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// `posts comments <article-id>` lists; `posts comments add|update|delete|resolve-ai <article-id> …` write.
|
|
234
|
+
async function postsComments(ctx) {
|
|
235
|
+
const verbs = {
|
|
236
|
+
add: ["add_article_comment", "Commented."],
|
|
237
|
+
update: ["update_article_comment", "Updated."],
|
|
238
|
+
delete: ["delete_article_comment", "Deleted."],
|
|
239
|
+
rm: ["delete_article_comment", "Deleted."],
|
|
240
|
+
"resolve-ai": ["resolve_comment_with_ai", "Resolved with AI."],
|
|
241
|
+
};
|
|
242
|
+
const verb = verbs[ctx.positionals[0]];
|
|
243
|
+
if (!verb) return postsRead(ctx, "list_article_comments");
|
|
244
|
+
return postsWrite({ ...ctx, positionals: ctx.positionals.slice(1) }, verb[0], verb[1]);
|
|
245
|
+
}
|
|
246
|
+
|
|
53
247
|
async function postsList(ctx) {
|
|
54
248
|
const { client, flags, io } = ctx;
|
|
55
|
-
const args = compact({
|
|
249
|
+
const args = compact({
|
|
250
|
+
limit: flagNum(flags.limit),
|
|
251
|
+
offset: flagNum(flags.offset),
|
|
252
|
+
collection_id: flagStr(flags.collection),
|
|
253
|
+
search: flagStr(flags.search),
|
|
254
|
+
mine: flags.mine === true ? true : undefined,
|
|
255
|
+
});
|
|
56
256
|
const result = await client.callTool("list_articles", args);
|
|
57
257
|
printResult(io, flags, result);
|
|
58
258
|
return 0;
|
|
@@ -5,7 +5,26 @@
|
|
|
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 {
|
|
8
|
+
import {
|
|
9
|
+
flagStr,
|
|
10
|
+
requireFlag,
|
|
11
|
+
requirePositional,
|
|
12
|
+
printResult,
|
|
13
|
+
compact,
|
|
14
|
+
ok,
|
|
15
|
+
argsFromFlags,
|
|
16
|
+
refuseUnknownFlags,
|
|
17
|
+
} from "./shared.mjs";
|
|
18
|
+
|
|
19
|
+
/** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
|
|
20
|
+
export const ARGS = {
|
|
21
|
+
list_assigned_reviews: {},
|
|
22
|
+
invite_guest_reviewer: {
|
|
23
|
+
article_id: { positional: 0, label: "article-id", required: true },
|
|
24
|
+
email: { flag: "email", required: true },
|
|
25
|
+
name: "name",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
9
28
|
|
|
10
29
|
export async function cmdReviewers(ctx) {
|
|
11
30
|
const sub = ctx.positionals[0];
|
|
@@ -13,11 +32,33 @@ export async function cmdReviewers(ctx) {
|
|
|
13
32
|
switch (sub) {
|
|
14
33
|
case "standing":
|
|
15
34
|
return cmdStandingReviewers(rest);
|
|
35
|
+
case "invite":
|
|
36
|
+
return inviteGuest(rest);
|
|
37
|
+
case "assigned":
|
|
38
|
+
return assigned(rest);
|
|
16
39
|
default:
|
|
17
|
-
throw new CliError(`Unknown reviewers subcommand: ${sub ?? "(none)"}. Try: standing`);
|
|
40
|
+
throw new CliError(`Unknown reviewers subcommand: ${sub ?? "(none)"}. Try: assigned, standing, invite`);
|
|
18
41
|
}
|
|
19
42
|
}
|
|
20
43
|
|
|
44
|
+
// `reviewers invite <article-id> --email <e> [--name <n>]` — a guest reviewer on one post.
|
|
45
|
+
async function inviteGuest(ctx) {
|
|
46
|
+
const { client, flags, io, positionals } = ctx;
|
|
47
|
+
refuseUnknownFlags(ctx, ARGS.invite_guest_reviewer);
|
|
48
|
+
const result = await client.callTool(
|
|
49
|
+
"invite_guest_reviewer",
|
|
50
|
+
argsFromFlags(ARGS.invite_guest_reviewer, flags, positionals)
|
|
51
|
+
);
|
|
52
|
+
ok(
|
|
53
|
+
ctx,
|
|
54
|
+
result?.email_sent
|
|
55
|
+
? `Invited ${flags.email}: the link and PIN are on their way.`
|
|
56
|
+
: `Invited ${flags.email}, but the email could not be sent: share the link and PIN yourself.`
|
|
57
|
+
);
|
|
58
|
+
printResult(io, flags, result);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
21
62
|
async function cmdStandingReviewers(ctx) {
|
|
22
63
|
const sub = ctx.positionals[0];
|
|
23
64
|
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
@@ -60,3 +101,11 @@ async function standingRemove(ctx) {
|
|
|
60
101
|
printResult(io, flags, result);
|
|
61
102
|
return 0;
|
|
62
103
|
}
|
|
104
|
+
|
|
105
|
+
// `reviewers assigned` — the posts waiting on your review, across every letterbase.
|
|
106
|
+
async function assigned(ctx) {
|
|
107
|
+
const { client, flags, io } = ctx;
|
|
108
|
+
const result = await client.callTool("list_assigned_reviews", argsFromFlags(ARGS.list_assigned_reviews, flags));
|
|
109
|
+
printResult(io, flags, result);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
package/lib/commands.mjs
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./commands/phantom.mjs";
|
|
|
11
11
|
export * from "./commands/mcp.mjs";
|
|
12
12
|
export * from "./commands/posts.mjs";
|
|
13
13
|
export * from "./commands/reviewers.mjs";
|
|
14
|
+
export * from "./commands/automations.mjs";
|
|
14
15
|
export * from "./commands/collections.mjs";
|
|
15
16
|
export * from "./commands/authors.mjs";
|
|
16
17
|
export * from "./commands/flows.mjs";
|