opencode-gitlab-dap 1.16.3 → 1.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3950,237 +3950,68 @@ ${e.content}`).join("\n\n---\n\n");
3950
3950
 
3951
3951
  // src/tools/skill-tools.ts
3952
3952
  var import_plugin6 = require("@opencode-ai/plugin");
3953
+ var import_fs3 = require("fs");
3954
+ var import_path3 = require("path");
3953
3955
 
3954
- // src/snippets.ts
3955
- function snippetApi(instanceUrl, token, projectId) {
3956
- const base = instanceUrl.replace(/\/$/, "");
3957
- const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
3958
- return {
3959
- url: `${base}/api/v4/projects/${encoded}/snippets`,
3960
- headers: {
3961
- "Content-Type": "application/json",
3962
- Authorization: `Bearer ${token}`
3963
- }
3964
- };
3965
- }
3966
- async function handleResponse2(res) {
3967
- if (!res.ok) {
3968
- const text = await res.text();
3969
- throw new Error(`Snippet API error (${res.status}): ${text}`);
3970
- }
3971
- return res.json();
3972
- }
3973
- async function createProjectSnippet(instanceUrl, token, projectId, title, description, files, visibility = "private") {
3974
- const { url, headers } = snippetApi(instanceUrl, token, projectId);
3975
- const res = await fetch(url, {
3976
- method: "POST",
3977
- headers,
3978
- body: JSON.stringify({ title, description, visibility, files })
3979
- });
3980
- return handleResponse2(res);
3981
- }
3982
- async function deleteProjectSnippet(instanceUrl, token, projectId, snippetId) {
3983
- const { url, headers } = snippetApi(instanceUrl, token, projectId);
3984
- const res = await fetch(`${url}/${snippetId}`, { method: "DELETE", headers });
3985
- if (!res.ok) {
3986
- const text = await res.text();
3987
- throw new Error(`Snippet API error (${res.status}): ${text}`);
3988
- }
3989
- }
3990
- async function getSnippetFileRaw(instanceUrl, token, projectId, snippetId, filePath, ref = "main") {
3991
- const base = instanceUrl.replace(/\/$/, "");
3992
- const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
3993
- const encodedPath = encodeURIComponent(filePath);
3994
- const url = `${base}/api/v4/projects/${encoded}/snippets/${snippetId}/files/${ref}/${encodedPath}/raw`;
3995
- const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
3996
- if (!res.ok) {
3997
- const text = await res.text();
3998
- throw new Error(`Snippet file API error (${res.status}): ${text}`);
3999
- }
4000
- return res.text();
4001
- }
4002
- async function listSnippetFiles(instanceUrl, token, projectId, snippetId) {
4003
- const base = instanceUrl.replace(/\/$/, "");
4004
- const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
4005
- const url = `${base}/api/v4/projects/${encoded}/snippets/${snippetId}`;
4006
- const res = await fetch(url, {
4007
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }
4008
- });
4009
- const snippet = await handleResponse2(res);
4010
- if (snippet.files) {
4011
- return snippet.files.map((f) => ({ path: f.path, raw_url: f.raw_url }));
4012
- }
4013
- if (snippet.file_name) {
4014
- return [{ path: snippet.file_name, raw_url: snippet.raw_url }];
4015
- }
4016
- return [];
4017
- }
4018
-
4019
- // src/tools/skill-tools.ts
3956
+ // src/tools/skill-helpers.ts
4020
3957
  var import_child_process = require("child_process");
4021
3958
  var import_fs2 = require("fs");
4022
3959
  var import_path2 = require("path");
4023
3960
  var import_os2 = require("os");
4024
3961
  var import_zlib = require("zlib");
4025
- var z6 = import_plugin6.tool.schema;
4026
3962
  var PREFIX2 = "agents";
4027
3963
  var SKILLS_PREFIX = `${PREFIX2}/skills`;
4028
3964
  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
- }
3965
+ var EMPTY_FILE_SENTINEL = "__EMPTY_FILE__";
3966
+ function parseFrontmatter(content) {
3967
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
3968
+ if (!match) return { meta: {}, body: content };
3969
+ const meta = {};
3970
+ for (const line of match[1].split("\n")) {
3971
+ const [key, ...rest] = line.split(":");
3972
+ const val = rest.join(":").trim();
3973
+ if (!key || !val) continue;
3974
+ const k = key.trim();
3975
+ if (k === "name") meta.name = val;
3976
+ else if (k === "description") meta.description = val;
3977
+ else if (k === "source") meta.source = val;
4108
3978
  }
3979
+ return { meta, body: match[2] };
4109
3980
  }
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);
3981
+ function formatFrontmatter(meta, body) {
3982
+ const lines = ["---", `name: ${meta.name}`, `description: ${meta.description}`];
3983
+ if (meta.source) lines.push(`source: ${meta.source}`);
3984
+ lines.push("---", "");
3985
+ return lines.join("\n") + body;
4119
3986
  }
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
- }
3987
+ function extractSkillNameFromSlug(slug, prefix) {
3988
+ if (!slug.startsWith(prefix + "/") || !slug.endsWith("/SKILL")) return null;
3989
+ const middle = slug.slice(prefix.length + 1, -"/SKILL".length);
3990
+ return middle && !middle.includes("/") ? middle : null;
4126
3991
  }
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
- }
3992
+ function isMarkdownFile(path) {
3993
+ return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
4153
3994
  }
4154
- var EMPTY_FILE_SENTINEL = "__EMPTY_FILE__";
4155
3995
  function packFiles(files) {
4156
3996
  const manifest = files.map((f) => ({
4157
3997
  path: f.path,
4158
3998
  content: f.content.trim() ? f.content : EMPTY_FILE_SENTINEL
4159
3999
  }));
4160
- const json = JSON.stringify(manifest);
4161
- return (0, import_zlib.gzipSync)(Buffer.from(json, "utf-8")).toString("base64");
4000
+ return (0, import_zlib.gzipSync)(Buffer.from(JSON.stringify(manifest), "utf-8")).toString("base64");
4162
4001
  }
4163
4002
  function unpackFiles(packed) {
4164
4003
  const json = (0, import_zlib.gunzipSync)(Buffer.from(packed, "base64")).toString("utf-8");
4165
- const manifest = JSON.parse(json);
4166
- return manifest.map((f) => ({
4004
+ return JSON.parse(json).map((f) => ({
4167
4005
  path: f.path,
4168
4006
  content: f.content === EMPTY_FILE_SENTINEL ? "" : f.content
4169
4007
  }));
4170
4008
  }
4171
- function isMarkdownFile(path) {
4172
- return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
4173
- }
4174
- function isAgentsGitignored(dir) {
4009
+ function ensureGitignore(dir) {
4175
4010
  try {
4176
4011
  (0, import_child_process.execSync)("git check-ignore -q .agents", { cwd: dir, stdio: "pipe" });
4177
- return true;
4012
+ return;
4178
4013
  } catch {
4179
- return false;
4180
4014
  }
4181
- }
4182
- function ensureGitignore(dir) {
4183
- if (isAgentsGitignored(dir)) return;
4184
4015
  const gitignorePath = (0, import_path2.join)(dir, ".gitignore");
4185
4016
  if ((0, import_fs2.existsSync)(gitignorePath)) {
4186
4017
  const content = (0, import_fs2.readFileSync)(gitignorePath, "utf-8");
@@ -4233,10 +4064,9 @@ function downloadSkillFromSkillsSh(identifier) {
4233
4064
  if (dirs.length === 0) return null;
4234
4065
  const skillName = dirs[0];
4235
4066
  const skillDir = (0, import_path2.join)(agentsDir, skillName);
4236
- const skillMd = (0, import_path2.join)(skillDir, "SKILL.md");
4237
4067
  let mainContent;
4238
4068
  try {
4239
- mainContent = (0, import_fs2.readFileSync)(skillMd, "utf-8");
4069
+ mainContent = (0, import_fs2.readFileSync)((0, import_path2.join)(skillDir, "SKILL.md"), "utf-8");
4240
4070
  } catch {
4241
4071
  return null;
4242
4072
  }
@@ -4245,8 +4075,7 @@ function downloadSkillFromSkillsSh(identifier) {
4245
4075
  if (descMatch) {
4246
4076
  description = descMatch[1].trim();
4247
4077
  } 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);
4078
+ description = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim().slice(0, 200);
4250
4079
  }
4251
4080
  const files = [];
4252
4081
  const walkStack = [{ dir: skillDir, prefix: "" }];
@@ -4275,6 +4104,51 @@ function downloadSkillFromSkillsSh(identifier) {
4275
4104
  }
4276
4105
  }
4277
4106
  }
4107
+
4108
+ // src/tools/skill-tools.ts
4109
+ var z6 = import_plugin6.tool.schema;
4110
+ var PROJECT_ID_DESC2 = "Project path from git remote";
4111
+ function resolveScope2(args) {
4112
+ if (args.scope === "groups" && args.group_id) {
4113
+ return { scope: "groups", id: args.group_id };
4114
+ }
4115
+ return { scope: "projects", id: args.project_id };
4116
+ }
4117
+ function validateProjectId2(projectId) {
4118
+ if (!projectId.includes("/")) {
4119
+ return `Invalid project_id "${projectId}". Must be the full project path containing at least one slash.`;
4120
+ }
4121
+ return null;
4122
+ }
4123
+ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
4124
+ try {
4125
+ await updateWikiPage(instanceUrl, token, scope, id, slug, content);
4126
+ } catch (err) {
4127
+ if (err.message?.includes("not found") || err.message?.includes("404")) {
4128
+ await createWikiPage(instanceUrl, token, scope, id, slug, content);
4129
+ return;
4130
+ }
4131
+ throw err;
4132
+ }
4133
+ }
4134
+ async function listSkills(instanceUrl, token, scope, id, prefix) {
4135
+ const pages = await listWikiPages(instanceUrl, token, scope, id, true);
4136
+ const skills = [];
4137
+ for (const page of pages) {
4138
+ const name = extractSkillNameFromSlug(page.slug, prefix);
4139
+ if (!name || !page.content) continue;
4140
+ const { meta } = parseFrontmatter(page.content);
4141
+ const hasBundle = pages.some((p) => p.slug === `${prefix}/${name}/bundle`);
4142
+ skills.push({
4143
+ name: meta.name ?? name,
4144
+ description: meta.description ?? "",
4145
+ source: meta.source,
4146
+ hasScripts: hasBundle,
4147
+ draft: prefix === DRAFTS_PREFIX
4148
+ });
4149
+ }
4150
+ return skills;
4151
+ }
4278
4152
  function makeSkillTools(ctx) {
4279
4153
  function authAndValidate(projectId) {
4280
4154
  const auth = ctx.ensureAuth();
@@ -4285,7 +4159,7 @@ function makeSkillTools(ctx) {
4285
4159
  }
4286
4160
  return {
4287
4161
  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).",
4162
+ description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks.",
4289
4163
  args: {
4290
4164
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4291
4165
  include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
@@ -4296,12 +4170,16 @@ function makeSkillTools(ctx) {
4296
4170
  const auth = authAndValidate(args.project_id);
4297
4171
  const { scope, id } = resolveScope2(args);
4298
4172
  try {
4299
- const published = await readIndex(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX);
4173
+ const published = await listSkills(
4174
+ auth.instanceUrl,
4175
+ auth.token,
4176
+ scope,
4177
+ id,
4178
+ SKILLS_PREFIX
4179
+ );
4300
4180
  let drafts = [];
4301
4181
  if (args.include_drafts) {
4302
- drafts = (await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX)).map(
4303
- (e) => ({ ...e, draft: true })
4304
- );
4182
+ drafts = await listSkills(auth.instanceUrl, auth.token, scope, id, DRAFTS_PREFIX);
4305
4183
  }
4306
4184
  const all = [...published, ...drafts];
4307
4185
  if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
@@ -4312,18 +4190,17 @@ function makeSkillTools(ctx) {
4312
4190
  }
4313
4191
  }),
4314
4192
  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.",
4193
+ 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
4194
  args: {
4317
4195
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4318
- name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
4196
+ name: z6.string().describe('Skill name (e.g., "incident-retro")'),
4319
4197
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4320
4198
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4321
4199
  },
4322
4200
  execute: async (args) => {
4323
4201
  const auth = authAndValidate(args.project_id);
4324
4202
  const { scope, id } = resolveScope2(args);
4325
- const prefixes = [SKILLS_PREFIX, DRAFTS_PREFIX];
4326
- for (const prefix of prefixes) {
4203
+ for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4327
4204
  try {
4328
4205
  const page = await getWikiPage(
4329
4206
  auth.instanceUrl,
@@ -4332,36 +4209,29 @@ function makeSkillTools(ctx) {
4332
4209
  id,
4333
4210
  `${prefix}/${args.name}/SKILL`
4334
4211
  );
4212
+ const { body } = parseFrontmatter(page.content);
4213
+ const isDraft = prefix === DRAFTS_PREFIX;
4335
4214
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4336
4215
  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;
4216
+ const refs = pages.filter(
4217
+ (p) => p.slug.startsWith(skillPrefix) && p.slug !== `${prefix}/${args.name}/SKILL`
4218
+ ).map((p) => p.slug.slice(skillPrefix.length));
4340
4219
  let result = isDraft ? `[DRAFT SKILL]
