@moderniser/plugin 0.1.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/dist/auth.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export declare function moderniserAuth(): {
2
+ provider: string;
3
+ methods: {
4
+ type: "oauth";
5
+ label: string;
6
+ authorize(): Promise<{
7
+ url: string;
8
+ instructions: string;
9
+ method: "auto";
10
+ callback(): Promise<{
11
+ type: "failed";
12
+ key?: undefined;
13
+ } | {
14
+ type: "success";
15
+ key: string;
16
+ }>;
17
+ }>;
18
+ }[];
19
+ loader(auth: () => Promise<any>): Promise<{
20
+ apiKey: any;
21
+ } | {
22
+ apiKey?: undefined;
23
+ }>;
24
+ };
@@ -0,0 +1,13 @@
1
+ export declare const API: () => string;
2
+ export declare class ApiError extends Error {
3
+ status: number;
4
+ constructor(status: number, message: string);
5
+ }
6
+ export declare function setStoredKey(key: string): void;
7
+ export declare function storedKey(): string | undefined;
8
+ export declare function api<T>(path: string, init?: RequestInit): Promise<T>;
9
+ export declare const get: <T>(path: string) => Promise<T>;
10
+ export declare const post: <T>(path: string, body: unknown) => Promise<T>;
11
+ export declare const patch: <T>(path: string, body: unknown) => Promise<T>;
12
+ export declare const del: <T>(path: string) => Promise<T>;
13
+ export declare const json: (value: unknown) => string;
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const ModerniserPlugin: Plugin;
3
+ export default ModerniserPlugin;
package/dist/index.js ADDED
@@ -0,0 +1,468 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // packages/plugin/src/client.ts
5
+ import { readFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ var API = () => (process.env.MODERNISER_API_URL ?? "https://v2.moderniser.ai/api").replace(/\/+$/, "");
9
+ var BASE = API;
10
+
11
+ class ApiError extends Error {
12
+ status;
13
+ constructor(status, message) {
14
+ super(message);
15
+ this.status = status;
16
+ }
17
+ }
18
+ var stored;
19
+ function setStoredKey(key) {
20
+ stored = key;
21
+ }
22
+ function storedKey() {
23
+ return stored;
24
+ }
25
+ function authFilePath() {
26
+ const data = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
27
+ return join(data, "moderniser", "auth.json");
28
+ }
29
+ var fileKey;
30
+ function fromAuthFile() {
31
+ if (fileKey !== undefined)
32
+ return fileKey ?? undefined;
33
+ fileKey = null;
34
+ let raw;
35
+ try {
36
+ raw = readFileSync(authFilePath(), "utf8");
37
+ } catch {
38
+ return;
39
+ }
40
+ try {
41
+ const entry = JSON.parse(raw)["moderniser"];
42
+ if (!entry || typeof entry !== "object")
43
+ return;
44
+ const { type, key } = entry;
45
+ if (type !== "api" || typeof key !== "string" || key.length === 0)
46
+ return;
47
+ fileKey = key;
48
+ return key;
49
+ } catch {
50
+ return;
51
+ }
52
+ }
53
+ function token() {
54
+ const value = process.env.MODERNISER_API_KEY ?? stored ?? fromAuthFile();
55
+ if (!value) {
56
+ throw new Error(`Not signed in to Moderniser. Run: moderniser providers login
57
+ ` + "(or set MODERNISER_API_KEY to a token from Settings in the web app).");
58
+ }
59
+ return value;
60
+ }
61
+ async function api(path, init = {}) {
62
+ const bearer = token();
63
+ let res;
64
+ try {
65
+ res = await fetch(`${BASE()}${path}`, {
66
+ ...init,
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ Authorization: `Bearer ${bearer}`,
70
+ ...init.headers
71
+ },
72
+ signal: AbortSignal.timeout(30000)
73
+ });
74
+ } catch (error) {
75
+ throw new ApiError(0, `Could not reach Moderniser at ${BASE()}: ${error.message}`);
76
+ }
77
+ if (res.status === 204)
78
+ return;
79
+ const body = await res.json().catch(() => ({}));
80
+ if (!res.ok) {
81
+ throw new ApiError(res.status, body.error ?? `Request failed with ${res.status}`);
82
+ }
83
+ return body;
84
+ }
85
+ var get = (path) => api(path);
86
+ var post = (path, body) => api(path, { method: "POST", body: JSON.stringify(body) });
87
+ var patch = (path, body) => api(path, { method: "PATCH", body: JSON.stringify(body) });
88
+ var json = (value) => JSON.stringify(value, null, 2);
89
+
90
+ // packages/plugin/src/auth.ts
91
+ async function start() {
92
+ const label = [process.env.USER, __require("node:os").hostname()].filter(Boolean).join("@");
93
+ const res = await fetch(`${API()}/auth/device`, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify({ label }),
97
+ signal: AbortSignal.timeout(15000)
98
+ });
99
+ if (!res.ok) {
100
+ throw new Error(`Could not reach Moderniser at ${API()} (${res.status}). Check MODERNISER_API_URL.`);
101
+ }
102
+ return await res.json();
103
+ }
104
+ async function poll(deviceCode, expiresIn, intervalSeconds) {
105
+ const deadline = Date.now() + expiresIn * 1000;
106
+ let interval = Math.max(intervalSeconds, 1) * 1000;
107
+ while (Date.now() < deadline) {
108
+ await new Promise((resolve) => setTimeout(resolve, interval));
109
+ let body;
110
+ try {
111
+ const res = await fetch(`${API()}/auth/device/token`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify({ deviceCode }),
115
+ signal: AbortSignal.timeout(15000)
116
+ });
117
+ body = await res.json();
118
+ } catch {
119
+ continue;
120
+ }
121
+ if (body.token)
122
+ return body.token;
123
+ if (body.error === "authorization_pending")
124
+ continue;
125
+ if (body.error === "slow_down") {
126
+ interval += 2000;
127
+ continue;
128
+ }
129
+ return null;
130
+ }
131
+ return null;
132
+ }
133
+ function moderniserAuth() {
134
+ return {
135
+ provider: "moderniser",
136
+ methods: [
137
+ {
138
+ type: "oauth",
139
+ label: "Sign in with Moderniser",
140
+ async authorize() {
141
+ const started = await start();
142
+ return {
143
+ url: started.verificationUrl,
144
+ instructions: `Check that your browser shows this code: ${started.userCode}`,
145
+ method: "auto",
146
+ async callback() {
147
+ const token2 = await poll(started.deviceCode, started.expiresIn, started.interval);
148
+ if (!token2)
149
+ return { type: "failed" };
150
+ setStoredKey(token2);
151
+ return { type: "success", key: token2 };
152
+ }
153
+ };
154
+ }
155
+ }
156
+ ],
157
+ async loader(auth) {
158
+ const stored2 = await auth().catch(() => {
159
+ return;
160
+ });
161
+ const key = stored2?.type === "api" ? stored2.key : undefined;
162
+ if (key)
163
+ setStoredKey(key);
164
+ return key ? { apiKey: key } : {};
165
+ }
166
+ };
167
+ }
168
+
169
+ // packages/plugin/src/tools-boards.ts
170
+ import { tool } from "@opencode-ai/plugin";
171
+ var z = tool.schema;
172
+ var STATUS = ["todo", "in_progress", "in_review", "blocked", "done"];
173
+ var TYPE = ["feature", "bug", "improvement", "chore"];
174
+ var FILES_TO_MODIFY = "Paths you have actually opened, written exactly as you reached them from the folder this session started in — 'orders/src/Order.java', not an absolute path and not a path you have rewritten. When a task spans two codebases the leading folder is the only thing telling a reader which one a change is in. Leave this empty rather than guessing.";
175
+ function commentPath(taskId, subtaskId) {
176
+ if (Boolean(taskId) === Boolean(subtaskId)) {
177
+ throw new Error("Pass exactly one of taskId or subtaskId");
178
+ }
179
+ return taskId ? `/projects/tasks/${taskId}/comments` : `/projects/subtasks/${subtaskId}/comments`;
180
+ }
181
+ var boardTools = {
182
+ list_projects: tool({
183
+ description: "List the projects belonging to the caller's company. Call this first — every task belongs to a project, and you need a project id before you can create one.",
184
+ args: {},
185
+ async execute() {
186
+ return json((await get("/projects")).projects);
187
+ }
188
+ }),
189
+ create_project: tool({
190
+ description: "Create a project. A project is an area of work, and it may span several codebases — a checkout modernisation touches the storefront and the system that receives the order. Check list_projects first, since a duplicate fragments the board.",
191
+ args: {
192
+ name: z.string().describe("Short name, e.g. 'Checkout modernisation'"),
193
+ description: z.string().describe("What this covers and why it is being worked on").optional(),
194
+ keyPrefix: z.string().regex(/^[A-Z]{3}$/).describe("Three uppercase letters used to key tasks, e.g. 'CAD' gives CAD-1. Omit it and one is derived from the name.").optional()
195
+ },
196
+ async execute(args) {
197
+ return json((await post("/projects", args)).project);
198
+ }
199
+ }),
200
+ list_tasks: tool({
201
+ description: "List the tasks in a project with their status. Use this to see what already exists before creating more.",
202
+ args: {
203
+ projectId: z.string().describe("Project id from list_projects"),
204
+ status: z.enum(STATUS).describe("Only tasks in this status").optional()
205
+ },
206
+ async execute({ projectId, status }) {
207
+ const query = status ? `?status=${status}` : "";
208
+ return json((await get(`/projects/${projectId}/tasks${query}`)).tasks);
209
+ }
210
+ }),
211
+ get_task: tool({
212
+ description: "Fetch one task with all of its subtasks. Use this before updating anything, so you are editing against the current state rather than what you remember.",
213
+ args: { taskId: z.string().describe("Task id") },
214
+ async execute(args) {
215
+ return json((await get(`/projects/tasks/${args.taskId}`)).task);
216
+ }
217
+ }),
218
+ create_task: tool({
219
+ description: "Create a task: one unit of work a team would plan together, such as 'Upgrade to SAP Commerce 2211'. Read the relevant code before writing the description — a task that describes the actual state of the code is worth keeping, one written from assumption is not. Break it into subtasks afterwards with create_subtask.",
220
+ args: {
221
+ projectId: z.string().describe("Project id from list_projects"),
222
+ title: z.string().max(300).describe("Imperative and specific: 'Replace deprecated FlexibleSearch calls'"),
223
+ description: z.string().describe("What needs doing and why, including what you found in the code — current version, affected modules, known blockers"),
224
+ type: z.enum(TYPE).describe("Defaults to feature").optional()
225
+ },
226
+ async execute({ projectId, ...body }) {
227
+ return json((await post(`/projects/${projectId}/tasks`, body)).task);
228
+ }
229
+ }),
230
+ update_task: tool({
231
+ description: "Update a task's title, description, type or status. Move it to in_progress when work starts. Prefer in_review over done when you have finished but a person has not checked it — done is a human's word. Use blocked when you cannot proceed, and say why in a comment.",
232
+ args: {
233
+ taskId: z.string().describe("Task id"),
234
+ title: z.string().max(300).describe("New title").optional(),
235
+ description: z.string().describe("New description").optional(),
236
+ type: z.enum(TYPE).describe("New type").optional(),
237
+ status: z.enum(STATUS).describe("New status").optional()
238
+ },
239
+ async execute({ taskId, ...body }) {
240
+ return json((await patch(`/projects/tasks/${taskId}`, body)).task);
241
+ }
242
+ }),
243
+ list_subtasks: tool({
244
+ description: "List the subtasks of a task, in order.",
245
+ args: { taskId: z.string().describe("Task id") },
246
+ async execute(args) {
247
+ return json((await get(`/projects/tasks/${args.taskId}/subtasks`)).subtasks);
248
+ }
249
+ }),
250
+ create_subtask: tool({
251
+ description: "Add a subtask: one concrete change a developer could pick up and finish, in one codebase. Populate filesToModify with paths you have actually opened; leave it empty rather than filling it with guesses. Work that genuinely spans two codebases is two subtasks, which is also what keeps 'done' honest.",
252
+ args: {
253
+ taskId: z.string().describe("Task id"),
254
+ title: z.string().max(300).describe("One specific change"),
255
+ description: z.string().describe("What to change and how you know it needs changing"),
256
+ filesToModify: z.array(z.string()).describe(FILES_TO_MODIFY).optional(),
257
+ implementationNotes: z.string().describe("Anything that saves the next person a search: the symbol to grep for, the call sites, the order to do things in").optional(),
258
+ order: z.number().int().describe("Position in the list. Lower runs first.").optional()
259
+ },
260
+ async execute({ taskId, ...body }) {
261
+ return json((await post(`/projects/tasks/${taskId}/subtasks`, body)).subtask);
262
+ }
263
+ }),
264
+ update_subtask: tool({
265
+ description: "Update a subtask. Use this to mark one done as you finish it, so the board reflects reality while the work is happening rather than after.",
266
+ args: {
267
+ subtaskId: z.string().describe("Subtask id"),
268
+ title: z.string().max(300).describe("New title").optional(),
269
+ description: z.string().describe("New description").optional(),
270
+ status: z.enum(STATUS).describe("New status").optional(),
271
+ filesToModify: z.array(z.string()).describe(FILES_TO_MODIFY).optional(),
272
+ implementationNotes: z.string().describe("Notes for whoever picks this up").optional(),
273
+ order: z.number().int().describe("Position in the list").optional()
274
+ },
275
+ async execute({ subtaskId, ...body }) {
276
+ return json((await patch(`/projects/subtasks/${subtaskId}`, body)).subtask);
277
+ }
278
+ }),
279
+ list_comments: tool({
280
+ description: "Read the comment thread on a task or a subtask. Do this before acting on work someone else planned — a correction from a human usually lives here rather than in the description. Pass exactly one of taskId or subtaskId.",
281
+ args: {
282
+ taskId: z.string().describe("Task id").optional(),
283
+ subtaskId: z.string().describe("Subtask id").optional()
284
+ },
285
+ async execute(args) {
286
+ const path = commentPath(args.taskId, args.subtaskId);
287
+ return json((await get(path)).comments);
288
+ }
289
+ }),
290
+ add_comment: tool({
291
+ description: "Leave a comment on a task or subtask. Use it to record what you found, what you changed, or why you departed from the plan — not to restate the description. Comments you write are labelled as coming from the agent. Pass exactly one of taskId or subtaskId.",
292
+ args: {
293
+ body: z.string().max(1e4).describe("The comment"),
294
+ taskId: z.string().describe("Task id").optional(),
295
+ subtaskId: z.string().describe("Subtask id").optional()
296
+ },
297
+ async execute(args) {
298
+ const path = commentPath(args.taskId, args.subtaskId);
299
+ return json((await post(path, { body: args.body, source: "agent" })).comment);
300
+ }
301
+ })
302
+ };
303
+
304
+ // packages/plugin/src/tools-context-hub.ts
305
+ import { tool as tool2 } from "@opencode-ai/plugin";
306
+ var z2 = tool2.schema;
307
+ var contextHubTools = {
308
+ search_context_hub: tool2({
309
+ description: "Search the company's internal documentation. Run this before answering a question about how something works here — the wiki usually explains the intent that the code only implies, and it is where conventions, past decisions and known traps are written down. Searches by meaning as well as by keyword, so a question phrased differently to the page still finds it. Results carry a score and a staleness flag; a stale page is past its review date and may be wrong. Follow up with read_context_hub_page for the full text.",
310
+ args: {
311
+ query: z2.string().describe("What you want to know. A phrase or question works better than a single word."),
312
+ mode: z2.enum(["hybrid", "keyword", "semantic"]).describe("hybrid (default) searches by meaning and by keyword. Use keyword when looking for an exact identifier, class name or error string.").optional(),
313
+ spaceId: z2.string().describe("Optional: restrict to one space, from list_context_hub_spaces").optional(),
314
+ tag: z2.string().describe("Optional: restrict to pages carrying this tag").optional(),
315
+ limit: z2.number().int().describe("Maximum results, default 20, maximum 50").optional()
316
+ },
317
+ async execute(args) {
318
+ const params = new URLSearchParams({ q: args.query });
319
+ if (args.mode)
320
+ params.set("mode", args.mode);
321
+ if (args.spaceId)
322
+ params.set("spaceId", args.spaceId);
323
+ if (args.tag)
324
+ params.set("tag", args.tag);
325
+ if (args.limit)
326
+ params.set("limit", String(args.limit));
327
+ return json((await get(`/context-hub/search?${params}`)).results);
328
+ }
329
+ }),
330
+ list_context_hub_spaces: tool2({
331
+ description: "List the spaces in the Context Hub. A space groups related pages — typically one per system, team or imported wiki. Use it to narrow a search when a keyword is too common.",
332
+ args: {},
333
+ async execute() {
334
+ return json((await get("/context-hub/spaces")).spaces);
335
+ }
336
+ }),
337
+ list_context_hub_pages: tool2({
338
+ description: "List the pages in a space, with titles and when they were last updated. Bodies are not included — read_context_hub_page fetches those.",
339
+ args: { spaceId: z2.string().describe("Space id from list_context_hub_spaces") },
340
+ async execute(args) {
341
+ return json((await get(`/context-hub/spaces/${args.spaceId}/pages`)).pages);
342
+ }
343
+ }),
344
+ read_context_hub_page: tool2({
345
+ description: "Read one page in full. Cite what you found from it when it changes your answer, so the person can check the source — the wiki can be out of date, and saying where a claim came from is what lets them notice.",
346
+ args: { pageId: z2.string().describe("Page id from search or list") },
347
+ async execute(args) {
348
+ return json((await get(`/context-hub/pages/${args.pageId}`)).page);
349
+ }
350
+ }),
351
+ create_context_hub_page: tool2({
352
+ description: "Write a new page. Use this when you have worked something out that the next person would otherwise have to rediscover — a migration procedure, why an approach was rejected, how two systems actually talk to each other. Search first: adding a second page on a subject that already has one makes both less trustworthy.",
353
+ args: {
354
+ spaceId: z2.string().describe("Space to create the page in"),
355
+ title: z2.string().max(300).describe("Specific and searchable: 'Upgrading SAP Commerce 1905 to 2211'"),
356
+ content: z2.string().describe("Markdown body")
357
+ },
358
+ async execute({ spaceId, ...body }) {
359
+ return json((await post(`/context-hub/spaces/${spaceId}/pages`, body)).page);
360
+ }
361
+ }),
362
+ update_context_hub_page: tool2({
363
+ description: "Replace a page's title or body. The content you send replaces what is there — read the page first and return the whole document with your changes folded in, or you will delete the parts you did not mention.",
364
+ args: {
365
+ pageId: z2.string().describe("Page id"),
366
+ title: z2.string().max(300).describe("New title").optional(),
367
+ content: z2.string().describe("The complete new markdown body, not a fragment").optional()
368
+ },
369
+ async execute({ pageId, ...body }) {
370
+ return json((await patch(`/context-hub/pages/${pageId}`, body)).page);
371
+ }
372
+ })
373
+ };
374
+
375
+ // packages/plugin/src/index.ts
376
+ var API2 = () => (process.env.MODERNISER_API_URL ?? "https://v2.moderniser.ai/api").replace(/\/+$/, "");
377
+ var KEY = () => process.env.MODERNISER_API_KEY;
378
+ var GATEWAY = () => {
379
+ const raw = (process.env.MODERNISER_GATEWAY_URL ?? "https://gateway.moderniser.ai").replace(/\/+$/, "");
380
+ return raw.endsWith("/v1") ? raw : `${raw}/v1`;
381
+ };
382
+ var PROVIDER_ID = "moderniser";
383
+ var DEFAULT_MODEL = `${PROVIDER_ID}/emm`;
384
+ async function call(path, body) {
385
+ const key = KEY() ?? storedKey();
386
+ if (!key)
387
+ return null;
388
+ try {
389
+ return await fetch(`${API2()}${path}`, {
390
+ method: "POST",
391
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
392
+ body: JSON.stringify(body),
393
+ signal: AbortSignal.timeout(1e4)
394
+ });
395
+ } catch {
396
+ return null;
397
+ }
398
+ }
399
+ var registered = new Set;
400
+ var reported = new Map;
401
+ var ModerniserPlugin = async () => {
402
+ return {
403
+ config: async (config) => {
404
+ const models = {
405
+ emm: { name: "Enterprise Modernisation Model", tool_call: true, reasoning: true },
406
+ "emm-high": {
407
+ name: "Enterprise Modernisation Model (deep reasoning)",
408
+ tool_call: true,
409
+ reasoning: true
410
+ }
411
+ };
412
+ const key = KEY();
413
+ config.provider ??= {};
414
+ config.provider[PROVIDER_ID] ??= {
415
+ name: "Moderniser",
416
+ npm: "@ai-sdk/openai-compatible",
417
+ options: { baseURL: GATEWAY(), ...key ? { apiKey: key } : {} },
418
+ models
419
+ };
420
+ const requested = process.env.MODERNISER_MODEL;
421
+ if (requested !== "off") {
422
+ config.model ??= requested || DEFAULT_MODEL;
423
+ config.small_model ??= config.model;
424
+ }
425
+ },
426
+ tool: { ...boardTools, ...contextHubTools },
427
+ auth: moderniserAuth(),
428
+ event: async ({ event }) => {
429
+ const type = event?.type;
430
+ if (type === "session.updated" || type === "session.status") {
431
+ const sessionId = event.properties?.info?.id ?? event.properties?.sessionID;
432
+ if (typeof sessionId === "string" && !registered.has(sessionId)) {
433
+ registered.add(sessionId);
434
+ await call("/billing/sessions", { sessionId });
435
+ }
436
+ return;
437
+ }
438
+ if (type !== "message.updated")
439
+ return;
440
+ const info = event.properties?.info;
441
+ if (!info || info.role !== "assistant")
442
+ return;
443
+ const tokens = info.tokens;
444
+ if (!tokens)
445
+ return;
446
+ const total = (tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0) + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0);
447
+ if (total === 0)
448
+ return;
449
+ if (reported.get(info.id) === total)
450
+ return;
451
+ reported.set(info.id, total);
452
+ await call("/billing/usage", {
453
+ sessionId: info.sessionID,
454
+ inputTokens: tokens.input ?? 0,
455
+ outputTokens: tokens.output ?? 0,
456
+ reasoningTokens: tokens.reasoning ?? 0,
457
+ cacheReadTokens: tokens.cache?.read ?? 0,
458
+ cacheWriteTokens: tokens.cache?.write ?? 0,
459
+ model: info.modelID
460
+ });
461
+ }
462
+ };
463
+ };
464
+ var src_default = ModerniserPlugin;
465
+ export {
466
+ src_default as default,
467
+ ModerniserPlugin
468
+ };
@@ -0,0 +1,2 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ export declare const boardTools: Record<string, ToolDefinition>;
@@ -0,0 +1,2 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ export declare const contextHubTools: Record<string, ToolDefinition>;
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@moderniser/plugin",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Moderniser plugin for opencode: points inference at the gateway, carries the board and Context Hub tools, and reports session telemetry",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/moderniser-ai/cloud.git",
10
+ "directory": "packages/plugin"
11
+ },
12
+ "homepage": "https://v2.moderniser.ai",
13
+ "main": "./dist/index.js",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "peerDependencies": {
24
+ "@opencode-ai/plugin": "*"
25
+ },
26
+ "types": "./dist/index.d.ts"
27
+ }