@blogic-cz/agent-tools 0.18.1 → 1.1.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.
Files changed (75) hide show
  1. package/README.md +35 -6
  2. package/dist/az-tool/config.d.ts +60 -12
  3. package/dist/az-tool/config.d.ts.map +1 -1
  4. package/dist/az-tool/errors.d.ts +10 -1
  5. package/dist/az-tool/errors.d.ts.map +1 -1
  6. package/dist/az-tool/profile.d.ts +16 -0
  7. package/dist/az-tool/profile.d.ts.map +1 -0
  8. package/dist/az-tool/security.d.ts +20 -3
  9. package/dist/az-tool/security.d.ts.map +1 -1
  10. package/dist/az-tool/service.d.ts +9 -4
  11. package/dist/az-tool/service.d.ts.map +1 -1
  12. package/dist/az-tool/types.d.ts +10 -60
  13. package/dist/az-tool/types.d.ts.map +1 -1
  14. package/dist/{az-tool → azdo-tool}/build.d.ts +9 -9
  15. package/dist/azdo-tool/build.d.ts.map +1 -0
  16. package/dist/azdo-tool/config.d.ts +13 -0
  17. package/dist/azdo-tool/config.d.ts.map +1 -0
  18. package/dist/azdo-tool/errors.d.ts +43 -0
  19. package/dist/azdo-tool/errors.d.ts.map +1 -0
  20. package/dist/azdo-tool/extract-option-value.d.ts.map +1 -0
  21. package/dist/azdo-tool/index.d.ts +3 -0
  22. package/dist/azdo-tool/index.d.ts.map +1 -0
  23. package/dist/azdo-tool/security.d.ts +4 -0
  24. package/dist/azdo-tool/security.d.ts.map +1 -0
  25. package/dist/azdo-tool/service.d.ts +15 -0
  26. package/dist/azdo-tool/service.d.ts.map +1 -0
  27. package/dist/azdo-tool/transformers.d.ts.map +1 -0
  28. package/dist/azdo-tool/types.d.ts +64 -0
  29. package/dist/azdo-tool/types.d.ts.map +1 -0
  30. package/dist/config/index.d.ts +1 -1
  31. package/dist/config/index.d.ts.map +1 -1
  32. package/dist/config/loader.d.ts +1 -1
  33. package/dist/config/loader.d.ts.map +1 -1
  34. package/dist/config/types.d.ts +18 -1
  35. package/dist/config/types.d.ts.map +1 -1
  36. package/dist/credential-guard/index.d.ts.map +1 -1
  37. package/dist/gh-tool/gist.d.ts +102 -0
  38. package/dist/gh-tool/gist.d.ts.map +1 -0
  39. package/dist/gh-tool/text-input.d.ts +1 -0
  40. package/dist/gh-tool/text-input.d.ts.map +1 -1
  41. package/dist/shared/azure-credentials.d.ts +20 -0
  42. package/dist/shared/azure-credentials.d.ts.map +1 -0
  43. package/dist/shared/binary-preflight.d.ts +1 -1
  44. package/package.json +8 -2
  45. package/schemas/agent-tools.schema.json +42 -1
  46. package/src/az-tool/config.ts +135 -25
  47. package/src/az-tool/errors.ts +14 -1
  48. package/src/az-tool/index.ts +67 -134
  49. package/src/az-tool/profile.ts +48 -0
  50. package/src/az-tool/security.ts +248 -89
  51. package/src/az-tool/service.ts +137 -242
  52. package/src/az-tool/types.ts +11 -64
  53. package/src/{az-tool → azdo-tool}/build.ts +11 -11
  54. package/src/azdo-tool/config.ts +33 -0
  55. package/src/azdo-tool/errors.ts +41 -0
  56. package/src/azdo-tool/index.ts +222 -0
  57. package/src/azdo-tool/security.ts +157 -0
  58. package/src/azdo-tool/service.ts +322 -0
  59. package/src/azdo-tool/types.ts +67 -0
  60. package/src/config/index.ts +1 -0
  61. package/src/config/loader.ts +10 -1
  62. package/src/config/types.ts +19 -1
  63. package/src/credential-guard/index.ts +7 -2
  64. package/src/gh-tool/gist.ts +625 -0
  65. package/src/gh-tool/index.ts +21 -1
  66. package/src/gh-tool/text-input.ts +10 -2
  67. package/src/shared/azure-credentials.ts +50 -0
  68. package/src/shared/binary-preflight.ts +1 -1
  69. package/dist/az-tool/build.d.ts.map +0 -1
  70. package/dist/az-tool/extract-option-value.d.ts.map +0 -1
  71. package/dist/az-tool/transformers.d.ts.map +0 -1
  72. /package/dist/{az-tool → azdo-tool}/extract-option-value.d.ts +0 -0
  73. /package/dist/{az-tool → azdo-tool}/transformers.d.ts +0 -0
  74. /package/src/{az-tool → azdo-tool}/extract-option-value.ts +0 -0
  75. /package/src/{az-tool → azdo-tool}/transformers.ts +0 -0
