@ory/opencode 0.14.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -66
- package/dist/cli/config.d.ts +90 -0
- package/dist/cli/config.js +267 -0
- package/dist/cli/main.js +94 -63
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.js +39 -56
- package/dist/plugin.d.ts +1 -16
- package/dist/plugin.js +90 -345
- package/package.json +9 -8
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
|
-
|
|
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
|
-
|
|
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
|
|
21
|
+
Run one command:
|
|
19
22
|
|
|
20
23
|
```bash
|
|
21
24
|
npx -y -p @ory/opencode ory-opencode install
|
|
22
25
|
```
|
|
23
26
|
|
|
24
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 **
|
|
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
|
|
99
|
+
Everything the plugin does is observable out of the box:
|
|
71
100
|
|
|
72
|
-
- **
|
|
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
|
-
|
|
81
|
-
- **
|
|
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
|
-
|
|
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
|
|
115
|
+
npx -y -p @ory/opencode ory-opencode permissions # what the project grants, and the live mode
|
|
89
116
|
```
|
|
90
117
|
|
|
91
|
-
|
|
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
|
-
##
|
|
120
|
+
## Connect to Ory Agent Security
|
|
94
121
|
|
|
95
|
-
|
|
122
|
+
Copy the connection details from the [Ory Console](https://console.ory.sh) under **Agent Security**:
|
|
96
123
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
--
|
|
133
|
+
--agent-security-url https://agents.console.ory.com
|
|
107
134
|
```
|
|
108
135
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
141
|
-
ory-opencode
|
|
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
|
-
- **
|
|
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
|
+
}
|