@ainyc/canonry 1.13.3 → 1.15.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/assets/index.html CHANGED
@@ -12,8 +12,8 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="/assets/index-DOhW7c21.js"></script>
16
- <link rel="stylesheet" crossorigin href="/assets/index-BUh6m3HV.css">
15
+ <script type="module" crossorigin src="/assets/index-s9Kz4WUv.js"></script>
16
+ <link rel="stylesheet" crossorigin href="/assets/index-CQoA5dn_.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -320,6 +320,7 @@ var googleConnections = sqliteTable("google_connections", {
320
320
  domain: text("domain").notNull(),
321
321
  connectionType: text("connection_type").notNull(),
322
322
  propertyId: text("property_id"),
323
+ sitemapUrl: text("sitemap_url"),
323
324
  accessToken: text("access_token"),
324
325
  refreshToken: text("refresh_token"),
325
326
  tokenExpiresAt: text("token_expires_at"),
@@ -619,7 +620,9 @@ var MIGRATIONS = [
619
620
  `ALTER TABLE projects ADD COLUMN default_location TEXT`,
620
621
  `ALTER TABLE query_snapshots ADD COLUMN location TEXT`,
621
622
  // v9: Add location column to runs for per-location run tracking
622
- `ALTER TABLE runs ADD COLUMN location TEXT`
623
+ `ALTER TABLE runs ADD COLUMN location TEXT`,
624
+ // v10: Add sitemapUrl to google_connections for persistent sitemap storage
625
+ `ALTER TABLE google_connections ADD COLUMN sitemap_url TEXT`
623
626
  ];
624
627
  function migrate(db) {
625
628
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -831,6 +834,7 @@ var googleConnectionDtoSchema = z4.object({
831
834
  domain: z4.string(),
832
835
  connectionType: googleConnectionTypeSchema,
833
836
  propertyId: z4.string().nullable().optional(),
837
+ sitemapUrl: z4.string().nullable().optional(),
834
838
  scopes: z4.array(z4.string()).default([]),
835
839
  createdAt: z4.string(),
836
840
  updatedAt: z4.string()
@@ -3696,6 +3700,14 @@ async function listSites(accessToken) {
3696
3700
  );
3697
3701
  return data.siteEntry ?? [];
3698
3702
  }
3703
+ async function listSitemaps(accessToken, siteUrl) {
3704
+ const encodedSiteUrl = encodeURIComponent(siteUrl);
3705
+ const data = await gscFetch(
3706
+ accessToken,
3707
+ `${GSC_API_BASE}/sites/${encodedSiteUrl}/sitemaps`
3708
+ );
3709
+ return data.sitemap ?? [];
3710
+ }
3699
3711
  async function fetchSearchAnalytics(accessToken, siteUrl, opts) {
3700
3712
  const allRows = [];
3701
3713
  let startRow = 0;
@@ -3813,6 +3825,7 @@ async function googleRoutes(app, opts) {
3813
3825
  domain: connection.domain,
3814
3826
  connectionType: connection.connectionType,
3815
3827
  propertyId: connection.propertyId ?? null,
3828
+ sitemapUrl: connection.sitemapUrl ?? null,
3816
3829
  scopes: connection.scopes ?? [],
3817
3830
  createdAt: connection.createdAt,
3818
3831
  updatedAt: connection.updatedAt
@@ -4233,6 +4246,69 @@ async function googleRoutes(app, opts) {
4233
4246
  reasonBreakdown: JSON.parse(r.reasonBreakdown)
4234
4247
  })).reverse();
4235
4248
  });
