@ory/cursor 0.0.0-bootstrap.0 → 1.0.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 +181 -0
- package/dist/cli/assets.d.ts +22 -0
- package/dist/cli/assets.js +107 -0
- package/dist/cli/main.d.ts +13 -0
- package/dist/cli/main.js +166 -0
- package/dist/cli/setup.d.ts +17 -0
- package/dist/cli/setup.js +292 -0
- package/dist/handlers.d.ts +14 -0
- package/dist/handlers.js +252 -0
- package/dist/hook.d.ts +9 -0
- package/dist/hook.js +75 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +2 -0
- package/package.json +76 -1
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Ory Agent Plugin: Cursor
|
|
2
|
+
|
|
3
|
+
Security and developer experience for [Cursor](https://cursor.com), powered by [Ory](https://ory.com).
|
|
4
|
+
|
|
5
|
+
Cursor runs real actions on your machine — editing files, running shell commands, calling APIs. This plugin gives those actions an identity, a permission check, and an audit trail.
|
|
6
|
+
|
|
7
|
+
One command installs two independent halves:
|
|
8
|
+
|
|
9
|
+
- **Developer experience** — the Ory `.cursor/rules/*.mdc` guidance rules, the local Ory dev stack, an MCP server, and activity logging. No account, no keys, nothing to configure: it works the moment it's installed.
|
|
10
|
+
- **Ory Agent Security** — browser sign-in, brokered permission checks before tool calls, and the delegation trail. Opt-in connection details from the [Ory Console](https://console.ory.sh) switch it on, and it takes nothing away from the half above. See [Connect to Ory Agent Security](#connect-to-ory-agent-security).
|
|
11
|
+
|
|
12
|
+
## What you'll need
|
|
13
|
+
|
|
14
|
+
- [Cursor](https://cursor.com), installed
|
|
15
|
+
- Node.js **22 or newer**
|
|
16
|
+
- [Docker](https://docs.docker.com/get-docker/) — only if you want to run Ory locally
|
|
17
|
+
- macOS or Linux (Windows works via WSL2)
|
|
18
|
+
|
|
19
|
+
## Get started
|
|
20
|
+
|
|
21
|
+
Run one command:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx -y -p @ory/cursor ory-cursor install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This merges the Ory entries into `~/.cursor/hooks.json` and `~/.cursor/mcp.json` and writes `~/.cursor/rules/ory-*.mdc` (use `--project` to target `./.cursor` instead). Restart Cursor (or reload the window) to pick up the hooks.
|
|
28
|
+
|
|
29
|
+
Confirm everything landed with:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx -y -p @ory/cursor ory-cursor status
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, and recent activity. Until you connect Agent Security, the identity and permission rows say so and name what's missing.
|
|
36
|
+
|
|
37
|
+
## Rules and commands
|
|
38
|
+
|
|
39
|
+
Installing the plugin drops the full Ory playbook catalog into Cursor as **`.cursor/rules/*.mdc` guidance rules**, which Cursor applies by relevance — say what you want in plain language and the matching one takes over.
|
|
40
|
+
|
|
41
|
+
| Rule | What it does for you |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `ory-auth-setup` | Adds a complete auth system to your app — login, registration, recovery, verification, settings — on [Ory Elements](https://github.com/ory/elements) |
|
|
44
|
+
| `ory-login-flow` | Builds just the pages, wired to Ory's self-service flows |
|
|
45
|
+
| `ory-social-login` | "Sign in with…" for Google, GitHub, Apple, Microsoft, Discord, Slack, GitLab, Facebook |
|
|
46
|
+
| `ory-local-dev` | Develops and tests login/permission flows against a local Ory — no project, no account, offline |
|
|
47
|
+
| `ory-permissions-onboarding` | Walks a fresh install from observe mode to enforced per-tool permissions without getting blocked |
|
|
48
|
+
| `ory-build-agent` | Drops `@ory/argus` into an agent *you* own — Claude Agent SDK, OpenAI Agents, Mastra, Vercel AI, LangGraph/PydanticAI |
|
|
49
|
+
| `ory-build-integration` | Wires Ory into your app: Action webhooks, JWT validation at a gateway, live event streams |
|
|
50
|
+
| `ory-contribute-integration` | Authors and submits an integration to the public `ory/integrates` registry |
|
|
51
|
+
| `ory-e2b-sandbox` | Scaffolds an E2B sandbox template that boots with this plugin preinstalled |
|
|
52
|
+
| `ory-temporal-worker` | Scaffolds a Temporal TypeScript worker where every Activity is authenticated, authorized, and audited |
|
|
53
|
+
|
|
54
|
+
The local stack has its own rules — ask for them by name:
|
|
55
|
+
|
|
56
|
+
| Command | What it does |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `ory-local-up` | Starts a local Ory (Identities, OAuth2, Permissions) in Docker and seeds a test user — it prints the email + password to sign in with |
|
|
59
|
+
| `ory-local-down` | Stops it, keeping your data volumes |
|
|
60
|
+
| `ory-temporal-up` | Starts a local Temporal dev server for the `ory-temporal-worker` scaffold |
|
|
61
|
+
|
|
62
|
+
The local stack runs entirely on your laptop: Ory APIs at `http://localhost:4000`, a login UI on `:4455` (not `:3000`, to dodge Next.js port clashes), and the Ory Console on `:4100`.
|
|
63
|
+
|
|
64
|
+
A built-in **Ory MCP server** rounds it out — Cursor can manage identities, projects, and permissions straight from chat.
|
|
65
|
+
|
|
66
|
+
So: ask Cursor *"add Ory login to this app"* and it scaffolds the pages, starts a local Ory, and wires them together.
|
|
67
|
+
|
|
68
|
+
## What you get
|
|
69
|
+
|
|
70
|
+
Out of the box, every tool Cursor runs produces a privacy-safe structured activity event in the unified local log.
|
|
71
|
+
|
|
72
|
+
Once you connect to Ory Agent Security, two more things happen automatically:
|
|
73
|
+
|
|
74
|
+
- **Who's driving.** You sign in once in your browser; Cursor (and any sub-agents it spawns) each get their own identity. No tokens to copy around, and the "who acted on whose behalf" trail stays queryable later.
|
|
75
|
+
- **What it's allowed to do.** Before a tool runs, Ory checks whether it's permitted. It starts in **observe mode** — nothing is blocked, you just *see* what would be — so it never gets in your way on day one.
|
|
76
|
+
|
|
77
|
+
If Ory is ever unreachable, the plugin gets out of the way and lets Cursor keep working — so it can't lock you out.
|
|
78
|
+
|
|
79
|
+
### See what's happening
|
|
80
|
+
|
|
81
|
+
Everything the plugin does is observable out of the box:
|
|
82
|
+
|
|
83
|
+
- **Activity log.** Privacy-safe activity is always appended to `~/.config/ory-agent-plugins/cursor/ory-agent-debug.log`. View events, decisions, and errors live with:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx -y -p @ory/cursor ory-cursor watch
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Set `ORY_AGENT_LOG_FILE` to override the path; set it empty to disable file persistence.
|
|
90
|
+
- **Live debug.** Launch Cursor with `ORY_AGENT_DEBUG=true` to add verbose local diagnostics, including raw shell commands, to the watched log and stderr. Secrets are recursively redacted; pass `watch --json` for NDJSON.
|
|
91
|
+
|
|
92
|
+
### Ready to enforce?
|
|
93
|
+
|
|
94
|
+
Once connected, the deny posture lives on the Ory project: when the observe-mode activity looks right, an admin promotes it to **enforce** in the **Ory Console** (Agent Security). Every session reads that posture live, so nothing has to be reinstalled.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx -y -p @ory/cursor ory-cursor permissions # what the project grants, and the live mode
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then a denied tool is actually blocked (`preToolUse` returns `permission: "deny"`) and Cursor shows why.
|
|
101
|
+
|
|
102
|
+
## How it maps onto Cursor's hooks
|
|
103
|
+
|
|
104
|
+
| Cursor hook | What the plugin does |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `sessionStart` | User + agent auth gates, user→agent delegation (never blocks) |
|
|
107
|
+
| `preToolUse` | The permission gate — blocks with `permission: "deny"` in enforce mode |
|
|
108
|
+
| `postToolUse` / `postToolUseFailure` | Record `tool.complete` / `tool.fail` activity |
|
|
109
|
+
| `beforeSubmitPrompt` | Record `user.prompt` activity (never blocks the human) |
|
|
110
|
+
| `subagentStart` / `subagentStop` | Sub-agent DCR identity + delegation activity |
|
|
111
|
+
|
|
112
|
+
The fine-grained `beforeShellExecution` / `beforeMCPExecution` / `beforeReadFile`
|
|
113
|
+
hooks are **not** registered: `preToolUse` already gates those categories, so
|
|
114
|
+
registering them would only spawn extra hook subprocesses without adding any
|
|
115
|
+
enforcement. (When they do fire, the handler records activity only.)
|
|
116
|
+
|
|
117
|
+
The `preToolUse` gate is registered fail-closed: if the hook can't produce a verdict, Cursor blocks the tool (exit code 2), so a crashed or timed-out gate can't silently defeat enforce mode. The audit/identity hooks stay fail-open — none of them gate a tool, and session start must never block.
|
|
118
|
+
|
|
119
|
+
## Connect to Ory Agent Security
|
|
120
|
+
|
|
121
|
+
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
122
|
+
|
|
123
|
+
| Value | Flag | Environment variable |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| Project URL | `--project-url` | `ORY_PROJECT_URL` |
|
|
126
|
+
| Agent Security URL | `--agent-security-url` | `ORY_AGENT_SECURITY_URL` |
|
|
127
|
+
| Sign-in client id override (default `ory-agent-security-login`) | `--oauth2-client-id` | `ORY_OAUTH2_CLIENT_ID` |
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx -y -p @ory/cursor ory-cursor configure \
|
|
131
|
+
--project-url https://<slug>.projects.oryapis.com \
|
|
132
|
+
--agent-security-url https://agents.console.ory.com
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`install` accepts the same flags, so you can register the plugin **and** connect in one shot (`install --project-url <URL> --agent-security-url <URL>`). The login client defaults to `ory-agent-security-login`; custom deployments can override it with `--oauth2-client-id`. Existing configurations fall back to the project URL when the Agent Security URL is unset. To turn sign-in and checks back off later, use `configure --disconnect`.
|
|
136
|
+
|
|
137
|
+
**Ory Network or OEL.** Either works. For an Ory Network project the URL is `https://<slug>.projects.oryapis.com`; for a self-hosted **Ory Enterprise License** deployment, point `--project-url` at that deployment's base URL and use the sign-in client id from its Agent Security configuration. Everything downstream — sign-in, checks, delegation — is identical.
|
|
138
|
+
|
|
139
|
+
**There is nothing else for you to create.** The sign-in client, the permission model, the per-tool grants and blocks, and the observe/enforce posture are all provisioned in the Console by someone with project access. At runtime the plugin only *reads* permissions — it has no write path into your project, which is why installing it needs no workspace privilege. Each Cursor session registers its own identity automatically on first use.
|
|
140
|
+
|
|
141
|
+
Sign-in runs at the start of every session and never blocks — a declined, skipped, or timed-out login simply leaves that session without a user identity.
|
|
142
|
+
|
|
143
|
+
Settings are saved to `~/.config/ory-agent-plugins/config.json` and shared across all your Ory agent plugins; environment variables win when both are set. Running headless with an OAuth2 access token already? Set `ORY_USER_OAUTH2_TOKEN` and the browser step is skipped entirely.
|
|
144
|
+
|
|
145
|
+
## Commands
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
ory-cursor install | uninstall Install (add --project-url to also connect Agent Security) / remove
|
|
149
|
+
ory-cursor status Show configuration, identities, permission coverage, recent activity
|
|
150
|
+
ory-cursor permissions <cmd> status (read-only; grants + posture live in the Ory Console)
|
|
151
|
+
ory-cursor configure <flags> Connect a project (--project-url) or --disconnect
|
|
152
|
+
ory-cursor agent <status|unregister> Manage Cursor's own auto-created identity
|
|
153
|
+
ory-cursor local <up|down|status|…> Run / manage a local Ory in Docker
|
|
154
|
+
ory-cursor version Print plugin, core, and Node versions (--json for machine-readable)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
All prefixed with `npx -y -p @ory/cursor`.
|
|
158
|
+
|
|
159
|
+
## Uninstall
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npx -y -p @ory/cursor ory-cursor uninstall
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Removes only the Ory entries from `hooks.json` / `mcp.json` and the Ory rules, and clears the shared credential store. Because configuration and credentials live in the shared `~/.config/ory-agent-plugins/config.json`, a login performed by any other Ory harness plugin or SDK integration is reused here.
|
|
166
|
+
|
|
167
|
+
## Troubleshooting
|
|
168
|
+
|
|
169
|
+
- **Hooks didn't take effect** — restart Cursor or reload the window so it re-reads `hooks.json`.
|
|
170
|
+
- **`local-up` fails** — make sure Docker is running and ports `4000`, `4100`, `4455`, and `16686` are free.
|
|
171
|
+
- **Browser sign-in loops** (after connecting) — reset with `ory-cursor agent unregister` and try again.
|
|
172
|
+
- **Running an older CLI than expected** — `npx -p @ory/cursor` (no version pin) reuses a previously-downloaded copy instead of re-resolving to the latest. The install banner prints the running version; confirm it with `npx -y -p @ory/cursor ory-cursor version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/cursor ory-cursor install`. Pinning an exact version (`@ory/cursor@<version>`) also bypasses the cached copy.
|
|
173
|
+
|
|
174
|
+
## Learn more
|
|
175
|
+
|
|
176
|
+
- [Ory documentation](https://www.ory.com/docs/) · [Ory Console](https://console.ory.sh) · [Ory Elements](https://github.com/ory/elements)
|
|
177
|
+
- [Cursor hooks documentation](https://cursor.com/docs/hooks)
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
Apache-2.0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize / clean the Ory skill catalog as Cursor **rules**.
|
|
3
|
+
*
|
|
4
|
+
* Cursor has no slash-command system and no SKILL.md discovery — the native way
|
|
5
|
+
* to inject reusable, model-facing guidance is a `.cursor/rules/*.mdc` file:
|
|
6
|
+
* YAML frontmatter (`description`, `alwaysApply`) followed by a markdown body.
|
|
7
|
+
* With `alwaysApply: false` + a description, Cursor's agent pulls a rule into
|
|
8
|
+
* context when the description matches what it's doing ("Agent Requested" rules).
|
|
9
|
+
*
|
|
10
|
+
* Both the guide skills and the local-stack commands are rendered from the
|
|
11
|
+
* shared `@ory/argus` templates and written as rules (commands are wrapped as
|
|
12
|
+
* skills first, mirroring the Codex/OpenClaw "commands-as-skills" pattern), so
|
|
13
|
+
* the guidance stays identical across every harness.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Write every Ory rule under `<rulesDir>` (e.g. `~/.cursor/rules` or
|
|
17
|
+
* `<project>/.cursor/rules`). Overwrites existing Ory rules so template
|
|
18
|
+
* updates take effect; leaves unrelated rules untouched.
|
|
19
|
+
*/
|
|
20
|
+
export declare function installCursorOryRules(rulesDir: string): void;
|
|
21
|
+
/** Remove every Ory rule this plugin installed under `<rulesDir>`. */
|
|
22
|
+
export declare function uninstallCursorOryRules(rulesDir: string): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Materialize / clean the Ory skill catalog as Cursor **rules**.
|
|
4
|
+
*
|
|
5
|
+
* Cursor has no slash-command system and no SKILL.md discovery — the native way
|
|
6
|
+
* to inject reusable, model-facing guidance is a `.cursor/rules/*.mdc` file:
|
|
7
|
+
* YAML frontmatter (`description`, `alwaysApply`) followed by a markdown body.
|
|
8
|
+
* With `alwaysApply: false` + a description, Cursor's agent pulls a rule into
|
|
9
|
+
* context when the description matches what it's doing ("Agent Requested" rules).
|
|
10
|
+
*
|
|
11
|
+
* Both the guide skills and the local-stack commands are rendered from the
|
|
12
|
+
* shared `@ory/argus` templates and written as rules (commands are wrapped as
|
|
13
|
+
* skills first, mirroring the Codex/OpenClaw "commands-as-skills" pattern), so
|
|
14
|
+
* the guidance stays identical across every harness.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.installCursorOryRules = installCursorOryRules;
|
|
51
|
+
exports.uninstallCursorOryRules = uninstallCursorOryRules;
|
|
52
|
+
const fs = __importStar(require("node:fs"));
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
const argus_1 = require("@ory/argus");
|
|
55
|
+
const RENDER_OPTS = {
|
|
56
|
+
binName: "ory-cursor",
|
|
57
|
+
packageName: "@ory/cursor",
|
|
58
|
+
};
|
|
59
|
+
/** Escape a value for a single-line YAML frontmatter scalar. */
|
|
60
|
+
function yamlScalar(value) {
|
|
61
|
+
const needsQuote = /[:#"']/.test(value);
|
|
62
|
+
return needsQuote ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
63
|
+
}
|
|
64
|
+
/** Serialize a rendered skill as a Cursor `.mdc` rule. */
|
|
65
|
+
function toMdc(skill) {
|
|
66
|
+
const body = skill.body.replace(/^\n+/, "").replace(/\s+$/, "");
|
|
67
|
+
return (`---\n` +
|
|
68
|
+
`description: ${yamlScalar(skill.description)}\n` +
|
|
69
|
+
`alwaysApply: false\n` +
|
|
70
|
+
`---\n\n` +
|
|
71
|
+
`${body}\n`);
|
|
72
|
+
}
|
|
73
|
+
/** Every Ory rule this plugin materializes (guides + commands-as-skills). */
|
|
74
|
+
function renderRules() {
|
|
75
|
+
return [
|
|
76
|
+
...(0, argus_1.renderOrySkills)("cursor", RENDER_OPTS),
|
|
77
|
+
...(0, argus_1.renderOryCommands)("cursor", RENDER_OPTS).map(argus_1.commandToSkill),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Write every Ory rule under `<rulesDir>` (e.g. `~/.cursor/rules` or
|
|
82
|
+
* `<project>/.cursor/rules`). Overwrites existing Ory rules so template
|
|
83
|
+
* updates take effect; leaves unrelated rules untouched.
|
|
84
|
+
*/
|
|
85
|
+
function installCursorOryRules(rulesDir) {
|
|
86
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
87
|
+
for (const rule of renderRules()) {
|
|
88
|
+
fs.writeFileSync(path.join(rulesDir, `${rule.name}.mdc`), toMdc(rule));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Remove every Ory rule this plugin installed under `<rulesDir>`. */
|
|
92
|
+
function uninstallCursorOryRules(rulesDir) {
|
|
93
|
+
if (!fs.existsSync(rulesDir))
|
|
94
|
+
return;
|
|
95
|
+
for (const name of [...argus_1.ORY_SKILL_NAMES, ...argus_1.ORY_COMMAND_SKILL_NAMES]) {
|
|
96
|
+
const f = path.join(rulesDir, `${name}.mdc`);
|
|
97
|
+
if (fs.existsSync(f))
|
|
98
|
+
fs.unlinkSync(f);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
if (fs.readdirSync(rulesDir).length === 0)
|
|
102
|
+
fs.rmdirSync(rulesDir);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* leave the dir if removal fails */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Main CLI for @ory/cursor.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx -y -p @ory/cursor ory-cursor install [--project] Install hooks/MCP/rules
|
|
7
|
+
* npx -y -p @ory/cursor ory-cursor uninstall [--project] Remove them
|
|
8
|
+
* npx -y -p @ory/cursor ory-cursor configure Set Ory project URL / API key
|
|
9
|
+
* npx -y -p @ory/cursor ory-cursor permissions Show permission mode and per-tool coverage
|
|
10
|
+
* npx -y -p @ory/cursor ory-cursor status Show plugin status
|
|
11
|
+
* npx -y -p @ory/cursor ory-cursor local <cmd> Manage the local Ory stack
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Main CLI for @ory/cursor.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx -y -p @ory/cursor ory-cursor install [--project] Install hooks/MCP/rules
|
|
8
|
+
* npx -y -p @ory/cursor ory-cursor uninstall [--project] Remove them
|
|
9
|
+
* npx -y -p @ory/cursor ory-cursor configure Set Ory project URL / API key
|
|
10
|
+
* npx -y -p @ory/cursor ory-cursor permissions Show permission mode and per-tool coverage
|
|
11
|
+
* npx -y -p @ory/cursor ory-cursor status Show plugin status
|
|
12
|
+
* npx -y -p @ory/cursor ory-cursor local <cmd> Manage the local Ory stack
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const fs = __importStar(require("node:fs"));
|
|
50
|
+
const os = __importStar(require("node:os"));
|
|
51
|
+
const argus_1 = require("@ory/argus");
|
|
52
|
+
const setup_js_1 = require("./setup.js");
|
|
53
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
54
|
+
function main() {
|
|
55
|
+
const [command, ...args] = process.argv.slice(2);
|
|
56
|
+
// `--help` after a matched subcommand: print usage, do nothing else.
|
|
57
|
+
// Without this the switch below ignores the flag and `install --help`
|
|
58
|
+
// performs a real install (#221).
|
|
59
|
+
if ((0, argus_1.shouldPrintHelp)(command, args))
|
|
60
|
+
return help();
|
|
61
|
+
switch (command) {
|
|
62
|
+
case "install":
|
|
63
|
+
(0, argus_1.beginDeferNextSteps)();
|
|
64
|
+
(0, setup_js_1.install)(args);
|
|
65
|
+
(0, argus_1.runPostInstall)("ory-cursor", "cursor", args).then(() => process.exit(0), (err) => {
|
|
66
|
+
console.error(err.message ?? err);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
case "uninstall":
|
|
71
|
+
(0, setup_js_1.uninstall)(args);
|
|
72
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
73
|
+
harness: "cursor",
|
|
74
|
+
purge: args.includes("--purge"),
|
|
75
|
+
}).then(() => process.exit(0), (err) => {
|
|
76
|
+
console.error(err.message ?? err);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
case "configure":
|
|
81
|
+
(0, argus_1.runConfigureCommand)("ory-cursor", args);
|
|
82
|
+
break;
|
|
83
|
+
case "agent":
|
|
84
|
+
(0, argus_1.runAgentCommand)("ory-cursor", "cursor", args).then((code) => process.exit(code), (err) => {
|
|
85
|
+
console.error(err.message ?? err);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
88
|
+
break;
|
|
89
|
+
case "permissions":
|
|
90
|
+
(0, argus_1.runPermissionsCommand)("ory-cursor", "cursor", args).then((code) => process.exit(code), (err) => {
|
|
91
|
+
console.error(err.message ?? err);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
});
|
|
94
|
+
break;
|
|
95
|
+
case "status":
|
|
96
|
+
status().then(() => process.exit(0), (err) => {
|
|
97
|
+
console.error(err.message ?? err);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
break;
|
|
101
|
+
case "watch":
|
|
102
|
+
(0, argus_1.runWatchCommand)("ory-cursor", "cursor", args).then((code) => process.exit(code), (err) => { console.error(err.message ?? err); process.exit(1); });
|
|
103
|
+
break;
|
|
104
|
+
case "local":
|
|
105
|
+
(0, argus_1.runLocalCommand)("ory-cursor", args).catch((err) => {
|
|
106
|
+
console.error(err.message ?? err);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
case "version":
|
|
111
|
+
case "--version":
|
|
112
|
+
case "-v":
|
|
113
|
+
process.exit((0, argus_1.runVersionCommand)("ory-cursor", args, { packageRoot: PACKAGE_ROOT }));
|
|
114
|
+
break;
|
|
115
|
+
case "help":
|
|
116
|
+
case "--help":
|
|
117
|
+
case "-h":
|
|
118
|
+
case undefined:
|
|
119
|
+
help();
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
console.error(`Unknown command: ${command}`);
|
|
123
|
+
help();
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function status() {
|
|
128
|
+
await (0, argus_1.runStatusCommand)("ory-cursor", "cursor", {
|
|
129
|
+
title: "Cursor",
|
|
130
|
+
printPluginSection: () => {
|
|
131
|
+
const userHooks = path.join(os.homedir(), ".cursor", "hooks.json");
|
|
132
|
+
const projectHooks = path.join(process.cwd(), ".cursor", "hooks.json");
|
|
133
|
+
console.log("Hooks & plugin:");
|
|
134
|
+
console.log(` User hooks: ${fs.existsSync(userHooks) ? userHooks : "(not installed at ~/.cursor)"}`);
|
|
135
|
+
console.log(` Project hooks: ${fs.existsSync(projectHooks) ? projectHooks : "(none in this project)"}`);
|
|
136
|
+
console.log(` Runtime: ${(0, argus_1.describeRuntimeHealth)((0, argus_1.checkRuntimeHealth)("cursor"))}`);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function help() {
|
|
141
|
+
console.log(`
|
|
142
|
+
ory-cursor — Ory plugin for Cursor
|
|
143
|
+
|
|
144
|
+
Usage:
|
|
145
|
+
npx -y -p @ory/cursor ory-cursor <command> [options]
|
|
146
|
+
|
|
147
|
+
Commands:
|
|
148
|
+
install [--project] Install Ory hooks, MCP server, and rules into ~/.cursor (or ./.cursor)
|
|
149
|
+
uninstall [--project] Remove them
|
|
150
|
+
configure Set or view Ory project URL and API key
|
|
151
|
+
agent <cmd> Manage the agent's OAuth2 (DCR) identity (status, unregister)
|
|
152
|
+
permissions Show the live permission mode and per-tool coverage
|
|
153
|
+
status Show plugin status and configuration
|
|
154
|
+
watch [--json] [--lines <count>]
|
|
155
|
+
Follow the live activity/debug log
|
|
156
|
+
version Show version and the ory-agent-plugins build commit
|
|
157
|
+
local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
npx -y -p @ory/cursor ory-cursor install
|
|
161
|
+
npx -y -p @ory/cursor ory-cursor configure --project-url https://your-project.projects.oryapis.com --agent-security-url https://agents.console.ory.com
|
|
162
|
+
npx -y -p @ory/cursor ory-cursor status
|
|
163
|
+
npx -y -p @ory/cursor ory-cursor uninstall
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install / uninstall the Ory plugin for Cursor.
|
|
4
|
+
*
|
|
5
|
+
* Cursor has no plugin marketplace or registration CLI — it discovers hooks
|
|
6
|
+
* from a `hooks.json`, MCP servers from an `mcp.json`, and guidance from
|
|
7
|
+
* `.cursor/rules/*.mdc`, in the user-global `~/.cursor/` dir (default) or a
|
|
8
|
+
* project-local `<cwd>/.cursor/` dir (`--project`). Install merges the Ory
|
|
9
|
+
* entries into those files non-destructively; uninstall removes only what Ory
|
|
10
|
+
* added, leaving everything else intact.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx -y -p @ory/cursor ory-cursor-setup [--project] [--print] Install
|
|
14
|
+
* npx -y -p @ory/cursor ory-cursor-setup --uninstall [--project] Remove
|
|
15
|
+
*/
|
|
16
|
+
export declare function install(args: string[]): void;
|
|
17
|
+
export declare function uninstall(args: string[]): void;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Install / uninstall the Ory plugin for Cursor.
|
|
5
|
+
*
|
|
6
|
+
* Cursor has no plugin marketplace or registration CLI — it discovers hooks
|
|
7
|
+
* from a `hooks.json`, MCP servers from an `mcp.json`, and guidance from
|
|
8
|
+
* `.cursor/rules/*.mdc`, in the user-global `~/.cursor/` dir (default) or a
|
|
9
|
+
* project-local `<cwd>/.cursor/` dir (`--project`). Install merges the Ory
|
|
10
|
+
* entries into those files non-destructively; uninstall removes only what Ory
|
|
11
|
+
* added, leaving everything else intact.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* npx -y -p @ory/cursor ory-cursor-setup [--project] [--print] Install
|
|
15
|
+
* npx -y -p @ory/cursor ory-cursor-setup --uninstall [--project] Remove
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.install = install;
|
|
52
|
+
exports.uninstall = uninstall;
|
|
53
|
+
const fs = __importStar(require("node:fs"));
|
|
54
|
+
const os = __importStar(require("node:os"));
|
|
55
|
+
const path = __importStar(require("node:path"));
|
|
56
|
+
const argus_1 = require("@ory/argus");
|
|
57
|
+
const assets_js_1 = require("./assets.js");
|
|
58
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
59
|
+
const PACKAGE_NAME = "@ory/cursor";
|
|
60
|
+
const MCP_KEY = "ory";
|
|
61
|
+
/** Marker identifying an Ory-owned hooks.json entry (for idempotent merge). */
|
|
62
|
+
/** SessionStart may run the interactive PKCE browser login — generous window.
|
|
63
|
+
* Tool-lifecycle hooks only make a permission check. Seconds (Cursor's unit).
|
|
64
|
+
* Derived in core so the session-start window cannot drift below the login
|
|
65
|
+
* timeout it has to outlast. */
|
|
66
|
+
const SESSION_START_TIMEOUT_SEC = (0, argus_1.sessionStartHookTimeout)("cursor", "seconds");
|
|
67
|
+
const TOOL_HOOK_TIMEOUT_SEC = (0, argus_1.toolHookTimeout)("cursor", "seconds");
|
|
68
|
+
function resolveTarget(args) {
|
|
69
|
+
const project = args.includes("--project");
|
|
70
|
+
const baseDir = project
|
|
71
|
+
? path.join(process.cwd(), ".cursor")
|
|
72
|
+
: path.join(os.homedir(), ".cursor");
|
|
73
|
+
return {
|
|
74
|
+
baseDir,
|
|
75
|
+
hooksPath: path.join(baseDir, "hooks.json"),
|
|
76
|
+
mcpPath: path.join(baseDir, "mcp.json"),
|
|
77
|
+
rulesDir: path.join(baseDir, "rules"),
|
|
78
|
+
scope: project ? "project" : "user",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function readJson(file) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function writeJson(file, value) {
|
|
90
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
91
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve this install's runtime and write its shims.
|
|
95
|
+
*
|
|
96
|
+
* Cursor's `preToolUse` gate is registered `failClosed: true` (see
|
|
97
|
+
* {@link oryHooksBlock}), so a hook that cannot answer blocks every tool. That
|
|
98
|
+
* makes the shim's runtime-missing path load-bearing: it has to emit an
|
|
99
|
+
* explicit allow, not just exit 0.
|
|
100
|
+
*/
|
|
101
|
+
function wireCursorRuntime(args) {
|
|
102
|
+
return (0, argus_1.wireRuntime)({
|
|
103
|
+
harness: "cursor",
|
|
104
|
+
packageName: PACKAGE_NAME,
|
|
105
|
+
packageRoot: PACKAGE_ROOT,
|
|
106
|
+
installCommand: "npx -y -p @ory/cursor ory-cursor install",
|
|
107
|
+
args,
|
|
108
|
+
fallbackStdout: JSON.stringify({ permission: "allow" }),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the Ory hooks block. Only the load-bearing events are registered:
|
|
113
|
+
* `preToolUse` is the permission gate; the rest are audit/identity. The
|
|
114
|
+
* fine-grained shell/MCP/file hooks are intentionally omitted — `preToolUse`
|
|
115
|
+
* already covers those categories, and registering them would spawn a hook
|
|
116
|
+
* subprocess per file read / shell line for no added enforcement.
|
|
117
|
+
*
|
|
118
|
+
* The `preToolUse` gate is registered `failClosed: true`. Cursor's hook default
|
|
119
|
+
* is fail-open: a crashed / timed-out / malformed-JSON hook lets the tool
|
|
120
|
+
* through. For the one hook that makes the permission decision that would
|
|
121
|
+
* silently defeat `enforce` mode (the whole point of the gate), so we opt it
|
|
122
|
+
* into fail-closed — if the gate can't produce a verdict, Cursor blocks the
|
|
123
|
+
* tool. The gate still fails *open* on the paths Ory treats as non-blocking
|
|
124
|
+
* (network / rate-limit), because those are handled inside the hook, which
|
|
125
|
+
* exits 0 with an allow verdict; `failClosed` only governs the hook process
|
|
126
|
+
* itself failing to answer. The audit/identity hooks stay fail-open — none of
|
|
127
|
+
* them gate a tool, and session start must never block.
|
|
128
|
+
*/
|
|
129
|
+
function oryHooksBlock(command) {
|
|
130
|
+
const entry = (timeout, failClosed = false) => [
|
|
131
|
+
failClosed ? { command, timeout, failClosed } : { command, timeout },
|
|
132
|
+
];
|
|
133
|
+
return {
|
|
134
|
+
sessionStart: entry(SESSION_START_TIMEOUT_SEC),
|
|
135
|
+
sessionEnd: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
136
|
+
preToolUse: entry(TOOL_HOOK_TIMEOUT_SEC, true),
|
|
137
|
+
postToolUse: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
138
|
+
postToolUseFailure: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
139
|
+
beforeSubmitPrompt: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
140
|
+
subagentStart: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
141
|
+
subagentStop: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function isOryEntry(entry) {
|
|
145
|
+
return (typeof entry === "object" &&
|
|
146
|
+
entry !== null &&
|
|
147
|
+
typeof entry.command === "string" &&
|
|
148
|
+
(0, argus_1.isOryRuntimeCommand)(entry.command));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Merge the Ory hooks into an existing hooks.json shape, dropping any prior Ory
|
|
152
|
+
* entries (idempotent re-install) and preserving every non-Ory entry.
|
|
153
|
+
*/
|
|
154
|
+
function mergeHooks(existing, command) {
|
|
155
|
+
const hooks = existing.hooks ?? {};
|
|
156
|
+
const merged = {};
|
|
157
|
+
// Keep every existing event's non-Ory entries.
|
|
158
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
159
|
+
const kept = Array.isArray(entries) ? entries.filter((e) => !isOryEntry(e)) : [];
|
|
160
|
+
if (kept.length > 0)
|
|
161
|
+
merged[event] = kept;
|
|
162
|
+
}
|
|
163
|
+
// Add the Ory entries.
|
|
164
|
+
for (const [event, entries] of Object.entries(oryHooksBlock(command))) {
|
|
165
|
+
merged[event] = [...(merged[event] ?? []), ...entries];
|
|
166
|
+
}
|
|
167
|
+
return { ...existing, version: existing.version ?? 1, hooks: merged };
|
|
168
|
+
}
|
|
169
|
+
function removeOryHooks(existing) {
|
|
170
|
+
const hooks = existing.hooks ?? {};
|
|
171
|
+
const cleaned = {};
|
|
172
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
173
|
+
const kept = Array.isArray(entries) ? entries.filter((e) => !isOryEntry(e)) : [];
|
|
174
|
+
if (kept.length > 0)
|
|
175
|
+
cleaned[event] = kept;
|
|
176
|
+
}
|
|
177
|
+
return { ...existing, hooks: cleaned };
|
|
178
|
+
}
|
|
179
|
+
function mergeMcp(existing, server) {
|
|
180
|
+
const servers = existing.mcpServers ?? {};
|
|
181
|
+
return { ...existing, mcpServers: { ...servers, [MCP_KEY]: server } };
|
|
182
|
+
}
|
|
183
|
+
function removeMcp(existing) {
|
|
184
|
+
const servers = { ...(existing.mcpServers ?? {}) };
|
|
185
|
+
delete servers[MCP_KEY];
|
|
186
|
+
return { ...existing, mcpServers: servers };
|
|
187
|
+
}
|
|
188
|
+
function install(args) {
|
|
189
|
+
const target = resolveTarget(args);
|
|
190
|
+
if (args.includes("--print")) {
|
|
191
|
+
// Nothing is resolved or written on a --print run.
|
|
192
|
+
console.log(JSON.stringify(mergeHooks(readJson(target.hooksPath), 'node "<runtime shim>"'), null, 2));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const runtime = wireCursorRuntime(args);
|
|
196
|
+
console.log(runtime.target.kind === "linked"
|
|
197
|
+
? `Runtime: linked to ${runtime.target.packageDir} (dev)`
|
|
198
|
+
: `Runtime: ${runtime.target.packageName}@${runtime.target.version} in ${runtime.target.storeDir}`);
|
|
199
|
+
for (const pruned of runtime.prunedStores) {
|
|
200
|
+
console.log(` Removed stale runtime: ${pruned}`);
|
|
201
|
+
}
|
|
202
|
+
writeJson(target.hooksPath, mergeHooks(readJson(target.hooksPath), (0, argus_1.requireHookCommand)(runtime)));
|
|
203
|
+
if (runtime.mcpServer) {
|
|
204
|
+
writeJson(target.mcpPath, mergeMcp(readJson(target.mcpPath), runtime.mcpServer));
|
|
205
|
+
}
|
|
206
|
+
(0, assets_js_1.installCursorOryRules)(target.rulesDir);
|
|
207
|
+
console.log(`Installed the Ory plugin for Cursor (${target.scope} scope):`);
|
|
208
|
+
console.log(` Hooks: ${target.hooksPath}`);
|
|
209
|
+
if (runtime.mcpServer) {
|
|
210
|
+
console.log(` MCP: ${target.mcpPath} (mcpServers.${MCP_KEY})`);
|
|
211
|
+
}
|
|
212
|
+
console.log(` Rules: ${target.rulesDir}/ory-*.mdc`);
|
|
213
|
+
printNextSteps();
|
|
214
|
+
}
|
|
215
|
+
function uninstall(args) {
|
|
216
|
+
const target = resolveTarget(args);
|
|
217
|
+
if (fs.existsSync(target.hooksPath)) {
|
|
218
|
+
writeJson(target.hooksPath, removeOryHooks(readJson(target.hooksPath)));
|
|
219
|
+
}
|
|
220
|
+
if (fs.existsSync(target.mcpPath)) {
|
|
221
|
+
writeJson(target.mcpPath, removeMcp(readJson(target.mcpPath)));
|
|
222
|
+
}
|
|
223
|
+
(0, assets_js_1.uninstallCursorOryRules)(target.rulesDir);
|
|
224
|
+
(0, argus_1.removeRuntimeWiring)("cursor");
|
|
225
|
+
for (const pruned of (0, argus_1.pruneRuntimeStores)()) {
|
|
226
|
+
console.log(` Removed runtime: ${pruned}`);
|
|
227
|
+
}
|
|
228
|
+
console.log(`Removed the Ory plugin for Cursor (${target.scope} scope).`);
|
|
229
|
+
}
|
|
230
|
+
function printNextSteps() {
|
|
231
|
+
const npx = "npx -y -p @ory/cursor ory-cursor";
|
|
232
|
+
(0, argus_1.nextStepsSink)(({ configured }) => {
|
|
233
|
+
console.log("");
|
|
234
|
+
console.log("Next steps:");
|
|
235
|
+
console.log(" Restart Cursor (or reload the window) so it picks up the new hooks.");
|
|
236
|
+
console.log("");
|
|
237
|
+
if (configured) {
|
|
238
|
+
if (!(0, argus_1.isSecurityConnected)()) {
|
|
239
|
+
console.log(" Agent Security not connected: tool activity is recorded locally — no checks, nothing blocked.");
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(" It starts in observe mode: every tool call is checked, nothing blocked yet.");
|
|
243
|
+
console.log(" Turn on enforcement: in the Ory Console (Agent Security)");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(" (Optional) Point at an Ory project — without it, the plugin runs in");
|
|
248
|
+
console.log(" pass-through mode (rules work, nothing is blocked):");
|
|
249
|
+
console.log(` ${npx} configure --project-url https://<slug>.projects.oryapis.com`);
|
|
250
|
+
console.log(" The broker and login client use production defaults; flags can override them.");
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(" Or spin up the local Ory stack: ask Cursor to run the ory-local-up rule.");
|
|
253
|
+
}
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(` See what Ory is doing: ${npx} status`);
|
|
256
|
+
console.log(" Stream activity live: export ORY_AGENT_DEBUG=true");
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log(`To uninstall: ${npx} uninstall`);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
// --- Standalone binary entry point (ory-cursor-setup) ---
|
|
262
|
+
if (require.main === module) {
|
|
263
|
+
const args = process.argv.slice(2);
|
|
264
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
265
|
+
console.log(`
|
|
266
|
+
ory-cursor-setup — Install or remove the Ory plugin for Cursor
|
|
267
|
+
|
|
268
|
+
Usage:
|
|
269
|
+
npx -y -p @ory/cursor ory-cursor-setup [options]
|
|
270
|
+
|
|
271
|
+
Options:
|
|
272
|
+
--uninstall Remove the Ory hooks, MCP server, and rules
|
|
273
|
+
--project Target <cwd>/.cursor instead of ~/.cursor
|
|
274
|
+
--print Print the merged hooks.json without writing (install only)
|
|
275
|
+
-h, --help Show this help
|
|
276
|
+
`);
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
if (args.includes("--uninstall")) {
|
|
280
|
+
uninstall(args);
|
|
281
|
+
(0, argus_1.clearCredentialsForUninstall)({
|
|
282
|
+
harness: "cursor",
|
|
283
|
+
purge: process.argv.includes("--purge"),
|
|
284
|
+
}).then(() => process.exit(0), (err) => {
|
|
285
|
+
console.error(err.message ?? err);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
install(args);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity, ensureSubAgentIdentity } from "@ory/argus";
|
|
2
|
+
import type { CursorHookInput, CursorHookOutput } from "./types.js";
|
|
3
|
+
export interface HandleHookEventDeps {
|
|
4
|
+
/** Test injection point for the user login flow. */
|
|
5
|
+
userLogin?: typeof ensureUserAuthenticated;
|
|
6
|
+
/** Test injection point for the agent identity gate. */
|
|
7
|
+
agentGate?: typeof ensureAgentIdentity;
|
|
8
|
+
/** Test injection point for the sub-agent identity resolver. */
|
|
9
|
+
subAgentGate?: typeof ensureSubAgentIdentity;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Route a Cursor hook event to the appropriate Ory integration.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleHookEvent(input: CursorHookInput, client: OryAgentClient, deps?: HandleHookEventDeps): Promise<CursorHookOutput>;
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleHookEvent = handleHookEvent;
|
|
4
|
+
const argus_1 = require("@ory/argus");
|
|
5
|
+
const HARNESS = "cursor";
|
|
6
|
+
/**
|
|
7
|
+
* Cursor doesn't always send `session_id` outside sessionStart — fall back to
|
|
8
|
+
* the conversation id so activity within one conversation still correlates.
|
|
9
|
+
*
|
|
10
|
+
* Returns `undefined` when Cursor sends neither, rather than a placeholder. A
|
|
11
|
+
* constant here would be read as a real session everywhere downstream: the
|
|
12
|
+
* derived per-session subject (`Agent:<clientId>|<session>`) would be the same
|
|
13
|
+
* string for every unidentified run, so a block meant for one of them would hit
|
|
14
|
+
* all of them, and the delegation graph would collapse them onto one node. No
|
|
15
|
+
* session is the honest answer, and the identity model handles it through the
|
|
16
|
+
* reserved sessionless credential slot.
|
|
17
|
+
*/
|
|
18
|
+
function sessionKey(input) {
|
|
19
|
+
return input.session_id ?? input.conversation_id;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Route a Cursor hook event to the appropriate Ory integration.
|
|
23
|
+
*/
|
|
24
|
+
async function handleHookEvent(input, client, deps = {}) {
|
|
25
|
+
const event = input.hook_event_name;
|
|
26
|
+
client.logger.debug("hook.received", {
|
|
27
|
+
event,
|
|
28
|
+
sessionId: sessionKey(input),
|
|
29
|
+
toolName: input.tool_name,
|
|
30
|
+
});
|
|
31
|
+
return (0, argus_1.withHookContext)(client, {
|
|
32
|
+
sessionId: sessionKey(input),
|
|
33
|
+
}, async () => {
|
|
34
|
+
switch (event) {
|
|
35
|
+
case "sessionStart":
|
|
36
|
+
return await handleSessionStart(input, client, deps);
|
|
37
|
+
case "preToolUse":
|
|
38
|
+
return await handlePreToolUse(input, client);
|
|
39
|
+
case "postToolUse":
|
|
40
|
+
return await handlePostToolUse(input, client);
|
|
41
|
+
case "postToolUseFailure":
|
|
42
|
+
return await handlePostToolUseFailure(input, client);
|
|
43
|
+
case "beforeSubmitPrompt":
|
|
44
|
+
return await handleBeforeSubmitPrompt(input, client);
|
|
45
|
+
case "subagentStart":
|
|
46
|
+
return await handleSubagentStart(input, client, deps);
|
|
47
|
+
case "subagentStop":
|
|
48
|
+
return await handleSubagentStop(input, client);
|
|
49
|
+
case "sessionEnd":
|
|
50
|
+
return await handleSessionEnd(input, client);
|
|
51
|
+
case "preCompact":
|
|
52
|
+
return await handleAudit(input, client, "compaction", {
|
|
53
|
+
trigger: input.trigger,
|
|
54
|
+
});
|
|
55
|
+
// Fine-grained shell / MCP / file hooks surface activity to the user and
|
|
56
|
+
// are activity-only — `preToolUse` already ran the permission gate for the
|
|
57
|
+
// corresponding tool category, so gating here would double-count.
|
|
58
|
+
case "beforeShellExecution":
|
|
59
|
+
case "afterShellExecution":
|
|
60
|
+
return await handleAudit(input, client, "user.interaction", {
|
|
61
|
+
kind: event,
|
|
62
|
+
hasCommand: input.command !== undefined,
|
|
63
|
+
});
|
|
64
|
+
case "beforeMCPExecution":
|
|
65
|
+
case "afterMCPExecution":
|
|
66
|
+
return await handleAudit(input, client, "user.interaction", {
|
|
67
|
+
kind: event,
|
|
68
|
+
mcpTool: input.tool_name,
|
|
69
|
+
});
|
|
70
|
+
case "beforeReadFile":
|
|
71
|
+
case "afterFileEdit":
|
|
72
|
+
return await handleAudit(input, client, "user.interaction", {
|
|
73
|
+
kind: event,
|
|
74
|
+
hasFilePath: input.file_path !== undefined,
|
|
75
|
+
});
|
|
76
|
+
default:
|
|
77
|
+
client.logger.debug("hook.passthrough", { event });
|
|
78
|
+
client.logger.activity("hook.passthrough", "skipped", {
|
|
79
|
+
attributes: { event },
|
|
80
|
+
});
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ─── sessionStart ───────────────────────────────────────────────────
|
|
86
|
+
async function handleSessionStart(input, client, deps) {
|
|
87
|
+
client.logger.info("lifecycle.session_start", {
|
|
88
|
+
sessionId: sessionKey(input),
|
|
89
|
+
model: input.model,
|
|
90
|
+
composerMode: input.composer_mode,
|
|
91
|
+
});
|
|
92
|
+
await (0, argus_1.sessionStart)(client, {
|
|
93
|
+
binName: "ory-cursor",
|
|
94
|
+
harness: HARNESS,
|
|
95
|
+
userLogin: deps.userLogin,
|
|
96
|
+
agentGate: deps.agentGate,
|
|
97
|
+
activityAttributes: { model: input.model, composerMode: input.composer_mode },
|
|
98
|
+
});
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
// ─── preToolUse — the permission gate ───────────────────────────────
|
|
102
|
+
async function handlePreToolUse(input, client) {
|
|
103
|
+
const toolName = input.tool_name ?? "unknown";
|
|
104
|
+
client.logger.info("lifecycle.pre_tool_use", {
|
|
105
|
+
sessionId: sessionKey(input),
|
|
106
|
+
toolName,
|
|
107
|
+
toolInput: input.tool_input,
|
|
108
|
+
});
|
|
109
|
+
// Only offer a `session:` fallback subject when Cursor actually sent a
|
|
110
|
+
// session — interpolating an absent one yields the literal
|
|
111
|
+
// `session:undefined`, a subject that matches nothing yet looks like a real
|
|
112
|
+
// one in the audit trail.
|
|
113
|
+
const session = sessionKey(input);
|
|
114
|
+
const subAgentType = input.subagent_type;
|
|
115
|
+
const subAgent = subAgentType
|
|
116
|
+
? session
|
|
117
|
+
? (0, argus_1.loadSubAgentDynamicCredentials)(HARNESS, session, subAgentType)
|
|
118
|
+
: (0, argus_1.loadSubAgentDynamicCredentials)(HARNESS, subAgentType)
|
|
119
|
+
: undefined;
|
|
120
|
+
const result = await (0, argus_1.gate)(client, {
|
|
121
|
+
harness: HARNESS,
|
|
122
|
+
toolName,
|
|
123
|
+
toolArgs: input.tool_input,
|
|
124
|
+
subjectFallback: session ? `session:${session}` : undefined,
|
|
125
|
+
principals: subAgent
|
|
126
|
+
? {
|
|
127
|
+
subAgentClientId: subAgent.clientId,
|
|
128
|
+
subAgentType,
|
|
129
|
+
perSpawnId: input.subagent_id,
|
|
130
|
+
sessionId: session,
|
|
131
|
+
}
|
|
132
|
+
: undefined,
|
|
133
|
+
});
|
|
134
|
+
if (result.blocked) {
|
|
135
|
+
const reason = result.denialMessage;
|
|
136
|
+
return { permission: "deny", agent_message: reason, user_message: reason };
|
|
137
|
+
}
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
// ─── postToolUse ────────────────────────────────────────────────────
|
|
141
|
+
async function handlePostToolUse(input, client) {
|
|
142
|
+
const toolName = input.tool_name ?? "unknown";
|
|
143
|
+
client.logger.info("lifecycle.post_tool_use", {
|
|
144
|
+
sessionId: sessionKey(input),
|
|
145
|
+
toolName,
|
|
146
|
+
});
|
|
147
|
+
(0, argus_1.complete)(client, {
|
|
148
|
+
toolName,
|
|
149
|
+
input: input.tool_input,
|
|
150
|
+
output: input.tool_output,
|
|
151
|
+
extraActivityAttributes: {
|
|
152
|
+
toolUseId: input.tool_use_id,
|
|
153
|
+
durationMs: input.duration,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
// ─── postToolUseFailure ─────────────────────────────────────────────
|
|
159
|
+
async function handlePostToolUseFailure(input, client) {
|
|
160
|
+
const toolName = input.tool_name ?? "unknown";
|
|
161
|
+
client.logger.info("lifecycle.post_tool_use_failure", {
|
|
162
|
+
sessionId: sessionKey(input),
|
|
163
|
+
toolName,
|
|
164
|
+
failureType: input.failure_type,
|
|
165
|
+
});
|
|
166
|
+
if (input.error_message !== undefined) {
|
|
167
|
+
client.logger.debug("lifecycle.post_tool_use_failure.error", {
|
|
168
|
+
error: input.error_message,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
(0, argus_1.fail)(client, {
|
|
172
|
+
toolName,
|
|
173
|
+
input: input.tool_input,
|
|
174
|
+
extraActivityAttributes: {
|
|
175
|
+
toolUseId: input.tool_use_id,
|
|
176
|
+
failureType: input.failure_type,
|
|
177
|
+
hasError: input.error_message !== undefined,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
// ─── beforeSubmitPrompt ─────────────────────────────────────────────
|
|
183
|
+
async function handleBeforeSubmitPrompt(input, client) {
|
|
184
|
+
client.logger.info("lifecycle.before_submit_prompt", {
|
|
185
|
+
sessionId: sessionKey(input),
|
|
186
|
+
promptLen: input.prompt?.length,
|
|
187
|
+
});
|
|
188
|
+
client.logger.activity("user.prompt", "ok", {
|
|
189
|
+
attributes: { promptLen: input.prompt?.length },
|
|
190
|
+
});
|
|
191
|
+
// Never block the human's prompt — the user is the decision-maker.
|
|
192
|
+
return { continue: true };
|
|
193
|
+
}
|
|
194
|
+
// ─── subagentStart / subagentStop ───────────────────────────────────
|
|
195
|
+
async function handleSubagentStart(input, client, deps) {
|
|
196
|
+
const subAgentType = input.subagent_type;
|
|
197
|
+
client.logger.info("lifecycle.subagent_start", {
|
|
198
|
+
sessionId: sessionKey(input),
|
|
199
|
+
subAgentType,
|
|
200
|
+
subAgentId: input.subagent_id,
|
|
201
|
+
});
|
|
202
|
+
client.logger.activity("subagent.start", "ok", {
|
|
203
|
+
attributes: { subAgentType, subAgentId: input.subagent_id },
|
|
204
|
+
});
|
|
205
|
+
if (subAgentType && (0, argus_1.isSecurityConnected)()) {
|
|
206
|
+
await (0, argus_1.registerSubagent)(client, {
|
|
207
|
+
harness: HARNESS,
|
|
208
|
+
subAgentType,
|
|
209
|
+
perSpawnId: input.subagent_id,
|
|
210
|
+
subAgentGate: deps.subAgentGate,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Subagent creation is never blocked — identity/audit only.
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
async function handleSubagentStop(input, client) {
|
|
217
|
+
client.logger.info("lifecycle.subagent_stop", {
|
|
218
|
+
sessionId: sessionKey(input),
|
|
219
|
+
subAgentType: input.subagent_type,
|
|
220
|
+
status: input.status,
|
|
221
|
+
});
|
|
222
|
+
client.logger.activity("subagent.stop", "ok", {
|
|
223
|
+
attributes: {
|
|
224
|
+
subAgentType: input.subagent_type,
|
|
225
|
+
status: input.status,
|
|
226
|
+
summaryLen: input.summary?.length,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
// ─── sessionEnd ─────────────────────────────────────────────────────
|
|
232
|
+
async function handleSessionEnd(input, client) {
|
|
233
|
+
client.logger.info("lifecycle.session_end", {
|
|
234
|
+
sessionId: sessionKey(input),
|
|
235
|
+
reason: input.reason,
|
|
236
|
+
});
|
|
237
|
+
client.logger.activity("session.end", "ok", {
|
|
238
|
+
attributes: { reason: input.reason, durationMs: input.duration_ms },
|
|
239
|
+
});
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
// ─── Generic activity event ─────────────────────────────────────────
|
|
243
|
+
async function handleAudit(input, client, activity, attributes) {
|
|
244
|
+
client.logger.debug("lifecycle.audit", {
|
|
245
|
+
sessionId: sessionKey(input),
|
|
246
|
+
event: input.hook_event_name,
|
|
247
|
+
activity,
|
|
248
|
+
});
|
|
249
|
+
client.logger.activity(activity, "ok", { attributes });
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
package/dist/hook.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor hook entry point.
|
|
4
|
+
* Invoked by Cursor as a subprocess for each hook event.
|
|
5
|
+
* Input: JSON on stdin. Output: JSON on stdout.
|
|
6
|
+
* A block is signaled by `permission: "deny"` / `continue: false` in the JSON
|
|
7
|
+
* output; exit code 2 is honored as an equivalent block signal.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/hook.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Cursor hook entry point.
|
|
5
|
+
* Invoked by Cursor as a subprocess for each hook event.
|
|
6
|
+
* Input: JSON on stdin. Output: JSON on stdout.
|
|
7
|
+
* A block is signaled by `permission: "deny"` / `continue: false` in the JSON
|
|
8
|
+
* output; exit code 2 is honored as an equivalent block signal.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const argus_1 = require("@ory/argus");
|
|
12
|
+
const handlers_js_1 = require("./handlers.js");
|
|
13
|
+
const EVENT_FLUSH_TIMEOUT_MS = 750;
|
|
14
|
+
let client;
|
|
15
|
+
async function flushEvents() {
|
|
16
|
+
try {
|
|
17
|
+
await client?.flushEvents(EVENT_FLUSH_TIMEOUT_MS);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// The hook decision must not depend on activity delivery.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read all of stdin. Resolves on `end`, or after a short idle gap once data has
|
|
25
|
+
* arrived (some parents don't close stdin promptly).
|
|
26
|
+
*/
|
|
27
|
+
function readStdin() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let resolved = false;
|
|
31
|
+
function done() {
|
|
32
|
+
if (!resolved) {
|
|
33
|
+
resolved = true;
|
|
34
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let idleTimer;
|
|
38
|
+
process.stdin.on("data", (chunk) => {
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
clearTimeout(idleTimer);
|
|
41
|
+
idleTimer = setTimeout(done, 100);
|
|
42
|
+
});
|
|
43
|
+
process.stdin.on("end", done);
|
|
44
|
+
process.stdin.on("error", (err) => {
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
resolved = true;
|
|
47
|
+
reject(err);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function main() {
|
|
53
|
+
client = argus_1.OryAgentClient.fromEnv("cursor");
|
|
54
|
+
const raw = await readStdin();
|
|
55
|
+
let input;
|
|
56
|
+
try {
|
|
57
|
+
input = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
61
|
+
process.exit(0); // Don't block on parse errors
|
|
62
|
+
}
|
|
63
|
+
const output = await (0, handlers_js_1.handleHookEvent)(input, client);
|
|
64
|
+
const blocked = output.permission === "deny" || output.continue === false;
|
|
65
|
+
if (Object.keys(output).length > 0) {
|
|
66
|
+
process.stdout.write(JSON.stringify(output));
|
|
67
|
+
}
|
|
68
|
+
await flushEvents();
|
|
69
|
+
process.exit(blocked ? 2 : 0);
|
|
70
|
+
}
|
|
71
|
+
main().catch(async (err) => {
|
|
72
|
+
process.stderr.write(`[ory-agent] fatal: ${err}\n`);
|
|
73
|
+
await flushEvents();
|
|
74
|
+
process.exit(0); // Fail open
|
|
75
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleHookEvent = void 0;
|
|
4
|
+
var handlers_js_1 = require("./handlers.js");
|
|
5
|
+
Object.defineProperty(exports, "handleHookEvent", { enumerable: true, get: function () { return handlers_js_1.handleHookEvent; } });
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor hook event payload delivered on stdin, and the response written to
|
|
3
|
+
* stdout.
|
|
4
|
+
*
|
|
5
|
+
* Cursor runs each hook as a standalone subprocess: it writes a JSON object to
|
|
6
|
+
* the hook's stdin and reads a JSON object from stdout. Blocking-capable hooks
|
|
7
|
+
* (`preToolUse`, `beforeShellExecution`, `beforeMCPExecution`, `beforeReadFile`,
|
|
8
|
+
* `subagentStart`) honor `permission: "deny"`; `beforeSubmitPrompt` blocks with
|
|
9
|
+
* `continue: false`. Exit code 2 also blocks. See https://cursor.com/docs/hooks
|
|
10
|
+
*
|
|
11
|
+
* Every hook additionally receives a set of base fields (conversation_id,
|
|
12
|
+
* generation_id, hook_event_name, workspace_roots, …). We model the union of
|
|
13
|
+
* everything the handler reads; unknown fields are ignored.
|
|
14
|
+
*/
|
|
15
|
+
export interface CursorHookInput {
|
|
16
|
+
/** Always present — the event that fired (e.g. "preToolUse"). */
|
|
17
|
+
hook_event_name: string;
|
|
18
|
+
conversation_id?: string;
|
|
19
|
+
generation_id?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
cursor_version?: string;
|
|
22
|
+
workspace_roots?: string[];
|
|
23
|
+
user_email?: string | null;
|
|
24
|
+
transcript_path?: string | null;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
session_id?: string;
|
|
27
|
+
is_background_agent?: boolean;
|
|
28
|
+
composer_mode?: string;
|
|
29
|
+
/** sessionEnd */
|
|
30
|
+
reason?: string;
|
|
31
|
+
duration_ms?: number;
|
|
32
|
+
tool_name?: string;
|
|
33
|
+
tool_input?: Record<string, unknown>;
|
|
34
|
+
/** postToolUse — JSON-stringified tool output. */
|
|
35
|
+
tool_output?: string;
|
|
36
|
+
tool_use_id?: string;
|
|
37
|
+
duration?: number;
|
|
38
|
+
/** postToolUseFailure */
|
|
39
|
+
error_message?: string;
|
|
40
|
+
failure_type?: "error" | "timeout" | "permission_denied" | string;
|
|
41
|
+
command?: string;
|
|
42
|
+
sandbox?: boolean;
|
|
43
|
+
output?: string;
|
|
44
|
+
/** MCP url (remote) — present for MCP hooks. */
|
|
45
|
+
url?: string;
|
|
46
|
+
file_path?: string;
|
|
47
|
+
edits?: Array<{
|
|
48
|
+
old_string?: string;
|
|
49
|
+
new_string?: string;
|
|
50
|
+
}>;
|
|
51
|
+
prompt?: string;
|
|
52
|
+
attachments?: Array<{
|
|
53
|
+
type?: string;
|
|
54
|
+
file_path?: string;
|
|
55
|
+
}>;
|
|
56
|
+
subagent_id?: string;
|
|
57
|
+
subagent_type?: string;
|
|
58
|
+
task?: string;
|
|
59
|
+
parent_conversation_id?: string;
|
|
60
|
+
tool_call_id?: string;
|
|
61
|
+
status?: string;
|
|
62
|
+
summary?: string;
|
|
63
|
+
trigger?: "auto" | "manual" | string;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Cursor hook output written to stdout.
|
|
68
|
+
*
|
|
69
|
+
* - `permission: "deny"` on `preToolUse` / `beforeShellExecution` /
|
|
70
|
+
* `beforeMCPExecution` / `beforeReadFile` / `subagentStart` blocks the action.
|
|
71
|
+
* `agent_message` is surfaced to the model; `user_message` to the human.
|
|
72
|
+
* - `continue: false` on `beforeSubmitPrompt` blocks the submission.
|
|
73
|
+
* - Fire-and-forget hooks (sessionStart, post*, after*, stop, …) return `{}`.
|
|
74
|
+
*/
|
|
75
|
+
export interface CursorHookOutput {
|
|
76
|
+
permission?: "allow" | "deny" | "ask";
|
|
77
|
+
continue?: boolean;
|
|
78
|
+
agent_message?: string;
|
|
79
|
+
user_message?: string;
|
|
80
|
+
/** preToolUse may rewrite the tool input; unused here. */
|
|
81
|
+
updated_input?: Record<string, unknown>;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1 +1,76 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@ory/cursor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ory plugin for Cursor: scaffolding rules, a local Ory instance, and authentication, authorization, and audit for every tool call",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://github.com/ory/ory-agent-plugins/tree/main/packages/cursor",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/ory/ory-agent-plugins/issues"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"ory",
|
|
12
|
+
"cursor",
|
|
13
|
+
"cursor-editor",
|
|
14
|
+
"hooks",
|
|
15
|
+
"plugin",
|
|
16
|
+
"identity",
|
|
17
|
+
"authentication",
|
|
18
|
+
"authorization",
|
|
19
|
+
"access-control",
|
|
20
|
+
"permissions",
|
|
21
|
+
"zanzibar",
|
|
22
|
+
"oauth2",
|
|
23
|
+
"audit",
|
|
24
|
+
"agent",
|
|
25
|
+
"ai-agent",
|
|
26
|
+
"agent-security",
|
|
27
|
+
"guardrails",
|
|
28
|
+
"mcp",
|
|
29
|
+
"activity-auditing",
|
|
30
|
+
"observability"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/",
|
|
35
|
+
"provenance": true
|
|
36
|
+
},
|
|
37
|
+
"reova": {
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"endpoint": "https://telemetry.reo.dev/data"
|
|
40
|
+
},
|
|
41
|
+
"main": "dist/index.js",
|
|
42
|
+
"types": "dist/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"default": "./dist/index.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"bin": {
|
|
50
|
+
"ory-cursor": "dist/cli/main.js",
|
|
51
|
+
"ory-cursor-hook": "dist/hook.js",
|
|
52
|
+
"ory-cursor-setup": "dist/cli/setup.js"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist",
|
|
56
|
+
"!dist/**/*.tsbuildinfo"
|
|
57
|
+
],
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"reova": "^0.7.0",
|
|
60
|
+
"@ory/argus": "1.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"typescript": "^6.0.2",
|
|
64
|
+
"vitest": "4.1.4"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsc",
|
|
71
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:watch": "vitest",
|
|
74
|
+
"typecheck": "tsc --noEmit"
|
|
75
|
+
}
|
|
76
|
+
}
|