@ory/cline 0.10.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,115 @@
1
+ # Ory Agent Plugin: Cline
2
+
3
+ [Ory](https://ory.com) bundled into [Cline](https://github.com/cline/cline): skills that scaffold Ory authentication into your codebase, a local Ory stack you can spin up in one command, and (when pointed at an Ory project) authentication, authorization, and audit for every tool Cline runs.
4
+
5
+ You don't need an Ory account or any prior Ory experience to start.
6
+
7
+ This integration uses Cline's **file-based hooks** (not the in-process SDK) and is verified against cline@3.0.29.
8
+
9
+ ## New to Ory?
10
+
11
+ [Ory](https://www.ory.com/docs/) is an open-source identity and access platform — login, registration, sessions, social sign-in, multi-factor auth, and fine-grained permissions, so you don't have to build any of that yourself. Two things make it easy to try with no prior experience:
12
+
13
+ - **Ory Elements** are prebuilt, themeable UI components for the auth pages. The scaffolding skills wire them into your app for you.
14
+ - **The local Ory stack** is a complete Ory running on your laptop in Docker — no account, no signup, no API key.
15
+
16
+ ## What this plugin does
17
+
18
+ Two independent things, and you can use either on its own:
19
+
20
+ 1. **Build auth into your app.** Have Cline scaffold Ory login, registration, social sign-in, and permissions into the project you're working on, backed by the local stack. This needs nothing but Docker.
21
+ 2. **Govern the agent itself.** Authenticate Cline's own session and authorize every tool it runs against Ory Permissions, with a full audit trail. See [Agent security](#agent-security).
22
+
23
+ ## Prerequisites
24
+
25
+ - [Cline](https://github.com/cline/cline) installed
26
+ - Node.js **≥ 22**
27
+ - [Docker](https://docs.docker.com/get-docker/) (only needed for the local Ory stack)
28
+ - macOS or Linux. Windows works via WSL2.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npx -y -p @ory/cline ory-cline install
34
+ ```
35
+
36
+ Cline discovers hooks by directory — the presence of an executable script named after the event *is* the registration. `install` writes one thin shell shim per event into the workspace hooks directory `<project>/.clinerules/hooks/` (or `~/.cline/hooks/` with `--global`), `chmod 0755`s each, and materializes the Ory skills. Each shim execs the published hook binary via `npx`.
37
+
38
+ ```bash
39
+ npx -y -p @ory/cline ory-cline install --global # install for all projects
40
+ npx -y -p @ory/cline ory-cline status # confirm what landed
41
+ npx -y -p @ory/cline ory-cline uninstall # remove only the Ory shims + skills
42
+ ```
43
+
44
+ Uninstall removes only the scripts the Ory installer wrote (matched by a marker comment) — it never touches a hook script you authored that happens to share an event name.
45
+
46
+ ## Quickstart (≈ 3 minutes)
47
+
48
+ From any project where you'd like Ory authentication, inside Cline:
49
+
50
+ 1. **Start a local Ory instance.** Ask Cline *"start the local Ory stack"* or run `/local-up`. A banner prints the seeded test user's email and password — note them.
51
+ 2. **Scaffold Ory into your project.** Ask Cline *"add Ory auth to this app"* (the `ory-auth-setup` skill). It installs Ory Elements, wires the SDK, and generates the login / registration / recovery / settings pages, all targeting the local stack.
52
+ 3. **Sign in.** Start your app, visit the login page, and sign in with the seeded credentials. You now have a real Ory session backed by a real Ory stack — locally, offline, zero configuration.
53
+
54
+ Continue to [Agent security](#agent-security) when you're ready to enforce.
55
+
56
+ ## How the integration works
57
+
58
+ Cline runs each hook script as a **subprocess**, passing the event payload as JSON on stdin and reading a JSON decision on stdout. A hook blocks a tool call by returning `{"cancel":true}`. Cline does **not** expose a separate approval-ask hook, so the plugin gates at the tool-call event. Hooks have a 30-second timeout.
59
+
60
+ The hook script filenames are PascalCase (`TaskStart`, `PreToolUse`, …); the payload Cline delivers on stdin carries the matching snake_case event name (`agent_start`, `tool_call`, …) the plugin acts on. The shims wire into these phases:
61
+
62
+ - **TaskStart** (`agent_start`) — run the user and agent auth gates.
63
+ - **PreToolUse** (`tool_call`) — check the tool against Ory Permissions; deny blocks the call (`{"cancel":true}`) in enforce mode.
64
+ - **PostToolUse / TaskComplete / TaskCancel / TaskError / UserPromptSubmit / PreCompact / SessionShutdown** — trace-only audit spans.
65
+
66
+ The plugin is **fail-open** on its own infrastructure failures (network errors, rate limits, missing config): the agent always starts, and enforcement is only as strong as your permission grants.
67
+
68
+ ## Agent security
69
+
70
+ Once pointed at an Ory project (local or hosted), Cline's task and every tool call can be governed by Ory.
71
+
72
+ - **Authentication.** The human at the keyboard (the **user**) authenticates interactively via Ory Identities when user login is on (`ORY_USER_LOGIN=true`, off by default — browser PKCE flow on first session, persisted thereafter). The Cline process (the **agent**) gets its own OAuth2 identity via [Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) on first run.
73
+ - **Authorization.** Before any tool runs, the plugin checks [Ory Permissions](https://www.ory.com/docs/keto) (Zanzibar-style relations) against the user's subject and blocks on `deny`.
74
+ - **Audit.** Every decision is recorded as a structured trace span (NDJSON file and/or OTLP export).
75
+
76
+ ### Permission modes: observe → enforce
77
+
78
+ After install the plugin runs in **observe mode**: every tool call is checked, but a deny is recorded as a `permission.observe_deny` audit span and the tool runs anyway.
79
+
80
+ ```bash
81
+ # Grant the current user `use` on every built-in tool (idempotent):
82
+ npx -y -p @ory/cline ory-cline permissions bootstrap
83
+
84
+ # See allowed/denied per tool:
85
+ npx -y -p @ory/cline ory-cline permissions status
86
+
87
+ # Turn on hard blocking once the observe-mode logs look right:
88
+ npx -y -p @ory/cline ory-cline permissions enforce
89
+ ```
90
+
91
+ Switch back any time with `permissions observe`. To disable Ory entirely (audit logging only), run `ory-cline configure --audit-only`.
92
+
93
+ ## Pointing at a real Ory project
94
+
95
+ The Quickstart uses the local stack. To point at a hosted [Ory Network](https://console.ory.sh) project:
96
+
97
+ ```bash
98
+ npx -y -p @ory/cline ory-cline configure \
99
+ --project-url https://<id>.projects.oryapis.com \
100
+ --oauth2-client-id <public OAuth2 client id>
101
+ ```
102
+
103
+ `--oauth2-client-id` is required whenever `--project-url` is set — the user PKCE flow needs a public OAuth2 client registered with the four loopback redirect URIs (`http://127.0.0.1:47823..47826/callback`). See the [repo README](../../README.md) and [`AGENTS.md`](../../AGENTS.md) for the full environment-variable reference and permission-mode semantics. Config is shared across every Ory agent plugin at `~/.config/ory-agent-plugins/config.json`.
104
+
105
+ Without configuration the plugin still loads cleanly and runs in **pass-through mode**: skills work, but nothing is blocked.
106
+
107
+ ## Links
108
+
109
+ - [Ory documentation](https://www.ory.com/docs/)
110
+ - [Cline repository](https://github.com/cline/cline)
111
+ - [Repo README](../../README.md) and [AGENTS.md](../../AGENTS.md) — full env-var and permission-mode reference
112
+
113
+ ## License
114
+
115
+ Apache-2.0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Materialize / clean the Ory skill catalog and command-skills for the Cline
3
+ * plugin.
4
+ *
5
+ * Cline has no documented native slash-command system we can rely on, so we
6
+ * follow the Codex/OpenClaw pattern (AGENTS.md Step 7.5): the guide skills
7
+ * plus the local-stack commands (rendered as user-invocable skills) all land
8
+ * under a single `skills/<name>/SKILL.md` tree.
9
+ *
10
+ * Verified against cline@3.0.29: skills live alongside Cline's rules under
11
+ * `.clinerules/skills/` (workspace) or `~/.cline/skills/` (global).
12
+ */
13
+ export declare function installClineOryAssets(rulesRoot: string): void;
14
+ /** Remove every skill + command-skill this plugin installed under the root. */
15
+ export declare function uninstallClineOryAssets(rulesRoot: string): void;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Materialize / clean the Ory skill catalog and command-skills for the Cline
4
+ * plugin.
5
+ *
6
+ * Cline has no documented native slash-command system we can rely on, so we
7
+ * follow the Codex/OpenClaw pattern (AGENTS.md Step 7.5): the guide skills
8
+ * plus the local-stack commands (rendered as user-invocable skills) all land
9
+ * under a single `skills/<name>/SKILL.md` tree.
10
+ *
11
+ * Verified against cline@3.0.29: skills live alongside Cline's rules under
12
+ * `.clinerules/skills/` (workspace) or `~/.cline/skills/` (global).
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
+ exports.installClineOryAssets = installClineOryAssets;
49
+ exports.uninstallClineOryAssets = uninstallClineOryAssets;
50
+ const fs = __importStar(require("node:fs"));
51
+ const path = __importStar(require("node:path"));
52
+ const argus_1 = require("@ory/argus");
53
+ const RENDER_OPTS = {
54
+ binName: "ory-cline",
55
+ packageName: "@ory/cline",
56
+ };
57
+ /**
58
+ * Skills directory, relative to the Cline rules root the install CLI passes
59
+ * in (`.clinerules/` for workspace installs, `~/.cline/` for
60
+ * global). The guide skills and command-skills share this one tree.
61
+ */
62
+ function skillsDir(rulesRoot) {
63
+ return path.join(rulesRoot, "skills");
64
+ }
65
+ function installClineOryAssets(rulesRoot) {
66
+ const dir = skillsDir(rulesRoot);
67
+ fs.mkdirSync(dir, { recursive: true });
68
+ const guideSkills = (0, argus_1.renderOrySkills)("cline", RENDER_OPTS);
69
+ const commandSkills = (0, argus_1.renderOryCommands)("cline", RENDER_OPTS).map(argus_1.commandToSkill);
70
+ (0, argus_1.writeSkillTree)(dir, [...guideSkills, ...commandSkills]);
71
+ }
72
+ function safeRmEmpty(dir) {
73
+ try {
74
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
75
+ fs.rmdirSync(dir);
76
+ }
77
+ }
78
+ catch {
79
+ /* ignore — leave the dir if removal fails. */
80
+ }
81
+ }
82
+ /** Remove every skill + command-skill this plugin installed under the root. */
83
+ function uninstallClineOryAssets(rulesRoot) {
84
+ const dir = skillsDir(rulesRoot);
85
+ if (fs.existsSync(dir)) {
86
+ (0, argus_1.removeSkillDirs)(dir, [...argus_1.ORY_SKILL_NAMES, ...argus_1.ORY_COMMAND_SKILL_NAMES]);
87
+ safeRmEmpty(dir);
88
+ }
89
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Main CLI for @ory/cline.
4
+ *
5
+ * Usage:
6
+ * npx ory-cline install [--global] Install Ory hook scripts into Cline
7
+ * npx ory-cline uninstall [--global] Remove the Ory hook scripts
8
+ * npx ory-cline configure Set or view Ory credentials
9
+ * npx ory-cline agent <cmd> Manage the agent's OAuth2 (DCR) identity
10
+ * npx ory-cline permissions <cmd> Manage permission mode and tool permissions
11
+ * npx ory-cline setup [options] Write hook scripts directly (fallback)
12
+ * npx ory-cline status Show plugin status
13
+ * npx ory-cline local <cmd> Manage the local Ory dev environment
14
+ */
15
+ export {};
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Main CLI for @ory/cline.
5
+ *
6
+ * Usage:
7
+ * npx ory-cline install [--global] Install Ory hook scripts into Cline
8
+ * npx ory-cline uninstall [--global] Remove the Ory hook scripts
9
+ * npx ory-cline configure Set or view Ory credentials
10
+ * npx ory-cline agent <cmd> Manage the agent's OAuth2 (DCR) identity
11
+ * npx ory-cline permissions <cmd> Manage permission mode and tool permissions
12
+ * npx ory-cline setup [options] Write hook scripts directly (fallback)
13
+ * npx ory-cline status Show plugin status
14
+ * npx ory-cline local <cmd> Manage the local Ory dev environment
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
+ const path = __importStar(require("node:path"));
51
+ const fs = __importStar(require("node:fs"));
52
+ const os = __importStar(require("node:os"));
53
+ const argus_1 = require("@ory/argus");
54
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
55
+ function main() {
56
+ const [command, ...args] = process.argv.slice(2);
57
+ switch (command) {
58
+ case "install":
59
+ install(args);
60
+ postInstallPermissions("ory-cline", "cline").then(() => process.exit(0), (err) => {
61
+ console.error(err.message ?? err);
62
+ process.exit(1);
63
+ });
64
+ break;
65
+ case "uninstall":
66
+ uninstall(args);
67
+ break;
68
+ case "configure":
69
+ (0, argus_1.runConfigureCommand)("ory-cline", args);
70
+ break;
71
+ case "agent":
72
+ (0, argus_1.runAgentCommand)("ory-cline", args).then((code) => process.exit(code), (err) => {
73
+ console.error(err.message ?? err);
74
+ process.exit(1);
75
+ });
76
+ break;
77
+ case "permissions":
78
+ (0, argus_1.runPermissionsCommand)("ory-cline", "cline", args).then((code) => process.exit(code), (err) => {
79
+ console.error(err.message ?? err);
80
+ process.exit(1);
81
+ });
82
+ break;
83
+ case "setup":
84
+ runSetup(args);
85
+ break;
86
+ case "status":
87
+ status().then(() => process.exit(0), (err) => {
88
+ console.error(err.message ?? err);
89
+ process.exit(1);
90
+ });
91
+ break;
92
+ case "local":
93
+ (0, argus_1.runLocalCommand)("ory-cline", args).catch((err) => {
94
+ console.error(err.message ?? err);
95
+ process.exit(1);
96
+ });
97
+ break;
98
+ case "help":
99
+ case "--help":
100
+ case "-h":
101
+ case undefined:
102
+ help();
103
+ break;
104
+ default:
105
+ console.error(`Unknown command: ${command}`);
106
+ help();
107
+ process.exit(1);
108
+ }
109
+ }
110
+ /**
111
+ * Cline discovers hooks by directory, with no native plugin-install command,
112
+ * so install IS the setup step: write the per-event hook scripts and
113
+ * materialize the Ory skills. The setup module reads its arguments from
114
+ * `process.argv`.
115
+ */
116
+ function install(args) {
117
+ console.log("Installing Ory hook scripts and skills for Cline...");
118
+ runSetup(args);
119
+ (0, argus_1.printEnvHelp)("ory-cline");
120
+ }
121
+ function uninstall(args) {
122
+ runSetup(["--uninstall", ...args]);
123
+ }
124
+ function runSetup(args) {
125
+ process.argv = ["node", "setup.js", ...args];
126
+ require("./setup.js");
127
+ }
128
+ async function postInstallPermissions(binName, harness) {
129
+ const bootstrapped = await (0, argus_1.maybeAutoBootstrap)(binName, harness);
130
+ (0, argus_1.printPermissionsOnboardingHelp)(binName, harness, {
131
+ bootstrappedAutomatically: bootstrapped,
132
+ });
133
+ }
134
+ async function status() {
135
+ await (0, argus_1.runStatusCommand)("ory-cline", "cline", {
136
+ title: "Cline",
137
+ printPluginSection: () => {
138
+ const workspaceHooks = path.join(process.cwd(), ".clinerules", "hooks");
139
+ const globalHooks = path.join(os.homedir(), ".cline", "hooks");
140
+ console.log("Hooks & plugin:");
141
+ console.log(` Hook script: ${fs.existsSync(path.join(PACKAGE_ROOT, "dist", "hook.js")) ? "built" : "NOT BUILT (run pnpm build)"}`);
142
+ console.log(` Workspace dir: ${fs.existsSync(workspaceHooks) ? `installed (${workspaceHooks})` : "not installed"}`);
143
+ console.log(` Global dir: ${fs.existsSync(globalHooks) ? `installed (${globalHooks})` : "not installed"}`);
144
+ },
145
+ });
146
+ }
147
+ function help() {
148
+ console.log(`
149
+ ory-cline — Ory plugin for Cline
150
+
151
+ Usage:
152
+ npx ory-cline <command> [options]
153
+
154
+ Commands:
155
+ install [--global] Install the Ory hook scripts into Cline
156
+ uninstall [--global] Remove the Ory hook scripts from Cline
157
+ configure Set or view Ory project URL and API key
158
+ agent <cmd> Manage the agent's OAuth2 (DCR) identity (status, unregister)
159
+ permissions <cmd> Manage permission mode and tool permissions (status, bootstrap, observe, enforce)
160
+ setup [options] Write hook scripts directly to the hooks directory (fallback)
161
+ status Show plugin status and configuration
162
+ local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
163
+
164
+ After installing, the per-event hook scripts wire into these Cline lifecycle events:
165
+ TaskStart Verify the user and agent identities via Ory
166
+ PreToolUse Check tool permissions via Ory Permissions (blocking)
167
+ PostToolUse Audit log tool executions
168
+
169
+ Examples:
170
+ npx ory-cline install # Install hook scripts into ./.clinerules/hooks
171
+ npx ory-cline install --global # Install into ~/.cline/hooks
172
+ npx ory-cline status # Check configuration
173
+ npx ory-cline uninstall # Remove hook scripts
174
+ `);
175
+ }
176
+ main();
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Setup CLI for the Ory Cline plugin.
4
+ *
5
+ * Cline discovers hooks by directory: it runs executable scripts whose
6
+ * filename matches the event name, found in a hooks directory. There is no
7
+ * per-hook config file — presence of the script IS the registration. We
8
+ * therefore write one thin shell shim per event into the hooks directory and
9
+ * `chmod 0755` it. Each shim execs the published hook binary via npx,
10
+ * forwarding stdin so the JSON payload reaches the Node entry point.
11
+ *
12
+ * Hooks directory (verified against cline@3.0.29):
13
+ * workspace : <projectDir>/.clinerules/hooks/
14
+ * global : ~/.cline/hooks/ (--global, --hooks-dir default)
15
+ *
16
+ * Usage:
17
+ * npx ory-cline-setup # workspace install (cwd)
18
+ * npx ory-cline-setup --project-dir /repo
19
+ * npx ory-cline-setup --global # install to ~/.cline/hooks
20
+ * npx ory-cline-setup --print # print the shim scripts to stdout, write nothing
21
+ * npx ory-cline-setup --uninstall # remove only the Ory-written scripts
22
+ */
23
+ export {};
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Setup CLI for the Ory Cline plugin.
5
+ *
6
+ * Cline discovers hooks by directory: it runs executable scripts whose
7
+ * filename matches the event name, found in a hooks directory. There is no
8
+ * per-hook config file — presence of the script IS the registration. We
9
+ * therefore write one thin shell shim per event into the hooks directory and
10
+ * `chmod 0755` it. Each shim execs the published hook binary via npx,
11
+ * forwarding stdin so the JSON payload reaches the Node entry point.
12
+ *
13
+ * Hooks directory (verified against cline@3.0.29):
14
+ * workspace : <projectDir>/.clinerules/hooks/
15
+ * global : ~/.cline/hooks/ (--global, --hooks-dir default)
16
+ *
17
+ * Usage:
18
+ * npx ory-cline-setup # workspace install (cwd)
19
+ * npx ory-cline-setup --project-dir /repo
20
+ * npx ory-cline-setup --global # install to ~/.cline/hooks
21
+ * npx ory-cline-setup --print # print the shim scripts to stdout, write nothing
22
+ * npx ory-cline-setup --uninstall # remove only the Ory-written scripts
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ const fs = __importStar(require("node:fs"));
59
+ const os = __importStar(require("node:os"));
60
+ const path = __importStar(require("node:path"));
61
+ const argus_1 = require("@ory/argus");
62
+ const assets_js_1 = require("./assets.js");
63
+ const PACKAGE_NAME = "@ory/cline";
64
+ const HOOK_BIN = "ory-cline-hook";
65
+ /**
66
+ * Cline hook events we register. Each maps to a script whose filename matches
67
+ * the PascalCase event name in the hooks directory (verified against
68
+ * cline@3.0.29). At runtime, the payload Cline writes to the script's stdin
69
+ * carries the snake_case `hookName` the handler switches on:
70
+ *
71
+ * TaskStart → agent_start (auth gates)
72
+ * TaskResume → agent_resume (trace-only)
73
+ * TaskCancel → agent_abort (trace-only)
74
+ * TaskComplete → agent_end (trace-only)
75
+ * TaskError → agent_error (trace-only)
76
+ * PreToolUse → tool_call (permission gate, blocking)
77
+ * PostToolUse → tool_result (trace-only)
78
+ * UserPromptSubmit → prompt_submit (trace-only)
79
+ * PreCompact → pre_compact (trace-only)
80
+ * SessionShutdown → session_shutdown (trace-only)
81
+ */
82
+ const HOOK_EVENTS = [
83
+ "TaskStart",
84
+ "TaskResume",
85
+ "TaskCancel",
86
+ "TaskComplete",
87
+ "TaskError",
88
+ "PreToolUse",
89
+ "PostToolUse",
90
+ "UserPromptSubmit",
91
+ "PreCompact",
92
+ "SessionShutdown",
93
+ ];
94
+ /**
95
+ * The version-pinned npx command that each shim execs. Resolving via npx
96
+ * keeps the binary off the PATH requirement and pins the version the shim was
97
+ * generated with (read from this package's own package.json).
98
+ */
99
+ function hookCommand() {
100
+ return `npx -y -p ${PACKAGE_NAME}@${readPackageVersion()} ${HOOK_BIN}`;
101
+ }
102
+ function readPackageVersion() {
103
+ // dist/cli/setup.js -> package root is two levels up.
104
+ const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
105
+ try {
106
+ const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
107
+ return parsed.version ?? "latest";
108
+ }
109
+ catch {
110
+ return "latest";
111
+ }
112
+ }
113
+ /**
114
+ * Body of a per-event shim. A POSIX shell script that execs the hook binary,
115
+ * forwarding stdin/stdout transparently. `exec` replaces the shell so the
116
+ * hook's exit status and stdout flow straight back to Cline.
117
+ */
118
+ function shimBody() {
119
+ return `#!/bin/sh
120
+ # Ory plugin hook shim for Cline. Generated by ${PACKAGE_NAME} — do not edit.
121
+ # Forwards the event payload (stdin JSON) to the Ory hook and returns its
122
+ # JSON decision on stdout.
123
+ exec ${hookCommand()}
124
+ `;
125
+ }
126
+ /**
127
+ * Resolve the hooks directory based on --global / --project-dir.
128
+ *
129
+ * Verified against `cline --help` (2026-06-28): the global hooks dir is
130
+ * `~/.cline/hooks` (the `--hooks-dir` default). The workspace hooks dir is
131
+ * `<projectDir>/.clinerules/hooks` (the string `clinerules/hooks` is present
132
+ * in the cline binary).
133
+ */
134
+ function getHooksDir(args) {
135
+ if (args.global) {
136
+ return path.join(os.homedir(), ".cline", "hooks");
137
+ }
138
+ return path.join(args.projectDir, ".clinerules", "hooks");
139
+ }
140
+ /**
141
+ * Rules root (parent of the hooks dir) where skills are materialized:
142
+ * `.clinerules/` for workspace installs, `~/.cline/` for global installs.
143
+ * Verified against cline@3.0.29.
144
+ */
145
+ function getRulesRoot(args) {
146
+ if (args.global) {
147
+ return path.join(os.homedir(), ".cline");
148
+ }
149
+ return path.join(args.projectDir, ".clinerules");
150
+ }
151
+ /**
152
+ * Marker so uninstall only removes scripts we wrote — never a hook script the
153
+ * user authored that happens to share an event name.
154
+ */
155
+ const ORY_MARKER = `${PACKAGE_NAME}`;
156
+ function isOryShim(file) {
157
+ try {
158
+ return fs.readFileSync(file, "utf-8").includes(ORY_MARKER);
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ function main() {
165
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
166
+ (0, argus_1.printSetupHelp)("ory-cline-setup", "Cline", {
167
+ supportsGlobal: true,
168
+ globalPath: "~/.cline/hooks",
169
+ });
170
+ process.exit(0);
171
+ }
172
+ const args = (0, argus_1.parseSetupArgs)({ supportsGlobal: true });
173
+ const body = shimBody();
174
+ if (args.print) {
175
+ for (const event of HOOK_EVENTS) {
176
+ console.log(`# ── ${event} ───────────────────────────────`);
177
+ console.log(body);
178
+ }
179
+ return;
180
+ }
181
+ const hooksDir = getHooksDir(args);
182
+ const rulesRoot = getRulesRoot(args);
183
+ if (args.uninstall) {
184
+ if (!fs.existsSync(hooksDir)) {
185
+ console.log("No hooks directory found. Nothing to uninstall.");
186
+ }
187
+ else {
188
+ let removed = 0;
189
+ for (const event of HOOK_EVENTS) {
190
+ const file = path.join(hooksDir, event);
191
+ if (fs.existsSync(file) && isOryShim(file)) {
192
+ fs.unlinkSync(file);
193
+ removed++;
194
+ }
195
+ }
196
+ console.log(`Removed ${removed} Ory hook script(s) from ${hooksDir}`);
197
+ }
198
+ (0, assets_js_1.uninstallClineOryAssets)(rulesRoot);
199
+ console.log(`Removed Ory skills from ${path.join(rulesRoot, "skills")}`);
200
+ return;
201
+ }
202
+ fs.mkdirSync(hooksDir, { recursive: true });
203
+ for (const event of HOOK_EVENTS) {
204
+ const file = path.join(hooksDir, event);
205
+ fs.writeFileSync(file, body, { mode: 0o755 });
206
+ // Re-assert the mode in case a pre-existing file ignored the create mode.
207
+ fs.chmodSync(file, 0o755);
208
+ }
209
+ (0, assets_js_1.installClineOryAssets)(rulesRoot);
210
+ console.log(`Ory hook scripts installed to ${hooksDir}`);
211
+ console.log(` Scripts: ${HOOK_EVENTS.join(", ")}`);
212
+ console.log(`Ory skills installed to ${path.join(rulesRoot, "skills")}`);
213
+ (0, argus_1.printNextSteps)("Cline", "npx ory-cline-setup --uninstall");
214
+ }
215
+ main();
@@ -0,0 +1,20 @@
1
+ import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
2
+ import type { ClineHookInput, ClineHookOutput } 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
+ }
9
+ /**
10
+ * Route a Cline file-hook event to the appropriate Ory integration.
11
+ *
12
+ * Cline has no dedicated approval-ask hook, so authorization is enforced at
13
+ * the `tool_call` event. A deny becomes `{ cancel: true, errorMessage }`, which
14
+ * prevents the tool from running.
15
+ *
16
+ * The on-disk script filenames are PascalCase (TaskStart, PreToolUse, …) but
17
+ * the `hookName` field inside the stdin payload is the snake_case
18
+ * `HookEventName` enum (verified against cline@3.0.29) — we switch on that.
19
+ */
20
+ export declare function handleHookEvent(input: ClineHookInput, client: OryAgentClient, deps?: HandleHookEventDeps): Promise<ClineHookOutput>;
@@ -0,0 +1,367 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleHookEvent = handleHookEvent;
4
+ const argus_1 = require("@ory/argus");
5
+ /**
6
+ * Route a Cline file-hook event to the appropriate Ory integration.
7
+ *
8
+ * Cline has no dedicated approval-ask hook, so authorization is enforced at
9
+ * the `tool_call` event. A deny becomes `{ cancel: true, errorMessage }`, which
10
+ * prevents the tool from running.
11
+ *
12
+ * The on-disk script filenames are PascalCase (TaskStart, PreToolUse, …) but
13
+ * the `hookName` field inside the stdin payload is the snake_case
14
+ * `HookEventName` enum (verified against cline@3.0.29) — we switch on that.
15
+ */
16
+ async function handleHookEvent(input, client, deps = {}) {
17
+ const event = input.hookName;
18
+ client.logger.debug("hook.received", {
19
+ event,
20
+ taskId: input.taskId,
21
+ toolName: input.preToolUse?.toolName ?? input.postToolUse?.toolName,
22
+ });
23
+ // Set trace context so all spans within this hook invocation correlate.
24
+ client.tracer.setContext({
25
+ traceId: (0, argus_1.deriveTraceId)(input.taskId),
26
+ sessionId: input.taskId,
27
+ });
28
+ try {
29
+ switch (event) {
30
+ case "agent_start":
31
+ return await handleTaskStart(input, client, deps);
32
+ case "agent_resume":
33
+ return await handleTaskResume(input, client);
34
+ case "agent_abort":
35
+ return await handleTaskCancel(input, client);
36
+ case "agent_end":
37
+ return await handleTaskComplete(input, client);
38
+ case "agent_error":
39
+ return await handleTaskError(input, client);
40
+ case "tool_call":
41
+ return await handlePreToolUse(input, client);
42
+ case "tool_result":
43
+ return await handlePostToolUse(input, client);
44
+ case "prompt_submit":
45
+ return await handleUserPromptSubmit(input, client);
46
+ case "pre_compact":
47
+ return await handlePreCompact(input, client);
48
+ case "session_shutdown":
49
+ return await handleSessionShutdown(input, client);
50
+ default:
51
+ client.logger.debug("hook.passthrough", { event });
52
+ client.tracer.record("hook.passthrough", "skipped", {
53
+ attributes: { event },
54
+ });
55
+ return {};
56
+ }
57
+ }
58
+ finally {
59
+ client.tracer.clearContext();
60
+ }
61
+ }
62
+ // ─── agent_start (session.start) ───────────────────────────────────
63
+ async function handleTaskStart(input, client, deps) {
64
+ client.logger.info("lifecycle.task_start", {
65
+ taskId: input.taskId,
66
+ workspaceRoots: input.workspaceRoots,
67
+ });
68
+ client.tracer.record("session.start", "ok", {
69
+ attributes: { clineVersion: input.clineVersion },
70
+ });
71
+ // 1. User gate. agent_start is a subprocess hook that can carry a hard block
72
+ // (cancel:true), so allowBlock:true.
73
+ const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
74
+ const decision = await userGate(client, {
75
+ binName: "ory-cline",
76
+ harness: "cline",
77
+ allowBlock: true,
78
+ });
79
+ // 2. Agent gate. Never blocks; attaches the agent's bearer token to
80
+ // outgoing Ory API calls.
81
+ const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
82
+ await agentGate(client, {
83
+ projectUrl: (0, argus_1.resolveConfig)().projectUrl,
84
+ harness: "cline",
85
+ });
86
+ // 3. User → agent delegation tuple. Best-effort; never blocks.
87
+ await writeDelegation(client);
88
+ if (!decision.proceed) {
89
+ return {
90
+ cancel: true,
91
+ errorMessage: decision.reason ?? "Ory user login was declined or could not complete.",
92
+ };
93
+ }
94
+ if (decision.mode !== "disabled") {
95
+ return {};
96
+ }
97
+ // Legacy verify/log path (ORY_USER_LOGIN unset).
98
+ const resolved = (0, argus_1.resolveConfig)();
99
+ if (resolved.auditOnly) {
100
+ client.logger.info("config.audit_only", {
101
+ message: "Audit-only mode enabled. Auth and permission checks are disabled.",
102
+ });
103
+ return {};
104
+ }
105
+ if (!resolved.projectUrl) {
106
+ client.logger.warn("config.not_configured", {
107
+ message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
108
+ "Run 'npx ory-cline configure' to connect to an Ory project.",
109
+ });
110
+ return {};
111
+ }
112
+ const sessionToken = process.env.ORY_SESSION_TOKEN;
113
+ const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
114
+ if (sessionToken) {
115
+ await verifySessionToken(sessionToken, client);
116
+ return {};
117
+ }
118
+ if (oauth2Token) {
119
+ await verifyOAuth2Token(oauth2Token, client);
120
+ return {};
121
+ }
122
+ client.logger.warn("session.no_credentials", {
123
+ message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
124
+ "Skipping authentication.",
125
+ });
126
+ return {};
127
+ }
128
+ /**
129
+ * Write the user → agent delegation tuple once both principal subjects are
130
+ * populated. Idempotent (409s absorbed by the core). Failures are logged and
131
+ * swallowed — delegation tuples are audit-trail only.
132
+ */
133
+ async function writeDelegation(client) {
134
+ const userSubject = client.userPrincipal?.subject;
135
+ const agentSubject = client.agentPrincipal?.subject;
136
+ if (!userSubject || !agentSubject)
137
+ return;
138
+ const namespace = resolveNamespace();
139
+ try {
140
+ await client.createRelationship({
141
+ namespace,
142
+ object: `agent:${agentSubject}`,
143
+ relation: "delegate",
144
+ subjectId: `user:${userSubject}`,
145
+ });
146
+ }
147
+ catch (err) {
148
+ client.logger.warn("delegation.write_failed", {
149
+ message: err?.message ?? String(err),
150
+ });
151
+ }
152
+ }
153
+ async function verifySessionToken(token, client) {
154
+ try {
155
+ const session = await client.verifySession(token);
156
+ if (!session.active) {
157
+ client.logger.warn("session.inactive", {
158
+ message: "Ory session is not active. Re-authenticate to enable auth checks.",
159
+ });
160
+ }
161
+ }
162
+ catch (err) {
163
+ const oryErr = err;
164
+ client.logger.warn("session.verify_failed", {
165
+ code: oryErr.code,
166
+ message: oryErr.message,
167
+ });
168
+ }
169
+ }
170
+ async function verifyOAuth2Token(token, client) {
171
+ try {
172
+ const tokenInfo = await client.introspectToken(token);
173
+ if (!tokenInfo.active) {
174
+ client.logger.warn("oauth2.token_inactive", {
175
+ message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
176
+ });
177
+ return;
178
+ }
179
+ client.logger.info("oauth2.session_authenticated", {
180
+ clientId: tokenInfo.clientId,
181
+ subject: tokenInfo.subject,
182
+ scope: tokenInfo.scope,
183
+ });
184
+ }
185
+ catch (err) {
186
+ const oryErr = err;
187
+ client.logger.warn("oauth2.introspect_failed", {
188
+ code: oryErr.code,
189
+ message: oryErr.message,
190
+ });
191
+ }
192
+ }
193
+ // ─── tool_call (tool.before, blocking) ─────────────────────────────
194
+ async function handlePreToolUse(input, client) {
195
+ const toolName = input.preToolUse?.toolName ?? "unknown";
196
+ const toolInput = input.preToolUse?.parameters;
197
+ client.logger.info("lifecycle.pre_tool_use", {
198
+ taskId: input.taskId,
199
+ toolName,
200
+ toolInput,
201
+ });
202
+ const inputSummary = (0, argus_1.summarizeToolInput)(toolName, toolInput);
203
+ // In audit-only mode, log the invocation but skip permission checks.
204
+ if ((0, argus_1.resolveConfig)().auditOnly) {
205
+ client.tracer.record("tool.invoke", "ok", {
206
+ attributes: { toolName, ...inputSummary },
207
+ });
208
+ return {};
209
+ }
210
+ const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.taskId}`);
211
+ const subjectId = (0, argus_1.subjectLabel)(subject);
212
+ const namespace = resolveNamespace();
213
+ try {
214
+ const outcome = await (0, argus_1.gateToolCall)(client, {
215
+ harness: "cline",
216
+ toolName,
217
+ check: { namespace, object: toolName, relation: "use", ...subject },
218
+ spanAttributes: { toolName },
219
+ });
220
+ // Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
221
+ // user.interaction span is already recorded; pass through.
222
+ if (outcome.kind === "interactive") {
223
+ return {};
224
+ }
225
+ const decision = outcome;
226
+ if (decision.kind === "fail_open") {
227
+ return handlePermissionError(decision.error, toolName, client);
228
+ }
229
+ const attrs = { toolName, ...inputSummary };
230
+ const decisionAttrs = decision.spanAttributes;
231
+ if (decision.kind === "allow") {
232
+ client.tracer.record("tool.invoke", "ok", {
233
+ attributes: { ...attrs, ...decisionAttrs, allowed: true },
234
+ });
235
+ return {};
236
+ }
237
+ if (decision.kind === "observe") {
238
+ client.tracer.record("tool.block", "denied", {
239
+ attributes: {
240
+ ...attrs,
241
+ ...decisionAttrs,
242
+ allowed: false,
243
+ ...(0, argus_1.alertAttributes)(false),
244
+ },
245
+ });
246
+ client.tracer.record("tool.invoke", "ok", {
247
+ attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
248
+ });
249
+ return {};
250
+ }
251
+ // deny → cancel the tool.
252
+ client.tracer.record("tool.block", "denied", {
253
+ attributes: {
254
+ ...attrs,
255
+ ...decisionAttrs,
256
+ allowed: false,
257
+ ...(0, argus_1.alertAttributes)(true),
258
+ },
259
+ });
260
+ return {
261
+ cancel: true,
262
+ errorMessage: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
263
+ };
264
+ }
265
+ catch (err) {
266
+ return handlePermissionError(err, toolName, client);
267
+ }
268
+ }
269
+ // ─── tool_result (tool.after) ──────────────────────────────────────
270
+ async function handlePostToolUse(input, client) {
271
+ const toolName = input.postToolUse?.toolName ?? "unknown";
272
+ const toolInput = input.postToolUse?.parameters;
273
+ const toolResult = input.postToolUse?.result;
274
+ client.logger.info("lifecycle.post_tool_use", {
275
+ taskId: input.taskId,
276
+ toolName,
277
+ success: input.postToolUse?.success,
278
+ });
279
+ client.tracer.record("tool.complete", "ok", {
280
+ attributes: {
281
+ toolName,
282
+ success: input.postToolUse?.success,
283
+ executionTimeMs: input.postToolUse?.executionTimeMs,
284
+ ...(0, argus_1.summarizeToolInput)(toolName, toolInput),
285
+ ...(0, argus_1.summarizeToolOutput)(toolName, toolResult),
286
+ },
287
+ });
288
+ return {};
289
+ }
290
+ // ─── Trace-only lifecycle events ───────────────────────────────────
291
+ async function handleTaskResume(input, client) {
292
+ client.logger.info("lifecycle.task_resume", { taskId: input.taskId });
293
+ client.tracer.record("session.start", "ok", {
294
+ attributes: { resumed: true, clineVersion: input.clineVersion },
295
+ });
296
+ return {};
297
+ }
298
+ async function handleTaskCancel(input, client) {
299
+ client.logger.info("lifecycle.task_cancel", { taskId: input.taskId });
300
+ client.tracer.record("session.end", "ok", {
301
+ attributes: { reason: "abort" },
302
+ });
303
+ return {};
304
+ }
305
+ async function handleTaskComplete(input, client) {
306
+ client.logger.info("lifecycle.task_complete", { taskId: input.taskId });
307
+ client.tracer.record("session.end", "ok", {
308
+ attributes: { reason: "complete" },
309
+ });
310
+ return {};
311
+ }
312
+ async function handleTaskError(input, client) {
313
+ client.logger.info("lifecycle.task_error", { taskId: input.taskId });
314
+ client.tracer.record("session.end", "error", {
315
+ attributes: { reason: "error" },
316
+ });
317
+ return {};
318
+ }
319
+ async function handleSessionShutdown(input, client) {
320
+ client.logger.info("lifecycle.session_shutdown", { taskId: input.taskId });
321
+ client.tracer.record("session.end", "ok", {
322
+ attributes: { stage: "session_shutdown" },
323
+ });
324
+ return {};
325
+ }
326
+ async function handleUserPromptSubmit(input, client) {
327
+ client.logger.info("lifecycle.user_prompt_submit", { taskId: input.taskId });
328
+ // User-facing phase: the human is the decision-maker. Audit span only.
329
+ client.tracer.record("user.prompt", "ok", {
330
+ attributes: { stage: "user_prompt_submit" },
331
+ });
332
+ return {};
333
+ }
334
+ async function handlePreCompact(input, client) {
335
+ client.logger.info("lifecycle.pre_compact", { taskId: input.taskId });
336
+ client.tracer.record("compaction", "ok", {
337
+ attributes: { stage: "pre_compact" },
338
+ });
339
+ return {};
340
+ }
341
+ // ─── Helpers ────────────────────────────────────────────────────────
342
+ function resolveNamespace() {
343
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
344
+ }
345
+ function handlePermissionError(oryErr, toolName, client) {
346
+ if (oryErr.code === "network_error") {
347
+ client.logger.warn("permission.network_error", {
348
+ toolName,
349
+ message: "Ory unreachable, failing open",
350
+ });
351
+ return {};
352
+ }
353
+ if (oryErr.code === "rate_limited") {
354
+ client.logger.warn("permission.rate_limited", {
355
+ toolName,
356
+ message: "Ory rate limited, failing open",
357
+ });
358
+ return {};
359
+ }
360
+ // For other errors, also fail open but log prominently.
361
+ client.logger.error("permission.check.error", {
362
+ toolName,
363
+ code: oryErr.code,
364
+ message: oryErr.message,
365
+ });
366
+ return {};
367
+ }
package/dist/hook.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cline file-hook entry point.
4
+ *
5
+ * Cline invokes this script (named after the event, e.g. PreToolUse) for each
6
+ * hook event.
7
+ * Input: JSON on stdin
8
+ * Output: JSON on stdout
9
+ *
10
+ * Blocking model: a tool_call response of `{ "cancel": true, ... }` prevents
11
+ * the tool from running. The `cancel` flag is the signal — exit code 0 is
12
+ * correct in all cases. (Cline also treats exit code 130 as a cancellation,
13
+ * but we deliberately do NOT rely on exit codes here.)
14
+ *
15
+ * Verified against cline@3.0.29.
16
+ */
17
+ export {};
package/dist/hook.js ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Cline file-hook entry point.
5
+ *
6
+ * Cline invokes this script (named after the event, e.g. PreToolUse) for each
7
+ * hook event.
8
+ * Input: JSON on stdin
9
+ * Output: JSON on stdout
10
+ *
11
+ * Blocking model: a tool_call response of `{ "cancel": true, ... }` prevents
12
+ * the tool from running. The `cancel` flag is the signal — exit code 0 is
13
+ * correct in all cases. (Cline also treats exit code 130 as a cancellation,
14
+ * but we deliberately do NOT rely on exit codes here.)
15
+ *
16
+ * Verified against cline@3.0.29.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ const argus_1 = require("@ory/argus");
20
+ const handlers_js_1 = require("./handlers.js");
21
+ /**
22
+ * Read all of stdin as a string.
23
+ *
24
+ * Uses event listeners instead of `for await` so that we don't depend on the
25
+ * parent process closing stdin (sending EOF). If no EOF arrives, we resolve
26
+ * after a short idle gap once data has been received.
27
+ */
28
+ function readStdin() {
29
+ return new Promise((resolve, reject) => {
30
+ const chunks = [];
31
+ let resolved = false;
32
+ function done() {
33
+ if (!resolved) {
34
+ resolved = true;
35
+ resolve(Buffer.concat(chunks).toString("utf-8"));
36
+ }
37
+ }
38
+ process.stdin.on("data", (chunk) => {
39
+ chunks.push(chunk);
40
+ // Reset idle timer on every chunk. Once data stops arriving for 100 ms
41
+ // we assume the full payload has been delivered.
42
+ clearTimeout(idleTimer);
43
+ idleTimer = setTimeout(done, 100);
44
+ });
45
+ process.stdin.on("end", done);
46
+ process.stdin.on("error", (err) => {
47
+ if (!resolved) {
48
+ resolved = true;
49
+ reject(err);
50
+ }
51
+ });
52
+ let idleTimer;
53
+ });
54
+ }
55
+ async function main() {
56
+ const client = argus_1.OryAgentClient.fromEnv("cline");
57
+ const raw = await readStdin();
58
+ let input;
59
+ try {
60
+ input = JSON.parse(raw);
61
+ }
62
+ catch {
63
+ client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
64
+ await client.tracer.shutdown();
65
+ process.exit(0); // Don't block on parse errors
66
+ }
67
+ const output = await (0, handlers_js_1.handleHookEvent)(input, client);
68
+ // `cancel: true` is the block signal. Emit the decision and exit 0 — Cline
69
+ // reads the JSON, not the exit code.
70
+ if (output.cancel || output.contextModification || output.errorMessage) {
71
+ process.stdout.write(JSON.stringify(output));
72
+ }
73
+ await client.tracer.shutdown();
74
+ process.exit(0);
75
+ }
76
+ main().catch((err) => {
77
+ process.stderr.write(`[ory-agent] fatal: ${err}\n`);
78
+ process.exit(0); // Fail open
79
+ });
@@ -0,0 +1,2 @@
1
+ export { handleHookEvent, type HandleHookEventDeps } from "./handlers.js";
2
+ export type { ClineHookInput, ClineHookOutput } from "./types.js";
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; } });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Cline file-hook event payload delivered on stdin.
3
+ *
4
+ * Cline invokes hooks as executable scripts whose name matches the event in
5
+ * PascalCase (`TaskStart`, `PreToolUse`, …), placed in a hooks directory. Each
6
+ * script receives the event payload as JSON on stdin and may write a JSON
7
+ * decision to stdout. A tool-call decision of `{ "cancel": true }` prevents the
8
+ * tool. Hooks have a 30s timeout and `contextModification` is capped at 50k
9
+ * characters.
10
+ *
11
+ * The on-disk script filenames are PascalCase, but the `hookName` field
12
+ * carried *inside* the stdin payload is the snake_case `HookEventName` enum.
13
+ * Tool-specific fields are nested under per-event sub-objects, not flat.
14
+ *
15
+ * Verified against cline@3.0.29 (@cline/shared/dist/hooks/events.d.ts).
16
+ */
17
+ /** Snake_case hook event names as delivered inside the stdin payload. */
18
+ export type ClineHookEventName = "agent_start" | "agent_resume" | "agent_abort" | "agent_end" | "agent_error" | "tool_call" | "tool_result" | "prompt_submit" | "pre_compact" | "session_shutdown";
19
+ /** Nested payload for the `tool_call` event. */
20
+ export interface ClinePreToolUsePayload {
21
+ toolName: string;
22
+ parameters: Record<string, string>;
23
+ }
24
+ /** Nested payload for the `tool_result` event. */
25
+ export interface ClinePostToolUsePayload {
26
+ toolName: string;
27
+ parameters: Record<string, string>;
28
+ result?: string;
29
+ success?: boolean;
30
+ executionTimeMs?: number;
31
+ }
32
+ /** Nested payload for the `prompt_submit` event. */
33
+ export interface ClineUserPromptSubmitPayload {
34
+ prompt: string;
35
+ attachments?: unknown[];
36
+ }
37
+ /** Nested payload for the `pre_compact` event. */
38
+ export interface ClinePreCompactPayload {
39
+ [key: string]: unknown;
40
+ }
41
+ export interface ClineHookInput {
42
+ /** Snake_case event name carried in the payload (NOT the script filename). */
43
+ hookName: ClineHookEventName | string;
44
+ /** Stable identifier for the current Cline task/session. */
45
+ taskId: string;
46
+ /** Workspace root paths Cline is operating against. */
47
+ workspaceRoots?: string[];
48
+ /** Version string reported by the Cline binary. */
49
+ clineVersion?: string;
50
+ /** ISO-8601 timestamp of the event. */
51
+ timestamp?: string;
52
+ /** Identity of the user driving the session, when known. */
53
+ userId?: string;
54
+ /** Identity of the agent, when known. */
55
+ agent_id?: string;
56
+ /** Parent agent identity for sub-agent invocations, when known. */
57
+ parent_agent_id?: string;
58
+ /** Present on `tool_call`. */
59
+ preToolUse?: ClinePreToolUsePayload;
60
+ /** Present on `tool_result`. */
61
+ postToolUse?: ClinePostToolUsePayload;
62
+ /** Present on `prompt_submit`. */
63
+ userPromptSubmit?: ClineUserPromptSubmitPayload;
64
+ /** Present on `pre_compact`. */
65
+ preCompact?: ClinePreCompactPayload;
66
+ }
67
+ /**
68
+ * Cline file-hook output written to stdout.
69
+ *
70
+ * For `tool_call`, `cancel: true` prevents the tool from running.
71
+ * `errorMessage` is surfaced to the user; `contextModification` is injected
72
+ * into the model context (50k char cap). All fields are optional — emitting
73
+ * `{}` (or nothing) lets the action proceed.
74
+ *
75
+ * Verified against cline@3.0.29.
76
+ */
77
+ export interface ClineHookOutput {
78
+ cancel?: boolean;
79
+ errorMessage?: string;
80
+ contextModification?: string;
81
+ }
package/dist/types.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ /**
3
+ * Cline file-hook event payload delivered on stdin.
4
+ *
5
+ * Cline invokes hooks as executable scripts whose name matches the event in
6
+ * PascalCase (`TaskStart`, `PreToolUse`, …), placed in a hooks directory. Each
7
+ * script receives the event payload as JSON on stdin and may write a JSON
8
+ * decision to stdout. A tool-call decision of `{ "cancel": true }` prevents the
9
+ * tool. Hooks have a 30s timeout and `contextModification` is capped at 50k
10
+ * characters.
11
+ *
12
+ * The on-disk script filenames are PascalCase, but the `hookName` field
13
+ * carried *inside* the stdin payload is the snake_case `HookEventName` enum.
14
+ * Tool-specific fields are nested under per-event sub-objects, not flat.
15
+ *
16
+ * Verified against cline@3.0.29 (@cline/shared/dist/hooks/events.d.ts).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@ory/cline",
3
+ "version": "0.10.0",
4
+ "description": "Ory plugin for Cline: scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://ory.com",
7
+ "keywords": [
8
+ "ory",
9
+ "cline",
10
+ "cline-cli",
11
+ "hooks",
12
+ "plugin",
13
+ "extension",
14
+ "identity",
15
+ "identity-management",
16
+ "iam",
17
+ "authentication",
18
+ "authorization",
19
+ "access-control",
20
+ "permissions",
21
+ "rbac",
22
+ "zanzibar",
23
+ "oauth",
24
+ "oauth2",
25
+ "openid-connect",
26
+ "oidc",
27
+ "session",
28
+ "mfa",
29
+ "sso",
30
+ "audit",
31
+ "audit-log",
32
+ "compliance",
33
+ "agent",
34
+ "ai-agent",
35
+ "agent-security",
36
+ "guardrails",
37
+ "llm",
38
+ "tracing",
39
+ "distributed-tracing",
40
+ "observability",
41
+ "kratos",
42
+ "keto",
43
+ "hydra"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org/",
48
+ "provenance": true
49
+ },
50
+ "main": "dist/index.js",
51
+ "types": "dist/index.d.ts",
52
+ "exports": {
53
+ ".": {
54
+ "types": "./dist/index.d.ts",
55
+ "default": "./dist/index.js"
56
+ }
57
+ },
58
+ "bin": {
59
+ "ory-cline": "dist/cli/main.js",
60
+ "ory-cline-hook": "dist/hook.js",
61
+ "ory-cline-setup": "dist/cli/setup.js"
62
+ },
63
+ "files": [
64
+ "dist",
65
+ "!dist/dev",
66
+ "!dist/**/*.tsbuildinfo"
67
+ ],
68
+ "dependencies": {
69
+ "@ory/argus": "0.10.0"
70
+ },
71
+ "devDependencies": {
72
+ "typescript": "^6.0.2",
73
+ "vitest": "4.1.4"
74
+ },
75
+ "engines": {
76
+ "node": ">=22"
77
+ },
78
+ "scripts": {
79
+ "build": "tsc",
80
+ "clean": "rm -rf dist *.tsbuildinfo",
81
+ "test": "vitest run",
82
+ "test:watch": "vitest",
83
+ "typecheck": "tsc --noEmit",
84
+ "dev": "node dist/dev/launcher.js"
85
+ }
86
+ }