@amerged/ohmyhost-mcp 0.1.40 → 0.1.42

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.
@@ -1,5 +1,28 @@
1
1
  import { isUserSecretName, USER_SECRET_NAME_PATTERN } from "@ohmyhost/contracts/secret-names";
2
2
  import type { DatabaseComputeSelection } from "@ohmyhost/contracts/database-sizes";
3
+ import {
4
+ DATABASE_WEBHOOK_EVENT_TYPES,
5
+ DATABASE_SCHEDULE_MAX_EVERY_SECONDS,
6
+ DATABASE_SCHEDULE_MAX_STATEMENTS,
7
+ DATABASE_SCHEDULE_MAX_TIMEOUT_MS,
8
+ DATABASE_SCHEDULE_MIN_EVERY_SECONDS,
9
+ DATABASE_SCHEDULE_MIN_TIMEOUT_MS,
10
+ DATABASE_SCHEDULE_STATEMENT_MAX_BYTES,
11
+ type DatabaseExecutionUsagePage,
12
+ type DatabaseSchedule,
13
+ type DatabaseSchedulePage,
14
+ type DatabaseScheduleDeleted,
15
+ type DatabaseScheduleRunAccepted,
16
+ type DatabaseScheduleRunPage,
17
+ type DatabaseWebhookCreated,
18
+ type DatabaseWebhookDeleted,
19
+ type DatabaseWebhookDeliveryPage,
20
+ type DatabaseWebhookEventType,
21
+ type DatabaseWebhookPage,
22
+ type DatabaseWebhookReplayAccepted,
23
+ type DatabaseWebhookTestResult,
24
+ } from "@ohmyhost/contracts/database-events";
25
+ import type { DatabaseEnvironment } from "@ohmyhost/contracts/database-access";
3
26
  import {
4
27
  normalizeMcpSourceChanges,
5
28
  formatMcpSourceFileResponse,
@@ -77,6 +100,26 @@ import type {
77
100
  ProjectDatabaseQueryResult,
78
101
  } from "@ohmyhost/sdk-ts";
79
102
 
103
+ interface DatabaseWebhookTarget {
104
+ readonly projectId: string;
105
+ readonly environment: DatabaseEnvironment;
106
+ readonly webhookId: string;
107
+ }
108
+
109
+ interface DatabaseScheduleTarget {
110
+ readonly projectId: string;
111
+ readonly environment: DatabaseEnvironment;
112
+ readonly name: string;
113
+ }
114
+
115
+ interface DatabaseScheduleSetInput extends DatabaseScheduleTarget {
116
+ readonly cron?: string;
117
+ readonly everySeconds?: number;
118
+ readonly statements: readonly string[];
119
+ readonly timeoutMs?: number;
120
+ readonly enabled?: boolean;
121
+ }
122
+
80
123
  export interface LocalMcpProductClient {
81
124
  readonly managedSource?: ManagedSourceApi;
82
125
  readonly productApiOrigin?: string;
@@ -189,11 +232,9 @@ export interface LocalMcpProductClient {
189
232
  idempotencyKey: string;
190
233
  }): Promise<import("@ohmyhost/sdk-ts").BillingRecharge>;
191
234
  createBillingPortal(organizationId: string): Promise<import("@ohmyhost/sdk-ts").BillingPortal>;
192
- getOrganizationCreditUsage(input: {
193
- organizationId: string;
194
- month: string;
195
- cursor?: string;
196
- }): Promise<import("@ohmyhost/sdk-ts").OrganizationCreditUsage>;
235
+ getOrganizationCreditUsage(
236
+ organizationId: string,
237
+ ): Promise<import("@ohmyhost/sdk-ts").OrganizationCreditUsage>;
197
238
  getOrganizationAccount(
198
239
  organizationId: string,
199
240
  ): Promise<import("@ohmyhost/sdk-ts").OrganizationAccount>;
@@ -278,6 +319,54 @@ export interface LocalMcpProductClient {
278
319
  readonly projectId: string;
279
320
  readonly accessId: string;
280
321
  }): Promise<import("@ohmyhost/contracts/database-access").ProjectDatabaseAccess>;
