opencode-gitlab-duo-agentic 0.1.2 → 0.1.4
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 +30 -24
- package/dist/index.js +46 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# opencode-gitlab-duo-agentic
|
|
2
2
|
|
|
3
|
-
OpenCode plugin
|
|
3
|
+
OpenCode plugin for GitLab Duo Agentic. It registers the provider, discovers models from the GitLab API, and exposes file-reading tools.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. Export your GitLab token:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
export GITLAB_TOKEN=glpat-...
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Add the plugin to `opencode.json`:
|
|
8
14
|
|
|
9
15
|
```json
|
|
10
16
|
{
|
|
@@ -13,40 +19,40 @@ Add the plugin to your OpenCode config:
|
|
|
13
19
|
}
|
|
14
20
|
```
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
3. Run `opencode`. The provider, models, and tools are registered automatically.
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
`GITLAB_INSTANCE_URL` defaults to `https://gitlab.com`. Set it only for self-managed GitLab.
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
export GITLAB_TOKEN=glpat-...
|
|
22
|
-
export GITLAB_INSTANCE_URL=https://gitlab.com
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`GITLAB_INSTANCE_URL` is optional and defaults to `https://gitlab.com`.
|
|
26
|
+
## Provider options
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Override defaults in `opencode.json` under `provider.gitlab-duo-agentic.options`.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
| Option | Type | Default | Description |
|
|
31
|
+
|--------|------|---------|-------------|
|
|
32
|
+
| `instanceUrl` | string | `GITLAB_INSTANCE_URL` or `https://gitlab.com` | GitLab instance URL |
|
|
33
|
+
| `apiKey` | string | `GITLAB_TOKEN` | Personal access token |
|
|
34
|
+
| `sendSystemContext` | boolean | `true` | Send system context to Duo |
|
|
35
|
+
| `enableMcp` | boolean | `true` | Enable MCP tools |
|
|
36
|
+
| `systemRules` | string | `""` | Inline system rules |
|
|
37
|
+
| `systemRulesPath` | string | `""` | Path to a system rules file |
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
2. `models.json` file resolution:
|
|
33
|
-
- `options.modelsPath` in provider config
|
|
34
|
-
- `GITLAB_DUO_MODELS_PATH`
|
|
35
|
-
- `models.json` found by walking upward from `process.cwd()`
|
|
36
|
-
3. Fallback default model `duo-agentic`
|
|
39
|
+
## Model discovery
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
Models are discovered in this order:
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
1. Local cache (TTL: 24h)
|
|
44
|
+
2. Live fetch from GitLab GraphQL API
|
|
45
|
+
3. Stale cache (if live fetch fails)
|
|
46
|
+
4. `models.json` on disk
|
|
47
|
+
5. Default `duo-agentic` model
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
Cache is stored in `~/.cache/opencode/` (or `XDG_CACHE_HOME`). Override TTL with `GITLAB_DUO_MODELS_CACHE_TTL` (seconds).
|
|
44
50
|
|
|
45
51
|
## Development
|
|
46
52
|
|
|
47
53
|
```bash
|
|
48
54
|
npm install
|
|
49
|
-
npm run typecheck
|
|
50
55
|
npm run build
|
|
56
|
+
npm run typecheck
|
|
51
57
|
npm run pack:check
|
|
52
58
|
```
|
package/dist/index.js
CHANGED
|
@@ -1179,6 +1179,26 @@ var TokenUsageEstimator = class {
|
|
|
1179
1179
|
}
|
|
1180
1180
|
};
|
|
1181
1181
|
|
|
1182
|
+
// src/provider/core/debug_log.ts
|
|
1183
|
+
import { appendFileSync, writeFileSync } from "fs";
|
|
1184
|
+
var LOG_PATH = "/tmp/duo-debug.log";
|
|
1185
|
+
function duoLog(...args) {
|
|
1186
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
1187
|
+
const line = ts + " " + args.map(
|
|
1188
|
+
(a) => typeof a === "object" && a !== null ? JSON.stringify(a) : String(a)
|
|
1189
|
+
).join(" ");
|
|
1190
|
+
try {
|
|
1191
|
+
appendFileSync(LOG_PATH, line + "\n");
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function duoLogReset() {
|
|
1196
|
+
try {
|
|
1197
|
+
writeFileSync(LOG_PATH, "");
|
|
1198
|
+
} catch {
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1182
1202
|
// src/provider/application/model.ts
|
|
1183
1203
|
var GitLabDuoAgenticLanguageModel = class {
|
|
1184
1204
|
specificationVersion = "v2";
|
|
@@ -1225,6 +1245,8 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1225
1245
|
const workflowType = "chat";
|
|
1226
1246
|
const promptText = extractLastUserText(options.prompt);
|
|
1227
1247
|
const toolResults = extractToolResults(options.prompt);
|
|
1248
|
+
duoLogReset();
|
|
1249
|
+
duoLog("doStream", "hasStarted=" + this.#runtime.hasStarted, "prompt=" + (promptText?.slice(0, 60) ?? "null"), "toolResults=" + toolResults.length);
|
|
1228
1250
|
this.#runtime.resetMapperState();
|
|
1229
1251
|
if (!this.#runtime.hasStarted) {
|
|
1230
1252
|
this.#sentToolCallIds.clear();
|
|
@@ -1242,6 +1264,7 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1242
1264
|
const mcpTools = this.#options.enableMcp === false ? [] : buildMcpTools(options);
|
|
1243
1265
|
const toolContext = buildToolContext(mcpTools);
|
|
1244
1266
|
const isNewUserMessage = promptText != null && promptText !== this.#lastSentPrompt;
|
|
1267
|
+
duoLog("gate", "isNewUserMessage=" + isNewUserMessage, "freshTools=" + freshToolResults.length, "lastSent=" + (this.#lastSentPrompt?.slice(0, 40) ?? "null"));
|
|
1245
1268
|
let sentToolResults = false;
|
|
1246
1269
|
if (freshToolResults.length > 0) {
|
|
1247
1270
|
for (const result of freshToolResults) {
|
|
@@ -1349,6 +1372,7 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1349
1372
|
}
|
|
1350
1373
|
});
|
|
1351
1374
|
}
|
|
1375
|
+
duoLog("sendStartRequest", "hasStarted=" + this.#runtime.hasStarted);
|
|
1352
1376
|
this.#runtime.sendStartRequest(
|
|
1353
1377
|
promptText,
|
|
1354
1378
|
workflowType,
|
|
@@ -1361,6 +1385,8 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1361
1385
|
for (const ctx of extraContext) {
|
|
1362
1386
|
if (ctx.content) this.#usageEstimator.addInputChars(ctx.content);
|
|
1363
1387
|
}
|
|
1388
|
+
} else {
|
|
1389
|
+
duoLog("SKIP startRequest", "sentToolResults=" + sentToolResults, "isNewUserMessage=" + isNewUserMessage);
|
|
1364
1390
|
}
|
|
1365
1391
|
const iterator = this.#mapEventsToStream(this.#runtime.getEventStream());
|
|
1366
1392
|
const stream = asyncIteratorToReadableStream(iterator);
|
|
@@ -1374,9 +1400,12 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1374
1400
|
async *#mapEventsToStream(events) {
|
|
1375
1401
|
const state = { textStarted: false };
|
|
1376
1402
|
const estimator = this.#usageEstimator;
|
|
1403
|
+
let eventCount = 0;
|
|
1377
1404
|
yield { type: "stream-start", warnings: [] };
|
|
1378
1405
|
try {
|
|
1379
1406
|
for await (const event of events) {
|
|
1407
|
+
eventCount++;
|
|
1408
|
+
duoLog("evt", event.type, eventCount);
|
|
1380
1409
|
if (event.type === "TEXT_CHUNK") {
|
|
1381
1410
|
if (event.content.length > 0) {
|
|
1382
1411
|
estimator.addOutputChars(event.content);
|
|
@@ -1422,9 +1451,11 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1422
1451
|
}
|
|
1423
1452
|
}
|
|
1424
1453
|
} catch (streamErr) {
|
|
1454
|
+
duoLog("streamErr", streamErr instanceof Error ? streamErr.message : String(streamErr));
|
|
1425
1455
|
yield { type: "error", error: streamErr instanceof Error ? streamErr : new Error(String(streamErr)) };
|
|
1426
1456
|
return;
|
|
1427
1457
|
}
|
|
1458
|
+
duoLog("finish", "events=" + eventCount);
|
|
1428
1459
|
yield { type: "finish", finishReason: "stop", usage: this.#currentUsage };
|
|
1429
1460
|
}
|
|
1430
1461
|
// ---------------------------------------------------------------------------
|
|
@@ -1871,7 +1902,9 @@ var GitLabAgenticRuntime = class {
|
|
|
1871
1902
|
// Connection lifecycle
|
|
1872
1903
|
// ---------------------------------------------------------------------------
|
|
1873
1904
|
async ensureConnected(goal, workflowType) {
|
|
1905
|
+
duoLog("ensureConnected", "stream=" + !!this.#stream, "wfId=" + !!this.#workflowId, "queue=" + !!this.#queue);
|
|
1874
1906
|
if (this.#stream && this.#workflowId && this.#queue) {
|
|
1907
|
+
duoLog("ensureConnected", "skip (connected)");
|
|
1875
1908
|
return;
|
|
1876
1909
|
}
|
|
1877
1910
|
if (!this.#containerParams) {
|
|
@@ -1879,6 +1912,9 @@ var GitLabAgenticRuntime = class {
|
|
|
1879
1912
|
}
|
|
1880
1913
|
if (!this.#workflowId) {
|
|
1881
1914
|
this.#workflowId = await this.#createWorkflow(goal, workflowType);
|
|
1915
|
+
duoLog("workflow created", this.#workflowId);
|
|
1916
|
+
} else {
|
|
1917
|
+
duoLog("workflow reuse", this.#workflowId);
|
|
1882
1918
|
}
|
|
1883
1919
|
const token = await this.#dependencies.workflowService.getWorkflowToken(
|
|
1884
1920
|
this.#options.instanceUrl,
|
|
@@ -1892,9 +1928,11 @@ var GitLabAgenticRuntime = class {
|
|
|
1892
1928
|
this.#queue = new AsyncQueue();
|
|
1893
1929
|
try {
|
|
1894
1930
|
await this.#connectWebSocket();
|
|
1931
|
+
duoLog("ws connected", "attempt=" + attempt);
|
|
1895
1932
|
return;
|
|
1896
1933
|
} catch (err2) {
|
|
1897
1934
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
1935
|
+
duoLog("ws error", msg);
|
|
1898
1936
|
if ((msg.includes("1013") || msg.includes("lock")) && attempt < MAX_LOCK_RETRIES) {
|
|
1899
1937
|
this.#resetStreamState();
|
|
1900
1938
|
await this.#dependencies.clock.sleep(LOCK_RETRY_DELAY_MS);
|
|
@@ -1939,7 +1977,8 @@ var GitLabAgenticRuntime = class {
|
|
|
1939
1977
|
preapproved_tools: preapprovedTools
|
|
1940
1978
|
}
|
|
1941
1979
|
};
|
|
1942
|
-
this.#stream.write(startRequest);
|
|
1980
|
+
const ok2 = this.#stream.write(startRequest);
|
|
1981
|
+
duoLog("startReq write=" + ok2, "wf=" + this.#workflowId);
|
|
1943
1982
|
this.#startRequestSent = true;
|
|
1944
1983
|
}
|
|
1945
1984
|
sendToolResponse(requestId, response, responseType) {
|
|
@@ -2028,12 +2067,14 @@ var GitLabAgenticRuntime = class {
|
|
|
2028
2067
|
#bindStream(stream, queue) {
|
|
2029
2068
|
const now = () => this.#dependencies.clock.now();
|
|
2030
2069
|
const closeWithError = (message) => {
|
|
2070
|
+
duoLog("streamErr", message);
|
|
2031
2071
|
queue.push({ type: "ERROR", message, timestamp: now() });
|
|
2032
2072
|
queue.close();
|
|
2033
2073
|
this.#resetStreamState();
|
|
2034
2074
|
};
|
|
2035
2075
|
const handleAction = async (action) => {
|
|
2036
2076
|
if (action.newCheckpoint) {
|
|
2077
|
+
duoLog("checkpoint", action.newCheckpoint.status);
|
|
2037
2078
|
const duoEvent = {
|
|
2038
2079
|
checkpoint: action.newCheckpoint.checkpoint,
|
|
2039
2080
|
errors: action.newCheckpoint.errors || [],
|
|
@@ -2041,6 +2082,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2041
2082
|
workflowStatus: action.newCheckpoint.status
|
|
2042
2083
|
};
|
|
2043
2084
|
const events = await this.#mapper.mapWorkflowEvent(duoEvent);
|
|
2085
|
+
duoLog("mapped", events.length, events.map((e) => e.type).join(","));
|
|
2044
2086
|
for (const event of events) {
|
|
2045
2087
|
queue.push(event);
|
|
2046
2088
|
}
|
|
@@ -2048,6 +2090,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2048
2090
|
}
|
|
2049
2091
|
const toolRequest = mapWorkflowActionToToolRequest(action);
|
|
2050
2092
|
if (toolRequest) {
|
|
2093
|
+
duoLog("toolReq", toolRequest.toolName);
|
|
2051
2094
|
queue.push({
|
|
2052
2095
|
type: "TOOL_REQUEST",
|
|
2053
2096
|
...toolRequest,
|
|
@@ -2066,6 +2109,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2066
2109
|
closeWithError(err2.message);
|
|
2067
2110
|
});
|
|
2068
2111
|
stream.on("end", () => {
|
|
2112
|
+
duoLog("stream end");
|
|
2069
2113
|
queue.close();
|
|
2070
2114
|
this.#resetStreamState();
|
|
2071
2115
|
});
|
|
@@ -2087,6 +2131,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2087
2131
|
this.#bindStream(stream, this.#queue);
|
|
2088
2132
|
}
|
|
2089
2133
|
#resetStreamState() {
|
|
2134
|
+
duoLog("reset", "wf=" + this.#workflowId);
|
|
2090
2135
|
this.#stream = void 0;
|
|
2091
2136
|
this.#queue = void 0;
|
|
2092
2137
|
this.#startRequestSent = false;
|