@vitalyostanin/youtrack-mcp 0.3.1 → 0.4.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/README-ru.md +40 -9
- package/README.md +34 -3
- package/dist/package.json +1 -1
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +7 -0
- package/dist/src/server.js +1 -1
- package/dist/src/tools/issue-search-tools.d.ts +2 -1
- package/dist/src/tools/issue-search-tools.js +8 -3
- package/dist/src/types.d.ts +2 -0
- package/dist/src/utils/mappers.d.ts +5 -0
- package/dist/src/utils/mappers.js +9 -0
- package/dist/src/utils/tool-response.d.ts +1 -1
- package/dist/src/utils/tool-response.js +8 -2
- package/dist/src/youtrack-client.d.ts +17 -1
- package/dist/src/youtrack-client.js +229 -85
- package/package.json +1 -1
package/README-ru.md
CHANGED
|
@@ -26,6 +26,7 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
26
26
|
- [Запуск сервера (stdio)](#запуск-сервера-stdio)
|
|
27
27
|
- [Конфигурация для Code (Рекомендуется)](#конфигурация-для-code-рекомендуется)
|
|
28
28
|
- [Конфигурация для Claude Code CLI](#конфигурация-для-claude-code-cli)
|
|
29
|
+
- [Конфигурация для VS Code Cline](#конфигурация-для-vs-code-cline)
|
|
29
30
|
- [Инструменты MCP](#инструменты-mcp)
|
|
30
31
|
- [Сервис](#сервис)
|
|
31
32
|
- [Задачи](#задачи)
|
|
@@ -38,12 +39,13 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
38
39
|
|
|
39
40
|
- Node.js ≥ 20
|
|
40
41
|
- Переменные окружения:
|
|
41
|
-
- `YOUTRACK_URL` — базовый URL YouTrack
|
|
42
|
-
- `YOUTRACK_TOKEN` — постоянный токен с правами на чтение задач и work items
|
|
43
|
-
- `YOUTRACK_TIMEZONE` — опциональная таймзона для операций с датами (по умолчанию: `Europe/Moscow`), должна быть валидным идентификатором IANA (например, `Europe/London`, `America/New_York`, `Asia/Tokyo`)
|
|
44
|
-
- `YOUTRACK_HOLIDAYS` — опциональный список праздничных дат через запятую (формат `YYYY-MM-DD`), исключаемых из отчётов и массовых операций
|
|
45
|
-
- `YOUTRACK_PRE_HOLIDAYS` — опциональный список предпраздничных дат через запятую, для которых норма времени уменьшается автоматически
|
|
46
|
-
- `YOUTRACK_USER_ALIASES` — опциональный список соответствий `alias:login` через запятую (например, `me:vyt,petya:p.petrov`), используется при автоматическом выборе исполнителей
|
|
42
|
+
- `YOUTRACK_URL` — базовый URL YouTrack
|
|
43
|
+
- `YOUTRACK_TOKEN` — постоянный токен с правами на чтение задач и work items
|
|
44
|
+
- `YOUTRACK_TIMEZONE` — опциональная таймзона для операций с датами (по умолчанию: `Europe/Moscow`), должна быть валидным идентификатором IANA (например, `Europe/London`, `America/New_York`, `Asia/Tokyo`)
|
|
45
|
+
- `YOUTRACK_HOLIDAYS` — опциональный список праздничных дат через запятую (формат `YYYY-MM-DD`), исключаемых из отчётов и массовых операций
|
|
46
|
+
- `YOUTRACK_PRE_HOLIDAYS` — опциональный список предпраздничных дат через запятую, для которых норма времени уменьшается автоматически
|
|
47
|
+
- `YOUTRACK_USER_ALIASES` — опциональный список соответствий `alias:login` через запятую (например, `me:vyt,petya:p.petrov`), используется при автоматическом выборе исполнителей
|
|
48
|
+
- `YOUTRACK_COMPACT_MODE` — опциональный, управляет минимизацией ответов для оптимизации контекстного окна AI (по умолчанию: `true`). Установите `false` для Claude Code, чтобы получать полный текст ответа в поле MCP content вместо минимальной заглушки. При `true` полные данные доступны в поле `structuredContent`
|
|
47
49
|
|
|
48
50
|
## Установка
|
|
49
51
|
|
|
@@ -54,7 +56,7 @@ MCP сервер для полноценной интеграции с YouTrack
|
|
|
54
56
|
```bash
|
|
55
57
|
YOUTRACK_URL="https://youtrack.example.com" \
|
|
56
58
|
YOUTRACK_TOKEN="perm:your-token-here" \
|
|
57
|
-
npx -y @vitalyostanin/youtrack-mcp
|
|
59
|
+
npx -y @vitalyostanin/youtrack-mcp@latest
|
|
58
60
|
```
|
|
59
61
|
|
|
60
62
|
### Используя Claude MCP CLI
|
|
@@ -189,7 +191,7 @@ node dist/index.js
|
|
|
189
191
|
```toml
|
|
190
192
|
[mcp_servers.youtrack-mcp]
|
|
191
193
|
command = "npx"
|
|
192
|
-
args = ["-y", "@vitalyostanin/youtrack-mcp"]
|
|
194
|
+
args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
|
|
193
195
|
env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
|
|
194
196
|
```
|
|
195
197
|
|
|
@@ -208,7 +210,36 @@ env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "per
|
|
|
208
210
|
"youtrack-mcp": {
|
|
209
211
|
"type": "stdio",
|
|
210
212
|
"command": "npx",
|
|
211
|
-
"args": ["-y", "@vitalyostanin/youtrack-mcp"],
|
|
213
|
+
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
214
|
+
"env": {
|
|
215
|
+
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
216
|
+
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
217
|
+
"YOUTRACK_COMPACT_MODE": "false"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Примечание:** Эта конфигурация использует npx для запуска опубликованного пакета. Для локальной разработки используйте `"command": "node"` с `"args": ["/абсолютный/путь/к/youtrack-mcp/dist/index.js"]`. Переменные окружения `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HOLIDAYS` и `YOUTRACK_USER_ALIASES` являются опциональными.
|
|
225
|
+
|
|
226
|
+
**Для пользователей Claude Code:** Установите `YOUTRACK_COMPACT_MODE` в `"false"`, чтобы включить полные данные ответа в поле MCP content. Это помогает Claude Code легче получать доступ к структурированным данным. При `true` (по умолчанию) в поле content возвращается только минимальная заглушка для оптимизации использования контекстного окна для других AI агентов.
|
|
227
|
+
|
|
228
|
+
## Конфигурация для VS Code Cline
|
|
229
|
+
|
|
230
|
+
Чтобы использовать этот MCP сервер с расширением [Cline](https://github.com/cline/cline) в VS Code:
|
|
231
|
+
|
|
232
|
+
1. Откройте VS Code с установленным расширением Cline
|
|
233
|
+
2. Нажмите на иконку MCP Servers в верхней навигации Cline
|
|
234
|
+
3. Выберите вкладку "Configure" и нажмите "Configure MCP Servers"
|
|
235
|
+
4. Добавьте следующую конфигурацию в `cline_mcp_settings.json`:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"mcpServers": {
|
|
240
|
+
"youtrack-mcp": {
|
|
241
|
+
"command": "npx",
|
|
242
|
+
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
212
243
|
"env": {
|
|
213
244
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
214
245
|
"YOUTRACK_TOKEN": "perm:your-token-here"
|
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
26
26
|
- [Running the server (stdio)](#running-the-server-stdio)
|
|
27
27
|
- [Configuration for Code (Recommended)](#configuration-for-code-recommended)
|
|
28
28
|
- [Configuration for Claude Code CLI](#configuration-for-claude-code-cli)
|
|
29
|
+
- [Configuration for VS Code Cline](#configuration-for-vs-code-cline)
|
|
29
30
|
- [MCP Tools](#mcp-tools)
|
|
30
31
|
- [Service](#service)
|
|
31
32
|
- [Issues](#issues)
|
|
@@ -45,6 +46,7 @@ MCP server for comprehensive YouTrack integration with the following capabilitie
|
|
|
45
46
|
- `YOUTRACK_HOLIDAYS` — optional comma-separated list of holiday dates (format `YYYY-MM-DD`), excluded from reports and batch operations
|
|
46
47
|
- `YOUTRACK_PRE_HOLIDAYS` — optional comma-separated list of pre-holiday dates with reduced working hours
|
|
47
48
|
- `YOUTRACK_USER_ALIASES` — optional comma-separated list of `alias:login` mappings (e.g., `me:vyt,petya:p.petrov`), used for automatic assignee selection
|
|
49
|
+
- `YOUTRACK_COMPACT_MODE` — optional, controls response minimization for AI context window optimization (default: `true`). Set to `false` for Claude Code to get full response text in MCP content field instead of minimal stub. When `true`, full data is available in `structuredContent` field
|
|
48
50
|
|
|
49
51
|
## Installation
|
|
50
52
|
|
|
@@ -55,7 +57,7 @@ You can run the server directly with npx without installation:
|
|
|
55
57
|
```bash
|
|
56
58
|
YOUTRACK_URL="https://youtrack.example.com" \
|
|
57
59
|
YOUTRACK_TOKEN="perm:your-token-here" \
|
|
58
|
-
npx -y @vitalyostanin/youtrack-mcp
|
|
60
|
+
npx -y @vitalyostanin/youtrack-mcp@latest
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
### Using Claude MCP CLI
|
|
@@ -190,7 +192,7 @@ To use this MCP server with [Code](https://github.com/just-every/code), add the
|
|
|
190
192
|
```toml
|
|
191
193
|
[mcp_servers.youtrack-mcp]
|
|
192
194
|
command = "npx"
|
|
193
|
-
args = ["-y", "@vitalyostanin/youtrack-mcp"]
|
|
195
|
+
args = ["-y", "@vitalyostanin/youtrack-mcp@latest"]
|
|
194
196
|
env = { "YOUTRACK_URL" = "https://youtrack.example.com", "YOUTRACK_TOKEN" = "perm:your-token-here" }
|
|
195
197
|
```
|
|
196
198
|
|
|
@@ -209,7 +211,36 @@ To use this MCP server with [Claude Code CLI](https://github.com/anthropics/clau
|
|
|
209
211
|
"youtrack-mcp": {
|
|
210
212
|
"type": "stdio",
|
|
211
213
|
"command": "npx",
|
|
212
|
-
"args": ["-y", "@vitalyostanin/youtrack-mcp"],
|
|
214
|
+
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
215
|
+
"env": {
|
|
216
|
+
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
217
|
+
"YOUTRACK_TOKEN": "perm:your-token-here",
|
|
218
|
+
"YOUTRACK_COMPACT_MODE": "false"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Note:** This configuration uses npx to run the published package. For local development, use `"command": "node"` with `"args": ["/absolute/path/to/youtrack-mcp/dist/index.js"]`. The `YOUTRACK_TIMEZONE`, `YOUTRACK_HOLIDAYS`, `YOUTRACK_PRE_HOLIDAYS`, and `YOUTRACK_USER_ALIASES` environment variables are optional.
|
|
226
|
+
|
|
227
|
+
**For Claude Code users:** Set `YOUTRACK_COMPACT_MODE` to `"false"` to include full response data in the MCP content field. This helps Claude Code access structured data more easily. When `true` (default), only a minimal stub is returned in the content field to optimize context window usage for other AI agents.
|
|
228
|
+
|
|
229
|
+
## Configuration for VS Code Cline
|
|
230
|
+
|
|
231
|
+
To use this MCP server with [Cline](https://github.com/cline/cline) extension in VS Code:
|
|
232
|
+
|
|
233
|
+
1. Open VS Code with Cline extension installed
|
|
234
|
+
2. Click the MCP Servers icon in Cline's top navigation
|
|
235
|
+
3. Select the "Configure" tab and click "Configure MCP Servers"
|
|
236
|
+
4. Add the following configuration to `cline_mcp_settings.json`:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"mcpServers": {
|
|
241
|
+
"youtrack-mcp": {
|
|
242
|
+
"command": "npx",
|
|
243
|
+
"args": ["-y", "@vitalyostanin/youtrack-mcp@latest"],
|
|
213
244
|
"env": {
|
|
214
245
|
"YOUTRACK_URL": "https://youtrack.example.com",
|
|
215
246
|
"YOUTRACK_TOKEN": "perm:your-token-here"
|
package/dist/package.json
CHANGED
package/dist/src/config.d.ts
CHANGED
package/dist/src/config.js
CHANGED
|
@@ -6,6 +6,11 @@ const configSchema = z.object({
|
|
|
6
6
|
YOUTRACK_HOLIDAYS: z.string().optional(),
|
|
7
7
|
YOUTRACK_PRE_HOLIDAYS: z.string().optional(),
|
|
8
8
|
YOUTRACK_USER_ALIASES: z.string().optional(),
|
|
9
|
+
YOUTRACK_COMPACT_MODE: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.default("true")
|
|
13
|
+
.transform((val) => val !== "false"),
|
|
9
14
|
});
|
|
10
15
|
export function loadConfig(env = process.env) {
|
|
11
16
|
const parsed = configSchema.safeParse(env);
|
|
@@ -32,6 +37,7 @@ export function loadConfig(env = process.env) {
|
|
|
32
37
|
userAliases: parsed.data.YOUTRACK_USER_ALIASES
|
|
33
38
|
? parseAliasMap(parsed.data.YOUTRACK_USER_ALIASES)
|
|
34
39
|
: undefined,
|
|
40
|
+
compactMode: parsed.data.YOUTRACK_COMPACT_MODE,
|
|
35
41
|
};
|
|
36
42
|
}
|
|
37
43
|
export function enrichConfigWithRedaction(config) {
|
|
@@ -41,6 +47,7 @@ export function enrichConfigWithRedaction(config) {
|
|
|
41
47
|
timezone: config.timezone,
|
|
42
48
|
holidays: config.holidays,
|
|
43
49
|
preHolidays: config.preHolidays,
|
|
50
|
+
compactMode: config.compactMode,
|
|
44
51
|
};
|
|
45
52
|
}
|
|
46
53
|
function parseCsvList(value) {
|
package/dist/src/server.js
CHANGED
|
@@ -35,7 +35,7 @@ export class YoutrackServer {
|
|
|
35
35
|
registerServiceInfoTool(this.server, this.client);
|
|
36
36
|
registerIssueTools(this.server, this.client);
|
|
37
37
|
registerIssueActivityTools(this.server, this.client);
|
|
38
|
-
registerIssueSearchTools(this.server, this.client);
|
|
38
|
+
registerIssueSearchTools(this.server, this.client, config);
|
|
39
39
|
registerWorkitemTools(this.server, this.client);
|
|
40
40
|
registerWorkitemReportTools(this.server, this.client);
|
|
41
41
|
registerArticleTools(this.server, this.client);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import type { YoutrackClient } from "../youtrack-client.js";
|
|
3
|
-
|
|
3
|
+
import type { YoutrackConfig } from "../types.js";
|
|
4
|
+
export declare function registerIssueSearchTools(server: McpServer, client: YoutrackClient, config: YoutrackConfig): void;
|
|
4
5
|
//# sourceMappingURL=issue-search-tools.d.ts.map
|
|
@@ -17,6 +17,10 @@ const issueSearchByUserActivityArgs = {
|
|
|
17
17
|
.enum(["issue_updated", "user_activity"])
|
|
18
18
|
.optional()
|
|
19
19
|
.describe("Date filter mode: 'issue_updated' (default, fast) filters by issue.updated field; 'user_activity' (slow, precise) filters by actual user activity dates including comments, mentions, and field changes history. Use 'user_activity' when you need exact date of user's involvement, e.g., when user was assignee but later changed."),
|
|
20
|
+
briefOutput: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Return brief issue data without description fields (default: true). Set to false to include full description and wikifiedDescription fields."),
|
|
20
24
|
limit: z
|
|
21
25
|
.number()
|
|
22
26
|
.int()
|
|
@@ -32,8 +36,8 @@ const issueSearchByUserActivityArgs = {
|
|
|
32
36
|
.describe("Number of results to skip for pagination (default: 0)"),
|
|
33
37
|
};
|
|
34
38
|
const issueSearchByUserActivitySchema = z.object(issueSearchByUserActivityArgs);
|
|
35
|
-
export function registerIssueSearchTools(server, client) {
|
|
36
|
-
server.tool("issue_search_by_user_activity", "Search for issues where specified users had activity (updated, mentioned, reported, assigned, commented) within a given time period. Supports two filter modes: 'issue_updated' (default, fast) uses issue.updated field, 'user_activity' (slow, precise) checks actual user activity dates including comments, mentions, and field changes history. Results are sorted by activity time (most recent first). When 'user_activity' mode is used, each issue includes 'lastActivityDate' field with exact timestamp of user's last activity. Supports pagination via limit and skip parameters. Note:
|
|
39
|
+
export function registerIssueSearchTools(server, client, config) {
|
|
40
|
+
server.tool("issue_search_by_user_activity", "Search for issues where specified users had activity (updated, mentioned, reported, assigned, commented) within a given time period. Supports two filter modes: 'issue_updated' (default, fast) uses issue.updated field, 'user_activity' (slow, precise) checks actual user activity dates including comments, mentions, and field changes history. Results are sorted by activity time (most recent first). When 'user_activity' mode is used, each issue includes 'lastActivityDate' field with exact timestamp of user's last activity. Supports pagination via limit and skip parameters. Note: By default (briefOutput=true), each issue includes minimal fields only - id, idReadable, summary, project (id, shortName, name), parent (id, idReadable), assignee (id, login, name). Description fields are excluded to reduce response size. Set briefOutput=false to include full description and wikifiedDescription fields. Custom fields are not included.", issueSearchByUserActivityArgs, async (rawInput) => {
|
|
37
41
|
try {
|
|
38
42
|
const payload = issueSearchByUserActivitySchema.parse(rawInput);
|
|
39
43
|
const results = await client.searchIssuesByUserActivity({
|
|
@@ -41,10 +45,11 @@ export function registerIssueSearchTools(server, client) {
|
|
|
41
45
|
startDate: payload.startDate,
|
|
42
46
|
endDate: payload.endDate,
|
|
43
47
|
dateFilterMode: payload.dateFilterMode,
|
|
48
|
+
briefOutput: payload.briefOutput,
|
|
44
49
|
limit: payload.limit,
|
|
45
50
|
skip: payload.skip,
|
|
46
51
|
});
|
|
47
|
-
const response = toolSuccess(results);
|
|
52
|
+
const response = toolSuccess(results, config.compactMode);
|
|
48
53
|
return response;
|
|
49
54
|
}
|
|
50
55
|
catch (error) {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface YoutrackConfig {
|
|
|
12
12
|
holidays?: string[];
|
|
13
13
|
preHolidays?: string[];
|
|
14
14
|
userAliases?: UserAliasMap;
|
|
15
|
+
compactMode: boolean;
|
|
15
16
|
}
|
|
16
17
|
export interface DurationValue {
|
|
17
18
|
minutes?: number;
|
|
@@ -344,6 +345,7 @@ export interface IssueSearchInput {
|
|
|
344
345
|
startDate?: string | number | Date;
|
|
345
346
|
endDate?: string | number | Date;
|
|
346
347
|
dateFilterMode?: "issue_updated" | "user_activity";
|
|
348
|
+
briefOutput?: boolean;
|
|
347
349
|
limit?: number;
|
|
348
350
|
skip?: number;
|
|
349
351
|
}
|
|
@@ -46,6 +46,11 @@ export declare function mapIssue(issue: YoutrackIssue): MappedYoutrackIssue;
|
|
|
46
46
|
* Map YoutrackIssueDetails to MappedYoutrackIssueDetails
|
|
47
47
|
*/
|
|
48
48
|
export declare function mapIssueDetails(issue: YoutrackIssueDetails): MappedYoutrackIssueDetails;
|
|
49
|
+
/**
|
|
50
|
+
* Map YoutrackIssue to brief version (without description fields)
|
|
51
|
+
* Used for reducing payload size in issue_search_by_user_activity tool
|
|
52
|
+
*/
|
|
53
|
+
export declare function mapIssueBrief(issue: YoutrackIssue): MappedYoutrackIssue;
|
|
49
54
|
/**
|
|
50
55
|
* Map YoutrackWorkItem to MappedYoutrackWorkItem
|
|
51
56
|
*/
|
|
@@ -48,6 +48,15 @@ export function mapIssueDetails(issue) {
|
|
|
48
48
|
resolved: timestampToIsoDateTime(issue.resolved),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Map YoutrackIssue to brief version (without description fields)
|
|
53
|
+
* Used for reducing payload size in issue_search_by_user_activity tool
|
|
54
|
+
*/
|
|
55
|
+
export function mapIssueBrief(issue) {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
57
|
+
const { description, wikifiedDescription, ...rest } = issue;
|
|
58
|
+
return rest;
|
|
59
|
+
}
|
|
51
60
|
/**
|
|
52
61
|
* Map YoutrackWorkItem to MappedYoutrackWorkItem
|
|
53
62
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
export declare function toolSuccess<T>(payload: T): CallToolResult;
|
|
2
|
+
export declare function toolSuccess<T>(payload: T, compactMode?: boolean): CallToolResult;
|
|
3
3
|
export declare function toolError(error: unknown): CallToolResult;
|
|
4
4
|
//# sourceMappingURL=tool-response.d.ts.map
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
|
-
export function toolSuccess(payload) {
|
|
2
|
+
export function toolSuccess(payload, compactMode = true) {
|
|
3
3
|
const structuredContent = {
|
|
4
4
|
success: true,
|
|
5
5
|
payload,
|
|
6
6
|
};
|
|
7
|
+
// In compact mode, minimize content field to reduce context window usage for AI agents.
|
|
8
|
+
// Claude Code (when compactMode=false) gets full data in content field for better accessibility.
|
|
9
|
+
// Other AI agents benefit from minimal content to save context window tokens.
|
|
10
|
+
const contentText = compactMode
|
|
11
|
+
? "Success. Use structuredContent for full data."
|
|
12
|
+
: JSON.stringify(structuredContent, null, 2);
|
|
7
13
|
return {
|
|
8
14
|
content: [
|
|
9
15
|
{
|
|
10
16
|
type: "text",
|
|
11
|
-
text:
|
|
17
|
+
text: contentText,
|
|
12
18
|
},
|
|
13
19
|
],
|
|
14
20
|
structuredContent,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MappedYoutrackIssueComment, type MappedYoutrackWorkItem } from "./utils/mappers.js";
|
|
2
|
-
import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueCreateInput, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
|
|
2
|
+
import type { ArticleCreateInput, ArticleListPayload, ArticlePayload, ArticleSearchInput, ArticleSearchPayload, ArticleUpdateInput, AttachmentDeleteInput, AttachmentDeletePayload, AttachmentDownloadPayload, AttachmentPayload, AttachmentsListPayload, AttachmentUploadInput, AttachmentUploadPayload, IssueChangeStateInput, IssueChangeStatePayload, IssueCommentsPayload, IssueCommentCreateInput, IssueCommentUpdateInput, IssueCommentUpdatePayload, IssueDetailsPayload, IssueLookupPayload, IssueSearchInput, IssueSearchPayload, IssuesCommentsPayload, IssuesDetailsPayload, IssuesLookupPayload, WorkItemBulkResultPayload, WorkItemDeletePayload, WorkItemInvalidDay, WorkItemReportPayload, WorkItemUsersReportPayload, YoutrackActivityItem, YoutrackConfig, YoutrackCustomField, YoutrackIssueAssignInput, YoutrackIssueComment, YoutrackIssueCreateInput, YoutrackIssueDetails, YoutrackIssueUpdateInput, YoutrackProject, YoutrackProjectListPayload, YoutrackUser, YoutrackUserListPayload, YoutrackWorkItem, YoutrackWorkItemCreateInput, YoutrackWorkItemIdempotentCreateInput, YoutrackWorkItemPeriodCreateInput, YoutrackWorkItemReportOptions, YoutrackWorkItemUpdateInput } from "./types.js";
|
|
3
3
|
export declare class YoutrackClient {
|
|
4
4
|
private readonly config;
|
|
5
5
|
private readonly http;
|
|
@@ -30,10 +30,26 @@ export declare class YoutrackClient {
|
|
|
30
30
|
assignIssue(input: YoutrackIssueAssignInput): Promise<IssueLookupPayload>;
|
|
31
31
|
getIssues(issueIds: string[]): Promise<IssuesLookupPayload>;
|
|
32
32
|
getIssuesDetails(issueIds: string[]): Promise<IssuesDetailsPayload>;
|
|
33
|
+
/**
|
|
34
|
+
* Light version of getIssuesDetails() that fetches only minimal fields (id, idReadable, updated, updater)
|
|
35
|
+
* Used for filtering in user_activity mode to reduce payload size
|
|
36
|
+
*/
|
|
37
|
+
getIssuesDetailsLight(issueIds: string[]): Promise<YoutrackIssueDetails[]>;
|
|
33
38
|
getMultipleIssuesComments(issueIds: string[]): Promise<IssuesCommentsPayload>;
|
|
39
|
+
/**
|
|
40
|
+
* Light version of getMultipleIssuesComments() that fetches only minimal comment fields
|
|
41
|
+
* (id, author.login, created, text) for filtering in user_activity mode to reduce payload size
|
|
42
|
+
*/
|
|
43
|
+
getMultipleIssuesCommentsLight(issueIds: string[]): Promise<Record<string, YoutrackIssueComment[]>>;
|
|
34
44
|
searchIssuesByUserActivity(input: IssueSearchInput): Promise<IssueSearchPayload>;
|
|
35
45
|
private searchIssuesByUserActivitySimple;
|
|
36
46
|
private searchIssuesByUserActivityStrict;
|
|
47
|
+
/**
|
|
48
|
+
* Filters candidate issues by user activity dates (comments, activities, updater)
|
|
49
|
+
* Used in user_activity mode to determine which issues had activity from specified users
|
|
50
|
+
*/
|
|
51
|
+
private filterIssuesByUserActivity;
|
|
52
|
+
private searchIssuesStrictFallbackRequest;
|
|
37
53
|
listWorkItems({ author, startDate, endDate, issueId, top: limit, allUsers, }?: {
|
|
38
54
|
author?: string;
|
|
39
55
|
startDate?: string | number | Date;
|
|
@@ -2,14 +2,17 @@ import axios from "axios";
|
|
|
2
2
|
import FormData from "form-data";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { calculateTotalMinutes, enumerateDateRange, filterWorkingDays, getDayBounds, groupWorkItemsByDate, isWeekend, minutesToHours, parseDateInput, toIsoDateString, validateDateRange, } from "./utils/date.js";
|
|
5
|
-
import { mapAttachment, mapAttachments, mapComment, mapComments, mapIssue, mapIssueDetails, mapWorkItem, mapWorkItems, } from "./utils/mappers.js";
|
|
5
|
+
import { mapAttachment, mapAttachments, mapComment, mapComments, mapIssue, mapIssueBrief, mapIssueDetails, mapWorkItem, mapWorkItems, } from "./utils/mappers.js";
|
|
6
6
|
const DEFAULT_PAGE_SIZE = 200;
|
|
7
7
|
const DEFAULT_EXPECTED_MINUTES = 8 * 60;
|
|
8
8
|
const defaultFields = {
|
|
9
9
|
issue: "id,idReadable,summary,description,wikifiedDescription,usesMarkdown,project(id,shortName,name),parent(id,idReadable),assignee(id,login,name)",
|
|
10
10
|
issueSearch: "id,idReadable,summary,description,wikifiedDescription,usesMarkdown,project(id,shortName,name),parent(id,idReadable),assignee(id,login,name)",
|
|
11
|
+
issueSearchBrief: "id,idReadable,summary,project(id,shortName,name),parent(id,idReadable),assignee(id,login,name)",
|
|
11
12
|
issueDetails: "id,idReadable,summary,description,wikifiedDescription,usesMarkdown,created,updated,resolved,project(id,shortName,name),parent(id,idReadable),assignee(id,login,name),reporter(id,login,name),updater(id,login,name)",
|
|
13
|
+
issueDetailsLight: "id,idReadable,updated,updater(login)",
|
|
12
14
|
comments: "id,text,textPreview,usesMarkdown,author(id,login,name),created,updated",
|
|
15
|
+
commentsLight: "id,author(login),created,text",
|
|
13
16
|
workItem: "id,date,updated,duration(minutes,presentation),text,textPreview,usesMarkdown,description,issue(id,idReadable),author(id,login,name,email)",
|
|
14
17
|
workItems: "id,date,updated,duration(minutes,presentation),text,textPreview,usesMarkdown,description,issue(id,idReadable),author(id,login,name,email)",
|
|
15
18
|
users: "id,login,name,fullName,email",
|
|
@@ -403,6 +406,30 @@ export class YoutrackClient {
|
|
|
403
406
|
throw this.normalizeError(error);
|
|
404
407
|
}
|
|
405
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Light version of getIssuesDetails() that fetches only minimal fields (id, idReadable, updated, updater)
|
|
411
|
+
* Used for filtering in user_activity mode to reduce payload size
|
|
412
|
+
*/
|
|
413
|
+
async getIssuesDetailsLight(issueIds) {
|
|
414
|
+
if (!issueIds.length) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
// Build query: "issue id: BC-123 BC-124 BC-125"
|
|
418
|
+
const query = `issue id: ${issueIds.join(" ")}`;
|
|
419
|
+
try {
|
|
420
|
+
const response = await this.http.get("/api/issues", {
|
|
421
|
+
params: {
|
|
422
|
+
fields: defaultFields.issueDetailsLight,
|
|
423
|
+
query,
|
|
424
|
+
$top: issueIds.length,
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
return response.data;
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
throw this.normalizeError(error);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
406
433
|
async getMultipleIssuesComments(issueIds) {
|
|
407
434
|
if (!issueIds.length) {
|
|
408
435
|
return { commentsByIssue: {}, errors: [] };
|
|
@@ -435,6 +462,36 @@ export class YoutrackClient {
|
|
|
435
462
|
errors: errors.length ? errors : undefined,
|
|
436
463
|
};
|
|
437
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Light version of getMultipleIssuesComments() that fetches only minimal comment fields
|
|
467
|
+
* (id, author.login, created, text) for filtering in user_activity mode to reduce payload size
|
|
468
|
+
*/
|
|
469
|
+
async getMultipleIssuesCommentsLight(issueIds) {
|
|
470
|
+
if (!issueIds.length) {
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
// Make parallel requests for all issues
|
|
474
|
+
const promises = issueIds.map(async (issueId) => {
|
|
475
|
+
try {
|
|
476
|
+
const response = await this.http.get(`/api/issues/${issueId}/comments`, {
|
|
477
|
+
params: { fields: defaultFields.commentsLight },
|
|
478
|
+
});
|
|
479
|
+
return { issueId, comments: response.data, success: true };
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
// Silently ignore errors in light mode - return empty array
|
|
483
|
+
return { issueId, success: false };
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
const results = await Promise.all(promises);
|
|
487
|
+
const commentsByIssue = {};
|
|
488
|
+
for (const result of results) {
|
|
489
|
+
if (result.success) {
|
|
490
|
+
commentsByIssue[result.issueId] = result.comments;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return commentsByIssue;
|
|
494
|
+
}
|
|
438
495
|
async searchIssuesByUserActivity(input) {
|
|
439
496
|
const mode = input.dateFilterMode ?? "issue_updated";
|
|
440
497
|
if (mode === "issue_updated") {
|
|
@@ -467,17 +524,18 @@ export class YoutrackClient {
|
|
|
467
524
|
const query = filterQuery ? `${filterQuery} ${sortPart}` : sortPart;
|
|
468
525
|
const limit = input.limit ?? 100;
|
|
469
526
|
const skip = input.skip ?? 0;
|
|
527
|
+
const briefOutput = input.briefOutput ?? true;
|
|
470
528
|
try {
|
|
471
529
|
const response = await this.http.get("/api/issues", {
|
|
472
530
|
params: {
|
|
473
|
-
fields: defaultFields.issueSearch,
|
|
531
|
+
fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
|
|
474
532
|
query,
|
|
475
533
|
$top: Math.min(limit, DEFAULT_PAGE_SIZE),
|
|
476
534
|
$skip: skip,
|
|
477
535
|
},
|
|
478
536
|
});
|
|
479
537
|
return {
|
|
480
|
-
issues: response.data.map(mapIssue),
|
|
538
|
+
issues: briefOutput ? response.data.map(mapIssueBrief) : response.data.map(mapIssue),
|
|
481
539
|
userLogins: input.userLogins,
|
|
482
540
|
period: {
|
|
483
541
|
startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
|
|
@@ -496,28 +554,31 @@ export class YoutrackClient {
|
|
|
496
554
|
}
|
|
497
555
|
async searchIssuesByUserActivityStrict(input) {
|
|
498
556
|
const filters = [];
|
|
499
|
-
// Build query
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
557
|
+
// Build query without user filters to avoid parser issues on some YT instances.
|
|
558
|
+
// We will fetch by date window only, then apply precise user-activity filtering client-side.
|
|
559
|
+
// Add date range filter (align with simple mode) to avoid YT query parser quirks
|
|
560
|
+
// and to reduce candidate set before precise post-filtering.
|
|
561
|
+
if (input.startDate || input.endDate) {
|
|
562
|
+
const startDateStr = input.startDate ? toIsoDateString(input.startDate) : "1970-01-01";
|
|
563
|
+
const endDateStr = input.endDate ? toIsoDateString(input.endDate) : toIsoDateString(new Date());
|
|
564
|
+
filters.push(`updated: ${startDateStr} .. ${endDateStr}`);
|
|
508
565
|
}
|
|
509
566
|
const sortPart = "sort by: updated desc";
|
|
510
567
|
const filterQuery = filters.length > 0 ? filters.join(" and ") : undefined;
|
|
511
568
|
const query = filterQuery ? `${filterQuery} ${sortPart}` : sortPart;
|
|
512
|
-
|
|
569
|
+
const briefOutput = input.briefOutput ?? true;
|
|
570
|
+
// Get candidate issues (use brief fields to reduce payload)
|
|
513
571
|
try {
|
|
572
|
+
// Fetch candidates by date only (no user filters) to ensure parser compatibility
|
|
514
573
|
const response = await this.http.get("/api/issues", {
|
|
515
574
|
params: {
|
|
516
|
-
fields: defaultFields.issueSearch,
|
|
517
|
-
query,
|
|
575
|
+
fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
|
|
576
|
+
query, // use full query with date filter and sorting
|
|
518
577
|
$top: DEFAULT_PAGE_SIZE,
|
|
519
578
|
},
|
|
520
579
|
});
|
|
580
|
+
// If no data returned, still attempt a fallback without braces
|
|
581
|
+
// No fallback needed here since we purposely avoided user filters in query
|
|
521
582
|
const candidateIssues = response.data;
|
|
522
583
|
if (candidateIssues.length === 0) {
|
|
523
584
|
return {
|
|
@@ -534,86 +595,25 @@ export class YoutrackClient {
|
|
|
534
595
|
},
|
|
535
596
|
};
|
|
536
597
|
}
|
|
537
|
-
// Get issueIds and fetch details + comments
|
|
598
|
+
// Get issueIds and fetch details + comments (light versions for filtering)
|
|
538
599
|
const issueIds = candidateIssues.map((issue) => issue.idReadable);
|
|
539
|
-
const [
|
|
540
|
-
this.
|
|
541
|
-
this.
|
|
600
|
+
const [detailsLight, commentsLight] = await Promise.all([
|
|
601
|
+
this.getIssuesDetailsLight(issueIds),
|
|
602
|
+
this.getMultipleIssuesCommentsLight(issueIds),
|
|
542
603
|
]);
|
|
543
604
|
// Get activities for each issue
|
|
544
605
|
const activitiesPromises = issueIds.map((issueId) => this.getIssueActivities(issueId));
|
|
545
606
|
const activitiesResults = await Promise.all(activitiesPromises);
|
|
546
|
-
const startTimestamp = input.startDate ? new Date(input.startDate).getTime() : 0;
|
|
547
|
-
const endTimestamp = input.endDate ? new Date(input.endDate).getTime() : Date.now();
|
|
548
607
|
// Process each issue to determine lastActivityDate
|
|
549
|
-
const issuesWithActivity =
|
|
550
|
-
for (let i = 0; i < candidateIssues.length; i++) {
|
|
551
|
-
const issue = candidateIssues[i];
|
|
552
|
-
const issueId = issue.idReadable;
|
|
553
|
-
const details = detailsResult.issues.find((d) => d.idReadable === issueId);
|
|
554
|
-
const comments = commentsResult.commentsByIssue[issueId] ?? [];
|
|
555
|
-
const activities = activitiesResults[i] ?? [];
|
|
556
|
-
const dates = [];
|
|
557
|
-
// Check comments from user
|
|
558
|
-
for (const userLogin of input.userLogins) {
|
|
559
|
-
for (const comment of comments) {
|
|
560
|
-
if (comment.author?.login === userLogin && comment.created) {
|
|
561
|
-
const commentDate = new Date(comment.created).getTime();
|
|
562
|
-
if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
|
|
563
|
-
dates.push(commentDate);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
// Check mentions in comment text
|
|
567
|
-
if (comment.text?.includes(`@${userLogin}`) && comment.created) {
|
|
568
|
-
const commentDate = new Date(comment.created).getTime();
|
|
569
|
-
if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
|
|
570
|
-
dates.push(commentDate);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
// Check activities from user or where user is in added/removed
|
|
575
|
-
for (const activity of activities) {
|
|
576
|
-
if (activity.timestamp >= startTimestamp && activity.timestamp <= endTimestamp) {
|
|
577
|
-
// Activity by user
|
|
578
|
-
if (activity.author?.login === userLogin) {
|
|
579
|
-
dates.push(activity.timestamp);
|
|
580
|
-
}
|
|
581
|
-
// User added or removed in field change (e.g., assignee)
|
|
582
|
-
const inAdded = activity.added?.some((v) => v.login === userLogin);
|
|
583
|
-
const inRemoved = activity.removed?.some((v) => v.login === userLogin);
|
|
584
|
-
if (inAdded || inRemoved) {
|
|
585
|
-
dates.push(activity.timestamp);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
// Check if user is updater and updated date is in range
|
|
590
|
-
if (details?.updater?.login === userLogin && details.updated) {
|
|
591
|
-
const updateDate = new Date(details.updated).getTime();
|
|
592
|
-
if (updateDate >= startTimestamp && updateDate <= endTimestamp) {
|
|
593
|
-
dates.push(updateDate);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
if (dates.length > 0) {
|
|
598
|
-
const lastActivityTimestamp = Math.max(...dates);
|
|
599
|
-
const lastActivityDate = new Date(lastActivityTimestamp).toISOString();
|
|
600
|
-
issuesWithActivity.push({
|
|
601
|
-
issue,
|
|
602
|
-
lastActivityDate,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
// Sort by lastActivityDate descending
|
|
607
|
-
issuesWithActivity.sort((a, b) => {
|
|
608
|
-
return new Date(b.lastActivityDate).getTime() - new Date(a.lastActivityDate).getTime();
|
|
609
|
-
});
|
|
608
|
+
const issuesWithActivity = this.filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input);
|
|
610
609
|
// Apply pagination
|
|
611
610
|
const skip = input.skip ?? 0;
|
|
612
611
|
const limit = input.limit ?? 100;
|
|
613
612
|
const paginatedIssues = issuesWithActivity.slice(skip, skip + limit);
|
|
614
|
-
// Map issues with lastActivityDate
|
|
613
|
+
// Map issues with lastActivityDate (use brief mapping if requested)
|
|
614
|
+
const mapperFn = briefOutput ? mapIssueBrief : mapIssue;
|
|
615
615
|
const resultIssues = paginatedIssues.map(({ issue, lastActivityDate }) => ({
|
|
616
|
-
...
|
|
616
|
+
...mapperFn(issue),
|
|
617
617
|
lastActivityDate,
|
|
618
618
|
}));
|
|
619
619
|
return {
|
|
@@ -631,9 +631,153 @@ export class YoutrackClient {
|
|
|
631
631
|
};
|
|
632
632
|
}
|
|
633
633
|
catch (error) {
|
|
634
|
-
|
|
634
|
+
const normalized = this.normalizeError(error);
|
|
635
|
+
// Retry once with a relaxed query (without braces) if YT parser rejects the original
|
|
636
|
+
if ((normalized.message || "").toLowerCase().includes("parse search query")) {
|
|
637
|
+
try {
|
|
638
|
+
const candidateIssues = await this.searchIssuesStrictFallbackRequest(briefOutput, sortPart, input);
|
|
639
|
+
if (candidateIssues.length === 0) {
|
|
640
|
+
return {
|
|
641
|
+
issues: [],
|
|
642
|
+
userLogins: input.userLogins,
|
|
643
|
+
period: {
|
|
644
|
+
startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
|
|
645
|
+
endDate: input.endDate ? toIsoDateString(input.endDate) : undefined,
|
|
646
|
+
},
|
|
647
|
+
pagination: {
|
|
648
|
+
returned: 0,
|
|
649
|
+
limit: input.limit ?? 100,
|
|
650
|
+
skip: input.skip ?? 0,
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
// Re-run the rest of the strict pipeline with the already fetched list
|
|
655
|
+
const issueIds = candidateIssues.map((issue) => issue.idReadable);
|
|
656
|
+
const [detailsLight, commentsLight] = await Promise.all([
|
|
657
|
+
this.getIssuesDetailsLight(issueIds),
|
|
658
|
+
this.getMultipleIssuesCommentsLight(issueIds),
|
|
659
|
+
]);
|
|
660
|
+
const activitiesPromises = issueIds.map((issueId) => this.getIssueActivities(issueId));
|
|
661
|
+
const activitiesResults = await Promise.all(activitiesPromises);
|
|
662
|
+
const issuesWithActivity = this.filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input);
|
|
663
|
+
const skip = input.skip ?? 0;
|
|
664
|
+
const limit = input.limit ?? 100;
|
|
665
|
+
const paginatedIssues = issuesWithActivity.slice(skip, skip + limit);
|
|
666
|
+
const mapperFn = briefOutput ? mapIssueBrief : mapIssue;
|
|
667
|
+
const resultIssues = paginatedIssues.map(({ issue, lastActivityDate }) => ({ ...mapperFn(issue), lastActivityDate }));
|
|
668
|
+
return {
|
|
669
|
+
issues: resultIssues,
|
|
670
|
+
userLogins: input.userLogins,
|
|
671
|
+
period: {
|
|
672
|
+
startDate: input.startDate ? toIsoDateString(input.startDate) : undefined,
|
|
673
|
+
endDate: input.endDate ? toIsoDateString(input.endDate) : undefined,
|
|
674
|
+
},
|
|
675
|
+
pagination: {
|
|
676
|
+
returned: resultIssues.length,
|
|
677
|
+
limit,
|
|
678
|
+
skip,
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
catch (fallbackError) {
|
|
683
|
+
// If fallback also fails, log for debugging and return original error
|
|
684
|
+
console.error("Fallback query also failed:", fallbackError);
|
|
685
|
+
throw normalized;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
throw normalized;
|
|
635
689
|
}
|
|
636
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* Filters candidate issues by user activity dates (comments, activities, updater)
|
|
693
|
+
* Used in user_activity mode to determine which issues had activity from specified users
|
|
694
|
+
*/
|
|
695
|
+
filterIssuesByUserActivity(candidateIssues, detailsLight, commentsLight, activitiesResults, input) {
|
|
696
|
+
const startTimestamp = input.startDate ? new Date(input.startDate).getTime() : 0;
|
|
697
|
+
const endTimestamp = input.endDate ? new Date(input.endDate).getTime() : Date.now();
|
|
698
|
+
const issuesWithActivity = [];
|
|
699
|
+
for (let i = 0; i < candidateIssues.length; i++) {
|
|
700
|
+
const issue = candidateIssues[i];
|
|
701
|
+
const issueId = issue.idReadable;
|
|
702
|
+
const details = detailsLight.find((d) => d.idReadable === issueId);
|
|
703
|
+
const comments = commentsLight[issueId] ?? [];
|
|
704
|
+
const activities = activitiesResults[i] ?? [];
|
|
705
|
+
const dates = [];
|
|
706
|
+
for (const userLogin of input.userLogins) {
|
|
707
|
+
// Check comments from user
|
|
708
|
+
for (const comment of comments) {
|
|
709
|
+
if (comment.author?.login === userLogin && comment.created) {
|
|
710
|
+
const commentDate = comment.created;
|
|
711
|
+
if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
|
|
712
|
+
dates.push(commentDate);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Check mentions in comment text
|
|
716
|
+
if (comment.text?.includes(`@${userLogin}`) && comment.created) {
|
|
717
|
+
const commentDate = comment.created;
|
|
718
|
+
if (commentDate >= startTimestamp && commentDate <= endTimestamp) {
|
|
719
|
+
dates.push(commentDate);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// Check activities from user or where user is in added/removed
|
|
724
|
+
for (const activity of activities) {
|
|
725
|
+
if (activity.timestamp >= startTimestamp && activity.timestamp <= endTimestamp) {
|
|
726
|
+
// Activity by user
|
|
727
|
+
if (activity.author?.login === userLogin) {
|
|
728
|
+
dates.push(activity.timestamp);
|
|
729
|
+
}
|
|
730
|
+
// User added or removed in field change (e.g., assignee)
|
|
731
|
+
const inAdded = activity.added?.some((v) => v.login === userLogin);
|
|
732
|
+
const inRemoved = activity.removed?.some((v) => v.login === userLogin);
|
|
733
|
+
if (inAdded || inRemoved) {
|
|
734
|
+
dates.push(activity.timestamp);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Check if user is updater and updated date is in range
|
|
739
|
+
if (details?.updater?.login === userLogin && details.updated) {
|
|
740
|
+
const updateDate = details.updated;
|
|
741
|
+
if (updateDate >= startTimestamp && updateDate <= endTimestamp) {
|
|
742
|
+
dates.push(updateDate);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (dates.length > 0) {
|
|
747
|
+
const lastActivityTimestamp = Math.max(...dates);
|
|
748
|
+
const lastActivityDate = new Date(lastActivityTimestamp).toISOString();
|
|
749
|
+
issuesWithActivity.push({
|
|
750
|
+
issue,
|
|
751
|
+
lastActivityDate,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// Sort by lastActivityDate descending
|
|
756
|
+
issuesWithActivity.sort((a, b) => new Date(b.lastActivityDate).getTime() - new Date(a.lastActivityDate).getTime());
|
|
757
|
+
return issuesWithActivity;
|
|
758
|
+
}
|
|
759
|
+
// Helper to perform fallback request without braces in user filters
|
|
760
|
+
async searchIssuesStrictFallbackRequest(briefOutput, sortPart, input) {
|
|
761
|
+
const fallbackFilters = [];
|
|
762
|
+
if (input.userLogins.length > 0) {
|
|
763
|
+
const noBraceClauses = input.userLogins.map((login) => `updater: ${login} or mentions: ${login} or reporter: ${login} or assignee: ${login}`);
|
|
764
|
+
fallbackFilters.push(noBraceClauses.length === 1 ? noBraceClauses[0] : `(${noBraceClauses.join(" or ")})`);
|
|
765
|
+
}
|
|
766
|
+
if (input.startDate || input.endDate) {
|
|
767
|
+
const startDateStr = input.startDate ? toIsoDateString(input.startDate) : "1970-01-01";
|
|
768
|
+
const endDateStr = input.endDate ? toIsoDateString(input.endDate) : toIsoDateString(new Date());
|
|
769
|
+
fallbackFilters.push(`updated: ${startDateStr} .. ${endDateStr}`);
|
|
770
|
+
}
|
|
771
|
+
const fallbackQuery = (fallbackFilters.length ? `${fallbackFilters.join(" and ")} ` : "") + sortPart;
|
|
772
|
+
const response = await this.http.get("/api/issues", {
|
|
773
|
+
params: {
|
|
774
|
+
fields: briefOutput ? defaultFields.issueSearchBrief : defaultFields.issueSearch,
|
|
775
|
+
query: fallbackQuery,
|
|
776
|
+
$top: DEFAULT_PAGE_SIZE,
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
return response.data;
|
|
780
|
+
}
|
|
637
781
|
async listWorkItems({ author, startDate, endDate, issueId, top: limit, allUsers = false, } = {}) {
|
|
638
782
|
const requestParams = {
|
|
639
783
|
fields: defaultFields.workItems,
|