@debugg-ai/debugg-ai-mcp 1.0.54 → 1.0.56

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.
Files changed (44) hide show
  1. package/README.md +57 -6
  2. package/dist/handlers/cancelExecutionHandler.js +41 -0
  3. package/dist/handlers/createCredentialHandler.js +60 -0
  4. package/dist/handlers/createEnvironmentHandler.js +54 -0
  5. package/dist/handlers/deleteCredentialHandler.js +51 -0
  6. package/dist/handlers/deleteEnvironmentHandler.js +51 -0
  7. package/dist/handlers/deleteProjectHandler.js +39 -0
  8. package/dist/handlers/getCredentialHandler.js +49 -0
  9. package/dist/handlers/getEnvironmentHandler.js +49 -0
  10. package/dist/handlers/getExecutionHandler.js +37 -0
  11. package/dist/handlers/getProjectHandler.js +37 -0
  12. package/dist/handlers/index.js +17 -0
  13. package/dist/handlers/listCredentialsHandler.js +71 -0
  14. package/dist/handlers/listEnvironmentsHandler.js +59 -0
  15. package/dist/handlers/listExecutionsHandler.js +31 -0
  16. package/dist/handlers/listProjectsHandler.js +30 -0
  17. package/dist/handlers/testPageChangesHandler.js +4 -1
  18. package/dist/handlers/updateCredentialHandler.js +70 -0
  19. package/dist/handlers/updateEnvironmentHandler.js +59 -0
  20. package/dist/handlers/updateProjectHandler.js +45 -0
  21. package/dist/index.js +2 -19
  22. package/dist/services/index.js +243 -0
  23. package/dist/services/projectContext.js +42 -26
  24. package/dist/services/workflows.js +26 -0
  25. package/dist/tools/cancelExecution.js +22 -0
  26. package/dist/tools/createCredential.js +52 -0
  27. package/dist/tools/createEnvironment.js +42 -0
  28. package/dist/tools/deleteCredential.js +24 -0
  29. package/dist/tools/deleteEnvironment.js +23 -0
  30. package/dist/tools/deleteProject.js +22 -0
  31. package/dist/tools/getCredential.js +24 -0
  32. package/dist/tools/getEnvironment.js +23 -0
  33. package/dist/tools/getExecution.js +22 -0
  34. package/dist/tools/getProject.js +22 -0
  35. package/dist/tools/index.js +61 -5
  36. package/dist/tools/listCredentials.js +40 -0
  37. package/dist/tools/listEnvironments.js +32 -0
  38. package/dist/tools/listExecutions.js +22 -0
  39. package/dist/tools/listProjects.js +28 -0
  40. package/dist/tools/updateCredential.js +28 -0
  41. package/dist/tools/updateEnvironment.js +26 -0
  42. package/dist/tools/updateProject.js +24 -0
  43. package/dist/types/index.js +82 -0
  44. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { detectRepoName } from '../utils/gitContext.js';
