opencode-gitlab-dap 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GitLab Community
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # opencode-gitlab-dap
2
+
3
+ OpenCode plugin for GitLab Duo Agent Platform (DAP) workflow model discovery and selection.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ This plugin connects OpenCode to the GitLab Duo Agent Platform so users can discover and select workflow models configured for their GitLab namespace. It detects the current GitLab project, queries the DAP API for available models, caches the user's choice per project, and surfaces a TUI selection prompt when multiple models are available.
10
+
11
+ ---
12
+
13
+ ## Architecture
14
+
15
+ ```mermaid
16
+ sequenceDiagram
17
+ participant TUI as OpenCode TUI
18
+ participant Server as OpenCode Server
19
+ participant Plugin as gitlab-dap Plugin
20
+ participant Cache as GitLabModelCache
21
+ participant API as GitLab DAP API
22
+
23
+ TUI->>Server: POST /plugin/gitlab/discover
24
+ Server->>Plugin: route handler
25
+ Plugin->>Cache: check cached selection
26
+ alt cache hit
27
+ Plugin-->>Server: { status: "cached", model }
28
+ else no cache
29
+ Plugin->>API: detect project + discover models
30
+ API-->>Plugin: DiscoveredModels
31
+ alt pinned model
32
+ Plugin-->>Server: { status: "pinned", model }
33
+ else multiple models
34
+ Plugin->>Server: askUser via plugin-select bus
35
+ Server->>TUI: render selection prompt
36
+ TUI-->>Server: user picks model
37
+ Server-->>Plugin: selected ref
38
+ Plugin->>Cache: save selection
39
+ Plugin-->>Server: { status: "selected", model }
40
+ else default only
41
+ Plugin-->>Server: { status: "default", model }
42
+ end
43
+ end
44
+ Server-->>TUI: JSON response
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Install
50
+
51
+ This plugin is bundled internally with OpenCode. It is not installed as a standalone package. The plugin entry is registered automatically and exposes routes under the `/plugin/gitlab/` prefix.
52
+
53
+ ---
54
+
55
+ ## Configure
56
+
57
+ Authentication is resolved in this order:
58
+
59
+ 1. Request headers (`x-plugin-auth-token`, `x-plugin-auth-instance`)
60
+ 2. OpenCode auth store via `input.getAuth("gitlab")` (OAuth or API key)
61
+ 3. Environment variables as fallback
62
+
63
+ | Variable | Description | Default |
64
+ | ------------------------ | ------------------------------------------------------- | -------------------- |
65
+ | `GITLAB_INSTANCE_URL` | Base URL of your GitLab instance | `https://gitlab.com` |
66
+ | `GITLAB_TOKEN` | Personal or project access token (when not using OAuth) | -- |
67
+ | `GITLAB_OAUTH_CLIENT_ID` | OAuth application ID for the OAuth flow | -- |
68
+
69
+ The plugin also reads `x-plugin-directory` from request headers to scope cache and project detection to a specific working directory.
70
+
71
+ ---
72
+
73
+ ## Routes
74
+
75
+ All routes are prefixed with `/plugin/gitlab`.
76
+
77
+ ### POST /discover
78
+
79
+ Detect the GitLab project, query DAP for available models, and return the resolved model or prompt the user to pick one.
80
+
81
+ **Request:**
82
+
83
+ ```json
84
+ { "fresh": true }
85
+ ```
86
+
87
+ Set `fresh: true` to clear the cache and re-discover. The body is optional.
88
+
89
+ **Response (pinned):**
90
+
91
+ ```json
92
+ {
93
+ "status": "pinned",
94
+ "model": { "name": "Claude 4", "ref": "claude_4" }
95
+ }
96
+ ```
97
+
98
+ **Response (asked then selected):**
99
+
100
+ ```json
101
+ {
102
+ "status": "selected",
103
+ "model": { "name": "GPT-5", "ref": "gpt_5" }
104
+ }
105
+ ```
106
+
107
+ **Response (no auth):**
108
+
109
+ ```json
110
+ {
111
+ "status": "no_provider",
112
+ "error": "No auth token found"
113
+ }
114
+ ```
115
+
116
+ Possible `status` values: `pinned`, `cached`, `default`, `selected`, `dismissed`, `no_provider`, `no_models`, `error`.
117
+
118
+ ---
119
+
120
+ ### POST /reply
121
+
122
+ Save a model selection directly, bypassing the interactive prompt.
123
+
124
+ **Request:**
125
+
126
+ ```json
127
+ { "ref": "claude_4" }
128
+ ```
129
+
130
+ **Response:**
131
+
132
+ ```json
133
+ { "ref": "claude_4", "name": "Claude 4" }
134
+ ```
135
+
136
+ ---
137
+
138
+ ### POST /clear
139
+
140
+ Clear the cached discovery and selection for the current project.
141
+
142
+ **Response:**
143
+
144
+ ```json
145
+ true
146
+ ```
147
+
148
+ ---
149
+
150
+ ### GET /models
151
+
152
+ Return the current discovery state without triggering a new discovery.
153
+
154
+ **Response:**
155
+
156
+ ```json
157
+ {
158
+ "models": [
159
+ { "name": "Claude 4", "ref": "claude_4" },
160
+ { "name": "GPT-5", "ref": "gpt_5" }
161
+ ],
162
+ "pinned": null,
163
+ "default": { "name": "Claude 4", "ref": "claude_4" },
164
+ "switching": true
165
+ }
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Discovery flow
171
+
172
+ The `discover` function resolves models through a priority chain:
173
+
174
+ 1. **No token** -- returns `no_provider` immediately.
175
+ 2. **Project detection** -- uses `GitLabProjectDetector` to find the namespace from the working directory. Fails with `no_provider` if no project is found.
176
+ 3. **DAP query** -- calls `GitLabModelDiscovery.discover()` with the namespace GID. Returns `no_models` if the API returns nothing.
177
+ 4. **Pinned** -- if the namespace has a pinned model, it is saved to cache and returned.
178
+ 5. **Cached** -- if a previous selection exists in `GitLabModelCache`, it is returned.
179
+ 6. **Asked** -- if selectable models exist and nothing is cached, the list is returned with `status: "asked"` so the route handler can prompt the user.
180
+ 7. **Default** -- if only a default model exists (no selectable list), it is saved and returned.
181
+ 8. **No models** -- fallthrough when none of the above match.
182
+
183
+ ---
184
+
185
+ ## Selection flow
186
+
187
+ When discovery returns `status: "asked"`, the route handler triggers an interactive prompt:
188
+
189
+ 1. The `/discover` route calls `askUser()` with the list of models.
190
+ 2. `askUser` sends a `POST` to the internal `plugin-select/ask` endpoint on the OpenCode server.
191
+ 3. The server forwards the selection prompt to the TUI.
192
+ 4. When the user picks a model, the response flows back through the same path.
193
+ 5. The selected `ref` is saved via `saveSelection()` into `GitLabModelCache`.
194
+ 6. Subsequent `/discover` calls return `status: "cached"` without prompting again.
195
+
196
+ If the user dismisses the prompt, the response is `{ "status": "dismissed" }`.
197
+
198
+ ---
199
+
200
+ ## Develop
201
+
202
+ ### Build
203
+
204
+ ```sh
205
+ npm run build
206
+ ```
207
+
208
+ Uses [tsup](https://tsup.egoist.dev) to produce CJS, ESM, and `.d.ts` outputs in `dist/`.
209
+
210
+ ### Test
211
+
212
+ ```sh
213
+ npm test
214
+ ```
215
+
216
+ Runs [Vitest](https://vitest.dev) against `test/`. Watch mode is available with `npm run test:watch`.
217
+
218
+ ### Lint and type-check
219
+
220
+ ```sh
221
+ npm run lint
222
+ npm run type-check
223
+ ```
224
+
225
+ ### Clean
226
+
227
+ ```sh
228
+ npm run clean
229
+ ```
230
+
231
+ ---
232
+
233
+ ## License
234
+
235
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => index_default,
24
+ plugin: () => plugin
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/select.ts
29
+ var import_gitlab_ai_provider = require("gitlab-ai-provider");
30
+ var discovery = null;
31
+ function getDiscovery() {
32
+ return discovery;
33
+ }
34
+ async function discover(opts) {
35
+ if (!opts.token) return { status: "no_provider" };
36
+ if (opts.fresh) {
37
+ const c = new import_gitlab_ai_provider.GitLabModelCache(opts.directory, opts.instanceUrl);
38
+ c.clear();
39
+ }
40
+ const headers = () => ({ Authorization: `Bearer ${opts.token}` });
41
+ const detector = new import_gitlab_ai_provider.GitLabProjectDetector({
42
+ instanceUrl: opts.instanceUrl,
43
+ getHeaders: headers
44
+ });
45
+ const project = await detector.detectProject(opts.directory).catch(() => null);
46
+ if (!project?.namespaceId)
47
+ return { status: "no_provider", error: "Could not detect GitLab project" };
48
+ const nsid = `gid://gitlab/Group/${project.namespaceId}`;
49
+ const disc = new import_gitlab_ai_provider.GitLabModelDiscovery({
50
+ instanceUrl: opts.instanceUrl,
51
+ getHeaders: headers
52
+ });
53
+ const result = await disc.discover(nsid).catch(() => null);
54
+ if (!result) return { status: "no_models" };
55
+ discovery = result;
56
+ const cache = new import_gitlab_ai_provider.GitLabModelCache(opts.directory, opts.instanceUrl);
57
+ cache.saveDiscovery(result);
58
+ if (result.pinnedModel) {
59
+ cache.saveSelection(result.pinnedModel.ref, result.pinnedModel.name);
60
+ return { status: "pinned", model: result.pinnedModel };
61
+ }
62
+ const cached = cache.load();
63
+ if (cached?.selectedModelRef) {
64
+ const match = result.selectableModels.find((m) => m.ref === cached.selectedModelRef);
65
+ if (match) return { status: "cached", model: match };
66
+ return {
67
+ status: "cached",
68
+ model: { name: cached.selectedModelRef, ref: cached.selectedModelRef }
69
+ };
70
+ }
71
+ if (result.selectableModels.length > 0) {
72
+ const models = result.selectableModels.map((m) => ({
73
+ name: m.name,
74
+ ref: m.ref,
75
+ isDefault: result.defaultModel?.ref === m.ref
76
+ }));
77
+ return { status: "asked", models };
78
+ }
79
+ if (result.defaultModel) {
80
+ cache.saveSelection(result.defaultModel.ref, result.defaultModel.name);
81
+ return { status: "default", model: result.defaultModel };
82
+ }
83
+ return { status: "no_models" };
84
+ }
85
+ function saveSelection(opts) {
86
+ const cache = new import_gitlab_ai_provider.GitLabModelCache(opts.directory, opts.instanceUrl);
87
+ const model = discovery?.selectableModels.find((m) => m.ref === opts.ref);
88
+ cache.saveSelection(opts.ref, model?.name ?? opts.ref);
89
+ return { ref: opts.ref, name: model?.name ?? opts.ref };
90
+ }
91
+ function clearCache(opts) {
92
+ const cache = new import_gitlab_ai_provider.GitLabModelCache(opts.directory, opts.instanceUrl);
93
+ cache.clear();
94
+ discovery = null;
95
+ }
96
+
97
+ // src/routes.ts
98
+ function auth(c) {
99
+ return {
100
+ token: c.req.header("x-plugin-auth-token") || "",
101
+ instanceUrl: c.req.header("x-plugin-auth-instance") || process.env.GITLAB_INSTANCE_URL || "https://gitlab.com",
102
+ directory: c.req.header("x-plugin-directory") || process.cwd()
103
+ };
104
+ }
105
+ function routes(ctx) {
106
+ return (app) => {
107
+ app.post("/discover", async (c) => {
108
+ try {
109
+ const hdr = auth(c);
110
+ const body = await c.req.json().catch(() => ({}));
111
+ const cred = hdr.token ? hdr : await ctx.creds();
112
+ if (!cred?.token)
113
+ return c.json({
114
+ status: "no_provider",
115
+ error: "No auth token found"
116
+ });
117
+ const result = await discover({
118
+ token: cred.token,
119
+ instanceUrl: cred.instanceUrl,
120
+ directory: hdr.directory,
121
+ fresh: body.fresh
122
+ });
123
+ if (result.status === "asked" && result.models) {
124
+ const value = await ctx.askUser(result.models);
125
+ if (value) {
126
+ saveSelection({
127
+ ref: value,
128
+ directory: hdr.directory,
129
+ instanceUrl: cred.instanceUrl
130
+ });
131
+ const match = result.models.find((m) => m.ref === value);
132
+ return c.json({
133
+ status: "selected",
134
+ model: { name: match?.name ?? value, ref: value }
135
+ });
136
+ }
137
+ return c.json({ status: "dismissed" });
138
+ }
139
+ return c.json(result);
140
+ } catch (e) {
141
+ return c.json({ status: "error", error: e?.message ?? String(e) });
142
+ }
143
+ });
144
+ app.post("/reply", async (c) => {
145
+ const body = await c.req.json();
146
+ const hdr = auth(c);
147
+ const result = saveSelection({
148
+ ref: body.ref,
149
+ directory: hdr.directory,
150
+ instanceUrl: hdr.instanceUrl
151
+ });
152
+ return c.json(result);
153
+ });
154
+ app.post("/clear", async (c) => {
155
+ const hdr = auth(c);
156
+ clearCache({ directory: hdr.directory, instanceUrl: hdr.instanceUrl });
157
+ return c.json(true);
158
+ });
159
+ app.get("/models", async (c) => {
160
+ const disc = getDiscovery();
161
+ return c.json({
162
+ models: disc?.selectableModels || [],
163
+ pinned: disc?.pinnedModel || null,
164
+ default: disc?.defaultModel || null,
165
+ switching: disc?.modelSwitchingEnabled || false
166
+ });
167
+ });
168
+ };
169
+ }
170
+
171
+ // src/index.ts
172
+ var plugin = async (input) => {
173
+ const client = input.client;
174
+ const serverFetch = client._client.getConfig().fetch ?? fetch;
175
+ async function creds() {
176
+ const auth2 = await input.getAuth("gitlab");
177
+ if (!auth2) return null;
178
+ const token = auth2.type === "oauth" ? auth2.access : auth2.type === "api" ? auth2.key : "";
179
+ const instanceUrl = auth2.enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com";
180
+ return { token, instanceUrl };
181
+ }
182
+ async function askUser(models) {
183
+ const res = await serverFetch(
184
+ new Request("http://localhost:4096/plugin-select/ask", {
185
+ method: "POST",
186
+ headers: { "Content-Type": "application/json" },
187
+ body: JSON.stringify({
188
+ title: "Select GitLab DAP model",
189
+ options: models.map((m) => ({
190
+ label: m.name,
191
+ value: m.ref,
192
+ isDefault: m.isDefault
193
+ }))
194
+ })
195
+ })
196
+ );
197
+ const data = await res.json();
198
+ return data?.value ?? null;
199
+ }
200
+ return {
201
+ route: {
202
+ prefix: "gitlab",
203
+ handler: routes({ creds, askUser })
204
+ }
205
+ };
206
+ };
207
+ var index_default = plugin;
208
+ // Annotate the CommonJS export names for ESM import in node:
209
+ 0 && (module.exports = {
210
+ plugin
211
+ });
212
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/select.ts","../src/routes.ts"],"sourcesContent":["import type { Plugin } from \"@opencode-ai/plugin\";\nimport { routes } from \"./routes\";\n\nexport const plugin: Plugin = async (input) => {\n const client = input.client as any;\n const serverFetch: typeof fetch = client._client.getConfig().fetch ?? fetch;\n\n async function creds() {\n const auth = await input.getAuth(\"gitlab\");\n if (!auth) return null;\n const token =\n auth.type === \"oauth\" ? (auth as any).access : auth.type === \"api\" ? (auth as any).key : \"\";\n const instanceUrl =\n (auth as any).enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? \"https://gitlab.com\";\n return { token: token as string, instanceUrl: instanceUrl as string };\n }\n\n async function askUser(models: { name: string; ref: string; isDefault?: boolean }[]) {\n const res = await serverFetch(\n new Request(\"http://localhost:4096/plugin-select/ask\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n title: \"Select GitLab DAP model\",\n options: models.map((m: any) => ({\n label: m.name,\n value: m.ref,\n isDefault: m.isDefault,\n })),\n }),\n })\n );\n const data = (await res.json()) as any;\n return (data?.value ?? null) as string | null;\n }\n\n return {\n route: {\n prefix: \"gitlab\",\n handler: routes({ creds, askUser }),\n },\n };\n};\n\nexport default plugin;\n","import {\n GitLabModelDiscovery,\n GitLabModelCache,\n GitLabProjectDetector,\n type DiscoveredModels,\n} from \"gitlab-ai-provider\";\n\nexport type DiscoverResult = {\n status: \"pinned\" | \"cached\" | \"default\" | \"asked\" | \"no_provider\" | \"no_models\" | \"error\";\n model?: { name: string; ref: string };\n models?: { name: string; ref: string; isDefault?: boolean }[];\n error?: string;\n};\n\nlet discovery: DiscoveredModels | null = null;\n\nexport function getDiscovery() {\n return discovery;\n}\n\nexport async function discover(opts: {\n token: string;\n instanceUrl: string;\n directory: string;\n fresh?: boolean;\n}): Promise<DiscoverResult> {\n if (!opts.token) return { status: \"no_provider\" };\n\n if (opts.fresh) {\n const c = new GitLabModelCache(opts.directory, opts.instanceUrl);\n c.clear();\n }\n\n const headers = () => ({ Authorization: `Bearer ${opts.token}` });\n\n const detector = new GitLabProjectDetector({\n instanceUrl: opts.instanceUrl,\n getHeaders: headers,\n });\n const project = await detector.detectProject(opts.directory).catch(() => null);\n if (!project?.namespaceId)\n return { status: \"no_provider\", error: \"Could not detect GitLab project\" };\n\n const nsid = `gid://gitlab/Group/${project.namespaceId}`;\n const disc = new GitLabModelDiscovery({\n instanceUrl: opts.instanceUrl,\n getHeaders: headers,\n });\n const result = await disc.discover(nsid).catch(() => null);\n if (!result) return { status: \"no_models\" };\n\n discovery = result;\n const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);\n cache.saveDiscovery(result);\n\n if (result.pinnedModel) {\n cache.saveSelection(result.pinnedModel.ref, result.pinnedModel.name);\n return { status: \"pinned\", model: result.pinnedModel };\n }\n\n const cached = cache.load();\n if (cached?.selectedModelRef) {\n const match = result.selectableModels.find((m) => m.ref === cached.selectedModelRef);\n if (match) return { status: \"cached\", model: match };\n return {\n status: \"cached\",\n model: { name: cached.selectedModelRef, ref: cached.selectedModelRef },\n };\n }\n\n if (result.selectableModels.length > 0) {\n const models = result.selectableModels.map((m) => ({\n name: m.name,\n ref: m.ref,\n isDefault: result.defaultModel?.ref === m.ref,\n }));\n return { status: \"asked\", models };\n }\n\n if (result.defaultModel) {\n cache.saveSelection(result.defaultModel.ref, result.defaultModel.name);\n return { status: \"default\", model: result.defaultModel };\n }\n\n return { status: \"no_models\" };\n}\n\nexport function saveSelection(opts: { ref: string; directory: string; instanceUrl: string }) {\n const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);\n const model = discovery?.selectableModels.find((m) => m.ref === opts.ref);\n cache.saveSelection(opts.ref, model?.name ?? opts.ref);\n return { ref: opts.ref, name: model?.name ?? opts.ref };\n}\n\nexport function clearCache(opts: { directory: string; instanceUrl: string }) {\n const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);\n cache.clear();\n discovery = null;\n}\n","import { discover, saveSelection, clearCache, getDiscovery } from \"./select\";\n\ntype Context = {\n creds: () => Promise<{ token: string; instanceUrl: string } | null>;\n askUser: (models: { name: string; ref: string; isDefault?: boolean }[]) => Promise<string | null>;\n};\n\nfunction auth(c: any) {\n return {\n token: c.req.header(\"x-plugin-auth-token\") || \"\",\n instanceUrl:\n c.req.header(\"x-plugin-auth-instance\") ||\n process.env.GITLAB_INSTANCE_URL ||\n \"https://gitlab.com\",\n directory: c.req.header(\"x-plugin-directory\") || process.cwd(),\n };\n}\n\nexport function routes(ctx: Context) {\n return (app: any) => {\n app.post(\"/discover\", async (c: any) => {\n try {\n const hdr = auth(c);\n const body = await c.req.json().catch(() => ({}));\n\n const cred = hdr.token ? hdr : await ctx.creds();\n if (!cred?.token)\n return c.json({\n status: \"no_provider\",\n error: \"No auth token found\",\n });\n\n const result = await discover({\n token: cred.token,\n instanceUrl: cred.instanceUrl,\n directory: hdr.directory,\n fresh: body.fresh,\n });\n\n if (result.status === \"asked\" && result.models) {\n const value = await ctx.askUser(result.models);\n if (value) {\n saveSelection({\n ref: value,\n directory: hdr.directory,\n instanceUrl: cred.instanceUrl,\n });\n const match = result.models.find((m) => m.ref === value);\n return c.json({\n status: \"selected\",\n model: { name: match?.name ?? value, ref: value },\n });\n }\n return c.json({ status: \"dismissed\" });\n }\n\n return c.json(result);\n } catch (e: any) {\n return c.json({ status: \"error\", error: e?.message ?? String(e) });\n }\n });\n\n app.post(\"/reply\", async (c: any) => {\n const body = await c.req.json();\n const hdr = auth(c);\n const result = saveSelection({\n ref: body.ref,\n directory: hdr.directory,\n instanceUrl: hdr.instanceUrl,\n });\n return c.json(result);\n });\n\n app.post(\"/clear\", async (c: any) => {\n const hdr = auth(c);\n clearCache({ directory: hdr.directory, instanceUrl: hdr.instanceUrl });\n return c.json(true);\n });\n\n app.get(\"/models\", async (c: any) => {\n const disc = getDiscovery();\n return c.json({\n models: disc?.selectableModels || [],\n pinned: disc?.pinnedModel || null,\n default: disc?.defaultModel || null,\n switching: disc?.modelSwitchingEnabled || false,\n });\n });\n };\n}\n"],"mappings":"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","names":["auth"]}
package/dist/index.js ADDED
@@ -0,0 +1,189 @@
1
+ // src/select.ts
2
+ import {
3
+ GitLabModelDiscovery,
4
+ GitLabModelCache,
5
+ GitLabProjectDetector
6
+ } from "gitlab-ai-provider";
7
+ var discovery = null;
8
+ function getDiscovery() {
9
+ return discovery;
10
+ }
11
+ async function discover(opts) {
12
+ if (!opts.token) return { status: "no_provider" };
13
+ if (opts.fresh) {
14
+ const c = new GitLabModelCache(opts.directory, opts.instanceUrl);
15
+ c.clear();
16
+ }
17
+ const headers = () => ({ Authorization: `Bearer ${opts.token}` });
18
+ const detector = new GitLabProjectDetector({
19
+ instanceUrl: opts.instanceUrl,
20
+ getHeaders: headers
21
+ });
22
+ const project = await detector.detectProject(opts.directory).catch(() => null);
23
+ if (!project?.namespaceId)
24
+ return { status: "no_provider", error: "Could not detect GitLab project" };
25
+ const nsid = `gid://gitlab/Group/${project.namespaceId}`;
26
+ const disc = new GitLabModelDiscovery({
27
+ instanceUrl: opts.instanceUrl,
28
+ getHeaders: headers
29
+ });
30
+ const result = await disc.discover(nsid).catch(() => null);
31
+ if (!result) return { status: "no_models" };
32
+ discovery = result;
33
+ const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);
34
+ cache.saveDiscovery(result);
35
+ if (result.pinnedModel) {
36
+ cache.saveSelection(result.pinnedModel.ref, result.pinnedModel.name);
37
+ return { status: "pinned", model: result.pinnedModel };
38
+ }
39
+ const cached = cache.load();
40
+ if (cached?.selectedModelRef) {
41
+ const match = result.selectableModels.find((m) => m.ref === cached.selectedModelRef);
42
+ if (match) return { status: "cached", model: match };
43
+ return {
44
+ status: "cached",
45
+ model: { name: cached.selectedModelRef, ref: cached.selectedModelRef }
46
+ };
47
+ }
48
+ if (result.selectableModels.length > 0) {
49
+ const models = result.selectableModels.map((m) => ({
50
+ name: m.name,
51
+ ref: m.ref,
52
+ isDefault: result.defaultModel?.ref === m.ref
53
+ }));
54
+ return { status: "asked", models };
55
+ }
56
+ if (result.defaultModel) {
57
+ cache.saveSelection(result.defaultModel.ref, result.defaultModel.name);
58
+ return { status: "default", model: result.defaultModel };
59
+ }
60
+ return { status: "no_models" };
61
+ }
62
+ function saveSelection(opts) {
63
+ const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);
64
+ const model = discovery?.selectableModels.find((m) => m.ref === opts.ref);
65
+ cache.saveSelection(opts.ref, model?.name ?? opts.ref);
66
+ return { ref: opts.ref, name: model?.name ?? opts.ref };
67
+ }
68
+ function clearCache(opts) {
69
+ const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);
70
+ cache.clear();
71
+ discovery = null;
72
+ }
73
+
74
+ // src/routes.ts
75
+ function auth(c) {
76
+ return {
77
+ token: c.req.header("x-plugin-auth-token") || "",
78
+ instanceUrl: c.req.header("x-plugin-auth-instance") || process.env.GITLAB_INSTANCE_URL || "https://gitlab.com",
79
+ directory: c.req.header("x-plugin-directory") || process.cwd()
80
+ };
81
+ }
82
+ function routes(ctx) {
83
+ return (app) => {
84
+ app.post("/discover", async (c) => {
85
+ try {
86
+ const hdr = auth(c);
87
+ const body = await c.req.json().catch(() => ({}));
88
+ const cred = hdr.token ? hdr : await ctx.creds();
89
+ if (!cred?.token)
90
+ return c.json({
91
+ status: "no_provider",
92
+ error: "No auth token found"
93
+ });
94
+ const result = await discover({
95
+ token: cred.token,
96
+ instanceUrl: cred.instanceUrl,
97
+ directory: hdr.directory,
98
+ fresh: body.fresh
99
+ });
100
+ if (result.status === "asked" && result.models) {
101
+ const value = await ctx.askUser(result.models);
102
+ if (value) {
103
+ saveSelection({
104
+ ref: value,
105
+ directory: hdr.directory,
106
+ instanceUrl: cred.instanceUrl
107
+ });
108
+ const match = result.models.find((m) => m.ref === value);
109
+ return c.json({
110
+ status: "selected",
111
+ model: { name: match?.name ?? value, ref: value }
112
+ });
113
+ }
114
+ return c.json({ status: "dismissed" });
115
+ }
116
+ return c.json(result);
117
+ } catch (e) {
118
+ return c.json({ status: "error", error: e?.message ?? String(e) });
119
+ }
120
+ });
121
+ app.post("/reply", async (c) => {
122
+ const body = await c.req.json();
123
+ const hdr = auth(c);
124
+ const result = saveSelection({
125
+ ref: body.ref,
126
+ directory: hdr.directory,
127
+ instanceUrl: hdr.instanceUrl
128
+ });
129
+ return c.json(result);
130
+ });
131
+ app.post("/clear", async (c) => {
132
+ const hdr = auth(c);
133
+ clearCache({ directory: hdr.directory, instanceUrl: hdr.instanceUrl });
134
+ return c.json(true);
135
+ });
136
+ app.get("/models", async (c) => {
137
+ const disc = getDiscovery();
138
+ return c.json({
139
+ models: disc?.selectableModels || [],
140
+ pinned: disc?.pinnedModel || null,
141
+ default: disc?.defaultModel || null,
142
+ switching: disc?.modelSwitchingEnabled || false
143
+ });
144
+ });
145
+ };
146
+ }
147
+
148
+ // src/index.ts
149
+ var plugin = async (input) => {
150
+ const client = input.client;
151
+ const serverFetch = client._client.getConfig().fetch ?? fetch;
152
+ async function creds() {
153
+ const auth2 = await input.getAuth("gitlab");
154
+ if (!auth2) return null;
155
+ const token = auth2.type === "oauth" ? auth2.access : auth2.type === "api" ? auth2.key : "";
156
+ const instanceUrl = auth2.enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com";
157
+ return { token, instanceUrl };
158
+ }
159
+ async function askUser(models) {
160
+ const res = await serverFetch(
161
+ new Request("http://localhost:4096/plugin-select/ask", {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({
165
+ title: "Select GitLab DAP model",
166
+ options: models.map((m) => ({
167
+ label: m.name,
168
+ value: m.ref,
169
+ isDefault: m.isDefault
170
+ }))
171
+ })
172
+ })
173
+ );
174
+ const data = await res.json();
175
+ return data?.value ?? null;
176
+ }
177
+ return {
178
+ route: {
179
+ prefix: "gitlab",
180
+ handler: routes({ creds, askUser })
181
+ }
182
+ };
183
+ };
184
+ var index_default = plugin;
185
+ export {
186
+ index_default as default,
187
+ plugin
188
+ };
189
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/select.ts","../src/routes.ts","../src/index.ts"],"sourcesContent":["import {\n GitLabModelDiscovery,\n GitLabModelCache,\n GitLabProjectDetector,\n type DiscoveredModels,\n} from \"gitlab-ai-provider\";\n\nexport type DiscoverResult = {\n status: \"pinned\" | \"cached\" | \"default\" | \"asked\" | \"no_provider\" | \"no_models\" | \"error\";\n model?: { name: string; ref: string };\n models?: { name: string; ref: string; isDefault?: boolean }[];\n error?: string;\n};\n\nlet discovery: DiscoveredModels | null = null;\n\nexport function getDiscovery() {\n return discovery;\n}\n\nexport async function discover(opts: {\n token: string;\n instanceUrl: string;\n directory: string;\n fresh?: boolean;\n}): Promise<DiscoverResult> {\n if (!opts.token) return { status: \"no_provider\" };\n\n if (opts.fresh) {\n const c = new GitLabModelCache(opts.directory, opts.instanceUrl);\n c.clear();\n }\n\n const headers = () => ({ Authorization: `Bearer ${opts.token}` });\n\n const detector = new GitLabProjectDetector({\n instanceUrl: opts.instanceUrl,\n getHeaders: headers,\n });\n const project = await detector.detectProject(opts.directory).catch(() => null);\n if (!project?.namespaceId)\n return { status: \"no_provider\", error: \"Could not detect GitLab project\" };\n\n const nsid = `gid://gitlab/Group/${project.namespaceId}`;\n const disc = new GitLabModelDiscovery({\n instanceUrl: opts.instanceUrl,\n getHeaders: headers,\n });\n const result = await disc.discover(nsid).catch(() => null);\n if (!result) return { status: \"no_models\" };\n\n discovery = result;\n const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);\n cache.saveDiscovery(result);\n\n if (result.pinnedModel) {\n cache.saveSelection(result.pinnedModel.ref, result.pinnedModel.name);\n return { status: \"pinned\", model: result.pinnedModel };\n }\n\n const cached = cache.load();\n if (cached?.selectedModelRef) {\n const match = result.selectableModels.find((m) => m.ref === cached.selectedModelRef);\n if (match) return { status: \"cached\", model: match };\n return {\n status: \"cached\",\n model: { name: cached.selectedModelRef, ref: cached.selectedModelRef },\n };\n }\n\n if (result.selectableModels.length > 0) {\n const models = result.selectableModels.map((m) => ({\n name: m.name,\n ref: m.ref,\n isDefault: result.defaultModel?.ref === m.ref,\n }));\n return { status: \"asked\", models };\n }\n\n if (result.defaultModel) {\n cache.saveSelection(result.defaultModel.ref, result.defaultModel.name);\n return { status: \"default\", model: result.defaultModel };\n }\n\n return { status: \"no_models\" };\n}\n\nexport function saveSelection(opts: { ref: string; directory: string; instanceUrl: string }) {\n const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);\n const model = discovery?.selectableModels.find((m) => m.ref === opts.ref);\n cache.saveSelection(opts.ref, model?.name ?? opts.ref);\n return { ref: opts.ref, name: model?.name ?? opts.ref };\n}\n\nexport function clearCache(opts: { directory: string; instanceUrl: string }) {\n const cache = new GitLabModelCache(opts.directory, opts.instanceUrl);\n cache.clear();\n discovery = null;\n}\n","import { discover, saveSelection, clearCache, getDiscovery } from \"./select\";\n\ntype Context = {\n creds: () => Promise<{ token: string; instanceUrl: string } | null>;\n askUser: (models: { name: string; ref: string; isDefault?: boolean }[]) => Promise<string | null>;\n};\n\nfunction auth(c: any) {\n return {\n token: c.req.header(\"x-plugin-auth-token\") || \"\",\n instanceUrl:\n c.req.header(\"x-plugin-auth-instance\") ||\n process.env.GITLAB_INSTANCE_URL ||\n \"https://gitlab.com\",\n directory: c.req.header(\"x-plugin-directory\") || process.cwd(),\n };\n}\n\nexport function routes(ctx: Context) {\n return (app: any) => {\n app.post(\"/discover\", async (c: any) => {\n try {\n const hdr = auth(c);\n const body = await c.req.json().catch(() => ({}));\n\n const cred = hdr.token ? hdr : await ctx.creds();\n if (!cred?.token)\n return c.json({\n status: \"no_provider\",\n error: \"No auth token found\",\n });\n\n const result = await discover({\n token: cred.token,\n instanceUrl: cred.instanceUrl,\n directory: hdr.directory,\n fresh: body.fresh,\n });\n\n if (result.status === \"asked\" && result.models) {\n const value = await ctx.askUser(result.models);\n if (value) {\n saveSelection({\n ref: value,\n directory: hdr.directory,\n instanceUrl: cred.instanceUrl,\n });\n const match = result.models.find((m) => m.ref === value);\n return c.json({\n status: \"selected\",\n model: { name: match?.name ?? value, ref: value },\n });\n }\n return c.json({ status: \"dismissed\" });\n }\n\n return c.json(result);\n } catch (e: any) {\n return c.json({ status: \"error\", error: e?.message ?? String(e) });\n }\n });\n\n app.post(\"/reply\", async (c: any) => {\n const body = await c.req.json();\n const hdr = auth(c);\n const result = saveSelection({\n ref: body.ref,\n directory: hdr.directory,\n instanceUrl: hdr.instanceUrl,\n });\n return c.json(result);\n });\n\n app.post(\"/clear\", async (c: any) => {\n const hdr = auth(c);\n clearCache({ directory: hdr.directory, instanceUrl: hdr.instanceUrl });\n return c.json(true);\n });\n\n app.get(\"/models\", async (c: any) => {\n const disc = getDiscovery();\n return c.json({\n models: disc?.selectableModels || [],\n pinned: disc?.pinnedModel || null,\n default: disc?.defaultModel || null,\n switching: disc?.modelSwitchingEnabled || false,\n });\n });\n };\n}\n","import type { Plugin } from \"@opencode-ai/plugin\";\nimport { routes } from \"./routes\";\n\nexport const plugin: Plugin = async (input) => {\n const client = input.client as any;\n const serverFetch: typeof fetch = client._client.getConfig().fetch ?? fetch;\n\n async function creds() {\n const auth = await input.getAuth(\"gitlab\");\n if (!auth) return null;\n const token =\n auth.type === \"oauth\" ? (auth as any).access : auth.type === \"api\" ? (auth as any).key : \"\";\n const instanceUrl =\n (auth as any).enterpriseUrl ?? process.env.GITLAB_INSTANCE_URL ?? \"https://gitlab.com\";\n return { token: token as string, instanceUrl: instanceUrl as string };\n }\n\n async function askUser(models: { name: string; ref: string; isDefault?: boolean }[]) {\n const res = await serverFetch(\n new Request(\"http://localhost:4096/plugin-select/ask\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n title: \"Select GitLab DAP model\",\n options: models.map((m: any) => ({\n label: m.name,\n value: m.ref,\n isDefault: m.isDefault,\n })),\n }),\n })\n );\n const data = (await res.json()) as any;\n return (data?.value ?? null) as string | null;\n }\n\n return {\n route: {\n prefix: \"gitlab\",\n handler: routes({ creds, askUser }),\n },\n };\n};\n\nexport default plugin;\n"],"mappings":"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","names":["auth"]}
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "opencode-gitlab-dap",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin for GitLab Duo Agent Platform (DAP) workflow model discovery and selection",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "bun": "./src/index.ts",
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://gitlab.com/vglafirov/opencode-gitlab-dap.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://gitlab.com/vglafirov/opencode-gitlab-dap/-/issues"
32
+ },
33
+ "homepage": "https://gitlab.com/vglafirov/opencode-gitlab-dap#readme",
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "clean": "rm -rf dist *.tsbuildinfo",
37
+ "rebuild": "npm run clean && npm run build",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "test:coverage": "vitest run --coverage",
41
+ "lint": "eslint \"src/**/*.ts\"",
42
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
43
+ "format": "prettier --write \"src/**/*.ts\"",
44
+ "format:check": "prettier --check \"src/**/*.ts\"",
45
+ "type-check": "tsc --noEmit",
46
+ "prepublishOnly": "npm run build",
47
+ "semantic-release": "semantic-release"
48
+ },
49
+ "dependencies": {
50
+ "gitlab-ai-provider": "^5.0.0"
51
+ },
52
+ "peerDependencies": {
53
+ "@opencode-ai/plugin": ">=1.2.24"
54
+ },
55
+ "devDependencies": {
56
+ "@commitlint/cli": "^18.4.3",
57
+ "@commitlint/config-conventional": "^18.4.3",
58
+ "@opencode-ai/plugin": ">=1.2.24",
59
+ "@semantic-release/changelog": "^6.0.3",
60
+ "@semantic-release/git": "^10.0.1",
61
+ "@semantic-release/gitlab": "^13.0.3",
62
+ "@semantic-release/npm": "^11.0.2",
63
+ "@types/node": "^20.17.24",
64
+ "@typescript-eslint/eslint-plugin": "^6.15.0",
65
+ "@typescript-eslint/parser": "^6.15.0",
66
+ "eslint": "^8.56.0",
67
+ "eslint-config-prettier": "^9.1.0",
68
+ "prettier": "^3.1.1",
69
+ "semantic-release": "^22.0.12",
70
+ "tsup": "^8.0.0",
71
+ "typescript": "^5.8.3",
72
+ "vitest": "^4.0.16"
73
+ },
74
+ "author": {
75
+ "name": "Vladimir Glafirov",
76
+ "email": "vglafirov@gitlab.com",
77
+ "url": "https://gitlab.com/vglafirov"
78
+ },
79
+ "keywords": [
80
+ "opencode",
81
+ "opencode-plugin",
82
+ "gitlab",
83
+ "gitlab-duo",
84
+ "dap",
85
+ "duo-agent-platform",
86
+ "workflow",
87
+ "model-discovery",
88
+ "plugin",
89
+ "ai",
90
+ "typescript"
91
+ ],
92
+ "engines": {
93
+ "node": ">=18.0.0"
94
+ }
95
+ }