opencode-gitlab-dap 1.13.0 → 1.14.1

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 CHANGED
@@ -3947,6 +3947,10 @@ ${e.content}`).join("\n\n---\n\n");
3947
3947
 
3948
3948
  // src/tools/skill-tools.ts
3949
3949
  var import_plugin6 = require("@opencode-ai/plugin");
3950
+ var import_child_process = require("child_process");
3951
+ var import_fs2 = require("fs");
3952
+ var import_path2 = require("path");
3953
+ var import_os2 = require("os");
3950
3954
  var z6 = import_plugin6.tool.schema;
3951
3955
  var PREFIX2 = "agents";
3952
3956
  var SKILLS_PREFIX = `${PREFIX2}/skills`;
@@ -4061,9 +4065,21 @@ async function rebuildIndex(instanceUrl, token, scope, id, prefix, indexSlug) {
4061
4065
  if (!dirty && added.length === 0) return currentEntries;
4062
4066
  const kept = currentEntries.filter((e) => actual.has(e.name));
4063
4067
  for (const name of added) {
4068
+ let description = "";
4069
+ try {
4070
+ const page = await getWikiPage(instanceUrl, token, scope, id, `${prefix}/${name}/SKILL`);
4071
+ const content = page.content ?? "";
4072
+ const fmMatch = content.match(/^---\s*\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/);
4073
+ if (fmMatch) {
4074
+ description = fmMatch[1].trim();
4075
+ } else {
4076
+ description = content.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim().slice(0, 200);
4077
+ }
4078
+ } catch {
4079
+ }
4064
4080
  kept.push({
4065
4081
  name,
4066
- description: "(auto-indexed \u2014 update description with gitlab_skill_save)",
4082
+ description: description || "(no description \u2014 update with gitlab_skill_save)",
4067
4083
  draft: prefix === DRAFTS_PREFIX
4068
4084
  });
4069
4085
  }
@@ -4077,6 +4093,83 @@ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
4077
4093
  await createWikiPage(instanceUrl, token, scope, id, slug, content);
4078
4094
  }
4079
4095
  }
4096
+ function searchSkillsSh(query) {
4097
+ try {
4098
+ const raw = (0, import_child_process.execSync)(`npx skills find ${JSON.stringify(query)}`, {
4099
+ timeout: 3e4,
4100
+ encoding: "utf-8",
4101
+ stdio: ["pipe", "pipe", "pipe"]
4102
+ });
4103
+ const lines = raw.split("\n");
4104
+ const results = [];
4105
+ for (let i = 0; i < lines.length; i++) {
4106
+ const clean = lines[i].replace(/\x1b\[[0-9;]*m/g, "").trim();
4107
+ const match = clean.match(/^(\S+\/\S+@\S+)\s+(.+installs?)$/);
4108
+ if (match) {
4109
+ const urlLine = (lines[i + 1] ?? "").replace(/\x1b\[[0-9;]*m/g, "").trim();
4110
+ const url = urlLine.startsWith("\u2514 ") ? urlLine.slice(2) : urlLine;
4111
+ results.push({ identifier: match[1], installs: match[2], url });
4112
+ }
4113
+ }
4114
+ return results;
4115
+ } catch {
4116
+ return [];
4117
+ }
4118
+ }
4119
+ function downloadSkillFromSkillsSh(identifier) {
4120
+ const tmp = (0, import_fs2.mkdtempSync)((0, import_path2.join)((0, import_os2.tmpdir)(), "skill-install-"));
4121
+ try {
4122
+ (0, import_child_process.execSync)(`npx skills add ${JSON.stringify(identifier)} -y --copy`, {
4123
+ timeout: 6e4,
4124
+ cwd: tmp,
4125
+ encoding: "utf-8",
4126
+ stdio: ["pipe", "pipe", "pipe"]
4127
+ });
4128
+ const agentsDir = (0, import_path2.join)(tmp, ".agents", "skills");
4129
+ if (!(0, import_fs2.statSync)(agentsDir).isDirectory()) return null;
4130
+ const dirs = (0, import_fs2.readdirSync)(agentsDir);
4131
+ if (dirs.length === 0) return null;
4132
+ const skillName = dirs[0];
4133
+ const skillDir = (0, import_path2.join)(agentsDir, skillName);
4134
+ const skillMd = (0, import_path2.join)(skillDir, "SKILL.md");
4135
+ let mainContent;
4136
+ try {
4137
+ mainContent = (0, import_fs2.readFileSync)(skillMd, "utf-8");
4138
+ } catch {
4139
+ return null;
4140
+ }
4141
+ let description = "";
4142
+ const descMatch = mainContent.match(/^---\s*\n[\s\S]*?description:\s*(.+)\n[\s\S]*?---/);
4143
+ if (descMatch) {
4144
+ description = descMatch[1].trim();
4145
+ } else {
4146
+ const firstParagraph = mainContent.replace(/^---[\s\S]*?---\s*\n/, "").replace(/^#[^\n]*\n+/, "").split("\n\n")[0].replace(/\n/g, " ").trim();
4147
+ description = firstParagraph.slice(0, 200);
4148
+ }
4149
+ const files = [];
4150
+ const walkStack = [{ dir: skillDir, prefix: "" }];
4151
+ while (walkStack.length > 0) {
4152
+ const { dir, prefix } = walkStack.pop();
4153
+ for (const entry of (0, import_fs2.readdirSync)(dir)) {
4154
+ const full = (0, import_path2.join)(dir, entry);
4155
+ const rel = prefix ? `${prefix}/${entry}` : entry;
4156
+ if ((0, import_fs2.statSync)(full).isDirectory()) {
4157
+ walkStack.push({ dir: full, prefix: rel });
4158
+ } else if (entry !== "SKILL.md") {
4159
+ files.push({ path: rel, content: (0, import_fs2.readFileSync)(full, "utf-8") });
4160
+ }
4161
+ }
4162
+ }
4163
+ return { name: skillName, content: mainContent, description, files };
4164
+ } catch {
4165
+ return null;
4166
+ } finally {
4167
+ try {
4168
+ (0, import_fs2.rmSync)(tmp, { recursive: true, force: true });
4169
+ } catch {
4170
+ }
4171
+ }
4172
+ }
4080
4173
  function makeSkillTools(ctx) {
4081
4174
  function authAndValidate(projectId) {
4082
4175
  const auth = ctx.ensureAuth();
@@ -4244,55 +4337,125 @@ Load with: gitlab_skill_load_reference(name="${args.name}", reference="<name>")`
4244
4337
  }