6
+ const logger = new Logger({ module: 'listEnvironmentsHandler' });
7
+ export async function listEnvironmentsHandler(input, _context) {
8
+ const start = Date.now();
9
+ logger.toolStart('list_environments', { projectUuid: input.projectUuid, q: input.q });
10
+ try {
11
+ const client = new DebuggAIServerClient(config.api.key);
12
+ await client.init();
13
+ let projectUuid = input.projectUuid;
14
+ let projectName = null;
15
+ let projectRepoName = null;
16
+ if (!projectUuid) {
17
+ const repoName = detectRepoName();
18
+ if (!repoName) {
19
+ const payload = {
20
+ error: 'NoProjectResolved',
21
+ message: 'No git repo detected and no projectUuid provided. Pass projectUuid (get it from list_projects) or invoke from a directory with a git origin.',
22
+ environments: [],
23
+ };
24
+ logger.toolComplete('list_environments', Date.now() - start);
25
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
26
+ }
27
+ const project = await client.findProjectByRepoName(repoName);
28
+ if (!project) {
29
+ const payload = {
30
+ error: 'NoProjectResolved',
31
+ message: `No DebuggAI project found for repo "${repoName}". Pass projectUuid explicitly or call list_projects to discover.`,
32
+ environments: [],
33
+ };
34
+ logger.toolComplete('list_environments', Date.now() - start);
35
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
36
+ }
37
+ projectUuid = project.uuid;
38
+ projectName = project.name;
39
+ projectRepoName = project.repo?.name ?? repoName;
40
+ }
41
+ const environments = await client.listEnvironmentsForProject(projectUuid, input.q);
42
+ const payload = {
43
+ project: {
44
+ uuid: projectUuid,
45
+ name: projectName,
46
+ repoName: projectRepoName,
47
+ },
48
+ query: input.q ?? null,
49
+ count: environments.length,
50
+ environments,
51
+ };
52
+ logger.toolComplete('list_environments', Date.now() - start);
53
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
54
+ }
55
+ catch (error) {
56
+ logger.toolError('list_environments', error, Date.now() - start);
57
+ throw handleExternalServiceError(error, 'DebuggAI', 'list_environments');
58
+ }
59
+ }
@@ -0,0 +1,31 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ const logger = new Logger({ module: 'listExecutionsHandler' });
6
+ export async function listExecutionsHandler(input, _context) {
7
+ const start = Date.now();
8
+ logger.toolStart('list_executions', { status: input.status, limit: input.limit });
9
+ try {
10
+ const client = new DebuggAIServerClient(config.api.key);
11
+ await client.init();
12
+ const { count, executions } = await client.workflows.listExecutions({
13
+ status: input.status,
14
+ limit: input.limit,
15
+ });
16
+ const payload = {
17
+ filter: {
18
+ status: input.status ?? null,
19
+ limit: input.limit ?? null,
20
+ },
21
+ count,
22
+ executions,
23
+ };
24
+ logger.toolComplete('list_executions', Date.now() - start);
25
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
26
+ }
27
+ catch (error) {
28
+ logger.toolError('list_executions', error, Date.now() - start);
29
+ throw handleExternalServiceError(error, 'DebuggAI', 'list_executions');
30
+ }
31
+ }
@@ -0,0 +1,30 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ const logger = new Logger({ module: 'listProjectsHandler' });
6
+ export async function listProjectsHandler(input, _context) {
7
+ const start = Date.now();
8
+ logger.toolStart('list_projects', { q: input.q });
9
+ try {
10
+ const client = new DebuggAIServerClient(config.api.key);
11
+ await client.init();
12
+ const projects = await client.listProjects(input.q);
13
+ const payload = {
14
+ query: input.q ?? null,
15
+ count: projects.length,
16
+ projects: projects.map(p => ({
17
+ uuid: p.uuid,
18
+ name: p.name,
19
+ slug: p.slug,
20
+ repoName: p.repo?.name ?? null,
21
+ })),
22
+ };
23
+ logger.toolComplete('list_projects', Date.now() - start);
24
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
25
+ }
26
+ catch (error) {
27
+ logger.toolError('list_projects', error, Date.now() - start);
28
+ throw handleExternalServiceError(error, 'DebuggAI', 'list_projects');
29
+ }
30
+ }
@@ -312,8 +312,11 @@ async function testPageChangesHandlerInner(input, context, progressCallback) {
312
312
  if (progressCallback) {
313
313
  await progressCallback({ progress: TOTAL_STEPS, total: TOTAL_STEPS, message: `Complete: ${outcome}` });
314
314
  }
315
+ // Sanitize the whole payload so no tunnel URL leaks anywhere — including
316
+ // agent-authored strings in actionTrace[*].intent, evaluation.reason, etc.
317
+ const sanitizedPayload = sanitizeResponseUrls(responsePayload, ctx);
315
318
  const content = [
316
- { type: 'text', text: JSON.stringify(responsePayload, null, 2) },
319
+ { type: 'text', text: JSON.stringify(sanitizedPayload, null, 2) },
317
320
  ];
318
321
  // Screenshot: check for already-base64 field first (subworkflow.run), then URL-based fields
319
322
  const SCREENSHOT_URL_KEYS = ['finalScreenshot', 'screenshot', 'screenshotUrl', 'screenshotUri'];
@@ -0,0 +1,70 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { detectRepoName } from '../utils/gitContext.js';
6
+ const logger = new Logger({ module: 'updateCredentialHandler' });
7
+ function notFound(uuid, context) {
8
+ return {
9
+ content: [{ type: 'text', text: JSON.stringify({
10
+ error: 'NotFound',
11
+ message: `Credential ${uuid} not found (${context}).`,
12
+ uuid,
13
+ }, null, 2) }],
14
+ isError: true,
15
+ };
16
+ }
17
+ // Defensive stripper: ensure no password/secret keys slip through into responses.
18
+ function stripSecrets(obj) {
19
+ const copy = { ...obj };
20
+ delete copy.password;
21
+ delete copy.secret;
22
+ return copy;
23
+ }
24
+ export async function updateCredentialHandler(input, _context) {
25
+ const start = Date.now();
26
+ logger.toolStart('update_credential', {
27
+ uuid: input.uuid,
28
+ environmentId: input.environmentId,
29
+ patchKeys: Object.keys(input).filter(k => !['uuid', 'environmentId', 'projectUuid', 'password'].includes(k)).concat(input.password !== undefined ? ['password'] : []),
30
+ });
31
+ try {
32
+ const client = new DebuggAIServerClient(config.api.key);
33
+ await client.init();
34
+ let projectUuid = input.projectUuid;
35
+ if (!projectUuid) {
36
+ const repoName = detectRepoName();
37
+ if (!repoName)
38
+ return notFound(input.uuid, 'no git repo and no projectUuid');
39
+ const project = await client.findProjectByRepoName(repoName);
40
+ if (!project)
41
+ return notFound(input.uuid, `no project for repo "${repoName}"`);
42
+ projectUuid = project.uuid;
43
+ }
44
+ try {
45
+ const credential = await client.updateCredential(projectUuid, input.environmentId, input.uuid, {
46
+ label: input.label,
47
+ username: input.username,
48
+ password: input.password,
49
+ role: input.role,
50
+ });
51
+ logger.toolComplete('update_credential', Date.now() - start);
52
+ return {
53
+ content: [{ type: 'text', text: JSON.stringify({
54
+ updated: true,
55
+ credential: stripSecrets(credential),
56
+ }, null, 2) }],
57
+ };
58
+ }
59
+ catch (err) {
60
+ if (err?.statusCode === 404 || err?.response?.status === 404) {
61
+ return notFound(input.uuid, `backend 404 for env ${input.environmentId}`);
62
+ }
63
+ throw err;
64
+ }
65
+ }
66
+ catch (error) {
67
+ logger.toolError('update_credential', error, Date.now() - start);
68
+ throw handleExternalServiceError(error, 'DebuggAI', 'update_credential');
69
+ }
70
+ }
@@ -0,0 +1,59 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { detectRepoName } from '../utils/gitContext.js';
6
+ const logger = new Logger({ module: 'updateEnvironmentHandler' });
7
+ function notFound(uuid, context) {
8
+ return {
9
+ content: [{ type: 'text', text: JSON.stringify({
10
+ error: 'NotFound',
11
+ message: `Environment ${uuid} not found (${context}).`,
12
+ uuid,
13
+ }, null, 2) }],
14
+ isError: true,
15
+ };
16
+ }
17
+ export async function updateEnvironmentHandler(input, _context) {
18
+ const start = Date.now();
19
+ logger.toolStart('update_environment', {
20
+ uuid: input.uuid,
21
+ patchKeys: Object.keys(input).filter(k => k !== 'uuid' && k !== 'projectUuid'),
22
+ projectUuid: input.projectUuid,
23
+ });
24
+ try {
25
+ const client = new DebuggAIServerClient(config.api.key);
26
+ await client.init();
27
+ let projectUuid = input.projectUuid;
28
+ if (!projectUuid) {
29
+ const repoName = detectRepoName();
30
+ if (!repoName)
31
+ return notFound(input.uuid, 'no git repo detected and no projectUuid provided');
32
+ const project = await client.findProjectByRepoName(repoName);
33
+ if (!project)
34
+ return notFound(input.uuid, `no project found for repo "${repoName}"`);
35
+ projectUuid = project.uuid;
36
+ }
37
+ try {
38
+ const environment = await client.updateEnvironment(projectUuid, input.uuid, {
39
+ name: input.name,
40
+ url: input.url,
41
+ description: input.description,
42
+ });
43
+ logger.toolComplete('update_environment', Date.now() - start);
44
+ return {
45
+ content: [{ type: 'text', text: JSON.stringify({ updated: true, environment }, null, 2) }],
46
+ };
47
+ }
48
+ catch (err) {
49
+ if (err?.statusCode === 404 || err?.response?.status === 404) {
50
+ return notFound(input.uuid, `backend returned 404 for project ${projectUuid}`);
51
+ }
52
+ throw err;
53
+ }
54
+ }
55
+ catch (error) {
56
+ logger.toolError('update_environment', error, Date.now() - start);
57
+ throw handleExternalServiceError(error, 'DebuggAI', 'update_environment');
58
+ }
59
+ }
@@ -0,0 +1,45 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ const logger = new Logger({ module: 'updateProjectHandler' });
6
+ function notFound(uuid) {
7
+ return {
8
+ content: [{ type: 'text', text: JSON.stringify({
9
+ error: 'NotFound',
10
+ message: `Project ${uuid} not found.`,
11
+ uuid,
12
+ }, null, 2) }],
13
+ isError: true,
14
+ };
15
+ }
16
+ export async function updateProjectHandler(input, _context) {
17
+ const start = Date.now();
18
+ logger.toolStart('update_project', {
19
+ uuid: input.uuid,
20
+ patchKeys: Object.keys(input).filter(k => k !== 'uuid'),
21
+ });
22
+ try {
23
+ const client = new DebuggAIServerClient(config.api.key);
24
+ await client.init();
25
+ try {
26
+ const project = await client.updateProject(input.uuid, {
27
+ name: input.name,
28
+ description: input.description,
29
+ });
30
+ logger.toolComplete('update_project', Date.now() - start);
31
+ return {
32
+ content: [{ type: 'text', text: JSON.stringify({ updated: true, project }, null, 2) }],
33
+ };
34
+ }
35
+ catch (err) {
36
+ if (err?.statusCode === 404 || err?.response?.status === 404)
37
+ return notFound(input.uuid);
38
+ throw err;
39
+ }
40
+ }
41
+ catch (error) {
42
+ logger.toolError('update_project', error, Date.now() - start);
43
+ throw handleExternalServiceError(error, 'DebuggAI', 'update_project');
44
+ }
45
+ }
package/dist/index.js CHANGED
@@ -21,7 +21,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
21
21
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
22
22
  import { config } from "./config/index.js";
23
23
  import { initTools, getTools, getTool } from "./tools/index.js";
24
- import { resolveProjectContext } from "./services/projectContext.js";
25
24
  import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError, Telemetry, TelemetryEvents, } from "./utils/index.js";