@@ -0,0 +1,322 @@
1
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2
+ import { Context, Effect, Layer, Option, Stream } from "effect";
3
+
4
+ import type { InvokeParams } from "./types";
5
+ import type { AzureConfig } from "#config/types";
6
+
7
+ import { DIRECT_AZ_COMMANDS, STANDALONE_AZ_COMMANDS } from "./config";
8
+ import { AzdoSecurityError, AzdoCommandError, AzdoTimeoutError, AzdoParseError } from "./errors";
9
+ import { isCommandAllowed, isInvokeAllowed } from "./security";
10
+ import { transformCmdOutput } from "./transformers";
11
+ import { ConfigService, getToolConfig } from "#config";
12
+ import { renderCommandLine, tokenizeCommandLine } from "#shared/exec";
13
+
14
+ export class AzdoService extends Context.Service<
15
+ AzdoService,
16
+ {
17
+ readonly runCommand: (
18
+ cmd: string,
19
+ project?: string,
20
+ ) => Effect.Effect<
21
+ string,
22
+ AzdoSecurityError | AzdoCommandError | AzdoTimeoutError | AzdoParseError
23
+ >;
24
+ readonly runInvoke: (
25
+ params: InvokeParams,
26
+ ) => Effect.Effect<
27
+ unknown,
28
+ AzdoSecurityError | AzdoCommandError | AzdoTimeoutError | AzdoParseError
29
+ >;
30
+ }
31
+ >()("@agent-tools/AzdoService") {
32
+ static readonly layer = Layer.effect(
33
+ AzdoService,
34
+ Effect.gen(function* () {
35
+ const config = yield* ConfigService;
36
+ const azConfig = getToolConfig<AzureConfig>(config, "azure");
37
+
38
+ if (!azConfig) {
39
+ const noConfigError = new AzdoCommandError({
40
+ message: "No Azure configuration found. Add an 'azure' section to agent-tools.json5.",
41
+ command: "unknown",
42
+ exitCode: -1,
43
+ hint: "Create or update agent-tools.json5 with an 'azure' section containing organization and defaultProject",
44
+ });
45
+ return {
46
+ runCommand: (_cmd: string, _project?: string) => Effect.fail(noConfigError),
47
+ runInvoke: (_params: InvokeParams) => Effect.fail(noConfigError),
48
+ };
49
+ }
50
+
51
+ const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
52
+
53
+ const runAzCommand = (argv: readonly string[], timeoutMs: number) =>
54
+ Effect.scoped(
55
+ Effect.gen(function* () {
56
+ const command = ChildProcess.make("az", argv, {
57
+ stdout: "pipe",
58
+ stderr: "pipe",
59
+ });
60
+ const process = yield* executor.spawn(command);
61
+
62
+ const stdoutChunk = yield* process.stdout.pipe(Stream.decodeText(), Stream.runCollect);
63
+ const stdout = stdoutChunk.join("");
64
+
65
+ const stderrChunk = yield* process.stderr.pipe(Stream.decodeText(), Stream.runCollect);
66
+ const stderr = stderrChunk.join("");
67
+
68
+ const exitCode = yield* process.exitCode;
69
+
70
+ return { stdout, stderr, exitCode };
71
+ }),
72
+ ).pipe(
73
+ Effect.timeoutOption(timeoutMs),
74
+ Effect.mapError(
75
+ (platformError) =>
76
+ new AzdoCommandError({
77
+ message: `Command execution failed: ${platformError.message}`,
78
+ command: renderCommandLine(["az", ...argv]),
79
+ exitCode: -1,
80
+ hint: "Check that the az CLI is installed and authenticated",
81
+ nextCommand: "az login",
82
+ retryable: true,
83
+ }),
84
+ ),
85
+ );
86
+
87
+ const resolveProject = (project?: string) => project ?? azConfig.defaultProject;
88
+
89
+ const runCommand = Effect.fn("AzdoService.runCommand")(function* (
90
+ cmd: string,
91
+ project?: string,
92
+ ) {
93
+ const projectName = resolveProject(project);
94
+
95
+ const securityCheck = isCommandAllowed(cmd);
96
+ if (!securityCheck.allowed) {
97
+ return yield* new AzdoSecurityError({
98
+ message: securityCheck.reason ?? "Command not allowed",
99
+ command: cmd,
100
+ hint: "Only read-only az devops commands are allowed. Use build helpers for common operations.",
101
+ });
102
+ }
103
+
104
+ const invokeParams = parseInvokeFromCommand(cmd);
105
+ if (invokeParams) {
106
+ const invokeResult = yield* runInvoke({
107
+ ...invokeParams,
108
+ project: projectName,
109
+ });
110
+ return JSON.stringify(invokeResult);
111
+ }
112
+
113
+ const cmdWords = cmd.trim().split(/\s+/);
114
+ const firstWord = cmdWords[0]?.toLowerCase() ?? "";
115
+ const isDirectCommand = DIRECT_AZ_COMMANDS.includes(
116
+ firstWord as (typeof DIRECT_AZ_COMMANDS)[number],
117
+ );
118
+ const isStandaloneCommand = STANDALONE_AZ_COMMANDS.includes(
119
+ firstWord as (typeof STANDALONE_AZ_COMMANDS)[number],
120
+ );
121
+
122
+ const cmdArgv = tokenizeCommandLine(cmd);
123
+ const scopeArgv = [
124
+ "--organization",
125
+ azConfig.organization,
126
+ "--project",
127
+ projectName,
128
+ ] as const;
129
+
130
+ const argv = isStandaloneCommand
131
+ ? cmdArgv
132
+ : isDirectCommand
133
+ ? [...cmdArgv, ...scopeArgv]
134
+ : ["devops", ...cmdArgv, ...scopeArgv];
135
+
136
+ const fullCommand = renderCommandLine(["az", ...argv]);
137
+ const resultOption = yield* runAzCommand(argv, azConfig.timeoutMs ?? 60000);
138
+
139
+ if (Option.isNone(resultOption)) {
140
+ return yield* new AzdoTimeoutError({
141
+ message: `Command timed out after ${azConfig.timeoutMs ?? 60000}ms`,
142
+ command: fullCommand,
143
+ timeoutMs: azConfig.timeoutMs ?? 60000,
144
+ retryable: true,
145
+ hint: "The command took too long. Retry or increase timeoutMs in azure config.",
146
+ });
147
+ }
148
+
149
+ const result = resultOption.value;
150
+
151
+ if (result.exitCode !== 0) {
152
+ return yield* new AzdoCommandError({
153
+ message: result.stderr || `Command failed with exit code ${result.exitCode}`,
154
+ command: fullCommand,
155
+ exitCode: result.exitCode,
156
+ ...(result.stderr ? { stderr: result.stderr } : {}),
157
+ });
158
+ }
159
+
160
+ const output = result.stdout.trim();
161
+ const transformed = transformCmdOutput(output);
162
+
163
+ if (typeof transformed === "string") {
164
+ return transformed;
165
+ }
166
+
167
+ return JSON.stringify(transformed);
168
+ });
169
+
170
+ const runInvoke = Effect.fn("AzdoService.runInvoke")(function* (params: InvokeParams) {
171
+ const securityCheck = isInvokeAllowed(params);
172
+ if (!securityCheck.allowed) {
173
+ return yield* new AzdoSecurityError({
174
+ message: securityCheck.reason ?? "Invoke not allowed",
175
+ command: `invoke --area ${params.area} --resource ${params.resource}`,
176
+ hint: "Only allowed invoke areas/resources can be used. Check azdo-tool security config.",
177
+ });
178
+ }
179
+
180
+ const argv = ["devops", "invoke", "--area", params.area, "--resource", params.resource];
181
+
182
+ const projectName = resolveProject(params.project);
183
+
184
+ const routeParameters = {
185
+ project: projectName,
186
+ ...params.routeParameters,
187
+ };
188
+
189
+ if (Object.keys(routeParameters).length > 0) {
190
+ argv.push(
191
+ "--route-parameters",
192
+ ...Object.entries(routeParameters).map(([k, v]) => `${k}=${v}`),
193
+ );
194
+ }
195
+
196
+ if (params.queryParameters) {
197
+ argv.push(
198
+ "--query-parameters",
199
+ ...Object.entries(params.queryParameters).map(([k, v]) => `${k}=${v}`),
200
+ );
201
+ }
202
+
203
+ argv.push("--organization", azConfig.organization, "--output", "json");
204
+
205
+ const fullCommand = renderCommandLine(["az", ...argv]);
206
+ const resultOption = yield* runAzCommand(argv, azConfig.timeoutMs ?? 60000);
207
+
208
+ if (Option.isNone(resultOption)) {
209
+ return yield* new AzdoTimeoutError({
210
+ message: `Invoke timed out after ${azConfig.timeoutMs ?? 60000}ms`,
211
+ command: fullCommand,
212
+ timeoutMs: azConfig.timeoutMs ?? 60000,
213
+ retryable: true,
214
+ hint: "The invoke took too long. Retry or increase timeoutMs in azure config.",
215
+ });
216
+ }
217
+
218
+ const result = resultOption.value;
219
+
220
+ if (result.exitCode !== 0) {
221
+ return yield* new AzdoCommandError({
222
+ message: result.stderr || `Invoke failed with exit code ${result.exitCode}`,
223
+ command: fullCommand,
224
+ exitCode: result.exitCode,
225
+ ...(result.stderr ? { stderr: result.stderr } : {}),
226
+ });
227
+ }
228
+
229
+ const jsonData = yield* Effect.try({
230
+ try: () => JSON.parse(result.stdout) as unknown,
231
+ catch: () =>
232
+ new AzdoParseError({
233
+ message: `Failed to parse JSON response from invoke`,
234
+ rawOutput: result.stdout.slice(0, 500),
235
+ hint: "The az CLI returned non-JSON output. Ensure --output json is used.",
236
+ }),
237
+ });
238
+ return jsonData;
239
+ });
240
+
241
+ return { runCommand, runInvoke };
242
+ }),
243
+ );
244
+ }
245
+
246
+ export const AzdoServiceLayer = AzdoService.layer;
247
+
248
+ function parseInvokeFromCommand(cmd: string): InvokeParams | undefined {
249
+ const words = cmd.trim().split(/\s+/);
250
+ const loweredWords = words.map((word) => word.toLowerCase());
251
+
252
+ if (!loweredWords.includes("invoke")) {
253
+ return undefined;
254
+ }
255
+
256
+ const area = extractOptionValue(words, "--area");
257
+ const resource = extractOptionValue(words, "--resource");
258
+
259
+ if (!area || !resource) {
260
+ return undefined;
261
+ }
262
+
263
+ const routeParameters = extractParametersOption(words, "--route-parameters");
264
+ const queryParameters = extractParametersOption(words, "--query-parameters");
265
+ const apiVersion = extractOptionValue(words, "--api-version");
266
+
267
+ const mergedQueryParameters = apiVersion
268
+ ? {
269
+ ...queryParameters,
270
+ "api-version": apiVersion,
271
+ }
272
+ : queryParameters;
273
+
274
+ return {
275
+ area,
276
+ resource,
277
+ ...(routeParameters ? { routeParameters } : {}),
278
+ ...(mergedQueryParameters ? { queryParameters: mergedQueryParameters } : {}),
279
+ };
280
+ }
281
+
282
+ function extractOptionValue(args: readonly string[], optionName: string): string | undefined {
283
+ const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
284
+
285
+ if (optionIndex === -1) {
286
+ return undefined;
287
+ }
288
+
289
+ return args[optionIndex + 1];
290
+ }
291
+
292
+ function extractParametersOption(
293
+ args: readonly string[],
294
+ optionName: string,
295
+ ): Record<string, string | number> | undefined {
296
+ const optionIndex = args.findIndex((arg) => arg.toLowerCase() === optionName.toLowerCase());
297
+
298
+ if (optionIndex === -1) {
299
+ return undefined;
300
+ }
301
+
302
+ const result: Record<string, string | number> = {};
303
+
304
+ for (let i = optionIndex + 1; i < args.length; i++) {
305
+ const token = args[i];
306
+ if (!token || token.startsWith("--")) {
307
+ break;
308
+ }
309
+
310
+ const equalsIndex = token.indexOf("=");
311
+ if (equalsIndex === -1) {
312
+ continue;
313
+ }
314
+
315
+ const key = token.slice(0, equalsIndex);
316
+ const rawValue = token.slice(equalsIndex + 1);
317
+ const parsedNumber = Number(rawValue);
318
+ result[key] = Number.isNaN(parsedNumber) ? rawValue : parsedNumber;
319
+ }
320
+
321
+ return Object.keys(result).length > 0 ? result : undefined;
322
+ }
@@ -0,0 +1,67 @@
1
+ export type SecurityCheckResult = {
2
+ allowed: boolean;
3
+ command?: string;
4
+ reason?: string;
5
+ };
6
+
7
+ export type InvokeParams = {
8
+ area: string;
9
+ resource: string;
10
+ project?: string;
11
+ routeParameters?: Record<string, string | number>;
12
+ queryParameters?: Record<string, string | number>;
13
+ };
14
+
15
+ export type BuildJob = {
16
+ id: string;
17
+ parentId?: string | null;
18
+ type: "Job" | "Stage" | "Task" | "Phase" | "Checkpoint";
19
+ name: string;
20
+ state: "pending" | "inProgress" | "completed";
21
+ result?: "succeeded" | "failed" | "canceled" | "skipped" | null;
22
+ startTime?: string | null;
23
+ finishTime?: string | null;
24
+ errorCount?: number | null;
25
+ warningCount?: number | null;
26
+ log?: { id: number; url: string } | null;
27
+ };
28
+
29
+ export type BuildTimeline = {
30
+ records: BuildJob[];
31
+ id: string;
32
+ changeId: number;
33
+ lastChangedBy: string;
34
+ lastChangedOn: string;
35
+ url: string;
36
+ };
37
+
38
+ export type BuildLog = {
39
+ id: number;
40
+ type: string;
41
+ url: string;
42
+ lineCount?: number;
43
+ };
44
+
45
+ export type BuildLogs = {
46
+ count: number;
47
+ value: BuildLog[];
48
+ };
49
+
50
+ export type JobSummary = {
51
+ name: string;
52
+ state: string;
53
+ result?: string;
54
+ stage?: string;
55
+ duration?: string;
56
+ logId?: number;
57
+ };
58
+
59
+ export type PipelineRun = {
60
+ id: number;
61
+ buildNumber: string;
62
+ status: string;
63
+ result?: string;
64
+ sourceBranch: string;
65
+ startTime?: string;
66
+ finishTime?: string;
67
+ };
@@ -1,6 +1,7 @@
1
1
  export type {
2
2
  AgentToolsConfig,
3
3
  AzureConfig,
4
+ AzurePlatformConfig,
4
5
  K8sConfig,
5
6
  DbEnvConfig,
6
7
  DbAllowedMutationTargets,
@@ -88,6 +88,13 @@ const AzureConfigSchema = Schema.Struct({
88
88
  timeoutMs: Schema.optionalKey(Schema.Number),
89
89
  });
90
90
 
91
+ const AzurePlatformConfigSchema = Schema.Struct({
92
+ subscription: Schema.String,
93
+ timeoutMs: Schema.optionalKey(Schema.Number),
94
+ production: Schema.optionalKey(Schema.Boolean),
95
+ allowedResourceGroups: Schema.optionalKey(Schema.Array(Schema.String)),
96
+ });
97
+
91
98
  const K8sConfigSchema = Schema.Struct({
92
99
  kubeconfig: Schema.optionalKey(Schema.String),
93
100
  clusterId: Schema.String,
@@ -178,6 +185,7 @@ const GitHubRepoConfigSchema = Schema.Struct({
178
185
  const KNOWN_TOP_LEVEL_KEYS = new Set([
179
186
  "$schema",
180
187
  "azure",
188
+ "azurePlatform",
181
189
  "vpns",
182
190
  "kubernetes",
183
191
  "database",
@@ -193,6 +201,7 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
193
201
  const AgentToolsConfigSchema = Schema.Struct({
194
202
  $schema: Schema.optionalKey(Schema.String),
195
203
  azure: Schema.optionalKey(Schema.Record(Schema.String, AzureConfigSchema)),
204
+ azurePlatform: Schema.optionalKey(Schema.Record(Schema.String, AzurePlatformConfigSchema)),
196
205
  vpns: Schema.optionalKey(Schema.Record(Schema.String, VpnConfigSchema)),
197
206
  kubernetes: Schema.optionalKey(Schema.Record(Schema.String, K8sConfigSchema)),
198
207
  database: Schema.optionalKey(Schema.Record(Schema.String, DatabaseConfigSchema)),
@@ -378,7 +387,7 @@ export const ConfigServiceLayer = Layer.effect(
378
387
 
379
388
  type ProfiledSection = keyof Pick<
380
389
  AgentToolsConfig,
381
- "azure" | "kubernetes" | "database" | "observability" | "logs"
390
+ "azure" | "azurePlatform" | "kubernetes" | "database" | "observability" | "logs"
382
391
  >;
383
392
 
384
393
  export function getToolConfig<T>(
@@ -1,12 +1,28 @@
1
1
  import { Schema } from "effect";
2
2
 
3
- /** Azure DevOps profile configuration */
3
+ /** Azure DevOps profile, consumed by azdo-tool. */
4
4
  export type AzureConfig = {
5
5
  organization: string;
6
6
  defaultProject: string;
7
7
  timeoutMs?: number;
8
8
  };
9
9
 
10
+ /** Azure platform (PaaS) profile, consumed by az-tool. */
11
+ export type AzurePlatformConfig = {
12
+ subscription: string;
13
+ timeoutMs?: number;
14
+ /**
15
+ * Require --profile to be passed explicitly before this subscription is
16
+ * touched. Defaults to true for profiles keyed "prod" or "production".
17
+ */
18
+ production?: boolean;
19
+ /**
20
+ * Resource groups this profile may address. Empty or absent means every
21
+ * resource group in the subscription is allowed.
22
+ */
23
+ allowedResourceGroups?: string[];
24
+ };
25
+
10
26
  export type CleanupPolicy = "leave-running" | "stop-if-started";
11
27
 
12
28
  export type VpnPrerequisite = {
@@ -206,6 +222,8 @@ export type AgentToolsConfig = {
206
222
  $schema?: string;
207
223
  /** Named Azure DevOps profiles. e.g. { default: { organization: "...", defaultProject: "..." } } */
208
224
  azure?: Record<string, AzureConfig>;
225
+ /** Named Azure platform profiles. e.g. { default: { subscription: "..." } } */
226
+ azurePlatform?: Record<string, AzurePlatformConfig>;
209
227
  /** Named VPN definitions referenced by profile prerequisites. */
210
228
  vpns?: Record<string, VpnConfig>;
211
229
  /** Named Kubernetes cluster profiles. e.g. { default: {...}, staging: {...} } */
@@ -173,6 +173,11 @@ const DEFAULT_BLOCKED_CLI_TOOLS: BlockedCliTool[] = [
173
173
  name: "psql",
174
174
  wrapper: "agent-tools-db",
175
175
  },
176
+ {
177
+ pattern: /(?:^|[;&|]\s*)az\s+(?:devops|pipelines|repos|boards|artifacts)\b/,
178
+ name: "az (Azure DevOps)",
179
+ wrapper: "agent-tools-azdo",
180
+ },
176
181
  {
177
182
  pattern: /(?:^|[;&|]\s*)az\s/,
178
183
  name: "az",
@@ -181,7 +186,7 @@ const DEFAULT_BLOCKED_CLI_TOOLS: BlockedCliTool[] = [
181
186
  {
182
187
  pattern: /(?:^|[;&|]\s*)curl\s.*dev\.azure\.com/,
183
188
  name: "curl (Azure DevOps)",
184
- wrapper: "agent-tools-az",
189
+ wrapper: "agent-tools-azdo",
185
190
  },
186
191
  ];
187
192
 
@@ -209,7 +214,7 @@ const DEFAULT_POLLING_DETECTION_RULES: PollingDetectionRule[] = [
209
214
  },
210
215
  {
211
216
  pattern: /\bpipelines?\s+runs?\b/,
212
- suggestion: "bun agent-tools-az build summary --build-id <ID>",
217
+ suggestion: "bun agent-tools-azdo build summary --build-id <ID>",
213
218
  },
214
219
  ];
215
220