opencode-gitlab-dap 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,7 +110,7 @@ The plugin provides a multi-round design workflow:
110
110
 
111
111
  The vendored `flow_v2.json` schema from GitLab Rails powers client-side validation using `ajv`, catching errors before hitting the API.
112
112
 
113
- ### 31 Tools
113
+ ### 34 Tools
114
114
 
115
115
  #### DAP Tools (20)
116
116
 
@@ -157,13 +157,18 @@ Say **"bootstrap project memory"** to automatically inspect a project and build
157
157
 
158
158
  ##### Skill Tools
159
159
 
160
- | Tool | Description |
161
- | ---------------------- | ---------------------------------------------- |
162
- | `gitlab_skill_list` | List available skills (and optionally drafts) |
163
- | `gitlab_skill_load` | Load a skill (published first, then drafts) |
164
- | `gitlab_skill_save` | Create or update a skill (supports draft flag) |
165
- | `gitlab_skill_promote` | Promote a draft skill to published |
166
-
160
+ | Tool | Description |
161
+ | ----------------------- | --------------------------------------------------------- |
162
+ | `gitlab_skill_list` | List skills with auto-rebuilding index |
163
+ | `gitlab_skill_load` | Load a skill (SKILL page + available references) |
164
+ | `gitlab_skill_save` | Create/update a skill with required description for index |
165
+ | `gitlab_skill_promote` | Promote a draft skill to published (moves all pages) |
166
+ | `gitlab_skill_discover` | Search group wiki for team-shared skills |
167
+ | `gitlab_skill_install` | Copy a skill from group wiki to project wiki |
168
+ | `gitlab_skill_delete` | Delete a skill and remove from index |
169
+
170
+ Each skill is a directory: `agents/skills/<name>/SKILL` with optional `references/` subpages.
171
+ Skills are indexed in `agents/skills/index` for fast discovery.
167
172
  All tools support project scope (default) and group scope (`scope="groups"`).
168
173
 
169
174
  ### Dynamic Refresh
package/dist/index.cjs CHANGED
@@ -3951,6 +3951,9 @@ var z6 = import_plugin6.tool.schema;
3951
3951
  var PREFIX2 = "agents";
3952
3952
  var SKILLS_PREFIX = `${PREFIX2}/skills`;
3953
3953
  var DRAFTS_PREFIX = `${PREFIX2}/skills-drafts`;