4249
+ app.get("/projects/:name/google/gsc/sitemaps", async (request, reply) => {
4250
+ const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
4251
+ if (!googleClientId || !googleClientSecret) {
4252
+ const err = validationError("Google OAuth is not configured");
4253
+ return reply.status(err.statusCode).send(err.toJSON());
4254
+ }
4255
+ const store = requireConnectionStore(reply);
4256
+ if (!store) return;
4257
+ const project = resolveProject(app.db, request.params.name);
4258
+ const { accessToken, propertyId } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
4259
+ if (!propertyId) {
4260
+ const err = validationError('No GSC property configured for this connection. Set one with "canonry google set-property".');
4261
+ return reply.status(err.statusCode).send(err.toJSON());
4262
+ }
4263
+ const sitemaps = await listSitemaps(accessToken, propertyId);
4264
+ return { sitemaps };
4265
+ });
4266
+ app.post("/projects/:name/google/gsc/discover-sitemaps", async (request, reply) => {
4267
+ const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
4268
+ if (!googleClientId || !googleClientSecret) {
4269
+ const err = validationError("Google OAuth is not configured");
4270
+ return reply.status(err.statusCode).send(err.toJSON());
4271
+ }
4272
+ const store = requireConnectionStore(reply);
4273
+ if (!store) return;
4274
+ const project = resolveProject(app.db, request.params.name);
4275
+ const conn = store.getConnection(project.canonicalDomain, "gsc");
4276
+ if (!conn) {
4277
+ const err = validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
4278
+ return reply.status(err.statusCode).send(err.toJSON());
4279
+ }
4280
+ if (!conn.propertyId) {
4281
+ const err = validationError("No GSC property configured for this connection");
4282
+ return reply.status(err.statusCode).send(err.toJSON());
4283
+ }
4284
+ const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
4285
+ const sitemaps = await listSitemaps(accessToken, conn.propertyId);
4286
+ if (sitemaps.length === 0) {
4287
+ const err = validationError("No sitemaps found for this GSC property. Submit a sitemap in Google Search Console first.");
4288
+ return reply.status(err.statusCode).send(err.toJSON());
4289
+ }
4290
+ const primary = sitemaps.find((s) => !s.isSitemapsIndex) ?? sitemaps[0];
4291
+ const sitemapUrl = primary.path;
4292
+ store.updateConnection(project.canonicalDomain, "gsc", {
4293
+ sitemapUrl,
4294
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4295
+ });
4296
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4297
+ const runId = crypto13.randomUUID();
4298
+ app.db.insert(runs).values({
4299
+ id: runId,
4300
+ projectId: project.id,
4301
+ kind: "inspect-sitemap",
4302
+ status: "queued",
4303
+ trigger: "manual",
4304
+ createdAt: now
4305
+ }).run();
4306
+ if (opts.onInspectSitemapRequested) {
4307
+ opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
4308
+ }
4309
+ const run = app.db.select().from(runs).where(eq12(runs.id, runId)).get();
4310
+ return { sitemaps, primarySitemapUrl: sitemapUrl, run };
4311
+ });
4236
4312
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request, reply) => {
4237
4313
  const store = requireConnectionStore(reply);
4238
4314
  if (!store) return;
@@ -4263,6 +4339,26 @@ async function googleRoutes(app, opts) {
4263
4339
  const run = app.db.select().from(runs).where(eq12(runs.id, runId)).get();
4264
4340
  return run;
4265
4341
  });
