opencode-gitlab-dap 1.14.1 → 1.15.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/README.md +15 -12
- package/dist/index.cjs +269 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +281 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ The plugin provides a multi-round design workflow:
|
|
|
110
110
|
|
|
111
111
|
The vendored `flow_v2.json` schema from GitLab Rails powers client-side validation using `ajv`, catching errors before hitting the API.
|
|
112
112
|
|
|
113
|
-
###
|
|
113
|
+
### 35 Tools
|
|
114
114
|
|
|
115
115
|
#### DAP Tools (20)
|
|
116
116
|
|
|
@@ -157,17 +157,20 @@ Say **"bootstrap project memory"** to automatically inspect a project and build
|
|
|
157
157
|
|
|
158
158
|
##### Skill Tools
|
|
159
159
|
|
|
160
|
-
| Tool | Description
|
|
161
|
-
| ----------------------- |
|
|
162
|
-
| `gitlab_skill_list` | List skills with auto-rebuilding index
|
|
163
|
-
| `gitlab_skill_load` | Load a skill (SKILL page +
|
|
164
|
-
| `gitlab_skill_save` | Create/update a skill with required description for index
|
|
165
|
-
| `gitlab_skill_promote` | Promote a draft skill to published (moves all pages)
|
|
166
|
-
| `gitlab_skill_discover` | Search group wiki for
|
|
167
|
-
| `gitlab_skill_install` |
|
|
168
|
-
| `
|
|
169
|
-
|
|
170
|
-
|
|
160
|
+
| Tool | Description |
|
|
161
|
+
| ----------------------- | ------------------------------------------------------------- |
|
|
162
|
+
| `gitlab_skill_list` | List skills with auto-rebuilding index |
|
|
163
|
+
| `gitlab_skill_load` | Load a skill (SKILL page + references + snippet info) |
|
|
164
|
+
| `gitlab_skill_save` | Create/update a skill with required description for index |
|
|
165
|
+
| `gitlab_skill_promote` | Promote a draft skill to published (moves all pages) |
|
|
166
|
+
| `gitlab_skill_discover` | Search group wiki and skills.sh for available skills |
|
|
167
|
+
| `gitlab_skill_install` | Install from group wiki or skills.sh (wiki + snippet) |
|
|
168
|
+
| `gitlab_skill_setup` | Extract skill to `.agents/skills/<name>/` for local execution |
|
|
169
|
+
| `gitlab_skill_delete` | Delete skill (wiki pages + snippet + index entry) |
|
|
170
|
+
|
|
171
|
+
**Hybrid storage:** SKILL.md and markdown references are stored in the wiki. Executable files
|
|
172
|
+
(`.py`, `.js`, `.sh`) are stored as a multi-file project snippet with extensions preserved.
|
|
173
|
+
Use `gitlab_skill_setup` to extract everything to `.agents/skills/<name>/` for local execution.
|
|
171
174
|
Skills are indexed in `agents/skills/index` for fast discovery.
|
|
172
175
|
All tools support project scope (default) and group scope (`scope="groups"`).
|
|
173
176
|
|
package/dist/index.cjs
CHANGED
|
@@ -3947,6 +3947,73 @@ ${e.content}`).join("\n\n---\n\n");
|
|
|
3947
3947
|
|
|
3948
3948
|
// src/tools/skill-tools.ts
|
|
3949
3949
|
var import_plugin6 = require("@opencode-ai/plugin");
|
|
3950
|
+
|
|
3951
|
+
// src/snippets.ts
|
|
3952
|
+
function snippetApi(instanceUrl, token, projectId) {
|
|
3953
|
+
const base = instanceUrl.replace(/\/$/, "");
|
|
3954
|
+
const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
|
|
3955
|
+
return {
|
|
3956
|
+
url: `${base}/api/v4/projects/${encoded}/snippets`,
|
|
3957
|
+
headers: {
|
|
3958
|
+
"Content-Type": "application/json",
|
|
3959
|
+
Authorization: `Bearer ${token}`
|
|
3960
|
+
}
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
async function handleResponse2(res) {
|
|
3964
|
+
if (!res.ok) {
|
|
3965
|
+
const text = await res.text();
|
|
3966
|
+
throw new Error(`Snippet API error (${res.status}): ${text}`);
|
|
3967
|
+
}
|
|
3968
|
+
return res.json();
|
|
3969
|
+
}
|
|
3970
|
+
async function createProjectSnippet(instanceUrl, token, projectId, title, description, files, visibility = "private") {
|
|
3971
|
+
const { url, headers } = snippetApi(instanceUrl, token, projectId);
|
|
3972
|
+
const res = await fetch(url, {
|
|
3973
|
+
method: "POST",
|
|
3974
|
+
headers,
|
|
3975
|
+
body: JSON.stringify({ title, description, visibility, files })
|
|
3976
|
+
});
|
|
3977
|
+
return handleResponse2(res);
|
|
3978
|
+
}
|
|
3979
|
+
async function deleteProjectSnippet(instanceUrl, token, projectId, snippetId) {
|
|
3980
|
+
const { url, headers } = snippetApi(instanceUrl, token, projectId);
|
|
3981
|
+
const res = await fetch(`${url}/${snippetId}`, { method: "DELETE", headers });
|
|
3982
|
+
if (!res.ok) {
|
|
3983
|
+
const text = await res.text();
|
|
3984
|
+
throw new Error(`Snippet API error (${res.status}): ${text}`);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
async function getSnippetFileRaw(instanceUrl, token, projectId, snippetId, filePath, ref = "main") {
|
|
3988
|
+
const base = instanceUrl.replace(/\/$/, "");
|
|
3989
|
+
const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
|
|
3990
|
+
const encodedPath = encodeURIComponent(filePath);
|
|
3991
|
+
const url = `${base}/api/v4/projects/${encoded}/snippets/${snippetId}/files/${ref}/${encodedPath}/raw`;
|
|
3992
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
3993
|
+
if (!res.ok) {
|
|
3994
|
+
const text = await res.text();
|
|
3995
|
+
throw new Error(`Snippet file API error (${res.status}): ${text}`);
|
|
3996
|
+
}
|
|
3997
|
+
return res.text();
|
|
3998
|
+
}
|
|
3999
|
+
async function listSnippetFiles(instanceUrl, token, projectId, snippetId) {
|
|
4000
|
+
const base = instanceUrl.replace(/\/$/, "");
|
|
4001
|
+
const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
|
|
4002
|
+
const url = `${base}/api/v4/projects/${encoded}/snippets/${snippetId}`;
|
|
4003
|
+
const res = await fetch(url, {
|
|
4004
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }
|
|
4005
|
+
});
|
|
4006
|
+
const snippet = await handleResponse2(res);
|
|
4007
|
+
if (snippet.files) {
|
|
4008
|
+
return snippet.files.map((f) => ({ path: f.path, raw_url: f.raw_url }));
|
|
4009
|
+
}
|
|
4010
|
+
if (snippet.file_name) {
|
|
4011
|
+
return [{ path: snippet.file_name, raw_url: snippet.raw_url }];
|
|
4012
|
+
}
|
|
4013
|
+
return [];
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
// src/tools/skill-tools.ts
|
|
3950
4017
|
var import_child_process = require("child_process");
|
|
3951
4018
|
var import_fs2 = require("fs");
|
|
3952
4019
|
var import_path2 = require("path");
|
|
@@ -3980,14 +4047,17 @@ function parseIndex(content) {
|
|
|
3980
4047
|
const rest = lines.slice(1).join("\n").trim();
|
|
3981
4048
|
const descLines = [];
|
|
3982
4049
|
let source;
|
|
4050
|
+
let snippetId;
|
|
3983
4051
|
for (const line of rest.split("\n")) {
|
|
3984
4052
|
if (line.startsWith("Source:")) {
|
|
3985
4053
|
source = line.slice(7).trim();
|
|
4054
|
+
} else if (line.startsWith("Snippet:")) {
|
|
4055
|
+
snippetId = parseInt(line.slice(8).trim(), 10) || void 0;
|
|
3986
4056
|
} else if (line.trim()) {
|
|
3987
4057
|
descLines.push(line);
|
|
3988
4058
|
}
|
|
3989
4059
|
}
|
|
3990
|
-
entries.push({ name, description: descLines.join("\n"), source, draft: false });
|
|
4060
|
+
entries.push({ name, description: descLines.join("\n"), source, snippetId, draft: false });
|
|
3991
4061
|
}
|
|
3992
4062
|
return entries;
|
|
3993
4063
|
}
|
|
@@ -3997,6 +4067,8 @@ function formatIndex(entries) {
|
|
|
3997
4067
|
${e.description}`;
|
|
3998
4068
|
if (e.source) block += `
|
|
3999
4069
|
Source: ${e.source}`;
|
|
4070
|
+
if (e.snippetId) block += `
|
|
4071
|
+
Snippet: ${e.snippetId}`;
|
|
4000
4072
|
return block;
|
|
4001
4073
|
}).join("\n\n");
|
|
4002
4074
|
}
|
|
@@ -4062,8 +4134,32 @@ async function rebuildIndex(instanceUrl, token, scope, id, prefix, indexSlug) {
|
|
|
4062
4134
|
dirty = true;
|
|
4063
4135
|
}
|
|
4064
4136
|
}
|
|
4137
|
+
const staleDescriptions = currentEntries.filter(
|
|
4138
|
+
(e) => actual.has(e.name) && (e.description.startsWith("(auto-indexed") || e.description.startsWith("(no description"))
|
|
4139
|
+
);
|
|
4140
|
+
if (staleDescriptions.length > 0) dirty = true;
|
|
4065
4141
|
if (!dirty && added.length === 0) return currentEntries;
|
|
4066
|
-
const kept =
|
|
4142
|
+
const kept = [];
|
|
4143
|
+
for (const e of currentEntries) {
|
|
4144
|
+
if (!actual.has(e.name)) continue;
|
|
4145
|
+
if (e.description.startsWith("(auto-indexed") || e.description.startsWith("(no description")) {
|
|
4146
|
+
let description = "";
|
|
4147
|
+
try {
|
|
4148
|
+
const page = await getWikiPage(instanceUrl, token, scope, id, `${prefix}/${e.name}/SKILL`);
|
|
4149
|
+
const content = page.content ?? "";
|
|
4150
|
+
const fmMatch = content.match(/^---\s*\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/);
|
|
4151
|
+
if (fmMatch) {
|
|
4152
|
+
description = fmMatch[1].trim();
|
|
4153
|
+
} else {
|
|
4154
|
+
description = content.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim().slice(0, 200);
|
|
4155
|
+
}
|
|
4156
|
+
} catch {
|
|
4157
|
+
}
|
|
4158
|
+
kept.push({ ...e, description: description || e.description });
|
|
4159
|
+
} else {
|
|
4160
|
+
kept.push(e);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4067
4163
|
for (const name of added) {
|
|
4068
4164
|
let description = "";
|
|
4069
4165
|
try {
|
|
@@ -4093,6 +4189,19 @@ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
|
|
|
4093
4189
|
await createWikiPage(instanceUrl, token, scope, id, slug, content);
|
|
4094
4190
|
}
|
|
4095
4191
|
}
|
|
4192
|
+
function isMarkdownFile(path) {
|
|
4193
|
+
return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
|
|
4194
|
+
}
|
|
4195
|
+
function ensureGitignore(dir) {
|
|
4196
|
+
const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
|
|
4197
|
+
if ((0, import_fs2.existsSync)(gitignorePath)) {
|
|
4198
|
+
const content = (0, import_fs2.readFileSync)(gitignorePath, "utf-8");
|
|
4199
|
+
if (content.includes(".agents/") || content.includes(".agents")) return;
|
|
4200
|
+
(0, import_fs2.writeFileSync)(gitignorePath, content.trimEnd() + "\n.agents/\n");
|
|
4201
|
+
} else {
|
|
4202
|
+
(0, import_fs2.writeFileSync)(gitignorePath, ".agents/\n");
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4096
4205
|
function searchSkillsSh(query) {
|
|
4097
4206
|
try {
|
|
4098
4207
|
const raw = (0, import_child_process.execSync)(`npx skills find ${JSON.stringify(query)}`, {
|
|
@@ -4252,8 +4361,23 @@ ${page.content}` : page.content;
|
|
|
4252
4361
|
|
|
4253
4362
|
---
|
|
4254
4363
|
Available references: ${refs.join(", ")}`;
|
|
4364
|
+
}
|
|
4365
|
+
const indexSlug = isDraft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4366
|
+
const indexEntries = await readIndex(
|
|
4367
|
+
auth.instanceUrl,
|
|
4368
|
+
auth.token,
|
|
4369
|
+
scope,
|
|
4370
|
+
id,
|
|
4371
|
+
indexSlug
|
|
4372
|
+
);
|
|
4373
|
+
const entry = indexEntries.find((e) => e.name === args.name);
|
|
4374
|
+
if (entry?.snippetId) {
|
|
4255
4375
|
result += `
|
|
4256
|
-
|
|
4376
|
+
|
|
4377
|
+
---
|
|
4378
|
+
This skill has executable scripts in snippet #${entry.snippetId}.`;
|
|
4379
|
+
result += `
|
|
4380
|
+
Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skills/${args.name}/ for execution.`;
|
|
4257
4381
|
}
|
|
4258
4382
|
return result;
|
|
4259
4383
|
} catch {
|
|
@@ -4411,29 +4535,42 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
|
|
|
4411
4535
|
if (!downloaded) {
|
|
4412
4536
|
return `Failed to download skill "${args.name}" from skills.sh. Check that the identifier is correct (e.g., "owner/repo@skill-name").`;
|
|
4413
4537
|
}
|
|
4414
|
-
const skillSlug = `${targetPrefix}/${downloaded.name}/SKILL`;
|
|
4415
4538
|
try {
|
|
4416
4539
|
await upsertPage(
|
|
4417
4540
|
auth.instanceUrl,
|
|
4418
4541
|
auth.token,
|
|
4419
4542
|
projectScope.scope,
|
|
4420
4543
|
projectScope.id,
|
|
4421
|
-
|
|
4544
|
+
`${targetPrefix}/${downloaded.name}/SKILL`,
|
|
4422
4545
|
downloaded.content
|
|
4423
4546
|
);
|
|
4424
|
-
let
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4547
|
+
let wikiCount = 1;
|
|
4548
|
+
const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
|
|
4549
|
+
const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
|
|
4550
|
+
for (const file of mdFiles) {
|
|
4551
|
+
const slug = `${targetPrefix}/${downloaded.name}/references/${file.path.replace(/\.[^.]+$/, "")}`;
|
|
4428
4552
|
await upsertPage(
|
|
4429
4553
|
auth.instanceUrl,
|
|
4430
4554
|
auth.token,
|
|
4431
4555
|
projectScope.scope,
|
|
4432
4556
|
projectScope.id,
|
|
4433
|
-
|
|
4557
|
+
slug,
|
|
4434
4558
|
file.content
|
|
4435
4559
|
);
|
|
4436
|
-
|
|
4560
|
+
wikiCount++;
|
|
4561
|
+
}
|
|
4562
|
+
let snippetId;
|
|
4563
|
+
if (scriptFiles.length > 0) {
|
|
4564
|
+
const snippet = await createProjectSnippet(
|
|
4565
|
+
auth.instanceUrl,
|
|
4566
|
+
auth.token,
|
|
4567
|
+
args.project_id,
|
|
4568
|
+
`skill:${downloaded.name}`,
|
|
4569
|
+
`Scripts for skill "${downloaded.name}" (installed from skills.sh:${args.name})`,
|
|
4570
|
+
scriptFiles.map((f) => ({ file_path: f.path, content: f.content })),
|
|
4571
|
+
"private"
|
|
4572
|
+
);
|
|
4573
|
+
snippetId = snippet.id;
|
|
4437
4574
|
}
|
|
4438
4575
|
await upsertIndexEntry(
|
|
4439
4576
|
auth.instanceUrl,
|
|
@@ -4445,10 +4582,13 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
|
|
|
4445
4582
|
name: downloaded.name,
|
|
4446
4583
|
description: downloaded.description,
|
|
4447
4584
|
source: `skills.sh:${args.name}`,
|
|
4585
|
+
snippetId,
|
|
4448
4586
|
draft: !!args.draft
|
|
4449
4587
|
}
|
|
4450
4588
|
);
|
|
4451
|
-
|
|
4589
|
+
const parts = [`${wikiCount} wiki page(s)`];
|
|
4590
|
+
if (snippetId) parts.push(`snippet #${snippetId} with ${scriptFiles.length} script(s)`);
|
|
4591
|
+
return `Installed skill "${downloaded.name}" from skills.sh. ${parts.join(", ")}. Use gitlab_skill_setup to extract scripts to disk.`;
|
|
4452
4592
|
} catch (err) {
|
|
4453
4593
|
return `Error installing skill from skills.sh: ${err.message}`;
|
|
4454
4594
|
}
|
|
@@ -4510,8 +4650,106 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
|
|
|
4510
4650
|
}
|
|
4511
4651
|
}
|
|
4512
4652
|
}),
|
|
4653
|
+
gitlab_skill_setup: (0, import_plugin6.tool)({
|
|
4654
|
+
description: "Extract a skill to the local .agents/skills/ directory for execution.\nDownloads SKILL.md from wiki and script files from the associated snippet.\nWrites to .agents/skills/<name>/ and ensures .agents/ is in .gitignore.\nAfter setup, OpenCode will auto-discover the skill from the local directory.",
|
|
4655
|
+
args: {
|
|
4656
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4657
|
+
name: z6.string().describe("Skill name to set up locally"),
|
|
4658
|
+
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
4659
|
+
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
4660
|
+
},
|
|
4661
|
+
execute: async (args) => {
|
|
4662
|
+
const auth = authAndValidate(args.project_id);
|
|
4663
|
+
const { scope, id } = resolveScope2(args);
|
|
4664
|
+
const targetDir = (0, import_path2.join)(process.cwd(), ".agents", "skills", args.name);
|
|
4665
|
+
try {
|
|
4666
|
+
let skillContent = null;
|
|
4667
|
+
for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
|
|
4668
|
+
try {
|
|
4669
|
+
const page = await getWikiPage(
|
|
4670
|
+
auth.instanceUrl,
|
|
4671
|
+
auth.token,
|
|
4672
|
+
scope,
|
|
4673
|
+
id,
|
|
4674
|
+
`${prefix}/${args.name}/SKILL`
|
|
4675
|
+
);
|
|
4676
|
+
skillContent = page.content;
|
|
4677
|
+
break;
|
|
4678
|
+
} catch {
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
if (!skillContent) {
|
|
4682
|
+
return `Skill "${args.name}" not found in wiki. Use gitlab_skill_list to see available skills.`;
|
|
4683
|
+
}
|
|
4684
|
+
(0, import_fs2.mkdirSync)(targetDir, { recursive: true });
|
|
4685
|
+
(0, import_fs2.writeFileSync)((0, import_path2.join)(targetDir, "SKILL.md"), skillContent);
|
|
4686
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
|
|
4687
|
+
let refCount = 0;
|
|
4688
|
+
for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
|
|
4689
|
+
const refPrefix = `${prefix}/${args.name}/references/`;
|
|
4690
|
+
const refPages = pages.filter(
|
|
4691
|
+
(p) => p.slug.startsWith(refPrefix) && p.content
|
|
4692
|
+
);
|
|
4693
|
+
for (const ref of refPages) {
|
|
4694
|
+
const refName = ref.slug.slice(refPrefix.length);
|
|
4695
|
+
const refDir = (0, import_path2.join)(targetDir, "references");
|
|
4696
|
+
(0, import_fs2.mkdirSync)(refDir, { recursive: true });
|
|
4697
|
+
(0, import_fs2.writeFileSync)((0, import_path2.join)(refDir, `${refName}.md`), ref.content);
|
|
4698
|
+
refCount++;
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
let scriptCount = 0;
|
|
4702
|
+
const indexEntries = await readIndex(
|
|
4703
|
+
auth.instanceUrl,
|
|
4704
|
+
auth.token,
|
|
4705
|
+
scope,
|
|
4706
|
+
id,
|
|
4707
|
+
SKILLS_INDEX
|
|
4708
|
+
);
|
|
4709
|
+
const draftsEntries = await readIndex(
|
|
4710
|
+
auth.instanceUrl,
|
|
4711
|
+
auth.token,
|
|
4712
|
+
scope,
|
|
4713
|
+
id,
|
|
4714
|
+
DRAFTS_INDEX
|
|
4715
|
+
);
|
|
4716
|
+
const entry = [...indexEntries, ...draftsEntries].find((e) => e.name === args.name);
|
|
4717
|
+
if (entry?.snippetId) {
|
|
4718
|
+
const files = await listSnippetFiles(
|
|
4719
|
+
auth.instanceUrl,
|
|
4720
|
+
auth.token,
|
|
4721
|
+
args.project_id,
|
|
4722
|
+
entry.snippetId
|
|
4723
|
+
);
|
|
4724
|
+
for (const file of files) {
|
|
4725
|
+
const filePath = (0, import_path2.join)(targetDir, file.path);
|
|
4726
|
+
(0, import_fs2.mkdirSync)((0, import_path2.dirname)(filePath), { recursive: true });
|
|
4727
|
+
const content = await getSnippetFileRaw(
|
|
4728
|
+
auth.instanceUrl,
|
|
4729
|
+
auth.token,
|
|
4730
|
+
args.project_id,
|
|
4731
|
+
entry.snippetId,
|
|
4732
|
+
file.path
|
|
4733
|
+
);
|
|
4734
|
+
(0, import_fs2.writeFileSync)(filePath, content);
|
|
4735
|
+
if (file.path.endsWith(".sh")) {
|
|
4736
|
+
(0, import_fs2.chmodSync)(filePath, 493);
|
|
4737
|
+
}
|
|
4738
|
+
scriptCount++;
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
ensureGitignore(process.cwd());
|
|
4742
|
+
const parts = ["SKILL.md"];
|
|
4743
|
+
if (refCount > 0) parts.push(`${refCount} reference(s)`);
|
|
4744
|
+
if (scriptCount > 0) parts.push(`${scriptCount} script(s)`);
|
|
4745
|
+
return `Skill "${args.name}" extracted to ${targetDir} (${parts.join(", ")}). OpenCode will auto-discover it on next session.`;
|
|
4746
|
+
} catch (err) {
|
|
4747
|
+
return `Error setting up skill: ${err.message}`;
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
}),
|
|
4513
4751
|
gitlab_skill_delete: (0, import_plugin6.tool)({
|
|
4514
|
-
description: "Delete a skill and remove it from the index.\nRemoves all pages under the skill directory
|
|
4752
|
+
description: "Delete a skill and remove it from the index.\nRemoves all wiki pages under the skill directory and any associated snippet.\nWiki and snippet git history preserves deleted content.",
|
|
4515
4753
|
args: {
|
|
4516
4754
|
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4517
4755
|
name: z6.string().describe("Skill name to delete"),
|
|
@@ -4534,8 +4772,25 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
|
|
|
4534
4772
|
for (const page of skillPages) {
|
|
4535
4773
|
await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
|
|
4536
4774
|
}
|
|
4775
|
+
const indexEntries = await readIndex(auth.instanceUrl, auth.token, scope, id, indexSlug);
|
|
4776
|
+
const entry = indexEntries.find((e) => e.name === args.name);
|
|
4777
|
+
let snippetDeleted = false;
|
|
4778
|
+
if (entry?.snippetId) {
|
|
4779
|
+
try {
|
|
4780
|
+
await deleteProjectSnippet(
|
|
4781
|
+
auth.instanceUrl,
|
|
4782
|
+
auth.token,
|
|
4783
|
+
args.project_id,
|
|
4784
|
+
entry.snippetId
|
|
4785
|
+
);
|
|
4786
|
+
snippetDeleted = true;
|
|
4787
|
+
} catch {
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4537
4790
|
await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
|
|
4538
|
-
|
|
4791
|
+
const parts = [`${skillPages.length} wiki page(s)`];
|
|
4792
|
+
if (snippetDeleted) parts.push("snippet");
|
|
4793
|
+
return `Deleted skill "${args.name}" (${parts.join(" + ")} removed).`;
|
|
4539
4794
|
} catch (err) {
|
|
4540
4795
|
return `Error deleting skill: ${err.message}`;
|
|
4541
4796
|
}
|