3954
+ var SKILLS_INDEX = `${SKILLS_PREFIX}/index`;
3955
+ var DRAFTS_INDEX = `${DRAFTS_PREFIX}/index`;
3956
+ var PROJECT_ID_DESC2 = 'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.';
3954
3957
  function resolveScope2(args) {
3955
3958
  if (args.scope === "groups" && args.group_id) {
3956
3959
  return { scope: "groups", id: args.group_id };
@@ -3963,6 +3966,117 @@ function validateProjectId2(projectId) {
3963
3966
  }
3964
3967
  return null;
3965
3968
  }
3969
+ function parseIndex(content) {
3970
+ const entries = [];
3971
+ const blocks = content.split(/^## /m).filter(Boolean);
3972
+ for (const block of blocks) {
3973
+ const lines = block.trim().split("\n");
3974
+ const name = lines[0].trim();
3975
+ if (!name) continue;
3976
+ const rest = lines.slice(1).join("\n").trim();
3977
+ const descLines = [];
3978
+ let source;
3979
+ for (const line of rest.split("\n")) {
3980
+ if (line.startsWith("Source:")) {
3981
+ source = line.slice(7).trim();
3982
+ } else if (line.trim()) {
3983
+ descLines.push(line);
3984
+ }
3985
+ }
3986
+ entries.push({ name, description: descLines.join("\n"), source, draft: false });
3987
+ }
3988
+ return entries;
3989
+ }
3990
+ function formatIndex(entries) {
3991
+ return entries.map((e) => {
3992
+ let block = `## ${e.name}
3993
+ ${e.description}`;
3994
+ if (e.source) block += `
3995
+ Source: ${e.source}`;
3996
+ return block;
3997
+ }).join("\n\n");
3998
+ }
3999
+ async function readIndex(instanceUrl, token, scope, id, indexSlug) {
4000
+ try {
4001
+ const page = await getWikiPage(instanceUrl, token, scope, id, indexSlug);
4002
+ return parseIndex(page.content);
4003
+ } catch {
4004
+ return [];
4005
+ }
4006
+ }
4007
+ async function writeIndex(instanceUrl, token, scope, id, indexSlug, entries) {
4008
+ const content = formatIndex(entries);
4009
+ try {
4010
+ await updateWikiPage(instanceUrl, token, scope, id, indexSlug, content);
4011
+ } catch {
4012
+ await createWikiPage(instanceUrl, token, scope, id, indexSlug, content || "# Skills Index");
4013
+ }
4014
+ }
4015
+ async function upsertIndexEntry(instanceUrl, token, scope, id, indexSlug, entry) {
4016
+ const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
4017
+ const idx = entries.findIndex((e) => e.name === entry.name);
4018
+ if (idx >= 0) {
4019
+ entries[idx] = entry;
4020
+ } else {
4021
+ entries.push(entry);
4022
+ }
4023
+ await writeIndex(instanceUrl, token, scope, id, indexSlug, entries);
4024
+ }
4025
+ async function removeIndexEntry(instanceUrl, token, scope, id, indexSlug, name) {
4026
+ const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
4027
+ const filtered = entries.filter((e) => e.name !== name);
4028
+ if (filtered.length !== entries.length) {
4029
+ await writeIndex(instanceUrl, token, scope, id, indexSlug, filtered);
4030
+ }
4031
+ }
4032
+ function extractSkillNames(pages, prefix) {
4033
+ const skillSuffix = "/SKILL";
4034
+ const names = /* @__PURE__ */ new Set();
4035
+ for (const p of pages) {
4036
+ if (p.slug.startsWith(prefix + "/") && p.slug.endsWith(skillSuffix)) {
4037
+ const middle = p.slug.slice(prefix.length + 1, -skillSuffix.length);
4038
+ if (middle && !middle.includes("/")) {
4039
+ names.add(middle);
4040
+ }
4041
+ }
4042
+ }
4043
+ return [...names];
4044
+ }
4045
+ async function rebuildIndex(instanceUrl, token, scope, id, prefix, indexSlug) {
4046
+ const pages = await listWikiPages(instanceUrl, token, scope, id);
4047
+ const actualNames = extractSkillNames(pages, prefix);
4048
+ const currentEntries = await readIndex(instanceUrl, token, scope, id, indexSlug);
4049
+ const indexed = new Set(currentEntries.map((e) => e.name));
4050
+ const actual = new Set(actualNames);
4051
+ let dirty = false;
4052
+ const removed = currentEntries.filter((e) => !actual.has(e.name));
4053
+ if (removed.length > 0) dirty = true;
4054
+ const added = [];
4055
+ for (const name of actualNames) {
4056
+ if (!indexed.has(name)) {
4057
+ added.push(name);
4058
+ dirty = true;
4059
+ }
4060
+ }
4061
+ if (!dirty && added.length === 0) return currentEntries;
4062
+ const kept = currentEntries.filter((e) => actual.has(e.name));
4063
+ for (const name of added) {
4064
+ kept.push({
4065
+ name,
4066
+ description: "(auto-indexed \u2014 update description with gitlab_skill_save)",
4067
+ draft: prefix === DRAFTS_PREFIX
4068
+ });
4069
+ }
4070
+ await writeIndex(instanceUrl, token, scope, id, indexSlug, kept);
4071
+ return kept;
4072
+ }
4073
+ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
4074
+ try {
4075
+ await updateWikiPage(instanceUrl, token, scope, id, slug, content);
4076
+ } catch {
4077
+ await createWikiPage(instanceUrl, token, scope, id, slug, content);
4078
+ }
4079
+ }
3966
4080
  function makeSkillTools(ctx) {
3967
4081
  function authAndValidate(projectId) {
3968
4082
  const auth = ctx.ensureAuth();
@@ -3973,11 +4087,9 @@ function makeSkillTools(ctx) {
3973
4087
  }
3974
4088
  return {
3975
4089
  gitlab_skill_list: (0, import_plugin6.tool)({
3976
- description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).",
4090
+ description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).\nAuto-rebuilds the skill index if it is out of sync with actual skill pages.",
3977
4091
  args: {
3978
- project_id: z6.string().describe(
3979
- 'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
3980
- ),
4092
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
3981
4093
  include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
3982
4094
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3983
4095
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
@@ -3986,23 +4098,27 @@ function makeSkillTools(ctx) {
3986
4098
  const auth = authAndValidate(args.project_id);
3987
4099
  const { scope, id } = resolveScope2(args);
3988
4100
  try {
3989
- const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
3990
- const indexSlug = `${SKILLS_PREFIX}/index`;
3991
- const skills = pages.filter((p) => p.slug.startsWith(`${SKILLS_PREFIX}/`) && p.slug !== indexSlug).map((p) => ({
3992
- name: p.slug.slice(SKILLS_PREFIX.length + 1),
3993
- title: p.title,
3994
- draft: false
3995
- }));
4101
+ const published = await rebuildIndex(
4102
+ auth.instanceUrl,
4103
+ auth.token,
4104
+ scope,
4105
+ id,
4106
+ SKILLS_PREFIX,
4107
+ SKILLS_INDEX
4108
+ );
3996
4109
  let drafts = [];
3997
4110
  if (args.include_drafts) {
3998
- const draftsIndexSlug = `${DRAFTS_PREFIX}/index`;
3999
- drafts = pages.filter((p) => p.slug.startsWith(`${DRAFTS_PREFIX}/`) && p.slug !== draftsIndexSlug).map((p) => ({
4000
- name: p.slug.slice(DRAFTS_PREFIX.length + 1),
4001
- title: p.title,
4002
- draft: true
4003
- }));
4111
+ drafts = await rebuildIndex(
4112
+ auth.instanceUrl,
4113
+ auth.token,
4114
+ scope,
4115
+ id,
4116
+ DRAFTS_PREFIX,
4117
+ DRAFTS_INDEX
4118
+ );
4119
+ drafts = drafts.map((e) => ({ ...e, draft: true }));
4004
4120
  }
4005
- const all = [...skills, ...drafts];
4121
+ const all = [...published, ...drafts];
4006
4122
  if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
4007
4123
  return JSON.stringify(all, null, 2);
4008
4124
  } catch (err) {
@@ -4011,11 +4127,9 @@ function makeSkillTools(ctx) {
4011
4127
  }
4012
4128
  }),
4013
4129
  gitlab_skill_load: (0, import_plugin6.tool)({
4014
- 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.",
4130
+ 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.",
4015
4131
  args: {
4016
- project_id: z6.string().describe(
4017
- 'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
4018
- ),
4132
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
4019
4133
  name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
4020
4134
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4021
4135
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
@@ -4023,41 +4137,46 @@ function makeSkillTools(ctx) {
4023
4137
  execute: async (args) => {
4024
4138
  const auth = authAndValidate(args.project_id);
4025
4139
  const { scope, id } = resolveScope2(args);
4026
- try {
4027
- const page = await getWikiPage(
4028
- auth.instanceUrl,
4029
- auth.token,
4030
- scope,
4031
- id,
4032
- `${SKILLS_PREFIX}/${args.name}`
4033
- );
4034
- return page.content;
4035
- } catch {
4140
+ const prefixes = [SKILLS_PREFIX, DRAFTS_PREFIX];
4141
+ for (const prefix of prefixes) {
4036
4142
  try {
4037
- const draft = await getWikiPage(
4143
+ const page = await getWikiPage(
4038
4144
  auth.instanceUrl,
4039
4145
  auth.token,
4040
4146
  scope,
4041
4147
  id,
4042
- `${DRAFTS_PREFIX}/${args.name}`
4148
+ `${prefix}/${args.name}/SKILL`
4043
4149
  );
4044
- return `[DRAFT SKILL]
4150
+ const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4151
+ const refPrefix = `${prefix}/${args.name}/references/`;
4152
+ const refs = pages.filter((p) => p.slug.startsWith(refPrefix)).map((p) => p.slug.slice(refPrefix.length));
4153
+ const isDraft = prefix === DRAFTS_PREFIX;
4154
+ let result = isDraft ? `[DRAFT SKILL]
4155
+
4156
+ ${page.content}` : page.content;
4157
+ if (refs.length > 0) {
4158
+ result += `
4045
4159
 
4046
- ${draft.content}`;
4160
+ ---
4161
+ Available references: ${refs.join(", ")}`;
4162
+ result += `
4163
+ Load with: gitlab_skill_load_reference(name="${args.name}", reference="<name>")`;
4164
+ }
4165
+ return result;
4047
4166
  } catch {
4048
- return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4167
+ continue;
4049
4168
  }
4050
4169
  }
4170
+ return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4051
4171
  }
4052
4172
  }),
4053
4173
  gitlab_skill_save: (0, import_plugin6.tool)({
4054
- 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.",
4174
+ 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.",
4055
4175
  args: {
4056
- project_id: z6.string().describe(
4057
- 'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
4058
- ),
4176
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
4059
4177
  name: z6.string().describe('Skill name (e.g., "incident-retro")'),
4060
4178
  content: z6.string().describe("Skill content in markdown"),
4179
+ description: z6.string().describe("Short description for the skill index (1-2 sentences)"),
4061
4180
  draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
4062
4181
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4063
4182
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
@@ -4066,35 +4185,27 @@ ${draft.content}`;
4066
4185
  const auth = authAndValidate(args.project_id);
4067
4186
  const { scope, id } = resolveScope2(args);
4068
4187
  const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4069
- const slug = `${prefix}/${args.name}`;
4188
+ const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4189
+ const slug = `${prefix}/${args.name}/SKILL`;
4070
4190
  const label = args.draft ? "draft " : "";
4071
- for (let attempt = 0; attempt < 3; attempt++) {
4072
- try {
4073
- await updateWikiPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
4074
- return `Updated ${label}skill: ${args.name}`;
4075
- } catch {
4076
- try {
4077
- await createWikiPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
4078
- return `Created ${label}skill: ${args.name}`;
4079
- } catch (err) {
4080
- const msg = err.message ?? "";
4081
- if (msg.includes("Duplicate page") || msg.includes("reference update")) {
4082
- await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
4083
- continue;
4084
- }
4085
- return `Error saving skill: ${msg}`;
4086
- }
4087
- }
4191
+ try {
4192
+ await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
4193
+ await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, {
4194
+ name: args.name,
4195
+ description: args.description,
4196
+ source: "project",
4197
+ draft: !!args.draft
4198
+ });
4199
+ return `Saved ${label}skill: ${args.name}`;
4200
+ } catch (err) {
4201
+ return `Error saving skill: ${err.message}`;
4088
4202
  }
4089
- return `Error saving skill: failed after 3 retries`;
4090
4203
  }
4091
4204
  }),
4092
4205
  gitlab_skill_promote: (0, import_plugin6.tool)({
4093
- description: "Promote a draft skill to published.\nMoves the skill from the drafts directory to the published skills directory.",
4206
+ description: "Promote a draft skill to published.\nMoves all skill pages from drafts to published and updates both indexes.",
4094
4207
  args: {
4095
- project_id: z6.string().describe(
4096
- 'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
4097
- ),
4208
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
4098
4209
  name: z6.string().describe("Skill name to promote"),
4099
4210
  scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4100
4211
  group_id: z6.string().optional().describe("Group path (required when scope is groups)")
@@ -4102,36 +4213,170 @@ ${draft.content}`;
4102
4213
  execute: async (args) => {
4103
4214
  const auth = authAndValidate(args.project_id);
4104
4215
  const { scope, id } = resolveScope2(args);
4105
- const draftSlug = `${DRAFTS_PREFIX}/${args.name}`;
4106
- const publishedSlug = `${SKILLS_PREFIX}/${args.name}`;
4107
4216
  try {
4108
- const draft = await getWikiPage(auth.instanceUrl, auth.token, scope, id, draftSlug);
4109
- try {
4110
- await updateWikiPage(
4111
- auth.instanceUrl,
4112
- auth.token,
4113
- scope,
4114
- id,
4115
- publishedSlug,
4116
- draft.content
4117
- );
4118
- } catch {
4119
- await createWikiPage(
4217
+ const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
4218
+ const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
4219
+ const draftPages = pages.filter(
4220
+ (p) => p.slug.startsWith(draftPrefix) && p.content
4221
+ );
4222
+ if (draftPages.length === 0) {
4223
+ return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
4224
+ }
4225
+ const draftIndex = await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX);
4226
+ const entry = draftIndex.find((e) => e.name === args.name);
4227
+ const description = entry?.description ?? "(promoted from draft)";
4228
+ for (const page of draftPages) {
4229
+ const newSlug = page.slug.replace(DRAFTS_PREFIX, SKILLS_PREFIX);
4230
+ await upsertPage(auth.instanceUrl, auth.token, scope, id, newSlug, page.content);
4231
+ await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4232
+ }
4233
+ await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX, args.name);
4234
+ await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX, {
4235
+ name: args.name,
4236
+ description,
4237
+ source: "project",
4238
+ draft: false
4239
+ });
4240
+ return `Promoted skill "${args.name}" from draft to published.`;
4241
+ } catch (err) {
4242
+ return `Error promoting skill: ${err.message}`;
4243
+ }
4244
+ }
4245
+ }),
4246
+ gitlab_skill_discover: (0, import_plugin6.tool)({
4247
+ description: "Search for skills available in the group wiki.\nReads the group-level skill index and filters by query.\nUse gitlab_skill_install to copy a discovered skill to your project.",
4248
+ args: {
4249
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
4250
+ query: z6.string().describe("Search query (matches skill name and description)"),
4251
+ group_id: z6.string().describe("Group path to search for shared skills")
4252
+ },
4253
+ execute: async (args) => {
4254
+ const auth = authAndValidate(args.project_id);
4255
+ try {
4256
+ const entries = await readIndex(
4257
+ auth.instanceUrl,
4258
+ auth.token,
4259
+ "groups",
4260
+ args.group_id,
4261
+ SKILLS_INDEX
4262
+ );
4263
+ if (entries.length === 0) {
4264
+ return `No skills found in group "${args.group_id}" wiki. The group skill index (${SKILLS_INDEX}) is empty or does not exist.`;
4265
+ }
4266
+ const q = args.query.toLowerCase();
4267
+ const matches = entries.filter(
4268
+ (e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
4269
+ );
4270
+ if (matches.length === 0) {
4271
+ return `No skills matching "${args.query}" in group "${args.group_id}". Available skills:
4272
+ ${entries.map((e) => `- ${e.name}: ${e.description}`).join("\n")}`;
4273
+ }
4274
+ return `Found ${matches.length} skill(s) in group "${args.group_id}":
4275
+
4276
+ ` + matches.map(
4277
+ (e) => `**${e.name}**: ${e.description}
4278
+ Install: gitlab_skill_install(name="${e.name}", group_id="${args.group_id}")`
4279
+ ).join("\n\n");
4280
+ } catch (err) {
4281
+ return `Error discovering skills: ${err.message}`;
4282
+ }
4283
+ }
4284
+ }),
4285
+ gitlab_skill_install: (0, import_plugin6.tool)({
4286
+ description: "Install a skill from a group wiki into the project wiki.\nCopies all skill pages (SKILL + references) from the group to the project.\nUpdates the project skill index with the installed skill.",
4287
+ args: {
4288
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
4289
+ name: z6.string().describe('Skill name to install (e.g., "incident-retro")'),
4290
+ group_id: z6.string().describe("Group path to install from"),
4291
+ draft: z6.boolean().optional().describe("Install as draft (default: false)")
4292
+ },
4293
+ execute: async (args) => {
4294
+ const auth = authAndValidate(args.project_id);
4295
+ const projectScope = resolveScope2(args);
4296
+ try {
4297
+ const groupPages = await listWikiPages(
4298
+ auth.instanceUrl,
4299
+ auth.token,
4300
+ "groups",
4301
+ args.group_id,
4302
+ true
4303
+ );
4304
+ const sourcePrefix = `${SKILLS_PREFIX}/${args.name}/`;
4305
+ const skillPages = groupPages.filter(
4306
+ (p) => p.slug.startsWith(sourcePrefix) && p.content
4307
+ );
4308
+ if (skillPages.length === 0) {
4309
+ return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
4310
+ }
4311
+ const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4312
+ const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4313
+ for (const page of skillPages) {
4314
+ const newSlug = page.slug.replace(SKILLS_PREFIX, targetPrefix);
4315
+ await upsertPage(
4120
4316
  auth.instanceUrl,
4121
4317
  auth.token,
4122
- scope,
4123
- id,
4124
- publishedSlug,
4125
- draft.content
4318
+ projectScope.scope,
4319
+ projectScope.id,
4320
+ newSlug,
4321
+ page.content
4126
4322
  );
4127
4323
  }
4128
- await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, draftSlug);
4129
- return `Promoted skill "${args.name}" from draft to published.`;
4324
+ const groupIndex = await readIndex(
4325
+ auth.instanceUrl,
4326
+ auth.token,
4327
+ "groups",
4328
+ args.group_id,
4329
+ SKILLS_INDEX
4330
+ );
4331
+ const entry = groupIndex.find((e) => e.name === args.name);
4332
+ const description = entry?.description ?? "(installed from group)";
4333
+ await upsertIndexEntry(
4334
+ auth.instanceUrl,
4335
+ auth.token,
4336
+ projectScope.scope,
4337
+ projectScope.id,
4338
+ targetIndex,
4339
+ {
4340
+ name: args.name,
4341
+ description,
4342
+ source: `group:${args.group_id}`,
4343
+ draft: !!args.draft
4344
+ }
4345
+ );
4346
+ return `Installed skill "${args.name}" from group "${args.group_id}" into project. ${skillPages.length} page(s) copied.`;
4130
4347
  } catch (err) {
4131
- if (err.message?.includes("not found") || err.message?.includes("404")) {
4132
- return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
4348
+ return `Error installing skill: ${err.message}`;
4349
+ }
4350
+ }
4351
+ }),
4352
+ gitlab_skill_delete: (0, import_plugin6.tool)({
4353
+ description: "Delete a skill and remove it from the index.\nRemoves all pages under the skill directory (SKILL + references).\nWiki git history preserves deleted content.",
4354
+ args: {
4355
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
4356
+ name: z6.string().describe("Skill name to delete"),
4357
+ draft: z6.boolean().optional().describe("Delete from drafts instead of published (default: false)"),
4358
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4359
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4360
+ },
4361
+ execute: async (args) => {
4362
+ const auth = authAndValidate(args.project_id);
4363
+ const { scope, id } = resolveScope2(args);
4364
+ const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4365
+ const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4366
+ const skillPrefix = `${prefix}/${args.name}/`;
4367
+ try {
4368
+ const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
4369
+ const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix));
4370
+ if (skillPages.length === 0) {
4371
+ return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
4133
4372
  }
4134
- return `Error promoting skill: ${err.message}`;
4373
+ for (const page of skillPages) {
4374
+ await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4375
+ }
4376
+ await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
4377
+ return `Deleted skill "${args.name}" (${skillPages.length} page(s) removed).`;
4378
+ } catch (err) {
4379
+ return `Error deleting skill: ${err.message}`;
4135
4380
  }
4136
4381
  }
4137
4382
  })