@ex-machina/opencode-anthropic-auth 1.8.4 → 2.0.0-next.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/dist/auth.d.ts CHANGED
@@ -14,3 +14,19 @@ export type ExchangeResult = {
14
14
  type: 'failed';
15
15
  };
16
16
  export declare function exchange(input: string, verifier: string, redirectUri: string, expectedState?: string): Promise<ExchangeResult>;
17
+ export type RefreshResult = {
18
+ type: 'success';
19
+ refresh: string;
20
+ access: string;
21
+ expires: number;
22
+ } | {
23
+ type: 'failed';
24
+ status: number;
25
+ };
26
+ /**
27
+ * Exchange a refresh token for a new access/refresh token pair.
28
+ * Retries transient (5xx, network) failures with exponential backoff;
29
+ * non-transient failures (e.g. 403 on a revoked/rotated-away token)
30
+ * are returned immediately as `{ type: 'failed' }`.
31
+ */
32
+ export declare function refreshToken(refreshTokenValue: string): Promise<RefreshResult>;
package/dist/auth.js CHANGED
@@ -1,5 +1,41 @@
1
1
  import { AUTHORIZE_URLS, CLIENT_ID, CODE_CALLBACK_URL, OAUTH_SCOPES, TOKEN_URL, } from "./constants.js";
2
2
  import { generatePKCE } from "./pkce.js";
3
+ const REFRESH_TIMEOUT_MS = 30_000;
4
+ function isTokenResponse(value) {
5
+ if (typeof value !== 'object' || value === null)
6
+ return false;
7
+ if (!('refresh_token' in value) || !('access_token' in value))
8
+ return false;
9
+ if (!('expires_in' in value))
10
+ return false;
11
+ return (typeof value.refresh_token === 'string' &&
12
+ value.refresh_token.length > 0 &&
13
+ typeof value.access_token === 'string' &&
14
+ value.access_token.length > 0 &&
15
+ typeof value.expires_in === 'number' &&
16
+ Number.isSafeInteger(value.expires_in) &&
17
+ value.expires_in > 0);
18
+ }
19
+ async function parseTokenResponse(response) {
20
+ try {
21
+ const value = await response.json();
22
+ if (!isTokenResponse(value))
23
+ return undefined;
24
+ const expires = Date.now() + value.expires_in * 1000;
25
+ if (!Number.isSafeInteger(expires))
26
+ return undefined;
27
+ return {
28
+ refresh: value.refresh_token,
29
+ access: value.access_token,
30
+ expires,
31
+ };
32
+ }
33
+ catch (error) {
34
+ if (error instanceof SyntaxError)
35
+ return undefined;
36
+ throw error;
37
+ }
38
+ }
3
39
  function generateState() {
4
40
  return crypto.randomUUID().replace(/-/g, '');
5
41
  }
@@ -50,12 +86,12 @@ async function exchangeCode(callback, verifier, redirectUri) {
50
86
  type: 'failed',
51
87
  };
52
88
  }
53
- const json = (await result.json());
89
+ const tokens = await parseTokenResponse(result);
90
+ if (!tokens)
91
+ return { type: 'failed' };
54
92
  return {
55
93
  type: 'success',
56
- refresh: json.refresh_token,
57
- access: json.access_token,
58
- expires: Date.now() + json.expires_in * 1000,
94
+ ...tokens,
59
95
  };
60
96
  }
61
97
  export async function authorize(mode) {
@@ -91,3 +127,71 @@ export async function exchange(input, verifier, redirectUri, expectedState) {
91
127
  }
92
128
  return exchangeCode(callback, verifier, redirectUri);
93
129
  }
