opencode-gitlab-dap 1.13.0 → 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/dist/index.js CHANGED
@@ -3778,6 +3778,10 @@ ${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`;
@@ -3908,6 +3912,83 @@ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
3908
3912
  await createWikiPage(instanceUrl, token, scope, id, slug, content);
3909
3913
  }
3910
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
+ }
3911
3992
  function makeSkillTools(ctx) {
3912
3993
  function authAndValidate(projectId) {
3913
3994
  const auth = ctx.ensureAuth();
@@ -4075,55 +4156,125 @@ Load with: gitlab_skill_load_reference(name="${args.name}", reference="<name>")`
4075
4156
  }
4076
4157
  }),
4077
4158
  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.",
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.",
4079
4160
  args: {
4080
4161
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4081
4162
  query: z6.string().describe("Search query (matches skill name and description)"),
4082
- group_id: z6.string().describe("Group path to search for shared skills")
4163
+ group_id: z6.string().optional().describe("Group path to search for shared skills (optional)")
4083
4164
  },
4084
4165
  execute: async (args) => {
4085
4166
  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}":
4167
+ const sections = [];
4168
+ if (args.group_id) {
4169
+ try {
4170
+ const entries = await readIndex(
4171
+ auth.instanceUrl,
4172
+ auth.token,
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)
4180
+ );
4181
+ if (matches.length > 0) {
4182
+ sections.push(
4183
+ `### Group skills (${matches.length})
4106
4184
 
4107
4185
  ` + 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}`;
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
+ }
4191
+ } catch {
4192
+ }
4113
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");
4114
4210
  }
4115
4211
  }),
4116
4212
  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.",
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.",
4118
4214
  args: {
4119
4215
  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"),
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)"),
4122
4221
  draft: z6.boolean().optional().describe("Install as draft (default: false)")
4123
4222
  },
4124
4223
  execute: async (args) => {
4125
4224
  const auth = authAndValidate(args.project_id);
4126
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(
4236
+ auth.instanceUrl,
4237
+ auth.token,
4238
+ projectScope.scope,
4239
+ projectScope.id,
4240
+ skillSlug,
4241
+ downloaded.content
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}`;
4273
+ }
4274
+ }
4275
+ if (!args.group_id) {
4276
+ return 'Error: group_id is required when source is "group".';
4277
+ }
4127
4278
  try {
4128
4279
  const groupPages = await listWikiPages(
4129
4280
  auth.instanceUrl,
@@ -4139,8 +4290,6 @@ Install: gitlab_skill_install(name="${e.name}", group_id="${args.group_id}")`
4139
4290
  if (skillPages.length === 0) {
4140
4291
  return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
4141
4292
  }
4142
- const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4143
- const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4144
4293
  for (const page of skillPages) {
4145
4294
  const newSlug = page.slug.replace(SKILLS_PREFIX, targetPrefix);
4146
4295
  await upsertPage(
@@ -4174,7 +4323,7 @@ Install: gitlab_skill_install(name="${e.name}", group_id="${args.group_id}")`
4174
4323
  draft: !!args.draft
4175
4324
  }
4176
4325
  );
4177
- return `Installed skill "${args.name}" from group "${args.group_id}" into project. ${skillPages.length} page(s) copied.`;
4326
+ return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s) copied.`;
4178
4327
  } catch (err) {
4179
4328
  return `Error installing skill: ${err.message}`;
4180
4329
  }