322
+ listDatabaseWebhooks(input: {
323
+ readonly projectId: string;
324
+ readonly environment?: DatabaseEnvironment;
325
+ }): Promise<DatabaseWebhookPage>;
326
+ createDatabaseWebhook(input: {
327
+ readonly projectId: string;
328
+ readonly environment: DatabaseEnvironment;
329
+ readonly url: string;
330
+ readonly eventTypes: readonly DatabaseWebhookEventType[];
331
+ readonly headers: Readonly<Record<string, string>>;
332
+ readonly idempotencyKey: string;
333
+ }): Promise<DatabaseWebhookCreated>;
334
+ testDatabaseWebhook(input: DatabaseWebhookTarget): Promise<DatabaseWebhookTestResult>;
335
+ deleteDatabaseWebhook(input: DatabaseWebhookTarget): Promise<DatabaseWebhookDeleted>;
336
+ listDatabaseWebhookDeliveries(input: {
337
+ readonly projectId: string;
338
+ readonly environment?: DatabaseEnvironment;
339
+ readonly webhookId?: string;
340
+ readonly limit?: number;
341
+ readonly cursor?: string;
342
+ }): Promise<DatabaseWebhookDeliveryPage>;
343
+ replayDatabaseWebhookDelivery(input: {
344
+ readonly projectId: string;
345
+ readonly environment: DatabaseEnvironment;
346
+ readonly deliveryId: string;
347
+ }): Promise<DatabaseWebhookReplayAccepted>;
348
+ listDatabaseSchedules(input: {
349
+ readonly projectId: string;
350
+ readonly environment?: DatabaseEnvironment;
351
+ }): Promise<DatabaseSchedulePage>;
352
+ setDatabaseSchedule(input: DatabaseScheduleSetInput): Promise<DatabaseSchedule>;
353
+ deleteDatabaseSchedule(input: DatabaseScheduleTarget): Promise<DatabaseScheduleDeleted>;
354
+ listDatabaseScheduleRuns(input: {
355
+ readonly projectId: string;
356
+ readonly environment?: DatabaseEnvironment;
357
+ readonly name: string;
358
+ readonly limit?: number;
359
+ }): Promise<DatabaseScheduleRunPage>;
360
+ runDatabaseSchedule(input: DatabaseScheduleTarget): Promise<DatabaseScheduleRunAccepted>;
361
+ getDatabaseExecutionUsage(input: {
362
+ readonly projectId: string;
363
+ readonly environment?: DatabaseEnvironment;
364
+ readonly from: string;
365
+ readonly to: string;
366
+ readonly granularity: "hour" | "day";
367
+ readonly limit?: number;
368
+ readonly cursor?: string;
369
+ }): Promise<DatabaseExecutionUsagePage>;
281
370
  connectGithub(input: {
282
371
  organizationId: string;
283
372
  idempotencyKey: string;
@@ -448,6 +537,10 @@ interface ProjectDeleteInput {
448
537
  }
449
538
 
450
539
  const identifier = z.string().min(1).max(256);
540
+ const databaseEnvironment = z.enum(["dev", "prod"]);
541
+ const databaseWebhookId = z.string().regex(/^whk_[a-z0-9]{20}$/u);
542
+ const databaseDeliveryId = z.string().regex(/^dlv_[0-9a-f]{20}$/u);
543
+ const databaseScheduleName = z.string().regex(/^[a-z][a-z0-9_-]{0,62}$/u);
451
544
  /** Every product tool accepts the saved login to act as; it is an alias, never a credential. */
452
545
  const profileName = z
453
546
  .string()
@@ -702,7 +795,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
702
795
  server,
703
796
  dependencies,
704
797
  "billing_checkout_create",
705
- "Owner-only: create or resume a hosted Checkout. Paid is USD 10/month; each top-up pack is USD 10 before tax; a purchase grants 100 credits per dollar up to USD 100 and 125 credits per dollar for the part above, so 10 packs grant 10000 and 20 packs grant 22500 top-up credits. Monthly Paid credits expire at billing-period end; top-ups carry over while Paid and expire on downgrade to Free. Return the URL to the human to review/pay; never auto-pay or treat browser return as confirmation. Preserve the same offer/packs/idempotency key after uncertainty. An existing active, overdue, unpaid or paused subscription must be managed through billing_portal_create, not replaced with another Paid purchase. Then read checkout status and organization credits. Works at zero credit. Top-ups and enabling auto-recharge require active Paid access.",
798
+ "Owner-only: create or resume a hosted Checkout. Paid is USD 10/month; each top-up pack is USD 10 before tax; a purchase grants 100 credits per dollar up to USD 100 and 125 credits per dollar for the part above, so 10 packs grant 10000 and 20 packs grant 22500 top-up credits. Monthly Paid credits expire at billing-period end; top-ups never expire, also after a downgrade to Free. Return the URL to the human to review/pay; never auto-pay or treat browser return as confirmation. Preserve the same offer/packs/idempotency key after uncertainty. An existing active, overdue, unpaid or paused subscription must be managed through billing_portal_create, not replaced with another Paid purchase. Then read checkout status and organization credits. Works at zero credit. Top-ups and enabling auto-recharge require active Paid access.",
706
799
  z.object({
707
800
  organization_id: identifier,
708
801
  offer: z.enum(["topup", "paid"]),
@@ -722,7 +815,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
722
815
  server,
723
816
  dependencies,
724
817
  "billing_checkout_get",
725
- "Owner-only: observe the original checkout and reconcile confirmed credits/refunds, without another purchase. payment_confirmed is historical; read organization_credits_get for spendable funds and organization_account_get's effective plan for Paid coverage; paid_until remains the paid Stripe period end during renewal grace. Follow required_action: complete_checkout means give its URL to the human; open_billing_portal means call billing_portal_create and give that fresh URL to the human to resolve payment. contact_support means contact https://ohmyho.st/contact about the original invoice; none means no human payment step now, not proof of current Paid coverage. Never start a second subscription to repair a failed renewal. Follow billing_issue when tax needs attention: use billing_portal_create for open_billing_portal, or https://ohmyho.st/contact for contact_support; retain the original invoice and never disable tax. Retry the same checkout after an outage. state=expired ends this attempt; use a new key only for an explicitly requested new purchase, never automatically after an error or timeout.",
818
+ "Owner-only: observe the original checkout and reconcile confirmed credits/refunds, without another purchase. payment_confirmed is historical; read organization_credits_get for spendable funds and organization_account_get's effective plan for Paid coverage; paid_until is the end of paid coverage: the paid Stripe period end plus its 14-day renewal grace. Follow required_action: complete_checkout means give its URL to the human; open_billing_portal means call billing_portal_create and give that fresh URL to the human to resolve payment. contact_support means contact https://ohmyho.st/contact about the original invoice; none means no human payment step now, not proof of current Paid coverage. Never start a second subscription to repair a failed renewal. Follow billing_issue when tax needs attention: use billing_portal_create for open_billing_portal, or https://ohmyho.st/contact for contact_support; retain the original invoice and never disable tax. Retry the same checkout after an outage. state=expired ends this attempt; use a new key only for an explicitly requested new purchase, never automatically after an error or timeout.",
726
819
  z.object({ organization_id: identifier, checkout_id: identifier }),
727
820
  (client, input) =>
728
821
  client.getBillingCheckout({
@@ -742,7 +835,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
742
835
  server,
743
836
  dependencies,
744
837
  "billing_recharge_configure",
745
- "Owner-only: enable or disable automatic off-session payments. Enabling requires active Paid access. NEVER enable without explicit human approval for USD 9 plus tax per 1000 top-up credits, which carry over while Paid and expire on downgrade to Free, refill below 100 credits, and the chosen gross USD monthly limit. Set consent off_session_v1 only after that approval; null when disabling. Read current revision first. Preserve the original idempotency key and payload after uncertainty. A saved Stripe card may require setup_url. Disabling prevents new payment initiation; in-progress payments may finish. This does not create a subscription or Paid access.",
838
+ "Owner-only: enable or disable automatic off-session payments. Enabling requires active Paid access. NEVER enable without explicit human approval for USD 9 plus tax per 1000 top-up credits, which never expire, refill below 100 credits, and the chosen gross USD monthly limit. Set consent off_session_v1 only after that approval; null when disabling. Read current revision first. Preserve the original idempotency key and payload after uncertainty. A saved Stripe card may require setup_url. Disabling prevents new payment initiation; in-progress payments may finish. This does not create a subscription or Paid access.",
746
839
  z.object({
747
840
  organization_id: identifier,
748
841
  enabled: z.boolean(),
@@ -804,24 +897,15 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
804
897
  server,
805
898
  dependencies,
806
899
  "organization_usage_get",
807
- "Read posted UTC-month usage by project, environment and published meter/rate. One credit is 1000000 microcredits. Includes corrections; null environment means shared project cost. Current reservations are separate. Follow next_cursor with the same month (20 projects/page). Late or unmetered usage is not zero consumption. platform_overrun_micros is platform expense, not customer debt. Owner-only; works at zero credit without new charges.",
808
- z.object({
809
- organization_id: identifier,
810
- month: z.string().regex(/^20[0-9]{2}-(0[1-9]|1[0-2])$/u),
811
- cursor: identifier.optional(),
812
- }),
813
- (client, input) =>
814
- client.getOrganizationCreditUsage({
815
- organizationId: input.organization_id,
816
- month: input.month,
817
- ...(input.cursor ? { cursor: input.cursor } : {}),
818
- }),
900
+ "Read the billed usage of the last up to 14 UTC days (today included) grouped by day, project and meter, and what each project has used so far this UTC month. One credit is 1000000 microcredits. Null environment means shared project cost. Charges can arrive late and unmetered usage is not zero consumption. Owner-only; works at zero credit without new charges.",
901
+ z.object({ organization_id: identifier }),
902
+ (client, input) => client.getOrganizationCreditUsage(input.organization_id),
819
903
  );
820
904
  registerProductTool(
821
905
  server,
822
906
  dependencies,
823
907
  "organization_account_get",
824
- "Owner-only: read the effective Free/Paid plan, its Stripe or granted source, available monthly credits that expire at period end and top-up credits that carry over while Paid but expire on downgrade to Free, reservations and next expiry. Granted Paid features do not imply a Stripe subscription or extra monthly Paid allowance. paid_until is the paid Stripe period end or grant end; null on Free or for an unbounded grant. An unpaid renewal may retain Paid features for up to 14 days after that Stripe period end, without new monthly credits. This read grants no access or credits.",
908
+ "Owner-only: read the effective Free/Paid plan, its Stripe or granted source, the monthly credits that expire at next_expiry, the top-up and bonus credits that never expire, the reservations that hold part of them, and available_micros (monthly plus one-time minus reservations). Granted Paid features do not imply a Stripe subscription or extra monthly Paid allowance. paid_until is the end of the paid coverage, which includes the 14-day renewal grace of a Stripe subscription; null on Free. This read grants no access or credits.",
825
909
  z.object({ organization_id: identifier }),
826
910
  (client, input) => client.getOrganizationAccount(input.organization_id),
827
911
  );
@@ -837,7 +921,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
837
921
  server,
838
922
  dependencies,
839
923
  "organization_credits_get",
840
- "Read the owner's shared organization credit pool, seven-day grace_started_at/grace_expires_at and published rate_cards. Existing services/domains are not immediately shut down at zero credit; an explicit project stop budget is separate. One credit is 1000000 microcredits. Each rate states credit_micros for units_per_charge; publication alone does not enable billing. active_meters identifies billed sources; provider outages and platform overrun are not customer debt. Works at zero credit. Top-ups and enabling auto-recharge require active Paid access.",
924
+ "Read the owner's shared organization credit pool (available_micros is monthly plus one-time credit minus reservations), the seven-day grace_started_at/grace_expires_at that begins when the credit is used up, and published rate_cards. Existing services/domains are not immediately shut down at zero credit; an explicit project stop budget is separate. One credit is 1000000 microcredits. Each rate states credit_micros for units_per_charge; publication alone does not enable billing. active_meters identifies billed sources; provider outages are not customer debt. Works at zero credit. Top-ups and enabling auto-recharge require active Paid access.",
841
925
  z.object({ organization_id: identifier }),
842
926
  (client, input) => client.getOrganizationCredits(input.organization_id),
843
927
  );
@@ -845,7 +929,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
845
929
  server,
846
930
  dependencies,
847
931
  "project_budget_get",
848
- "Read the owner's project UTC-month budget, measured usage and open reservations. A budget never allocates a second wallet.",
932
+ "Read the owner's project UTC-month budget, this month's use and open reservations. A budget never allocates a second wallet.",
849
933
  z.object({ project_id: identifier }),
850
934
  (client, input) => client.getProjectCreditBudget(input.project_id),
851
935
  );
@@ -1038,6 +1122,263 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1038
1122
  client.revokeDatabaseAccess({ projectId: input.project_id, accessId: input.access_id }),
1039
1123
  destructiveAnnotations(),
1040
1124
  );
1125
+ registerProductTool(
1126
+ server,
1127
+ dependencies,
1128
+ "database_webhook_list",
1129
+ "List this project's database webhooks of one Dev or Prod environment (dev when omitted), without signing secrets. Each webhook lists its event types and header names only.",
1130
+ z.object({ project_id: identifier, environment: databaseEnvironment.optional() }),
1131
+ (client, input) =>
1132
+ client.listDatabaseWebhooks({
1133
+ projectId: input.project_id,
1134
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
1135
+ }),
1136
+ );
1137
+ registerProductTool(
1138
+ server,
1139
+ dependencies,
1140
+ "database_webhook_create",
1141
+ "Subscribe one HTTPS receiver to database lifecycle events (database.created, ready, woke, slept, suspended, resized, deleted) of one Dev or Prod database. The signing secret is RETURNED EXACTLY ONCE: store it in a secret manager and never write it into files, notes, source, commit messages or later messages. Deliveries are signed, retried and dead-lettered when retries stop. At most five webhooks exist per project environment. Requires confirmed: true and an idempotency key; the same key with the same request returns the stored answer.",
1142
+ z.object({
1143
+ project_id: identifier,
1144
+ environment: databaseEnvironment,
1145
+ url: z
1146
+ .string()
1147
+ .max(2048)
1148
+ .regex(/^https:\/\//u),
1149
+ event_types: z.array(z.enum(DATABASE_WEBHOOK_EVENT_TYPES)).min(1).max(16),
1150
+ headers: z.record(z.string(), z.string()).optional(),
1151
+ idempotency_key: idempotencyKey,
1152
+ confirmed: z.literal(true),
1153
+ }),
1154
+ (client, input) =>
1155
+ client.createDatabaseWebhook({
1156
+ projectId: input.project_id,
1157
+ environment: input.environment,
1158
+ url: input.url,
1159
+ eventTypes: input.event_types,
1160
+ headers: input.headers ?? {},
1161
+ idempotencyKey: input.idempotency_key,
1162
+ }),
1163
+ mutationAnnotations(),
1164
+ );
1165
+ registerProductTool(
1166
+ server,
1167
+ dependencies,
1168
+ "database_webhook_test",
1169
+ "Send one signed test request to a database webhook and report the outcome synchronously. Each call sends one request to the receiver; the platform allows one test per webhook every five seconds. If the answer is lost, read database_webhook_delivery_list before calling again; a second call sends another request.",
1170
+ z.object({
1171
+ project_id: identifier,
1172
+ environment: databaseEnvironment,
1173
+ webhook_id: databaseWebhookId,
1174
+ }),
1175
+ (client, input) =>
1176
+ client.testDatabaseWebhook({
1177
+ projectId: input.project_id,
1178
+ environment: input.environment,
1179
+ webhookId: input.webhook_id,
1180
+ }),
1181
+ { ...mutationAnnotations(), idempotentHint: false },
1182
+ );
1183
+ registerProductTool(
1184
+ server,
1185
+ dependencies,
1186
+ "database_webhook_delete",
1187
+ "Stop all deliveries to one database webhook. Destructive: requires confirmed: true. Deleting an already deleted webhook answers not found.",
1188
+ z.object({
1189
+ project_id: identifier,
1190
+ environment: databaseEnvironment,
1191
+ webhook_id: databaseWebhookId,
1192
+ confirmed: z.literal(true),
1193
+ }),
1194
+ (client, input) =>
1195
+ client.deleteDatabaseWebhook({
1196
+ projectId: input.project_id,
1197
+ environment: input.environment,
1198
+ webhookId: input.webhook_id,
1199
+ }),
1200
+ destructiveAnnotations(),
1201
+ );
1202
+ registerProductTool(
1203
+ server,
1204
+ dependencies,
1205
+ "database_webhook_delivery_list",
1206
+ "List recent database webhook deliveries, newest first, optionally for one webhook. Each delivery shows its state (pending, delivered or dead_lettered), attempts and last error.",
1207
+ z.object({
1208
+ project_id: identifier,
1209
+ environment: databaseEnvironment.optional(),
1210
+ webhook_id: databaseWebhookId.optional(),
1211
+ limit: z.number().int().min(1).max(100).optional(),
1212
+ cursor: z.string().min(1).max(200).optional(),
1213
+ }),
1214
+ (client, input) =>
1215
+ client.listDatabaseWebhookDeliveries({
1216
+ projectId: input.project_id,
1217
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
1218
+ ...(input.webhook_id === undefined ? {} : { webhookId: input.webhook_id }),
1219
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1220
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1221
+ }),
1222
+ );
1223
+ registerProductTool(
1224
+ server,
1225
+ dependencies,
1226
+ "database_webhook_delivery_replay",
1227
+ "Queue one more attempt for a database webhook delivery that is no longer being retried. Each call queues one attempt; the delivery returns to pending until it is delivered or dead-lettered again. If the answer is lost, read database_webhook_delivery_list before calling again; a second call queues another attempt.",
1228
+ z.object({
1229
+ project_id: identifier,
1230
+ environment: databaseEnvironment,
1231
+ delivery_id: databaseDeliveryId,
1232
+ }),
1233
+ (client, input) =>
1234
+ client.replayDatabaseWebhookDelivery({
1235
+ projectId: input.project_id,
1236
+ environment: input.environment,
1237
+ deliveryId: input.delivery_id,
1238
+ }),
1239
+ { ...mutationAnnotations(), idempotentHint: false },
1240
+ );
1241
+ registerProductTool(
1242
+ server,
1243
+ dependencies,
1244
+ "database_schedule_list",
1245
+ "List this project's database schedules of one Dev or Prod environment (dev when omitted), with their cadence, statements, timeout and next run.",
1246
+ z.object({ project_id: identifier, environment: databaseEnvironment.optional() }),
1247
+ (client, input) =>
1248
+ client.listDatabaseSchedules({
1249
+ projectId: input.project_id,
1250
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
1251
+ }),
1252
+ );
1253
+ registerProductTool(
1254
+ server,
1255
+ dependencies,
1256
+ "database_schedule_set",
1257
+ "Create or replace a named database schedule that runs SQL statements repeatedly on a timer, as the application's own runtime role, so row-level security applies. Send exactly one of cron (five-field UTC expression) or every_seconds (60 to 2678400). Each run is cancelled after timeout_ms (1000 to 30000, default 10000). Statements run in order and must be safe to repeat: write idempotent statements. Destructive: requires confirmed: true.",
1258
+ z
1259
+ .object({
1260
+ project_id: identifier,
1261
+ environment: databaseEnvironment,
1262
+ name: databaseScheduleName,
1263
+ cron: z.string().min(9).max(120).optional(),
1264
+ every_seconds: z
1265
+ .number()
1266
+ .int()
1267
+ .min(DATABASE_SCHEDULE_MIN_EVERY_SECONDS)
1268
+ .max(DATABASE_SCHEDULE_MAX_EVERY_SECONDS)
1269
+ .optional(),
1270
+ statements: z
1271
+ .array(z.string().min(1).max(DATABASE_SCHEDULE_STATEMENT_MAX_BYTES))
1272
+ .min(1)
1273
+ .max(DATABASE_SCHEDULE_MAX_STATEMENTS),
1274
+ timeout_ms: z
1275
+ .number()
1276
+ .int()
1277
+ .min(DATABASE_SCHEDULE_MIN_TIMEOUT_MS)
1278
+ .max(DATABASE_SCHEDULE_MAX_TIMEOUT_MS)
1279
+ .optional(),
1280
+ enabled: z.boolean().optional(),
1281
+ confirmed: z.literal(true),
1282
+ })
1283
+ .refine((value) => (value.cron === undefined) !== (value.every_seconds === undefined), {
1284
+ message: "Send exactly one of cron or every_seconds",
1285
+ path: ["every_seconds"],
1286
+ }),
1287
+ (client, input) =>
1288
+ client.setDatabaseSchedule({
1289
+ projectId: input.project_id,
1290
+ environment: input.environment,
1291
+ name: input.name,
1292
+ cron: input.cron,
1293
+ everySeconds: input.every_seconds,
1294
+ statements: input.statements,
1295
+ timeoutMs: input.timeout_ms,
1296
+ enabled: input.enabled,
1297
+ }),
1298
+ destructiveAnnotations(),
1299
+ );
1300
+ registerProductTool(
1301
+ server,
1302
+ dependencies,
1303
+ "database_schedule_delete",
1304
+ "Remove one database schedule so it never runs again. Destructive: requires confirmed: true.",
1305
+ z.object({
1306
+ project_id: identifier,
1307
+ environment: databaseEnvironment,
1308
+ name: databaseScheduleName,
1309
+ confirmed: z.literal(true),
1310
+ }),
1311
+ (client, input) =>
1312
+ client.deleteDatabaseSchedule({
1313
+ projectId: input.project_id,
1314
+ environment: input.environment,
1315
+ name: input.name,
1316
+ }),
1317
+ destructiveAnnotations(),
1318
+ );
1319
+ registerProductTool(
1320
+ server,
1321
+ dependencies,
1322
+ "database_schedule_run_list",
1323
+ "List the newest runs of one database schedule, newest first, with each run's outcome (succeeded, failed, timed_out, outcome_unknown, rejected or a skipped reason) and SQLSTATE when one applies.",
1324
+ z.object({
1325
+ project_id: identifier,
1326
+ environment: databaseEnvironment.optional(),
1327
+ name: databaseScheduleName,
1328
+ limit: z.number().int().min(1).max(50).optional(),
1329
+ }),
1330
+ (client, input) =>
1331
+ client.listDatabaseScheduleRuns({
1332
+ projectId: input.project_id,
1333
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
1334
+ name: input.name,
1335
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1336
+ }),
1337
+ );
1338
+ registerProductTool(
1339
+ server,
1340
+ dependencies,
1341
+ "database_schedule_run_now",
1342
+ "Queue one immediate run of a database schedule. Refused while the schedule is disabled or a run is already queued. Destructive: the statements execute against the database; requires confirmed: true. If the answer is lost, read database_schedule_run_list before calling again; a second call queues another run.",
1343
+ z.object({
1344
+ project_id: identifier,
1345
+ environment: databaseEnvironment,
1346
+ name: databaseScheduleName,
1347
+ confirmed: z.literal(true),
1348
+ }),
1349
+ (client, input) =>
1350
+ client.runDatabaseSchedule({
1351
+ projectId: input.project_id,
1352
+ environment: input.environment,
1353
+ name: input.name,
1354
+ }),
1355
+ { ...destructiveAnnotations(), idempotentHint: false },
1356
+ );
1357
+ registerProductTool(
1358
+ server,
1359
+ dependencies,
1360
+ "database_execution_usage_get",
1361
+ "Read metered database execution usage: schedule runs and webhook attempts per UTC hour or day window. from and to are UTC timestamps with milliseconds (Z), aligned to the granularity, at most 31 days apart. Only completed windows are final. Rejected, skipped, event and delivery counts are reported but not billed.",
1362
+ z.object({
1363
+ project_id: identifier,
1364
+ environment: databaseEnvironment.optional(),
1365
+ from: z.string().regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u),
1366
+ to: z.string().regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/u),
1367
+ granularity: z.enum(["hour", "day"]),
1368
+ limit: z.number().int().min(1).max(100).optional(),
1369
+ cursor: z.string().min(1).max(200).optional(),
1370
+ }),
1371
+ (client, input) =>
1372
+ client.getDatabaseExecutionUsage({
1373
+ projectId: input.project_id,
1374
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
1375
+ from: input.from,
1376
+ to: input.to,
1377
+ granularity: input.granularity,
1378
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1379
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1380
+ }),
1381
+ );
1041
1382
  registerProductTool(
1042
1383
  server,
1043
1384
  dependencies,
@@ -2730,7 +3071,7 @@ function safeFailure(error: unknown, commandPrefix: string): SafeFailure {
2730
3071
  code: "request_timeout",
2731
3072
  retryable: true,
2732
3073
  suggestedAction:
2733
- "The ohmyho.st API did not answer within 30 seconds. Call this tool again; for a change use the same idempotency_key, because it may already be under way.",
3074
+ "The ohmyho.st API did not answer within 30 seconds. Call this tool again; for a change that takes an idempotency_key use the same key, because it may already be under way. A change without a key (webhook delete, test or replay, schedule set, delete or run) is repeated only after a read tool shows the current state.",
2734
3075
  };
2735
3076
  if (error instanceof Error) {
2736
3077
  const local = localFailure(error, commandPrefix);
@@ -2812,7 +3153,7 @@ function localFailure(error: Error, commandPrefix: string): SafeFailure | undefi
2812
3153
  code: "network_error",
2813
3154
  retryable: true,
2814
3155
  suggestedAction:
2815
- "The request did not reach the ohmyho.st API, or its answer was cut off. Call this tool again; for a change use the same idempotency_key, because it may already have been applied.",
3156
+ "The request did not reach the ohmyho.st API, or its answer was cut off. Call this tool again; for a change that takes an idempotency_key use the same key, because it may already have been applied. A change without a key (webhook delete, test or replay, schedule set, delete or run) is repeated only after a read tool shows the current state.",
2816
3157
  };
2817
3158
  case "ResponseContractError":
2818
3159
  return {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/product-cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",