4342
+ app.put("/projects/:name/google/connections/:type/sitemap", async (request, reply) => {
4343
+ const store = requireConnectionStore(reply);
4344
+ if (!store) return;
4345
+ const project = resolveProject(app.db, request.params.name);
4346
+ const { sitemapUrl } = request.body ?? {};
4347
+ if (!sitemapUrl || !sitemapUrl.trim()) {
4348
+ const err = validationError("sitemapUrl is required");
4349
+ return reply.status(err.statusCode).send(err.toJSON());
4350
+ }
4351
+ const conn = store.updateConnection(
4352
+ project.canonicalDomain,
4353
+ request.params.type,
4354
+ { sitemapUrl: sitemapUrl.trim(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
4355
+ );
4356
+ if (!conn) {
4357
+ const err = notFound("Google connection", request.params.type);
4358
+ return reply.status(err.statusCode).send(err.toJSON());
4359
+ }
4360
+ return { sitemapUrl: sitemapUrl.trim() };
4361
+ });
4266
4362
  app.put("/projects/:name/google/connections/:type/property", async (request, reply) => {
4267
4363
  const store = requireConnectionStore(reply);
4268
4364
  if (!store) return;
@@ -5313,6 +5409,7 @@ function upsertGoogleConnection(config, connection) {
5313
5409
  const normalized = {
5314
5410
  ...connection,
5315
5411
  propertyId: connection.propertyId ?? null,
5412
+ sitemapUrl: connection.sitemapUrl ?? null,
5316
5413
  refreshToken: connection.refreshToken ?? null,
5317
5414
  tokenExpiresAt: connection.tokenExpiresAt ?? null,
5318
5415
  scopes: connection.scopes ?? []
@@ -5331,6 +5428,7 @@ function patchGoogleConnection(config, domain, connectionType, patch) {
5331
5428
  ...existing,
5332
5429
  ...patch,
5333
5430
  propertyId: Object.prototype.hasOwnProperty.call(patch, "propertyId") ? patch.propertyId ?? null : existing.propertyId ?? null,
5431
+ sitemapUrl: Object.prototype.hasOwnProperty.call(patch, "sitemapUrl") ? patch.sitemapUrl ?? null : existing.sitemapUrl ?? null,
5334
5432
  refreshToken: Object.prototype.hasOwnProperty.call(patch, "refreshToken") ? patch.refreshToken ?? null : existing.refreshToken ?? null,
5335
5433
  tokenExpiresAt: Object.prototype.hasOwnProperty.call(patch, "tokenExpiresAt") ? patch.tokenExpiresAt ?? null : existing.tokenExpiresAt ?? null,
5336
5434
  scopes: patch.scopes ?? existing.scopes ?? []
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  setGoogleAuthConfig,
20
20
  showFirstRunNotice,
21
21
  trackEvent
22
- } from "./chunk-JKIWFSYI.js";
22
+ } from "./chunk-WCZMFJUY.js";
23
23
 
24
24
  // src/cli.ts
25
25
  import { parseArgs } from "util";
@@ -635,6 +635,9 @@ var ApiClient = class {
635
635
  async googleSetProperty(project, type, propertyId) {
636
636
  return this.request("PUT", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}/property`, { propertyId });
637
637
  }
638
+ async googleSetSitemap(project, type, sitemapUrl) {
639
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/google/connections/${encodeURIComponent(type)}/sitemap`, { sitemapUrl });
640
+ }
638
641
  // GSC data
639
642
  async gscSync(project, body) {
640
643
  return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/sync`, body ?? {});
@@ -663,6 +666,12 @@ var ApiClient = class {
663
666
  async gscInspectSitemap(project, body) {
664
667
  return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/inspect-sitemap`, body ?? {});
665
668
  }
669
+ async gscSitemaps(project) {
670
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/google/gsc/sitemaps`);
671
+ }
672
+ async gscDiscoverSitemaps(project) {
673
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/google/gsc/discover-sitemaps`, {});
674
+ }
666
675
  };
667
676
 
668
677
  // src/commands/project.ts
@@ -1759,6 +1768,33 @@ Index Coverage for "${project}"
1759
1768
  console.log(` Last inspected: ${result.lastInspectedAt}`);
1760
1769
  }
1761
1770
  }
