opencode-gitlab-dap 1.7.1 → 1.8.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
@@ -2085,9 +2085,12 @@ prompts:
2085
2085
  user: "Fix this vulnerability: {{vuln_data}}"
2086
2086
  placeholder: history
2087
2087
  \`\`\``;
2088
- var WIKI_MEMORY_HINT = `## Wiki Memory
2089
- Persistent project memory and skills are available via GitLab wiki tools (gitlab_wiki_list, gitlab_wiki_read, gitlab_wiki_write, gitlab_wiki_append, gitlab_wiki_search, gitlab_wiki_delete). Use gitlab_wiki_list to discover available memory pages and project or group specific skills.
2090
- IMPORTANT: Always use the project path (e.g., "gitlab-org/gitlab") as the project_id for wiki tools, never a numeric ID. Use the SAME project_id value consistently across all wiki tool calls in a session.`;
2088
+ var PROJECT_KNOWLEDGE_HINT = `## Project Knowledge
2089
+ This project may have persistent memory and skills available via knowledge tools.
2090
+ Use gitlab_memory_load to check for existing project context (facts, decisions, patterns).
2091
+ Use gitlab_skill_list to discover available task-specific skills.
2092
+ When you learn something new about the project, use gitlab_memory_record to preserve it.
2093
+ When you complete a significant task, consider using gitlab_memory_log_session to log learnings.`;
2091
2094
 
2092
2095
  // src/hooks.ts
2093
2096
  function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
@@ -2247,7 +2250,7 @@ function makeSystemTransformHook(flowAgents, getAuthCache) {
2247
2250
  }
2248
2251
  if (getAuthCache()) {
2249
2252
  output.system.push(AGENT_CREATION_GUIDELINES);
2250
- output.system.push(WIKI_MEMORY_HINT);
2253
+ output.system.push(PROJECT_KNOWLEDGE_HINT);
2251
2254
  }
2252
2255
  };
2253
2256
  }
@@ -3356,7 +3359,7 @@ function makeMcpTools(getCachedAgents) {
3356
3359
  };
3357
3360
  }
3358
3361
 
3359
- // src/tools/wiki-tools.ts
3362
+ // src/tools/memory-tools.ts
3360
3363
  import { tool as tool5 } from "@opencode-ai/plugin";
3361
3364
 
3362
3365
  // src/wiki.ts
@@ -3437,192 +3440,358 @@ async function searchWikiPages(instanceUrl, token, scope, id, query) {
3437
3440
  return handleResponse(res);
3438
3441
  }
3439
3442
 
3440
- // src/tools/wiki-tools.ts
3443
+ // src/tools/memory-tools.ts
3441
3444
  var z5 = tool5.schema;
