@ainyc/canonry 4.188.4 → 4.189.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 (27) hide show
  1. package/assets/assets/{AuditHistoryPanel-Cn1xsaFZ.js → AuditHistoryPanel-CZ7ygQfy.js} +1 -1
  2. package/assets/assets/{BacklinksPage-CfTxBHZn.js → BacklinksPage-CYEYxjE8.js} +1 -1
  3. package/assets/assets/{HistoryPage-BvgKqrbW.js → HistoryPage-CvAllKzD.js} +1 -1
  4. package/assets/assets/{MeasurementPropertyPage-CGbNTIdz.js → MeasurementPropertyPage-BED9ak5a.js} +1 -1
  5. package/assets/assets/ProjectPage-Cgl5v16o.js +9 -0
  6. package/assets/assets/{RunRow-DzneLSnv.js → RunRow-DUlFaUBp.js} +1 -1
  7. package/assets/assets/{RunsPage-BgByi_t3.js → RunsPage-BcbBe_aQ.js} +1 -1
  8. package/assets/assets/{SettingsPage-DBv-GyPu.js → SettingsPage-gCyOuoD_.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-DaNslx5J.js → SiteHealthSection-DJfFmT3n.js} +3 -3
  10. package/assets/assets/{TrafficPage-B6mfVlPe.js → TrafficPage-2ATExz7V.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BXwAVIDo.js → TrafficSourceDetailPage-0PBMk9UH.js} +1 -1
  12. package/assets/assets/{extract-error-message-DMJa2ku8.js → extract-error-message-D8N7K1pb.js} +1 -1
  13. package/assets/assets/index-Bhwxe6W_.js +86 -0
  14. package/assets/assets/{react-sigma_core.esm.min-DiTvmmQd.js → react-sigma_core.esm.min-CU1UA6VY.js} +1 -1
  15. package/assets/index.html +1 -1
  16. package/dist/{chunk-3HZELBZZ.js → chunk-CDI3YR57.js} +2587 -2569
  17. package/dist/{chunk-7NMMQ7QO.js → chunk-HG6NLN7H.js} +330 -30
  18. package/dist/{chunk-VJRQTKUB.js → chunk-J533DKYE.js} +2 -2
  19. package/dist/{chunk-AC3ZIRHR.js → chunk-QQLMRABM.js} +645 -502
  20. package/dist/{chunk-OXYM6VCJ.js → chunk-YDYB3H4O.js} +19 -1
  21. package/dist/cli.js +48 -22
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-GMG2S55U.js → intelligence-service-M5TYY4TH.js} +2 -2
  24. package/dist/mcp.js +3 -3
  25. package/package.json +11 -11
  26. package/assets/assets/ProjectPage-BQ4lzu49.js +0 -9
  27. package/assets/assets/index-BRnD7hdh.js +0 -86
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ApiClient,
3
+ CANONRY_MCP_TIERS,
3
4
  CANONRY_MCP_TOOLKIT_NAMES,
4
5
  CanonryMcpToolNames,
5
6
  CliError,
