opencode-gitlab-dap 1.7.1 → 1.8.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/README.md CHANGED
@@ -110,7 +110,7 @@ The plugin provides a multi-round design workflow:
110
110
 
111
111
  The vendored `flow_v2.json` schema from GitLab Rails powers client-side validation using `ajv`, catching errors before hitting the API.
112
112
 
113
- ### 26 Tools
113
+ ### 28 Tools
114
114
 
115
115
  #### DAP Tools (20)
116
116
 
@@ -137,33 +137,29 @@ The vendored `flow_v2.json` schema from GitLab Rails powers client-side validati
137
137
  | `gitlab_get_workflow_status` | Monitor workflow execution status and logs |
138
138
  | `gitlab_list_project_mcp_servers` | List MCP servers available for project agents |
139
139
 
140
- #### Wiki Memory Tools (6)
140
+ #### Project Knowledge Tools (8)
141
141
 
142
- Persistent project memory and skill storage via GitLab wikis.
142
+ Persistent project memory and reusable skills. Knowledge is stored in GitLab project/group wikis but tools abstract the storage — the agent works with facts, decisions, patterns, and skills.
143
143
 
144
- | Tool | Description |
145
- | -------------------- | -------------------------------------------------------- |
146
- | `gitlab_wiki_read` | Read a wiki page by slug |
147
- | `gitlab_wiki_write` | Create or update a wiki page (upsert) |
148
- | `gitlab_wiki_append` | Append text to a wiki page (create if missing) |
149
- | `gitlab_wiki_list` | List wiki pages with optional prefix filter |
150
- | `gitlab_wiki_delete` | Delete a wiki page |
151
- | `gitlab_wiki_search` | Search wiki pages by content (server-side GitLab search) |
144
+ ##### Memory Tools
152
145
 
153
- All wiki tools support both project wikis (default) and group wikis (`scope="groups"`).
146
+ | Tool | Description |
147
+ | --------------------------- | -------------------------------------------------------- |
148
+ | `gitlab_memory_load` | Load project memory (facts, decisions, patterns, or all) |
149
+ | `gitlab_memory_record` | Record a fact, decision, or pattern (auto-timestamped) |
150
+ | `gitlab_memory_recall` | Search project knowledge for relevant information |
151
+ | `gitlab_memory_log_session` | Log a session summary with learnings |
154
152
 
155
- ##### Recommended Wiki Structure
153
+ ##### Skill Tools
156
154
 
157
- ```
158
- memory/
159
- ├── facts # Stable project truths (append-only)
160
- ├── decisions # Architecture decisions with context
161
- ├── patterns # Observations across sessions
162
- └── sessions/ # Per-session learning logs
163
- skills/
164
- ├── index # Skill routing table
165
- └── <name> # Individual skill pages
166
- ```
155
+ | Tool | Description |
156
+ | ---------------------- | ---------------------------------------------- |
157
+ | `gitlab_skill_list` | List available skills (and optionally drafts) |
158
+ | `gitlab_skill_load` | Load a skill (published first, then drafts) |
159
+ | `gitlab_skill_save` | Create or update a skill (supports draft flag) |
160
+ | `gitlab_skill_promote` | Promote a draft skill to published |
161
+
162
+ All tools support project scope (default) and group scope (`scope="groups"`).
167
163
 
168
164
  ### Dynamic Refresh
169
165
 
package/dist/index.cjs CHANGED
@@ -2254,9 +2254,12 @@ prompts:
2254
2254
  user: "Fix this vulnerability: {{vuln_data}}"
2255
2255
  placeholder: history
2256
2256
  \`\`\``;
