@cursor/july 0.1.1 → 0.1.2
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/AGENTS.md +24 -2
- package/README.md +25 -15
- package/dist/bin/agent-serve.js +101 -12
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +31 -14
- package/dist/channels/github/cursor-account.d.ts +43 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -0
- package/dist/channels/github/cursor-account.js +95 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +46 -9
- package/dist/channels/github/index.d.ts +2 -2
- package/dist/channels/github/index.js +2 -2
- package/dist/channels/github/types.d.ts +17 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.d.ts +8 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +8 -0
- package/dist/channels/slack/types.d.ts +24 -3
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/channels/slack/types.js +15 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +8 -8
- package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
- package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
- package/dist/docs/assets/{app.DqfFEmJd.js → app.Oje4vhlk.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.zwQ9RCQ7.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.H5XZ2zCB.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.CTR_TuaE.js} +2 -2
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
- package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
- package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
- package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
- package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
- package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
- package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
- package/dist/docs/assets/quickstart.md.CfU8_uTC.js +192 -0
- package/dist/docs/assets/quickstart.md.CfU8_uTC.lean.js +1 -0
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
- package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
- package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
- package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
- package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
- package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +7 -7
- package/dist/docs/guides/agent-to-agent.html +6 -6
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +14 -7
- package/dist/docs/guides/human-in-the-loop.html +6 -6
- package/dist/docs/guides/slack.html +8 -8
- package/dist/docs/guides/webhooks.html +6 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +5 -5
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +180 -23
- package/dist/docs/reference/agent-config.html +7 -7
- package/dist/docs/reference/channels.html +8 -8
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +5 -5
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/instructions.html +13 -11
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +7 -7
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/subagents.html +6 -6
- package/dist/docs/reference/tools.html +7 -7
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +41 -0
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/internal/cli-deploy.d.ts +52 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -0
- package/dist/internal/cli-deploy.js +731 -0
- package/dist/internal/cursor/backend-client.d.ts +10 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +79 -1
- package/dist/internal/cursor/github-credentials.d.ts +44 -0
- package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
- package/dist/internal/cursor/github-credentials.js +195 -0
- package/dist/internal/deploy-client.d.ts +176 -0
- package/dist/internal/deploy-client.d.ts.map +1 -0
- package/dist/internal/deploy-client.js +375 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +73 -5
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/eval-run-store.d.ts +22 -3
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +37 -19
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +10 -12
- package/dist/internal/host-platforms.d.ts +7 -2
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +15 -10
- package/dist/internal/hosting.d.ts +37 -0
- package/dist/internal/hosting.d.ts.map +1 -0
- package/dist/internal/hosting.js +67 -0
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +50 -6
- package/dist/internal/reminder-store.d.ts +2 -0
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +18 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +173 -42
- package/dist/internal/session-engine.d.ts +49 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +246 -17
- package/dist/internal/storage-coordinator.d.ts +139 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -0
- package/dist/internal/storage-coordinator.js +499 -0
- package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
- package/dist/playground/assets/index-B1Qc9h2u.css +1 -0
- package/dist/playground/assets/{index-FlWjhg3x.js → index-CpDYCj8W.js} +42 -42
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +204 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +153 -0
- package/dist/types.d.ts +44 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -2
- package/docs/guides/github.md +43 -8
- package/docs/guides/slack.md +1 -1
- package/docs/quickstart.md +329 -51
- package/docs/reference/instructions.md +8 -6
- package/docs/scaffolding-agents.md +1 -1
- package/docs/storage.md +98 -0
- package/package.json +8 -1
- package/skills/github/SKILL.md +9 -1
- package/src/bin/agent-serve.ts +139 -0
- package/src/channels/github/api.ts +42 -23
- package/src/channels/github/cursor-account.ts +165 -0
- package/src/channels/github/github-channel.ts +66 -6
- package/src/channels/github/index.ts +2 -2
- package/src/channels/github/types.ts +19 -0
- package/src/channels/slack/slack-channel.ts +17 -3
- package/src/channels/slack/types.ts +44 -3
- package/src/index.ts +13 -0
- package/src/internal/cli-deploy.ts +940 -0
- package/src/internal/cursor/backend-client.ts +103 -1
- package/src/internal/cursor/github-credentials.ts +248 -0
- package/src/internal/deploy-client.ts +591 -0
- package/src/internal/discovery.ts +88 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/eval-run-store.ts +48 -19
- package/src/internal/handleAgentServeTrigger.ts +10 -12
- package/src/internal/host-platforms.ts +28 -11
- package/src/internal/hosting.ts +77 -0
- package/src/internal/reminder-runner.ts +50 -6
- package/src/internal/reminder-store.ts +21 -0
- package/src/internal/server.ts +213 -27
- package/src/internal/session-engine.ts +285 -7
- package/src/internal/storage-coordinator.ts +615 -0
- package/src/storage.ts +325 -0
- package/src/types.ts +40 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
- package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
- package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
- package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
- package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
- package/dist/playground/assets/index-1K-hG-7p.css +0 -1
- /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
- /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
- /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.DF5kwlt0.lean.js → reference_tools.md.CqgJroI0.lean.js} +0 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin client for the Agent Serve deployment management API
|
|
3
|
+
* (`/internal/agent-serve/deployments` on the Cursor backend). Pure
|
|
4
|
+
* request/response + error mapping — no console I/O, no polling loops;
|
|
5
|
+
* rendering lives in `cli-deploy.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Auth is the user API key sent directly as the bearer (the routes are
|
|
8
|
+
* `wrapUserApiRoute` on the backend); there is no token exchange here.
|
|
9
|
+
* The endpoints are gated on the `agent_serve_mvp` Statsig gate and 404
|
|
10
|
+
* when it is off — the error mapping says so instead of "not found".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import {
|
|
15
|
+
MAX_SECRET_VALUE_BYTES,
|
|
16
|
+
MAX_SECRETS,
|
|
17
|
+
validateEgressDomain,
|
|
18
|
+
validateSettableSecretName,
|
|
19
|
+
} from "./hosting.js";
|
|
20
|
+
|
|
21
|
+
export type DeploymentStatus =
|
|
22
|
+
| "pending"
|
|
23
|
+
| "deploying"
|
|
24
|
+
| "running"
|
|
25
|
+
| "degraded"
|
|
26
|
+
| "failed"
|
|
27
|
+
| "stopping"
|
|
28
|
+
| "stopped";
|
|
29
|
+
|
|
30
|
+
export const TERMINAL_DEPLOY_STATUSES: ReadonlySet<DeploymentStatus> = new Set([
|
|
31
|
+
"running",
|
|
32
|
+
"degraded",
|
|
33
|
+
"failed",
|
|
34
|
+
"stopped",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What gets deployed. Today the control plane builds the engine itself from
|
|
39
|
+
* a git ref (or serves the static placeholder when no repo is given), so the
|
|
40
|
+
* only member is the slug/git-ref shape. When artifact-based deploys ship
|
|
41
|
+
* ("build locally + upload"), add an `{ kind: "artifact", ... }` member here
|
|
42
|
+
* and a branch in {@link AgentServeDeployClient.deploy} — the command
|
|
43
|
+
* surface and everything downstream stays unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export type DeploySource = {
|
|
46
|
+
kind: "git-ref";
|
|
47
|
+
/** Omit for the control-plane static engine (`static-http-200`). */
|
|
48
|
+
gitRepoUrl?: string;
|
|
49
|
+
gitRef?: string;
|
|
50
|
+
agentPath?: string;
|
|
51
|
+
/** Repos whose SCM events the deployed engine pulls (`owner/name`). */
|
|
52
|
+
cursorEventRepos?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Egress allowlist for the engine pod. Only valid on repo-backed
|
|
55
|
+
* deploys — the backend 400s when set on a static deploy.
|
|
56
|
+
*/
|
|
57
|
+
egressAllowedDomains?: string[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SLUG_PATTERN = /^[a-z0-9_-]{1,64}$/;
|
|
61
|
+
|
|
62
|
+
/** Returns a human error message, or undefined when the slug is valid. */
|
|
63
|
+
export function validateDeploymentSlug(slug: string): string | undefined {
|
|
64
|
+
if (SLUG_PATTERN.test(slug)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return `Invalid slug ${JSON.stringify(slug)}: use lowercase letters, digits, '-' or '_' (max 64 chars)`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Derive a valid slug from a directory name, or undefined if impossible. */
|
|
71
|
+
export function slugFromDirectoryName(name: string): string | undefined {
|
|
72
|
+
const slug = name
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
75
|
+
.replace(/^-+|-+$/g, "")
|
|
76
|
+
.slice(0, 64);
|
|
77
|
+
return SLUG_PATTERN.test(slug) ? slug : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class DeployApiError extends Error {
|
|
81
|
+
constructor(
|
|
82
|
+
message: string,
|
|
83
|
+
readonly status: number,
|
|
84
|
+
/** Seconds to wait before retrying (from `Retry-After` on 429s). */
|
|
85
|
+
readonly retryAfterSeconds?: number
|
|
86
|
+
) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.name = "DeployApiError";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Response shapes (backend `serializeDeployment` + route-specific bodies).
|
|
94
|
+
// Interfaces are hand-written (the package compiles with
|
|
95
|
+
// --isolatedDeclarations, so exported types cannot be z.infer of an
|
|
96
|
+
// unannotated schema); the zod schemas below stay module-internal.
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
export interface DeployAccepted {
|
|
100
|
+
id: number;
|
|
101
|
+
slug: string;
|
|
102
|
+
teamId: number;
|
|
103
|
+
status: string;
|
|
104
|
+
generation: number;
|
|
105
|
+
deploymentKind: string;
|
|
106
|
+
engineAlias: string;
|
|
107
|
+
/** Present exactly once, on the deploy that minted the record. */
|
|
108
|
+
aliasToken?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface Deployment {
|
|
112
|
+
id: number;
|
|
113
|
+
slug: string;
|
|
114
|
+
teamId: number;
|
|
115
|
+
deploymentKind: string;
|
|
116
|
+
gitRepoUrl?: string | null;
|
|
117
|
+
gitRef?: string;
|
|
118
|
+
agentPath?: string;
|
|
119
|
+
desiredState?: string;
|
|
120
|
+
status: DeploymentStatus;
|
|
121
|
+
generation: number;
|
|
122
|
+
observedGeneration?: number | null;
|
|
123
|
+
engineGeneration?: number | null;
|
|
124
|
+
publicId?: string;
|
|
125
|
+
engineUrl?: string | null;
|
|
126
|
+
lastError?: string | null;
|
|
127
|
+
engineAlias: string;
|
|
128
|
+
desiredUpdatedAt?: string;
|
|
129
|
+
lastReconciledAt?: string | null;
|
|
130
|
+
/** SCM-event repos configured on the deployment (`owner/name`). */
|
|
131
|
+
cursorEventRepos?: string[];
|
|
132
|
+
/** Normalized egress allowlist for the engine pod. */
|
|
133
|
+
egressAllowedDomains?: string[];
|
|
134
|
+
serviceAccountId?: number | null;
|
|
135
|
+
/** Masked pod credential; the raw key is never returned. */
|
|
136
|
+
podCredentialMaskedKey?: string | null;
|
|
137
|
+
/** Names of the secrets set on the deployment (values never returned). */
|
|
138
|
+
secretNames?: string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Short-lived direct pod access minted by the status route. */
|
|
142
|
+
export interface EngineAccess {
|
|
143
|
+
url: string;
|
|
144
|
+
headers: Record<string, string>;
|
|
145
|
+
expiresAt: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface DeploymentDetail extends Deployment {
|
|
149
|
+
engineAccess?: EngineAccess | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface StopAccepted {
|
|
153
|
+
id: number;
|
|
154
|
+
status: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** One secret's metadata from the list endpoint — names only, never values. */
|
|
158
|
+
export interface SecretInfo {
|
|
159
|
+
name: string;
|
|
160
|
+
createdAt: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// All response schemas are `.passthrough()`: the backend keeps growing the
|
|
164
|
+
// deployment payload (serviceAccountId, egressAllowedDomains, secret
|
|
165
|
+
// metadata, ...) and unknown fields must flow through untouched — both so
|
|
166
|
+
// parsing never breaks on a newer backend and so `--json` output stays
|
|
167
|
+
// faithful to the API instead of silently dropping fields.
|
|
168
|
+
const deployAcceptedSchema = z
|
|
169
|
+
.object({
|
|
170
|
+
id: z.number(),
|
|
171
|
+
slug: z.string(),
|
|
172
|
+
teamId: z.number(),
|
|
173
|
+
status: z.string(),
|
|
174
|
+
generation: z.number(),
|
|
175
|
+
deploymentKind: z.string(),
|
|
176
|
+
engineAlias: z.string(),
|
|
177
|
+
aliasToken: z.string().optional(),
|
|
178
|
+
})
|
|
179
|
+
.passthrough();
|
|
180
|
+
|
|
181
|
+
const deploymentSchema = z.object({
|
|
182
|
+
id: z.number(),
|
|
183
|
+
slug: z.string(),
|
|
184
|
+
teamId: z.number(),
|
|
185
|
+
deploymentKind: z.string(),
|
|
186
|
+
gitRepoUrl: z.string().nullable().optional(),
|
|
187
|
+
gitRef: z.string().optional(),
|
|
188
|
+
agentPath: z.string().optional(),
|
|
189
|
+
desiredState: z.string().optional(),
|
|
190
|
+
status: z.enum([
|
|
191
|
+
"pending",
|
|
192
|
+
"deploying",
|
|
193
|
+
"running",
|
|
194
|
+
"degraded",
|
|
195
|
+
"failed",
|
|
196
|
+
"stopping",
|
|
197
|
+
"stopped",
|
|
198
|
+
]),
|
|
199
|
+
generation: z.number(),
|
|
200
|
+
observedGeneration: z.number().nullable().optional(),
|
|
201
|
+
engineGeneration: z.number().nullable().optional(),
|
|
202
|
+
publicId: z.string().optional(),
|
|
203
|
+
engineUrl: z.string().nullable().optional(),
|
|
204
|
+
lastError: z.string().nullable().optional(),
|
|
205
|
+
engineAlias: z.string(),
|
|
206
|
+
desiredUpdatedAt: z.string().optional(),
|
|
207
|
+
lastReconciledAt: z.string().nullable().optional(),
|
|
208
|
+
cursorEventRepos: z.array(z.string()).optional(),
|
|
209
|
+
egressAllowedDomains: z.array(z.string()).optional(),
|
|
210
|
+
// Null on legacy rows that predate service-account-backed deploys.
|
|
211
|
+
serviceAccountId: z.number().nullable().optional(),
|
|
212
|
+
podCredentialMaskedKey: z.string().nullable().optional(),
|
|
213
|
+
secretNames: z.array(z.string()).optional(),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const deploymentListItemSchema = deploymentSchema.passthrough();
|
|
217
|
+
|
|
218
|
+
const deploymentDetailSchema = deploymentSchema
|
|
219
|
+
.extend({
|
|
220
|
+
engineAccess: z
|
|
221
|
+
.object({
|
|
222
|
+
url: z.string(),
|
|
223
|
+
headers: z.record(z.string()),
|
|
224
|
+
expiresAt: z.string(),
|
|
225
|
+
})
|
|
226
|
+
.nullable()
|
|
227
|
+
.optional(),
|
|
228
|
+
})
|
|
229
|
+
.passthrough();
|
|
230
|
+
|
|
231
|
+
const listResponseSchema = z.object({
|
|
232
|
+
deployments: z.array(deploymentListItemSchema),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const stopAcceptedSchema = z
|
|
236
|
+
.object({
|
|
237
|
+
id: z.number(),
|
|
238
|
+
status: z.string(),
|
|
239
|
+
})
|
|
240
|
+
.passthrough();
|
|
241
|
+
|
|
242
|
+
const rotateResponseSchema = z
|
|
243
|
+
.object({
|
|
244
|
+
aliasToken: z.string().min(1),
|
|
245
|
+
})
|
|
246
|
+
.passthrough();
|
|
247
|
+
|
|
248
|
+
const rotatePodCredentialResponseSchema = z
|
|
249
|
+
.object({
|
|
250
|
+
podCredentialMaskedKey: z.string().min(1),
|
|
251
|
+
})
|
|
252
|
+
.passthrough();
|
|
253
|
+
|
|
254
|
+
const setSecretsResponseSchema = z
|
|
255
|
+
.object({
|
|
256
|
+
secretNames: z.array(z.string()),
|
|
257
|
+
})
|
|
258
|
+
.passthrough();
|
|
259
|
+
|
|
260
|
+
const listSecretsResponseSchema = z
|
|
261
|
+
.object({
|
|
262
|
+
secrets: z.array(
|
|
263
|
+
z.object({ name: z.string(), createdAt: z.string() }).passthrough()
|
|
264
|
+
),
|
|
265
|
+
})
|
|
266
|
+
.passthrough();
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Client
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
export interface AgentServeDeployClientOptions {
|
|
273
|
+
backendUrl: string;
|
|
274
|
+
apiKey: string;
|
|
275
|
+
/** Test seam; defaults to global fetch. */
|
|
276
|
+
fetchImpl?: typeof fetch;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export class AgentServeDeployClient {
|
|
280
|
+
private readonly backendUrl: string;
|
|
281
|
+
private readonly apiKey: string;
|
|
282
|
+
private readonly fetchImpl: typeof fetch;
|
|
283
|
+
|
|
284
|
+
constructor(options: AgentServeDeployClientOptions) {
|
|
285
|
+
this.backendUrl = options.backendUrl.endsWith("/")
|
|
286
|
+
? options.backendUrl.slice(0, -1)
|
|
287
|
+
: options.backendUrl;
|
|
288
|
+
this.apiKey = options.apiKey;
|
|
289
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Create or redeploy; 202 means accepted, not running (poll status). */
|
|
293
|
+
async deploy(args: {
|
|
294
|
+
teamId: number;
|
|
295
|
+
slug: string;
|
|
296
|
+
source: DeploySource;
|
|
297
|
+
}): Promise<DeployAccepted> {
|
|
298
|
+
const slugError = validateDeploymentSlug(args.slug);
|
|
299
|
+
if (slugError !== undefined) {
|
|
300
|
+
throw new DeployApiError(slugError, 400);
|
|
301
|
+
}
|
|
302
|
+
for (const domain of args.source.egressAllowedDomains ?? []) {
|
|
303
|
+
const domainError = validateEgressDomain(domain);
|
|
304
|
+
if (domainError !== undefined) {
|
|
305
|
+
throw new DeployApiError(domainError, 400);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Only the git-ref member exists today; a future artifact member would
|
|
309
|
+
// upload the build first and post a different body from this branch.
|
|
310
|
+
const body: Record<string, unknown> = {
|
|
311
|
+
teamId: args.teamId,
|
|
312
|
+
slug: args.slug,
|
|
313
|
+
...(args.source.gitRepoUrl === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { gitRepoUrl: args.source.gitRepoUrl }),
|
|
316
|
+
...(args.source.gitRef === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { gitRef: args.source.gitRef }),
|
|
319
|
+
...(args.source.agentPath === undefined
|
|
320
|
+
? {}
|
|
321
|
+
: { agentPath: args.source.agentPath }),
|
|
322
|
+
...(args.source.cursorEventRepos === undefined
|
|
323
|
+
? {}
|
|
324
|
+
: { cursorEventRepos: args.source.cursorEventRepos }),
|
|
325
|
+
...(args.source.egressAllowedDomains === undefined
|
|
326
|
+
? {}
|
|
327
|
+
: { egressAllowedDomains: args.source.egressAllowedDomains }),
|
|
328
|
+
};
|
|
329
|
+
const raw = await this.request({
|
|
330
|
+
method: "POST",
|
|
331
|
+
path: "/internal/agent-serve/deployments",
|
|
332
|
+
body,
|
|
333
|
+
context: { verb: "deploy", slug: args.slug },
|
|
334
|
+
});
|
|
335
|
+
return deployAcceptedSchema.parse(raw);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async listDeployments(args: { teamId: number }): Promise<Deployment[]> {
|
|
339
|
+
const raw = await this.request({
|
|
340
|
+
method: "GET",
|
|
341
|
+
path: `/internal/agent-serve/deployments?teamId=${args.teamId}`,
|
|
342
|
+
context: { verb: "list deployments" },
|
|
343
|
+
});
|
|
344
|
+
return listResponseSchema.parse(raw).deployments;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async getDeployment(args: {
|
|
348
|
+
teamId: number;
|
|
349
|
+
slug: string;
|
|
350
|
+
}): Promise<DeploymentDetail> {
|
|
351
|
+
const raw = await this.request({
|
|
352
|
+
method: "GET",
|
|
353
|
+
path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}?teamId=${args.teamId}`,
|
|
354
|
+
context: { verb: "get deployment", slug: args.slug },
|
|
355
|
+
});
|
|
356
|
+
return deploymentDetailSchema.parse(raw);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async stopDeployment(args: {
|
|
360
|
+
teamId: number;
|
|
361
|
+
slug: string;
|
|
362
|
+
}): Promise<StopAccepted> {
|
|
363
|
+
const raw = await this.request({
|
|
364
|
+
method: "POST",
|
|
365
|
+
path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}/stop?teamId=${args.teamId}`,
|
|
366
|
+
context: { verb: "stop", slug: args.slug },
|
|
367
|
+
});
|
|
368
|
+
return stopAcceptedSchema.parse(raw);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Returns the new alias token exactly once; the old one dies now. */
|
|
372
|
+
async rotateAliasToken(args: {
|
|
373
|
+
teamId: number;
|
|
374
|
+
slug: string;
|
|
375
|
+
}): Promise<string> {
|
|
376
|
+
const raw = await this.request({
|
|
377
|
+
method: "POST",
|
|
378
|
+
path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}/rotate-alias-token?teamId=${args.teamId}`,
|
|
379
|
+
context: { verb: "rotate alias token", slug: args.slug },
|
|
380
|
+
});
|
|
381
|
+
return rotateResponseSchema.parse(raw).aliasToken;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Upsert secrets by name (unlisted names stay untouched). Returns the
|
|
386
|
+
* post-write union of secret names, sorted. Validates names and value
|
|
387
|
+
* sizes client-side with the backend's grammar before any network call.
|
|
388
|
+
*/
|
|
389
|
+
async setSecrets(args: {
|
|
390
|
+
teamId: number;
|
|
391
|
+
slug: string;
|
|
392
|
+
secrets: Array<{ name: string; value: string }>;
|
|
393
|
+
}): Promise<string[]> {
|
|
394
|
+
for (const { name, value } of args.secrets) {
|
|
395
|
+
const nameError = validateSettableSecretName(name);
|
|
396
|
+
if (nameError !== undefined) {
|
|
397
|
+
throw new DeployApiError(nameError, 400);
|
|
398
|
+
}
|
|
399
|
+
if (Buffer.byteLength(value, "utf8") > MAX_SECRET_VALUE_BYTES) {
|
|
400
|
+
throw new DeployApiError(
|
|
401
|
+
`The value for ${name} is too large (max ${MAX_SECRET_VALUE_BYTES} bytes).`,
|
|
402
|
+
400
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (args.secrets.length > MAX_SECRETS) {
|
|
407
|
+
throw new DeployApiError(
|
|
408
|
+
`Too many secrets in one request (${args.secrets.length}); deployments hold at most ${MAX_SECRETS}.`,
|
|
409
|
+
400
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const raw = await this.request({
|
|
413
|
+
method: "PUT",
|
|
414
|
+
path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}/secrets`,
|
|
415
|
+
body: { teamId: args.teamId, secrets: args.secrets },
|
|
416
|
+
context: { verb: "set secrets", slug: args.slug },
|
|
417
|
+
});
|
|
418
|
+
return setSecretsResponseSchema.parse(raw).secretNames;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Secret names + creation times; values are never returned. */
|
|
422
|
+
async listSecrets(args: {
|
|
423
|
+
teamId: number;
|
|
424
|
+
slug: string;
|
|
425
|
+
}): Promise<SecretInfo[]> {
|
|
426
|
+
const raw = await this.request({
|
|
427
|
+
method: "GET",
|
|
428
|
+
path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}/secrets?teamId=${args.teamId}`,
|
|
429
|
+
context: { verb: "list secrets", slug: args.slug },
|
|
430
|
+
});
|
|
431
|
+
return listSecretsResponseSchema.parse(raw).secrets;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Remove one secret by name; 404 when it is not set. */
|
|
435
|
+
async deleteSecret(args: {
|
|
436
|
+
teamId: number;
|
|
437
|
+
slug: string;
|
|
438
|
+
name: string;
|
|
439
|
+
}): Promise<void> {
|
|
440
|
+
const nameError = validateSettableSecretName(args.name);
|
|
441
|
+
if (nameError !== undefined) {
|
|
442
|
+
throw new DeployApiError(nameError, 400);
|
|
443
|
+
}
|
|
444
|
+
await this.request({
|
|
445
|
+
method: "DELETE",
|
|
446
|
+
path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}/secrets/${encodeURIComponent(args.name)}?teamId=${args.teamId}`,
|
|
447
|
+
context: { verb: "unset secret", slug: args.slug },
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Rotate the deployment's pod credential. Returns the masked new key —
|
|
453
|
+
* the raw key is injected into the pod on the next deploy, never
|
|
454
|
+
* returned to clients.
|
|
455
|
+
*/
|
|
456
|
+
async rotatePodCredential(args: {
|
|
457
|
+
teamId: number;
|
|
458
|
+
slug: string;
|
|
459
|
+
}): Promise<string> {
|
|
460
|
+
const raw = await this.request({
|
|
461
|
+
method: "POST",
|
|
462
|
+
path: `/internal/agent-serve/deployments/${encodeURIComponent(args.slug)}/rotate-pod-credential?teamId=${args.teamId}`,
|
|
463
|
+
context: { verb: "rotate pod credential", slug: args.slug },
|
|
464
|
+
});
|
|
465
|
+
return rotatePodCredentialResponseSchema.parse(raw).podCredentialMaskedKey;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Full URL of the stable engine alias for a deployment. */
|
|
469
|
+
aliasUrl(engineAlias: string): string {
|
|
470
|
+
return `${this.backendUrl}${engineAlias}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async request(args: {
|
|
474
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
475
|
+
path: string;
|
|
476
|
+
body?: unknown;
|
|
477
|
+
context: { verb: string; slug?: string };
|
|
478
|
+
}): Promise<unknown> {
|
|
479
|
+
let response: Response;
|
|
480
|
+
try {
|
|
481
|
+
response = await this.fetchImpl(`${this.backendUrl}${args.path}`, {
|
|
482
|
+
method: args.method,
|
|
483
|
+
headers: {
|
|
484
|
+
accept: "application/json",
|
|
485
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
486
|
+
...(args.body === undefined
|
|
487
|
+
? {}
|
|
488
|
+
: { "content-type": "application/json" }),
|
|
489
|
+
},
|
|
490
|
+
...(args.body === undefined ? {} : { body: JSON.stringify(args.body) }),
|
|
491
|
+
signal: AbortSignal.timeout(60_000),
|
|
492
|
+
});
|
|
493
|
+
} catch (error) {
|
|
494
|
+
throw new DeployApiError(
|
|
495
|
+
`Could not reach the Cursor backend at ${this.backendUrl}: ${
|
|
496
|
+
error instanceof Error ? error.message : String(error)
|
|
497
|
+
}`,
|
|
498
|
+
0
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const text = await response.text();
|
|
502
|
+
if (!response.ok) {
|
|
503
|
+
throw mapErrorResponse(response, text, args.context);
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
return JSON.parse(text) as unknown;
|
|
507
|
+
} catch {
|
|
508
|
+
throw new DeployApiError(
|
|
509
|
+
`The Cursor backend returned a non-JSON response to ${args.context.verb}.`,
|
|
510
|
+
response.status
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ============================================================================
|
|
517
|
+
// Error mapping (HTTP → actionable human messages)
|
|
518
|
+
// ============================================================================
|
|
519
|
+
|
|
520
|
+
function serverErrorText(bodyText: string): string | undefined {
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(bodyText) as { error?: unknown };
|
|
523
|
+
if (typeof parsed.error === "string" && parsed.error !== "") {
|
|
524
|
+
return parsed.error;
|
|
525
|
+
}
|
|
526
|
+
} catch {
|
|
527
|
+
// Non-JSON body — fall through.
|
|
528
|
+
}
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function mapErrorResponse(
|
|
533
|
+
response: Response,
|
|
534
|
+
bodyText: string,
|
|
535
|
+
context: { verb: string; slug?: string }
|
|
536
|
+
): DeployApiError {
|
|
537
|
+
const serverError = serverErrorText(bodyText);
|
|
538
|
+
const slugPart = context.slug === undefined ? "" : ` "${context.slug}"`;
|
|
539
|
+
switch (response.status) {
|
|
540
|
+
case 400:
|
|
541
|
+
return new DeployApiError(
|
|
542
|
+
serverError ?? `The ${context.verb} request was rejected as invalid.`,
|
|
543
|
+
400
|
|
544
|
+
);
|
|
545
|
+
case 401:
|
|
546
|
+
return new DeployApiError(
|
|
547
|
+
"The Cursor API key was rejected. Run `agentkit login` again or update CURSOR_API_KEY.",
|
|
548
|
+
401
|
|
549
|
+
);
|
|
550
|
+
case 403:
|
|
551
|
+
return new DeployApiError(
|
|
552
|
+
`${serverError ?? "Not authorized"} — deploying needs team-admin permission and the cloud-agent entitlement on that team.`,
|
|
553
|
+
403
|
|
554
|
+
);
|
|
555
|
+
case 404:
|
|
556
|
+
return new DeployApiError(
|
|
557
|
+
context.slug === undefined
|
|
558
|
+
? `Not found — the agent_serve_mvp feature gate may not be enabled for your team.`
|
|
559
|
+
: `Deployment${slugPart} was not found — check the slug and --team, or the agent_serve_mvp feature gate may not be enabled for your team.`,
|
|
560
|
+
404
|
|
561
|
+
);
|
|
562
|
+
case 409:
|
|
563
|
+
return new DeployApiError(
|
|
564
|
+
`A concurrent deploy or stop is changing deployment${slugPart}; retry in a moment.`,
|
|
565
|
+
409
|
|
566
|
+
);
|
|
567
|
+
case 429: {
|
|
568
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
569
|
+
const retryAfterSeconds =
|
|
570
|
+
retryAfterHeader !== null && /^\d+$/.test(retryAfterHeader)
|
|
571
|
+
? Number(retryAfterHeader)
|
|
572
|
+
: undefined;
|
|
573
|
+
return new DeployApiError(
|
|
574
|
+
`${serverError ?? "Rate limited"}${
|
|
575
|
+
retryAfterSeconds === undefined
|
|
576
|
+
? ""
|
|
577
|
+
: ` — retry in ${retryAfterSeconds}s`
|
|
578
|
+
}`,
|
|
579
|
+
429,
|
|
580
|
+
retryAfterSeconds
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
default:
|
|
584
|
+
return new DeployApiError(
|
|
585
|
+
`The ${context.verb} request failed (${response.status}): ${
|
|
586
|
+
serverError ?? bodyText.slice(0, 300) ?? "(empty body)"
|
|
587
|
+
}`,
|
|
588
|
+
response.status
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|