@ainyc/canonry 4.99.1 → 4.100.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 (26) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +13 -1
  2. package/assets/assets/{BacklinksPage-v2RC8wlg.js → BacklinksPage-DWZdWEm3.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-B74e5pqn.js → ChartPrimitives-C-ky5206.js} +1 -1
  4. package/assets/assets/{ProjectPage-BCzS7TQn.js → ProjectPage-CpbFpi6J.js} +4 -4
  5. package/assets/assets/{RunRow-BmVPjO-m.js → RunRow-Bj-D4kZe.js} +1 -1
  6. package/assets/assets/{RunsPage-CK5atXvL.js → RunsPage-GktitsAV.js} +1 -1
  7. package/assets/assets/{SettingsPage-DarPFUPl.js → SettingsPage-AsFLFDS3.js} +1 -1
  8. package/assets/assets/{TrafficPage-Ddnn_WqU.js → TrafficPage-Cf3uCkxs.js} +1 -1
  9. package/assets/assets/{TrafficSourceDetailPage-g08SKnwZ.js → TrafficSourceDetailPage-B5t7nf9C.js} +1 -1
  10. package/assets/assets/{arrow-left-D39Vxasb.js → arrow-left-CZFQuQO_.js} +1 -1
  11. package/assets/assets/{extract-error-message-BiNWqBeh.js → extract-error-message--bieZ2IX.js} +1 -1
  12. package/assets/assets/index-9hI0ODQU.css +1 -0
  13. package/assets/assets/index-Cb2h7KkD.js +210 -0
  14. package/assets/assets/{trash-2-xh6gsnap.js → trash-2-sKO0k75f.js} +1 -1
  15. package/assets/index.html +2 -2
  16. package/dist/{chunk-4Q3LBD7A.js → chunk-74SOIAIY.js} +15 -12
  17. package/dist/{chunk-JF7EXHGA.js → chunk-ADLS64PG.js} +105 -0
  18. package/dist/{chunk-YBIT5O62.js → chunk-SNHYRSUF.js} +366 -12
  19. package/dist/{chunk-IDI6LMYL.js → chunk-UVIHWF45.js} +27 -1
  20. package/dist/cli.js +110 -4
  21. package/dist/index.js +4 -4
  22. package/dist/{intelligence-service-TWCEKVKZ.js → intelligence-service-3CRNJLUZ.js} +2 -2
  23. package/dist/mcp.js +2 -2
  24. package/package.json +8 -8
  25. package/assets/assets/index-BCuWbxUj.css +0 -1
  26. package/assets/assets/index-Dd4J1VhB.js +0 -210
@@ -18,6 +18,7 @@ import {
18
18
  DiscoveryBuckets,
19
19
  DiscoveryCompetitorTypes,
20
20
  DiscoverySessionStatuses,
21
+ PortfolioChangeTypes,
21
22
  RunKinds,
22
23
  RunStatuses,
23
24
  RunTriggers,
@@ -131,6 +132,7 @@ import {
131
132
  formatIsoDate,
132
133
  formatNumber,
133
134
  formatRatio,
135
+ formatRunErrorOneLine,
134
136
  formatWindowCountDelta,
135
137
  ga4AiReferralHistoryEntrySchema,
136
138
  ga4SessionHistoryEntrySchema,
@@ -184,6 +186,7 @@ import {
184
186
  parseRunError,
185
187
  parseWindow,
186
188
  pickClusterRepresentative,
189
+ portfolioDtoSchema,
187
190
  projectConfigSchema,
188
191
  projectDtoSchema,
189
192
  projectOverviewDtoSchema,
@@ -263,10 +266,10 @@ import {
263
266
  wordpressSchemaDeployResultDtoSchema,
264
267
  wordpressSchemaStatusResultDtoSchema,
265
268
  wordpressStatusDtoSchema
266
- } from "./chunk-JF7EXHGA.js";
269
+ } from "./chunk-ADLS64PG.js";
267
270
 
268
271
  // src/intelligence-service.ts
269
- import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
272
+ import { eq as eq37, desc as desc18, asc as asc6, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
270
273
 
271
274
  // ../db/src/client.ts
272
275
  import { mkdirSync } from "fs";
@@ -5045,6 +5048,233 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
5045
5048
  });
5046
5049
  }
