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.
- package/README.md +6 -42
- package/dist/index.js +441 -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
20
|
+
## Authentication
|
|
40
21
|
|
|
41
|
-
|
|
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:
|
|
1961
|
+
system: bridgedPrompt
|
|
1524
1962
|
}
|
|
1525
1963
|
}
|
|
1526
1964
|
]
|