2257
- var WIKI_MEMORY_HINT = `## Wiki Memory
2258
- 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.
2259
- 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.`;
2257
+ var PROJECT_KNOWLEDGE_HINT = `## Project Knowledge
2258
+ This project may have persistent memory and skills available via knowledge tools.
2259
+ Use gitlab_memory_load to check for existing project context (facts, decisions, patterns).
2260
+ Use gitlab_skill_list to discover available task-specific skills.
2261
+ When you learn something new about the project, use gitlab_memory_record to preserve it.
2262
+ When you complete a significant task, consider using gitlab_memory_log_session to log learnings.`;
2260
2263
 
2261
2264
  // src/hooks.ts
2262
2265
  function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
@@ -2416,7 +2419,7 @@ function makeSystemTransformHook(flowAgents, getAuthCache) {
2416
2419
  }
2417
2420
  if (getAuthCache()) {
2418
2421
  output.system.push(AGENT_CREATION_GUIDELINES);
2419
- output.system.push(WIKI_MEMORY_HINT);
2422
+ output.system.push(PROJECT_KNOWLEDGE_HINT);
2420
2423
  }
2421
2424
  };
2422
2425
  }
@@ -3525,7 +3528,7 @@ function makeMcpTools(getCachedAgents) {
3525
3528
  };
3526
3529
  }
3527
3530
 
3528
- // src/tools/wiki-tools.ts
3531
+ // src/tools/memory-tools.ts
3529
3532
  var import_plugin5 = require("@opencode-ai/plugin");
3530
3533
 
3531
3534
  // src/wiki.ts
@@ -3606,192 +3609,344 @@ async function searchWikiPages(instanceUrl, token, scope, id, query) {
3606
3609
  return handleResponse(res);
3607
3610
  }
3608
3611
 
3609
- // src/tools/wiki-tools.ts
3612
+ // src/tools/memory-tools.ts
3610
3613
  var z5 = import_plugin5.tool.schema;
