opencode-gitlab-dap 1.14.2 → 1.15.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.js CHANGED
@@ -3778,9 +3778,86 @@ ${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
+
3782
+ // src/snippets.ts
3783
+ function snippetApi(instanceUrl, token, projectId) {
3784
+ const base = instanceUrl.replace(/\/$/, "");
3785
+ const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
3786
+ return {
3787
+ url: `${base}/api/v4/projects/${encoded}/snippets`,
3788
+ headers: {
3789
+ "Content-Type": "application/json",
3790
+ Authorization: `Bearer ${token}`
3791
+ }
3792
+ };
3793
+ }
3794
+ async function handleResponse2(res) {
3795
+ if (!res.ok) {
3796
+ const text = await res.text();
3797
+ throw new Error(`Snippet API error (${res.status}): ${text}`);
3798
+ }
3799
+ return res.json();
3800
+ }
3801
+ async function createProjectSnippet(instanceUrl, token, projectId, title, description, files, visibility = "private") {
3802
+ const { url, headers } = snippetApi(instanceUrl, token, projectId);
3803
+ const res = await fetch(url, {
3804
+ method: "POST",
3805
+ headers,
3806
+ body: JSON.stringify({ title, description, visibility, files })
3807
+ });
3808
+ return handleResponse2(res);
3809
+ }
3810
+ async function deleteProjectSnippet(instanceUrl, token, projectId, snippetId) {
3811
+ const { url, headers } = snippetApi(instanceUrl, token, projectId);
3812
+ const res = await fetch(`${url}/${snippetId}`, { method: "DELETE", headers });
3813
+ if (!res.ok) {
3814
+ const text = await res.text();
3815
+ throw new Error(`Snippet API error (${res.status}): ${text}`);
3816
+ }
3817
+ }
3818
+ async function getSnippetFileRaw(instanceUrl, token, projectId, snippetId, filePath, ref = "main") {
3819
+ const base = instanceUrl.replace(/\/$/, "");
3820
+ const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
3821
+ const encodedPath = encodeURIComponent(filePath);
3822
+ const url = `${base}/api/v4/projects/${encoded}/snippets/${snippetId}/files/${ref}/${encodedPath}/raw`;
3823
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
3824
+ if (!res.ok) {
3825
+ const text = await res.text();
3826
+ throw new Error(`Snippet file API error (${res.status}): ${text}`);
3827
+ }
3828
+ return res.text();
3829
+ }
3830
+ async function listSnippetFiles(instanceUrl, token, projectId, snippetId) {
3831
+ const base = instanceUrl.replace(/\/$/, "");
3832
+ const encoded = typeof projectId === "number" ? projectId : encodeURIComponent(projectId);
3833
+ const url = `${base}/api/v4/projects/${encoded}/snippets/${snippetId}`;
3834
+ const res = await fetch(url, {
3835
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }
3836
+ });
3837
+ const snippet = await handleResponse2(res);
3838
+ if (snippet.files) {
3839
+ return snippet.files.map((f) => ({ path: f.path, raw_url: f.raw_url }));
3840
+ }
3841
+ if (snippet.file_name) {
3842
+ return [{ path: snippet.file_name, raw_url: snippet.raw_url }];
3843
+ }
3844
+ return [];
3845
+ }
3846
+
3847
+ // src/tools/skill-tools.ts
3781
3848
  import { execSync } from "child_process";
3782
- import { mkdtempSync, readFileSync as readFileSync2, rmSync, readdirSync, statSync } from "fs";
3783
- import { join as join2 } from "path";
3849
+ import {
3850
+ mkdtempSync,
3851
+ readFileSync as readFileSync2,
3852
+ rmSync,
3853
+ readdirSync,
3854
+ statSync,
3855
+ writeFileSync,
3856
+ mkdirSync,
3857
+ existsSync,
3858
+ chmodSync
3859
+ } from "fs";
3860
+ import { join as join2, dirname } from "path";
3784
3861
  import { tmpdir } from "os";
3785
3862
  var z6 = tool6.schema;
3786
3863
  var PREFIX2 = "agents";
