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/dist/index.js CHANGED
@@ -2085,6 +2085,9 @@ 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
2091
 
2089
2092
  // src/hooks.ts
2090
2093
  function buildFlowSubagentPrompt(flow, projectPath, projectUrl) {
@@ -2244,6 +2247,7 @@ function makeSystemTransformHook(flowAgents, getAuthCache) {
2244
2247
  }
2245
2248
  if (getAuthCache()) {
2246
2249
  output.system.push(AGENT_CREATION_GUIDELINES);
2250
+ output.system.push(WIKI_MEMORY_HINT);
2247
2251
  }
2248
2252
  };
2249
2253
  }
@@ -3352,6 +3356,279 @@ function makeMcpTools(getCachedAgents) {
3352
3356
  };
3353
3357
  }
3354
3358
 
3359
+ // src/tools/wiki-tools.ts
3360
+ import { tool as tool5 } from "@opencode-ai/plugin";
3361
+
3362
+ // src/wiki.ts
3363
+ function wikiApi(instanceUrl, token, scope, id, path = "") {
3364
+ const base = instanceUrl.replace(/\/$/, "");
3365
+ const encodedId = encodeURIComponent(String(id));
3366
+ return {
3367
+ url: `${base}/api/v4/${scope}/${encodedId}/wikis${path}`,
3368
+ headers: {
3369
+ Authorization: `Bearer ${token}`,
3370
+ "Content-Type": "application/json"
3371
+ }
3372
+ };
3373
+ }
3374
+ async function handleResponse(res) {
3375
+ if (res.ok) return res.json();
3376
+ const status = res.status;
3377
+ if (status === 404)
3378
+ throw new Error(
3379
+ "Page not found. Use gitlab_wiki_list to see available pages, or gitlab_wiki_write to create it."
3380
+ );
3381
+ if (status === 403)
3382
+ throw new Error(
3383
+ "Permission denied. The wiki may not be enabled for this project. Enable it in Settings > General > Visibility."
3384
+ );
3385
+ if (status === 422)
3386
+ throw new Error("Invalid page title or content. Check that the slug and content are valid.");
3387
+ if (status === 401) throw new Error("Authentication failed. Check your GitLab token.");
3388
+ const text = await res.text();
3389
+ throw new Error(`Wiki API error (${status}): ${text}`);
3390
+ }
3391
+ async function listWikiPages(instanceUrl, token, scope, id, withContent) {
3392
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id);
3393
+ const fullUrl = withContent ? `${url}?with_content=1` : url;
3394
+ const res = await fetch(fullUrl, { headers });
3395
+ return handleResponse(res);
3396
+ }
3397
+ async function getWikiPage(instanceUrl, token, scope, id, slug) {
3398
+ const encodedSlug = encodeURIComponent(slug);
3399
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id, `/${encodedSlug}`);
3400
+ const res = await fetch(url, { headers });
3401
+ return handleResponse(res);
3402
+ }
3403
+ async function createWikiPage(instanceUrl, token, scope, id, title, content, format = "markdown") {
3404
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id);
3405
+ const res = await fetch(url, {
3406
+ method: "POST",
3407
+ headers,
3408
+ body: JSON.stringify({ title, content, format })
3409
+ });
3410
+ return handleResponse(res);
3411
+ }
3412
+ async function updateWikiPage(instanceUrl, token, scope, id, slug, content, title) {
3413
+ const encodedSlug = encodeURIComponent(slug);
3414
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id, `/${encodedSlug}`);
3415
+ const body = { content };
3416
+ if (title) body.title = title;
3417
+ const res = await fetch(url, {
3418
+ method: "PUT",
3419
+ headers,
3420
+ body: JSON.stringify(body)
3421
+ });
3422
+ return handleResponse(res);
3423
+ }
3424
+ async function deleteWikiPage(instanceUrl, token, scope, id, slug) {
3425
+ const encodedSlug = encodeURIComponent(slug);
3426
+ const { url, headers } = wikiApi(instanceUrl, token, scope, id, `/${encodedSlug}`);
3427
+ const res = await fetch(url, { method: "DELETE", headers });
3428
+ if (!res.ok) await handleResponse(res);
3429
+ }
3430
+ async function searchWikiPages(instanceUrl, token, scope, id, query) {
3431
+ const base = instanceUrl.replace(/\/$/, "");
3432
+ const encodedId = encodeURIComponent(String(id));
3433
+ const url = `${base}/api/v4/${scope}/${encodedId}/search?scope=wiki_blobs&search=${encodeURIComponent(query)}`;
3434
+ const res = await fetch(url, {
3435
+ headers: { Authorization: `Bearer ${token}` }
3436
+ });
3437
+ return handleResponse(res);
3438
+ }
3439
+
3440
+ // src/tools/wiki-tools.ts
3441
+ 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 };
3448
+ }
3449
+ 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.",
3452
+ 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")
3459
+ },
3460
+ execute: async (args) => {
3461
+ const auth = ctx.ensureAuth();
3462
+ if (!auth) throw new Error("GitLab authentication not available");
3463
+ const { scope, id } = resolveScope(args);
3464
+ try {
3465
+ const page = await getWikiPage(auth.instanceUrl, auth.token, scope, id, args.slug);
3466
+ return page.content;
3467
+ } catch (err) {
3468
+ return `Error reading wiki page: ${err.message}`;
3469
+ }
3470
+ }
3471
+ }),
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.",
3474
+ 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")
3483
+ },
3484
+ execute: async (args) => {
3485
+ const auth = ctx.ensureAuth();
3486
+ if (!auth) throw new Error("GitLab authentication not available");
3487
+ const { scope, id } = resolveScope(args);
3488
+ try {
3489
+ const page = await updateWikiPage(
3490
+ auth.instanceUrl,
3491
+ auth.token,
3492
+ scope,
3493
+ id,
3494
+ args.slug,
3495
+ args.content,
3496
+ args.title
3497
+ );
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
+ }
3514
+ }
3515
+ }
3516
+ }),
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.",
3519
+ 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.'
3522
+ ),
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")
3527
+ },
3528
+ execute: async (args) => {
3529
+ const auth = ctx.ensureAuth();
3530
+ if (!auth) throw new Error("GitLab authentication not available");
3531
+ const { scope, id } = resolveScope(args);
3532
+ 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}`;
3544
+ }
3545
+ }
3546
+ }
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.",
3550
+ 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")
3557
+ },
3558
+ execute: async (args) => {
3559
+ const auth = ctx.ensureAuth();
3560
+ if (!auth) throw new Error("GitLab authentication not available");
3561
+ const { scope, id } = resolveScope(args);
3562
+ try {
3563
+ 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
+ );
3570
+ } catch (err) {
3571
+ return `Error listing wiki pages: ${err.message}`;
3572
+ }
3573
+ }
3574
+ }),
3575
+ gitlab_wiki_delete: tool5({
3576
+ description: "Delete a wiki page by slug.",
3577
+ 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")
3584
+ },
3585
+ execute: async (args) => {
3586
+ const auth = ctx.ensureAuth();
3587
+ if (!auth) throw new Error("GitLab authentication not available");
3588
+ const { scope, id } = resolveScope(args);
3589
+ 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}`;
3594
+ }
3595
+ }
3596
+ }),
3597
+ gitlab_wiki_search: tool5({
3598
+ description: "Search wiki pages via GitLab search API.\nReturns matching page paths and content snippets.",
3599
+ 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")
3606
+ },
3607
+ execute: async (args) => {
3608
+ const auth = ctx.ensureAuth();
3609
+ if (!auth) throw new Error("GitLab authentication not available");
3610
+ const { scope, id } = resolveScope(args);
3611
+ try {
3612
+ const results = await searchWikiPages(
3613
+ auth.instanceUrl,
3614
+ auth.token,
3615
+ scope,
3616
+ id,
3617
+ args.query
3618
+ );
3619
+ return JSON.stringify(
3620
+ results.map((r) => ({ path: r.path, snippet: r.data })),
3621
+ null,
3622
+ 2
3623
+ );
3624
+ } catch (err) {
3625
+ return `Error searching wiki: ${err.message}`;
3626
+ }
3627
+ }
3628
+ })
3629
+ };
3630
+ }
3631
+
3355
3632
  // src/index.ts
3356
3633
  var memo = /* @__PURE__ */ new Map();
3357
3634
  var plugin = async (input) => {
@@ -3505,7 +3782,8 @@ var plugin = async (input) => {
3505
3782
  ...makeFlowTools(ctx),
3506
3783
  ...makeMcpTools(() => cachedAgents),
3507
3784
  ...makeCatalogCrudTools(ctx),
3508
- ...makeCatalogItemTools(ctx)
3785
+ ...makeCatalogItemTools(ctx),
3786
+ ...makeWikiTools(ctx)
3509
3787
  }
3510
3788
  };
3511
3789
  };