26
25
  // Logger and server are initialized lazily in main() to avoid triggering
27
26
  // config loading at module load time. If config validation fails (bad env vars),
@@ -155,8 +154,8 @@ async function main() {
155
154
  }));
156
155
  logger.info('Telemetry enabled (PostHog)');
157
156
  }
158
- // Connect transport FIRST so the MCP client handshake succeeds immediately.
159
- // Tools start with no project context; enriched once the API responds.
157
+ // No API calls at boot. Project context is resolved lazily on first tool
158
+ // invocation (list_environments / list_credentials / check_app_in_browser).
160
159
  initTools(null);
161
160
  const transport = new StdioServerTransport();
162
161
  await server.connect(transport);
@@ -164,22 +163,6 @@ async function main() {
164
163
  transport: 'stdio',
165
164
  toolsAvailable: getTools().map(t => t.name),
166
165
  });
167
- // Resolve project context in the background — enriches tool descriptions
168
- // with available environments/credentials once the API responds.
169
- resolveProjectContext().then((projectCtx) => {
170
- if (projectCtx) {
171
- initTools(projectCtx);
172
- logger.info('Tool descriptions enriched with project context', {
173
- project: projectCtx.project.name,
174
- environments: projectCtx.environments.length,
175
- credentials: projectCtx.environments.reduce((n, e) => n + e.credentials.length, 0),
176
- });
177
- }
178
- }).catch((err) => {
179
- logger.warn('Background project context resolution failed', {
180
- error: err instanceof Error ? err.message : String(err),
181
- });
182
- });
183
166
  }