4341
4220
 
4342
- ${page.content}` : page.content;
4221
+ ${body}` : body;
4343
4222
  if (refs.length > 0) {
4344
4223
  result += `
4345
4224
 
4346
4225
  ---
4347
4226
  Available references: ${refs.join(", ")}`;
4348
4227
  }
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) {
4228
+ const hasBundle = refs.some((r) => r === "bundle");
4229
+ if (hasBundle) {
4359
4230
  result += `
4360
4231
 
4361
- ---
4362
- This skill has executable scripts in snippet #${entry.snippetId}.`;
4232
+ This skill has executable scripts.`;
4363
4233
  result += `
4364
- Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skills/${args.name}/ for execution.`;
4234
+ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them locally.`;
4365
4235
  }
4366
4236
  return result;
4367
4237
  } catch {
@@ -4372,13 +4242,13 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4372
4242
  }
4373
4243
  }),
4374
4244
  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.",
4245
+ description: "Create or update a skill.\nUse draft=true for skills that haven't been proven yet.",
4376
4246
  args: {
4377
4247
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4378
4248
  name: z6.string().describe('Skill name (e.g., "incident-retro")'),
4379
4249
  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)"),
4250
+ description: z6.string().describe("Short description (1-2 sentences)"),
4251
+ draft: z6.boolean().optional().describe("Save as draft (default: false)"),
4382
4252
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4383
4253
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4384
4254
  },
