opencode-gitlab-dap 1.12.1 → 1.14.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 +13 -8
- package/dist/index.cjs +476 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +476 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3947,10 +3947,17 @@ ${e.content}`).join("\n\n---\n\n");
|
|
|
3947
3947
|
|
|
3948
3948
|
// src/tools/skill-tools.ts
|
|
3949
3949
|
var import_plugin6 = require("@opencode-ai/plugin");
|
|
3950
|
+
var import_child_process = require("child_process");
|
|
3951
|
+
var import_fs2 = require("fs");
|
|
3952
|
+
var import_path2 = require("path");
|
|
3953
|
+
var import_os2 = require("os");
|
|
3950
3954
|
var z6 = import_plugin6.tool.schema;
|
|
3951
3955
|
var PREFIX2 = "agents";
|
|
3952
3956
|
var SKILLS_PREFIX = `${PREFIX2}/skills`;
|
|
3953
3957
|
var DRAFTS_PREFIX = `${PREFIX2}/skills-drafts`;
|
|
3958
|
+
var SKILLS_INDEX = `${SKILLS_PREFIX}/index`;
|
|
3959
|
+
var DRAFTS_INDEX = `${DRAFTS_PREFIX}/index`;
|
|
3960
|
+
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
3961
|
function resolveScope2(args) {
|
|
3955
3962
|
if (args.scope === "groups" && args.group_id) {
|
|
3956
3963
|
return { scope: "groups", id: args.group_id };
|
|
@@ -3963,6 +3970,194 @@ function validateProjectId2(projectId) {
|
|
|
3963
3970
|
}
|
|
3964
3971
|
return null;
|
|
3965
3972
|
}
|
|
3973
|
+
function parseIndex(content) {
|
|
3974
|
+
const entries = [];
|
|
3975
|
+
const blocks = content.split(/^## /m).filter(Boolean);
|
|
3976
|
+
for (const block of blocks) {
|
|
3977
|
+
const lines = block.trim().split("\n");
|
|
3978
|
+
const name = lines[0].trim();
|
|
3979
|
+
if (!name) continue;
|
|
3980
|
+
const rest = lines.slice(1).join("\n").trim();
|
|
3981
|
+
const descLines = [];
|
|
3982
|
+
let source;
|
|
3983
|
+
for (const line of rest.split("\n")) {
|
|
3984
|
+
if (line.startsWith("Source:")) {
|
|
3985
|
+
source = line.slice(7).trim();
|
|
3986
|
+
} else if (line.trim()) {
|
|
3987
|
+
descLines.push(line);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
entries.push({ name, description: descLines.join("\n"), source, draft: false });
|
|
3991
|
+
}
|
|
3992
|
+
return entries;
|
|
3993
|
+
}
|
|
3994
|
+
function formatIndex(entries) {
|
|
3995
|
+
return entries.map((e) => {
|
|
3996
|
+
let block = `## ${e.name}
|
|
3997
|
+
${e.description}`;
|
|
3998
|
+
if (e.source) block += `
|
|
3999
|
+
Source: ${e.source}`;
|
|
4000
|
+
return block;
|
|
4001
|
+
}).join("\n\n");
|
|
4002
|
+
}
|
|
4003
|
+
async function readIndex(instanceUrl, token, scope, id, indexSlug) {
|
|
4004
|
+
try {
|
|
4005
|
+
const page = await getWikiPage(instanceUrl, token, scope, id, indexSlug);
|
|
4006
|
+
return parseIndex(page.content);
|
|
4007
|
+
} catch {
|
|
4008
|
+
return [];
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
async function writeIndex(instanceUrl, token, scope, id, indexSlug, entries) {
|
|
4012
|
+
const content = formatIndex(entries);
|
|
4013
|
+
try {
|
|
4014
|
+
await updateWikiPage(instanceUrl, token, scope, id, indexSlug, content);
|
|
4015
|
+
} catch {
|
|
4016
|
+
await createWikiPage(instanceUrl, token, scope, id, indexSlug, content || "# Skills Index");
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
async function upsertIndexEntry(instanceUrl, token, scope, id, indexSlug, entry) {
|
|
4020
|
+
const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
4021
|
+
const idx = entries.findIndex((e) => e.name === entry.name);
|
|
4022
|
+
if (idx >= 0) {
|
|
4023
|
+
entries[idx] = entry;
|
|
4024
|
+
} else {
|
|
4025
|
+
entries.push(entry);
|
|
4026
|
+
}
|
|
4027
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, entries);
|
|
4028
|
+
}
|
|
4029
|
+
async function removeIndexEntry(instanceUrl, token, scope, id, indexSlug, name) {
|
|
4030
|
+
const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
4031
|
+
const filtered = entries.filter((e) => e.name !== name);
|
|
4032
|
+
if (filtered.length !== entries.length) {
|
|
4033
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, filtered);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
function extractSkillNames(pages, prefix) {
|
|
4037
|
+
const skillSuffix = "/SKILL";
|
|
4038
|
+
const names = /* @__PURE__ */ new Set();
|
|
4039
|
+
for (const p of pages) {
|
|
4040
|
+
if (p.slug.startsWith(prefix + "/") && p.slug.endsWith(skillSuffix)) {
|
|
4041
|
+
const middle = p.slug.slice(prefix.length + 1, -skillSuffix.length);
|
|
4042
|
+
if (middle && !middle.includes("/")) {
|
|
4043
|
+
names.add(middle);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
return [...names];
|
|
4048
|
+
}
|
|
4049
|
+
async function rebuildIndex(instanceUrl, token, scope, id, prefix, indexSlug) {
|
|
4050
|
+
const pages = await listWikiPages(instanceUrl, token, scope, id);
|
|
4051
|
+
const actualNames = extractSkillNames(pages, prefix);
|
|
4052
|
+
const currentEntries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
4053
|
+
const indexed = new Set(currentEntries.map((e) => e.name));
|
|
4054
|
+
const actual = new Set(actualNames);
|
|
4055
|
+
let dirty = false;
|
|
4056
|
+
const removed = currentEntries.filter((e) => !actual.has(e.name));
|
|
4057
|
+
if (removed.length > 0) dirty = true;
|
|
4058
|
+
const added = [];
|
|
4059
|
+
for (const name of actualNames) {
|
|
4060
|
+
if (!indexed.has(name)) {
|
|
4061
|
+
added.push(name);
|
|
4062
|
+
dirty = true;
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
if (!dirty && added.length === 0) return currentEntries;
|
|
4066
|
+
const kept = currentEntries.filter((e) => actual.has(e.name));
|
|
4067
|
+
for (const name of added) {
|
|
4068
|
+
kept.push({
|
|
4069
|
+
name,
|
|
4070
|
+
description: "(auto-indexed \u2014 update description with gitlab_skill_save)",
|
|
4071
|
+
draft: prefix === DRAFTS_PREFIX
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, kept);
|
|
4075
|
+
return kept;
|
|
4076
|
+
}
|
|
4077
|
+
async function upsertPage(instanceUrl, token, scope, id, slug, content) {
|
|
4078
|
+
try {
|
|
4079
|
+
await updateWikiPage(instanceUrl, token, scope, id, slug, content);
|
|
4080
|
+
} catch {
|
|
4081
|
+
await createWikiPage(instanceUrl, token, scope, id, slug, content);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
function searchSkillsSh(query) {
|
|
4085
|
+
try {
|
|
4086
|
+
const raw = (0, import_child_process.execSync)(`npx skills find ${JSON.stringify(query)}`, {
|
|
4087
|
+
timeout: 3e4,
|
|
4088
|
+
encoding: "utf-8",
|
|
4089
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4090
|
+
});
|
|
4091
|
+
const lines = raw.split("\n");
|
|
4092
|
+
const results = [];
|
|
4093
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4094
|
+
const clean = lines[i].replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
4095
|
+
const match = clean.match(/^(\S+\/\S+@\S+)\s+(.+installs?)$/);
|
|
4096
|
+
if (match) {
|
|
4097
|
+
const urlLine = (lines[i + 1] ?? "").replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
4098
|
+
const url = urlLine.startsWith("\u2514 ") ? urlLine.slice(2) : urlLine;
|
|
4099
|
+
results.push({ identifier: match[1], installs: match[2], url });
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
return results;
|
|
4103
|
+
} catch {
|
|
4104
|
+
return [];
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
function downloadSkillFromSkillsSh(identifier) {
|
|
4108
|
+
const tmp = (0, import_fs2.mkdtempSync)((0, import_path2.join)((0, import_os2.tmpdir)(), "skill-install-"));
|
|
4109
|
+
try {
|
|
4110
|
+
(0, import_child_process.execSync)(`npx skills add ${JSON.stringify(identifier)} -y --copy`, {
|
|
4111
|
+
timeout: 6e4,
|
|
4112
|
+
cwd: tmp,
|
|
4113
|
+
encoding: "utf-8",
|
|
4114
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4115
|
+
});
|
|
4116
|
+
const agentsDir = (0, import_path2.join)(tmp, ".agents", "skills");
|
|
4117
|
+
if (!(0, import_fs2.statSync)(agentsDir).isDirectory()) return null;
|
|
4118
|
+
const dirs = (0, import_fs2.readdirSync)(agentsDir);
|
|
4119
|
+
if (dirs.length === 0) return null;
|
|
4120
|
+
const skillName = dirs[0];
|
|
4121
|
+
const skillDir = (0, import_path2.join)(agentsDir, skillName);
|
|
4122
|
+
const skillMd = (0, import_path2.join)(skillDir, "SKILL.md");
|
|
4123
|
+
let mainContent;
|
|
4124
|
+
try {
|
|
4125
|
+
mainContent = (0, import_fs2.readFileSync)(skillMd, "utf-8");
|
|
4126
|
+
} catch {
|
|
4127
|
+
return null;
|
|
4128
|
+
}
|
|
4129
|
+
let description = "";
|
|
4130
|
+
const descMatch = mainContent.match(/^---\s*\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/);
|
|
4131
|
+
if (descMatch) {
|
|
4132
|
+
description = descMatch[1].trim();
|
|
4133
|
+
} else {
|
|
4134
|
+
const firstParagraph = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim();
|
|
4135
|
+
description = firstParagraph.slice(0, 200);
|
|
4136
|
+
}
|
|
4137
|
+
const files = [];
|
|
4138
|
+
const walkStack = [{ dir: skillDir, prefix: "" }];
|
|
4139
|
+
while (walkStack.length > 0) {
|
|
4140
|
+
const { dir, prefix } = walkStack.pop();
|
|
4141
|
+
for (const entry of (0, import_fs2.readdirSync)(dir)) {
|
|
4142
|
+
const full = (0, import_path2.join)(dir, entry);
|
|
4143
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
4144
|
+
if ((0, import_fs2.statSync)(full).isDirectory()) {
|
|
4145
|
+
walkStack.push({ dir: full, prefix: rel });
|
|
4146
|
+
} else if (entry !== "SKILL.md") {
|
|
4147
|
+
files.push({ path: rel, content: (0, import_fs2.readFileSync)(full, "utf-8") });
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
return { name: skillName, content: mainContent, description, files };
|
|
4152
|
+
} catch {
|
|
4153
|
+
return null;
|
|
4154
|
+
} finally {
|
|
4155
|
+
try {
|
|
4156
|
+
(0, import_fs2.rmSync)(tmp, { recursive: true, force: true });
|
|
4157
|
+
} catch {
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
3966
4161
|
function makeSkillTools(ctx) {
|
|
3967
4162
|
function authAndValidate(projectId) {
|
|
3968
4163
|
const auth = ctx.ensureAuth();
|
|
@@ -3973,11 +4168,9 @@ function makeSkillTools(ctx) {
|
|
|
3973
4168
|
}
|
|
3974
4169
|
return {
|
|
3975
4170
|
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).",
|
|
4171
|
+
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
4172
|
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
|
-
),
|
|
4173
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3981
4174
|
include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
|
|
3982
4175
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3983
4176
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3986,23 +4179,27 @@ function makeSkillTools(ctx) {
|
|
|
3986
4179
|
const auth = authAndValidate(args.project_id);
|
|
3987
4180
|
const { scope, id } = resolveScope2(args);
|
|
3988
4181
|
try {
|
|
3989
|
-
const
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
4182
|
+
const published = await rebuildIndex(
|
|
4183
|
+
auth.instanceUrl,
|
|
4184
|
+
auth.token,
|
|
4185
|
+
scope,
|
|
4186
|
+
id,
|
|
4187
|
+
SKILLS_PREFIX,
|
|
4188
|
+
SKILLS_INDEX
|
|
4189
|
+
);
|
|
3996
4190
|
let drafts = [];
|
|
3997
4191
|
if (args.include_drafts) {
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4192
|
+
drafts = await rebuildIndex(
|
|
4193
|
+
auth.instanceUrl,
|
|
4194
|
+
auth.token,
|
|
4195
|
+
scope,
|
|
4196
|
+
id,
|
|
4197
|
+
DRAFTS_PREFIX,
|
|
4198
|
+
DRAFTS_INDEX
|
|
4199
|
+
);
|
|
4200
|
+
drafts = drafts.map((e) => ({ ...e, draft: true }));
|
|
4004
4201
|
}
|
|
4005
|
-
const all = [...
|
|
4202
|
+
const all = [...published, ...drafts];
|
|
4006
4203
|
if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
|
|
4007
4204
|
return JSON.stringify(all, null, 2);
|
|
4008
4205
|
} catch (err) {
|
|
@@ -4011,11 +4208,9 @@ function makeSkillTools(ctx) {
|
|
|
4011
4208
|
}
|
|
4012
4209
|
}),
|
|
4013
4210
|
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.",
|
|
4211
|
+
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
4212
|
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
|
-
),
|
|
4213
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4019
4214
|
name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
|
|
4020
4215
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
4021
4216
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -4023,41 +4218,46 @@ function makeSkillTools(ctx) {
|
|
|
4023
4218
|
execute: async (args) => {
|
|
4024
4219
|
const auth = authAndValidate(args.project_id);
|
|
4025
4220
|
const { scope, id } = resolveScope2(args);
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
auth.instanceUrl,
|
|
4029
|
-
auth.token,
|
|
4030
|
-
scope,
|
|
4031
|
-
id,
|
|
4032
|
-
`${SKILLS_PREFIX}/${args.name}`
|
|
4033
|
-
);
|
|
4034
|
-
return page.content;
|
|
4035
|
-
} catch {
|
|
4221
|
+
const prefixes = [SKILLS_PREFIX, DRAFTS_PREFIX];
|
|
4222
|
+
for (const prefix of prefixes) {
|
|
4036
4223
|
try {
|
|
4037
|
-
const
|
|
4224
|
+
const page = await getWikiPage(
|
|
4038
4225
|
auth.instanceUrl,
|
|
4039
4226
|
auth.token,
|
|
4040
4227
|
scope,
|
|
4041
4228
|
id,
|
|
4042
|
-
`${
|
|
4229
|
+
`${prefix}/${args.name}/SKILL`
|
|
4043
4230
|
);
|
|
4044
|
-
|
|
4231
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
|
|
4232
|
+
const refPrefix = `${prefix}/${args.name}/references/`;
|
|
4233
|
+
const refs = pages.filter((p) => p.slug.startsWith(refPrefix)).map((p) => p.slug.slice(refPrefix.length));
|
|
4234
|
+
const isDraft = prefix === DRAFTS_PREFIX;
|
|
4235
|
+
let result = isDraft ? `[DRAFT SKILL]
|
|
4045
4236
|
|
|
4046
|
-
${
|
|
4237
|
+
${page.content}` : page.content;
|
|
4238
|
+
if (refs.length > 0) {
|
|
4239
|
+
result += `
|
|
4240
|
+
|
|
4241
|
+
---
|
|
4242
|
+
Available references: ${refs.join(", ")}`;
|
|
4243
|
+
result += `
|
|
4244
|
+
Load with: gitlab_skill_load_reference(name="${args.name}", reference="<name>")`;
|
|
4245
|
+
}
|
|
4246
|
+
return result;
|
|
4047
4247
|
} catch {
|
|
4048
|
-
|
|
4248
|
+
continue;
|
|
4049
4249
|
}
|
|
4050
4250
|
}
|
|
4251
|
+
return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
|
|
4051
4252
|
}
|
|
4052
4253
|
}),
|
|
4053
4254
|
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.",
|
|
4255
|
+
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
4256
|
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
|
-
),
|
|
4257
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4059
4258
|
name: z6.string().describe('Skill name (e.g., "incident-retro")'),
|
|
4060
4259
|
content: z6.string().describe("Skill content in markdown"),
|
|
4260
|
+
description: z6.string().describe("Short description for the skill index (1-2 sentences)"),
|
|
4061
4261
|
draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
|
|
4062
4262
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
4063
4263
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -4066,35 +4266,27 @@ ${draft.content}`;
|
|
|
4066
4266
|
const auth = authAndValidate(args.project_id);
|
|
4067
4267
|
const { scope, id } = resolveScope2(args);
|
|
4068
4268
|
const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
4069
|
-
const
|
|
4269
|
+
const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4270
|
+
const slug = `${prefix}/${args.name}/SKILL`;
|
|
4070
4271
|
const label = args.draft ? "draft " : "";
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
4083
|
-
continue;
|
|
4084
|
-
}
|
|
4085
|
-
return `Error saving skill: ${msg}`;
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4272
|
+
try {
|
|
4273
|
+
await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
|
|
4274
|
+
await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, {
|
|
4275
|
+
name: args.name,
|
|
4276
|
+
description: args.description,
|
|
4277
|
+
source: "project",
|
|
4278
|
+
draft: !!args.draft
|
|
4279
|
+
});
|
|
4280
|
+
return `Saved ${label}skill: ${args.name}`;
|
|
4281
|
+
} catch (err) {
|
|
4282
|
+
return `Error saving skill: ${err.message}`;
|
|
4088
4283
|
}
|
|
4089
|
-
return `Error saving skill: failed after 3 retries`;
|
|
4090
4284
|
}
|
|
4091
4285
|
}),
|
|
4092
4286
|
gitlab_skill_promote: (0, import_plugin6.tool)({
|
|
4093
|
-
description: "Promote a draft skill to published.\nMoves
|
|
4287
|
+
description: "Promote a draft skill to published.\nMoves all skill pages from drafts to published and updates both indexes.",
|
|
4094
4288
|
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
|
-
),
|
|
4289
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4098
4290
|
name: z6.string().describe("Skill name to promote"),
|
|
4099
4291
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
4100
4292
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -4102,36 +4294,238 @@ ${draft.content}`;
|
|
|
4102
4294
|
execute: async (args) => {
|
|
4103
4295
|
const auth = authAndValidate(args.project_id);
|
|
4104
4296
|
const { scope, id } = resolveScope2(args);
|
|
4105
|
-
const draftSlug = `${DRAFTS_PREFIX}/${args.name}`;
|
|
4106
|
-
const publishedSlug = `${SKILLS_PREFIX}/${args.name}`;
|
|
4107
4297
|
try {
|
|
4108
|
-
const
|
|
4298
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
|
|
4299
|
+
const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
|
|
4300
|
+
const draftPages = pages.filter(
|
|
4301
|
+
(p) => p.slug.startsWith(draftPrefix) && p.content
|
|
4302
|
+
);
|
|
4303
|
+
if (draftPages.length === 0) {
|
|
4304
|
+
return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
|
|
4305
|
+
}
|
|
4306
|
+
const draftIndex = await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX);
|
|
4307
|
+
const entry = draftIndex.find((e) => e.name === args.name);
|
|
4308
|
+
const description = entry?.description ?? "(promoted from draft)";
|
|
4309
|
+
for (const page of draftPages) {
|
|
4310
|
+
const newSlug = page.slug.replace(DRAFTS_PREFIX, SKILLS_PREFIX);
|
|
4311
|
+
await upsertPage(auth.instanceUrl, auth.token, scope, id, newSlug, page.content);
|
|
4312
|
+
await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
|
|
4313
|
+
}
|
|
4314
|
+
await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX, args.name);
|
|
4315
|
+
await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX, {
|
|
4316
|
+
name: args.name,
|
|
4317
|
+
description,
|
|
4318
|
+
source: "project",
|
|
4319
|
+
draft: false
|
|
4320
|
+
});
|
|
4321
|
+
return `Promoted skill "${args.name}" from draft to published.`;
|
|
4322
|
+
} catch (err) {
|
|
4323
|
+
return `Error promoting skill: ${err.message}`;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}),
|
|
4327
|
+
gitlab_skill_discover: (0, import_plugin6.tool)({
|
|
4328
|
+
description: "Search for skills in the group wiki and the skills.sh public registry.\nGroup wiki skills are searched first, then skills.sh for community skills.\nUse gitlab_skill_install to install a discovered skill into your project.",
|
|
4329
|
+
args: {
|
|
4330
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4331
|
+
query: z6.string().describe("Search query (matches skill name and description)"),
|
|
4332
|
+
group_id: z6.string().optional().describe("Group path to search for shared skills (optional)")
|
|
4333
|
+
},
|
|
4334
|
+
execute: async (args) => {
|
|
4335
|
+
const auth = authAndValidate(args.project_id);
|
|
4336
|
+
const sections = [];
|
|
4337
|
+
if (args.group_id) {
|
|
4109
4338
|
try {
|
|
4110
|
-
await
|
|
4339
|
+
const entries = await readIndex(
|
|
4111
4340
|
auth.instanceUrl,
|
|
4112
4341
|
auth.token,
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4342
|
+
"groups",
|
|
4343
|
+
args.group_id,
|
|
4344
|
+
SKILLS_INDEX
|
|
4345
|
+
);
|
|
4346
|
+
const q = args.query.toLowerCase();
|
|
4347
|
+
const matches = entries.filter(
|
|
4348
|
+
(e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
|
|
4117
4349
|
);
|
|
4350
|
+
if (matches.length > 0) {
|
|
4351
|
+
sections.push(
|
|
4352
|
+
`### Group skills (${matches.length})
|
|
4353
|
+
|
|
4354
|
+
` + matches.map(
|
|
4355
|
+
(e) => `**${e.name}**: ${e.description}
|
|
4356
|
+
Install: \`gitlab_skill_install(name="${e.name}", source="group", group_id="${args.group_id}")\``
|
|
4357
|
+
).join("\n\n")
|
|
4358
|
+
);
|
|
4359
|
+
}
|
|
4118
4360
|
} catch {
|
|
4119
|
-
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
const shResults = searchSkillsSh(args.query);
|
|
4364
|
+
if (shResults.length > 0) {
|
|
4365
|
+
sections.push(
|
|
4366
|
+
`### skills.sh (${shResults.length})
|
|
4367
|
+
|
|
4368
|
+
` + shResults.map(
|
|
4369
|
+
(r) => `**${r.identifier}** (${r.installs})
|
|
4370
|
+
${r.url}
|
|
4371
|
+
Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
|
|
4372
|
+
).join("\n\n")
|
|
4373
|
+
);
|
|
4374
|
+
}
|
|
4375
|
+
if (sections.length === 0) {
|
|
4376
|
+
return `No skills found matching "${args.query}" in ${args.group_id ? "group wiki or " : ""}skills.sh.`;
|
|
4377
|
+
}
|
|
4378
|
+
return sections.join("\n\n---\n\n");
|
|
4379
|
+
}
|
|
4380
|
+
}),
|
|
4381
|
+
gitlab_skill_install: (0, import_plugin6.tool)({
|
|
4382
|
+
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.",
|
|
4383
|
+
args: {
|
|
4384
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4385
|
+
name: z6.string().describe(
|
|
4386
|
+
'Skill identifier. For group: skill name (e.g., "incident-retro"). For skills.sh: full identifier (e.g., "vercel-labs/agent-skills@nextjs-developer").'
|
|
4387
|
+
),
|
|
4388
|
+
source: z6.enum(["group", "skills.sh"]).describe('Where to install from: "group" (group wiki) or "skills.sh" (public registry)'),
|
|
4389
|
+
group_id: z6.string().optional().describe("Group path (required when source is group)"),
|
|
4390
|
+
draft: z6.boolean().optional().describe("Install as draft (default: false)")
|
|
4391
|
+
},
|
|
4392
|
+
execute: async (args) => {
|
|
4393
|
+
const auth = authAndValidate(args.project_id);
|
|
4394
|
+
const projectScope = resolveScope2(args);
|
|
4395
|
+
const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
4396
|
+
const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4397
|
+
if (args.source === "skills.sh") {
|
|
4398
|
+
const downloaded = downloadSkillFromSkillsSh(args.name);
|
|
4399
|
+
if (!downloaded) {
|
|
4400
|
+
return `Failed to download skill "${args.name}" from skills.sh. Check that the identifier is correct (e.g., "owner/repo@skill-name").`;
|
|
4401
|
+
}
|
|
4402
|
+
const skillSlug = `${targetPrefix}/${downloaded.name}/SKILL`;
|
|
4403
|
+
try {
|
|
4404
|
+
await upsertPage(
|
|
4120
4405
|
auth.instanceUrl,
|
|
4121
4406
|
auth.token,
|
|
4122
|
-
scope,
|
|
4123
|
-
id,
|
|
4124
|
-
|
|
4125
|
-
|
|
4407
|
+
projectScope.scope,
|
|
4408
|
+
projectScope.id,
|
|
4409
|
+
skillSlug,
|
|
4410
|
+
downloaded.content
|
|
4126
4411
|
);
|
|
4412
|
+
let fileCount = 1;
|
|
4413
|
+
for (const file of downloaded.files) {
|
|
4414
|
+
const ext = file.path.replace(/\.[^.]+$/, "");
|
|
4415
|
+
const refSlug = `${targetPrefix}/${downloaded.name}/${ext}`;
|
|
4416
|
+
await upsertPage(
|
|
4417
|
+
auth.instanceUrl,
|
|
4418
|
+
auth.token,
|
|
4419
|
+
projectScope.scope,
|
|
4420
|
+
projectScope.id,
|
|
4421
|
+
refSlug,
|
|
4422
|
+
file.content
|
|
4423
|
+
);
|
|
4424
|
+
fileCount++;
|
|
4425
|
+
}
|
|
4426
|
+
await upsertIndexEntry(
|
|
4427
|
+
auth.instanceUrl,
|
|
4428
|
+
auth.token,
|
|
4429
|
+
projectScope.scope,
|
|
4430
|
+
projectScope.id,
|
|
4431
|
+
targetIndex,
|
|
4432
|
+
{
|
|
4433
|
+
name: downloaded.name,
|
|
4434
|
+
description: downloaded.description,
|
|
4435
|
+
source: `skills.sh:${args.name}`,
|
|
4436
|
+
draft: !!args.draft
|
|
4437
|
+
}
|
|
4438
|
+
);
|
|
4439
|
+
return `Installed skill "${downloaded.name}" from skills.sh. ${fileCount} page(s) written.`;
|
|
4440
|
+
} catch (err) {
|
|
4441
|
+
return `Error installing skill from skills.sh: ${err.message}`;
|
|
4127
4442
|
}
|
|
4128
|
-
|
|
4129
|
-
|
|
4443
|
+
}
|
|
4444
|
+
if (!args.group_id) {
|
|
4445
|
+
return 'Error: group_id is required when source is "group".';
|
|
4446
|
+
}
|
|
4447
|
+
try {
|
|
4448
|
+
const groupPages = await listWikiPages(
|
|
4449
|
+
auth.instanceUrl,
|
|
4450
|
+
auth.token,
|
|
4451
|
+
"groups",
|
|
4452
|
+
args.group_id,
|
|
4453
|
+
true
|
|
4454
|
+
);
|
|
4455
|
+
const sourcePrefix = `${SKILLS_PREFIX}/${args.name}/`;
|
|
4456
|
+
const skillPages = groupPages.filter(
|
|
4457
|
+
(p) => p.slug.startsWith(sourcePrefix) && p.content
|
|
4458
|
+
);
|
|
4459
|
+
if (skillPages.length === 0) {
|
|
4460
|
+
return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
|
|
4461
|
+
}
|
|
4462
|
+
for (const page of skillPages) {
|
|
4463
|
+
const newSlug = page.slug.replace(SKILLS_PREFIX, targetPrefix);
|
|
4464
|
+
await upsertPage(
|
|
4465
|
+
auth.instanceUrl,
|
|
4466
|
+
auth.token,
|
|
4467
|
+
projectScope.scope,
|
|
4468
|
+
projectScope.id,
|
|
4469
|
+
newSlug,
|
|
4470
|
+
page.content
|
|
4471
|
+
);
|
|
4472
|
+
}
|
|
4473
|
+
const groupIndex = await readIndex(
|
|
4474
|
+
auth.instanceUrl,
|
|
4475
|
+
auth.token,
|
|
4476
|
+
"groups",
|
|
4477
|
+
args.group_id,
|
|
4478
|
+
SKILLS_INDEX
|
|
4479
|
+
);
|
|
4480
|
+
const entry = groupIndex.find((e) => e.name === args.name);
|
|
4481
|
+
const description = entry?.description ?? "(installed from group)";
|
|
4482
|
+
await upsertIndexEntry(
|
|
4483
|
+
auth.instanceUrl,
|
|
4484
|
+
auth.token,
|
|
4485
|
+
projectScope.scope,
|
|
4486
|
+
projectScope.id,
|
|
4487
|
+
targetIndex,
|
|
4488
|
+
{
|
|
4489
|
+
name: args.name,
|
|
4490
|
+
description,
|
|
4491
|
+
source: `group:${args.group_id}`,
|
|
4492
|
+
draft: !!args.draft
|
|
4493
|
+
}
|
|
4494
|
+
);
|
|
4495
|
+
return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s) copied.`;
|
|
4130
4496
|
} catch (err) {
|
|
4131
|
-
|
|
4132
|
-
|
|
4497
|
+
return `Error installing skill: ${err.message}`;
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
}),
|
|
4501
|
+
gitlab_skill_delete: (0, import_plugin6.tool)({
|
|
4502
|
+
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.",
|
|
4503
|
+
args: {
|
|
4504
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4505
|
+
name: z6.string().describe("Skill name to delete"),
|
|
4506
|
+
draft: z6.boolean().optional().describe("Delete from drafts instead of published (default: false)"),
|
|
4507
|
+
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
4508
|
+
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
4509
|
+
},
|
|
4510
|
+
execute: async (args) => {
|
|
4511
|
+
const auth = authAndValidate(args.project_id);
|
|
4512
|
+
const { scope, id } = resolveScope2(args);
|
|
4513
|
+
const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
4514
|
+
const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4515
|
+
const skillPrefix = `${prefix}/${args.name}/`;
|
|
4516
|
+
try {
|
|
4517
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
|
|
4518
|
+
const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix));
|
|
4519
|
+
if (skillPages.length === 0) {
|
|
4520
|
+
return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
|
|
4133
4521
|
}
|
|
4134
|
-
|
|
4522
|
+
for (const page of skillPages) {
|
|
4523
|
+
await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
|
|
4524
|
+
}
|
|
4525
|
+
await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
|
|
4526
|
+
return `Deleted skill "${args.name}" (${skillPages.length} page(s) removed).`;
|
|
4527
|
+
} catch (err) {
|
|
4528
|
+
return `Error deleting skill: ${err.message}`;
|
|
4135
4529
|
}
|
|
4136
4530
|
}
|
|
4137
4531
|
})
|