@pulsemcp/air-adapter-cursor 0.8.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 ADDED
@@ -0,0 +1,84 @@
1
+ # @pulsemcp/air-adapter-cursor
2
+
3
+ AIR adapter extension for the [Cursor CLI](https://cursor.com/docs/cli) (`cursor-agent`). Translates AIR artifacts into Cursor's native formats and prepares working directories for agent sessions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @pulsemcp/air-adapter-cursor
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### With the AIR CLI
14
+
15
+ ```bash
16
+ # Install the adapter globally alongside the CLI
17
+ npm install -g @pulsemcp/air-cli @pulsemcp/air-adapter-cursor
18
+
19
+ # Start a Cursor session
20
+ air start cursor --root web-app
21
+ ```
22
+
23
+ ### Programmatic
24
+
25
+ ```typescript
26
+ import { resolveArtifacts } from "@pulsemcp/air-core";
27
+ import { CursorAdapter } from "@pulsemcp/air-adapter-cursor";
28
+
29
+ const artifacts = await resolveArtifacts("./air.json");
30
+ const adapter = new CursorAdapter();
31
+
32
+ // Prepare a working directory for a Cursor session
33
+ const session = await adapter.prepareSession(artifacts, "./my-project", {
34
+ root: artifacts.roots["web-app"],
35
+ });
36
+
37
+ // session.configFiles — [] (secrets are Cursor-native ${env:VAR}, see "Secrets" below)
38
+ // session.skillPaths — skill dirs created in .cursor/skills/
39
+ // session.hookPaths — hook dirs created in .cursor/hooks/
40
+ // session.startCommand — { command: "cursor-agent", args: [], cwd: "..." }
41
+ ```
42
+
43
+ ## What `prepareSession()` does
44
+
45
+ 1. **Writes `.cursor/mcp.json`** — translates AIR MCP server configs into a top-level `mcpServers` map. User-authored servers and top-level keys are preserved; only AIR-owned keys are replaced.
46
+ 2. **Writes `.cursor/hooks.json`** — registers path-based hooks under `hooks.<event>` with a required `version: 1`. AIR-owned entries are tagged with `_air_hook_id`; user-authored hooks are preserved.
47
+ 3. **Injects skills** — copies `SKILL.md` files and associated content into `.cursor/skills/{name}/`, where Cursor discovers them.
48
+ 4. **Injects hooks** — copies hook directories into `.cursor/hooks/{name}/` and registers their command in `hooks.json`, anchored to the repo root.
49
+ 5. **Copies references** — attaches referenced documents into `{artifact}/references/`.
50
+ 6. **Respects local priority** — if a skill or hook directory already exists in the target, it is not overwritten.
51
+
52
+ ## Translation Details
53
+
54
+ | AIR Format | Cursor Format |
55
+ |------------|---------------|
56
+ | `mcp.json` (flat map with `type`, `title`, `description`) | `mcpServers.<name>` entries in `.cursor/mcp.json` (metadata stripped) |
57
+ | `stdio` servers | `{ command, args, env }` |
58
+ | `sse` / `streamable-http` servers | `{ url, headers }` (Cursor auto-detects transport from the URL) |
59
+ | Any `${VAR}` ref in `command` / `args` / `env` / `url` / `headers` | rewritten to Cursor-native `${env:VAR}` (anywhere in the string) |
60
+ | Skills (`SKILL.md` + content) | `.cursor/skills/{name}/` |
61
+ | Hooks (`HOOK.json` + scripts) | `.cursor/hooks/{name}/` + `hooks.<event>` registration in `.cursor/hooks.json` |
62
+ | Hook events `session_start`, `session_end`, `pre_tool_call`, `post_tool_call`, `user_prompt_submit`, `stop`, `subagent_stop`, `pre_compact` | Cursor `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop`, `subagentStop`, `preCompact` |
63
+ | References | `{artifact}/references/` |
64
+
65
+ ## Secrets
66
+
67
+ Cursor natively expands `${env:VAR}` references in `mcp.json` values. Instead of writing AIR `${VAR}` placeholders, the adapter rewrites every AIR `${VAR}` reference to Cursor's **`${env:VAR}`** form at translation time:
68
+
69
+ - `env: { GITHUB_TOKEN: "${GITHUB_TOKEN}" }` → `env: { GITHUB_TOKEN: "${env:GITHUB_TOKEN}" }`
70
+ - `headers: { Authorization: "Bearer ${API_TOKEN}" }` → `headers: { Authorization: "Bearer ${env:API_TOKEN}" }`
71
+ - `env: { KEY: "${OTHER}" }` (renamed) → `env: { KEY: "${env:OTHER}" }`
72
+
73
+ Cursor expands these from the host environment at launch. As a result, `prepareSession()` returns an **empty `configFiles` array** — there is no AIR `${VAR}` left for secret transforms to post-process.
74
+
75
+ **No unforwardable shapes.** Because Cursor's `${env:VAR}` interpolation works *anywhere in a string*, every secret reference forwards cleanly — whole-value, partial, and renamed alike. The adapter therefore never warns about secrets. Cursor's own built-in interpolation tokens (`${userHome}`, `${workspaceFolder}`, `${workspaceFolderBasename}`, `${pathSeparator}`) and already-`${env:…}` references are left untouched so they are not double-wrapped.
76
+
77
+ ## Known gaps
78
+
79
+ These AIR features have no static Cursor equivalent and are handled out of band:
80
+
81
+ - **OAuth MCP servers** — AIR's detailed OAuth config (`clientId`/`scopes`/`redirectUri`/…) has no static `mcp.json` form. Cursor performs interactive OAuth via `cursor-agent mcp login <name>`.
82
+ - **Plugins** — Cursor has no local plugin package an adapter can materialize. AIR treats plugins as composition sugar: a plugin's declared MCP servers / skills / hooks are expanded into the activation set and materialized as their underlying Cursor-native artifacts.
83
+ - **Subagent context** — Cursor has no `--append-system-prompt` flag, so subagent-root context is returned to the caller via `PreparedSession.subagentContext` rather than passed to the CLI.
84
+ - **`notification` hook event** — AIR's `notification` event has no Cursor equivalent; hooks targeting it are skipped with a `console.warn`.
@@ -0,0 +1,225 @@
1
+ import type { AgentAdapter, AgentSessionConfig, StartCommand, ResolvedArtifacts, RootEntry, McpServerEntry, PluginEntry, PrepareSessionOptions, PreparedSession, CleanSessionOptions, CleanSessionResult, LocalArtifacts } from "@pulsemcp/air-core";
2
+ export declare class CursorAdapter implements AgentAdapter {
3
+ name: string;
4
+ displayName: string;
5
+ /**
6
+ * Map AIR lifecycle event names to Cursor `hooks.json` event names.
7
+ *
8
+ * Accepts both snake_case AIR names and camelCase Cursor event names as
9
+ * identity mappings, so hook authors targeting the Cursor runtime can write
10
+ * Cursor-native event names directly without translating to snake_case.
11
+ *
12
+ * The only AIR event without a Cursor equivalent is `notification` — it is
13
+ * intentionally absent, so `reconcileHooks` warns and skips registration when
14
+ * it encounters it. Cursor-only events (beforeShellExecution, afterFileEdit,
15
+ * etc.) have no AIR analog and are therefore not generated by AIR, but
16
+ * user-authored hooks targeting them are preserved.
17
+ */
18
+ private static readonly AIR_TO_CURSOR_EVENT;
19
+ /** Canonical AIR snake_case event names with a Cursor mapping. */
20
+ private static readonly AIR_EVENT_NAMES;
21
+ isAvailable(): Promise<boolean>;
22
+ generateConfig(artifacts: ResolvedArtifacts, root?: RootEntry, _workDir?: string): AgentSessionConfig;
23
+ buildStartCommand(config: AgentSessionConfig): StartCommand;
24
+ /**
25
+ * Prepare a working directory for a Cursor CLI session.
26
+ *
27
+ * Writes `.cursor/mcp.json` (MCP servers under `mcpServers`) and
28
+ * `.cursor/hooks.json` (hook registrations under `hooks.<event>`), injects
29
+ * skills + references into `.cursor/skills/<name>/`, copies path-based hooks
30
+ * into `.cursor/hooks/<name>/`, and returns the start command.
31
+ *
32
+ * Inputs to activation lists (root defaults, overrides, plugin-declared
33
+ * primitives) are accepted as either qualified (`@scope/id`) or short form;
34
+ * ambiguous short forms are rejected. Filesystem materialization uses
35
+ * shortnames — Cursor's config files, `.cursor/skills/`, `.cursor/hooks/`,
36
+ * and the manifest are scope-naive. Two activated qualified IDs that share
37
+ * a shortname hard-fail with a clear "add one to exclude" message.
38
+ *
39
+ * NOTE: `configFiles` is intentionally returned empty. AIR `${VAR}`
40
+ * references in MCP env/headers are rewritten to Cursor's native
41
+ * `${env:VAR}` interpolation at translation time, so there is no resolvable
42
+ * `${VAR}` left for AIR's transform/validation pipeline. The `.cursor/mcp.json`
43
+ * we wrote above is deliberately not surfaced as a config file.
44
+ */
45
+ prepareSession(artifacts: ResolvedArtifacts, targetDir: string, options?: PrepareSessionOptions): Promise<PreparedSession>;
46
+ /**
47
+ * Enumerate skills checked into `<targetDir>/.cursor/skills/`. Cursor loads
48
+ * these directly from the filesystem regardless of AIR's involvement, so
49
+ * they're always active and must not be overwritten or removed. The TUI uses
50
+ * this list to surface them as read-only entries.
51
+ */
52
+ listLocalArtifacts(targetDir: string): Promise<LocalArtifacts>;
53
+ /**
54
+ * Remove every artifact AIR has previously written into `targetDir`.
55
+ *
56
+ * Reads the per-target manifest, deletes each tracked skill / hook
57
+ * directory, removes tracked MCP server keys from `.cursor/mcp.json`, and
58
+ * removes AIR-managed hook entries from `.cursor/hooks.json`. When every
59
+ * category is cleaned, the manifest itself is deleted; partial cleans (any
60
+ * `keep*` flag set) update the manifest with the kept entries so future runs
61
+ * continue to track them.
62
+ *
63
+ * Items in the manifest that no longer exist on disk are silently skipped
64
+ * — the manifest can drift if a user removed files manually between runs.
65
+ */
66
+ cleanSession(targetDir: string, options?: CleanSessionOptions): Promise<CleanSessionResult>;
67
+ /**
68
+ * Read `.cursor/mcp.json` and return the subset of `ids` whose key is
69
+ * actually present under `mcpServers`. Returns an empty list if the file
70
+ * can't be parsed — we'd rather under-report than claim to have removed
71
+ * entries we never touched.
72
+ */
73
+ private mcpServerIdsPresent;
74
+ /**
75
+ * Return true if `.cursor/hooks.json` contains at least one AIR-owned hook
76
+ * entry (`_air_hook_id` ∈ `managedHookIds`). Used to avoid rewriting a file
77
+ * we wouldn't actually change.
78
+ */
79
+ private hookEntriesPresent;
80
+ /**
81
+ * Resolve subagent roots from the root's default_subagent_roots.
82
+ * IDs are already qualified after composition-time canonicalization.
83
+ */
84
+ private resolveSubagentRoots;
85
+ /**
86
+ * Merge subagent roots' default_mcp_servers and default_skills into the
87
+ * parent's activated sets (union, preserving order with parent first).
88
+ */
89
+ private mergeSubagentArtifacts;
90
+ /**
91
+ * Build a system prompt section describing the subagent root dependencies.
92
+ */
93
+ private buildSubagentContext;
94
+ /**
95
+ * Translate a shortname-keyed MCP server map into Cursor's `mcpServers`
96
+ * shape (a plain object ready for JSON serialization). Callers convert
97
+ * qualified IDs to shortnames before invoking this — Cursor's config is
98
+ * scope-naive.
99
+ *
100
+ * Secret handling is Cursor-native: AIR `${VAR}` references in any string
101
+ * value (command, args, env values, url, headers) are rewritten to Cursor's
102
+ * `${env:VAR}` interpolation, which Cursor expands from the host environment
103
+ * at launch (see `toCursorVars`). Because Cursor's interpolation works
104
+ * anywhere in a string, partial (`"Bearer ${TOKEN}"`) and renamed
105
+ * (`KEY = "${OTHER}"`) references all forward cleanly — there are no
106
+ * unforwardable shapes, so no warnings are emitted.
107
+ */
108
+ translateMcpServersByShort(servers: Record<string, McpServerEntry>): Record<string, Record<string, unknown>>;
109
+ private translateMcpServer;
110
+ /**
111
+ * Rewrite AIR `${VAR}` references into Cursor's native `${env:VAR}`
112
+ * interpolation. Each `${...}` token is wrapped unless it is already a
113
+ * Cursor `${env:...}` reference or one of Cursor's built-in interpolation
114
+ * tokens (`${userHome}`, `${workspaceFolder}`, …), which must be left
115
+ * untouched. Strings without a `${...}` token are returned unchanged.
116
+ */
117
+ private toCursorVars;
118
+ /**
119
+ * Translate an AIR plugin to a Cursor-facing descriptor.
120
+ *
121
+ * Cursor's marketplace plugins are remote-installed and have no local package
122
+ * file an adapter can materialize. AIR therefore treats plugins as composition
123
+ * sugar: a plugin's declared MCP servers / skills / hooks are expanded into the
124
+ * activation set and materialized as their underlying Cursor-native artifacts.
125
+ * This descriptor is informational only.
126
+ */
127
+ translatePlugin(shortId: string, plugin: PluginEntry): Record<string, unknown>;
128
+ /**
129
+ * Resolve a list of activation IDs (each qualified or short) into qualified
130
+ * IDs paired with shortnames suitable for filesystem materialization.
131
+ *
132
+ * Throws on:
133
+ * - unknown IDs (after attempting both qualified and short-form lookup)
134
+ * - ambiguous short references (multiple scopes contribute the shortname)
135
+ * - shortname collisions in the activation set itself (two qualified IDs
136
+ * with the same shortname can't share a single materialization dir)
137
+ */
138
+ private resolveActivations;
139
+ /**
140
+ * Build the warning emitted when a registered artifact's `path` does not
141
+ * exist on disk at materialization time. The qualified ID encodes the
142
+ * declaring catalog's scope, so a reviewer can trace the offending entry
143
+ * back to its index file. Materialization is skipped for this artifact and
144
+ * the rest of the session proceeds.
145
+ */
146
+ private missingSourceDirMessage;
147
+ private formatPoolKeys;
148
+ /**
149
+ * Copy referenced documents into a references/ subdirectory of the target.
150
+ * `refIds` are qualified IDs (post-canonicalization).
151
+ */
152
+ private copyReferences;
153
+ /** Parse an existing JSON config, returning `{}` when absent/unparseable. */
154
+ private readJson;
155
+ private writeJson;
156
+ /**
157
+ * Merge AIR-managed MCP servers into `.cursor/mcp.json`, preserving any
158
+ * user-authored config. Keys in `staleMcpIds` are removed; keys in
159
+ * `translatedServers` are set/replaced; other servers and top-level keys pass
160
+ * through. A run that leaves the config empty deletes the file rather than
161
+ * leaving an empty stub. Returns the path written, or null if the file was
162
+ * deleted / not created.
163
+ */
164
+ private writeCursorMcpConfig;
165
+ /**
166
+ * Merge AIR-owned hook registrations into `.cursor/hooks.json`, preserving
167
+ * any user-authored hooks. AIR-owned entries (tagged with `_air_hook_id`)
168
+ * whose ID is in `managedHookIds` are pruned, then the current selection is
169
+ * registered. A run that leaves no hooks deletes the file (an empty hooks
170
+ * config is a useless stub) unless the user kept other top-level keys.
171
+ * Returns the path written, or null if the file was deleted / not created.
172
+ */
173
+ private writeCursorHooksConfig;
174
+ private isManagedHookEntry;
175
+ /**
176
+ * Remove `mcpIds` from `mcpServers` in `.cursor/mcp.json`, preserving
177
+ * user-authored entries and other top-level fields. Returns the path of the
178
+ * file that was rewritten, or null if the file became empty and was deleted.
179
+ */
180
+ private pruneCursorMcpConfig;
181
+ /**
182
+ * Remove AIR-managed hook entries (matched by `_air_hook_id` ∈
183
+ * `managedHookIds`) from `.cursor/hooks.json`, preserving user-authored
184
+ * entries and other top-level fields. Returns the path of the file that was
185
+ * rewritten, or null if no hooks remain and the file was deleted.
186
+ */
187
+ private pruneCursorHooksConfig;
188
+ /**
189
+ * Build a shell command string from HOOK.json's command and args fields.
190
+ *
191
+ * Hook authors write paths relative to their own hook directory. Cursor
192
+ * invokes hooks with a working directory that is not guaranteed to be the
193
+ * project root, so hook-relative paths are anchored to the repository root
194
+ * via `"$(git rev-parse --show-toplevel)/.cursor/hooks/<id>/<path>"`. The
195
+ * double quotes keep the path safe for project directories with spaces.
196
+ *
197
+ * - `command` is anchored if it starts with `./` (explicit hook-relative).
198
+ * - Each `args` entry is anchored if it looks like a path AND the file
199
+ * exists under the hook's installed directory.
200
+ *
201
+ * Args that are not rewritten and contain shell metacharacters are
202
+ * single-quoted for safety.
203
+ */
204
+ private buildHookCommand;
205
+ /**
206
+ * Wrap a project-root-relative path in `"$(git rev-parse --show-toplevel)/..."`
207
+ * so the resolved path is independent of the cwd at hook invocation. Double
208
+ * quotes are required so the command substitution runs; characters that are
209
+ * special inside double quotes (`$`, `` ` ``, `"`, `\`) are escaped in the
210
+ * path component so an unusual hook ID or filename can't break out of the
211
+ * quoting.
212
+ */
213
+ private anchorHookPath;
214
+ /**
215
+ * If `arg` is a hook-relative path that points at a real file under the
216
+ * hook's installed directory, return its project-root-relative form
217
+ * (`.cursor/hooks/<id>/<path>`) flagged as anchored so the caller can wrap it.
218
+ * Otherwise return `arg` unchanged with `anchored: false`.
219
+ */
220
+ private rewriteHookArgPath;
221
+ /** Human-readable list of the supported AIR hook event names (snake_case only). */
222
+ private supportedEventList;
223
+ private copyDirRecursive;
224
+ }
225
+ //# sourceMappingURL=cursor-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-adapter.d.ts","sourceRoot":"","sources":["../src/cursor-adapter.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EAEf,MAAM,oBAAoB,CAAC;AAsC5B,qBAAa,aAAc,YAAW,YAAY;IAChD,IAAI,SAAY;IAChB,WAAW,SAAY;IAEvB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAgCzC;IAEF,kEAAkE;IAClE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CASrC;IAEI,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC,cAAc,CACZ,SAAS,EAAE,iBAAiB,EAC5B,IAAI,CAAC,EAAE,SAAS,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,kBAAkB;IAmDrB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY;IAa3D;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,cAAc,CAClB,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,eAAe,CAAC;IAqM3B;;;;;OAKG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAIpE;;;;;;;;;;;;OAYG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC;IA6G9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAM3B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAY1B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAwB9B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA4B5B;;;;;;;;;;;;;OAaG;IACH,0BAA0B,CACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAQ1C,OAAO,CAAC,kBAAkB;IAgC1B;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;;;;;OAQG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAQ9E;;;;;;;;;OASG;IACH,OAAO,CAAC,kBAAkB;IAgD1B;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,cAAc;IAStB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAyBtB,6EAA6E;IAC7E,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,SAAS;IAKjB;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IA0B5B;;;;;;;OAOG;IACH,OAAO,CAAC,sBAAsB;IA8F9B,OAAO,CAAC,kBAAkB;IAO1B;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAkC9B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,gBAAgB;IA4BxB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAKtB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAoB1B,mFAAmF;IACnF,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,gBAAgB;CAYzB"}