opencode-gitlab-dap 1.6.1 → 1.7.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,9 @@ 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
- ### 20 DAP Tools
113
+ ### 26 Tools
114
+
115
+ #### DAP Tools (20)
114
116
 
115
117
  | Tool | Description |
116
118
  | --------------------------------- | ------------------------------------------------------- |
@@ -135,6 +137,34 @@ The vendored `flow_v2.json` schema from GitLab Rails powers client-side validati
135
137
  | `gitlab_get_workflow_status` | Monitor workflow execution status and logs |
136
138
  | `gitlab_list_project_mcp_servers` | List MCP servers available for project agents |
137
139
 
140
+ #### Wiki Memory Tools (6)
141
+
142
+ Persistent project memory and skill storage via GitLab wikis.
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) |
152
+
153
+ All wiki tools support both project wikis (default) and group wikis (`scope="groups"`).
154
+
155
+ ##### Recommended Wiki Structure
156
+
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
+ ```
167
+
138
168
  ### Dynamic Refresh
139
169
 
140
170
  After enabling or disabling an agent/flow, the plugin automatically refreshes the
package/dist/index.cjs CHANGED
@@ -2254,6 +2254,9 @@ 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
2260
 
2258
2261
  // src/hooks.ts
2259
2262
  function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
@@ -2413,6 +2416,7 @@ function makeSystemTransformHook(flowAgents, getAuthCache) {
2413
2416
  }
2414
2417
  if (getAuthCache()) {
2415
2418
  output.system.push(AGENT_CREATION_GUIDELINES);
2419
+ output.system.push(WIKI_MEMORY_HINT);
2416
2420
  }
2417
2421
  };
2418
2422
  }
@@ -3521,6 +3525,279 @@ function makeMcpTools(getCachedAgents) {
3521
3525
  };
3522
3526
  }
3523
3527
 
3528
+ // src/tools/wiki-tools.ts
3529
+ var import_plugin5 = require("@opencode-ai/plugin");
3530
+
3531
+ // src/wiki.ts
3532
+ function wikiApi(instanceUrl, token, scope, id, path = "") {
3533
+ const base = instanceUrl.replace(/\/$/, "");
3534
+ const encodedId = encodeURIComponent(String(id));
3535
+ return {
3536
+ url: `${base}/api/v4/${scope}/${encodedId}/wikis${path}`,
3537
+ headers: {
3538
+ Authorization: `Bearer ${token}`,
3539
+ "Content-Type": "application/json"
3540
+ }
3541
+ };
3542
+ }
3543
+ async function handleResponse(res) {
3544
+ if (res.ok) return res.json();
3545
+ const status = res.status;
3546
+ if (status === 404)
3547
+ throw new Error(
3548
+ "Page not found. Use gitlab_wiki_list to see available pages, or gitlab_wiki_write to create it."
3549
+ );
3550
+ if (status === 403)
3551
+ throw new Error(
3552
+ "Permission denied. The wiki may not be enabled for this project. Enable it in Settings > General > Visibility."
3553
+ );
3554
+ if (status === 422)
3555
+ throw new Error("Invalid page title or content. Check that the slug and content are valid.");
3556
+ if (status === 401) throw new Error("Authentication failed. Check your GitLab token.");
3557
+ const text = await res.text();
3558
+ throw new Error(`Wiki API error (${status}): ${text}`);
3559
+ }
3560
+ async function listWikiPages(instanceUrl, token, scope, id, withContent) {
3561
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id);
3562
+ const fullUrl = withContent ? `${url}?with_content=1` : url;
3563
+ const res = await fetch(fullUrl, { headers });
3564
+ return handleResponse(res);
3565
+ }
3566
+ async function getWikiPage(instanceUrl, token, scope, id, slug) {
3567
+ const encodedSlug = encodeURIComponent(slug);
3568
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id, `/${encodedSlug}`);
3569
+ const res = await fetch(url, { headers });
3570
+ return handleResponse(res);
3571
+ }
3572
+ async function createWikiPage(instanceUrl, token, scope, id, title, content, format = "markdown") {
3573
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id);
3574
+ const res = await fetch(url, {
3575
+ method: "POST",
3576
+ headers,
3577
+ body: JSON.stringify({ title, content, format })
3578
+ });
3579
+ return handleResponse(res);
3580
+ }
3581
+ async function updateWikiPage(instanceUrl, token, scope, id, slug, content, title) {
3582
+ const encodedSlug = encodeURIComponent(slug);
3583
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id, `/${encodedSlug}`);
3584
+ const body = { content };
3585
+ if (title) body.title = title;
3586
+ const res = await fetch(url, {
3587
+ method: "PUT",
3588
+ headers,
3589
+ body: JSON.stringify(body)
3590
+ });
3591
+ return handleResponse(res);
3592
+ }
3593
+ async function deleteWikiPage(instanceUrl, token, scope, id, slug) {
3594
+ const encodedSlug = encodeURIComponent(slug);
3595
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id, `/${encodedSlug}`);
3596
+ const res = await fetch(url, { method: "DELETE", headers });
3597
+ if (!res.ok) await handleResponse(res);
3598
+ }
3599
+ async function searchWikiPages(instanceUrl, token, scope, id, query) {
3600
+ const base = instanceUrl.replace(/\/$/, "");
3601
+ const encodedId = encodeURIComponent(String(id));
3602
+ const url = `${base}/api/v4/${scope}/${encodedId}/search?scope=wiki_blobs&search=${encodeURIComponent(query)}`;
3603
+ const res = await fetch(url, {
3604
+ headers: { Authorization: `Bearer ${token}` }
3605
+ });
3606
+ return handleResponse(res);
3607
+ }
3608
+
3609
+ // src/tools/wiki-tools.ts
3610
+ 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 };
3617
+ }
3618
+ 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.",
3621
+ 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")
3628
+ },
3629
+ execute: async (args) => {
3630
+ const auth = ctx.ensureAuth();
3631
+ if (!auth) throw new Error("GitLab authentication not available");
3632
+ const { scope, id } = resolveScope(args);
3633
+ try {
3634
+ const page = await getWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3635
+ return page.content;
3636
+ } catch (err) {
3637
+ return `Error reading wiki page: ${err.message}`;
3638
+ }
3639
+ }
3640
+ }),
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.",
3643
+ 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")
3652
+ },
3653
+ execute: async (args) => {
3654
+ const auth = ctx.ensureAuth();
3655
+ if (!auth) throw new Error("GitLab authentication not available");
3656
+ const { scope, id } = resolveScope(args);
3657
+ try {
3658
+ const page = await updateWikiPage(
3659
+ auth.instanceUrl,
3660
+ auth.token,
3661
+ scope,
3662
+ id,
3663
+ args.slug,
3664
+ args.content,
3665
+ args.title
3666
+ );
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
+ }
3683
+ }
3684
+ }
3685
+ }),
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.",
3688
+ 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.'
3691
+ ),
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")
3696
+ },
3697
+ execute: async (args) => {
3698
+ const auth = ctx.ensureAuth();
3699
+ if (!auth) throw new Error("GitLab authentication not available");
3700
+ const { scope, id } = resolveScope(args);
3701
+ 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}`;
3713
+ }
3714
+ }
3715
+ }
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.",
3719
+ 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")
3726
+ },
3727
+ execute: async (args) => {
3728
+ const auth = ctx.ensureAuth();
3729
+ if (!auth) throw new Error("GitLab authentication not available");
3730
+ const { scope, id } = resolveScope(args);
3731
+ try {
3732
+ 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
+ );
3739
+ } catch (err) {
3740
+ return `Error listing wiki pages: ${err.message}`;
3741
+ }
3742
+ }
3743
+ }),
3744
+ gitlab_wiki_delete: (0, import_plugin5.tool)({
3745
+ description: "Delete a wiki page by slug.",
3746
+ 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")
3753
+ },
3754
+ execute: async (args) => {
3755
+ const auth = ctx.ensureAuth();
3756
+ if (!auth) throw new Error("GitLab authentication not available");
3757
+ const { scope, id } = resolveScope(args);
3758
+ 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}`;
3763
+ }
3764
+ }
3765
+ }),
3766
+ gitlab_wiki_search: (0, import_plugin5.tool)({
3767
+ description: "Search wiki pages via GitLab search API.\nReturns matching page paths and content snippets.",
3768
+ 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")
3775
+ },
3776
+ execute: async (args) => {
3777
+ const auth = ctx.ensureAuth();
3778
+ if (!auth) throw new Error("GitLab authentication not available");
3779
+ const { scope, id } = resolveScope(args);
3780
+ 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
+ );
3793
+ } catch (err) {
3794
+ return `Error searching wiki: ${err.message}`;
3795
+ }
3796
+ }
3797
+ })
3798
+ };
3799
+ }
3800
+
3524
3801
  // src/index.ts
3525
3802
  var memo = /* @__PURE__ */ new Map();
3526
3803
  var plugin = async (input) => {
@@ -3674,7 +3951,8 @@ var plugin = async (input) => {
3674
3951
  ...makeFlowTools(ctx),
3675
3952
  ...makeMcpTools(() => cachedAgents),
3676
3953
  ...makeCatalogCrudTools(ctx),
3677
- ...makeCatalogItemTools(ctx)
3954
+ ...makeCatalogItemTools(ctx),
3955
+ ...makeWikiTools(ctx)
3678
3956
  }
3679
3957
  };
3680
3958
  };