opencode-gitlab-dap 1.16.3 → 1.16.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/dist/index.js CHANGED
@@ -3848,6 +3848,10 @@ async function listSnippetFiles(instanceUrl, token, projectId, snippetId) {
3848
3848
  }
3849
3849
 
3850
3850
  // src/tools/skill-tools.ts
3851
+ import { writeFileSync as writeFileSync2, mkdirSync, chmodSync } from "fs";
3852
+ import { join as join3, dirname } from "path";
3853
+
3854
+ // src/tools/skill-helpers.ts
3851
3855
  import { execSync } from "child_process";
3852
3856
  import {
3853
3857
  mkdtempSync,
@@ -3856,172 +3860,66 @@ import {
3856
3860
  readdirSync,
3857
3861
  statSync,
3858
3862
  writeFileSync,
3859
- mkdirSync,
3860
- existsSync,
3861
- chmodSync
3863
+ existsSync
3862
3864
  } from "fs";
3863
- import { join as join2, dirname } from "path";
3865
+ import { join as join2 } from "path";
3864
3866
  import { tmpdir } from "os";
3865
3867
  import { gzipSync, gunzipSync } from "zlib";
3866
- var z6 = tool6.schema;
3867
3868
  var PREFIX2 = "agents";
3868
3869
  var SKILLS_PREFIX = `${PREFIX2}/skills`;
3869
3870
  var DRAFTS_PREFIX = `${PREFIX2}/skills-drafts`;
3870
- var SKILLS_INDEX = `${SKILLS_PREFIX}/_registry`;
3871
- var DRAFTS_INDEX = `${DRAFTS_PREFIX}/_registry`;
3872
- var PROJECT_ID_DESC2 = "Project path from git remote";
3873
- function resolveScope2(args) {
3874
- if (args.scope === "groups" && args.group_id) {
3875
- return { scope: "groups", id: args.group_id };
3876
- }
3877
- return { scope: "projects", id: args.project_id };
3878
- }
3879
- function validateProjectId2(projectId) {
3880
- if (!projectId.includes("/")) {
3881
- return `Invalid project_id "${projectId}". Must be the full project path containing at least one slash (e.g., "my-group/my-project"), not just the project name.`;
3882
- }
3883
- return null;
3884
- }
3885
- function parseIndex(content) {
3886
- const entries = [];
3887
- const blocks = content.split(/^## /m).filter(Boolean);
3888
- for (const block of blocks) {
3889
- const lines = block.trim().split("\n");
3890
- const name = lines[0].trim();
3891
- if (!name) continue;
3892
- const rest = lines.slice(1).join("\n").trim();
3893
- const descLines = [];
3894
- let source;
3895
- let snippetId;
3896
- for (const line of rest.split("\n")) {
3897
- if (line.startsWith("Source:")) {
3898
- source = line.slice(7).trim();
3899
- } else if (line.startsWith("Snippet:")) {
3900
- snippetId = parseInt(line.slice(8).trim(), 10) || void 0;
3901
- } else if (line.trim()) {
3902
- descLines.push(line);
3903
- }
3904
- }
3905
- entries.push({ name, description: descLines.join("\n"), source, snippetId, draft: false });
3906
- }
3907
- return entries;
3908
- }
3909
- function formatIndex(entries) {
3910
- return entries.map((e) => {
3911
- let block = `## ${e.name}
3912
- ${e.description}`;
3913
- if (e.source) block += `
3914
- Source: ${e.source}`;
3915
- if (e.snippetId) block += `
3916
- Snippet: ${e.snippetId}`;
3917
- return block;
3918
- }).join("\n\n");
3919
- }
3920
- async function readIndex(instanceUrl, token, scope, id, indexSlug) {
3921
- try {
3922
- const page = await getWikiPage(instanceUrl, token, scope, id, indexSlug);
3923
- return parseIndex(page.content);
3924
- } catch {
3925
- return [];
3926
- }
3927
- }
3928
- async function sleep2(ms) {
3929
- return new Promise((resolve) => setTimeout(resolve, ms));
3930
- }
3931
- async function writeIndex(instanceUrl, token, scope, id, indexSlug, entries) {
3932
- const content = formatIndex(entries) || "# Skills Registry";
3933
- for (let attempt = 0; attempt < 3; attempt++) {
3934
- try {
3935
- await updateWikiPage(instanceUrl, token, scope, id, indexSlug, content);
3936
- return;
3937
- } catch (updateErr) {
3938
- const msg = updateErr.message ?? "";
3939
- if (msg.includes("not found") || msg.includes("404")) {
3940
- await createWikiPage(instanceUrl, token, scope, id, indexSlug, content);
3941
- return;
3942
- }
3943
- if (attempt < 2) {
3944
- await sleep2(1e3 * (attempt + 1));
3945
- continue;
3946
- }
3947
- throw updateErr;
3948
- }
3871
+ var EMPTY_FILE_SENTINEL = "__EMPTY_FILE__";
3872
+ function parseFrontmatter(content) {
3873
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
3874
+ if (!match) return { meta: {}, body: content };
3875
+ const meta = {};
3876
+ for (const line of match[1].split("\n")) {
3877
+ const [key, ...rest] = line.split(":");
3878
+ const val = rest.join(":").trim();
3879
+ if (!key || !val) continue;
3880
+ const k = key.trim();
3881
+ if (k === "name") meta.name = val;
3882
+ else if (k === "description") meta.description = val;
3883
+ else if (k === "source") meta.source = val;
3884
+ else if (k === "snippet") meta.snippetId = parseInt(val, 10) || void 0;
3949
3885
  }
3886
+ return { meta, body: match[2] };
3950
3887
  }
3951
- async function upsertIndexEntry(instanceUrl, token, scope, id, indexSlug, entry) {
3952
- const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
3953
- const idx = entries.findIndex((e) => e.name === entry.name);
3954
- if (idx >= 0) {
3955
- entries[idx] = entry;
3956
- } else {
3957
- entries.push(entry);
3958
- }
3959
- await writeIndex(instanceUrl, token, scope, id, indexSlug, entries);
3888
+ function formatFrontmatter(meta, body) {
3889
+ const lines = ["---", `name: ${meta.name}`, `description: ${meta.description}`];
3890
+ if (meta.source) lines.push(`source: ${meta.source}`);
3891
+ if (meta.snippetId) lines.push(`snippet: ${meta.snippetId}`);
3892
+ lines.push("---", "");
3893
+ return lines.join("\n") + body;
3960
3894
  }
3961
- async function removeIndexEntry(instanceUrl, token, scope, id, indexSlug, name) {
3962
- const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
3963
- const filtered = entries.filter((e) => e.name !== name);
3964
- if (filtered.length !== entries.length) {
3965
- await writeIndex(instanceUrl, token, scope, id, indexSlug, filtered);
3966
- }
3895
+ function extractSkillNameFromSlug(slug, prefix) {
3896
+ if (!slug.startsWith(prefix + "/") || !slug.endsWith("/SKILL")) return null;
3897
+ const middle = slug.slice(prefix.length + 1, -"/SKILL".length);
3898
+ return middle && !middle.includes("/") ? middle : null;
3967
3899
  }
3968
- async function upsertPage(instanceUrl, token, scope, id, slug, content) {
3969
- for (let attempt = 0; attempt < 3; attempt++) {
3970
- try {
3971
- await updateWikiPage(instanceUrl, token, scope, id, slug, content);
3972
- return;
3973
- } catch (updateErr) {
3974
- const msg = updateErr.message ?? "";
3975
- if (msg.includes("not found") || msg.includes("404")) {
3976
- try {
3977
- await createWikiPage(instanceUrl, token, scope, id, slug, content);
3978
- return;
3979
- } catch (createErr) {
3980
- if (attempt < 2 && (createErr.message?.includes("Duplicate") || createErr.message?.includes("reference"))) {
3981
- await sleep2(1e3 * (attempt + 1));
3982
- continue;
3983
- }
3984
- throw createErr;
3985
- }
3986
- }
3987
- if (attempt < 2) {
3988
- await sleep2(1e3 * (attempt + 1));
3989
- continue;
3990
- }
3991
- throw updateErr;
3992
- }
3993
- }
3900
+ function isMarkdownFile(path) {
3901
+ return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
3994
3902
  }
3995
- var EMPTY_FILE_SENTINEL = "__EMPTY_FILE__";
3996
3903
  function packFiles(files) {
3997
3904
  const manifest = files.map((f) => ({
3998
3905
  path: f.path,
3999
3906
  content: f.content.trim() ? f.content : EMPTY_FILE_SENTINEL
4000
3907
  }));
4001
- const json = JSON.stringify(manifest);
4002
- return gzipSync(Buffer.from(json, "utf-8")).toString("base64");
3908
+ return gzipSync(Buffer.from(JSON.stringify(manifest), "utf-8")).toString("base64");
4003
3909
  }
4004
3910
  function unpackFiles(packed) {
4005
3911
  const json = gunzipSync(Buffer.from(packed, "base64")).toString("utf-8");
4006
- const manifest = JSON.parse(json);
4007
- return manifest.map((f) => ({
3912
+ return JSON.parse(json).map((f) => ({
4008
3913
  path: f.path,
4009
3914
  content: f.content === EMPTY_FILE_SENTINEL ? "" : f.content
4010
3915
  }));
4011
3916
  }
4012
- function isMarkdownFile(path) {
4013
- return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
4014
- }
4015
- function isAgentsGitignored(dir) {
3917
+ function ensureGitignore(dir) {
4016
3918
  try {
4017
3919
  execSync("git check-ignore -q .agents", { cwd: dir, stdio: "pipe" });
4018
- return true;
3920
+ return;
4019
3921
  } catch {
4020
- return false;
4021
3922
  }
4022
- }
4023
- function ensureGitignore(dir) {
4024
- if (isAgentsGitignored(dir)) return;
4025
3923
  const gitignorePath = join2(dir, ".gitignore");
4026
3924
  if (existsSync(gitignorePath)) {
4027
3925
  const content = readFileSync2(gitignorePath, "utf-8");
@@ -4074,10 +3972,9 @@ function downloadSkillFromSkillsSh(identifier) {
4074
3972
  if (dirs.length === 0) return null;
4075
3973
  const skillName = dirs[0];
4076
3974
  const skillDir = join2(agentsDir, skillName);
4077
- const skillMd = join2(skillDir, "SKILL.md");
4078
3975
  let mainContent;
4079
3976
  try {
4080
- mainContent = readFileSync2(skillMd, "utf-8");
3977
+ mainContent = readFileSync2(join2(skillDir, "SKILL.md"), "utf-8");
4081
3978
  } catch {
4082
3979
  return null;
4083
3980
  }
@@ -4086,8 +3983,7 @@ function downloadSkillFromSkillsSh(identifier) {
4086
3983
  if (descMatch) {
4087
3984
  description = descMatch[1].trim();
4088
3985
  } else {
4089
- const firstParagraph = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim();
4090
- description = firstParagraph.slice(0, 200);
3986
+ description = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim().slice(0, 200);
4091
3987
  }
4092
3988
  const files = [];
4093
3989
  const walkStack = [{ dir: skillDir, prefix: "" }];
@@ -4116,6 +4012,50 @@ function downloadSkillFromSkillsSh(identifier) {
4116
4012
  }
4117
4013
  }
4118
4014
  }
4015
+
4016
+ // src/tools/skill-tools.ts
4017
+ var z6 = tool6.schema;
4018
+ var PROJECT_ID_DESC2 = "Project path from git remote";
4019
+ function resolveScope2(args) {
4020
+ if (args.scope === "groups" && args.group_id) {
4021
+ return { scope: "groups", id: args.group_id };
4022
+ }
4023
+ return { scope: "projects", id: args.project_id };
4024
+ }
4025
+ function validateProjectId2(projectId) {
4026
+ if (!projectId.includes("/")) {
4027
+ return `Invalid project_id "${projectId}". Must be the full project path containing at least one slash.`;
4028
+ }
4029
+ return null;
4030
+ }
4031
+ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
4032
+ try {
4033
+ await updateWikiPage(instanceUrl, token, scope, id, slug, content);
4034
+ } catch (err) {
4035
+ if (err.message?.includes("not found") || err.message?.includes("404")) {
4036
+ await createWikiPage(instanceUrl, token, scope, id, slug, content);
4037
+ return;
4038
+ }
4039
+ throw err;
4040
+ }
4041
+ }
4042
+ async function listSkills(instanceUrl, token, scope, id, prefix) {
4043
+ const pages = await listWikiPages(instanceUrl, token, scope, id, true);
4044
+ const skills = [];
4045
+ for (const page of pages) {
4046
+ const name = extractSkillNameFromSlug(page.slug, prefix);
4047
+ if (!name || !page.content) continue;
4048
+ const { meta } = parseFrontmatter(page.content);
4049
+ skills.push({
4050
+ name: meta.name ?? name,
4051
+ description: meta.description ?? "",
4052
+ source: meta.source,
4053
+ snippetId: meta.snippetId,
4054
+ draft: prefix === DRAFTS_PREFIX
4055
+ });
4056
+ }
4057
+ return skills;
4058
+ }
4119
4059
  function makeSkillTools(ctx) {
4120
4060
  function authAndValidate(projectId) {
4121
4061
  const auth = ctx.ensureAuth();
@@ -4126,7 +4066,7 @@ function makeSkillTools(ctx) {
4126
4066
  }
4127
4067
  return {
4128
4068
  gitlab_skill_list: tool6({
4129
- description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).",
4069
+ description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks.",
4130
4070
  args: {
4131
4071
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4132
4072
  include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
@@ -4137,12 +4077,16 @@ function makeSkillTools(ctx) {
4137
4077
  const auth = authAndValidate(args.project_id);
4138
4078
  const { scope, id } = resolveScope2(args);
4139
4079
  try {
4140
- const published = await readIndex(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX);
4080
+ const published = await listSkills(
4081
+ auth.instanceUrl,
4082
+ auth.token,
4083
+ scope,
4084
+ id,
4085
+ SKILLS_PREFIX
4086
+ );
4141
4087
  let drafts = [];
4142
4088
  if (args.include_drafts) {
4143
- drafts = (await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX)).map(
4144
- (e) => ({ ...e, draft: true })
4145
- );
4089
+ drafts = await listSkills(auth.instanceUrl, auth.token, scope, id, DRAFTS_PREFIX);
4146
4090
  }
4147
4091
  const all = [...published, ...drafts];
4148
4092
  if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
@@ -4153,18 +4097,17 @@ function makeSkillTools(ctx) {
4153
4097
  }
4154
4098
  }),
4155
4099
  gitlab_skill_load: tool6({
4156
- description: "Load a specific skill by name.\nSkills contain step-by-step instructions for common tasks.\nChecks published skills first, then falls back to draft skills.\nReturns the SKILL content and lists available reference pages.",
4100
+ description: "Load a specific skill by name.\nChecks published skills first, then falls back to draft skills.\nReturns the SKILL content and lists available reference pages.",
4157
4101
  args: {
4158
4102
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4159
- name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
4103
+ name: z6.string().describe('Skill name (e.g., "incident-retro")'),
4160
4104
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4161
4105
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4162
4106
  },
4163
4107
  execute: async (args) => {
4164
4108
  const auth = authAndValidate(args.project_id);
4165
4109
  const { scope, id } = resolveScope2(args);
4166
- const prefixes = [SKILLS_PREFIX, DRAFTS_PREFIX];
4167
- for (const prefix of prefixes) {
4110
+ for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4168
4111
  try {
4169
4112
  const page = await getWikiPage(
4170
4113
  auth.instanceUrl,
@@ -4173,36 +4116,28 @@ function makeSkillTools(ctx) {
4173
4116
  id,
4174
4117
  `${prefix}/${args.name}/SKILL`
4175
4118
  );
4119
+ const { meta, body } = parseFrontmatter(page.content);
4120
+ const isDraft = prefix === DRAFTS_PREFIX;
4176
4121
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4177
4122
  const skillPrefix = `${prefix}/${args.name}/`;
4178
- const skillSlug = `${prefix}/${args.name}/SKILL`;
4179
- const refs = pages.filter((p) => p.slug.startsWith(skillPrefix) && p.slug !== skillSlug).map((p) => p.slug.slice(skillPrefix.length));
4180
- const isDraft = prefix === DRAFTS_PREFIX;
4123
+ const refs = pages.filter(
4124
+ (p) => p.slug.startsWith(skillPrefix) && p.slug !== `${prefix}/${args.name}/SKILL`
4125
+ ).map((p) => p.slug.slice(skillPrefix.length));
4181
4126
  let result = isDraft ? `[DRAFT SKILL]
4182
4127
 
4183
- ${page.content}` : page.content;
4128
+ ${body}` : body;
4184
4129
  if (refs.length > 0) {
4185
4130
  result += `
4186
4131
 
4187
4132
  ---
4188
4133
  Available references: ${refs.join(", ")}`;
4189
4134
  }
4190
- const indexSlug = isDraft ? DRAFTS_INDEX : SKILLS_INDEX;
4191
- const indexEntries = await readIndex(
4192
- auth.instanceUrl,
4193
- auth.token,
4194
- scope,
4195
- id,
4196
- indexSlug
4197
- );
4198
- const entry = indexEntries.find((e) => e.name === args.name);
4199
- if (entry?.snippetId) {
4135
+ if (meta.snippetId) {
4200
4136
  result += `
4201
4137
 
4202
- ---
4203
- This skill has executable scripts in snippet #${entry.snippetId}.`;
4138
+ This skill has executable scripts in snippet #${meta.snippetId}.`;
4204
4139
  result += `
4205
- Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skills/${args.name}/ for execution.`;
4140
+ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them locally.`;
4206
4141
  }
4207
4142
  return result;
4208
4143
  } catch {
@@ -4213,13 +4148,13 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4213
4148
  }
4214
4149
  }),
4215
4150
  gitlab_skill_save: tool6({
4216
- description: "Create or update a skill.\nSkills define step-by-step procedures for common tasks.\nUse draft=true for skills that haven't been proven yet.\nUpdates the skill index with the provided description.",
4151
+ description: "Create or update a skill.\nUse draft=true for skills that haven't been proven yet.",
4217
4152
  args: {
4218
4153
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4219
4154
  name: z6.string().describe('Skill name (e.g., "incident-retro")'),
4220
4155
  content: z6.string().describe("Skill content in markdown"),
4221
- description: z6.string().describe("Short description for the skill index (1-2 sentences)"),
4222
- draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
4156
+ description: z6.string().describe("Short description (1-2 sentences)"),
4157
+ draft: z6.boolean().optional().describe("Save as draft (default: false)"),
4223
4158
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4224
4159
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4225
4160
  },
@@ -4227,31 +4162,25 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4227
4162
  const auth = authAndValidate(args.project_id);
4228
4163
  const { scope, id } = resolveScope2(args);
4229
4164
  const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4230
- const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4231
4165
  const slug = `${prefix}/${args.name}/SKILL`;
4232
- const label = args.draft ? "draft " : "";
4166
+ const body = formatFrontmatter(
4167
+ { name: args.name, description: args.description, source: "project" },
4168
+ args.content
4169
+ );
4233
4170
  try {
4234
- await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
4235
- await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, {
4236
- name: args.name,
4237
- description: args.description,
4238
- source: "project",
4239
- draft: !!args.draft
4240
- });
4241
- return `Saved ${label}skill: ${args.name}`;
4171
+ await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, body);
4172
+ return `Saved ${args.draft ? "draft " : ""}skill: ${args.name}`;
4242
4173
  } catch (err) {
4243
4174
  return `Error saving skill: ${err.message}`;
4244
4175
  }
4245
4176
  }
4246
4177
  }),
4247
4178
  gitlab_skill_promote: tool6({
4248
- description: "Promote a skill.\nDefault (target='published'): moves a draft skill to published within the same scope.\nTarget 'group': copies a published project skill to the group wiki, making it available to all projects in the group.",
4179
+ description: "Promote a skill.\nDefault (target='published'): moves a draft to published.\nTarget 'group': moves a project skill to the group wiki.",
4249
4180
  args: {
4250
4181
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4251
4182
  name: z6.string().describe("Skill name to promote"),
4252
- target: z6.enum(["published", "group"]).optional().describe(
4253
- 'Promotion target: "published" (default, draft\u2192published) or "group" (project\u2192group wiki)'
4254
- ),
4183
+ target: z6.enum(["published", "group"]).optional().describe('"published" (default) or "group"'),
4255
4184
  group_id: z6.string().optional().describe("Group path (required when target is group)"),
4256
4185
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)")
4257
4186
  },
@@ -4259,9 +4188,7 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4259
4188
  const auth = authAndValidate(args.project_id);
4260
4189
  const promotionTarget = args.target ?? "published";
4261
4190
  if (promotionTarget === "group") {
4262
- if (!args.group_id) {
4263
- return 'Error: group_id is required when target is "group".';
4264
- }
4191
+ if (!args.group_id) return 'Error: group_id is required when target is "group".';
4265
4192
  const projectScope = resolveScope2(args);
4266
4193
  try {
4267
4194
  const pages = await listWikiPages(
@@ -4272,11 +4199,9 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4272
4199
  true
4273
4200
  );
4274
4201
  const skillPrefix = `${SKILLS_PREFIX}/${args.name}/`;
4275
- const skillPages = pages.filter(
4276
- (p) => p.slug.startsWith(skillPrefix) && p.content
4277
- );
4202
+ const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix) && p.content);
4278
4203
  if (skillPages.length === 0) {
4279
- return `Skill "${args.name}" not found in project. Use gitlab_skill_list to see available skills.`;
4204
+ return `Skill "${args.name}" not found in project.`;
4280
4205
  }
4281
4206
  for (const page of skillPages) {
4282
4207
  await upsertPage(
@@ -4288,54 +4213,6 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4288
4213
  page.content
4289
4214
  );
4290
4215
  }
4291
- const projectIndex = await readIndex(
4292
- auth.instanceUrl,
4293
- auth.token,
4294
- projectScope.scope,
4295
- projectScope.id,
4296
- SKILLS_INDEX
4297
- );
4298
- const entry = projectIndex.find((e) => e.name === args.name);
4299
- const description = entry?.description ?? "(promoted from project)";
4300
- if (entry?.snippetId) {
4301
- const bundleFiles = await listSnippetFiles(
4302
- auth.instanceUrl,
4303
- auth.token,
4304
- args.project_id,
4305
- entry.snippetId
4306
- );
4307
- for (const bf of bundleFiles) {
4308
- const raw = await getSnippetFileRaw(
4309
- auth.instanceUrl,
4310
- auth.token,
4311
- args.project_id,
4312
- entry.snippetId,
4313
- bf.path
4314
- );
4315
- await upsertPage(
4316
- auth.instanceUrl,
4317
- auth.token,
4318
- "groups",
4319
- args.group_id,
4320
- `${SKILLS_PREFIX}/${args.name}/bundle/${bf.path}`,
4321
- raw
4322
- );
4323
- }
4324
- }
4325
- await upsertIndexEntry(
4326
- auth.instanceUrl,
4327
- auth.token,
4328
- "groups",
4329
- args.group_id,
4330
- SKILLS_INDEX,
4331
- {
4332
- name: args.name,
4333
- description,
4334
- source: `project:${args.project_id}`,
4335
- snippetId: entry?.snippetId,
4336
- draft: false
4337
- }
4338
- );
4339
4216
  for (const page of skillPages) {
4340
4217
  await deleteWikiPage(
4341
4218
  auth.instanceUrl,
@@ -4345,55 +4222,30 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4345
4222
  page.slug
4346
4223
  );
4347
4224
  }
4348
- if (entry?.snippetId) {
4349
- try {
4350
- await deleteProjectSnippet(
4351
- auth.instanceUrl,
4352
- auth.token,
4353
- args.project_id,
4354
- entry.snippetId
4355
- );
4356
- } catch {
4357
- }
4358
- }
4359
- await removeIndexEntry(
4360
- auth.instanceUrl,
4361
- auth.token,
4362
- projectScope.scope,
4363
- projectScope.id,
4364
- SKILLS_INDEX,
4365
- args.name
4366
- );
4367
- return `Promoted skill "${args.name}" to group "${args.group_id}". ${skillPages.length} page(s) moved (removed from project).`;
4225
+ return `Promoted skill "${args.name}" to group "${args.group_id}". ${skillPages.length} page(s) moved.`;
4368
4226
  } catch (err) {
4369
4227
  return `Error promoting skill to group: ${err.message}`;
4370
4228
  }
4371
4229
  }
4372
4230
  const { scope, id } = resolveScope2(args);
4373
4231
  try {
4374
- const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
4375
- const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
4376
- const draftPages = pages.filter(
4377
- (p) => p.slug.startsWith(draftPrefix) && p.content
4232
+ const pages = await listWikiPages(
4233
+ auth.instanceUrl,
4234
+ auth.token,
4235
+ scope,
4236
+ id,
4237
+ true
4378
4238
  );
4239
+ const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
4240
+ const draftPages = pages.filter((p) => p.slug.startsWith(draftPrefix) && p.content);
4379
4241
  if (draftPages.length === 0) {
4380
- return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
4242
+ return `Draft skill "${args.name}" not found.`;
4381
4243
  }
4382
- const draftIndex = await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX);
4383
- const entry = draftIndex.find((e) => e.name === args.name);
4384
- const description = entry?.description ?? "(promoted from draft)";
4385
4244
  for (const page of draftPages) {
4386
4245
  const newSlug = page.slug.replace(DRAFTS_PREFIX, SKILLS_PREFIX);
4387
4246
  await upsertPage(auth.instanceUrl, auth.token, scope, id, newSlug, page.content);
4388
4247
  await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4389
4248
  }
4390
- await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX, args.name);
4391
- await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX, {
4392
- name: args.name,
4393
- description,
4394
- source: "project",
4395
- draft: false
4396
- });
4397
4249
  return `Promoted skill "${args.name}" from draft to published.`;
4398
4250
  } catch (err) {
4399
4251
  return `Error promoting skill: ${err.message}`;
@@ -4401,11 +4253,11 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4401
4253
  }
4402
4254
  }),
4403
4255
  gitlab_skill_discover: tool6({
4404
- description: "Search for skills in the skills.sh public registry and optionally a group wiki.\nUse gitlab_skill_install to install a discovered skill into your project.\nIMPORTANT: project_id is only needed when searching a group wiki. For skills.sh only, it is optional.",
4256
+ description: "Search for skills in skills.sh and optionally a group wiki.\nUse gitlab_skill_install to install a discovered skill.",
4405
4257
  args: {
4406
- query: z6.string().describe("Search query (matches skill name and description)"),
4407
- project_id: z6.string().optional().describe(PROJECT_ID_DESC2 + " Only needed for group wiki search."),
4408
- group_id: z6.string().optional().describe("Group path to search for shared skills (optional)")
4258
+ query: z6.string().describe("Search query"),
4259
+ project_id: z6.string().optional().describe(PROJECT_ID_DESC2 + " Only needed for group search."),
4260
+ group_id: z6.string().optional().describe("Group path to search")
4409
4261
  },
4410
4262
  execute: async (args) => {
4411
4263
  let auth = null;
@@ -4417,15 +4269,15 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4417
4269
  const sections = [];
4418
4270
  if (args.group_id && auth) {
4419
4271
  try {
4420
- const entries = await readIndex(
4272
+ const skills = await listSkills(
4421
4273
  auth.instanceUrl,
4422
4274
  auth.token,
4423
4275
  "groups",
4424
4276
  args.group_id,
4425
- SKILLS_INDEX
4277
+ SKILLS_PREFIX
4426
4278
  );
4427
4279
  const q = args.query.toLowerCase();
4428
- const matches = entries.filter(
4280
+ const matches = skills.filter(
4429
4281
  (e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
4430
4282
  );
4431
4283
  if (matches.length > 0) {
@@ -4454,19 +4306,17 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4454
4306
  );
4455
4307
  }
4456
4308
  if (sections.length === 0) {
4457
- return `No skills found matching "${args.query}" in ${args.group_id ? "group wiki or " : ""}skills.sh.`;
4309
+ return `No skills found matching "${args.query}".`;
4458
4310
  }
4459
4311
  return sections.join("\n\n---\n\n");
4460
4312
  }
4461
4313
  }),
4462
4314
  gitlab_skill_install: tool6({
4463
- description: "Install a skill from a group wiki or skills.sh into the project wiki.\nFor group: copies all skill pages (SKILL + references) from the group.\nFor skills.sh: downloads via npx, extracts SKILL.md and files, writes to wiki.\nUpdates the project skill index with the installed skill.",
4315
+ description: "Install a skill from group wiki or skills.sh into the project wiki.\nMarkdown files go to wiki pages, scripts are bundled in a project snippet.",
4464
4316
  args: {
4465
4317
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4466
- name: z6.string().describe(
4467
- 'Skill identifier. For group: skill name (e.g., "incident-retro"). For skills.sh: full identifier (e.g., "vercel-labs/agent-skills@nextjs-developer").'
4468
- ),
4469
- source: z6.enum(["group", "skills.sh"]).describe('Where to install from: "group" (group wiki) or "skills.sh" (public registry)'),
4318
+ name: z6.string().describe("Skill identifier"),
4319
+ source: z6.enum(["group", "skills.sh"]).describe("Where to install from"),
4470
4320
  group_id: z6.string().optional().describe("Group path (required when source is group)"),
4471
4321
  draft: z6.boolean().optional().describe("Install as draft (default: false)")
4472
4322
  },
@@ -4474,24 +4324,45 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4474
4324
  const auth = authAndValidate(args.project_id);
4475
4325
  const projectScope = resolveScope2(args);
4476
4326
  const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4477
- const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4478
4327
  if (args.source === "skills.sh") {
4479
4328
  const downloaded = downloadSkillFromSkillsSh(args.name);
4480
4329
  if (!downloaded) {
4481
- return `Failed to download skill "${args.name}" from skills.sh. Check that the identifier is correct (e.g., "owner/repo@skill-name").`;
4330
+ return `Failed to download skill "${args.name}" from skills.sh.`;
4482
4331
  }
4483
4332
  try {
4333
+ const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
4334
+ const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
4335
+ let snippetId;
4336
+ if (scriptFiles.length > 0) {
4337
+ const snippet = await createProjectSnippet(
4338
+ auth.instanceUrl,
4339
+ auth.token,
4340
+ args.project_id,
4341
+ `skill:${downloaded.name}`,
4342
+ `Scripts for skill "${downloaded.name}"`,
4343
+ [{ file_path: `${downloaded.name}.bundle`, content: packFiles(scriptFiles) }],
4344
+ "private"
4345
+ );
4346
+ snippetId = snippet.id;
4347
+ }
4348
+ const skillBody = formatFrontmatter(
4349
+ {
4350
+ name: downloaded.name,
4351
+ description: downloaded.description,
4352
+ source: `skills.sh:${args.name}`,
4353
+ snippetId
4354
+ },
4355
+ downloaded.content.replace(/^---[\s\S]*?---\s*\n/, "")
4356
+ );
4484
4357
  await upsertPage(
4485
4358
  auth.instanceUrl,
4486
4359
  auth.token,
4487
4360
  projectScope.scope,
4488
4361
  projectScope.id,
4489
4362
  `${targetPrefix}/${downloaded.name}/SKILL`,
4490
- downloaded.content
4363
+ skillBody
4491
4364
  );
4492
4365
  let wikiCount = 1;
4493
- const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
4494
- const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
4495
4366
  for (const file of mdFiles) {
4496
4367
  const slug = `${targetPrefix}/${downloaded.name}/${file.path.replace(/\.[^.]+$/, "")}`;
4497
4368
  await upsertPage(
@@ -4504,44 +4375,14 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4504
4375
  );
4505
4376
  wikiCount++;
4506
4377
  }
4507
- let snippetId;
4508
- if (scriptFiles.length > 0) {
4509
- const packed = packFiles(scriptFiles);
4510
- const snippet = await createProjectSnippet(
4511
- auth.instanceUrl,
4512
- auth.token,
4513
- args.project_id,
4514
- `skill:${downloaded.name}`,
4515
- `Scripts for skill "${downloaded.name}" (${scriptFiles.length} files, installed from skills.sh:${args.name})`,
4516
- [{ file_path: `${downloaded.name}.bundle`, content: packed }],
4517
- "private"
4518
- );
4519
- snippetId = snippet.id;
4520
- }
4521
- await upsertIndexEntry(
4522
- auth.instanceUrl,
4523
- auth.token,
4524
- projectScope.scope,
4525
- projectScope.id,
4526
- targetIndex,
4527
- {
4528
- name: downloaded.name,
4529
- description: downloaded.description,
4530
- source: `skills.sh:${args.name}`,
4531
- snippetId,
4532
- draft: !!args.draft
4533
- }
4534
- );
4535
4378
  const parts = [`${wikiCount} wiki page(s)`];
4536
4379
  if (snippetId) parts.push(`snippet #${snippetId} with ${scriptFiles.length} script(s)`);
4537
- return `Installed skill "${downloaded.name}" from skills.sh. ${parts.join(", ")}. Use gitlab_skill_setup to extract scripts to disk.`;
4380
+ return `Installed skill "${downloaded.name}" from skills.sh. ${parts.join(", ")}. Use gitlab_skill_setup to extract scripts.`;
4538
4381
  } catch (err) {
4539
- return `Error installing skill from skills.sh: ${err.message}`;
4382
+ return `Error installing from skills.sh: ${err.message}`;
4540
4383
  }
4541
4384
  }
4542
- if (!args.group_id) {
4543
- return 'Error: group_id is required when source is "group".';
4544
- }
4385
+ if (!args.group_id) return 'Error: group_id is required when source is "group".';
4545
4386
  try {
4546
4387
  const groupPages = await listWikiPages(
4547
4388
  auth.instanceUrl,
@@ -4551,9 +4392,7 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4551
4392
  true
4552
4393
  );
4553
4394
  const sourcePrefix = `${SKILLS_PREFIX}/${args.name}/`;
4554
- const skillPages = groupPages.filter(
4555
- (p) => p.slug.startsWith(sourcePrefix) && p.content
4556
- );
4395
+ const skillPages = groupPages.filter((p) => p.slug.startsWith(sourcePrefix) && p.content);
4557
4396
  if (skillPages.length === 0) {
4558
4397
  return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
4559
4398
  }
@@ -4568,39 +4407,17 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4568
4407
  page.content
4569
4408
  );
4570
4409
  }
4571
- const groupIndex = await readIndex(
4572
- auth.instanceUrl,
4573
- auth.token,
4574
- "groups",
4575
- args.group_id,
4576
- SKILLS_INDEX
4577
- );
4578
- const entry = groupIndex.find((e) => e.name === args.name);
4579
- const description = entry?.description ?? "(installed from group)";
4580
- await upsertIndexEntry(
4581
- auth.instanceUrl,
4582
- auth.token,
4583
- projectScope.scope,
4584
- projectScope.id,
4585
- targetIndex,
4586
- {
4587
- name: args.name,
4588
- description,
4589
- source: `group:${args.group_id}`,
4590
- draft: !!args.draft
4591
- }
4592
- );
4593
- return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s) copied.`;
4410
+ return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s).`;
4594
4411
  } catch (err) {
4595
- return `Error installing skill: ${err.message}`;
4412
+ return `Error installing from group: ${err.message}`;
4596
4413
  }
4597
4414
  }
4598
4415
  }),
4599
4416
  gitlab_skill_setup: tool6({
4600
- 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.",
4417
+ description: "Extract a skill to .agents/skills/ for local execution.\nDownloads SKILL.md from wiki and scripts from the associated snippet.\nOpenCode will auto-discover the skill from the local directory.",
4601
4418
  args: {
4602
4419
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4603
- name: z6.string().describe("Skill name to set up locally"),
4420
+ name: z6.string().describe("Skill name to set up"),
4604
4421
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4605
4422
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4606
4423
  },
@@ -4608,29 +4425,35 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4608
4425
  const auth = authAndValidate(args.project_id);
4609
4426
  const { scope, id } = resolveScope2(args);
4610
4427
  const workDir = ctx.getDirectory();
4611
- const targetDir = join2(workDir, ".agents", "skills", args.name);
4428
+ const targetDir = join3(workDir, ".agents", "skills", args.name);
4612
4429
  try {
4613
- let skillContent = null;
4430
+ let skillPage = null;
4614
4431
  for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4615
4432
  try {
4616
- const page = await getWikiPage(
4433
+ skillPage = await getWikiPage(
4617
4434
  auth.instanceUrl,
4618
4435
  auth.token,
4619
4436
  scope,
4620
4437
  id,
4621
4438
  `${prefix}/${args.name}/SKILL`
4622
4439
  );
4623
- skillContent = page.content;
4624
4440
  break;
4625
4441
  } catch {
4626
4442
  }
4627
4443
  }
4628
- if (!skillContent) {
4629
- return `Skill "${args.name}" not found in wiki. Use gitlab_skill_list to see available skills.`;
4444
+ if (!skillPage) {
4445
+ return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4630
4446
  }
4447
+ const { meta, body } = parseFrontmatter(skillPage.content);
4631
4448
  mkdirSync(targetDir, { recursive: true });
4632
- writeFileSync(join2(targetDir, "SKILL.md"), skillContent);
4633
- const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
4449
+ writeFileSync2(join3(targetDir, "SKILL.md"), body);
4450
+ const pages = await listWikiPages(
4451
+ auth.instanceUrl,
4452
+ auth.token,
4453
+ scope,
4454
+ id,
4455
+ true
4456
+ );
4634
4457
  let refCount = 0;
4635
4458
  for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4636
4459
  const skillPagePrefix = `${prefix}/${args.name}/`;
@@ -4639,59 +4462,36 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4639
4462
  );
4640
4463
  for (const page of extraPages) {
4641
4464
  const relPath = page.slug.slice(skillPagePrefix.length);
4642
- const filePath = join2(targetDir, `${relPath}.md`);
4465
+ const filePath = join3(targetDir, `${relPath}.md`);
4643
4466
  mkdirSync(dirname(filePath), { recursive: true });
4644
- writeFileSync(filePath, page.content);
4467
+ writeFileSync2(filePath, page.content);
4645
4468
  refCount++;
4646
4469
  }
4647
4470
  }
4648
4471
  let scriptCount = 0;
4649
- const indexEntries = await readIndex(
4650
- auth.instanceUrl,
4651
- auth.token,
4652
- scope,
4653
- id,
4654
- SKILLS_INDEX
4655
- );
4656
- const draftsEntries = await readIndex(
4657
- auth.instanceUrl,
4658
- auth.token,
4659
- scope,
4660
- id,
4661
- DRAFTS_INDEX
4662
- );
4663
- const entry = [...indexEntries, ...draftsEntries].find((e) => e.name === args.name);
4664
- if (entry?.snippetId) {
4472
+ if (meta.snippetId) {
4665
4473
  const bundleFiles = await listSnippetFiles(
4666
4474
  auth.instanceUrl,
4667
4475
  auth.token,
4668
4476
  args.project_id,
4669
- entry.snippetId
4477
+ meta.snippetId
4670
4478
  );
4671
4479
  for (const bf of bundleFiles) {
4672
4480
  const raw = await getSnippetFileRaw(
4673
4481
  auth.instanceUrl,
4674
4482
  auth.token,
4675
4483
  args.project_id,
4676
- entry.snippetId,
4484
+ meta.snippetId,
4677
4485
  bf.path
4678
4486
  );
4679
4487
  if (bf.path.endsWith(".bundle")) {
4680
- const unpacked = unpackFiles(raw);
4681
- for (const file of unpacked) {
4682
- const filePath = join2(targetDir, file.path);
4488
+ for (const file of unpackFiles(raw)) {
4489
+ const filePath = join3(targetDir, file.path);
4683
4490
  mkdirSync(dirname(filePath), { recursive: true });
4684
- writeFileSync(filePath, file.content);
4685
- if (file.path.endsWith(".sh")) {
4686
- chmodSync(filePath, 493);
4687
- }
4491
+ writeFileSync2(filePath, file.content);
4492
+ if (file.path.endsWith(".sh")) chmodSync(filePath, 493);
4688
4493
  scriptCount++;
4689
4494
  }
4690
- } else {
4691
- const filePath = join2(targetDir, bf.path);
4692
- mkdirSync(dirname(filePath), { recursive: true });
4693
- writeFileSync(filePath, raw);
4694
- scriptCount++;
4695
4495
  }
4696
4496
  }
4697
4497
  }
@@ -4699,18 +4499,18 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4699
4499
  const parts = ["SKILL.md"];
4700
4500
  if (refCount > 0) parts.push(`${refCount} reference(s)`);
4701
4501
  if (scriptCount > 0) parts.push(`${scriptCount} script(s)`);
4702
- return `Skill "${args.name}" extracted to ${targetDir} (${parts.join(", ")}). OpenCode will auto-discover it on next session.`;
4502
+ return `Skill "${args.name}" extracted to ${targetDir} (${parts.join(", ")}).`;
4703
4503
  } catch (err) {
4704
4504
  return `Error setting up skill: ${err.message}`;
4705
4505
  }
4706
4506
  }
4707
4507
  }),
4708
4508
  gitlab_skill_delete: tool6({
4709
- 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.",
4509
+ description: "Delete a skill and its associated snippet.\nRemoves all wiki pages under the skill directory.",
4710
4510
  args: {
4711
4511
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4712
4512
  name: z6.string().describe("Skill name to delete"),
4713
- draft: z6.boolean().optional().describe("Delete from drafts instead of published (default: false)"),
4513
+ draft: z6.boolean().optional().describe("Delete from drafts (default: false)"),
4714
4514
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4715
4515
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4716
4516
  },
@@ -4718,36 +4518,36 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4718
4518
  const auth = authAndValidate(args.project_id);
4719
4519
  const { scope, id } = resolveScope2(args);
4720
4520
  const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4721
- const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4722
4521
  const skillPrefix = `${prefix}/${args.name}/`;
4723
4522
  try {
4724
4523
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4725
4524
  const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix));
4726
4525
  if (skillPages.length === 0) {
4727
- return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4526
+ return `Skill "${args.name}" not found.`;
4728
4527
  }
4528
+ const skillPage = await getWikiPage(
4529
+ auth.instanceUrl,
4530
+ auth.token,
4531
+ scope,
4532
+ id,
4533
+ `${prefix}/${args.name}/SKILL`
4534
+ );
4535
+ const { meta } = parseFrontmatter(skillPage.content);
4729
4536
  for (const page of skillPages) {
4730
4537
  await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4731
4538
  }
4732
- const indexEntries = await readIndex(auth.instanceUrl, auth.token, scope, id, indexSlug);
4733
- const entry = indexEntries.find((e) => e.name === args.name);
4734
- let snippetDeleted = false;
4735
- if (entry?.snippetId) {
4539
+ if (meta.snippetId) {
4736
4540
  try {
4737
4541
  await deleteProjectSnippet(
4738
4542
  auth.instanceUrl,
4739
4543
  auth.token,
4740
4544
  args.project_id,
4741
- entry.snippetId
4545
+ meta.snippetId
4742
4546
  );
4743
- snippetDeleted = true;
4744
4547
  } catch {
4745
4548
  }
4746
4549
  }
4747
- await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
4748
- const parts = [`${skillPages.length} wiki page(s)`];
4749
- if (snippetDeleted) parts.push("snippet");
4750
- return `Deleted skill "${args.name}" (${parts.join(" + ")} removed).`;
4550
+ return `Deleted skill "${args.name}" (${skillPages.length} page(s)${meta.snippetId ? " + snippet" : ""}).`;
4751
4551
  } catch (err) {
4752
4552
  return `Error deleting skill: ${err.message}`;
4753
4553
  }