@cursor/july 0.1.73 → 0.1.75
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 +13 -8
- package/dist/bin/agent-serve.js +7 -5
- package/dist/channels/checks.d.ts +111 -0
- package/dist/channels/checks.d.ts.map +1 -0
- package/dist/channels/checks.js +86 -0
- package/dist/channels/deployments/deployments-channel.d.ts +44 -0
- package/dist/channels/deployments/deployments-channel.d.ts.map +1 -0
- package/dist/channels/deployments/deployments-channel.js +92 -0
- package/dist/channels/deployments/index.d.ts +26 -0
- package/dist/channels/deployments/index.d.ts.map +1 -0
- package/dist/channels/deployments/index.js +25 -0
- package/dist/channels/deployments/types.d.ts +69 -0
- package/dist/channels/deployments/types.d.ts.map +1 -0
- package/dist/channels/deployments/types.js +6 -0
- package/dist/channels/github/checks.d.ts +13 -48
- package/dist/channels/github/checks.d.ts.map +1 -1
- package/dist/channels/github/checks.js +56 -64
- package/dist/channels/origin/api.d.ts +100 -0
- package/dist/channels/origin/api.d.ts.map +1 -0
- package/dist/channels/origin/api.js +225 -0
- package/dist/channels/origin/binding.d.ts +44 -0
- package/dist/channels/origin/binding.d.ts.map +1 -0
- package/dist/channels/origin/binding.js +45 -0
- package/dist/channels/origin/checks.d.ts +107 -0
- package/dist/channels/origin/checks.d.ts.map +1 -0
- package/dist/channels/origin/checks.js +145 -0
- package/dist/channels/origin/index.d.ts +3 -0
- package/dist/channels/origin/index.d.ts.map +1 -1
- package/dist/channels/origin/index.js +3 -0
- package/dist/channels/origin/origin-channel.d.ts +4 -4
- package/dist/channels/origin/origin-channel.d.ts.map +1 -1
- package/dist/channels/origin/origin-channel.js +12 -2
- package/dist/channels/origin/types.d.ts +36 -0
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels.d.ts +1 -0
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +1 -0
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.CdbcFD8T.js → app.b8bhB2SV.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.D_NKImM7.js → VPLocalSearchBox.lLSeIyWw.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CMvz7cu9.js → theme.C_owesa-.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.MN8iDy8V.js → reference_channels.md.G5GysZ7e.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.MN8iDy8V.lean.js → reference_channels.md.G5GysZ7e.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.Cl14Jfo3.js → reference_cli.md.TAaYU8br.js} +3 -3
- package/dist/docs/assets/{reference_cli.md.Cl14Jfo3.lean.js → reference_cli.md.TAaYU8br.lean.js} +1 -1
- package/dist/docs/assets/scaffolding-agents.md.CiGsJ1aw.js +1 -0
- package/dist/docs/assets/{scaffolding-agents.md.CrWSA0Ko.lean.js → scaffolding-agents.md.CiGsJ1aw.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +5 -5
- package/dist/docs/reference/cli.html +6 -6
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cli-skills.d.ts +4 -2
- package/dist/internal/cli-skills.d.ts.map +1 -1
- package/dist/internal/cli-skills.js +8 -3
- package/dist/internal/cursor-deployment-relay.d.ts +77 -0
- package/dist/internal/cursor-deployment-relay.d.ts.map +1 -0
- package/dist/internal/cursor-deployment-relay.js +201 -0
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/install-cursor-skills.d.ts +71 -16
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +160 -48
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +6 -2
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +114 -5
- package/dist/playground/assets/index-BJy9UJrF.js +85 -0
- package/dist/playground/assets/index-BrSd5vff.css +1 -0
- package/dist/playground/index.html +2 -2
- package/docs/reference/channels.md +15 -0
- package/docs/reference/cli.md +16 -8
- package/docs/scaffolding-agents.md +14 -6
- package/package.json +9 -1
- package/scripts/install-cursor-skills.mjs +36 -0
- package/skills/ab/SKILL.md +1 -1
- package/skills/create-agent/SKILL.md +1 -1
- package/skills/debug/SKILL.md +1 -1
- package/skills/evals/SKILL.md +1 -1
- package/skills/framework-map/SKILL.md +1 -1
- package/skills/github/SKILL.md +1 -1
- package/skills/hillclimb/SKILL.md +1 -1
- package/skills/mcp-auth/SKILL.md +1 -1
- package/skills/otel/SKILL.md +1 -1
- package/skills/setup-slack/SKILL.md +1 -1
- package/src/bin/agent-serve.ts +7 -5
- package/src/channels/checks.ts +185 -0
- package/src/channels/deployments/deployments-channel.ts +109 -0
- package/src/channels/deployments/index.ts +26 -0
- package/src/channels/deployments/types.ts +80 -0
- package/src/channels/github/checks.ts +90 -115
- package/src/channels/origin/api.ts +318 -0
- package/src/channels/origin/binding.ts +96 -0
- package/src/channels/origin/checks.ts +250 -0
- package/src/channels/origin/index.ts +3 -0
- package/src/channels/origin/origin-channel.ts +18 -6
- package/src/channels/origin/types.ts +37 -0
- package/src/channels.ts +16 -0
- package/src/internal/cli-skills.ts +10 -2
- package/src/internal/cursor-deployment-relay.ts +274 -0
- package/src/internal/distribution.ts +1 -0
- package/src/internal/install-cursor-skills.ts +204 -49
- package/src/internal/sdk-runner.ts +6 -3
- package/src/internal/server.ts +149 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.DQad--_z.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CrWSA0Ko.js +0 -1
- package/dist/playground/assets/index-77uhu5Kr.css +0 -1
- package/dist/playground/assets/index-D4buox4m.js +0 -85
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ChannelDefinition, ChannelHandlerArgs } from "../../types.js";
|
|
2
|
+
|
|
3
|
+
/** {@link ChannelMeta.kind} value used by {@link deploymentsChannel}. */
|
|
4
|
+
export const DEPLOYMENTS_CHANNEL_KIND = "deployments";
|
|
5
|
+
|
|
6
|
+
export const DEPLOYMENT_EVENT_TYPES = [
|
|
7
|
+
"deployment.created",
|
|
8
|
+
"deployment.succeeded",
|
|
9
|
+
] as const;
|
|
10
|
+
export type DeploymentEventType = (typeof DEPLOYMENT_EVENT_TYPES)[number];
|
|
11
|
+
|
|
12
|
+
export type DeploymentStatus = "pending" | "in_progress" | "success" | "failed";
|
|
13
|
+
|
|
14
|
+
/** One deployment event, as the control plane records it. */
|
|
15
|
+
export interface DeploymentEvent {
|
|
16
|
+
eventType: DeploymentEventType;
|
|
17
|
+
/** Stable id; dedupe on it if you act more than once per event. */
|
|
18
|
+
eventId: string;
|
|
19
|
+
/** Owning Cursor team. */
|
|
20
|
+
teamId: number;
|
|
21
|
+
/** The Deployment's repository string, case-folded. */
|
|
22
|
+
repo: string;
|
|
23
|
+
deploymentId: string;
|
|
24
|
+
/** Environment slug, e.g. `prod`. */
|
|
25
|
+
environment: string;
|
|
26
|
+
/** Absent when the deployment records no service. */
|
|
27
|
+
service?: string;
|
|
28
|
+
sha: string;
|
|
29
|
+
status: DeploymentStatus;
|
|
30
|
+
occurredAtMs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What a repository subscription asks for. */
|
|
34
|
+
export interface DeploymentSubscription {
|
|
35
|
+
/**
|
|
36
|
+
* The repository string as the factory records it on a Deployment. Opaque:
|
|
37
|
+
* whatever your deployment writer puts there, matched case-insensitively.
|
|
38
|
+
*/
|
|
39
|
+
repo: string;
|
|
40
|
+
/** Omit or leave empty for every environment. */
|
|
41
|
+
environments?: readonly string[];
|
|
42
|
+
/** Defaults to `deployment.succeeded`. */
|
|
43
|
+
events?: readonly DeploymentEventType[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DeploymentsChannelOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Repositories to watch, spelled exactly as your deployment writer records
|
|
49
|
+
* `Deployment.repository`. The field has no format — whatever the writer
|
|
50
|
+
* puts there is the key — and matching is case-insensitive but otherwise
|
|
51
|
+
* literal, so a value that does not match subscribes successfully and then
|
|
52
|
+
* receives nothing. A bare string takes the default environment and event
|
|
53
|
+
* filters.
|
|
54
|
+
*
|
|
55
|
+
* There is no team-wide subscription: every repository is named. At least
|
|
56
|
+
* one is required — an empty list is how a consumer *deregisters*, so it
|
|
57
|
+
* cannot also mean "everything".
|
|
58
|
+
*/
|
|
59
|
+
repos: readonly (string | DeploymentSubscription)[];
|
|
60
|
+
/**
|
|
61
|
+
* Applied to every subscription that does not set its own. Omit or leave
|
|
62
|
+
* empty for every environment.
|
|
63
|
+
*/
|
|
64
|
+
environments?: readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* Applied to every subscription that does not set its own. Defaults to
|
|
67
|
+
* `deployment.succeeded`.
|
|
68
|
+
*/
|
|
69
|
+
events?: readonly DeploymentEventType[];
|
|
70
|
+
/** Called for each matching event. Failures retry a few times, then skip. */
|
|
71
|
+
onEvent: (
|
|
72
|
+
event: DeploymentEvent,
|
|
73
|
+
args: ChannelHandlerArgs
|
|
74
|
+
) => Promise<unknown> | unknown;
|
|
75
|
+
/** Route path for the local replay POST. Defaults to `/`. */
|
|
76
|
+
path?: string;
|
|
77
|
+
auth?: ChannelDefinition["auth"];
|
|
78
|
+
onStart?: ChannelDefinition["onStart"];
|
|
79
|
+
onStop?: ChannelDefinition["onStop"];
|
|
80
|
+
}
|
|
@@ -1,81 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GitHub CI checks (Checks API) — the GitHub {@link CiCheckWriter} behind the
|
|
3
|
+
* shared check lifecycle in `../checks.js`.
|
|
3
4
|
*
|
|
4
|
-
* `createGitHubCheck` opens a merge-box check run on a commit and returns
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Works with any client exposing `getOctokit()` (`ctx.github`,
|
|
5
|
+
* `createGitHubCheck` opens a merge-box check run on a commit and returns the
|
|
6
|
+
* shared `CiCheck` handle; `resumeGitHubCheck` rebuilds one from a stored
|
|
7
|
+
* check-run id. Works with any client exposing `getOctokit()` (`ctx.github`,
|
|
8
8
|
* `ctx.host.github`), and is also reachable as `ctx.github.createCheck(...)`.
|
|
9
9
|
*
|
|
10
10
|
* Posting checks needs `checks:write`. With `cursorAccount`, declare
|
|
11
11
|
* `githubChannel({ checks: true })` so the hosted lease is minted on the
|
|
12
12
|
* `contents-write` tier; GitHub App / PAT credentials need the equivalent
|
|
13
|
-
* permission.
|
|
13
|
+
* permission. API failures are normalized into `GitHubApiError`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import type { Octokit } from "@octokit/rest";
|
|
17
17
|
import {
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
type CiCheck,
|
|
19
|
+
type CiCheckConcludeInput,
|
|
20
|
+
type CiCheckCreateInput,
|
|
21
|
+
type CiCheckOutput,
|
|
22
|
+
type CiCheckRef,
|
|
23
|
+
type CiCheckUpdateInput,
|
|
24
|
+
type CiCheckWriter,
|
|
25
|
+
openCiCheck,
|
|
26
|
+
resumeCiCheck,
|
|
27
|
+
} from "../checks.js";
|
|
28
|
+
import { wrapOctokitError } from "./api.js";
|
|
22
29
|
|
|
23
30
|
/** Any client that can hand out an authenticated Octokit. */
|
|
24
31
|
export interface GitHubCheckClient {
|
|
25
32
|
getOctokit(): Promise<Octokit>;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
+
export type GitHubCheckOutput = CiCheckOutput;
|
|
36
|
+
export type GitHubCheckUpdateInput = CiCheckUpdateInput;
|
|
37
|
+
export type GitHubCheckConcludeInput = CiCheckConcludeInput;
|
|
38
|
+
|
|
39
|
+
/** A created check run. All methods throw `GitHubApiError` on API failure. */
|
|
40
|
+
export type GitHubCheck = CiCheck & CiCheckRef;
|
|
35
41
|
|
|
36
|
-
export interface GitHubCheckCreateInput {
|
|
42
|
+
export interface GitHubCheckCreateInput extends CiCheckCreateInput {
|
|
37
43
|
/** Client used for every API call on the returned handle. */
|
|
38
44
|
github: GitHubCheckClient;
|
|
39
45
|
/** Repository owner login, e.g. `"acme"`. */
|
|
40
46
|
owner: string;
|
|
41
47
|
/** Repository name, e.g. `"widgets"`. */
|
|
42
48
|
repo: string;
|
|
43
|
-
/** Check name shown in the PR merge box, e.g. `"Cursor Security Review"`. */
|
|
44
|
-
name: string;
|
|
45
|
-
/** Commit the check attaches to (usually the PR head SHA). */
|
|
46
|
-
sha: string;
|
|
47
|
-
/** Initial status. Defaults to `"in_progress"`. */
|
|
48
|
-
status?: "queued" | "in_progress";
|
|
49
|
-
/** "Details" link on the check, e.g. the agent session run URL. */
|
|
50
|
-
detailsUrl?: string;
|
|
51
|
-
/** Title + markdown summary shown on the check page. */
|
|
52
|
-
output?: GitHubCheckOutput;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface GitHubCheckUpdateInput {
|
|
56
|
-
status?: GitHubCheckRunStatus;
|
|
57
|
-
conclusion?: GitHubCheckRunConclusion;
|
|
58
|
-
detailsUrl?: string;
|
|
59
|
-
output?: GitHubCheckOutput;
|
|
60
49
|
}
|
|
61
50
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
51
|
+
/**
|
|
52
|
+
* GitHub writer: first write creates the check run (capturing its id), later
|
|
53
|
+
* writes patch it.
|
|
54
|
+
*/
|
|
55
|
+
function githubCheckWriter(input: {
|
|
56
|
+
github: GitHubCheckClient;
|
|
57
|
+
owner: string;
|
|
58
|
+
repo: string;
|
|
59
|
+
name: string;
|
|
60
|
+
sha?: string;
|
|
61
|
+
checkRunId?: number;
|
|
62
|
+
}): CiCheckWriter {
|
|
63
|
+
let checkRunId = input.checkRunId;
|
|
64
|
+
return async (write) => {
|
|
65
|
+
const octokit = await input.github.getOctokit();
|
|
66
|
+
const base = {
|
|
67
|
+
owner: input.owner,
|
|
68
|
+
repo: input.repo,
|
|
69
|
+
name: input.name,
|
|
70
|
+
status: write.status,
|
|
71
|
+
conclusion: write.conclusion,
|
|
72
|
+
completed_at: write.completedAt,
|
|
73
|
+
details_url: write.detailsUrl,
|
|
74
|
+
output: write.output,
|
|
75
|
+
};
|
|
76
|
+
if (checkRunId === undefined) {
|
|
77
|
+
if (input.sha === undefined) {
|
|
78
|
+
throw new Error("githubChannel: cannot create a check without a sha.");
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const response = await octokit.rest.checks.create({
|
|
82
|
+
...base,
|
|
83
|
+
head_sha: input.sha,
|
|
84
|
+
started_at: write.startedAt,
|
|
85
|
+
});
|
|
86
|
+
checkRunId = response.data.id;
|
|
87
|
+
return {
|
|
88
|
+
id: response.data.id,
|
|
89
|
+
url:
|
|
90
|
+
typeof response.data.html_url === "string"
|
|
91
|
+
? response.data.html_url
|
|
92
|
+
: undefined,
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw wrapOctokitError(
|
|
96
|
+
error,
|
|
97
|
+
"POST",
|
|
98
|
+
`/repos/${input.owner}/${input.repo}/check-runs`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await octokit.rest.checks.update({ ...base, check_run_id: checkRunId });
|
|
104
|
+
return undefined;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw wrapOctokitError(
|
|
107
|
+
error,
|
|
108
|
+
"PATCH",
|
|
109
|
+
`/repos/${input.owner}/${input.repo}/check-runs/${checkRunId}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
/**
|
|
@@ -95,38 +129,10 @@ export interface GitHubCheck {
|
|
|
95
129
|
* await check.conclude({ conclusion: "success", output: { … } });
|
|
96
130
|
* ```
|
|
97
131
|
*/
|
|
98
|
-
export
|
|
132
|
+
export function createGitHubCheck(
|
|
99
133
|
input: GitHubCheckCreateInput
|
|
100
134
|
): Promise<GitHubCheck> {
|
|
101
|
-
|
|
102
|
-
let created: { id: number; html_url?: string | null };
|
|
103
|
-
try {
|
|
104
|
-
const response = await octokit.rest.checks.create({
|
|
105
|
-
owner: input.owner,
|
|
106
|
-
repo: input.repo,
|
|
107
|
-
name: input.name,
|
|
108
|
-
head_sha: input.sha,
|
|
109
|
-
status: input.status ?? "in_progress",
|
|
110
|
-
started_at: new Date().toISOString(),
|
|
111
|
-
details_url: input.detailsUrl,
|
|
112
|
-
output: input.output,
|
|
113
|
-
});
|
|
114
|
-
created = response.data;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
throw wrapOctokitError(
|
|
117
|
-
error,
|
|
118
|
-
"POST",
|
|
119
|
-
`/repos/${input.owner}/${input.repo}/check-runs`
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
return resumeGitHubCheck({
|
|
123
|
-
github: input.github,
|
|
124
|
-
owner: input.owner,
|
|
125
|
-
repo: input.repo,
|
|
126
|
-
name: input.name,
|
|
127
|
-
checkRunId: created.id,
|
|
128
|
-
url: typeof created.html_url === "string" ? created.html_url : undefined,
|
|
129
|
-
});
|
|
135
|
+
return openCiCheck(githubCheckWriter(input), input);
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/**
|
|
@@ -141,39 +147,8 @@ export function resumeGitHubCheck(input: {
|
|
|
141
147
|
checkRunId: number;
|
|
142
148
|
url?: string;
|
|
143
149
|
}): GitHubCheck {
|
|
144
|
-
|
|
145
|
-
const octokit = await input.github.getOctokit();
|
|
146
|
-
try {
|
|
147
|
-
await octokit.rest.checks.update({
|
|
148
|
-
owner: input.owner,
|
|
149
|
-
repo: input.repo,
|
|
150
|
-
check_run_id: input.checkRunId,
|
|
151
|
-
name: input.name,
|
|
152
|
-
status: update.status,
|
|
153
|
-
conclusion: update.conclusion,
|
|
154
|
-
completed_at:
|
|
155
|
-
update.status === "completed" ? new Date().toISOString() : undefined,
|
|
156
|
-
details_url: update.detailsUrl,
|
|
157
|
-
output: update.output,
|
|
158
|
-
});
|
|
159
|
-
} catch (error) {
|
|
160
|
-
throw wrapOctokitError(
|
|
161
|
-
error,
|
|
162
|
-
"PATCH",
|
|
163
|
-
`/repos/${input.owner}/${input.repo}/check-runs/${input.checkRunId}`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
return {
|
|
150
|
+
return resumeCiCheck(githubCheckWriter(input), {
|
|
168
151
|
id: input.checkRunId,
|
|
169
152
|
url: input.url,
|
|
170
|
-
|
|
171
|
-
conclude: (conclude) =>
|
|
172
|
-
patch({
|
|
173
|
-
status: "completed",
|
|
174
|
-
conclusion: conclude.conclusion,
|
|
175
|
-
detailsUrl: conclude.detailsUrl,
|
|
176
|
-
output: conclude.output,
|
|
177
|
-
}),
|
|
178
|
-
};
|
|
153
|
+
});
|
|
179
154
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Origin public REST helpers (`https://api.cursor.com/v1/origin/...`).
|
|
3
|
+
*
|
|
4
|
+
* Origin writes such as CI check runs are installation-only: they need an
|
|
5
|
+
* `oit_…` installation token minted from Origin app credentials (EdDSA app
|
|
6
|
+
* JWT → `/v1/origin/app/installations/{id}/access_tokens`). This module
|
|
7
|
+
* consolidates that flow so agents configure credentials once and call
|
|
8
|
+
* {@link createOriginClient} — the GitHub-channel analogue of
|
|
9
|
+
* `createGitHubOctokit`.
|
|
10
|
+
*
|
|
11
|
+
* Credentials resolve from options first, then the environment:
|
|
12
|
+
* `ORIGIN_APP_ID` (or `AGENT_SERVE_ORIGIN_APP_ID`),
|
|
13
|
+
* `ORIGIN_APP_INSTALLATION_ID`, `ORIGIN_APP_PRIVATE_KEY` (PKCS8 PEM for the
|
|
14
|
+
* app's Ed25519 key; literal `\n` sequences are normalized), and
|
|
15
|
+
* `ORIGIN_API_BASE_URL` for the public REST base.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Buffer } from "node:buffer";
|
|
19
|
+
import { createPrivateKey, sign } from "node:crypto";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Public Origin REST base (check runs, installation tokens). Distinct from
|
|
23
|
+
* the webhook JWKS base (`DEFAULT_ORIGIN_API_BASE_URL` in
|
|
24
|
+
* `origin-webhook.ts`).
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_ORIGIN_PUBLIC_API_BASE_URL = "https://api.cursor.com";
|
|
27
|
+
|
|
28
|
+
/** Fixed audience for Origin app JWTs (`APP_TOKEN_AUDIENCE` backend-side). */
|
|
29
|
+
const ORIGIN_APP_TOKEN_AUDIENCE = "origin-apps";
|
|
30
|
+
const APP_JWT_TTL_SECONDS = 5 * 60;
|
|
31
|
+
/** Re-mint the `oit_` token after this long; tokens outlive it comfortably. */
|
|
32
|
+
const TOKEN_REUSE_MS = 4 * 60 * 1000;
|
|
33
|
+
const TOKEN_EXCHANGE_TIMEOUT_MS = 30_000;
|
|
34
|
+
|
|
35
|
+
export type OriginApiMethod = "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
|
|
36
|
+
|
|
37
|
+
/** Normalized Origin REST failure — the `GitHubApiError` analogue. */
|
|
38
|
+
export class OriginApiError extends Error {
|
|
39
|
+
readonly body: unknown;
|
|
40
|
+
readonly method: string;
|
|
41
|
+
readonly path: string;
|
|
42
|
+
readonly status: number;
|
|
43
|
+
|
|
44
|
+
constructor(input: {
|
|
45
|
+
body: unknown;
|
|
46
|
+
method: string;
|
|
47
|
+
path: string;
|
|
48
|
+
status: number;
|
|
49
|
+
}) {
|
|
50
|
+
super(
|
|
51
|
+
`Origin ${input.method} ${input.path} failed with HTTP ${input.status}.`
|
|
52
|
+
);
|
|
53
|
+
this.name = "OriginApiError";
|
|
54
|
+
this.body = input.body;
|
|
55
|
+
this.method = input.method;
|
|
56
|
+
this.path = input.path;
|
|
57
|
+
this.status = input.status;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Yields a bearer for Origin REST calls (usually a short-lived `oit_…`). */
|
|
62
|
+
export type OriginTokenProvider = () => Promise<string>;
|
|
63
|
+
|
|
64
|
+
export interface OriginAppCredentials {
|
|
65
|
+
/** Origin app id (`app_<typeid>`). */
|
|
66
|
+
appId: string;
|
|
67
|
+
/** Installation the token is minted for. */
|
|
68
|
+
installationId: string;
|
|
69
|
+
/** PKCS8 PEM for the app's EdDSA (Ed25519) private key. */
|
|
70
|
+
privateKey: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Vault/env round-trip: literal `\n` sequences appear in practice. */
|
|
74
|
+
function normalizePrivateKeyPem(raw: string | undefined): string | undefined {
|
|
75
|
+
const pem = raw?.trim().replace(/\\n/g, "\n");
|
|
76
|
+
return pem === undefined || pem === "" ? undefined : pem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function envValue(value: string | undefined): string | undefined {
|
|
80
|
+
const trimmed = value?.trim();
|
|
81
|
+
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Each Origin app credential field found in the environment, when set. */
|
|
85
|
+
export function resolveOriginAppCredentialParts(
|
|
86
|
+
env: NodeJS.ProcessEnv = process.env
|
|
87
|
+
): Partial<OriginAppCredentials> {
|
|
88
|
+
return {
|
|
89
|
+
appId:
|
|
90
|
+
envValue(env.ORIGIN_APP_ID) ?? envValue(env.AGENT_SERVE_ORIGIN_APP_ID),
|
|
91
|
+
installationId: envValue(env.ORIGIN_APP_INSTALLATION_ID),
|
|
92
|
+
privateKey: normalizePrivateKeyPem(env.ORIGIN_APP_PRIVATE_KEY),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read Origin app credentials from the environment. Returns `undefined` when
|
|
98
|
+
* incomplete so callers can fail with a clear message at use time instead of
|
|
99
|
+
* at startup.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveOriginAppCredentials(
|
|
102
|
+
env: NodeJS.ProcessEnv = process.env
|
|
103
|
+
): OriginAppCredentials | undefined {
|
|
104
|
+
const { appId, installationId, privateKey } =
|
|
105
|
+
resolveOriginAppCredentialParts(env);
|
|
106
|
+
if (
|
|
107
|
+
appId === undefined ||
|
|
108
|
+
installationId === undefined ||
|
|
109
|
+
privateKey === undefined
|
|
110
|
+
) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return { appId, installationId, privateKey };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Public Origin REST base. Trailing slashes are stripped for path joins. */
|
|
117
|
+
export function originPublicApiBaseUrl(
|
|
118
|
+
env: NodeJS.ProcessEnv = process.env
|
|
119
|
+
): string {
|
|
120
|
+
const fromEnv = envValue(env.ORIGIN_API_BASE_URL)?.replace(/\/+$/, "");
|
|
121
|
+
return fromEnv ?? DEFAULT_ORIGIN_PUBLIC_API_BASE_URL;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function base64UrlJson(value: unknown): string {
|
|
125
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mintOriginAppJwt(
|
|
129
|
+
credentials: OriginAppCredentials,
|
|
130
|
+
nowMs: number
|
|
131
|
+
): string {
|
|
132
|
+
const now = Math.floor(nowMs / 1000);
|
|
133
|
+
const header = base64UrlJson({
|
|
134
|
+
alg: "EdDSA",
|
|
135
|
+
kid: credentials.appId,
|
|
136
|
+
typ: "JWT",
|
|
137
|
+
});
|
|
138
|
+
const payload = base64UrlJson({
|
|
139
|
+
iss: credentials.appId,
|
|
140
|
+
aud: ORIGIN_APP_TOKEN_AUDIENCE,
|
|
141
|
+
iat: now,
|
|
142
|
+
exp: now + APP_JWT_TTL_SECONDS,
|
|
143
|
+
});
|
|
144
|
+
const signingInput = `${header}.${payload}`;
|
|
145
|
+
const key = createPrivateKey(credentials.privateKey);
|
|
146
|
+
const signature = sign(null, Buffer.from(signingInput), key).toString(
|
|
147
|
+
"base64url"
|
|
148
|
+
);
|
|
149
|
+
return `${signingInput}.${signature}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function mintOriginInstallationToken(input: {
|
|
153
|
+
credentials: OriginAppCredentials;
|
|
154
|
+
apiBaseUrl: string;
|
|
155
|
+
fetchImpl: typeof fetch;
|
|
156
|
+
nowMs: number;
|
|
157
|
+
}): Promise<string> {
|
|
158
|
+
const jwt = mintOriginAppJwt(input.credentials, input.nowMs);
|
|
159
|
+
const path = `/v1/origin/app/installations/${encodeURIComponent(
|
|
160
|
+
input.credentials.installationId
|
|
161
|
+
)}/access_tokens`;
|
|
162
|
+
const response = await input.fetchImpl(`${input.apiBaseUrl}${path}`, {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { authorization: `Bearer ${jwt}` },
|
|
165
|
+
signal: AbortSignal.timeout(TOKEN_EXCHANGE_TIMEOUT_MS),
|
|
166
|
+
});
|
|
167
|
+
const body: unknown = await response.json().catch(() => undefined);
|
|
168
|
+
const token =
|
|
169
|
+
typeof body === "object" && body !== null && !Array.isArray(body)
|
|
170
|
+
? (body as Record<string, unknown>).token
|
|
171
|
+
: undefined;
|
|
172
|
+
if (!response.ok || typeof token !== "string" || token === "") {
|
|
173
|
+
// Never echo the body: on success it contains the installation token.
|
|
174
|
+
throw new OriginApiError({
|
|
175
|
+
body: undefined,
|
|
176
|
+
method: "POST",
|
|
177
|
+
path,
|
|
178
|
+
status: response.status,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return token;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A cached per-use token provider for one set of app credentials. Every call
|
|
186
|
+
* inside the reuse window returns the same `oit_…`; the first call after it
|
|
187
|
+
* re-mints.
|
|
188
|
+
*/
|
|
189
|
+
export function originAppTokenProvider(input: {
|
|
190
|
+
credentials: OriginAppCredentials;
|
|
191
|
+
apiBaseUrl?: string;
|
|
192
|
+
fetch?: typeof fetch;
|
|
193
|
+
now?: () => number;
|
|
194
|
+
}): OriginTokenProvider {
|
|
195
|
+
const apiBaseUrl = input.apiBaseUrl ?? originPublicApiBaseUrl();
|
|
196
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
197
|
+
const now = input.now ?? Date.now;
|
|
198
|
+
let cached: { token: string; mintedAtMs: number } | undefined;
|
|
199
|
+
return async () => {
|
|
200
|
+
const nowMs = now();
|
|
201
|
+
if (cached !== undefined && nowMs - cached.mintedAtMs < TOKEN_REUSE_MS) {
|
|
202
|
+
return cached.token;
|
|
203
|
+
}
|
|
204
|
+
const token = await mintOriginInstallationToken({
|
|
205
|
+
credentials: input.credentials,
|
|
206
|
+
apiBaseUrl,
|
|
207
|
+
fetchImpl,
|
|
208
|
+
nowMs,
|
|
209
|
+
});
|
|
210
|
+
cached = { token, mintedAtMs: nowMs };
|
|
211
|
+
return token;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface OriginClientOptions {
|
|
216
|
+
/** Static bearer (`oit_…`) or refreshing provider used for every call. */
|
|
217
|
+
token?: string | OriginTokenProvider;
|
|
218
|
+
/**
|
|
219
|
+
* App credentials to mint installation tokens from. Defaults to the
|
|
220
|
+
* environment ({@link resolveOriginAppCredentials}).
|
|
221
|
+
*/
|
|
222
|
+
credentials?: OriginAppCredentials;
|
|
223
|
+
/** Public Origin REST base. Defaults to `ORIGIN_API_BASE_URL` / prod. */
|
|
224
|
+
apiBaseUrl?: string;
|
|
225
|
+
/** Injectable fetch for tests. */
|
|
226
|
+
fetch?: typeof fetch;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Minimal authenticated Origin REST client (the `Octokit` analogue). */
|
|
230
|
+
export interface OriginClient {
|
|
231
|
+
readonly apiBaseUrl: string;
|
|
232
|
+
/**
|
|
233
|
+
* Call an Origin public REST path (e.g. `/v1/origin/repos/o/r/check-runs`).
|
|
234
|
+
* Throws {@link OriginApiError} on non-2xx responses.
|
|
235
|
+
*/
|
|
236
|
+
request<T = unknown>(input: {
|
|
237
|
+
method: OriginApiMethod;
|
|
238
|
+
path: string;
|
|
239
|
+
body?: unknown;
|
|
240
|
+
}): Promise<T>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build an authenticated Origin REST client. Auth resolution: explicit
|
|
245
|
+
* token/provider, then app credentials (options or environment). Missing
|
|
246
|
+
* auth fails at the first request with a clear message.
|
|
247
|
+
*/
|
|
248
|
+
export function createOriginClient(
|
|
249
|
+
options: OriginClientOptions = {}
|
|
250
|
+
): OriginClient {
|
|
251
|
+
const apiBaseUrl = (options.apiBaseUrl ?? originPublicApiBaseUrl()).replace(
|
|
252
|
+
/\/+$/,
|
|
253
|
+
""
|
|
254
|
+
);
|
|
255
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
256
|
+
|
|
257
|
+
let tokenProvider: OriginTokenProvider | undefined;
|
|
258
|
+
const getToken = async (): Promise<string> => {
|
|
259
|
+
if (typeof options.token === "string") {
|
|
260
|
+
return options.token;
|
|
261
|
+
}
|
|
262
|
+
if (typeof options.token === "function") {
|
|
263
|
+
return await options.token();
|
|
264
|
+
}
|
|
265
|
+
if (tokenProvider === undefined) {
|
|
266
|
+
const credentials = options.credentials ?? resolveOriginAppCredentials();
|
|
267
|
+
if (credentials === undefined) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"originClient: no Origin credential available (pass token / credentials, or set ORIGIN_APP_ID + ORIGIN_APP_INSTALLATION_ID + ORIGIN_APP_PRIVATE_KEY)."
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
tokenProvider = originAppTokenProvider({
|
|
273
|
+
credentials,
|
|
274
|
+
apiBaseUrl,
|
|
275
|
+
fetch: fetchImpl,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return await tokenProvider();
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
apiBaseUrl,
|
|
283
|
+
async request<T = unknown>(input: {
|
|
284
|
+
method: OriginApiMethod;
|
|
285
|
+
path: string;
|
|
286
|
+
body?: unknown;
|
|
287
|
+
}): Promise<T> {
|
|
288
|
+
const path = input.path.startsWith("/") ? input.path : `/${input.path}`;
|
|
289
|
+
const token = await getToken();
|
|
290
|
+
const response = await fetchImpl(`${apiBaseUrl}${path}`, {
|
|
291
|
+
method: input.method,
|
|
292
|
+
headers: {
|
|
293
|
+
authorization: `Bearer ${token}`,
|
|
294
|
+
...(input.body === undefined
|
|
295
|
+
? {}
|
|
296
|
+
: { "content-type": "application/json" }),
|
|
297
|
+
},
|
|
298
|
+
body: input.body === undefined ? undefined : JSON.stringify(input.body),
|
|
299
|
+
});
|
|
300
|
+
const text = await response.text();
|
|
301
|
+
let parsed: unknown;
|
|
302
|
+
try {
|
|
303
|
+
parsed = text === "" ? undefined : JSON.parse(text);
|
|
304
|
+
} catch {
|
|
305
|
+
parsed = text;
|
|
306
|
+
}
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new OriginApiError({
|
|
309
|
+
body: parsed,
|
|
310
|
+
method: input.method,
|
|
311
|
+
path,
|
|
312
|
+
status: response.status,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return parsed as T;
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|