@@ -4386,31 +4256,25 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4386
4256
  const auth = authAndValidate(args.project_id);
4387
4257
  const { scope, id } = resolveScope2(args);
4388
4258
  const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4389
- const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4390
4259
  const slug = `${prefix}/${args.name}/SKILL`;
4391
- const label = args.draft ? "draft " : "";
4260
+ const body = formatFrontmatter(
4261
+ { name: args.name, description: args.description, source: "project" },
4262
+ args.content
4263
+ );
4392
4264
  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}`;
4265
+ await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, body);
4266
+ return `Saved ${args.draft ? "draft " : ""}skill: ${args.name}`;
4401
4267
  } catch (err) {
4402
4268
  return `Error saving skill: ${err.message}`;
4403
4269
  }
4404
4270
  }
4405
4271
  }),
4406
4272
  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.",
4273
+ description: "Promote a skill.\nDefault (target='published'): moves a draft to published.\nTarget 'group': moves a project skill to the group wiki.",
4408
4274
  args: {
4409
4275
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4410
4276
  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
- ),
4277
+ target: z6.enum(["published", "group"]).optional().describe('"published" (default) or "group"'),
4414
4278
  group_id: z6.string().optional().describe("Group path (required when target is group)"),
4415
4279
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)")
4416
4280
  },
@@ -4418,9 +4282,7 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4418
4282
  const auth = authAndValidate(args.project_id);
4419
4283
  const promotionTarget = args.target ?? "published";
4420
4284
  if (promotionTarget === "group") {
4421
- if (!args.group_id) {
4422
- return 'Error: group_id is required when target is "group".';
4423
- }
4285
+ if (!args.group_id) return 'Error: group_id is required when target is "group".';
4424
4286
  const projectScope = resolveScope2(args);
4425
4287
  try {
4426
4288
  const pages = await listWikiPages(
@@ -4431,11 +4293,9 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4431
4293
  true
4432
4294
  );
4433
4295
  const skillPrefix = `${SKILLS_PREFIX}/${args.name}/`;
4434
- const skillPages = pages.filter(
4435
- (p) => p.slug.startsWith(skillPrefix) && p.content
4436
- );
4296
+ const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix) && p.content);
4437
4297
  if (skillPages.length === 0) {
4438
- return `Skill "${args.name}" not found in project. Use gitlab_skill_list to see available skills.`;
4298
+ return `Skill "${args.name}" not found in project.`;
4439
4299
  }
4440
4300
  for (const page of skillPages) {
4441
4301
  await upsertPage(
@@ -4447,54 +4307,6 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4447
4307
  page.content
4448
4308
  );
4449
4309
  }
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
4310
  for (const page of skillPages) {
4499
4311
  await deleteWikiPage(
4500
4312
  auth.instanceUrl,
@@ -4504,55 +4316,30 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4504
4316
  page.slug
4505
4317
  );
4506
4318
  }
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).`;
4319
+ return `Promoted skill "${args.name}" to group "${args.group_id}". ${skillPages.length} page(s) moved.`;
4527
4320
  } catch (err) {
4528
4321
  return `Error promoting skill to group: ${err.message}`;
4529
4322
  }
