@ainyc/canonry 4.188.5 → 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-BidWghRA.js → AuditHistoryPanel-CZ7ygQfy.js} +1 -1
  2. package/assets/assets/{BacklinksPage-BnD3QJGh.js → BacklinksPage-CYEYxjE8.js} +1 -1
  3. package/assets/assets/{HistoryPage-gIPJzBFJ.js → HistoryPage-CvAllKzD.js} +1 -1
  4. package/assets/assets/{MeasurementPropertyPage-BFrE3x1E.js → MeasurementPropertyPage-BED9ak5a.js} +1 -1
  5. package/assets/assets/ProjectPage-Cgl5v16o.js +9 -0
  6. package/assets/assets/{RunRow-Q3ZUbaPe.js → RunRow-DUlFaUBp.js} +1 -1
  7. package/assets/assets/{RunsPage-Comm-TNN.js → RunsPage-BcbBe_aQ.js} +1 -1
  8. package/assets/assets/{SettingsPage-BCXBuXkb.js → SettingsPage-gCyOuoD_.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-Pzebrg1V.js → SiteHealthSection-DJfFmT3n.js} +3 -3
  10. package/assets/assets/{TrafficPage-BDfDAaiV.js → TrafficPage-2ATExz7V.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-Da9rSFpW.js → TrafficSourceDetailPage-0PBMk9UH.js} +1 -1
  12. package/assets/assets/{extract-error-message-QpSUO14Z.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-5G7c_8Fy.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-3WSOY5VU.js → chunk-HG6NLN7H.js} +327 -28
  18. package/dist/{chunk-YI7PHVUQ.js → chunk-J533DKYE.js} +2 -2
  19. package/dist/{chunk-AC3ZIRHR.js → chunk-QQLMRABM.js} +645 -502
  20. package/dist/{chunk-RYKQLQPS.js → chunk-YDYB3H4O.js} +1 -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 +12 -12
  26. package/assets/assets/ProjectPage-DjxZiTRe.js +0 -9
  27. package/assets/assets/index-_RSAg-e8.js +0 -86
