opencode-gitlab-duo-agentic 0.3.0 → 0.3.5

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.
Files changed (3) hide show
  1. package/README.md +6 -42
  2. package/dist/index.js +441 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,16 +1,10 @@
1
1
  # opencode-gitlab-duo-agentic
2
2
 
3
- OpenCode plugin for GitLab Duo Agentic. It registers the provider, discovers models from the GitLab API, and exposes file-reading tools.
3
+ OpenCode plugin for GitLab Duo Agentic. Registers a provider that routes models through the Duo Agentic Workflow Service, enabling native tool calling via WebSocket sessions.
4
4
 
5
5
  ## Setup
6
6
 
7
- 1. Export your GitLab token:
8
-
9
- ```bash
10
- export GITLAB_TOKEN=glpat-...
11
- ```
12
-
13
- 2. Add the plugin to `opencode.json`:
7
+ Add the plugin to your `opencode.json`:
14
8
 
15
9
  ```json
16
10
  {
@@ -19,40 +13,10 @@ export GITLAB_TOKEN=glpat-...
19
13
  }
20
14
  ```
21
15
 
22
- 3. Run `opencode`. The provider, models, and tools are registered automatically.
23
-
24
- `GITLAB_INSTANCE_URL` defaults to `https://gitlab.com`. Set it only for self-managed GitLab.
25
-
26
- ## Provider options
27
-
28
- Override defaults in `opencode.json` under `provider.gitlab-duo-agentic.options`.
16
+ Run `opencode`. The provider and models are registered automatically.
29
17
 
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 |
18
+ For self-managed GitLab, set `GITLAB_INSTANCE_URL`.
38
19
 
39
- ## Model discovery
20
+ ## Authentication
40
21
 
41
- Models are discovered in this order:
42
-
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
48
-
49
- Cache is stored in `~/.cache/opencode/` (or `XDG_CACHE_HOME`). Override TTL with `GITLAB_DUO_MODELS_CACHE_TTL` (seconds).
50
-
51
- ## Development
52
-
53
- ```bash
54
- npm install
55
- npm run build
56
- npm run typecheck
57
- npm run pack:check
58
- ```
22
+ Authentication is managed by `@gitlab/opencode-gitlab-auth`, which is natively integrated into OpenCode. Run `/connect`, select GitLab, and choose OAuth or Personal Access Token. No additional setup is required.
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/plugin/hooks.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
1
4
  // src/constants.ts
2
5
  var PROVIDER_ID = "gitlab";
3
6
  var DEFAULT_MODEL_ID = "duo-chat-sonnet-4-5";