4530
4323
  }
4531
4324
  const { scope, id } = resolveScope2(args);
4532
4325
  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
4326
+ const pages = await listWikiPages(
4327
+ auth.instanceUrl,
4328
+ auth.token,
4329
+ scope,
4330
+ id,
4331
+ true
4537
4332
  );
4333
+ const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
4334
+ const draftPages = pages.filter((p) => p.slug.startsWith(draftPrefix) && p.content);
4538
4335
  if (draftPages.length === 0) {
4539
- return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
4336
+ return `Draft skill "${args.name}" not found.`;
4540
4337
  }
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
4338
  for (const page of draftPages) {
4545
4339
  const newSlug = page.slug.replace(DRAFTS_PREFIX, SKILLS_PREFIX);
4546
4340
  await upsertPage(auth.instanceUrl, auth.token, scope, id, newSlug, page.content);
4547
4341
  await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4548
4342
  }
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
4343
  return `Promoted skill "${args.name}" from draft to published.`;
4557
4344
  } catch (err) {
4558
4345
  return `Error promoting skill: ${err.message}`;
@@ -4560,11 +4347,11 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4560
4347
  }
4561
4348
  }),
4562
4349
  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.",
4350
+ description: "Search for skills in skills.sh and optionally a group wiki.\nUse gitlab_skill_install to install a discovered skill.",
4564
4351
  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)")