3611
- function makeWikiTools(ctx) {
3612
- function resolveScope(args) {
3613
- if (args.scope === "groups" && args.group_id) {
3614
- return { scope: "groups", id: args.group_id };
3615
- }
3616
- return { scope: "projects", id: args.project_id };
3614
+ var PREFIX = "agents";
3615
+ var MEMORY_SLUGS = {
3616
+ all: [`${PREFIX}/memory/facts`, `${PREFIX}/memory/decisions`, `${PREFIX}/memory/patterns`],
3617
+ facts: [`${PREFIX}/memory/facts`],
3618
+ decisions: [`${PREFIX}/memory/decisions`],
3619
+ patterns: [`${PREFIX}/memory/patterns`]
3620
+ };
3621
+ var RECORD_SLUG = {
3622
+ fact: `${PREFIX}/memory/facts`,
3623
+ decision: `${PREFIX}/memory/decisions`,
3624
+ pattern: `${PREFIX}/memory/patterns`
3625
+ };
3626
+ function today() {
3627
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3628
+ }
3629
+ function slugify(text) {
3630
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "").slice(0, 60);
3631
+ }
3632
+ async function safeRead(instanceUrl, token, scope, id, slug) {
3633
+ try {
3634
+ const page = await getWikiPage(instanceUrl, token, scope, id, slug);
3635
+ return page.content;
3636
+ } catch {
3637
+ return null;
3617
3638
  }
3639
+ }
3640
+ async function appendToPage(instanceUrl, token, scope, id, slug, newContent) {
3641
+ const existing = await safeRead(instanceUrl, token, scope, id, slug);
3642
+ if (existing !== null) {
3643
+ await updateWikiPage(instanceUrl, token, scope, id, slug, existing + "\n\n" + newContent);
3644
+ } else {
3645
+ await createWikiPage(instanceUrl, token, scope, id, slug, newContent);
3646
+ }
3647
+ }
3648
+ function resolveScope(args) {
3649
+ if (args.scope === "groups" && args.group_id) {
3650
+ return { scope: "groups", id: args.group_id };
3651
+ }
3652
+ return { scope: "projects", id: args.project_id };
3653
+ }
3654
+ function makeMemoryTools(ctx) {
3618
3655
  return {
3619
- gitlab_wiki_read: (0, import_plugin5.tool)({
3620
- description: "Read a wiki page by slug. Returns the page content.\nUse this to read memory pages, decisions, or any wiki page.",
3656
+ gitlab_memory_load: (0, import_plugin5.tool)({
3657
+ 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.",
3621
3658
  args: {
3622
- project_id: z5.string().describe(
3623
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3624
- ),
3625
- slug: z5.string().describe("Wiki page slug (e.g., memory/facts)"),
3626
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3627
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3659
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3660
+ type: z5.enum(["all", "facts", "decisions", "patterns"]).optional().describe('Which memory to load: "all" (default), "facts", "decisions", or "patterns"'),
3661
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3662
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3628
3663
  },
3629
3664
  execute: async (args) => {
3630
3665
  const auth = ctx.ensureAuth();
3631
3666
  if (!auth) throw new Error("GitLab authentication not available");
3632
3667
  const { scope, id } = resolveScope(args);
3668
+ const memType = args.type ?? "all";
3669
+ const slugs = MEMORY_SLUGS[memType];
3670
+ if (!slugs) return `Unknown memory type: ${memType}`;
3671
+ const sections = [];
3672
+ for (const slug of slugs) {
3673
+ const content = await safeRead(auth.instanceUrl, auth.token, scope, id, slug);
3674
+ if (content) {
3675
+ const label = slug.split("/").pop();
3676
+ sections.push(`## ${label.charAt(0).toUpperCase() + label.slice(1)}
3677
+
3678
+ ${content}`);
3679
+ }
3680
+ }
3681
+ if (sections.length === 0)
3682
+ return "No project memory found. Use gitlab_memory_record to start building project knowledge.";
3683
+ return sections.join("\n\n---\n\n");
3684
+ }
3685
+ }),
3686
+ gitlab_memory_record: (0, import_plugin5.tool)({
3687
+ 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 and appended to the appropriate memory page.\nMultiple facts can be recorded in a single call by separating them with newlines.",
3688
+ args: {
3689
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3690
+ type: z5.enum(["fact", "decision", "pattern"]).describe("Type of knowledge to record"),
3691
+ content: z5.string().describe("The knowledge to record (markdown)"),
3692
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3693
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3694
+ },
3695
+ execute: async (args) => {
3696
+ const auth = ctx.ensureAuth();
3697
+ if (!auth) throw new Error("GitLab authentication not available");
3698
+ const { scope, id } = resolveScope(args);
3699
+ const slug = RECORD_SLUG[args.type];
3700
+ if (!slug) return `Unknown memory type: ${args.type}`;
3701
+ const date = today();
3702
+ let formatted;
3703
+ if (args.type === "decision") {
3704
+ const firstLine = args.content.split("\n")[0];
3705
+ const rest = args.content.split("\n").slice(1).join("\n").trim();
3706
+ formatted = `## ${date}: ${firstLine}${rest ? "\n" + rest : ""}`;
3707
+ } else if (args.type === "fact") {
3708
+ formatted = `## ${date}
3709
+ - ${args.content.replace(/\n/g, "\n- ")}`;
3710
+ } else {
3711
+ formatted = `## ${date}
3712
+ ${args.content}`;
3713
+ }
3633
3714
  try {
3634
- const page = await getWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3635
- return page.content;
3715
+ await appendToPage(auth.instanceUrl, auth.token, scope, id, slug, formatted);
3716
+ return `Recorded ${args.type} in project memory.`;
3636
3717
  } catch (err) {
3637
- return `Error reading wiki page: ${err.message}`;
3718
+ return `Error recording ${args.type}: ${err.message}`;
3638
3719
  }
3639
3720
  }
3640
3721
  }),
3641
- gitlab_wiki_write: (0, import_plugin5.tool)({
3642
- 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.",
3722
+ gitlab_memory_recall: (0, import_plugin5.tool)({
3723
+ 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.",
3643
3724
  args: {
3644
- project_id: z5.string().describe(
3645
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3646
- ),
3647
- slug: z5.string().describe("Wiki page slug (e.g., memory/facts)"),
3648
- content: z5.string().describe("Full page content in Markdown"),
3649
- title: z5.string().optional().describe("Page title (defaults to slug if omitted)"),
3650
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3651
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3725
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3726
+ query: z5.string().describe("What to search for"),
3727
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3728
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3652
3729
  },
3653
3730
  execute: async (args) => {
3654
3731
  const auth = ctx.ensureAuth();
3655
3732
  if (!auth) throw new Error("GitLab authentication not available");
3656
3733
  const { scope, id } = resolveScope(args);
3657
3734
  try {
3658
- const page = await updateWikiPage(
3735
+ const results = await searchWikiPages(
3659
3736
  auth.instanceUrl,
3660
3737
  auth.token,
3661
3738
  scope,
3662
3739
  id,
3663
- args.slug,
3664
- args.content,
3665
- args.title
3740
+ args.query
3666
3741
  );
3667
- return `Updated wiki page: ${page.slug}`;
3668
- } catch {
3669
- try {
3670
- const title = args.title || args.slug.split("/").pop() || args.slug;
3671
- const page = await createWikiPage(
3672
- auth.instanceUrl,
3673
- auth.token,
3674
- scope,
3675
- id,
3676
- title,
3677
- args.content
3678
- );
3679
- return `Created wiki page: ${page.slug}`;
3680
- } catch (err) {
3681
- return `Error writing wiki page: ${err.message}`;
3682
- }
3742
+ if (results.length === 0) return `No knowledge found matching "${args.query}".`;
3743
+ return results.map((r) => {
3744
+ const category = r.path.includes("/memory/") ? "memory" : r.path.includes("/skills") ? "skill" : "other";
3745
+ const snippet = (r.data ?? "").slice(0, 200).replace(/\n/g, " ");
3746
+ return `[${category}] ${r.path}: ${snippet}`;
3747
+ }).join("\n\n");
3748
+ } catch (err) {
3749
+ return `Error searching knowledge: ${err.message}`;
3683
3750
  }
3684
3751
  }
3685
3752
  }),
3686
- gitlab_wiki_append: (0, import_plugin5.tool)({
3687
- 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.",
3753
+ gitlab_memory_log_session: (0, import_plugin5.tool)({
3754
+ 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.",
3688
3755
  args: {
3689
- project_id: z5.string().describe(
3690
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3756
+ project_id: z5.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3757
+ title: z5.string().describe('Brief session title (e.g., "fix-ai-gateway-healthcheck")'),
3758
+ summary: z5.string().describe(
3759
+ "Session summary in markdown (what happened, what was learned, what went wrong)"
3691
3760
  ),
3692
- slug: z5.string().describe("Wiki page slug (e.g., memory/facts)"),
3693
- text: z5.string().describe("Text to append to the page"),
3694
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3695
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3761
+ scope: z5.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3762
+ group_id: z5.string().optional().describe("Group path (required when scope is groups)")
3696
3763
  },
3697
3764
  execute: async (args) => {
3698
3765
  const auth = ctx.ensureAuth();
3699
3766
  if (!auth) throw new Error("GitLab authentication not available");
3700
3767
  const { scope, id } = resolveScope(args);
3768
+ const date = today();
3769
+ const slug = `${PREFIX}/memory/sessions/${date}-${slugify(args.title)}`;
3701
3770
  try {
3702
- const page = await getWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3703
- const newContent = page.content + "\n" + args.text;
3704
- await updateWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug, newContent);
3705
- return `Appended to wiki page: ${args.slug}`;
3706
- } catch {
3707
- try {
3708
- const title = args.slug.split("/").pop() || args.slug;
3709
- await createWikiPage(auth.instanceUrl, auth.token, scope, id, title, args.text);
3710
- return `Created wiki page with initial content: ${args.slug}`;
3711
- } catch (err) {
3712
- return `Error appending to wiki page: ${err.message}`;
3771
+ await createWikiPage(auth.instanceUrl, auth.token, scope, id, slug, args.summary);
3772
+ return `Session logged: ${slug}`;
3773
+ } catch (err) {
3774
+ if (err.message?.includes("already exists") || err.message?.includes("422")) {
3775
+ try {
3776
+ await updateWikiPage(auth.instanceUrl, auth.token, scope, id, slug, args.summary);
3777
+ return `Session log updated: ${slug}`;
3778
+ } catch (err2) {
3779
+ return `Error logging session: ${err2.message}`;
3780
+ }
3713
3781
  }
3782
+ return `Error logging session: ${err.message}`;
3714
3783
  }
3715
3784
  }
3716
- }),
3717
- gitlab_wiki_list: (0, import_plugin5.tool)({
3718
- 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.",
3785
+ })
3786
+ };
3787
+ }
3788
+
3789
+ // src/tools/skill-tools.ts
3790
+ var import_plugin6 = require("@opencode-ai/plugin");
3791
+ var z6 = import_plugin6.tool.schema;
3792
+ var PREFIX2 = "agents";
3793
+ var SKILLS_PREFIX = `${PREFIX2}/skills`;
3794
+ var DRAFTS_PREFIX = `${PREFIX2}/skills-drafts`;
3795
+ function resolveScope2(args) {
3796
+ if (args.scope === "groups" && args.group_id) {
3797
+ return { scope: "groups", id: args.group_id };
3798
+ }
3799
+ return { scope: "projects", id: args.project_id };
3800
+ }
3801
+ function makeSkillTools(ctx) {
3802
+ return {
3803
+ gitlab_skill_list: (0, import_plugin6.tool)({
3804
+ description: "List available project skills and optionally draft skills.\nSkills define step-by-step procedures for common tasks (e.g., incident retros, debugging, deployments).",
3719
3805
  args: {
3720
- project_id: z5.string().describe(
3721
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3722
- ),
3723
- prefix: z5.string().optional().describe('Filter pages by slug prefix (e.g., "memory/")'),
3724
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3725
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3806
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3807
+ include_drafts: z6.boolean().optional().describe("Also list draft skills (default: false)"),
3808
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3809
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3726
3810
  },
3727
3811
  execute: async (args) => {
3728
3812
  const auth = ctx.ensureAuth();
3729
3813
  if (!auth) throw new Error("GitLab authentication not available");
3730
- const { scope, id } = resolveScope(args);
3814
+ const { scope, id } = resolveScope2(args);
3731
3815
  try {
3732
3816
  const pages = await listWikiPages(auth.instanceUrl, auth.token, scope, id);
3733
- const filtered = args.prefix ? pages.filter((p) => p.slug.startsWith(args.prefix)) : pages;
3734
- return JSON.stringify(
3735
- filtered.map((p) => ({ slug: p.slug, title: p.title })),
3736
- null,
3737
- 2
3738
- );
3817
+ const indexSlug = `${SKILLS_PREFIX}/index`;
3818
+ const skills = pages.filter((p) => p.slug.startsWith(`${SKILLS_PREFIX}/`) && p.slug !== indexSlug).map((p) => ({
3819
+ name: p.slug.slice(SKILLS_PREFIX.length + 1),
3820
+ title: p.title,
3821
+ draft: false
3822
+ }));
3823
+ let drafts = [];
3824
+ if (args.include_drafts) {
3825
+ const draftsIndexSlug = `${DRAFTS_PREFIX}/index`;
3826
+ drafts = pages.filter((p) => p.slug.startsWith(`${DRAFTS_PREFIX}/`) && p.slug !== draftsIndexSlug).map((p) => ({
3827
+ name: p.slug.slice(DRAFTS_PREFIX.length + 1),
3828
+ title: p.title,
3829
+ draft: true
3830
+ }));
3831
+ }
3832
+ const all = [...skills, ...drafts];
3833
+ if (all.length === 0) return "No skills found. Use gitlab_skill_save to create one.";
3834
+ return JSON.stringify(all, null, 2);
3739
3835
  } catch (err) {
3740
- return `Error listing wiki pages: ${err.message}`;
3836
+ return `Error listing skills: ${err.message}`;
3741
3837
  }
3742
3838
  }
3743
3839
  }),
3744
- gitlab_wiki_delete: (0, import_plugin5.tool)({
3745
- description: "Delete a wiki page by slug.",
3840
+ gitlab_skill_load: (0, import_plugin6.tool)({
3841
+ 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.",
3746
3842
  args: {
3747
- project_id: z5.string().describe(
3748
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3749
- ),
3750
- slug: z5.string().describe("Wiki page slug to delete"),
3751
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3752
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3843
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3844
+ name: z6.string().describe('Skill name (e.g., "incident-retro", "helm-rollback")'),
3845
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3846
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3753
3847
  },
3754
3848
  execute: async (args) => {
3755
3849
  const auth = ctx.ensureAuth();
3756
3850
  if (!auth) throw new Error("GitLab authentication not available");
3757
- const { scope, id } = resolveScope(args);
3851
+ const { scope, id } = resolveScope2(args);
3758
3852
  try {
3759
- await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3760
- return `Deleted wiki page: ${args.slug}`;
3761
- } catch (err) {
3762
- return `Error deleting wiki page: ${err.message}`;
3853
+ const page = await getWikiPage(
3854
+ auth.instanceUrl,
3855
+ auth.token,
3856
+ scope,
3857
+ id,
3858
+ `${SKILLS_PREFIX}/${args.name}`
3859
+ );
3860
+ return page.content;
3861
+ } catch {
3862
+ try {
3863
+ const draft = await getWikiPage(
3864
+ auth.instanceUrl,
3865
+ auth.token,
3866
+ scope,
3867
+ id,
3868
+ `${DRAFTS_PREFIX}/${args.name}`
3869
+ );
3870
+ return `[DRAFT SKILL]
3871
+
3872
+ ${draft.content}`;
3873
+ } catch {
3874
+ return `Skill "${args.name}" not found. Use gitlab_skill_list to see available skills.`;
3875
+ }
3763
3876
  }
3764
3877
  }
3765
3878
  }),
3766
- gitlab_wiki_search: (0, import_plugin5.tool)({
3767
- description: "Search wiki pages via GitLab search API.\nReturns matching page paths and content snippets.",
3879
+ gitlab_skill_save: (0, import_plugin6.tool)({
3880
+ 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.",
3768
3881
  args: {
3769
- project_id: z5.string().describe(
3770
- 'Project path (e.g., "gitlab-org/gitlab") or numeric ID. IMPORTANT: use the same format consistently across all wiki calls.'
3771
- ),
3772
- query: z5.string().describe("Search query string"),
3773
- scope: z5.enum(["projects", "groups"]).optional().describe('Scope: "projects" (default) or "groups"'),
3774
- group_id: z5.string().optional().describe("Group path, required when scope is groups")
3882
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3883
+ name: z6.string().describe('Skill name (e.g., "incident-retro")'),
3884
+ content: z6.string().describe("Skill content in markdown"),
3885
+ draft: z6.boolean().optional().describe("Save as draft skill (default: false)"),
3886
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3887
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3775
3888
  },
3776
3889
  execute: async (args) => {
3777
3890
  const auth = ctx.ensureAuth();
3778
3891
  if (!auth) throw new Error("GitLab authentication not available");
3779
- const { scope, id } = resolveScope(args);
3892
+ const { scope, id } = resolveScope2(args);
3893
+ const prefix = args.draft ? DRAFTS_PREFIX : SKILLS_PREFIX;
3894
+ const slug = `${prefix}/${args.name}`;
3780
3895
  try {
3781
- const results = await searchWikiPages(
3782
- auth.instanceUrl,
3783
- auth.token,
3784
- scope,
3785
- id,
3786
- args.query
3787
- );
3788
- return JSON.stringify(
3789
- results.map((r) => ({ path: r.path, snippet: r.data })),
3790
- null,
3791
- 2
3792
- );
3896
+ await updateWikiPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
3897
+ return `Updated ${args.draft ? "draft " : ""}skill: ${args.name}`;
3898
+ } catch {
3899
+ try {
3900
+ await createWikiPage(auth.instanceUrl, auth.token, scope, id, slug, args.content);
3901
+ return `Created ${args.draft ? "draft " : ""}skill: ${args.name}`;
3902
+ } catch (err) {
3903
+ return `Error saving skill: ${err.message}`;
3904
+ }
3905
+ }
3906
+ }
3907
+ }),
3908
+ gitlab_skill_promote: (0, import_plugin6.tool)({
3909
+ description: "Promote a draft skill to published.\nMoves the skill from the drafts directory to the published skills directory.",
3910
+ args: {
3911
+ project_id: z6.string().describe('Project path (e.g., "gitlab-org/gitlab"). Use the same value consistently.'),
3912
+ name: z6.string().describe("Skill name to promote"),
3913
+ scope: z6.enum(["projects", "groups"]).optional().describe("Scope (default: projects)"),
3914
+ group_id: z6.string().optional().describe("Group path (required when scope is groups)")
3915
+ },
3916
+ execute: async (args) => {
3917
+ const auth = ctx.ensureAuth();
3918
+ if (!auth) throw new Error("GitLab authentication not available");
3919
+ const { scope, id } = resolveScope2(args);
3920
+ const draftSlug = `${DRAFTS_PREFIX}/${args.name}`;
3921
+ const publishedSlug = `${SKILLS_PREFIX}/${args.name}`;
3922
+ try {
3923
+ const draft = await getWikiPage(auth.instanceUrl, auth.token, scope, id, draftSlug);
3924
+ try {
3925
+ await updateWikiPage(
3926
+ auth.instanceUrl,
3927
+ auth.token,
3928
+ scope,
3929
+ id,
3930
+ publishedSlug,
3931
+ draft.content
3932
+ );
3933
+ } catch {
3934
+ await createWikiPage(
3935
+ auth.instanceUrl,
3936
+ auth.token,
3937
+ scope,
3938
+ id,
3939
+ publishedSlug,
3940
+ draft.content
3941
+ );
3942
+ }
3943
+ await deleteWikiPage(auth.instanceUrl, auth.token, scope, id, draftSlug);
3944
+ return `Promoted skill "${args.name}" from draft to published.`;
3793
3945
  } catch (err) {
3794
- return `Error searching wiki: ${err.message}`;
3946
+ if (err.message?.includes("not found") || err.message?.includes("404")) {
3947
+ return `Draft skill "${args.name}" not found. Use gitlab_skill_list(include_drafts=true) to see available drafts.`;
3948
+ }
3949
+ return `Error promoting skill: ${err.message}`;
3795
3950
  }
3796
3951
  }
3797
3952
  })
@@ -3952,7 +4107,8 @@ var plugin = async (input) => {
3952
4107
  ...makeMcpTools(() => cachedAgents),
3953
4108
  ...makeCatalogCrudTools(ctx),
3954
4109
  ...makeCatalogItemTools(ctx),
3955
- ...makeWikiTools(ctx)
4110
+ ...makeMemoryTools(ctx),
4111
+ ...makeSkillTools(ctx)
3956
4112
  }
3957
4113
  };
3958
4114
  };