@mirage-cli/contentdynamite-cli 1.0.0 → 1.1.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.
package/README.md CHANGED
@@ -37,7 +37,7 @@ Or pass a token per call: `--token ved_xxx` or `VE_DYNAMITE_TOKEN=ved_xxx`. Prec
37
37
  | `icp` | `show`, `export`, `regenerate`, `update` |
38
38
  | `categories` | `list`, `add` |
39
39
  | `design-system` | `show`, `set <profile-id> <zip>`, `rescan`, `clear` |
40
- | `articles` | `write`, `research-prompt`, `get`, `list`, `watch`, `update`, `delete`, `download`, `export`, `parts`, `reject`, `revisions`, `provenance-audit`, `clean-provenance`, `embed-audit`, `strip-embeds` |
40
+ | `articles` | `write`, `research-prompt`, `get`, `list`, `watch`, `update`, `delete`, `download`, `export-docs`, `export`, `parts`, `reject`, `revisions`, `provenance-audit`, `clean-provenance`, `embed-audit`, `strip-embeds` |
41
41
  | `revisions` | `list`, `get`, `watch`, `verdict` |
42
42
  | `batches` | `create`, `list`, `get`, `watch`, `delete` |
43
43
  | `gbp` | `write`, `batch`, `list`, `get`, `watch`, `update`, `regenerate`, `delete`, `image-options` |
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- run
4
- } from "./index-b00ea6js.js";
3
+ run2
4
+ } from "./index-3v4effdk.js";
5
5
 
6
6
  // src/bin.ts
7
- process.exitCode = await run(process.argv.slice(2));
7
+ process.exitCode = await run2(process.argv.slice(2));
package/dist/client.d.ts CHANGED
@@ -13,7 +13,7 @@ export declare class ApiClient {
13
13
  content: Uint8Array;
14
14
  filename: string | null;
15
15
  }>;
16
- post<T = unknown>(path: string, body?: unknown, params?: Params): Promise<T>;
16
+ post<T = unknown>(path: string, body?: unknown, params?: Params, timeoutMs?: number): Promise<T>;
17
17
  postForm<T = unknown>(path: string, file: Blob, filename: string, params?: Params): Promise<T>;
18
18
  put<T = unknown>(path: string, body?: unknown): Promise<T>;
19
19
  patch<T = unknown>(path: string, body?: unknown): Promise<T>;
@@ -1,6 +1,3 @@
1
- // src/version.ts
2
- var VERSION = "1.0.0";
3
-
4
1
  // src/program.ts
5
2
  import { Command, CommanderError as CommanderError2, Option as Option4 } from "commander";
6
3
 
@@ -217,45 +214,45 @@ function cleanTableSources(sources) {
217
214
  return pairs.length > 0 ? Object.fromEntries(pairs) : null;
218
215
  }