3442
- function makeWikiTools(ctx) {
3443
- function resolveScope(args) {
3444
- if (args.scope === "groups" && args.group_id) {
3445
- return { scope: "groups", id: args.group_id };
3446
- }
3447
- return { scope: "projects", id: args.project_id };
3445
+ var MEMORY_SLUGS = {
3446
+ all: ["memory/facts", "memory/decisions", "memory/patterns"],
3447
+ facts: ["memory/facts"],
3448
+ decisions: ["memory/decisions"],
3449
+ patterns: ["memory/patterns"]
3450
+ };
3451
+ var RECORD_SLUG = {
3452
+ fact: "memory/facts",
3453
+ decision: "memory/decisions",
3454
+ pattern: "memory/patterns"
3455
+ };
3456
+ function today() {
3457
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3458
+ }
3459
+ function slugify(text) {
3460
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60);
3461
+ }
3462
+ async function safeRead(instanceUrl, token, scope, id, slug) {
3463
+ try {
3464
+ const page = await getWikiPage(instanceUrl, token, scope, id, slug);
3465
+ return page.content;
3466
+ } catch {
3467
+ return null;
3448
3468
  }
3469
+ }
3470
+ async function appendToPage(instanceUrl, token, scope, id, slug, newContent, pageTitle) {
3471
+ const existing = await safeRead(instanceUrl, token, scope, id, slug);
3472
+ if (existing !== null) {
3473
+ await updateWikiPage(instanceUrl, token, scope, id, slug, existing + "\n\n" + newContent);
3474
+ } else {
3475
+ const title = pageTitle ?? slug.split("/").pop() ?? slug;
3476
+ await createWikiPage(instanceUrl, token, scope, id, title, newContent);
3477
+ }
3478
+ }
3479
+ function resolveScope(args) {
3480
+ if (args.scope === "groups" && args.group_id) {
3481
+ return { scope: "groups", id: args.group_id };
3482
+ }
3483
+ return { scope: "projects", id: args.project_id };
3484
+ }
3485
+ function makeMemoryTools(ctx) {
3449
3486
  return {
3450
- gitlab_wiki_read: tool5({
3451
- description: "Read a wiki page by slug. Returns the page content.\nUse this to read memory pages, decisions, or any wiki page.",
3487
+ gitlab_memory_load: tool5({
3488
+ description: "Load project memory to understand context, known facts, past decisions, and observed patterns.\nUse this at the start of complex tasks to check what is already known about the project.\nReturns accumulated knowledge from previous sessions.",
3452
3489
  args: {
3453
- project_id: z5.string().describe(
3454
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3455
- ),
3456
- slug: z5.string().describe("Wiki page slug (e.g., memory/facts)"),
3457
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3458
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3490
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3491
+ type: z5.enum(["all", "facts", "decisions", "patterns"]).optional().describe('Which memory to load: "all" (default), "facts", "decisions", or "patterns"'),
3492
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3493
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3459
3494
  },
3460
3495
  execute: async (args) => {
3461
3496
  const auth = ctx.ensureAuth();
3462
3497
  if (!auth) throw new Error("GitLab authentication not available");
3463
3498
  const { scope, id } = resolveScope(args);
3499
+ const memType = args.type ?? "all";
3500
+ const slugs = MEMORY_SLUGS[memType];
3501
+ if (!slugs) return `Unknown memory type: ${memType}`;
3502
+ const sections = [];
3503
+ for (const slug of slugs) {
3504
+ const content = await safeRead(auth.instanceUrl, auth.token, scope, id, slug);
3505
+ if (content) {
3506
+ const label = slug.split("/").pop();
3507
+ sections.push(`## ${label.charAt(0).toUpperCase() + label.slice(1)}
3508
+
3509
+ ${content}`);
3510
+ }
3511
+ }
3512
+ if (sections.length === 0)
3513
+ return "No project memory found. Use gitlab_memory_record to start building project knowledge.";
3514
+ return sections.join("\n\n---\n\n");
3515
+ }
3516
+ }),
3517
+ gitlab_memory_record: tool5({
3518
+ description: "Record a fact, decision, or pattern in project memory.\nFacts: stable truths about the project (e.g., deploy targets, tech stack, team conventions).\nDecisions: architectural choices with reasoning (why X was chosen over Y).\nPatterns: recurring observations that may evolve into skills over time.\nEntries are automatically timestamped.",
3519
+ args: {
3520
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3521
+ type: z5.enum(["fact", "decision", "pattern"]).describe("Type of knowledge to record"),
3522
+ content: z5.string().describe("The knowledge to record (markdown)"),
3523
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3524
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3525
+ },
3526
+ execute: async (args) => {
3527
+ const auth = ctx.ensureAuth();
3528
+ if (!auth) throw new Error("GitLab authentication not available");
3529
+ const { scope, id } = resolveScope(args);
3530
+ const slug = RECORD_SLUG[args.type];
3531
+ if (!slug) return `Unknown memory type: ${args.type}`;
3532
+ const date = today();
3533
+ let formatted;
3534
+ if (args.type === "decision") {
3535
+ const firstLine = args.content.split("\n")[0];
3536
+ const rest = args.content.split("\n").slice(1).join("\n").trim();
3537
+ formatted = `## ${date}: ${firstLine}${rest ? "\n" + rest : ""}`;
3538
+ } else if (args.type === "fact") {
3539
+ formatted = `## ${date}
3540
+ - ${args.content.replace(/\n/g, "\n- ")}`;
3541
+ } else {
3542
+ formatted = `## ${date}
3543
+ ${args.content}`;
3544
+ }
3464
3545
  try {
3465
- const page = await getWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3466
- return page.content;
3546
+ await appendToPage(auth.instanceUrl, auth.token, scope, id, slug, formatted);
3547
+ return `Recorded ${args.type} in project memory.`;
3467
3548
  } catch (err) {
3468
- return `Error reading wiki page: ${err.message}`;
3549
+ return `Error recording ${args.type}: ${err.message}`;
3469
3550
  }
3470
3551
  }
3471
3552
  }),
3472
- gitlab_wiki_write: tool5({
3473
- description: "Create or update a wiki page (upsert).\nTries to update first; if the page does not exist, creates it.\nUse this to write memory pages, decisions, or any wiki content.",
3553
+ gitlab_memory_recall: tool5({
3554
+ description: "Search project knowledge for relevant information.\nSearches across all memory pages, session logs, and skills.\nUse this to check if something is already known before investigating.",
3474
3555
  args: {
3475
- project_id: z5.string().describe(
3476
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3477
- ),
3478
- slug: z5.string().describe("Wiki page slug (e.g., memory/facts)"),
3479
- content: z5.string().describe("Full page content in Markdown"),
3480
- title: z5.string().optional().describe("Page title (defaults to slug if omitted)"),
3481
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3482
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3556
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3557
+ query: z5.string().describe("What to search for"),
3558
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3559
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3483
3560
  },
3484
3561
  execute: async (args) => {
3485
3562
  const auth = ctx.ensureAuth();
3486
3563
  if (!auth) throw new Error("GitLab authentication not available");
3487
3564
  const { scope, id } = resolveScope(args);
3488
3565
  try {
3489
- const page = await updateWikiPage(
3566
+ const results = await searchWikiPages(
3490
3567
  auth.instanceUrl,
3491
3568
  auth.token,
3492
3569
  scope,
3493
3570
  id,
3494
- args.slug,
3495
- args.content,
3496
- args.title
3571
+ args.query
3497
3572
  );
3498
- return `Updated wiki page: ${page.slug}`;
3499
- } catch {
3500
- try {
3501
- const title = args.title || args.slug.split("/").pop() || args.slug;
3502
- const page = await createWikiPage(
3503
- auth.instanceUrl,
3504
- auth.token,
3505
- scope,
3506
- id,
3507
- title,
3508
- args.content
3509
- );
3510
- return `Created wiki page: ${page.slug}`;
3511
- } catch (err) {
3512
- return `Error writing wiki page: ${err.message}`;
3513
- }
3573
+ if (results.length === 0) return `No knowledge found matching "${args.query}".`;
3574
+ return results.map((r) => {
3575
+ const category = r.path.startsWith("memory/") ? "memory" : r.path.startsWith("skills") ? "skill" : "other";
3576
+ const snippet = (r.data ?? "").slice(0, 200).replace(/\n/g, " ");
3577
+ return `[${category}] ${r.path}: ${snippet}`;
3578
+ }).join("\n\n");
3579
+ } catch (err) {
3580
+ return `Error searching knowledge: ${err.message}`;
3514
3581
  }
3515
3582
  }
3516
3583
  }),
3517
- gitlab_wiki_append: tool5({
3518
- description: "Append text to an existing wiki page.\nReads the current content, appends the new text, and writes it back.\nIf the page does not exist, creates it with the provided text.",
3584
+ gitlab_memory_log_session: tool5({
3585
+ description: "Log a session summary including what was accomplished, what was learned, and any suggestions.\nUse this at the end of significant work sessions to preserve context for future sessions.",
3519
3586
  args: {
3520
- project_id: z5.string().describe(
3521
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3587
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3588
+ title: z5.string().describe('Brief session title (e.g., "fix-ai-gateway-healthcheck")'),
3589
+ summary: z5.string().describe(
3590
+ "Session summary in markdown (what happened, what was learned, what went wrong)"
3522
3591
  ),
3523
- slug: z5.string().describe("Wiki page slug (e.g., memory/facts)"),
3524
- text: z5.string().describe("Text to append to the page"),
3525
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3526
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3592
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3593
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3527
3594
  },
3528
3595
  execute: async (args) => {
3529
3596
  const auth = ctx.ensureAuth();
3530
3597
  if (!auth) throw new Error("GitLab authentication not available");
3531
3598
  const { scope, id } = resolveScope(args);
3599
+ const date = today();
3600
+ const slug = `memory/sessions/${date}-${slugify(args.title)}`;
3601
+ const pageTitle = `${date}: ${args.title}`;
3532
3602
  try {
3533
- const page = await getWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3534
- const newContent = page.content + "\n" + args.text;
3535
- await updateWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug, newContent);
3536
- return `Appended to wiki page: ${args.slug}`;
3537
- } catch {
3538
- try {
3539
- const title = args.slug.split("/").pop() || args.slug;
3540
- await createWikiPage(auth.instanceUrl, auth.token, scope, id, title, args.text);
3541
- return `Created wiki page with initial content: ${args.slug}`;
3542
- } catch (err) {
3543
- return `Error appending to wiki page: ${err.message}`;
3603
+ await createWikiPage(auth.instanceUrl, auth.token, scope, id, pageTitle, args.summary);
3604
+ return `Session logged: ${slug}`;
3605
+ } catch (err) {
3606
+ if (err.message?.includes("already exists") || err.message?.includes("422")) {
3607
+ try {
3608
+ await updateWikiPage(
3609
+ auth.instanceUrl,
3610
+ auth.token,
3611
+ scope,
3612
+ id,
3613
+ slug,
3614
+ args.summary,
3615
+ pageTitle
3616
+ );
3617
+ return `Session log updated: ${slug}`;
3618
+ } catch (err2) {
3619
+ return `Error logging session: ${err2.message}`;
3620
+ }
3544
3621
  }
3622
+ return `Error logging session: ${err.message}`;
3545
3623
  }
3546
3624
  }
3547
- }),
3548
- gitlab_wiki_list: tool5({
3549
- description: "List wiki pages, optionally filtered by slug prefix.\nRecommended wiki structure for agent memory:\n memory/facts \u2014 persistent facts about the project\n memory/decisions \u2014 architectural and design decisions\n memory/plans \u2014 current plans and roadmaps\n memory/sessions \u2014 session logs and context\n memory/snippets \u2014 reusable code snippets and patterns\nTwo-tier resolution: tries project wiki first, then group wiki if project has no results.",
3625
+ })
3626
+ };
3627
+ }
3628
+
3629
+ // src/tools/skill-tools.ts
3630
+ import { tool as tool6 } from "@opencode-ai/plugin";
3631
+ var z6 = tool6.schema;
3632
+ function resolveScope2(args) {
3633
+ if (args.scope === "groups" && args.group_id) {
3634
+ return { scope: "groups", id: args.group_id };
3635
+ }
3636
+ return { scope: "projects", id: args.project_id };
3637
+ }
3638
+ function makeSkillTools(ctx) {
3639
+ return {
3640
+ gitlab_skill_list: tool6({
3641
+ description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).",
3550
3642
  args: {
3551
- project_id: z5.string().describe(
3552
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3553
- ),
3554
- prefix: z5.string().optional().describe('Filter pages by slug prefix (e.g., "memory/")'),
3555
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3556
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3643
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3644
+ include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
3645
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3646
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3557
3647
  },
3558
3648
  execute: async (args) => {
3559
3649
  const auth = ctx.ensureAuth();
3560
3650
  if (!auth) throw new Error("GitLab authentication not available");
3561
- const { scope, id } = resolveScope(args);
3651
+ const { scope, id } = resolveScope2(args);
3562
3652
  try {
3563
3653
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
3564
- const filtered = args.prefix ? pages.filter((p) => p.slug.startsWith(args.prefix)) : pages;
3565
- return JSON.stringify(
3566
- filtered.map((p) => ({ slug: p.slug, title: p.title })),
3567
- null,
3568
- 2
3569
- );
3654
+ const skills = pages.filter((p) => p.slug.startsWith("skills/") && p.slug !== "skills/index").map((p) => ({ name: p.slug.replace("skills/", ""), title: p.title, draft: false }));
3655
+ let drafts = [];
3656
+ if (args.include_drafts) {
3657
+ drafts = pages.filter(
3658
+ (p) => p.slug.startsWith("skills-drafts/") && p.slug !== "skills-drafts/index"
3659
+ ).map((p) => ({
3660
+ name: p.slug.replace("skills-drafts/", ""),
3661
+ title: p.title,
3662
+ draft: true
3663
+ }));
3664
+ }
3665
+ const all = [...skills, ...drafts];
3666
+ if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
3667
+ return JSON.stringify(all, null, 2);
3570
3668
  } catch (err) {
3571
- return `Error listing wiki pages: ${err.message}`;
3669
+ return `Error listing skills: ${err.message}`;
3572
3670
  }
3573
3671
  }
3574
3672
  }),
3575
- gitlab_wiki_delete: tool5({
3576
- description: "Delete a wiki page by slug.",
3673
+ gitlab_skill_load: tool6({
3674
+ 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.",
3577
3675
  args: {
3578
- project_id: z5.string().describe(
3579
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3580
- ),
3581
- slug: z5.string().describe("Wiki page slug to delete"),
3582
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3583
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3676
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3677
+ name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
3678
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3679
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3584
3680
  },
3585
3681
  execute: async (args) => {
3586
3682
  const auth = ctx.ensureAuth();
3587
3683
  if (!auth) throw new Error("GitLab authentication not available");
3588
- const { scope, id } = resolveScope(args);
3684
+ const { scope, id } = resolveScope2(args);
3589
3685
  try {
3590
- await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3591
- return `Deleted wiki page: ${args.slug}`;
3592
- } catch (err) {
3593
- return `Error deleting wiki page: ${err.message}`;
3686
+ const page = await getWikiPage(
3687
+ auth.instanceUrl,
3688
+ auth.token,
3689
+ scope,
3690
+ id,
3691
+ `skills/${args.name}`
3692
+ );
3693
+ return page.content;
3694
+ } catch {
3695
+ try {
3696
+ const draft = await getWikiPage(
3697
+ auth.instanceUrl,
3698
+ auth.token,
3699
+ scope,
3700
+ id,
3701
+ `skills-drafts/${args.name}`
3702
+ );
3703
+ return `[DRAFT SKILL]
3704
+
3705
+ ${draft.content}`;
3706
+ } catch {
3707
+ return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
3708
+ }
3594
3709
  }
3595
3710
  }
3596
3711
  }),
3597
- gitlab_wiki_search: tool5({
3598
- description: "Search wiki pages via GitLab search API.\nReturns matching page paths and content snippets.",
3712
+ gitlab_skill_save: tool6({
3713
+ 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.",
3599
3714
  args: {
3600
- project_id: z5.string().describe(
3601
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3602
- ),
3603
- query: z5.string().describe("Search query string"),
3604
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3605
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3715
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3716
+ name: z6.string().describe('Skill name (e.g., "incident-retro")'),
3717
+ content: z6.string().describe("Skill content in markdown"),
3718
+ draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
3719
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3720
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3606
3721
  },
3607
3722
  execute: async (args) => {
3608
3723
  const auth = ctx.ensureAuth();
3609
3724
  if (!auth) throw new Error("GitLab authentication not available");
3610
- const { scope, id } = resolveScope(args);
3725
+ const { scope, id } = resolveScope2(args);
3726
+ const prefix = args.draft ? "skills-drafts" : "skills";
3727
+ const slug = `${prefix}/${args.name}`;
3611
3728
  try {
3612
- const results = await searchWikiPages(
3729
+ await updateWikiPage(
3613
3730
  auth.instanceUrl,
3614
3731
  auth.token,
3615
3732
  scope,
3616
3733
  id,
3617
- args.query
3734
+ slug,
3735
+ args.content,
3736
+ args.name
3737
+ );
3738
+ return `Updated ${args.draft ? "draft " : ""}skill: ${args.name}`;
3739
+ } catch {
3740
+ try {
3741
+ await createWikiPage(auth.instanceUrl, auth.token, scope, id, args.name, args.content);
3742
+ return `Created ${args.draft ? "draft " : ""}skill: ${args.name}`;
3743
+ } catch (err) {
3744
+ return `Error saving skill: ${err.message}`;
3745
+ }
3746
+ }
3747
+ }
3748
+ }),
3749
+ gitlab_skill_promote: tool6({
3750
+ description: "Promote a draft skill to published.\nMoves the skill from the drafts directory to the published skills directory.",
3751
+ args: {
3752
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3753
+ name: z6.string().describe("Skill name to promote"),
3754
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3755
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3756
+ },
3757
+ execute: async (args) => {
3758
+ const auth = ctx.ensureAuth();
3759
+ if (!auth) throw new Error("GitLab authentication not available");
3760
+ const { scope, id } = resolveScope2(args);
3761
+ try {
3762
+ const draft = await getWikiPage(
3763
+ auth.instanceUrl,
3764
+ auth.token,
3765
+ scope,
3766
+ id,
3767
+ `skills-drafts/${args.name}`
3618
3768
  );
3619
- return JSON.stringify(
3620
- results.map((r) => ({ path: r.path, snippet: r.data })),
3621
- null,
3622
- 2
3769
+ try {
3770
+ await updateWikiPage(
3771
+ auth.instanceUrl,
3772
+ auth.token,
3773
+ scope,
3774
+ id,
3775
+ `skills/${args.name}`,
3776
+ draft.content,
3777
+ args.name
3778
+ );
3779
+ } catch {
3780
+ await createWikiPage(auth.instanceUrl, auth.token, scope, id, args.name, draft.content);
3781
+ }
3782
+ await deleteWikiPage(
3783
+ auth.instanceUrl,
3784
+ auth.token,
3785
+ scope,
3786
+ id,
3787
+ `skills-drafts/${args.name}`
3623
3788
  );
3789
+ return `Promoted skill "${args.name}" from draft to published.`;
3624
3790
  } catch (err) {
3625
- return `Error searching wiki: ${err.message}`;
3791
+ if (err.message?.includes("not found") || err.message?.includes("404")) {
3792
+ return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
3793
+ }
3794
+ return `Error promoting skill: ${err.message}`;
3626
3795
  }
3627
3796
  }
3628
3797
  })
@@ -3783,7 +3952,8 @@ var plugin = async (input) => {
3783
3952
  ...makeMcpTools(() => cachedAgents),
3784
3953
  ...makeCatalogCrudTools(ctx),
3785
3954
  ...makeCatalogItemTools(ctx),
3786
- ...makeWikiTools(ctx)
3955
+ ...makeMemoryTools(ctx),
3956
+ ...makeSkillTools(ctx)
3787
3957
  }
3788
3958
  };
3789
3959
  };