184
167
  catch (error) {
185
168
  logger.error('Failed to start DebuggAI MCP Server', {
@@ -73,6 +73,249 @@ export class DebuggAIServerClient {
73
73
  // Fallback to first search result
74
74
  return projects[0];
75
75
  }
76
+ /**
77
+ * Simplified project shape used by get/update tools — drops heavy internal
78
+ * fields (team, runner_configuration, github_auth_details) that most MCP
79
+ * clients don't need.
80
+ */
81
+ mapProjectDetail(p) {
82
+ return {
83
+ uuid: p.uuid,
84
+ name: p.name,
85
+ slug: p.slug,
86
+ platform: p.platform ?? null,
87
+ repoName: p.repo?.name ?? null,
88
+ description: p.description ?? null,
89
+ status: p.status ?? null,
90
+ language: p.language ?? null,
91
+ framework: p.framework ?? null,
92
+ timestamp: p.timestamp,
93
+ lastMod: p.lastMod,
94
+ };
95
+ }
96
+ async getProject(uuid) {
97
+ if (!this.tx)
98
+ throw new Error('Client not initialized — call init() first');
99
+ const p = await this.tx.get(`api/v1/projects/${uuid}/`);
100
+ return this.mapProjectDetail(p);
101
+ }
102
+ async updateProject(uuid, patch) {
103
+ if (!this.tx)
104
+ throw new Error('Client not initialized — call init() first');
105
+ const body = {};
106
+ if (patch.name !== undefined)
107
+ body.name = patch.name;
108
+ if (patch.description !== undefined)
109
+ body.description = patch.description;
110
+ const p = await this.tx.patch(`api/v1/projects/${uuid}/`, body);
111
+ return this.mapProjectDetail(p);
112
+ }
113
+ async deleteProject(uuid) {
114
+ if (!this.tx)
115
+ throw new Error('Client not initialized — call init() first');
116
+ await this.tx.delete(`api/v1/projects/${uuid}/`);
117
+ }
118
+ /**
119
+ * List projects accessible to the current API key.
120
+ * Optional q filters by project name / repo name server-side (backend `?search=`).
121
+ * Returns the first page only; pagination can be added if needed.
122
+ */
123
+ async listProjects(q) {
124
+ if (!this.tx)
125
+ throw new Error('Client not initialized — call init() first');
126
+ const params = q ? { search: q } : undefined;
127
+ const response = await this.tx.get('api/v1/projects/', params);
128
+ return response?.results ?? [];
129
+ }
130
+ /**
131
+ * List environments for a project. Optional q filters by name via backend ?search=.
132
+ */
133
+ async listEnvironmentsForProject(projectUuid, q) {
134
+ if (!this.tx)
135
+ throw new Error('Client not initialized — call init() first');
136
+ const params = q ? { search: q } : undefined;
137
+ const response = await this.tx.get(`api/v1/projects/${projectUuid}/environments/`, params);
138
+ return (response?.results ?? []).map((e) => ({
139
+ uuid: e.uuid,
140
+ name: e.name,
141
+ url: e.url || e.activeUrl || '',
142
+ isActive: e.isActive,
143
+ }));
144
+ }
145
+ /**
146
+ * Create a new environment under a project.
147
+ * Backend requires `name`. Other fields optional.
148
+ */
149
+ async createEnvironment(projectUuid, input) {
150
+ if (!this.tx)
151
+ throw new Error('Client not initialized — call init() first');
152
+ const body = { name: input.name };
153
+ if (input.url)
154
+ body.url = input.url;
155
+ if (input.description)
156
+ body.description = input.description;
157
+ const response = await this.tx.post(`api/v1/projects/${projectUuid}/environments/`, body);
158
+ return {
159
+ uuid: response.uuid,
160
+ name: response.name,
161
+ url: response.url || response.activeUrl || '',
162
+ isActive: response.isActive,
163
+ };
164
+ }
165
+ /**
166
+ * Delete an environment. Used by evals to clean up throwaway test envs.
167
+ */
168
+ async deleteEnvironment(projectUuid, envUuid) {
169
+ if (!this.tx)
170
+ throw new Error('Client not initialized — call init() first');
171
+ await this.tx.delete(`api/v1/projects/${projectUuid}/environments/${envUuid}/`);
172
+ }
173
+ /**
174
+ * Fetch a single environment by UUID. Throws AxiosError with status 404 if not found.
175
+ */
176
+ async getEnvironment(projectUuid, envUuid) {
177
+ if (!this.tx)
178
+ throw new Error('Client not initialized — call init() first');
179
+ const e = await this.tx.get(`api/v1/projects/${projectUuid}/environments/${envUuid}/`);
180
+ return {
181
+ uuid: e.uuid,
182
+ name: e.name,
183
+ url: e.url ?? '',
184
+ isActive: e.isActive,
185
+ description: e.description ?? null,
186
+ endpointType: e.endpointType,
187
+ activeUrl: e.activeUrl ?? null,
188
+ timestamp: e.timestamp,
189
+ lastMod: e.lastMod,
190
+ };
191
+ }
192
+ /**
193
+ * Patch an environment. Backend PATCH response omits uuid — caller should echo it.
194
+ */
195
+ async updateEnvironment(projectUuid, envUuid, patch) {
196
+ if (!this.tx)
197
+ throw new Error('Client not initialized — call init() first');
198
+ const body = {};
199
+ if (patch.name !== undefined)
200
+ body.name = patch.name;
201
+ if (patch.url !== undefined)
202
+ body.url = patch.url;
203
+ if (patch.description !== undefined)
204
+ body.description = patch.description;
205
+ const e = await this.tx.patch(`api/v1/projects/${projectUuid}/environments/${envUuid}/`, body);
206
+ return {
207
+ uuid: envUuid, // echo from input; backend PATCH response omits it
208
+ name: e.name,
209
+ url: e.url ?? '',
210
+ isActive: e.isActive,
211
+ description: e.description ?? null,
212
+ endpointType: e.endpointType,
213
+ };
214
+ }
215
+ /**
216
+ * List credentials for a specific environment. q filters label/username client-side.
217
+ * role filters for exact match.
218
+ */
219
+ async listCredentialsForEnvironment(projectUuid, envUuid, q, role) {
220
+ if (!this.tx)
221
+ throw new Error('Client not initialized — call init() first');
222
+ const response = await this.tx.get(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/`);
223
+ let creds = (response?.results ?? [])
224
+ .filter((c) => c.isActive)
225
+ .map((c) => ({
226
+ uuid: c.uuid,
227
+ label: c.label || c.username,
228
+ username: c.username,
229
+ role: c.role,
230
+ environmentUuid: envUuid,
231
+ }));
232
+ if (q) {
233
+ const needle = q.toLowerCase();
234
+ creds = creds.filter(c => c.label.toLowerCase().includes(needle) ||
235
+ c.username.toLowerCase().includes(needle));
236
+ }
237
+ if (role) {
238
+ creds = creds.filter(c => c.role === role);
239
+ }
240
+ return creds;
241
+ }
242
+ /**
243
+ * Create a credential on an environment. password is write-only — never echoed back.
244
+ */
245
+ async createCredential(projectUuid, envUuid, input) {
246
+ if (!this.tx)
247
+ throw new Error('Client not initialized — call init() first');
248
+ const body = {
249
+ label: input.label,
250
+ username: input.username,
251
+ password: input.password,
252
+ };
253
+ if (input.role)
254
+ body.role = input.role;
255
+ const response = await this.tx.post(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/`, body);
256
+ return {
257
+ uuid: response.uuid,
258
+ label: response.label || response.username,
259
+ username: response.username,
260
+ role: response.role,
261
+ environmentUuid: envUuid,
262
+ };
263
+ }
264
+ /**
265
+ * Delete a credential. Used by evals to clean up throwaway test creds.
266
+ */
267
+ async deleteCredential(projectUuid, envUuid, credUuid) {
268
+ if (!this.tx)
269
+ throw new Error('Client not initialized — call init() first');
270
+ await this.tx.delete(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/${credUuid}/`);
271
+ }
272
+ /**
273
+ * Fetch a single credential by UUID. Throws AxiosError wrapper with statusCode=404 if not found.
274
+ * Response shape omits any password field — backend credential schema has no password field.
275
+ */
276
+ async getCredential(projectUuid, envUuid, credUuid) {
277
+ if (!this.tx)
278
+ throw new Error('Client not initialized — call init() first');
279
+ const c = await this.tx.get(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/${credUuid}/`);
280
+ return {
281
+ uuid: c.uuid,
282
+ label: c.label ?? c.username,
283
+ username: c.username,
284
+ role: c.role ?? null,
285
+ environmentUuid: envUuid,
286
+ environmentName: c.environmentName ?? null,
287
+ isActive: c.isActive,
288
+ isDefault: c.isDefault,
289
+ description: c.description ?? null,
290
+ timestamp: c.timestamp,
291
+ lastMod: c.lastMod,
292
+ };
293
+ }
294
+ /**
295
+ * Update a credential via partial PATCH. Only the specified fields change.
296
+ */
297
+ async updateCredential(projectUuid, envUuid, credUuid, patch) {
298
+ if (!this.tx)
299
+ throw new Error('Client not initialized — call init() first');
300
+ const body = {};
301
+ if (patch.label !== undefined)
302
+ body.label = patch.label;
303
+ if (patch.username !== undefined)
304
+ body.username = patch.username;
305
+ if (patch.password !== undefined)
306
+ body.password = patch.password;
307
+ if (patch.role !== undefined)
308
+ body.role = patch.role;
309
+ const c = await this.tx.patch(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/${credUuid}/`, body);
310
+ return {
311
+ uuid: credUuid, // echo from input; backend PATCH response omits it
312
+ label: c.label,
313
+ username: c.username,
314
+ role: c.role ?? null,
315
+ environmentUuid: envUuid,
316
+ isActive: c.isActive,
317
+ };
318
+ }
76
319
  /**
77
320
  * Revoke an ngrok API key by its key ID.
78
321
  * Call this after workflow execution completes to clean up the short-lived key.