@f5xc-salesdemos/xcsh 18.35.3 → 18.36.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/package.json +7 -7
- package/src/config/settings-schema.ts +11 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/prompts/tools/glab-issue-list.md +7 -0
- package/src/prompts/tools/glab-issue-view.md +7 -0
- package/src/prompts/tools/glab-search.md +7 -0
- package/src/prompts/tools/glab-setup.md +6 -0
- package/src/tools/bash.ts +17 -1
- package/src/tools/glab/config.ts +37 -0
- package/src/tools/glab/exec.ts +66 -0
- package/src/tools/glab/formatters.ts +73 -0
- package/src/tools/glab/graphql.ts +59 -0
- package/src/tools/glab/types.ts +86 -0
- package/src/tools/glab.ts +409 -0
- package/src/tools/index.ts +7 -0
- package/src/utils/git.ts +10 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.36.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
50
50
|
"@mozilla/readability": "^0.6",
|
|
51
|
-
"@f5xc-salesdemos/xcsh-stats": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
56
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
51
|
+
"@f5xc-salesdemos/xcsh-stats": "18.36.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-agent-core": "18.36.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-ai": "18.36.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-natives": "18.36.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-tui": "18.36.0",
|
|
56
|
+
"@f5xc-salesdemos/pi-utils": "18.36.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34",
|
|
58
58
|
"@xterm/headless": "^6.0",
|
|
59
59
|
"ajv": "^8.18",
|
|
@@ -1328,6 +1328,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1328
1328
|
},
|
|
1329
1329
|
},
|
|
1330
1330
|
|
|
1331
|
+
"gitlab.enabled": {
|
|
1332
|
+
type: "boolean",
|
|
1333
|
+
default: true,
|
|
1334
|
+
ui: {
|
|
1335
|
+
tab: "tools",
|
|
1336
|
+
label: "GitLab CLI",
|
|
1337
|
+
description:
|
|
1338
|
+
"Enable read-only glab_* tools for GitLab issue tracking, search, and work item access via glab CLI",
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
|
|
1331
1342
|
"web_search.enabled": {
|
|
1332
1343
|
type: "boolean",
|
|
1333
1344
|
default: true,
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.36.0",
|
|
21
|
+
"commit": "e184ba33c5bf2f52613a85f40b7ebd0d5043deb5",
|
|
22
|
+
"shortCommit": "e184ba3",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-05-
|
|
26
|
-
"buildDate": "2026-05-
|
|
24
|
+
"tag": "v18.36.0",
|
|
25
|
+
"commitDate": "2026-05-04T03:30:53Z",
|
|
26
|
+
"buildDate": "2026-05-04T03:50:55.993Z",
|
|
27
27
|
"dirty": false,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
|
|
30
30
|
"repoSlug": "f5xc-salesdemos/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/e184ba33c5bf2f52613a85f40b7ebd0d5043deb5",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.36.0"
|
|
33
33
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
List GitLab issues with structured filters via glab CLI. Returns a markdown summary table.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
Use for "show open bugs", "list issues assigned to X", "show high priority issues".
|
|
5
|
+
Supports: state (opened/closed/all), labels, assignee, search text, milestone, sort field, order direction, limit.
|
|
6
|
+
Defaults to the configured project. Pass project explicitly to override.
|
|
7
|
+
</instruction>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
View a single GitLab issue with full details, description, and comments via glab CLI.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
Use when the user asks to "show issue #N" or "view details of issue X".
|
|
5
|
+
Returns structured markdown with title, state, author, labels, assignee, milestone, description, and comments thread.
|
|
6
|
+
System notes (automated messages) are filtered out.
|
|
7
|
+
</instruction>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Full-text search across GitLab issue titles, descriptions, labels, and comments via glab CLI.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
Use for "find issues about Tempus", "search for login timeout bugs", "bugs mentioning Safari".
|
|
5
|
+
Three-tier search: REST API (fast, titles+descriptions), GraphQL (includes comments), client-side dedup.
|
|
6
|
+
Supports state filtering and label filtering alongside text search.
|
|
7
|
+
</instruction>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
GitLab onboarding wizard via glab CLI. Check installation, authenticate, discover projects, and persist configuration.
|
|
2
|
+
|
|
3
|
+
<instruction>
|
|
4
|
+
Actions: "check" (verify glab installed), "status" (auth + config), "login" (browser OAuth), "select_project" (list available projects), "save_project" (persist selected project path).
|
|
5
|
+
Run this sequence when the user first asks about GitLab issues and no project is configured.
|
|
6
|
+
</instruction>
|
package/src/tools/bash.ts
CHANGED
|
@@ -772,10 +772,26 @@ export const bashToolRenderer = {
|
|
|
772
772
|
const verbose = getBashVerboseSetting();
|
|
773
773
|
const hasAsyncDetails = details?.async != null;
|
|
774
774
|
const forceExpand = isError || hasAsyncDetails || hasSixelOutput;
|
|
775
|
-
if (!verbose && !expanded && !forceExpand
|
|
775
|
+
if (!verbose && !expanded && !forceExpand) {
|
|
776
776
|
const rawCmd = args?.command;
|
|
777
777
|
const summaryText =
|
|
778
778
|
args?.description ?? (rawCmd && rawCmd.length > 60 ? `${rawCmd.slice(0, 60)}…` : rawCmd) ?? "…";
|
|
779
|
+
|
|
780
|
+
if (options.isPartial) {
|
|
781
|
+
const lineCount = rawOutputLines.filter(l => l.trim().length > 0).length;
|
|
782
|
+
const line = renderStatusLine(
|
|
783
|
+
{
|
|
784
|
+
icon: "running",
|
|
785
|
+
spinnerFrame: options.spinnerFrame,
|
|
786
|
+
title: "Bash",
|
|
787
|
+
description: summaryText,
|
|
788
|
+
meta: lineCount > 0 ? [`${lineCount} lines`] : undefined,
|
|
789
|
+
},
|
|
790
|
+
uiTheme,
|
|
791
|
+
);
|
|
792
|
+
return [truncateToWidth(line, width)];
|
|
793
|
+
}
|
|
794
|
+
|
|
779
795
|
const line = renderStatusLine(
|
|
780
796
|
{
|
|
781
797
|
title: "Bash",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { GlabConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILENAME = "glab-config.json";
|
|
7
|
+
const XCSH_DIR = ".xcsh";
|
|
8
|
+
|
|
9
|
+
export async function loadConfig(cwd: string): Promise<GlabConfig | null> {
|
|
10
|
+
const projectConfig = path.join(cwd, XCSH_DIR, CONFIG_FILENAME);
|
|
11
|
+
try {
|
|
12
|
+
const raw = await fs.readFile(projectConfig, "utf8");
|
|
13
|
+
return JSON.parse(raw) as GlabConfig;
|
|
14
|
+
} catch {
|
|
15
|
+
// try user-level fallback
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const userConfig = path.join(os.homedir(), ".xcsh", "agent", CONFIG_FILENAME);
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs.readFile(userConfig, "utf8");
|
|
21
|
+
return JSON.parse(raw) as GlabConfig;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveConfig(cwd: string, config: GlabConfig): Promise<void> {
|
|
28
|
+
const dir = path.join(cwd, XCSH_DIR);
|
|
29
|
+
await fs.mkdir(dir, { recursive: true });
|
|
30
|
+
await fs.writeFile(path.join(dir, CONFIG_FILENAME), JSON.stringify(config, null, 2), "utf8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveProject(paramProject: string | undefined, cwd: string): Promise<string | null> {
|
|
34
|
+
if (paramProject) return paramProject;
|
|
35
|
+
const config = await loadConfig(cwd);
|
|
36
|
+
return config?.project ?? null;
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface GlabExecResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
code: number;
|
|
5
|
+
killed: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface GlabExecApi {
|
|
9
|
+
cwd: string;
|
|
10
|
+
exec(command: string, args: string[], options?: { signal?: AbortSignal; cwd?: string }): Promise<GlabExecResult>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class GlabAuthError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(`GitLab auth error: ${message}. Run glab_setup with action "login".`);
|
|
16
|
+
this.name = "GlabAuthError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class GlabNotFoundError extends Error {
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super(`GitLab resource not found (404/403): ${message}`);
|
|
23
|
+
this.name = "GlabNotFoundError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class GlabExecError extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
message: string,
|
|
30
|
+
public readonly code: number,
|
|
31
|
+
) {
|
|
32
|
+
super(`glab command failed (exit ${code}): ${message}`);
|
|
33
|
+
this.name = "GlabExecError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function checkInstalled(pi: GlabExecApi): Promise<boolean> {
|
|
38
|
+
const result = await pi.exec("which", ["glab"], { cwd: pi.cwd });
|
|
39
|
+
return result.code === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function checkAuth(pi: GlabExecApi): Promise<boolean> {
|
|
43
|
+
const result = await pi.exec("glab", ["auth", "status"], { cwd: pi.cwd });
|
|
44
|
+
return result.code === 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function execGlab(pi: GlabExecApi, args: string[], signal?: AbortSignal): Promise<GlabExecResult> {
|
|
48
|
+
const result = await pi.exec("glab", args, { signal, cwd: pi.cwd });
|
|
49
|
+
if (result.killed) throw new Error("Command was cancelled");
|
|
50
|
+
if (result.code !== 0) {
|
|
51
|
+
const stderr = result.stderr.toLowerCase();
|
|
52
|
+
if (stderr.includes("auth") || stderr.includes("not logged in") || stderr.includes("token")) {
|
|
53
|
+
throw new GlabAuthError(result.stderr);
|
|
54
|
+
}
|
|
55
|
+
if (stderr.includes("404") || stderr.includes("403") || stderr.includes("not found")) {
|
|
56
|
+
throw new GlabNotFoundError(result.stderr);
|
|
57
|
+
}
|
|
58
|
+
throw new GlabExecError(result.stderr, result.code);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function execGlabJson<T = unknown>(pi: GlabExecApi, args: string[], signal?: AbortSignal): Promise<T> {
|
|
64
|
+
const result = await execGlab(pi, args, signal);
|
|
65
|
+
return JSON.parse(result.stdout) as T;
|
|
66
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { GlabIssue } from "./types";
|
|
2
|
+
|
|
3
|
+
function formatDate(iso: string): string {
|
|
4
|
+
return iso.slice(0, 10);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function truncateLabels(labels: string[], maxLen = 30): string {
|
|
8
|
+
if (!labels.length) return "(none)";
|
|
9
|
+
const joined = labels.join(", ");
|
|
10
|
+
if (joined.length <= maxLen) return joined;
|
|
11
|
+
const truncated = labels.slice(0, 3).join(", ");
|
|
12
|
+
const remaining = labels.length - 3;
|
|
13
|
+
return remaining > 0 ? `${truncated}, +${remaining}` : truncated;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatIssueTable(issues: GlabIssue[]): string {
|
|
17
|
+
if (issues.length === 0) {
|
|
18
|
+
return "No issues found matching the criteria.";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const header = "| IID | Title | State | Labels | Assignee | Updated |";
|
|
22
|
+
const divider = "|-----|-------|-------|--------|----------|---------|";
|
|
23
|
+
|
|
24
|
+
const rows = issues.map(issue => {
|
|
25
|
+
const title = issue.title.length > 50 ? `${issue.title.slice(0, 47)}...` : issue.title;
|
|
26
|
+
const labels = truncateLabels(issue.labels);
|
|
27
|
+
const assignee = issue.assignees.length > 0 ? `@${issue.assignees[0].username}` : "unassigned";
|
|
28
|
+
const updated = formatDate(issue.updated_at);
|
|
29
|
+
return `| ${issue.iid} | ${title} | ${issue.state} | ${labels} | ${assignee} | ${updated} |`;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return [header, divider, ...rows].join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatIssueDetail(issue: GlabIssue): string {
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
|
|
38
|
+
lines.push(`# Issue #${issue.iid}: ${issue.title}`);
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push(
|
|
41
|
+
`State: ${issue.state} | Author: @${issue.author.username} | Created: ${formatDate(issue.created_at)} | Updated: ${formatDate(issue.updated_at)}`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (issue.labels.length > 0) {
|
|
45
|
+
lines.push(`Labels: ${issue.labels.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const assigneeStr =
|
|
49
|
+
issue.assignees.length > 0 ? issue.assignees.map(a => `@${a.username}`).join(", ") : "unassigned";
|
|
50
|
+
lines.push(`Assignee: ${assigneeStr}${issue.milestone ? ` | Milestone: ${issue.milestone.title}` : ""}`);
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push("---");
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(issue.description || "(no description)");
|
|
55
|
+
|
|
56
|
+
const humanNotes = (issue.notes ?? []).filter(n => !n.system);
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push("---");
|
|
59
|
+
lines.push("");
|
|
60
|
+
|
|
61
|
+
if (humanNotes.length === 0) {
|
|
62
|
+
lines.push("No comments.");
|
|
63
|
+
} else {
|
|
64
|
+
lines.push(`## Comments (${humanNotes.length})`);
|
|
65
|
+
for (const note of humanNotes) {
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push(`**@${note.author.username}** (${formatDate(note.created_at)}):`);
|
|
68
|
+
lines.push("> " + note.body.split("\n").join("\n> "));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { GlabExecApi } from "./exec";
|
|
2
|
+
import { execGlab } from "./exec";
|
|
3
|
+
import type { GraphQLIssueNode, GraphQLSearchResponse } from "./types";
|
|
4
|
+
|
|
5
|
+
export function buildSearchQuery(includeState: boolean): string {
|
|
6
|
+
const stateVar = includeState ? ", $state: IssuableState" : "";
|
|
7
|
+
const stateArg = includeState ? ", state: $state" : "";
|
|
8
|
+
return `
|
|
9
|
+
query SearchIssues($projectPath: ID!, $search: String!, $first: Int!${stateVar}) {
|
|
10
|
+
project(fullPath: $projectPath) {
|
|
11
|
+
issues(search: $search, first: $first${stateArg}) {
|
|
12
|
+
nodes {
|
|
13
|
+
iid
|
|
14
|
+
title
|
|
15
|
+
state
|
|
16
|
+
labels { nodes { title } }
|
|
17
|
+
assignees { nodes { username } }
|
|
18
|
+
updatedAt
|
|
19
|
+
notes(first: 20) {
|
|
20
|
+
nodes {
|
|
21
|
+
body
|
|
22
|
+
author { username }
|
|
23
|
+
createdAt
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}`.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function executeGraphQL(
|
|
33
|
+
pi: GlabExecApi,
|
|
34
|
+
projectPath: string,
|
|
35
|
+
searchText: string,
|
|
36
|
+
limit: number,
|
|
37
|
+
signal?: AbortSignal,
|
|
38
|
+
state?: string,
|
|
39
|
+
): Promise<GraphQLIssueNode[]> {
|
|
40
|
+
const includeState = !!state && state !== "all";
|
|
41
|
+
const query = buildSearchQuery(includeState);
|
|
42
|
+
const vars: Record<string, unknown> = { projectPath, search: searchText, first: limit };
|
|
43
|
+
if (includeState) vars.state = state === "closed" ? "closed" : "opened";
|
|
44
|
+
const variables = JSON.stringify(vars);
|
|
45
|
+
|
|
46
|
+
const result = await execGlab(
|
|
47
|
+
pi,
|
|
48
|
+
["api", "graphql", "-f", `query=${query}`, "-f", `variables=${variables}`],
|
|
49
|
+
signal,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const response = JSON.parse(result.stdout) as GraphQLSearchResponse;
|
|
53
|
+
|
|
54
|
+
if (response.errors && response.errors.length > 0) {
|
|
55
|
+
throw new Error(response.errors.map(e => e.message).join("; "));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response.data?.project?.issues?.nodes ?? [];
|
|
59
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// .xcsh/tools/glab/lib/types.ts
|
|
2
|
+
|
|
3
|
+
export interface GlabLabel {
|
|
4
|
+
name: string;
|
|
5
|
+
color: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface GlabUser {
|
|
9
|
+
username: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface GlabMilestone {
|
|
14
|
+
title: string;
|
|
15
|
+
iid: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GlabNote {
|
|
19
|
+
id: number;
|
|
20
|
+
body: string;
|
|
21
|
+
author: GlabUser;
|
|
22
|
+
created_at: string;
|
|
23
|
+
updated_at: string;
|
|
24
|
+
system: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GlabIssue {
|
|
28
|
+
id: number;
|
|
29
|
+
iid: number;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
state: "opened" | "closed";
|
|
33
|
+
labels: string[];
|
|
34
|
+
assignees: GlabUser[];
|
|
35
|
+
author: GlabUser;
|
|
36
|
+
milestone: GlabMilestone | null;
|
|
37
|
+
created_at: string;
|
|
38
|
+
updated_at: string;
|
|
39
|
+
web_url: string;
|
|
40
|
+
references: { full: string };
|
|
41
|
+
issue_type: string;
|
|
42
|
+
notes?: GlabNote[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface GlabProject {
|
|
46
|
+
id: number;
|
|
47
|
+
name: string;
|
|
48
|
+
name_with_namespace: string;
|
|
49
|
+
path_with_namespace: string;
|
|
50
|
+
web_url: string;
|
|
51
|
+
description: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GlabConfig {
|
|
55
|
+
project: string;
|
|
56
|
+
hostname: string;
|
|
57
|
+
defaultState: "opened" | "closed" | "all";
|
|
58
|
+
perPage: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface GraphQLIssueNode {
|
|
62
|
+
iid: string;
|
|
63
|
+
title: string;
|
|
64
|
+
state: string;
|
|
65
|
+
labels: { nodes: Array<{ title: string }> };
|
|
66
|
+
assignees: { nodes: Array<{ username: string }> };
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
notes: {
|
|
69
|
+
nodes: Array<{
|
|
70
|
+
body: string;
|
|
71
|
+
author: { username: string };
|
|
72
|
+
createdAt: string;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface GraphQLSearchResponse {
|
|
78
|
+
data?: {
|
|
79
|
+
project?: {
|
|
80
|
+
issues?: {
|
|
81
|
+
nodes: GraphQLIssueNode[];
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
errors?: Array<{ message: string }>;
|
|
86
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentTool,
|
|
3
|
+
AgentToolContext,
|
|
4
|
+
AgentToolResult,
|
|
5
|
+
AgentToolUpdateCallback,
|
|
6
|
+
} from "@f5xc-salesdemos/pi-agent-core";
|
|
7
|
+
import { prompt } from "@f5xc-salesdemos/pi-utils";
|
|
8
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
+
import glabIssueListDescription from "../prompts/tools/glab-issue-list.md" with { type: "text" };
|
|
10
|
+
import glabIssueViewDescription from "../prompts/tools/glab-issue-view.md" with { type: "text" };
|
|
11
|
+
import glabSearchDescription from "../prompts/tools/glab-search.md" with { type: "text" };
|
|
12
|
+
import glabSetupDescription from "../prompts/tools/glab-setup.md" with { type: "text" };
|
|
13
|
+
import * as git from "../utils/git";
|
|
14
|
+
import type { ToolSession } from ".";
|
|
15
|
+
import { loadConfig, resolveProject, saveConfig } from "./glab/config";
|
|
16
|
+
import type { GlabExecApi } from "./glab/exec";
|
|
17
|
+
import { checkAuth, checkInstalled, execGlabJson, GlabAuthError } from "./glab/exec";
|
|
18
|
+
import { formatIssueDetail, formatIssueTable } from "./glab/formatters";
|
|
19
|
+
import { executeGraphQL } from "./glab/graphql";
|
|
20
|
+
import type { GlabIssue, GlabProject, GraphQLIssueNode } from "./glab/types";
|
|
21
|
+
|
|
22
|
+
function makeExecApi(cwd: string): GlabExecApi {
|
|
23
|
+
return {
|
|
24
|
+
cwd,
|
|
25
|
+
async exec(command: string, args: string[], options?: { signal?: AbortSignal; cwd?: string }) {
|
|
26
|
+
const child = Bun.spawn([command, ...args], {
|
|
27
|
+
cwd: options?.cwd ?? cwd,
|
|
28
|
+
stdin: "ignore",
|
|
29
|
+
stdout: "pipe",
|
|
30
|
+
stderr: "pipe",
|
|
31
|
+
signal: options?.signal,
|
|
32
|
+
});
|
|
33
|
+
if (!child.stdout || !child.stderr) {
|
|
34
|
+
return { stdout: "", stderr: "Failed to capture output", code: 1, killed: false };
|
|
35
|
+
}
|
|
36
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
37
|
+
new Response(child.stdout).text(),
|
|
38
|
+
new Response(child.stderr).text(),
|
|
39
|
+
child.exited,
|
|
40
|
+
]);
|
|
41
|
+
return {
|
|
42
|
+
stdout: stdout.trim(),
|
|
43
|
+
stderr: stderr.trim(),
|
|
44
|
+
code: exitCode ?? 0,
|
|
45
|
+
killed: child.killed,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Schemas ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const glabSetupSchema = Type.Object({
|
|
54
|
+
action: Type.Union(
|
|
55
|
+
[
|
|
56
|
+
Type.Literal("check"),
|
|
57
|
+
Type.Literal("login"),
|
|
58
|
+
Type.Literal("select_project"),
|
|
59
|
+
Type.Literal("save_project"),
|
|
60
|
+
Type.Literal("status"),
|
|
61
|
+
],
|
|
62
|
+
{ description: "Onboarding action to perform" },
|
|
63
|
+
),
|
|
64
|
+
project: Type.Optional(Type.String({ description: "Project path to persist (used with save_project)" })),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const glabIssueListSchema = Type.Object({
|
|
68
|
+
project: Type.Optional(
|
|
69
|
+
Type.String({ description: "GitLab project path (e.g. group/repo). Defaults to configured project." }),
|
|
70
|
+
),
|
|
71
|
+
state: Type.Optional(
|
|
72
|
+
Type.Union([Type.Literal("opened"), Type.Literal("closed"), Type.Literal("all")], {
|
|
73
|
+
description: "Filter by issue state",
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
labels: Type.Optional(Type.Array(Type.String(), { description: "Filter by labels" })),
|
|
77
|
+
assignee: Type.Optional(Type.String({ description: "Filter by assignee username" })),
|
|
78
|
+
search: Type.Optional(Type.String({ description: "Search text in title and description" })),
|
|
79
|
+
milestone: Type.Optional(Type.String()),
|
|
80
|
+
sort: Type.Optional(
|
|
81
|
+
Type.Union(
|
|
82
|
+
[Type.Literal("created_at"), Type.Literal("updated_at"), Type.Literal("priority"), Type.Literal("due_date")],
|
|
83
|
+
{ description: "Sort field" },
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
order: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")], { description: "Sort direction" })),
|
|
87
|
+
limit: Type.Optional(Type.Number({ default: 30, maximum: 100, description: "Max results" })),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const glabIssueViewSchema = Type.Object({
|
|
91
|
+
issue: Type.Union([Type.Number(), Type.String()], { description: "Issue IID number or full URL" }),
|
|
92
|
+
project: Type.Optional(Type.String()),
|
|
93
|
+
comments: Type.Optional(Type.Boolean({ default: true })),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const glabSearchSchema = Type.Object({
|
|
97
|
+
query: Type.String({ description: "Search text to find across issue titles, descriptions, labels, and comments" }),
|
|
98
|
+
project: Type.Optional(Type.String()),
|
|
99
|
+
state: Type.Optional(
|
|
100
|
+
Type.Union([Type.Literal("opened"), Type.Literal("closed"), Type.Literal("all")], {
|
|
101
|
+
description: "Filter by issue state",
|
|
102
|
+
}),
|
|
103
|
+
),
|
|
104
|
+
labels: Type.Optional(Type.Array(Type.String())),
|
|
105
|
+
limit: Type.Optional(Type.Number({ default: 20, maximum: 100 })),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
type GlabSetupInput = Static<typeof glabSetupSchema>;
|
|
109
|
+
type GlabIssueListInput = Static<typeof glabIssueListSchema>;
|
|
110
|
+
type GlabIssueViewInput = Static<typeof glabIssueViewSchema>;
|
|
111
|
+
type GlabSearchInput = Static<typeof glabSearchSchema>;
|
|
112
|
+
|
|
113
|
+
interface GlabToolDetails {
|
|
114
|
+
items?: GlabIssue[];
|
|
115
|
+
issue?: GlabIssue;
|
|
116
|
+
projects?: GlabProject[];
|
|
117
|
+
total?: number;
|
|
118
|
+
project?: string;
|
|
119
|
+
query?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function textResult(text: string, details?: GlabToolDetails): AgentToolResult<GlabToolDetails> {
|
|
123
|
+
return { content: [{ type: "text", text }], details };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── GlabSetupTool ───────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export class GlabSetupTool implements AgentTool<typeof glabSetupSchema, GlabToolDetails> {
|
|
129
|
+
readonly name = "glab_setup";
|
|
130
|
+
readonly label = "GitLab Setup";
|
|
131
|
+
readonly description = prompt.render(glabSetupDescription);
|
|
132
|
+
readonly parameters = glabSetupSchema;
|
|
133
|
+
|
|
134
|
+
constructor(private readonly session: ToolSession) {}
|
|
135
|
+
|
|
136
|
+
static createIf(session: ToolSession): GlabSetupTool | null {
|
|
137
|
+
if (!git.gitlab.available()) return null;
|
|
138
|
+
return new GlabSetupTool(session);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async execute(
|
|
142
|
+
_toolCallId: string,
|
|
143
|
+
params: GlabSetupInput,
|
|
144
|
+
signal?: AbortSignal,
|
|
145
|
+
_onUpdate?: AgentToolUpdateCallback<GlabToolDetails>,
|
|
146
|
+
_context?: AgentToolContext,
|
|
147
|
+
): Promise<AgentToolResult<GlabToolDetails>> {
|
|
148
|
+
const api = makeExecApi(this.session.cwd);
|
|
149
|
+
|
|
150
|
+
switch (params.action) {
|
|
151
|
+
case "check": {
|
|
152
|
+
const installed = await checkInstalled(api);
|
|
153
|
+
if (!installed) {
|
|
154
|
+
return textResult(
|
|
155
|
+
'glab is not installed.\n\nInstall it with:\n- **macOS**: `brew install glab`\n- **Linux**: Download from https://gitlab.com/gitlab-org/cli/-/releases\n- **Windows**: `winget install gitlab.glab`\n\nAfter installing, call glab_setup with action "login" to authenticate.',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const ver = await api.exec("glab", ["--version"], { signal });
|
|
159
|
+
return textResult(`glab is installed: ${ver.stdout.trim()}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case "status": {
|
|
163
|
+
const authResult = await api.exec("glab", ["auth", "status"], { signal });
|
|
164
|
+
const config = await loadConfig(this.session.cwd);
|
|
165
|
+
const projectInfo = config?.project
|
|
166
|
+
? `\nConfigured project: ${config.project}`
|
|
167
|
+
: "\nNo project configured. Run select_project to choose one.";
|
|
168
|
+
const authStatus = authResult.code === 0 ? authResult.stdout : `Not authenticated: ${authResult.stderr}`;
|
|
169
|
+
return textResult(authStatus + projectInfo);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case "login":
|
|
173
|
+
return textResult(
|
|
174
|
+
"Starting GitLab authentication...\n\nRunning: `glab auth login --hostname gitlab.com --git-protocol https --web`\n\nYour browser will open for you to authorize access. Return here after authorizing.",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
case "select_project": {
|
|
178
|
+
const authenticated = await checkAuth(api);
|
|
179
|
+
if (!authenticated) {
|
|
180
|
+
return textResult('Not authenticated. Run glab_setup with action "login" first.');
|
|
181
|
+
}
|
|
182
|
+
const projects = await execGlabJson<GlabProject[]>(
|
|
183
|
+
api,
|
|
184
|
+
["repo", "list", "--member", "--output", "json", "--per-page", "50"],
|
|
185
|
+
signal,
|
|
186
|
+
);
|
|
187
|
+
if (!projects.length) {
|
|
188
|
+
return textResult("No projects found for your account.");
|
|
189
|
+
}
|
|
190
|
+
const list = projects
|
|
191
|
+
.map((p, i) => `${i + 1}. **${p.name_with_namespace}** — \`${p.path_with_namespace}\``)
|
|
192
|
+
.join("\n");
|
|
193
|
+
return textResult(
|
|
194
|
+
`Found ${projects.length} projects:\n\n${list}\n\nWhich project do you want to use for GitLab issue tracking? Reply with the number or full path.`,
|
|
195
|
+
{ projects },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "save_project": {
|
|
200
|
+
if (!params.project) {
|
|
201
|
+
return textResult("Error: project parameter is required for save_project action.");
|
|
202
|
+
}
|
|
203
|
+
const existing = (await loadConfig(this.session.cwd)) ?? {
|
|
204
|
+
project: "",
|
|
205
|
+
hostname: "gitlab.com",
|
|
206
|
+
defaultState: "opened" as const,
|
|
207
|
+
perPage: 30,
|
|
208
|
+
};
|
|
209
|
+
await saveConfig(this.session.cwd, { ...existing, project: params.project });
|
|
210
|
+
return textResult(`Configuration saved. Default project set to: **${params.project}**`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
return textResult(`Unknown action: ${params.action}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── GlabIssueListTool ──────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export class GlabIssueListTool implements AgentTool<typeof glabIssueListSchema, GlabToolDetails> {
|
|
222
|
+
readonly name = "glab_issue_list";
|
|
223
|
+
readonly label = "GitLab Issues";
|
|
224
|
+
readonly description = prompt.render(glabIssueListDescription);
|
|
225
|
+
readonly parameters = glabIssueListSchema;
|
|
226
|
+
|
|
227
|
+
constructor(private readonly session: ToolSession) {}
|
|
228
|
+
|
|
229
|
+
static createIf(session: ToolSession): GlabIssueListTool | null {
|
|
230
|
+
if (!git.gitlab.available()) return null;
|
|
231
|
+
return new GlabIssueListTool(session);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async execute(
|
|
235
|
+
_toolCallId: string,
|
|
236
|
+
params: GlabIssueListInput,
|
|
237
|
+
signal?: AbortSignal,
|
|
238
|
+
_onUpdate?: AgentToolUpdateCallback<GlabToolDetails>,
|
|
239
|
+
_context?: AgentToolContext,
|
|
240
|
+
): Promise<AgentToolResult<GlabToolDetails>> {
|
|
241
|
+
const api = makeExecApi(this.session.cwd);
|
|
242
|
+
const project = await resolveProject(params.project, this.session.cwd);
|
|
243
|
+
if (!project) {
|
|
244
|
+
return textResult("No GitLab project configured. Run glab_setup to set one up.");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const args = ["issue", "list", "--output", "json", "--repo", project];
|
|
248
|
+
if (params.state === "opened") args.push("--opened");
|
|
249
|
+
else if (params.state === "closed") args.push("--closed");
|
|
250
|
+
else if (params.state === "all") args.push("--all");
|
|
251
|
+
if (params.labels?.length) args.push("--label", params.labels.join(","));
|
|
252
|
+
if (params.assignee) args.push("--assignee", params.assignee);
|
|
253
|
+
if (params.search) args.push("--search", params.search);
|
|
254
|
+
if (params.milestone) args.push("--milestone", params.milestone);
|
|
255
|
+
if (params.sort) args.push("--order", params.sort);
|
|
256
|
+
if (params.order) args.push("--sort", params.order);
|
|
257
|
+
args.push("--per-page", String(Math.min(params.limit ?? 30, 100)));
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const issues = await execGlabJson<GlabIssue[]>(api, args, signal);
|
|
261
|
+
return textResult(formatIssueTable(issues), { items: issues, total: issues.length, project });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
if (err instanceof GlabAuthError) return textResult((err as Error).message);
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── GlabIssueViewTool ──────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
export class GlabIssueViewTool implements AgentTool<typeof glabIssueViewSchema, GlabToolDetails> {
|
|
272
|
+
readonly name = "glab_issue_view";
|
|
273
|
+
readonly label = "GitLab Issue";
|
|
274
|
+
readonly description = prompt.render(glabIssueViewDescription);
|
|
275
|
+
readonly parameters = glabIssueViewSchema;
|
|
276
|
+
|
|
277
|
+
constructor(private readonly session: ToolSession) {}
|
|
278
|
+
|
|
279
|
+
static createIf(session: ToolSession): GlabIssueViewTool | null {
|
|
280
|
+
if (!git.gitlab.available()) return null;
|
|
281
|
+
return new GlabIssueViewTool(session);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async execute(
|
|
285
|
+
_toolCallId: string,
|
|
286
|
+
params: GlabIssueViewInput,
|
|
287
|
+
signal?: AbortSignal,
|
|
288
|
+
_onUpdate?: AgentToolUpdateCallback<GlabToolDetails>,
|
|
289
|
+
_context?: AgentToolContext,
|
|
290
|
+
): Promise<AgentToolResult<GlabToolDetails>> {
|
|
291
|
+
const api = makeExecApi(this.session.cwd);
|
|
292
|
+
const project = await resolveProject(params.project, this.session.cwd);
|
|
293
|
+
if (!project) {
|
|
294
|
+
return textResult("No GitLab project configured. Run glab_setup to set one up.");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const issueId = String(params.issue);
|
|
298
|
+
const args = ["issue", "view", issueId, "--output", "json", "--repo", project];
|
|
299
|
+
if (params.comments !== false) args.push("--comments");
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const issue = await execGlabJson<GlabIssue>(api, args, signal);
|
|
303
|
+
return textResult(formatIssueDetail(issue), { issue, project });
|
|
304
|
+
} catch (err) {
|
|
305
|
+
if (err instanceof GlabAuthError) return textResult((err as Error).message);
|
|
306
|
+
throw err;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── GlabSearchTool ─────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
export class GlabSearchTool implements AgentTool<typeof glabSearchSchema, GlabToolDetails> {
|
|
314
|
+
readonly name = "glab_search";
|
|
315
|
+
readonly label = "GitLab Search";
|
|
316
|
+
readonly description = prompt.render(glabSearchDescription);
|
|
317
|
+
readonly parameters = glabSearchSchema;
|
|
318
|
+
|
|
319
|
+
constructor(private readonly session: ToolSession) {}
|
|
320
|
+
|
|
321
|
+
static createIf(session: ToolSession): GlabSearchTool | null {
|
|
322
|
+
if (!git.gitlab.available()) return null;
|
|
323
|
+
return new GlabSearchTool(session);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async execute(
|
|
327
|
+
_toolCallId: string,
|
|
328
|
+
params: GlabSearchInput,
|
|
329
|
+
signal?: AbortSignal,
|
|
330
|
+
_onUpdate?: AgentToolUpdateCallback<GlabToolDetails>,
|
|
331
|
+
_context?: AgentToolContext,
|
|
332
|
+
): Promise<AgentToolResult<GlabToolDetails>> {
|
|
333
|
+
const api = makeExecApi(this.session.cwd);
|
|
334
|
+
const project = await resolveProject(params.project, this.session.cwd);
|
|
335
|
+
if (!project) {
|
|
336
|
+
return textResult("No GitLab project configured. Run glab_setup to set one up.");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const limit = Math.min(params.limit ?? 20, 100);
|
|
340
|
+
let issues: GlabIssue[] = [];
|
|
341
|
+
|
|
342
|
+
const restArgs = [
|
|
343
|
+
"issue",
|
|
344
|
+
"list",
|
|
345
|
+
"--output",
|
|
346
|
+
"json",
|
|
347
|
+
"--repo",
|
|
348
|
+
project,
|
|
349
|
+
"--search",
|
|
350
|
+
params.query,
|
|
351
|
+
"--per-page",
|
|
352
|
+
String(limit),
|
|
353
|
+
];
|
|
354
|
+
if (params.state === "opened") restArgs.push("--opened");
|
|
355
|
+
else if (params.state === "closed") restArgs.push("--closed");
|
|
356
|
+
else if (params.state === "all") restArgs.push("--all");
|
|
357
|
+
if (params.labels?.length) restArgs.push("--label", params.labels.join(","));
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
issues = await execGlabJson<GlabIssue[]>(api, restArgs, signal);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
if (err instanceof GlabAuthError) return textResult((err as Error).message);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let graphqlNodes: GraphQLIssueNode[] = [];
|
|
366
|
+
try {
|
|
367
|
+
graphqlNodes = await executeGraphQL(api, project, params.query, limit, signal, params.state);
|
|
368
|
+
} catch {
|
|
369
|
+
// GraphQL unavailable — use REST results only
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (graphqlNodes.length > 0) {
|
|
373
|
+
const seenIids = new Set(issues.map(i => i.iid));
|
|
374
|
+
for (const node of graphqlNodes) {
|
|
375
|
+
const iid = parseInt(node.iid, 10);
|
|
376
|
+
if (seenIids.has(iid)) continue;
|
|
377
|
+
seenIids.add(iid);
|
|
378
|
+
const lowerQuery = params.query.toLowerCase();
|
|
379
|
+
const inTitle = node.title.toLowerCase().includes(lowerQuery);
|
|
380
|
+
const inComments = node.notes.nodes.some(n => n.body.toLowerCase().includes(lowerQuery));
|
|
381
|
+
if (inTitle || inComments) {
|
|
382
|
+
issues.push({
|
|
383
|
+
id: iid,
|
|
384
|
+
iid,
|
|
385
|
+
title: node.title,
|
|
386
|
+
description: "",
|
|
387
|
+
state: node.state === "OPEN" ? "opened" : "closed",
|
|
388
|
+
labels: node.labels.nodes.map(l => l.title),
|
|
389
|
+
assignees: node.assignees.nodes.map(a => ({ username: a.username, name: a.username })),
|
|
390
|
+
author: { username: "", name: "" },
|
|
391
|
+
milestone: null,
|
|
392
|
+
created_at: node.updatedAt,
|
|
393
|
+
updated_at: node.updatedAt,
|
|
394
|
+
web_url: `https://gitlab.com/${project}/-/issues/${iid}`,
|
|
395
|
+
references: { full: `${project}#${iid}` },
|
|
396
|
+
issue_type: "issue",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return textResult(formatIssueTable(issues), {
|
|
403
|
+
items: issues,
|
|
404
|
+
total: issues.length,
|
|
405
|
+
project,
|
|
406
|
+
query: params.query,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
GhSearchIssuesTool,
|
|
41
41
|
GhSearchPrsTool,
|
|
42
42
|
} from "./gh";
|
|
43
|
+
import { GlabIssueListTool, GlabIssueViewTool, GlabSearchTool, GlabSetupTool } from "./glab";
|
|
43
44
|
import { GrepTool } from "./grep";
|
|
44
45
|
import { InspectImageTool } from "./inspect-image";
|
|
45
46
|
import { NotebookTool } from "./notebook";
|
|
@@ -80,6 +81,7 @@ export * from "./exit-plan-mode";
|
|
|
80
81
|
export * from "./find";
|
|
81
82
|
export * from "./gemini-image";
|
|
82
83
|
export * from "./gh";
|
|
84
|
+
export * from "./glab";
|
|
83
85
|
export * from "./grep";
|
|
84
86
|
export * from "./inspect-image";
|
|
85
87
|
export * from "./notebook";
|
|
@@ -229,6 +231,10 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
229
231
|
gh_run_watch: GhRunWatchTool.createIf,
|
|
230
232
|
gh_search_issues: GhSearchIssuesTool.createIf,
|
|
231
233
|
gh_search_prs: GhSearchPrsTool.createIf,
|
|
234
|
+
glab_setup: GlabSetupTool.createIf,
|
|
235
|
+
glab_issue_list: GlabIssueListTool.createIf,
|
|
236
|
+
glab_issue_view: GlabIssueViewTool.createIf,
|
|
237
|
+
glab_search: GlabSearchTool.createIf,
|
|
232
238
|
find: s => new FindTool(s),
|
|
233
239
|
grep: s => new GrepTool(s),
|
|
234
240
|
lsp: LspTool.createIf,
|
|
@@ -398,6 +404,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
398
404
|
if (name === "find") return session.settings.get("find.enabled");
|
|
399
405
|
if (name === "grep") return session.settings.get("grep.enabled");
|
|
400
406
|
if (name.startsWith("gh_")) return session.settings.get("github.enabled");
|
|
407
|
+
if (name.startsWith("glab_")) return session.settings.get("gitlab.enabled");
|
|
401
408
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
402
409
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
403
410
|
if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
|
package/src/utils/git.ts
CHANGED
|
@@ -1421,3 +1421,13 @@ export const github = {
|
|
|
1421
1421
|
return result.stdout;
|
|
1422
1422
|
},
|
|
1423
1423
|
};
|
|
1424
|
+
|
|
1425
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1426
|
+
// API: gitlab (GitLab CLI — glab)
|
|
1427
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1428
|
+
|
|
1429
|
+
export const gitlab = {
|
|
1430
|
+
available(): boolean {
|
|
1431
|
+
return Boolean($which("glab"));
|
|
1432
|
+
},
|
|
1433
|
+
};
|