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.js
CHANGED
|
@@ -3778,10 +3778,17 @@ ${e.content}`).join("\n\n---\n\n");
|
|
|
3778
3778
|
|
|
3779
3779
|
// src/tools/skill-tools.ts
|
|
3780
3780
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
3781
|
+
import { execSync } from "child_process";
|
|
3782
|
+
import { mkdtempSync, readFileSync as readFileSync2, rmSync, readdirSync, statSync } from "fs";
|
|
3783
|
+
import { join as join2 } from "path";
|
|
3784
|
+
import { tmpdir } from "os";
|
|
3781
3785
|
var z6 = tool6.schema;
|
|
3782
3786
|
var PREFIX2 = "agents";
|
|
3783
3787
|
var SKILLS_PREFIX = `${PREFIX2}/skills`;
|
|
3784
3788
|
var DRAFTS_PREFIX = `${PREFIX2}/skills-drafts`;
|
|
3789
|
+
var SKILLS_INDEX = `${SKILLS_PREFIX}/index`;
|
|
3790
|
+
var DRAFTS_INDEX = `${DRAFTS_PREFIX}/index`;
|
|
3791
|
+
var PROJECT_ID_DESC2 = 'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.';
|
|
3785
3792
|
function resolveScope2(args) {
|
|
3786
3793
|
if (args.scope === "groups" && args.group_id) {
|
|
3787
3794
|
return { scope: "groups", id: args.group_id };
|
|
@@ -3794,6 +3801,194 @@ function validateProjectId2(projectId) {
|
|
|
3794
3801
|
}
|
|
3795
3802
|
return null;
|
|
3796
3803
|
}
|
|
3804
|
+
function parseIndex(content) {
|
|
3805
|
+
const entries = [];
|
|
3806
|
+
const blocks = content.split(/^## /m).filter(Boolean);
|
|
3807
|
+
for (const block of blocks) {
|
|
3808
|
+
const lines = block.trim().split("\n");
|
|
3809
|
+
const name = lines[0].trim();
|
|
3810
|
+
if (!name) continue;
|
|
3811
|
+
const rest = lines.slice(1).join("\n").trim();
|
|
3812
|
+
const descLines = [];
|
|
3813
|
+
let source;
|
|
3814
|
+
for (const line of rest.split("\n")) {
|
|
3815
|
+
if (line.startsWith("Source:")) {
|
|
3816
|
+
source = line.slice(7).trim();
|
|
3817
|
+
} else if (line.trim()) {
|
|
3818
|
+
descLines.push(line);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
entries.push({ name, description: descLines.join("\n"), source, draft: false });
|
|
3822
|
+
}
|
|
3823
|
+
return entries;
|
|
3824
|
+
}
|
|
3825
|
+
function formatIndex(entries) {
|
|
3826
|
+
return entries.map((e) => {
|
|
3827
|
+
let block = `## ${e.name}
|
|
3828
|
+
${e.description}`;
|
|
3829
|
+
if (e.source) block += `
|
|
3830
|
+
Source: ${e.source}`;
|
|
3831
|
+
return block;
|
|
3832
|
+
}).join("\n\n");
|
|
3833
|
+
}
|
|
3834
|
+
async function readIndex(instanceUrl, token, scope, id, indexSlug) {
|
|
3835
|
+
try {
|
|
3836
|
+
const page = await getWikiPage(instanceUrl, token, scope, id, indexSlug);
|
|
3837
|
+
return parseIndex(page.content);
|
|
3838
|
+
} catch {
|
|
3839
|
+
return [];
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
async function writeIndex(instanceUrl, token, scope, id, indexSlug, entries) {
|
|
3843
|
+
const content = formatIndex(entries);
|
|
3844
|
+
try {
|
|
3845
|
+
await updateWikiPage(instanceUrl, token, scope, id, indexSlug, content);
|
|
3846
|
+
} catch {
|
|
3847
|
+
await createWikiPage(instanceUrl, token, scope, id, indexSlug, content || "# Skills Index");
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
async function upsertIndexEntry(instanceUrl, token, scope, id, indexSlug, entry) {
|
|
3851
|
+
const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
3852
|
+
const idx = entries.findIndex((e) => e.name === entry.name);
|
|
3853
|
+
if (idx >= 0) {
|
|
3854
|
+
entries[idx] = entry;
|
|
3855
|
+
} else {
|
|
3856
|
+
entries.push(entry);
|
|
3857
|
+
}
|
|
3858
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, entries);
|
|
3859
|
+
}
|
|
3860
|
+
async function removeIndexEntry(instanceUrl, token, scope, id, indexSlug, name) {
|
|
3861
|
+
const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
3862
|
+
const filtered = entries.filter((e) => e.name !== name);
|
|
3863
|
+
if (filtered.length !== entries.length) {
|
|
3864
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, filtered);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
function extractSkillNames(pages, prefix) {
|
|
3868
|
+
const skillSuffix = "/SKILL";
|
|
3869
|
+
const names = /* @__PURE__ */ new Set();
|
|
3870
|
+
for (const p of pages) {
|
|
3871
|
+
if (p.slug.startsWith(prefix + "/") && p.slug.endsWith(skillSuffix)) {
|
|
3872
|
+
const middle = p.slug.slice(prefix.length + 1, -skillSuffix.length);
|
|
3873
|
+
if (middle && !middle.includes("/")) {
|
|
3874
|
+
names.add(middle);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
return [...names];
|
|
3879
|
+
}
|
|
3880
|
+
async function rebuildIndex(instanceUrl, token, scope, id, prefix, indexSlug) {
|
|
3881
|
+
const pages = await listWikiPages(instanceUrl, token, scope, id);
|
|
3882
|
+
const actualNames = extractSkillNames(pages, prefix);
|
|
3883
|
+
const currentEntries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
3884
|
+
const indexed = new Set(currentEntries.map((e) => e.name));
|
|
3885
|
+
const actual = new Set(actualNames);
|
|
3886
|
+
let dirty = false;
|
|
3887
|
+
const removed = currentEntries.filter((e) => !actual.has(e.name));
|
|
3888
|
+
if (removed.length > 0) dirty = true;
|
|
3889
|
+
const added = [];
|
|
3890
|
+
for (const name of actualNames) {
|
|
3891
|
+
if (!indexed.has(name)) {
|
|
3892
|
+
added.push(name);
|
|
3893
|
+
dirty = true;
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
if (!dirty && added.length === 0) return currentEntries;
|
|
3897
|
+
const kept = currentEntries.filter((e) => actual.has(e.name));
|
|
3898
|
+
for (const name of added) {
|
|
3899
|
+
kept.push({
|
|
3900
|
+
name,
|
|
3901
|
+
description: "(auto-indexed \u2014 update description with gitlab_skill_save)",
|
|
3902
|
+
draft: prefix === DRAFTS_PREFIX
|
|
3903
|
+
});
|
|
3904
|
+
}
|
|
3905
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, kept);
|
|
3906
|
+
return kept;
|
|
3907
|
+
}
|
|
3908
|
+
async function upsertPage(instanceUrl, token, scope, id, slug, content) {
|
|
3909
|
+
try {
|
|
3910
|
+
await updateWikiPage(instanceUrl, token, scope, id, slug, content);
|
|
3911
|
+
} catch {
|
|
3912
|
+
await createWikiPage(instanceUrl, token, scope, id, slug, content);
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
function searchSkillsSh(query) {
|
|
3916
|
+
try {
|
|
3917
|
+
const raw = execSync(`npx skills find ${JSON.stringify(query)}`, {
|
|
3918
|
+
timeout: 3e4,
|
|
3919
|
+
encoding: "utf-8",
|
|
3920
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3921
|
+
});
|
|
3922
|
+
const lines = raw.split("\n");
|
|
3923
|
+
const results = [];
|
|
3924
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3925
|
+
const clean = lines[i].replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
3926
|
+
const match = clean.match(/^(\S+\/\S+@\S+)\s+(.+installs?)$/);
|
|
3927
|
+
if (match) {
|
|
3928
|
+
const urlLine = (lines[i + 1] ?? "").replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
3929
|
+
const url = urlLine.startsWith("\u2514 ") ? urlLine.slice(2) : urlLine;
|
|
3930
|
+
results.push({ identifier: match[1], installs: match[2], url });
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
return results;
|
|
3934
|
+
} catch {
|
|
3935
|
+
return [];
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
function downloadSkillFromSkillsSh(identifier) {
|
|
3939
|
+
const tmp = mkdtempSync(join2(tmpdir(), "skill-install-"));
|
|
3940
|
+
try {
|
|
3941
|
+
execSync(`npx skills add ${JSON.stringify(identifier)} -y --copy`, {
|
|
3942
|
+
timeout: 6e4,
|
|
3943
|
+
cwd: tmp,
|
|
3944
|
+
encoding: "utf-8",
|
|
3945
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3946
|
+
});
|
|
3947
|
+
const agentsDir = join2(tmp, ".agents", "skills");
|
|
3948
|
+
if (!statSync(agentsDir).isDirectory()) return null;
|
|
3949
|
+
const dirs = readdirSync(agentsDir);
|
|
3950
|
+
if (dirs.length === 0) return null;
|
|
3951
|
+
const skillName = dirs[0];
|
|
3952
|
+
const skillDir = join2(agentsDir, skillName);
|
|
3953
|
+
const skillMd = join2(skillDir, "SKILL.md");
|
|
3954
|
+
let mainContent;
|
|
3955
|
+
try {
|
|
3956
|
+
mainContent = readFileSync2(skillMd, "utf-8");
|
|
3957
|
+
} catch {
|
|
3958
|
+
return null;
|
|
3959
|
+
}
|
|
3960
|
+
let description = "";
|
|
3961
|
+
const descMatch = mainContent.match(/^---\s*\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/);
|
|
3962
|
+
if (descMatch) {
|
|
3963
|
+
description = descMatch[1].trim();
|
|
3964
|
+
} else {
|
|
3965
|
+
const firstParagraph = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim();
|
|
3966
|
+
description = firstParagraph.slice(0, 200);
|
|
3967
|
+
}
|
|
3968
|
+
const files = [];
|
|
3969
|
+
const walkStack = [{ dir: skillDir, prefix: "" }];
|
|
3970
|
+
while (walkStack.length > 0) {
|
|
3971
|
+
const { dir, prefix } = walkStack.pop();
|
|
3972
|
+
for (const entry of readdirSync(dir)) {
|
|
3973
|
+
const full = join2(dir, entry);
|
|
3974
|
+
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
3975
|
+
if (statSync(full).isDirectory()) {
|
|
3976
|
+
walkStack.push({ dir: full, prefix: rel });
|
|
3977
|
+
} else if (entry !== "SKILL.md") {
|
|
3978
|
+
files.push({ path: rel, content: readFileSync2(full, "utf-8") });
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
return { name: skillName, content: mainContent, description, files };
|
|
3983
|
+
} catch {
|
|
3984
|
+
return null;
|
|
3985
|
+
} finally {
|
|
3986
|
+
try {
|
|
3987
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
3988
|
+
} catch {
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3797
3992
|
function makeSkillTools(ctx) {
|
|
3798
3993
|
function authAndValidate(projectId) {
|
|
3799
3994
|
const auth = ctx.ensureAuth();
|
|
@@ -3804,11 +3999,9 @@ function makeSkillTools(ctx) {
|
|
|
3804
3999
|
}
|
|
3805
4000
|
return {
|
|
3806
4001
|
gitlab_skill_list: tool6({
|
|
3807
|
-
description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).",
|
|
4002
|
+
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.",
|
|
3808
4003
|
args: {
|
|
3809
|
-
project_id: z6.string().describe(
|
|
3810
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3811
|
-
),
|
|
4004
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3812
4005
|
include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
|
|
3813
4006
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3814
4007
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3817,23 +4010,27 @@ function makeSkillTools(ctx) {
|
|
|
3817
4010
|
const auth = authAndValidate(args.project_id);
|
|
3818
4011
|
const { scope, id } = resolveScope2(args);
|
|
3819
4012
|
try {
|
|
3820
|
-
const
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
4013
|
+
const published = await rebuildIndex(
|
|
4014
|
+
auth.instanceUrl,
|
|
4015
|
+
auth.token,
|
|
4016
|
+
scope,
|
|
4017
|
+
id,
|
|
4018
|
+
SKILLS_PREFIX,
|
|
4019
|
+
SKILLS_INDEX
|
|
4020
|
+
);
|
|
3827
4021
|
let drafts = [];
|
|
3828
4022
|
if (args.include_drafts) {
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
4023
|
+
drafts = await rebuildIndex(
|
|
4024
|
+
auth.instanceUrl,
|
|
4025
|
+
auth.token,
|
|
4026
|
+
scope,
|
|
4027
|
+
id,
|
|
4028
|
+
DRAFTS_PREFIX,
|
|
4029
|
+
DRAFTS_INDEX
|
|
4030
|
+
);
|
|
4031
|
+
drafts = drafts.map((e) => ({ ...e, draft: true }));
|
|
3835
4032
|
}
|
|
3836
|
-
const all = [...
|
|
4033
|
+
const all = [...published, ...drafts];
|
|
3837
4034
|
if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
|
|
3838
4035
|
return JSON.stringify(all, null, 2);
|
|
3839
4036
|
} catch (err) {
|
|
@@ -3842,11 +4039,9 @@ function makeSkillTools(ctx) {
|
|
|
3842
4039
|
}
|
|
3843
4040
|
}),
|
|
3844
4041
|
gitlab_skill_load: tool6({
|
|
3845
|
-
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.",
|
|
4042
|
+
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.",
|
|
3846
4043
|
args: {
|
|
3847
|
-
project_id: z6.string().describe(
|
|
3848
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3849
|
-
),
|
|
4044
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3850
4045
|
name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
|
|
3851
4046
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3852
4047
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3854,41 +4049,46 @@ function makeSkillTools(ctx) {
|
|
|
3854
4049
|
execute: async (args) => {
|
|
3855
4050
|
const auth = authAndValidate(args.project_id);
|
|
3856
4051
|
const { scope, id } = resolveScope2(args);
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
auth.instanceUrl,
|
|
3860
|
-
auth.token,
|
|
3861
|
-
scope,
|
|
3862
|
-
id,
|
|
3863
|
-
`${SKILLS_PREFIX}/${args.name}`
|
|
3864
|
-
);
|
|
3865
|
-
return page.content;
|
|
3866
|
-
} catch {
|
|
4052
|
+
const prefixes = [SKILLS_PREFIX, DRAFTS_PREFIX];
|
|
4053
|
+
for (const prefix of prefixes) {
|
|
3867
4054
|
try {
|
|
3868
|
-
const
|
|
4055
|
+
const page = await getWikiPage(
|
|
3869
4056
|
auth.instanceUrl,
|
|
3870
4057
|
auth.token,
|
|
3871
4058
|
scope,
|
|
3872
4059
|
id,
|
|
3873
|
-
`${
|
|
4060
|
+
`${prefix}/${args.name}/SKILL`
|
|
3874
4061
|
);
|
|
3875
|
-
|
|
4062
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
|
|
4063
|
+
const refPrefix = `${prefix}/${args.name}/references/`;
|
|
4064
|
+
const refs = pages.filter((p) => p.slug.startsWith(refPrefix)).map((p) => p.slug.slice(refPrefix.length));
|
|
4065
|
+
const isDraft = prefix === DRAFTS_PREFIX;
|
|
4066
|
+
let result = isDraft ? `[DRAFT SKILL]
|
|
3876
4067
|
|
|
3877
|
-
${
|
|
4068
|
+
${page.content}` : page.content;
|
|
4069
|
+
if (refs.length > 0) {
|
|
4070
|
+
result += `
|
|
4071
|
+
|
|
4072
|
+
---
|
|
4073
|
+
Available references: ${refs.join(", ")}`;
|
|
4074
|
+
result += `
|
|
4075
|
+
Load with: gitlab_skill_load_reference(name="${args.name}", reference="<name>")`;
|
|
4076
|
+
}
|
|
4077
|
+
return result;
|
|
3878
4078
|
} catch {
|
|
3879
|
-
|
|
4079
|
+
continue;
|
|
3880
4080
|
}
|
|
3881
4081
|
}
|
|
4082
|
+
return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
|
|
3882
4083
|
}
|
|
3883
4084
|
}),
|
|
3884
4085
|
gitlab_skill_save: tool6({
|
|
3885
|
-
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.",
|
|
4086
|
+
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.",
|
|
3886
4087
|
args: {
|
|
3887
|
-
project_id: z6.string().describe(
|
|
3888
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3889
|
-
),
|
|
4088
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3890
4089
|
name: z6.string().describe('Skill name (e.g., "incident-retro")'),
|
|
3891
4090
|
content: z6.string().describe("Skill content in markdown"),
|
|
4091
|
+
description: z6.string().describe("Short description for the skill index (1-2 sentences)"),
|
|
3892
4092
|
draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
|
|
3893
4093
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3894
4094
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3897,35 +4097,27 @@ ${draft.content}`;
|
|
|
3897
4097
|
const auth = authAndValidate(args.project_id);
|
|
3898
4098
|
const { scope, id } = resolveScope2(args);
|
|
3899
4099
|
const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
3900
|
-
const
|
|
4100
|
+
const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4101
|
+
const slug = `${prefix}/${args.name}/SKILL`;
|
|
3901
4102
|
const label = args.draft ? "draft " : "";
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
3914
|
-
continue;
|
|
3915
|
-
}
|
|
3916
|
-
return `Error saving skill: ${msg}`;
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
4103
|
+
try {
|
|
4104
|
+
await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
|
|
4105
|
+
await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, {
|
|
4106
|
+
name: args.name,
|
|
4107
|
+
description: args.description,
|
|
4108
|
+
source: "project",
|
|
4109
|
+
draft: !!args.draft
|
|
4110
|
+
});
|
|
4111
|
+
return `Saved ${label}skill: ${args.name}`;
|
|
4112
|
+
} catch (err) {
|
|
4113
|
+
return `Error saving skill: ${err.message}`;
|
|
3919
4114
|
}
|
|
3920
|
-
return `Error saving skill: failed after 3 retries`;
|
|
3921
4115
|
}
|
|
3922
4116
|
}),
|
|
3923
4117
|
gitlab_skill_promote: tool6({
|
|
3924
|
-
description: "Promote a draft skill to published.\nMoves
|
|
4118
|
+
description: "Promote a draft skill to published.\nMoves all skill pages from drafts to published and updates both indexes.",
|
|
3925
4119
|
args: {
|
|
3926
|
-
project_id: z6.string().describe(
|
|
3927
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3928
|
-
),
|
|
4120
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3929
4121
|
name: z6.string().describe("Skill name to promote"),
|
|
3930
4122
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3931
4123
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3933,36 +4125,238 @@ ${draft.content}`;
|
|
|
3933
4125
|
execute: async (args) => {
|
|
3934
4126
|
const auth = authAndValidate(args.project_id);
|
|
3935
4127
|
const { scope, id } = resolveScope2(args);
|
|
3936
|
-
const draftSlug = `${DRAFTS_PREFIX}/${args.name}`;
|
|
3937
|
-
const publishedSlug = `${SKILLS_PREFIX}/${args.name}`;
|
|
3938
4128
|
try {
|
|
3939
|
-
const
|
|
4129
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
|
|
4130
|
+
const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
|
|
4131
|
+
const draftPages = pages.filter(
|
|
4132
|
+
(p) => p.slug.startsWith(draftPrefix) && p.content
|
|
4133
|
+
);
|
|
4134
|
+
if (draftPages.length === 0) {
|
|
4135
|
+
return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
|
|
4136
|
+
}
|
|
4137
|
+
const draftIndex = await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX);
|
|
4138
|
+
const entry = draftIndex.find((e) => e.name === args.name);
|
|
4139
|
+
const description = entry?.description ?? "(promoted from draft)";
|
|
4140
|
+
for (const page of draftPages) {
|
|
4141
|
+
const newSlug = page.slug.replace(DRAFTS_PREFIX, SKILLS_PREFIX);
|
|
4142
|
+
await upsertPage(auth.instanceUrl, auth.token, scope, id, newSlug, page.content);
|
|
4143
|
+
await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
|
|
4144
|
+
}
|
|
4145
|
+
await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX, args.name);
|
|
4146
|
+
await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX, {
|
|
4147
|
+
name: args.name,
|
|
4148
|
+
description,
|
|
4149
|
+
source: "project",
|
|
4150
|
+
draft: false
|
|
4151
|
+
});
|
|
4152
|
+
return `Promoted skill "${args.name}" from draft to published.`;
|
|
4153
|
+
} catch (err) {
|
|
4154
|
+
return `Error promoting skill: ${err.message}`;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}),
|
|
4158
|
+
gitlab_skill_discover: tool6({
|
|
4159
|
+
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.",
|
|
4160
|
+
args: {
|
|
4161
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4162
|
+
query: z6.string().describe("Search query (matches skill name and description)"),
|
|
4163
|
+
group_id: z6.string().optional().describe("Group path to search for shared skills (optional)")
|
|
4164
|
+
},
|
|
4165
|
+
execute: async (args) => {
|
|
4166
|
+
const auth = authAndValidate(args.project_id);
|
|
4167
|
+
const sections = [];
|
|
4168
|
+
if (args.group_id) {
|
|
3940
4169
|
try {
|
|
3941
|
-
await
|
|
4170
|
+
const entries = await readIndex(
|
|
3942
4171
|
auth.instanceUrl,
|
|
3943
4172
|
auth.token,
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
4173
|
+
"groups",
|
|
4174
|
+
args.group_id,
|
|
4175
|
+
SKILLS_INDEX
|
|
4176
|
+
);
|
|
4177
|
+
const q = args.query.toLowerCase();
|
|
4178
|
+
const matches = entries.filter(
|
|
4179
|
+
(e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
|
|
3948
4180
|
);
|
|
4181
|
+
if (matches.length > 0) {
|
|
4182
|
+
sections.push(
|
|
4183
|
+
`### Group skills (${matches.length})
|
|
4184
|
+
|
|
4185
|
+
` + matches.map(
|
|
4186
|
+
(e) => `**${e.name}**: ${e.description}
|
|
4187
|
+
Install: \`gitlab_skill_install(name="${e.name}", source="group", group_id="${args.group_id}")\``
|
|
4188
|
+
).join("\n\n")
|
|
4189
|
+
);
|
|
4190
|
+
}
|
|
3949
4191
|
} catch {
|
|
3950
|
-
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
const shResults = searchSkillsSh(args.query);
|
|
4195
|
+
if (shResults.length > 0) {
|
|
4196
|
+
sections.push(
|
|
4197
|
+
`### skills.sh (${shResults.length})
|
|
4198
|
+
|
|
4199
|
+
` + shResults.map(
|
|
4200
|
+
(r) => `**${r.identifier}** (${r.installs})
|
|
4201
|
+
${r.url}
|
|
4202
|
+
Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
|
|
4203
|
+
).join("\n\n")
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
if (sections.length === 0) {
|
|
4207
|
+
return `No skills found matching "${args.query}" in ${args.group_id ? "group wiki or " : ""}skills.sh.`;
|
|
4208
|
+
}
|
|
4209
|
+
return sections.join("\n\n---\n\n");
|
|
4210
|
+
}
|
|
4211
|
+
}),
|
|
4212
|
+
gitlab_skill_install: tool6({
|
|
4213
|
+
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.",
|
|
4214
|
+
args: {
|
|
4215
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4216
|
+
name: z6.string().describe(
|
|
4217
|
+
'Skill identifier. For group: skill name (e.g., "incident-retro"). For skills.sh: full identifier (e.g., "vercel-labs/agent-skills@nextjs-developer").'
|
|
4218
|
+
),
|
|
4219
|
+
source: z6.enum(["group", "skills.sh"]).describe('Where to install from: "group" (group wiki) or "skills.sh" (public registry)'),
|
|
4220
|
+
group_id: z6.string().optional().describe("Group path (required when source is group)"),
|
|
4221
|
+
draft: z6.boolean().optional().describe("Install as draft (default: false)")
|
|
4222
|
+
},
|
|
4223
|
+
execute: async (args) => {
|
|
4224
|
+
const auth = authAndValidate(args.project_id);
|
|
4225
|
+
const projectScope = resolveScope2(args);
|
|
4226
|
+
const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
4227
|
+
const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4228
|
+
if (args.source === "skills.sh") {
|
|
4229
|
+
const downloaded = downloadSkillFromSkillsSh(args.name);
|
|
4230
|
+
if (!downloaded) {
|
|
4231
|
+
return `Failed to download skill "${args.name}" from skills.sh. Check that the identifier is correct (e.g., "owner/repo@skill-name").`;
|
|
4232
|
+
}
|
|
4233
|
+
const skillSlug = `${targetPrefix}/${downloaded.name}/SKILL`;
|
|
4234
|
+
try {
|
|
4235
|
+
await upsertPage(
|
|
3951
4236
|
auth.instanceUrl,
|
|
3952
4237
|
auth.token,
|
|
3953
|
-
scope,
|
|
3954
|
-
id,
|
|
3955
|
-
|
|
3956
|
-
|
|
4238
|
+
projectScope.scope,
|
|
4239
|
+
projectScope.id,
|
|
4240
|
+
skillSlug,
|
|
4241
|
+
downloaded.content
|
|
3957
4242
|
);
|
|
4243
|
+
let fileCount = 1;
|
|
4244
|
+
for (const file of downloaded.files) {
|
|
4245
|
+
const ext = file.path.replace(/\.[^.]+$/, "");
|
|
4246
|
+
const refSlug = `${targetPrefix}/${downloaded.name}/${ext}`;
|
|
4247
|
+
await upsertPage(
|
|
4248
|
+
auth.instanceUrl,
|
|
4249
|
+
auth.token,
|
|
4250
|
+
projectScope.scope,
|
|
4251
|
+
projectScope.id,
|
|
4252
|
+
refSlug,
|
|
4253
|
+
file.content
|
|
4254
|
+
);
|
|
4255
|
+
fileCount++;
|
|
4256
|
+
}
|
|
4257
|
+
await upsertIndexEntry(
|
|
4258
|
+
auth.instanceUrl,
|
|
4259
|
+
auth.token,
|
|
4260
|
+
projectScope.scope,
|
|
4261
|
+
projectScope.id,
|
|
4262
|
+
targetIndex,
|
|
4263
|
+
{
|
|
4264
|
+
name: downloaded.name,
|
|
4265
|
+
description: downloaded.description,
|
|
4266
|
+
source: `skills.sh:${args.name}`,
|
|
4267
|
+
draft: !!args.draft
|
|
4268
|
+
}
|
|
4269
|
+
);
|
|
4270
|
+
return `Installed skill "${downloaded.name}" from skills.sh. ${fileCount} page(s) written.`;
|
|
4271
|
+
} catch (err) {
|
|
4272
|
+
return `Error installing skill from skills.sh: ${err.message}`;
|
|
3958
4273
|
}
|
|
3959
|
-
|
|
3960
|
-
|
|
4274
|
+
}
|
|
4275
|
+
if (!args.group_id) {
|
|
4276
|
+
return 'Error: group_id is required when source is "group".';
|
|
4277
|
+
}
|
|
4278
|
+
try {
|
|
4279
|
+
const groupPages = await listWikiPages(
|
|
4280
|
+
auth.instanceUrl,
|
|
4281
|
+
auth.token,
|
|
4282
|
+
"groups",
|
|
4283
|
+
args.group_id,
|
|
4284
|
+
true
|
|
4285
|
+
);
|
|
4286
|
+
const sourcePrefix = `${SKILLS_PREFIX}/${args.name}/`;
|
|
4287
|
+
const skillPages = groupPages.filter(
|
|
4288
|
+
(p) => p.slug.startsWith(sourcePrefix) && p.content
|
|
4289
|
+
);
|
|
4290
|
+
if (skillPages.length === 0) {
|
|
4291
|
+
return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
|
|
4292
|
+
}
|
|
4293
|
+
for (const page of skillPages) {
|
|
4294
|
+
const newSlug = page.slug.replace(SKILLS_PREFIX, targetPrefix);
|
|
4295
|
+
await upsertPage(
|
|
4296
|
+
auth.instanceUrl,
|
|
4297
|
+
auth.token,
|
|
4298
|
+
projectScope.scope,
|
|
4299
|
+
projectScope.id,
|
|
4300
|
+
newSlug,
|
|
4301
|
+
page.content
|
|
4302
|
+
);
|
|
4303
|
+
}
|
|
4304
|
+
const groupIndex = await readIndex(
|
|
4305
|
+
auth.instanceUrl,
|
|
4306
|
+
auth.token,
|
|
4307
|
+
"groups",
|
|
4308
|
+
args.group_id,
|
|
4309
|
+
SKILLS_INDEX
|
|
4310
|
+
);
|
|
4311
|
+
const entry = groupIndex.find((e) => e.name === args.name);
|
|
4312
|
+
const description = entry?.description ?? "(installed from group)";
|
|
4313
|
+
await upsertIndexEntry(
|
|
4314
|
+
auth.instanceUrl,
|
|
4315
|
+
auth.token,
|
|
4316
|
+
projectScope.scope,
|
|
4317
|
+
projectScope.id,
|
|
4318
|
+
targetIndex,
|
|
4319
|
+
{
|
|
4320
|
+
name: args.name,
|
|
4321
|
+
description,
|
|
4322
|
+
source: `group:${args.group_id}`,
|
|
4323
|
+
draft: !!args.draft
|
|
4324
|
+
}
|
|
4325
|
+
);
|
|
4326
|
+
return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s) copied.`;
|
|
3961
4327
|
} catch (err) {
|
|
3962
|
-
|
|
3963
|
-
|
|
4328
|
+
return `Error installing skill: ${err.message}`;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
}),
|
|
4332
|
+
gitlab_skill_delete: tool6({
|
|
4333
|
+
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.",
|
|
4334
|
+
args: {
|
|
4335
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4336
|
+
name: z6.string().describe("Skill name to delete"),
|
|
4337
|
+
draft: z6.boolean().optional().describe("Delete from drafts instead of published (default: false)"),
|
|
4338
|
+
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
4339
|
+
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
4340
|
+
},
|
|
4341
|
+
execute: async (args) => {
|
|
4342
|
+
const auth = authAndValidate(args.project_id);
|
|
4343
|
+
const { scope, id } = resolveScope2(args);
|
|
4344
|
+
const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
4345
|
+
const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4346
|
+
const skillPrefix = `${prefix}/${args.name}/`;
|
|
4347
|
+
try {
|
|
4348
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
|
|
4349
|
+
const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix));
|
|
4350
|
+
if (skillPages.length === 0) {
|
|
4351
|
+
return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
|
|
3964
4352
|
}
|
|
3965
|
-
|
|
4353
|
+
for (const page of skillPages) {
|
|
4354
|
+
await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
|
|
4355
|
+
}
|
|
4356
|
+
await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
|
|
4357
|
+
return `Deleted skill "${args.name}" (${skillPages.length} page(s) removed).`;
|
|
4358
|
+
} catch (err) {
|
|
4359
|
+
return `Error deleting skill: ${err.message}`;
|
|
3966
4360
|
}
|
|
3967
4361
|
}
|
|
3968
4362
|
})
|