@amerged/ohmyhost-mcp 0.1.41 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-mcp",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/mcp",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",
@@ -12,6 +12,20 @@ import {
12
12
  } from "@ohmyhost/contracts/database-access";
13
13
  import { assertOrganizationAccount } from "@ohmyhost/contracts/credit-pricing";
14
14
  import { assertDatabaseCompute } from "@ohmyhost/contracts/database-compute";
15
+ import {
16
+ assertDatabaseExecutionUsagePage,
17
+ assertDatabaseSchedule,
18
+ assertDatabaseSchedulePage,
19
+ assertDatabaseScheduleDeleted,
20
+ assertDatabaseScheduleRunAccepted,
21
+ assertDatabaseScheduleRunPage,
22
+ assertDatabaseWebhookCreated,
23
+ assertDatabaseWebhookDeleted,
24
+ assertDatabaseWebhookDeliveryPage,
25
+ assertDatabaseWebhookPage,
26
+ assertDatabaseWebhookReplayAccepted,
27
+ assertDatabaseWebhookTest,
28
+ } from "@ohmyhost/contracts/database-events";
15
29
  import type { DatabaseComputeSelection } from "@ohmyhost/contracts/database-sizes";
16
30
  import {
17
31
  assertProjectContext,
@@ -98,6 +112,18 @@ import {
98
112
  createProjectDatabaseAccess,
99
113
  listProjectDatabaseAccess,
100
114
  revokeProjectDatabaseAccess,
115
+ createProjectDatabaseWebhook,
116
+ listProjectDatabaseWebhooks,
117
+ deleteProjectDatabaseWebhook,
118
+ testProjectDatabaseWebhook,
119
+ listProjectDatabaseWebhookDeliveries,
120
+ replayProjectDatabaseWebhookDelivery,
121
+ listProjectDatabaseSchedules,
122
+ setProjectDatabaseSchedule,
123
+ deleteProjectDatabaseSchedule,
124
+ listProjectDatabaseScheduleRuns,
125
+ runProjectDatabaseSchedule,
126
+ getProjectDatabaseExecutionUsage,
101
127
  planProjectDeletion,
102
128
  rollbackDeployment,
103
129
  reconcileOperation,
@@ -214,6 +240,11 @@ export async function createAuthenticatedLocalMcpClient(
214
240
  }
215
241
  }
216
242
 
243
+ /** A database event answer must belong to the environment and name that were asked for. */
244
+ function requireScope(matches: boolean): void {
245
+ if (!matches) throw new TypeError("Database event scope mismatch");
246
+ }
247
+
217
248
  export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
218
249
  public readonly managedSource: ManagedSourceApi;
219
250
  public readonly productApiOrigin: string;
@@ -806,6 +837,194 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
806
837
  assertProjectDatabaseAccess(value);
807
838
  return value;
808
839
  }
