drivethrough-cli 0.1.26 → 0.1.28

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 +683 -75
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -21658,6 +21658,67 @@ var init_names = __esm({
21658
21658
  }
21659
21659
  });
21660
21660
 
21661
+ // ../schema/src/integrations.ts
21662
+ var SECRET_NAME, ROUTE_METHODS, ROUTE_CACHE_SECONDS, SCHEDULE_EVERY, MAX_SECRETS, Headers2, RouteConfig, ActionConfig, PullConfig, ScheduleConfig, SecretsList, RoutesConfig, ActionsConfig, SchedulesConfig, HOUR, FIXED;
21663
+ var init_integrations = __esm({
21664
+ "../schema/src/integrations.ts"() {
21665
+ "use strict";
21666
+ init_zod();
21667
+ SECRET_NAME = /^[A-Z][A-Z0-9_]{0,63}$/;
21668
+ ROUTE_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
21669
+ ROUTE_CACHE_SECONDS = { "0": 0, "30s": 30, "1m": 60, "5m": 300, "15m": 900, "1h": 3600 };
21670
+ SCHEDULE_EVERY = ["1h", "3h", "6h", "12h", "1d", "1w", "1mo"];
21671
+ MAX_SECRETS = 20;
21672
+ Headers2 = external_exports.record(external_exports.string(), external_exports.string().max(4096));
21673
+ RouteConfig = external_exports.object({
21674
+ /** Literal https URL: host plus an optional base path. */
21675
+ upstream: external_exports.string().min(1).max(2048),
21676
+ methods: external_exports.array(external_exports.enum(ROUTE_METHODS)).min(1, "methods must list at least one of GET, POST, PUT, PATCH, DELETE"),
21677
+ paths: external_exports.array(external_exports.string().min(1).max(512)).min(1, 'paths must list at least one path prefix, like "/v2/account"').max(50),
21678
+ headers: Headers2.optional(),
21679
+ access: external_exports.enum(["owner", "viewers", "public"]).optional(),
21680
+ cache: external_exports.enum(Object.keys(ROUTE_CACHE_SECONDS), {
21681
+ error: 'cache must be one of "0", "30s", "1m", "5m", "15m", "1h"'
21682
+ }).optional(),
21683
+ query: external_exports.boolean().optional()
21684
+ }).strict();
21685
+ ActionConfig = external_exports.object({
21686
+ /** https URL the action POSTs to. `{{field}}` allowed in the path only. */
21687
+ post: external_exports.string().min(1).max(2048),
21688
+ headers: Headers2.optional(),
21689
+ /** A JSON object template: `${NAME}` and `{{field}}` substitute inside string values. */
21690
+ body: external_exports.record(external_exports.string(), external_exports.unknown())
21691
+ }).strict();
21692
+ PullConfig = external_exports.object({
21693
+ route: external_exports.string().min(1),
21694
+ path: external_exports.string().max(512).optional(),
21695
+ into: external_exports.string().min(1),
21696
+ map: external_exports.record(external_exports.string(), external_exports.string())
21697
+ }).strict();
21698
+ ScheduleConfig = external_exports.object({
21699
+ every: external_exports.enum(SCHEDULE_EVERY, {
21700
+ error: 'every must be one of "1h", "3h", "6h", "12h", "1d", "1w", "1mo". There is no time-of-day setting and no timezone: a schedule starts from the deploy that first declares it.'
21701
+ }),
21702
+ action: external_exports.string().optional(),
21703
+ rotate: external_exports.string().optional(),
21704
+ pull: PullConfig.optional()
21705
+ }).strict();
21706
+ SecretsList = external_exports.array(external_exports.string().regex(SECRET_NAME, "secret names are UPPER_CASE letters, numbers and underscores, like ALPACA_KEY")).max(MAX_SECRETS);
21707
+ RoutesConfig = external_exports.record(external_exports.string(), RouteConfig);
21708
+ ActionsConfig = external_exports.record(external_exports.string(), ActionConfig);
21709
+ SchedulesConfig = external_exports.record(external_exports.string(), ScheduleConfig);
21710
+ HOUR = 36e5;
21711
+ FIXED = {
21712
+ "1h": HOUR,
21713
+ "3h": 3 * HOUR,
21714
+ "6h": 6 * HOUR,
21715
+ "12h": 12 * HOUR,
21716
+ "1d": 24 * HOUR,
21717
+ "1w": 7 * 24 * HOUR
21718
+ };
21719
+ }
21720
+ });
21721
+
21661
21722
  // ../schema/src/site-config.ts