1771
+ async function googleSetSitemap(project, sitemapUrl) {
1772
+ const client = getClient11();
1773
+ await client.googleSetSitemap(project, "gsc", sitemapUrl);
1774
+ console.log(`GSC sitemap URL set to "${sitemapUrl}" for project "${project}".`);
1775
+ }
1776
+ async function googleListSitemaps(project, opts) {
1777
+ const client = getClient11();
1778
+ const result = await client.gscSitemaps(project);
1779
+ if (opts.format === "json") {
1780
+ console.log(JSON.stringify(result, null, 2));
1781
+ return;
1782
+ }
1783
+ if (result.sitemaps.length === 0) {
1784
+ console.log(`No sitemaps found for project "${project}". Submit a sitemap in Google Search Console first.`);
1785
+ return;
1786
+ }
1787
+ console.log(`
1788
+ Sitemaps for project "${project}":
1789
+ `);
1790
+ for (const s of result.sitemaps) {
1791
+ const indexed = s.contents?.[0]?.indexed ?? "?";
1792
+ const submitted = s.contents?.[0]?.submitted ?? "?";
1793
+ const isIndex = s.isSitemapsIndex ? " [index]" : "";
1794
+ console.log(` ${s.path}${isIndex}`);
1795
+ console.log(` Indexed: ${indexed} / ${submitted} submitted | Last submitted: ${s.lastSubmitted ?? "unknown"}`);
1796
+ }
1797
+ }
1762
1798
  async function googleInspectSitemap(project, opts) {
1763
1799
  const client = getClient11();
1764
1800
  const run = await client.gscInspectSitemap(project, {
@@ -1816,6 +1852,51 @@ GSC Coverage History for "${project}" (${rows.length} snapshots):
1816
1852
  console.log(` ${row.date.padEnd(12)}${String(row.indexed).padEnd(10)}${String(row.notIndexed).padEnd(14)}${reasonStr}`);
1817
1853
  }
1818
1854
  }
1855
+ async function googleDiscoverSitemaps(project, opts) {
1856
+ const client = getClient11();
1857
+ const result = await client.gscDiscoverSitemaps(project);
1858
+ if (opts.format === "json") {
1859
+ console.log(JSON.stringify(result, null, 2));
1860
+ return;
1861
+ }
1862
+ console.log(`
1863
+ Discovered ${result.sitemaps.length} sitemap(s) for project "${project}":
1864
+ `);
1865
+ for (const s of result.sitemaps) {
1866
+ const primary = s.path === result.primarySitemapUrl ? " (primary)" : "";
1867
+ const indexed = s.contents?.[0]?.indexed ?? "?";
1868
+ const submitted = s.contents?.[0]?.submitted ?? "?";
1869
+ console.log(` ${s.path}${primary}`);
1870
+ console.log(` Indexed: ${indexed} / ${submitted} submitted | Last submitted: ${s.lastSubmitted ?? "unknown"}`);
1871
+ }
1872
+ console.log(`
1873
+ Primary sitemap: ${result.primarySitemapUrl}`);
1874
+ console.log(`Sitemap URL saved. Inspection run queued (run ${result.run.id}).`);
1875
+ if (opts.wait) {
1876
+ const timeout = 30 * 60 * 1e3;
1877
+ const start = Date.now();
1878
+ process.stderr.write("Waiting for sitemap inspection to complete");
1879
+ while (Date.now() - start < timeout) {
1880
+ await new Promise((r) => setTimeout(r, 3e3));
1881
+ const current = await client.getRun(result.run.id);
1882
+ process.stderr.write(".");
1883
+ if (current.status === "completed" || current.status === "partial" || current.status === "failed") {
1884
+ process.stderr.write("\n");
1885
+ if (current.status === "completed") {
1886
+ console.log("Sitemap inspection completed successfully.");
1887
+ } else if (current.status === "partial") {
1888
+ console.log("Sitemap inspection completed with some errors.");
1889
+ } else {
1890
+ console.error("Sitemap inspection failed.");
1891
+ }
1892
+ return;
1893
+ }
1894
+ }
1895
+ process.stderr.write("\n");
1896
+ console.error("Timed out waiting for sitemap inspection to complete.");
1897
+ process.exit(1);
1898
+ }
1899
+ }
1819
1900
  async function googleDeindexed(project, format) {
1820
1901
  const client = getClient11();
1821
1902
  const rows = await client.gscDeindexed(project);
@@ -1893,6 +1974,9 @@ Usage:
1893
1974
  canonry google status <project> Show Google connection status
1894
1975
  canonry google properties <project> List available GSC properties
1895
1976
  canonry google set-property <project> <url> Set GSC property URL
1977
+ canonry google set-sitemap <project> <url> Set GSC sitemap URL
1978
+ canonry google list-sitemaps <project> List submitted sitemaps from GSC (no run queued)
1979
+ canonry google discover-sitemaps <project> Auto-discover sitemaps from GSC and queue inspection (--wait)
1896
1980
  canonry google sync <project> Sync GSC data (--days 30, --full, --wait)
1897
1981
  canonry google performance <project> Show GSC search performance data
1898
1982
  canonry google inspect <project> <url> Inspect a URL via GSC
@@ -2664,6 +2748,25 @@ async function main() {
2664
2748
  await googleSetProperty(project, propertyUrl);
2665
2749
  break;
2666
2750
  }
2751
+ case "set-sitemap": {
2752
+ const project = args[2];
2753
+ const sitemapUrl = args[3];
2754
+ if (!project || !sitemapUrl) {
2755
+ console.error("Error: project name and sitemap URL are required");
2756
+ process.exit(1);
2757
+ }
2758
+ await googleSetSitemap(project, sitemapUrl);
2759
+ break;
2760
+ }
2761
+ case "list-sitemaps": {
2762
+ const project = args[2];
2763
+ if (!project) {
2764
+ console.error("Error: project name is required");
2765
+ process.exit(1);
2766
+ }
2767
+ await googleListSitemaps(project, { format });
2768
+ break;
2769
+ }
2667
2770
  case "sync": {
2668
2771
  const project = args[2];
2669
2772
  if (!project) {
@@ -2805,9 +2908,29 @@ async function main() {
2805
2908
  await googleDeindexed(project, format);
2806
2909
  break;
2807
2910
  }
2911
+ case "discover-sitemaps": {
2912
+ const project = args[2];
2913
+ if (!project) {
2914
+ console.error("Error: project name is required");
2915
+ process.exit(1);
2916
+ }
2917
+ const { values: discoverValues } = parseArgs({
2918
+ args: args.slice(3),
2919
+ options: {
2920
+ wait: { type: "boolean", default: false },
2921
+ format: { type: "string" }
2922
+ },
2923
+ allowPositionals: false
2924
+ });
2925
+ await googleDiscoverSitemaps(project, {
2926
+ wait: discoverValues.wait ?? false,
2927
+ format: discoverValues.format === "json" ? "json" : format
2928
+ });
2929
+ break;
2930
+ }
2808
2931
  default:
2809
2932
  console.error(`Unknown google subcommand: ${subcommand ?? "(none)"}`);
2810
- console.log("Available: connect, disconnect, status, properties, set-property, sync, performance, inspect, inspect-sitemap, coverage, coverage-history, inspections, deindexed");
2933
+ console.log("Available: connect, disconnect, status, properties, set-property, set-sitemap, list-sitemaps, discover-sitemaps, sync, performance, inspect, inspect-sitemap, coverage, coverage-history, inspections, deindexed");
2811
2934
  process.exit(1);
2812
2935
  }
2813
2936
  break;
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ interface GoogleConnectionConfigEntry {
13
13
  domain: string;
14
14
  connectionType: GoogleConnectionType;
15
15
  propertyId?: string | null;
16
+ sitemapUrl?: string | null;
16
17
  accessToken?: string;
17
18
  refreshToken?: string | null;
18
19
  tokenExpiresAt?: string | null;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-JKIWFSYI.js";
4
+ } from "./chunk-WCZMFJUY.js";
5
5
  export {
6
6
  createServer,
7
7
  loadConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.13.3",
3
+ "version": "1.15.0",
4
4
  "type": "module",
5
5
  "description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -53,13 +53,13 @@
53
53
  "tsx": "^4.19.0",
54
54
  "@ainyc/canonry-api-routes": "0.0.0",
55
55
  "@ainyc/canonry-config": "0.0.0",
56
- "@ainyc/canonry-db": "0.0.0",
57
56
  "@ainyc/canonry-provider-claude": "0.0.0",
58
- "@ainyc/canonry-provider-local": "0.0.0",
59
57
  "@ainyc/canonry-provider-gemini": "0.0.0",
60
- "@ainyc/canonry-provider-openai": "0.0.0",
58
+ "@ainyc/canonry-integration-google": "0.0.0",
61
59
  "@ainyc/canonry-contracts": "0.0.0",
62
- "@ainyc/canonry-integration-google": "0.0.0"
60
+ "@ainyc/canonry-provider-local": "0.0.0",
61
+ "@ainyc/canonry-provider-openai": "0.0.0",
62
+ "@ainyc/canonry-db": "0.0.0"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsup && tsx build-web.ts",