@letterstory/cli 0.1.1 → 0.2.0

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.
@@ -0,0 +1,122 @@
1
+ // `posts` (draft/read/publish articles) and `published` (the frozen public view).
2
+ // Ported from phantomstory-cli's posts.ts — same tool calls, same shape, adapted to
3
+ // this CLI's flag/positional/io conventions (no commander, no color).
4
+
5
+ import { CliError } from "../client.mjs";
6
+ import {
7
+ flagStr,
8
+ flagNum,
9
+ requireFlag,
10
+ requirePositional,
11
+ printResult,
12
+ compact,
13
+ ok,
14
+ readBodyInput,
15
+ } from "./shared.mjs";
16
+
17
+ export async function cmdPosts(ctx) {
18
+ const sub = ctx.positionals[0];
19
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
20
+ switch (sub) {
21
+ case "list":
22
+ case "ls":
23
+ return postsList(rest);
24
+ case "show":
25
+ return postsShow(rest);
26
+ case "new":
27
+ return postsNew(rest);
28
+ case "publish":
29
+ return postsPublish(rest);
30
+ case "unpublish":
31
+ return postsUnpublish(rest);
32
+ default:
33
+ throw new CliError(
34
+ `Unknown posts subcommand: ${sub ?? "(none)"}. Try: list, show, new, publish, unpublish`
35
+ );
36
+ }
37
+ }
38
+
39
+ async function postsList(ctx) {
40
+ const { client, flags, io } = ctx;
41
+ const args = compact({ limit: flagNum(flags.limit), collection_id: flagStr(flags.collection) });
42
+ const result = await client.callTool("list_articles", args);
43
+ printResult(io, flags, result);
44
+ return 0;
45
+ }
46
+
47
+ async function postsShow(ctx) {
48
+ const { client, positionals, flags, io } = ctx;
49
+ const id = requirePositional(positionals, 0, "article-id");
50
+ const result = await client.callTool("get_article", { article_id: id });
51
+ printResult(io, flags, result);
52
+ return 0;
53
+ }
54
+
55
+ async function postsNew(ctx) {
56
+ const { client, flags, io } = ctx;
57
+ const title = requireFlag(flags, "title");
58
+ const collection = requireFlag(flags, "collection");
59
+ const body = readBodyInput(flags);
60
+ if (!body) throw new CliError("Provide --body or --file <path>.");
61
+ const result = await client.callTool("ingest_article", { title, body, collection_id: collection });
62
+ ok(ctx, `Created draft "${title}".`);
63
+ printResult(io, flags, result);
64
+ return 0;
65
+ }
66
+
67
+ async function postsPublish(ctx) {
68
+ const { client, positionals, flags, io } = ctx;
69
+ const id = requirePositional(positionals, 0, "article-id");
70
+ const result = await client.callTool("publish_article", { article_id: id });
71
+ ok(ctx, "Published.");
72
+ printResult(io, flags, result);
73
+ return 0;
74
+ }
75
+
76
+ async function postsUnpublish(ctx) {
77
+ const { client, positionals, flags, io } = ctx;
78
+ const id = requirePositional(positionals, 0, "article-id");
79
+ const result = await client.callTool("unpublish_article", { article_id: id });
80
+ ok(ctx, "Unpublished.");
81
+ printResult(io, flags, result);
82
+ return 0;
83
+ }
84
+
85
+ // --- published (frozen public view) -----------------------------------------
86
+
87
+ export async function cmdPublished(ctx) {
88
+ const sub = ctx.positionals[0];
89
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
90
+ switch (sub) {
91
+ case "list":
92
+ case "ls":
93
+ return publishedList(rest);
94
+ case "show":
95
+ return publishedShow(rest);
96
+ default:
97
+ throw new CliError(`Unknown published subcommand: ${sub ?? "(none)"}. Try: list, show`);
98
+ }
99
+ }
100
+
101
+ async function publishedList(ctx) {
102
+ const { client, flags, io } = ctx;
103
+ const args = compact({
104
+ limit: flagNum(flags.limit),
105
+ collection_id: flagStr(flags.collection),
106
+ format: flagStr(flags.format),
107
+ });
108
+ const result = await client.callTool("list_published", args);
109
+ printResult(io, flags, result);
110
+ return 0;
111
+ }
112
+
113
+ async function publishedShow(ctx) {
114
+ const { client, flags, io } = ctx;
115
+ const id = flagStr(flags.id);
116
+ const slug = flagStr(flags.slug);
117
+ if (id === undefined && slug === undefined) throw new CliError("Pass --id or --slug.");
118
+ const args = compact({ article_id: id, slug, format: flagStr(flags.format) });
119
+ const result = await client.callTool("get_published", args);
120
+ printResult(io, flags, result);
121
+ return 0;
122
+ }
@@ -0,0 +1,95 @@
1
+ // Helpers shared by every command module: flag/positional readers, JSON-vs-formatted
2
+ // output, and the --quiet-aware success line. Kept dependency-free like the rest of
3
+ // the CLI — no formatting library, just small string builders.
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { CliError } from "../client.mjs";
7
+
8
+ // A flag with no value parses to `true`; coerce that back to undefined so a bare
9
+ // `--name` reads as "missing", not the string "true". A repeated flag (see cli.mjs's
10
+ // parseArgs) parses to an array — also "missing" for a single-value read.
11
+ export function flagStr(v) {
12
+ return typeof v === "string" ? v : undefined;
13
+ }
14
+
15
+ export function flagNum(v) {
16
+ const s = flagStr(v);
17
+ if (s === undefined) return undefined;
18
+ const n = Number(s);
19
+ if (!Number.isFinite(n)) throw new CliError(`Expected a number, got "${s}"`);
20
+ return n;
21
+ }
22
+
23
+ export function flagBool(v) {
24
+ return v === true;
25
+ }
26
+
27
+ // Repeatable flags (e.g. `--topic a --topic b`) accumulate into an array in
28
+ // parseArgs; a single occurrence stays a plain string. Normalize both to a list.
29
+ export function flagList(v) {
30
+ if (v === undefined) return [];
31
+ if (Array.isArray(v)) return v.filter((x) => typeof x === "string");
32
+ return typeof v === "string" ? [v] : [];
33
+ }
34
+
35
+ export function requireFlag(flags, name) {
36
+ const v = flagStr(flags[name]);
37
+ if (v === undefined) throw new CliError(`Missing required --${name}`);
38
+ return v;
39
+ }
40
+
41
+ export function requirePositional(positionals, index, label) {
42
+ const v = positionals[index];
43
+ if (v === undefined) throw new CliError(`Missing required <${label}>`);
44
+ return v;
45
+ }
46
+
47
+ // Drop undefined entries so callTool args only carry fields the user actually set.
48
+ export function compact(obj) {
49
+ const out = {};
50
+ for (const [k, v] of Object.entries(obj)) {
51
+ if (v !== undefined) out[k] = v;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ export function printResult(io, flags, value, formatter) {
57
+ if (flags.json || !formatter) {
58
+ io.log(JSON.stringify(value, null, 2));
59
+ } else {
60
+ io.log(formatter(value));
61
+ }
62
+ }
63
+
64
+ // A success line that stays quiet under --quiet or --json (scripting doesn't want
65
+ // chatter mixed into a parseable stream). Existing commands (deploy/domain) print
66
+ // directly through io.log and are intentionally left alone; this gate is for the
67
+ // new command groups only.
68
+ export function ok(ctx, message) {
69
+ if (ctx.flags.quiet || ctx.flags.json) return;
70
+ ctx.io.log(message);
71
+ }
72
+
73
+ // Read a text body from --body, or --file (a path, or "-" for stdin). Returns
74
+ // undefined if neither flag was given, so callers can decide whether that's an error.
75
+ export function readBodyInput(flags) {
76
+ const body = flagStr(flags.body);
77
+ if (body !== undefined) return body;
78
+ const file = flagStr(flags.file);
79
+ if (file === undefined) return undefined;
80
+ if (file === "-") {
81
+ if (process.stdin.isTTY) {
82
+ throw new CliError("No data on stdin. Pipe input in, or use --body/--file <path> instead of --file -.");
83
+ }
84
+ try {
85
+ return readFileSync(0, "utf8");
86
+ } catch (err) {
87
+ throw new CliError(`Could not read stdin: ${err.message}`);
88
+ }
89
+ }
90
+ try {
91
+ return readFileSync(file, "utf8");
92
+ } catch (err) {
93
+ throw new CliError(`Could not read --file ${file}: ${err.message}`);
94
+ }
95
+ }
@@ -0,0 +1,209 @@
1
+ // `strategy` (company profile, positioning, competitors, sitemap import) and
2
+ // `onboarding` (checklist status/step). Ported from phantomstory-cli's strategy.ts,
3
+ // which registers `onboarding` as its own top-level command alongside `strategy` —
4
+ // mirrored here as two exported handlers for the same reason.
5
+
6
+ import { CliError } from "../client.mjs";
7
+ import {
8
+ flagStr,
9
+ requireFlag,
10
+ requirePositional,
11
+ printResult,
12
+ compact,
13
+ flagList,
14
+ ok,
15
+ readBodyInput,
16
+ } from "./shared.mjs";
17
+
18
+ export async function cmdStrategy(ctx) {
19
+ const sub = ctx.positionals[0];
20
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
21
+ switch (sub) {
22
+ case "company":
23
+ return strategyCompany(rest);
24
+ case "positioning":
25
+ return strategyPositioning(rest);
26
+ case "competitors":
27
+ return strategyCompetitors(rest);
28
+ case "sitemap":
29
+ return strategySitemap(rest);
30
+ default:
31
+ throw new CliError(
32
+ `Unknown strategy subcommand: ${sub ?? "(none)"}. Try: company, positioning, competitors, sitemap`
33
+ );
34
+ }
35
+ }
36
+
37
+ // -- company -------------------------------------------------------------
38
+
39
+ async function strategyCompany(ctx) {
40
+ const sub = ctx.positionals[0];
41
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
42
+ switch (sub) {
43
+ case "get":
44
+ return companyGet(rest);
45
+ case "set":
46
+ return companySet(rest);
47
+ default:
48
+ throw new CliError(`Unknown strategy company subcommand: ${sub ?? "(none)"}. Try: get, set`);
49
+ }
50
+ }
51
+
52
+ async function companyGet(ctx) {
53
+ const { client, flags, io } = ctx;
54
+ const result = await client.callTool("get_company_info", {});
55
+ printResult(io, flags, result);
56
+ return 0;
57
+ }
58
+
59
+ async function companySet(ctx) {
60
+ const { client, flags, io } = ctx;
61
+ const args = compact({ name: flagStr(flags.name), domain: flagStr(flags.domain) });
62
+ // The API clears the manifesto with an empty string, which compact() would drop —
63
+ // so set it explicitly whenever the user supplied a source (text or file).
64
+ const manifesto = flagStr(flags.manifesto);
65
+ const manifestoFile = flagStr(flags["manifesto-file"]);
66
+ if (manifesto !== undefined || manifestoFile !== undefined) {
67
+ args.manifesto = manifesto ?? readBodyInput({ file: manifestoFile }) ?? "";
68
+ }
69
+ if (Object.keys(args).length === 0) {
70
+ throw new CliError('Nothing to set — pass --name/--domain/--manifesto (empty --manifesto "" clears it).');
71
+ }
72
+ const result = await client.callTool("set_company_info", args);
73
+ ok(ctx, "Company profile updated.");
74
+ printResult(io, flags, result);
75
+ return 0;
76
+ }
77
+
78
+ // -- positioning -----------------------------------------------------------
79
+
80
+ async function strategyPositioning(ctx) {
81
+ const sub = ctx.positionals[0];
82
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
83
+ switch (sub) {
84
+ case "get":
85
+ return positioningGet(rest);
86
+ case "set":
87
+ return positioningSet(rest);
88
+ default:
89
+ throw new CliError(`Unknown strategy positioning subcommand: ${sub ?? "(none)"}. Try: get, set`);
90
+ }
91
+ }
92
+
93
+ async function positioningGet(ctx) {
94
+ const { client, flags, io } = ctx;
95
+ const result = await client.callTool("get_positioning", {});
96
+ printResult(io, flags, result);
97
+ return 0;
98
+ }
99
+
100
+ async function positioningSet(ctx) {
101
+ const { client, flags, io } = ctx;
102
+ const topics = flagList(flags.topic);
103
+ const stances = flagList(flags.stance);
104
+ const avoid = flagList(flags.avoid);
105
+ // The API clears a list with an explicit empty array (which compact() keeps).
106
+ const args = compact({
107
+ priority_topics: flags["clear-topics"] ? [] : topics.length ? topics : undefined,
108
+ stances: flags["clear-stances"] ? [] : stances.length ? stances : undefined,
109
+ avoid_topics: flags["clear-avoid"] ? [] : avoid.length ? avoid : undefined,
110
+ });
111
+ if (Object.keys(args).length === 0) {
112
+ throw new CliError("Pass at least one --topic/--stance/--avoid (or a --clear-* flag).");
113
+ }
114
+ const result = await client.callTool("set_positioning", args);
115
+ ok(ctx, "Positioning updated.");
116
+ printResult(io, flags, result);
117
+ return 0;
118
+ }
119
+
120
+ // -- competitors -------------------------------------------------------------
121
+
122
+ async function strategyCompetitors(ctx) {
123
+ const sub = ctx.positionals[0];
124
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
125
+ switch (sub) {
126
+ case "list":
127
+ case "ls":
128
+ return competitorsList(rest);
129
+ case "add":
130
+ return competitorsAdd(rest);
131
+ default:
132
+ throw new CliError(`Unknown strategy competitors subcommand: ${sub ?? "(none)"}. Try: list, add`);
133
+ }
134
+ }
135
+
136
+ async function competitorsList(ctx) {
137
+ const { client, flags, io } = ctx;
138
+ const result = await client.callTool("list_competitors", {});
139
+ printResult(io, flags, result);
140
+ return 0;
141
+ }
142
+
143
+ async function competitorsAdd(ctx) {
144
+ const { client, positionals, flags, io } = ctx;
145
+ const name = requirePositional(positionals, 0, "name");
146
+ const domain = requirePositional(positionals, 1, "domain");
147
+ const result = await client.callTool("add_competitor", { name, domain });
148
+ ok(ctx, `Added competitor ${name}.`);
149
+ printResult(io, flags, result);
150
+ return 0;
151
+ }
152
+
153
+ // -- sitemap -------------------------------------------------------------
154
+
155
+ async function strategySitemap(ctx) {
156
+ const { client, flags, io } = ctx;
157
+ const collection = requireFlag(flags, "collection");
158
+ const url = requireFlag(flags, "url");
159
+ const sub = flagList(flags.sub);
160
+ const args = compact({
161
+ collection_id: collection,
162
+ sitemap_url: url,
163
+ sitemap_urls: sub.length ? sub : undefined,
164
+ url_pattern: flagStr(flags.pattern),
165
+ });
166
+ const result = await client.callTool("import_sitemap", args);
167
+ ok(ctx, "Sitemap import started.");
168
+ printResult(io, flags, result);
169
+ return 0;
170
+ }
171
+
172
+ // --- onboarding ---------------------------------------------------------
173
+
174
+ export async function cmdOnboarding(ctx) {
175
+ const sub = ctx.positionals[0];
176
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
177
+ switch (sub) {
178
+ case "status":
179
+ return onboardingStatus(rest);
180
+ case "step":
181
+ return onboardingStep(rest);
182
+ default:
183
+ throw new CliError(`Unknown onboarding subcommand: ${sub ?? "(none)"}. Try: status, step`);
184
+ }
185
+ }
186
+
187
+ async function onboardingStatus(ctx) {
188
+ const { client, flags, io } = ctx;
189
+ const result = await client.callTool("get_onboarding_status", {});
190
+ printResult(io, flags, result);
191
+ return 0;
192
+ }
193
+
194
+ async function onboardingStep(ctx) {
195
+ const { client, flags, io } = ctx;
196
+ const args = compact({
197
+ current_step: flagStr(flags.current),
198
+ complete_step: flagStr(flags.complete),
199
+ skip_step: flagStr(flags.skip),
200
+ status: flagStr(flags.status),
201
+ });
202
+ if (Object.keys(args).length === 0) {
203
+ throw new CliError("Pass --current/--complete/--skip/--status.");
204
+ }
205
+ const result = await client.callTool("set_onboarding_step", args);
206
+ ok(ctx, "Onboarding updated.");
207
+ printResult(io, flags, result);
208
+ return 0;
209
+ }