@canonry/canonry 4.118.0 → 4.123.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.
Files changed (34) hide show
  1. package/README.md +9 -0
  2. package/assets/agent-workspace/skills/canonry/SKILL.md +3 -0
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +135 -28
  4. package/assets/assets/{AuditHistoryPanel-Yh7VEe4P.js → AuditHistoryPanel-BVQMDIhQ.js} +1 -1
  5. package/assets/assets/{BacklinksPage-BmO77kMs.js → BacklinksPage-nrHT9VMG.js} +1 -1
  6. package/assets/assets/ChartPrimitives-_8qLsQzI.js +1 -0
  7. package/assets/assets/{HistoryPage-DkLV_iRc.js → HistoryPage-DqsKKdyy.js} +1 -1
  8. package/assets/assets/ProjectPage-GyKoQUv_.js +7 -0
  9. package/assets/assets/{RunRow-BDH9agxQ.js → RunRow-BRZZcSjQ.js} +1 -1
  10. package/assets/assets/{RunsPage-D9YsEeT3.js → RunsPage-24aI8DKF.js} +1 -1
  11. package/assets/assets/{SettingsPage-o5as3Ixw.js → SettingsPage-BaNeTG1i.js} +1 -1
  12. package/assets/assets/{TrafficPage-BCK203aQ.js → TrafficPage-ByvcLSjv.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-DsVMAZvS.js → TrafficSourceDetailPage-B-siTXVg.js} +1 -1
  14. package/assets/assets/{arrow-left-B7ZR6VC9.js → arrow-left-CBh1cZNH.js} +1 -1
  15. package/assets/assets/{extract-error-message-DeYDUpew.js → extract-error-message-DJZCPudR.js} +1 -1
  16. package/assets/assets/index-jymsMBpZ.css +1 -0
  17. package/assets/assets/index-mzLganDP.js +210 -0
  18. package/assets/assets/{trash-2-xxHTqWbF.js → trash-2-Bh-Yvkgc.js} +1 -1
  19. package/assets/assets/vendor-recharts-DSxTZhaH.js +36 -0
  20. package/assets/index.html +3 -3
  21. package/dist/{chunk-CMZIPFNQ.js → chunk-4AZ7VES6.js} +4915 -719
  22. package/dist/{chunk-75UBTCKS.js → chunk-HJIIJI7K.js} +321 -6
  23. package/dist/{chunk-HGNY5GL4.js → chunk-JYBNBK25.js} +273 -37
  24. package/dist/{chunk-KEZWLP4O.js → chunk-MMFSI3OG.js} +3281 -2341
  25. package/dist/cli.js +554 -35
  26. package/dist/index.js +4 -4
  27. package/dist/{intelligence-service-FWPXMAVK.js → intelligence-service-K35EN43I.js} +2 -2
  28. package/dist/mcp.js +4 -4
  29. package/package.json +7 -7
  30. package/assets/assets/ChartPrimitives-H0ICVpw_.js +0 -1
  31. package/assets/assets/ProjectPage-R3odr_eA.js +0 -7
  32. package/assets/assets/index-Bot6YiD6.css +0 -1
  33. package/assets/assets/index-QCFQv0xh.js +0 -210
  34. package/assets/assets/vendor-recharts-C9EZkgbP.js +0 -36
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  setTelemetrySource,
30
30
  showFirstRunNotice,
31
31
  trackEvent
32
- } from "./chunk-HGNY5GL4.js";
32
+ } from "./chunk-JYBNBK25.js";
33
33
  import {
34
34
  CliError,
35
35
  EXIT_SYSTEM_ERROR,
@@ -46,7 +46,7 @@ import {
46
46
  saveConfig,
47
47
  saveConfigPatch,
48
48
  usageError
49
- } from "./chunk-75UBTCKS.js";
49
+ } from "./chunk-HJIIJI7K.js";
50
50
  import {
51
51
  apiKeys,
52
52
  createClient,
@@ -54,8 +54,11 @@ import {
54
54
  projects,
55
55
  queries,
56
56
  renderReportHtml
57
- } from "./chunk-CMZIPFNQ.js";
57
+ } from "./chunk-4AZ7VES6.js";
58
58
  import {
59
+ AdsOperationKinds,
60
+ AdsOperationStates,
61
+ AdsOperationStepStates,
59
62
  BacklinkSources,
60
63
  CcReleaseSyncStatuses,
61
64
  CheckScopes,
@@ -65,15 +68,19 @@ import {
65
68
  REPORT_PERIOD_OPTIONS,
66
69
  RunStatuses,
67
70
  TrafficEventKinds,
71
+ adsActivateTreeRequestSchema,
72
+ adsActivationGrantCreateRequestSchema,
68
73
  adsAdCreateRequestSchema,
69
74
  adsAdGroupCreateRequestSchema,
70
75
  adsAdGroupUpdateRequestSchema,
71
76
  adsAdUpdateRequestSchema,
72
77
  adsCampaignCreateRequestSchema,
73
78
  adsCampaignUpdateRequestSchema,
79
+ adsGeoSearchQuerySchema,
74
80
  adsImageUploadRequestSchema,
75
81
  adsPauseRequestSchema,
76
82
  backlinkSourceSchema,
83
+ buildModelChangeNotice,
77
84
  discoveryBucketSchema,
78
85
  discoveryCompetitorTypeSchema,
79
86
  effectiveDomains,
@@ -88,7 +95,7 @@ import {
88
95
  providerQuotaPolicySchema,
89
96
  resolveProviderInput,
90
97
  winnabilityClassSchema
91
- } from "./chunk-KEZWLP4O.js";
98
+ } from "./chunk-MMFSI3OG.js";
92
99
 
93
100
  // src/cli.ts
94
101
  import { pathToFileURL } from "url";
@@ -2746,20 +2753,54 @@ function readRequest(inputPath, schema) {
2746
2753
  });
2747
2754
  }
2748
2755
  }