21662
21723
  function parseSiteConfig(input2) {
21663
21724
  const stripped = stripRemovedKeys(input2);
@@ -21686,6 +21747,7 @@ var init_site_config = __esm({
21686
21747
  "use strict";
21687
21748
  init_zod();
21688
21749
  init_query();
21750
+ init_integrations();
21689
21751
  FieldSpec = external_exports.enum(["string", "string?", "number", "number?", "boolean", "boolean?"]);
21690
21752
  RateLimit = external_exports.string().regex(/^\d+\/(minute|hour|day)$/, 'rate_limit must look like "5/hour"');
21691
21753
  TYPE_NAME = external_exports.string().regex(/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/, "type names are lowercase letters, numbers, hyphens and underscores").max(64);
@@ -21709,7 +21771,13 @@ var init_site_config = __esm({
21709
21771
  notify: external_exports.enum(["owner-email"]).optional(),
21710
21772
  /** Constrains tree shape: a comment may only hang off a page, etc. */
21711
21773
  parent_types: external_exports.array(external_exports.string().min(1)).optional(),
21712
- rate_limit: RateLimit.optional()
21774
+ rate_limit: RateLimit.optional(),
21775
+ /**
21776
+ * An action (declared under "actions") to fire after a row of this type is
21777
+ * inserted or upserted through the page-facing Data API. Owner-API writes,
21778
+ * imports and schedule pulls never fire it.
21779
+ */
21780
+ on_write: external_exports.string().optional()
21713
21781
  }).strict();
21714
21782
  SiteConfig = external_exports.object({
21715
21783
  /** Sitename: lowercase alphanumeric + hyphens, unique per user. */
@@ -21728,7 +21796,15 @@ var init_site_config = __esm({
21728
21796
  */
21729
21797
  query: QueryConfig.default({}),
21730
21798
  /** Pro (§3): additive custom-schema SQL applied to the site database. */
21731
- schema: external_exports.string().max(2e4).optional()
21799
+ schema: external_exports.string().max(2e4).optional(),
21800
+ /**
21801
+ * Declared integrations (Pro) — see integrations.ts. All four are optional
21802
+ * and additive: a bundle without them means exactly what it always did.
21803
+ */
21804
+ secrets: SecretsList.optional(),
21805
+ routes: RoutesConfig.optional(),
21806
+ actions: ActionsConfig.optional(),
21807
+ schedules: SchedulesConfig.optional()
21732
21808
  }).strict();
21733
21809
  REMOVED_KEYS = {
21734
21810
  indexes: 'The "indexes" key was replaced by "query" \u2014 see query.<type>.fields.'
@@ -21752,6 +21828,7 @@ var init_src = __esm({
21752
21828
  init_names();
21753
21829
  init_site_config();
21754
21830
  init_bundle();
21831
+ init_integrations();
21755
21832
  }
21756
21833
  });
21757
21834
 
@@ -39638,7 +39715,7 @@ var init_inline_files = __esm({
39638
39715
  }
39639
39716
  });
39640
39717
 
39641
- // ../mcp-tools/src/remote-assets.ts
39718
+ // ../shared/src/outbound.ts
39642
39719
  function isPrivateIpv4(host) {
39643
39720
  const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
39644
39721
  if (!match) return false;
@@ -39667,7 +39744,7 @@ async function resolveViaDoh(hostname3) {
39667
39744
  for (const type of ["A", "AAAA"]) {
39668
39745
  const res = await fetch(
39669
39746
  `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(hostname3)}&type=${type}`,
39670
- { headers: { Accept: "application/dns-json" }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }
39747
+ { headers: { Accept: "application/dns-json" }, signal: AbortSignal.timeout(DNS_TIMEOUT_MS) }
39671
39748
  );
39672
39749
  if (!res.ok) throw new Error(`DNS lookup returned HTTP ${res.status}`);
39673
39750
  const body = await res.json();
@@ -39691,6 +39768,20 @@ async function resolutionProblem(hostname3) {
39691
39768
  if (blocked) return `it resolves to a private or reserved address (${blocked})`;
39692
39769
  return null;
39693
39770
  }
39771
+ var BLOCKED_HOST_SUFFIXES, DNS_TIMEOUT_MS, resolveAddresses, IP_LITERAL, UPSTREAM_MAX_BODY, UPSTREAM_MAX_RESPONSE;
39772
+ var init_outbound = __esm({
39773
+ "../shared/src/outbound.ts"() {
39774
+ "use strict";
39775
+ BLOCKED_HOST_SUFFIXES = [".localhost", ".local", ".internal", ".home.arpa"];
39776
+ DNS_TIMEOUT_MS = 2e4;
39777
+ resolveAddresses = resolveViaDoh;
39778
+ IP_LITERAL = /^\d{1,3}(\.\d{1,3}){3}$/;
39779
+ UPSTREAM_MAX_BODY = 64 * 1024;
39780
+ UPSTREAM_MAX_RESPONSE = 1024 * 1024;
39781
+ }
39782
+ });
39783
+
39784
+ // ../mcp-tools/src/remote-assets.ts
39694
39785
  function urlProblem(url2, why) {
39695
39786
  return {
39696
39787
  code: "invalid_source_url",
@@ -39893,24 +39984,278 @@ async function resolveDeployFiles(files, opts = {}) {
39893
39984
  }
39894
39985
  return { ok: true, files: out };
39895
39986
  }
39896
- var MAX_REMOTE_FILES, MAX_REMOTE_FILE_MB, MAX_BUNDLE_TOTAL_MB, MAX_REDIRECTS, FETCH_TIMEOUT_MS, BLOCKED_HOST_SUFFIXES, resolveAddresses, IP_LITERAL;
39987
+ var MAX_REMOTE_FILES, MAX_REMOTE_FILE_MB, MAX_BUNDLE_TOTAL_MB, MAX_REDIRECTS, FETCH_TIMEOUT_MS;
39897
39988
  var init_remote_assets = __esm({
39898
39989
  "../mcp-tools/src/remote-assets.ts"() {
39899
39990
  "use strict";
39991
+ init_outbound();
39992
+ init_outbound();
39900
39993
  MAX_REMOTE_FILES = 20;
39901
39994
  MAX_REMOTE_FILE_MB = 10;
39902
39995
  MAX_BUNDLE_TOTAL_MB = 50;
39903
39996
  MAX_REDIRECTS = 3;
39904
39997
  FETCH_TIMEOUT_MS = 2e4;
39905
- BLOCKED_HOST_SUFFIXES = [".localhost", ".local", ".internal", ".home.arpa"];
39906
- resolveAddresses = resolveViaDoh;
39907
- IP_LITERAL = /^\d{1,3}(\.\d{1,3}){3}$/;
39998
+ }
39999
+ });
40000
+
40001
+ // ../mcp-tools/src/output-schemas.ts
40002
+ var ok, site, deployResult, dataDoc, dnsRecord, fileEntry, OUTPUT_SCHEMAS;
40003
+ var init_output_schemas = __esm({
40004
+ "../mcp-tools/src/output-schemas.ts"() {
40005
+ "use strict";
40006
+ init_zod();
40007
+ ok = external_exports.literal(true).describe("Always true on success; failures come back as an error result instead");
40008
+ site = external_exports.looseObject({
40009
+ id: external_exports.string().optional().describe("Stable site id \u2014 accepted anywhere a site name is"),
40010
+ sitename: external_exports.string().optional().describe("The site's name within the account"),
40011
+ hostname: external_exports.string().optional().describe("DNS label on the platform domain ({username}-{sitename})"),
40012
+ title: external_exports.string().nullish().describe("Human title shown to guests"),
40013
+ visibility: external_exports.string().optional().describe('"public" or "private"'),
40014
+ status: external_exports.string().optional().describe('"active", "suspended" or "deleted"'),
40015
+ custom_domain: external_exports.string().nullish().describe("Attached custom domain, if any"),
40016
+ domain_status: external_exports.string().optional().describe("Custom-domain verification state, when one is attached"),
40017
+ current_version: external_exports.number().optional().describe("The live version number (0 = never deployed)"),
40018
+ url: external_exports.string().optional().describe("Public address of the site \u2014 give this to the user"),
40019
+ local_url: external_exports.string().optional().describe("Local-dev address of the site"),
40020
+ created_at: external_exports.string().optional(),
40021
+ deleted_at: external_exports.string().nullish().describe("Deleted sites only"),
40022
+ purges_after: external_exports.string().nullish().describe("Deleted sites only: when it is destroyed for good")
40023
+ }).describe("The site");
40024
+ deployResult = external_exports.looseObject({
40025
+ site_id: external_exports.string().optional(),
40026
+ hostname: external_exports.string().optional(),
40027
+ version: external_exports.number().describe("The new version number \u2014 a restore point for revert_site, and the base_version for the next deploy"),
40028
+ files: external_exports.number().optional().describe("Files in the deployed version"),
40029
+ total_bytes: external_exports.number().optional(),
40030
+ bundle_hash: external_exports.string().optional().describe("Content hash of the deployed bundle"),
40031
+ url: external_exports.string().optional().describe("Where the site is served"),
40032
+ local_url: external_exports.string().optional(),
40033
+ scan: external_exports.looseObject({
40034
+ score: external_exports.number().optional(),
40035
+ verdict: external_exports.string().optional().describe('"clean", or "suspended" when the abuse scan took the site down')
40036
+ }).optional(),
40037
+ query_plan: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Per data type: the indexes the query block built and what each insert now costs in billed writes"),
40038
+ warnings: external_exports.array(external_exports.string()).optional().describe("Runtime problems only visitors would hit (missing Turnstile, unreadable fetched types\u2026). Resolve before reporting success")
40039
+ });
40040
+ dataDoc = external_exports.looseObject({
40041
+ seq: external_exports.number().describe("Row number \u2014 the row's permanent identity"),
40042
+ type: external_exports.string(),
40043
+ parent_seq: external_exports.number().nullish(),
40044
+ json: external_exports.record(external_exports.string(), external_exports.unknown()).describe("The row's document"),
40045
+ created_at: external_exports.string().optional(),
40046
+ updated_at: external_exports.string().optional()
40047
+ });
40048
+ dnsRecord = external_exports.looseObject({
40049
+ type: external_exports.string(),
40050
+ name: external_exports.string(),
40051
+ value: external_exports.string(),
40052
+ note: external_exports.string().optional()
40053
+ });
40054
+ fileEntry = external_exports.looseObject({
40055
+ path: external_exports.string(),
40056
+ size: external_exports.number().optional(),
40057
+ content_type: external_exports.string().optional(),
40058
+ sha256: external_exports.string().optional(),
40059
+ content: external_exports.string().optional().describe("Inlined contents (text, or base64 when named in paths)"),
40060
+ encoding: external_exports.string().optional().describe('"utf8" or "base64"'),
40061
+ content_omitted: external_exports.string().optional().describe('Why content is absent: "binary", "too_large" or "budget"'),
40062
+ status: external_exports.string().optional().describe('compare only: "added", "removed" or "changed"'),
40063
+ diff: external_exports.string().optional().describe("compare only: unified line diff of a changed text file"),
40064
+ diff_omitted: external_exports.string().optional()
40065
+ });
40066
+ OUTPUT_SCHEMAS = {
40067
+ deploy_site: external_exports.looseObject({
40068
+ ok,
40069
+ site,
40070
+ deploy: deployResult.describe("The deploy itself"),
40071
+ created: external_exports.boolean().optional().describe("True when this call created the site")
40072
+ }),
40073
+ create_upload: external_exports.looseObject({
40074
+ ok,
40075
+ session: external_exports.string().optional().describe("Upload session id \u2014 pass it back to list what the user uploaded"),
40076
+ upload_url: external_exports.string().optional().describe("Mint: the drag-and-drop page to send the user as a clickable link"),
40077
+ put_url: external_exports.string().optional().describe("Mint: PUT target for bytes you already hold ({filename} = last path segment)"),
40078
+ expires_in: external_exports.number().optional().describe("Mint: seconds until the link expires"),
40079
+ max_mb: external_exports.number().optional(),
40080
+ max_files: external_exports.number().optional(),
40081
+ uploads: external_exports.array(
40082
+ external_exports.looseObject({
40083
+ upload_id: external_exports.string().describe("Reference this in deploy_site {path, upload_id}"),
40084
+ filename: external_exports.string().optional(),
40085
+ size_bytes: external_exports.number().optional()
40086
+ })
40087
+ ).optional().describe("List (with session): the files staged so far"),
40088
+ next: external_exports.string().optional().describe("What to do next")
40089
+ }).describe("Mint returns the link fields; a call with `session` returns `uploads`"),
40090
+ edit_site: external_exports.looseObject({
40091
+ ok,
40092
+ site: external_exports.looseObject({ id: external_exports.string(), sitename: external_exports.string().optional() }),
40093
+ deploy: deployResult.describe("The patch deploy the edits produced"),
40094
+ files_edited: external_exports.number().optional(),
40095
+ replacements: external_exports.number().optional().describe("Total occurrences replaced across all edits")
40096
+ }),
40097
+ get_site: external_exports.looseObject({
40098
+ ok,
40099
+ site_id: external_exports.string().optional(),
40100
+ sitename: external_exports.string().optional(),
40101
+ version: external_exports.number().optional().describe("The version these files or matches were read from"),
40102
+ current_version: external_exports.number().optional().describe("What is actually live (history, or a pinned-version read)"),
40103
+ total_bytes: external_exports.number().optional(),
40104
+ bundle_hash: external_exports.string().optional(),
40105
+ config: external_exports.unknown().optional().describe("The deployed drivethrough.json"),
40106
+ files: external_exports.array(fileEntry).optional().describe("File reads: entries in deploy_site {path, content, encoding} form. compare: per-file diffs"),
40107
+ find: external_exports.string().optional(),
40108
+ files_searched: external_exports.number().optional(),
40109
+ matches: external_exports.array(
40110
+ external_exports.looseObject({
40111
+ path: external_exports.string(),
40112
+ line: external_exports.number(),
40113
+ text: external_exports.string().describe("The byte-exact deployed line \u2014 usable as edit_site `find` context"),
40114
+ occurrences: external_exports.number().optional()
40115
+ })
40116
+ ).optional().describe("find: where the text occurs"),
40117
+ truncated: external_exports.boolean().optional(),
40118
+ versions: external_exports.array(
40119
+ external_exports.looseObject({
40120
+ version: external_exports.number(),
40121
+ created_at: external_exports.string().optional(),
40122
+ files: external_exports.number().optional(),
40123
+ total_bytes: external_exports.number().optional(),
40124
+ current: external_exports.boolean().optional(),
40125
+ changes: external_exports.unknown().optional().describe('{added, changed, removed} paths vs the version before, or "initial"')
40126
+ })
40127
+ ).optional().describe("history: newest first \u2014 the map for revert_site"),
40128
+ from: external_exports.number().optional().describe("compare: older version"),
40129
+ to: external_exports.number().optional().describe("compare: newer version"),
40130
+ note: external_exports.string().optional()
40131
+ }).describe("Shape depends on the mode: file reads (files, config), find (matches), history (versions), compare (from, to, files)"),
40132
+ preview_site: external_exports.looseObject({
40133
+ ok,
40134
+ site_id: external_exports.string().optional(),
40135
+ sitename: external_exports.string().optional(),
40136
+ version: external_exports.number().optional().describe("The version that was rendered"),
40137
+ current_version: external_exports.number().optional(),
40138
+ path: external_exports.string().optional(),
40139
+ viewport: external_exports.looseObject({ width: external_exports.number(), height: external_exports.number() }).optional(),
40140
+ full_page: external_exports.boolean().optional(),
40141
+ renderer: external_exports.string().optional(),
40142
+ note: external_exports.string().optional()
40143
+ }).describe("Metadata about the screenshot; the image itself is the result's image content block"),
40144
+ revert_site: deployResult.extend({
40145
+ ok,
40146
+ reverted_to: external_exports.number().optional().describe("The version whose content was restored"),
40147
+ byte_identical: external_exports.boolean().optional().describe("True proves the new version is byte-for-byte the target version")
40148
+ }).describe("A revert is a new deploy: the new version number plus the proof of restoration"),
40149
+ create_db: external_exports.looseObject({
40150
+ ok,
40151
+ site_id: external_exports.string().optional(),
40152
+ d1_database_id: external_exports.string().optional(),
40153
+ created: external_exports.boolean().optional().describe("False when the database already existed")
40154
+ }),
40155
+ query_db: external_exports.looseObject({
40156
+ ok,
40157
+ types: external_exports.array(external_exports.looseObject({ type: external_exports.string(), count: external_exports.number() })).optional().describe("types: row count per type"),
40158
+ items: external_exports.array(dataDoc).optional().describe("list/get: the rows"),
40159
+ next_cursor: external_exports.string().nullish().describe("list: pass back as `cursor` for the next page; null at the end"),
40160
+ count: external_exports.number().optional(),
40161
+ count_capped: external_exports.boolean().optional(),
40162
+ partial: external_exports.boolean().optional().describe("Owner scan of an undeclared query stopped at its row cap"),
40163
+ declare: external_exports.string().optional().describe("The query declaration that would make this list complete and cheap"),
40164
+ search_status: external_exports.string().optional()
40165
+ }).describe('"types" returns `types`; "list" and "get" return `items`'),
40166
+ write_db: external_exports.looseObject({
40167
+ ok,
40168
+ item: dataDoc.optional().describe("insert/update: the row as stored"),
40169
+ inserted: external_exports.number().optional().describe("Bulk insert: rows written"),
40170
+ writes_charged: external_exports.number().optional(),
40171
+ first_seq: external_exports.number().optional(),
40172
+ last_seq: external_exports.number().optional(),
40173
+ deleted: external_exports.boolean().optional(),
40174
+ seq: external_exports.number().optional().describe("delete: the row removed")
40175
+ }).describe("Single insert/update \u2192 item; bulk insert \u2192 counts and seq range; delete \u2192 {deleted, seq}"),
40176
+ set_privacy: external_exports.looseObject({ ok, site }),
40177
+ share_site: external_exports.looseObject({
40178
+ ok,
40179
+ share_link: external_exports.looseObject({
40180
+ id: external_exports.string().optional(),
40181
+ url: external_exports.string().describe("The link to send \u2014 anyone holding it can view"),
40182
+ local_url: external_exports.string().optional(),
40183
+ expires_at: external_exports.string().nullish().describe("null = never expires"),
40184
+ revoked: external_exports.boolean().optional(),
40185
+ created_at: external_exports.string().optional()
40186
+ }).optional(),
40187
+ sent_to: external_exports.string().optional().describe("Invitation: the address the magic link went to")
40188
+ }).describe("A share link, or (with email) confirmation the invitation was sent"),
40189
+ list_sites: external_exports.looseObject({ ok, sites: external_exports.array(site) }),
40190
+ delete_site: external_exports.looseObject({
40191
+ ok,
40192
+ deleted: external_exports.boolean().optional(),
40193
+ site: site.optional(),
40194
+ recoverable_until: external_exports.string().nullish().describe("Restore with restore_site before this date"),
40195
+ message: external_exports.string().optional()
40196
+ }),
40197
+ check_domain: external_exports.looseObject({
40198
+ ok,
40199
+ domain: external_exports.string().optional(),
40200
+ status: external_exports.string().optional().describe("status: pending_dns \u2192 pending_ssl \u2192 active"),
40201
+ target: external_exports.string().optional(),
40202
+ records: external_exports.array(dnsRecord).optional().describe("The DNS records to create, exact and paste-ready"),
40203
+ hostnames: external_exports.array(external_exports.unknown()).optional(),
40204
+ supported: external_exports.boolean().optional().describe("connect: whether the registrar supports one-click setup"),
40205
+ provider: external_exports.string().optional(),
40206
+ url: external_exports.string().optional().describe("connect: the one-click registrar link to relay"),
40207
+ next: external_exports.string().optional()
40208
+ }).describe('"status" \u2192 verification progress; "connect" \u2192 one-click setup link or manual records'),
40209
+ manage_domain: external_exports.looseObject({
40210
+ ok,
40211
+ domain: external_exports.string().optional(),
40212
+ status: external_exports.string().optional(),
40213
+ target: external_exports.string().optional(),
40214
+ records: external_exports.array(dnsRecord).optional().describe("attach: the DNS records the user must add"),
40215
+ next: external_exports.string().optional(),
40216
+ detached: external_exports.string().optional().describe("detach: the domain removed")
40217
+ }).describe("attach \u2192 DNS records to add; detach \u2192 the removed domain"),
40218
+ restore_site: external_exports.looseObject({
40219
+ ok,
40220
+ restored: external_exports.boolean().optional(),
40221
+ site: site.optional(),
40222
+ renamed: external_exports.boolean().optional().describe("True when restored under a new name"),
40223
+ domain_lost: external_exports.string().nullish().describe("A custom domain that could not be re-attached"),
40224
+ message: external_exports.string().optional()
40225
+ }),
40226
+ manage_integrations: external_exports.looseObject({
40227
+ ok,
40228
+ // secrets
40229
+ url: external_exports.string().optional().describe("secrets: the one-time link to relay to the user (they paste keys there, never in chat)"),
40230
+ session: external_exports.string().optional().describe(`secrets: pass back with action "secrets" after the user says they're done`),
40231
+ expires_at: external_exports.string().optional(),
40232
+ names: external_exports.array(external_exports.string()).optional().describe("secrets: the names the link asks for"),
40233
+ 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."),
40234
+ all_set: external_exports.boolean().optional().describe("secrets (with session): true once every declared secret is set"),
40235
+ // status
40236
+ secrets: external_exports.record(external_exports.string(), external_exports.string()).optional().describe('status: name \u2192 "set" | "unset"'),
40237
+ routes: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("status: declared routes"),
40238
+ actions: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("status: declared actions, their fields and triggering types"),
40239
+ 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"),
40240
+ action_log: external_exports.array(external_exports.unknown()).optional().describe("status: the latest 20 outcomes, newest first"),
40241
+ // test
40242
+ target: external_exports.string().optional(),
40243
+ kind: external_exports.string().optional().describe('test: "route", "action" or "schedule"'),
40244
+ status: external_exports.string().optional().describe('test: "ok", "failed", "skipped", "plan" or "quota"'),
40245
+ http_status: external_exports.number().nullish().describe("test: the upstream's HTTP status"),
40246
+ duration_ms: external_exports.number().optional(),
40247
+ body_preview: external_exports.string().nullish().describe("test: first 1 KB of the upstream response, secrets redacted"),
40248
+ error: external_exports.string().nullish(),
40249
+ next: external_exports.string().optional()
40250
+ }).describe('"secrets" \u2192 a link or the set/unset map; "status" \u2192 everything declared plus recent outcomes; "test" \u2192 one live run')
40251
+ };
39908
40252
  }
39909
40253
  });
39910
40254
 
39911
40255
  // ../mcp-tools/src/tools.ts
39912
- function ok(data) {
39913
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, ...data }) }] };
40256
+ function ok2(data) {
40257
+ const structured = { ok: true, ...data };
40258
+ return { content: [{ type: "text", text: JSON.stringify(structured) }], structuredContent: structured };
39914
40259
  }
39915
40260
  function toolError(error62) {
39916
40261
  const body = apiErrorBody(error62) ?? {
@@ -39921,8 +40266,8 @@ function toolError(error62) {
39921
40266
  function problemError(problem) {
39922
40267
  return new ToolApiError(422, { error: problem });
39923
40268
  }
39924
- function sitePath(site) {
39925
- return `/v1/sites/${encodeURIComponent(site)}`;
40269
+ function sitePath(site2) {
40270
+ return `/v1/sites/${encodeURIComponent(site2)}`;
39926
40271
  }
39927
40272
  function registerTools(server, ctx) {
39928
40273
  const canDeployDirectory = Boolean(ctx.localDirectories);
@@ -39931,6 +40276,7 @@ function registerTools(server, ctx) {
39931
40276
  server.registerTool(
39932
40277
  "deploy_site",
39933
40278
  {
40279
+ outputSchema: OUTPUT_SCHEMAS.deploy_site,
39934
40280
  title: "Deploy site",
39935
40281
  description: `Deploy a site to Drivethrough. Idempotent: creates the site if needed, else ships a new version. Sites are private by default. ${deploySources} Images and other binary assets \u2014 pick the route by where the bytes live: already at a public URL \u2192 {path, source_url} (fetched server-side); ${binaryRoutes}already deployed on this site \u2192 {path, from_path} copies it server-side (with delete_paths, a move). Never base64-encode a file, by hand or in code \u2014 inline base64 is capped at 1 MB and is only for tiny pasted assets; a file shipped via upload_id is referenced, never additionally inlined. To rework an already-deployed site, read it back with get_site first, then deploy with mode "patch" \u2014 only the files that change. For small text changes to existing files, use edit_site instead: it sends find/replace pairs, never whole files. Every deploy's version number is a restore point: prior versions stay listed (get_site history), diffable, viewable (preview_site), and returnable (revert_site). A successful deploy may still return \`warnings\` \u2014 things that will bite at runtime but only for visitors, never for you: a form type anonymous visitors can write with no page loading the Turnstile widget, or a page that fetches a data type its visitors have no permission to read. Resolve them before telling the user it works.`,
39936
40282
  // Destructive: the default "replace" mode drops every deployed file the
@@ -39977,7 +40323,7 @@ function registerTools(server, ctx) {
39977
40323
  async ({
39978
40324
  directory,
39979
40325
  files,
39980
- site,
40326
+ site: site2,
39981
40327
  title,
39982
40328
  mode,
39983
40329
  delete_paths,
@@ -40053,7 +40399,7 @@ function registerTools(server, ctx) {
40053
40399
  }
40054
40400
  });
40055
40401
  }
40056
- const sitename = site ?? config2?.site ?? fallbackName;
40402
+ const sitename = site2 ?? config2?.site ?? fallbackName;
40057
40403
  if (!sitename) {
40058
40404
  throw new ToolApiError(422, {
40059
40405
  error: {
@@ -40081,7 +40427,7 @@ function registerTools(server, ctx) {
40081
40427
  `${sitePath(siteRes.site.id)}/deployments`,
40082
40428
  { form }
40083
40429
  );
40084
- return ok({ site: siteRes.site, deploy, created: siteRes.created });
40430
+ return ok2({ site: siteRes.site, deploy, created: siteRes.created });
40085
40431
  } catch (error62) {
40086
40432
  return toolError(error62);
40087
40433
  }
@@ -40090,6 +40436,7 @@ function registerTools(server, ctx) {
40090
40436
  server.registerTool(
40091
40437
  "create_upload",
40092
40438
  {
40439
+ outputSchema: OUTPUT_SCHEMAS.create_upload,
40093
40440
  title: "Create upload link",
40094
40441
  description: "Mint a short-lived upload link the USER opens in their browser to add local files (photos, PDFs, audio, video) \u2014 the route for binary assets when you cannot reach the bytes and they aren't hosted anywhere. Two-step: (1) relay the returned upload_url as a clickable link \u2014 it opens a drag-and-drop page (no sign-in needed) that takes up to 20 files, 25 MB each, and expires in 30 minutes. (2) When the user says they're done, call this tool again with the SAME `site` plus the returned `session` \u2014 it lists what they uploaded ({upload_id, filename, size_bytes}); ship each with deploy_site files: [{path, upload_id}] (mode \"patch\" adds them to a live site). Never base64-encode a file instead \u2014 but if the bytes are already in YOUR runtime (e.g. a chat attachment mounted in your sandbox) AND you can make HTTP requests, you may PUT them straight to the returned put_url (one file per request, its filename as the last path segment) without involving the user. Choosing a route for a binary asset: already on the public web \u2192 deploy_site {path, source_url}; this server runs on the user's machine \u2192 deploy the directory; file only in the chat or on the user's machine \u2192 this tool. Tiny pasted assets (\u22641 MB, e.g. a favicon) may still go inline as base64 content. Upload ids are single-use: the deploy that references one consumes it (the bundle keeps the bytes; the staged copy is removed and its quota refunded) \u2014 to rename or duplicate the file later, use deploy_site {path, from_path}. An upload no deploy references within 24 hours is deleted, so deploy soon after the user confirms. Pass size_bytes when the user told you a file's size \u2014 quota errors then surface here instead of failing their upload.",
40095
40442
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
@@ -40103,13 +40450,13 @@ function registerTools(server, ctx) {
40103
40450
  )
40104
40451
  }
40105
40452
  },
40106
- async ({ site, session, size_bytes }) => {
40453
+ async ({ site: site2, session, size_bytes }) => {
40107
40454
  try {
40108
40455
  if (session !== void 0) {
40109
- return ok(await ctx.api.request("GET", `${sitePath(site)}/uploads/${encodeURIComponent(session)}`));
40456
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/uploads/${encodeURIComponent(session)}`));
40110
40457
  }
40111
- return ok(
40112
- await ctx.api.request("POST", `${sitePath(site)}/uploads`, {
40458
+ return ok2(
40459
+ await ctx.api.request("POST", `${sitePath(site2)}/uploads`, {
40113
40460
  body: { ...size_bytes !== void 0 ? { size_bytes } : {} }
40114
40461
  })
40115
40462
  );
@@ -40121,6 +40468,7 @@ function registerTools(server, ctx) {
40121
40468
  server.registerTool(
40122
40469
  "edit_site",
40123
40470
  {
40471
+ outputSchema: OUTPUT_SCHEMAS.edit_site,
40124
40472
  title: "Edit site files",
40125
40473
  description: "Edit a deployed site's text files in place \u2014 the efficient way to make small changes (wording, values, a link, a style tweak) to a live site. Send only find/replace pairs, never whole file contents: the platform reads the current version, applies the edits, and ships a new version with everything else carried forward. Prefer this over get_site + deploy_site whenever the change is small relative to the file. Each `find` must match the deployed text exactly (whitespace included) and exactly once \u2014 add surrounding context to pin down one occurrence, or set replace_all. All-or-nothing: if any edit fails to match, nothing deploys and the error says how to recover, so you can try an edit without reading the file first. Edits are never lossy: the pre-edit version stays restorable via revert_site, so to undo an edit, revert \u2014 don't hand-invert it. After visual changes, preview_site shows what actually rendered.",
40126
40474
  // Destructive: find/replace overwrites text that is currently live.
@@ -40138,7 +40486,7 @@ function registerTools(server, ctx) {
40138
40486
  ).min(1).describe("Edits applied in order; later edits see earlier results")
40139
40487
  }
40140
40488
  },
40141
- async ({ site, edits }) => {
40489
+ async ({ site: site2, edits }) => {
40142
40490
  try {
40143
40491
  const normalized = edits.map((e3) => ({ ...e3, path: e3.path.startsWith("/") ? e3.path : `/${e3.path}` }));
40144
40492
  for (const edit of normalized) {
@@ -40154,7 +40502,7 @@ function registerTools(server, ctx) {
40154
40502
  }
40155
40503
  const paths = [...new Set(normalized.map((e3) => e3.path))];
40156
40504
  const query = paths.map((p2) => `path=${encodeURIComponent(p2)}`).join("&");
40157
- const read = await ctx.api.request("GET", `${sitePath(site)}/files?${query}`);
40505
+ const read = await ctx.api.request("GET", `${sitePath(site2)}/files?${query}`);
40158
40506
  const texts = /* @__PURE__ */ new Map();
40159
40507
  for (const path8 of paths) {
40160
40508
  const file2 = read.files.find((f3) => f3.path === path8);
@@ -40198,7 +40546,7 @@ function registerTools(server, ctx) {
40198
40546
  `${sitePath(read.site_id)}/deployments`,
40199
40547
  { form: assembled.bundle.form }
40200
40548
  );
40201
- return ok({
40549
+ return ok2({
40202
40550
  site: { id: read.site_id, sitename: read.sitename },
40203
40551
  deploy,
40204
40552
  files_edited: paths.length,
@@ -40212,6 +40560,7 @@ function registerTools(server, ctx) {
40212
40560
  server.registerTool(
40213
40561
  "get_site",
40214
40562
  {
40563
+ outputSchema: OUTPUT_SCHEMAS.get_site,
40215
40564
  title: "Read site files & history",
40216
40565
  description: 'Read back a deployed site: file manifest, file contents, drivethrough.json \u2014 and the site\'s version history. Use this to resume work on a site from an earlier conversation. Read the minimum you need: `find` locates text without returning file contents, `manifest_only` lists what exists, `paths` fetches only named files \u2014 a bare call inlines every text file, so reserve it for genuinely reading a whole small site. Versions: every deploy is a numbered version the platform keeps. `history: true` lists them with what each one changed (the map for revert_site), `compare: {from, to}` returns line diffs between two versions ("what did v9\u2192v10 change?"), and `version` pins any read mode to a prior version instead of the live one. Modes are exclusive per call \u2014 compare wins over history, history over find/manifest_only. Entries come back deploy_site-compatible ({path, content, encoding}); binary files are listed but only inlined when named in `paths`. For small text changes you may not need this at all \u2014 edit_site applies find/replace to the deployed files directly.',
40217
40566
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
@@ -40239,7 +40588,7 @@ function registerTools(server, ctx) {
40239
40588
  }
40240
40589
  },
40241
40590
  async ({
40242
- site,
40591
+ site: site2,
40243
40592
  paths,
40244
40593
  find,
40245
40594
  manifest_only,
@@ -40249,7 +40598,7 @@ function registerTools(server, ctx) {
40249
40598
  }) => {
40250
40599
  try {
40251
40600
  if (compare) {
40252
- return ok(await ctx.api.request("GET", `${sitePath(site)}/diff?from=${compare.from}&to=${compare.to}`));
40601
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/diff?from=${compare.from}&to=${compare.to}`));
40253
40602
  }
40254
40603
  if (history !== void 0 && history !== false) {
40255
40604
  if (typeof history === "number" && history < 1) {
@@ -40262,7 +40611,7 @@ function registerTools(server, ctx) {
40262
40611
  });
40263
40612
  }
40264
40613
  const before = typeof history === "number" ? `?before=${history}` : "";
40265
- return ok(await ctx.api.request("GET", `${sitePath(site)}/versions${before}`));
40614
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/versions${before}`));
40266
40615
  }
40267
40616
  if (find !== void 0 && find.length === 0) {
40268
40617
  throw new ToolApiError(422, {
@@ -40279,7 +40628,7 @@ function registerTools(server, ctx) {
40279
40628
  else if (manifest_only) params.set("manifest_only", "1");
40280
40629
  if (version2 !== void 0) params.set("version", String(version2));
40281
40630
  const query = params.toString();
40282
- return ok(await ctx.api.request("GET", `${sitePath(site)}/files${query ? `?${query}` : ""}`));
40631
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/files${query ? `?${query}` : ""}`));
40283
40632
  } catch (error62) {
40284
40633
  return toolError(error62);
40285
40634
  }
@@ -40288,9 +40637,12 @@ function registerTools(server, ctx) {
40288
40637
  server.registerTool(
40289
40638
  "preview_site",
40290
40639
  {
40640
+ outputSchema: OUTPUT_SCHEMAS.preview_site,
40291
40641
  title: "Preview site (screenshot)",
40292
40642
  description: "See a deployed site the way a visitor does: renders the page in a real browser server-side and returns a screenshot image you can look at directly. You have no browser of your own \u2014 this is your only view of rendered results, so use it to verify visual work (layout, styling, animations, responsive behavior) actually shipped correctly instead of assuming it did, and to eyeball a prior `version` before reverting to it. Works on private sites; nothing becomes public. Takes a few seconds \u2014 look once after a coherent batch of changes, not after every edit. Animations: the capture happens at page load; pass wait_ms to let one settle or reach mid-cycle.",
40293
- annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
40643
+ // Open-world: a real browser loads the page, and with it whatever the
40644
+ // page pulls in from elsewhere (fonts, scripts, images, embeds).
40645
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
40294
40646
  inputSchema: {
40295
40647
  site: external_exports.string().describe("Site name or id"),
40296
40648
  path: external_exports.string().optional().describe('Page to render, like "/about.html" (default "/")'),
@@ -40301,7 +40653,7 @@ function registerTools(server, ctx) {
40301
40653
  }
40302
40654
  },
40303
40655
  async ({
40304
- site,
40656
+ site: site2,
40305
40657
  path: path8,
40306
40658
  version: version2,
40307
40659
  viewport,
@@ -40309,7 +40661,7 @@ function registerTools(server, ctx) {
40309
40661
  full_page
40310
40662
  }) => {
40311
40663
  try {
40312
- const { image_base64, content_type, ...meta3 } = await ctx.api.request("POST", `${sitePath(site)}/preview`, {
40664
+ const { image_base64, content_type, ...meta3 } = await ctx.api.request("POST", `${sitePath(site2)}/preview`, {
40313
40665
  body: {
40314
40666
  ...path8 !== void 0 ? { path: path8 } : {},
40315
40667
  ...version2 !== void 0 ? { version: version2 } : {},
@@ -40318,11 +40670,13 @@ function registerTools(server, ctx) {
40318
40670
  ...full_page !== void 0 ? { full_page } : {}
40319
40671
  }
40320
40672
  });
40673
+ const structured = { ok: true, ...meta3 };
40321
40674
  return {
40322
40675
  content: [
40323
40676
  { type: "image", data: image_base64, mimeType: content_type },
40324
- { type: "text", text: JSON.stringify({ ok: true, ...meta3 }) }
40325
- ]
40677
+ { type: "text", text: JSON.stringify(structured) }
40678
+ ],
40679
+ structuredContent: structured
40326
40680
  };
40327
40681
  } catch (error62) {
40328
40682
  return toolError(error62);
@@ -40332,6 +40686,7 @@ function registerTools(server, ctx) {
40332
40686
  server.registerTool(
40333
40687
  "revert_site",
40334
40688
  {
40689
+ outputSchema: OUTPUT_SCHEMAS.revert_site,
40335
40690
  title: "Revert site to a version",
40336
40691
  description: `Return a site to a prior version, byte-exactly, in one call \u2014 the safe way to undo. When the user says "undo that" or "put it back how it was", use this; never reconstruct the old state by hand with inverse edits, which risks leaving the site neither old nor new. The platform redeploys the target version's exact stored files as a new version \u2014 history is preserved, so a revert can itself be reverted \u2014 and the response proves the restore: byte_identical: true means the new version's content hash equals the target's. Find the target with get_site {history: true} (each entry shows what that version changed), or use the version number a deploy/edit response reported. Undoing just one file of a multi-file change? Read that file at the old version (get_site {version, paths}) and deploy it with mode "patch" instead.`,
40337
40692
  // Destructive: the live site's current content is replaced by the older
@@ -40344,9 +40699,9 @@ function registerTools(server, ctx) {
40344
40699
  to_version: external_exports.number().int().min(1).describe("The version to return to (see get_site {history: true})")
40345
40700
  }
40346
40701
  },
40347
- async ({ site, to_version }) => {
40702
+ async ({ site: site2, to_version }) => {
40348
40703
  try {
40349
- return ok(await ctx.api.request("POST", `${sitePath(site)}/revert`, { body: { to_version } }));
40704
+ return ok2(await ctx.api.request("POST", `${sitePath(site2)}/revert`, { body: { to_version } }));
40350
40705
  } catch (error62) {
40351
40706
  return toolError(error62);
40352
40707
  }
@@ -40355,14 +40710,15 @@ function registerTools(server, ctx) {
40355
40710
  server.registerTool(
40356
40711
  "create_db",
40357
40712
  {
40713
+ outputSchema: OUTPUT_SCHEMAS.create_db,
40358
40714
  title: "Create site database",
40359
40715
  description: `Provision the per-site database. Idempotent. Deploying a drivethrough.json that declares types does this automatically. Site pages read and write rows via the same-origin Data API: GET /api/data?type=X lists, POST /api/data with {type, json} inserts. Anonymous writes (write: "public", or "viewers" on a public site) additionally require Cloudflare Turnstile in that POST: GET /api/config for the turnstile_site_key, load https://challenges.cloudflare.com/turnstile/v0/api.js, render <div class="cf-turnstile" data-sitekey="\u2026"></div> inside the form, and send the widget's "cf-turnstile-response" input value as "turnstile_token" in the body. The widget is usually invisible; owner and invited-viewer writes on private sites skip it entirely.`,
40360
40716
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
40361
40717
  inputSchema: { site: external_exports.string().describe("Site name or id") }
40362
40718
  },
40363
- async ({ site }) => {
40719
+ async ({ site: site2 }) => {
40364
40720
  try {
40365
- return ok(await ctx.api.request("POST", `${sitePath(site)}/db`));
40721
+ return ok2(await ctx.api.request("POST", `${sitePath(site2)}/db`));
40366
40722
  } catch (error62) {
40367
40723
  return toolError(error62);
40368
40724
  }
@@ -40371,6 +40727,7 @@ function registerTools(server, ctx) {
40371
40727
  server.registerTool(
40372
40728
  "query_db",
40373
40729
  {
40730
+ outputSchema: OUTPUT_SCHEMAS.query_db,
40374
40731
  title: "Query site database",
40375
40732
  description: 'Read the site\'s database as its owner (read-only \u2014 writes go through write_db). Same query surface the site\'s own pages use, so what works here works there. Every row is {seq, type, parent_seq, json, created_at, updated_at}: `seq` is the row number and its permanent identity (never reused, even after a delete), `json` is the document. Newest-first is the default and always free. Filtering, sorting and search need a matching entry in the site\'s "query" block in drivethrough.json \u2014 an undeclared query is refused with the exact line to add. As the owner you get a bounded 5,000-row scan instead of a refusal, marked "partial": true, so you can look at data you have not modelled yet; a visitor page gets the refusal.',
40376
40733
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
@@ -40402,11 +40759,11 @@ function registerTools(server, ctx) {
40402
40759
  }
40403
40760
  },
40404
40761
  async (args) => {
40405
- const { site, action, type, seq } = args;
40762
+ const { site: site2, action, type, seq } = args;
40406
40763
  try {
40407
40764
  switch (action) {
40408
40765
  case "types":
40409
- return ok(await ctx.api.request("GET", `${sitePath(site)}/data/types`));
40766
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/data/types`));
40410
40767
  case "list": {
40411
40768
  if (!type) {
40412
40769
  throw new ToolApiError(422, {
@@ -40434,7 +40791,7 @@ function registerTools(server, ctx) {
40434
40791
  if (args.cursor) params.set("cursor", args.cursor);
40435
40792
  if (args.offset !== void 0) params.set("offset", String(args.offset));
40436
40793
  if (args.count) params.set("count", "1");
40437
- return ok(await ctx.api.request("GET", `${sitePath(site)}/data?${params}`));
40794
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/data?${params}`));
40438
40795
  }
40439
40796
  case "get": {
40440
40797
  if (seq === void 0) {
@@ -40446,7 +40803,7 @@ function registerTools(server, ctx) {
40446
40803
  }
40447
40804
  });
40448
40805
  }
40449
- return ok(await ctx.api.request("GET", `${sitePath(site)}/data?seq=${encodeURIComponent(String(seq))}`));
40806
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/data?seq=${encodeURIComponent(String(seq))}`));
40450
40807
  }
40451
40808
  }
40452
40809
  } catch (error62) {
@@ -40457,6 +40814,7 @@ function registerTools(server, ctx) {
40457
40814
  server.registerTool(
40458
40815
  "write_db",
40459
40816
  {
40817
+ outputSchema: OUTPUT_SCHEMAS.write_db,
40460
40818
  title: "Write site database",
40461
40819
  description: "Write to the site's database as its owner: insert one row, import many, update, or delete (reads go through query_db). Rows are addressed by `seq`, the row number query_db returns. To change a few fields of a row, `update` with `merge` \u2014 only the fields that change, no fetch needed. Reserve `json` (wholesale replacement) for rewriting the whole document. To load a catalogue, use `rows` for a handful and `rows_url` for thousands \u2014 never loop insert, and never try to emit 100,000 rows yourself. Writes are validated against the types declared in the site's drivethrough.json (unknown types and mismatched fields are rejected, and a bulk import is checked in full BEFORE the first row is written, so it is all-or-nothing); pass validate: false only for a deliberate migration. Deleted and overwritten rows are gone for good \u2014 there is no version history for data, so read a row (query_db) before destructive rewrites you might need to undo. Deleting a row also deletes its children. Every write counts against the account's monthly write allowance, and an import larger than what remains is refused before anything is written.",
40462
40820
  // Open-world for the same reason deploy_site is: rows_url makes the
@@ -40482,7 +40840,7 @@ function registerTools(server, ctx) {
40482
40840
  }
40483
40841
  },
40484
40842
  async ({
40485
- site,
40843
+ site: site2,
40486
40844
  action,
40487
40845
  type,
40488
40846
  seq,
@@ -40536,8 +40894,8 @@ function registerTools(server, ctx) {
40536
40894
  }
40537
40895
  });
40538
40896
  }
40539
- return ok(
40540
- await ctx.api.request("POST", `${sitePath(site)}/data`, {
40897
+ return ok2(
40898
+ await ctx.api.request("POST", `${sitePath(site2)}/data`, {
40541
40899
  body: {
40542
40900
  type,
40543
40901
  ...parent_seq !== void 0 ? { parent_seq } : {},
@@ -40557,8 +40915,8 @@ function registerTools(server, ctx) {
40557
40915
  }
40558
40916
  });
40559
40917
  }
40560
- return ok(
40561
- await ctx.api.request("POST", `${sitePath(site)}/data`, {
40918
+ return ok2(
40919
+ await ctx.api.request("POST", `${sitePath(site2)}/data`, {
40562
40920
  body: { type, parent_seq, json: json2, ...validate3 !== void 0 ? { validate: validate3 } : {} }
40563
40921
  })
40564
40922
  );
@@ -40573,8 +40931,8 @@ function registerTools(server, ctx) {
40573
40931
  }
40574
40932
  });
40575
40933
  }
40576
- return ok(
40577
- await ctx.api.request("PATCH", `${sitePath(site)}/data/${encodeURIComponent(String(needSeq()))}`, {
40934
+ return ok2(
40935
+ await ctx.api.request("PATCH", `${sitePath(site2)}/data/${encodeURIComponent(String(needSeq()))}`, {
40578
40936
  body: {
40579
40937
  ...type !== void 0 ? { type } : {},
40580
40938
  ...parent_seq !== void 0 ? { parent_seq } : {},
@@ -40585,8 +40943,8 @@ function registerTools(server, ctx) {
40585
40943
  })
40586
40944
  );
40587
40945
  case "delete":
40588
- return ok(
40589
- await ctx.api.request("DELETE", `${sitePath(site)}/data/${encodeURIComponent(String(needSeq()))}`)
40946
+ return ok2(
40947
+ await ctx.api.request("DELETE", `${sitePath(site2)}/data/${encodeURIComponent(String(needSeq()))}`)
40590
40948
  );
40591
40949
  }
40592
40950
  } catch (error62) {
@@ -40597,6 +40955,7 @@ function registerTools(server, ctx) {
40597
40955
  server.registerTool(
40598
40956
  "set_privacy",
40599
40957
  {
40958
+ outputSchema: OUTPUT_SCHEMAS.set_privacy,
40600
40959
  title: "Set site visibility",
40601
40960
  description: "Set a site's visibility to public or private. Privacy is a setting \u2014 nothing redeploys.",
40602
40961
  // Open-world: going public puts the site on the open web.
@@ -40606,9 +40965,9 @@ function registerTools(server, ctx) {
40606
40965
  visibility: external_exports.enum(["public", "private"]).describe("Who can see the site")
40607
40966
  }
40608
40967
  },
40609
- async ({ site, visibility }) => {
40968
+ async ({ site: site2, visibility }) => {
40610
40969
  try {
40611
- return ok(await ctx.api.request("POST", `${sitePath(site)}/privacy`, { body: { visibility } }));
40970
+ return ok2(await ctx.api.request("POST", `${sitePath(site2)}/privacy`, { body: { visibility } }));
40612
40971
  } catch (error62) {
40613
40972
  return toolError(error62);
40614
40973
  }
@@ -40617,6 +40976,7 @@ function registerTools(server, ctx) {
40617
40976
  server.registerTool(
40618
40977
  "share_site",
40619
40978
  {
40979
+ outputSchema: OUTPUT_SCHEMAS.share_site,
40620
40980
  title: "Share site",
40621
40981
  description: "Create a share link for a private site, or send a magic-link invitation by email. Anyone with the link can view; guests never make accounts.",
40622
40982
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
@@ -40625,10 +40985,10 @@ function registerTools(server, ctx) {
40625
40985
  email: external_exports.string().email().optional().describe("Send a magic-link invitation to this email instead of returning a link")
40626
40986
  }
40627
40987
  },
40628
- async ({ site, email: email3 }) => {
40988
+ async ({ site: site2, email: email3 }) => {
40629
40989
  try {
40630
- if (email3) return ok(await ctx.api.request("POST", `${sitePath(site)}/invite`, { body: { email: email3 } }));
40631
- return ok(await ctx.api.request("POST", `${sitePath(site)}/share`));
40990
+ if (email3) return ok2(await ctx.api.request("POST", `${sitePath(site2)}/invite`, { body: { email: email3 } }));
40991
+ return ok2(await ctx.api.request("POST", `${sitePath(site2)}/share`));
40632
40992
  } catch (error62) {
40633
40993
  return toolError(error62);
40634
40994
  }
@@ -40637,6 +40997,7 @@ function registerTools(server, ctx) {
40637
40997
  server.registerTool(
40638
40998
  "list_sites",
40639
40999
  {
41000
+ outputSchema: OUTPUT_SCHEMAS.list_sites,
40640
41001
  title: "List sites",
40641
41002
  description: "List the authenticated account's sites with their addresses, visibility, and status. Pass include_deleted to also see recently deleted sites (recoverable for 30 days via restore_site).",
40642
41003
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
@@ -40646,7 +41007,7 @@ function registerTools(server, ctx) {
40646
41007
  },
40647
41008
  async ({ include_deleted }) => {
40648
41009
  try {
40649
- return ok(await ctx.api.request("GET", `/v1/sites${include_deleted ? "?include_deleted=1" : ""}`));
41010
+ return ok2(await ctx.api.request("GET", `/v1/sites${include_deleted ? "?include_deleted=1" : ""}`));
40650
41011
  } catch (error62) {
40651
41012
  return toolError(error62);
40652
41013
  }
@@ -40655,14 +41016,15 @@ function registerTools(server, ctx) {
40655
41016
  server.registerTool(
40656
41017
  "delete_site",
40657
41018
  {
41019
+ outputSchema: OUTPUT_SCHEMAS.delete_site,
40658
41020
  title: "Delete site",
40659
41021
  description: "Delete a site. Two-phase and safe: the site leaves the driveway immediately (guests lose access, share links are revoked, the name frees up) but stays recoverable for 30 days via restore_site. Only after 30 days is it destroyed for good.",
40660
41022
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
40661
41023
  inputSchema: { site: external_exports.string().describe("Site name or id") }
40662
41024
  },
40663
- async ({ site }) => {
41025
+ async ({ site: site2 }) => {
40664
41026
  try {
40665
- return ok(await ctx.api.request("DELETE", sitePath(site)));
41027
+ return ok2(await ctx.api.request("DELETE", sitePath(site2)));
40666
41028
  } catch (error62) {
40667
41029
  return toolError(error62);
40668
41030
  }
@@ -40671,6 +41033,7 @@ function registerTools(server, ctx) {
40671
41033
  server.registerTool(
40672
41034
  "check_domain",
40673
41035
  {
41036
+ outputSchema: OUTPUT_SCHEMAS.check_domain,
40674
41037
  title: "Check custom domain",
40675
41038
  description: `Check a site's custom domain, read-only. "status": live verification progress \u2014 pending_dns (waiting on DNS records) \u2192 pending_ssl (certificate issuing) \u2192 active (serving with TLS); poll after setup and tell the user when it's live. "connect": look up one-click setup at the user's registrar (GoDaddy, Cloudflare, IONOS, Squarespace, \u2026) \u2014 when a URL comes back, relay it and their registrar creates the records in one click; otherwise relay the copy-paste records manage_domain attach returned.`,
40676
41039
  // Not readOnlyHint despite being the "read" half of the domain pair:
@@ -40683,10 +41046,10 @@ function registerTools(server, ctx) {
40683
41046
  action: external_exports.enum(["status", "connect"]).describe('"status" = verification progress; "connect" = one-click registrar setup lookup')
40684
41047
  }
40685
41048
  },
40686
- async ({ site, action }) => {
41049
+ async ({ site: site2, action }) => {
40687
41050
  try {
40688
- if (action === "connect") return ok(await ctx.api.request("GET", `${sitePath(site)}/domain/connect`));
40689
- return ok(await ctx.api.request("GET", `${sitePath(site)}/domain`));
41051
+ if (action === "connect") return ok2(await ctx.api.request("GET", `${sitePath(site2)}/domain/connect`));
41052
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/domain`));
40690
41053
  } catch (error62) {
40691
41054
  return toolError(error62);
40692
41055
  }
@@ -40695,6 +41058,7 @@ function registerTools(server, ctx) {
40695
41058
  server.registerTool(
40696
41059
  "manage_domain",
40697
41060
  {
41061
+ outputSchema: OUTPUT_SCHEMAS.manage_domain,
40698
41062
  title: "Attach/detach custom domain",
40699
41063
  description: 'Attach or detach a custom domain the user already owns (paid plans). "attach": connect the domain to the site \u2014 returns the exact DNS records to add; afterwards use check_domain {action: "connect"} for a one-click registrar link and check_domain {action: "status"} to poll until active. "detach": remove the domain from the site. The site keeps its drivethrough.site address throughout.',
40700
41064
  // Open-world like check_domain: attach initiates certificate issuance
@@ -40709,7 +41073,7 @@ function registerTools(server, ctx) {
40709
41073
  },
40710
41074
  async ({
40711
41075
  action,
40712
- site,
41076
+ site: site2,
40713
41077
  domain: domain2
40714
41078
  }) => {
40715
41079
  try {
@@ -40723,9 +41087,9 @@ function registerTools(server, ctx) {
40723
41087
  }
40724
41088
  });
40725
41089
  }
40726
- return ok(await ctx.api.request("POST", `${sitePath(site)}/domain`, { body: { domain: domain2 } }));
41090
+ return ok2(await ctx.api.request("POST", `${sitePath(site2)}/domain`, { body: { domain: domain2 } }));
40727
41091
  }
40728
- return ok(await ctx.api.request("DELETE", `${sitePath(site)}/domain`));
41092
+ return ok2(await ctx.api.request("DELETE", `${sitePath(site2)}/domain`));
40729
41093
  } catch (error62) {
40730
41094
  return toolError(error62);
40731
41095
  }
@@ -40734,6 +41098,7 @@ function registerTools(server, ctx) {
40734
41098
  server.registerTool(
40735
41099
  "restore_site",
40736
41100
  {
41101
+ outputSchema: OUTPUT_SCHEMAS.restore_site,
40737
41102
  title: "Restore deleted site",
40738
41103
  description: "Restore a deleted site within its 30-day recovery window. It comes back exactly as it was \u2014 content, data, visibility \u2014 minus guest access (share links stay revoked; create new ones). If the old name is taken, pass `sitename` to restore under a new name. (Undoing changes to a live site is different \u2014 that's revert_site.)",
40739
41104
  // Open-world: a site that was public comes back public, served to visitors.
@@ -40743,7 +41108,7 @@ function registerTools(server, ctx) {
40743
41108
  sitename: external_exports.string().optional().describe("Restore under this new name instead (when the old name is taken)")
40744
41109
  }
40745
41110
  },
40746
- async ({ site, sitename }) => {
41111
+ async ({ site: site2, sitename }) => {
40747
41112
  try {
40748
41113
  if (sitename !== void 0 && sitename.length === 0) {
40749
41114
  throw new ToolApiError(422, {
@@ -40754,8 +41119,8 @@ function registerTools(server, ctx) {
40754
41119
  }
40755
41120
  });
40756
41121
  }
40757
- return ok(
40758
- await ctx.api.request("POST", `${sitePath(site)}/restore`, {
41122
+ return ok2(
41123
+ await ctx.api.request("POST", `${sitePath(site2)}/restore`, {
40759
41124
  body: sitename ? { sitename } : {}
40760
41125
  })
40761
41126
  );
@@ -40764,8 +41129,81 @@ function registerTools(server, ctx) {
40764
41129
  }
40765
41130
  }
40766
41131
  );
41132
+ server.registerTool(
41133
+ "manage_integrations",
41134
+ {
41135
+ outputSchema: OUTPUT_SCHEMAS.manage_integrations,
41136
+ title: "Manage integrations",
41137
+ 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). 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. (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. action "status" shows secrets set/unset, routes, schedules (next run, last result, failures) 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.`,
41138
+ // Open-world: tests call a third-party API on the user's behalf.
41139
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
41140
+ inputSchema: {
41141
+ site: external_exports.string().describe("Site name or id"),
41142
+ 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'),
41143
+ 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."),
41144
+ 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"),
41145
+ target: external_exports.string().optional().describe('test: a route key plus path ("/api/ext/weather/v1/forecast?city=Oslo"), an action name, or a schedule name'),
41146
+ fields: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).optional().describe("test, action targets only: the row values the action's {{field}} references use")
41147
+ }
41148
+ },
41149
+ async ({
41150
+ site: site2,
41151
+ action,
41152
+ names,
41153
+ session,
41154
+ target,
41155
+ fields
41156
+ }) => {
41157
+ try {
41158
+ const setMap = async () => {
41159
+ const listed = await ctx.api.request(
41160
+ "GET",
41161
+ `${sitePath(site2)}/secrets`
41162
+ );
41163
+ return listed.secrets;
41164
+ };
41165
+ if (action === "secrets") {
41166
+ if (session !== void 0) {
41167
+ const secrets2 = await setMap();
41168
+ const declared = secrets2.filter((s2) => s2.declared);
41169
+ const unset = declared.filter((s2) => !s2.set).map((s2) => s2.name);
41170
+ return ok2({
41171
+ session,
41172
+ set: Object.fromEntries(secrets2.map((s2) => [s2.name, s2.set])),
41173
+ all_set: unset.length === 0,
41174
+ 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 and schedule."
41175
+ });
41176
+ }
41177
+ const link = await ctx.api.request("POST", `${sitePath(site2)}/secrets/link`, {
41178
+ body: names ? { names } : {}
41179
+ });
41180
+ const secrets = await setMap();
41181
+ return ok2({ ...link, set: Object.fromEntries(secrets.map((s2) => [s2.name, s2.set])) });
41182
+ }
41183
+ if (action === "status") {
41184
+ return ok2(await ctx.api.request("GET", `${sitePath(site2)}/integrations`));
41185
+ }
41186
+ if (!target) {
41187
+ throw new ToolApiError(422, {
41188
+ error: {
41189
+ code: "target_required",
41190
+ message: 'action "test" needs a target.',
41191
+ next: 'Pass target: a route plus path ("/api/ext/name/path"), an action name, or a schedule name. action "status" lists them.'
41192
+ }
41193
+ });
41194
+ }
41195
+ return ok2(
41196
+ await ctx.api.request("POST", `${sitePath(site2)}/integrations/test`, {
41197
+ body: { target, ...fields ? { fields } : {} }
41198
+ })
41199
+ );
41200
+ } catch (error62) {
41201
+ return toolError(error62);
41202
+ }
41203
+ }
41204
+ );
40767
41205
  }
40768
- var MAX_BUNDLE_MB, SERVER_INSTRUCTIONS;
41206
+ var MAX_BUNDLE_MB, SERVER_INSTRUCTIONS, INTEGRATION_EXAMPLE_A, INTEGRATION_EXAMPLE_B;
40769
41207
  var init_tools = __esm({
40770
41208
  "../mcp-tools/src/tools.ts"() {
40771
41209
  "use strict";
@@ -40774,6 +41212,7 @@ var init_tools = __esm({
40774
41212
  init_edits();
40775
41213
  init_inline_files();
40776
41214
  init_remote_assets();
41215
+ init_output_schemas();
40777
41216
  MAX_BUNDLE_MB = 50;
40778
41217
  SERVER_INSTRUCTIONS = `Drivethrough hosts websites: deploy, share, collect data. Sites are private by default; guests never need accounts.
40779
41218
 
@@ -40845,7 +41284,51 @@ RECIPE \u2014 a searchable, filterable list (catalogue, directory, archive)
40845
41284
  whose query the config doesn't declare (it will render empty for visitors),
40846
41285
  and a declaration no page uses (pure cost on every insert).
40847
41286
 
41287
+ RECIPE \u2014 call another service's API (stock prices, a smart display, a CRM, weather; Pro plan):
41288
+ No server code. Declare it in drivethrough.json and the platform enforces it:
41289
+ - "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.
41290
+ - "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.
41291
+ - "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.
41292
+ - "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).
41293
+ 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.
41294
+
40848
41295
  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.`;
41296
+ INTEGRATION_EXAMPLE_A = JSON.stringify({
41297
+ secrets: ["ALPACA_KEY", "ALPACA_SECRET"],
41298
+ routes: {
41299
+ "/api/ext/alpaca": {
41300
+ upstream: "https://paper-api.alpaca.markets",
41301
+ methods: ["GET"],
41302
+ paths: ["/v2/account", "/v2/positions", "/v2/account/portfolio/history"],
41303
+ headers: { "APCA-API-KEY-ID": "${ALPACA_KEY}", "APCA-API-SECRET-KEY": "${ALPACA_SECRET}" },
41304
+ access: "viewers",
41305
+ cache: "30s",
41306
+ query: true
41307
+ }
41308
+ },
41309
+ types: { snapshot: { fields: { equity: "number", cash: "number" } } },
41310
+ schedules: {
41311
+ portfolio: {
41312
+ every: "1h",
41313
+ pull: { route: "/api/ext/alpaca", path: "/v2/account", into: "snapshot", map: { equity: "$.equity", cash: "$.cash" } }
41314
+ }
41315
+ }
41316
+ });
41317
+ INTEGRATION_EXAMPLE_B = JSON.stringify({
41318
+ secrets: ["VESTABOARD_KEY"],
41319
+ actions: {
41320
+ show: {
41321
+ post: "https://rw.vestaboard.com/",
41322
+ headers: { "X-Vestaboard-Read-Write-Key": "${VESTABOARD_KEY}" },
41323
+ body: { text: "{{text}}" }
41324
+ }
41325
+ },
41326
+ types: {
41327
+ quote: { fields: { text: "string" } },
41328
+ activation: { fields: { text: "string" }, on_write: "show" }
41329
+ },
41330
+ schedules: { cycle: { every: "3h", rotate: "quote", action: "show" } }
41331
+ });
40849
41332
  }
40850
41333
  });
40851
41334
 
@@ -40858,6 +41341,7 @@ var init_src2 = __esm({
40858
41341
  init_inline_files();
40859
41342
  init_remote_assets();
40860
41343
  init_tools();
41344
+ init_output_schemas();
40861
41345
  }
40862
41346
  });
40863
41347
 
@@ -42786,6 +43270,28 @@ var dD = class extends x2 {
42786
43270
  };
42787
43271
  var A3;
42788
43272
  A3 = /* @__PURE__ */ new WeakMap();
43273
+ var TD = Object.defineProperty;
43274
+ var jD2 = (e3, u4, t2) => u4 in e3 ? TD(e3, u4, { enumerable: true, configurable: true, writable: true, value: t2 }) : e3[u4] = t2;
43275
+ var U = (e3, u4, t2) => (jD2(e3, typeof u4 != "symbol" ? u4 + "" : u4, t2), t2);
43276
+ var MD = class extends x2 {
43277
+ constructor({ mask: u4, ...t2 }) {
43278
+ super(t2), U(this, "valueWithCursor", ""), U(this, "_mask", "\u2022"), this._mask = u4 ?? "\u2022", this.on("finalize", () => {
43279
+ this.valueWithCursor = this.masked;
43280
+ }), this.on("value", () => {
43281
+ if (this.cursor >= this.value.length) this.valueWithCursor = `${this.masked}${import_picocolors.default.inverse(import_picocolors.default.hidden("_"))}`;
43282
+ else {
43283
+ const F4 = this.masked.slice(0, this.cursor), s2 = this.masked.slice(this.cursor);
43284
+ this.valueWithCursor = `${F4}${import_picocolors.default.inverse(s2[0])}${s2.slice(1)}`;
43285
+ }
43286
+ });
43287
+ }
43288
+ get cursor() {
43289
+ return this._cursor;
43290
+ }
43291
+ get masked() {
43292
+ return this.value.replaceAll(/./g, this._mask);
43293
+ }
43294
+ };
42789
43295
  var RD = class extends x2 {
42790
43296
  get valueWithCursor() {
42791
43297
  if (this.state === "submit") return this.value;
@@ -42831,7 +43337,7 @@ var de = u3("\u251C", "+");
42831
43337
  var pe = u3("\u256F", "+");
42832
43338
  var q2 = u3("\u25CF", "\u2022");
42833
43339
  var D2 = u3("\u25C6", "*");
42834
- var U = u3("\u25B2", "!");
43340
+ var U2 = u3("\u25B2", "!");
42835
43341
  var K2 = u3("\u25A0", "x");
42836
43342
  var b3 = (t2) => {
42837
43343
  switch (t2) {
@@ -42867,6 +43373,27 @@ ${import_picocolors2.default.cyan(d3)}
42867
43373
  `;
42868
43374
  }
42869
43375
  } }).prompt();
43376
+ var ge = (t2) => new MD({ validate: t2.validate, mask: t2.mask ?? $e, render() {
43377
+ const n2 = `${import_picocolors2.default.gray(o3)}
43378
+ ${b3(this.state)} ${t2.message}
43379
+ `, r3 = this.valueWithCursor, i2 = this.masked;
43380
+ switch (this.state) {
43381
+ case "error":
43382
+ return `${n2.trim()}
43383
+ ${import_picocolors2.default.yellow(o3)} ${i2}
43384
+ ${import_picocolors2.default.yellow(d3)} ${import_picocolors2.default.yellow(this.error)}
43385
+ `;
43386
+ case "submit":
43387
+ return `${n2}${import_picocolors2.default.gray(o3)} ${import_picocolors2.default.dim(i2)}`;
43388
+ case "cancel":
43389
+ return `${n2}${import_picocolors2.default.gray(o3)} ${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(i2 ?? ""))}${i2 ? `
43390
+ ${import_picocolors2.default.gray(o3)}` : ""}`;
43391
+ default:
43392
+ return `${n2}${import_picocolors2.default.cyan(o3)} ${r3}
43393
+ ${import_picocolors2.default.cyan(d3)}
43394
+ `;
43395
+ }
43396
+ } }).prompt();
42870
43397
  var ye2 = (t2) => {
42871
43398
  const n2 = t2.active ?? "Yes", r3 = t2.inactive ?? "No";
42872
43399
  return new dD({ active: n2, inactive: r3, initialValue: t2.initialValue ?? true, render() {
@@ -42928,7 +43455,7 @@ var M2 = { message: (t2 = "", { symbol: n2 = import_picocolors2.default.gray(o3)
42928
43455
  }, step: (t2) => {
42929
43456
  M2.message(t2, { symbol: import_picocolors2.default.green(C4) });
42930
43457
  }, warn: (t2) => {
42931
- M2.message(t2, { symbol: import_picocolors2.default.yellow(U) });
43458
+ M2.message(t2, { symbol: import_picocolors2.default.yellow(U2) });
42932
43459
  }, warning: (t2) => {
42933
43460
  M2.warn(t2);
42934
43461
  }, error: (t2) => {
@@ -43073,15 +43600,15 @@ var sitesCommand = defineCommand({
43073
43600
  );
43074
43601
  return;
43075
43602
  }
43076
- for (const site of sites) {
43077
- if (site.status === "deleted") {
43603
+ for (const site2 of sites) {
43604
+ if (site2.status === "deleted") {
43078
43605
  M2.message(
43079
- `${site.title} \u2014 ${site.sitename} (deleted, recoverable until ${site.purges_after ?? "?"} \u2014 \`drivethrough restore ${site.sitename}\`)`
43606
+ `${site2.title} \u2014 ${site2.sitename} (deleted, recoverable until ${site2.purges_after ?? "?"} \u2014 \`drivethrough restore ${site2.sitename}\`)`
43080
43607
  );
43081
43608
  continue;
43082
43609
  }
43083
- const flags = [site.visibility, site.status !== "active" ? site.status : null].filter(Boolean).join(", ");
43084
- M2.message(`${site.title} \u2014 ${site.local_url} (${flags}, v${site.current_version})`);
43610
+ const flags = [site2.visibility, site2.status !== "active" ? site2.status : null].filter(Boolean).join(", ");
43611
+ M2.message(`${site2.title} \u2014 ${site2.local_url} (${flags}, v${site2.current_version})`);
43085
43612
  }
43086
43613
  });
43087
43614
  } catch (error62) {
@@ -43826,6 +44353,86 @@ var collaboratorsCommand = defineCommand({
43826
44353
  subCommands: { list: listCmd, add: addCmd }
43827
44354
  });
43828
44355
 
44356
+ // src/commands/secrets.ts
44357
+ init_api();
44358
+ var listCmd2 = defineCommand({
44359
+ meta: { name: "list", description: "List a site's secrets (names and whether each is set \u2014 never values)" },
44360
+ args: {
44361
+ site: { type: "positional", description: "Site name", required: true },
44362
+ json: { type: "boolean" }
44363
+ },
44364
+ async run({ args }) {
44365
+ const jsonArgs = { json: args.json };
44366
+ try {
44367
+ const res = await api("GET", `/v1/sites/${encodeURIComponent(args.site)}/secrets`);
44368
+ emit(jsonArgs, { secrets: res.secrets }, () => {
44369
+ if (!res.secrets.length) M2.info("This site declares no secrets.");
44370
+ for (const s2 of res.secrets) {
44371
+ const note = s2.declared ? "" : " (not in drivethrough.json)";
44372
+ M2.message(`${s2.name} ${s2.set ? `set ${s2.updated_at?.slice(0, 10) ?? ""}` : "NOT SET"}${note}`);
44373
+ }
44374
+ });
44375
+ } catch (error62) {
44376
+ fail(jsonArgs, error62);
44377
+ }
44378
+ }
44379
+ });
44380
+ async function readValue(name) {
44381
+ if (!process.stdin.isTTY) {
44382
+ const chunks = [];
44383
+ for await (const chunk of process.stdin) chunks.push(chunk);
44384
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
44385
+ }
44386
+ const value = await ge({ message: `Value for ${name}` });
44387
+ if (pD2(value)) process.exit(1);
44388
+ return value;
44389
+ }
44390
+ var setCmd = defineCommand({
44391
+ meta: { name: "set", description: "Set (or replace) a secret \u2014 the value is prompted for, or read from stdin" },
44392
+ args: {
44393
+ site: { type: "positional", description: "Site name", required: true },
44394
+ name: { type: "positional", description: "Secret name, e.g. API_KEY", required: true },
44395
+ json: { type: "boolean" }
44396
+ },
44397
+ async run({ args }) {
44398
+ const jsonArgs = { json: args.json };
44399
+ try {
44400
+ const value = await readValue(args.name);
44401
+ await api("PUT", `/v1/sites/${encodeURIComponent(args.site)}/secrets/${encodeURIComponent(args.name)}`, {
44402
+ body: { value }
44403
+ });
44404
+ emit(jsonArgs, { name: args.name, set: true }, () => M2.success(`${args.name} is set on ${args.site}.`));
44405
+ } catch (error62) {
44406
+ fail(jsonArgs, error62);
44407
+ }
44408
+ }
44409
+ });
44410
+ var unsetCmd = defineCommand({
44411
+ meta: { name: "unset", description: "Remove a secret's value" },
44412
+ args: {
44413
+ site: { type: "positional", description: "Site name", required: true },
44414
+ name: { type: "positional", description: "Secret name", required: true },
44415
+ json: { type: "boolean" }
44416
+ },
44417
+ async run({ args }) {
44418
+ const jsonArgs = { json: args.json };
44419
+ try {
44420
+ await api("DELETE", `/v1/sites/${encodeURIComponent(args.site)}/secrets/${encodeURIComponent(args.name)}`);
44421
+ emit(
44422
+ jsonArgs,
44423
+ { name: args.name, set: false },
44424
+ () => M2.success(`${args.name} is unset on ${args.site}. Anything that uses it now fails until it's set again.`)
44425
+ );
44426
+ } catch (error62) {
44427
+ fail(jsonArgs, error62);
44428
+ }
44429
+ }
44430
+ });
44431
+ var secretsCommand = defineCommand({
44432
+ meta: { name: "secrets", description: "API keys a site's integrations use (Pro) \u2014 write-only" },
44433
+ subCommands: { list: listCmd2, set: setCmd, unset: unsetCmd }
44434
+ });
44435
+
43829
44436
  // src/commands/usage.ts
43830
44437
  init_api();
43831
44438
  function human(value, unit) {
@@ -44401,8 +45008,8 @@ function resolveSitename(root, explicit) {
44401
45008
  const raw = localConfigRaw(root);
44402
45009
  if (raw) {
44403
45010
  try {
44404
- const site = JSON.parse(raw).site;
44405
- if (site) return site;
45011
+ const site2 = JSON.parse(raw).site;
45012
+ if (site2) return site2;
44406
45013
  } catch {
44407
45014
  }
44408
45015
  }
@@ -44744,6 +45351,7 @@ var main = defineCommand({
44744
45351
  upgrade: upgradeCommand,
44745
45352
  domain: domainCommand,
44746
45353
  collaborators: collaboratorsCommand,
45354
+ secrets: secretsCommand,
44747
45355
  usage: usageCommand,
44748
45356
  mcp: mcpCommand,
44749
45357
  "agent-setup": agentSetupCommand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drivethrough-cli",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Agent-first hosting CLI. Say what you want. Stay in the car.",
5
5
  "type": "module",
6
6
  "license": "MIT",