@daghis/teamcity-mcp 1.3.5 → 1.5.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/CHANGELOG.md +14 -0
- package/dist/index.js +450 -17
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0](https://github.com/Daghis/teamcity-mcp/compare/v1.4.0...v1.5.0) (2025-09-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **teamcity:** add streaming artifact downloads ([#161](https://github.com/Daghis/teamcity-mcp/issues/161)) ([b50b773](https://github.com/Daghis/teamcity-mcp/commit/b50b773ebee6f5cc85f4c53c98fef525e7098fe9)), closes [#151](https://github.com/Daghis/teamcity-mcp/issues/151)
|
|
9
|
+
|
|
10
|
+
## [1.4.0](https://github.com/Daghis/teamcity-mcp/compare/v1.3.5...v1.4.0) (2025-09-20)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **tests:** add batched mcp tool execution ([#163](https://github.com/Daghis/teamcity-mcp/issues/163)) ([5f48060](https://github.com/Daghis/teamcity-mcp/commit/5f4806043b95686d5dac41a9d67515740b82a3f8)), closes [#162](https://github.com/Daghis/teamcity-mcp/issues/162)
|
|
16
|
+
|
|
3
17
|
## [1.3.5](https://github.com/Daghis/teamcity-mcp/compare/v1.3.4...v1.3.5) (2025-09-19)
|
|
4
18
|
|
|
5
19
|
|
package/dist/index.js
CHANGED
|
@@ -758,9 +758,9 @@ var TeamCityLogger = class _TeamCityLogger {
|
|
|
758
758
|
*/
|
|
759
759
|
ensureLogDirectory(directory) {
|
|
760
760
|
try {
|
|
761
|
-
const
|
|
762
|
-
if (
|
|
763
|
-
|
|
761
|
+
const fs2 = require("fs");
|
|
762
|
+
if (fs2.existsSync(directory) === false) {
|
|
763
|
+
fs2.mkdirSync(directory, { recursive: true });
|
|
764
764
|
}
|
|
765
765
|
} catch (error2) {
|
|
766
766
|
this.winston?.warn?.("Failed to create log directory, using current directory", { error: error2 });
|
|
@@ -921,6 +921,11 @@ function debug(message, meta) {
|
|
|
921
921
|
}
|
|
922
922
|
|
|
923
923
|
// src/tools.ts
|
|
924
|
+
var import_node_crypto = require("node:crypto");
|
|
925
|
+
var import_node_fs = require("node:fs");
|
|
926
|
+
var import_node_os = require("node:os");
|
|
927
|
+
var import_node_path = require("node:path");
|
|
928
|
+
var import_promises = require("node:stream/promises");
|
|
924
929
|
var import_axios35 = require("axios");
|
|
925
930
|
var import_zod4 = require("zod");
|
|
926
931
|
|
|
@@ -931,6 +936,318 @@ var ResolutionTypeEnum = {
|
|
|
931
936
|
AtTime: "atTime"
|
|
932
937
|
};
|
|
933
938
|
|
|
939
|
+
// src/teamcity/utils/build-locator.ts
|
|
940
|
+
var toBuildLocator = (buildId) => buildId.includes(":") ? buildId : `id:${buildId}`;
|
|
941
|
+
|
|
942
|
+
// src/teamcity/artifact-manager.ts
|
|
943
|
+
var ArtifactManager = class _ArtifactManager {
|
|
944
|
+
client;
|
|
945
|
+
cache = /* @__PURE__ */ new Map();
|
|
946
|
+
static cacheTtlMs = 6e4;
|
|
947
|
+
// 1 minute
|
|
948
|
+
static defaultLimit = 100;
|
|
949
|
+
static maxLimit = 1e3;
|
|
950
|
+
constructor(client) {
|
|
951
|
+
this.client = client;
|
|
952
|
+
}
|
|
953
|
+
getBaseUrl() {
|
|
954
|
+
const baseUrl = this.client.getApiConfig().baseUrl;
|
|
955
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* List artifacts for a build
|
|
959
|
+
*/
|
|
960
|
+
async listArtifacts(buildId, options = {}) {
|
|
961
|
+
const cacheKey = this.getCacheKey(buildId, options);
|
|
962
|
+
if (!options.forceRefresh) {
|
|
963
|
+
const cached = this.getFromCache(cacheKey);
|
|
964
|
+
if (cached) {
|
|
965
|
+
return cached;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const buildLocator = toBuildLocator(buildId);
|
|
970
|
+
const response = await this.client.modules.builds.getFilesListOfBuild(
|
|
971
|
+
buildLocator,
|
|
972
|
+
void 0,
|
|
973
|
+
void 0,
|
|
974
|
+
"file(name,fullName,size,modificationTime,href,children(file(name,fullName,size,modificationTime,href)))"
|
|
975
|
+
);
|
|
976
|
+
const baseUrl = this.getBaseUrl();
|
|
977
|
+
let artifacts = this.parseArtifacts(
|
|
978
|
+
response.data ?? {},
|
|
979
|
+
buildId,
|
|
980
|
+
options.includeNested,
|
|
981
|
+
baseUrl
|
|
982
|
+
);
|
|
983
|
+
artifacts = this.applyFilters(artifacts, options);
|
|
984
|
+
if (options.limit ?? options.offset) {
|
|
985
|
+
artifacts = this.paginate(
|
|
986
|
+
artifacts,
|
|
987
|
+
options.offset ?? 0,
|
|
988
|
+
options.limit ?? _ArtifactManager.defaultLimit
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
this.cacheResult(cacheKey, artifacts);
|
|
992
|
+
return artifacts;
|
|
993
|
+
} catch (error2) {
|
|
994
|
+
const err = error2;
|
|
995
|
+
if (err.response?.status === 401) {
|
|
996
|
+
throw new Error("Authentication failed: Invalid TeamCity token");
|
|
997
|
+
}
|
|
998
|
+
if (err.response?.status === 404) {
|
|
999
|
+
throw new Error(`Build not found: ${buildId}`);
|
|
1000
|
+
}
|
|
1001
|
+
const errMsg = err.message ?? String(error2);
|
|
1002
|
+
throw new Error(`Failed to fetch artifacts: ${errMsg}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Download a specific artifact
|
|
1007
|
+
*/
|
|
1008
|
+
async downloadArtifact(buildId, artifactPath, options = {}) {
|
|
1009
|
+
const artifacts = await this.listArtifacts(buildId);
|
|
1010
|
+
const artifact = artifacts.find((a) => a.path === artifactPath || a.name === artifactPath);
|
|
1011
|
+
if (!artifact) {
|
|
1012
|
+
throw new Error(`Artifact not found: ${artifactPath}`);
|
|
1013
|
+
}
|
|
1014
|
+
if (options.maxSize && artifact.size > options.maxSize) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`Artifact size exceeds maximum allowed size: ${artifact.size} > ${options.maxSize}`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const encoding = options.encoding ?? "buffer";
|
|
1021
|
+
const normalizedPath = artifact.path.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
1022
|
+
const buildLocator = toBuildLocator(buildId);
|
|
1023
|
+
const artifactRequestPath = `content/${normalizedPath}`;
|
|
1024
|
+
if (encoding === "text") {
|
|
1025
|
+
const response2 = await this.client.modules.builds.downloadFileOfBuild(
|
|
1026
|
+
artifactRequestPath,
|
|
1027
|
+
buildLocator,
|
|
1028
|
+
void 0,
|
|
1029
|
+
void 0,
|
|
1030
|
+
{ responseType: "text" }
|
|
1031
|
+
);
|
|
1032
|
+
const axiosResponse2 = response2;
|
|
1033
|
+
const { data, headers } = axiosResponse2;
|
|
1034
|
+
if (typeof data !== "string") {
|
|
1035
|
+
throw new Error("Artifact download returned a non-text payload when text was expected");
|
|
1036
|
+
}
|
|
1037
|
+
const mimeType = typeof headers?.["content-type"] === "string" ? headers["content-type"] : void 0;
|
|
1038
|
+
return {
|
|
1039
|
+
name: artifact.name,
|
|
1040
|
+
path: artifact.path,
|
|
1041
|
+
size: artifact.size,
|
|
1042
|
+
content: data,
|
|
1043
|
+
mimeType
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
if (encoding === "stream") {
|
|
1047
|
+
const response2 = await this.client.modules.builds.downloadFileOfBuild(
|
|
1048
|
+
artifactRequestPath,
|
|
1049
|
+
buildLocator,
|
|
1050
|
+
void 0,
|
|
1051
|
+
void 0,
|
|
1052
|
+
{ responseType: "stream" }
|
|
1053
|
+
);
|
|
1054
|
+
const axiosResponse2 = response2;
|
|
1055
|
+
const stream = axiosResponse2.data;
|
|
1056
|
+
if (!this.isReadableStream(stream)) {
|
|
1057
|
+
throw new Error(
|
|
1058
|
+
"Artifact download returned a non-stream payload when stream was requested"
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
const mimeType = typeof axiosResponse2.headers?.["content-type"] === "string" ? axiosResponse2.headers["content-type"] : void 0;
|
|
1062
|
+
return {
|
|
1063
|
+
name: artifact.name,
|
|
1064
|
+
path: artifact.path,
|
|
1065
|
+
size: artifact.size,
|
|
1066
|
+
content: stream,
|
|
1067
|
+
mimeType
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
const response = await this.client.modules.builds.downloadFileOfBuild(
|
|
1071
|
+
artifactRequestPath,
|
|
1072
|
+
buildLocator,
|
|
1073
|
+
void 0,
|
|
1074
|
+
void 0,
|
|
1075
|
+
{ responseType: "arraybuffer" }
|
|
1076
|
+
);
|
|
1077
|
+
const axiosResponse = response;
|
|
1078
|
+
const buffer = this.ensureBinaryBuffer(axiosResponse.data);
|
|
1079
|
+
let content;
|
|
1080
|
+
if (encoding === "base64") {
|
|
1081
|
+
content = buffer.toString("base64");
|
|
1082
|
+
} else {
|
|
1083
|
+
content = buffer;
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
name: artifact.name,
|
|
1087
|
+
path: artifact.path,
|
|
1088
|
+
size: artifact.size,
|
|
1089
|
+
content,
|
|
1090
|
+
mimeType: typeof axiosResponse.headers?.["content-type"] === "string" ? axiosResponse.headers["content-type"] : void 0
|
|
1091
|
+
};
|
|
1092
|
+
} catch (error2) {
|
|
1093
|
+
const errMsg = error2 instanceof Error ? error2.message : "Unknown error";
|
|
1094
|
+
throw new Error(`Failed to download artifact: ${errMsg}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Download multiple artifacts
|
|
1099
|
+
*/
|
|
1100
|
+
async downloadMultipleArtifacts(buildId, artifactPaths, options = {}) {
|
|
1101
|
+
if (options.encoding === "stream") {
|
|
1102
|
+
throw new Error("Streaming downloads are only supported when requesting a single artifact");
|
|
1103
|
+
}
|
|
1104
|
+
const downloadOptions = { encoding: "base64", ...options };
|
|
1105
|
+
const results = await Promise.allSettled(
|
|
1106
|
+
artifactPaths.map((path) => this.downloadArtifact(buildId, path, downloadOptions))
|
|
1107
|
+
);
|
|
1108
|
+
return results.map((result, index) => {
|
|
1109
|
+
if (result.status === "fulfilled") {
|
|
1110
|
+
return result.value;
|
|
1111
|
+
} else {
|
|
1112
|
+
const fallbackName = artifactPaths[index] ?? "unknown";
|
|
1113
|
+
return {
|
|
1114
|
+
name: fallbackName,
|
|
1115
|
+
path: fallbackName,
|
|
1116
|
+
size: 0,
|
|
1117
|
+
error: result.reason.message
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Parse artifacts from API response
|
|
1124
|
+
*/
|
|
1125
|
+
parseArtifacts(data, buildId, includeNested, baseUrl) {
|
|
1126
|
+
const artifacts = [];
|
|
1127
|
+
const files = data.file ?? [];
|
|
1128
|
+
for (const file of files) {
|
|
1129
|
+
if (file.children && includeNested) {
|
|
1130
|
+
const nested = this.parseArtifacts(file.children, buildId, includeNested, baseUrl);
|
|
1131
|
+
artifacts.push(...nested);
|
|
1132
|
+
} else if (!file.children) {
|
|
1133
|
+
artifacts.push({
|
|
1134
|
+
name: file.name ?? "",
|
|
1135
|
+
path: file.fullName ?? file.name ?? "",
|
|
1136
|
+
size: file.size ?? 0,
|
|
1137
|
+
modificationTime: file.modificationTime ?? "",
|
|
1138
|
+
downloadUrl: `${baseUrl}/app/rest/builds/id:${buildId}/artifacts/content/${file.fullName ?? file.name ?? ""}`,
|
|
1139
|
+
isDirectory: false
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return artifacts;
|
|
1144
|
+
}
|
|
1145
|
+
ensureBinaryBuffer(payload) {
|
|
1146
|
+
if (Buffer.isBuffer(payload)) {
|
|
1147
|
+
return payload;
|
|
1148
|
+
}
|
|
1149
|
+
if (payload instanceof ArrayBuffer) {
|
|
1150
|
+
return Buffer.from(payload);
|
|
1151
|
+
}
|
|
1152
|
+
throw new Error("Artifact download returned unexpected binary payload type");
|
|
1153
|
+
}
|
|
1154
|
+
isReadableStream(value) {
|
|
1155
|
+
if (value == null || typeof value !== "object") {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
const candidate = value;
|
|
1159
|
+
return typeof candidate.pipe === "function";
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Apply filters to artifacts
|
|
1163
|
+
*/
|
|
1164
|
+
applyFilters(artifacts, options) {
|
|
1165
|
+
let filtered = artifacts;
|
|
1166
|
+
if (options.nameFilter) {
|
|
1167
|
+
const regex = this.globToRegex(options.nameFilter);
|
|
1168
|
+
filtered = filtered.filter((a) => regex.test(a.name));
|
|
1169
|
+
}
|
|
1170
|
+
if (options.pathFilter) {
|
|
1171
|
+
const regex = this.globToRegex(options.pathFilter);
|
|
1172
|
+
filtered = filtered.filter((a) => regex.test(a.path));
|
|
1173
|
+
}
|
|
1174
|
+
if (options.extension) {
|
|
1175
|
+
const ext = options.extension.startsWith(".") ? options.extension : `.${options.extension}`;
|
|
1176
|
+
filtered = filtered.filter((a) => a.name.endsWith(ext));
|
|
1177
|
+
}
|
|
1178
|
+
if (options.minSize !== void 0) {
|
|
1179
|
+
const minSize = options.minSize;
|
|
1180
|
+
filtered = filtered.filter((a) => a.size >= minSize);
|
|
1181
|
+
}
|
|
1182
|
+
if (options.maxSize !== void 0) {
|
|
1183
|
+
const maxSize = options.maxSize;
|
|
1184
|
+
filtered = filtered.filter((a) => a.size <= maxSize);
|
|
1185
|
+
}
|
|
1186
|
+
return filtered;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Convert glob pattern to regex
|
|
1190
|
+
*/
|
|
1191
|
+
globToRegex(pattern) {
|
|
1192
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1193
|
+
return new RegExp(`^${escaped}$`);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Paginate results
|
|
1197
|
+
*/
|
|
1198
|
+
paginate(artifacts, offset, limit) {
|
|
1199
|
+
const effectiveLimit = Math.min(limit, _ArtifactManager.maxLimit);
|
|
1200
|
+
return artifacts.slice(offset, offset + effectiveLimit);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Generate cache key
|
|
1204
|
+
*/
|
|
1205
|
+
getCacheKey(buildId, options) {
|
|
1206
|
+
const { forceRefresh: _forceRefresh, ...cacheOptions } = options;
|
|
1207
|
+
return `${buildId}:${JSON.stringify(cacheOptions)}`;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Get from cache if valid
|
|
1211
|
+
*/
|
|
1212
|
+
getFromCache(key) {
|
|
1213
|
+
const entry = this.cache.get(key);
|
|
1214
|
+
if (!entry) {
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
const age = Date.now() - entry.timestamp;
|
|
1218
|
+
if (age > _ArtifactManager.cacheTtlMs) {
|
|
1219
|
+
this.cache.delete(key);
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
return entry.artifacts;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Cache artifacts
|
|
1226
|
+
*/
|
|
1227
|
+
cacheResult(key, artifacts) {
|
|
1228
|
+
this.cache.set(key, {
|
|
1229
|
+
artifacts,
|
|
1230
|
+
timestamp: Date.now()
|
|
1231
|
+
});
|
|
1232
|
+
this.cleanCache();
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Remove expired cache entries
|
|
1236
|
+
*/
|
|
1237
|
+
cleanCache() {
|
|
1238
|
+
const now = Date.now();
|
|
1239
|
+
const expired = [];
|
|
1240
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
1241
|
+
if (now - entry.timestamp > _ArtifactManager.cacheTtlMs) {
|
|
1242
|
+
expired.push(key);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
for (const key of expired) {
|
|
1246
|
+
this.cache.delete(key);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
|
|
934
1251
|
// src/teamcity/build-configuration-update-manager.ts
|
|
935
1252
|
var BuildConfigurationUpdateManager = class {
|
|
936
1253
|
client;
|
|
@@ -1317,9 +1634,6 @@ var BuildConfigurationUpdateManager = class {
|
|
|
1317
1634
|
}
|
|
1318
1635
|
};
|
|
1319
1636
|
|
|
1320
|
-
// src/teamcity/utils/build-locator.ts
|
|
1321
|
-
var toBuildLocator = (buildId) => buildId.includes(":") ? buildId : `id:${buildId}`;
|
|
1322
|
-
|
|
1323
1637
|
// src/teamcity/build-results-manager.ts
|
|
1324
1638
|
var BuildResultsManager = class _BuildResultsManager {
|
|
1325
1639
|
client;
|
|
@@ -1621,7 +1935,14 @@ var BuildResultsManager = class _BuildResultsManager {
|
|
|
1621
1935
|
{ responseType: "arraybuffer" }
|
|
1622
1936
|
);
|
|
1623
1937
|
const axiosResponse = response;
|
|
1624
|
-
|
|
1938
|
+
const { data } = axiosResponse;
|
|
1939
|
+
if (data instanceof ArrayBuffer) {
|
|
1940
|
+
return data.slice(0);
|
|
1941
|
+
}
|
|
1942
|
+
if (Buffer.isBuffer(data)) {
|
|
1943
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
1944
|
+
}
|
|
1945
|
+
throw new Error("Artifact download returned unexpected binary payload type");
|
|
1625
1946
|
}
|
|
1626
1947
|
/**
|
|
1627
1948
|
* Parse TeamCity date format
|
|
@@ -1695,12 +2016,24 @@ var BuildResultsManager = class _BuildResultsManager {
|
|
|
1695
2016
|
// src/teamcity/client-adapter.ts
|
|
1696
2017
|
var import_axios = __toESM(require("axios"));
|
|
1697
2018
|
var FALLBACK_BASE_URL = "http://not-configured";
|
|
2019
|
+
var toRecord = (value) => {
|
|
2020
|
+
if (typeof value === "object" && value !== null) {
|
|
2021
|
+
return value;
|
|
2022
|
+
}
|
|
2023
|
+
return {};
|
|
2024
|
+
};
|
|
2025
|
+
var createBuildApiBridge = (api) => ({
|
|
2026
|
+
getAllBuilds: (locator, fields, options) => api.getAllBuilds(locator, fields, options),
|
|
2027
|
+
getBuild: (buildLocator, fields, options) => api.getBuild(buildLocator, fields, options),
|
|
2028
|
+
getMultipleBuilds: (locator, fields, options) => api.getMultipleBuilds(locator, fields, options),
|
|
2029
|
+
getBuildProblems: (buildLocator, fields, options) => api.getBuildProblems(buildLocator, fields, options)
|
|
2030
|
+
});
|
|
1698
2031
|
var resolveModules = (api) => {
|
|
1699
2032
|
const candidate = api.modules;
|
|
1700
2033
|
if (candidate != null) {
|
|
1701
2034
|
return candidate;
|
|
1702
2035
|
}
|
|
1703
|
-
const legacy = api;
|
|
2036
|
+
const legacy = toRecord(api);
|
|
1704
2037
|
const pick = (key) => legacy[key] ?? {};
|
|
1705
2038
|
const fallback = {
|
|
1706
2039
|
agents: pick("agents"),
|
|
@@ -1762,7 +2095,7 @@ function createAdapterFromTeamCityAPI(api, options = {}) {
|
|
|
1762
2095
|
});
|
|
1763
2096
|
}
|
|
1764
2097
|
const request = async (fn) => fn({ axios: httpInstance, baseUrl: resolvedApiConfig.baseUrl, requestId: void 0 });
|
|
1765
|
-
const buildApi = modules.builds;
|
|
2098
|
+
const buildApi = createBuildApiBridge(modules.builds);
|
|
1766
2099
|
return {
|
|
1767
2100
|
modules,
|
|
1768
2101
|
http: httpInstance,
|
|
@@ -1783,7 +2116,7 @@ function createAdapterFromTeamCityAPI(api, options = {}) {
|
|
|
1783
2116
|
listTestFailures: (buildId) => api.listTestFailures(buildId),
|
|
1784
2117
|
builds: buildApi,
|
|
1785
2118
|
listBuildArtifacts: (buildId, options2) => api.listBuildArtifacts(buildId, options2),
|
|
1786
|
-
downloadArtifactContent: (buildId, artifactPath) => api.downloadBuildArtifact(buildId, artifactPath),
|
|
2119
|
+
downloadArtifactContent: (buildId, artifactPath, requestOptions) => api.downloadBuildArtifact(buildId, artifactPath, requestOptions),
|
|
1787
2120
|
getBuildStatistics: (buildId, fields) => api.getBuildStatistics(buildId, fields),
|
|
1788
2121
|
listChangesForBuild: (buildId, fields) => api.listChangesForBuild(buildId, fields),
|
|
1789
2122
|
listSnapshotDependencies: (buildId) => api.listSnapshotDependencies(buildId),
|
|
@@ -2483,6 +2816,12 @@ var esm_default = axiosRetry;
|
|
|
2483
2816
|
// src/teamcity/auth.ts
|
|
2484
2817
|
var import_crypto = require("crypto");
|
|
2485
2818
|
init_errors();
|
|
2819
|
+
var asTimingMetaContainer = (value) => {
|
|
2820
|
+
if (typeof value === "object" && value !== null) {
|
|
2821
|
+
return value;
|
|
2822
|
+
}
|
|
2823
|
+
return null;
|
|
2824
|
+
};
|
|
2486
2825
|
function generateRequestId() {
|
|
2487
2826
|
return (0, import_crypto.randomUUID)();
|
|
2488
2827
|
}
|
|
@@ -2491,7 +2830,10 @@ function addRequestId(config2) {
|
|
|
2491
2830
|
config2.headers["X-Request-ID"] = requestId;
|
|
2492
2831
|
const configWithId = config2;
|
|
2493
2832
|
configWithId.requestId = requestId;
|
|
2494
|
-
|
|
2833
|
+
const metaContainer = asTimingMetaContainer(config2);
|
|
2834
|
+
if (metaContainer) {
|
|
2835
|
+
metaContainer._tcMeta = { start: Date.now() };
|
|
2836
|
+
}
|
|
2495
2837
|
info("Starting TeamCity API request", {
|
|
2496
2838
|
requestId,
|
|
2497
2839
|
method: config2.method?.toUpperCase(),
|
|
@@ -2505,7 +2847,7 @@ function addRequestId(config2) {
|
|
|
2505
2847
|
}
|
|
2506
2848
|
function logResponse(response) {
|
|
2507
2849
|
const requestId = response.config?.requestId;
|
|
2508
|
-
const meta = response.config
|
|
2850
|
+
const meta = asTimingMetaContainer(response.config)?._tcMeta;
|
|
2509
2851
|
const headers = response.headers;
|
|
2510
2852
|
const headerDuration = headers?.["x-response-time"] ?? headers?.["x-response-duration"];
|
|
2511
2853
|
const duration = headerDuration ?? (meta?.start ? Date.now() - meta.start : void 0);
|
|
@@ -2521,7 +2863,7 @@ function logResponse(response) {
|
|
|
2521
2863
|
function logAndTransformError(error2) {
|
|
2522
2864
|
const requestId = error2.config?.requestId;
|
|
2523
2865
|
const tcError = TeamCityAPIError.fromAxiosError(error2, requestId);
|
|
2524
|
-
const meta = error2.config?._tcMeta;
|
|
2866
|
+
const meta = asTimingMetaContainer(error2.config)?._tcMeta;
|
|
2525
2867
|
const duration = meta?.start ? Date.now() - meta.start : void 0;
|
|
2526
2868
|
const sanitize = (val) => {
|
|
2527
2869
|
const redact = (s) => s.replace(/(token[=:\s]*)[^\s&]+/gi, "$1***").replace(/(password[=:\s]*)[^\s&]+/gi, "$1***").replace(/(apikey[=:\s]*)[^\s&]+/gi, "$1***").replace(/(authorization[=:\s:]*)[^\s&]+/gi, "$1***");
|
|
@@ -36343,13 +36685,15 @@ var TeamCityAPI = class _TeamCityAPI {
|
|
|
36343
36685
|
options?.logBuildUsage
|
|
36344
36686
|
);
|
|
36345
36687
|
}
|
|
36346
|
-
async downloadBuildArtifact(buildId, artifactPath) {
|
|
36688
|
+
async downloadBuildArtifact(buildId, artifactPath, options) {
|
|
36347
36689
|
const normalizedPath = artifactPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
36690
|
+
const requestOptions = {
|
|
36691
|
+
...options ?? {},
|
|
36692
|
+
responseType: options?.responseType ?? "arraybuffer"
|
|
36693
|
+
};
|
|
36348
36694
|
return this.axiosInstance.get(
|
|
36349
36695
|
`/app/rest/builds/id:${buildId}/artifacts/content/${normalizedPath}`,
|
|
36350
|
-
|
|
36351
|
-
responseType: "arraybuffer"
|
|
36352
|
-
}
|
|
36696
|
+
requestOptions
|
|
36353
36697
|
);
|
|
36354
36698
|
}
|
|
36355
36699
|
async getBuildStatistics(buildId, fields) {
|
|
@@ -37944,6 +38288,95 @@ var DEV_TOOLS = [
|
|
|
37944
38288
|
);
|
|
37945
38289
|
}
|
|
37946
38290
|
},
|
|
38291
|
+
{
|
|
38292
|
+
name: "download_build_artifact",
|
|
38293
|
+
description: "Download a single artifact with optional streaming output",
|
|
38294
|
+
inputSchema: {
|
|
38295
|
+
type: "object",
|
|
38296
|
+
properties: {
|
|
38297
|
+
buildId: { type: "string", description: "Build ID" },
|
|
38298
|
+
artifactPath: { type: "string", description: "Artifact path or name" },
|
|
38299
|
+
encoding: {
|
|
38300
|
+
type: "string",
|
|
38301
|
+
description: "Response encoding: 'base64' (default), 'text', or 'stream'",
|
|
38302
|
+
enum: ["base64", "text", "stream"],
|
|
38303
|
+
default: "base64"
|
|
38304
|
+
},
|
|
38305
|
+
maxSize: {
|
|
38306
|
+
type: "number",
|
|
38307
|
+
description: "Maximum artifact size (bytes) allowed before aborting"
|
|
38308
|
+
},
|
|
38309
|
+
outputPath: {
|
|
38310
|
+
type: "string",
|
|
38311
|
+
description: "Optional absolute path to write streamed content; defaults to a temp file when streaming"
|
|
38312
|
+
}
|
|
38313
|
+
},
|
|
38314
|
+
required: ["buildId", "artifactPath"]
|
|
38315
|
+
},
|
|
38316
|
+
handler: async (args) => {
|
|
38317
|
+
const schema = import_zod4.z.object({
|
|
38318
|
+
buildId: import_zod4.z.string().min(1),
|
|
38319
|
+
artifactPath: import_zod4.z.string().min(1),
|
|
38320
|
+
encoding: import_zod4.z.enum(["base64", "text", "stream"]).default("base64"),
|
|
38321
|
+
maxSize: import_zod4.z.number().int().positive().optional(),
|
|
38322
|
+
outputPath: import_zod4.z.string().min(1).optional()
|
|
38323
|
+
});
|
|
38324
|
+
const isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
|
|
38325
|
+
const toTempFilePath = (artifactName) => {
|
|
38326
|
+
const base = (0, import_node_path.basename)(artifactName || "artifact");
|
|
38327
|
+
const safeStem = base.replace(/[^a-zA-Z0-9._-]/g, "_") || "artifact";
|
|
38328
|
+
const ext = (0, import_node_path.extname)(safeStem);
|
|
38329
|
+
const stemWithoutExt = ext ? safeStem.slice(0, -ext.length) : safeStem;
|
|
38330
|
+
const finalStem = stemWithoutExt || "artifact";
|
|
38331
|
+
const fileName = `${finalStem}-${(0, import_node_crypto.randomUUID)()}${ext}`;
|
|
38332
|
+
return (0, import_node_path.join)((0, import_node_os.tmpdir)(), fileName);
|
|
38333
|
+
};
|
|
38334
|
+
return runTool(
|
|
38335
|
+
"download_build_artifact",
|
|
38336
|
+
schema,
|
|
38337
|
+
async (typed) => {
|
|
38338
|
+
const adapter = createAdapterFromTeamCityAPI(TeamCityAPI.getInstance());
|
|
38339
|
+
const manager = new ArtifactManager(adapter);
|
|
38340
|
+
const artifact = await manager.downloadArtifact(typed.buildId, typed.artifactPath, {
|
|
38341
|
+
encoding: typed.encoding,
|
|
38342
|
+
maxSize: typed.maxSize
|
|
38343
|
+
});
|
|
38344
|
+
if (typed.encoding === "stream") {
|
|
38345
|
+
const stream = artifact.content;
|
|
38346
|
+
if (!isReadableStream(stream)) {
|
|
38347
|
+
throw new Error("Streaming download did not return a readable stream");
|
|
38348
|
+
}
|
|
38349
|
+
const targetPath = typed.outputPath ?? toTempFilePath(artifact.name);
|
|
38350
|
+
await import_node_fs.promises.mkdir((0, import_node_path.dirname)(targetPath), { recursive: true });
|
|
38351
|
+
await (0, import_promises.pipeline)(stream, (0, import_node_fs.createWriteStream)(targetPath));
|
|
38352
|
+
const stats = await import_node_fs.promises.stat(targetPath);
|
|
38353
|
+
return json({
|
|
38354
|
+
name: artifact.name,
|
|
38355
|
+
path: artifact.path,
|
|
38356
|
+
size: artifact.size,
|
|
38357
|
+
mimeType: artifact.mimeType,
|
|
38358
|
+
encoding: "stream",
|
|
38359
|
+
outputPath: targetPath,
|
|
38360
|
+
bytesWritten: stats.size
|
|
38361
|
+
});
|
|
38362
|
+
}
|
|
38363
|
+
const payloadContent = artifact.content;
|
|
38364
|
+
if (typeof payloadContent !== "string") {
|
|
38365
|
+
throw new Error(`Expected ${typed.encoding} artifact content as string`);
|
|
38366
|
+
}
|
|
38367
|
+
return json({
|
|
38368
|
+
name: artifact.name,
|
|
38369
|
+
path: artifact.path,
|
|
38370
|
+
size: artifact.size,
|
|
38371
|
+
mimeType: artifact.mimeType,
|
|
38372
|
+
encoding: typed.encoding,
|
|
38373
|
+
content: payloadContent
|
|
38374
|
+
});
|
|
38375
|
+
},
|
|
38376
|
+
args
|
|
38377
|
+
);
|
|
38378
|
+
}
|
|
38379
|
+
},
|
|
37947
38380
|
{
|
|
37948
38381
|
name: "get_test_details",
|
|
37949
38382
|
description: "Get detailed information about test failures",
|