@@ -3811,14 +3888,17 @@ function parseIndex(content) {
3811
3888
  const rest = lines.slice(1).join("\n").trim();
3812
3889
  const descLines = [];
3813
3890
  let source;
3891
+ let snippetId;
3814
3892
  for (const line of rest.split("\n")) {
3815
3893
  if (line.startsWith("Source:")) {
3816
3894
  source = line.slice(7).trim();
3895
+ } else if (line.startsWith("Snippet:")) {
3896
+ snippetId = parseInt(line.slice(8).trim(), 10) || void 0;
3817
3897
  } else if (line.trim()) {
3818
3898
  descLines.push(line);
3819
3899
  }
3820
3900
  }
3821
- entries.push({ name, description: descLines.join("\n"), source, draft: false });
3901
+ entries.push({ name, description: descLines.join("\n"), source, snippetId, draft: false });
3822
3902
  }
3823
3903
  return entries;
3824
3904
  }
@@ -3828,6 +3908,8 @@ function formatIndex(entries) {
3828
3908
  ${e.description}`;
3829
3909
  if (e.source) block += `
3830
3910
  Source: ${e.source}`;
3911
+ if (e.snippetId) block += `
3912
+ Snippet: ${e.snippetId}`;
3831
3913
  return block;
3832
3914
  }).join("\n\n");
3833
3915
  }
@@ -3948,6 +4030,33 @@ async function upsertPage(instanceUrl, token, scope, id, slug, content) {
3948
4030
  await createWikiPage(instanceUrl, token, scope, id, slug, content);
3949
4031
  }
3950
4032
  }
4033
+ function isMarkdownFile(path) {
4034
+ return path === "SKILL.md" || path.endsWith(".md") || path.endsWith(".markdown");
4035
+ }
4036
+ function isAgentsGitignored(dir) {
4037
+ try {
4038
+ execSync("git check-ignore -q .agents", { cwd: dir, stdio: "pipe" });
4039
+ return true;
4040
+ } catch {
4041
+ return false;
4042
+ }
4043
+ }
4044
+ function ensureGitignore(dir) {
4045
+ if (isAgentsGitignored(dir)) return;
4046
+ const gitignorePath = join2(dir, ".gitignore");
4047
+ if (existsSync(gitignorePath)) {
4048
+ const content = readFileSync2(gitignorePath, "utf-8");
4049
+ writeFileSync(
4050
+ gitignorePath,
4051
+ content.trimEnd() + "\n\n# Agent skills cache (managed by opencode-gitlab-dap)\n.agents/\n"
4052
+ );
4053
+ } else {
4054
+ writeFileSync(
4055
+ gitignorePath,
4056
+ "# Agent skills cache (managed by opencode-gitlab-dap)\n.agents/\n"
4057
+ );
4058
+ }
4059
+ }
3951
4060
  function searchSkillsSh(query) {
3952
4061
  try {
3953
4062
  const raw = execSync(`npx skills find ${JSON.stringify(query)}`, {
@@ -4107,8 +4216,23 @@ ${page.content}` : page.content;
4107
4216
 
4108
4217
  ---
4109
4218
  Available references: ${refs.join(", ")}`;
4219
+ }
4220
+ const indexSlug = isDraft ? DRAFTS_INDEX : SKILLS_INDEX;
4221
+ const indexEntries = await readIndex(
4222
+ auth.instanceUrl,
4223
+ auth.token,
4224
+ scope,
4225
+ id,
4226
+ indexSlug
4227
+ );
4228
+ const entry = indexEntries.find((e) => e.name === args.name);
4229
+ if (entry?.snippetId) {
4230
+ result += `
4231
+
4232
+ ---
4233
+ This skill has executable scripts in snippet #${entry.snippetId}.`;
4110
4234
  result += `
4111
- Load with: gitlab_skill_load_reference(name="${args.name}", reference="<name>")`;
4235
+ Run \`gitlab_skill_setup(name="${args.name}")\` to extract them to .agents/skills/${args.name}/ for execution.`;
4112
4236
  }
4113
4237
  return result;
