helipod-mcp 0.5.11 → 0.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js CHANGED
@@ -35686,7 +35686,7 @@ class HelipodClient {
35686
35686
  const details = json2?.details && typeof json2.details === "object" && !Array.isArray(json2.details) ? json2.details : undefined;
35687
35687
  throw new HelipodError(res.status, json2?.code, retry ? `${msg} (retry after ${retry}s)` : String(msg), details);
35688
35688
  }
35689
- return json2 && "data" in json2 ? json2.data : json2;
35689
+ return json2 && "data" in json2 ? json2.data : json2 ?? (text || null);
35690
35690
  }
35691
35691
  }
35692
35692
 
@@ -35730,7 +35730,7 @@ function purchaseKey(dryRun, given) {
35730
35730
 
35731
35731
  // src/tools/containers.ts
35732
35732
  var id = (what) => exports_external.string().uuid().describe(`${what} id (UUID)`);
35733
- var dryRun = exports_external.boolean().optional().describe("true: return the daily cost (IDR) only, change nothing");
35733
+ var dryRun = exports_external.boolean().optional().describe("true: return the cost only (dailyCost list price, walletDailyCost what the wallet pays after the bundle), change nothing");
35734
35734
  var idem = exports_external.string().min(8).max(128).optional().describe("Idempotency-Key: a retry with the same value returns the first result instead of doing it twice");
35735
35735
  var C = "/containers";
35736
35736
  var port = exports_external.object({
@@ -35739,6 +35739,14 @@ var port = exports_external.object({
35739
35739
  public: exports_external.boolean().optional().describe("http ports: give it a *.helipod.app domain (default true)")
35740
35740
  });
35741
35741
  var containerTools = [
35742
+ defineTool({
35743
+ name: "get_bundle",
35744
+ title: "Container bundle",
35745
+ kind: "read",
35746
+ description: "The account's container bundle: whether one is active, until when, quota used / max (vCPU, GB RAM, GB storage), what it covers and what it does not. Container services within the quota cost the wallet NOTHING: read this before telling the user to top up for a container deploy. The bundle covers containers only (not VPS, databases, email, domains or AI).",
35747
+ input: {},
35748
+ run: (_a3, api2) => api2.api("GET", `${C}/bundle`)
35749
+ }),
35742
35750
  defineTool({
35743
35751
  name: "list_projects",
35744
35752
  title: "List projects",
@@ -35884,7 +35892,7 @@ var containerTools = [
35884
35892
  name: "create_service",
35885
35893
  title: "Create service",
35886
35894
  kind: "write",
35887
- description: "Create a service from a container image, a Git repository or an inline Dockerfile, and (unless deploy=false) start its first deploy. Private GitHub / GitLab repos work once connected (get_git_connections, list_git_repos); such services also redeploy on every push. A private repo that is not connected is refused (409 GITHUB_NOT_CONNECTED / GITLAB_NOT_CONNECTED / REPO_NOT_ACCESSIBLE, nothing created): give the user details.connectUrl and retry after they connect. COSTS MONEY once deployed (billed daily from the wallet): call with dryRun=true first, show the dailyCost to the user and get their OK.",
35895
+ description: "Create a service from a container image, a Git repository or an inline Dockerfile, and (unless deploy=false) start its first deploy. Private GitHub / GitLab repos work once connected (get_git_connections, list_git_repos); such services also redeploy on every push. A private repo that is not connected is refused (409 GITHUB_NOT_CONNECTED / GITLAB_NOT_CONNECTED / REPO_NOT_ACCESSIBLE, nothing created): give the user details.connectUrl and retry after they connect. COSTS MONEY once deployed, beyond the account's bundle: billed daily, from the bundle quota first and the wallet only beyond it. Call with dryRun=true first, tell the user what the wallet pays (walletDailyCost) and get their OK. Before saying a price, read walletDailyCost (what the wallet really pays per day) and bundle: with an active bundle that covers it (bundle.coversIt, walletDailyCost 0) nothing is charged and NO top-up is needed, even with a Rp 0 balance.",
35888
35896
  input: {
35889
35897
  environmentId: id("Environment"),
35890
35898
  name: exports_external.string().regex(/^[A-Za-z][A-Za-z0-9_-]{0,39}$/).describe("Service name (letters, digits, _ -; starts with a letter). Also used in references like ${{name.KEY}}"),
@@ -35902,6 +35910,7 @@ var containerTools = [
35902
35910
  ram: exports_external.number().int().min(128).max(8192).optional().describe("MB, multiple of 128"),
35903
35911
  startCommand: exports_external.string().optional(),
35904
35912
  healthcheckPath: exports_external.string().optional().describe("HTTP path answering 2xx when ready, e.g. /health"),
35913
+ healthcheckTimeout: exports_external.number().int().min(1).max(3600).optional().describe("Seconds the first start may take before the health check counts (default from the platform)"),
35905
35914
  deploy: exports_external.boolean().optional().describe("false: create without deploying (nothing is charged)"),
35906
35915
  dryRun,
35907
35916
  idempotencyKey: idem
@@ -35923,7 +35932,7 @@ var containerTools = [
35923
35932
  cpu: a.cpu,
35924
35933
  ram: a.ram,
35925
35934
  startCommand: a.startCommand,
35926
- healthcheck: a.healthcheckPath ? { path: a.healthcheckPath } : undefined,
35935
+ healthcheck: a.healthcheckPath ? { path: a.healthcheckPath, timeout: a.healthcheckTimeout } : undefined,
35927
35936
  deploy: a.deploy
35928
35937
  }
35929
35938
  });
@@ -35966,7 +35975,7 @@ var containerTools = [
35966
35975
  name: "deploy_service",
35967
35976
  title: "Deploy service",
35968
35977
  kind: "write",
35969
- description: "Start a new deployment (Git services build the latest commit). COSTS MONEY: charges today's cost when the build starts, refunded if it fails. dryRun=true returns the cost only. The deployment runs in the background; get_deployment returns its status.",
35978
+ description: "Start a new deployment (Git services build the latest commit). COSTS MONEY beyond the bundle: charges today's cost when the build starts (refunded if it fails), nothing within an active bundle's quota. dryRun=true returns the cost only. Before saying a price, read walletDailyCost (what the wallet really pays per day) and bundle: with an active bundle that covers it (bundle.coversIt, walletDailyCost 0) nothing is charged and NO top-up is needed, even with a Rp 0 balance. The deployment runs in the background; get_deployment returns its status.",
35970
35979
  input: { serviceId: id("Service"), dryRun, idempotencyKey: idem },
35971
35980
  run: (a, api2) => api2.api("POST", `${C}/services/${a.serviceId}/deployments`, { query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
35972
35981
  }),
@@ -36053,6 +36062,14 @@ var containerTools = [
36053
36062
  },
36054
36063
  run: (a, api2) => api2.api("POST", `${C}/services/${a.serviceId}/domains`, { body: { kind: a.kind, domain: a.domain, port: a.port, protocol: a.protocol } })
36055
36064
  }),
36065
+ defineTool({
36066
+ name: "verify_service_domain",
36067
+ title: "Check custom domain DNS",
36068
+ kind: "write",
36069
+ description: "Check a custom domain's DNS now instead of waiting for the automatic check. Answers the domain with its DNS state (verified, or what is still wrong); the certificate follows once the record points to Helipod (the record to add is in list_service_domains).",
36070
+ input: { serviceId: id("Service"), domainId: id("Domain") },
36071
+ run: (a, api2) => api2.api("POST", `${C}/services/${a.serviceId}/domains/${a.domainId}/verify`)
36072
+ }),
36056
36073
  defineTool({
36057
36074
  name: "remove_service_domain",
36058
36075
  title: "Remove domain from service",
@@ -36074,7 +36091,7 @@ var containerTools = [
36074
36091
  title: "Scale service",
36075
36092
  kind: "write",
36076
36093
  idempotent: true,
36077
- description: "Change replicas, CPU or RAM (applies right away). COSTS MONEY on increase: call with dryRun=true first to show currentDailyCost vs newDailyCost and get the user's OK.",
36094
+ description: "Change replicas, CPU or RAM (applies right away). An increase COSTS MONEY unless the bundle covers it: call with dryRun=true first to show currentDailyCost vs newDailyCost and walletDailyCost, and get the user's OK. Before saying a price, read walletDailyCost (what the wallet really pays per day) and bundle: with an active bundle that covers it (bundle.coversIt, walletDailyCost 0) nothing is charged and NO top-up is needed, even with a Rp 0 balance.",
36078
36095
  input: {
36079
36096
  serviceId: id("Service"),
36080
36097
  replicas: exports_external.number().int().min(1).max(20).optional(),
@@ -36122,7 +36139,7 @@ var containerTools = [
36122
36139
  name: "resize_volume",
36123
36140
  title: "Resize volume",
36124
36141
  kind: "write",
36125
- description: "Change a volume's size (1-50 GB; never below the space already used). A larger volume COSTS MONEY: it raises the daily cost, charged pro rata when the service is redeployed and in full from the next renewal. dryRun=true returns currentDailyCost and newDailyCost (IDR) without changing anything. Applies on the next deploy.",
36142
+ description: "Change a volume's size (1-50 GB; never below the space already used). A larger volume COSTS MONEY: it raises the daily cost, charged pro rata when the service is redeployed and in full from the next renewal. dryRun=true returns currentDailyCost, newDailyCost and walletDailyCost (IDR) without changing anything. Applies on the next deploy. Before saying a price, read walletDailyCost (what the wallet really pays per day) and bundle: with an active bundle that covers it (bundle.coversIt, walletDailyCost 0) nothing is charged and NO top-up is needed, even with a Rp 0 balance.",
36126
36143
  input: {
36127
36144
  serviceId: id("Service"),
36128
36145
  volumeId: id("Volume"),
@@ -36244,20 +36261,21 @@ var containerTools = [
36244
36261
  name: "deploy_template",
36245
36262
  title: "Deploy template",
36246
36263
  kind: "write",
36247
- description: "Create every service of a template in an environment and deploy them. COSTS MONEY (each service is billed daily).",
36264
+ description: "Create every service of a template in an environment and deploy them. COSTS MONEY beyond the bundle: each service is billed daily (bundle quota first, then the wallet). Call with dryRun=true first. " + "Before saying a price, read walletDailyCost (what the wallet really pays per day) and bundle: with an active bundle that covers it (bundle.coversIt, walletDailyCost 0) nothing is charged and NO top-up is needed, even with a Rp 0 balance.",
36248
36265
  input: {
36249
36266
  environmentId: id("Environment"),
36250
36267
  slug: exports_external.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/),
36251
36268
  inputs: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Answers to the template's inputs"),
36269
+ dryRun,
36252
36270
  idempotencyKey: idem
36253
36271
  },
36254
- run: (a, api2) => api2.api("POST", `${C}/environments/${a.environmentId}/templates/${a.slug}`, { body: { inputs: a.inputs }, idempotencyKey: a.idempotencyKey })
36272
+ run: (a, api2) => api2.api("POST", `${C}/environments/${a.environmentId}/templates/${a.slug}`, { body: { inputs: a.inputs }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
36255
36273
  }),
36256
36274
  defineTool({
36257
36275
  name: "create_stack",
36258
36276
  title: "Create stack from spec",
36259
36277
  kind: "write",
36260
- description: "Create several connected services from a template spec ({ version: 1, inputs?, services: [...] }) and deploy them in dependency order. COSTS MONEY: dryRun=true first. See https://docs.helipod.io/docs/container-api/templates-and-stacks",
36278
+ description: "Create several connected services from a template spec ({ version: 1, inputs?, services: [...] }) and deploy them in dependency order. COSTS MONEY beyond the bundle: billed daily (bundle quota first, then the wallet). dryRun=true first. Before saying a price, read walletDailyCost (what the wallet really pays per day) and bundle: with an active bundle that covers it (bundle.coversIt, walletDailyCost 0) nothing is charged and NO top-up is needed, even with a Rp 0 balance. See https://docs.helipod.io/docs/container-api/templates-and-stacks",
36261
36279
  input: {
36262
36280
  environmentId: id("Environment"),
36263
36281
  spec: exports_external.record(exports_external.string(), exports_external.any()).describe("Template spec object"),
@@ -36774,16 +36792,21 @@ var domainTools = [
36774
36792
  title: "Check domain availability",
36775
36793
  kind: "read",
36776
36794
  description: "Is a domain name available to register? No key needed.",
36777
- input: { domain: exports_external.string().describe("e.g. tokosaya.id") },
36778
- run: (a, api2) => api2.api("GET", "/domains/search", { query: { domain: a.domain }, auth: false })
36795
+ input: { domain: exports_external.string().describe("e.g. tokosaya.id"), includePremium: exports_external.boolean().optional().describe("Also check names the registry sells as premium (is_premium_name). Premium names cannot be bought online: the user must contact support") },
36796
+ run: (a, api2) => api2.api("GET", "/domains/search", { query: { domain: a.domain, includePremium: a.includePremium ? "true" : undefined }, auth: false })
36779
36797
  }),
36780
36798
  defineTool({
36781
36799
  name: "domain_prices",
36782
36800
  title: "Domain prices",
36783
36801
  kind: "read",
36784
36802
  description: "Registration, renewal and transfer prices per extension (IDR, VAT included) and the yearly WHOIS privacy price.",
36785
- input: { extension: exports_external.string().optional().describe("e.g. .id or .com"), limit: exports_external.number().int().min(1).max(100).optional() },
36786
- run: (a, api2) => api2.api("GET", "/domains/prices", { query: { extension: a.extension, limit: a.limit ?? 50 }, auth: false })
36803
+ input: {
36804
+ extension: exports_external.string().optional().describe("e.g. .id or .com"),
36805
+ promo: exports_external.boolean().optional().describe("true: only extensions on promotion"),
36806
+ limit: exports_external.number().int().min(1).max(100).optional(),
36807
+ page: exports_external.number().int().min(1).optional()
36808
+ },
36809
+ run: (a, api2) => api2.api("GET", "/domains/prices", { query: { extension: a.extension, promo: a.promo === undefined ? undefined : String(a.promo), limit: a.limit ?? 50, page: a.page }, auth: false })
36787
36810
  }),
36788
36811
  defineTool({
36789
36812
  name: "list_my_domains",
@@ -36842,9 +36865,22 @@ var domainTools = [
36842
36865
  name: exports_external.string().describe("Full domain, e.g. tokosaya.id"),
36843
36866
  nameservers: exports_external.array(exports_external.string()).min(2).max(5).optional(),
36844
36867
  whoisProtection: exports_external.boolean().optional(),
36868
+ registrant: exports_external.object({
36869
+ name: exports_external.string().min(3).max(255),
36870
+ email: exports_external.string().max(255).optional().describe("Gets the registry's verification email; defaults to the account email"),
36871
+ organization: exports_external.string().max(255).optional(),
36872
+ phone: exports_external.string().min(9).max(20).describe("e.g. +6281234567890"),
36873
+ addressLine1: exports_external.string().min(3).max(500),
36874
+ addressLine2: exports_external.string().max(500).optional(),
36875
+ city: exports_external.string().min(3).max(100),
36876
+ state: exports_external.string().min(1).max(100),
36877
+ countryCode: exports_external.string().regex(/^[A-Za-z]{2}$/).describe("ISO 3166-1 alpha-2, e.g. ID"),
36878
+ postalCode: exports_external.string().min(1).max(20),
36879
+ fax: exports_external.string().max(50).optional()
36880
+ }).optional().describe("Owner of THIS domain instead of the account's owner details (e.g. a reseller's customer). .id domains only: others answer 400 REGISTRANT_NOT_SUPPORTED. Use only details the user gave."),
36845
36881
  dryRun: dryRun3
36846
36882
  },
36847
- run: (a, api2) => api2.api("POST", "/domains/register", { body: { name: a.name, period: 1, nameservers: a.nameservers, whoisProtection: a.whoisProtection }, query: { dryRun: a.dryRun ? "true" : undefined } })
36883
+ run: (a, api2) => api2.api("POST", "/domains/register", { body: { name: a.name, period: 1, nameservers: a.nameservers, whoisProtection: a.whoisProtection, registrant: a.registrant }, query: { dryRun: a.dryRun ? "true" : undefined } })
36848
36884
  }),
36849
36885
  defineTool({
36850
36886
  name: "transfer_domain",
@@ -36899,9 +36935,9 @@ var domainTools = [
36899
36935
  name: "enable_whois_privacy",
36900
36936
  title: "Enable WHOIS privacy",
36901
36937
  kind: "write",
36902
- description: "Hide the owner's details in WHOIS. COSTS MONEY (the whois_protection price per year).",
36903
- input: { domainId },
36904
- run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/whois-protection`)
36938
+ description: `Hide the owner's details in WHOIS. COSTS MONEY (the whois_protection price per year). ${DRY2}`,
36939
+ input: { domainId, dryRun: dryRun3 },
36940
+ run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/whois-protection`, { query: { dryRun: a.dryRun ? "true" : undefined } })
36905
36941
  }),
36906
36942
  defineTool({
36907
36943
  name: "get_domain_price",
@@ -36944,6 +36980,23 @@ var domainTools = [
36944
36980
  input: { domainId, emailDomainId: exports_external.string().uuid() },
36945
36981
  run: (a, api2) => api2.api("POST", `/domains/${a.domainId}/email-records`, { body: { emailDomainId: a.emailDomainId } })
36946
36982
  }),
36983
+ defineTool({
36984
+ name: "get_child_nameserver",
36985
+ title: "Get child nameserver",
36986
+ kind: "read",
36987
+ description: "One child nameserver (glue record) of a registered domain: hostname and IP address.",
36988
+ input: { domainId, hostId },
36989
+ run: (a, api2) => api2.api("GET", `/domains/${a.domainId}/hosts/${a.hostId}`)
36990
+ }),
36991
+ defineTool({
36992
+ name: "set_domain_auto_renew",
36993
+ title: "Set domain auto-renew",
36994
+ kind: "write",
36995
+ idempotent: true,
36996
+ description: "Turn automatic renewal of a domain on or off (sets the value given; calling it twice is safe). On: renewed from the wallet before it expires. Off: it expires unless renewed by hand (renew_domain). Nothing is charged now.",
36997
+ input: { domainId, autoRenew: exports_external.boolean() },
36998
+ run: (a, api2) => api2.api("PATCH", `/domains/${a.domainId}/auto-renew`, { body: { autoRenew: a.autoRenew } })
36999
+ }),
36947
37000
  defineTool({
36948
37001
  name: "list_child_nameservers",
36949
37002
  title: "List child nameservers",
@@ -37020,7 +37073,10 @@ var message = exports_external.object({
37020
37073
  templateId: exports_external.string().optional(),
37021
37074
  templateData: exports_external.record(exports_external.string(), exports_external.string()).optional(),
37022
37075
  variables: exports_external.record(exports_external.string(), exports_external.string()).optional(),
37023
- idempotencyKey: exports_external.string().max(128).optional()
37076
+ idempotencyKey: exports_external.string().max(128).optional(),
37077
+ headers: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra MIME headers (letters, digits, hyphens in names; no line breaks). Not with templateId"),
37078
+ metadata: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Your own key/value data, stored with the email (get_email returns it)"),
37079
+ sensitive: exports_external.boolean().optional().describe("true when templateData or variables carry a secret (password, one-time code): their values are wiped from the job record after the send")
37024
37080
  });
37025
37081
  var dryRun4 = exports_external.boolean().optional().describe("true = return the price (IDR) and the wallet balance after it; nothing is charged or changed");
37026
37082
  var idem2 = exports_external.string().regex(/^[A-Za-z0-9_.:-]{8,128}$/).optional().describe("Idempotency-Key for the real purchase. Generated when omitted; pass the same value when retrying so it is never charged twice");
@@ -37129,7 +37185,10 @@ var emailTools = [
37129
37185
  templateId: exports_external.string().optional(),
37130
37186
  templateData: exports_external.record(exports_external.string(), exports_external.string()).optional(),
37131
37187
  variables: exports_external.record(exports_external.string(), exports_external.string()).optional(),
37132
- idempotencyKey: exports_external.string().max(128).optional()
37188
+ idempotencyKey: exports_external.string().max(128).optional(),
37189
+ headers: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra MIME headers (letters, digits, hyphens in names; no line breaks). Not with templateId"),
37190
+ metadata: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Your own key/value data, stored with the email (get_email returns it)"),
37191
+ sensitive: exports_external.boolean().optional().describe("true when templateData or variables carry a secret (password, one-time code): their values are wiped from the job record after the send")
37133
37192
  },
37134
37193
  run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/send", { body: a, project })
37135
37194
  }),
@@ -37138,7 +37197,15 @@ var emailTools = [
37138
37197
  title: "List sent emails",
37139
37198
  kind: "read",
37140
37199
  description: "Email log, newest first, with delivery status.",
37141
- input: { projectId, status: exports_external.string().optional(), search: exports_external.string().optional(), limit: exports_external.number().int().min(1).max(100).optional(), cursor: exports_external.string().optional() },
37200
+ input: {
37201
+ projectId,
37202
+ status: exports_external.string().optional(),
37203
+ search: exports_external.string().optional(),
37204
+ from: exports_external.string().optional().describe("Sent at or after (ISO 8601), e.g. 2026-10-01T00:00:00Z"),
37205
+ to: exports_external.string().optional().describe("Sent before (ISO 8601)"),
37206
+ limit: exports_external.number().int().min(1).max(100).optional(),
37207
+ cursor: exports_external.string().optional()
37208
+ },
37142
37209
  run: ({ projectId: project, ...a }, api2) => api2.email("GET", "/emails", { query: a, project })
37143
37210
  }),
37144
37211
  defineTool({
@@ -37186,7 +37253,16 @@ var emailTools = [
37186
37253
  title: "Add contact",
37187
37254
  kind: "write",
37188
37255
  description: "Add or update an audience contact.",
37189
- input: { projectId, email: exports_external.string(), firstName: exports_external.string().optional(), lastName: exports_external.string().optional() },
37256
+ input: {
37257
+ projectId,
37258
+ email: exports_external.string(),
37259
+ firstName: exports_external.string().optional(),
37260
+ lastName: exports_external.string().optional(),
37261
+ status: exports_external.enum(["subscribed", "unsubscribed"]).optional(),
37262
+ properties: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Custom properties (used by segments and {{placeholders}})"),
37263
+ consentConfirmed: exports_external.boolean().optional().describe("true only when the person agreed to receive email (you have their opt-in)"),
37264
+ consentSource: exports_external.string().max(50).optional().describe("Where the consent came from, e.g. signup_form")
37265
+ },
37190
37266
  run: ({ projectId: project, ...a }, api2) => api2.email("POST", "/contacts", { body: a, project })
37191
37267
  }),
37192
37268
  defineTool({
@@ -37665,6 +37741,162 @@ var emailTools = [
37665
37741
  description: "Delete a received message permanently.",
37666
37742
  input: { projectId, messageId: uid("Message") },
37667
37743
  run: (a, api2) => api2.email("DELETE", `/inbound/messages/${a.messageId}`, { project: a.projectId })
37744
+ }),
37745
+ defineTool({
37746
+ name: "add_email_contacts",
37747
+ title: "Add contacts (bulk)",
37748
+ kind: "write",
37749
+ description: "Add or update up to 1000 contacts in one call (same fields as add_contact). For a CSV file use import_email_contacts.",
37750
+ input: {
37751
+ projectId,
37752
+ contacts: exports_external.array(exports_external.object({
37753
+ email: exports_external.string(),
37754
+ firstName: exports_external.string().max(100).optional(),
37755
+ lastName: exports_external.string().max(100).optional(),
37756
+ status: exports_external.enum(["subscribed", "unsubscribed"]).optional(),
37757
+ properties: exports_external.record(exports_external.string(), exports_external.string()).optional()
37758
+ })).min(1).max(1000),
37759
+ consentConfirmed: exports_external.boolean().optional().describe("true only when every one of them opted in"),
37760
+ consentSource: exports_external.string().max(50).optional()
37761
+ },
37762
+ run: ({ projectId: project, ...body }, api2) => api2.email("POST", "/contacts", { body, project })
37763
+ }),
37764
+ defineTool({
37765
+ name: "import_email_contacts",
37766
+ title: "Import contacts from CSV",
37767
+ kind: "write",
37768
+ description: "Import contacts from CSV text (header row with email, optional firstName, lastName and property columns). Runs in the background: the answer has a job id for get_email_job.",
37769
+ input: {
37770
+ projectId,
37771
+ csv: exports_external.string().min(1).max(1e7),
37772
+ consentConfirmed: exports_external.boolean().optional().describe("true only when every contact in the file opted in"),
37773
+ consentSource: exports_external.string().max(50).optional()
37774
+ },
37775
+ run: ({ projectId: project, ...body }, api2) => api2.email("POST", "/contacts/import", { body, project })
37776
+ }),
37777
+ defineTool({
37778
+ name: "export_email_contacts",
37779
+ title: "Export contacts (CSV)",
37780
+ kind: "read",
37781
+ description: "Every contact as CSV text (up to 50,000). Contact data is personal: give it only to the user who asked.",
37782
+ input: { projectId },
37783
+ run: (a, api2) => api2.email("GET", "/contacts/export", { project: a.projectId })
37784
+ }),
37785
+ defineTool({
37786
+ name: "set_email_contact_properties",
37787
+ title: "Set contact properties",
37788
+ kind: "write",
37789
+ idempotent: true,
37790
+ description: "Set custom properties of a contact. Given keys are overwritten, other keys are kept.",
37791
+ input: { projectId, contactId: uid("Contact"), properties: exports_external.record(exports_external.string(), exports_external.string()) },
37792
+ run: (a, api2) => api2.email("PUT", `/contacts/${a.contactId}/properties`, { body: { properties: a.properties }, project: a.projectId })
37793
+ }),
37794
+ defineTool({
37795
+ name: "set_email_contact_topics",
37796
+ title: "Subscribe contact to topics",
37797
+ kind: "write",
37798
+ idempotent: true,
37799
+ description: "Subscribe a contact to the given topics (ids from list_email_topics).",
37800
+ input: { projectId, contactId: uid("Contact"), topicIds: exports_external.array(exports_external.string().uuid()).max(50) },
37801
+ run: (a, api2) => api2.email("PUT", `/contacts/${a.contactId}/topics`, { body: { topicIds: a.topicIds }, project: a.projectId })
37802
+ }),
37803
+ defineTool({
37804
+ name: "get_email_segment",
37805
+ title: "Get segment",
37806
+ kind: "read",
37807
+ description: "One segment with its filter and contact count.",
37808
+ input: { projectId, segmentId: uid("Segment") },
37809
+ run: (a, api2) => api2.email("GET", `/segments/${a.segmentId}`, { project: a.projectId })
37810
+ }),
37811
+ defineTool({
37812
+ name: "recalculate_email_segment",
37813
+ title: "Recalculate segment",
37814
+ kind: "write",
37815
+ description: "Rebuild a segment's member list now (in the background) after contacts or properties changed.",
37816
+ input: { projectId, segmentId: uid("Segment") },
37817
+ run: (a, api2) => api2.email("POST", `/segments/${a.segmentId}/recalculate`, { project: a.projectId })
37818
+ }),
37819
+ defineTool({
37820
+ name: "delete_email_broadcast",
37821
+ title: "Delete broadcast",
37822
+ kind: "destructive",
37823
+ description: "Delete a broadcast that is not sending right now (a draft, scheduled, cancelled or finished one). Use cancel_email_broadcast to stop one that is sending.",
37824
+ input: { projectId, broadcastId: uid("Broadcast") },
37825
+ run: (a, api2) => api2.email("DELETE", `/broadcasts/${a.broadcastId}`, { project: a.projectId })
37826
+ }),
37827
+ defineTool({
37828
+ name: "check_email_inbound",
37829
+ title: "Check domain for receiving",
37830
+ kind: "read",
37831
+ description: "Before turning on receiving: the MX records of the host that would receive mail. existingMx not empty = mail for that host currently goes to another provider and would move to Helipod: suggest a subdomain (e.g. mail.example.com) to keep it.",
37832
+ input: { projectId, domainId: uid("Sending domain"), host: exports_external.string().max(254).optional().describe("The domain or a subdomain of it; default the domain") },
37833
+ run: (a, api2) => api2.email("GET", `/domains/${a.domainId}/inbound/check`, { query: { host: a.host }, project: a.projectId })
37834
+ }),
37835
+ defineTool({
37836
+ name: "set_email_inbound",
37837
+ title: "Turn receiving on or off",
37838
+ kind: "write",
37839
+ idempotent: true,
37840
+ description: "Turn receiving mail on (or off) for a verified sending domain, on the domain or a subdomain. Run check_email_inbound first and tell the user if existing mail would move. The answer is the domain: its dnsRecords now include the receiving MX record to publish, then verify_email_domain (inboundMxValid turns true).",
37841
+ input: { projectId, domainId: uid("Sending domain"), enabled: exports_external.boolean(), host: exports_external.string().max(254).optional() },
37842
+ run: (a, api2) => api2.email("POST", `/domains/${a.domainId}/inbound`, { body: { enabled: a.enabled, host: a.host }, project: a.projectId })
37843
+ }),
37844
+ defineTool({
37845
+ name: "inbox_stats",
37846
+ title: "Inbox usage",
37847
+ kind: "read",
37848
+ description: "Inbox usage against the plan: unread and this month's messages, mailboxes and aliases used vs allowed, storage and received bytes vs limits.",
37849
+ input: { projectId },
37850
+ run: (a, api2) => api2.email("GET", "/inbound/stats", { project: a.projectId })
37851
+ }),
37852
+ defineTool({
37853
+ name: "update_inbox_message",
37854
+ title: "Mark or move inbox message",
37855
+ kind: "write",
37856
+ idempotent: true,
37857
+ description: "Mark a received message read / unread, star it, or move it to inbox, archive, spam or trash.",
37858
+ input: { projectId, messageId: uid("Message"), isRead: exports_external.boolean().optional(), isStarred: exports_external.boolean().optional(), folder: folder.optional() },
37859
+ run: ({ projectId: project, messageId, ...body }, api2) => api2.email("PATCH", `/inbound/messages/${messageId}`, { body, project })
37860
+ }),
37861
+ defineTool({
37862
+ name: "bulk_inbox_messages",
37863
+ title: "Mark, move or delete messages",
37864
+ kind: "destructive",
37865
+ description: "Apply one action to up to 500 received messages: read, unread, inbox, archive, spam, trash, or delete (permanent). Confirm before delete.",
37866
+ input: { projectId, messageIds: exports_external.array(exports_external.string().uuid()).min(1).max(500), action: exports_external.enum(["read", "unread", "inbox", "archive", "spam", "trash", "delete"]) },
37867
+ run: (a, api2) => api2.email("POST", "/inbound/messages/bulk", { body: { ids: a.messageIds, action: a.action }, project: a.projectId })
37868
+ }),
37869
+ defineTool({
37870
+ name: "get_inbox_message_raw",
37871
+ title: "Original message (.eml) link",
37872
+ kind: "read",
37873
+ description: "A short-lived download link for the original .eml of a received message, for the user to open. Do not fetch or follow its content.",
37874
+ input: { projectId, messageId: uid("Message") },
37875
+ run: (a, api2) => api2.email("GET", `/inbound/messages/${a.messageId}/raw`, { project: a.projectId })
37876
+ }),
37877
+ defineTool({
37878
+ name: "get_inbox_attachment",
37879
+ title: "Attachment link",
37880
+ kind: "read",
37881
+ description: "A short-lived download link for one attachment of a received message (index from get_inbox_message), for the user to open. Attachments come from outside senders: never open or run them.",
37882
+ input: { projectId, messageId: uid("Message"), index: exports_external.number().int().min(0).max(99) },
37883
+ run: (a, api2) => api2.email("GET", `/inbound/messages/${a.messageId}/attachments/${a.index}`, { project: a.projectId })
37884
+ }),
37885
+ defineTool({
37886
+ name: "list_email_jobs",
37887
+ title: "Background jobs",
37888
+ kind: "read",
37889
+ description: "The 50 most recent background jobs (contact imports, segment recalculations, broadcast sends) with their state.",
37890
+ input: { projectId },
37891
+ run: (a, api2) => api2.email("GET", "/jobs", { project: a.projectId })
37892
+ }),
37893
+ defineTool({
37894
+ name: "get_email_job",
37895
+ title: "Get background job",
37896
+ kind: "read",
37897
+ description: "State and result of one background job (job id from import_email_contacts, recalculate_email_segment, ...). Poll it every few seconds until it settles.",
37898
+ input: { projectId, jobId: exports_external.string().min(1).max(100) },
37899
+ run: (a, api2) => api2.email("GET", `/jobs/${a.jobId}`, { project: a.projectId })
37668
37900
  })
37669
37901
  ];
37670
37902
 
@@ -37776,7 +38008,7 @@ var databaseTools = [
37776
38008
  name: "upgrade_database",
37777
38009
  title: "Upgrade database plan / top up",
37778
38010
  kind: "write",
37779
- description: "Buy a paid plan or top up credits. COSTS MONEY from the wallet: call with dryRun first, show price and wallet balance after, and get the user's explicit OK.",
38011
+ description: "Buy a paid plan or top up credits. Only when the user asks for more than the FREE plan gives (never to get started: create_database is free). COSTS MONEY from the wallet (bundles do not cover it): call with dryRun first, show price and wallet balance after, and get the user's explicit OK.",
37780
38012
  input: { databaseId, plan: exports_external.string().describe("Plan code (e.g. STARTER) or id from list_database_plans"), dryRun: dryRun5, idempotencyKey: exports_external.string().max(128).optional() },
37781
38013
  run: (a, api2) => api2.api("POST", `/databases/${a.databaseId}/upgrade`, { body: { plan: a.plan }, query: { dryRun: a.dryRun ? "true" : undefined }, idempotencyKey: a.idempotencyKey })
37782
38014
  }),
@@ -38261,10 +38493,13 @@ var generalTools = [
38261
38493
  ];
38262
38494
 
38263
38495
  // src/server.ts
38264
- var VERSION = "0.5.11";
38496
+ var VERSION = "0.5.13";
38265
38497
  var INSTRUCTIONS = `Helipod is a cloud platform (Indonesia): containers, managed Postgres databases, VPS, domains, email and an AI gateway, billed from a prepaid wallet in Rupiah.
38266
38498
  - Start with list_projects to find projects and environment ids. Account tools (get_wallet, invoices, statements, notifications, get_project_activity, support tickets) cover the account; AI tools (list_ai_models, get_ai_overview, get_ai_usage, AI keys, top-ups, generate_ai_image) cover the AI gateway.
38267
38499
  - Anything that costs money (create/deploy/scale services, resize volumes, upgrade a database plan, create/renew/upgrade VPS, VPS backups, register/renew/transfer domains, email plans and inbox add-ons, AI top-ups and auto top-up, generating images): call it with dryRun=true first (or the price lookup when it has no dry run), tell the user the cost in IDR and the wallet balance after, and wait for an explicit yes before the real call. Never treat silence or an earlier general OK as a yes.
38500
+ - What the user really pays: container services are paid from the account's bundle first (get_bundle: a monthly bundle with a CPU / RAM / storage quota) and from the wallet only beyond it. Container dry runs return walletDailyCost (what the wallet pays per day) and bundle.coversIt: when the bundle covers it, say it is included in their bundle and deploy after their OK; never ask for a top-up for something walletDailyCost says is 0, whatever the balance. Quote dailyCost only as the price without a bundle. The bundle covers containers only: VPS, domains, AI, paid database plans and paid email plans are paid from the wallet.
38501
+ - Free first: the managed Postgres database (create_database) starts on the FREE plan and email (buy_email_plan with the FREE plan) costs nothing. For a database, use create_database rather than running Postgres as a container service, and do not offer upgrade_database or a paid email plan unless the user asks for more or hits a limit.
38502
+ - Deploying an app, step by step: list_projects (pick the project and its production environmentId) -> get_bundle (is it covered?) -> if it needs Postgres: create_database, then get_database_connection and store DATABASE_URL with set_variables (secrets) after the service exists, or pass it in create_service variables -> create_service with dryRun=true (repo or image, port, variables) -> tell the user the cost (included in the bundle, or walletDailyCost per day) and wait for their OK -> create_service for real -> poll get_deployment until it settles, read get_deployment_logs if it failed -> give the user the *.helipod.app URL from get_service. Ask the user for secrets (API keys) only when the app needs them; never invent values.
38268
38503
  - Destructive tools (delete, terminate, reinstall, rollback, restore a backup or an import, copy a backup onto another VPS, replace DNS, revoke a key, remove an SSH key or port forward, send a broadcast, execute_database_sql): confirm with the user and name what will be lost. Before execute_database_sql, run classify_database_sql.
38269
38504
  - Email: when a project has no email yet (email_status shows activated false) or its plan ended, start it with buy_email_plan (dryRun first; the FREE plan costs nothing, still confirm). That also sets email up for the project; then add and verify a sending domain.
38270
38505
  - Before replacing all DNS records (set_dns_records), read list_dns_records and keep every record the user still needs. Before sending or scheduling a broadcast, tell the user how many contacts it reaches. Before deleting or replacing code, files or variables, read the current value. After renaming a service, fix the variable references listed in warnings.