130
+ /**
131
+ * Exchange a refresh token for a new access/refresh token pair.
132
+ * Retries transient (5xx, network) failures with exponential backoff;
133
+ * non-transient failures (e.g. 403 on a revoked/rotated-away token)
134
+ * are returned immediately as `{ type: 'failed' }`.
135
+ */
136
+ export async function refreshToken(refreshTokenValue) {
137
+ const maxRetries = 2;
138
+ const baseDelayMs = 500;
139
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
140
+ try {
141
+ if (attempt > 0) {
142
+ const delay = baseDelayMs * 2 ** (attempt - 1);
143
+ await new Promise((resolve) => setTimeout(resolve, delay));
144
+ }
145
+ const response = await fetch(TOKEN_URL, {
146
+ method: 'POST',
147
+ signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ Accept: 'application/json, text/plain, */*',
151
+ 'User-Agent': 'axios/1.13.6',
152
+ },
153
+ body: JSON.stringify({
154
+ grant_type: 'refresh_token',
155
+ refresh_token: refreshTokenValue,
156
+ client_id: CLIENT_ID,
157
+ }),
158
+ });
159
+ if (!response.ok) {
160
+ if (response.status >= 500 && attempt < maxRetries) {
161
+ await response.body?.cancel();
162
+ continue;
163
+ }
164
+ await response.body?.cancel();
165
+ return { type: 'failed', status: response.status };
166
+ }
167
+ const tokens = await parseTokenResponse(response);
168
+ if (!tokens) {
169
+ return { type: 'failed', status: response.status };
170
+ }
171
+ return {
172
+ type: 'success',
173
+ ...tokens,
174
+ };
175
+ }
176
+ catch (error) {
177
+ const isNetworkError = (typeof error === 'object' &&
178
+ error !== null &&
179
+ 'name' in error &&
180
+ (error.name === 'TimeoutError' || error.name === 'AbortError')) ||
181
+ (error instanceof Error &&
182
+ (error.message.includes('fetch failed') ||
183
+ ('code' in error &&
184
+ (error.code === 'ECONNRESET' ||
185
+ error.code === 'ECONNREFUSED' ||
186
+ error.code === 'ETIMEDOUT' ||
187
+ error.code === 'UND_ERR_CONNECT_TIMEOUT'))));
188
+ if (attempt < maxRetries && isNetworkError) {
189
+ continue;
190
+ }
191
+ throw error;
192
+ }
193
+ }
194
+ // Unreachable — each iteration either returns or throws.
195
+ // Kept as a TypeScript exhaustiveness guard.
196
+ throw new Error('Token refresh exhausted all retries');
197
+ }
package/dist/config.d.ts CHANGED
@@ -16,15 +16,15 @@ export declare const ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR = "ANTHROPIC_CLAUDE_C
16
16
  * it with the warning explaining why reporting it is risky.
17
17
  */
18
18
  export type ClaudeCodeVersionResolution = {
19
- type: 'success';
20
- version: string;
19
+ readonly type: 'success';
20
+ readonly version: string;
21
21
  } | {
22
- type: 'outdated';
23
- version: string;
24
- warning: string;
22
+ readonly type: 'outdated';
23
+ readonly version: string;
24
+ readonly warning: string;
25
25
  } | {
26
- type: 'invalid';
27
- error: string;
26
+ readonly type: 'invalid';
27
+ readonly error: string;
28
28
  };
29
29
  /**
30
30
  * Resolve the Claude Code version to report to Anthropic.
package/dist/config.js CHANGED
@@ -8,8 +8,13 @@ import { CLAUDE_CODE_VERSION } from "./constants.js";
8
8
  * waiting for a published bump.
9
9
  */
10
10
  export const ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR = 'ANTHROPIC_CLAUDE_CODE_VERSION';