@@ -13,7 +14,7 @@ import {
13
14
  loadConfig,
14
15
  loadConfigRaw,
15
16
  saveConfigPatch
16
- } from "./chunk-OXYM6VCJ.js";
17
+ } from "./chunk-YDYB3H4O.js";
17
18
  import {
18
19
  CC_CACHE_DIR,
19
20
  DUCKDB_SPEC,
@@ -119,6 +120,7 @@ import {
119
120
  measurementRunCompleteness,
120
121
  migrate,
121
122
  nextRunFromCron,
123
+ nextRunFromSchedule,
122
124
  notifications,
123
125
  parseCookieHeader,
124
126
  parseJsonColumn,
@@ -160,7 +162,7 @@ import {
160
162
  siteCrawlSnapshots,
161
163
  toAlertView,
162
164
  usageCounters
163
- } from "./chunk-AC3ZIRHR.js";
165
+ } from "./chunk-QQLMRABM.js";
164
166
  import {
165
167
  AGENT_MEMORY_VALUE_MAX_BYTES,
166
168
  AGENT_PROVIDER_IDS,
@@ -222,6 +224,7 @@ import {
222
224
  bucketOnboardingCount,
223
225
  buildRunErrorFromMessages,
224
226
  buildSimpleMeasurementDefinition,
227
+ calendarRecurrenceSchema,
225
228
  canonicalizeGtmAccountId,
226
229
  canonicalizeGtmContainerId,
227
230
  canonicalizeGtmResourceSelection,
@@ -302,7 +305,7 @@ import {
302
305
  validationError,
303
306
  winnabilityClassLabel,
304
307
  withRetry
305
- } from "./chunk-3HZELBZZ.js";
308
+ } from "./chunk-CDI3YR57.js";
306
309
 
307
310
  // src/telemetry.ts
308
311
  import crypto from "crypto";
@@ -3759,6 +3762,35 @@ function createGeminiEmbedClient(apiKey, baseUrl) {
3759
3762
  };
3760
3763
  }
3761
3764
 
3765
+ // ../provider-gemini/src/list-models.ts
3766
+ function listModels(config, signal) {
3767
+ return withRetry(async () => {
3768
+ signal.throwIfAborted();
3769
+ const client = createClient2(config);
3770
+ const page = await client.models.list({ config: { pageSize: 100, queryBase: true, abortSignal: signal, httpOptions: { timeout: 2500 } } });
3771
+ const models = [];
3772
+ let seen = 0;
3773
+ for await (const model of page) {
3774
+ signal.throwIfAborted();
3775
+ if (++seen > 1e3) throw new Error("Model catalog exceeded the discovery limit");
3776
+ const id = model.name?.replace(/^(?:publishers\/google\/)?models\//, "");
3777
+ if (!id?.startsWith("gemini-") || /image|tts|audio|live|embedding/.test(id)) continue;
3778
+ if (model.supportedActions && !model.supportedActions.includes("generateContent")) continue;
3779
+ models.push({ id, displayName: model.displayName || id, tier: "standard" });
3780
+ }
3781
+ return models;
3782
+ }, {
3783
+ maxRetries: 1,
3784
+ baseDelayMs: 200,
3785
+ isRetryable: (error) => !signal.aborted && isRetryableHttpError(error),
3786
+ computeDelayMs: (_attempt, error, delay) => {
3787
+ const retryDelay = retryAfterDelayMs(error) ?? delay;
3788
+ if (retryDelay >= 2500) throw error;
3789
+ return retryDelay;
3790
+ }
3791
+ });
3792
+ }
3793
+
3762
3794
  // ../provider-gemini/src/adapter.ts
3763
3795
  function toGeminiConfig(config) {
3764
3796
  return {
@@ -3790,6 +3822,7 @@ var geminiAdapter = {
3790
3822
  { id: "gemini-2.0-flash", displayName: "Gemini 2.0 Flash", tier: "standard" }
3791
3823
  ]
3792
3824
  },
3825
+ listModels: (config, signal) => listModels(toGeminiConfig(config), signal),
3793
3826
  validateConfig(config) {
3794
3827
  const result = validateConfig(toGeminiConfig(config));
3795
3828
  return {
@@ -4112,6 +4145,35 @@ function responseToRecord2(response) {
4112
4145
  }
4113
4146
  }
4114
4147
 
4148
+ // ../provider-openai/src/list-models.ts
4149
+ import OpenAI2 from "openai";
4150
+ function listModels2(config, signal) {
4151
+ return withRetry(async () => {
4152
+ signal.throwIfAborted();
4153
+ const client = new OpenAI2({ apiKey: config.apiKey, baseURL: config.baseUrl, maxRetries: 0, timeout: 2500 });
4154
+ const models = [];
4155
+ const page = client.models.list({ signal });
4156
+ let seen = 0;
4157
+ for await (const model of page) {
4158
+ signal.throwIfAborted();
4159
+ if (++seen > 1e3) throw new Error("Model catalog exceeded the discovery limit");
4160
+ if (!/^(?:gpt-|o\d|chat-)/.test(model.id) || /audio|realtime|transcrib|tts|image|codex|search|instruct/.test(model.id)) continue;
4161
+ if (/^gpt-(?:3\.5|4)(?:-|$)/.test(model.id)) continue;
4162
+ models.push({ id: model.id, displayName: model.id, tier: "standard" });
4163
+ }
4164
+ return models;
4165
+ }, {
4166
+ maxRetries: 1,
4167
+ baseDelayMs: 200,
4168
+ isRetryable: (error) => !signal.aborted && isRetryableHttpError(error),
4169
+ computeDelayMs: (_attempt, error, delay) => {
4170
+ const retryDelay = retryAfterDelayMs(error) ?? delay;
4171
+ if (retryDelay >= 2500) throw error;
4172
+ return retryDelay;
4173
+ }
4174
+ });
4175
+ }
4176
+
4115
4177
  // ../provider-openai/src/adapter.ts
4116
4178
  function toOpenAIConfig(config) {
4117
4179
  return {
@@ -4142,6 +4204,7 @@ var openaiAdapter = {
4142
4204
  { id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
4143
4205
  ]
4144
4206
  },
4207
+ listModels: listModels2,
4145
4208
  validateConfig(config) {
4146
4209
  const result = validateConfig2(toOpenAIConfig(config));
4147
4210
  return {
@@ -4506,6 +4569,33 @@ function responseToRecord3(response) {
4506
4569
  }
4507
4570
  }
4508
4571
 
4572
+ // ../provider-claude/src/list-models.ts
4573
+ import Anthropic2 from "@anthropic-ai/sdk";
4574
+ function listModels3(config, signal) {
4575
+ return withRetry(async () => {
4576
+ signal.throwIfAborted();
4577
+ const client = new Anthropic2({ apiKey: config.apiKey, baseURL: config.baseUrl, maxRetries: 0, timeout: 2500 });
4578
+ const models = [];
4579
+ const page = client.models.list({ limit: 100 }, { signal });
4580
+ let seen = 0;
4581
+ for await (const model of page) {
4582
+ signal.throwIfAborted();
4583
+ if (++seen > 1e3) throw new Error("Model catalog exceeded the discovery limit");
4584
+ models.push({ id: model.id, displayName: model.display_name, tier: "standard" });
4585
+ }
4586
+ return models;
4587
+ }, {
4588
+ maxRetries: 1,
4589
+ baseDelayMs: 200,
4590
+ isRetryable: (error) => !signal.aborted && isRetryableHttpError(error),
4591
+ computeDelayMs: (_attempt, error, delay) => {
4592
+ const retryDelay = retryAfterDelayMs(error) ?? delay;
4593
+ if (retryDelay >= 2500) throw error;
4594
+ return retryDelay;
4595
+ }
4596
+ });
4597
+ }
4598
+
4509
4599
  // ../provider-claude/src/adapter.ts
4510
4600
  function toClaudeConfig(config) {
4511
4601
  return {
@@ -4532,6 +4622,7 @@ var claudeAdapter = {
4532
4622
  { id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
4533
4623
  ]
4534
4624
  },
4625
+ listModels: listModels3,
4535
4626
  validateConfig(config) {
4536
4627
  const result = validateConfig3(toClaudeConfig(config));
4537
4628
  return {
@@ -4598,7 +4689,7 @@ var claudeAdapter = {
4598
4689
  };
4599
4690
 
4600
4691
  // ../provider-local/src/normalize.ts
4601
- import OpenAI2 from "openai";
4692
+ import OpenAI3 from "openai";
4602
4693
 
4603
4694
  // ../provider-local/src/utils.ts
4604
4695
  async function withRetry5(fn, options = {}) {
@@ -4636,7 +4727,7 @@ async function healthcheck4(config) {
4636
4727
  const validation = validateConfig4(config);
4637
4728
  if (!validation.ok) return validation;
4638
4729
  try {
4639
- const client = new OpenAI2({
4730
+ const client = new OpenAI3({
4640
4731
  baseURL: config.baseUrl,
4641
4732
  apiKey: config.apiKey || "not-needed"
4642
4733
  });
@@ -4666,7 +4757,7 @@ async function healthcheck4(config) {
4666
4757
  }
4667
4758
  async function executeTrackedQuery4(input) {
4668
4759
  const model = input.config.model ?? DEFAULT_MODEL4;
4669
- const client = new OpenAI2({
4760
+ const client = new OpenAI3({
4670
4761
  baseURL: input.config.baseUrl,
4671
4762
  apiKey: input.config.apiKey || "not-needed"
4672
4763
  });
@@ -4729,7 +4820,7 @@ function extractAnswerText2(rawResponse) {
4729
4820
  }
4730
4821
  async function generateText4(prompt, config) {
4731
4822
  const model = config.model ?? DEFAULT_MODEL4;
4732
- const client = new OpenAI2({
4823
+ const client = new OpenAI3({
4733
4824
  baseURL: config.baseUrl,
4734
4825
  apiKey: config.apiKey || "not-needed"
4735
4826
  });
@@ -4752,6 +4843,31 @@ function responseToRecord4(response) {
4752
4843
  }
4753
4844
  }
4754
4845
 
4846
+ // ../provider-local/src/list-models.ts
4847
+ function listModels4(config, signal) {
4848
+ return withRetry(async () => {
4849
+ signal.throwIfAborted();
4850
+ if (!config.baseUrl) throw new Error("Local model discovery requires a configured endpoint");
4851
+ const response = await fetch(`${config.baseUrl.replace(/\/$/, "")}/models`, {
4852
+ method: "GET",
4853
+ signal,
4854
+ headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}
4855
+ });
4856
+ if (!response.ok) throw Object.assign(new Error("Local model discovery failed"), { status: response.status, headers: response.headers });
4857
+ const body = await response.json();
4858
+ return body.data.filter((model) => typeof model.id === "string" && model.id.trim()).slice(0, 1e3).map((model) => ({ id: model.id, displayName: model.id, tier: "standard" }));
4859
+ }, {
4860
+ maxRetries: 1,
4861
+ baseDelayMs: 200,
4862
+ isRetryable: (error) => !signal.aborted && isRetryableHttpError(error),
4863
+ computeDelayMs: (_attempt, error, delay) => {
4864
+ const retryDelay = retryAfterDelayMs(error) ?? delay;
4865
+ if (retryDelay >= 2500) throw error;
4866
+ return retryDelay;
4867
+ }
4868
+ });
4869
+ }
4870
+
4755
4871
  // ../provider-local/src/adapter.ts
4756
4872
  function toLocalConfig(config) {
4757
4873
  return {
@@ -4775,6 +4891,7 @@ var localAdapter = {
4775
4891
  { id: "llama3", displayName: "Llama 3", tier: "standard" }
4776
4892
  ]
4777
4893
  },
4894
+ listModels: listModels4,
4778
4895
  validateConfig(config) {
4779
4896
  const result = validateConfig4(toLocalConfig(config));
4780
4897
  return {
@@ -5414,7 +5531,7 @@ var cdpChatgptAdapter = {
5414
5531
  };
5415
5532
 
5416
5533
  // ../provider-perplexity/src/normalize.ts
5417
- import OpenAI3 from "openai";
5534
+ import OpenAI4 from "openai";
5418
5535
 
5419
5536
  // ../provider-perplexity/src/utils.ts
5420
5537
  async function withRetry6(fn, options = {}) {
@@ -5453,7 +5570,7 @@ async function healthcheck5(config) {
5453
5570
  const validation = validateConfig5(config);
5454
5571
  if (!validation.ok) return validation;
5455
5572
  try {
5456
- const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
5573
+ const client = new OpenAI4({ apiKey: config.apiKey, baseURL: BASE_URL });
5457
5574
  const response = await withRetry6(
5458
5575
  () => client.chat.completions.create({
5459
5576
  model: config.model ?? DEFAULT_MODEL5,
@@ -5478,7 +5595,7 @@ async function healthcheck5(config) {
5478
5595
  }
5479
5596
  async function executeTrackedQuery5(input) {
5480
5597
  const model = input.config.model ?? DEFAULT_MODEL5;
5481
- const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
5598
+ const client = new OpenAI4({ apiKey: input.config.apiKey, baseURL: BASE_URL });
5482
5599
  const prompt = buildPrompt4(input.query, input.location);
5483
5600
  try {
5484
5601
  const response = await withRetry6(
@@ -5638,7 +5755,7 @@ function extractDomainFromUri4(uri) {
5638
5755
  }
5639
5756
  async function generateText5(prompt, config) {
5640
5757
  const model = config.model ?? DEFAULT_MODEL5;
5641
- const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
5758
+ const client = new OpenAI4({ apiKey: config.apiKey, baseURL: BASE_URL });
5642
5759
  const response = await withRetry6(
5643
5760
  () => client.chat.completions.create({
5644
5761
  model,
@@ -11924,7 +12041,7 @@ function readStoredGroundingSources(rawResponse) {
11924
12041
  return result;
11925
12042
  }
11926
12043
  async function backfillInsightsCommand(project, opts) {
11927
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-GMG2S55U.js");
12044
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-M5TYY4TH.js");
11928
12045
  const config = loadConfig();
11929
12046
  const db = createClient(config.database);
11930
12047
  migrate(db);
@@ -12499,6 +12616,58 @@ var ProviderRegistry = class {
12499
12616
  }
12500
12617
  };
12501
12618
 
12619
+ // src/provider-model-catalog.ts
12620
+ import { createHash } from "crypto";
12621
+ var TTL_MS = 60 * 60 * 1e3;
12622
+ var RETRY_MS = 60 * 1e3;
12623
+ var TIMEOUT_MS2 = 3e3;
12624
+ function createProviderModelCatalog(registry) {
12625
+ const cache = /* @__PURE__ */ new Map();
12626
+ return async (name) => {
12627
+ const provider = registry.get(name);
12628
+ if (!provider) {
12629
+ cache.delete(name);
12630
+ return [];
12631
+ }
12632
+ const fallback = provider.adapter.modelRegistry.knownModels;
12633
+ if (!provider.adapter.listModels) return fallback;
12634
+ const identity = createHash("sha256").update(JSON.stringify(provider.config)).digest("hex");
12635
+ let entry = cache.get(name);
12636
+ if (entry?.identity !== identity) {
12637
+ entry = { identity, expiresAt: 0 };
12638
+ cache.set(name, entry);
12639
+ }
12640
+ if (entry.pending) return entry.pending;
12641
+ if (entry.expiresAt > Date.now()) return entry.models ?? fallback;
12642
+ const current = entry;
12643
+ const controller = new AbortController();
12644
+ let timer;
12645
+ const deadline = new Promise((_resolve, reject) => {
12646
+ timer = setTimeout(() => {
12647
+ controller.abort();
12648
+ reject(new Error("Model discovery timed out"));
12649
+ }, TIMEOUT_MS2);
12650
+ });
12651
+ current.pending = Promise.race([
12652
+ Promise.resolve().then(() => provider.adapter.listModels(provider.config, controller.signal)),
12653
+ deadline
12654
+ ]).then((models) => {
12655
+ const usableModels = [...new Map(models.filter((model) => model.id.trim()).map((model) => [model.id, model])).values()].sort((a, b) => b.id.localeCompare(a.id, "en", { numeric: true }));
12656
+ if (!usableModels.length) throw new Error("Empty model catalog");
12657
+ current.models = usableModels;
12658
+ current.expiresAt = Date.now() + TTL_MS;
12659
+ return current.models;
12660
+ }).catch((error) => {
12661
+ current.expiresAt = Date.now() + Math.max(RETRY_MS, retryAfterDelayMs(error) ?? 0);
12662
+ return current.models ?? fallback;
12663
+ }).finally(() => {
12664
+ clearTimeout(timer);
12665
+ current.pending = void 0;
12666
+ });
12667
+ return current.pending;
12668
+ };
12669
+ }
12670
+
12502
12671
  // src/scheduler.ts
12503
12672
  import crypto20 from "crypto";
12504
12673
  import cron from "node-cron";
@@ -12523,6 +12692,10 @@ function ensureDefaultHealthSchedule(db, projectId, now = (/* @__PURE__ */ new D
12523
12692
  function taskKey(projectId, kind) {
12524
12693
  return `${projectId}::${kind}`;
12525
12694
  }
12695
+ function scheduleRecurrence(schedule) {
12696
+ if (!schedule.recurrence) return null;
12697
+ return calendarRecurrenceSchema.safeParse(schedule.recurrence).data ?? null;
12698
+ }
12526
12699
  var Scheduler = class {
12527
12700
  db;
12528
12701
  callbacks;
@@ -12597,10 +12770,14 @@ var Scheduler = class {
12597
12770
  const allSchedules = this.db.select().from(schedules).where(eq17(schedules.enabled, true)).all();
12598
12771
  for (const schedule of allSchedules) {
12599
12772
  const missedRunAt = schedule.nextRunAt;
12600
- this.registerCronTask(schedule);
12601
- if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
12602
- log14.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
12603
- this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
12773
+ const recurrence = scheduleRecurrence(schedule);
12774
+ const registered = this.registerCronTask(schedule, recurrence ? { preserveNextRunAt: true } : {});
12775
+ if (registered && missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
12776
+ const answerRecurrence = recurrence && schedule.kind === SchedulableRunKinds["answer-visibility"];
12777
+ if (!recurrence || answerRecurrence || this.claimCalendarOccurrence(schedule, missedRunAt, /* @__PURE__ */ new Date())) {
12778
+ log14.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
12779
+ this.triggerRun(schedule.id, schedule.projectId, schedule.kind, answerRecurrence ? missedRunAt : void 0);
12780
+ }
12604
12781
  }
12605
12782
  }
12606
12783
  log14.info("started", { scheduleCount: allSchedules.length });
@@ -12660,12 +12837,21 @@ var Scheduler = class {
12660
12837
  }).where(eq17(schedules.id, scheduleId)).run();
12661
12838
  });
12662
12839
  }
12663
- registerCronTask(schedule) {
12840
+ registerCronTask(schedule, options = {}) {
12841
+ const recurrence = scheduleRecurrence(schedule);
12842
+ if (recurrence) {
12843
+ this.registerCalendarTask(schedule, recurrence, options);
12844
+ return true;
12845
+ }
12846
+ if (schedule.recurrence) {
12847
+ log14.error("calendar.invalid", { projectId: schedule.projectId, kind: schedule.kind });
12848
+ return false;
12849
+ }
12664
12850
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
12665
12851
  const kind = schedule.kind;
12666
12852
  if (!cron.validate(cronExpr)) {
12667
12853
  log14.error("cron.invalid", { projectId, kind, cronExpr });
12668
- return;
12854
+ return false;
12669
12855
  }
12670
12856
  const task = cron.schedule(cronExpr, () => {
12671
12857
  this.triggerRun(scheduleId, projectId, kind);
@@ -12673,13 +12859,94 @@ var Scheduler = class {
12673
12859
  timezone
12674
12860
  });
12675
12861
  this.tasks.set(taskKey(projectId, kind), task);
12676
- this.updateScheduleTiming(scheduleId, {
12677
- nextRunAt: nextRunFromCron(cronExpr, timezone)
12678
- });
12862
+ if (!options.preserveNextRunAt || !schedule.nextRunAt) {
12863
+ this.updateScheduleTiming(scheduleId, {
12864
+ nextRunAt: nextRunFromCron(cronExpr, timezone)
12865
+ });
12866
+ }
12679
12867
  const label = schedule.preset ?? cronExpr;
12680
12868
  log14.info("cron.registered", { projectId, kind, schedule: label, timezone });
12869
+ return true;
12870
+ }
12871
+ /** Calendar schedules are checked at most once a minute so long intervals
12872
+ * never exceed Node's timeout limit and a delayed event loop cannot fire an
12873
+ * old occurrence twice. The row's nextRunAt is a persistent claim token. */
12874
+ registerCalendarTask(schedule, recurrence, options) {
12875
+ const { id: scheduleId, projectId, timezone } = schedule;
12876
+ const kind = schedule.kind;
12877
+ const key = taskKey(projectId, kind);
12878
+ let timer;
12879
+ let stopped = false;
12880
+ const task = {
12881
+ stop: () => {
12882
+ stopped = true;
12883
+ if (timer) clearTimeout(timer);
12884
+ },
12885
+ destroy: () => {
12886
+ stopped = true;
12887
+ if (timer) clearTimeout(timer);
12888
+ }
12889
+ };
12890
+ const arm = (nextRunAt) => {
12891
+ if (stopped) return;
12892
+ const dueMs = nextRunAt ? Date.parse(nextRunAt) : Number.NaN;
12893
+ const delay = Number.isFinite(dueMs) && dueMs > Date.now() ? Math.min(6e4, Math.max(1, dueMs - Date.now())) : 6e4;
12894
+ timer = setTimeout(tick, delay);
12895
+ };
12896
+ const tick = () => {
12897
+ if (stopped) return;
12898
+ const current = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
12899
+ if (!current || !current.enabled || !scheduleRecurrence(current)) {
12900
+ this.remove(projectId, kind);
12901
+ return;
12902
+ }
12903
+ const now = /* @__PURE__ */ new Date();
12904
+ let nextRunAt = current.nextRunAt;
12905
+ if (!nextRunAt || Number.isNaN(Date.parse(nextRunAt))) {
12906
+ nextRunAt = nextRunFromSchedule({ cronExpr: current.cronExpr, timezone: current.timezone, recurrence: scheduleRecurrence(current) }, now);
12907
+ if (nextRunAt) this.updateScheduleTiming(current.id, { nextRunAt });
12908
+ }
12909
+ if (nextRunAt && Date.parse(nextRunAt) <= now.getTime()) {
12910
+ if (kind === SchedulableRunKinds["answer-visibility"]) {
12911
+ this.triggerRun(scheduleId, projectId, kind, nextRunAt);
12912
+ } else if (this.claimCalendarOccurrence(current, nextRunAt, now)) {
12913
+ this.triggerRun(scheduleId, projectId, kind);
12914
+ }
12915
+ }
12916
+ arm(this.db.select({ nextRunAt: schedules.nextRunAt }).from(schedules).where(eq17(schedules.id, scheduleId)).get()?.nextRunAt);
12917
+ };
12918
+ this.tasks.set(key, task);
12919
+ let registeredNextRunAt = schedule.nextRunAt;
12920
+ if (!options.preserveNextRunAt || !registeredNextRunAt) {
12921
+ registeredNextRunAt = nextRunFromSchedule({ cronExpr: schedule.cronExpr, timezone, recurrence }, /* @__PURE__ */ new Date());
12922
+ if (registeredNextRunAt) this.updateScheduleTiming(scheduleId, { nextRunAt: registeredNextRunAt });
12923
+ }
12924
+ if (options.preserveNextRunAt) arm(registeredNextRunAt);
12925
+ else timer = setTimeout(tick, 0);
12926
+ log14.info("calendar.registered", { projectId, kind, recurrence, timezone });
12927
+ }
12928
+ /** Atomically advance a due callback-only calendar occurrence before dispatch. */
12929
+ claimCalendarOccurrence(schedule, dueAt, now) {
12930
+ return this.db.transaction((tx) => {
12931
+ const current = tx.select().from(schedules).where(eq17(schedules.id, schedule.id)).get();
12932
+ if (!current || !current.enabled || current.updatedAt !== schedule.updatedAt || current.nextRunAt !== dueAt) return false;
12933
+ const recurrence = scheduleRecurrence(current);
12934
+ if (!recurrence) return false;
12935
+ const nextRunAt = nextRunFromSchedule({ cronExpr: current.cronExpr, timezone: current.timezone, recurrence }, now);
12936
+ if (!nextRunAt) return false;
12937
+ const result = tx.update(schedules).set({
12938
+ nextRunAt,
12939
+ updatedAt: nextScheduleUpdatedAt(current.updatedAt)
12940
+ }).where(and15(
12941
+ eq17(schedules.id, current.id),
12942
+ eq17(schedules.enabled, true),
12943
+ eq17(schedules.nextRunAt, dueAt),
12944
+ eq17(schedules.updatedAt, schedule.updatedAt)
12945
+ )).run();
12946
+ return result.changes === 1;
12947
+ });
12681
12948
  }
12682
- triggerRun(scheduleId, projectId, kind) {
12949
+ triggerRun(scheduleId, projectId, kind, claimedOccurrence) {
12683
12950
  try {
12684
12951
  const now = (/* @__PURE__ */ new Date()).toISOString();
12685
12952
  const currentSchedule = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
@@ -12688,7 +12955,16 @@ var Scheduler = class {
12688
12955
  this.remove(projectId, kind);
12689
12956
  return;
12690
12957
  }
12691
- const nextRunAt = nextRunFromCron(currentSchedule.cronExpr, currentSchedule.timezone);
12958
+ const recurrence = scheduleRecurrence(currentSchedule);
12959
+ if (claimedOccurrence && (!recurrence || currentSchedule.nextRunAt !== claimedOccurrence)) {
12960
+ log14.info("calendar.stale-claim", { projectId, scheduleId, kind });
12961
+ return;
12962
+ }
12963
+ const nextRunAt = nextRunFromSchedule({
12964
+ cronExpr: currentSchedule.cronExpr,
12965
+ timezone: currentSchedule.timezone,
12966
+ recurrence
12967
+ });
12692
12968
  const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
12693
12969
  if (!project) {
12694
12970
  log14.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
@@ -12861,6 +13137,10 @@ var Scheduler = class {
12861
13137
  const locationLabel = resolvedLocation?.label ?? null;
12862
13138
  const scheduleProviders = currentSchedule.providers;
12863
13139
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13140
+ if (claimedOccurrence && recurrence && !nextRunAt) {
13141
+ log14.error("calendar.invalid", { projectId, kind, scheduleId });
13142
+ return;
13143
+ }
12864
13144
  const queueResult = queueRunIfProjectIdle(this.db, {
12865
13145
  createdAt: now,
12866
13146
  kind: "answer-visibility",
@@ -12869,17 +13149,29 @@ var Scheduler = class {
12869
13149
  location: locationLabel,
12870
13150
  providers,
12871
13151
  runnableProviders: this.callbacks.getRunnableProviderNames?.(),
12872
- providerModels: this.callbacks.getEffectiveProviderModels?.()
13152
+ providerModels: this.callbacks.getEffectiveProviderModels?.(),
13153
+ ...claimedOccurrence && recurrence && currentSchedule.nextRunAt === claimedOccurrence ? {
13154
+ scheduleClaim: {
13155
+ scheduleId: currentSchedule.id,
13156
+ dueAt: claimedOccurrence,
13157
+ expectedUpdatedAt: currentSchedule.updatedAt,
13158
+ nextRunAt
13159
+ }
13160
+ } : {}
12873
13161
  });
12874
13162
  if (queueResult.conflict) {
13163
+ if (queueResult.scheduleClaimed === false) {
13164
+ log14.info("calendar.skipped-claimed", { projectName: project.name, scheduleId: currentSchedule.id });
13165
+ return;
13166
+ }
12875
13167
  log14.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
12876
- this.updateScheduleTiming(currentSchedule.id, {
12877
- nextRunAt
12878
- });
13168
+ if (!claimedOccurrence) this.updateScheduleTiming(currentSchedule.id, { nextRunAt });
12879
13169
  return;
12880
13170
  }
12881
13171
  const runId = queueResult.runId;
12882
- this.updateScheduleTiming(currentSchedule.id, {
13172
+ this.updateScheduleTiming(currentSchedule.id, claimedOccurrence ? {
13173
+ lastRunAt: now
13174
+ } : {
12883
13175
  lastRunAt: now,
12884
13176
  nextRunAt
12885
13177
  });
@@ -15439,6 +15731,12 @@ function mcpTransportPaths() {
15439
15731
  }
15440
15732
  return paths;
15441
15733
  }
15734
+ function mcpHttpHealth(app, apiPrefix) {
15735
+ const available = mcpTransportPaths().every(
15736
+ (path9) => ["POST", "GET", "DELETE"].every((method) => app.hasRoute({ method, url: `${apiPrefix}${path9}` }))
15737
+ );
15738
+ return { status: available ? "available" : "unavailable" };
15739
+ }
15442
15740
  function registerMcpHttpRoutes(scope, opts) {
15443
15741
  const sessions = /* @__PURE__ */ new Map();
15444
15742
  const now = opts.now ?? (() => Date.now());
@@ -15547,8 +15845,8 @@ function registerMcpHttpRoutes(scope, opts) {
15547
15845
  scope.get(pathname, { config }, run);
15548
15846
  scope.delete(pathname, { config }, run);
15549
15847
  }
15550
- mount("/mcp", { id: "core", tiers: ["core"], readOnly: false });
15551
- mount("/mcp/readonly", { id: "core:ro", tiers: ["core"], readOnly: true });
15848
+ mount("/mcp", { id: "default", tiers: CANONRY_MCP_TIERS, readOnly: false });
15849
+ mount("/mcp/readonly", { id: "default:ro", tiers: CANONRY_MCP_TIERS, readOnly: true });
15552
15850
  for (const toolkit of CANONRY_MCP_TOOLKIT_NAMES) {
15553
15851
  const tiers = ["core", toolkit];
15554
15852
  mount(`/mcp/x/${toolkit}`, { id: toolkit, tiers, readOnly: false });
@@ -18676,6 +18974,7 @@ async function createServer(opts) {
18676
18974
  includeCanonryLocal: true
18677
18975
  },
18678
18976
  providerSummary,
18977
+ getProviderModels: createProviderModelCatalog(registry),
18679
18978
  providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
18680
18979
  name: a.name,
18681
18980
  displayName: a.displayName,
@@ -19098,6 +19397,7 @@ async function createServer(opts) {
19098
19397
  status: "ok",
19099
19398
  service: "canonry",
19100
19399
  version: PKG_VERSION2,
19400
+ mcp: mcpHttpHealth(app, apiPrefix),
19101
19401
  ...basePath ? { basePath: basePath.replace(/\/$/, "") } : {},
19102
19402
  ...update ? { updateAvailable: update } : {}
19103
19403
  };
@@ -5,11 +5,11 @@ import {
5
5
  installSkills,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-OXYM6VCJ.js";
8
+ } from "./chunk-YDYB3H4O.js";
9
9
  import {
10
10
  SKILL_MANIFEST_FILENAME,
11
11
  SkillsClients
12
- } from "./chunk-3HZELBZZ.js";
12
+ } from "./chunk-CDI3YR57.js";
13
13
 
14
14
  // src/skills-autosync.ts
15
15
  import fs from "fs";