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