219
216
  function normaliseJobItem(item) {
220
- const out2 = { ...item, extra_guidelines: blankToNull(item.extra_guidelines), image_guideline: blankToNull(item.image_guideline) };
221
- if (out2.internal_links !== null) {
222
- const cleaned = cleanList(out2.internal_links);
223
- out2.internal_links = cleaned.length > 0 ? cleaned : null;
217
+ const out = { ...item, extra_guidelines: blankToNull(item.extra_guidelines), image_guideline: blankToNull(item.image_guideline) };
218
+ if (out.internal_links !== null) {
219
+ const cleaned = cleanList(out.internal_links);
220
+ out.internal_links = cleaned.length > 0 ? cleaned : null;
224
221
  }
225
- if (out2.external_links !== null) {
226
- const cleaned = cleanList(out2.external_links);
222
+ if (out.external_links !== null) {
223
+ const cleaned = cleanList(out.external_links);
227
224
  if (cleaned.length > 5)
228
225
  return "external_links: maximum 5 URLs allowed";
229
226
  const invalid = cleaned.find((url) => !isHttpUrl(url));
230
227
  if (invalid !== undefined)
231
228
  return `external_links: '${invalid}' is not an http(s) URL`;
232
- out2.external_links = cleaned.length > 0 ? cleaned : null;
229
+ out.external_links = cleaned.length > 0 ? cleaned : null;
233
230
  }
234
- if (out2.competitor_links !== null) {
235
- const cleaned = cleanList(out2.competitor_links);
231
+ if (out.competitor_links !== null) {
232
+ const cleaned = cleanList(out.competitor_links);
236
233
  if (cleaned.length > 3)
237
234
  return "competitor_links: maximum 3 URLs allowed";
238
- out2.competitor_links = cleaned.length > 0 ? cleaned : null;
235
+ out.competitor_links = cleaned.length > 0 ? cleaned : null;
239
236
  }
240
- out2.listicle_research = blankToNull(out2.listicle_research);
241
- if (out2.article_type === "listicle" && !out2.listicle_research)
237
+ out.listicle_research = blankToNull(out.listicle_research);
238
+ if (out.article_type === "listicle" && !out.listicle_research)
242
239
  return "listicle_research is required when article_type is 'listicle'";
243
- if (out2.item_count !== null && !(out2.item_count >= 3 && out2.item_count <= 10))
240
+ if (out.item_count !== null && !(out.item_count >= 3 && out.item_count <= 10))
244
241
  return "item_count must be between 3 and 10";
245
- out2.brand_facts = blankToNull(out2.brand_facts);
246
- if (out2.allowed_competitors !== null) {
247
- const cleaned = cleanList(out2.allowed_competitors);
242
+ out.brand_facts = blankToNull(out.brand_facts);
243
+ if (out.allowed_competitors !== null) {
244
+ const cleaned = cleanList(out.allowed_competitors);
248
245
  if (cleaned.length > 12)
249
246
  return "allowed_competitors: maximum 12 names allowed";
250
- out2.allowed_competitors = cleaned.length > 0 ? cleaned : null;
247
+ out.allowed_competitors = cleaned.length > 0 ? cleaned : null;
251
248
  }
252
- if (out2.table_sources !== null) {
253
- const sources = cleanTableSources(out2.table_sources);
249
+ if (out.table_sources !== null) {
250
+ const sources = cleanTableSources(out.table_sources);
254
251
  if (typeof sources === "string")
255
252
  return sources;
256
- out2.table_sources = sources;
253
+ out.table_sources = sources;
257
254
  }
258
- return out2;
255
+ return out;
259
256
  }
260
257
  var articleDto = articleJobItem.transform((item, context) => {
261
258
  const normalised = normaliseJobItem(item);
@@ -309,24 +306,24 @@ var gbpPostJobItem = z3.object({
309
306
  var TEXT_FIELDS = ["topic", "location", "facts", "guidelines", "image_url", "image_guideline"];
310
307
  var isHttpUrl2 = (url) => /^https?:\/\//.test(url.toLowerCase());
311
308
  function normaliseGbpJob(item) {
312
- const out2 = { ...item };
309
+ const out = { ...item };
313
310
  for (const field of TEXT_FIELDS) {
314
- const value = out2[field];
311
+ const value = out[field];
315
312
  if (value !== null)
316
- out2[field] = value.trim() || null;
313
+ out[field] = value.trim() || null;
317
314
  }
318
- if (!out2.topic && out2.article_id === null)
315
+ if (!out.topic && out.article_id === null)
319
316
  return "a post needs either a topic or an article_id to repurpose";
320
- const mode = out2.image_mode ?? (out2.image_url ? "supplied" : out2.article_id !== null ? "article" : "generate");
321
- if (mode === "supplied" && !out2.image_url)
317
+ const mode = out.image_mode ?? (out.image_url ? "supplied" : out.article_id !== null ? "article" : "generate");
318
+ if (mode === "supplied" && !out.image_url)
322
319
  return "image_mode 'supplied' needs an image_url";
323
- if (mode === "article" && out2.article_id === null)
320
+ if (mode === "article" && out.article_id === null)
324
321
  return "image_mode 'article' needs an article_id to take the image from";
325
- if (out2.image_url && !isHttpUrl2(out2.image_url))
326
- return `image_url: '${out2.image_url}' is not an http(s) URL`;
327
- if (out2.image_guideline && mode !== "generate")
322
+ if (out.image_url && !isHttpUrl2(out.image_url))
323
+ return `image_url: '${out.image_url}' is not an http(s) URL`;
324
+ if (out.image_guideline && mode !== "generate")
328
325
  return "image_guideline only applies when image_mode is 'generate'";
329
- return { ...out2, image_mode: mode };
326
+ return { ...out, image_mode: mode };
330
327
  }
331
328
  var gbpPostDto = gbpPostJobItem.transform((item, context) => {
332
329
  const normalised = normaliseGbpJob(item);
@@ -348,16 +345,16 @@ var updateGbpPostDto = z3.object({
348
345
  else if (dto.image_url != null && dto.image_url.trim() && !isHttpUrl2(dto.image_url.trim())) {
349
346
  context.addIssue({ code: "custom", message: `Value error, image_url: '${dto.image_url.trim()}' is not an http(s) URL` });
350
347
  } else {
351
- const out2 = {};
348
+ const out = {};
352
349
  if (dto.content != null)
353
- out2.content = dto.content;
350
+ out.content = dto.content;
354
351
  if (dto.status != null)
355
- out2.status = dto.status;
352
+ out.status = dto.status;
356
353
  if (dto.topic !== undefined)
357
- out2.topic = dto.topic;
354
+ out.topic = dto.topic;
358
355
  if (dto.image_url !== undefined)
359
- out2.image_url = dto.image_url?.trim() || null;
360
- return out2;
356
+ out.image_url = dto.image_url?.trim() || null;
357
+ return out;
361
358
  }
362
359
  return z3.NEVER;
363
360
  });
@@ -426,7 +423,7 @@ function quote(value) {
426
423
  function toCsv(jobs, required, optional) {
427
424
  const columns = [...required, ...optional.filter((field) => jobs.some((job) => job[field] != null))];
428
425
  const lines = [columns, ...jobs.map((job) => columns.map((column) => cell(job[column])))];
429
- return lines.map((line2) => line2.map(quote).join(",") + `
426
+ return lines.map((line) => line.map(quote).join(",") + `
430
427
  `).join("");
431
428
  }
432
429
  var ARTICLE_REQUIRED = ["company_profile_id", "search_query", "primary_keywords"];
@@ -534,8 +531,8 @@ function parseCsv(text) {
534
531
  return;
535
532
  }
536
533
  };
537
- for (const line2 of text.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
538
- for (const character of line2)
534
+ for (const line of text.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
535
+ for (const character of line)
539
536
  process2(character);
540
537
  process2(EOL);
541
538
  if (state === "start_record")
@@ -741,6 +738,11 @@ function registerArticles(program, cli) {
741
738
  const target = await downloadDocument(cli, `content-writing/article/${articleId}/export`, options.format, options.output, `article-${articleId}`);
742
739
  err(`exported article ${articleId} to ${target}`);
743
740
  });
741
+ articles.command("export-docs").description("Export a finished article to a Google Doc in your connected Google Drive (free); Dynamite then tracks the writer's edits and QA comments on it").argument("<ID>", "Article id", integer).option("--replace", "Overwrite the existing doc with the current article instead of linking to it").action(async (articleId, options) => {
742
+ const res = await cli.client().post(`content-writing/article/${articleId}/export-to-docs`, undefined, { replace: options.replace ? "true" : undefined });
743
+ err(`${res.created ? "exported" : options.replace ? "replaced" : "already exported"} article ${articleId} to ${res.folder_path}`);
744
+ out(res);
745
+ });
744
746
  articles.command("get").description("Show one article (slim {article_id, status} shape until the article completes)").argument("<ID>", "Article id", integer).action(async (articleId) => out(await cli.client().get(`content-writing/article/${articleId}`)));
745
747
  articles.command("list").description("List articles, newest first").option("--page <page>", "Page number", integer, 1).option("--limit <limit>", "Page size (max 100)", integer, 10).option("--company <name>", "Filter by exact company name").option("--status <status>", "Filter by status: pending, success, failed").option("--batch-id <id>", "Filter by batch id", integer).action(async (options) => {
746
748
  const status = wireStatus(options.status);
@@ -896,6 +898,10 @@ function describeDetail(text) {
896
898
  return detail;
897
899
  if (Array.isArray(detail))
898
900
  return detail.map((item) => `${(item.loc ?? []).slice(1).join(".")}: ${item.msg}`).join("; ");
901
+ if (detail && typeof detail === "object" && typeof detail.msg === "string") {
902
+ const { msg, authorize_url: url } = detail;
903
+ return typeof url === "string" && url ? `${msg} Open this link to connect: ${url}` : msg;
904
+ }
899
905
  return null;
900
906
  } catch {
901
907
  return null;
@@ -924,8 +930,8 @@ class ApiClient {
924
930
  const filename = /filename="([^"]+)"/.exec(response.headers.get("content-disposition") ?? "")?.[1] ?? null;
925
931
  return { content: new Uint8Array(await response.arrayBuffer()), filename };
926
932
  }
927
- post(path, body, params) {
928
- return this.json("POST", path, { body, params });
933
+ post(path, body, params, timeoutMs) {
934
+ return this.json("POST", path, { body, params, timeoutMs });
929
935
  }
930
936
  postForm(path, file, filename, params) {
931
937
  const form = new FormData;
@@ -944,14 +950,15 @@ class ApiClient {
944
950
  async json(method, path, payload) {
945
951
  return await (await this.request(method, path, payload)).json();
946
952
  }
947
- async request(method, path, { params, body, form }) {
953
+ async request(method, path, { params, body, form, timeoutMs }) {
948
954
  const headers = this.token ? { Authorization: `Bearer ${this.token}` } : {};
949
955
  let content = form;
950
956
  if (!form && method !== "GET" && method !== "DELETE") {
951
957
  headers["Content-Type"] = "application/json";
952
958
  content = body == null ? undefined : JSON.stringify(body);
953
959
  }
954
- const response = await fetch(this.url(path, params), { method, headers, body: content, redirect: "manual" });
960
+ const limit = timeoutMs ? { signal: AbortSignal.timeout(timeoutMs), timeout: false } : {};
961
+ const response = await fetch(this.url(path, params), { method, headers, body: content, redirect: "manual", ...limit });
955
962
  if (!response.ok)
956
963
  throw await apiError(response);
957
964
  return response;
@@ -1187,7 +1194,7 @@ function registerBatches(program, cli) {
1187
1194
  if (!cli.mount) {
1188
1195
  batches.command("watch").description("Poll a batch until every article is completed or failed").argument("<ID>", "Batch id", integer).option("--interval <seconds>", "Seconds between polls", decimal, 10).option("--timeout <seconds>", "Give up after this many seconds", decimal).action(async (batchId, options) => {
1189
1196
  const client = cli.client();
1190
- const settled = (batch2) => batch2.completed_articles + batch2.failed_articles;
1197
+ const settled = (batch) => batch.completed_articles + batch.failed_articles;
1191
1198
  const batch = await watchLoop({
1192
1199
  interval: options.interval,
1193
1200
  timeout: options.timeout,
@@ -1210,6 +1217,7 @@ function registerBatches(program, cli) {
1210
1217
 
1211
1218
  // src/commands/evals.ts
1212
1219
  var DEFAULT_THRESHOLD = 7;
1220
+ var JUDGED_QA_TIMEOUT_MS = 20 * 60 * 1000;
1213
1221
  var ORIGIN_ORDER = ["copy", "inputs", "age", "blocked"];
1214
1222
  var articleIds = (value, previous = []) => [...previous, integer(value)];
1215
1223
  function originLine(lost) {
@@ -1249,6 +1257,17 @@ function printPart(name, part) {
1249
1257
  for (const skipped of part.skipped)
1250
1258
  line(` skipped: ${skipped}`);
1251
1259
  }
1260
+ function printFormat(format) {
1261
+ line(` format ${format.format} code checks failing ${format.code.length}${format.rubric ? ` rubric ${format.rubric.passed}/${format.rubric.asked} passed` : ""}`);
1262
+ for (const failure of format.code)
1263
+ line(` - code ${failure.check.padEnd(24)} ${(failure.part ?? "-").padEnd(10)} ${failure.message}`);
1264
+ for (const failure of format.rubric?.failures ?? [])
1265
+ line(` - judge ${failure.check.padEnd(24)} ${(failure.part ?? "-").padEnd(10)} ${failure.message}`);
1266
+ if (format.rubric_error)
1267
+ err(` rubric did not run: ${format.rubric_error}`);
1268
+ for (const note of format.notes)
1269
+ line(` note: ${note}`);
1270
+ }
1252
1271
  function printReport(articleId, res) {
1253
1272
  const { content, image } = res.report;
1254
1273
  const blended = content.score_with_model ?? content.score;
@@ -1271,10 +1290,12 @@ function printReport(articleId, res) {
1271
1290
  printPart("craft", content.craft);
1272
1291
  if (image)
1273
1292
  printPart("image", image);
1293
+ if (res.report.format)
1294
+ printFormat(res.report.format);
1274
1295
  }
1275
1296
  function registerEvals(program, cli) {
1276
1297
  const evals = program.command("evals").description("Score finished articles against the deterministic quality checks");
1277
- evals.command("run").description("Score one or more articles. Free: no model is called.").argument("[ID...]", "Article ids. Omit with --all.", articleIds).option("--all", "Score every shipped article").option("--company <name>", "Only this company's articles (with --all)").option("--no-images", "Skip the per image fetch").option("--judge", "Also run the model judge. Costs a model call per article.").option("--store", "Keep the result on article.qa_report (the default)").option("--no-store", "Score without keeping the result").option("--threshold <score>", "Exit 1 if any article scores below this", decimal, DEFAULT_THRESHOLD).option("--limit <limit>", "Page size for --all", integer, 200).option("--json", "Emit the full report instead of a summary").action(async (ids, options) => {
1298
+ evals.command("run").description("Score one or more articles. Free: no model is called.").argument("[ID...]", "Article ids. Omit with --all.", articleIds).option("--all", "Score every shipped article").option("--company <name>", "Only this company's articles (with --all)").option("--no-images", "Skip the per image fetch").option("--judge", "Also run the model judge, and the format rubric on an article written in a format. Each costs a model call per article.").option("--store", "Keep the result on article.qa_report (the default)").option("--no-store", "Score without keeping the result").option("--threshold <score>", "Exit 1 if any article scores below this", decimal, DEFAULT_THRESHOLD).option("--limit <limit>", "Page size for --all", integer, 200).option("--json", "Emit the full report instead of a summary").action(async (ids, options) => {
1278
1299
  const articles = ids ?? [];
1279
1300
  if (options.all && articles.length > 0)
1280
1301
  throw new Error("pass article ids or --all, not both");
@@ -1297,16 +1318,13 @@ function registerEvals(program, cli) {
1297
1318
  const rows = [];
1298
1319
  try {
1299
1320
  for (const articleId of articles) {
1300
- const res = await client.post(`content-writing/article/${articleId}/qa`, undefined, {
1301
- model_checks: options.judge ? "true" : "false",
1302
- store: options.store === false ? "false" : "true"
1303
- });
1321
+ const res = await client.post(`content-writing/article/${articleId}/qa`, undefined, { model_checks: options.judge ? "true" : "false", store: options.store === false ? "false" : "true" }, options.judge ? JUDGED_QA_TIMEOUT_MS : undefined);
1304
1322
  rows.push({ article_id: articleId, score: res.score, report: res.report });
1305
1323
  if (!options.json)
1306
1324
  printReport(articleId, res);
1307
1325
  }
1308
1326
  } finally {
1309
- cli.billed(rows.filter((row) => row.report.content.judge).length);
1327
+ cli.billed(rows.filter((row) => row.report.content.judge).length + rows.filter((row) => row.report.format?.rubric).length);
1310
1328
  }
1311
1329
  if (options.json)
1312
1330
  out(rows);
@@ -2122,9 +2140,12 @@ function cliFor(program, { mount = false, reportCost }) {
2122
2140
  return cli;
2123
2141
  }
2124
2142
 
2143
+ // src/version.ts
2144
+ var VERSION2 = "1.1.0";
2145
+
2125
2146
  // src/program.ts
2126
- function buildProgram(options = {}) {
2127
- const program = new Command("ve-dynamite").description("Content Dynamite — content pipeline CLI").version(VERSION, "-V, --version", "Print the version and exit").option("--token <token>", "Access token override (or set VE_DYNAMITE_TOKEN env)").addOption(new Option4("--pretty", "Has no effect: output is always indented JSON. Kept so older scripts still run").hideHelp());
2147
+ function buildProgram2(options = {}) {
2148
+ const program = new Command("ve-dynamite").description("Content Dynamite — content pipeline CLI").version(VERSION2, "-V, --version", "Print the version and exit").option("--token <token>", "Access token override (or set VE_DYNAMITE_TOKEN env)").addOption(new Option4("--pretty", "Has no effect: output is always indented JSON. Kept so older scripts still run").hideHelp());
2128
2149
  if (!options.mount)
2129
2150
  program.option("--url <url>", "API base URL (or set VE_DYNAMITE_API_URL env)");
2130
2151
  program.exitOverride().configureOutput({ writeOut: (text) => io.stdout(text), writeErr: (text) => io.stderr(text) }).showHelpAfterError("(add --help for additional information)");
@@ -2143,9 +2164,9 @@ function buildProgram(options = {}) {
2143
2164
  registerUpload(program, cli);
2144
2165
  return program;
2145
2166
  }
2146
- async function run(argv, options = {}) {
2167
+ async function run2(argv, options = {}) {
2147
2168
  try {
2148
- await buildProgram(options).parseAsync(argv, { from: "user" });
2169
+ await buildProgram2(options).parseAsync(argv, { from: "user" });
2149
2170
  return 0;
2150
2171
  } catch (error) {
2151
2172
  if (error instanceof CommanderError2)
@@ -2155,4 +2176,4 @@ async function run(argv, options = {}) {
2155
2176
  }
2156
2177
  }
2157
2178
 
2158
- export { VERSION, buildProgram, run };
2179
+ export { VERSION2, buildProgram2, run2 };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
- VERSION,
3
- buildProgram,
4
- run
5
- } from "./index-b00ea6js.js";
2
+ VERSION2,
3
+ buildProgram2,
4
+ run2
5
+ } from "./index-3v4effdk.js";
6
6
  export {
7
- run,
8
- buildProgram,
9
- VERSION
7
+ VERSION2 as VERSION,
8
+ buildProgram2 as buildProgram,
9
+ run2 as run
10
10
  };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "1.0.0";
1
+ export declare const VERSION = "1.1.0";
package/dist/wire.d.ts CHANGED
@@ -24,8 +24,13 @@ export type CompanyProfileResponseDto = { id: number; company_name: string; webs
24
24
  type JsonObject = { [key: string]: unknown };
25
25
  type OwnerSummary = { username: string; profile: null | string };
26
26
  export type ArticleDoneRes = { article_id: number; status: 'success'; title: string; slug: string; meta_description: null | string; h1: null | string; intro: null | string; body: null | string; faqs: null | string; feat_image: null | string; is_internal_link_set: boolean; article_type: null | 'listicle'; video_url: null | string; video_status: null | 'pending' | 'done' | 'failed'; interactive_type: null | 'quiz' | 'game' | 'calculator' | 'chart'; interactive_status: null | 'pending' | 'done' | 'failed' | 'skipped'; interactive_payload: null | JsonObject; interactive_embed: null | string; gbp_content: null | string; gbp_image_url: null | string; gbp_status: null | 'pending' | 'done' | 'failed'; provenance_report: null | JsonObject; dewatermark_status: null | 'done' | 'failed' | 'skipped'; created_at: string; owner: null | OwnerSummary; gdoc_url: null | string; company_name: string };
27
+ export type ArticleGdocRes = { gdoc_id: string; gdoc_url: string; created: boolean; folder_path: string };
27
28
  export type ArticlePendingRes = { article_id: number; status: 'pending' | 'failed' };
28
- export type ArticleQaRes = { article_id: number; score: number; stored: boolean; report: { image: null; score: number; notes: string[]; article_id: null | number; title: null | string; article_type: null | string; company: string; content: { score_with_model: number; weights_applied: { code: number; model: number }; judge: null | { scores: Record<'answers_the_search' | 'helpful' | 'first_hand' | 'client_trust' | 'structure' | 'anti_slop' | 'faq_consistent' | 'specific_reader', number>; total: number; ceiling: number; score: number; verdict: string; violations: string[]; strengths: string[]; top_fixes: string[]; model: string; body_trimmed: boolean; thresholds: { pass_total: number; min_dimension: number; min_anti_slop: number } }; judge_error: null | string; score: number; blended: boolean; weights: { compliance: number; craft: number }; compliance: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] }; craft: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] } }; weights: { content: number; image: number }; model_checks: boolean } | { image: { scaled_score: number; name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] }; score: number; notes: string[]; article_id: null | number; title: null | string; article_type: null | string; company: string; content: { score_with_model: number; weights_applied: { code: number; model: number }; judge: null | { scores: Record<'answers_the_search' | 'helpful' | 'first_hand' | 'client_trust' | 'structure' | 'anti_slop' | 'faq_consistent' | 'specific_reader', number>; total: number; ceiling: number; score: number; verdict: string; violations: string[]; strengths: string[]; top_fixes: string[]; model: string; body_trimmed: boolean; thresholds: { pass_total: number; min_dimension: number; min_anti_slop: number } }; judge_error: null | string; score: number; blended: boolean; weights: { compliance: number; craft: number }; compliance: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] }; craft: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] } }; weights: { content: number; image: number }; model_checks: boolean } };
29
+ type CheckSource = 'code' | 'jev' | 'judge';
30
+ type ReportedFailure = { source: CheckSource; check: string; part: null | string; message: string };
31
+ type RubricScore = { asked: number; passed: number; failures: ReportedFailure[] };
32
+ type FormatEval = { format: 'listicle' | 'how-to' | 'answer' | 'explainer' | 'comparison' | 'troubleshooting' | 'cost'; code: ReportedFailure[]; rubric: null | RubricScore; rubric_error: null | string; notes: string[] };
33
+ export type ArticleQaRes = { article_id: number; score: number; stored: boolean; report: { image: null; score: number; notes: string[]; format?: undefined | FormatEval; article_id: null | number; title: null | string; article_type: null | string; company: string; content: { score_with_model: number; weights_applied: { code: number; model: number }; judge: null | { scores: Record<'answers_the_search' | 'helpful' | 'first_hand' | 'client_trust' | 'structure' | 'anti_slop' | 'faq_consistent' | 'specific_reader', number>; total: number; ceiling: number; score: number; verdict: string; violations: string[]; strengths: string[]; top_fixes: string[]; model: string; body_trimmed: boolean; thresholds: { pass_total: number; min_dimension: number; min_anti_slop: number } }; judge_error: null | string; score: number; blended: boolean; weights: { compliance: number; craft: number }; compliance: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] }; craft: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] } }; weights: { content: number; image: number }; model_checks: boolean } | { image: { scaled_score: number; name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] }; score: number; notes: string[]; format?: undefined | FormatEval; article_id: null | number; title: null | string; article_type: null | string; company: string; content: { score_with_model: number; weights_applied: { code: number; model: number }; judge: null | { scores: Record<'answers_the_search' | 'helpful' | 'first_hand' | 'client_trust' | 'structure' | 'anti_slop' | 'faq_consistent' | 'specific_reader', number>; total: number; ceiling: number; score: number; verdict: string; violations: string[]; strengths: string[]; top_fixes: string[]; model: string; body_trimmed: boolean; thresholds: { pass_total: number; min_dimension: number; min_anti_slop: number } }; judge_error: null | string; score: number; blended: boolean; weights: { compliance: number; craft: number }; compliance: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] }; craft: { name: string; budget: number; score: number; trusted: boolean; deductions: { points: number; code: string; origin: string; message: string; evidence: string }[]; skipped: string[] } }; weights: { content: number; image: number }; model_checks: boolean } };
29
34
  export type BatchRes = { batch_id: number; name: string; total_articles: number; completed_articles: number; failed_articles: number; created_at: string };
30
35
  export type BatchWriteRes = { msg: string; batch_id: number; article_ids: number[]; total: number };
31
36
  type CarrierCounts = { [key: string]: number };
@@ -56,8 +61,8 @@ type FaqContent = { html: string };
56
61
  type VideoContent = { url: string };
57
62
  type InteractiveContent = { type: 'quiz' | 'game' | 'calculator' | 'chart'; payload: JsonObject };
58
63
  type PartContent = IntroContent | SectionContent | ImageContent | FaqContent | VideoContent | InteractiveContent;
59
- export type RevisionDetailRes = { refused_report: null | JsonObject; replacement_report: null | JsonObject; revision_id: number; article_id: null | number; company_profile_id: null | number; part: 'intro' | 'section' | 'image' | 'feat_image' | 'faq' | 'video' | 'interactive'; index: null | number; target: null | 'section' | 'heading' | 'passage' | 'question' | 'answer'; text: null | string; reason: null | string; status: 'pending' | 'failed' | 'ready'; error: null | string; refused: PartContent; replacement: null | IntroContent | SectionContent | ImageContent | FaqContent | VideoContent | InteractiveContent; refused_score: null | number; replacement_score: null | number; verdict: null | 'better' | 'same' | 'worse'; verdict_note: null | string; applied: boolean; created_at: string; ready_at: null | string; decided_at: null | string };
60
- export type RevisionRes = { revision_id: number; article_id: null | number; company_profile_id: null | number; part: 'intro' | 'section' | 'image' | 'feat_image' | 'faq' | 'video' | 'interactive'; index: null | number; target: null | 'section' | 'heading' | 'passage' | 'question' | 'answer'; text: null | string; reason: null | string; status: 'pending' | 'failed' | 'ready'; error: null | string; refused: PartContent; replacement: null | IntroContent | SectionContent | ImageContent | FaqContent | VideoContent | InteractiveContent; refused_score: null | number; replacement_score: null | number; verdict: null | 'better' | 'same' | 'worse'; verdict_note: null | string; applied: boolean; created_at: string; ready_at: null | string; decided_at: null | string };
64
+ export type RevisionDetailRes = { refused_report: null | JsonObject; replacement_report: null | JsonObject; revision_id: number; article_id: null | number; company_profile_id: null | number; part: 'intro' | 'section' | 'image' | 'feat_image' | 'faq' | 'video' | 'interactive'; index: null | number; target: null | 'answer' | 'section' | 'heading' | 'passage' | 'question'; text: null | string; reason: null | string; status: 'pending' | 'failed' | 'ready'; error: null | string; refused: PartContent; replacement: null | IntroContent | SectionContent | ImageContent | FaqContent | VideoContent | InteractiveContent; refused_score: null | number; replacement_score: null | number; verdict: null | 'better' | 'same' | 'worse'; verdict_note: null | string; applied: boolean; created_at: string; ready_at: null | string; decided_at: null | string };
65
+ export type RevisionRes = { revision_id: number; article_id: null | number; company_profile_id: null | number; part: 'intro' | 'section' | 'image' | 'feat_image' | 'faq' | 'video' | 'interactive'; index: null | number; target: null | 'answer' | 'section' | 'heading' | 'passage' | 'question'; text: null | string; reason: null | string; status: 'pending' | 'failed' | 'ready'; error: null | string; refused: PartContent; replacement: null | IntroContent | SectionContent | ImageContent | FaqContent | VideoContent | InteractiveContent; refused_score: null | number; replacement_score: null | number; verdict: null | 'better' | 'same' | 'worse'; verdict_note: null | string; applied: boolean; created_at: string; ready_at: null | string; decided_at: null | string };
61
66
  export type VerdictRes = { revision: RevisionRes; applied: boolean; msg: string };
62
67
  export type SetRoleRes = { msg: string; user: UserRes };
63
68
  export type UserRes = { user_id: number; full_name: string; username: string; email: string; profile: null | string; role: 'admin' | 'normal'; is_verified: boolean; created_at: string };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirage-cli/contentdynamite-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "The Content Dynamite CLI (ve-dynamite): SEO articles, Google Business Profile posts, BOFU landing pages, company profiles, ICP, design systems, revisions and featured images over the Content Dynamite API.",
5
5
  "license": "MIT",
6
6
  "repository": {