11
- /** Claude Code releases are `major.minor.patch` with numeric components. */
12
- const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
11
+ /**
12
+ * Claude Code releases are `major.minor.patch` with numeric components.
13
+ *
14
+ * Leading zeros are rejected: `02.1.258` is not a release Anthropic publishes,
15
+ * so accepting it would report a version string no server-side gate expects.
16
+ */
17
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
13
18
  /**
14
19
  * Is `candidate` an older Claude Code release than `baseline`?
15
20
  *
@@ -52,8 +57,8 @@ export function resolveClaudeCodeVersion(raw = process.env[ANTHROPIC_CLAUDE_CODE
52
57
  return {
53
58
  type: 'invalid',
54
59
  error: `${ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR} is set to ${JSON.stringify(raw)}, which is not a ` +
55
- `Claude Code version. Expected major.minor.patch (e.g. ${CLAUDE_CODE_VERSION}). ` +
56
- `Reporting the bundled version ${CLAUDE_CODE_VERSION} instead — correct or unset ` +
60
+ `Claude Code version. Expected major.minor.patch (for example, ${CLAUDE_CODE_VERSION}). ` +
61
+ `Reporting the bundled version ${CLAUDE_CODE_VERSION} instead; correct or unset ` +
57
62
  `${ANTHROPIC_CLAUDE_CODE_VERSION_ENV_VAR} and restart OpenCode to use the override.`,
58
63
  };
59
64
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import type { Plugin } from '@opencode-ai/plugin';
2
- export declare const AnthropicAuthPlugin: Plugin;
1
+ import { Plugin } from '@opencode-ai/plugin';
2
+ declare const _default: Plugin.Plugin;
3
+ export default _default;
package/dist/index.js CHANGED
@@ -1,210 +1,162 @@
1
- import { authorize, exchange } from "./auth.js";
1
+ import { Plugin } from '@opencode-ai/plugin';
2
+ import { authorize, exchange, refreshToken } from "./auth.js";
2
3
  import { resolveClaudeCodeVersion } from "./config.js";
3
- import { CLAUDE_CODE_VERSION, CLIENT_ID, TOKEN_URL } from "./constants.js";
4
+ import { CLAUDE_CODE_VERSION, REQUIRED_BETAS } from "./constants.js";
4
5
  import { createStrippedStream, isInsecure, mergeHeaders, rewriteRequestBody, rewriteUrl, setOAuthHeaders, } from "./transform.js";
5
- /**
6
- * Report a problem with the version override to the server log.
7
- *
8
- * Best-effort: a misconfigured override either degrades to the bundled version
9
- * or is honoured as set, so a logging failure must not take the plugin down
10
- * with it.
11
- */
12
- async function logVersionOverrideIssue(client, level, message) {
13
- try {
14
- // biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose app.log
15
- await client?.app?.log({
16
- body: {
17
- service: 'anthropic-auth',
18
- level,
19
- message,
20
- },
21
- });
22
- }
23
- catch {
24
- /* Logging is best-effort; the resolved version still applies. */
25
- }
6
+ const PLUGIN_ID = 'ex-machina.anthropic-auth';
7
+ const INTEGRATION_ID = 'anthropic';
8
+ const REFRESH_CACHE_GRACE_MS = 30_000;
9
+ // `methodID` is a branded `Integration.MethodID` at the type level (a
10
+ // compile-time-only tag — there's no runtime representation), so a plain
11
+ // string literal needs a cast to satisfy the branded field.
12
+ const METHOD_ID = 'claude-max';
13
+ function toCredential(exchanged) {
14
+ return {
15
+ type: 'oauth',
16
+ methodID: METHOD_ID,
17
+ refresh: exchanged.refresh,
18
+ access: exchanged.access,
19
+ expires: exchanged.expires,
20
+ };
26
21
  }
27
- export const AnthropicAuthPlugin = async ({ client }) => {
28
- // Resolved once per plugin instance so every request reports the same
29
- // version in both the user-agent and the billing header.
30
- const resolution = resolveClaudeCodeVersion();
31
- if (resolution.type === 'invalid') {
32
- await logVersionOverrideIssue(client, 'error', resolution.error);
33
- }
34
- else if (resolution.type === 'outdated') {
35
- await logVersionOverrideIssue(client, 'warn', resolution.warning);
22
+ async function resolveActiveOAuth(ctx) {
23
+ const connection = await ctx.integration.connection.active(INTEGRATION_ID);
24
+ if (!connection)
25
+ return undefined;
26
+ const credential = await ctx.integration.connection.resolve(connection);
27
+ if (credential?.type === 'oauth' && credential.methodID === METHOD_ID) {
28
+ return credential;
36
29
  }
37
- // Only a malformed override lacks a usable version; an outdated one was set
38
- // deliberately, so it is reported as configured.
39
- const claudeCodeVersion = resolution.type === 'invalid' ? CLAUDE_CODE_VERSION : resolution.version;
40
- return {
41
- auth: {
42
- provider: 'anthropic',
43
- async loader(getAuth, provider) {
44
- const auth = await getAuth();
45
- if (auth.type === 'oauth') {
46
- // zero out cost for max plan
47
- for (const model of Object.values(provider.models)) {
48
- model.cost = {
49
- input: 0,
50
- output: 0,
51
- cache: {
52
- read: 0,
53
- write: 0,
54
- },
55
- };
30
+ return undefined;
31
+ }
32
+ function warnIfInsecureUnsupported() {
33
+ if (!isInsecure())
34
+ return;
35
+ console.warn('[ex-machina.anthropic-auth] ANTHROPIC_INSECURE is set, but OpenCode v2 ' +
36
+ 'plugin request hooks cannot disable TLS verification for a custom ' +
37
+ 'ANTHROPIC_BASE_URL endpoint. TLS verification remains enabled — ' +
38
+ 'requests to an untrusted/self-signed endpoint will fail.');
39
+ }
40
+ function isTransformedOAuthRequest(request) {
41
+ const betas = new Set((request.headers.get('anthropic-beta') ?? '')
42
+ .split(',')
43
+ .map((beta) => beta.trim()));
44
+ return (request.headers.get('authorization')?.startsWith('Bearer ') === true &&
45
+ REQUIRED_BETAS.every((beta) => betas.has(beta)) &&
46
+ new URL(request.url).searchParams.get('beta') === 'true');
47
+ }
48
+ export default Plugin.define({
49
+ id: PLUGIN_ID,
50
+ setup: async (ctx) => {
51
+ warnIfInsecureUnsupported();
52
+ // Resolved once per plugin instance so every request reports the same
53
+ // version in both the user-agent and the billing header.
54
+ const versionResolution = resolveClaudeCodeVersion();
55
+ if (versionResolution.type === 'invalid') {
56
+ console.error(`[ex-machina.anthropic-auth] ${versionResolution.error}`);
57
+ }
58
+ else if (versionResolution.type === 'outdated') {
59
+ console.warn(`[ex-machina.anthropic-auth] ${versionResolution.warning}`);
60
+ }
61
+ // Only a malformed override lacks a usable version; an outdated one was
62
+ // set deliberately, so it is reported as configured.
63
+ const claudeCodeVersion = versionResolution.type === 'invalid'
64
+ ? CLAUDE_CODE_VERSION
65
+ : versionResolution.version;
66
+ // Retain successful refreshes for this plugin generation so a host call
67
+ // holding the rotated token cannot submit it again before persistence.
68
+ const refreshInFlight = new Map();
69
+ const refreshCredential = async (credential) => {
70
+ const existing = refreshInFlight.get(credential.refresh);
71
+ if (existing)
72
+ return existing;
73
+ const pending = (async () => {
74
+ const result = await refreshToken(credential.refresh);
75
+ if (result.type === 'failed') {
76
+ throw new Error(`Anthropic token refresh failed: ${result.status}`);
77
+ }
78
+ return toCredential(result);
79
+ })();
80
+ refreshInFlight.set(credential.refresh, pending);
81
+ try {
82
+ const rotated = await pending;
83
+ const timer = setTimeout(() => {
84
+ if (refreshInFlight.get(credential.refresh) === pending) {
85
+ refreshInFlight.delete(credential.refresh);
56
86
  }
57
- // Shared inflight refresh promise — prevents concurrent token refreshes
58
- // from racing against each other (and causing 401 cascades with token rotation)
59
- let refreshPromise = null;
87
+ }, REFRESH_CACHE_GRACE_MS);
88
+ timer.unref?.();
89
+ return rotated;
90
+ }
91
+ catch (error) {
92
+ if (refreshInFlight.get(credential.refresh) === pending) {
93
+ refreshInFlight.delete(credential.refresh);
94
+ }
95
+ throw error;
96
+ }
97
+ };
98
+ await ctx.integration.transform((draft) => {
99
+ draft.method.update({
100
+ integrationID: INTEGRATION_ID,
101
+ method: {
102
+ id: METHOD_ID,
103
+ type: 'oauth',
104
+ label: 'Claude Pro/Max',
105
+ },
106
+ authorize: async () => {
107
+ const result = await authorize('max');
60
108
  return {
61
- apiKey: '',
62
- async fetch(input, init) {
63
- const auth = await getAuth();
64
- if (auth.type !== 'oauth')
65
- return fetch(input, init);
66
- if (!auth.access || !auth.expires || auth.expires < Date.now()) {
67
- if (!refreshPromise) {
68
- refreshPromise = (async () => {
69
- const maxRetries = 2;
70
- const baseDelayMs = 500;
71
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
72
- try {
73
- if (attempt > 0) {
74
- const delay = baseDelayMs * 2 ** (attempt - 1);
75
- await new Promise((resolve) => setTimeout(resolve, delay));
76
- }
77
- // Re-read auth to get the latest refresh token.
78
- // The outer `auth` snapshot may be stale if tokens
79
- // were rotated since the fetch() call was made.
80
- const freshAuth = await getAuth();
81
- const response = await fetch(TOKEN_URL, {
82
- method: 'POST',
83
- headers: {
84
- 'Content-Type': 'application/json',
85
- Accept: 'application/json, text/plain, */*',
86
- 'User-Agent': 'axios/1.13.6',
87
- },
88
- body: JSON.stringify({
89
- grant_type: 'refresh_token',
90
- refresh_token: freshAuth.refresh,
91
- client_id: CLIENT_ID,
92
- }),
93
- });
94
- if (!response.ok) {
95
- if (response.status >= 500 && attempt < maxRetries) {
96
- await response.body?.cancel();
97
- continue;
98
- }
99
- const body = await response.text().catch(() => '');
100
- throw new Error(`Token refresh failed: ${response.status} — ${body}`);
101
- }
102
- const json = (await response.json());
103
- // biome-ignore lint/suspicious/noExplicitAny: SDK types don't expose auth.set
104
- await client.auth.set({
105
- path: {
106
- id: 'anthropic',
107
- },
108
- body: {
109
- type: 'oauth',
110
- refresh: json.refresh_token,
111
- access: json.access_token,
112
- expires: Date.now() + json.expires_in * 1000,
113
- },
114
- });
115
- return json.access_token;
116
- }
117
- catch (error) {
118
- const isNetworkError = error instanceof Error &&
119
- (error.message.includes('fetch failed') ||
120
- ('code' in error &&
121
- (error.code === 'ECONNRESET' ||
122
- error.code === 'ECONNREFUSED' ||
123
- error.code === 'ETIMEDOUT' ||
124
- error.code === 'UND_ERR_CONNECT_TIMEOUT')));
125
- if (attempt < maxRetries && isNetworkError) {
126
- continue;
127
- }
128
- throw error;
129
- }
130
- }
131
- // Unreachable — each iteration either returns or throws.
132
- // Kept as a TypeScript exhaustiveness guard.
133
- throw new Error('Token refresh exhausted all retries');
134
- })().finally(() => {
135
- refreshPromise = null;
136
- });
137
- }
138
- auth.access = await refreshPromise;
109
+ url: result.url,
110
+ instructions: 'Paste the authorization code here:',
111
+ mode: 'code',
112
+ callback: async (code) => {
113
+ const exchanged = await exchange(code, result.verifier, result.redirectUri, result.state);
114
+ if (exchanged.type === 'failed') {
115
+ throw new Error('Failed to exchange the Claude Pro/Max authorization code. ' +
116
+ 'Double-check that you pasted the full code and try again.');
139
117
  }
140
- const requestHeaders = mergeHeaders(input, init);
141
- // biome-ignore lint/style/noNonNullAssertion: access is guaranteed set above
142
- setOAuthHeaders(requestHeaders, auth.access, claudeCodeVersion);
143
- let body = init?.body;
144
- if (body && typeof body === 'string') {
145
- body = rewriteRequestBody(body, claudeCodeVersion);
146
- }
147
- const rewritten = rewriteUrl(input);
148
- const response = await fetch(rewritten.input, {
149
- ...init,
150
- body,
151
- headers: requestHeaders,
152
- ...(isInsecure() && { tls: { rejectUnauthorized: false } }),
153
- });
154
- return createStrippedStream(response);
118
+ return toCredential(exchanged);
155
119
  },
156
120
  };
157
- }
158
- return {};
159
- },
160
- methods: [
161
- {
162
- label: 'Claude Pro/Max',
163
- type: 'oauth',
164
- authorize: async () => {
165
- const result = await authorize('max');
166
- return {
167
- url: result.url,
168
- instructions: 'Paste the authorization code here:',
169
- method: 'code',
170
- callback: async (code) => {
171
- return exchange(code, result.verifier, result.redirectUri, result.state);
172
- },
173
- };
174
- },
175
121
  },
176
- {
177
- label: 'Create an API Key',
178
- type: 'oauth',
179
- authorize: async () => {
180
- const result = await authorize('console');
181
- return {
182
- url: result.url,
183
- instructions: 'Paste the authorization code here:',
184
- method: 'code',
185
- callback: async (code) => {
186
- const credentials = await exchange(code, result.verifier, result.redirectUri, result.state);
187
- if (credentials.type === 'failed')
188
- return credentials;
189
- const apiKey = await fetch(`https://api.anthropic.com/api/oauth/claude_cli/create_api_key`, {
190
- method: 'POST',
191
- headers: {
192
- 'Content-Type': 'application/json',
193
- authorization: `Bearer ${credentials.access}`,
194
- },
195
- }).then((r) => r.json());
196
- return { type: 'success', key: apiKey.raw_key };
197
- },
198
- };
199
- },
200
- },
201
- {
202
- provider: 'anthropic',
203
- label: 'Manually enter API Key',
204
- type: 'api',
205
- },
206
- ],
207
- },
208
- // biome-ignore lint/suspicious/noExplicitAny: Plugin type doesn't include undocumented auth/hooks
209
- };
210
- };
122
+ refresh: refreshCredential,
123
+ });
124
+ });
125
+ await ctx.session.hook('http.request', async (event) => {
126
+ if (event.model.providerID !== INTEGRATION_ID)
127
+ return;
128
+ const credential = await resolveActiveOAuth(ctx);
129
+ if (!credential)
130
+ return;
131
+ const request = event.request;
132
+ const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
133
+ const bodyText = hasBody ? await request.clone().text() : undefined;
134
+ const rewrittenBody = bodyText !== undefined
135
+ ? rewriteRequestBody(bodyText, claudeCodeVersion)
136
+ : undefined;
137
+ const headers = mergeHeaders(request);
138
+ setOAuthHeaders(headers, credential.access, claudeCodeVersion);
139
+ if (rewrittenBody !== undefined)
140
+ headers.delete('content-length');
141
+ const { input: rewrittenInput } = rewriteUrl(request.url);
142
+ const url = typeof rewrittenInput === 'string'
143
+ ? rewrittenInput
144
+ : rewrittenInput instanceof Request
145
+ ? rewrittenInput.url
146
+ : rewrittenInput.toString();
147
+ event.request = new Request(url, {
148
+ method: request.method,
149
+ headers,
150
+ body: rewrittenBody,
151
+ signal: request.signal,
152
+ });
153
+ });
154
+ await ctx.session.hook('http.response', (event) => {
155
+ if (event.model.providerID !== INTEGRATION_ID)
156
+ return;
157
+ if (!isTransformedOAuthRequest(event.request))
158
+ return;
159
+ event.response = createStrippedStream(event.response);
160
+ });
161
+ },
162
+ });
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@ex-machina/opencode-anthropic-auth",
3
- "version": "1.8.4",
3
+ "version": "2.0.0-next.1",
4
+ "type": "module",
4
5
  "repository": {
5
6
  "type": "git",
6
7
  "url": "https://github.com/ex-machina-co/opencode-anthropic-auth"
@@ -23,20 +24,21 @@
23
24
  "check": "bun turbo check:all",
24
25
  "test": "bun test",
25
26
  "types": "tsc",
27
+ "check:package": "bun run build && bun scripts/check-package.ts",
26
28
  "format": "biome check --write --unsafe",
27
29
  "format:check": "biome format .",
28
30
  "lint": "biome lint --error-on-warnings .",
29
31
  "change": "changeset",
30
- "release": "bun run build && bun change publish"
32
+ "release": "bun run build && bun change publish",
33
+ "release:next": "bun scripts/validate-next-release.ts && bun run release"
31
34
  },
32
- "peerDependencies": {
33
- "@opencode-ai/plugin": "*"
35
+ "dependencies": {
36
+ "@opencode-ai/plugin": "0.0.0-next-17444"
34
37
  },
35
38
  "devDependencies": {
36
39
  "@biomejs/biome": "2.5.2",
37
40
  "@changesets/changelog-github": "^0.7.0",
38
41
  "@changesets/cli": "^2.31.0",
39
- "@opencode-ai/plugin": "1.17.13",
40
42
  "@tsconfig/bun": "1.0.10",
41
43
  "@types/bun": "1.3.14",
42
44
  "dedent": "^1.7.2",