2756
+ function printOperationDetails(operation) {
2757
+ console.log(`Operation: ${operation.operationKey}`);
2758
+ if (operation.entityId) console.log(`Entity: ${operation.entityType ?? "unknown"} ${operation.entityId}`);
2759
+ if (operation.upstreamUpdatedAt != null) console.log(`Updated: ${operation.upstreamUpdatedAt}`);
2760
+ if (operation.errorCode) console.log(`Error: ${operation.errorCode}: ${operation.errorMessage ?? ""}`);
2761
+ if (operation.state === AdsOperationStates.pending || operation.state === AdsOperationStates.unknown || operation.state === AdsOperationStates.reconciling) {
2762
+ if (operation.kind === AdsOperationKinds.campaign_tree_activate) {
2763
+ console.log("Do not retry with a new operation key. Resume activation recovery for the original receipt instead.");
2764
+ } else {
2765
+ console.log("Do not retry with a new operation key. Reconcile the original receipt instead.");
2766
+ }
2767
+ }
2768
+ }
2749
2769
  function printOperation(result, format) {
2750
2770
  if (isMachineFormat(format)) {
2751
2771
  console.log(JSON.stringify(result, null, 2));
2752
2772
  return;
2753
2773
  }
2754
- const operation = result.operation;
2755
- console.log(`${result.replayed ? "Replayed" : "Recorded"} ${operation.kind}: ${operation.state}`);
2756
- console.log(`Operation: ${operation.operationKey}`);
2757
- if (operation.entityId) console.log(`Entity: ${operation.entityType ?? "unknown"} ${operation.entityId}`);
2758
- if (operation.upstreamUpdatedAt != null) console.log(`Updated: ${operation.upstreamUpdatedAt}`);
2759
- if (operation.errorCode) console.log(`Error: ${operation.errorCode}: ${operation.errorMessage ?? ""}`);
2760
- if (operation.state === "unknown") {
2761
- console.log("Do not retry with a new operation key. Reconcile this outcome with a human first.");
2774
+ console.log(`${result.replayed ? "Replayed" : "Recorded"} ${result.operation.kind}: ${result.operation.state}`);
2775
+ printOperationDetails(result.operation);
2776
+ }
2777
+ function printReconciliation(result, format) {
2778
+ if (isMachineFormat(format)) {
2779
+ console.log(JSON.stringify(result, null, 2));
2780
+ return;
2781
+ }
2782
+ console.log(`${result.resolved ? "Resolved" : "Still unresolved"} ${result.operation.kind}: ${result.operation.state}`);
2783
+ printOperationDetails(result.operation);
2784
+ }
2785
+ function printActivationGrant(result, verb, format) {
2786
+ if (isMachineFormat(format)) {
2787
+ console.log(JSON.stringify(result, null, 2));
2788
+ return;
2762
2789
  }
2790
+ const cancellationRequested = verb === "Revoked" && result.grant.revocationRequestedAt !== null;
2791
+ console.log(cancellationRequested ? `Cancellation requested for activation grant ${result.grant.id}: ${result.grant.state}` : `${verb} activation grant ${result.grant.id}: ${result.grant.state}`);
2792
+ if (cancellationRequested) console.log(`Requested: ${result.grant.revocationRequestedAt}`);
2793
+ console.log(`Manifest: ${result.grant.manifestHash}`);
2794
+ console.log(`Expires: ${result.grant.expiresAt}`);
2795
+ }
2796
+ function printActivation(result, format) {
2797
+ if (isMachineFormat(format)) {
2798
+ console.log(JSON.stringify(result, null, 2));
2799
+ return;
2800
+ }
2801
+ const activeSteps = result.steps.filter((step) => step.state === AdsOperationStepStates.active).length;
2802
+ console.log(`Activation ${result.operation.operationKey}: ${result.operation.state}`);
2803
+ console.log(`Steps: ${activeSteps}/${result.steps.length} active`);
2763
2804
  }
2764
2805
  function describeConnection(status) {
2765
2806
  const lines = [];
@@ -2813,9 +2854,155 @@ async function adsStatus(project, opts) {
2813
2854
  console.log("Connect with: canonry ads connect " + project + " --api-key <sdk-key>");
2814
2855
  }
2815
2856
  }
2857
+ async function adsAccount(project, opts) {
2858
+ const result = await getClient5().getAdsAccount(project);
2859
+ if (isMachineFormat(opts?.format)) {
2860
+ console.log(JSON.stringify(result, null, 2));
2861
+ return;
2862
+ }
2863
+ console.log(`Account: ${result.name}`);
2864
+ console.log(`ID: ${result.id}`);
2865
+ console.log(`Status: ${result.status}`);
2866
+ console.log(`Currency: ${result.currencyCode ?? "unknown"}`);
2867
+ console.log(`Timezone: ${result.timezone ?? "unknown"}`);
2868
+ console.log(`Review: ${result.reviewStatus ?? "unknown"}`);
2869
+ console.log(`Integrity: ${result.integrityReviewStatus ?? "unknown"}`);
2870
+ if (result.integrityDecision) console.log(`Decision: ${result.integrityDecision}`);
2871
+ if (result.url) console.log(`Ads Manager: ${result.url}`);
2872
+ }
2873
+ async function adsGeoSearch(project, opts) {
2874
+ const parsed = adsGeoSearchQuerySchema.safeParse({ q: opts.q, limit: opts.limit });
2875
+ if (!parsed.success) {
2876
+ throw new CliError({
2877
+ code: "ADS_GEO_QUERY_INVALID",
2878
+ message: "A valid geo search query is required",
2879
+ displayMessage: "Error: --query is required and --limit must be an integer from 1 to 100",
2880
+ details: {
2881
+ project,
2882
+ issues: parsed.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
2883
+ }
2884
+ });
2885
+ }
2886
+ const query = parsed.data;
2887
+ const result = await getClient5().searchAdsGeo(project, query);
2888
+ if (opts.format === "jsonl") {
2889
+ emitJsonl(result.results.map((location) => ({ project, query: result.query, ...location })));
2890
+ return;
2891
+ }
2892
+ if (isMachineFormat(opts.format)) {
2893
+ console.log(JSON.stringify(result, null, 2));
2894
+ return;
2895
+ }
2896
+ if (result.results.length === 0) {
2897
+ console.log(`No OpenAI Ads locations matched "${result.query}".`);
2898
+ return;
2899
+ }
2900
+ for (const location of result.results) {
2901
+ const region = location.regionCode ? `, ${location.regionCode}` : "";
2902
+ console.log(`${location.canonicalName} (${location.type}, ${location.countryCode}${region}) [${location.id}]`);
2903
+ }
2904
+ }
2905
+ async function adsConversionPixels(project, opts) {
2906
+ const result = await getClient5().getAdsConversionPixels(project);
2907
+ if (opts?.format === "jsonl") {
2908
+ emitJsonl(result.pixels.map((pixel) => ({ project, ...pixel })));
2909
+ return;
2910
+ }
2911
+ if (isMachineFormat(opts?.format)) {
2912
+ console.log(JSON.stringify(result, null, 2));
2913
+ return;
2914
+ }
2915
+ if (result.pixels.length === 0) {
2916
+ console.log("No OpenAI Ads conversion pixels are configured.");
2917
+ return;
2918
+ }
2919
+ for (const pixel of result.pixels) {
2920
+ const name = pixel.name ?? "Unnamed conversion pixel";
2921
+ const clientType = pixel.clientType ?? "unknown client";
2922
+ const pixelId = pixel.pixelId ? ` pixel ${pixel.pixelId}` : "";
2923
+ console.log(`${name} (${clientType}) [${pixel.id}]${pixelId}`);
2924
+ }
2925
+ }
2926
+ async function adsConversionEventSettings(project, opts) {
2927
+ const result = await getClient5().getAdsConversionEventSettings(project);
2928
+ if (opts?.format === "jsonl") {
2929
+ emitJsonl(result.eventSettings.map((eventSetting) => ({ project, ...eventSetting })));
2930
+ return;
2931
+ }
2932
+ if (isMachineFormat(opts?.format)) {
2933
+ console.log(JSON.stringify(result, null, 2));
2934
+ return;
2935
+ }
2936
+ if (result.eventSettings.length === 0) {
2937
+ console.log("No OpenAI Ads conversion event settings are configured.");
2938
+ return;
2939
+ }
2940
+ for (const eventSetting of result.eventSettings) {
2941
+ const name = eventSetting.name ?? "Unnamed conversion event";
2942
+ const eventType = eventSetting.eventType ?? "unknown event";
2943
+ const attribution = eventSetting.attributionWindowDays === void 0 ? "unknown attribution window" : `${eventSetting.attributionWindowDays}d attribution`;
2944
+ const sources = eventSetting.sources?.map((source) => source.name ?? source.id).join(", ") || "no source details";
2945
+ const archived = eventSetting.archived === true ? " [archived]" : "";
2946
+ console.log(
2947
+ `${name}${archived}: ${eventType}, ${attribution}, ${sources} [${eventSetting.id}]`
2948
+ );
2949
+ }
2950
+ }
2816
2951
  async function adsOperationGet(project, opts) {
2817
2952
  printOperation(await getClient5().getAdsOperation(project, opts.operationKey), opts.format);
2818
2953
  }
2954
+ async function adsOperationsUnresolved(project, opts) {
2955
+ const query = {
2956
+ state: opts?.state,
2957
+ limit: opts?.limit,
2958
+ cursor: opts?.cursor
2959
+ };
2960
+ const client = getClient5();
2961
+ const result = Object.values(query).some((value) => value !== void 0) ? await client.getUnresolvedAdsOperations(project, query) : await client.getUnresolvedAdsOperations(project);
2962
+ if (opts?.format === "jsonl") {
2963
+ emitJsonl(result.operations.map((operation) => ({ project, ...operation })));
2964
+ return;
2965
+ }
2966
+ if (isMachineFormat(opts?.format)) {
2967
+ console.log(JSON.stringify(result, null, 2));
2968
+ return;
2969
+ }
2970
+ if (result.operations.length === 0) {
2971
+ console.log("No unresolved OpenAI Ads mutation receipts.");
2972
+ return;
2973
+ }
2974
+ console.log("STATE KIND OPERATION KEY");
2975
+ for (const operation of result.operations) {
2976
+ console.log(`${operation.state.padEnd(13)} ${operation.kind.padEnd(21)} ${operation.operationKey}`);
2977
+ }
2978
+ if (result.nextCursor) console.log(`Next cursor: ${result.nextCursor}`);
2979
+ }
2980
+ async function adsOperationReconcile(project, opts) {
2981
+ printReconciliation(
2982
+ await getClient5().reconcileAdsOperation(project, opts.operationKey),
2983
+ opts.format
2984
+ );
2985
+ }
2986
+ async function adsOperationResumeActivation(project, opts) {
2987
+ printActivation(
2988
+ await getClient5().resumeAdsActivation(project, opts.operationKey),
2989
+ opts.format
2990
+ );
2991
+ }
2992
+ async function adsActivationGrantCreate(project, opts) {
2993
+ const request = readRequest(
2994
+ opts.input,
2995
+ adsActivationGrantCreateRequestSchema
2996
+ );
2997
+ printActivationGrant(await getClient5().createAdsActivationGrant(project, request), "Approved", opts.format);
2998
+ }
2999
+ async function adsActivationGrantRevoke(project, grantId, opts) {
3000
+ printActivationGrant(
3001
+ await getClient5().revokeAdsActivationGrant(project, grantId),
3002
+ "Revoked",
3003
+ opts?.format
3004
+ );
3005
+ }
2819
3006
  async function adsImageUpload(project, opts) {
2820
3007
  const request = readRequest(opts.input, adsImageUploadRequestSchema);
2821
3008
  printOperation(await getClient5().uploadAdsImage(project, request), opts.format);
@@ -2832,6 +3019,10 @@ async function adsCampaignPause(project, campaignId, opts) {
2832
3019
  const request = readRequest(opts.input, adsPauseRequestSchema);
2833
3020
  printOperation(await getClient5().pauseAdsCampaign(project, campaignId, request), opts.format);
2834
3021
  }
3022
+ async function adsCampaignActivateTree(project, campaignId, opts) {
3023
+ const request = readRequest(opts.input, adsActivateTreeRequestSchema);
3024
+ printActivation(await getClient5().activateAdsCampaignTree(project, campaignId, request), opts.format);
3025
+ }
2835
3026
  async function adsAdGroupCreate(project, opts) {
2836
3027
  const request = readRequest(opts.input, adsAdGroupCreateRequestSchema);
2837
3028
  printOperation(await getClient5().createAdsAdGroup(project, request), opts.format);
@@ -2973,6 +3164,87 @@ var ADS_CLI_COMMANDS = [
2973
3164
  await adsStatus(project, { format: input.format });
2974
3165
  }
2975
3166
  },
3167
+ {
3168
+ path: ["ads", "account"],
3169
+ usage: "canonry ads account <project> [--format json]",
3170
+ run: async (input) => {
3171
+ const project = requireProject(input, "ads.account", "canonry ads account <project> [--format json]");
3172
+ await adsAccount(project, { format: input.format });
3173
+ }
3174
+ },
3175
+ {
3176
+ path: ["ads", "geo", "search"],
3177
+ usage: "canonry ads geo search <project> --query <text> [--limit <n>] [--format json|jsonl]",
3178
+ options: {
3179
+ query: stringOption(),
3180
+ limit: stringOption()
3181
+ },
3182
+ run: async (input) => {
3183
+ const usage = "canonry ads geo search <project> --query <text> [--limit <n>] [--format json|jsonl]";
3184
+ const project = requireProject(input, "ads.geo.search", usage);
3185
+ await adsGeoSearch(project, {
3186
+ q: getString(input.values, "query"),
3187
+ limit: parseIntegerOption(input, "limit", {
3188
+ command: "ads.geo.search",
3189
+ message: "--limit must be an integer from 1 to 100",
3190
+ usage
3191
+ }),
3192
+ format: input.format
3193
+ });
3194
+ }
3195
+ },
3196
+ {
3197
+ path: ["ads", "conversions", "pixels"],
3198
+ usage: "canonry ads conversions pixels <project> [--format json|jsonl]",
3199
+ run: async (input) => {
3200
+ const project = requireProject(input, "ads.conversions.pixels", "canonry ads conversions pixels <project> [--format json|jsonl]");
3201
+ await adsConversionPixels(project, { format: input.format });
3202
+ }
3203
+ },
3204
+ {
3205
+ path: ["ads", "conversions", "event-settings"],
3206
+ usage: "canonry ads conversions event-settings <project> [--format json|jsonl]",
3207
+ run: async (input) => {
3208
+ const project = requireProject(input, "ads.conversions.event-settings", "canonry ads conversions event-settings <project> [--format json|jsonl]");
3209
+ await adsConversionEventSettings(project, { format: input.format });
3210
+ }
3211
+ },
3212
+ {
3213
+ path: ["ads", "operations", "unresolved"],
3214
+ usage: "canonry ads operations unresolved <project> [--state <csv>] [--limit <n>] [--cursor <opaque>] [--format json|jsonl]",
3215
+ options: {
3216
+ state: stringOption(),
3217
+ limit: stringOption(),
3218
+ cursor: stringOption()
3219
+ },
3220
+ run: async (input) => {
3221
+ const usage = "canonry ads operations unresolved <project> [--state <csv>] [--limit <n>] [--cursor <opaque>] [--format json|jsonl]";
3222
+ const project = requireProject(
3223
+ input,
3224
+ "ads.operations.unresolved",
3225
+ usage
3226
+ );
3227
+ const rawState = getString(input.values, "state");
3228
+ const state = rawState === void 0 ? void 0 : rawState.split(",").filter((value) => value === "pending" || value === "unknown" || value === "reconciling");
3229
+ if (rawState !== void 0 && state?.length !== rawState.split(",").length) {
3230
+ throw usageError(`Error: --state must contain only pending, unknown, or reconciling
3231
+ Usage: ${usage}`, {
3232
+ message: "--state must contain only pending, unknown, or reconciling",
3233
+ details: { command: "ads.operations.unresolved", usage }
3234
+ });
3235
+ }
3236
+ await adsOperationsUnresolved(project, {
3237
+ state,
3238
+ limit: parseIntegerOption(input, "limit", {
3239
+ command: "ads.operations.unresolved",
3240
+ message: "--limit must be an integer from 1 to 200",
3241
+ usage
3242
+ }),
3243
+ cursor: getString(input.values, "cursor"),
3244
+ format: input.format
3245
+ });
3246
+ }
3247
+ },
2976
3248
  {
2977
3249
  path: ["ads", "operation"],
2978
3250
  usage: "canonry ads operation <project> <operation-key> [--format json]",
@@ -2987,6 +3259,70 @@ var ADS_CLI_COMMANDS = [
2987
3259
  await adsOperationGet(project, { operationKey, format: input.format });
2988
3260
  }
2989
3261
  },
3262
+ {
3263
+ path: ["ads", "operation", "reconcile"],
3264
+ usage: "canonry ads operation reconcile <project> --operation-key <key> [--format json]",
3265
+ options: {
3266
+ "operation-key": stringOption()
3267
+ },
3268
+ run: async (input) => {
3269
+ const usage = "canonry ads operation reconcile <project> --operation-key <key> [--format json]";
3270
+ const project = requireProject(input, "ads.operation.reconcile", usage);
3271
+ const operationKey = requireStringOption(input, "operation-key", {
3272
+ command: "ads.operation.reconcile",
3273
+ usage,
3274
+ message: "--operation-key is required"
3275
+ });
3276
+ await adsOperationReconcile(project, {
3277
+ operationKey,
3278
+ format: input.format
3279
+ });
3280
+ }
3281
+ },
3282
+ {
3283
+ path: ["ads", "operation", "resume-activation"],
3284
+ usage: "canonry ads operation resume-activation <project> --operation-key <key> [--format json]",
3285
+ options: { "operation-key": stringOption() },
3286
+ run: async (input) => {
3287
+ const usage = "canonry ads operation resume-activation <project> --operation-key <key> [--format json]";
3288
+ const project = requireProject(input, "ads.operation.resume-activation", usage);
3289
+ await adsOperationResumeActivation(project, {
3290
+ operationKey: requireStringOption(input, "operation-key", {
3291
+ command: "ads.operation.resume-activation",
3292
+ message: "--operation-key is required",
3293
+ usage
3294
+ }),
3295
+ format: input.format
3296
+ });
3297
+ }
3298
+ },
3299
+ {
3300
+ path: ["ads", "activation-grant", "create"],
3301
+ usage: "canonry ads activation-grant create <project> --input <json-file|-> [--format json]",
3302
+ options: { input: stringOption() },
3303
+ run: async (input) => {
3304
+ const usage = "canonry ads activation-grant create <project> --input <json-file|-> [--format json]";
3305
+ const project = requireProject(input, "ads.activation-grant.create", usage);
3306
+ await adsActivationGrantCreate(project, {
3307
+ input: getString(input.values, "input"),
3308
+ format: input.format
3309
+ });
3310
+ }
3311
+ },
3312
+ {
3313
+ path: ["ads", "activation-grant", "revoke"],
3314
+ usage: "canonry ads activation-grant revoke <project> <grant-id> [--format json]",
3315
+ run: async (input) => {
3316
+ const usage = "canonry ads activation-grant revoke <project> <grant-id> [--format json]";
3317
+ const project = requireProject(input, "ads.activation-grant.revoke", usage);
3318
+ const grantId = requirePositional(input, 1, {
3319
+ command: "ads.activation-grant.revoke",
3320
+ usage,
3321
+ message: "activation grant id is required"
3322
+ });
3323
+ await adsActivationGrantRevoke(project, grantId, { format: input.format });
3324
+ }
3325
+ },
2990
3326
  {
2991
3327
  path: ["ads", "image", "upload"],
2992
3328
  usage: "canonry ads image upload <project> --input <json-file|-> [--format json]",
@@ -3016,6 +3352,24 @@ var ADS_CLI_COMMANDS = [
3016
3352
  await adsCampaignUpdate(project, id, { input: getString(input.values, "input"), format: input.format });
3017
3353
  }
3018
3354
  },
3355
+ {
3356
+ path: ["ads", "campaign", "activate-tree"],
3357
+ usage: "canonry ads campaign activate-tree <project> <campaign-id> --input <json-file|-> [--format json]",
3358
+ options: { input: stringOption() },
3359
+ run: async (input) => {
3360
+ const usage = "canonry ads campaign activate-tree <project> <campaign-id> --input <json-file|-> [--format json]";
3361
+ const project = requireProject(input, "ads.campaign.activate-tree", usage);
3362
+ const id = requirePositional(input, 1, {
3363
+ command: "ads.campaign.activate-tree",
3364
+ usage,
3365
+ message: "campaign id is required"
3366
+ });
3367
+ await adsCampaignActivateTree(project, id, {
3368
+ input: getString(input.values, "input"),
3369
+ format: input.format
3370
+ });
3371
+ }
3372
+ },
3019
3373
  {
3020
3374
  path: ["ads", "campaign", "pause"],
3021
3375
  usage: "canonry ads campaign pause <project> <campaign-id> --input <json-file|-> [--format json]",
@@ -7634,11 +7988,29 @@ async function showAnalytics(project, options) {
7634
7988
  console.log(JSON.stringify(results, null, 2));
7635
7989
  }
7636
7990
  }
7991
+ var BUCKET_DATE_WIDTH = 35;
7992
+ function bucketDates(bucket) {
7993
+ const legacyCompatible = bucket;
7994
+ const start = legacyCompatible.dataStartDate?.slice(0, 10);
7995
+ const end = legacyCompatible.dataEndDate?.slice(0, 10);
7996
+ if (!start || !end) return "date unavailable";
7997
+ const sweeps = legacyCompatible.sweepCount ?? 1;
7998
+ const pooled = sweeps > 1 ? ` (${sweeps} sweeps)` : "";
7999
+ if (start === end) return `${start}${pooled}`;
8000
+ return `${start} \u2192 ${end}${pooled}`;
8001
+ }
7637
8002
  function printMetrics(data) {
7638
8003
  console.log(`
7639
8004
  Citation Rate Trends (${data.window})`);
7640
8005
  console.log("\u2500".repeat(50));
7641
8006
  const pct4 = (n) => `${(n * 100).toFixed(1)}%`;
8007
+ const modelChangeNotice = buildModelChangeNotice(readModelPointerChanges(data));
8008
+ if (modelChangeNotice?.kind === "change") {
8009
+ console.log(`
8010
+ Model Updates Behind These Numbers:`);
8011
+ console.log(` ${modelChangeNotice.text}`);
8012
+ console.log("");
8013
+ }
7642
8014
  console.log(` Overall: ${pct4(data.overall.citationRate)} (${data.overall.cited}/${data.overall.total})`);
7643
8015
  console.log(` Trend: ${data.trend}`);
7644
8016
  if (Object.keys(data.byProvider).length > 0) {
@@ -7650,28 +8022,95 @@ Citation Rate Trends (${data.window})`);
7650
8022
  }
7651
8023
  if (data.buckets.length > 0) {
7652
8024
  console.log(`
7653
- Timeline:`);
8025
+ Timeline (dates in UTC):`);
7654
8026
  for (const bucket of data.buckets) {
7655
- const start = bucket.startDate.slice(0, 10);
7656
8027
  const bar = bucket.total > 0 ? "\u2588".repeat(Math.round(bucket.citationRate * 20)) : "";
7657
- console.log(` ${start} ${pct4(bucket.citationRate).padStart(6)} ${bar}`);
8028
+ console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${pct4(bucket.citationRate).padStart(6)} ${bar}`);
7658
8029
  }
7659
8030
  }
7660
8031
  const providersInBuckets = [...new Set(data.buckets.flatMap((b) => Object.keys(b.byProvider ?? {})))].sort();
7661
8032
  if (data.buckets.length > 0 && providersInBuckets.length > 0) {
7662
8033
  console.log(`
7663
- By Provider Timeline:`);
8034
+ By Provider Timeline (dates in UTC):`);
7664
8035
  for (const provider of providersInBuckets) {
7665
8036
  console.log(` ${provider}:`);
7666
8037
  for (const bucket of data.buckets) {
7667
8038
  const metric = bucket.byProvider?.[provider];
7668
8039
  if (!metric) continue;
7669
- const start = bucket.startDate.slice(0, 10);
7670
8040
  const bar = metric.total > 0 ? "\u2588".repeat(Math.round(metric.citationRate * 20)) : "";
7671
- console.log(` ${start} ${pct4(metric.citationRate).padStart(6)} ${bar}`);
8041
+ console.log(` ${bucketDates(bucket).padEnd(BUCKET_DATE_WIDTH)} ${pct4(metric.citationRate).padStart(6)} ${bar}`);
8042
+ }
8043
+ }
8044
+ }
8045
+ if (modelChangeNotice?.kind === "no-known-change") {
8046
+ console.log(`
8047
+ Model Updates Behind These Numbers:`);
8048
+ console.log(` ${modelChangeNotice.text}`);
8049
+ console.log(` ${modelChangeNotice.detail}`);
8050
+ }
8051
+ const attributionEntries = Object.entries(readModelAttribution(data)).sort(([a], [b]) => a.localeCompare(b));
8052
+ if (attributionEntries.length > 0) {
8053
+ console.log(`
8054
+ Model Evidence:`);
8055
+ for (const [provider, attribution] of attributionEntries) {
8056
+ const latest = attribution.latestObservation;
8057
+ console.log(` ${provider}: latest ${formatModelEvidence(latest.state)} at ${latest.observedAt}`);
8058
+ for (const event of attribution.events) {
8059
+ const dating = event.fromPreWindowAnchor ? " (on or before)" : "";
8060
+ const priorSweep = event.fromPreWindowAnchor && event.anchorObservedAt ? ` [last seen ${formatModelEvidence(event.from)} on ${event.anchorObservedAt}]` : "";
8061
+ console.log(` ${event.observedAt}${dating} ${formatModelEvidence(event.from)} \u2192 ${formatModelEvidence(event.to)}${priorSweep}`);
8062
+ }
8063
+ const eventTotal = attribution.eventTotal ?? attribution.events.length;
8064
+ if (eventTotal > attribution.events.length) {
8065
+ console.log(` Showing the latest ${attribution.events.length} of ${eventTotal} model changes.`);
8066
+ }
8067
+ if (attribution.anchorUnavailable) {
8068
+ console.log(` We did not look far enough back to be sure this is every change.`);
7672
8069
  }
7673
8070
  }
7674
8071
  }
8072
+ const servedEntries = Object.entries(readServedModelAttribution(data)).sort(([a], [b]) => a.localeCompare(b));
8073
+ if (servedEntries.length > 0) {
8074
+ const mismatch = readModelServiceMismatch(data);
8075
+ console.log(`
8076
+ What the Engines Answered With:`);
8077
+ for (const [provider, served] of servedEntries) {
8078
+ const rawIds = served.latestServedModelIds.length > 0 ? served.latestServedModelIds.join(", ") : formatModelEvidence(served.latestObservation.state);
8079
+ const substituted = mismatch[provider];
8080
+ const note = substituted ? ` \u2014 not the ${formatModelEvidence(substituted.configured)} you selected` : "";
8081
+ console.log(` ${provider}: ${rawIds} at ${served.latestObservation.observedAt}${note}`);
8082
+ for (const event of served.events) {
8083
+ const dating = event.fromPreWindowAnchor ? " (on or before)" : "";
8084
+ console.log(` ${event.observedAt}${dating} ${formatModelEvidence(event.from)} \u2192 ${formatModelEvidence(event.to)}`);
8085
+ }
8086
+ }
8087
+ }
8088
+ }
8089
+ function readServedModelAttribution(data) {
8090
+ const legacyCompatible = data;
8091
+ return legacyCompatible.servedModelAttribution ?? {};
8092
+ }
8093
+ function readModelServiceMismatch(data) {
8094
+ const legacyCompatible = data;
8095
+ return legacyCompatible.modelServiceMismatch ?? {};
8096
+ }
8097
+ function readModelPointerChanges(data) {
8098
+ const legacyCompatible = data;
8099
+ return legacyCompatible.modelPointerChanges ?? {};
8100
+ }
8101
+ function readModelAttribution(data) {
8102
+ const legacyCompatible = data;
8103
+ return legacyCompatible.modelAttribution ?? {};
8104
+ }
8105
+ function formatModelEvidence(state) {
8106
+ switch (state.status) {
8107
+ case "known":
8108
+ return `known ${state.model}`;
8109
+ case "unknown":
8110
+ return "unknown";
8111
+ case "mixed":
8112
+ return `mixed ${state.models.join(", ")}${state.includesUnknown ? " + unknown" : ""}`;
8113
+ }
7675
8114
  }
7676
8115
  function printGaps(data) {
7677
8116
  console.log(`
@@ -8128,7 +8567,9 @@ async function createProject(name, opts) {
8128
8567
  ownedDomains: opts.ownedDomains ?? [],
8129
8568
  aliases: normalizeProjectAliases(opts.displayName, opts.aliases ?? []),
8130
8569
  country: opts.country,
8131
- language: opts.language
8570
+ language: opts.language,
8571
+ providers: opts.providers ?? [],
8572
+ providerModels: opts.providerModels ?? {}
8132
8573
  });
8133
8574
  if (isMachineFormat(opts.format)) {
8134
8575
  console.log(JSON.stringify(result, null, 2));
@@ -8191,6 +8632,9 @@ async function showProject(name, format) {
8191
8632
  console.log(` Language: ${project.language}`);
8192
8633
  console.log(` Config source: ${project.configSource}`);
8193
8634
  console.log(` Config revision: ${project.configRevision}`);
8635
+ console.log(` Providers: ${(project.providers ?? []).length > 0 ? project.providers.join(", ") : "all configured"}`);
8636
+ const providerModels = Object.entries(project.providerModels ?? {});
8637
+ console.log(` Model overrides: ${providerModels.length > 0 ? providerModels.map(([provider, model]) => `${provider}=${model}`).join(", ") : "(none; instance settings inherited)"}`);
8194
8638
  console.log(` Tags: ${project.tags.length > 0 ? project.tags.join(", ") : "(none)"}`);
8195
8639
  const labelEntries = Object.entries(project.labels);
8196
8640
  console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
@@ -8220,19 +8664,45 @@ async function updateProjectSettings(name, opts) {
8220
8664
  const toRemove = new Set(opts.removeAlias.map((a) => a.toLowerCase()));
8221
8665
  aliases = aliases.filter((a) => !toRemove.has(a.toLowerCase()));
8222
8666
  }
8667
+ const providerModels = { ...project.providerModels ?? {}, ...opts.providerModels ?? {} };
8668
+ for (const provider of opts.clearProviderModels ?? []) delete providerModels[provider];
8669
+ const nextProviders = opts.providers ?? project.providers ?? [];
8670
+ if (nextProviders.length > 0) {
8671
+ const requested = Object.keys(opts.providerModels ?? {}).filter((p) => !nextProviders.includes(p));
8672
+ if (requested.length > 0) {
8673
+ throw usageError(
8674
+ `Error: --provider-model set for ${requested.join(", ")}, which ${requested.length > 1 ? "are" : "is"} not a selected engine for "${name}"`,
8675
+ {
8676
+ message: "provider-model set for a provider the project does not run",
8677
+ details: { command: "project.update", project: name, providers: nextProviders, unselected: requested }
8678
+ }
8679
+ );
8680
+ }
8681
+ }
8223
8682
  const result = await client.putProject(name, {
8224
8683
  displayName: nextDisplayName,
8225
8684
  canonicalDomain: opts.domain ?? project.canonicalDomain,
8226
8685
  ownedDomains,
8227
8686
  aliases: normalizeProjectAliases(nextDisplayName, aliases),
8228
8687
  country: opts.country ?? project.country,
8229
- language: opts.language ?? project.language
8688
+ language: opts.language ?? project.language,
8689
+ tags: project.tags,
8690
+ labels: project.labels,
8691
+ providers: opts.providers ?? project.providers,
8692
+ providerModels,
8693
+ locations: project.locations,
8694
+ defaultLocation: project.defaultLocation,
8695
+ autoExtractBacklinks: project.autoExtractBacklinks
8230
8696
  });
8231
8697
  if (isMachineFormat(opts.format)) {
8232
8698
  console.log(JSON.stringify(result, null, 2));
8233
8699
  return;
8234
8700
  }
8235
8701
  console.log(`Project updated: ${result.name}`);
8702
+ const dropped = Object.keys(providerModels).filter((provider) => !(provider in result.providerModels));
8703
+ if (dropped.length > 0) {
8704
+ console.log(` Dropped model override for deselected engine(s): ${dropped.join(", ")}`);
8705
+ }
8236
8706
  }
8237
8707
  async function deleteProject(name, opts) {
8238
8708
  const client = getClient20();
@@ -8336,20 +8806,22 @@ async function setDefaultLocation(project, label, format) {
8336
8806
  var PROJECT_CLI_COMMANDS = [
8337
8807
  {
8338
8808
  path: ["project", "create"],
8339
- usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]",
8809
+ usage: "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--format json]",
8340
8810
  options: {
8341
8811
  domain: { type: "string", short: "d" },
8342
8812
  "owned-domain": multiStringOption(),
8343
8813
  alias: multiStringOption(),
8344
8814
  country: stringOption(),
8345
8815
  language: stringOption(),
8346
- "display-name": stringOption()
8816
+ "display-name": stringOption(),
8817
+ provider: multiStringOption(),
8818
+ "provider-model": multiStringOption()
8347
8819
  },
8348
8820
  run: async (input) => {
8349
8821
  const name = requireProject(
8350
8822
  input,
8351
8823
  "project.create",
8352
- "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]"
8824
+ "canonry project create <name> [--domain <domain>] [--owned-domain <domain>...] [--alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--provider-model provider=model...] [--format json]"
8353
8825
  );
8354
8826
  await createProject(name, {
8355
8827
  domain: getString(input.values, "domain") ?? name,
@@ -8358,13 +8830,15 @@ var PROJECT_CLI_COMMANDS = [
8358
8830
  country: getString(input.values, "country") ?? "US",
8359
8831
  language: getString(input.values, "language") ?? "en",
8360
8832
  displayName: getString(input.values, "display-name") ?? name,
8833
+ providers: getStringArray(input.values, "provider") ?? [],
8834
+ providerModels: parseProviderModelAssignments(getStringArray(input.values, "provider-model")),
8361
8835
  format: input.format
8362
8836
  });
8363
8837
  }
8364
8838
  },
8365
8839
  {
8366
8840
  path: ["project", "update"],
8367
- usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]",
8841
+ usage: "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--format json]",
8368
8842
  options: {
8369
8843
  domain: { type: "string", short: "d" },
8370
8844
  "owned-domain": multiStringOption(),
@@ -8375,14 +8849,26 @@ var PROJECT_CLI_COMMANDS = [
8375
8849
  "remove-alias": multiStringOption(),
8376
8850
  country: stringOption(),
8377
8851
  language: stringOption(),
8378
- "display-name": stringOption()
8852
+ "display-name": stringOption(),
8853
+ provider: multiStringOption(),
8854
+ "all-providers": { type: "boolean" },
8855
+ "provider-model": multiStringOption(),
8856
+ "clear-provider-model": multiStringOption()
8379
8857
  },
8380
8858
  run: async (input) => {
8381
8859
  const name = requireProject(
8382
8860
  input,
8383
8861
  "project.update",
8384
- "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--format json]"
8862
+ "canonry project update <name> [--domain <domain>] [--owned-domain <domain>...] [--add-domain <domain>...] [--remove-domain <domain>...] [--alias <name>...] [--add-alias <name>...] [--remove-alias <name>...] [--country <code>] [--language <lang>] [--display-name <name>] [--provider <name>...] [--all-providers] [--provider-model provider=model...] [--clear-provider-model <provider>...] [--format json]"
8385
8863
  );
8864
+ const providers = getStringArray(input.values, "provider");
8865
+ const allProviders = getBoolean(input.values, "all-providers");
8866
+ if (allProviders && providers?.length) throw usageError("Error: --all-providers conflicts with --provider");
8867
+ const providerModels = parseProviderModelAssignments(getStringArray(input.values, "provider-model"));
8868
+ const clearProviderModels = getStringArray(input.values, "clear-provider-model") ?? [];
8869
+ for (const provider of clearProviderModels) {
8870
+ if (provider in providerModels) throw usageError(`Error: --provider-model and --clear-provider-model conflict for ${provider}`);
8871
+ }
8386
8872
  await updateProjectSettings(name, {
8387
8873
  displayName: getString(input.values, "display-name"),
8388
8874
  domain: getString(input.values, "domain"),
@@ -8394,6 +8880,9 @@ var PROJECT_CLI_COMMANDS = [
8394
8880
  removeAlias: getStringArray(input.values, "remove-alias"),
8395
8881
  country: getString(input.values, "country"),
8396
8882
  language: getString(input.values, "language"),
8883
+ providers: allProviders ? [] : providers,
8884
+ providerModels,
8885
+ clearProviderModels,
8397
8886
  format: input.format
8398
8887
  });
8399
8888
  }
@@ -8509,6 +8998,18 @@ var PROJECT_CLI_COMMANDS = [
8509
8998
  }
8510
8999
  }
8511
9000
  ];
9001
+ function parseProviderModelAssignments(assignments) {
9002
+ const result = {};
9003
+ for (const assignment of assignments ?? []) {
9004
+ const separator = assignment.indexOf("=");
9005
+ const provider = separator === -1 ? "" : assignment.slice(0, separator).trim();
9006
+ const model = separator === -1 ? "" : assignment.slice(separator + 1).trim();
9007
+ if (!provider || !model) throw usageError(`Error: --provider-model must use provider=model (received ${assignment})`);
9008
+ if (provider in result) throw usageError(`Error: duplicate --provider-model assignment for ${provider}`);
9009
+ result[provider] = model;
9010
+ }
9011
+ return result;
9012
+ }
8512
9013
 
8513
9014
  // src/commands/report.ts
8514
9015
  import fs9 from "fs";
@@ -10944,18 +11445,36 @@ async function bootstrapCommand(_opts) {
10944
11445
  } : existingConfig?.google;
10945
11446
  const keyHash = crypto.createHash("sha256").update(rawApiKey).digest("hex");
10946
11447
  const keyPrefix = rawApiKey.slice(0, 9);
11448
+ const existingConfigKeyHash = existingConfig ? crypto.createHash("sha256").update(existingConfig.apiKey).digest("hex") : void 0;
10947
11449
  const db = createClient(databasePath);
10948
11450
  migrate(db);
10949
11451
  db.transaction((tx) => {
10950
- tx.delete(apiKeys).where(eq(apiKeys.name, "default")).run();
10951
- tx.insert(apiKeys).values({
10952
- id: crypto.randomUUID(),
10953
- name: "default",
10954
- keyHash,
10955
- keyPrefix,
10956
- scopes: ["*"],
10957
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
10958
- }).run();
11452
+ const rotatedAt = (/* @__PURE__ */ new Date()).toISOString();
11453
+ const existingDefaults = tx.select({ id: apiKeys.id, keyHash: apiKeys.keyHash }).from(apiKeys).where(eq(apiKeys.name, "default")).all();
11454
+ const existingDefault = existingDefaults.find((key) => key.keyHash === existingConfigKeyHash) ?? existingDefaults.at(0);
11455
+ if (existingDefault) {
11456
+ tx.update(apiKeys).set({
11457
+ keyHash,
11458
+ keyPrefix,
11459
+ scopes: ["*"],
11460
+ projectId: null,
11461
+ lastUsedAt: null,
11462
+ revokedAt: null
11463
+ }).where(eq(apiKeys.id, existingDefault.id)).run();
11464
+ for (const duplicate of existingDefaults) {
11465
+ if (duplicate.id === existingDefault.id) continue;
11466
+ tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq(apiKeys.id, duplicate.id)).run();
11467
+ }
11468
+ } else {
11469
+ tx.insert(apiKeys).values({
11470
+ id: crypto.randomUUID(),
11471
+ name: "default",
11472
+ keyHash,
11473
+ keyPrefix,
11474
+ scopes: ["*"],
11475
+ createdAt: rotatedAt
11476
+ }).run();
11477
+ }
10959
11478
  });
10960
11479
  saveConfig({
10961
11480
  apiUrl: env.apiUrl || existingConfig?.apiUrl || `http://localhost:${process.env.CANONRY_PORT || "4100"}`,