setorra 0.2.5 → 0.3.0

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 CHANGED
@@ -4,13 +4,13 @@ Onboards a GitHub repository for automated API migration. It installs two workfl
4
4
  and opens a pull request for you to review.
5
5
 
6
6
  ```bash
7
- npx setorra init claude # or: codex
7
+ npx setorra init claude # or: codex, cursor
8
8
  ```
9
9
 
10
10
  ## What it writes
11
11
 
12
- One agent workflow (Claude shown; Codex uses `api-migration-codex.yml`) and a shared
13
- completion workflow:
12
+ One agent workflow (Claude shown; Codex and Cursor use `api-migration-codex.yml` and
13
+ `api-migration-cursor.yml` respectively) and a shared completion workflow:
14
14
 
15
15
  ```
16
16
  .github/workflows/api-migration-claude.yml
@@ -52,6 +52,7 @@ The workflow needs the selected agent credential:
52
52
  | --- | --- |
53
53
  | `claude` | `CLAUDE_CODE_OAUTH_TOKEN` (or `ANTHROPIC_API_KEY` with `--credential api_key`) |
54
54
  | `codex` | `OPENAI_API_KEY` |
55
+ | `cursor` | `CURSOR_API_KEY` (a Cursor Enterprise service-account key is recommended) |
55
56
 
56
57
  Add it under **Settings → Secrets and variables → Actions**.
57
58
 
@@ -70,7 +71,7 @@ revoke execution. Removing the agent secret alone does not disable PR completion
70
71
 
71
72
  | Command | Purpose |
72
73
  | --- | --- |
73
- | `setorra init <claude\|codex>` | Install both workflows and open a pull request. |
74
+ | `setorra init <claude\|codex\|cursor>` | Install both workflows and open a pull request. |
74
75
  | `setorra status` | Check the agent workflow and reviewed completion contract. |
75
76
  | `setorra sync` | Upgrade both workflows, including adding a missing companion. |
76
77
 
package/dist/setorra.js CHANGED
@@ -7301,13 +7301,15 @@ var require_dist = __commonJS({
7301
7301
  var import_yaml = __toESM(require_dist());
7302
7302
 
7303
7303
  // src/workflow/contracts.ts
7304
+ var AGENT_KINDS = ["claude", "codex", "cursor"];
7304
7305
  var WORKFLOW_CONTRACT_VERSION = "agent-workflow/1";
7305
7306
  var RESULT_CONTRACT_VERSION = "cloud-agent-result/v1";
7306
7307
  var RESULT_ARTIFACT_NAME = "cloud-agent-result";
7307
7308
  var RESULT_ARTIFACT_FILE = "cloud-agent-result.json";
7308
7309
  var AGENT_WORKFLOW_PATHS = {
7309
7310
  claude: ".github/workflows/api-migration-claude.yml",
7310
- codex: ".github/workflows/api-migration-codex.yml"
7311
+ codex: ".github/workflows/api-migration-codex.yml",
7312
+ cursor: ".github/workflows/api-migration-cursor.yml"
7311
7313
  };
7312
7314
  var COMPLETION_WORKFLOW_PATH = ".github/workflows/setorra-pr-completion.yml";
7313
7315
  var COMPLETION_MARKER = "# setorra-pr-completion/v1";
@@ -21835,7 +21837,7 @@ var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]
21835
21837
  var DIGEST = /^[a-f0-9]{64}$/u;
21836
21838
  var SHA = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u;
21837
21839
  var GITHUB_ID = /^[1-9][0-9]{0,19}$/u;
21838
- var WORKFLOW_PATH = /^\.github\/workflows\/api-migration-(?:claude|codex)\.yml$/u;
21840
+ var WORKFLOW_PATH = /^\.github\/workflows\/api-migration-(?:claude|codex|cursor)\.yml$/u;
21839
21841
  var HandoffFields = {
21840
21842
  handoffId: external_exports.string().regex(UUID),
21841
21843
  handoffDigest: external_exports.string().regex(DIGEST),
@@ -21935,7 +21937,7 @@ function renderProvenanceValidationScript() {
21935
21937
  "const digest = /^[a-f0-9]{64}$/;",
21936
21938
  "const sha = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;",
21937
21939
  "const githubId = /^[1-9][0-9]{0,19}$/;",
21938
- "const workflowPath = /^\\.github\\/workflows\\/api-migration-(?:claude|codex)\\.yml$/;",
21940
+ "const workflowPath = /^\\.github\\/workflows\\/api-migration-(?:claude|codex|cursor)\\.yml$/;",
21939
21941
  "if (!['release-agent-handoff/v2', 'release-agent-handoff/v3', 'release-agent-handoff/v4'].includes(provenance.schemaVersion) || provenance.workflowContractVersion !== 'agent-workflow/1' || provenance.resultContractVersion !== 'cloud-agent-result/v1' || !uuid.test(provenance.handoffId) || (provenance.runId !== undefined && !uuid.test(provenance.runId)) || !digest.test(provenance.handoffDigest) || !digest.test(provenance.contextDigest) || !githubId.test(provenance.repositoryId) || !sha.test(provenance.baseSha) || !githubId.test(provenance.workflowId) || !workflowPath.test(provenance.workflowPath)) fail('invalid_fields');",
21940
21942
  "if (['release-agent-handoff/v3', 'release-agent-handoff/v4'].includes(provenance.schemaVersion) && !digest.test(provenance.contextPayloadDigest)) fail('invalid_fields');",
21941
21943
  "if (provenance.schemaVersion === 'release-agent-handoff/v2' && provenance.contextPayloadDigest !== undefined && !digest.test(provenance.contextPayloadDigest)) fail('invalid_fields');",
@@ -22024,7 +22026,7 @@ const baseBranch = repository.default_branch;
22024
22026
  const run = request(prefix + '/actions/runs/' + inputs.original_run_id);
22025
22027
  const attempt = request(prefix + '/actions/runs/' + inputs.original_run_id + '/attempts/' + inputs.original_attempt);
22026
22028
  for (const r of [run, attempt]) {
22027
- if (String(r.id) !== inputs.original_run_id || String(r.run_attempt) !== inputs.original_attempt || r.status !== 'completed' || r.event !== 'issues' || r.head_sha !== inputs.expected_base_sha || String(r.repository?.id) !== process.env.GITHUB_REPOSITORY_ID || r.display_title !== 'api-migration-' + inputs.issue_number || !/^\.github\/workflows\/api-migration-(claude|codex)\.yml$/.test(r.path)) fail('original_workflow_changed');
22029
+ if (String(r.id) !== inputs.original_run_id || String(r.run_attempt) !== inputs.original_attempt || r.status !== 'completed' || r.event !== 'issues' || r.head_sha !== inputs.expected_base_sha || String(r.repository?.id) !== process.env.GITHUB_REPOSITORY_ID || r.display_title !== 'api-migration-' + inputs.issue_number || !/^\.github\/workflows\/api-migration-(claude|codex|cursor)\.yml$/.test(r.path)) fail('original_workflow_changed');
22028
22030
  }
22029
22031
  if (run.workflow_id !== attempt.workflow_id || run.path !== attempt.path) fail('original_workflow_changed');
22030
22032
  function validateIssue(issue) {
@@ -22118,6 +22120,11 @@ var AGENT_ACTION_REPOSITORIES = {
22118
22120
  claude: "anthropics/claude-code-action",
22119
22121
  codex: "openai/codex-action"
22120
22122
  };
22123
+ var AGENT_TITLES = {
22124
+ claude: "Claude",
22125
+ codex: "Codex",
22126
+ cursor: "Cursor"
22127
+ };
22121
22128
  var CHECKOUT_ACTION_REPOSITORY = "actions/checkout";
22122
22129
  var UPLOAD_ARTIFACT_ACTION_REPOSITORY = "actions/upload-artifact";
22123
22130
  var AGENT_CREDENTIALS = {
@@ -22130,6 +22137,10 @@ var AGENT_CREDENTIALS = {
22130
22137
  },
22131
22138
  codex: {
22132
22139
  api_key: { input: "openai-api-key", secret: "OPENAI_API_KEY" }
22140
+ },
22141
+ // The Cursor CLI reads its key from the environment, so its input is a variable name.
22142
+ cursor: {
22143
+ api_key: { input: "CURSOR_API_KEY", secret: "CURSOR_API_KEY" }
22133
22144
  }
22134
22145
  };
22135
22146
  function agentCredentialInputs(agent) {
@@ -22144,15 +22155,49 @@ var PinnedActionSchema = external_exports.object({
22144
22155
  sha: external_exports.string().regex(/^[a-f0-9]{40}$/u),
22145
22156
  version: external_exports.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
22146
22157
  }).strict();
22147
- var WorkflowTemplateInputSchema = external_exports.object({
22148
- agent: external_exports.enum(["claude", "codex"]),
22158
+ var CursorCliPinSchema = external_exports.object({
22159
+ version: external_exports.string().regex(/^[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[a-f0-9]{7,40}$/u),
22160
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/u)
22161
+ }).strict();
22162
+ var CURSOR_CLI_DOWNLOAD_PREFIX = "https://downloads.cursor.com/lab/";
22163
+ function cursorCliUrl(pin) {
22164
+ return `${CURSOR_CLI_DOWNLOAD_PREFIX}${pin.version}/linux/x64/agent-cli-package.tar.gz`;
22165
+ }
22166
+ var CURSOR_DENIED_PERMISSIONS = [
22167
+ "Shell(git)",
22168
+ "Shell(gh)",
22169
+ "Shell(curl)",
22170
+ "Shell(wget)",
22171
+ "Shell(ssh)",
22172
+ "Shell(scp)",
22173
+ "Shell(sftp)",
22174
+ "Shell(nc)",
22175
+ "Shell(ncat)",
22176
+ "Shell(sudo)",
22177
+ "Write(.github/**)",
22178
+ "Write(.git/**)"
22179
+ ];
22180
+ var BotLoginSchema = external_exports.string().min(4).max(64).regex(/^[a-z0-9][a-z0-9-]*\[bot\]$/u);
22181
+ var LabelSchema = external_exports.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9._-]*$/u);
22182
+ var TemplateCommonFields = {
22149
22183
  credential: external_exports.enum(["api_key", "oauth_token"]).default("api_key"),
22150
- botLogin: external_exports.string().min(4).max(64).regex(/^[a-z0-9][a-z0-9-]*\[bot\]$/u),
22151
- label: external_exports.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9._-]*$/u),
22184
+ botLogin: BotLoginSchema,
22185
+ label: LabelSchema,
22152
22186
  checkoutAction: PinnedActionSchema,
22153
- agentAction: PinnedActionSchema,
22154
22187
  uploadArtifactAction: PinnedActionSchema
22155
- }).strict();
22188
+ };
22189
+ var WorkflowTemplateInputSchema = external_exports.discriminatedUnion("agent", [
22190
+ external_exports.object({
22191
+ agent: external_exports.enum(["claude", "codex"]),
22192
+ ...TemplateCommonFields,
22193
+ agentAction: PinnedActionSchema
22194
+ }).strict(),
22195
+ external_exports.object({
22196
+ agent: external_exports.literal("cursor"),
22197
+ ...TemplateCommonFields,
22198
+ cursorCli: CursorCliPinSchema
22199
+ }).strict()
22200
+ ]);
22156
22201
  function yamlString(value) {
22157
22202
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
22158
22203
  }
@@ -22295,6 +22340,9 @@ function renderHelperStep() {
22295
22340
  ];
22296
22341
  }
22297
22342
  function renderAgentSteps(input, selected) {
22343
+ if (input.agent === "cursor") {
22344
+ return renderCursorAgentStep(input.cursorCli, selected);
22345
+ }
22298
22346
  const { agent, agentAction, botLogin } = input;
22299
22347
  const credential = `${selected.input}: \${{ secrets.${selected.secret} }}`;
22300
22348
  const allowlist = `${AGENT_BOT_ALLOWLIST_INPUTS[agent]}: ${yamlString(botLogin)}`;
@@ -22347,6 +22395,38 @@ function renderAgentSteps(input, selected) {
22347
22395
  " prompt-file: ${{ runner.temp }}/agent-prompt.md"
22348
22396
  ];
22349
22397
  }
22398
+ function renderCursorAgentStep(pin, selected) {
22399
+ const config2 = JSON.stringify({
22400
+ version: 1,
22401
+ editor: { vimMode: false },
22402
+ permissions: { allow: [], deny: CURSOR_DENIED_PERMISSIONS }
22403
+ });
22404
+ return [
22405
+ " - name: Run the Cursor coding agent",
22406
+ " id: agent",
22407
+ " continue-on-error: true",
22408
+ " env:",
22409
+ " GITHUB_ENV: /dev/null",
22410
+ " GITHUB_PATH: /dev/null",
22411
+ ` ${selected.input}: \${{ secrets.${selected.secret} }}`,
22412
+ ` CURSOR_CLI_URL: ${yamlString(cursorCliUrl(pin))}`,
22413
+ ` CURSOR_CLI_SHA256: ${yamlString(pin.sha256)}`,
22414
+ " CURSOR_CONFIG_DIR: ${{ runner.temp }}/cursor-config",
22415
+ " AGENT_PROMPT_FILE: ${{ runner.temp }}/agent-prompt.md",
22416
+ " shell: bash",
22417
+ " run: |",
22418
+ ` sudo -n sh -c 'user="$1"; gpasswd -d "$user" sudo >/dev/null 2>&1 || true; for file in /etc/sudoers /etc/sudoers.d/*; do if [ -f "$file" ]; then sed -i "/^$user[[:space:]]/d" "$file"; fi; done' sh "$(id -un)"`,
22419
+ " sudo -K || true",
22420
+ " if sudo -n true 2>/dev/null; then echo 'sudo is still available to the agent' >&2; exit 1; fi",
22421
+ ' CURSOR_CLI_DIR="$(mktemp -d "$RUNNER_TEMP/cursor-cli.XXXXXX")"',
22422
+ ` curl --fail --silent --show-error --retry 3 --retry-all-errors --proto '=https' --tlsv1.2 --output "$CURSOR_CLI_DIR/cli.tar.gz" "$CURSOR_CLI_URL"`,
22423
+ ` printf '%s %s\\n' "$CURSOR_CLI_SHA256" "$CURSOR_CLI_DIR/cli.tar.gz" | sha256sum --check --strict --quiet`,
22424
+ ' tar -xzf "$CURSOR_CLI_DIR/cli.tar.gz" -C "$CURSOR_CLI_DIR"',
22425
+ ' mkdir -p "$CURSOR_CONFIG_DIR"',
22426
+ ` printf '%s\\n' '${config2}' > "$CURSOR_CONFIG_DIR/cli-config.json"`,
22427
+ ' "$CURSOR_CLI_DIR/dist-package/cursor-agent" --print --force --disable-auto-update < "$AGENT_PROMPT_FILE"'
22428
+ ];
22429
+ }
22350
22430
  function renderPreparationStep() {
22351
22431
  return [
22352
22432
  " - name: Assess the agent result",
@@ -22580,7 +22660,7 @@ function renderAgentWorkflow(input) {
22580
22660
  );
22581
22661
  }
22582
22662
  const value = parsed.data;
22583
- if (value.agentAction.repository !== AGENT_ACTION_REPOSITORIES[value.agent]) {
22663
+ if (value.agent !== "cursor" && value.agentAction.repository !== AGENT_ACTION_REPOSITORIES[value.agent]) {
22584
22664
  throw new RepositoryAgentConfigError(
22585
22665
  "invalid_template_input",
22586
22666
  `agentAction.repository must be ${AGENT_ACTION_REPOSITORIES[value.agent]}`
@@ -22606,7 +22686,7 @@ function renderAgentWorkflow(input) {
22606
22686
  );
22607
22687
  }
22608
22688
  const workflowPath = AGENT_WORKFLOW_PATHS[value.agent];
22609
- const title = value.agent === "claude" ? "Claude" : "Codex";
22689
+ const title = AGENT_TITLES[value.agent];
22610
22690
  return [
22611
22691
  "# Managed by Setorra. Regenerate with `setorra sync`.",
22612
22692
  "# The agent can edit only; deterministic workflow steps own GitHub mutations.",
@@ -22631,7 +22711,9 @@ function renderAgentWorkflow(input) {
22631
22711
  " contents: write",
22632
22712
  " issues: write",
22633
22713
  " pull-requests: write",
22634
- " id-token: write",
22714
+ // The marketplace actions may exchange an OIDC token; the Cursor CLI never does, and
22715
+ // its agent has a shell that could otherwise request one.
22716
+ ...value.agent === "cursor" ? [] : [" id-token: write"],
22635
22717
  " steps:",
22636
22718
  ...renderProvenanceStep(workflowPath),
22637
22719
  " - name: Check out the handoff commit",
@@ -22662,8 +22744,8 @@ function renderAgentWorkflow(input) {
22662
22744
  ].join("\n");
22663
22745
  }
22664
22746
  function renderCompletionWorkflow(input) {
22665
- WorkflowTemplateInputSchema.shape.botLogin.parse(input.botLogin);
22666
- WorkflowTemplateInputSchema.shape.label.parse(input.label);
22747
+ BotLoginSchema.parse(input.botLogin);
22748
+ LabelSchema.parse(input.label);
22667
22749
  const upload = PinnedActionSchema.parse(input.uploadArtifactAction);
22668
22750
  if (upload.repository !== UPLOAD_ARTIFACT_ACTION_REPOSITORY) {
22669
22751
  throw new Error("completion requires actions/upload-artifact");
@@ -22785,7 +22867,7 @@ function stripProvenance(content) {
22785
22867
 
22786
22868
  // src/workflow/plan.ts
22787
22869
  function managedPaths() {
22788
- return [AGENT_WORKFLOW_PATHS.claude, AGENT_WORKFLOW_PATHS.codex, COMPLETION_WORKFLOW_PATH];
22870
+ return [...Object.values(AGENT_WORKFLOW_PATHS), COMPLETION_WORKFLOW_PATH];
22789
22871
  }
22790
22872
  function planFile(path, desired, existing, force) {
22791
22873
  const current = existing.get(path);
@@ -22806,21 +22888,14 @@ function planFile(path, desired, existing, force) {
22806
22888
  return { kind: "update", path, contents: desired };
22807
22889
  }
22808
22890
  function planRepositoryAgentFiles(input) {
22891
+ const { existing, force, ...template } = input;
22809
22892
  const actions = [
22810
- planFile(COMPLETION_WORKFLOW_PATH, stampProvenance(renderCompletionWorkflow(input)), input.existing, input.force),
22893
+ planFile(COMPLETION_WORKFLOW_PATH, stampProvenance(renderCompletionWorkflow(template)), existing, force),
22811
22894
  planFile(
22812
22895
  AGENT_WORKFLOW_PATHS[input.agent],
22813
- stampProvenance(renderAgentWorkflow({
22814
- agent: input.agent,
22815
- credential: input.credential,
22816
- botLogin: input.botLogin,
22817
- label: input.label,
22818
- checkoutAction: input.checkoutAction,
22819
- agentAction: input.agentAction,
22820
- uploadArtifactAction: input.uploadArtifactAction
22821
- })),
22822
- input.existing,
22823
- input.force
22896
+ stampProvenance(renderAgentWorkflow(template)),
22897
+ existing,
22898
+ force
22824
22899
  )
22825
22900
  ];
22826
22901
  for (const [agent, path] of Object.entries(AGENT_WORKFLOW_PATHS)) {
@@ -22858,6 +22933,10 @@ var AGENT_ACTIONS = {
22858
22933
  version: "v1"
22859
22934
  }
22860
22935
  };
22936
+ var CURSOR_CLI = {
22937
+ version: "2026.10.01-e373342",
22938
+ sha256: "a79726c6e644520e993970be4c45775a6889802b67abe461a677a53219ae28e8"
22939
+ };
22861
22940
  var UPLOAD_ARTIFACT_ACTION = {
22862
22941
  repository: UPLOAD_ARTIFACT_ACTION_REPOSITORY,
22863
22942
  sha: "ea165f8d65b6e75b540449e92b4886f43607fa02",
@@ -22866,6 +22945,8 @@ var UPLOAD_ARTIFACT_ACTION = {
22866
22945
 
22867
22946
  // src/workflow/workflow-validate.ts
22868
22947
  var PINNED_ACTION = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[^@\s]+@[a-f0-9]{40}$/u;
22948
+ var CURSOR_AGENT_INVOCATION = '"$CURSOR_CLI_DIR/dist-package/cursor-agent" --print --force --disable-auto-update < "$AGENT_PROMPT_FILE"';
22949
+ var PINNED_CURSOR_CLI_URL = /^https:\/\/downloads\.cursor\.com\/lab\/[0-9]{4}\.[0-9]{2}\.[0-9]{2}-[a-f0-9]{7,40}\/linux\/x64\/agent-cli-package\.tar\.gz$/u;
22869
22950
  var BASE_SHA_OUTPUT = "${{ steps.provenance.outputs.base_sha }}";
22870
22951
  var HANDOFF_BRANCH = "setorra/${{ steps.provenance.outputs.handoff_id }}";
22871
22952
  function asRecord(value) {
@@ -22942,9 +23023,9 @@ function checkGuards(job, input, findings) {
22942
23023
  push(findings, "missing_label_guard", `The workflow must require the '${input.label}' label.`);
22943
23024
  }
22944
23025
  }
22945
- function checkAgentInputs(step, input, findings) {
23026
+ function checkAgentInputs(step, agent, input, findings) {
22946
23027
  const withInputs = stepWith(step);
22947
- const allowlistInput = AGENT_BOT_ALLOWLIST_INPUTS[input.agent];
23028
+ const allowlistInput = AGENT_BOT_ALLOWLIST_INPUTS[agent];
22948
23029
  const allowlist = asString(withInputs?.[allowlistInput]);
22949
23030
  if (allowlist === void 0) {
22950
23031
  push(findings, "missing_bot_allowlist_input", `The agent step must set '${allowlistInput}'.`);
@@ -22958,11 +23039,38 @@ function checkAgentInputs(step, input, findings) {
22958
23039
  } else if (!allowlist.split(",").map((entry) => entry.trim()).includes(input.botLogin)) {
22959
23040
  push(findings, "bot_allowlist_mismatch", `'${allowlistInput}' must list '${input.botLogin}'.`);
22960
23041
  }
22961
- const credentialInputs = agentCredentialInputs(input.agent);
23042
+ const credentialInputs = agentCredentialInputs(agent);
22962
23043
  if (!credentialInputs.some((name) => (asString(withInputs?.[name]) ?? "").trim() !== "")) {
22963
23044
  push(findings, "missing_credential_input", "The agent step has no supported credential input.");
22964
23045
  }
22965
23046
  }
23047
+ function checkCursorAgent(job, step, findings) {
23048
+ const env = stepEnv(step);
23049
+ const run2 = stepRun(step);
23050
+ if (asString(env?.["CURSOR_API_KEY"]) !== "${{ secrets.CURSOR_API_KEY }}") {
23051
+ push(findings, "missing_credential_input", "The agent step must read 'CURSOR_API_KEY' from secrets.");
23052
+ }
23053
+ if (!PINNED_CURSOR_CLI_URL.test(asString(env?.["CURSOR_CLI_URL"]) ?? "") || !/^[a-f0-9]{64}$/u.test(asString(env?.["CURSOR_CLI_SHA256"]) ?? "") || !run2.includes('"$CURSOR_CLI_URL"') || !run2.includes('"$CURSOR_CLI_SHA256"') || !run2.includes("sha256sum --check --strict")) {
23054
+ push(
23055
+ findings,
23056
+ "unpinned_agent_cli",
23057
+ "The Cursor CLI must be one exact release verified against its SHA-256."
23058
+ );
23059
+ }
23060
+ if (!run2.includes(CURSOR_AGENT_INVOCATION)) {
23061
+ push(findings, "missing_agent_invocation", "The Cursor step must run the pinned CLI on the prompt file.");
23062
+ }
23063
+ if (!run2.includes("if sudo -n true 2>/dev/null; then")) {
23064
+ push(findings, "agent_keeps_sudo", "The Cursor step must drop sudo before the agent runs.");
23065
+ }
23066
+ const permissions = asRecord(job["permissions"]);
23067
+ if (permissions === void 0 || permissions["id-token"] !== void 0) {
23068
+ push(findings, "agent_receives_oidc_token", "The Cursor job must not request an OIDC token.");
23069
+ }
23070
+ }
23071
+ function isAgentStep(agent, step) {
23072
+ return agent === "cursor" ? asString(stepEnv(step)?.["CURSOR_CLI_URL"])?.startsWith(CURSOR_CLI_DOWNLOAD_PREFIX) === true : stepUses(step)?.startsWith(`${AGENT_ACTION_REPOSITORIES[agent]}@`) === true;
23073
+ }
22966
23074
  function oneStep(steps, id) {
22967
23075
  const matches = steps.filter((step) => stepId(step) === id);
22968
23076
  return matches.length === 1 ? matches[0] : void 0;
@@ -23073,18 +23181,16 @@ function validateAgentWorkflow(document, input) {
23073
23181
  for (const { steps } of jobs) {
23074
23182
  checkPinning(steps, findings);
23075
23183
  }
23076
- const expectedRepository = AGENT_ACTION_REPOSITORIES[input.agent];
23077
- const otherRepositories = Object.entries(AGENT_ACTION_REPOSITORIES).filter(([agent]) => agent !== input.agent).map(([, repository]) => repository);
23184
+ const otherAgents = AGENT_KINDS.filter((agent) => agent !== input.agent);
23078
23185
  const expectedMatches = [];
23079
23186
  const agentSteps = [];
23080
23187
  let foundOther = false;
23081
23188
  for (const entry of jobs) {
23082
23189
  for (const step of entry.steps) {
23083
- const uses = stepUses(step);
23084
- if (uses?.startsWith(`${expectedRepository}@`)) {
23190
+ if (isAgentStep(input.agent, step)) {
23085
23191
  expectedMatches.push({ ...entry, step });
23086
23192
  agentSteps.push(step);
23087
- } else if (uses !== void 0 && otherRepositories.some((repository) => uses.startsWith(`${repository}@`))) {
23193
+ } else if (otherAgents.some((agent) => isAgentStep(agent, step))) {
23088
23194
  foundOther = true;
23089
23195
  agentSteps.push(step);
23090
23196
  }
@@ -23118,12 +23224,16 @@ function validateAgentWorkflow(document, input) {
23118
23224
  push(
23119
23225
  findings,
23120
23226
  foundOther ? "unexpected_agent_action" : "missing_agent_action",
23121
- foundOther ? "The workflow runs the wrong agent action." : `No step uses '${expectedRepository}'.`
23227
+ foundOther ? "The workflow runs the wrong agent action." : input.agent === "cursor" ? "No step runs the pinned Cursor CLI." : `No step uses '${AGENT_ACTION_REPOSITORIES[input.agent]}'.`
23122
23228
  );
23123
23229
  return { valid: false, findings };
23124
23230
  }
23125
23231
  checkGuards(matched.job, input, findings);
23126
- checkAgentInputs(matched.step, input, findings);
23232
+ if (input.agent === "cursor") {
23233
+ checkCursorAgent(matched.job, matched.step, findings);
23234
+ } else {
23235
+ checkAgentInputs(matched.step, input.agent, input, findings);
23236
+ }
23127
23237
  checkContractControls(workflow, matched.steps, allSteps, findings);
23128
23238
  return { valid: !findings.some((finding) => finding.severity === "error"), findings };
23129
23239
  }
@@ -23134,7 +23244,8 @@ var BOT_LOGIN = process.env["SETORRA_BOT_LOGIN"] ?? "setorra[bot]";
23134
23244
  var LABEL = process.env["SETORRA_LABEL"] ?? "api-migration";
23135
23245
  var DEFAULT_CREDENTIALS = {
23136
23246
  claude: "oauth_token",
23137
- codex: "api_key"
23247
+ codex: "api_key",
23248
+ cursor: "api_key"
23138
23249
  };
23139
23250
  var UsageError = class extends Error {
23140
23251
  };
@@ -23142,7 +23253,7 @@ function parseArguments(argv) {
23142
23253
  const [command, ...rest] = argv;
23143
23254
  if (command !== "init" && command !== "status" && command !== "sync") {
23144
23255
  throw new UsageError(
23145
- "usage: setorra <init|status|sync> [claude|codex] [--credential api_key|oauth_token] [--force] [--dry-run]"
23256
+ "usage: setorra <init|status|sync> [claude|codex|cursor] [--credential api_key|oauth_token] [--force] [--dry-run]"
23146
23257
  );
23147
23258
  }
23148
23259
  let agent;
@@ -23151,7 +23262,7 @@ function parseArguments(argv) {
23151
23262
  let dryRun = false;
23152
23263
  for (let index = 0; index < rest.length; index += 1) {
23153
23264
  const argument = rest[index];
23154
- if (argument === "claude" || argument === "codex") {
23265
+ if (AGENT_KINDS.some((kind) => kind === argument)) {
23155
23266
  agent = argument;
23156
23267
  } else if (argument === "--force") {
23157
23268
  force = true;
@@ -23198,10 +23309,14 @@ function readManaged(root) {
23198
23309
  }
23199
23310
  return existing;
23200
23311
  }
23312
+ var AGENT_WORKFLOW_MARKERS = {
23313
+ ...AGENT_ACTION_REPOSITORIES,
23314
+ cursor: CURSOR_CLI_DOWNLOAD_PREFIX
23315
+ };
23201
23316
  function detectAgent(existing) {
23202
- for (const agent of ["claude", "codex"]) {
23317
+ for (const agent of AGENT_KINDS) {
23203
23318
  const workflow = existing.get(AGENT_WORKFLOW_PATHS[agent]);
23204
- if (workflow?.includes(AGENT_ACTION_REPOSITORIES[agent]) === true) {
23319
+ if (workflow?.includes(AGENT_WORKFLOW_MARKERS[agent]) === true) {
23205
23320
  return agent;
23206
23321
  }
23207
23322
  }
@@ -23324,7 +23439,7 @@ pull request updated: ${url2}`);
23324
23439
  }