840
+ public async listDatabaseWebhooks(
841
+ input: Parameters<LocalMcpProductClient["listDatabaseWebhooks"]>[0],
842
+ ) {
843
+ const environment = input.environment ?? "dev";
844
+ const value = await listProjectDatabaseWebhooks(
845
+ { project_id: input.projectId, environment },
846
+ { client: this.#client },
847
+ );
848
+ assertDatabaseWebhookPage(value);
849
+ requireScope(value.items.every((webhook) => webhook.environment === environment));
850
+ return value;
851
+ }
852
+ /** The signing secret exists only in this response and is never stored by the client. */
853
+ public async createDatabaseWebhook(
854
+ input: Parameters<LocalMcpProductClient["createDatabaseWebhook"]>[0],
855
+ ) {
856
+ const value = await createProjectDatabaseWebhook(
857
+ {
858
+ project_id: input.projectId,
859
+ environment: input.environment,
860
+ "Idempotency-Key": input.idempotencyKey,
861
+ databaseWebhookCreateRequest: {
862
+ url: input.url,
863
+ event_types: [...input.eventTypes],
864
+ headers: { ...input.headers },
865
+ },
866
+ },
867
+ { client: this.#client },
868
+ );
869
+ assertDatabaseWebhookCreated(value);
870
+ requireScope(value.environment === input.environment);
871
+ return value;
872
+ }
873
+ public async testDatabaseWebhook(
874
+ input: Parameters<LocalMcpProductClient["testDatabaseWebhook"]>[0],
875
+ ) {
876
+ const value = await testProjectDatabaseWebhook(
877
+ {
878
+ project_id: input.projectId,
879
+ environment: input.environment,
880
+ webhook_id: input.webhookId,
881
+ },
882
+ { client: this.#client },
883
+ );
884
+ assertDatabaseWebhookTest(value);
885
+ return value;
886
+ }
887
+ public async deleteDatabaseWebhook(
888
+ input: Parameters<LocalMcpProductClient["deleteDatabaseWebhook"]>[0],
889
+ ) {
890
+ const value = await deleteProjectDatabaseWebhook(
891
+ {
892
+ project_id: input.projectId,
893
+ environment: input.environment,
894
+ webhook_id: input.webhookId,
895
+ },
896
+ { client: this.#client },
897
+ );
898
+ assertDatabaseWebhookDeleted(value);
899
+ requireScope(value.id === input.webhookId);
900
+ return value;
901
+ }
902
+ public async listDatabaseWebhookDeliveries(
903
+ input: Parameters<LocalMcpProductClient["listDatabaseWebhookDeliveries"]>[0],
904
+ ) {
905
+ const value = await listProjectDatabaseWebhookDeliveries(
906
+ {
907
+ project_id: input.projectId,
908
+ environment: input.environment ?? "dev",
909
+ ...(input.webhookId === undefined ? {} : { webhook_id: input.webhookId }),
910
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
911
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
912
+ },
913
+ { client: this.#client },
914
+ );
915
+ assertDatabaseWebhookDeliveryPage(value);
916
+ requireScope(
917
+ input.webhookId === undefined ||
918
+ value.items.every((delivery) => delivery.webhook_id === input.webhookId),
919
+ );
920
+ return value;
921
+ }
922
+ public async replayDatabaseWebhookDelivery(
923
+ input: Parameters<LocalMcpProductClient["replayDatabaseWebhookDelivery"]>[0],
924
+ ) {
925
+ const value = await replayProjectDatabaseWebhookDelivery(
926
+ {
927
+ project_id: input.projectId,
928
+ environment: input.environment,
929
+ delivery_id: input.deliveryId,
930
+ },
931
+ { client: this.#client },
932
+ );
933
+ assertDatabaseWebhookReplayAccepted(value);
934
+ requireScope(value.delivery_id === input.deliveryId);
935
+ return value;
936
+ }
937
+ public async listDatabaseSchedules(
938
+ input: Parameters<LocalMcpProductClient["listDatabaseSchedules"]>[0],
939
+ ) {
940
+ const environment = input.environment ?? "dev";
941
+ const value = await listProjectDatabaseSchedules(
942
+ { project_id: input.projectId, environment },
943
+ { client: this.#client },
944
+ );
945
+ assertDatabaseSchedulePage(value);
946
+ requireScope(value.items.every((schedule) => schedule.environment === environment));
947
+ return value;
948
+ }
949
+ public async setDatabaseSchedule(
950
+ input: Parameters<LocalMcpProductClient["setDatabaseSchedule"]>[0],
951
+ ) {
952
+ const value = await setProjectDatabaseSchedule(
953
+ {
954
+ project_id: input.projectId,
955
+ name: input.name,
956
+ environment: input.environment,
957
+ databaseScheduleSetRequest: {
958
+ ...(input.cron === undefined ? {} : { cron: input.cron }),
959
+ ...(input.everySeconds === undefined ? {} : { every_seconds: input.everySeconds }),
960
+ statements: [...input.statements],
961
+ ...(input.timeoutMs === undefined ? {} : { timeout_ms: input.timeoutMs }),
962
+ ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
963
+ confirm: true,
964
+ },
965
+ },
966
+ { client: this.#client },
967
+ );
968
+ assertDatabaseSchedule(value);
969
+ requireScope(value.name === input.name && value.environment === input.environment);
970
+ return value;
971
+ }
972
+ public async deleteDatabaseSchedule(
973
+ input: Parameters<LocalMcpProductClient["deleteDatabaseSchedule"]>[0],
974
+ ) {
975
+ const value = await deleteProjectDatabaseSchedule(
976
+ { project_id: input.projectId, name: input.name, environment: input.environment },
977
+ { client: this.#client },
978
+ );
979
+ assertDatabaseScheduleDeleted(value);
980
+ requireScope(value.name === input.name);
981
+ return value;
982
+ }
983
+ public async listDatabaseScheduleRuns(
984
+ input: Parameters<LocalMcpProductClient["listDatabaseScheduleRuns"]>[0],
985
+ ) {
986
+ const value = await listProjectDatabaseScheduleRuns(
987
+ {
988
+ project_id: input.projectId,
989
+ name: input.name,
990
+ environment: input.environment ?? "dev",
991
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
992
+ },
993
+ { client: this.#client },
994
+ );
995
+ assertDatabaseScheduleRunPage(value);
996
+ requireScope(value.items.every((run) => run.schedule === input.name));
997
+ return value;
998
+ }
999
+ public async runDatabaseSchedule(
1000
+ input: Parameters<LocalMcpProductClient["runDatabaseSchedule"]>[0],
1001
+ ) {
1002
+ const value = await runProjectDatabaseSchedule(
1003
+ { project_id: input.projectId, name: input.name, environment: input.environment },
1004
+ { client: this.#client },
1005
+ );
1006
+ assertDatabaseScheduleRunAccepted(value);
1007
+ requireScope(value.schedule === input.name);
1008
+ return value;
1009
+ }
1010
+ public async getDatabaseExecutionUsage(
1011
+ input: Parameters<LocalMcpProductClient["getDatabaseExecutionUsage"]>[0],
1012
+ ) {
1013
+ const value = await getProjectDatabaseExecutionUsage(
1014
+ {
1015
+ project_id: input.projectId,
1016
+ environment: input.environment ?? "dev",
1017
+ from: input.from,
1018
+ to: input.to,
1019
+ granularity: input.granularity,
1020
+ ...(input.limit === undefined ? {} : { limit: input.limit }),
1021
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
1022
+ },
1023
+ { client: this.#client },
1024
+ );
1025
+ assertDatabaseExecutionUsagePage(value);
1026
+ return value;
1027
+ }
809
1028
  public planPromotion(input: Parameters<LocalMcpProductClient["planPromotion"]>[0]) {
810
1029
  return planDeploymentPromotion(
811
1030
  { project_id: input.projectId, source_deployment_id: input.sourceDeploymentId },
@@ -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;
@@ -276,6 +319,54 @@ export interface LocalMcpProductClient {
276
319
  readonly projectId: string;
277
320
  readonly accessId: string;
278
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>;
279
370
  connectGithub(input: {
280
371
  organizationId: string;
281
372
  idempotencyKey: string;
@@ -446,6 +537,10 @@ interface ProjectDeleteInput {
446
537
  }
447
538
 
448
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);
449
544
  /** Every product tool accepts the saved login to act as; it is an alias, never a credential. */
450
545
  const profileName = z
451
546
  .string()
@@ -1027,6 +1122,263 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1027
1122
  client.revokeDatabaseAccess({ projectId: input.project_id, accessId: input.access_id }),
1028
1123
  destructiveAnnotations(),
1029
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
+ );
1030
1382
  registerProductTool(
1031
1383
  server,
1032
1384
  dependencies,
@@ -2719,7 +3071,7 @@ function safeFailure(error: unknown, commandPrefix: string): SafeFailure {
2719
3071
  code: "request_timeout",
2720
3072
  retryable: true,
2721
3073
  suggestedAction:
2722
- "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.",
2723
3075
  };
2724
3076
  if (error instanceof Error) {
2725
3077
  const local = localFailure(error, commandPrefix);
@@ -2801,7 +3153,7 @@ function localFailure(error: Error, commandPrefix: string): SafeFailure | undefi
2801
3153
  code: "network_error",
2802
3154
  retryable: true,
2803
3155
  suggestedAction:
2804
- "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.",
2805
3157
  };
2806
3158
  case "ResponseContractError":
2807
3159
  return {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/product-cli",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",