opencode-gitlab-dap 1.12.0 → 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 +13 -8
- package/dist/index.cjs +338 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +338 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3467,8 +3467,11 @@ var PROJECT_ID_DESC = 'FULL project path with namespace (e.g., "gitlab-org/gitla
|
|
|
3467
3467
|
function today() {
|
|
3468
3468
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3469
3469
|
}
|
|
3470
|
+
function shortHash() {
|
|
3471
|
+
return Math.random().toString(36).slice(2, 8);
|
|
3472
|
+
}
|
|
3470
3473
|
function slugify(text) {
|
|
3471
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0,
|
|
3474
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 50);
|
|
3472
3475
|
}
|
|
3473
3476
|
function titleFromContent(content) {
|
|
3474
3477
|
const first = content.split("\n")[0].replace(/^[#*\->\s]+/, "").trim();
|
|
@@ -3556,7 +3559,7 @@ ${entries}`
|
|
|
3556
3559
|
if (!dir) return `Unknown memory type: ${args.type}`;
|
|
3557
3560
|
const date = today();
|
|
3558
3561
|
const title = titleFromContent(args.content);
|
|
3559
|
-
const slug = `${dir}/${date}-${slugify(title)}`;
|
|
3562
|
+
const slug = `${dir}/${date}-${slugify(title)}-${shortHash()}`;
|
|
3560
3563
|
const header = `*Recorded: ${date} | Type: ${args.type}*
|
|
3561
3564
|
|
|
3562
3565
|
`;
|
|
@@ -3761,7 +3764,7 @@ ${e.content}`).join("\n\n---\n\n");
|
|
|
3761
3764
|
const auth = authAndValidate(args.project_id);
|
|
3762
3765
|
const { scope, id } = resolveScope(args);
|
|
3763
3766
|
const date = today();
|
|
3764
|
-
const slug = `${PREFIX}/memory/sessions/${date}-${slugify(args.title)}`;
|
|
3767
|
+
const slug = `${PREFIX}/memory/sessions/${date}-${slugify(args.title)}-${shortHash()}`;
|
|
3765
3768
|
try {
|
|
3766
3769
|
await createWikiPage(auth.instanceUrl, auth.token, scope, id, slug, args.summary);
|
|
3767
3770
|
return `Session logged: ${slug}`;
|
|
@@ -3779,6 +3782,9 @@ var z6 = tool6.schema;
|
|
|
3779
3782
|
var PREFIX2 = "agents";
|
|
3780
3783
|
var SKILLS_PREFIX = `${PREFIX2}/skills`;
|
|
3781
3784
|
var DRAFTS_PREFIX = `${PREFIX2}/skills-drafts`;
|
|
3785
|
+
var SKILLS_INDEX = `${SKILLS_PREFIX}/index`;
|
|
3786
|
+
var DRAFTS_INDEX = `${DRAFTS_PREFIX}/index`;
|
|
3787
|
+
var PROJECT_ID_DESC2 = 'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.';
|
|
3782
3788
|
function resolveScope2(args) {
|
|
3783
3789
|
if (args.scope === "groups" && args.group_id) {
|
|
3784
3790
|
return { scope: "groups", id: args.group_id };
|
|
@@ -3791,6 +3797,117 @@ function validateProjectId2(projectId) {
|
|
|
3791
3797
|
}
|
|
3792
3798
|
return null;
|
|
3793
3799
|
}
|
|
3800
|
+
function parseIndex(content) {
|
|
3801
|
+
const entries = [];
|
|
3802
|
+
const blocks = content.split(/^## /m).filter(Boolean);
|
|
3803
|
+
for (const block of blocks) {
|
|
3804
|
+
const lines = block.trim().split("\n");
|
|
3805
|
+
const name = lines[0].trim();
|
|
3806
|
+
if (!name) continue;
|
|
3807
|
+
const rest = lines.slice(1).join("\n").trim();
|
|
3808
|
+
const descLines = [];
|
|
3809
|
+
let source;
|
|
3810
|
+
for (const line of rest.split("\n")) {
|
|
3811
|
+
if (line.startsWith("Source:")) {
|
|
3812
|
+
source = line.slice(7).trim();
|
|
3813
|
+
} else if (line.trim()) {
|
|
3814
|
+
descLines.push(line);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
entries.push({ name, description: descLines.join("\n"), source, draft: false });
|
|
3818
|
+
}
|
|
3819
|
+
return entries;
|
|
3820
|
+
}
|
|
3821
|
+
function formatIndex(entries) {
|
|
3822
|
+
return entries.map((e) => {
|
|
3823
|
+
let block = `## ${e.name}
|
|
3824
|
+
${e.description}`;
|
|
3825
|
+
if (e.source) block += `
|
|
3826
|
+
Source: ${e.source}`;
|
|
3827
|
+
return block;
|
|
3828
|
+
}).join("\n\n");
|
|
3829
|
+
}
|
|
3830
|
+
async function readIndex(instanceUrl, token, scope, id, indexSlug) {
|
|
3831
|
+
try {
|
|
3832
|
+
const page = await getWikiPage(instanceUrl, token, scope, id, indexSlug);
|
|
3833
|
+
return parseIndex(page.content);
|
|
3834
|
+
} catch {
|
|
3835
|
+
return [];
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
async function writeIndex(instanceUrl, token, scope, id, indexSlug, entries) {
|
|
3839
|
+
const content = formatIndex(entries);
|
|
3840
|
+
try {
|
|
3841
|
+
await updateWikiPage(instanceUrl, token, scope, id, indexSlug, content);
|
|
3842
|
+
} catch {
|
|
3843
|
+
await createWikiPage(instanceUrl, token, scope, id, indexSlug, content || "# Skills Index");
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
async function upsertIndexEntry(instanceUrl, token, scope, id, indexSlug, entry) {
|
|
3847
|
+
const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
3848
|
+
const idx = entries.findIndex((e) => e.name === entry.name);
|
|
3849
|
+
if (idx >= 0) {
|
|
3850
|
+
entries[idx] = entry;
|
|
3851
|
+
} else {
|
|
3852
|
+
entries.push(entry);
|
|
3853
|
+
}
|
|
3854
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, entries);
|
|
3855
|
+
}
|
|
3856
|
+
async function removeIndexEntry(instanceUrl, token, scope, id, indexSlug, name) {
|
|
3857
|
+
const entries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
3858
|
+
const filtered = entries.filter((e) => e.name !== name);
|
|
3859
|
+
if (filtered.length !== entries.length) {
|
|
3860
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, filtered);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
function extractSkillNames(pages, prefix) {
|
|
3864
|
+
const skillSuffix = "/SKILL";
|
|
3865
|
+
const names = /* @__PURE__ */ new Set();
|
|
3866
|
+
for (const p of pages) {
|
|
3867
|
+
if (p.slug.startsWith(prefix + "/") && p.slug.endsWith(skillSuffix)) {
|
|
3868
|
+
const middle = p.slug.slice(prefix.length + 1, -skillSuffix.length);
|
|
3869
|
+
if (middle && !middle.includes("/")) {
|
|
3870
|
+
names.add(middle);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
return [...names];
|
|
3875
|
+
}
|
|
3876
|
+
async function rebuildIndex(instanceUrl, token, scope, id, prefix, indexSlug) {
|
|
3877
|
+
const pages = await listWikiPages(instanceUrl, token, scope, id);
|
|
3878
|
+
const actualNames = extractSkillNames(pages, prefix);
|
|
3879
|
+
const currentEntries = await readIndex(instanceUrl, token, scope, id, indexSlug);
|
|
3880
|
+
const indexed = new Set(currentEntries.map((e) => e.name));
|
|
3881
|
+
const actual = new Set(actualNames);
|
|
3882
|
+
let dirty = false;
|
|
3883
|
+
const removed = currentEntries.filter((e) => !actual.has(e.name));
|
|
3884
|
+
if (removed.length > 0) dirty = true;
|
|
3885
|
+
const added = [];
|
|
3886
|
+
for (const name of actualNames) {
|
|
3887
|
+
if (!indexed.has(name)) {
|
|
3888
|
+
added.push(name);
|
|
3889
|
+
dirty = true;
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
if (!dirty && added.length === 0) return currentEntries;
|
|
3893
|
+
const kept = currentEntries.filter((e) => actual.has(e.name));
|
|
3894
|
+
for (const name of added) {
|
|
3895
|
+
kept.push({
|
|
3896
|
+
name,
|
|
3897
|
+
description: "(auto-indexed \u2014 update description with gitlab_skill_save)",
|
|
3898
|
+
draft: prefix === DRAFTS_PREFIX
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
await writeIndex(instanceUrl, token, scope, id, indexSlug, kept);
|
|
3902
|
+
return kept;
|
|
3903
|
+
}
|
|
3904
|
+
async function upsertPage(instanceUrl, token, scope, id, slug, content) {
|
|
3905
|
+
try {
|
|
3906
|
+
await updateWikiPage(instanceUrl, token, scope, id, slug, content);
|
|
3907
|
+
} catch {
|
|
3908
|
+
await createWikiPage(instanceUrl, token, scope, id, slug, content);
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3794
3911
|
function makeSkillTools(ctx) {
|
|
3795
3912
|
function authAndValidate(projectId) {
|
|
3796
3913
|
const auth = ctx.ensureAuth();
|
|
@@ -3801,11 +3918,9 @@ function makeSkillTools(ctx) {
|
|
|
3801
3918
|
}
|
|
3802
3919
|
return {
|
|
3803
3920
|
gitlab_skill_list: tool6({
|
|
3804
|
-
description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).",
|
|
3921
|
+
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.",
|
|
3805
3922
|
args: {
|
|
3806
|
-
project_id: z6.string().describe(
|
|
3807
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3808
|
-
),
|
|
3923
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3809
3924
|
include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
|
|
3810
3925
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3811
3926
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3814,23 +3929,27 @@ function makeSkillTools(ctx) {
|
|
|
3814
3929
|
const auth = authAndValidate(args.project_id);
|
|
3815
3930
|
const { scope, id } = resolveScope2(args);
|
|
3816
3931
|
try {
|
|
3817
|
-
const
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3932
|
+
const published = await rebuildIndex(
|
|
3933
|
+
auth.instanceUrl,
|
|
3934
|
+
auth.token,
|
|
3935
|
+
scope,
|
|
3936
|
+
id,
|
|
3937
|
+
SKILLS_PREFIX,
|
|
3938
|
+
SKILLS_INDEX
|
|
3939
|
+
);
|
|
3824
3940
|
let drafts = [];
|
|
3825
3941
|
if (args.include_drafts) {
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3942
|
+
drafts = await rebuildIndex(
|
|
3943
|
+
auth.instanceUrl,
|
|
3944
|
+
auth.token,
|
|
3945
|
+
scope,
|
|
3946
|
+
id,
|
|
3947
|
+
DRAFTS_PREFIX,
|
|
3948
|
+
DRAFTS_INDEX
|
|
3949
|
+
);
|
|
3950
|
+
drafts = drafts.map((e) => ({ ...e, draft: true }));
|
|
3832
3951
|
}
|
|
3833
|
-
const all = [...
|
|
3952
|
+
const all = [...published, ...drafts];
|
|
3834
3953
|
if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
|
|
3835
3954
|
return JSON.stringify(all, null, 2);
|
|
3836
3955
|
} catch (err) {
|
|
@@ -3839,11 +3958,9 @@ function makeSkillTools(ctx) {
|
|
|
3839
3958
|
}
|
|
3840
3959
|
}),
|
|
3841
3960
|
gitlab_skill_load: tool6({
|
|
3842
|
-
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.",
|
|
3961
|
+
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.",
|
|
3843
3962
|
args: {
|
|
3844
|
-
project_id: z6.string().describe(
|
|
3845
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3846
|
-
),
|
|
3963
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3847
3964
|
name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
|
|
3848
3965
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3849
3966
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3851,41 +3968,46 @@ function makeSkillTools(ctx) {
|
|
|
3851
3968
|
execute: async (args) => {
|
|
3852
3969
|
const auth = authAndValidate(args.project_id);
|
|
3853
3970
|
const { scope, id } = resolveScope2(args);
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
auth.instanceUrl,
|
|
3857
|
-
auth.token,
|
|
3858
|
-
scope,
|
|
3859
|
-
id,
|
|
3860
|
-
`${SKILLS_PREFIX}/${args.name}`
|
|
3861
|
-
);
|
|
3862
|
-
return page.content;
|
|
3863
|
-
} catch {
|
|
3971
|
+
const prefixes = [SKILLS_PREFIX, DRAFTS_PREFIX];
|
|
3972
|
+
for (const prefix of prefixes) {
|
|
3864
3973
|
try {
|
|
3865
|
-
const
|
|
3974
|
+
const page = await getWikiPage(
|
|
3866
3975
|
auth.instanceUrl,
|
|
3867
3976
|
auth.token,
|
|
3868
3977
|
scope,
|
|
3869
3978
|
id,
|
|
3870
|
-
`${
|
|
3979
|
+
`${prefix}/${args.name}/SKILL`
|
|
3871
3980
|
);
|
|
3872
|
-
|
|
3981
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
|
|
3982
|
+
const refPrefix = `${prefix}/${args.name}/references/`;
|
|
3983
|
+
const refs = pages.filter((p) => p.slug.startsWith(refPrefix)).map((p) => p.slug.slice(refPrefix.length));
|
|
3984
|
+
const isDraft = prefix === DRAFTS_PREFIX;
|
|
3985
|
+
let result = isDraft ? `[DRAFT SKILL]
|
|
3986
|
+
|
|
3987
|
+
${page.content}` : page.content;
|
|
3988
|
+
if (refs.length > 0) {
|
|
3989
|
+
result += `
|
|
3873
3990
|
|
|
3874
|
-
|
|
3991
|
+
---
|
|
3992
|
+
Available references: ${refs.join(", ")}`;
|
|
3993
|
+
result += `
|
|
3994
|
+
Load with: gitlab_skill_load_reference(name="${args.name}", reference="<name>")`;
|
|
3995
|
+
}
|
|
3996
|
+
return result;
|
|
3875
3997
|
} catch {
|
|
3876
|
-
|
|
3998
|
+
continue;
|
|
3877
3999
|
}
|
|
3878
4000
|
}
|
|
4001
|
+
return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
|
|
3879
4002
|
}
|
|
3880
4003
|
}),
|
|
3881
4004
|
gitlab_skill_save: tool6({
|
|
3882
|
-
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.",
|
|
4005
|
+
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.",
|
|
3883
4006
|
args: {
|
|
3884
|
-
project_id: z6.string().describe(
|
|
3885
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3886
|
-
),
|
|
4007
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3887
4008
|
name: z6.string().describe('Skill name (e.g., "incident-retro")'),
|
|
3888
4009
|
content: z6.string().describe("Skill content in markdown"),
|
|
4010
|
+
description: z6.string().describe("Short description for the skill index (1-2 sentences)"),
|
|
3889
4011
|
draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
|
|
3890
4012
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3891
4013
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3894,35 +4016,27 @@ ${draft.content}`;
|
|
|
3894
4016
|
const auth = authAndValidate(args.project_id);
|
|
3895
4017
|
const { scope, id } = resolveScope2(args);
|
|
3896
4018
|
const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
3897
|
-
const
|
|
4019
|
+
const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4020
|
+
const slug = `${prefix}/${args.name}/SKILL`;
|
|
3898
4021
|
const label = args.draft ? "draft " : "";
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
3911
|
-
continue;
|
|
3912
|
-
}
|
|
3913
|
-
return `Error saving skill: ${msg}`;
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
4022
|
+
try {
|
|
4023
|
+
await upsertPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
|
|
4024
|
+
await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, {
|
|
4025
|
+
name: args.name,
|
|
4026
|
+
description: args.description,
|
|
4027
|
+
source: "project",
|
|
4028
|
+
draft: !!args.draft
|
|
4029
|
+
});
|
|
4030
|
+
return `Saved ${label}skill: ${args.name}`;
|
|
4031
|
+
} catch (err) {
|
|
4032
|
+
return `Error saving skill: ${err.message}`;
|
|
3916
4033
|
}
|
|
3917
|
-
return `Error saving skill: failed after 3 retries`;
|
|
3918
4034
|
}
|
|
3919
4035
|
}),
|
|
3920
4036
|
gitlab_skill_promote: tool6({
|
|
3921
|
-
description: "Promote a draft skill to published.\nMoves
|
|
4037
|
+
description: "Promote a draft skill to published.\nMoves all skill pages from drafts to published and updates both indexes.",
|
|
3922
4038
|
args: {
|
|
3923
|
-
project_id: z6.string().describe(
|
|
3924
|
-
'FULL project path with namespace (e.g., "gitlab-org/gitlab"). Must contain a slash. Never use just the project name.'
|
|
3925
|
-
),
|
|
4039
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
3926
4040
|
name: z6.string().describe("Skill name to promote"),
|
|
3927
4041
|
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
3928
4042
|
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
@@ -3930,36 +4044,170 @@ ${draft.content}`;
|
|
|
3930
4044
|
execute: async (args) => {
|
|
3931
4045
|
const auth = authAndValidate(args.project_id);
|
|
3932
4046
|
const { scope, id } = resolveScope2(args);
|
|
3933
|
-
const draftSlug = `${DRAFTS_PREFIX}/${args.name}`;
|
|
3934
|
-
const publishedSlug = `${SKILLS_PREFIX}/${args.name}`;
|
|
3935
4047
|
try {
|
|
3936
|
-
const
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
4048
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
|
|
4049
|
+
const draftPrefix = `${DRAFTS_PREFIX}/${args.name}/`;
|
|
4050
|
+
const draftPages = pages.filter(
|
|
4051
|
+
(p) => p.slug.startsWith(draftPrefix) && p.content
|
|
4052
|
+
);
|
|
4053
|
+
if (draftPages.length === 0) {
|
|
4054
|
+
return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
|
|
4055
|
+
}
|
|
4056
|
+
const draftIndex = await readIndex(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX);
|
|
4057
|
+
const entry = draftIndex.find((e) => e.name === args.name);
|
|
4058
|
+
const description = entry?.description ?? "(promoted from draft)";
|
|
4059
|
+
for (const page of draftPages) {
|
|
4060
|
+
const newSlug = page.slug.replace(DRAFTS_PREFIX, SKILLS_PREFIX);
|
|
4061
|
+
await upsertPage(auth.instanceUrl, auth.token, scope, id, newSlug, page.content);
|
|
4062
|
+
await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
|
|
4063
|
+
}
|
|
4064
|
+
await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, DRAFTS_INDEX, args.name);
|
|
4065
|
+
await upsertIndexEntry(auth.instanceUrl, auth.token, scope, id, SKILLS_INDEX, {
|
|
4066
|
+
name: args.name,
|
|
4067
|
+
description,
|
|
4068
|
+
source: "project",
|
|
4069
|
+
draft: false
|
|
4070
|
+
});
|
|
4071
|
+
return `Promoted skill "${args.name}" from draft to published.`;
|
|
4072
|
+
} catch (err) {
|
|
4073
|
+
return `Error promoting skill: ${err.message}`;
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
}),
|
|
4077
|
+
gitlab_skill_discover: tool6({
|
|
4078
|
+
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.",
|
|
4079
|
+
args: {
|
|
4080
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4081
|
+
query: z6.string().describe("Search query (matches skill name and description)"),
|
|
4082
|
+
group_id: z6.string().describe("Group path to search for shared skills")
|
|
4083
|
+
},
|
|
4084
|
+
execute: async (args) => {
|
|
4085
|
+
const auth = authAndValidate(args.project_id);
|
|
4086
|
+
try {
|
|
4087
|
+
const entries = await readIndex(
|
|
4088
|
+
auth.instanceUrl,
|
|
4089
|
+
auth.token,
|
|
4090
|
+
"groups",
|
|
4091
|
+
args.group_id,
|
|
4092
|
+
SKILLS_INDEX
|
|
4093
|
+
);
|
|
4094
|
+
if (entries.length === 0) {
|
|
4095
|
+
return `No skills found in group "${args.group_id}" wiki. The group skill index (${SKILLS_INDEX}) is empty or does not exist.`;
|
|
4096
|
+
}
|
|
4097
|
+
const q = args.query.toLowerCase();
|
|
4098
|
+
const matches = entries.filter(
|
|
4099
|
+
(e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
|
|
4100
|
+
);
|
|
4101
|
+
if (matches.length === 0) {
|
|
4102
|
+
return `No skills matching "${args.query}" in group "${args.group_id}". Available skills:
|
|
4103
|
+
${entries.map((e) => `- ${e.name}: ${e.description}`).join("\n")}`;
|
|
4104
|
+
}
|
|
4105
|
+
return `Found ${matches.length} skill(s) in group "${args.group_id}":
|
|
4106
|
+
|
|
4107
|
+
` + matches.map(
|
|
4108
|
+
(e) => `**${e.name}**: ${e.description}
|
|
4109
|
+
Install: gitlab_skill_install(name="${e.name}", group_id="${args.group_id}")`
|
|
4110
|
+
).join("\n\n");
|
|
4111
|
+
} catch (err) {
|
|
4112
|
+
return `Error discovering skills: ${err.message}`;
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
}),
|
|
4116
|
+
gitlab_skill_install: tool6({
|
|
4117
|
+
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.",
|
|
4118
|
+
args: {
|
|
4119
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4120
|
+
name: z6.string().describe('Skill name to install (e.g., "incident-retro")'),
|
|
4121
|
+
group_id: z6.string().describe("Group path to install from"),
|
|
4122
|
+
draft: z6.boolean().optional().describe("Install as draft (default: false)")
|
|
4123
|
+
},
|
|
4124
|
+
execute: async (args) => {
|
|
4125
|
+
const auth = authAndValidate(args.project_id);
|
|
4126
|
+
const projectScope = resolveScope2(args);
|
|
4127
|
+
try {
|
|
4128
|
+
const groupPages = await listWikiPages(
|
|
4129
|
+
auth.instanceUrl,
|
|
4130
|
+
auth.token,
|
|
4131
|
+
"groups",
|
|
4132
|
+
args.group_id,
|
|
4133
|
+
true
|
|
4134
|
+
);
|
|
4135
|
+
const sourcePrefix = `${SKILLS_PREFIX}/${args.name}/`;
|
|
4136
|
+
const skillPages = groupPages.filter(
|
|
4137
|
+
(p) => p.slug.startsWith(sourcePrefix) && p.content
|
|
4138
|
+
);
|
|
4139
|
+
if (skillPages.length === 0) {
|
|
4140
|
+
return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
|
|
4141
|
+
}
|
|
4142
|
+
const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
4143
|
+
const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4144
|
+
for (const page of skillPages) {
|
|
4145
|
+
const newSlug = page.slug.replace(SKILLS_PREFIX, targetPrefix);
|
|
4146
|
+
await upsertPage(
|
|
3948
4147
|
auth.instanceUrl,
|
|
3949
4148
|
auth.token,
|
|
3950
|
-
scope,
|
|
3951
|
-
id,
|
|
3952
|
-
|
|
3953
|
-
|
|
4149
|
+
projectScope.scope,
|
|
4150
|
+
projectScope.id,
|
|
4151
|
+
newSlug,
|
|
4152
|
+
page.content
|
|
3954
4153
|
);
|
|
3955
4154
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
4155
|
+
const groupIndex = await readIndex(
|
|
4156
|
+
auth.instanceUrl,
|
|
4157
|
+
auth.token,
|
|
4158
|
+
"groups",
|
|
4159
|
+
args.group_id,
|
|
4160
|
+
SKILLS_INDEX
|
|
4161
|
+
);
|
|
4162
|
+
const entry = groupIndex.find((e) => e.name === args.name);
|
|
4163
|
+
const description = entry?.description ?? "(installed from group)";
|
|
4164
|
+
await upsertIndexEntry(
|
|
4165
|
+
auth.instanceUrl,
|
|
4166
|
+
auth.token,
|
|
4167
|
+
projectScope.scope,
|
|
4168
|
+
projectScope.id,
|
|
4169
|
+
targetIndex,
|
|
4170
|
+
{
|
|
4171
|
+
name: args.name,
|
|
4172
|
+
description,
|
|
4173
|
+
source: `group:${args.group_id}`,
|
|
4174
|
+
draft: !!args.draft
|
|
4175
|
+
}
|
|
4176
|
+
);
|
|
4177
|
+
return `Installed skill "${args.name}" from group "${args.group_id}" into project. ${skillPages.length} page(s) copied.`;
|
|
3958
4178
|
} catch (err) {
|
|
3959
|
-
|
|
3960
|
-
|
|
4179
|
+
return `Error installing skill: ${err.message}`;
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
}),
|
|
4183
|
+
gitlab_skill_delete: tool6({
|
|
4184
|
+
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.",
|
|
4185
|
+
args: {
|
|
4186
|
+
project_id: z6.string().describe(PROJECT_ID_DESC2),
|
|
4187
|
+
name: z6.string().describe("Skill name to delete"),
|
|
4188
|
+
draft: z6.boolean().optional().describe("Delete from drafts instead of published (default: false)"),
|
|
4189
|
+
scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
|
|
4190
|
+
group_id: z6.string().optional().describe("Group path (required when scope is groups)")
|
|
4191
|
+
},
|
|
4192
|
+
execute: async (args) => {
|
|
4193
|
+
const auth = authAndValidate(args.project_id);
|
|
4194
|
+
const { scope, id } = resolveScope2(args);
|
|
4195
|
+
const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
|
|
4196
|
+
const indexSlug = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
|
|
4197
|
+
const skillPrefix = `${prefix}/${args.name}/`;
|
|
4198
|
+
try {
|
|
4199
|
+
const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
|
|
4200
|
+
const skillPages = pages.filter((p) => p.slug.startsWith(skillPrefix));
|
|
4201
|
+
if (skillPages.length === 0) {
|
|
4202
|
+
return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
|
|
3961
4203
|
}
|
|
3962
|
-
|
|
4204
|
+
for (const page of skillPages) {
|
|
4205
|
+
await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
|
|
4206
|
+
}
|
|
4207
|
+
await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
|
|
4208
|
+
return `Deleted skill "${args.name}" (${skillPages.length} page(s) removed).`;
|
|
4209
|
+
} catch (err) {
|
|
4210
|
+
return `Error deleting skill: ${err.message}`;
|
|
3963
4211
|
}
|
|
3964
4212
|
}
|
|
3965
4213
|
})
|