4245
4338
  }),
4246
4339
  gitlab_skill_discover: (0, import_plugin6.tool)({
4247
- 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.",
4340
+ 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.",
4248
4341
  args: {
4249
4342
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4250
4343
  query: z6.string().describe("Search query (matches skill name and description)"),
4251
- group_id: z6.string().describe("Group path to search for shared skills")
4344
+ group_id: z6.string().optional().describe("Group path to search for shared skills (optional)")
4252
4345
  },
4253
4346
  execute: async (args) => {
4254
4347
  const auth = authAndValidate(args.project_id);
4255
- try {
4256
- const entries = await readIndex(
4257
- auth.instanceUrl,
4258
- auth.token,
4259
- "groups",
4260
- args.group_id,
4261
- SKILLS_INDEX
4262
- );
4263
- if (entries.length === 0) {
4264
- return `No skills found in group "${args.group_id}" wiki. The group skill index (${SKILLS_INDEX}) is empty or does not exist.`;
4265
- }
4266
- const q = args.query.toLowerCase();
4267
- const matches = entries.filter(
4268
- (e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
4269
- );
4270
- if (matches.length === 0) {
4271
- return `No skills matching "${args.query}" in group "${args.group_id}". Available skills:
4272
- ${entries.map((e) => `- ${e.name}: ${e.description}`).join("\n")}`;
4273
- }
4274
- return `Found ${matches.length} skill(s) in group "${args.group_id}":
4348
+ const sections = [];
4349
+ if (args.group_id) {
4350
+ try {
4351
+ const entries = await readIndex(
4352
+ auth.instanceUrl,
4353
+ auth.token,
4354
+ "groups",
4355
+ args.group_id,
4356
+ SKILLS_INDEX
4357
+ );
4358
+ const q = args.query.toLowerCase();
4359
+ const matches = entries.filter(
4360
+ (e) => e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q)
4361
+ );
4362
+ if (matches.length > 0) {
4363
+ sections.push(
4364
+ `### Group skills (${matches.length})
4275
4365
 
4276
4366
  ` + matches.map(
4277
- (e) => `**${e.name}**: ${e.description}
4278
- Install: gitlab_skill_install(name="${e.name}", group_id="${args.group_id}")`
4279
- ).join("\n\n");
4280
- } catch (err) {
4281
- return `Error discovering skills: ${err.message}`;
4367
+ (e) => `**${e.name}**: ${e.description}
4368
+ Install: \`gitlab_skill_install(name="${e.name}", source="group", group_id="${args.group_id}")\``
4369
+ ).join("\n\n")
4370
+ );
4371
+ }
4372
+ } catch {
4373
+ }
4374
+ }
4375
+ const shResults = searchSkillsSh(args.query);
4376
+ if (shResults.length > 0) {
4377
+ sections.push(
4378
+ `### skills.sh (${shResults.length})
4379
+
4380
+ ` + shResults.map(
4381
+ (r) => `**${r.identifier}** (${r.installs})
4382
+ ${r.url}
4383
+ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4384
+ ).join("\n\n")
4385
+ );
4282
4386
  }
4387
+ if (sections.length === 0) {
4388
+ return `No skills found matching "${args.query}" in ${args.group_id ? "group wiki or " : ""}skills.sh.`;
4389
+ }
4390
+ return sections.join("\n\n---\n\n");
4283
4391
  }
4284
4392
  }),
4285
4393
  gitlab_skill_install: (0, import_plugin6.tool)({
4286
- 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.",
4394
+ 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.",
4287
4395
  args: {
4288
4396
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4289
- name: z6.string().describe('Skill name to install (e.g., "incident-retro")'),
4290
- group_id: z6.string().describe("Group path to install from"),
4397
+ name: z6.string().describe(
4398
+ 'Skill identifier. For group: skill name (e.g., "incident-retro"). For skills.sh: full identifier (e.g., "vercel-labs/agent-skills@nextjs-developer").'
4399
+ ),
4400
+ source: z6.enum(["group", "skills.sh"]).describe('Where to install from: "group" (group wiki) or "skills.sh" (public registry)'),
4401
+ group_id: z6.string().optional().describe("Group path (required when source is group)"),
4291
4402
  draft: z6.boolean().optional().describe("Install as draft (default: false)")
4292
4403
  },
4293
4404
  execute: async (args) => {
4294
4405
  const auth = authAndValidate(args.project_id);
4295
4406
  const projectScope = resolveScope2(args);
4407
+ const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4408
+ const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4409
+ if (args.source === "skills.sh") {
4410
+ const downloaded = downloadSkillFromSkillsSh(args.name);
4411
+ if (!downloaded) {
4412
+ return `Failed to download skill "${args.name}" from skills.sh. Check that the identifier is correct (e.g., "owner/repo@skill-name").`;
4413
+ }
4414
+ const skillSlug = `${targetPrefix}/${downloaded.name}/SKILL`;
4415
+ try {
4416
+ await upsertPage(
4417
+ auth.instanceUrl,
4418
+ auth.token,
4419
+ projectScope.scope,
4420
+ projectScope.id,
4421
+ skillSlug,
4422
+ downloaded.content
4423
+ );
4424
+ let fileCount = 1;
4425
+ for (const file of downloaded.files) {
4426
+ const ext = file.path.replace(/\.[^.]+$/, "");
4427
+ const refSlug = `${targetPrefix}/${downloaded.name}/${ext}`;
4428
+ await upsertPage(
4429
+ auth.instanceUrl,
4430
+ auth.token,
4431
+ projectScope.scope,
4432
+ projectScope.id,
4433
+ refSlug,
4434
+ file.content
4435
+ );
4436
+ fileCount++;
4437
+ }
4438
+ await upsertIndexEntry(
4439
+ auth.instanceUrl,
4440
+ auth.token,
4441
+ projectScope.scope,
4442
+ projectScope.id,
4443
+ targetIndex,
4444
+ {
4445
+ name: downloaded.name,
4446
+ description: downloaded.description,
4447
+ source: `skills.sh:${args.name}`,
4448
+ draft: !!args.draft
4449
+ }
4450
+ );
4451
+ return `Installed skill "${downloaded.name}" from skills.sh. ${fileCount} page(s) written.`;
4452
+ } catch (err) {
4453
+ return `Error installing skill from skills.sh: ${err.message}`;
4454
+ }
4455
+ }
4456
+ if (!args.group_id) {
4457
+ return 'Error: group_id is required when source is "group".';
4458
+ }
4296
4459
  try {
4297
4460
  const groupPages = await listWikiPages(
4298
4461
  auth.instanceUrl,
@@ -4308,8 +4471,6 @@ Install: gitlab_skill_install(name="${e.name}", group_id="${args.group_id}")`
4308
4471
  if (skillPages.length === 0) {
4309
4472
  return `Skill "${args.name}" not found in group "${args.group_id}" wiki.`;
4310
4473
  }
4311
- const targetPrefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
4312
- const targetIndex = args.draft ? DRAFTS_INDEX : SKILLS_INDEX;
4313
4474
  for (const page of skillPages) {
4314
4475
  const newSlug = page.slug.replace(SKILLS_PREFIX, targetPrefix);
4315
4476
  await upsertPage(
@@ -4343,7 +4504,7 @@ Install: gitlab_skill_install(name="${e.name}", group_id="${args.group_id}")`
4343
4504
  draft: !!args.draft
4344
4505
  }
4345
4506
  );
4346
- return `Installed skill "${args.name}" from group "${args.group_id}" into project. ${skillPages.length} page(s) copied.`;
4507
+ return `Installed skill "${args.name}" from group "${args.group_id}". ${skillPages.length} page(s) copied.`;
4347
4508
  } catch (err) {
4348
4509
  return `Error installing skill: ${err.message}`;
4349
4510
  }