@jonaspauleta/cursor-bridge 0.2.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/classify.d.ts +8 -0
  4. package/dist/classify.js +62 -0
  5. package/dist/classify.js.map +1 -0
  6. package/dist/codex.classify.d.ts +15 -0
  7. package/dist/codex.classify.js +34 -0
  8. package/dist/codex.classify.js.map +1 -0
  9. package/dist/codex.d.ts +20 -0
  10. package/dist/codex.js +158 -0
  11. package/dist/codex.js.map +1 -0
  12. package/dist/codex.preflight.d.ts +14 -0
  13. package/dist/codex.preflight.js +27 -0
  14. package/dist/codex.preflight.js.map +1 -0
  15. package/dist/cursor.d.ts +17 -0
  16. package/dist/cursor.js +88 -0
  17. package/dist/cursor.js.map +1 -0
  18. package/dist/errors.d.ts +36 -0
  19. package/dist/errors.js +8 -0
  20. package/dist/errors.js.map +1 -0
  21. package/dist/imagegen.tool.d.ts +57 -0
  22. package/dist/imagegen.tool.js +79 -0
  23. package/dist/imagegen.tool.js.map +1 -0
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +26 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/logger.d.ts +16 -0
  28. package/dist/logger.js +27 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/preflight.d.ts +11 -0
  31. package/dist/preflight.js +35 -0
  32. package/dist/preflight.js.map +1 -0
  33. package/dist/runner.d.ts +25 -0
  34. package/dist/runner.js +35 -0
  35. package/dist/runner.js.map +1 -0
  36. package/dist/server.d.ts +5 -0
  37. package/dist/server.js +19 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/tool.d.ts +38 -0
  40. package/dist/tool.js +41 -0
  41. package/dist/tool.js.map +1 -0
  42. package/dist/validate.d.ts +23 -0
  43. package/dist/validate.js +112 -0
  44. package/dist/validate.js.map +1 -0
  45. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 João Paulo Santos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # cursor-bridge
