@amerged/ohmyhost-mcp 0.1.10 → 0.1.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.
@@ -26,7 +26,7 @@ import {
26
26
  serverIdentityOf,
27
27
  setNegotiatedProtocolVersion,
28
28
  validateEnvelopeMeta
29
- } from "./chunk-7MV2EVXV.js";
29
+ } from "./chunk-475EJ7IK.js";
30
30
 
31
31
  // node_modules/.pnpm/@modelcontextprotocol+server@2.0.0/node_modules/@modelcontextprotocol/server/dist/stdio.mjs
32
32
  var StdioServerTransport = class {
@@ -754,7 +754,7 @@ function assertOrganizationAccount(value) {
754
754
  if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "as_of,available_micros,monthly_micros,name,next_expiry,one_time_micros,organization_id,paid_until,plan,plan_source,reserved_micros,workos_organization_id")
755
755
  throw new TypeError("Invalid account fields");
756
756
  const v = value;
757
- if (typeof v["organization_id"] !== "string" || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(v["organization_id"]) || typeof v["name"] !== "string" || v["name"].length > 128 || typeof v["workos_organization_id"] !== "string" || !/^org_[A-Za-z0-9_]+$/u.test(v["workos_organization_id"]) || v["plan"] !== "paid" && v["plan"] !== "free" || !["free", "stripe", "beta", "manual"].includes(String(v["plan_source"])) || v["plan"] === "free" !== (v["plan_source"] === "free"))
757
+ if (typeof v["organization_id"] !== "string" || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(v["organization_id"]) || typeof v["name"] !== "string" || v["name"].length > 128 || typeof v["workos_organization_id"] !== "string" || !/^org_[A-Za-z0-9_]+$/u.test(v["workos_organization_id"]) || v["plan"] !== "paid" && v["plan"] !== "free" || !["free", "stripe", "granted", "manual"].includes(String(v["plan_source"])) || v["plan"] === "free" !== (v["plan_source"] === "free"))
758
758
  throw new TypeError("Invalid account scope");
759
759
  for (const field of ["available_micros", "monthly_micros", "one_time_micros", "reserved_micros"])
760
760
  if (typeof v[field] !== "string" || !/^(0|[1-9][0-9]{0,18})$/u.test(v[field]))
@@ -799,7 +799,7 @@ function exact2(v, keys) {
799
799
  // apps/product-cli/package.json
800
800
  var package_default = {
801
801
  name: "@ohmyhost/product-cli",
802
- version: "0.1.10",
802
+ version: "0.1.13",
803
803
  private: true,
804
804
  ohmyhost: {
805
805
  deployment: "production",
@@ -1661,6 +1661,26 @@ var PROBLEM_POLICY = {
1661
1661
  retryable: true,
1662
1662
  action: "Retry project creation with the same idempotency key."
1663
1663
  },
1664
+ project_handle_taken: {
1665
+ status: 409,
1666
+ retryable: false,
1667
+ action: "Run 'ohmyhost project handle check' for a free address, then rename to one of the alternatives it returns."
1668
+ },
1669
+ project_handle_invalid: {
1670
+ status: 409,
1671
+ retryable: false,
1672
+ action: "Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words."
1673
+ },
1674
+ project_handle_unchanged: {
1675
+ status: 409,
1676
+ retryable: false,
1677
+ action: "The project already answers on this address; no rename is needed."
1678
+ },
1679
+ project_rename_blocked: {
1680
+ status: 409,
1681
+ retryable: true,
1682
+ action: "Wait for the running operation to finish, read the project again, then rename with the new ETag."
1683
+ },
1664
1684
  project_identity_unavailable: {
1665
1685
  status: 503,
1666
1686
  retryable: true,
@@ -1726,6 +1746,11 @@ var PROBLEM_POLICY = {
1726
1746
  retryable: false,
1727
1747
  action: "Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit."
1728
1748
  },
1749
+ repository_configuration_missing: {
1750
+ status: 409,
1751
+ retryable: false,
1752
+ action: "Place ohmyhost.yaml at the Git repository root and set applicationRoot to the application directory (for example website). Commit and push the change, then plan the new commit."
1753
+ },
1729
1754
  migration_filename_noncanonical: {
1730
1755
  status: 409,
1731
1756
  retryable: false,
@@ -1874,7 +1899,7 @@ var parseDevAccessTicket = (value) => {
1874
1899
  ]);
1875
1900
  const origin = input["origin"];
1876
1901
  const redeemUrl = input["redeem_url"];
1877
- if (!isUlid(input["ticket_id"]) || !isUlid(input["project_id"]) || !isUlid(input["environment_id"]) || typeof origin !== "string" || !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$/u.test(origin) || typeof redeemUrl !== "string" || !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(redeemUrl) || new URL(redeemUrl).origin !== origin || !isIsoInstant(input["expires_at"])) {
1902
+ if (!isUlid(input["ticket_id"]) || !isUlid(input["project_id"]) || !isUlid(input["environment_id"]) || typeof origin !== "string" || !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st$/u.test(origin) || typeof redeemUrl !== "string" || !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(redeemUrl) || new URL(redeemUrl).origin !== origin || !isIsoInstant(input["expires_at"])) {
1878
1903
  throw new ResponseContractError();
1879
1904
  }
1880
1905
  return input;
@@ -1935,13 +1960,14 @@ var parseSafeProblem = (value) => {
1935
1960
  if (typeof code !== "string" || !(code in PROBLEM_POLICY))
1936
1961
  throw new ResponseContractError();
1937
1962
  const policy = PROBLEM_POLICY[code];
1938
- if (input["status"] !== policy.status || !boundedString(input["type"], 1, 512) || !isHttpsUrl(input["type"]) || !boundedString(input["title"], 1, 160) || !boundedString(input["request_id"], 1, 128) || typeof input["retryable"] !== "boolean" || !boundedString(input["suggested_action"], 1, 500) || input["detail"] !== void 0 && !boundedString(input["detail"], 1, 2e3) || input["instance"] !== void 0 && !boundedString(input["instance"], 1, 512)) {
1963
+ if (input["status"] !== policy.status || !boundedString(input["type"], 1, 512) || !isHttpsUrl(input["type"]) || !boundedString(input["title"], 1, 160) || !boundedString(input["request_id"], 1, 128) || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(input["request_id"]) || typeof input["retryable"] !== "boolean" || !boundedString(input["suggested_action"], 1, 500) || input["detail"] !== void 0 && !boundedString(input["detail"], 1, 2e3) || input["instance"] !== void 0 && !boundedString(input["instance"], 1, 512)) {
1939
1964
  throw new ResponseContractError();
1940
1965
  }
1941
1966
  const retryAfter = input["retry_after_seconds"];
1942
1967
  if (retryAfter !== void 0 && (code !== "rate_limited" || typeof retryAfter !== "number" || !Number.isSafeInteger(retryAfter) || retryAfter < 1 || retryAfter > 86400))
1943
1968
  throw new ResponseContractError();
1944
1969
  return {
1970
+ requestId: input["request_id"],
1945
1971
  code,
1946
1972
  retryable: policy.retryable,
1947
1973
  suggestedAction: (code === "framework_conversion_required" ? parseFrameworkConversionDetail(input["detail"]) ?? policy.action : policy.action) + (retryAfter === void 0 ? "" : ` Retry after ${String(retryAfter)} seconds.`),
@@ -2117,6 +2143,8 @@ var HELP_USAGE = Object.freeze({
2117
2143
  "project notes set": "ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
2118
2144
  "project status": "ohmyhost project status --project ULID --json",
2119
2145
  "project dev-access create": "ohmyhost project dev-access create --project ULID --json",
2146
+ "project handle check": "ohmyhost project handle check --handle HANDLE --json (is this address free? answers with a reason and free alternatives; the address becomes HANDLE.check.omh.st)",
2147
+ "project handle set": "ohmyhost project handle set --project ULID --handle HANDLE --if-match ETAG --idempotency-key KEY --json (moves the project to a free address; the old one stops working and anyone may claim it)",
2120
2148
  "database compute set": "ohmyhost database compute set --project ULID --environment dev|prod --profile standard|performance --idempotency-key KEY --yes [--wait] --json\n standard: Free 0.25 CU/1 GB/60s idle, Paid 0.5 CU/2 GB/60s idle. performance: Paid 1 CU/4 GB/300s idle, 2.5x database compute credits per equal active minute; longer idle time also consumes credits.",
2121
2149
  "database compute get": "ohmyhost database compute get --project ULID [--environment dev|prod] --json",
2122
2150
  "database write": "ohmyhost database write --project ULID --environment dev|prod --statement-file PATH --idempotency-key KEY [--parameters-json JSON] --yes --json",
@@ -3531,6 +3559,23 @@ var getProjectStatus = (parameters, options) => {
3531
3559
  ...params
3532
3560
  });
3533
3561
  };
3562
+ var getProjectHandleAvailability = (parameters, options) => {
3563
+ const params = buildClientParams([parameters], [
3564
+ {
3565
+ args: [
3566
+ { in: "headers", key: "X-Request-Id" },
3567
+ { in: "path", key: "handle" }
3568
+ ]
3569
+ }
3570
+ ]);
3571
+ return (options?.client ?? client).get({
3572
+ responseStyle: "data",
3573
+ security: [{ scheme: "bearer", type: "http" }],
3574
+ url: "/v1/project-handles/{handle}",
3575
+ ...options,
3576
+ ...params
3577
+ });
3578
+ };
3534
3579
  var createProjectDevAccessTicket = (parameters, options) => {
3535
3580
  const params = buildClientParams([parameters], [
3536
3581
  {
@@ -3647,6 +3692,31 @@ var getCloudflareDnsAuthorizationStatus = (parameters, options) => {
3647
3692
  ...params
3648
3693
  });
3649
3694
  };
3695
+ var changeProjectHandle = (parameters, options) => {
3696
+ const params = buildClientParams([parameters], [
3697
+ {
3698
+ args: [
3699
+ { in: "headers", key: "X-Request-Id" },
3700
+ { in: "headers", key: "Idempotency-Key" },
3701
+ { in: "headers", key: "If-Match" },
3702
+ { in: "path", key: "project_id" },
3703
+ { in: "body", key: "handle" }
3704
+ ]
3705
+ }
3706
+ ]);
3707
+ return (options?.client ?? client).put({
3708
+ responseStyle: "data",
3709
+ security: [{ scheme: "bearer", type: "http" }],
3710
+ url: "/v1/projects/{project_id}/handle",
3711
+ ...options,
3712
+ ...params,
3713
+ headers: {
3714
+ "Content-Type": "application/json",
3715
+ ...options?.headers,
3716
+ ...params.headers
3717
+ }
3718
+ });
3719
+ };
3650
3720
  var planPaidProjectDomain = (parameters, options) => {
3651
3721
  const params = buildClientParams([parameters], [
3652
3722
  {
@@ -4651,6 +4721,17 @@ var GeneratedSdkLocalMcpClient = class {
4651
4721
  const scoped = (await this.#workspace.store.read())?.accessToken;
4652
4722
  return scoped === void 0 ? identity : getCurrentIdentity({}, { client: this.#rescope(scoped) });
4653
4723
  }
4724
+ async changeProjectHandle(input) {
4725
+ return changeProjectHandle({
4726
+ project_id: input.projectId,
4727
+ "Idempotency-Key": input.idempotencyKey,
4728
+ "If-Match": input.ifMatch,
4729
+ handle: input.handle
4730
+ }, { client: this.#client });
4731
+ }
4732
+ async checkProjectHandle(input) {
4733
+ return getProjectHandleAvailability({ handle: input.handle }, { client: this.#client });
4734
+ }
4654
4735
  async planPaidDomain(input) {
4655
4736
  const value = await planPaidProjectDomain({ project_id: input.projectId, paidDomainRequest: { hostname: input.hostname } }, { client: this.#client });
4656
4737
  parsePaidDomainPlan(value);
@@ -5186,7 +5267,7 @@ function createLocalOhmyhostMcpServer(dependencies) {
5186
5267
  month: input.month,
5187
5268
  ...input.cursor ? { cursor: input.cursor } : {}
5188
5269
  }));
5189
- registerProductTool(server, dependencies, "organization_account_get", "Owner-only: read the effective Free/Paid plan, its Stripe/Beta/manual source, available monthly and non-expiring one-time credits, reservations and next expiry. Beta/manual Paid features do not imply a Stripe subscription or extra monthly Paid allowance. This read grants no access or credits.", external_exports.object({ organization_id: identifier2 }), (client2, input) => client2.getOrganizationAccount(input.organization_id));
5270
+ registerProductTool(server, dependencies, "organization_account_get", "Owner-only: read the effective Free/Paid plan, its Stripe or granted source, available monthly and non-expiring one-time credits, reservations and next expiry. Granted Paid features do not imply a Stripe subscription or extra monthly Paid allowance. This read grants no access or credits.", external_exports.object({ organization_id: identifier2 }), (client2, input) => client2.getOrganizationAccount(input.organization_id));
5190
5271
  registerProductTool(server, dependencies, "organization_credits_get", "Read the owner's shared organization credit pool, seven-day grace_started_at/grace_expires_at and published rate_cards. Existing services/domains are not immediately shut down at zero credit; an explicit project stop budget is separate. One credit is 1000000 microcredits. Each rate states credit_micros for units_per_charge; publication alone does not enable billing. active_meters identifies billed sources; provider outages and platform overrun are not customer debt. Works at zero credit.", external_exports.object({ organization_id: identifier2 }), (client2, input) => client2.getOrganizationCredits(input.organization_id));
5191
5272
  registerProductTool(server, dependencies, "project_budget_get", "Read the owner's project UTC-month budget, measured usage and open reservations. A budget never allocates a second wallet.", external_exports.object({ project_id: identifier2 }), (client2, input) => client2.getProjectCreditBudget(input.project_id));
5192
5273
  registerProductTool(server, dependencies, "project_budget_set", "Set an owner's optional monthly project budget in microcredits (1000000 = one credit). continue is the default and uses the org pool after the threshold; stop blocks new billable work. null clears the budget and requires continue. Changing it never resets usage or adds credits. Replay the same key after uncertainty; read the budget again for current usage.", external_exports.object({
@@ -5309,6 +5390,18 @@ function createLocalOhmyhostMcpServer(dependencies) {
5309
5390
  return { revoked: true, key_id: input.key_id };
5310
5391
  }, destructiveAnnotations());
5311
5392
  registerProductTool(server, dependencies, "identity_get", "Get the current ohmyho.st customer/agent identity. This does not authenticate the hosted application's end users or provision a customer AuthKit tenant; application authentication is the customer's chosen integration.", external_exports.object({}), (client2, _input, signal) => client2.getCurrentIdentity(signal));
5393
+ registerProductTool(server, dependencies, "project_handle_check", "Check whether a project address is free before offering it to the customer. A handle is one to five lowercase words of letters and digits joined by single hyphens, three to fifty-nine characters, and never a reserved word such as admin, api, dev or www; it becomes <handle>.check.omh.st for Prod and dev-<handle>.check.omh.st for Dev. The answer names why a handle cannot be used and returns free alternatives, so offer one of those instead of guessing again. Check the name here first, then move an existing project onto it with project_handle_set.", external_exports.object({ handle: external_exports.string().min(1).max(120) }), (client2, input) => client2.checkProjectHandle({ handle: input.handle }));
5394
+ registerProductTool(server, dependencies, "project_handle_set", "Move a project to an address the customer chose, after project_handle_check said it is free. The Dev and Prod gateways are re-published at the new address and it is stored only once both serve it; the previous address stops answering and anyone may claim it, so a link already shared stops working. Needs the project's current ETag from project_status and one idempotency key; reuse the same key after an uncertain answer. Refused while another operation is running for the project, and refused for an address that is taken, unusable, or the one the project already has. Returns an operation to follow with operation_get.", external_exports.object({
5395
+ project_id: identifier2,
5396
+ handle: external_exports.string().min(3).max(59),
5397
+ if_match: external_exports.string().min(3).max(128),
5398
+ idempotency_key: idempotencyKey
5399
+ }), (client2, input) => client2.changeProjectHandle({
5400
+ projectId: input.project_id,
5401
+ handle: input.handle,
5402
+ ifMatch: input.if_match,
5403
+ idempotencyKey: input.idempotency_key
5404
+ }));
5312
5405
  registerProductTool(server, dependencies, "projects_list", "List projects visible to the current identity", external_exports.object({
5313
5406
  cursor: identifier2.optional(),
5314
5407
  limit: external_exports.number().int().min(1).max(100).optional()
@@ -5411,7 +5504,7 @@ function createLocalOhmyhostMcpServer(dependencies) {
5411
5504
  operationId: input.operation_id,
5412
5505
  idempotencyKey: input.idempotency_key
5413
5506
  })), mutationAnnotations());
5414
- registerProductTool(server, dependencies, "mail_domain_set", "Configure the canonical transactional-mail sender domain. Requires effective Paid access. Read organization_account_get first: Stripe, Beta and manual grants all qualify; credit balance alone is not feature access. Only suggest a Paid Checkout when the account is Free and the owner wants to purchase. Reading existing configuration remains available.", external_exports.object({
5507
+ registerProductTool(server, dependencies, "mail_domain_set", "Configure the canonical transactional-mail sender domain. Requires effective Paid access. Read organization_account_get first: a Stripe subscription and a granted entitlement both qualify; credit balance alone is not feature access. Only suggest a Paid Checkout when the account is Free and the owner wants to purchase. Reading existing configuration remains available.", external_exports.object({
5415
5508
  project_id: identifier2,
5416
5509
  domain: domainName,
5417
5510
  idempotency_key: idempotencyKey
@@ -5537,6 +5630,7 @@ function registerProductTool(server, dependencies, name, description, inputSchem
5537
5630
  code: safe.code,
5538
5631
  retryable: safe.retryable,
5539
5632
  suggested_action: safe.suggestedAction,
5633
+ ...safe.requestId === void 0 ? {} : { request_id: safe.requestId },
5540
5634
  ...safe.retryAfterSeconds === void 0 ? {} : { retry_after_seconds: safe.retryAfterSeconds }
5541
5635
  }
5542
5636
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-mcp",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -59,6 +59,8 @@ import {
59
59
  deleteProject,
60
60
  getCurrentIdentity,
61
61
  getDeployment,
62
+ changeProjectHandle,
63
+ getProjectHandleAvailability,
62
64
  getOperation,
63
65
  getProjectMailDomain,
64
66
  getProject,
@@ -282,6 +284,25 @@ export class GeneratedSdkLocalMcpClient implements LocalMcpProductClient {
282
284
  ? identity
283
285
  : getCurrentIdentity({}, { client: this.#rescope(scoped) });
284
286
  }
287
+ public async changeProjectHandle(input: {
288
+ projectId: string;
289
+ handle: string;
290
+ ifMatch: string;
291
+ idempotencyKey: string;
292
+ }) {
293
+ return changeProjectHandle(
294
+ {
295
+ project_id: input.projectId,
296
+ "Idempotency-Key": input.idempotencyKey,
297
+ "If-Match": input.ifMatch,
298
+ handle: input.handle,
299
+ },
300
+ { client: this.#client },
301
+ );
302
+ }
303
+ public async checkProjectHandle(input: { handle: string }) {
304
+ return getProjectHandleAvailability({ handle: input.handle }, { client: this.#client });
305
+ }
285
306
  public async planPaidDomain(input: { projectId: string; hostname: string }) {
286
307
  const value = await planPaidProjectDomain(
287
308
  { project_id: input.projectId, paidDomainRequest: { hostname: input.hostname } },
@@ -39,6 +39,13 @@ import type {
39
39
  } from "@ohmyhost/sdk-ts";
40
40
 
41
41
  export interface LocalMcpProductClient {
42
+ checkProjectHandle(input: { handle: string }): Promise<unknown>;
43
+ changeProjectHandle(input: {
44
+ projectId: string;
45
+ handle: string;
46
+ ifMatch: string;
47
+ idempotencyKey: string;
48
+ }): Promise<unknown>;
42
49
  authorizeCloudflareDns(input: {
43
50
  projectId: string;
44
51
  zone: string;
@@ -633,7 +640,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
633
640
  server,
634
641
  dependencies,
635
642
  "organization_account_get",
636
- "Owner-only: read the effective Free/Paid plan, its Stripe/Beta/manual source, available monthly and non-expiring one-time credits, reservations and next expiry. Beta/manual Paid features do not imply a Stripe subscription or extra monthly Paid allowance. This read grants no access or credits.",
643
+ "Owner-only: read the effective Free/Paid plan, its Stripe or granted source, available monthly and non-expiring one-time credits, reservations and next expiry. Granted Paid features do not imply a Stripe subscription or extra monthly Paid allowance. This read grants no access or credits.",
637
644
  z.object({ organization_id: identifier }),
638
645
  (client, input) => client.getOrganizationAccount(input.organization_id),
639
646
  );
@@ -924,6 +931,33 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
924
931
  z.object({}),
925
932
  (client, _input, signal) => client.getCurrentIdentity(signal),
926
933
  );
934
+ registerProductTool(
935
+ server,
936
+ dependencies,
937
+ "project_handle_check",
938
+ "Check whether a project address is free before offering it to the customer. A handle is one to five lowercase words of letters and digits joined by single hyphens, three to fifty-nine characters, and never a reserved word such as admin, api, dev or www; it becomes <handle>.check.omh.st for Prod and dev-<handle>.check.omh.st for Dev. The answer names why a handle cannot be used and returns free alternatives, so offer one of those instead of guessing again. Check the name here first, then move an existing project onto it with project_handle_set.",
939
+ z.object({ handle: z.string().min(1).max(120) }),
940
+ (client, input) => client.checkProjectHandle({ handle: input.handle }),
941
+ );
942
+ registerProductTool(
943
+ server,
944
+ dependencies,
945
+ "project_handle_set",
946
+ "Move a project to an address the customer chose, after project_handle_check said it is free. The Dev and Prod gateways are re-published at the new address and it is stored only once both serve it; the previous address stops answering and anyone may claim it, so a link already shared stops working. Needs the project's current ETag from project_status and one idempotency key; reuse the same key after an uncertain answer. Refused while another operation is running for the project, and refused for an address that is taken, unusable, or the one the project already has. Returns an operation to follow with operation_get.",
947
+ z.object({
948
+ project_id: identifier,
949
+ handle: z.string().min(3).max(59),
950
+ if_match: z.string().min(3).max(128),
951
+ idempotency_key: idempotencyKey,
952
+ }),
953
+ (client, input) =>
954
+ client.changeProjectHandle({
955
+ projectId: input.project_id,
956
+ handle: input.handle,
957
+ ifMatch: input.if_match,
958
+ idempotencyKey: input.idempotency_key,
959
+ }),
960
+ );
927
961
  registerProductTool(
928
962
  server,
929
963
  dependencies,
@@ -1192,7 +1226,7 @@ export function createLocalOhmyhostMcpServer(dependencies: LocalOhmyhostMcpDepen
1192
1226
  server,
1193
1227
  dependencies,
1194
1228
  "mail_domain_set",
1195
- "Configure the canonical transactional-mail sender domain. Requires effective Paid access. Read organization_account_get first: Stripe, Beta and manual grants all qualify; credit balance alone is not feature access. Only suggest a Paid Checkout when the account is Free and the owner wants to purchase. Reading existing configuration remains available.",
1229
+ "Configure the canonical transactional-mail sender domain. Requires effective Paid access. Read organization_account_get first: a Stripe subscription and a granted entitlement both qualify; credit balance alone is not feature access. Only suggest a Paid Checkout when the account is Free and the owner wants to purchase. Reading existing configuration remains available.",
1196
1230
  z.object({
1197
1231
  project_id: identifier,
1198
1232
  domain: domainName,
@@ -1391,6 +1425,7 @@ function registerProductTool<Schema extends z.ZodObject<z.ZodRawShape>, Result>(
1391
1425
  retryable: boolean;
1392
1426
  suggestedAction: string;
1393
1427
  retryAfterSeconds?: number;
1428
+ requestId?: string;
1394
1429
  };
1395
1430
  try {
1396
1431
  safe =
@@ -1437,6 +1472,7 @@ function registerProductTool<Schema extends z.ZodObject<z.ZodRawShape>, Result>(
1437
1472
  code: safe.code,
1438
1473
  retryable: safe.retryable,
1439
1474
  suggested_action: safe.suggestedAction,
1475
+ ...(safe.requestId === undefined ? {} : { request_id: safe.requestId }),
1440
1476
  ...(safe.retryAfterSeconds === undefined
1441
1477
  ? {}
1442
1478
  : { retry_after_seconds: safe.retryAfterSeconds }),
@@ -254,6 +254,7 @@ interface CliErrorDocument {
254
254
  readonly suggested_action: string;
255
255
  readonly message?: string;
256
256
  readonly retry_after_seconds?: number;
257
+ readonly request_id?: string;
257
258
  };
258
259
  }
259
260
 
@@ -310,6 +311,10 @@ const HELP_USAGE = Object.freeze({
310
311
  "ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
311
312
  "project status": "ohmyhost project status --project ULID --json",
312
313
  "project dev-access create": "ohmyhost project dev-access create --project ULID --json",
314
+ "project handle check":
315
+ "ohmyhost project handle check --handle HANDLE --json (is this address free? answers with a reason and free alternatives; the address becomes HANDLE.check.omh.st)",
316
+ "project handle set":
317
+ "ohmyhost project handle set --project ULID --handle HANDLE --if-match ETAG --idempotency-key KEY --json (moves the project to a free address; the old one stops working and anyone may claim it)",
313
318
  "database compute set":
314
319
  "ohmyhost database compute set --project ULID --environment dev|prod --profile standard|performance --idempotency-key KEY --yes [--wait] --json\n" +
315
320
  " standard: Free 0.25 CU/1 GB/60s idle, Paid 0.5 CU/2 GB/60s idle. performance: Paid 1 CU/4 GB/300s idle, 2.5x database compute credits per equal active minute; longer idle time also consumes credits.",
@@ -687,6 +692,10 @@ export class ProductCli {
687
692
  }
688
693
  if (command.kind === "project-create") return await this.createProject(command, signal);
689
694
  if (command.kind === "project-list") return await this.listProjects(command, signal);
695
+ if (command.kind === "project-handle-check")
696
+ return await this.checkProjectHandle(command, signal);
697
+ if (command.kind === "project-handle-set")
698
+ return await this.changeProjectHandle(command, signal);
690
699
  if (command.kind === "database-compute-set") {
691
700
  const api = await this.authenticatedApi(command.credentialStore, signal);
692
701
  let operation = parseOperation(await api.changeDatabaseCompute(command));
@@ -1165,6 +1174,31 @@ export class ProductCli {
1165
1174
  return success("project list", { status: "succeeded", projects });
1166
1175
  }
1167
1176
 
1177
+ private async changeProjectHandle(
1178
+ command: Extract<ProductCliCommand, { kind: "project-handle-set" }>,
1179
+ signal: AbortSignal,
1180
+ ): Promise<CliResult> {
1181
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1182
+ const operation = parseOperation(
1183
+ await api.changeProjectHandle({
1184
+ projectId: command.projectId,
1185
+ handle: command.handle,
1186
+ ifMatch: command.ifMatch,
1187
+ idempotencyKey: command.idempotencyKey,
1188
+ }),
1189
+ );
1190
+ return success("project handle set", { status: "accepted", operation });
1191
+ }
1192
+
1193
+ private async checkProjectHandle(
1194
+ command: Extract<ProductCliCommand, { kind: "project-handle-check" }>,
1195
+ signal: AbortSignal,
1196
+ ): Promise<CliResult> {
1197
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1198
+ const handle = await api.checkProjectHandle({ handle: command.handle });
1199
+ return success("project handle check", { status: "succeeded", handle });
1200
+ }
1201
+
1168
1202
  private async getProjectStatus(
1169
1203
  command: Extract<ProductCliCommand, { kind: "project-status" }>,
1170
1204
  signal: AbortSignal,
@@ -2017,6 +2051,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
2017
2051
  problem.suggestedAction,
2018
2052
  undefined,
2019
2053
  problem.retryAfterSeconds,
2054
+ problem.requestId,
2020
2055
  );
2021
2056
  } catch {
2022
2057
  return failure(
@@ -2066,6 +2101,7 @@ const failure = (
2066
2101
  suggestedAction: string,
2067
2102
  operation?: ReturnType<typeof parseOperation>,
2068
2103
  retryAfterSeconds?: number,
2104
+ requestId?: string,
2069
2105
  ): CliResult => {
2070
2106
  const document: CliErrorDocument = {
2071
2107
  version: 1,
@@ -2077,6 +2113,7 @@ const failure = (
2077
2113
  retryable,
2078
2114
  suggested_action: suggestedAction,
2079
2115
  ...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
2116
+ ...(requestId === undefined ? {} : { request_id: requestId }),
2080
2117
  ...(operation?.error === undefined ? {} : { message: operation.error.message }),
2081
2118
  },
2082
2119
  };
@@ -2115,6 +2152,8 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2115
2152
  if (command.kind === "project-notes-set") return "project notes set";
2116
2153
  if (command.kind === "project-status") return "project status";
2117
2154
  if (command.kind === "project-dev-access-create") return "project dev-access create";
2155
+ if (command.kind === "project-handle-check") return "project handle check";
2156
+ if (command.kind === "project-handle-set") return "project handle set";
2118
2157
  if (command.kind === "database-write") return "database write";
2119
2158
  if (command.kind === "database-query") return "database query";
2120
2159
  if (command.kind === "database-access-create") return "database access create";
@@ -132,6 +132,21 @@ export type ProductCliCommand =
132
132
  readonly idempotencyKey: string;
133
133
  readonly credentialStore: "native";
134
134
  }
135
+ | {
136
+ /** Moves a project to a chosen address once its gateways serve it. */
137
+ readonly kind: "project-handle-set";
138
+ readonly projectId: string;
139
+ readonly handle: string;
140
+ readonly ifMatch: string;
141
+ readonly idempotencyKey: string;
142
+ readonly credentialStore: "native";
143
+ }
144
+ | {
145
+ /** Asks whether a project address is free; the candidate is sent exactly as the agent typed it. */
146
+ readonly kind: "project-handle-check";
147
+ readonly handle: string;
148
+ readonly credentialStore: "native";
149
+ }
135
150
  | { readonly kind: "help"; readonly topic?: string }
136
151
  | { readonly kind: "version" }
137
152
  | { readonly kind: "whoami"; readonly credentialStore: "native" }
@@ -487,6 +502,8 @@ export type RecognizedCommandLabel =
487
502
  | "project list"
488
503
  | "project status"
489
504
  | "project dev-access create"
505
+ | "project handle check"
506
+ | "project handle set"
490
507
  | "database write"
491
508
  | "database query"
492
509
  | "database access create"
@@ -576,6 +593,10 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
576
593
  if (value === "project" && values[0] === "notes" && values[1] === "set")
577
594
  return "project notes set";
578
595
  if (value === "project" && values[0] === "status") return "project status";
596
+ if (value === "project" && values[0] === "handle" && values[1] === "check")
597
+ return "project handle check";
598
+ if (value === "project" && values[0] === "handle" && values[1] === "set")
599
+ return "project handle set";
579
600
  if (value === "project" && values[0] === "dev-access" && values[1] === "create")
580
601
  return "project dev-access create";
581
602
  if (value === "database" && values[0] === "compute" && values[1] === "set")
@@ -1045,6 +1066,29 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1045
1066
  }
1046
1067
  if (first === "project") {
1047
1068
  const subcommand = values.shift();
1069
+ if (subcommand === "handle" && values[0] === "set") {
1070
+ values.shift();
1071
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1072
+ const options = parseOptions(values, [
1073
+ "--project",
1074
+ "--handle",
1075
+ "--if-match",
1076
+ "--idempotency-key",
1077
+ ]);
1078
+ return {
1079
+ kind: "project-handle-set",
1080
+ projectId: options["--project"],
1081
+ handle: options["--handle"],
1082
+ ifMatch: options["--if-match"],
1083
+ idempotencyKey: options["--idempotency-key"],
1084
+ credentialStore,
1085
+ };
1086
+ }
1087
+ if (subcommand === "handle" && values.shift() === "check") {
1088
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1089
+ const options = parseOptions(values, ["--handle"]);
1090
+ return { kind: "project-handle-check", handle: options["--handle"], credentialStore };
1091
+ }
1048
1092
  if (subcommand === "dev-access" && values.shift() === "create") {
1049
1093
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1050
1094
  const options = parseOptions(values, ["--project"]);
@@ -429,9 +429,11 @@ const runOfflineInit = async (
429
429
  false,
430
430
  error instanceof RepositoryInitConflictError
431
431
  ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying."
432
- : error instanceof RepositoryInitError && error.code === "project_name_invalid"
433
- ? "Pass a lowercase deployment slug with '--project', then rerun init."
434
- : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
432
+ : error instanceof RepositoryInitError && error.code === "repository_root_required"
433
+ ? "Run ohmyhost init from the Git repository root with --root set to the application directory (for example --root website); commit the root ohmyhost.yaml before planning."
434
+ : error instanceof RepositoryInitError && error.code === "project_name_invalid"
435
+ ? "Pass a lowercase deployment slug with '--project', then rerun init."
436
+ : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
435
437
  );
436
438
  io.writeStdout(result.stdout);
437
439
  return result.exitCode;
@@ -83,6 +83,8 @@ import {
83
83
  listEnvironmentSecrets,
84
84
  listFunctionRuns,
85
85
  listProjects,
86
+ changeProjectHandle,
87
+ getProjectHandleAvailability,
86
88
  planDeployment,
87
89
  planPaidProjectDomain,
88
90
  planDeploymentRollback,
@@ -115,6 +117,7 @@ import {
115
117
  type CurrentIdentity,
116
118
  type DevAccessTicket,
117
119
  type ProjectPage,
120
+ type ProjectHandleAvailability,
118
121
  type ProjectStatus,
119
122
  type PaidDomain,
120
123
  type PaidDomainPlan,
@@ -208,6 +211,13 @@ export interface ProductApi {
208
211
  /** The signed-in user's workspaces; an interactive session is required for this route. */
209
212
  getAccountProfile(): Promise<readonly PublicWorkspace[]>;
210
213
  listProjects(input: { readonly cursor?: string; readonly limit: number }): Promise<ProjectPage>;
214
+ checkProjectHandle(input: { readonly handle: string }): Promise<ProjectHandleAvailability>;
215
+ changeProjectHandle(input: {
216
+ readonly projectId: string;
217
+ readonly handle: string;
218
+ readonly ifMatch: string;
219
+ readonly idempotencyKey: string;
220
+ }): Promise<Operation>;
211
221
  getProjectStatus(projectId: string): Promise<ProjectStatus>;
212
222
  writeProjectDatabase(
213
223
  input: ProjectDatabaseWriteRequest & { projectId: string; idempotencyKey: string },
@@ -582,6 +592,29 @@ export class GeneratedSdkProductApi implements ProductApi {
582
592
  return listProjects(input, { client: this.#client });
583
593
  }
584
594
 
595
+ public async changeProjectHandle(input: {
596
+ readonly projectId: string;
597
+ readonly handle: string;
598
+ readonly ifMatch: string;
599
+ readonly idempotencyKey: string;
600
+ }): Promise<Operation> {
601
+ return changeProjectHandle(
602
+ {
603
+ project_id: input.projectId,
604
+ "Idempotency-Key": input.idempotencyKey,
605
+ "If-Match": input.ifMatch,
606
+ handle: input.handle,
607
+ },
608
+ { client: this.#client },
609
+ );
610
+ }
611
+
612
+ public async checkProjectHandle(input: {
613
+ readonly handle: string;
614
+ }): Promise<ProjectHandleAvailability> {
615
+ return getProjectHandleAvailability({ handle: input.handle }, { client: this.#client });
616
+ }
617
+
585
618
  public async getProjectStatus(projectId: string): Promise<ProjectStatus> {
586
619
  return getProjectStatus({ project_id: projectId }, { client: this.#client });
587
620
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, open, readFile, readdir, stat } from "node:fs/promises";
3
- import { posix, resolve } from "node:path";
3
+ import { dirname, posix, resolve } from "node:path";
4
4
 
5
5
  import {
6
6
  normalizeApplicationRoot,
@@ -876,6 +876,30 @@ async function assertRepositoryRoot(root: string): Promise<void> {
876
876
  if (!information.isDirectory()) {
877
877
  throw new RepositoryInitError("repository_not_directory", "Repository path is not a directory");
878
878
  }
879
+ // A Git worktree uses a .git file; never read or follow that file's target.
880
+ for (let ancestor = root; ; ancestor = dirname(ancestor)) {
881
+ let marker;
882
+ try {
883
+ marker = await lstat(resolve(ancestor, ".git"));
884
+ } catch (error) {
885
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
886
+ throw new RepositoryInitError(
887
+ "repository_unreadable",
888
+ "Could not determine the Git repository root",
889
+ );
890
+ }
891
+ }
892
+ if (marker !== undefined) {
893
+ if (ancestor !== root) {
894
+ throw new RepositoryInitError(
895
+ "repository_root_required",
896
+ "Run ohmyhost init from the Git repository root and select the application directory with --root; ohmyhost.yaml must be at the repository root.",
897
+ );
898
+ }
899
+ return;
900
+ }
901
+ if (dirname(ancestor) === ancestor) return;
902
+ }
879
903
  }
880
904
 
881
905
  async function collectRepositoryFiles(root: string): Promise<string[]> {