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