2
+
3
+ A thin MCP server bridging coding-agent CLIs to Claude Code (Opus). It exposes two
4
+ tools: `run_cursor_agent`, which offloads implementation/review tasks to Cursor's
5
+ `cursor-agent` CLI (Composer 2.5 Fast by default), and `generate_image`, which
6
+ generates/edits images with `gpt-image-2` by driving the Codex CLI. The final review
7
+ gate stays on Opus.
8
+
9
+ ## Use via npx (recommended)
10
+
11
+ Published as [`@jonaspauleta/cursor-bridge`](https://www.npmjs.com/package/@jonaspauleta/cursor-bridge). No clone or build needed:
12
+
13
+ # one-off
14
+ CURSOR_BRIDGE_ALLOWED_ROOT="$(pwd)" npx -y @jonaspauleta/cursor-bridge
15
+
16
+ Register with Claude Code:
17
+
18
+ claude mcp add --transport stdio --scope project \
19
+ --env CURSOR_BRIDGE_ALLOWED_ROOT="$(pwd)" \
20
+ cursor-bridge -- npx -y @jonaspauleta/cursor-bridge
21
+
22
+ Or as project-scoped `.mcp.json`:
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "cursor-bridge": {
28
+ "type": "stdio",
29
+ "command": "npx",
30
+ "args": ["-y", "@jonaspauleta/cursor-bridge"],
31
+ "env": {
32
+ "CURSOR_API_KEY": "${CURSOR_API_KEY:-}",
33
+ "CURSOR_BRIDGE_ALLOWED_ROOT": "${CLAUDE_PROJECT_DIR}"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ You can also run straight from GitHub without npm: `npx github:jonaspauleta/cursor-bridge`
41
+ (npx clones and builds via the `prepare` hook).
42
+
43
+ **Prerequisites (not bundled):** the `cursor-agent` CLI (for `run_cursor_agent`) and/or
44
+ the `codex` CLI v0.134.0+ (for `generate_image`) must be installed on PATH and logged in
45
+ (`cursor-agent login` / `codex login`). `CURSOR_BRIDGE_ALLOWED_ROOT` is required and must
46
+ be an absolute path. (This repo's own committed `.mcp.json` intentionally keeps pointing
47
+ at local `dist/` for development.)
48
+
49
+ ## Build & run
50
+
51
+ npm install
52
+ npm run build # -> dist/index.js
53
+ node dist/index.js # stdio MCP server (Claude Code launches this for you)
54
+
55
+ ## Register with Claude Code
56
+
57
+ Either commit `.mcp.json` (project scope) or run:
58
+
59
+ # Export CURSOR_API_KEY in your shell profile — do NOT put it on argv (ps/history leak).
60
+ claude mcp add --transport stdio --scope project \
61
+ --env CURSOR_BRIDGE_ALLOWED_ROOT="$(pwd)" \
62
+ cursor-bridge -- node dist/index.js
63
+
64
+ `CURSOR_API_KEY` is read from the ambient environment via `.mcp.json`'s
65
+ `${CURSOR_API_KEY}` and never hardcoded or passed on a command line. If unset,
66
+ `cursor-agent` falls back to your interactive `cursor-agent login` session.
67
+ `CURSOR_BRIDGE_ALLOWED_ROOT` is required and must be an absolute path.
68
+
69
+ ## The tool: `run_cursor_agent`
70
+
71
+ | input | type | default | notes |
72
+ |-------|------|---------|-------|
73
+ | `prompt` | string | — | task/prompt sent to the worker |
74
+ | `cwd` | string | — | working dir; validated against `CURSOR_BRIDGE_ALLOWED_ROOT` |
75
+ | `mode` | `default`/`plan`/`ask` | `default` | default = read-write (`--force`); plan/ask = read-only |
76
+ | `model` | string | `composer-2.5-fast` | allowlisted (`composer-2.5-fast`, `composer-2.5`, `claude-4.6-sonnet-medium`) |
77
+ | `timeoutMs` | number | `270000` | wall-clock; wrapper SIGKILLs cursor-agent past this |
78
+
79
+ Returns the worker's final text on `SUCCESS` (plus `structuredContent` with usage).
80
+ Every other outcome returns `isError: true` with a taxonomy-tagged message.
81
+
82
+ ## The tool: `generate_image`
83
+
84
+ Generates or edits images with `gpt-image-2` by driving `codex exec` with the
85
+ `$imagegen` skill. **Subscription-only:** it uses your `codex login` (ChatGPT/Codex
86
+ plan) auth and counts toward your Codex usage limits — no API key. `OPENAI_API_KEY` is
87
+ scrubbed from the child env so an ambient key can't silently switch you to API billing.
88
+
89
+ | input | type | default | notes |
90
+ |-------|------|---------|-------|
91
+ | `prompt` | string | — | image task; free-form, may include edit/iterate instructions |
92
+ | `outputDir` | string | `<allowedRoot>/generated-images` | absolute, validated inside the allowed root |
93
+ | `referenceImages` | string[] | — | paths attached via Codex `-i` (edit/iterate); png/jpg/jpeg/webp, inside root, max 8 |
94
+ | `count` | number | `1` | 1–10 (gpt-image-2 ceiling) |
95
+ | `size` | enum | `auto` | `auto`/`1024x1024`/`1536x1024`/`1024x1536` (best-effort directive) |
96
+ | `quality` | enum | `auto` | `auto`/`low`/`medium`/`high` (best-effort directive) |
97
+ | `inlineImages` | boolean | `false` | also return generated images as base64 blocks (first 4) |
98
+ | `timeoutMs` | number | `300000` | wall-clock; wrapper SIGKILLs codex past this |
99
+
100
+ Each call writes into a fresh `<outputDir>/<runId>/` subdir. On `SUCCESS` it returns the
101
+ generated file paths (and Codex's summary); every other outcome returns `isError: true`.
102
+ `size`/`quality`/`count` are natural-language directives templated into the prompt (the
103
+ built-in `image_gen` tool is invoked by the model, not via CLI flags), so they are
104
+ best-effort rather than hard API parameters.
105
+
106
+ **Status taxonomy:** `SUCCESS | NO_IMAGE | AUTH_REQUIRED | TIMEOUT | TOOL_ERROR`. Success
107
+ means ≥1 image file actually landed on disk — the exit code is not trusted.
108
+
109
+ **Requirements:** the `codex` CLI (v0.134.0+) must be installed and on the server's PATH,
110
+ and you must be logged in (`codex login`; check with `codex login status`). Override the
111
+ binary with `CODEX_AGENT_BIN`. Unlike `cursor-agent`, Codex's `-s workspace-write`
112
+ sandbox is genuinely enforced, so the image worker can only write within the workspace
113
+ root and cannot run destructive shell outside it.
114
+
115
+ Smoke test: `npm run test:smoke:codex` (live gpt-image-2 call; requires `codex login`).
116
+
117
+ ## Status taxonomy
118
+
119
+ `SUCCESS | AUTH_REQUIRED | TIMEOUT | TOOL_ERROR | MALFORMED`. Success requires a
120
+ parsed `{"type":"result"}` envelope with `is_error:false`; the exit code is NOT
121
+ trusted (the auth-failure path is exit-code-unstable).
122
+
123
+ ## Auth pre-flight
124
+
125
+ cursor-agent status --format json # {"isAuthenticated":true,...}
126
+
127
+ The server exposes `preflightAuth()` for a fail-fast check before a real call.
128
+
129
+ ## Security notes
130
+
131
+ - argv is always an array; `spawn` runs with no shell — no command injection via the wrapper.
132
+ - All MCP-tool inputs pass `src/validate.ts` (mode/model allowlist, cwd allowlist + symlink-safe) before reaching argv. The git-diff SHAs used by the *final review gate* are NOT validated here — they live in the orchestrator's shell (Part B) and are resolved mechanically with `git rev-parse`, never taken from task text.
133
+ - Secrets come from env only; the logger redacts secret-looking values and never logs prompt bodies or raw output (stderr only). Never pass `CURSOR_API_KEY` on a command line (`ps`/history leak) — export it in your shell profile so `.mcp.json`'s `${CURSOR_API_KEY}` picks it up.
134
+ - `CURSOR_BRIDGE_ALLOWED_ROOT` is REQUIRED and must be absolute; the server refuses to start otherwise and logs the resolved root. Set it as narrowly as possible.
135
+ - **Blast radius — VERIFIED on `cursor-agent` v2026.05.28: `--sandbox enabled` does NOT contain a `--force` worker on this build.** A probe worker wrote a file outside `--workspace` (into `$HOME`) despite `--sandbox enabled`. `--mode plan`/`ask` are genuinely read-only and safe. But `mode:default` passes `--force` (auto-approves arbitrary shell + writes) and `--workspace` is only a cwd, not a jail — so a default-mode worker is effectively **full user-level shell**: it can read `~/.ssh`, exfiltrate over the network, or `git push`. The wrapper still passes `--sandbox enabled` (harmless future-proofing should a later build enforce it — re-run the Task A20 Step 5 probe to recheck), but **today you MUST treat `mode:default` as unsandboxed** and run workers ONLY in disposable/throwaway git worktrees with NO access to real secrets. The `assertWorkspacePath` allow-root and the Opus security gate are the real controls; cwd validation alone does not contain `--force`.
136
+
137
+ ## Tests
138
+
139
+ npm test # unit + in-memory round-trip (no network)
140
+ npm run test:smoke # RUN_CURSOR_SMOKE=1; live call to Composer 2.5 Fast
@@ -0,0 +1,8 @@
1
+ import type { CursorResult } from "./errors.js";
2
+ export declare const stripAnsi: (s: string) => string;
3
+ /**
4
+ * Pure classification of captured streams into a CursorResult.
5
+ * Never spawns, never times. The exit code is intentionally ignored: the
6
+ * auth-failure path is exit-code-unstable and emits no result envelope.
7
+ */
8
+ export declare function classify(stdout: string, stderr: string): CursorResult;
@@ -0,0 +1,62 @@
1
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
2
+ export const stripAnsi = (s) => s.replace(ANSI_RE, "");
3
+ const AUTH_MARKERS = [
4
+ /API key is invalid/i,
5
+ /not logged in/i,
6
+ /\bauthenticate\b/i,
7
+ /unauthorized/i,
8
+ /session expired/i,
9
+ /provided API key/i,
10
+ ];
11
+ const RESULT_HINT = /"type"\s*:\s*"result"/;
12
+ /**
13
+ * Pure classification of captured streams into a CursorResult.
14
+ * Never spawns, never times. The exit code is intentionally ignored: the
15
+ * auth-failure path is exit-code-unstable and emits no result envelope.
16
+ */
17
+ export function classify(stdout, stderr) {
18
+ let envelope = null;
19
+ let sawUnparseableResultLine = false;
20
+ for (const rawLine of stripAnsi(stdout).split("\n")) {
21
+ const line = rawLine.trim();
22
+ if (!line || line[0] !== "{")
23
+ continue;
24
+ let obj;
25
+ try {
26
+ obj = JSON.parse(line);
27
+ }
28
+ catch {
29
+ // Looks like JSON, mentions a result envelope, but won't parse -> malformed candidate.
30
+ if (RESULT_HINT.test(line))
31
+ sawUnparseableResultLine = true;
32
+ continue;
33
+ }
34
+ if (obj && typeof obj === "object" && obj.type === "result") {
35
+ envelope = obj;
36
+ break;
37
+ }
38
+ }
39
+ if (envelope) {
40
+ if (envelope.is_error === false && typeof envelope.result === "string") {
41
+ return {
42
+ status: "SUCCESS",
43
+ result: envelope.result,
44
+ usage: envelope.usage,
45
+ sessionId: envelope.session_id,
46
+ requestId: envelope.request_id,
47
+ raw: stdout,
48
+ };
49
+ }
50
+ // Parsed a result envelope, but it's an error / has no string result.
51
+ return { status: "TOOL_ERROR", error: "cursor-agent returned an error result", raw: stdout };
52
+ }
53
+ if (sawUnparseableResultLine) {
54
+ return { status: "MALFORMED", error: "result line present but unparseable", raw: stdout };
55
+ }
56
+ const scan = stripAnsi(`${stdout}\n${stderr}`);
57
+ if (AUTH_MARKERS.some((re) => re.test(scan))) {
58
+ return { status: "AUTH_REQUIRED", error: "cursor-agent reported an auth problem", raw: scan };
59
+ }
60
+ return { status: "TOOL_ERROR", error: "no result envelope emitted", raw: scan };
61
+ }
62
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,GAAG,iBAAiB,CAAC;AAClC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAEvE,MAAM,YAAY,GAAsB;IACtC,qBAAqB;IACrB,gBAAgB;IAChB,mBAAmB;IACnB,eAAe;IACf,kBAAkB;IAClB,mBAAmB;CACpB,CAAC;AAEF,MAAM,WAAW,GAAG,uBAAuB,CAAC;AAW5C;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,MAAc;IACrD,IAAI,QAAQ,GAA0B,IAAI,CAAC;IAC3C,IAAI,wBAAwB,GAAG,KAAK,CAAC;IAErC,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,SAAS;QACvC,IAAI,GAAY,CAAC;QACjB,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,uFAAuF;YACvF,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,wBAAwB,GAAG,IAAI,CAAC;YAC5D,SAAS;QACX,CAAC;QACD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAK,GAA0B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACpF,QAAQ,GAAG,GAAqB,CAAC;YACjC,MAAM;QACR,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,QAAQ,KAAK,KAAK,IAAI,OAAO,QAAQ,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACvE,OAAO;gBACL,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,SAAS,EAAE,QAAQ,CAAC,UAAU;gBAC9B,SAAS,EAAE,QAAQ,CAAC,UAAU;gBAC9B,GAAG,EAAE,MAAM;aACZ,CAAC;QACJ,CAAC;QACD,sEAAsE;QACtE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,uCAAuC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAC/F,CAAC;IAED,IAAI,wBAAwB,EAAE,CAAC;QAC7B,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,qCAAqC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAC5F,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC;IAC/C,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,uCAAuC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IAChG,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,4BAA4B,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AAClF,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { ImageResult } from "./errors.js";
2
+ export interface ClassifyImageInput {
3
+ /** Image files found in the run subdir (absolute paths). */
4
+ files: string[];
5
+ /** Contents of --output-last-message (may be empty). */
6
+ summary: string;
7
+ stdout: string;
8
+ stderr: string;
9
+ }
10
+ /**
11
+ * Pure classification of an image-generation run. Success is determined by the
12
+ * FILESYSTEM (image files on disk), never the exit code. The caller (codex.ts)
13
+ * handles TIMEOUT and spawn/validation TOOL_ERROR before calling this.
14
+ */
15
+ export declare function classifyImage(input: ClassifyImageInput): ImageResult;
@@ -0,0 +1,34 @@
1
+ import { stripAnsi } from "./classify.js";
2
+ const AUTH_MARKERS = [
3
+ /not logged in/i,
4
+ /codex login/i,
5
+ /unauthorized/i,
6
+ /\b401\b/,
7
+ /please (?:run )?login/i,
8
+ ];
9
+ /**
10
+ * Pure classification of an image-generation run. Success is determined by the
11
+ * FILESYSTEM (image files on disk), never the exit code. The caller (codex.ts)
12
+ * handles TIMEOUT and spawn/validation TOOL_ERROR before calling this.
13
+ */
14
+ export function classifyImage(input) {
15
+ if (input.files.length > 0) {
16
+ return {
17
+ status: "SUCCESS",
18
+ images: input.files,
19
+ summary: input.summary.trim() || undefined,
20
+ count: input.files.length,
21
+ };
22
+ }
23
+ const scan = stripAnsi(`${input.stdout}\n${input.stderr}`);
24
+ if (AUTH_MARKERS.some((re) => re.test(scan))) {
25
+ return { status: "AUTH_REQUIRED", error: "codex reported an auth problem", raw: scan };
26
+ }
27
+ return {
28
+ status: "NO_IMAGE",
29
+ error: "codex produced no image file",
30
+ summary: input.summary.trim() || undefined,
31
+ raw: scan,
32
+ };
33
+ }
34
+ //# sourceMappingURL=codex.classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex.classify.js","sourceRoot":"","sources":["../src/codex.classify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG1C,MAAM,YAAY,GAAsB;IACtC,gBAAgB;IAChB,cAAc;IACd,eAAe;IACf,SAAS;IACT,wBAAwB;CACzB,CAAC;AAWF;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,KAAK,CAAC,KAAK;YACnB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,SAAS;YAC1C,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM;SAC1B,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3D,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,gCAAgC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACzF,CAAC;IACD,OAAO;QACL,MAAM,EAAE,UAAU;QAClB,KAAK,EAAE,8BAA8B;QACrC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,SAAS;QAC1C,GAAG,EAAE,IAAI;KACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { type Runner } from "./runner.js";
2
+ import { type ImageSize, type ImageQuality } from "./validate.js";
3
+ import { type LogFields } from "./logger.js";
4
+ import type { ImageResult } from "./errors.js";
5
+ export interface GenerateImageOptions {
6
+ outputDir?: string;
7
+ referenceImages?: string[];
8
+ count?: number;
9
+ size?: ImageSize;
10
+ quality?: ImageQuality;
11
+ timeoutMs?: number;
12
+ allowedRoot?: string;
13
+ env?: NodeJS.ProcessEnv;
14
+ runner?: Runner;
15
+ log?: (fields: LogFields) => void;
16
+ bin?: string;
17
+ /** Injectable for deterministic test output subdirs; defaults to timestamp+random. */
18
+ runId?: string;
19
+ }
20
+ export declare function generateImage(prompt: string, opts?: GenerateImageOptions): Promise<ImageResult>;
package/dist/codex.js ADDED
@@ -0,0 +1,158 @@
1
+ import { mkdirSync, readdirSync, readFileSync, rmSync, copyFileSync } from "node:fs";
2
+ import { join, isAbsolute } from "node:path";
3
+ import { tmpdir, homedir } from "node:os";
4
+ import { realRunner, collectStream } from "./runner.js";
5
+ import { classifyImage } from "./codex.classify.js";
6
+ import { assertOutputDir, assertReferenceImages, assertCount, assertSize, assertQuality, } from "./validate.js";
7
+ import { createLogger } from "./logger.js";
8
+ import { ValidationError } from "./errors.js";
9
+ const DEFAULT_TIMEOUT_MS = 300_000;
10
+ const IMAGE_EXT_RE = /\.(png|jpe?g|webp)$/i;
11
+ /** Builds the natural-language directive sent to codex (size/quality/count are best-effort hints). */
12
+ function buildDirective(prompt, count, size, quality, runSubdir) {
13
+ const sizeClause = size === "auto" ? "at the most appropriate size" : `at ${size}`;
14
+ const qualityClause = quality === "auto" ? "" : ` Use ${quality} quality.`;
15
+ return (`$imagegen\n${prompt}\n\n` +
16
+ `Generate exactly ${count} image${count === 1 ? "" : "s"} ${sizeClause}.${qualityClause} ` +
17
+ `Save every generated image as a PNG file into the directory ${runSubdir}. ` +
18
+ `Do not write any other files.`);
19
+ }
20
+ function safeReaddir(dir) {
21
+ try {
22
+ return readdirSync(dir);
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ function scanImages(dir) {
29
+ return safeReaddir(dir)
30
+ .filter((f) => IMAGE_EXT_RE.test(f))
31
+ .map((f) => join(dir, f))
32
+ .sort();
33
+ }
34
+ export async function generateImage(prompt, opts = {}) {
35
+ const runner = opts.runner ?? realRunner;
36
+ const log = opts.log ?? createLogger();
37
+ const bin = opts.bin ?? process.env.CODEX_AGENT_BIN ?? "codex";
38
+ const env = opts.env ?? process.env;
39
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
40
+ let refs;
41
+ let count;
42
+ let size;
43
+ let quality;
44
+ let runSubdir;
45
+ let allowedRoot;
46
+ try {
47
+ allowedRoot = opts.allowedRoot ?? process.env.CURSOR_BRIDGE_ALLOWED_ROOT ?? "";
48
+ if (!allowedRoot || !isAbsolute(allowedRoot)) {
49
+ throw new ValidationError("CURSOR_BRIDGE_ALLOWED_ROOT must be set to an absolute path (refusing to default to cwd)");
50
+ }
51
+ const outputDir = assertOutputDir(opts.outputDir ?? join(allowedRoot, "generated-images"), allowedRoot);
52
+ count = assertCount(opts.count ?? 1);
53
+ size = assertSize(opts.size ?? "auto");
54
+ quality = assertQuality(opts.quality ?? "auto");
55
+ refs = assertReferenceImages(opts.referenceImages ?? [], allowedRoot);
56
+ const runId = opts.runId ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
57
+ runSubdir = join(outputDir, runId);
58
+ mkdirSync(runSubdir, { recursive: true });
59
+ }
60
+ catch (e) {
61
+ const r = { status: "TOOL_ERROR", error: e instanceof ValidationError ? e.message : String(e) };
62
+ log({ event: "codex_imagegen", level: "error", status: r.status });
63
+ return r;
64
+ }
65
+ const promptText = buildDirective(prompt, count, size, quality, runSubdir);
66
+ const lastMsgFile = join(tmpdir(), `codex-lastmsg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`);
67
+ const args = [
68
+ "exec",
69
+ promptText,
70
+ "-C", allowedRoot,
71
+ "-s", "workspace-write",
72
+ "--skip-git-repo-check",
73
+ "--add-dir", runSubdir,
74
+ "--output-last-message", lastMsgFile,
75
+ "--color", "never",
76
+ ];
77
+ for (const ref of refs) {
78
+ args.push("--image", ref);
79
+ }
80
+ // Subscription-only: scrub OPENAI_API_KEY so an ambient key can't switch codex to API billing.
81
+ const childEnv = { ...env };
82
+ delete childEnv.OPENAI_API_KEY;
83
+ // Snapshot codex's default image dir so we can recover images if it ignores the save-path directive.
84
+ const homeImagesDir = join(env.CODEX_HOME ?? join(homedir(), ".codex"), "generated_images");
85
+ const beforeHome = new Set(safeReaddir(homeImagesDir));
86
+ const startedAt = Date.now();
87
+ const child = runner(bin, args, { env: childEnv, cwd: allowedRoot });
88
+ const outP = collectStream(child.stdout);
89
+ const errP = collectStream(child.stderr);
90
+ try {
91
+ let timedOut = false;
92
+ const timer = setTimeout(() => {
93
+ timedOut = true;
94
+ child.kill("SIGKILL");
95
+ }, timeoutMs);
96
+ let spawnError;
97
+ try {
98
+ await child.done;
99
+ }
100
+ catch (e) {
101
+ spawnError = e;
102
+ }
103
+ clearTimeout(timer);
104
+ const durationMs = Date.now() - startedAt;
105
+ if (timedOut) {
106
+ const r = { status: "TIMEOUT", error: `killed after ${timeoutMs}ms`, raw: outP.current() };
107
+ log({ event: "codex_imagegen", level: "warn", status: r.status, durationMs });
108
+ return r;
109
+ }
110
+ if (spawnError) {
111
+ const r = { status: "TOOL_ERROR", error: spawnError.message };
112
+ log({ event: "codex_imagegen", level: "error", status: r.status, durationMs });
113
+ return r;
114
+ }
115
+ const [out, err] = await Promise.all([outP.done, errP.done]);
116
+ let summary = "";
117
+ try {
118
+ summary = readFileSync(lastMsgFile, "utf8");
119
+ }
120
+ catch {
121
+ /* no last-message file — summary stays empty */
122
+ }
123
+ let files = scanImages(runSubdir);
124
+ if (files.length === 0) {
125
+ // Fallback: codex may have saved to its default ~/.codex/generated_images instead.
126
+ // Copy any NEW images (by filename) into our run subdir. Defensive; covered by smoke.
127
+ for (const f of safeReaddir(homeImagesDir)) {
128
+ if (IMAGE_EXT_RE.test(f) && !beforeHome.has(f)) {
129
+ try {
130
+ copyFileSync(join(homeImagesDir, f), join(runSubdir, f));
131
+ }
132
+ catch {
133
+ /* ignore */
134
+ }
135
+ }
136
+ }
137
+ files = scanImages(runSubdir);
138
+ }
139
+ const r = classifyImage({ files, summary, stdout: out, stderr: err });
140
+ log({
141
+ event: "codex_imagegen",
142
+ level: r.status === "SUCCESS" ? "info" : "warn",
143
+ status: r.status,
144
+ durationMs,
145
+ count: r.count,
146
+ });
147
+ return r;
148
+ }
149
+ finally {
150
+ try {
151
+ rmSync(lastMsgFile, { force: true });
152
+ }
153
+ catch {
154
+ /* ignore */
155
+ }
156
+ }
157
+ }
158
+ //# sourceMappingURL=codex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex.js","sourceRoot":"","sources":["../src/codex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { type Runner } from "./runner.js";
2
+ export interface CodexPreflightResult {
3
+ ok: boolean;
4
+ reason?: string;
5
+ }
6
+ /**
7
+ * Fail-fast auth check via `codex login status`. "Not logged in" is checked BEFORE
8
+ * "logged in" because the negative string contains the positive substring.
9
+ */
10
+ export declare function preflightCodexAuth(opts?: {
11
+ env?: NodeJS.ProcessEnv;
12
+ runner?: Runner;
13
+ bin?: string;
14
+ }): Promise<CodexPreflightResult>;
@@ -0,0 +1,27 @@
1
+ import { realRunner, collectStream } from "./runner.js";
2
+ import { stripAnsi } from "./classify.js";
3
+ /**
4
+ * Fail-fast auth check via `codex login status`. "Not logged in" is checked BEFORE
5
+ * "logged in" because the negative string contains the positive substring.
6
+ */
7
+ export async function preflightCodexAuth(opts = {}) {
8
+ const runner = opts.runner ?? realRunner;
9
+ const bin = opts.bin ?? process.env.CODEX_AGENT_BIN ?? "codex";
10
+ const child = runner(bin, ["login", "status"], { env: opts.env ?? process.env });
11
+ const outP = collectStream(child.stdout);
12
+ const errP = collectStream(child.stderr);
13
+ try {
14
+ await child.done;
15
+ }
16
+ catch (e) {
17
+ return { ok: false, reason: e.message };
18
+ }
19
+ const [out, err] = await Promise.all([outP.done, errP.done]);
20
+ const clean = stripAnsi(`${out}\n${err}`);
21
+ if (/not logged in/i.test(clean))
22
+ return { ok: false, reason: "codex reports not logged in" };
23
+ if (/logged in/i.test(clean))
24
+ return { ok: true };
25
+ return { ok: false, reason: "could not determine codex login status" };
26
+ }
27
+ //# sourceMappingURL=codex.preflight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex.preflight.js","sourceRoot":"","sources":["../src/codex.preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAe,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAO1C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAmE,EAAE;IAErE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC;IAC/D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,IAAI,CAAC;IACnB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;IACrD,CAAC;IACD,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,GAAG,KAAK,GAAG,EAAE,CAAC,CAAC;IAC1C,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,6BAA6B,EAAE,CAAC;IAC9F,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAClD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,wCAAwC,EAAE,CAAC;AACzE,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { type Runner } from "./runner.js";
2
+ import { type CursorMode } from "./validate.js";
3
+ import { type LogFields } from "./logger.js";
4
+ import type { CursorResult } from "./errors.js";
5
+ export interface RunOptions {
6
+ cwd: string;
7
+ mode?: CursorMode;
8
+ model?: string;
9
+ timeoutMs?: number;
10
+ allowedRoot?: string;
11
+ sandbox?: "enabled" | "disabled";
12
+ env?: NodeJS.ProcessEnv;
13
+ runner?: Runner;
14
+ log?: (fields: LogFields) => void;
15
+ bin?: string;
16
+ }
17
+ export declare function runCursorAgent(prompt: string, opts: RunOptions): Promise<CursorResult>;
package/dist/cursor.js ADDED
@@ -0,0 +1,88 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { realRunner, collectStream } from "./runner.js";
3
+ import { classify } from "./classify.js";
4
+ import { assertMode, assertModel, assertWorkspacePath } from "./validate.js";
5
+ import { createLogger } from "./logger.js";
6
+ import { ValidationError } from "./errors.js";
7
+ const DEFAULT_TIMEOUT_MS = 270_000; // under cursor-agent's ~5min ceiling
8
+ export async function runCursorAgent(prompt, opts) {
9
+ const runner = opts.runner ?? realRunner;
10
+ const log = opts.log ?? createLogger();
11
+ const bin = opts.bin ?? process.env.CURSOR_AGENT_BIN ?? "cursor-agent";
12
+ const env = opts.env ?? process.env;
13
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
14
+ let mode;
15
+ let model;
16
+ let cwd;
17
+ try {
18
+ mode = assertMode(opts.mode ?? "default");
19
+ model = assertModel(opts.model ?? "composer-2.5-fast");
20
+ // No cwd fallback: an unset root must fail loudly, never silently widen to the launch cwd (often $HOME).
21
+ const allowedRoot = opts.allowedRoot ?? process.env.CURSOR_BRIDGE_ALLOWED_ROOT;
22
+ if (!allowedRoot || !isAbsolute(allowedRoot)) {
23
+ throw new ValidationError("CURSOR_BRIDGE_ALLOWED_ROOT must be set to an absolute path (refusing to default to cwd)");
24
+ }
25
+ cwd = assertWorkspacePath(opts.cwd, allowedRoot);
26
+ }
27
+ catch (e) {
28
+ const r = {
29
+ status: "TOOL_ERROR",
30
+ error: e instanceof ValidationError ? e.message : String(e),
31
+ };
32
+ log({ event: "cursor_agent", level: "error", status: r.status });
33
+ return r;
34
+ }
35
+ // --sandbox enabled bounds filesystem/network for the --force write path (defense in depth).
36
+ // It must be VERIFIED on your cursor-agent build (see Task A20); --workspace alone is only a cwd, not a jail.
37
+ const sandbox = opts.sandbox ?? (process.env.CURSOR_BRIDGE_SANDBOX === "disabled" ? "disabled" : "enabled");
38
+ const args = ["-p", prompt, "--model", model, "--output-format", "json", "--workspace", cwd, "--trust", "--sandbox", sandbox];
39
+ if (mode === "default") {
40
+ args.push("--force"); // auto-approve writes/shell (headless cannot prompt)
41
+ }
42
+ else {
43
+ args.push("--mode", mode); // plan|ask -> read-only enforcement
44
+ }
45
+ const startedAt = Date.now();
46
+ const child = runner(bin, args, { env, cwd });
47
+ const outP = collectStream(child.stdout);
48
+ const errP = collectStream(child.stderr);
49
+ let timedOut = false;
50
+ const timer = setTimeout(() => {
51
+ timedOut = true;
52
+ child.kill("SIGKILL");
53
+ }, timeoutMs);
54
+ let spawnError;
55
+ try {
56
+ await child.done;
57
+ }
58
+ catch (e) {
59
+ spawnError = e;
60
+ }
61
+ clearTimeout(timer);
62
+ const durationMs = Date.now() - startedAt;
63
+ if (timedOut) {
64
+ const r = { status: "TIMEOUT", error: `killed after ${timeoutMs}ms`, raw: outP.current() };
65
+ log({ event: "cursor_agent", level: "warn", status: r.status, durationMs, model, mode });
66
+ return r;
67
+ }
68
+ if (spawnError) {
69
+ const r = { status: "TOOL_ERROR", error: spawnError.message };
70
+ log({ event: "cursor_agent", level: "error", status: r.status, durationMs, model, mode });
71
+ return r;
72
+ }
73
+ const [out, err] = await Promise.all([outP.done, errP.done]);
74
+ const r = classify(out, err);
75
+ log({
76
+ event: "cursor_agent",
77
+ level: r.status === "SUCCESS" ? "info" : "warn",
78
+ status: r.status,
79
+ durationMs,
80
+ model,
81
+ mode,
82
+ sessionId: r.sessionId,
83
+ requestId: r.requestId,
84
+ tokens: r.usage,
85
+ });
86
+ return r;
87
+ }
88
+ //# sourceMappingURL=cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../src/cursor.ts"],"names":[],"mappings":"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"}