4352
+ query: z6.string().describe("Search query"),
4353
+ project_id: z6.string().optional().describe(PROJECT_ID_DESC2 + " Only needed for group search."),
4354
+ group_id: z6.string().optional().describe("Group path to search")
4568
4355
  },
4569
4356
  execute: async (args) => {
4570
4357
  let auth = null;
@@ -4576,15 +4363,15 @@ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skill
4576
4363
  const sections = [];
4577
4364
  if (args.group_id && auth) {
4578
4365
  try {
4579
- const entries = await readIndex(
4366
+ const skills = await listSkills(
4580
4367
  auth.instanceUrl,
4581
4368
  auth.token,
4582
4369
  "groups",
4583
4370
  args.group_id,
4584
- SKILLS_INDEX
4371
+ SKILLS_PREFIX
4585
4372
  );
4586
4373
  const q = args.query.toLowerCase();
4587
- const matches = entries.filter(
4374
+ const matches = skills.filter(
4588
4375
  (e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
4589
4376
  );
4590
4377
  if (matches.length > 0) {
@@ -4613,19 +4400,17 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4613
4400
  );
4614
4401
  }
4615
4402
  if (sections.length === 0) {
4616
- return `No skills found matching "${args.query}" in ${args.group_id ? "group wiki or " : ""}skills.sh.`;
4403
+ return `No skills found matching "${args.query}".`;
4617
4404
  }
4618
4405
  return sections.join("\n\n---\n\n");
4619
4406
  }
4620
4407
  }),
4621
4408
  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.",
4409
+ 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
4410
  args: {
4624
4411
  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)'),
4412
+ name: z6.string().describe("Skill identifier"),
4413
+ source: z6.enum(["group", "skills.sh"]).describe("Where to install from"),
4629
4414
  group_id: z6.string().optional().describe("Group path (required when source is group)"),
4630
4415
  draft: z6.boolean().optional().describe("Install as draft (default: false)")
4631
4416
  },
@@ -4633,24 +4418,32 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4633
4418
  const auth = authAndValidate(args.project_id);
4634
4419
  const projectScope = resolveScope2(args);
4635
4420
  const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4636
- const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4637
4421
  if (args.source === "skills.sh") {
4638
4422
  const downloaded = downloadSkillFromSkillsSh(args.name);
4639
4423
  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").`;
4424
+ return `Failed to download skill "${args.name}" from skills.sh.`;
4641
4425
  }
4642
4426
  try {
4427
+ const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
4428
+ const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
4429
+ const hasBundle = scriptFiles.length > 0;
4430
+ const skillBody = formatFrontmatter(
4431
+ {
4432
+ name: downloaded.name,
4433
+ description: downloaded.description,
4434
+ source: `skills.sh:${args.name}`
4435
+ },
4436
+ downloaded.content.replace(/^---[\s\S]*?---\s*\n/, "")
4437
+ );
4643
4438
  await upsertPage(
4644
4439
  auth.instanceUrl,
4645
4440
  auth.token,
4646
4441
  projectScope.scope,
4647
4442
  projectScope.id,
4648
4443
  `${targetPrefix}/${downloaded.name}/SKILL`,
4649
- downloaded.content
4444
+ skillBody
4650
4445
  );
4651
4446
  let wikiCount = 1;
4652
- const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
4653
- const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
4654
4447
  for (const file of mdFiles) {
4655
4448
  const slug = `${targetPrefix}/${downloaded.name}/${file.path.replace(/\.[^.]+$/, "")}`;
4656
4449
  await upsertPage(
@@ -4663,44 +4456,26 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4663
4456
  );
4664
4457
  wikiCount++;
4665
4458
  }
4666
- let snippetId;
4667
- if (scriptFiles.length > 0) {
4668
- const packed = packFiles(scriptFiles);
4669
- const snippet = await createProjectSnippet(
4459
+ if (hasBundle) {
4460
+ const bundleSlug = `${targetPrefix}/${downloaded.name}/bundle`;
4461
+ await upsertPage(
4670
4462
  auth.instanceUrl,
4671
4463
  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"
4464
+ projectScope.scope,
4465
+ projectScope.id,
4466
+ bundleSlug,
4467
+ packFiles(scriptFiles)
4677
4468
  );
4678
- snippetId = snippet.id;
4469
+ wikiCount++;
4679
4470
  }
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
4471
  const parts = [`${wikiCount} wiki page(s)`];
4695
- 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.`;
4472
+ if (hasBundle) parts.push(`${scriptFiles.length} bundled script(s)`);
4473
+ return `Installed skill "${downloaded.name}" from skills.sh. ${parts.join(", ")}. Use gitlab_skill_setup to extract scripts.`;
4697
4474
  } catch (err) {
4698
- return `Error installing skill from skills.sh: ${err.message}`;
4475
+ return `Error installing from skills.sh: ${err.message}`;
4699
4476
  }
4700
4477
  }
4701
- if (!args.group_id) {
4702
- return 'Error: group_id is required when source is "group".';
4703
- }
4478
+ if (!args.group_id) return 'Error: group_id is required when source is "group".';
4704
4479
  try {
4705
4480
  const groupPages = await listWikiPages(
4706
4481
  auth.instanceUrl,
@@ -4710,9 +4485,7 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4710
4485
  true
4711
4486
  );
4712
4487
  const sourcePrefix = `${SKILLS_PREFIX}/${args.name}/`;
4713
- const skillPages = groupPages.filter(
4714
- (p) => p.slug.startsWith(sourcePrefix) && p.content
4715
- );
4488
+ const skillPages = groupPages.filter((p) => p.slug.startsWith(sourcePrefix) && p.content);
4716
4489
  if (skillPages.length === 0) {
4717
4490
  return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
4718
4491
  }
@@ -4727,39 +4500,17 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4727
4500
  page.content
4728
4501
  );
4729
4502
  }
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.`;
4503
+ return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s).`;
4753
4504
  } catch (err) {
4754
- return `Error installing skill: ${err.message}`;
4505
+ return `Error installing from group: ${err.message}`;
4755
4506
  }
4756
4507
  }
4757
4508
  }),
4758
4509
  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.",
4510
+ 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
4511
  args: {
4761
4512
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4762
- name: z6.string().describe("Skill name to set up locally"),
4513
+ name: z6.string().describe("Skill name to set up"),
4763
4514
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4764
4515
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4765
4516
  },
@@ -4767,29 +4518,35 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4767
4518
  const auth = authAndValidate(args.project_id);
4768
4519
  const { scope, id } = resolveScope2(args);
4769
4520
  const workDir = ctx.getDirectory();
4770
- const targetDir = (0, import_path2.join)(workDir, ".agents", "skills", args.name);
4521
+ const targetDir = (0, import_path3.join)(workDir, ".agents", "skills", args.name);
4771
4522
  try {
4772
- let skillContent = null;
4523
+ let skillPage = null;
4773
4524
  for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4774
4525
  try {
4775
- const page = await getWikiPage(
4526
+ skillPage = await getWikiPage(
4776
4527
  auth.instanceUrl,
4777
4528
  auth.token,
4778
4529
  scope,
4779
4530
  id,
4780
4531
  `${prefix}/${args.name}/SKILL`
4781
4532
  );
4782
- skillContent = page.content;
4783
4533
  break;
4784
4534
  } catch {
4785
4535
  }
4786
4536
  }
4787
- if (!skillContent) {
4788
- return `Skill "${args.name}" not found in wiki. Use gitlab_skill_list to see available skills.`;
4537
+ if (!skillPage) {
4538
+ return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4789
4539
  }
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);
4540
+ const { body } = parseFrontmatter(skillPage.content);
4541
+ (0, import_fs3.mkdirSync)(targetDir, { recursive: true });
4542
+ (0, import_fs3.writeFileSync)((0, import_path3.join)(targetDir, "SKILL.md"), body);
4543
+ const pages = await listWikiPages(
4544
+ auth.instanceUrl,
4545
+ auth.token,
4546
+ scope,
4547
+ id,
4548
+ true
4549
+ );
4793
4550
  let refCount = 0;
4794
4551
  for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4795
4552
  const skillPagePrefix = `${prefix}/${args.name}/`;
@@ -4798,78 +4555,51 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4798
4555
  );
4799
4556
  for (const page of extraPages) {
4800
4557
  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);
4558
+ const filePath = (0, import_path3.join)(targetDir, `${relPath}.md`);
4559
+ (0, import_fs3.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
4560
+ (0, import_fs3.writeFileSync)(filePath, page.content);
4804
4561
  refCount++;
4805
4562
  }
4806
4563
  }
4807
4564
  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) {
4824
- const bundleFiles = await listSnippetFiles(
4825
- auth.instanceUrl,
4826
- auth.token,
4827
- args.project_id,
4828
- entry.snippetId
4829
- );
4830
- for (const bf of bundleFiles) {
4831
- const raw = await getSnippetFileRaw(
4565
+ for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4566
+ try {
4567
+ const bundlePage = await getWikiPage(
4832
4568
  auth.instanceUrl,
4833
4569
  auth.token,
4834
- args.project_id,
4835
- entry.snippetId,
4836
- bf.path
4570
+ scope,
4571
+ id,
4572
+ `${prefix}/${args.name}/bundle`
4837
4573
  );
4838
- 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
- }
4574
+ if (bundlePage.content) {
4575
+ for (const file of unpackFiles(bundlePage.content)) {
4576
+ const filePath = (0, import_path3.join)(targetDir, file.path);
4577
+ (0, import_fs3.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
4578
+ (0, import_fs3.writeFileSync)(filePath, file.content);
4579
+ if (file.path.endsWith(".sh")) (0, import_fs3.chmodSync)(filePath, 493);
4847
4580
  scriptCount++;
4848
4581
  }
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
4582
  }
4583
+ break;
4584
+ } catch {
4855
4585
  }
4856
4586
  }
4857
4587
  ensureGitignore(workDir);
4858
4588
  const parts = ["SKILL.md"];
4859
4589
  if (refCount > 0) parts.push(`${refCount} reference(s)`);
4860
4590
  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.`;
4591
+ return `Skill "${args.name}" extracted to ${targetDir} (${parts.join(", ")}).`;
4862
4592
  } catch (err) {
4863
4593
  return `Error setting up skill: ${err.message}`;
4864
4594
  }
4865
4595
  }
4866
4596
  }),
