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.cjs CHANGED
@@ -4017,170 +4017,70 @@ async function listSnippetFiles(instanceUrl, token, projectId, snippetId) {
4017
4017
  }
4018
4018
 
4019
4019
  // src/tools/skill-tools.ts
4020
+ var import_fs3 = require("fs");
4021
+ var import_path3 = require("path");
4022
+
4023
+ // src/tools/skill-helpers.ts
4020
4024
  var import_child_process = require("child_process");
4021
4025
  var import_fs2 = require("fs");
4022
4026
  var import_path2 = require("path");
4023
4027
  var import_os2 = require("os");
4024
4028
  var import_zlib = require("zlib");
4025
- var z6 = import_plugin6.tool.schema;
4026
4029
  var PREFIX2 = "agents";
4027
4030
  var SKILLS_PREFIX = `${PREFIX2}/skills`;
4028
4031
  var DRAFTS_PREFIX = `${PREFIX2}/skills-drafts`;
4029
- var SKILLS_INDEX = `${SKILLS_PREFIX}/_registry`;
4030
- var DRAFTS_INDEX = `${DRAFTS_PREFIX}/_registry`;
4031
- var PROJECT_ID_DESC2 = "Project path from git remote";
4032
- function resolveScope2(args) {
4033
- if (args.scope === "groups" && args.group_id) {
4034
- return { scope: "groups", id: args.group_id };
4035
- }
4036
- return { scope: "projects", id: args.project_id };
4037
- }
4038
- function validateProjectId2(projectId) {
4039
- if (!projectId.includes("/")) {
4040
- 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.`;
4041
- }
4042
- return null;
4043
- }
4044
- function parseIndex(content) {
4045
- const entries = [];
4046
- const blocks = content.split(/^## /m).filter(Boolean);
4047
- for (const block of blocks) {
4048
- const lines = block.trim().split("\n");
4049
- const name = lines[0].trim();
4050
- if (!name) continue;
4051
- const rest = lines.slice(1).join("\n").trim();
4052
- const descLines = [];
4053
- let source;
4054
- let snippetId;
4055
- for (const line of rest.split("\n")) {
4056
- if (line.startsWith("Source:")) {
4057
- source = line.slice(7).trim();
4058
- } else if (line.startsWith("Snippet:")) {
4059
- snippetId = parseInt(line.slice(8).trim(), 10) || void 0;
4060
- } else if (line.trim()) {
4061
- descLines.push(line);
4062
- }
4063
- }
4064
- entries.push({ name, description: descLines.join("\n"), source, snippetId, draft: false });
4065
- }
4066
- return entries;
4067
- }
4068
- function formatIndex(entries) {
4069
- return entries.map((e) => {
4070
- let block = `## ${e.name}
4071
- ${e.description}`;
4072
- if (e.source) block += `
4073
- Source: ${e.source}`;
4074
- if (e.snippetId) block += `
4075
- Snippet: ${e.snippetId}`;
4076
- return block;
4077
- }).join("\n\n");
4078
- }
4079
- async function readIndex(instanceUrl, token, scope, id, indexSlug) {
4080
- try {
4081
- const page = await getWikiPage(instanceUrl, token, scope, id, indexSlug);
4082
- return parseIndex(page.content);
4083
- } catch {
4084
- return [];
4085
- }
4086
- }
4087
- async function sleep2(ms) {
4088
- return new Promise((resolve) => setTimeout(resolve, ms));
4089
- }
4090
- async function writeIndex(instanceUrl, token, scope, id, indexSlug, entries) {
4091
- const content = formatIndex(entries) || "# Skills Registry";
4092
- for (let attempt = 0; attempt < 3; attempt++) {
4093
- try {
4094
- await updateWikiPage(instanceUrl, token, scope, id, indexSlug, content);
4095
- return;
4096
- } catch (updateErr) {
4097
- const msg = updateErr.message ?? "";
4098
- if (msg.includes("not found") || msg.includes("404")) {
4099
- await createWikiPage(instanceUrl, token, scope, id, indexSlug, content);
4100
- return;
4101
- }
4102
- if (attempt < 2) {
4103
- await sleep2(1e3 * (attempt + 1));
4104
- continue;
4105
- }
4106
- throw updateErr;
4107
- }
4032
+ var EMPTY_FILE_SENTINEL = "__EMPTY_FILE__";
4033
+ function parseFrontmatter(content) {
4034
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
4035
+ if (!match) return { meta: {}, body: content };
4036
+ const meta = {};
4037
+ for (const line of match[1].split("\n")) {
4038
+ const [key, ...rest] = line.split(":");
4039
+ const val = rest.join(":").trim();
4040
+ if (!key || !val) continue;
4041
+ const k = key.trim();
4042
+ if (k === "name") meta.name = val;
4043
+ else if (k === "description") meta.description = val;
4044
+ else if (k === "source") meta.source = val;
4045
+ else if (k === "snippet") meta.snippetId = parseInt(val, 10) || void 0;
4108
4046
  }
4047
+ return { meta, body: match[2] };
4109
4048
  }
4110
- async function upsertIndexEntry(instanceUrl, token, scope, id, indexSlug, entry) {
4111
- const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
4112
- const idx = entries.findIndex((e) => e.name === entry.name);
4113
- if (idx >= 0) {
4114
- entries[idx] = entry;
4115
- } else {
4116
- entries.push(entry);
4117
- }
4118
- await writeIndex(instanceUrl, token, scope, id, indexSlug, entries);
4049
+ function formatFrontmatter(meta, body) {
4050
+ const lines = ["---", `name: ${meta.name}`, `description: ${meta.description}`];
4051
+ if (meta.source) lines.push(`source: ${meta.source}`);
4052
+ if (meta.snippetId) lines.push(`snippet: ${meta.snippetId}`);
4053
+ lines.push("---", "");
4054
+ return lines.join("\n") + body;
4119
4055
  }
4120
- async function removeIndexEntry(instanceUrl, token, scope, id, indexSlug, name) {
4121
- const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
4122
- const filtered = entries.filter((e) => e.name !== name);
4123
- if (filtered.length !== entries.length) {
4124
- await writeIndex(instanceUrl, token, scope, id, indexSlug, filtered);
4125
- }
4056
+ function extractSkillNameFromSlug(slug, prefix) {
4057
+ if (!slug.startsWith(prefix + "/") || !slug.endsWith("/SKILL")) return null;
4058
+ const middle = slug.slice(prefix.length + 1, -"/SKILL".length);
4059
+ return middle && !middle.includes("/") ? middle : null;
4126
4060
  }
4127
- async function upsertPage(instanceUrl, token, scope, id, slug, content) {
4128
- for (let attempt = 0; attempt < 3; attempt++) {
4129
- try {
4130
- await updateWikiPage(instanceUrl, token, scope, id, slug, content);
4131
- return;
4132
- } catch (updateErr) {
4133
- const msg = updateErr.message ?? "";
4134
- if (msg.includes("not found") || msg.includes("404")) {
4135
- try {
4136
- await createWikiPage(instanceUrl, token, scope, id, slug, content);
4137
- return;
4138
- } catch (createErr) {
4139
- if (attempt < 2 && (createErr.message?.includes("Duplicate") || createErr.message?.includes("reference"))) {
4140
- await sleep2(1e3 * (attempt + 1));
4141
- continue;
4142
- }
4143
- throw createErr;
4144
- }
4145
- }
4146
- if (attempt < 2) {
4147
- await sleep2(1e3 * (attempt + 1));
4148
- continue;
4149
- }
4150
- throw updateErr;
4151
- }
4152
- }
4061
+ function isMarkdownFile(path) {
4062
+ return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
4153
4063
  }
4154
- var EMPTY_FILE_SENTINEL = "__EMPTY_FILE__";
4155
4064
  function packFiles(files) {
4156
4065
  const manifest = files.map((f) => ({
4157
4066
  path: f.path,
4158
4067
  content: f.content.trim() ? f.content : EMPTY_FILE_SENTINEL
4159
4068
  }));
4160
- const json = JSON.stringify(manifest);
4161
- return (0, import_zlib.gzipSync)(Buffer.from(json, "utf-8")).toString("base64");
4069
+ return (0, import_zlib.gzipSync)(Buffer.from(JSON.stringify(manifest), "utf-8")).toString("base64");
4162
4070
  }
4163
4071
  function unpackFiles(packed) {
4164
4072
  const json = (0, import_zlib.gunzipSync)(Buffer.from(packed, "base64")).toString("utf-8");
4165
- const manifest = JSON.parse(json);
4166
- return manifest.map((f) => ({
4073
+ return JSON.parse(json).map((f) => ({
4167
4074
  path: f.path,
4168
4075
  content: f.content === EMPTY_FILE_SENTINEL ? "" : f.content
4169
4076
  }));
4170
4077
  }
4171
- function isMarkdownFile(path) {
4172
- return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
4173
- }
4174
- function isAgentsGitignored(dir) {
4078
+ function ensureGitignore(dir) {
4175
4079
  try {
4176
4080
  (0, import_child_process.execSync)("git check-ignore -q .agents", { cwd: dir, stdio: "pipe" });
4177
- return true;
4081
+ return;
4178
4082
  } catch {
4179
- return false;
4180
4083
  }
4181
- }
4182
- function ensureGitignore(dir) {
4183
- if (isAgentsGitignored(dir)) return;
4184
4084
  const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
4185
4085
  if ((0, import_fs2.existsSync)(gitignorePath)) {
4186
4086
  const content = (0, import_fs2.readFileSync)(gitignorePath, "utf-8");
@@ -4233,10 +4133,9 @@ function downloadSkillFromSkillsSh(identifier) {
4233
4133
  if (dirs.length === 0) return null;
4234
4134
  const skillName = dirs[0];
4235
4135
  const skillDir = (0, import_path2.join)(agentsDir, skillName);
4236
- const skillMd = (0, import_path2.join)(skillDir, "SKILL.md");
4237
4136
  let mainContent;
4238
4137
  try {
4239
- mainContent = (0, import_fs2.readFileSync)(skillMd, "utf-8");
4138
+ mainContent = (0, import_fs2.readFileSync)((0, import_path2.join)(skillDir, "SKILL.md"), "utf-8");
4240
4139
  } catch {
4241
4140
  return null;
4242
4141
  }
@@ -4245,8 +4144,7 @@ function downloadSkillFromSkillsSh(identifier) {
4245
4144
  if (descMatch) {
4246
4145
  description = descMatch[1].trim();
4247
4146
  } else {
4248
- const firstParagraph = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim();
4249
- description = firstParagraph.slice(0, 200);
4147
+ description = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim().slice(0, 200);
4250
4148
  }
4251
4149
  const files = [];
4252
4150
  const walkStack = [{ dir: skillDir, prefix: "" }];
@@ -4275,6 +4173,50 @@ function downloadSkillFromSkillsSh(identifier) {
4275
4173
  }
4276
4174
  }
4277
4175
  }
4176
+
4177
+ // src/tools/skill-tools.ts
4178
+ var z6 = import_plugin6.tool.schema;
4179
+ var PROJECT_ID_DESC2 = "Project path from git remote";
4180
+ function resolveScope2(args) {
4181
+ if (args.scope === "groups" && args.group_id) {
4182
+ return { scope: "groups", id: args.group_id };
4183
+ }
4184
+ return { scope: "projects", id: args.project_id };
4185
+ }
4186
+ function validateProjectId2(projectId) {
4187
+ if (!projectId.includes("/")) {
4188
+ return `Invalid project_id "${projectId}". Must be the full project path containing at least one slash.`;
4189
+ }
4190
+ return null;
4191
+ }
4192
+ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
4193
+ try {
4194
+ await updateWikiPage(instanceUrl, token, scope, id, slug, content);
4195
+ } catch (err) {
4196
+ if (err.message?.includes("not found") || err.message?.includes("404")) {
4197
+ await createWikiPage(instanceUrl, token, scope, id, slug, content);
4198
+ return;
4199
+ }
4200
+ throw err;
4201
+ }
4202
+ }
4203
+ async function listSkills(instanceUrl, token, scope, id, prefix) {
4204
+ const pages = await listWikiPages(instanceUrl, token, scope, id, true);
4205
+ const skills = [];
4206
+ for (const page of pages) {
4207
+ const name = extractSkillNameFromSlug(page.slug, prefix);
4208
+ if (!name || !page.content) continue;
4209
+ const { meta } = parseFrontmatter(page.content);
4210
+ skills.push({
4211
+ name: meta.name ?? name,
4212
+ description: meta.description ?? "",
4213
+ source: meta.source,
4214
+ snippetId: meta.snippetId,
4215
+ draft: prefix === DRAFTS_PREFIX
4216
+ });
4217
+ }
4218
+ return skills;
4219
+ }
4278
4220
  function makeSkillTools(ctx) {
4279
4221
  function authAndValidate(projectId) {
4280
4222
  const auth = ctx.ensureAuth();
@@ -4285,7 +4227,7 @@ function makeSkillTools(ctx) {
4285
4227
  }
4286
4228
  return {
4287
4229
  gitlab_skill_list: (0, import_plugin6.tool)({
4288
- description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).",
4230
+ description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks.",
4289
4231
  args: {
4290
4232
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4291
4233
  include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
@@ -4296,12 +4238,16 @@ function makeSkillTools(ctx) {
4296
4238
  const auth = authAndValidate(args.project_id);
4297
4239
  const { scope, id } = resolveScope2(args);
4298
4240
  try {
4299
- const published = await readIndex(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX);
4241
+ const published = await listSkills(
4242
+ auth.instanceUrl,
4243
+ auth.token,
4244
+ scope,
4245
+ id,
4246
+ SKILLS_PREFIX
4247
+ );
4300
4248
  let drafts = [];
4301
4249
  if (args.include_drafts) {
4302
- drafts = (await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX)).map(
4303
- (e) => ({ ...e, draft: true })
4304
- );
4250
+ drafts = await listSkills(auth.instanceUrl, auth.token, scope, id, DRAFTS_PREFIX);
4305
4251
  }
4306
4252
  const all = [...published, ...drafts];
4307
4253
  if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
@@ -4312,18 +4258,17 @@ function makeSkillTools(ctx) {
4312
4258
  }
4313
4259
  }),
4314
4260
  gitlab_skill_load: (0, import_plugin6.tool)({
4315
- 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.",
4261
+ 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.",
4316
4262
  args: {
4317
4263
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4318
- name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
4264
+ name: z6.string().describe('Skill name (e.g., "incident-retro")'),
4319
4265
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4320
4266
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4321
4267
  },
4322
4268
  execute: async (args) => {
4323
4269
  const auth = authAndValidate(args.project_id);
4324
4270
  const { scope, id } = resolveScope2(args);
4325
- const prefixes = [SKILLS_PREFIX, DRAFTS_PREFIX];
4326
- for (const prefix of prefixes) {
4271
+ for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4327
4272
  try {
4328
4273
  const page = await getWikiPage(
4329
4274
  auth.instanceUrl,
@@ -4332,36 +4277,28 @@ function makeSkillTools(ctx) {
4332
4277
  id,
4333
4278
  `${prefix}/${args.name}/SKILL`
4334
4279
  );
4280
+ const { meta, body } = parseFrontmatter(page.content);
4281
+ const isDraft = prefix === DRAFTS_PREFIX;
4335
4282
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4336
4283
  const skillPrefix = `${prefix}/${args.name}/`;
4337
- const skillSlug = `${prefix}/${args.name}/SKILL`;
4338
- const refs = pages.filter((p) => p.slug.startsWith(skillPrefix) && p.slug !== skillSlug).map((p) => p.slug.slice(skillPrefix.length));
4339
- const isDraft = prefix === DRAFTS_PREFIX;
4284
+ const refs = pages.filter(
4285
+ (p) => p.slug.startsWith(skillPrefix) && p.slug !== `${prefix}/${args.name}/SKILL`
4286
+ ).map((p) => p.slug.slice(skillPrefix.length));
4340
4287
  let result = isDraft ? `[DRAFT SKILL]
4341
4288
 
4342
- ${page.content}` : page.content;
4289
+ ${body}` : body;
4343
4290
  if (refs.length > 0) {
4344
4291
  result += `
4345
4292
 
4346
4293
  ---
4347
4294
  Available references: ${refs.join(", ")}`;
4348
4295
  }
4349
- const indexSlug = isDraft ? DRAFTS_INDEX : SKILLS_INDEX;
4350
- const indexEntries = await readIndex(
4351
- auth.instanceUrl,
4352
- auth.token,
4353
- scope,
4354
- id,
4355
- indexSlug
4356
- );
4357
- const entry = indexEntries.find((e) => e.name === args.name);
4358
- if (entry?.snippetId) {
4296
+ if (meta.snippetId) {
4359
4297
  result += `
4360
4298
 
4361
- ---
4362
- This skill has executable scripts in snippet #${entry.snippetId}.`;
4299
+ This skill has executable scripts in snippet #${meta.snippetId}.`;
4363
4300
  result += `
4364
- Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skills/${args.name}/ for execution.`;
4301
+ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them locally.`;
4365
4302
  }
4366
4303
  return result;
4367
4304
  } catch {
@@ -4372,13 +4309,13 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4372
4309
  }
4373
4310
  }),
4374
4311
  gitlab_skill_save: (0, import_plugin6.tool)({
4375
- 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.",
4312
+ description: "Create or update a skill.\nUse draft=true for skills that haven't been proven yet.",
4376
4313
  args: {
4377
4314
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4378
4315
  name: z6.string().describe('Skill name (e.g., "incident-retro")'),
4379
4316
  content: z6.string().describe("Skill content in markdown"),
4380
- description: z6.string().describe("Short description for the skill index (1-2 sentences)"),
4381
- draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
4317
+ description: z6.string().describe("Short description (1-2 sentences)"),
4318
+ draft: z6.boolean().optional().describe("Save as draft (default: false)"),
4382
4319
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4383
4320
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4384
4321
  },
@@ -4386,31 +4323,25 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4386
4323
  const auth = authAndValidate(args.project_id);
4387
4324
  const { scope, id } = resolveScope2(args);
4388
4325
  const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4389
- const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4390
4326
  const slug = `${prefix}/${args.name}/SKILL`;
4391
- const label = args.draft ? "draft " : "";
4327
+ const body = formatFrontmatter(
4328
+ { name: args.name, description: args.description, source: "project" },
4329
+ args.content
4330
+ );
4392
4331
  try {
4393
- await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
4394
- await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, {
4395
- name: args.name,
4396
- description: args.description,
4397
- source: "project",
4398
- draft: !!args.draft
4399
- });
4400
- return `Saved ${label}skill: ${args.name}`;
4332
+ await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, body);
4333
+ return `Saved ${args.draft ? "draft " : ""}skill: ${args.name}`;
4401
4334
  } catch (err) {
4402
4335
  return `Error saving skill: ${err.message}`;
4403
4336
  }
4404
4337
  }
4405
4338
  }),
4406
4339
  gitlab_skill_promote: (0, import_plugin6.tool)({
4407
- 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.",
4340
+ description: "Promote a skill.\nDefault (target='published'): moves a draft to published.\nTarget 'group': moves a project skill to the group wiki.",
4408
4341
  args: {
4409
4342
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4410
4343
  name: z6.string().describe("Skill name to promote"),
4411
- target: z6.enum(["published", "group"]).optional().describe(
4412
- 'Promotion target: "published" (default, draft\u2192published) or "group" (project\u2192group wiki)'
4413
- ),
4344
+ target: z6.enum(["published", "group"]).optional().describe('"published" (default) or "group"'),
4414
4345
  group_id: z6.string().optional().describe("Group path (required when target is group)"),
4415
4346
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)")
4416
4347
  },
@@ -4418,9 +4349,7 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4418
4349
  const auth = authAndValidate(args.project_id);
4419
4350
  const promotionTarget = args.target ?? "published";
4420
4351
  if (promotionTarget === "group") {
4421
- if (!args.group_id) {
4422
- return 'Error: group_id is required when target is "group".';
4423
- }
4352
+ if (!args.group_id) return 'Error: group_id is required when target is "group".';
4424
4353
  const projectScope = resolveScope2(args);
4425
4354
  try {
4426
4355
  const pages = await listWikiPages(
@@ -4431,11 +4360,9 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4431
4360
  true
4432
4361
  );
4433
4362
  const skillPrefix = `${SKILLS_PREFIX}/${args.name}/`;
4434
- const skillPages = pages.filter(
4435
- (p) => p.slug.startsWith(skillPrefix) && p.content
4436
- );
4363
+ const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix) && p.content);
4437
4364
  if (skillPages.length === 0) {
4438
- return `Skill "${args.name}" not found in project. Use gitlab_skill_list to see available skills.`;
4365
+ return `Skill "${args.name}" not found in project.`;
4439
4366
  }
4440
4367
  for (const page of skillPages) {
4441
4368
  await upsertPage(
@@ -4447,54 +4374,6 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4447
4374
  page.content
4448
4375
  );
4449
4376
  }
4450
- const projectIndex = await readIndex(
4451
- auth.instanceUrl,
4452
- auth.token,
4453
- projectScope.scope,
4454
- projectScope.id,
4455
- SKILLS_INDEX
4456
- );
4457
- const entry = projectIndex.find((e) => e.name === args.name);
4458
- const description = entry?.description ?? "(promoted from project)";
4459
- if (entry?.snippetId) {
4460
- const bundleFiles = await listSnippetFiles(
4461
- auth.instanceUrl,
4462
- auth.token,
4463
- args.project_id,
4464
- entry.snippetId
4465
- );
4466
- for (const bf of bundleFiles) {
4467
- const raw = await getSnippetFileRaw(
4468
- auth.instanceUrl,
4469
- auth.token,
4470
- args.project_id,
4471
- entry.snippetId,
4472
- bf.path
4473
- );
4474
- await upsertPage(
4475
- auth.instanceUrl,
4476
- auth.token,
4477
- "groups",
4478
- args.group_id,
4479
- `${SKILLS_PREFIX}/${args.name}/bundle/${bf.path}`,
4480
- raw
4481
- );
4482
- }
4483
- }
4484
- await upsertIndexEntry(
4485
- auth.instanceUrl,
4486
- auth.token,
4487
- "groups",
4488
- args.group_id,
4489
- SKILLS_INDEX,
4490
- {
4491
- name: args.name,
4492
- description,
4493
- source: `project:${args.project_id}`,
4494
- snippetId: entry?.snippetId,
4495
- draft: false
4496
- }
4497
- );
4498
4377
  for (const page of skillPages) {
4499
4378
  await deleteWikiPage(
4500
4379
  auth.instanceUrl,
@@ -4504,55 +4383,30 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4504
4383
  page.slug
4505
4384
  );
4506
4385
  }
4507
- if (entry?.snippetId) {
4508
- try {
4509
- await deleteProjectSnippet(
4510
- auth.instanceUrl,
4511
- auth.token,
4512
- args.project_id,
4513
- entry.snippetId
4514
- );
4515
- } catch {
4516
- }
4517
- }
4518
- await removeIndexEntry(
4519
- auth.instanceUrl,
4520
- auth.token,
4521
- projectScope.scope,
4522
- projectScope.id,
4523
- SKILLS_INDEX,
4524
- args.name
4525
- );
4526
- return `Promoted skill "${args.name}" to group "${args.group_id}". ${skillPages.length} page(s) moved (removed from project).`;
4386
+ return `Promoted skill "${args.name}" to group "${args.group_id}". ${skillPages.length} page(s) moved.`;
4527
4387
  } catch (err) {
4528
4388
  return `Error promoting skill to group: ${err.message}`;
4529
4389
  }
4530
4390
  }
4531
4391
  const { scope, id } = resolveScope2(args);
4532
4392
  try {
4533
- const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
4534
- const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
4535
- const draftPages = pages.filter(
4536
- (p) => p.slug.startsWith(draftPrefix) && p.content
4393
+ const pages = await listWikiPages(
4394
+ auth.instanceUrl,
4395
+ auth.token,
4396
+ scope,
4397
+ id,
4398
+ true
4537
4399
  );
4400
+ const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
4401
+ const draftPages = pages.filter((p) => p.slug.startsWith(draftPrefix) && p.content);
4538
4402
  if (draftPages.length === 0) {
4539
- return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
4403
+ return `Draft skill "${args.name}" not found.`;
4540
4404
  }
4541
- const draftIndex = await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX);
4542
- const entry = draftIndex.find((e) => e.name === args.name);
4543
- const description = entry?.description ?? "(promoted from draft)";
4544
4405
  for (const page of draftPages) {
4545
4406
  const newSlug = page.slug.replace(DRAFTS_PREFIX, SKILLS_PREFIX);
4546
4407
  await upsertPage(auth.instanceUrl, auth.token, scope, id, newSlug, page.content);
4547
4408
  await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4548
4409
  }
4549
- await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX, args.name);
4550
- await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX, {
4551
- name: args.name,
4552
- description,
4553
- source: "project",
4554
- draft: false
4555
- });
4556
4410
  return `Promoted skill "${args.name}" from draft to published.`;
4557
4411
  } catch (err) {
4558
4412
  return `Error promoting skill: ${err.message}`;
@@ -4560,11 +4414,11 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4560
4414
  }
4561
4415
  }),
4562
4416
  gitlab_skill_discover: (0, import_plugin6.tool)({
4563
- 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.",
4417
+ description: "Search for skills in skills.sh and optionally a group wiki.\nUse gitlab_skill_install to install a discovered skill.",
4564
4418
  args: {
4565
- query: z6.string().describe("Search query (matches skill name and description)"),
4566
- project_id: z6.string().optional().describe(PROJECT_ID_DESC2 + " Only needed for group wiki search."),
4567
- group_id: z6.string().optional().describe("Group path to search for shared skills (optional)")
4419
+ query: z6.string().describe("Search query"),
4420
+ project_id: z6.string().optional().describe(PROJECT_ID_DESC2 + " Only needed for group search."),
4421
+ group_id: z6.string().optional().describe("Group path to search")
4568
4422
  },
4569
4423
  execute: async (args) => {
4570
4424
  let auth = null;
@@ -4576,15 +4430,15 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4576
4430
  const sections = [];
4577
4431
  if (args.group_id && auth) {
4578
4432
  try {
4579
- const entries = await readIndex(
4433
+ const skills = await listSkills(
4580
4434
  auth.instanceUrl,
4581
4435
  auth.token,
4582
4436
  "groups",
4583
4437
  args.group_id,
4584
- SKILLS_INDEX
4438
+ SKILLS_PREFIX
4585
4439
  );
4586
4440
  const q = args.query.toLowerCase();
4587
- const matches = entries.filter(
4441
+ const matches = skills.filter(
4588
4442
  (e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
4589
4443
  );
4590
4444
  if (matches.length > 0) {
@@ -4613,19 +4467,17 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4613
4467
  );
4614
4468
  }
4615
4469
  if (sections.length === 0) {
4616
- return `No skills found matching "${args.query}" in ${args.group_id ? "group wiki or " : ""}skills.sh.`;
4470
+ return `No skills found matching "${args.query}".`;
4617
4471
  }
4618
4472
  return sections.join("\n\n---\n\n");
4619
4473
  }
4620
4474
  }),
4621
4475
  gitlab_skill_install: (0, import_plugin6.tool)({
4622
- 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.",
4476
+ 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.",
4623
4477
  args: {
4624
4478
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4625
- name: z6.string().describe(
4626
- 'Skill identifier. For group: skill name (e.g., "incident-retro"). For skills.sh: full identifier (e.g., "vercel-labs/agent-skills@nextjs-developer").'
4627
- ),
4628
- source: z6.enum(["group", "skills.sh"]).describe('Where to install from: "group" (group wiki) or "skills.sh" (public registry)'),
4479
+ name: z6.string().describe("Skill identifier"),
4480
+ source: z6.enum(["group", "skills.sh"]).describe("Where to install from"),
4629
4481
  group_id: z6.string().optional().describe("Group path (required when source is group)"),
4630
4482
  draft: z6.boolean().optional().describe("Install as draft (default: false)")
4631
4483
  },
@@ -4633,24 +4485,45 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4633
4485
  const auth = authAndValidate(args.project_id);
4634
4486
  const projectScope = resolveScope2(args);
4635
4487
  const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4636
- const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4637
4488
  if (args.source === "skills.sh") {
4638
4489
  const downloaded = downloadSkillFromSkillsSh(args.name);
4639
4490
  if (!downloaded) {
4640
- return `Failed to download skill "${args.name}" from skills.sh. Check that the identifier is correct (e.g., "owner/repo@skill-name").`;
4491
+ return `Failed to download skill "${args.name}" from skills.sh.`;
4641
4492
  }
4642
4493
  try {
4494
+ const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
4495
+ const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
4496
+ let snippetId;
4497
+ if (scriptFiles.length > 0) {
4498
+ const snippet = await createProjectSnippet(
4499
+ auth.instanceUrl,
4500
+ auth.token,
4501
+ args.project_id,
4502
+ `skill:${downloaded.name}`,
4503
+ `Scripts for skill "${downloaded.name}"`,
4504
+ [{ file_path: `${downloaded.name}.bundle`, content: packFiles(scriptFiles) }],
4505
+ "private"
4506
+ );
4507
+ snippetId = snippet.id;
4508
+ }
4509
+ const skillBody = formatFrontmatter(
4510
+ {
4511
+ name: downloaded.name,
4512
+ description: downloaded.description,
4513
+ source: `skills.sh:${args.name}`,
4514
+ snippetId
4515
+ },
4516
+ downloaded.content.replace(/^---[\s\S]*?---\s*\n/, "")
4517
+ );
4643
4518
  await upsertPage(
4644
4519
  auth.instanceUrl,
4645
4520
  auth.token,
4646
4521
  projectScope.scope,
4647
4522
  projectScope.id,
4648
4523
  `${targetPrefix}/${downloaded.name}/SKILL`,
4649
- downloaded.content
4524
+ skillBody
4650
4525
  );
4651
4526
  let wikiCount = 1;
4652
- const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
4653
- const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
4654
4527
  for (const file of mdFiles) {
4655
4528
  const slug = `${targetPrefix}/${downloaded.name}/${file.path.replace(/\.[^.]+$/, "")}`;
4656
4529
  await upsertPage(
@@ -4663,44 +4536,14 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4663
4536
  );
4664
4537
  wikiCount++;
4665
4538
  }
4666
- let snippetId;
4667
- if (scriptFiles.length > 0) {
4668
- const packed = packFiles(scriptFiles);
4669
- const snippet = await createProjectSnippet(
4670
- auth.instanceUrl,
4671
- auth.token,
4672
- args.project_id,
4673
- `skill:${downloaded.name}`,
4674
- `Scripts for skill "${downloaded.name}" (${scriptFiles.length} files, installed from skills.sh:${args.name})`,
4675
- [{ file_path: `${downloaded.name}.bundle`, content: packed }],
4676
- "private"
4677
- );
4678
- snippetId = snippet.id;
4679
- }
4680
- await upsertIndexEntry(
4681
- auth.instanceUrl,
4682
- auth.token,
4683
- projectScope.scope,
4684
- projectScope.id,
4685
- targetIndex,
4686
- {
4687
- name: downloaded.name,
4688
- description: downloaded.description,
4689
- source: `skills.sh:${args.name}`,
4690
- snippetId,
4691
- draft: !!args.draft
4692
- }
4693
- );
4694
4539
  const parts = [`${wikiCount} wiki page(s)`];
4695
4540
  if (snippetId) parts.push(`snippet #${snippetId} with ${scriptFiles.length} script(s)`);
4696
- return `Installed skill "${downloaded.name}" from skills.sh. ${parts.join(", ")}. Use gitlab_skill_setup to extract scripts to disk.`;
4541
+ return `Installed skill "${downloaded.name}" from skills.sh. ${parts.join(", ")}. Use gitlab_skill_setup to extract scripts.`;
4697
4542
  } catch (err) {
4698
- return `Error installing skill from skills.sh: ${err.message}`;
4543
+ return `Error installing from skills.sh: ${err.message}`;
4699
4544
  }
4700
4545
  }
4701
- if (!args.group_id) {
4702
- return 'Error: group_id is required when source is "group".';
4703
- }
4546
+ if (!args.group_id) return 'Error: group_id is required when source is "group".';
4704
4547
  try {
4705
4548
  const groupPages = await listWikiPages(
4706
4549
  auth.instanceUrl,
@@ -4710,9 +4553,7 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4710
4553
  true
4711
4554
  );
4712
4555
  const sourcePrefix = `${SKILLS_PREFIX}/${args.name}/`;
4713
- const skillPages = groupPages.filter(
4714
- (p) => p.slug.startsWith(sourcePrefix) && p.content
4715
- );
4556
+ const skillPages = groupPages.filter((p) => p.slug.startsWith(sourcePrefix) && p.content);
4716
4557
  if (skillPages.length === 0) {
4717
4558
  return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
4718
4559
  }
@@ -4727,39 +4568,17 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4727
4568
  page.content
4728
4569
  );
4729
4570
  }
4730
- const groupIndex = await readIndex(
4731
- auth.instanceUrl,
4732
- auth.token,
4733
- "groups",
4734
- args.group_id,
4735
- SKILLS_INDEX
4736
- );
4737
- const entry = groupIndex.find((e) => e.name === args.name);
4738
- const description = entry?.description ?? "(installed from group)";
4739
- await upsertIndexEntry(
4740
- auth.instanceUrl,
4741
- auth.token,
4742
- projectScope.scope,
4743
- projectScope.id,
4744
- targetIndex,
4745
- {
4746
- name: args.name,
4747
- description,
4748
- source: `group:${args.group_id}`,
4749
- draft: !!args.draft
4750
- }
4751
- );
4752
- return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s) copied.`;
4571
+ return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s).`;
4753
4572
  } catch (err) {
4754
- return `Error installing skill: ${err.message}`;
4573
+ return `Error installing from group: ${err.message}`;
4755
4574
  }