5047
5050
 
5051
+ // ../intelligence/src/portfolio-change-feed.ts
5052
+ var PORTFOLIO_CHANGE_FEED_LIMIT = 12;
5053
+ var TONE_SEVERITY = {
5054
+ negative: 0,
5055
+ caution: 1,
5056
+ positive: 2,
5057
+ neutral: 3
5058
+ };
5059
+ function pluralizeQueries(n) {
5060
+ return n === 1 ? "query" : "queries";
5061
+ }
5062
+ function pluralizeAnswers(n) {
5063
+ return n === 1 ? "answer" : "answers";
5064
+ }
5065
+ function affectedDetail(queries2, max = 3) {
5066
+ const list = queries2 ?? [];
5067
+ if (list.length === 0) return "";
5068
+ const shown = list.slice(0, max).map((q) => `"${q}"`).join(", ");
5069
+ const remaining = list.length - max;
5070
+ return remaining > 0 ? `${shown} +${remaining} more` : shown;
5071
+ }
5072
+ function buildProjectChanges(input) {
5073
+ const {
5074
+ projectName,
5075
+ projectSlug,
5076
+ citationMovement,
5077
+ mentionMovement,
5078
+ movementComparison,
5079
+ attentionItems,
5080
+ latestRun
5081
+ } = input;
5082
+ const href = `/projects/${encodeURIComponent(projectSlug)}`;
5083
+ const rows = [];
5084
+ if (!latestRun) {
5085
+ rows.push({
5086
+ id: `${projectSlug}:${PortfolioChangeTypes["project-never-run"]}`,
5087
+ projectName,
5088
+ projectSlug,
5089
+ changeType: PortfolioChangeTypes["project-never-run"],
5090
+ tone: "neutral",
5091
+ title: `${projectName} has no completed sweep yet`,
5092
+ detail: `${input.trackedQueryCount} ${pluralizeQueries(input.trackedQueryCount)} tracked \u2014 run a sweep to start measuring.`,
5093
+ occurredAt: input.projectCreatedAt,
5094
+ href,
5095
+ actionLabel: "Run sweep",
5096
+ comparable: false
5097
+ });
5098
+ return rows;
5099
+ }
5100
+ const { runId, occurredAt } = latestRun;
5101
+ const comparable = movementComparison.comparable;
5102
+ if (latestRun.status === RunStatuses.failed) {
5103
+ rows.push({
5104
+ id: `${projectSlug}:${PortfolioChangeTypes["run-failed"]}:${runId}`,
5105
+ projectName,
5106
+ projectSlug,
5107
+ changeType: PortfolioChangeTypes["run-failed"],
5108
+ tone: "negative",
5109
+ title: `${projectName} sweep failed`,
5110
+ detail: latestRun.error ? formatRunErrorOneLine(latestRun.error) : "The latest visibility sweep failed.",
5111
+ occurredAt,
5112
+ href,
5113
+ actionLabel: "View run",
5114
+ comparable
5115
+ });
5116
+ }
5117
+ if (comparable) {
5118
+ if (citationMovement.lost > 0) {
5119
+ rows.push({
5120
+ id: `${projectSlug}:${PortfolioChangeTypes["citation-lost"]}:${runId}`,
5121
+ projectName,
5122
+ projectSlug,
5123
+ changeType: PortfolioChangeTypes["citation-lost"],
5124
+ tone: "negative",
5125
+ title: `${projectName} lost ${citationMovement.lost} cited ${pluralizeQueries(citationMovement.lost)}`,
5126
+ detail: affectedDetail(citationMovement.lostQueries),
5127
+ occurredAt,
5128
+ href,
5129
+ actionLabel: "Open project",
5130
+ comparable: true
5131
+ });
5132
+ }
5133
+ if (citationMovement.gained > 0) {
5134
+ rows.push({
5135
+ id: `${projectSlug}:${PortfolioChangeTypes["citation-gained"]}:${runId}`,
5136
+ projectName,
5137
+ projectSlug,
5138
+ changeType: PortfolioChangeTypes["citation-gained"],
5139
+ tone: "positive",
5140
+ title: `${projectName} gained ${citationMovement.gained} cited ${pluralizeQueries(citationMovement.gained)}`,
5141
+ detail: affectedDetail(citationMovement.gainedQueries),
5142
+ occurredAt,
5143
+ href,
5144
+ actionLabel: "Open project",
5145
+ comparable: true
5146
+ });
5147
+ }
5148
+ if (mentionMovement.lost > 0) {
5149
+ const queries2 = affectedDetail(mentionMovement.lostQueries);
5150
+ rows.push({
5151
+ id: `${projectSlug}:${PortfolioChangeTypes["mention-lost"]}:${runId}`,
5152
+ projectName,
5153
+ projectSlug,
5154
+ changeType: PortfolioChangeTypes["mention-lost"],
5155
+ tone: "negative",
5156
+ title: `${projectName} dropped from ${mentionMovement.lost} ${pluralizeAnswers(mentionMovement.lost)}`,
5157
+ detail: queries2 ? `No longer mentioned for ${queries2}` : "",
5158
+ occurredAt,
5159
+ href,
5160
+ actionLabel: "Open project",
5161
+ comparable: true
5162
+ });
5163
+ }
5164
+ if (mentionMovement.gained > 0) {
5165
+ rows.push({
5166
+ id: `${projectSlug}:${PortfolioChangeTypes["mention-gained"]}:${runId}`,
5167
+ projectName,
5168
+ projectSlug,
5169
+ changeType: PortfolioChangeTypes["mention-gained"],
5170
+ tone: "positive",
5171
+ title: `${projectName} now mentioned in ${mentionMovement.gained} more ${pluralizeAnswers(mentionMovement.gained)}`,
5172
+ detail: affectedDetail(mentionMovement.gainedQueries),
5173
+ occurredAt,
5174
+ href,
5175
+ actionLabel: "Open project",
5176
+ comparable: true
5177
+ });
5178
+ }
5179
+ } else if (movementComparison.hasPreviousRun && movementComparison.querySetChanged) {
5180
+ rows.push({
5181
+ id: `${projectSlug}:${PortfolioChangeTypes["query-set-changed"]}:${runId}`,
5182
+ projectName,
5183
+ projectSlug,
5184
+ changeType: PortfolioChangeTypes["query-set-changed"],
5185
+ tone: "neutral",
5186
+ title: `${projectName} query set changed`,
5187
+ detail: `+${movementComparison.addedQueryCount} added \xB7 \u2212${movementComparison.removedQueryCount} removed since last sweep \u2014 movement compares the shared queries`,
5188
+ occurredAt,
5189
+ href,
5190
+ actionLabel: "Open project",
5191
+ comparable: false
5192
+ });
5193
+ }
5194
+ for (const item of attentionItems) {
5195
+ if (item.id.startsWith("insight_")) {
5196
+ const isCritical = item.tone === "negative";
5197
+ const changeType = isCritical ? PortfolioChangeTypes["insight-critical"] : PortfolioChangeTypes["insight-high"];
5198
+ rows.push({
5199
+ id: `${projectSlug}:${changeType}:${item.id}`,
5200
+ projectName,
5201
+ projectSlug,
5202
+ changeType,
5203
+ tone: item.tone,
5204
+ title: item.title,
5205
+ detail: item.detail,
5206
+ occurredAt,
5207
+ href,
5208
+ actionLabel: isCritical ? "Critical" : "High",
5209
+ comparable
5210
+ });
5211
+ } else if (item.id === "stale_visibility") {
5212
+ rows.push({
5213
+ id: `${projectSlug}:${PortfolioChangeTypes["stale-visibility"]}:${runId}`,
5214
+ projectName,
5215
+ projectSlug,
5216
+ changeType: PortfolioChangeTypes["stale-visibility"],
5217
+ tone: "caution",
5218
+ title: `${projectName} visibility data is stale`,
5219
+ detail: "Integration syncs have run since the last sweep.",
5220
+ occurredAt,
5221
+ href,
5222
+ actionLabel: "Re-sweep",
5223
+ comparable
5224
+ });
5225
+ }
5226
+ }
5227
+ return rows;
5228
+ }
5229
+ function resolveEmptyState(projectCount, comparableProjectCount) {
5230
+ if (comparableProjectCount === 0) {
5231
+ return {
5232
+ kind: "awaiting-second-sweep",
5233
+ title: "No changes to compare yet",
5234
+ detail: "Visibility movement appears after a second sweep."
5235
+ };
5236
+ }
5237
+ return {
5238
+ kind: "all-clear",
5239
+ title: "No changes since the last sweep",
5240
+ detail: `All ${projectCount} project${projectCount === 1 ? "" : "s"} held their mention and citation coverage.`
5241
+ };
5242
+ }
5243
+ function buildPortfolioChangeFeed(inputs, _nowIso) {
5244
+ const all = [];
5245
+ let comparableProjectCount = 0;
5246
+ let firstSweepProjectCount = 0;
5247
+ for (const input of inputs) {
5248
+ if (input.latestRun) {
5249
+ if (input.movementComparison.comparable) comparableProjectCount += 1;
5250
+ if (!input.movementComparison.hasPreviousRun) firstSweepProjectCount += 1;
5251
+ }
5252
+ all.push(...buildProjectChanges(input));
5253
+ }
5254
+ const byId = /* @__PURE__ */ new Map();
5255
+ for (const row of all) {
5256
+ if (!byId.has(row.id)) byId.set(row.id, row);
5257
+ }
5258
+ const deduped = [...byId.values()];
5259
+ deduped.sort((a, b) => {
5260
+ if (a.occurredAt !== b.occurredAt) return a.occurredAt < b.occurredAt ? 1 : -1;
5261
+ const severity = TONE_SEVERITY[a.tone] - TONE_SEVERITY[b.tone];
5262
+ if (severity !== 0) return severity;
5263
+ if (a.projectName !== b.projectName) return a.projectName < b.projectName ? -1 : 1;
5264
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
5265
+ });
5266
+ const changeFeedTotal = deduped.length;
5267
+ const changeFeed = deduped.slice(0, PORTFOLIO_CHANGE_FEED_LIMIT);
5268
+ const feedEmptyState = changeFeedTotal === 0 ? resolveEmptyState(inputs.length, comparableProjectCount) : null;
5269
+ return {
5270
+ changeFeed,
5271
+ changeFeedTotal,
5272
+ feedEmptyState,
5273
+ comparableProjectCount,
5274
+ firstSweepProjectCount
5275
+ };
5276
+ }
5277
+
5048
5278
  // ../intelligence/src/provider-trends.ts
