@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.
- package/README.md +1 -1
- package/dist/{chunk-7MV2EVXV.js → chunk-475EJ7IK.js} +13 -4
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +101 -7
- package/package.json +1 -1
- package/src/apps/mcp/local-client.ts +21 -0
- package/src/apps/mcp/local-server.ts +38 -2
- package/src/apps/product-cli/cli.ts +39 -0
- package/src/apps/product-cli/command.ts +44 -0
- package/src/apps/product-cli/composition.ts +5 -3
- package/src/apps/product-cli/product-api.ts +33 -0
- package/src/apps/product-cli/repository-init.ts +25 -1
- package/src/apps/product-cli/runtime-contract.ts +41 -6
- package/src/packages/agent-skills/generated-skill-resources.ts +12 -3
- package/src/packages/contracts/credit-pricing.ts +1 -1
- package/src/packages/sdk-ts/generated/index.ts +13 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +97 -3
- package/src/packages/sdk-ts/generated/types.gen.ts +160 -4
package/dist/stdio-main.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
serverIdentityOf,
|
|
27
27
|
setNegotiatedProtocolVersion,
|
|
28
28
|
validateEnvelopeMeta
|
|
29
|
-
} from "./chunk-
|
|
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", "
|
|
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.
|
|
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-
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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 === "
|
|
433
|
-
? "
|
|
434
|
-
:
|
|
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[]> {
|