4756
4575
  }
4757
4576
  }),
4758
4577
  gitlab_skill_setup: (0, import_plugin6.tool)({
4759
- 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.",
4578
+ 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.",
4760
4579
  args: {
4761
4580
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4762
- name: z6.string().describe("Skill name to set up locally"),
4581
+ name: z6.string().describe("Skill name to set up"),
4763
4582
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4764
4583
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4765
4584
  },
@@ -4767,29 +4586,35 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4767
4586
  const auth = authAndValidate(args.project_id);
4768
4587
  const { scope, id } = resolveScope2(args);
4769
4588
  const workDir = ctx.getDirectory();
4770
- const targetDir = (0, import_path2.join)(workDir, ".agents", "skills", args.name);
4589
+ const targetDir = (0, import_path3.join)(workDir, ".agents", "skills", args.name);
4771
4590
  try {
4772
- let skillContent = null;
4591
+ let skillPage = null;
4773
4592
  for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4774
4593
  try {
4775
- const page = await getWikiPage(
4594
+ skillPage = await getWikiPage(
4776
4595
  auth.instanceUrl,
4777
4596
  auth.token,
4778
4597
  scope,
4779
4598
  id,
4780
4599
  `${prefix}/${args.name}/SKILL`
4781
4600
  );
4782
- skillContent = page.content;
4783
4601
  break;
4784
4602
  } catch {
4785
4603
  }
4786
4604
  }
4787
- if (!skillContent) {
4788
- return `Skill "${args.name}" not found in wiki. Use gitlab_skill_list to see available skills.`;
4605
+ if (!skillPage) {
4606
+ return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4789
4607
  }
4790
- (0, import_fs2.mkdirSync)(targetDir, { recursive: true });
4791
- (0, import_fs2.writeFileSync)((0, import_path2.join)(targetDir, "SKILL.md"), skillContent);
4792
- const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
4608
+ const { meta, body } = parseFrontmatter(skillPage.content);
4609
+ (0, import_fs3.mkdirSync)(targetDir, { recursive: true });
4610
+ (0, import_fs3.writeFileSync)((0, import_path3.join)(targetDir, "SKILL.md"), body);
4611
+ const pages = await listWikiPages(
4612
+ auth.instanceUrl,
4613
+ auth.token,
4614
+ scope,
4615
+ id,
4616
+ true
4617
+ );
4793
4618
  let refCount = 0;
4794
4619
  for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4795
4620
  const skillPagePrefix = `${prefix}/${args.name}/`;
@@ -4798,59 +4623,36 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4798
4623
  );
4799
4624
  for (const page of extraPages) {
4800
4625
  const relPath = page.slug.slice(skillPagePrefix.length);
4801
- const filePath = (0, import_path2.join)(targetDir, `${relPath}.md`);
4802
- (0, import_fs2.mkdirSync)((0, import_path2.dirname)(filePath), { recursive: true });
4803
- (0, import_fs2.writeFileSync)(filePath, page.content);
4626
+ const filePath = (0, import_path3.join)(targetDir, `${relPath}.md`);
4627
+ (0, import_fs3.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
4628
+ (0, import_fs3.writeFileSync)(filePath, page.content);
4804
4629
  refCount++;
4805
4630
  }
4806
4631
  }
4807
4632
  let scriptCount = 0;
4808
- const indexEntries = await readIndex(
4809
- auth.instanceUrl,
4810
- auth.token,
4811
- scope,
4812
- id,
4813
- SKILLS_INDEX
4814
- );
4815
- const draftsEntries = await readIndex(
4816
- auth.instanceUrl,
4817
- auth.token,
4818
- scope,
4819
- id,
4820
- DRAFTS_INDEX
4821
- );
4822
- const entry = [...indexEntries, ...draftsEntries].find((e) => e.name === args.name);
4823
- if (entry?.snippetId) {
4633
+ if (meta.snippetId) {
4824
4634
  const bundleFiles = await listSnippetFiles(
4825
4635
  auth.instanceUrl,
4826
4636
  auth.token,
4827
4637
  args.project_id,
4828
- entry.snippetId
4638
+ meta.snippetId
4829
4639
  );
4830
4640
  for (const bf of bundleFiles) {
4831
4641
  const raw = await getSnippetFileRaw(
4832
4642
  auth.instanceUrl,
4833
4643
  auth.token,
4834
4644
  args.project_id,
4835
- entry.snippetId,
4645
+ meta.snippetId,
4836
4646
  bf.path
4837
4647
  );
4838
4648
  if (bf.path.endsWith(".bundle")) {
4839
- const unpacked = unpackFiles(raw);
4840
- for (const file of unpacked) {
4841
- const filePath = (0, import_path2.join)(targetDir, file.path);
4842
- (0, import_fs2.mkdirSync)((0, import_path2.dirname)(filePath), { recursive: true });
4843
- (0, import_fs2.writeFileSync)(filePath, file.content);
4844
- if (file.path.endsWith(".sh")) {
4845
- (0, import_fs2.chmodSync)(filePath, 493);
4846
- }
4649
+ for (const file of unpackFiles(raw)) {
4650
+ const filePath = (0, import_path3.join)(targetDir, file.path);
4651
+ (0, import_fs3.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
4652
+ (0, import_fs3.writeFileSync)(filePath, file.content);
4653
+ if (file.path.endsWith(".sh")) (0, import_fs3.chmodSync)(filePath, 493);
4847
4654
  scriptCount++;
4848
4655
  }
4849
- } else {
4850
- const filePath = (0, import_path2.join)(targetDir, bf.path);
4851
- (0, import_fs2.mkdirSync)((0, import_path2.dirname)(filePath), { recursive: true });
4852
- (0, import_fs2.writeFileSync)(filePath, raw);
4853
- scriptCount++;
4854
4656
  }
4855
4657
  }
4856
4658
  }
@@ -4858,18 +4660,18 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4858
4660
  const parts = ["SKILL.md"];
4859
4661
  if (refCount > 0) parts.push(`${refCount} reference(s)`);
4860
4662
  if (scriptCount > 0) parts.push(`${scriptCount} script(s)`);
4861
- return `Skill "${args.name}" extracted to ${targetDir} (${parts.join(", ")}). OpenCode will auto-discover it on next session.`;
4663
+ return `Skill "${args.name}" extracted to ${targetDir} (${parts.join(", ")}).`;
4862
4664
  } catch (err) {
4863
4665
  return `Error setting up skill: ${err.message}`;
4864
4666
  }
4865
4667
  }
4866
4668
  }),
4867
4669
  gitlab_skill_delete: (0, import_plugin6.tool)({
4868
- 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.",
4670
+ description: "Delete a skill and its associated snippet.\nRemoves all wiki pages under the skill directory.",
4869
4671
  args: {
4870
4672
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4871
4673
  name: z6.string().describe("Skill name to delete"),
4872
- draft: z6.boolean().optional().describe("Delete from drafts instead of published (default: false)"),
4674
+ draft: z6.boolean().optional().describe("Delete from drafts (default: false)"),
4873
4675
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4874
4676
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4875
4677
  },
@@ -4877,36 +4679,36 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4877
4679
  const auth = authAndValidate(args.project_id);
4878
4680
  const { scope, id } = resolveScope2(args);
4879
4681
  const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4880
- const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4881
4682
  const skillPrefix = `${prefix}/${args.name}/`;
4882
4683
  try {
4883
4684
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4884
4685
  const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix));
4885
4686
  if (skillPages.length === 0) {
4886
- return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4687
+ return `Skill "${args.name}" not found.`;
4887
4688
  }
4689
+ const skillPage = await getWikiPage(
4690
+ auth.instanceUrl,
4691
+ auth.token,
4692
+ scope,
4693
+ id,
4694
+ `${prefix}/${args.name}/SKILL`
4695
+ );
4696
+ const { meta } = parseFrontmatter(skillPage.content);
4888
4697
  for (const page of skillPages) {
4889
4698
  await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4890
4699
  }
4891
- const indexEntries = await readIndex(auth.instanceUrl, auth.token, scope, id, indexSlug);
4892
- const entry = indexEntries.find((e) => e.name === args.name);
4893
- let snippetDeleted = false;
4894
- if (entry?.snippetId) {
4700
+ if (meta.snippetId) {
4895
4701
  try {
4896
4702
  await deleteProjectSnippet(
4897
4703
  auth.instanceUrl,
4898
4704
  auth.token,
4899
4705
  args.project_id,
4900
- entry.snippetId
4706
+ meta.snippetId
4901
4707
  );
4902
- snippetDeleted = true;
4903
4708
  } catch {
4904
4709
  }
4905
4710
  }
4906
- await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
4907
- const parts = [`${skillPages.length} wiki page(s)`];
4908
- if (snippetDeleted) parts.push("snippet");
4909
- return `Deleted skill "${args.name}" (${parts.join(" + ")} removed).`;
4711
+ return `Deleted skill "${args.name}" (${skillPages.length} page(s)${meta.snippetId ? " + snippet" : ""}).`;
4910
4712
  } catch (err) {
4911
4713
  return `Error deleting skill: ${err.message}`;
4912
4714
  }