drivethrough-cli 0.1.29 → 0.1.31

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 (2) hide show
  1. package/dist/main.mjs +174 -91
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -23218,8 +23218,8 @@ var init_types = __esm({
23218
23218
  }
23219
23219
  } else if (check2.kind === "regex") {
23220
23220
  check2.regex.lastIndex = 0;
23221
- const testResult = check2.regex.test(input2.data);
23222
- if (!testResult) {
23221
+ const testResult2 = check2.regex.test(input2.data);
23222
+ if (!testResult2) {
23223
23223
  ctx = this._getOrReturnCtx(input2, ctx);
23224
23224
  addIssueToContext(ctx, {
23225
23225
  validation: "regex",
@@ -40013,7 +40013,7 @@ var init_remote_assets = __esm({
40013
40013
  });
40014
40014
 
40015
40015
  // ../mcp-tools/src/output-schemas.ts
40016
- var ok, site, deployResult, dataDoc, dnsRecord, fileEntry, OUTPUT_SCHEMAS;
40016
+ var ok, site, deployResult, dataDoc, dnsRecord, fileEntry, testResult, OUTPUT_SCHEMAS;
40017
40017
  var init_output_schemas = __esm({
40018
40018
  "../mcp-tools/src/output-schemas.ts"() {
40019
40019
  "use strict";
@@ -40077,6 +40077,17 @@ var init_output_schemas = __esm({
40077
40077
  diff: external_exports.string().optional().describe("compare only: unified line diff of a changed text file"),
40078
40078
  diff_omitted: external_exports.string().optional()
40079
40079
  });
40080
+ testResult = external_exports.looseObject({
40081
+ ok,
40082
+ target: external_exports.string().optional(),
40083
+ kind: external_exports.string().optional().describe('"route", "action", "schedule" or "webhook"'),
40084
+ status: external_exports.string().optional().describe('"ok", "failed", "skipped", "plan" or "quota"'),
40085
+ http_status: external_exports.number().nullish().describe("The other service's HTTP status"),
40086
+ duration_ms: external_exports.number().optional(),
40087
+ body_preview: external_exports.string().nullish().describe("First 1 KB of the other service's response, secrets redacted"),
40088
+ error: external_exports.string().nullish(),
40089
+ next: external_exports.string().optional()
40090
+ });
40080
40091
  OUTPUT_SCHEMAS = {
40081
40092
  deploy_site: external_exports.looseObject({
40082
40093
  ok,
@@ -40237,33 +40248,33 @@ var init_output_schemas = __esm({
40237
40248
  domain_lost: external_exports.string().nullish().describe("A custom domain that could not be re-attached"),
40238
40249
  message: external_exports.string().optional()
40239
40250
  }),
40240
- manage_integrations: external_exports.looseObject({
40251
+ get_integrations: external_exports.looseObject({
40252
+ ok,
40253
+ secrets: external_exports.record(external_exports.string(), external_exports.string()).optional().describe('name \u2192 "set" | "unset" (values are never returned)'),
40254
+ routes: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Declared routes"),
40255
+ actions: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Declared actions, their fields and triggering types"),
40256
+ schedules: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Each schedule's enabled, next_at, last_run_at, last_status, failures, disabled_reason"),
40257
+ webhooks: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Each webhook's url (what the other service POSTs to), verify, secret_set, last_event_at, last_status and setup steps"),
40258
+ action_log: external_exports.array(external_exports.unknown()).optional().describe("The latest 20 outcomes, newest first"),
40259
+ next: external_exports.string().optional()
40260
+ }).describe("Everything declared plus recent outcomes"),
40261
+ request_secrets: external_exports.looseObject({
40241
40262
  ok,
40242
- // secrets
40243
- url: external_exports.string().optional().describe("secrets: the one-time link to relay to the user (they paste keys there, never in chat); test of a webhook: the URL the other service POSTs to"),
40244
- session: external_exports.string().optional().describe(`secrets: pass back with action "secrets" after the user says they're done`),
40263
+ url: external_exports.string().optional().describe("The one-time link to relay to the user (they paste keys there, never in chat)"),
40264
+ session: external_exports.string().optional().describe("Pass back after the user says they're done"),
40245
40265
  expires_at: external_exports.string().optional(),
40246
- names: external_exports.array(external_exports.string()).optional().describe("secrets: the names the link asks for"),
40247
- set: external_exports.record(external_exports.string(), external_exports.boolean()).optional().describe("secrets: every secret name \u2192 whether it has a value. Values are never returned."),
40248
- all_set: external_exports.boolean().optional().describe("secrets (with session): true once every declared secret is set"),
40249
- // status
40250
- secrets: external_exports.record(external_exports.string(), external_exports.string()).optional().describe('status: name \u2192 "set" | "unset"'),
40251
- routes: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("status: declared routes"),
40252
- actions: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("status: declared actions, their fields and triggering types"),
40253
- schedules: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("status: each schedule's enabled, next_at, last_run_at, last_status, failures, disabled_reason"),
40254
- webhooks: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("status: each webhook's url (what the other service POSTs to), verify, secret_set, last_event_at, last_status and setup steps"),
40255
- action_log: external_exports.array(external_exports.unknown()).optional().describe("status: the latest 20 outcomes, newest first"),
40256
- // test
40257
- target: external_exports.string().optional(),
40258
- kind: external_exports.string().optional().describe('test: "route", "action", "schedule" or "webhook"'),
40259
- status: external_exports.string().optional().describe('test: "ok", "failed", "skipped", "plan" or "quota"'),
40260
- http_status: external_exports.number().nullish().describe("test: the upstream's HTTP status"),
40261
- duration_ms: external_exports.number().optional(),
40262
- body_preview: external_exports.string().nullish().describe("test: first 1 KB of the upstream response, secrets redacted"),
40263
- error: external_exports.string().nullish(),
40264
- row: external_exports.record(external_exports.string(), external_exports.unknown()).nullish().describe("test, webhook targets: the row an event like the sample would store (nothing is written)"),
40266
+ names: external_exports.array(external_exports.string()).optional().describe("The names the link asks for"),
40267
+ set: external_exports.record(external_exports.string(), external_exports.boolean()).optional().describe("Every secret name \u2192 whether it has a value. Values are never returned."),
40268
+ all_set: external_exports.boolean().optional().describe("With session: true once every declared secret is set"),
40265
40269
  next: external_exports.string().optional()
40266
- }).describe('"secrets" \u2192 a link or the set/unset map; "status" \u2192 everything declared plus recent outcomes; "test" \u2192 one live run, or a webhook dry run')
40270
+ }).describe("A link, or (with session) the set/unset map"),
40271
+ test_route: testResult.describe("One GET through the route"),
40272
+ run_action: testResult.describe("One real run of the action"),
40273
+ run_schedule: testResult.describe("One real run of the schedule"),
40274
+ preview_webhook: testResult.extend({
40275
+ url: external_exports.string().optional().describe("The URL the other service POSTs to"),
40276
+ row: external_exports.record(external_exports.string(), external_exports.unknown()).nullish().describe("The row an event like the sample would store (nothing is written)")
40277
+ }).describe("A dry run: the row a delivery like the sample would store")
40267
40278
  };
40268
40279
  }
40269
40280
  });
@@ -41145,31 +41156,43 @@ function registerTools(server, ctx) {
41145
41156
  }
41146
41157
  }
41147
41158
  );
41159
+ const testTarget = (site2, kind, target, fields) => ctx.api.request("POST", `${sitePath(site2)}/integrations/test`, {
41160
+ body: { target, kind, ...fields ? { fields } : {} }
41161
+ });
41162
+ const fieldValues = external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]));
41148
41163
  server.registerTool(
41149
- "manage_integrations",
41164
+ "get_integrations",
41150
41165
  {
41151
- outputSchema: OUTPUT_SCHEMAS.manage_integrations,
41152
- title: "Manage integrations",
41153
- description: `Connect a site to another service's API \u2014 get its keys set, check what's running, and test it \u2014 with no server code (Pro plan). (1) THE SETUP NEEDS NO TOOL: declare it in drivethrough.json and deploy. "secrets" names the keys; "routes" proxies same-origin paths under /api/ext/<name> to an upstream, allowing only the listed methods and path prefixes and adding secret headers server-side (\${NAME}); "actions" POST a JSON body ({{field}} reads a row's field) when a type with "on_write" gets a row; "schedules" run every 1h/3h/6h/12h/1d/1w/1mo \u2014 "rotate" sends the next row of a type through an action, "pull" saves a route's JSON response as a new row ("map" uses $.dotted.paths; numeric strings become numbers); "webhooks" receive events another service POSTs to https://<site>/api/hook/<name> \u2014 "verify" is "stripe", "github", "standard" (Standard Webhooks: Resend, Clerk, Svix\u2026), "hmac_sha256" (with "header": the signature header's name) or, only for services that can't sign, "url_token"; "secret" names the declared secret; each verified event is mapped ("map", same $.paths) into a row of "into", which fires that type's on_write. Unverified, duplicate and replayed deliveries are never stored. No cron strings, no times of day, no timezones. Two complete examples \u2014 a read-only brokerage dashboard that also snapshots equity hourly for a chart: \`${INTEGRATION_EXAMPLE_A}\` \u2014 and a display that shows a quote when the owner clicks, and cycles through saved quotes every 3 hours: \`${INTEGRATION_EXAMPLE_B}\`. Every mistake is refused at deploy time with the corrected snippet \u2014 follow it. (2) SECRETS, TWO STEPS, like create_upload: action "secrets" returns a one-time link (10 minutes, no sign-in) \u2014 send it to the user as a clickable link and have them paste the keys there. NEVER ask for a key in the chat, and never put one in a file. When they say they're done, call again with the returned session to confirm every name is set. A deploy that needs an unset secret is refused with a fresh link already in the error \u2014 relay that one. Pass names to rotate a key that is already set. A webhook's secret comes FROM the other service: tell the user to add a webhook there pointing at its URL (status and the deploy error give it) and paste the signing secret it shows into the link page; a url_token secret is generated on the link page, which then shows the full URL to paste. (3) TEST BEFORE YOU SAY IT WORKS: action "test" with target = a route plus path ("/api/ext/alpaca/v2/account"), an action name (with fields: the row values it uses), or a schedule name runs it once, now, for real (a schedule test inserts its row or advances its rotation) and returns the upstream's status and a redacted preview. Read the preview \u2014 a 200 with an error body is still a failure. A webhook target ("/api/hook/payments", with fields: sample values) is a dry run that shows the row such an event would store; real delivery is proven by the other service's own "send test event" and then status. action "status" shows secrets set/unset, routes, schedules (next run, last result, failures), webhooks (URL, last event) and the latest outcomes. (4) WHAT FIRES AN ACTION: only a row written through the page-facing POST /api/data (a visitor's form, or the owner's own button on the page). write_db, imports and schedule pulls do NOT fire actions \u2014 to trigger one by hand, test it. Route access defaults to the owner only; set "access": "viewers" or "public" for pages others use. Schedules pause after 10 failures in a row (the owner gets one email); a deploy or a successful test re-enables them.`,
41154
- // Open-world: tests call a third-party API on the user's behalf.
41155
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
41166
+ outputSchema: OUTPUT_SCHEMAS.get_integrations,
41167
+ title: "Get integrations",
41168
+ description: `Show a site's connections to other services' APIs (Pro plan) and how they ran: which secrets are set (never their values), the declared routes and actions, each schedule's next run, last result and failures, each webhook's URL and last event, and the latest 20 outcomes. Read-only. THE CONFIGURATION FORMAT \u2014 integrations are declared in the site's drivethrough.json and deployed with deploy_site; once deployed, the platform runs them for the site, and the only operations you can perform on them are this tool, request_secrets, test_route, run_action, run_schedule and preview_webhook. A site can declare: "secrets" names the keys; "routes" proxies same-origin paths under /api/ext/<name> to an upstream, allowing only the listed methods and path prefixes and adding secret headers server-side (\${NAME}); "actions" POST a JSON body ({{field}} reads a row's field) when a type with "on_write" gets a row; "schedules" run every 1h/3h/6h/12h/1d/1w/1mo \u2014 "rotate" sends the next row of a type through an action, "pull" saves a route's JSON response as a new row ("map" uses $.dotted.paths; numeric strings become numbers); "webhooks" receive events another service POSTs to https://<site>/api/hook/<name> \u2014 "verify" is "stripe", "github", "standard" (Standard Webhooks: Resend, Clerk, Svix\u2026), "hmac_sha256" (with "header": the signature header's name) or, only for services that can't sign, "url_token"; "secret" names the declared secret; each verified event is mapped ("map", same $.paths) into a row of "into", which fires that type's on_write. Unverified, duplicate and replayed deliveries are never stored. No cron strings, no times of day, no timezones. Two complete examples \u2014 a read-only brokerage dashboard that also snapshots equity hourly for a chart: \`${INTEGRATION_EXAMPLE_A}\` \u2014 and a display that shows a quote when the owner clicks, and cycles through saved quotes every 3 hours: \`${INTEGRATION_EXAMPLE_B}\`. Every mistake is refused at deploy time with the corrected snippet \u2014 follow it. Keys are set with request_secrets; each piece is checked with test_route, run_action, run_schedule or preview_webhook. WHEN A DEPLOYED ACTION RUNS: when a visitor's form (or the owner's own button) on the page writes a row of a type with "on_write" \u2014 never from write_db, imports or schedule pulls. The only way for you to run one is run_action. Route access defaults to the owner only; set "access": "viewers" or "public" for pages others use. Schedules pause after 10 failures in a row (the owner gets one email); a deploy or a successful run_schedule re-enables them.`,
41169
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
41170
+ inputSchema: {
41171
+ site: external_exports.string().describe("Site name or id")
41172
+ }
41173
+ },
41174
+ async ({ site: site2 }) => {
41175
+ try {
41176
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/integrations`));
41177
+ } catch (error62) {
41178
+ return toolError(error62);
41179
+ }
41180
+ }
41181
+ );
41182
+ server.registerTool(
41183
+ "request_secrets",
41184
+ {
41185
+ outputSchema: OUTPUT_SCHEMAS.request_secrets,
41186
+ title: "Request secrets",
41187
+ description: `Get a one-time link where the user sets the API keys a site's integrations need (Pro plan) \u2014 two steps, like create_upload. Call it with the site: it returns a link (10 minutes, one use, no sign-in). Send it to the user as a clickable link and have them paste the keys there. NEVER ask for a key in the chat, and never put one in a file \u2014 this tool never receives or returns a key's value. When they say they're done, call again with the returned session: it reports which names are set. A deploy that needs an unset secret is refused with a fresh link already in the error \u2014 relay that one instead. Pass names to rotate a key that is already set. A webhook's secret comes FROM the other service: tell the user to add a webhook there pointing at its URL (get_integrations and the deploy error give it) and paste the signing secret it shows into the link page; a url_token secret is generated on the link page, which then shows the full URL to paste.`,
41188
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
41156
41189
  inputSchema: {
41157
41190
  site: external_exports.string().describe("Site name or id"),
41158
- action: external_exports.enum(["secrets", "status", "test"]).describe('"secrets": get a link for the user to set keys (or, with session, confirm they are set); "status": what is declared and how it ran; "test": run one route, action or schedule now, or dry-run a webhook'),
41159
- names: external_exports.array(external_exports.string()).optional().describe("secrets: which secret names the link asks for. Default: every declared secret that is still unset. Include a set one to replace (rotate) it."),
41160
- session: external_exports.string().optional().describe("secrets: the session a previous call returned \u2014 confirms what is set now instead of minting a new link"),
41161
- target: external_exports.string().optional().describe('test: a route key plus path ("/api/ext/weather/v1/forecast?city=Oslo"), an action name, a schedule name, or a webhook path ("/api/hook/payments")'),
41162
- fields: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).optional().describe("test, action and webhook targets only: for an action, the row values its {{field}} references use; for a webhook, sample values for the fields its map fills")
41191
+ names: external_exports.array(external_exports.string()).optional().describe("Which secret names the link asks for. Default: every declared secret that is still unset. Include a set one to replace (rotate) it."),
41192
+ session: external_exports.string().optional().describe("The session a previous call returned \u2014 confirms what is set now instead of creating a new link")
41163
41193
  }
41164
41194
  },
41165
- async ({
41166
- site: site2,
41167
- action,
41168
- names,
41169
- session,
41170
- target,
41171
- fields
41172
- }) => {
41195
+ async ({ site: site2, names, session }) => {
41173
41196
  try {
41174
41197
  const setMap = async () => {
41175
41198
  const listed = await ctx.api.request(
@@ -41178,50 +41201,117 @@ function registerTools(server, ctx) {
41178
41201
  );
41179
41202
  return listed.secrets;
41180
41203
  };
41181
- if (action === "secrets") {
41182
- if (session !== void 0) {
41183
- const secrets2 = await setMap();
41184
- const declared = secrets2.filter((s2) => s2.declared);
41185
- const unset = declared.filter((s2) => !s2.set).map((s2) => s2.name);
41186
- if (declared.length === 0) {
41187
- return ok2({
41188
- session,
41189
- set: Object.fromEntries(secrets2.map((s2) => [s2.name, s2.set])),
41190
- next: "This site has no deployed secrets list yet, so deploy again now \u2014 the deploy checks every secret it needs and answers with a fresh link if one is still missing."
41191
- });
41192
- }
41204
+ if (session !== void 0) {
41205
+ const secrets2 = await setMap();
41206
+ const declared = secrets2.filter((s2) => s2.declared);
41207
+ const unset = declared.filter((s2) => !s2.set).map((s2) => s2.name);
41208
+ if (declared.length === 0) {
41193
41209
  return ok2({
41194
41210
  session,
41195
41211
  set: Object.fromEntries(secrets2.map((s2) => [s2.name, s2.set])),
41196
- all_set: unset.length === 0,
41197
- next: unset.length ? `Still unset: ${unset.join(", ")}. Ask the user to finish on the page \u2014 or, if the link was used or expired, call this tool with action "secrets" again for a fresh one.` : "Every declared secret is set. Deploy (or redeploy) the site, then test each route, action, schedule and webhook."
41212
+ next: "This site has no deployed secrets list yet, so deploy again now \u2014 the deploy checks every secret it needs and answers with a fresh link if one is still missing."
41198
41213
  });
41199
41214
  }
41200
- const link = await ctx.api.request("POST", `${sitePath(site2)}/secrets/link`, {
41201
- body: names ? { names } : {}
41215
+ return ok2({
41216
+ session,
41217
+ set: Object.fromEntries(secrets2.map((s2) => [s2.name, s2.set])),
41218
+ all_set: unset.length === 0,
41219
+ next: unset.length ? `Still unset: ${unset.join(", ")}. Ask the user to finish on the page \u2014 or, if the link was used or expired, call request_secrets again without a session for a fresh one.` : "Every declared secret is set. Deploy (or redeploy) the site, then check each piece with test_route, run_action, run_schedule or preview_webhook."
41202
41220
  });
41203
- const secrets = await setMap();
41204
- const set2 = Object.fromEntries((link.names ?? []).map((name) => [name, false]));
41205
- for (const s2 of secrets) set2[s2.name] = s2.set;
41206
- return ok2({ ...link, set: set2 });
41207
- }
41208
- if (action === "status") {
41209
- return ok2(await ctx.api.request("GET", `${sitePath(site2)}/integrations`));
41210
41221
  }
41211
- if (!target) {
41212
- throw new ToolApiError(422, {
41213
- error: {
41214
- code: "target_required",
41215
- message: 'action "test" needs a target.',
41216
- next: 'Pass target: a route plus path ("/api/ext/name/path"), an action name, a schedule name, or a webhook path ("/api/hook/name"). action "status" lists them.'
41217
- }
41218
- });
41219
- }
41220
- return ok2(
41221
- await ctx.api.request("POST", `${sitePath(site2)}/integrations/test`, {
41222
- body: { target, ...fields ? { fields } : {} }
41223
- })
41224
- );
41222
+ const link = await ctx.api.request("POST", `${sitePath(site2)}/secrets/link`, {
41223
+ body: names ? { names } : {}
41224
+ });
41225
+ const secrets = await setMap();
41226
+ const set2 = Object.fromEntries((link.names ?? []).map((name) => [name, false]));
41227
+ for (const s2 of secrets) set2[s2.name] = s2.set;
41228
+ return ok2({ ...link, set: set2 });
41229
+ } catch (error62) {
41230
+ return toolError(error62);
41231
+ }
41232
+ }
41233
+ );
41234
+ server.registerTool(
41235
+ "test_route",
41236
+ {
41237
+ outputSchema: OUTPUT_SCHEMAS.test_route,
41238
+ title: "Test route",
41239
+ description: `Send one GET through a site's declared API route now, exactly as a page would (Pro plan), and return the other service's HTTP status and the first 1 KB of its response with every secret redacted. Only GET is ever sent, only to a path the route allows, and it counts as one call against the month's integration allowance. Read the preview before telling the user it works \u2014 a 200 with an error body is still a failure.`,
41240
+ // Not read-only: it reaches a third party (some GETs have effects there)
41241
+ // and spends one of the month's metered calls. Open-world for the same reason.
41242
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
41243
+ inputSchema: {
41244
+ site: external_exports.string().describe("Site name or id"),
41245
+ path: external_exports.string().describe('The route key plus a path it allows, with an optional query, e.g. "/api/ext/weather/v1/forecast?city=Oslo"')
41246
+ }
41247
+ },
41248
+ async ({ site: site2, path: path8 }) => {
41249
+ try {
41250
+ return ok2(await testTarget(site2, "route", path8));
41251
+ } catch (error62) {
41252
+ return toolError(error62);
41253
+ }
41254
+ }
41255
+ );
41256
+ server.registerTool(
41257
+ "run_action",
41258
+ {
41259
+ outputSchema: OUTPUT_SCHEMAS.run_action,
41260
+ title: "Run action",
41261
+ description: `Run one of a site's declared actions now, for real (Pro plan): it sends the action's POST to the other service with fields standing in for the row that would normally trigger it, and returns that service's HTTP status and a redacted preview of its response. The other service acts on it as it would on any request \u2014 a display shows the text, a message is sent \u2014 so confirm with the user before running anything that can't be taken back. This is the way to trigger an action by hand: write_db, imports and schedule pulls never fire actions.`,
41262
+ // Destructive + open-world: the other service may act irreversibly on the request.
41263
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
41264
+ inputSchema: {
41265
+ site: external_exports.string().describe("Site name or id"),
41266
+ action: external_exports.string().describe(`The action's name under "actions" in drivethrough.json`),
41267
+ fields: fieldValues.describe("The row values its {{field}} references use; {} when it uses none")
41268
+ }
41269
+ },
41270
+ async ({ site: site2, action, fields }) => {
41271
+ try {
41272
+ return ok2(await testTarget(site2, "action", action, fields));
41273
+ } catch (error62) {
41274
+ return toolError(error62);
41275
+ }
41276
+ }
41277
+ );
41278
+ server.registerTool(
41279
+ "run_schedule",
41280
+ {
41281
+ outputSchema: OUTPUT_SCHEMAS.run_schedule,
41282
+ title: "Run schedule",
41283
+ description: `Run one of a site's declared schedules once, now, for real (Pro plan), and return the outcome. A "pull" schedule calls its route and saves the response as a new row; a "rotate" schedule sends the next row through its action (the other service acts on it) and advances the rotation. A successful run also re-enables a schedule that was paused after repeated failures. The regular timetable is unchanged.`,
41284
+ // Destructive + open-world: a rotate run fires an action the other service acts on.
41285
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
41286
+ inputSchema: {
41287
+ site: external_exports.string().describe("Site name or id"),
41288
+ schedule: external_exports.string().describe(`The schedule's name under "schedules" in drivethrough.json`)
41289
+ }
41290
+ },
41291
+ async ({ site: site2, schedule }) => {
41292
+ try {
41293
+ return ok2(await testTarget(site2, "schedule", schedule));
41294
+ } catch (error62) {
41295
+ return toolError(error62);
41296
+ }
41297
+ }
41298
+ );
41299
+ server.registerTool(
41300
+ "preview_webhook",
41301
+ {
41302
+ outputSchema: OUTPUT_SCHEMAS.preview_webhook,
41303
+ title: "Preview webhook",
41304
+ description: `Dry-run one of a site's declared webhooks (Pro plan): builds an event from sample field values, takes it through the same mapping and validation a real delivery gets, and returns the row it would store and the URL the other service must POST to. Nothing is stored, sent or charged. Real delivery is proven by the other service's own "send test event", then get_integrations (last event).`,
41305
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
41306
+ inputSchema: {
41307
+ site: external_exports.string().describe("Site name or id"),
41308
+ webhook: external_exports.string().describe(`The webhook's name under "webhooks" in drivethrough.json (the <name> in /api/hook/<name>)`),
41309
+ fields: fieldValues.describe("Sample values for the fields its map fills")
41310
+ }
41311
+ },
41312
+ async ({ site: site2, webhook, fields }) => {
41313
+ try {
41314
+ return ok2(await testTarget(site2, "webhook", `/api/hook/${webhook.replace(/^\/?api\/hook\//, "")}`, fields));
41225
41315
  } catch (error62) {
41226
41316
  return toolError(error62);
41227
41317
  }
@@ -41309,14 +41399,7 @@ RECIPE \u2014 a searchable, filterable list (catalogue, directory, archive)
41309
41399
  whose query the config doesn't declare (it will render empty for visitors),
41310
41400
  and a declaration no page uses (pure cost on every insert).
41311
41401
 
41312
- RECIPE \u2014 call another service's API (stock prices, a smart display, a CRM, weather; Pro plan):
41313
- No server code. Declare it in drivethrough.json and the platform enforces it:
41314
- - "secrets": names of the keys the site needs, like ["WEATHER_KEY"]. Values are NEVER typed into the chat \u2014 manage_integrations {action: "secrets"} gives you a one-time link to send the user, who pastes the keys there.
41315
- - "routes": {"/api/ext/weather": {"upstream": "https://api.example.com", "methods": ["GET"], "paths": ["/v1/forecast"], "headers": {"Authorization": "Bearer \${WEATHER_KEY}"}, "access": "public", "cache": "5m", "query": true}} \u2192 the page fetches /api/ext/weather/v1/forecast?city=\u2026 same-origin; the key is added server-side and only the listed methods and path prefixes go through.
41316
- - "actions": {"notify": {"post": "https://\u2026", "headers": {\u2026}, "body": {"text": "{{message}}"}}} plus "on_write": "notify" on a type \u2192 each row a page POSTs to /api/data fires the call. {{field}} reads the row; \${NAME} reads a secret.
41317
- - "schedules": {"name": {"every": "1h"|"3h"|"6h"|"12h"|"1d"|"1w"|"1mo", "rotate": "<type>", "action": "<action>"}} cycles rows through an action; {"every": \u2026, "pull": {"route", "path", "into": "<type>", "map": {"field": "$.json.path"}}} saves a route's JSON as rows (for charts and history).
41318
- - "webhooks": {"payments": {"verify": "stripe", "secret": "STRIPE_WEBHOOK_SECRET", "into": "payment", "map": {"amount": "$.data.object.amount"}}} \u2192 the other service POSTs events to https://<site>/api/hook/payments; each is verified ("stripe", "github", "standard", "hmac_sha256" + "header", or "url_token" for services that can't sign) and stored as a row, which fires that type's on_write. Give the event type "read": "owner" unless visitors should see it.
41319
- Deploy, relay the secrets link if the deploy asks for one, then manage_integrations {action: "test"} each piece before telling the user it works. Full examples are in manage_integrations' description.
41402
+ Connecting a site to another service's API (Pro plan): this is site configuration, not something you run. The site's drivethrough.json declares it and deploy_site deploys it, like any other file; the format and two complete examples are in get_integrations' description. Every integration operation you can perform is its own tool: get_integrations (what is declared and how it ran), request_secrets (a one-time link where the user sets API keys \u2014 never ask for a key in the chat), test_route, run_action, run_schedule (one real run each) and preview_webhook (a dry run). Check each piece with them before telling the user it works.
41320
41403
 
41321
41404
  Custom domains (paid plans): manage_domain attaches a domain the user already owns. After attaching, check_domain {action: "connect"} looks up one-click setup at their registrar (relay the URL when offered, else the exact DNS records from attach), and check_domain {action: "status"} polls until "active" \u2014 DNS seen, then TLS issued.`;
41322
41405
  INTEGRATION_EXAMPLE_A = JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drivethrough-cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Agent-first hosting CLI. Say what you want. Stay in the car.",
5
5
  "type": "module",
6
6
  "license": "MIT",