@verbatims/cli 0.1.15 → 0.1.17

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,6 +19,19 @@ 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
+ # Manage provenance (moderator/admin API key)
27
+ verbatims quotes attributions list 123
28
+ verbatims quotes attributions create 123 --author-id 1 --reference-id 2 --status manually_verified --primary
29
+ verbatims quotes sources list 123
30
+ verbatims quotes sources create 123 --source-type book --source-url https://example.com/edition --status externally_verified
31
+
32
+ # Create a quote with a source
33
+ verbatims quotes create --source-type book --source-url https://example.com/edition
34
+
22
35
  # Browse interactively
23
36
  verbatims quotes browse
24
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbatims/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "CLI for the Verbatims quotes API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,16 @@ 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({
88
+ content: name as string,
89
+ name: name as string,
90
+ language: language as string,
91
+ provenance: {
92
+ author_id: authorId,
93
+ reference_id: referenceId,
94
+ source: options.sourceType ? { source_type: options.sourceType, source_url: options.sourceUrl } : undefined,
95
+ },
96
+ }),
84
97
  format,
85
98
  )
86
99
  console.log(chalk.green(' Quote created'))
@@ -94,6 +107,8 @@ export function registerQuotesCommand(program: Command) {
94
107
  .option('--language <lang>', 'Language')
95
108
  .option('--author-id <id>', 'Author ID')
96
109
  .option('--reference-id <id>', 'Reference ID')
110
+ .option('--source-type <type>', 'Source type')
111
+ .option('--source-url <url>', 'Source URL')
97
112
  .action(async (id: string, options: Record<string, string>) => {
98
113
  const client = await getClient()
99
114
  const format = getFormat(program)
@@ -101,8 +116,14 @@ export function registerQuotesCommand(program: Command) {
101
116
  const data_: Record<string, unknown> = {}
102
117
  if (options.name) { data_.name = options.name; data_.content = options.name }
103
118
  if (options.language) data_.language = options.language
104
- if (options.authorId !== undefined) data_.author_id = options.authorId === 'null' ? null : Number(options.authorId)
105
- if (options.referenceId !== undefined) data_.reference_id = options.referenceId === 'null' ? null : Number(options.referenceId)
119
+ if (options.authorId !== undefined) data_.provenance = { ...(data_.provenance as object), author_id: options.authorId === 'null' ? null : Number(options.authorId) }
120
+ if (options.referenceId !== undefined) data_.provenance = { ...(data_.provenance as object), reference_id: options.referenceId === 'null' ? null : Number(options.referenceId) }
121
+ if (options.sourceType !== undefined || options.sourceUrl !== undefined) {
122
+ data_.provenance = {
123
+ ...(data_.provenance as object),
124
+ source: { source_type: options.sourceType ?? 'manual', source_url: options.sourceUrl },
125
+ }
126
+ }
106
127
 
107
128
  const { data } = await withSpinner('Updating quote', () => client.quotes.update(Number(id), data_ as any), format)
108
129
  console.log(chalk.green(' Quote updated'))
@@ -172,6 +193,7 @@ export function registerQuotesCommand(program: Command) {
172
193
  .description('Browse quotes interactively')
173
194
  .option('--language <lang>', 'Filter by language')
174
195
  .option('--author <id>', 'Filter by author ID')
196
+ .option('--reference <id>', 'Filter by reference ID')
175
197
  .option('--tag <name>', 'Filter by tag')
176
198
  .option('--search <q>', 'Search in quote text')
177
199
  .option('--status <status>', 'Filter by status (draft|pending|approved|rejected)')
@@ -185,6 +207,7 @@ export function registerQuotesCommand(program: Command) {
185
207
  page,
186
208
  language: options.language,
187
209
  author_id: options.author ? Number(options.author) : undefined,
210
+ reference_id: options.reference ? Number(options.reference) : undefined,
188
211
  tag: options.tag,
189
212
  search: options.search,
190
213
  q: options.search,
@@ -196,6 +219,123 @@ export function registerQuotesCommand(program: Command) {
196
219
  )
197
220
  })
198
221
 
222
+ quotes
223
+ .command('attributions <action> <quoteId> [attributionId]')
224
+ .description('Manage quote attributions (moderator/admin API key required)')
225
+ .option('--author-id <id>', 'Author ID')
226
+ .option('--reference-id <id>', 'Reference ID')
227
+ .option('--status <status>', 'Verification status')
228
+ .option('--notes <text>', 'Internal notes')
229
+ .option('--primary', 'Set as primary attribution')
230
+ .option('--yes', 'Skip confirmation')
231
+ .action(async (action: string, quoteId: string, attributionId: string | undefined, options: Record<string, any>) => {
232
+ const client = await getClient()
233
+ const format = getFormat(program)
234
+ const id = Number(quoteId)
235
+
236
+ if (action === 'list') {
237
+ const { data } = await withSpinner('Fetching attributions', () => client.quotes.listAttributions(id), format)
238
+ output(data, format)
239
+ return
240
+ }
241
+ if (action === 'create') {
242
+ const { data } = await withSpinner('Creating attribution', () => client.quotes.createAttribution(id, {
243
+ author_id: options.authorId ? Number(options.authorId) : null,
244
+ reference_id: options.referenceId ? Number(options.referenceId) : null,
245
+ status: options.status,
246
+ notes: options.notes,
247
+ is_primary: options.primary,
248
+ }), format)
249
+ output(data, format)
250
+ return
251
+ }
252
+ if (!attributionId) throw new Error('Attribution ID is required')
253
+ const attribution = Number(attributionId)
254
+ if (action === 'update') {
255
+ const data_: Record<string, unknown> = {}
256
+ if (options.authorId !== undefined) data_.author_id = Number(options.authorId)
257
+ if (options.referenceId !== undefined) data_.reference_id = Number(options.referenceId)
258
+ if (options.status !== undefined) data_.status = options.status
259
+ if (options.notes !== undefined) data_.notes = options.notes
260
+ if (options.primary !== undefined) data_.is_primary = options.primary
261
+ const { data } = await withSpinner('Updating attribution', () => client.quotes.updateAttribution(id, attribution, data_), format)
262
+ output(data, format)
263
+ return
264
+ }
265
+ if (action === 'delete') {
266
+ if (!options.yes) {
267
+ const confirmed = await confirm({ message: `Delete attribution #${attribution} from quote #${id}?` })
268
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
269
+ }
270
+ await withSpinner('Deleting attribution', () => client.quotes.deleteAttribution(id, attribution), format)
271
+ console.log(chalk.green(' Attribution deleted'))
272
+ return
273
+ }
274
+ throw new Error('Action must be list, create, update, or delete')
275
+ })
276
+
277
+ quotes
278
+ .command('sources <action> <quoteId> [sourceId]')
279
+ .description('Manage quote sources (moderator/admin API key required)')
280
+ .option('--attribution-id <id>', 'Attribution ID')
281
+ .option('--source-type <type>', 'Source type')
282
+ .option('--source-url <url>', 'Source URL')
283
+ .option('--label <label>', 'Source label')
284
+ .option('--status <status>', 'Verification status')
285
+ .option('--notes <text>', 'Internal notes')
286
+ .option('--primary', 'Set as primary source')
287
+ .option('--yes', 'Skip confirmation')
288
+ .action(async (action: string, quoteId: string, sourceId: string | undefined, options: Record<string, any>) => {
289
+ const client = await getClient()
290
+ const format = getFormat(program)
291
+ const id = Number(quoteId)
292
+
293
+ if (action === 'list') {
294
+ const { data } = await withSpinner('Fetching sources', () => client.quotes.listSources(id), format)
295
+ output(data, format)
296
+ return
297
+ }
298
+ if (action === 'create') {
299
+ if (!options.sourceType) throw new Error('--source-type is required')
300
+ const { data } = await withSpinner('Creating source', () => client.quotes.createSource(id, {
301
+ attribution_id: options.attributionId ? Number(options.attributionId) : null,
302
+ source_type: options.sourceType,
303
+ source_url: options.sourceUrl,
304
+ label: options.label,
305
+ verification_status: options.status,
306
+ notes: options.notes,
307
+ is_primary: options.primary,
308
+ }), format)
309
+ output(data, format)
310
+ return
311
+ }
312
+ if (!sourceId) throw new Error('Source ID is required')
313
+ const source = Number(sourceId)
314
+ if (action === 'update') {
315
+ const data_: Record<string, unknown> = {}
316
+ if (options.attributionId !== undefined) data_.attribution_id = Number(options.attributionId)
317
+ if (options.sourceType !== undefined) data_.source_type = options.sourceType
318
+ if (options.sourceUrl !== undefined) data_.source_url = options.sourceUrl
319
+ if (options.label !== undefined) data_.label = options.label
320
+ if (options.status !== undefined) data_.verification_status = options.status
321
+ if (options.notes !== undefined) data_.notes = options.notes
322
+ if (options.primary !== undefined) data_.is_primary = options.primary
323
+ const { data } = await withSpinner('Updating source', () => client.quotes.updateSource(id, source, data_), format)
324
+ output(data, format)
325
+ return
326
+ }
327
+ if (action === 'delete') {
328
+ if (!options.yes) {
329
+ const confirmed = await confirm({ message: `Delete source #${source} from quote #${id}?` })
330
+ if (isCancel(confirmed) || !confirmed) cancel('Cancelled')
331
+ }
332
+ await withSpinner('Deleting source', () => client.quotes.deleteSource(id, source), format)
333
+ console.log(chalk.green(' Source deleted'))
334
+ return
335
+ }
336
+ throw new Error('Action must be list, create, update, or delete')
337
+ })
338
+
199
339
  const tags = quotes.command('tags').description('Manage quote tags')
200
340
 
201
341
  tags
@@ -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', 'platform', 'label', 'enabled', 'status', 'position', 'quote_text', 'quote_language', 'author_name', 'reference_name', 'post_text', 'post_url', 'posted_at', 'error_message']
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 }))