4867
4597
  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.",
4598
+ description: "Delete a skill and its associated snippet.\nRemoves all wiki pages under the skill directory.",
4869
4599
  args: {
4870
4600
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4871
4601
  name: z6.string().describe("Skill name to delete"),
4872
- draft: z6.boolean().optional().describe("Delete from drafts instead of published (default: false)"),
4602
+ draft: z6.boolean().optional().describe("Delete from drafts (default: false)"),
4873
4603
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4874
4604
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4875
4605
  },
@@ -4877,36 +4607,17 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4877
4607
  const auth = authAndValidate(args.project_id);
4878
4608
  const { scope, id } = resolveScope2(args);
4879
4609
  const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4880
- const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4881
4610
  const skillPrefix = `${prefix}/${args.name}/`;
4882
4611
  try {
4883
4612
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4884
4613
  const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix));
4885
4614
  if (skillPages.length === 0) {
4886
- return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4615
+ return `Skill "${args.name}" not found.`;
4887
4616
  }
4888
4617
  for (const page of skillPages) {
4889
4618
  await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4890
4619
  }
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) {
4895
- try {
4896
- await deleteProjectSnippet(
4897
- auth.instanceUrl,
4898
- auth.token,
4899
- args.project_id,
4900
- entry.snippetId
4901
- );
4902
- snippetDeleted = true;
4903
- } catch {
4904
- }
4905
- }
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).`;
4620
+ return `Deleted skill "${args.name}" (${skillPages.length} page(s) removed).`;
4910
4621
  } catch (err) {
4911
4622
  return `Error deleting skill: ${err.message}`;
4912
4623
  }