@ory/opencode 0.14.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  Security and developer experience for [OpenCode](https://opencode.ai), powered by [Ory](https://ory.com).
4
4
 
5
- **Security.** OpenCode runs real actions on your machine — editing files, running shell commands, calling APIs. The plugin gives every session a verifiable identity (you sign in once; OpenCode and any sub-agents it spawns each get their own), checks every tool call against permissions you control, and records each decision as an audit trace you can ship to your observability stack. It starts in watch mode so nothing is blocked on day one, and if Ory is ever unreachable it steps aside rather than locking you out.
5
+ OpenCode 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
6
 
7
- **Developer experience.** A single command installs the plugin and walks you through connecting — choose Ory Network, a local Docker stack, or audit-only, and it wires up the project, sign-in client, login, and permissions for you. It also helps you build Ory into your own app: ask in plain language to scaffold login, registration, and recovery pages, run a local Ory, or manage identities and permissions through the bundled MCP server.
7
+ One command installs two independent halves:
8
+
9
+ - **Developer experience** — the Ory skill catalog, `/ory:` slash commands, 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).
8
11
 
9
12
  ## What you'll need
10
13
 
@@ -15,134 +18,141 @@ Security and developer experience for [OpenCode](https://opencode.ai), powered b
15
18
 
16
19
  ## Get started
17
20
 
18
- Run one command. It installs the plugin and walks you through connecting:
21
+ Run one command:
19
22
 
20
23
  ```bash
21
24
  npx -y -p @ory/opencode ory-opencode install
22
25
  ```
23
26
 
24
- It opens a guided setup **in your browser** where you pick how to connect — the default takes just a click:
25
-
26
- - **Ory Network** *(default)* — sign in, or create a free account, in your browser. The project, keys, permissions, and login are all set up for you. Nothing to configure by hand.
27
- - **Local** — run a complete Ory on your laptop with Docker. No account, no signup, no keys. Great for trying it out.
28
- - **Audit-only** — skip Ory entirely and just log what OpenCode does.
29
-
30
- > No browser available (CI, SSH, headless)? The same walkthrough runs right in your terminal instead — or force it with `--no-web`.
31
-
32
- That's it. Confirm everything landed with:
27
+ Confirm everything landed with:
33
28
 
34
29
  ```bash
35
30
  npx -y -p @ory/opencode ory-opencode status
36
31
  ```
37
32
 
38
- `status` is your one-stop check: what's configured, who's signed in, which tools are covered by permissions, and recent activity. Anything not set up yet shows as `(unset)`.
33
+ `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.
39
34
 
40
- The installer registers the plugin in OpenCode's config (`opencode.json`) along with the Ory MCP server, a set of skills in `.opencode/skills/`, and the local-stack slash commands. Re-run install with `--reconfigure` to change your connection later, or `--no-configure` to skip the wizard. `ory-opencode uninstall` reverses it all.
35
+ The installer registers the plugin in OpenCode's config (`opencode.json`) along with the Ory MCP server, a set of skills in `.opencode/skills/`, and the local-stack slash commands. `ory-opencode uninstall` reverses it all.
41
36
 
42
37
  <details>
43
38
  <summary>Prefer to register the plugin by hand?</summary>
44
39
 
45
- OpenCode loads the plugin in-process from `opencode.json`. If you only want the plugin — no MCP server, skills, or slash commands, and no guided setup — add it directly:
40
+ OpenCode loads the plugin in-process from `opencode.json`. If you only want the plugin — no MCP server, skills, or slash commands — add it directly:
46
41
 
47
42
  ```json
48
43
  {
49
44
  "$schema": "https://opencode.ai/config.json",
50
- "plugin": ["@ory/opencode"]
45
+ "plugin": ["@ory/opencode@0.14.0"]
51
46
  }
52
47
  ```
53
48
 
54
- OpenCode fetches it from npm on next launch. Connect afterwards by running `ory-opencode install` (or `configure`) in a terminal — that's also what lands the skills and `/ory:` commands on disk.
49
+ OpenCode fetches it from npm on next launch. Run `ory-opencode install` in a terminal to also land the skills and `/ory:` commands on disk.
50
+
51
+ **Pin the version.** OpenCode installs each plugin spec into its own cache directory once and never re-resolves it, so a bare `"@ory/opencode"` (which means `@latest`) keeps loading whatever version was cached first — upgrades silently never take effect. `ory-opencode install` writes the pinned form for you, and checks that OpenCode will be able to install it.
55
52
 
56
53
  </details>
57
54
 
55
+ ## Skills and commands
56
+
57
+ Installing the plugin drops the full Ory playbook catalog into OpenCode. **Skills** are model-invoked — just say what you want in plain language and the matching one takes over.
58
+
59
+ | Skill | What it does for you |
60
+ |---|---|
61
+ | `ory-auth-setup` | Adds a complete auth system to your app — login, registration, recovery, verification, settings — on [Ory Elements](https://github.com/ory/elements) |
62
+ | `ory-login-flow` | Builds just the pages, wired to Ory's self-service flows |
63
+ | `ory-social-login` | "Sign in with…" for Google, GitHub, Apple, Microsoft, Discord, Slack, GitLab, Facebook |
64
+ | `ory-local-dev` | Develops and tests login/permission flows against a local Ory — no project, no account, offline |
65
+ | `ory-permissions-onboarding` | Walks a fresh install from observe mode to enforced per-tool permissions without getting blocked |
66
+ | `ory-build-agent` | Drops `@ory/argus` into an agent *you* own — Claude Agent SDK, OpenAI Agents, Mastra, Vercel AI, LangGraph/PydanticAI |
67
+ | `ory-build-integration` | Wires Ory into your app: Action webhooks, JWT validation at a gateway, live event streams |
68
+ | `ory-contribute-integration` | Authors and submits an integration to the public `ory/integrates` registry |
69
+ | `ory-e2b-sandbox` | Scaffolds an E2B sandbox template that boots with this plugin preinstalled |
70
+ | `ory-temporal-worker` | Scaffolds a Temporal TypeScript worker where every Activity is authenticated, authorized, and audited |
71
+
72
+ **Slash commands** run the local stack directly:
73
+
74
+ | Command | What it does |
75
+ |---|---|
76
+ | `/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 |
77
+ | `/ory:local-down` | Stops it, keeping your data volumes |
78
+ | `/ory:temporal-up` | Starts a local Temporal dev server for the `ory-temporal-worker` scaffold |
79
+
80
+ 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`.
81
+
82
+ A built-in **Ory MCP server** rounds it out — OpenCode can manage identities, projects, and permissions straight from chat.
83
+
84
+ So: ask OpenCode *"add Ory login to this app"* and it scaffolds the pages, starts a local Ory, and wires them together.
85
+
58
86
  ## What you get
59
87
 
60
- Once connected, every tool OpenCode runs is governed by Ory — three things happen automatically:
88
+ Out of the box, every tool OpenCode runs produces a privacy-safe structured activity event in the unified local log.
89
+
90
+ Once you connect to Ory Agent Security, two more things happen automatically:
61
91
 
62
92
  - **Who's driving.** You sign in once in your browser; OpenCode gets its own identity too. No tokens to copy around, and the "who acted on whose behalf" trail stays queryable later — even after tokens expire. (One nuance: OpenCode can't hard-block at session start, so browser sign-in there is advisory — it still runs and records correctly, the session just always proceeds. Per-tool checks are unaffected.)
63
- - **What it's allowed to do.** When OpenCode asks to use a tool, Ory checks whether it's permitted. It starts in **watch mode** — nothing is blocked, you just *see* what would be — so it never gets in your way on day one.
64
- - **A record of everything.** Every decision (allowed, denied, skipped) is logged as a trace you can send to Jaeger, Honeycomb, Grafana, or just a file.
93
+ - **What it's allowed to do.** When OpenCode asks to use a tool, 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.
65
94
 
66
95
  If Ory is ever unreachable, the plugin gets out of the way and lets OpenCode keep working — so it can't lock you out.
67
96
 
68
97
  ### See what's happening
69
98
 
70
- Everything the plugin does is observable out of the box — no configuration required:
99
+ Everything the plugin does is observable out of the box:
71
100
 
72
- - **Status at a glance.** `npx -y -p @ory/opencode ory-opencode status` shows what's configured, who's signed in, how many built-in tools your permissions cover, and the most recent tool-call activity.
73
- - **Live dashboard.** `npx -y -p @ory/opencode ory-opencode dashboard` opens the same picture in your browser — configuration, identities, permission coverage, Ory service health, and the latest tool-call activity, all refreshing live. From there you can flip **enforcement** on or off, toggle **user login**, and use **Change stack** to reconnect to a different Ory — no CLI required.
74
- - **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
101
+ - **Activity log.** Privacy-safe activity is always appended to `~/.config/ory-agent-plugins/opencode/ory-agent-debug.log`. View events, decisions, and errors live with:
75
102
 
76
103
  ```bash
77
104
  npx -y -p @ory/opencode ory-opencode watch
78
105
  ```
79
106
 
80
- Spans are also written to `~/.config/ory-agent-plugins/opencode/ory-agent-trace.ndjson` (NDJSON, one span per line) — tail that file, or point `OTEL_EXPORTER_OTLP_ENDPOINT` at a collector to ship them straight to Jaeger, Honeycomb, or Grafana.
81
- - **Debug log.** For a verbose play-by-play, set `ORY_AGENT_DEBUG=true`; structured logs land in `~/.config/ory-agent-plugins/opencode/ory-agent-debug.log`.
107
+ Set `ORY_AGENT_LOG_FILE` to override the path; set it empty to disable file persistence.
108
+ - **Live debug.** Launch OpenCode 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.
82
109
 
83
110
  ### Ready to enforce?
84
111
 
85
- When the watch-mode logs look right, turn on blocking with one command (setup already granted you the built-in tools):
112
+ 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.
86
113
 
87
114
  ```bash
88
- npx -y -p @ory/opencode ory-opencode permissions enforce
115
+ npx -y -p @ory/opencode ory-opencode permissions # what the project grants, and the live mode
89
116
  ```
90
117
 
91
- Now a denied tool is actually blocked and OpenCode shows why. In watch mode, OpenCode's own permission prompt is left in place for you to answer; only an explicit allow or a block-mode deny changes that. Go back to watch mode anytime with `permissions observe`. Prefer clicking? The dashboard has the same **Enforce** switch — flip it on, or back to watch mode, without touching the CLI. Use `permissions status` to see what's covered and `permissions bootstrap` to (re-)grant the built-in tools — or just ask OpenCode in chat, e.g. *"grant me use of the bash tool."*
118
+ Then a denied tool is actually blocked and OpenCode shows why. In observe mode, OpenCode's own permission prompt is left in place for you to answer; only an explicit allow or an enforce-mode deny changes that.
92
119
 
93
- ## Also: add login to your own app
120
+ ## Connect to Ory Agent Security
94
121
 
95
- Beyond securing OpenCode, the plugin helps you build Ory into whatever you're working on. Ask OpenCode *"add Ory login to this app"* and it scaffolds the login, registration, recovery, verification, and settings pages (using [Ory Elements](https://github.com/ory/elements)) wired to a local Ory — no signup or keys needed. Start that local Ory with `/ory:local-up` (it prints a test email + password to sign in with) and tear it down with `/ory:local-down`.
122
+ Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
96
123
 
97
- Bundled **skills** (just ask in plain language) cover more: `ory-auth-setup`, `ory-login-flow`, `ory-social-login` (Google, GitHub, Apple…), `ory-permissions-onboarding`, and playbooks for wiring Ory into your own agents, E2B sandboxes, or Temporal workers. A built-in **Ory MCP server** lets OpenCode manage identities, projects, and permissions straight from chat.
98
-
99
- ## Configure by hand (CI / advanced)
100
-
101
- The guided setup covers most people. For scripted or CI setups, or to point at an existing Ory Network project, configure directly. 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.
124
+ | Value | Flag | Environment variable |
125
+ |---|---|---|
126
+ | Project URL | `--project-url` | `ORY_PROJECT_URL` |
127
+ | Agent Security URL | `--agent-security-url` | `ORY_AGENT_SECURITY_URL` |
128
+ | Sign-in client id override (default `ory-agent-security-login`) | `--oauth2-client-id` | `ORY_OAUTH2_CLIENT_ID` |
102
129
 
103
130
  ```bash
104
131
  npx -y -p @ory/opencode ory-opencode configure \
105
132
  --project-url https://<slug>.projects.oryapis.com \
106
- --oauth2-client-id <sign-in client id>
133
+ --agent-security-url https://agents.console.ory.com
107
134
  ```
108
135
 
109
- OpenCode's own identity registers itself automatically on first run — nothing to create. The `--oauth2-client-id` is the one piece browser sign-in needs; the guided setup makes it for you, or see below to do it by hand. For logging-only with no checks, use `--audit-only`. The interactive user login runs every session (always on, non-blocking) and only needs `ORY_OAUTH2_CLIENT_ID` to complete the browser flow. The equivalent env vars are `ORY_PROJECT_URL`, `ORY_OAUTH2_CLIENT_ID`, and `ORY_AGENT_API_KEY`.
136
+ `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`.
110
137
 
111
- <details>
112
- <summary>Create the sign-in client by hand</summary>
138
+ **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.
113
139
 
114
- The guided setup normally does this. To do it yourself, create a **public** OAuth2 client (no secret) listing all four loopback URLs — the plugin tries each in turn so sign-in survives a busy port:
140
+ **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 OpenCode session registers its own identity automatically on first use.
115
141
 
116
- ```bash
117
- ory create oauth2-client --project <project-id> \
118
- --name "Ory Agent Security · user login (PKCE)" \
119
- --grant-type authorization_code,refresh_token \
120
- --response-type code \
121
- --scope openid,offline_access \
122
- --token-endpoint-auth-method none \
123
- --redirect-uri http://127.0.0.1:47823/callback \
124
- --redirect-uri http://127.0.0.1:47824/callback \
125
- --redirect-uri http://127.0.0.1:47825/callback \
126
- --redirect-uri http://127.0.0.1:47826/callback
127
- ```
128
-
129
- …or in the [Ory Console](https://console.ory.sh) under *OAuth2* → *Clients* → *Create client* (pick "Public client", "Authorization Code" + "Refresh Token" grants, scopes `openid offline_access`, and paste the four redirect URIs). Pass the resulting id to `configure --oauth2-client-id`. Running headless with a session token already? Set `ORY_USER_SESSION_TOKEN` and skip the browser step entirely.
130
-
131
- </details>
142
+ 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.
132
143
 
133
- With nothing configured, the plugin still loads and runs in **pass-through mode**: skills, commands, and logging work, but no checks run and nothing is blocked. Perfectly fine if you only want the app-building features.
144
+ 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.
134
145
 
135
146
  ## Commands
136
147
 
137
148
  ```
138
- ory-opencode install | uninstall Install/remove; --reconfigure re-runs setup, --no-web forces the terminal wizard, --no-configure skips it
149
+ ory-opencode install | uninstall Install (add --project-url to also connect Agent Security) / remove
139
150
  ory-opencode status Show configuration, identities, permission coverage, recent activity
140
- ory-opencode dashboard Open the live browser dashboard (status + service health + Change stack)
141
- ory-opencode watch Tail the live trace stream (OTel spans)
142
- ory-opencode permissions <cmd> status | bootstrap | observe (watch) | enforce (block)
143
- ory-opencode configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --audit-only)
151
+ ory-opencode permissions <cmd> status (read-only; grants + posture live in the Ory Console)
152
+ ory-opencode configure <flags> Connect a project (--project-url) or --disconnect
144
153
  ory-opencode agent <status|unregister> Manage OpenCode's own auto-created identity