4114
4238
  } catch {
@@ -4266,29 +4390,42 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4266
4390
  if (!downloaded) {
4267
4391
  return `Failed to download skill "${args.name}" from skills.sh. Check that the identifier is correct (e.g., "owner/repo@skill-name").`;
4268
4392
  }
4269
- const skillSlug = `${targetPrefix}/${downloaded.name}/SKILL`;
4270
4393
  try {
4271
4394
  await upsertPage(
4272
4395
  auth.instanceUrl,
4273
4396
  auth.token,
4274
4397
  projectScope.scope,
4275
4398
  projectScope.id,
4276
- skillSlug,
4399
+ `${targetPrefix}/${downloaded.name}/SKILL`,
4277
4400
  downloaded.content
4278
4401
  );
4279
- let fileCount = 1;
4280
- for (const file of downloaded.files) {
4281
- const ext = file.path.replace(/\.[^.]+$/, "");
4282
- const refSlug = `${targetPrefix}/${downloaded.name}/${ext}`;
4402
+ let wikiCount = 1;
4403
+ const mdFiles = downloaded.files.filter((f) => isMarkdownFile(f.path));
4404
+ const scriptFiles = downloaded.files.filter((f) => !isMarkdownFile(f.path));
4405
+ for (const file of mdFiles) {
4406
+ const slug = `${targetPrefix}/${downloaded.name}/references/${file.path.replace(/\.[^.]+$/, "")}`;
4283
4407
  await upsertPage(
4284
4408
  auth.instanceUrl,
4285
4409
  auth.token,
4286
4410
  projectScope.scope,
4287
4411
  projectScope.id,
4288
- refSlug,
4412
+ slug,
4289
4413
  file.content
4290
4414
  );
4291
- fileCount++;
4415
+ wikiCount++;
4416
+ }
4417
+ let snippetId;
4418
+ if (scriptFiles.length > 0) {
4419
+ const snippet = await createProjectSnippet(
4420
+ auth.instanceUrl,
4421
+ auth.token,
4422
+ args.project_id,
4423
+ `skill:${downloaded.name}`,
4424
+ `Scripts for skill "${downloaded.name}" (installed from skills.sh:${args.name})`,
4425
+ scriptFiles.map((f) => ({ file_path: f.path, content: f.content })),
4426
+ "private"
4427
+ );
4428
+ snippetId = snippet.id;
4292
4429
  }
4293
4430
  await upsertIndexEntry(
4294
4431
  auth.instanceUrl,
@@ -4300,10 +4437,13 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4300
4437
  name: downloaded.name,
4301
4438
  description: downloaded.description,
4302
4439
  source: `skills.sh:${args.name}`,
4440
+ snippetId,
4303
4441
  draft: !!args.draft
4304
4442
  }
4305
4443
  );
4306
- return `Installed skill "${downloaded.name}" from skills.sh. ${fileCount} page(s) written.`;
4444
+ const parts = [`${wikiCount} wiki page(s)`];
4445
+ if (snippetId) parts.push(`snippet #${snippetId} with ${scriptFiles.length} script(s)`);
4446
+ return `Installed skill "${downloaded.name}" from skills.sh. ${parts.join(", ")}. Use gitlab_skill_setup to extract scripts to disk.`;
4307
4447
  } catch (err) {
4308
4448
  return `Error installing skill from skills.sh: ${err.message}`;
4309
4449
  }
@@ -4365,8 +4505,106 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4365
4505
  }
4366
4506
  }
4367
4507
  }),
