@ainyc/canonry 4.64.1 → 4.67.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.
@@ -524,4 +524,33 @@ cnry agent detach <project> --format json # JSON output
524
524
 
525
525
  ## Output Formats
526
526
 
527
- Most commands support `--format json` for machine-readable output.
527
+ Every command takes `--format`:
528
+
529
+ - **`text`** (default) — human-readable, decorated. Not a stable parse target.
530
+ - **`json`** — one pretty-printed JSON document (the full envelope). Stable contract.
531
+ - **`jsonl`** — newline-delimited JSON: the command's **primary collection**, one self-contained record per line. The agent-friendly machine format — no envelope key to guess (`.checks` vs `.results` vs `.rows`), no `jq` flattening, greppable line by line.
532
+
533
+ `jsonl` is supported by every **collection** command — one whose primary output is a list: `insights`, `runs`, `evidence`, `history`, `query/keyword/competitor list`, `notify list/events`, `google` reads (`performance`, `performance-daily`, `inspections`, `coverage-history`, `deindexed`, `status`, `properties`, `list-sitemaps`), `bing` reads (`coverage-history`, `inspections`, `performance`, `sites`), `ga` reads (`ai-referral-history`, `social-referral-history`, `session-history`, `coverage`), `traffic events/sources/status`, `discover list/show`, `content targets/sources/gaps`, `backlinks list/releases`, `project list/locations`, `agent memory list`, `agent providers`, and `doctor`.
534
+
535
+ Each `jsonl` line re-injects the envelope context it would otherwise lose, so a line lifted out still self-describes:
536
+
537
+ - project-scoped lists stamp `{ "project": "<name>", …row }`;
538
+ - `ga *-history` also stamps `window`; `traffic events` stamps `windowStart`/`windowEnd`; `backlinks list` stamps `release`/`targetDomain`; `discover show` stamps `sessionId`; `content targets` stamps `latestRunId`; `project locations` stamps `isDefault`;
539
+ - global lists whose rows already self-identify (`project list`, `notify events`, `backlinks releases`) emit bare rows.
540
+
541
+ Empty collection → **no output** (the exit code still conveys success, so "no records" stays distinct from "failure"). On failure a command prints its records (if any), then exits non-zero — branch on the **exit code**, never on parsing stderr. JSON field names and the `{ "error": { "code", "message" } }` envelope are a public contract.
542
+
543
+ **Composite** commands return a single aggregate object (not a list), so there is nothing to stream — on them `--format jsonl` **degrades to the same JSON document** as `--format json`; it never falls through to decorated human text. So `--format jsonl` is safe to pass to *any* command: collection commands stream their records, every other command emits its JSON document. Composite shapes are below.
544
+
545
+ ## Output schema per command
546
+
547
+ Compact reference for the composite / keyed commands agents read most (shapes can drift — the linked DTO source file is the source of truth; collection commands simply emit their primary array, see each command's own section above).
548
+
549
+ | Command | JSON output shape (top-level keys → DTO) | `jsonl` |
550
+ |---|---|---|
551
+ | `cnry doctor [--project p] [--all]` | `{ scope, project, generatedAt, durationMs, summary{total,ok,warn,fail,skipped}, checks[] }` — `DoctorReportDto` @ `contracts/doctor.ts`. `checks[]` = `CheckResultDto{ id, category, scope, title, status(ok\|warn\|fail\|skipped), code, summary, remediation?, details?, durationMs }`. With `--all`: an object keyed by `__global__` + each project name, each value a full report. | ✅ one check / line as `{project, …check}`; still exits non-zero if any `fail` |
552
+ | `cnry analytics <p> [--feature metrics\|gaps\|sources] [--window 7d\|30d\|90d\|all]` | Object **keyed by feature**: `{ metrics?, gaps?, sources? }` (all three present with no `--feature`; one with `--feature X`). `metrics`=`BrandMetricsDto{ window, buckets[], overall, byProvider, trend, mentionTrend, queryChanges[] }`; `gaps`=`GapAnalysisDto{ cited[], gap[], uncited[], mentionedQueries[], mentionGap[], notMentioned[], runId, window }` (each `[]`=`GapQuery`); `sources`=`SourceBreakdownDto{ overall[], byQuery, runId, window }`. @ `contracts/analytics.ts` | → degrades to the `json` document |
553
+ | `cnry google coverage <p>` (index coverage) | `{ summary{total,indexed,notIndexed,deindexed,percentage}, lastInspectedAt, lastSyncedAt, indexed[], notIndexed[], deindexed[], reasonGroups[] }` — `GscCoverageSummaryDto` @ `contracts/google.ts`. `indexed[]`/`notIndexed[]`=`GscUrlInspectionDto`, `deindexed[]`=`GscDeindexedRowDto`. | → degrades to the `json` document. The single-array reads `google inspections` / `coverage-history` / `deindexed` **stream** `jsonl`. |
554
+ | `cnry ga traffic <p> [--window …]` | Object summary — `GA4TrafficSummaryDto` / `GaTrafficResponse` @ `contracts/ga.ts`: `{ totalSessions, totalOrganicSessions, totalDirectSessions, totalUsers, aiSessionsDeduped, aiUsersDeduped, aiSessionsBySession, aiUsersBySession, socialSessions, socialUsers, channelBreakdown{organic,social,direct,ai,other→{sessions,sharePct,sharePctDisplay}}, *SharePct (+ `*Display`), topPages[], aiReferrals[], aiReferralLandingPages[], socialReferrals[], lastSyncedAt, periodStart, periodEnd }`. | → degrades to the `json` document |
555
+ | `cnry ga attribution <p> [--trend]` | Object — a **renamed projection** of `GaTrafficResponse` (⚠️ field names differ from the DTO): `aiSessions`(←`aiSessionsDeduped`), `organicSessions`(←`totalOrganicSessions`), `directSessions`(←`totalDirectSessions`), plus `totalSessions, totalUsers, aiUsers, aiSessionsBySession, aiUsersBySession, socialSessions, socialUsers, {ai,social,organic,direct}SharePct (+ `*Display`), otherSessions, otherSharePct, channelBreakdown, aiReferrals[], aiReferralLandingPages[], socialReferrals[], periodStart, periodEnd`. With `--trend`: drops `periodStart/End`, adds `trend` (`GaAttributionTrendResponse`). Assembled inline in `commands/ga.ts`. | → degrades to the `json` document |
556
+ | `cnry gbp summary <p> [--location …]` | `{ scope{locationName,locationCount}, performance{totals,recent7d,prior7d,deltaPct} (metric-keyed maps; keys are raw `BUSINESS_*` / `WEBSITE_CLICKS` tokens — label via `formatGbpMetricLabel`), freshness{dataThroughDate,latestStoredDate,pendingDays}, timeseries[], keywords{total,thresholdedCount,thresholdedPct}, placeActions{total,hasReservationCta,hasBookingCta,hasDirectMerchantCta}, lodging{lodgingLocationCount,populatedLodgingCount,emptyLodgingCount} }` — `GbpSummaryDto` @ `contracts/gbp.ts`. `timeseries[]`=`{date,pending,metrics}`. | → degrades to the `json` document |
@@ -5,10 +5,11 @@ import {
5
5
  canonryMcpTools,
6
6
  configExists,
7
7
  getConfigPath,
8
+ isMachineFormat,
8
9
  loadConfig,
9
10
  loadConfigRaw,
10
11
  saveConfigPatch
11
- } from "./chunk-64IDABSF.js";
12
+ } from "./chunk-RQCVITY4.js";
12
13
  import {
13
14
  CC_CACHE_DIR,
14
15
  DUCKDB_SPEC,
@@ -5296,7 +5297,7 @@ async function backfillAnswerVisibilityCommand(opts) {
5296
5297
  result.dryRun = true;
5297
5298
  result.wouldUpdate = wouldUpdate;
5298
5299
  }
5299
- if (opts?.format === "json") {
5300
+ if (isMachineFormat(opts?.format)) {
5300
5301
  console.log(JSON.stringify(result, null, 2));
5301
5302
  return;
5302
5303
  }
@@ -5366,7 +5367,7 @@ async function backfillNormalizedPathsCommand(opts) {
5366
5367
  updated: 0,
5367
5368
  unchanged: 0
5368
5369
  };
5369
- if (opts?.format === "json") {
5370
+ if (isMachineFormat(opts?.format)) {
5370
5371
  console.log(JSON.stringify(result2, null, 2));
5371
5372
  return;
5372
5373
  }
@@ -5382,7 +5383,7 @@ async function backfillNormalizedPathsCommand(opts) {
5382
5383
  updated,
5383
5384
  unchanged
5384
5385
  };
5385
- if (opts?.format === "json") {
5386
+ if (isMachineFormat(opts?.format)) {
5386
5387
  console.log(JSON.stringify(result, null, 2));
5387
5388
  return;
5388
5389
  }
@@ -5438,7 +5439,7 @@ async function backfillAiReferralPathsCommand(opts) {
5438
5439
  updated: 0,
5439
5440
  unchanged: 0
5440
5441
  };
5441
- if (opts?.format === "json") {
5442
+ if (isMachineFormat(opts?.format)) {
5442
5443
  console.log(JSON.stringify(result2, null, 2));
5443
5444
  return;
5444
5445
  }
@@ -5454,7 +5455,7 @@ async function backfillAiReferralPathsCommand(opts) {
5454
5455
  updated,
5455
5456
  unchanged
5456
5457
  };
5457
- if (opts?.format === "json") {
5458
+ if (isMachineFormat(opts?.format)) {
5458
5459
  console.log(JSON.stringify(result, null, 2));
5459
5460
  return;
5460
5461
  }
@@ -5579,7 +5580,7 @@ async function backfillAnswerMentionsCommand(opts) {
5579
5580
  result.dryRun = true;
5580
5581
  result.wouldUpdate = wouldUpdate;
5581
5582
  }
5582
- if (opts?.format === "json") {
5583
+ if (isMachineFormat(opts?.format)) {
5583
5584
  console.log(JSON.stringify(result, null, 2));
5584
5585
  return;
5585
5586
  }
@@ -5621,7 +5622,7 @@ async function backfillInsightsCommand(project, opts) {
5621
5622
  const db = createClient(config.database);
5622
5623
  migrate(db);
5623
5624
  const service = new IntelligenceService2(db);
5624
- const isJson = opts?.format === "json";
5625
+ const isJson = isMachineFormat(opts?.format);
5625
5626
  const isDryRun = opts?.dryRun === true;
5626
5627
  if (!isJson) {
5627
5628
  const scope = opts?.since ? ` (since ${opts.since})` : "";
@@ -5779,7 +5780,7 @@ async function backfillSnapshotAttributionCommand(opts) {
5779
5780
  if (!project) {
5780
5781
  throw new Error(`Project "${opts.project}" not found`);
5781
5782
  }
5782
- const isJson = opts.format === "json";
5783
+ const isJson = isMachineFormat(opts.format);
5783
5784
  const isDryRun = opts.dryRun === true;
5784
5785
  if (!isJson) {
5785
5786
  const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
@@ -5958,7 +5959,7 @@ async function backfillTrafficClassificationCommand(opts) {
5958
5959
  migrate(db);
5959
5960
  const projectFilter = opts?.project?.trim();
5960
5961
  const isDryRun = opts?.dryRun === true;
5961
- const isJson = opts?.format === "json";
5962
+ const isJson = isMachineFormat(opts?.format);
5962
5963
  const scopedProjects = projectFilter ? db.select().from(projects).where(eq10(projects.name, projectFilter)).all() : db.select().from(projects).all();
5963
5964
  if (scopedProjects.length === 0) {
5964
5965
  if (projectFilter && !isJson) {
@@ -6395,7 +6396,7 @@ async function installSkills(opts = {}) {
6395
6396
  }
6396
6397
  async function listSkills(opts = {}) {
6397
6398
  const skills = getBundledSkills();
6398
- if (opts.format === "json") {
6399
+ if (isMachineFormat(opts.format)) {
6399
6400
  console.log(JSON.stringify({
6400
6401
  skills: skills.map((s) => ({
6401
6402
  name: s.name,
@@ -6416,7 +6417,7 @@ async function listSkills(opts = {}) {
6416
6417
  }
6417
6418
  }
6418
6419
  function emitInstallSummary(summary, format) {
6419
- if (format === "json") {
6420
+ if (isMachineFormat(format)) {
6420
6421
  console.log(JSON.stringify(summary, null, 2));
6421
6422
  return;
6422
6423
  }
@@ -190,6 +190,9 @@ function configExists() {
190
190
  }
191
191
 
192
192
  // src/cli-error.ts
193
+ function isMachineFormat(format) {
194
+ return format === "json" || format === "jsonl";
195
+ }
193
196
  var EXIT_USER_ERROR = 1;
194
197
  var EXIT_SYSTEM_ERROR = 2;
195
198
  var CliError = class extends Error {
@@ -221,36 +224,9 @@ function isEndpointMissing(err) {
221
224
  return status === 404 || status === 405;
222
225
  }
223
226
  function printCliError(err, format) {
224
- if (format === "json") {
225
- if (err instanceof CliError) {
226
- console.error(
227
- JSON.stringify(
228
- {
229
- error: {
230
- code: err.code,
231
- message: err.message,
232
- ...err.details ? { details: err.details } : {}
233
- }
234
- },
235
- null,
236
- 2
237
- )
238
- );
239
- return;
240
- }
241
- const message = err instanceof Error ? err.message : "An unexpected error occurred";
242
- console.error(
243
- JSON.stringify(
244
- {
245
- error: {
246
- code: "CLI_ERROR",
247
- message
248
- }
249
- },
250
- null,
251
- 2
252
- )
253
- );
227
+ if (isMachineFormat(format)) {
228
+ const envelope = err instanceof CliError ? { error: { code: err.code, message: err.message, ...err.details ? { details: err.details } : {} } } : { error: { code: "CLI_ERROR", message: err instanceof Error ? err.message : "An unexpected error occurred" } };
229
+ console.error(JSON.stringify(envelope, null, format === "jsonl" ? 0 : 2));
254
230
  return;
255
231
  }
256
232
  if (err instanceof CliError && err.displayMessage) {
@@ -6256,6 +6232,7 @@ export {
6256
6232
  saveConfig,
6257
6233
  saveConfigPatch,
6258
6234
  configExists,
6235
+ isMachineFormat,
6259
6236
  EXIT_USER_ERROR,
6260
6237
  EXIT_SYSTEM_ERROR,
6261
6238
  CliError,