145
154
  ory-opencode local <up|down|status|…> Run / manage a local Ory in Docker
155
+ ory-opencode version Print plugin, core, and Node versions (--json for machine-readable)
146
156
  ```
147
157
 
148
158
  All prefixed with `npx -y -p @ory/opencode`.
@@ -150,10 +160,11 @@ All prefixed with `npx -y -p @ory/opencode`.
150
160
  ## Troubleshooting
151
161
 
152
162
  - **`/ory:local-up` fails** — make sure Docker is running and ports `4000`, `4100`, `4455`, and `16686` are free.
153
- - **Browser sign-in loops** — reset with `ory-opencode agent unregister` and try again.
154
- - **Install ran but never asked how to connect** — you're almost certainly on a stale `npx` cache. `npx -p @ory/opencode` (no version pin) reuses a previously-downloaded copy instead of re-resolving to the latest, so an older CLI whose install predates the setup wizard can run while you believe you're on the current release. The install banner prints the running version; confirm it with `npx -y -p @ory/opencode ory-opencode version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/opencode ory-opencode install`. Pinning an exact version (`@ory/opencode@<version>`) also bypasses the cached copy.
163
+ - **Browser sign-in loops** (after connecting) — reset with `ory-opencode agent unregister` and try again.
164
+ - **Running an older CLI than expected** — `npx -p @ory/opencode` (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/opencode ory-opencode version`. To force the current release, clear the cache and reinstall: `rm -rf ~/.npm/_npx` then `npx -y -p @ory/opencode ory-opencode install`. Pinning an exact version (`@ory/opencode@<version>`) also bypasses the cached copy.
165
+ - **Hooks don't seem to run, but skills and `/ory:` commands are there** — OpenCode isn't loading the plugin. It fetches plugins itself, with npm, into its own cache (`~/.cache/opencode/packages/<spec>/`), and neither outcome is visible in OpenCode: an install that succeeded is never re-resolved (so an older cached copy keeps loading), and one that failed is retried and fails the same way every session. Run `npx -y -p @ory/opencode ory-opencode status` — it reports the copy OpenCode will actually load, says when an install was attempted and failed, explains why the spec can't be resolved, and flags stale cache directories.
166
+ - **The plugin can't be installed from your registry** — OpenCode's plugin install uses the npm configuration its own process inherits, so it goes to the public registry unless `npm_config_registry` says otherwise, and any `min-release-age` in your `.npmrc` applies to it as well. A build that isn't on the registry OpenCode reaches (a private or local publish) or a version newer than your freshness gate can't be installed, and `install`/`status` will tell you so. Either launch OpenCode against the right registry — `npm_config_registry=<registry> npm_config_min_release_age=0 opencode` — or point it at a local copy, which skips the registry entirely: `ory-opencode install --plugin-spec file:///path/to/node_modules/@ory/opencode`.
155
167
  - **`npm error code ENOVERSIONS` on install** — your npm has a freshness filter hiding brand-new versions. Wait it out, or run `npx -y --min-release-age=0 -p @ory/opencode ory-opencode install`.