4508
+ gitlab_skill_setup: tool6({
4509
+ description: "Extract a skill to the local .agents/skills/ directory for execution.\nDownloads SKILL.md from wiki and script files from the associated snippet.\nWrites to .agents/skills/<name>/ and ensures .agents/ is in .gitignore.\nAfter setup, OpenCode will auto-discover the skill from the local directory.",
4510
+ args: {
4511
+ project_id: z6.string().describe(PROJECT_ID_DESC2),
4512
+ name: z6.string().describe("Skill name to set up locally"),
4513
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
4514
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
4515
+ },
4516
+ execute: async (args) => {
4517
+ const auth = authAndValidate(args.project_id);
4518
+ const { scope, id } = resolveScope2(args);
4519
+ const targetDir = join2(process.cwd(), ".agents", "skills", args.name);
4520
+ try {
4521
+ let skillContent = null;
4522
+ for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4523
+ try {
4524
+ const page = await getWikiPage(
4525
+ auth.instanceUrl,
4526
+ auth.token,
4527
+ scope,
4528
+ id,
4529
+ `${prefix}/${args.name}/SKILL`
4530
+ );
4531
+ skillContent = page.content;
4532
+ break;
4533
+ } catch {
4534
+ }
4535
+ }
4536
+ if (!skillContent) {
4537
+ return `Skill "${args.name}" not found in wiki. Use gitlab_skill_list to see available skills.`;
4538
+ }
4539
+ mkdirSync(targetDir, { recursive: true });
4540
+ writeFileSync(join2(targetDir, "SKILL.md"), skillContent);
4541
+ const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id, true);
4542
+ let refCount = 0;
4543
+ for (const prefix of [SKILLS_PREFIX, DRAFTS_PREFIX]) {
4544
+ const refPrefix = `${prefix}/${args.name}/references/`;
4545
+ const refPages = pages.filter(
4546
+ (p) => p.slug.startsWith(refPrefix) && p.content
4547
+ );
4548
+ for (const ref of refPages) {
4549
+ const refName = ref.slug.slice(refPrefix.length);
4550
+ const refDir = join2(targetDir, "references");
4551
+ mkdirSync(refDir, { recursive: true });
4552
+ writeFileSync(join2(refDir, `${refName}.md`), ref.content);
4553
+ refCount++;
4554
+ }
4555
+ }
4556
+ let scriptCount = 0;
4557
+ const indexEntries = await readIndex(
4558
+ auth.instanceUrl,
4559
+ auth.token,
4560
+ scope,
4561
+ id,
4562
+ SKILLS_INDEX
4563
+ );
4564
+ const draftsEntries = await readIndex(
4565
+ auth.instanceUrl,
4566
+ auth.token,
4567
+ scope,
4568
+ id,
4569
+ DRAFTS_INDEX
4570
+ );
4571
+ const entry = [...indexEntries, ...draftsEntries].find((e) => e.name === args.name);
4572
+ if (entry?.snippetId) {
4573
+ const files = await listSnippetFiles(
4574
+ auth.instanceUrl,
4575
+ auth.token,
4576
+ args.project_id,
4577
+ entry.snippetId
4578
+ );
4579
+ for (const file of files) {
4580
+ const filePath = join2(targetDir, file.path);
4581
+ mkdirSync(dirname(filePath), { recursive: true });
4582
+ const content = await getSnippetFileRaw(
4583
+ auth.instanceUrl,
4584
+ auth.token,
4585
+ args.project_id,
4586
+ entry.snippetId,
4587
+ file.path
4588
+ );
4589
+ writeFileSync(filePath, content);
4590
+ if (file.path.endsWith(".sh")) {
4591
+ chmodSync(filePath, 493);
4592
+ }
4593
+ scriptCount++;
4594
+ }
4595
+ }
4596
+ ensureGitignore(process.cwd());
4597
+ const parts = ["SKILL.md"];
4598
+ if (refCount > 0) parts.push(`${refCount} reference(s)`);
4599
+ if (scriptCount > 0) parts.push(`${scriptCount} script(s)`);
4600
+ return `Skill "${args.name}" extracted to ${targetDir} (${parts.join(", ")}). OpenCode will auto-discover it on next session.`;
4601
+ } catch (err) {
4602
+ return `Error setting up skill: ${err.message}`;
4603
+ }
4604
+ }
4605
+ }),
4368
4606
  gitlab_skill_delete: tool6({
4369
- 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.",
4607
+ description: "Delete a skill and remove it from the index.\nRemoves all wiki pages under the skill directory and any associated snippet.\nWiki and snippet git history preserves deleted content.",
4370
4608
  args: {
4371
4609
  project_id: z6.string().describe(PROJECT_ID_DESC2),
4372
4610
  name: z6.string().describe("Skill name to delete"),
@@ -4389,8 +4627,25 @@ Install: \`gitlab_skill_install(name="${r.identifier}", source="skills.sh")\``
4389
4627
  for (const page of skillPages) {
4390
4628
  await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, page.slug);
4391
4629
  }
4630
+ const indexEntries = await readIndex(auth.instanceUrl, auth.token, scope, id, indexSlug);
4631
+ const entry = indexEntries.find((e) => e.name === args.name);
4632
+ let snippetDeleted = false;
4633
+ if (entry?.snippetId) {
4634
+ try {
4635
+ await deleteProjectSnippet(
4636
+ auth.instanceUrl,
4637
+ auth.token,
4638
+ args.project_id,
4639
+ entry.snippetId
4640
+ );
4641
+ snippetDeleted = true;
4642
+ } catch {
4643
+ }
4644
+ }
4392
4645
  await removeIndexEntry(auth.instanceUrl, auth.token, scope, id, indexSlug, args.name);
4393
- return `Deleted skill "${args.name}" (${skillPages.length} page(s) removed).`;
4646
+ const parts = [`${skillPages.length} wiki page(s)`];
4647
+ if (snippetDeleted) parts.push("snippet");
4648
+ return `Deleted skill "${args.name}" (${parts.join(" + ")} removed).`;
4394
4649
  } catch (err) {
4395
4650
  return `Error deleting skill: ${err.message}`;
4396
4651
  }