@@ -345,6 +348,26 @@ function toModelsConfig(available) {
345
348
  // src/plugin/hooks.ts
346
349
  async function createPluginHooks(input) {
347
350
  return {
351
+ tool: {
352
+ todoread: tool({
353
+ description: "Use this tool to read your todo list",
354
+ args: {},
355
+ async execute(_args, ctx) {
356
+ await ctx.ask({
357
+ permission: "todoread",
358
+ patterns: ["*"],
359
+ always: ["*"],
360
+ metadata: {}
361
+ });
362
+ const response = await input.client.session.todo({
363
+ path: { id: ctx.sessionID },
364
+ throwOnError: true
365
+ });
366
+ const payload = response.data ?? [];
367
+ return JSON.stringify(payload, null, 2);
368
+ }
369
+ })
370
+ },
348
371
  config: async (config) => applyRuntimeConfig(config, input.directory),
349
372
  "chat.message": async ({ sessionID }, { parts }) => {
350
373
  const text2 = parts.filter((p) => p.type === "text" && !("synthetic" in p && p.synthetic)).map((p) => "text" in p ? p.text : "").join(" ").trim();
@@ -382,7 +405,6 @@ function isUtilityAgent(agent) {
382
405
  return UTILITY_AGENTS.has(name);
383
406
  }
384
407
  function isGitLabProvider(model) {
385
- if (model.api?.npm === "opencode-gitlab-duo-agentic") return true;
386
408
  if (model.api?.npm === "opencode-gitlab-duo-agentic") return true;
387
409
  if (model.providerID === "gitlab" && model.api?.npm !== "@gitlab/gitlab-ai-provider") return true;
388
410
  return model.providerID.toLowerCase().includes("gitlab-duo");
@@ -1240,6 +1262,14 @@ function isPlainObject(value) {
1240
1262
  }
1241
1263
 
1242
1264
  // src/provider/tool-mapping.ts
1265
+ var TODO_WRITE_PROGRAM = "__todo_write__";
1266
+ var TODO_READ_PROGRAM = "__todo_read__";
1267
+ var WEBFETCH_PROGRAM = "__webfetch__";
1268
+ var QUESTION_PROGRAM = "__question__";
1269
+ var SKILL_PROGRAM = "__skill__";
1270
+ var TODO_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed", "cancelled"]);
1271
+ var TODO_PRIORITIES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
1272
+ var WEBFETCH_FORMATS = /* @__PURE__ */ new Set(["text", "markdown", "html"]);
1243
1273
  function mapDuoToolRequest(toolName, args) {
1244
1274
  switch (toolName) {
1245
1275
  case "list_dir": {
@@ -1295,17 +1325,38 @@ function mapDuoToolRequest(toolName, args) {
1295
1325
  case "shell_command": {
1296
1326
  const command = asString2(args.command);
1297
1327
  if (!command) return { toolName, args };
1328
+ const bridged = mapBridgeCommand(command);
1329
+ if (bridged) return bridged;
1298
1330
  return { toolName: "bash", args: { command, description: "Run shell command", workdir: "." } };
1299
1331
  }
1300
1332
  case "run_command": {
1333
+ const command = asString2(args.command);
1334
+ if (command) {
1335
+ const bridged = mapBridgeCommand(command);
1336
+ if (bridged) return bridged;
1337
+ }
1301
1338
  const program = asString2(args.program);
1339
+ if (program === TODO_READ_PROGRAM) {
1340
+ return { toolName: "todoread", args: {} };
1341
+ }
1342
+ if (program === TODO_WRITE_PROGRAM) {
1343
+ return mapTodoWriteCall(args.arguments);
1344
+ }
1345
+ if (program === WEBFETCH_PROGRAM) {
1346
+ return mapWebfetchCall(args.arguments);
1347
+ }
1348
+ if (program === QUESTION_PROGRAM) {
1349
+ return mapQuestionCall(args.arguments);
1350
+ }
1351
+ if (program === SKILL_PROGRAM) {
1352
+ return mapSkillCall(args.arguments);
1353
+ }
1302
1354
  if (program) {
1303
1355
  const parts = [shellQuote(program)];
1304
1356
  if (Array.isArray(args.flags)) parts.push(...args.flags.map((f) => shellQuote(String(f))));
1305
1357
  if (Array.isArray(args.arguments)) parts.push(...args.arguments.map((a) => shellQuote(String(a))));
1306
1358
  return { toolName: "bash", args: { command: parts.join(" "), description: "Run command", workdir: "." } };
1307
1359
  }
1308
- const command = asString2(args.command);
1309
1360
  if (!command) return { toolName, args };
1310
1361
  return { toolName: "bash", args: { command, description: "Run command", workdir: "." } };
1311
1362
  }
@@ -1348,6 +1399,327 @@ function mapDuoToolRequest(toolName, args) {
1348
1399
  function asString2(value) {
1349
1400
  return typeof value === "string" ? value : void 0;
1350
1401
  }
1402
+ function mapBridgeCommand(command) {
1403
+ const normalized = command.trim();
1404
+ if (normalized === TODO_READ_PROGRAM) {
1405
+ return { toolName: "todoread", args: {} };
1406
+ }
1407
+ if (normalized.startsWith(`${TODO_READ_PROGRAM} `)) {
1408
+ return invalidTool("todoread", `${TODO_READ_PROGRAM} does not accept a payload`);
1409
+ }
1410
+ if (normalized === TODO_WRITE_PROGRAM) {
1411
+ return invalidTool("todowrite", `${TODO_WRITE_PROGRAM} expects JSON payload after command prefix`);
1412
+ }
1413
+ if (normalized === WEBFETCH_PROGRAM) {
1414
+ return invalidTool("webfetch", `${WEBFETCH_PROGRAM} expects JSON payload after command prefix`);
1415
+ }
1416
+ if (normalized === QUESTION_PROGRAM) {
1417
+ return invalidTool("question", `${QUESTION_PROGRAM} expects JSON payload after command prefix`);
1418
+ }
1419
+ if (normalized === SKILL_PROGRAM) {
1420
+ return invalidTool("skill", `${SKILL_PROGRAM} expects JSON payload after command prefix`);
1421
+ }
1422
+ if (normalized.startsWith(`${TODO_WRITE_PROGRAM} `)) {
1423
+ const payload = normalized.slice(TODO_WRITE_PROGRAM.length).trim();
1424
+ if (!payload) {
1425
+ return invalidTool("todowrite", `${TODO_WRITE_PROGRAM} expects JSON payload after command prefix`);
1426
+ }
1427
+ return mapTodoWritePayload(payload);
1428
+ }
1429
+ if (normalized.startsWith(`${WEBFETCH_PROGRAM} `)) {
1430
+ const payload = normalized.slice(WEBFETCH_PROGRAM.length).trim();
1431
+ if (!payload) {
1432
+ return invalidTool("webfetch", `${WEBFETCH_PROGRAM} expects JSON payload after command prefix`);
1433
+ }
1434
+ return mapWebfetchPayload(payload);
1435
+ }
1436
+ if (normalized.startsWith(`${QUESTION_PROGRAM} `)) {
1437
+ const payload = normalized.slice(QUESTION_PROGRAM.length).trim();
1438
+ if (!payload) {
1439
+ return invalidTool("question", `${QUESTION_PROGRAM} expects JSON payload after command prefix`);
1440
+ }
1441
+ return mapQuestionPayload(payload);
1442
+ }
1443
+ if (normalized.startsWith(`${SKILL_PROGRAM} `)) {
1444
+ const payload = normalized.slice(SKILL_PROGRAM.length).trim();
1445
+ if (!payload) {
1446
+ return invalidTool("skill", `${SKILL_PROGRAM} expects JSON payload after command prefix`);
1447
+ }
1448
+ return mapSkillPayload(payload);
1449
+ }
1450
+ return null;
1451
+ }
1452
+ function mapTodoWriteCall(rawArguments) {
1453
+ const payloadResult = parsePayloadFromArguments(rawArguments, TODO_WRITE_PROGRAM);
1454
+ if ("error" in payloadResult) return invalidTool("todowrite", payloadResult.error);
1455
+ return mapTodoWritePayload(payloadResult.payload);
1456
+ }
1457
+ function mapWebfetchCall(rawArguments) {
1458
+ const payloadResult = parsePayloadFromArguments(rawArguments, WEBFETCH_PROGRAM);
1459
+ if ("error" in payloadResult) return invalidTool("webfetch", payloadResult.error);
1460
+ return mapWebfetchPayload(payloadResult.payload);
1461
+ }
1462
+ function mapQuestionCall(rawArguments) {
1463
+ const payloadResult = parsePayloadFromArguments(rawArguments, QUESTION_PROGRAM);
1464
+ if ("error" in payloadResult) return invalidTool("question", payloadResult.error);
1465
+ return mapQuestionPayload(payloadResult.payload);
1466
+ }
1467
+ function mapSkillCall(rawArguments) {
1468
+ const payloadResult = parsePayloadFromArguments(rawArguments, SKILL_PROGRAM);
1469
+ if ("error" in payloadResult) return invalidTool("skill", payloadResult.error);
1470
+ return mapSkillPayload(payloadResult.payload);
1471
+ }
1472
+ function mapTodoWritePayload(rawPayload) {
1473
+ const payloadResult = parseTodoPayload(rawPayload);
1474
+ if ("error" in payloadResult) return invalidTool("todowrite", payloadResult.error);
1475
+ const todosResult = parseTodos(payloadResult.payload);
1476
+ if ("error" in todosResult) return invalidTool("todowrite", todosResult.error);
1477
+ return {
1478
+ toolName: "todowrite",
1479
+ args: {
1480
+ todos: todosResult.todos
1481
+ }
1482
+ };
1483
+ }
1484
+ function mapWebfetchPayload(rawPayload) {
1485
+ const payloadResult = parseWebfetchPayload(rawPayload);
1486
+ if ("error" in payloadResult) return invalidTool("webfetch", payloadResult.error);
1487
+ return {
1488
+ toolName: "webfetch",
1489
+ args: payloadResult.args
1490
+ };
1491
+ }
1492
+ function mapQuestionPayload(rawPayload) {
1493
+ const payloadResult = parseQuestionPayload(rawPayload);
1494
+ if ("error" in payloadResult) return invalidTool("question", payloadResult.error);
1495
+ return {
1496
+ toolName: "question",
1497
+ args: payloadResult.args
1498
+ };
1499
+ }
1500
+ function mapSkillPayload(rawPayload) {
1501
+ const payloadResult = parseSkillPayload(rawPayload);
1502
+ if ("error" in payloadResult) return invalidTool("skill", payloadResult.error);
1503
+ return {
1504
+ toolName: "skill",
1505
+ args: payloadResult.args
1506
+ };
1507
+ }
1508
+ function parsePayloadFromArguments(rawArguments, program) {
1509
+ if (!Array.isArray(rawArguments) || rawArguments.length === 0) {
1510
+ return {
1511
+ error: `${program} expects JSON payload in arguments[0]`
1512
+ };
1513
+ }
1514
+ const rawPayload = rawArguments[0];
1515
+ if (typeof rawPayload !== "string") {
1516
+ return {
1517
+ error: `${program} expects arguments[0] to be a JSON string`
1518
+ };
1519
+ }
1520
+ return { payload: rawPayload };
1521
+ }
1522
+ function parseTodoPayload(rawPayload) {
1523
+ return parseObjectPayload(rawPayload, TODO_WRITE_PROGRAM);
1524
+ }
1525
+ function parseObjectPayload(rawPayload, program) {
1526
+ const normalized = unwrapWrappingQuotes(rawPayload);
1527
+ try {
1528
+ const parsed = JSON.parse(normalized);
1529
+ if (!isRecord(parsed)) {
1530
+ return {
1531
+ error: `${program} payload must be a JSON object`
1532
+ };
1533
+ }
1534
+ return { payload: parsed };
1535
+ } catch {
1536
+ return {
1537
+ error: `${program} payload is not valid JSON`
1538
+ };
1539
+ }
1540
+ }
1541
+ function parseWebfetchPayload(rawPayload) {
1542
+ const payloadResult = parseObjectPayload(rawPayload, WEBFETCH_PROGRAM);
1543
+ if ("error" in payloadResult) return payloadResult;
1544
+ const url = asString2(payloadResult.payload.url);
1545
+ if (!url) {
1546
+ return {
1547
+ error: `${WEBFETCH_PROGRAM} payload must include a url string`
1548
+ };
1549
+ }
1550
+ const args = { url };
1551
+ const format = asString2(payloadResult.payload.format);
1552
+ if (format !== void 0) {
1553
+ if (!WEBFETCH_FORMATS.has(format)) {
1554
+ return {
1555
+ error: `${WEBFETCH_PROGRAM} format must be one of: text, markdown, html`
1556
+ };
1557
+ }
1558
+ args.format = format;
1559
+ }
1560
+ const timeout = payloadResult.payload.timeout;
1561
+ if (timeout !== void 0) {
1562
+ if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) {
1563
+ return {
1564
+ error: `${WEBFETCH_PROGRAM} timeout must be a positive number`
1565
+ };
1566
+ }
1567
+ args.timeout = timeout;
1568
+ }
1569
+ return { args };
1570
+ }
1571
+ function parseQuestionPayload(rawPayload) {
1572
+ const payloadResult = parseObjectPayload(rawPayload, QUESTION_PROGRAM);
1573
+ if ("error" in payloadResult) return payloadResult;
1574
+ const rawQuestions = payloadResult.payload.questions;
1575
+ if (!Array.isArray(rawQuestions) || rawQuestions.length === 0) {
1576
+ return {
1577
+ error: `${QUESTION_PROGRAM} payload must include a non-empty questions array`
1578
+ };
1579
+ }
1580
+ const questions = [];
1581
+ for (let i = 0; i < rawQuestions.length; i += 1) {
1582
+ const rawQuestion = rawQuestions[i];
1583
+ if (!isRecord(rawQuestion)) {
1584
+ return {
1585
+ error: `${QUESTION_PROGRAM} questions[${i}] must be an object`
1586
+ };
1587
+ }
1588
+ const question = asString2(rawQuestion.question);
1589
+ if (!question) {
1590
+ return {
1591
+ error: `${QUESTION_PROGRAM} questions[${i}].question must be a string`
1592
+ };
1593
+ }
1594
+ const header = asString2(rawQuestion.header);
1595
+ if (!header) {
1596
+ return {
1597
+ error: `${QUESTION_PROGRAM} questions[${i}].header must be a string`
1598
+ };
1599
+ }
1600
+ const rawOptions = rawQuestion.options;
1601
+ if (!Array.isArray(rawOptions) || rawOptions.length === 0) {
1602
+ return {
1603
+ error: `${QUESTION_PROGRAM} questions[${i}].options must be a non-empty array`
1604
+ };
1605
+ }
1606
+ const options = [];
1607
+ for (let j = 0; j < rawOptions.length; j += 1) {
1608
+ const rawOption = rawOptions[j];
1609
+ if (!isRecord(rawOption)) {
1610
+ return {
1611
+ error: `${QUESTION_PROGRAM} questions[${i}].options[${j}] must be an object`
1612
+ };
1613
+ }
1614
+ const label = asString2(rawOption.label);
1615
+ if (!label) {
1616
+ return {
1617
+ error: `${QUESTION_PROGRAM} questions[${i}].options[${j}].label must be a string`
1618
+ };
1619
+ }
1620
+ const description = asString2(rawOption.description);
1621
+ if (!description) {
1622
+ return {
1623
+ error: `${QUESTION_PROGRAM} questions[${i}].options[${j}].description must be a string`
1624
+ };
1625
+ }
1626
+ options.push({ label, description });
1627
+ }
1628
+ const mappedQuestion = { question, header, options };
1629
+ if (rawQuestion.multiple !== void 0) {
1630
+ if (typeof rawQuestion.multiple !== "boolean") {
1631
+ return {
1632
+ error: `${QUESTION_PROGRAM} questions[${i}].multiple must be a boolean`
1633
+ };
1634
+ }
1635
+ mappedQuestion.multiple = rawQuestion.multiple;
1636
+ }
1637
+ questions.push(mappedQuestion);
1638
+ }
1639
+ return {
1640
+ args: {
1641
+ questions
1642
+ }
1643
+ };
1644
+ }
1645
+ function parseSkillPayload(rawPayload) {
1646
+ const payloadResult = parseObjectPayload(rawPayload, SKILL_PROGRAM);
1647
+ if ("error" in payloadResult) return payloadResult;
1648
+ const name = asString2(payloadResult.payload.name)?.trim();
1649
+ if (!name) {
1650
+ return {
1651
+ error: `${SKILL_PROGRAM} payload must include a name string`
1652
+ };
1653
+ }
1654
+ return {
1655
+ args: {
1656
+ name
1657
+ }
1658
+ };
1659
+ }
1660
+ function unwrapWrappingQuotes(value) {
1661
+ const trimmed = value.trim();
1662
+ if (trimmed.length < 2) return trimmed;
1663
+ const first = trimmed[0];
1664
+ const last = trimmed[trimmed.length - 1];
1665
+ if (first === last && (first === "'" || first === '"')) {
1666
+ return trimmed.slice(1, -1);
1667
+ }
1668
+ return trimmed;
1669
+ }
1670
+ function parseTodos(payload) {
1671
+ const rawTodos = payload.todos;
1672
+ if (!Array.isArray(rawTodos)) {
1673
+ return {
1674
+ error: `${TODO_WRITE_PROGRAM} payload must include a todos array`
1675
+ };
1676
+ }
1677
+ const todos = [];
1678
+ for (let index = 0; index < rawTodos.length; index += 1) {
1679
+ const rawTodo = rawTodos[index];
1680
+ if (!isRecord(rawTodo)) {
1681
+ return {
1682
+ error: `${TODO_WRITE_PROGRAM} todos[${index}] must be an object`
1683
+ };
1684
+ }
1685
+ const content = asString2(rawTodo.content);
1686
+ if (content === void 0) {
1687
+ return {
1688
+ error: `${TODO_WRITE_PROGRAM} todos[${index}].content must be a string`
1689
+ };
1690
+ }
1691
+ const status = asString2(rawTodo.status);
1692
+ if (!status || !TODO_STATUSES.has(status)) {
1693
+ return {
1694
+ error: `${TODO_WRITE_PROGRAM} todos[${index}].status must be one of: pending, in_progress, completed, cancelled`
1695
+ };
1696
+ }
1697
+ const priority = asString2(rawTodo.priority);
1698
+ if (!priority || !TODO_PRIORITIES.has(priority)) {
1699
+ return {
1700
+ error: `${TODO_WRITE_PROGRAM} todos[${index}].priority must be one of: high, medium, low`
1701
+ };
1702
+ }
1703
+ todos.push({
1704
+ content,
1705
+ status,
1706
+ priority
1707
+ });
1708
+ }
1709
+ return { todos };
1710
+ }
1711
+ function invalidTool(tool2, error) {
1712
+ return {
1713
+ toolName: "invalid",
1714
+ args: {
1715
+ tool: tool2,
1716
+ error
1717
+ }
1718
+ };
1719
+ }
1720
+ function isRecord(value) {
1721
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1722
+ }
1351
1723
  function asStringArray(value) {
1352
1724
  if (!Array.isArray(value)) return [];
1353
1725
  return value.filter((v) => typeof v === "string");
@@ -1501,7 +1873,73 @@ var TOOL_ALLOWLIST = [
1501
1873
  "mkdir",
1502
1874
  "run_command"
1503
1875
  ];
1876
+ var BRIDGE_WHEN_TO_USE = [
1877
+ "Bridge tools \u2014 when to use:",
1878
+ "",
1879
+ "Todo (__todo_read__, __todo_write__):",
1880
+ "- Use for planning and tracking when the task has 3+ steps, multiple files, or multiple user requirements.",
1881
+ "- Keep exactly one todo in_progress at a time.",
1882
+ "- Mark tasks completed immediately after finishing each step.",
1883
+ "- Update the todo list when scope changes or new requirements appear.",
1884
+ "- Skip for trivial one-step requests.",
1885
+ "",
1886
+ "Web fetch (__webfetch__):",
1887
+ "- Use only when external URL content is needed and repo/local context is insufficient.",
1888
+ "- Prefer targeted URLs and the minimum needed format.",
1889
+ "- Do not use for local files or codebase exploration.",
1890
+ "",
1891
+ "Question (__question__):",
1892
+ "- Use only when blocked by missing user input that materially changes the implementation.",
1893
+ "- Complete all non-blocked work first, then ask.",
1894
+ "- Ask exactly one targeted question unless multiple answers are strictly required together.",
1895
+ "- If a safe low-risk default exists, proceed without asking and state the assumption.",
1896
+ "- Must ask for: credentials/secrets, destructive/irreversible actions, production/billing/security impact, ambiguity where different answers produce different implementations.",
1897
+ '- Do not ask for: permission-style prompts ("Should I proceed?"), trivial preferences, questions answerable from repo/docs/context.',
1898
+ "",
1899
+ "Skill (__skill__):",
1900
+ "- Use when the task clearly matches an available skill and specialized instructions are needed.",
1901
+ "- Load the matching skill once, then follow its workflow.",
1902
+ "- Do not use if no available skill clearly matches."
1903
+ ].join("\n");
1904
+ var BRIDGE_HOW_TO_USE = [
1905
+ "Bridge tools \u2014 how to use:",
1906
+ "",
1907
+ "All bridge tools are called via run_command with a single command string.",
1908
+ "",
1909
+ "Read todos:",
1910
+ " __todo_read__",
1911
+ "Write/update todos:",
1912
+ ' __todo_write__ {"todos":[{"content":"...","status":"pending|in_progress|completed|cancelled","priority":"high|medium|low"}]}',
1913
+ "Fetch web content:",
1914
+ ' __webfetch__ {"url":"https://example.com","format":"markdown","timeout":30}',
1915
+ "Ask user question(s):",
1916
+ ' __question__ {"questions":[{"question":"...","header":"...","options":[{"label":"Option A","description":"..."}],"multiple":false}]}',
1917
+ "Load a skill:",
1918
+ ' __skill__ {"name":"skill-name"}'
1919
+ ].join("\n");
1920
+ var BRIDGE_FORMATTING_RULES = [
1921
+ "Bridge tools \u2014 formatting and validation:",
1922
+ "",
1923
+ "Payloads:",
1924
+ "- Use strict JSON with double quotes in all payloads.",
1925
+ "- If validation fails, correct the JSON and retry.",
1926
+ "- Do not use regular shell commands for bridge operations.",
1927
+ "",
1928
+ "Question formatting:",
1929
+ "- Provide 2-5 concrete options.",
1930
+ "- Labels: 1-5 words, concise.",
1931
+ "- Header: max 30 chars.",
1932
+ '- Put recommended option first; append "(Recommended)" to its label.',
1933
+ "- Set multiple=true only when selecting more than one is valid.",
1934
+ '- Custom free-text is enabled by default; do not add generic "Other" options.'
1935
+ ].join("\n");
1504
1936
  function buildFlowConfig(systemPrompt) {
1937
+ const bridgedPrompt = [
1938
+ systemPrompt.trim(),
1939
+ BRIDGE_WHEN_TO_USE,
1940
+ BRIDGE_HOW_TO_USE,
1941
+ BRIDGE_FORMATTING_RULES
1942
+ ].filter(Boolean).join("\n\n");
1505
1943
  return {
1506
1944
  version: "v1",
1507
1945
  environment: "chat-partial",
@@ -1520,7 +1958,7 @@ function buildFlowConfig(systemPrompt) {
1520
1958
  prompt_id: "chat/agent",
1521
1959
  unit_primitives: ["duo_chat"],
1522
1960
  prompt_template: {
1523
- system: systemPrompt
1961
+ system: bridgedPrompt
1524
1962
  }
1525
1963
  }
1526
1964
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.3.0",
3
+ "version": "0.3.5",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",