5049
5279
  function buildProviderTrends(runs2, snapshotsByRunId, limit = 12) {
5050
5280
  const recent = [...runs2].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
@@ -13107,8 +13337,9 @@ async function visibilityStatsRoutes(app) {
13107
13337
  }
13108
13338
 
13109
13339
  // ../api-routes/src/composites.ts
13110
- import { eq as eq17, and as and12, desc as desc9, sql as sql7, like as like2, or as or4, inArray as inArray9 } from "drizzle-orm";
13340
+ import { eq as eq17, and as and12, asc as asc2, desc as desc9, sql as sql7, like as like2, or as or4, inArray as inArray9 } from "drizzle-orm";
13111
13341
  var TOP_INSIGHT_LIMIT = 5;
13342
+ var PORTFOLIO_RECENT_RUNS_LIMIT = 8;
13112
13343
  var SEARCH_HIT_HARD_LIMIT = 50;
13113
13344
  var SEARCH_SNIPPET_RADIUS = 80;
13114
13345
  var INTEGRATION_SYNC_KINDS = /* @__PURE__ */ new Set([
@@ -13125,6 +13356,14 @@ async function compositeRoutes(app) {
13125
13356
  const project = resolveProject(app.db, request.params.name);
13126
13357
  const filterLocation = (request.query.location ?? "").trim() || null;
13127
13358
  const sinceIso = parseSinceFilter(request.query.since);
13359
+ return reply.send(computeProjectOverview(project, { filterLocation, sinceIso }));
13360
+ });
13361
+ app.get("/portfolio", async (_request, reply) => {
13362
+ return reply.send(buildPortfolioOverview());
13363
+ });
13364
+ function computeProjectOverview(project, opts = {}) {
13365
+ const filterLocation = opts.filterLocation ?? null;
13366
+ const sinceIso = opts.sinceIso ?? null;
13128
13367
  const allRunsRaw = app.db.select().from(runs).where(and12(eq17(runs.projectId, project.id), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).all();
13129
13368
  const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
13130
13369
  const totalRuns = allRuns.length;
@@ -13257,8 +13496,111 @@ async function compositeRoutes(app) {
13257
13496
  dateRangeLabel: "All time",
13258
13497
  contextLabel: `${project.country} / ${project.language.toUpperCase()}`
13259
13498
  };
13260
- return reply.send(result);
13261
- });
13499
+ return result;
13500
+ }
13501
+ function buildPortfolioOverview() {
13502
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
13503
+ const projectRows = app.db.select().from(projects).orderBy(asc2(projects.name)).all();
13504
+ const trackedCountRows = app.db.select({ projectId: queries.projectId, total: sql7`COUNT(*)` }).from(queries).groupBy(queries.projectId).all();
13505
+ const trackedCountByProject = new Map(trackedCountRows.map((r) => [r.projectId, Number(r.total)]));
13506
+ const lastSweepRow = app.db.select({ createdAt: runs.createdAt, finishedAt: runs.finishedAt }).from(runs).where(and12(
13507
+ eq17(runs.kind, RunKinds["answer-visibility"]),
13508
+ notProbeRun(),
13509
+ inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial])
13510
+ )).orderBy(desc9(runs.createdAt), desc9(runs.id)).limit(1).get();
13511
+ const lastSweepAt = lastSweepRow ? lastSweepRow.finishedAt ?? lastSweepRow.createdAt : null;
13512
+ const feedInputs = [];
13513
+ const projectRowDtos = [];
13514
+ const runHistoryById = /* @__PURE__ */ new Map();
13515
+ for (const projectRow of projectRows) {
13516
+ const overview = computeProjectOverview(projectRow);
13517
+ for (const point of overview.runHistory) runHistoryById.set(point.runId, point);
13518
+ const latestVisRunRow = app.db.select().from(runs).where(and12(
13519
+ eq17(runs.projectId, projectRow.id),
13520
+ eq17(runs.kind, RunKinds["answer-visibility"]),
13521
+ notProbeRun()
13522
+ )).orderBy(desc9(runs.createdAt), desc9(runs.id)).limit(1).get();
13523
+ const projectName = projectRow.displayName || projectRow.name;
13524
+ feedInputs.push({
13525
+ projectName,
13526
+ projectSlug: projectRow.name,
13527
+ citationMovement: overview.citationMovement,
13528
+ mentionMovement: overview.mentionMovement,
13529
+ movementComparison: overview.movementComparison,
13530
+ attentionItems: overview.attentionItems,
13531
+ latestRun: latestVisRunRow ? {
13532
+ runId: latestVisRunRow.id,
13533
+ status: latestVisRunRow.status,
13534
+ occurredAt: latestVisRunRow.finishedAt ?? latestVisRunRow.createdAt,
13535
+ error: parseRunError(latestVisRunRow.error)
13536
+ } : null,
13537
+ trackedQueryCount: trackedCountByProject.get(projectRow.id) ?? 0,
13538
+ projectCreatedAt: projectRow.createdAt
13539
+ });
13540
+ const mention = overview.scores.mention;
13541
+ projectRowDtos.push({
13542
+ projectSlug: projectRow.name,
13543
+ projectName,
13544
+ canonicalDomain: projectRow.canonicalDomain,
13545
+ mentionScore: mention.progress ?? 0,
13546
+ mentionTone: mention.tone,
13547
+ mentionedOfTotal: { mentioned: overview.queryCounts.mentionedQueries, total: overview.queryCounts.totalQueries },
13548
+ citedOfTotal: { cited: overview.queryCounts.citedQueries, total: overview.queryCounts.totalQueries },
13549
+ mentionDelta: {
13550
+ gained: overview.mentionMovement.gained,
13551
+ lost: overview.mentionMovement.lost,
13552
+ comparable: overview.movementComparison.comparable
13553
+ },
13554
+ citationDelta: {
13555
+ gained: overview.citationMovement.gained,
13556
+ lost: overview.citationMovement.lost,
13557
+ comparable: overview.movementComparison.comparable
13558
+ },
13559
+ competitorPressureLabel: overview.scores.competitorPressure.value,
13560
+ mentionTrend: mention.trend,
13561
+ lastRunAt: latestVisRunRow ? latestVisRunRow.finishedAt ?? latestVisRunRow.createdAt : null,
13562
+ hasEverRun: latestVisRunRow != null
13563
+ });
13564
+ }
13565
+ const feed = buildPortfolioChangeFeed(feedInputs, generatedAt);
13566
+ const projectById = new Map(projectRows.map((p) => [p.id, p]));
13567
+ const recentRunRows = app.db.select().from(runs).where(and12(eq17(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).orderBy(desc9(runs.createdAt), desc9(runs.id)).limit(PORTFOLIO_RECENT_RUNS_LIMIT).all();
13568
+ const recentRuns = [];
13569
+ for (const run of recentRunRows) {
13570
+ const projectRow = projectById.get(run.projectId);
13571
+ if (!projectRow) continue;
13572
+ const point = runHistoryById.get(run.id);
13573
+ const countable = run.status === RunStatuses.completed || run.status === RunStatuses.partial;
13574
+ const error = parseRunError(run.error);
13575
+ recentRuns.push({
13576
+ runId: run.id,
13577
+ projectName: projectRow.displayName || projectRow.name,
13578
+ projectSlug: projectRow.name,
13579
+ kindLabel: "Answer visibility sweep",
13580
+ status: run.status,
13581
+ createdAt: run.createdAt,
13582
+ startedAt: run.startedAt ?? null,
13583
+ finishedAt: run.finishedAt ?? null,
13584
+ durationMs: run.startedAt && run.finishedAt ? new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime() : null,
13585
+ mentionedCount: countable && point ? point.mentionedCount : null,
13586
+ citedCount: countable && point ? point.citedCount : null,
13587
+ totalCount: countable && point ? point.totalCount : null,
13588
+ errorSummary: error ? formatRunErrorOneLine(error) : null
13589
+ });
13590
+ }
13591
+ return {
13592
+ generatedAt,
13593
+ lastSweepAt,
13594
+ projectCount: projectRows.length,
13595
+ comparableProjectCount: feed.comparableProjectCount,
13596
+ firstSweepProjectCount: feed.firstSweepProjectCount,
13597
+ changeFeed: feed.changeFeed,
13598
+ changeFeedTotal: feed.changeFeedTotal,
13599
+ feedEmptyState: feed.feedEmptyState,
13600
+ recentRuns,
13601
+ projects: projectRowDtos
13602
+ };
13603
+ }
13262
13604
  app.get("/projects/:name/search", async (request, reply) => {
13263
13605
  const project = resolveProject(app.db, request.params.name);
13264
13606
  const rawQuery = (request.query.q ?? "").trim();
@@ -13885,6 +14227,7 @@ var SCHEMA_TABLE = {
13885
14227
  LatestProjectRunDto: latestProjectRunDtoSchema,
13886
14228
  LocationContext: locationContextSchema,
13887
14229
  NotificationDto: notificationDtoSchema,
14230
+ PortfolioDto: portfolioDtoSchema,
13888
14231
  ProjectDto: projectDtoSchema,
13889
14232
  ProjectOverviewDto: projectOverviewDtoSchema,
13890
14233
  ProjectReportDto: projectReportDtoSchema,
@@ -17139,6 +17482,17 @@ var routeCatalog = [
17139
17482
  404: errorResponse("Project not found.")
17140
17483
  }
17141
17484
  },
17485
+ {
17486
+ method: "get",
17487
+ path: "/api/v1/portfolio",
17488
+ summary: "Cross-project portfolio: what changed, recent runs, project state",
17489
+ description: 'One call for the dashboard Portfolio page and `canonry portfolio`. Returns a server-ordered "what changed" feed (citation/mention gains and losses over each project\'s comparable basket, failed sweeps, critical/high insight echoes, stale-visibility, query-set changes, and never-run projects \u2014 recency then severity), a timestamped recent-runs log with independent mention and cited result counts, and a per-project state table. Probe runs are excluded. `generatedAt` is the freshness anchor for every relative timestamp.',
17490
+ tags: ["intelligence"],
17491
+ parameters: [],
17492
+ responses: {
17493
+ 200: jsonResponse("Portfolio overview returned.", "PortfolioDto")
17494
+ }
17495
+ },
17142
17496
  {
17143
17497
  method: "get",
17144
17498
  path: "/api/v1/projects/{name}/overview",
@@ -21534,7 +21888,7 @@ async function googleRoutes(app, opts) {
21534
21888
 
21535
21889
  // ../api-routes/src/ads.ts
21536
21890
  import crypto19 from "crypto";
21537
- import { eq as eq22, and as and15, asc as asc2, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
21891
+ import { eq as eq22, and as and15, asc as asc3, gte as gte3, lte as lte2, inArray as inArray11 } from "drizzle-orm";
21538
21892
  function statusDto(row) {
21539
21893
  if (!row) return { connected: false };
21540
21894
  return {
@@ -21735,7 +22089,7 @@ async function adsRoutes(app, opts) {
21735
22089
  if (entityId) conditions.push(eq22(adsInsightsDaily.entityId, entityId));
21736
22090
  if (from) conditions.push(gte3(adsInsightsDaily.date, from));
21737
22091
  if (to) conditions.push(lte2(adsInsightsDaily.date, to));
21738
- const rows = app.db.select().from(adsInsightsDaily).where(and15(...conditions)).orderBy(asc2(adsInsightsDaily.date)).all();
22092
+ const rows = app.db.select().from(adsInsightsDaily).where(and15(...conditions)).orderBy(asc3(adsInsightsDaily.date)).all();
21739
22093
  const dtoRows = rows.map((row) => ({
21740
22094
  level: row.level,
21741
22095
  entityId: row.entityId,
@@ -25144,7 +25498,7 @@ async function wordpressRoutes(app, opts) {
25144
25498
 
25145
25499
  // ../api-routes/src/backlinks.ts
25146
25500
  import crypto23 from "crypto";
25147
- import { and as and20, asc as asc3, desc as desc14, eq as eq26, sql as sql10 } from "drizzle-orm";
25501
+ import { and as and20, asc as asc4, desc as desc14, eq as eq26, sql as sql10 } from "drizzle-orm";
25148
25502
 
25149
25503
  // ../integration-commoncrawl/src/constants.ts
25150
25504
  import os2 from "os";
@@ -25903,7 +26257,7 @@ async function backlinksRoutes(app, opts) {
25903
26257
  async (request, reply) => {
25904
26258
  const project = resolveProject(app.db, request.params.name);
25905
26259
  const source = parseSourceParam(request.query.source);
25906
- const rows = app.db.select().from(backlinkSummaries).where(and20(eq26(backlinkSummaries.projectId, project.id), eq26(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
26260
+ const rows = app.db.select().from(backlinkSummaries).where(and20(eq26(backlinkSummaries.projectId, project.id), eq26(backlinkSummaries.source, source))).orderBy(asc4(backlinkSummaries.queriedAt)).all();
25907
26261
  const response = rows.map((r) => ({
25908
26262
  release: r.release,
25909
26263
  totalLinkingDomains: r.totalLinkingDomains,
@@ -33868,7 +34222,7 @@ function dedupeStrings(input) {
33868
34222
 
33869
34223
  // ../api-routes/src/technical-aeo.ts
33870
34224
  import crypto29 from "crypto";
33871
- import { and as and26, asc as asc4, count, desc as desc17, eq as eq36, inArray as inArray13 } from "drizzle-orm";
34225
+ import { and as and26, asc as asc5, count, desc as desc17, eq as eq36, inArray as inArray13 } from "drizzle-orm";
33872
34226
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
33873
34227
  function emptyScore(projectName) {
33874
34228
  return {
@@ -33952,7 +34306,7 @@ async function technicalAeoRoutes(app, opts) {
33952
34306
  const total = totalRow?.value ?? 0;
33953
34307
  const limit = parsePositiveInt(request.query.limit, 100, 500);
33954
34308
  const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
33955
- const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc4(siteAuditPages.url) : asc4(siteAuditPages.overallScore);
34309
+ const orderBy = request.query.sort === "score-desc" ? desc17(siteAuditPages.overallScore) : request.query.sort === "url" ? asc5(siteAuditPages.url) : asc5(siteAuditPages.overallScore);
33956
34310
  const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
33957
34311
  const pages = rows.map((row) => ({
33958
34312
  url: row.url,
@@ -34887,7 +35241,7 @@ var IntelligenceService = class {
34887
35241
  // Backfill must not replay probe runs as if they were real sweeps.
34888
35242
  ne5(runs.trigger, RunTriggers.probe)
34889
35243
  )
34890
- ).orderBy(asc5(runs.finishedAt)).all();
35244
+ ).orderBy(asc6(runs.finishedAt)).all();
34891
35245
  let startIdx = 0;
34892
35246
  let endIdx = allRuns.length;
34893
35247
  if (opts?.fromRunId) {
@@ -24,7 +24,7 @@ import {
24
24
  trafficConnectVercelRequestSchema,
25
25
  trafficConnectWordpressRequestSchema,
26
26
  trafficEventKindSchema
27
- } from "./chunk-JF7EXHGA.js";
27
+ } from "./chunk-ADLS64PG.js";
28
28
 
29
29
  // src/config.ts
30
30
  import fs from "fs";
@@ -3085,6 +3085,18 @@ var getApiV1ProjectsByNameContentGaps = (options) => {
3085
3085
  ...options
3086
3086
  });
3087
3087
  };
3088
+ var getApiV1Portfolio = (options) => {
3089
+ return (options?.client ?? client).get({
3090
+ security: [
3091
+ {
3092
+ scheme: "bearer",
3093
+ type: "http"
3094
+ }
3095
+ ],
3096
+ url: "/api/v1/portfolio",
3097
+ ...options
3098
+ });
3099
+ };
3088
3100
  var getApiV1ProjectsByNameOverview = (options) => {
3089
3101
  return (options.client ?? client).get({
3090
3102
  security: [
@@ -3908,6 +3920,9 @@ var ApiClient = class {
3908
3920
  async apply(config) {
3909
3921
  return this.invoke(() => postApiV1Apply({ client: this.heyClient, body: config }));
3910
3922
  }
3923
+ async getPortfolio() {
3924
+ return this.invoke(() => getApiV1Portfolio({ client: this.heyClient }));
3925
+ }
3911
3926
  async getProjectOverview(project, opts) {
3912
3927
  return this.invoke(
3913
3928
  () => getApiV1ProjectsByNameOverview({
@@ -5681,6 +5696,17 @@ var canonryMcpTools = [
5681
5696
  openApiOperations: ["GET /api/v1/projects/{name}/report"],
5682
5697
  handler: (client2, input) => client2.getReport(input.project, input.period !== void 0 ? { period: input.period } : void 0)
5683
5698
  }),
5699
+ defineTool({
5700
+ name: "canonry_portfolio",
5701
+ title: "Get portfolio change feed (composite)",
5702
+ description: 'One-call cross-project portfolio: a server-ordered "what changed" feed \u2014 citation and mention gains/losses over each project\'s comparable query basket, failed sweeps, critical/high insight echoes, stale-visibility, query-set changes, and never-run projects, ranked by recency then severity \u2014 plus a timestamped recent-runs log carrying BOTH result signals (mentioned vs cited counts, independent) and a per-project state table. Probe runs excluded. Use it to answer "what changed across all my projects" without fanning out per project. `generatedAt` anchors every relative timestamp.',
5703
+ access: "read",
5704
+ tier: "monitoring",
5705
+ inputSchema: emptyInputSchema,
5706
+ annotations: readAnnotations(),
5707
+ openApiOperations: ["GET /api/v1/portfolio"],
5708
+ handler: (client2) => client2.getPortfolio()
5709
+ }),
5684
5710
  defineTool({
5685
5711
  name: "canonry_analytics_metrics",
5686
5712
  title: "Get citation & mention trend",