@@ -14,7 +14,7 @@ import {
14
14
  loadConfig,
15
15
  loadConfigRaw,
16
16
  saveConfigPatch
17
- } from "./chunk-RYKQLQPS.js";
17
+ } from "./chunk-YDYB3H4O.js";
18
18
  import {
19
19
  CC_CACHE_DIR,
20
20
  DUCKDB_SPEC,
@@ -120,6 +120,7 @@ import {
120
120
  measurementRunCompleteness,
121
121
  migrate,
122
122
  nextRunFromCron,
123
+ nextRunFromSchedule,
123
124
  notifications,
124
125
  parseCookieHeader,
125
126
  parseJsonColumn,
@@ -161,7 +162,7 @@ import {
161
162
  siteCrawlSnapshots,
162
163
  toAlertView,
163
164
  usageCounters
164
- } from "./chunk-AC3ZIRHR.js";
165
+ } from "./chunk-QQLMRABM.js";
165
166
  import {
166
167
  AGENT_MEMORY_VALUE_MAX_BYTES,
167
168
  AGENT_PROVIDER_IDS,
@@ -223,6 +224,7 @@ import {
223
224
  bucketOnboardingCount,
224
225
  buildRunErrorFromMessages,
225
226
  buildSimpleMeasurementDefinition,
227
+ calendarRecurrenceSchema,
226
228
  canonicalizeGtmAccountId,
227
229
  canonicalizeGtmContainerId,
228
230
  canonicalizeGtmResourceSelection,
@@ -303,7 +305,7 @@ import {
303
305
  validationError,
304
306
  winnabilityClassLabel,
305
307
  withRetry
306
- } from "./chunk-3HZELBZZ.js";
308
+ } from "./chunk-CDI3YR57.js";
307
309
 
308
310
  // src/telemetry.ts
309
311
  import crypto from "crypto";
@@ -3760,6 +3762,35 @@ function createGeminiEmbedClient(apiKey, baseUrl) {
3760
3762
  };
3761
3763
  }
3762
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
+
3763
3794
  // ../provider-gemini/src/adapter.ts
3764
3795
  function toGeminiConfig(config) {
3765
3796
  return {
@@ -3791,6 +3822,7 @@ var geminiAdapter = {
3791
3822
  { id: "gemini-2.0-flash", displayName: "Gemini 2.0 Flash", tier: "standard" }
3792
3823
  ]
3793
3824
  },
3825
+ listModels: (config, signal) => listModels(toGeminiConfig(config), signal),
3794
3826
  validateConfig(config) {
3795
3827
  const result = validateConfig(toGeminiConfig(config));
3796
3828
  return {
@@ -4113,6 +4145,35 @@ function responseToRecord2(response) {
4113
4145
  }
4114
4146
  }
4115
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
+
4116
4177
  // ../provider-openai/src/adapter.ts
4117
4178
  function toOpenAIConfig(config) {
4118
4179
  return {
@@ -4143,6 +4204,7 @@ var openaiAdapter = {
4143
4204
  { id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
4144
4205
  ]
4145
4206
  },
4207
+ listModels: listModels2,
4146
4208
  validateConfig(config) {
4147
4209
  const result = validateConfig2(toOpenAIConfig(config));
4148
4210
  return {
@@ -4507,6 +4569,33 @@ function responseToRecord3(response) {
4507
4569
  }
4508
4570
  }
4509
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
+
4510
4599
  // ../provider-claude/src/adapter.ts
4511
4600
  function toClaudeConfig(config) {
4512
4601
  return {
@@ -4533,6 +4622,7 @@ var claudeAdapter = {
4533
4622
  { id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
4534
4623
  ]
4535
4624
  },
4625
+ listModels: listModels3,
4536
4626
  validateConfig(config) {
4537
4627
  const result = validateConfig3(toClaudeConfig(config));
4538
4628
  return {
@@ -4599,7 +4689,7 @@ var claudeAdapter = {
4599
4689
  };
4600
4690
 
4601
4691
  // ../provider-local/src/normalize.ts
4602
- import OpenAI2 from "openai";
4692
+ import OpenAI3 from "openai";
4603
4693
 
4604
4694
  // ../provider-local/src/utils.ts
4605
4695
  async function withRetry5(fn, options = {}) {
@@ -4637,7 +4727,7 @@ async function healthcheck4(config) {
4637
4727
  const validation = validateConfig4(config);
4638
4728
  if (!validation.ok) return validation;
4639
4729
  try {
4640
- const client = new OpenAI2({
4730
+ const client = new OpenAI3({
4641
4731
  baseURL: config.baseUrl,
4642
4732
  apiKey: config.apiKey || "not-needed"
4643
4733
  });
@@ -4667,7 +4757,7 @@ async function healthcheck4(config) {
4667
4757
  }
4668
4758
  async function executeTrackedQuery4(input) {
4669
4759
  const model = input.config.model ?? DEFAULT_MODEL4;
4670
- const client = new OpenAI2({
4760
+ const client = new OpenAI3({
4671
4761
  baseURL: input.config.baseUrl,
4672
4762
  apiKey: input.config.apiKey || "not-needed"
4673
4763
  });
@@ -4730,7 +4820,7 @@ function extractAnswerText2(rawResponse) {
4730
4820
  }
4731
4821
  async function generateText4(prompt, config) {
4732
4822
  const model = config.model ?? DEFAULT_MODEL4;
4733
- const client = new OpenAI2({
4823
+ const client = new OpenAI3({
4734
4824
  baseURL: config.baseUrl,
4735
4825
  apiKey: config.apiKey || "not-needed"
4736
4826
  });
@@ -4753,6 +4843,31 @@ function responseToRecord4(response) {
4753
4843
  }
4754
4844
  }
4755
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
+
4756
4871
  // ../provider-local/src/adapter.ts
4757
4872
  function toLocalConfig(config) {
4758
4873
  return {
@@ -4776,6 +4891,7 @@ var localAdapter = {
4776
4891
  { id: "llama3", displayName: "Llama 3", tier: "standard" }
4777
4892
  ]
4778
4893
  },
4894
+ listModels: listModels4,
4779
4895
  validateConfig(config) {
4780
4896
  const result = validateConfig4(toLocalConfig(config));
4781
4897
  return {
@@ -5415,7 +5531,7 @@ var cdpChatgptAdapter = {
5415
5531
  };
5416
5532
 
5417
5533
  // ../provider-perplexity/src/normalize.ts
5418
- import OpenAI3 from "openai";
5534
+ import OpenAI4 from "openai";
5419
5535
 
5420
5536
  // ../provider-perplexity/src/utils.ts
5421
5537
  async function withRetry6(fn, options = {}) {
@@ -5454,7 +5570,7 @@ async function healthcheck5(config) {
5454
5570
  const validation = validateConfig5(config);
5455
5571
  if (!validation.ok) return validation;
5456
5572
  try {
5457
- const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
5573
+ const client = new OpenAI4({ apiKey: config.apiKey, baseURL: BASE_URL });
5458
5574
  const response = await withRetry6(
5459
5575
  () => client.chat.completions.create({
5460
5576
  model: config.model ?? DEFAULT_MODEL5,
@@ -5479,7 +5595,7 @@ async function healthcheck5(config) {
5479
5595
  }
5480
5596
  async function executeTrackedQuery5(input) {
5481
5597
  const model = input.config.model ?? DEFAULT_MODEL5;
5482
- const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
5598
+ const client = new OpenAI4({ apiKey: input.config.apiKey, baseURL: BASE_URL });
5483
5599
  const prompt = buildPrompt4(input.query, input.location);
5484
5600
  try {
5485
5601
  const response = await withRetry6(
@@ -5639,7 +5755,7 @@ function extractDomainFromUri4(uri) {
5639
5755
  }
5640
5756
  async function generateText5(prompt, config) {
5641
5757
  const model = config.model ?? DEFAULT_MODEL5;
5642
- const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
5758
+ const client = new OpenAI4({ apiKey: config.apiKey, baseURL: BASE_URL });
5643
5759
  const response = await withRetry6(
5644
5760
  () => client.chat.completions.create({
5645
5761
  model,
@@ -11925,7 +12041,7 @@ function readStoredGroundingSources(rawResponse) {
11925
12041
  return result;
11926
12042
  }
11927
12043
  async function backfillInsightsCommand(project, opts) {
11928
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-GMG2S55U.js");
12044
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-M5TYY4TH.js");
11929
12045
  const config = loadConfig();
11930
12046
  const db = createClient(config.database);
11931
12047
  migrate(db);
@@ -12500,6 +12616,58 @@ var ProviderRegistry = class {
12500
12616
  }
12501
12617
  };
12502
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
+
12503
12671
  // src/scheduler.ts
12504
12672
  import crypto20 from "crypto";
12505
12673
  import cron from "node-cron";
@@ -12524,6 +12692,10 @@ function ensureDefaultHealthSchedule(db, projectId, now = (/* @__PURE__ */ new D
12524
12692
  function taskKey(projectId, kind) {
12525
12693
  return `${projectId}::${kind}`;
12526
12694
  }
12695
+ function scheduleRecurrence(schedule) {
12696
+ if (!schedule.recurrence) return null;
12697
+ return calendarRecurrenceSchema.safeParse(schedule.recurrence).data ?? null;
12698
+ }
12527
12699
  var Scheduler = class {
12528
12700
  db;
12529
12701
  callbacks;
@@ -12598,10 +12770,14 @@ var Scheduler = class {
12598
12770
  const allSchedules = this.db.select().from(schedules).where(eq17(schedules.enabled, true)).all();
12599
12771
  for (const schedule of allSchedules) {
12600
12772
  const missedRunAt = schedule.nextRunAt;
12601
- this.registerCronTask(schedule);
12602
- if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
12603
- log14.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
12604
- 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
+ }
12605
12781
  }
12606
12782
  }
12607
12783
  log14.info("started", { scheduleCount: allSchedules.length });
@@ -12661,12 +12837,21 @@ var Scheduler = class {
12661
12837
  }).where(eq17(schedules.id, scheduleId)).run();
12662
12838
  });
12663
12839
  }
12664
- 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
+ }
12665
12850
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
12666
12851
  const kind = schedule.kind;
12667
12852
  if (!cron.validate(cronExpr)) {
12668
12853
  log14.error("cron.invalid", { projectId, kind, cronExpr });
12669
- return;
12854
+ return false;
12670
12855
  }
12671
12856
  const task = cron.schedule(cronExpr, () => {
12672
12857
  this.triggerRun(scheduleId, projectId, kind);
@@ -12674,13 +12859,94 @@ var Scheduler = class {
12674
12859
  timezone
12675
12860
  });
12676
12861
  this.tasks.set(taskKey(projectId, kind), task);
12677
- this.updateScheduleTiming(scheduleId, {
12678
- nextRunAt: nextRunFromCron(cronExpr, timezone)
12679
- });
12862
+ if (!options.preserveNextRunAt || !schedule.nextRunAt) {
12863
+ this.updateScheduleTiming(scheduleId, {
12864
+ nextRunAt: nextRunFromCron(cronExpr, timezone)
12865
+ });
12866
+ }
12680
12867
  const label = schedule.preset ?? cronExpr;
12681
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
+ });
12682
12948
  }
12683
- triggerRun(scheduleId, projectId, kind) {
12949
+ triggerRun(scheduleId, projectId, kind, claimedOccurrence) {
12684
12950
  try {
12685
12951
  const now = (/* @__PURE__ */ new Date()).toISOString();
12686
12952
  const currentSchedule = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
@@ -12689,7 +12955,16 @@ var Scheduler = class {
12689
12955
  this.remove(projectId, kind);
12690
12956
  return;
12691
12957
  }
12692
- 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
+ });
12693
12968
  const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
12694
12969
  if (!project) {
12695
12970
  log14.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
@@ -12862,6 +13137,10 @@ var Scheduler = class {
12862
13137
  const locationLabel = resolvedLocation?.label ?? null;
12863
13138
  const scheduleProviders = currentSchedule.providers;
12864
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
+ }
12865
13144
  const queueResult = queueRunIfProjectIdle(this.db, {
12866
13145
  createdAt: now,
12867
13146
  kind: "answer-visibility",
@@ -12870,17 +13149,29 @@ var Scheduler = class {
12870
13149
  location: locationLabel,
12871
13150
  providers,
12872
13151
  runnableProviders: this.callbacks.getRunnableProviderNames?.(),
12873
- 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
+ } : {}
12874
13161
  });
12875
13162
  if (queueResult.conflict) {
13163
+ if (queueResult.scheduleClaimed === false) {
13164
+ log14.info("calendar.skipped-claimed", { projectName: project.name, scheduleId: currentSchedule.id });
13165
+ return;
13166
+ }
12876
13167
  log14.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
12877
- this.updateScheduleTiming(currentSchedule.id, {
12878
- nextRunAt
12879
- });
13168
+ if (!claimedOccurrence) this.updateScheduleTiming(currentSchedule.id, { nextRunAt });
12880
13169
  return;
12881
13170
  }
12882
13171
  const runId = queueResult.runId;
12883
- this.updateScheduleTiming(currentSchedule.id, {
13172
+ this.updateScheduleTiming(currentSchedule.id, claimedOccurrence ? {
13173
+ lastRunAt: now
13174
+ } : {
12884
13175
  lastRunAt: now,
12885
13176
  nextRunAt
12886
13177
  });
@@ -15440,6 +15731,12 @@ function mcpTransportPaths() {
15440
15731
  }
15441
15732
  return paths;
15442
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
+ }
15443
15740
  function registerMcpHttpRoutes(scope, opts) {
15444
15741
  const sessions = /* @__PURE__ */ new Map();
15445
15742
  const now = opts.now ?? (() => Date.now());
@@ -18677,6 +18974,7 @@ async function createServer(opts) {
18677
18974
  includeCanonryLocal: true
18678
18975
  },
18679
18976
  providerSummary,
18977
+ getProviderModels: createProviderModelCatalog(registry),
18680
18978
  providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
18681
18979
  name: a.name,
18682
18980
  displayName: a.displayName,
@@ -19099,6 +19397,7 @@ async function createServer(opts) {
19099
19397
  status: "ok",
19100
19398
  service: "canonry",
19101
19399
  version: PKG_VERSION2,
19400
+ mcp: mcpHttpHealth(app, apiPrefix),
19102
19401
  ...basePath ? { basePath: basePath.replace(/\/$/, "") } : {},
19103
19402
  ...update ? { updateAvailable: update } : {}
19104
19403
  };
@@ -5,11 +5,11 @@ import {
5
5
  installSkills,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-RYKQLQPS.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";