23325
23440
  function runStatus(options) {
23326
23441
  const existing = readManaged(options.root);
23327
- const present = ["claude", "codex"].filter((agent2) => existing.has(AGENT_WORKFLOW_PATHS[agent2]));
23442
+ const present = AGENT_KINDS.filter((agent2) => existing.has(AGENT_WORKFLOW_PATHS[agent2]));
23328
23443
  if (present.length === 0) {
23329
23444
  console.error("no migration workflow; run `setorra init <agent>` first");
23330
23445
  return 1;
@@ -23332,7 +23447,7 @@ function runStatus(options) {
23332
23447
  let failures = 0;
23333
23448
  if (present.length > 1) {
23334
23449
  console.error(
23335
- `error: ${present.length} migration workflows exist (${present.join(", ")}). Both would run on the same issue.`
23450
+ `error: ${present.length} migration workflows exist (${present.join(", ")}). Each would run on the same issue.`
23336
23451
  );
23337
23452
  failures += 1;
23338
23453
  }
@@ -23383,7 +23498,7 @@ function runReconcile(options) {
23383
23498
  const agent = options.command === "sync" ? detectAgent(existing) : options.agent ?? detectAgent(existing);
23384
23499
  if (agent === void 0) {
23385
23500
  console.error(
23386
- options.command === "sync" ? "no migration workflow; run `setorra init <agent>` first" : "could not determine the agent; pass `claude` or `codex`"
23501
+ options.command === "sync" ? "no migration workflow; run `setorra init <agent>` first" : "could not determine the agent; pass `claude`, `codex` or `cursor`"
23387
23502
  );
23388
23503
  return 1;
23389
23504
  }
@@ -23396,12 +23511,11 @@ function runReconcile(options) {
23396
23511
  return 1;
23397
23512
  }
23398
23513
  const plan = planRepositoryAgentFiles({
23399
- agent,
23400
23514
  credential,
23401
23515
  botLogin: BOT_LOGIN,
23402
23516
  label: LABEL,
23403
23517
  checkoutAction: CHECKOUT_ACTION,
23404
- agentAction: AGENT_ACTIONS[agent],
23518
+ ...agent === "cursor" ? { agent, cursorCli: CURSOR_CLI } : { agent, agentAction: AGENT_ACTIONS[agent] },
23405
23519
  uploadArtifactAction: UPLOAD_ARTIFACT_ACTION,
23406
23520
  existing,
23407
23521
  force: options.force
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "setorra",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Configure automated API migration for a GitHub repository.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",