156
- - **Want to see what's happening** — `npx -y -p @ory/opencode ory-opencode status` for a snapshot, `npx -y -p @ory/opencode ory-opencode watch` for the live trace stream, or set `ORY_AGENT_DEBUG=true` for a verbose log. Traces and logs live under `~/.config/ory-agent-plugins/opencode/` (see [See what's happening](#see-whats-happening)).
157
168
 
158
169
  ## Learn more
159
170
 
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared `opencode.json` mutation helpers for the Ory OpenCode plugin.
3
+ *
4
+ * Both CLI entry points (`ory-opencode install` and the `ory-opencode-setup`
5
+ * fallback binary) write the same two config keys, so the spec construction
6
+ * and the entry matching live here rather than being transcribed twice.
7
+ *
8
+ * ## Why the plugin spec is always a path
9
+ *
10
+ * OpenCode never loads an npm plugin from the project's `node_modules`. For an
11
+ * npm spec it installs the package into its own cache
12
+ * (`<xdgCache>/opencode/packages/<spec>/node_modules/<pkg>`) and short-circuits
13
+ * whenever that directory already exists — which produced two failure modes
14
+ * that were invisible to the user: a successful install was never re-resolved
15
+ * (so upgrades silently never took effect), and a failed install left an empty
16
+ * spec directory that was retried identically every session (so hooks never
17
+ * fired while skills and `/ory:` commands looked perfectly installed).
18
+ *
19
+ * Both are gone by construction. The plugin's runtime is resolved once, at
20
+ * install time, into the shared runtime store, and the spec written here is a
21
+ * `file://` URL pointing at it — so OpenCode imports the resolved copy
22
+ * directly, bypassing its cache and any registry. `resolveInstalledPlugin`
23
+ * remains, purely so `status` can report what will load and flag cache
24
+ * directories left over from earlier npm-spec installs.
25
+ */
26
+ export declare const PLUGIN_MODULE = "@ory/opencode";
27
+ export declare const MCP_SERVER_NAME = "ory";
28
+ export declare const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
29
+ /** A `plugin` array entry: a spec, or a `[spec, options]` pair. */
30
+ export type PluginEntry = string | [string, unknown];
31
+ export declare function entrySpec(entry: PluginEntry): string;
32
+ /**
33
+ * Reduce a plugin spec to the package it points at, so config entries are
34
+ * matched by identity instead of by exact text. Handles the bare name
35
+ * (`@ory/opencode`), a pinned version (`@ory/opencode@0.14.0`), and the
36
+ * `file://…/node_modules/@ory/opencode` form a source install can write — so an
37
+ * install/uninstall replaces a previously written entry in any of those shapes
38
+ * rather than leaving a duplicate behind.
39
+ */
40
+ export declare function pluginSpecName(spec: string): string;
41
+ export declare function isOryPluginEntry(entry: PluginEntry): boolean;
42
+ /**
43
+ * The spec to write into `opencode.json`: a `file://` URL for the directory the
44
+ * runtime resolved to.
45
+ *
46
+ * A path spec is what makes OpenCode import the copy we resolved rather than
47
+ * running its own npm install into a cache it then never re-checks.
48
+ */
49
+ export declare function runtimePluginSpec(packageDir: string): string;
50
+ export declare function mergePlugin(config: Record<string, unknown>, spec: string): Record<string, unknown>;
51
+ export declare function removePlugin(config: Record<string, unknown>): Record<string, unknown>;
52
+ export declare function mergeMcp(config: Record<string, unknown>, command: readonly string[]): Record<string, unknown>;
53
+ export declare function removeMcp(config: Record<string, unknown>): Record<string, unknown>;
54
+ export declare function findOryPluginEntry(config: Record<string, unknown>): PluginEntry | undefined;
55
+ /**
56
+ * OpenCode's package cache root — `<xdgCache>/opencode`, matching the
57
+ * `xdg-basedir` resolution it uses internally.
58
+ */
59
+ export declare function opencodeCacheRoot(): string;
60
+ export declare function isPathSpec(spec: string): boolean;
61
+ export interface ResolvedPlugin {
62
+ spec: string;
63
+ source: "file" | "npm";
64
+ /** Directory OpenCode will import the plugin from, if it exists yet. */
65
+ dir?: string;
66
+ /** Version found at `dir`. */
67
+ version?: string;
68
+ /**
69
+ * OpenCode has already created this spec's cache directory. With no `version`
70
+ * alongside it, that is positive evidence of an install that ran and *failed*
71
+ * — as opposed to one that has not been attempted yet.
72
+ */
73
+ attempted: boolean;
74
+ /** The spec's cache directory (npm specs only), whether or not it exists. */
75
+ cacheDir?: string;
76
+ /**
77
+ * Other spec directories OpenCode has already cached for this package. A
78
+ * leftover `@ory/opencode@latest` here is the classic stale-plugin symptom.
79
+ */
80
+ otherCached: Array<{
81
+ spec: string;
82
+ version?: string;
83
+ }>;
84
+ }
85
+ /**
86
+ * Work out which copy of the plugin OpenCode will load for a configured spec,
87
+ * plus any other copies it has cached. Purely observational — used by `status`
88
+ * so a stale cache is visible instead of being reported as a healthy install.
89
+ */
90
+ export declare function resolveInstalledPlugin(spec: string): ResolvedPlugin;
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ /**
3
+ * Shared `opencode.json` mutation helpers for the Ory OpenCode plugin.
4
+ *
5
+ * Both CLI entry points (`ory-opencode install` and the `ory-opencode-setup`
6
+ * fallback binary) write the same two config keys, so the spec construction
7
+ * and the entry matching live here rather than being transcribed twice.
8
+ *
9
+ * ## Why the plugin spec is always a path
10
+ *
11
+ * OpenCode never loads an npm plugin from the project's `node_modules`. For an
12
+ * npm spec it installs the package into its own cache
13
+ * (`<xdgCache>/opencode/packages/<spec>/node_modules/<pkg>`) and short-circuits
14
+ * whenever that directory already exists — which produced two failure modes
15
+ * that were invisible to the user: a successful install was never re-resolved
16
+ * (so upgrades silently never took effect), and a failed install left an empty
17
+ * spec directory that was retried identically every session (so hooks never
18
+ * fired while skills and `/ory:` commands looked perfectly installed).
19
+ *
20
+ * Both are gone by construction. The plugin's runtime is resolved once, at
21
+ * install time, into the shared runtime store, and the spec written here is a
22
+ * `file://` URL pointing at it — so OpenCode imports the resolved copy
23
+ * directly, bypassing its cache and any registry. `resolveInstalledPlugin`
24
+ * remains, purely so `status` can report what will load and flag cache
25
+ * directories left over from earlier npm-spec installs.
26
+ */
27
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ var desc = Object.getOwnPropertyDescriptor(m, k);
30
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
31
+ desc = { enumerable: true, get: function() { return m[k]; } };
32
+ }
33
+ Object.defineProperty(o, k2, desc);
34
+ }) : (function(o, m, k, k2) {
35
+ if (k2 === undefined) k2 = k;
36
+ o[k2] = m[k];
37
+ }));
38
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
39
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
40
+ }) : function(o, v) {
41
+ o["default"] = v;
42
+ });
43
+ var __importStar = (this && this.__importStar) || (function () {
44
+ var ownKeys = function(o) {
45
+ ownKeys = Object.getOwnPropertyNames || function (o) {
46
+ var ar = [];
47
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
48
+ return ar;
49
+ };
50
+ return ownKeys(o);
51
+ };
52
+ return function (mod) {
53
+ if (mod && mod.__esModule) return mod;
54
+ var result = {};
55
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
56
+ __setModuleDefault(result, mod);
57
+ return result;
58
+ };
59
+ })();
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.OPENCODE_SCHEMA_URL = exports.MCP_SERVER_NAME = exports.PLUGIN_MODULE = void 0;
62
+ exports.entrySpec = entrySpec;
63
+ exports.pluginSpecName = pluginSpecName;
64
+ exports.isOryPluginEntry = isOryPluginEntry;
65
+ exports.runtimePluginSpec = runtimePluginSpec;
66
+ exports.mergePlugin = mergePlugin;
67
+ exports.removePlugin = removePlugin;
68
+ exports.mergeMcp = mergeMcp;
69
+ exports.removeMcp = removeMcp;
70
+ exports.findOryPluginEntry = findOryPluginEntry;
71
+ exports.opencodeCacheRoot = opencodeCacheRoot;
72
+ exports.isPathSpec = isPathSpec;
73
+ exports.resolveInstalledPlugin = resolveInstalledPlugin;
74
+ const fs = __importStar(require("node:fs"));
75
+ const os = __importStar(require("node:os"));
76
+ const path = __importStar(require("node:path"));
77
+ const node_url_1 = require("node:url");
78
+ exports.PLUGIN_MODULE = "@ory/opencode";
79
+ exports.MCP_SERVER_NAME = "ory";
80
+ exports.OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
81
+ function entrySpec(entry) {
82
+ return typeof entry === "string" ? entry : entry[0];
83
+ }
84
+ /**
85
+ * Reduce a plugin spec to the package it points at, so config entries are
86
+ * matched by identity instead of by exact text. Handles the bare name
87
+ * (`@ory/opencode`), a pinned version (`@ory/opencode@0.14.0`), and the
88
+ * `file://…/node_modules/@ory/opencode` form a source install can write — so an
89
+ * install/uninstall replaces a previously written entry in any of those shapes
90
+ * rather than leaving a duplicate behind.
91
+ */
92
+ function pluginSpecName(spec) {
93
+ let raw = spec.trim();
94
+ if (raw.startsWith("file://")) {
95
+ try {
96
+ raw = (0, node_url_1.fileURLToPath)(raw);
97
+ }
98
+ catch {
99
+ raw = raw.slice("file://".length);
100
+ }
101
+ }
102
+ raw = raw.replace(/[/\\]+$/, "");
103
+ // Strip a trailing `@<version>`, but never the `@` that starts an npm scope
104
+ // (position 0) or a scope `@` in the middle of a path — a version suffix is
105
+ // the only one with no path separator after it.
106
+ const at = raw.lastIndexOf("@");
107
+ if (at > 0 && !/[/\\]/.test(raw.slice(at + 1)))
108
+ raw = raw.slice(0, at);
109
+ const parts = raw.split(/[/\\]/).filter(Boolean);
110
+ if (parts.length >= 2) {
111
+ return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
112
+ }
113
+ return raw;
114
+ }
115
+ function isOryPluginEntry(entry) {
116
+ return pluginSpecName(entrySpec(entry)) === exports.PLUGIN_MODULE;
117
+ }
118
+ function readVersion(packageRoot) {
119
+ try {
120
+ const json = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
121
+ return typeof json.version === "string" ? json.version : undefined;
122
+ }
123
+ catch {
124
+ return undefined;
125
+ }
126
+ }
127
+ /**
128
+ * The spec to write into `opencode.json`: a `file://` URL for the directory the
129
+ * runtime resolved to.
130
+ *
131
+ * A path spec is what makes OpenCode import the copy we resolved rather than
132
+ * running its own npm install into a cache it then never re-checks.
133
+ */
134
+ function runtimePluginSpec(packageDir) {
135
+ return (0, node_url_1.pathToFileURL)(packageDir).href;
136
+ }
137
+ function mergePlugin(config, spec) {
138
+ const merged = { ...config };
139
+ const plugin = (merged.plugin ?? []).filter((p) => !isOryPluginEntry(p));
140
+ plugin.push(spec);
141
+ merged.plugin = plugin;
142
+ return merged;
143
+ }
144
+ function removePlugin(config) {
145
+ const merged = { ...config };
146
+ const plugin = (merged.plugin ?? []).filter((p) => !isOryPluginEntry(p));
147
+ if (plugin.length === 0) {
148
+ delete merged.plugin;
149
+ }
150
+ else {
151
+ merged.plugin = plugin;
152
+ }
153
+ return merged;
154
+ }
155
+ // OpenCode's config schema uses `mcp` (singular) with a `local` connector
156
+ // shape `{ type: "local", command: [...] }`, not the generic `mcpServers`
157
+ // helper from core. `command` is the resolved runtime's MCP shim, so the server
158
+ // starts from the materialized copy instead of being re-resolved by npx.
159
+ function mergeMcp(config, command) {
160
+ const merged = { ...config };
161
+ const mcp = { ...(merged.mcp ?? {}) };
162
+ mcp[exports.MCP_SERVER_NAME] = { type: "local", command: [...command] };
163
+ merged.mcp = mcp;
164
+ return merged;
165
+ }
166
+ function removeMcp(config) {
167
+ const merged = { ...config };
168
+ const mcp = { ...(merged.mcp ?? {}) };
169
+ delete mcp[exports.MCP_SERVER_NAME];
170
+ if (Object.keys(mcp).length === 0) {
171
+ delete merged.mcp;
172
+ }
173
+ else {
174
+ merged.mcp = mcp;
175
+ }
176
+ return merged;
177
+ }
178
+ function findOryPluginEntry(config) {
179
+ return (config.plugin ?? []).find(isOryPluginEntry);
180
+ }
181
+ // ─── resolving what OpenCode will actually load ─────────────────────
182
+ /**
183
+ * OpenCode's package cache root — `<xdgCache>/opencode`, matching the
184
+ * `xdg-basedir` resolution it uses internally.
185
+ */
186
+ function opencodeCacheRoot() {
187
+ const xdg = process.env.XDG_CACHE_HOME?.trim();
188
+ if (xdg)
189
+ return path.join(xdg, "opencode");
190
+ if (process.platform === "win32") {
191
+ const local = process.env.LOCALAPPDATA?.trim();
192
+ if (local)
193
+ return path.join(local, "opencode");
194
+ }
195
+ return path.join(os.homedir(), ".cache", "opencode");
196
+ }
197
+ function isPathSpec(spec) {
198
+ return (spec.startsWith("file://") ||
199
+ spec.startsWith(".") ||
200
+ path.isAbsolute(spec) ||
201
+ /^[A-Za-z]:[\\/]/.test(spec));
202
+ }
203
+ function specToDir(spec) {
204
+ if (!spec.startsWith("file://"))
205
+ return spec;
206
+ try {
207
+ return (0, node_url_1.fileURLToPath)(spec);
208
+ }
209
+ catch {
210
+ return spec.slice("file://".length);
211
+ }
212
+ }
213
+ function versionAt(dir) {
214
+ return readVersion(dir);
215
+ }
216
+ /**
217
+ * Work out which copy of the plugin OpenCode will load for a configured spec,
218
+ * plus any other copies it has cached. Purely observational — used by `status`
219
+ * so a stale cache is visible instead of being reported as a healthy install.
220
+ */
221
+ function resolveInstalledPlugin(spec) {
222
+ const packagesDir = path.join(opencodeCacheRoot(), "packages");
223
+ const scopeDir = path.join(packagesDir, "@ory");
224
+ const otherCached = [];
225
+ let entries = [];
226
+ try {
227
+ entries = fs.readdirSync(scopeDir);
228
+ }
229
+ catch {
230
+ /* no cache yet */
231
+ }
232
+ for (const name of entries) {
233
+ if (name !== "opencode" && !name.startsWith("opencode@"))
234
+ continue;
235
+ const cachedSpec = `@ory/${name}`;
236
+ if (cachedSpec === spec)
237
+ continue;
238
+ otherCached.push({
239
+ spec: cachedSpec,
240
+ version: versionAt(path.join(packagesDir, cachedSpec, "node_modules", exports.PLUGIN_MODULE)),
241
+ });
242
+ }
243
+ if (isPathSpec(spec)) {
244
+ const dir = specToDir(spec);
245
+ const version = versionAt(dir);
246
+ return {
247
+ spec,
248
+ source: "file",
249
+ dir: version ? dir : undefined,
250
+ version,
251
+ attempted: fs.existsSync(dir),
252
+ otherCached,
253
+ };
254
+ }
255
+ const specDir = path.join(packagesDir, spec);
256
+ const dir = path.join(specDir, "node_modules", exports.PLUGIN_MODULE);
257
+ const version = versionAt(dir);
258
+ return {
259
+ spec,
260
+ source: "npm",
261
+ dir: version ? dir : undefined,
262
+ version,
263
+ attempted: fs.existsSync(specDir),
264
+ cacheDir: specDir,
265
+ otherCached,
266
+ };
267
+ }