duoops 0.1.7 → 0.1.9
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/commands/ask.d.ts +3 -0
- package/dist/commands/ask.js +9 -3
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +47 -0
- package/dist/commands/portal.js +7 -5
- package/dist/lib/ai/agent.d.ts +6 -2
- package/dist/lib/ai/agent.js +50 -57
- package/dist/lib/ai/tools/gitlab.d.ts +4 -0
- package/dist/lib/ai/tools/gitlab.js +162 -11
- package/dist/lib/ai/tools/measure.js +7 -3
- package/dist/lib/ai/tools/types.d.ts +3 -0
- package/dist/lib/ai/tools/types.js +1 -0
- package/dist/portal/assets/MetricsDashboard-Bnj-jtu6.js +27 -0
- package/dist/portal/assets/index-B1SGDQNX.css +1 -0
- package/dist/portal/assets/index-Bk8OVV7a.js +106 -0
- package/dist/portal/assets/{index-B6bzT1Vv.js → index-C54ZhVUo.js} +1 -1
- package/dist/portal/index.html +2 -2
- package/oclif.manifest.json +46 -2
- package/package.json +1 -1
- package/dist/portal/assets/MetricsDashboard-DIsoz4Sl.js +0 -71
- package/dist/portal/assets/index-BP8FwWqA.css +0 -1
- package/dist/portal/assets/index-DkVG3jel.js +0 -70
package/dist/commands/ask.d.ts
CHANGED
|
@@ -4,5 +4,8 @@ export default class Ask extends Command {
|
|
|
4
4
|
question: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
5
|
};
|
|
6
6
|
static description: string;
|
|
7
|
+
static flags: {
|
|
8
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
7
10
|
run(): Promise<void>;
|
|
8
11
|
}
|
package/dist/commands/ask.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { search } from '@inquirer/prompts';
|
|
2
|
-
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
3
3
|
import { runAgent } from '../lib/ai/agent.js';
|
|
4
4
|
import { PROMPT_LIBRARY } from '../lib/ai/prompt-library.js';
|
|
5
5
|
export default class Ask extends Command {
|
|
@@ -10,8 +10,14 @@ export default class Ask extends Command {
|
|
|
10
10
|
}),
|
|
11
11
|
};
|
|
12
12
|
static description = 'Ask questions about your CI/CD pipelines, logs, and sustainability';
|
|
13
|
+
static flags = {
|
|
14
|
+
project: Flags.string({
|
|
15
|
+
char: 'P',
|
|
16
|
+
description: 'GitLab Project ID to contextually use',
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
13
19
|
async run() {
|
|
14
|
-
const { args } = await this.parse(Ask);
|
|
20
|
+
const { args, flags } = await this.parse(Ask);
|
|
15
21
|
let { question } = args;
|
|
16
22
|
if (!question) {
|
|
17
23
|
question = await search({
|
|
@@ -37,7 +43,7 @@ export default class Ask extends Command {
|
|
|
37
43
|
}
|
|
38
44
|
this.log('Thinking...');
|
|
39
45
|
try {
|
|
40
|
-
const response = await runAgent(question);
|
|
46
|
+
const response = await runAgent(question, { projectId: flags.project });
|
|
41
47
|
this.log('\n' + response);
|
|
42
48
|
}
|
|
43
49
|
catch (error) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Config extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { bold, green } from 'kleur/colors';
|
|
3
|
+
import { configManager } from '../lib/config.js';
|
|
4
|
+
export default class Config extends Command {
|
|
5
|
+
static args = {
|
|
6
|
+
key: Args.string({
|
|
7
|
+
description: 'Config key (e.g., gitlabUrl, defaultProjectId)',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
value: Args.string({
|
|
11
|
+
description: 'Config value',
|
|
12
|
+
required: true,
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
static description = 'Set a configuration value directly';
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> <%= command.id %> gitlabUrl https://gitlab.com',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> defaultProjectId 123456',
|
|
19
|
+
];
|
|
20
|
+
async run() {
|
|
21
|
+
const { args } = await this.parse(Config);
|
|
22
|
+
const { key, value } = args;
|
|
23
|
+
const currentConfig = configManager.get();
|
|
24
|
+
// Basic nested update support for measure.*
|
|
25
|
+
if (key.startsWith('measure.')) {
|
|
26
|
+
const subKey = key.split('.')[1];
|
|
27
|
+
const measure = currentConfig.measure || {
|
|
28
|
+
bigqueryDataset: '',
|
|
29
|
+
bigqueryTable: '',
|
|
30
|
+
googleProjectId: ''
|
|
31
|
+
};
|
|
32
|
+
// @ts-expect-error - dynamic assignment
|
|
33
|
+
measure[subKey] = value;
|
|
34
|
+
configManager.set({
|
|
35
|
+
...currentConfig,
|
|
36
|
+
measure
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
configManager.set({
|
|
41
|
+
...currentConfig,
|
|
42
|
+
[key]: value,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
this.log(green(`Updated ${bold(key)} to ${bold(value)}`));
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/commands/portal.js
CHANGED
|
@@ -28,13 +28,13 @@ export default class Portal extends Command {
|
|
|
28
28
|
/* eslint-disable max-depth -- stream consumer adds necessary nesting */
|
|
29
29
|
app.post('/api/chat', async (req, res) => {
|
|
30
30
|
try {
|
|
31
|
-
const { messages } = req.body;
|
|
31
|
+
const { messages, projectId } = req.body;
|
|
32
32
|
if (!messages || !Array.isArray(messages)) {
|
|
33
33
|
res.status(400).json({ error: 'Messages array is required' });
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
const modelMessages = await convertToModelMessages(messages);
|
|
37
|
-
const result = streamAgent(modelMessages);
|
|
37
|
+
const result = streamAgent(modelMessages, { projectId });
|
|
38
38
|
const response = result.toUIMessageStreamResponse();
|
|
39
39
|
res.status(response.status);
|
|
40
40
|
for (const [k, v] of response.headers.entries())
|
|
@@ -79,12 +79,14 @@ export default class Portal extends Command {
|
|
|
79
79
|
});
|
|
80
80
|
app.get('/api/metrics', async (req, res) => {
|
|
81
81
|
try {
|
|
82
|
+
const config = configManager.get();
|
|
82
83
|
const { projectId } = req.query;
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const targetProjectId = projectId ? String(projectId) : config.defaultProjectId;
|
|
85
|
+
if (!targetProjectId) {
|
|
86
|
+
res.status(400).json({ error: 'Project ID is required and no default is configured' });
|
|
85
87
|
return;
|
|
86
88
|
}
|
|
87
|
-
const metrics = await fetchCarbonMetrics(
|
|
89
|
+
const metrics = await fetchCarbonMetrics(targetProjectId, 50);
|
|
88
90
|
res.json(metrics);
|
|
89
91
|
}
|
|
90
92
|
catch (error) {
|
package/dist/lib/ai/agent.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ModelMessage } from 'ai';
|
|
2
|
-
export declare const runAgent: (input: ModelMessage[] | string
|
|
2
|
+
export declare const runAgent: (input: ModelMessage[] | string, context?: {
|
|
3
|
+
projectId?: string;
|
|
4
|
+
}) => Promise<string>;
|
|
3
5
|
/** Same as runAgent but returns a streamText result for UI streaming (tool steps, etc.). */
|
|
4
|
-
export declare function streamAgent(messages: ModelMessage[]
|
|
6
|
+
export declare function streamAgent(messages: ModelMessage[], context?: {
|
|
7
|
+
projectId?: string;
|
|
8
|
+
}): {
|
|
5
9
|
toUIMessageStreamResponse(): Response;
|
|
6
10
|
};
|
package/dist/lib/ai/agent.js
CHANGED
|
@@ -6,6 +6,48 @@ import { editingTools } from './tools/editing.js';
|
|
|
6
6
|
import { fileTools } from './tools/filesystem.js';
|
|
7
7
|
import { gitlabTools } from './tools/gitlab.js';
|
|
8
8
|
import { measureTools } from './tools/measure.js';
|
|
9
|
+
const baseTools = {
|
|
10
|
+
...editingTools,
|
|
11
|
+
...fileTools,
|
|
12
|
+
...gitlabTools,
|
|
13
|
+
...measureTools,
|
|
14
|
+
};
|
|
15
|
+
function createSystemPrompt(hasProjectId) {
|
|
16
|
+
const projectLine = hasProjectId
|
|
17
|
+
? 'The portal already selected the GitLab project for you. All GitLab tools automatically target that project. Never attempt to guess, request, or change the project ID.'
|
|
18
|
+
: 'If a GitLab project has not been configured yet, ask the user to select one before running GitLab tools.';
|
|
19
|
+
return `You are a DevOps assistant.
|
|
20
|
+
${projectLine}
|
|
21
|
+
Use the available tools to answer questions about pipelines, jobs, and sustainability.`;
|
|
22
|
+
}
|
|
23
|
+
function createTools(projectId) {
|
|
24
|
+
return Object.fromEntries(Object.entries(baseTools).map(([name, t]) => [
|
|
25
|
+
name,
|
|
26
|
+
{
|
|
27
|
+
...t,
|
|
28
|
+
async execute(args, options) {
|
|
29
|
+
const toolOptions = {
|
|
30
|
+
...(typeof options === 'object' && options !== null ? options : {}),
|
|
31
|
+
projectId,
|
|
32
|
+
};
|
|
33
|
+
const logArgs = typeof args === 'object' && args !== null
|
|
34
|
+
? Object.fromEntries(Object.entries(args).filter(([key]) => key !== 'projectId' && key !== 'project_id'))
|
|
35
|
+
: args;
|
|
36
|
+
logger.info({ args: logArgs, tool: name }, 'tool call');
|
|
37
|
+
try {
|
|
38
|
+
const toolInstance = t;
|
|
39
|
+
const result = await toolInstance.execute(args, toolOptions);
|
|
40
|
+
logger.info({ result, tool: name }, 'tool result');
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.warn({ error: String(error), tool: name }, 'tool error');
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
]));
|
|
50
|
+
}
|
|
9
51
|
/** Ensure no message has empty text content (Anthropic rejects "text content blocks must be non-empty"). */
|
|
10
52
|
function sanitizeMessages(messages) {
|
|
11
53
|
return messages.map((m) => {
|
|
@@ -38,37 +80,12 @@ function sanitizeMessages(messages) {
|
|
|
38
80
|
return m;
|
|
39
81
|
});
|
|
40
82
|
}
|
|
41
|
-
export const runAgent = async (input) => {
|
|
83
|
+
export const runAgent = async (input, context) => {
|
|
42
84
|
const model = createModel();
|
|
43
85
|
const config = configManager.get();
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const rawTools = {
|
|
48
|
-
...editingTools,
|
|
49
|
-
...fileTools,
|
|
50
|
-
...gitlabTools,
|
|
51
|
-
...measureTools,
|
|
52
|
-
};
|
|
53
|
-
/** Wrap each tool's execute to log invocations and results (for debugging tool round-trips). */
|
|
54
|
-
const tools = Object.fromEntries(Object.entries(rawTools).map(([name, t]) => [
|
|
55
|
-
name,
|
|
56
|
-
{
|
|
57
|
-
...t,
|
|
58
|
-
async execute(args, options) {
|
|
59
|
-
logger.info({ args, tool: name }, 'tool call');
|
|
60
|
-
try {
|
|
61
|
-
const result = await t.execute(args, options);
|
|
62
|
-
logger.info({ result, tool: name }, 'tool result');
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
logger.warn({ error: String(error), tool: name }, 'tool error');
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
]));
|
|
86
|
+
const projectId = context?.projectId || config.defaultProjectId;
|
|
87
|
+
const system = createSystemPrompt(Boolean(projectId));
|
|
88
|
+
const tools = createTools(projectId);
|
|
72
89
|
const rawMessages = Array.isArray(input)
|
|
73
90
|
? input
|
|
74
91
|
: [{ content: input.trim() || ' ', role: 'user' }];
|
|
@@ -91,36 +108,12 @@ Use the available tools to answer questions about pipelines, jobs, and sustainab
|
|
|
91
108
|
return result.text ?? '';
|
|
92
109
|
};
|
|
93
110
|
/** Same as runAgent but returns a streamText result for UI streaming (tool steps, etc.). */
|
|
94
|
-
export function streamAgent(messages) {
|
|
111
|
+
export function streamAgent(messages, context) {
|
|
95
112
|
const model = createModel();
|
|
96
113
|
const config = configManager.get();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const rawTools = {
|
|
101
|
-
...editingTools,
|
|
102
|
-
...fileTools,
|
|
103
|
-
...gitlabTools,
|
|
104
|
-
...measureTools,
|
|
105
|
-
};
|
|
106
|
-
const tools = Object.fromEntries(Object.entries(rawTools).map(([name, t]) => [
|
|
107
|
-
name,
|
|
108
|
-
{
|
|
109
|
-
...t,
|
|
110
|
-
async execute(args, options) {
|
|
111
|
-
logger.info({ args, tool: name }, 'tool call');
|
|
112
|
-
try {
|
|
113
|
-
const result = await t.execute(args, options);
|
|
114
|
-
logger.info({ result, tool: name }, 'tool result');
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
logger.warn({ error: String(error), tool: name }, 'tool error');
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
]));
|
|
114
|
+
const projectId = context?.projectId || config.defaultProjectId;
|
|
115
|
+
const system = createSystemPrompt(Boolean(projectId));
|
|
116
|
+
const tools = createTools(projectId);
|
|
124
117
|
const sanitized = sanitizeMessages(messages);
|
|
125
118
|
if (sanitized.length === 0) {
|
|
126
119
|
throw new Error('Please send a non-empty message.');
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare const gitlabTools: {
|
|
2
2
|
get_job_logs: import("ai").Tool<unknown, unknown>;
|
|
3
|
+
get_pipeline_details: import("ai").Tool<unknown, unknown>;
|
|
4
|
+
get_project: import("ai").Tool<unknown, unknown>;
|
|
5
|
+
list_jobs: import("ai").Tool<unknown, unknown>;
|
|
3
6
|
list_pipelines: import("ai").Tool<unknown, unknown>;
|
|
7
|
+
search_projects: import("ai").Tool<unknown, unknown>;
|
|
4
8
|
};
|
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
import { generateText, tool } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { configManager } from '../../config.js';
|
|
3
4
|
import { createGitlabClient } from '../../gitlab/client.js';
|
|
4
5
|
import { createModel } from '../model.js';
|
|
5
6
|
/* eslint-disable camelcase */
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
8
|
const getJobLogsSchema = z.object({
|
|
7
9
|
jobId: z.number().describe('GitLab Job ID'),
|
|
8
|
-
projectId: z.string().describe('GitLab Project ID'),
|
|
9
10
|
});
|
|
10
11
|
const listPipelinesSchema = z.object({
|
|
11
12
|
limit: z.number().default(5).optional(),
|
|
12
|
-
|
|
13
|
+
status: z.string().optional().describe('Filter by status (e.g. success, failed, running)'),
|
|
14
|
+
});
|
|
15
|
+
const getPipelineDetailsSchema = z.object({
|
|
16
|
+
pipelineId: z.number().describe('GitLab Pipeline ID'),
|
|
17
|
+
});
|
|
18
|
+
const listJobsSchema = z.object({
|
|
19
|
+
limit: z.number().default(20).optional(),
|
|
20
|
+
pipelineId: z.number().optional().describe('Filter by Pipeline ID'),
|
|
21
|
+
scope: z.enum(['created', 'pending', 'running', 'failed', 'success', 'canceled', 'skipped', 'manual']).optional(),
|
|
22
|
+
});
|
|
23
|
+
const searchProjectsSchema = z.object({
|
|
24
|
+
query: z.string().describe('Search query (project name or path)'),
|
|
25
|
+
});
|
|
26
|
+
const getProjectSchema = z.object({
|
|
27
|
+
projectPathOrId: z.string().optional().describe('GitLab Project Path (group/project) or ID'),
|
|
13
28
|
});
|
|
14
29
|
export const gitlabTools = {
|
|
15
30
|
get_job_logs: tool({
|
|
16
31
|
description: 'Get the logs of a specific job to debug failures. If logs are long, it uses a subagent to summarize them.',
|
|
17
|
-
|
|
18
|
-
async execute(args) {
|
|
32
|
+
async execute(args, options) {
|
|
19
33
|
try {
|
|
34
|
+
const projectId = options?.projectId || configManager.get().defaultProjectId;
|
|
35
|
+
const jobId = args.jobId || args.job_id;
|
|
36
|
+
if (!projectId)
|
|
37
|
+
throw new Error('Project ID is required (argument or default config)');
|
|
38
|
+
if (!jobId)
|
|
39
|
+
throw new Error('Job ID is required');
|
|
20
40
|
const client = createGitlabClient();
|
|
21
|
-
|
|
22
|
-
const trace = await client.Jobs.showTrace(args.projectId, args.jobId);
|
|
41
|
+
const trace = await client.Jobs.showLog(projectId, jobId);
|
|
23
42
|
const logText = typeof trace === 'string' ? trace : JSON.stringify(trace);
|
|
24
43
|
// If logs are short, return directly
|
|
25
44
|
if (logText.length < 5000) {
|
|
@@ -51,15 +70,113 @@ ${content}`
|
|
|
51
70
|
}
|
|
52
71
|
},
|
|
53
72
|
parameters: getJobLogsSchema,
|
|
54
|
-
|
|
73
|
+
}),
|
|
74
|
+
get_pipeline_details: tool({
|
|
75
|
+
description: 'Get detailed information about a specific pipeline, including its jobs and status.',
|
|
76
|
+
async execute(args, options) {
|
|
77
|
+
try {
|
|
78
|
+
const projectId = options?.projectId || configManager.get().defaultProjectId;
|
|
79
|
+
const pipelineId = args.pipelineId || args.pipeline_id;
|
|
80
|
+
if (!projectId)
|
|
81
|
+
throw new Error('Project ID is required (argument or default config)');
|
|
82
|
+
if (!pipelineId)
|
|
83
|
+
throw new Error('Pipeline ID is required');
|
|
84
|
+
const client = createGitlabClient();
|
|
85
|
+
const pipeline = await client.Pipelines.show(projectId, pipelineId);
|
|
86
|
+
const jobs = await client.Pipelines.showJobs(projectId, pipelineId);
|
|
87
|
+
return {
|
|
88
|
+
jobs: jobs.map((j) => ({
|
|
89
|
+
id: j.id,
|
|
90
|
+
name: j.name,
|
|
91
|
+
stage: j.stage,
|
|
92
|
+
status: j.status,
|
|
93
|
+
})),
|
|
94
|
+
pipeline: {
|
|
95
|
+
created_at: pipeline.created_at,
|
|
96
|
+
duration: pipeline.duration,
|
|
97
|
+
id: pipeline.id,
|
|
98
|
+
ref: pipeline.ref,
|
|
99
|
+
status: pipeline.status,
|
|
100
|
+
web_url: pipeline.web_url,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return { error: String(error) };
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
parameters: getPipelineDetailsSchema,
|
|
109
|
+
}),
|
|
110
|
+
get_project: tool({
|
|
111
|
+
description: 'Get details of a specific project by its path (e.g. "group/project") or ID to find its numeric ID.',
|
|
112
|
+
async execute(args, options) {
|
|
113
|
+
try {
|
|
114
|
+
const projectPathOrId = options?.projectId || args.projectPathOrId || configManager.get().defaultProjectId;
|
|
115
|
+
if (!projectPathOrId)
|
|
116
|
+
throw new Error('Project Path or ID is required (argument or default config)');
|
|
117
|
+
const client = createGitlabClient();
|
|
118
|
+
const project = await client.Projects.show(projectPathOrId);
|
|
119
|
+
return {
|
|
120
|
+
project: {
|
|
121
|
+
description: project.description,
|
|
122
|
+
id: project.id,
|
|
123
|
+
name: project.name,
|
|
124
|
+
path_with_namespace: project.path_with_namespace,
|
|
125
|
+
web_url: project.web_url,
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return { error: `Failed to find project "${args.projectPathOrId || options?.projectId || 'unknown'}": ${String(error)}` };
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
parameters: getProjectSchema,
|
|
134
|
+
}),
|
|
135
|
+
list_jobs: tool({
|
|
136
|
+
description: 'List jobs for a project, optionally filtered by pipeline or status scope.',
|
|
137
|
+
async execute(args, options) {
|
|
138
|
+
try {
|
|
139
|
+
const projectId = options?.projectId || configManager.get().defaultProjectId;
|
|
140
|
+
const pipelineId = args.pipelineId || args.pipeline_id;
|
|
141
|
+
if (!projectId)
|
|
142
|
+
throw new Error('Project ID is required (argument or default config)');
|
|
143
|
+
const client = createGitlabClient();
|
|
144
|
+
const queryOptions = { perPage: args.limit };
|
|
145
|
+
if (args.scope)
|
|
146
|
+
queryOptions.scope = args.scope;
|
|
147
|
+
const jobs = pipelineId
|
|
148
|
+
? await client.Pipelines.showJobs(projectId, pipelineId, queryOptions)
|
|
149
|
+
: await client.Jobs.all(projectId, queryOptions);
|
|
150
|
+
return {
|
|
151
|
+
jobs: jobs.map((j) => ({
|
|
152
|
+
created_at: j.created_at,
|
|
153
|
+
id: j.id,
|
|
154
|
+
name: j.name,
|
|
155
|
+
pipeline: { id: j.pipeline.id },
|
|
156
|
+
stage: j.stage,
|
|
157
|
+
status: j.status,
|
|
158
|
+
web_url: j.web_url,
|
|
159
|
+
})),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return { error: String(error) };
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
parameters: listJobsSchema,
|
|
55
167
|
}),
|
|
56
168
|
list_pipelines: tool({
|
|
57
169
|
description: 'List recent pipelines for a project to check status',
|
|
58
|
-
|
|
59
|
-
async execute(args) {
|
|
170
|
+
async execute(args, options) {
|
|
60
171
|
try {
|
|
172
|
+
const projectId = options?.projectId || configManager.get().defaultProjectId;
|
|
173
|
+
if (!projectId)
|
|
174
|
+
throw new Error('Project ID is required (argument or default config)');
|
|
61
175
|
const client = createGitlabClient();
|
|
62
|
-
const
|
|
176
|
+
const queryOptions = { perPage: args.limit };
|
|
177
|
+
if (args.status)
|
|
178
|
+
queryOptions.status = args.status;
|
|
179
|
+
const pipelines = await client.Pipelines.all(projectId, queryOptions);
|
|
63
180
|
return {
|
|
64
181
|
pipelines: pipelines.map((p) => ({
|
|
65
182
|
created_at: p.created_at,
|
|
@@ -75,7 +192,41 @@ ${content}`
|
|
|
75
192
|
}
|
|
76
193
|
},
|
|
77
194
|
parameters: listPipelinesSchema,
|
|
78
|
-
|
|
195
|
+
}),
|
|
196
|
+
search_projects: tool({
|
|
197
|
+
description: 'Search for a GitLab project by name or path to find its ID.',
|
|
198
|
+
async execute(args) {
|
|
199
|
+
try {
|
|
200
|
+
const client = createGitlabClient();
|
|
201
|
+
// search() is good for name but less precise than path.
|
|
202
|
+
// It's often useful for discovering projects.
|
|
203
|
+
// In some GitBeaker versions, this might be Projects.search(query) or Projects.all({search: query})
|
|
204
|
+
// We will try Projects.search(query) first, fallback to all({search}) if fails
|
|
205
|
+
let projects;
|
|
206
|
+
try {
|
|
207
|
+
projects = await client.Projects.search(args.query);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
projects = await client.Projects.all({ search: args.query });
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(projects)) {
|
|
213
|
+
projects = [projects];
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
projects: projects.map((p) => ({
|
|
217
|
+
description: p.description,
|
|
218
|
+
id: p.id,
|
|
219
|
+
name: p.name,
|
|
220
|
+
path_with_namespace: p.path_with_namespace,
|
|
221
|
+
web_url: p.web_url,
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return { error: String(error) };
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
parameters: searchProjectsSchema,
|
|
79
230
|
}),
|
|
80
231
|
};
|
|
81
232
|
/* eslint-enable camelcase */
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { configManager } from '../../config.js';
|
|
3
4
|
import { fetchCarbonMetrics } from '../../measure/bigquery-service.js';
|
|
4
5
|
/* eslint-disable camelcase */
|
|
5
6
|
const getCarbonMetricsSchema = z.object({
|
|
6
7
|
limit: z.number().default(10).optional(),
|
|
7
|
-
projectId: z.string().describe('GitLab Project ID (numeric)'),
|
|
8
8
|
});
|
|
9
9
|
export const measureTools = {
|
|
10
10
|
get_carbon_metrics: tool({
|
|
11
11
|
description: 'Query carbon emissions for a project from BigQuery',
|
|
12
12
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
async execute(args) {
|
|
13
|
+
async execute(args, options) {
|
|
14
14
|
try {
|
|
15
|
-
const
|
|
15
|
+
const projectId = options?.projectId || configManager.get().defaultProjectId;
|
|
16
|
+
if (!projectId) {
|
|
17
|
+
return { error: 'Project ID is required. Please select a project in the portal or configure a default project.' };
|
|
18
|
+
}
|
|
19
|
+
const metrics = await fetchCarbonMetrics(projectId, args.limit);
|
|
16
20
|
return { metrics };
|
|
17
21
|
}
|
|
18
22
|
catch (error) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|