@verbatims/cli 0.1.14 → 0.1.16

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 CHANGED
@@ -19,12 +19,51 @@ verbatims config
19
19
  # List quotes
20
20
  verbatims quotes list --language fr --limit 10
21
21
 
22
+ # Filter quotes by reference and inspect provenance
23
+ verbatims quotes list --reference 42
24
+ verbatims quotes get 123 --format json
25
+
26
+ # Create a quote with a source
27
+ verbatims quotes create --source-type book --source-url https://example.com/edition
28
+
22
29
  # Browse interactively
23
30
  verbatims quotes browse
24
31
 
25
32
  # Search
26
33
  verbatims search "love" --type quotes
27
34
 
35
+ # List social platforms with queue stats
36
+ verbatims social platforms
37
+
38
+ # List the social queue (defaults to platform x)
39
+ verbatims social queue list --platform bluesky --status queued
40
+
41
+ # Add approved quotes to the queue
42
+ verbatims social queue add 42 57 --platform bluesky
43
+
44
+ # Publish the next eligible queued item immediately
45
+ verbatims social queue run-now --platform bluesky
46
+
28
47
  # See all commands
29
48
  verbatims --help
30
49
  ```
50
+
51
+ ### Social commands
52
+
53
+ ```bash
54
+ verbatims social platforms # list platforms + queue stats
55
+ verbatims social posts [--platform <p>] [--status <s>] # audit trail of published posts
56
+ verbatims social queue list [--platform <p>] [--status <s>] [--search <q>] [--limit <n>]
57
+ verbatims social queue get <id> # single queue item
58
+ verbatims social queue add <ids...> [--platform <p>] [--scheduled-for <iso>]
59
+ verbatims social queue random [--platform <p>] [--count <n>] [--language <lang>]
60
+ verbatims social queue remove <id> [--yes]
61
+ verbatims social queue requeue <id> # reset a failed item to queued
62
+ verbatims social queue reorder <id> [--direction up|down] [--before <id>]
63
+ verbatims social queue clear [--platform <p>] [--scope all|finished] [--yes]
64
+ verbatims social queue run-now [--platform <p>] # publish immediately (1/min)
65
+ ```
66
+
67
+ Platforms: `x`, `bluesky`, `instagram`, `threads`, `facebook`, `pinterest`.
68
+ Queue statuses: `queued`, `processing`, `posted`, `failed`, `active`.
69
+ Requires an API key with the `social:read` / `social:write` permissions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbatims/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "CLI for the Verbatims quotes API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,7 @@ import { registerCollectionsCommand } from './collections.js'
7
7
  import { registerSearchCommand } from './search.js'
8
8
  import { registerConfigCommand } from './config.js'
9
9
  import { registerThemesCommand } from './themes.js'
10
+ import { registerSocialCommand } from './social.js'
10
11
 
11
12
  export function registerCommands(program: Command) {
12
13
  registerQuotesCommand(program)
@@ -17,4 +18,5 @@ export function registerCommands(program: Command) {
17
18
  registerSearchCommand(program)
18
19
  registerConfigCommand(program)
19
20
  registerThemesCommand(program)
21
+ registerSocialCommand(program)
20
22
  }
@@ -19,6 +19,7 @@ export function registerQuotesCommand(program: Command) {
19
19
  .option('--language <lang>', 'Filter by language')
20
20
  .option('--limit <n>', 'Number of results', '20')
21
21
  .option('--author <id>', 'Filter by author ID')
22
+ .option('--reference <id>', 'Filter by reference ID')
22
23
  .option('--tag <name>', 'Filter by tag')
23
24
  .option('--search <q>', 'Search in quote text')
24
25
  .option('--status <status>', 'Filter by status (draft|pending|approved|rejected)')
@@ -33,6 +34,7 @@ export function registerQuotesCommand(program: Command) {
33
34
  language: options.language,
34
35
  limit: Number(options.limit),
35
36
  author_id: options.author ? Number(options.author) : undefined,
37
+ reference_id: options.reference ? Number(options.reference) : undefined,
36
38
  tag: options.tag,
37
39
  search: options.search,
38
40
  q: options.search,
@@ -66,6 +68,8 @@ export function registerQuotesCommand(program: Command) {
66
68
  .option('--language <lang>', 'Language')
67
69
  .option('--author-id <id>', 'Author ID')
68
70
  .option('--reference-id <id>', 'Reference ID')
71
+ .option('--source-type <type>', 'Source type')
72
+ .option('--source-url <url>', 'Source URL')
69
73
  .action(async (options: Record<string, string>) => {
70
74
  const client = await getClient()
71
75
  const format = getFormat(program)
@@ -80,7 +84,7 @@ export function registerQuotesCommand(program: Command) {
80
84
  const referenceId = options.referenceId ? Number(options.referenceId) : undefined
81
85
 
82
86
  const { data } = await withSpinner('Creating quote', () =>
83
- client.quotes.create({ content: name as string, name: name as string, language: language as string, author_id: authorId, reference_id: referenceId }),
87
+ client.quotes.create({ content: name as string, name: name as string, language: language as string, author_id: authorId, reference_id: referenceId, source_type: options.sourceType, source_url: options.sourceUrl }),
84
88
  format,
85
89
  )
86
90
  console.log(chalk.green(' Quote created'))
@@ -94,6 +98,8 @@ export function registerQuotesCommand(program: Command) {
94
98
  .option('--language <lang>', 'Language')
95
99
  .option('--author-id <id>', 'Author ID')
96
100
  .option('--reference-id <id>', 'Reference ID')
101
+ .option('--source-type <type>', 'Source type')
102
+ .option('--source-url <url>', 'Source URL')
97
103
  .action(async (id: string, options: Record<string, string>) => {
98
104
  const client = await getClient()
99
105
  const format = getFormat(program)
@@ -103,6 +109,8 @@ export function registerQuotesCommand(program: Command) {
103
109
  if (options.language) data_.language = options.language
104
110
  if (options.authorId !== undefined) data_.author_id = options.authorId === 'null' ? null : Number(options.authorId)
105
111
  if (options.referenceId !== undefined) data_.reference_id = options.referenceId === 'null' ? null : Number(options.referenceId)
112
+ if (options.sourceType !== undefined) data_.source_type = options.sourceType
113
+ if (options.sourceUrl !== undefined) data_.source_url = options.sourceUrl
106
114
 
107
115
  const { data } = await withSpinner('Updating quote', () => client.quotes.update(Number(id), data_ as any), format)
108
116
  console.log(chalk.green(' Quote updated'))
@@ -172,6 +180,7 @@ export function registerQuotesCommand(program: Command) {
172
180
  .description('Browse quotes interactively')
173
181
  .option('--language <lang>', 'Filter by language')
174
182
  .option('--author <id>', 'Filter by author ID')
183
+ .option('--reference <id>', 'Filter by reference ID')
175
184
  .option('--tag <name>', 'Filter by tag')
176
185
  .option('--search <q>', 'Search in quote text')
177
186
  .option('--status <status>', 'Filter by status (draft|pending|approved|rejected)')
@@ -185,6 +194,7 @@ export function registerQuotesCommand(program: Command) {
185
194
  page,
186
195
  language: options.language,
187
196
  author_id: options.author ? Number(options.author) : undefined,
197
+ reference_id: options.reference ? Number(options.reference) : undefined,
188
198
  tag: options.tag,
189
199
  search: options.search,
190
200
  q: options.search,
@@ -0,0 +1,247 @@
1
+ import type { Command } from 'commander'
2
+ import { text, isCancel, cancel, confirm, select } from '@clack/prompts'
3
+ import chalk from 'chalk'
4
+ import { getClient } from '../utils/client.js'
5
+ import { output, type Format } from '../utils/format.js'
6
+ import { withSpinner } from '../utils/spinner.js'
7
+
8
+ function getFormat(program: Command): Format {
9
+ return (program.opts().format ?? 'table') as Format
10
+ }
11
+
12
+ const PLATFORM_CHOICES = ['x', 'bluesky', 'instagram', 'threads', 'facebook', 'pinterest']
13
+
14
+ export function registerSocialCommand(program: Command) {
15
+ const social = program.command('social').description('Manage the social auto-post queue')
16
+
17
+ social
18
+ .command('platforms')
19
+ .description('List social platforms with their queue stats')
20
+ .action(async () => {
21
+ const client = await getClient()
22
+ const format = getFormat(program)
23
+ const { data } = await withSpinner('Fetching platforms', () => client.social.listPlatforms(), format)
24
+ output(data, format)
25
+ })
26
+
27
+ social
28
+ .command('posts')
29
+ .description('List published social posts (audit trail)')
30
+ .option('--platform <platform>', 'Filter by platform')
31
+ .option('--status <status>', 'Filter by status (success|failed)')
32
+ .option('--limit <n>', 'Number of results', '20')
33
+ .action(async (options: Record<string, string>) => {
34
+ const client = await getClient()
35
+ const format = getFormat(program)
36
+ const { data, pagination } = await withSpinner('Fetching posts', () =>
37
+ client.social.listPosts({
38
+ platform: options.platform as any,
39
+ status: options.status as any,
40
+ limit: Number(options.limit),
41
+ }),
42
+ format,
43
+ )
44
+ if (data) output(data.posts, format)
45
+ if (pagination && format !== 'json') {
46
+ console.log(chalk.dim(`\nPage ${pagination.page}/${pagination.totalPages} — ${pagination.total} total`))
47
+ }
48
+ })
49
+
50
+ const queue = social.command('queue').description('Manage the social queue')
51
+
52
+ queue
53
+ .command('list')
54
+ .description('List the social queue for a platform')
55
+ .option('--platform <platform>', 'Platform (default: x)')
56
+ .option('--status <status>', 'Filter by status (queued|processing|posted|failed|active)')
57
+ .option('--search <q>', 'Search by quote text, author, reference, or source')
58
+ .option('--limit <n>', 'Number of results', '20')
59
+ .action(async (options: Record<string, string>) => {
60
+ const client = await getClient()
61
+ const format = getFormat(program)
62
+ const { data, pagination } = await withSpinner('Fetching queue', () =>
63
+ client.social.listQueue({
64
+ platform: options.platform as any,
65
+ status: options.status as any,
66
+ search: options.search,
67
+ limit: Number(options.limit),
68
+ }),
69
+ format,
70
+ )
71
+ if (data) {
72
+ if (format !== 'json') {
73
+ const stats = data.stats
74
+ console.log(chalk.dim(
75
+ `Queued: ${stats.queued} · Processing: ${stats.processing} · Posted: ${stats.posted} · Failed: ${stats.failed}`,
76
+ ))
77
+ }
78
+ output(data.queue, format)
79
+ }
80
+ if (pagination && format !== 'json') {
81
+ console.log(chalk.dim(`\nPage ${pagination.page}/${pagination.totalPages} — ${pagination.total} total`))
82
+ }
83
+ })
84
+
85
+ queue
86
+ .command('get <id>')
87
+ .description('Get a single queue item')
88
+ .action(async (id: string) => {
89
+ const client = await getClient()
90
+ const format = getFormat(program)
91
+ const { data } = await withSpinner('Fetching queue item', () => client.social.getQueueItem(Number(id)), format)
92
+ output(data, format)
93
+ })
94
+
95
+ queue
96
+ .command('add <ids...>')
97
+ .description('Add approved quotes to the queue')
98
+ .option('--platform <platform>', 'Platform (default: x)')
99
+ .option('--scheduled-for <iso>', 'Optional future publish time (ISO 8601)')
100
+ .action(async (ids: string[], options: Record<string, string>) => {
101
+ const client = await getClient()
102
+ const format = getFormat(program)
103
+ const quoteIds = ids.map(Number).filter((n) => Number.isInteger(n) && n > 0)
104
+ if (!quoteIds.length) {
105
+ console.log(chalk.red(' Provide at least one quote ID'))
106
+ process.exit(1)
107
+ }
108
+
109
+ const { data, count } = await withSpinner('Adding quotes to queue', () =>
110
+ client.social.addToQueue({
111
+ quote_ids: quoteIds,
112
+ platform: options.platform as any,
113
+ scheduled_for: options.scheduledFor || null,
114
+ }),
115
+ format,
116
+ )
117
+ console.log(chalk.green(` ${count ?? data?.length ?? 0} quote(s) queued`))
118
+ output(data, format)
119
+ })
120
+
121
+ queue
122
+ .command('random')
123
+ .description('Enqueue N random approved quotes')
124
+ .option('--platform <platform>', 'Platform (default: x)')
125
+ .option('--count <n>', 'Number of quotes (default: 5, max 100)', '5')
126
+ .option('--language <lang>', 'Filter by quote language')
127
+ .action(async (options: Record<string, string>) => {
128
+ const client = await getClient()
129
+ const format = getFormat(program)
130
+ const { data, count } = await withSpinner('Enqueueing random quotes', () =>
131
+ client.social.addRandomToQueue({
132
+ platform: options.platform as any,
133
+ count: Number(options.count),
134
+ language: options.language,
135
+ }),
136
+ format,
137
+ )
138
+ console.log(chalk.green(` ${count ?? data?.length ?? 0} quote(s) queued`))
139
+ output(data, format)
140
+ })
141
+
142
+ queue
143
+ .command('remove <id>')
144
+ .description('Remove a queue item')
145
+ .option('--yes', 'Skip confirmation')
146
+ .action(async (id: string, options: Record<string, unknown>) => {
147
+ if (!options.yes) {
148
+ const confirmed = await confirm({ message: `Remove queue item #${id}?` })
149
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
150
+ }
151
+
152
+ const client = await getClient()
153
+ await withSpinner('Removing queue item', () => client.social.removeQueueItem(Number(id)))
154
+ console.log(chalk.green(` Queue item #${id} removed`))
155
+ })
156
+
157
+ queue
158
+ .command('requeue <id>')
159
+ .description('Reset a failed queue item back to queued')
160
+ .action(async (id: string) => {
161
+ const client = await getClient()
162
+ const format = getFormat(program)
163
+ const { data } = await withSpinner('Requeueing item', () => client.social.requeueQueueItem(Number(id)), format)
164
+ console.log(chalk.green(` Queue item #${id} requeued`))
165
+ output(data, format)
166
+ })
167
+
168
+ queue
169
+ .command('reorder <id>')
170
+ .description('Reorder a queued item (relative to another item or up/down)')
171
+ .option('--direction <dir>', 'Move direction (up|down)')
172
+ .option('--before <id>', 'Place before this queue item ID (or "end" to move to the end)')
173
+ .action(async (id: string, options: Record<string, string>) => {
174
+ const client = await getClient()
175
+ const format = getFormat(program)
176
+
177
+ let beforeId: number | null | undefined
178
+ if (options.before !== undefined) {
179
+ beforeId = options.before === 'end' ? null : Number(options.before)
180
+ }
181
+
182
+ const direction = (options.direction ?? '') as 'up' | 'down' | ''
183
+ if (beforeId === undefined && direction !== 'up' && direction !== 'down') {
184
+ console.log(chalk.red(' Provide --direction up|down or --before <id>'))
185
+ process.exit(1)
186
+ }
187
+
188
+ const { data } = await withSpinner('Reordering queue item', () =>
189
+ client.social.reorderQueueItem({
190
+ id: Number(id),
191
+ direction: direction || undefined,
192
+ before_id: beforeId,
193
+ }),
194
+ format,
195
+ )
196
+ if (!data?.moved) {
197
+ console.log(chalk.dim(' Item did not move'))
198
+ return
199
+ }
200
+ console.log(chalk.green(` Queue item #${id} moved to position ${data.position}`))
201
+ })
202
+
203
+ queue
204
+ .command('clear')
205
+ .description('Clear the queue for a platform')
206
+ .option('--platform <platform>', 'Platform', 'x')
207
+ .option('--scope <scope>', 'Scope: all or finished (posted/failed)', 'all')
208
+ .option('--yes', 'Skip confirmation')
209
+ .action(async (options: Record<string, string>) => {
210
+ const platform = options.platform ?? await select({
211
+ message: 'Platform',
212
+ options: PLATFORM_CHOICES.map((p) => ({ value: p, label: p })),
213
+ })
214
+ if (isCancel(platform)) cancel('Cancelled')
215
+
216
+ const scope = (options.scope ?? 'all') as 'all' | 'finished'
217
+ const label = scope === 'finished' ? 'finished items (posted/failed)' : 'entire queue'
218
+
219
+ if (!options.yes) {
220
+ const confirmed = await confirm({ message: `Clear the ${label} for ${String(platform)}? This cannot be undone.` })
221
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
222
+ }
223
+
224
+ const client = await getClient()
225
+ const format = getFormat(program)
226
+ const { data } = await withSpinner('Clearing queue', () =>
227
+ client.social.clearQueue({ platform: platform as string as any, confirm: true, scope }),
228
+ format,
229
+ )
230
+ console.log(chalk.green(` ${data?.deletedCount ?? 0} item(s) cleared from ${data?.platform ?? String(platform)}`))
231
+ output(data, format)
232
+ })
233
+
234
+ queue
235
+ .command('run-now')
236
+ .description('Immediately run the social autopost (throttled to 1 per minute)')
237
+ .option('--platform <platform>', 'Target platform (all enabled if omitted)')
238
+ .action(async (options: Record<string, string>) => {
239
+ const client = await getClient()
240
+ const format = getFormat(program)
241
+ const { data } = await withSpinner('Running autopost', () =>
242
+ client.social.runNow({ platform: options.platform as any }),
243
+ format,
244
+ )
245
+ output(data, format)
246
+ })
247
+ }
@@ -32,7 +32,7 @@ export async function browse(fetchPage: PageFetcher, format: Format): Promise<vo
32
32
  }
33
33
 
34
34
  const allKeys = Object.keys(items[0] as Record<string, unknown>)
35
- const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'quotes_count']
35
+ const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'source', 'attributions', 'sources', 'quotes_count']
36
36
  const columns = keyOrder
37
37
  .filter((k) => allKeys.includes(k))
38
38
  .map((k) => ({ key: k, label: k }))
@@ -137,7 +137,7 @@ export function output(data: unknown, format: Format): void {
137
137
  }
138
138
 
139
139
  const allKeys = Object.keys(data[0] as Record<string, unknown>)
140
- const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'quotes_count']
140
+ const keyOrder = ['id', 'content', 'name', 'language', 'type', 'author', 'reference', 'source', 'attributions', 'sources', 'quotes_count', 'platform', 'label', 'enabled', 'status', 'position', 'quote_text', 'quote_language', 'author_name', 'reference_name', 'post_text', 'post_url', 'posted_at', 'error_message']
141
141
  const columns = keyOrder
142
142
  .filter((k) => allKeys.includes(k))
143
143
  .map((k) => ({ key: k, label: k }))