@smwb/onreel-git-mcp 0.2.1 → 0.3.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.
@@ -9,6 +9,13 @@ var TRACKER_PAGE_LIMIT_DEFAULT = 50;
9
9
  var TRACKER_ERROR_BODY_MAX = 300;
10
10
  var TRACKER_SECRETS_FILE = "tracker-secrets.json";
11
11
  var TRACKER_LOCAL_AUTHOR = "owner";
12
+ var ISSUE_FILTERS = ["status", "labels", "assignee", "author", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"];
13
+ var ISSUE_SORT_FIELDS = ["created", "updated", "priority", "title", "comments"];
14
+ var COMMENT_FILTERS = ["updatedAfter"];
15
+ var LEGACY_ISSUE_CAPABILITIES = {
16
+ list: { filters: ["status"], clientSide: ["status"], sorts: [] },
17
+ comments: { filters: [], clientSide: [], order: ["asc"] }
18
+ };
12
19
 
13
20
  // src/tracker/tracker.lib.ts
14
21
  var TrackerError = class extends Error {
@@ -50,6 +57,89 @@ function nameOf(table, category) {
50
57
  function priorityName(table, priority) {
51
58
  return Object.entries(table).find(([, value]) => value === priority)?.[0];
52
59
  }
60
+ function usedFilters(query, names) {
61
+ return names.filter((name) => {
62
+ const value = query[name];
63
+ return value !== void 0 && !(Array.isArray(value) && value.length === 0);
64
+ });
65
+ }
66
+ function assertListQuery(subject, capabilities, query, names) {
67
+ const used = usedFilters(query, names);
68
+ for (const name of used) {
69
+ if (!capabilities.filters.includes(name)) throw new TrackerError("unsupported", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
70
+ }
71
+ const sort = query.sort;
72
+ if (sort !== void 0 && !capabilities.sorts.includes(sort.field)) {
73
+ throw new TrackerError("unsupported", `${subject}: \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E ${sort.field} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
74
+ }
75
+ if (sort?.direction !== void 0 && sort.direction !== "asc" && sort.direction !== "desc") {
76
+ throw new TrackerError("invalid_request", `${subject}: \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438 \u2014 asc \u0438\u043B\u0438 desc.`);
77
+ }
78
+ const limit = query.limit;
79
+ if (limit !== void 0 && !(typeof limit === "number" && Number.isSafeInteger(limit) && limit >= 1)) {
80
+ throw new TrackerError("invalid_request", `${subject}: limit \u2014 \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 1.`);
81
+ }
82
+ for (const name of used) {
83
+ const value = query[name];
84
+ if (isTimeFilter(name)) {
85
+ if (typeof value !== "number" || !(Number.isSafeInteger(value) && value >= 0)) {
86
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0432\u0440\u0435\u043C\u044F \u0432 \u043C\u0441, \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 0.`);
87
+ }
88
+ continue;
89
+ }
90
+ if (isArrayFilter(name)) {
91
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
92
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0441\u043F\u0438\u0441\u043E\u043A \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u0445 \u0441\u0442\u0440\u043E\u043A.`);
93
+ }
94
+ continue;
95
+ }
96
+ if (value === null) continue;
97
+ if (typeof value === "string") {
98
+ if (value.trim() === "") throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430.`);
99
+ continue;
100
+ }
101
+ if (Array.isArray(value)) {
102
+ if (value.some((item) => typeof item !== "string" || item.trim() === "")) {
103
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0441\u043F\u0438\u0441\u043E\u043A \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u0445 \u0441\u0442\u0440\u043E\u043A.`);
104
+ }
105
+ continue;
106
+ }
107
+ throw new TrackerError("invalid_request", `${subject}: \u0444\u0438\u043B\u044C\u0442\u0440 ${name} \u2014 \u0441\u0442\u0440\u043E\u043A\u0430, \u0441\u043F\u0438\u0441\u043E\u043A \u0441\u0442\u0440\u043E\u043A \u0438\u043B\u0438 null.`);
108
+ }
109
+ }
110
+ function isTimeFilter(name) {
111
+ return name.endsWith("After") || name.endsWith("Before");
112
+ }
113
+ function isArrayFilter(name) {
114
+ return name === "status" || name === "labels";
115
+ }
116
+ function assertCommentQuery(subject, capabilities, query) {
117
+ assertListQuery(subject, { filters: capabilities.filters, clientSide: capabilities.clientSide, sorts: [] }, query, COMMENT_FILTERS);
118
+ if (query.order !== void 0 && !capabilities.order.includes(query.order)) {
119
+ throw new TrackerError("unsupported", `${subject}: \u043F\u043E\u0440\u044F\u0434\u043E\u043A ${query.order} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
120
+ }
121
+ }
122
+ function issueCapabilitiesOf(definition) {
123
+ return definition.issues ?? LEGACY_ISSUE_CAPABILITIES;
124
+ }
125
+ function isoTime(ms) {
126
+ return new Date(ms).toISOString();
127
+ }
128
+ function inTimeRange(value, after, before) {
129
+ if (after === void 0 && before === void 0) return true;
130
+ if (value === void 0) return false;
131
+ return (after === void 0 || value >= after) && (before === void 0 || value <= before);
132
+ }
133
+ function containsText(texts, needle) {
134
+ const lower = needle.toLowerCase();
135
+ return texts.some((value) => value !== void 0 && value.toLowerCase().includes(lower));
136
+ }
137
+ function quoted(value) {
138
+ return `"${value.replace(/[\\"]/g, "\\$&")}"`;
139
+ }
140
+ function keysFor(table, values) {
141
+ return Object.entries(table).filter(([, value]) => values.includes(value)).map(([key]) => key);
142
+ }
53
143
 
54
144
  // src/tracker/mapping.const.ts
55
145
  var SOURCECRAFT_STATUSES = {
@@ -66,6 +156,12 @@ var SOURCECRAFT_STATUS_SLUGS = {
66
156
  done: "closed",
67
157
  declined: "declined"
68
158
  };
159
+ var SOURCECRAFT_STATUS_FILTER = {
160
+ open: ["open", "paused"],
161
+ in_progress: ["in_progress"],
162
+ done: ["closed"],
163
+ declined: ["declined", "duplicate"]
164
+ };
69
165
  var SOURCECRAFT_PRIORITIES = {
70
166
  trivial: "trivial",
71
167
  minor: "minor",
@@ -130,11 +226,7 @@ async function requestJson(url, request) {
130
226
  throw new TrackerError("failed", redactSecretText(`\u0422\u0440\u0435\u043A\u0435\u0440 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B (${hostOf(url)}): ${error instanceof Error ? error.message : String(error)}`));
131
227
  }
132
228
  const raw = await response.text();
133
- if (!response.ok) {
134
- if (response.status === 404 && request.missingIsIssue === true) throw new TrackerError("unknown_issue", "\u0417\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0442\u0440\u0435\u043A\u0435\u0440\u0435.");
135
- const detail = redactSecretText(raw.replace(/\s+/g, " ").trim()).slice(0, TRACKER_ERROR_BODY_MAX);
136
- throw new TrackerError("failed", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${String(response.status)}${detail === "" ? "" : `: ${detail}`}.`);
137
- }
229
+ if (!response.ok) throw failureOf(url, response, raw, request);
138
230
  if (raw === "") return { body: void 0, headers: response.headers };
139
231
  try {
140
232
  return { body: JSON.parse(raw), headers: response.headers };
@@ -142,6 +234,38 @@ async function requestJson(url, request) {
142
234
  throw new TrackerError("failed", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435 JSON.`);
143
235
  }
144
236
  }
237
+ function failureOf(url, response, raw, request) {
238
+ const status = response.status;
239
+ if (status === 404 && request.missing !== void 0) return new TrackerError(request.missing.failure, request.missing.message);
240
+ if (status === 404 && request.missingIsIssue === true) return new TrackerError("unknown_issue", "\u0417\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0442\u0440\u0435\u043A\u0435\u0440\u0435.");
241
+ const detail = redactSecretText(raw.replace(/\s+/g, " ").trim()).slice(0, TRACKER_ERROR_BODY_MAX);
242
+ const answered = `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u043E\u0442\u0432\u0435\u0442\u0438\u043B ${String(status)}${detail === "" ? "" : `: ${detail}`}.`;
243
+ if (request.conflictOn?.includes(status) === true) return new TrackerError("conflict", answered);
244
+ const secondaryLimit = status === 403 && (response.headers.get("x-ratelimit-remaining")?.trim() === "0" || retryAfterOf(response.headers.get("retry-after")?.trim(), Date.now()) !== void 0);
245
+ if (status === 429 || secondaryLimit) {
246
+ const delay = retryDelayOf(response.headers);
247
+ return new TrackerError("rate_limited", `\u0422\u0440\u0435\u043A\u0435\u0440 ${hostOf(url)} \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u043B \u0447\u0430\u0441\u0442\u043E\u0442\u0443 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432${delay === void 0 ? "" : `, \u043F\u043E\u0432\u0442\u043E\u0440 \u0447\u0435\u0440\u0435\u0437 ${String(delay)} \u0441`}.`);
248
+ }
249
+ return new TrackerError("failed", answered);
250
+ }
251
+ function retryDelayOf(headers, now = Date.now()) {
252
+ const retryAfter = retryAfterOf(headers.get("retry-after")?.trim(), now);
253
+ if (retryAfter !== void 0) return retryAfter;
254
+ for (const name of ["x-ratelimit-reset", "ratelimit-reset"]) {
255
+ const reset = headers.get(name)?.trim();
256
+ if (reset !== void 0 && /^\d+$/.test(reset)) return resetDelay(Number(reset), now);
257
+ }
258
+ return void 0;
259
+ }
260
+ function retryAfterOf(value, now) {
261
+ if (value === void 0 || value === "") return void 0;
262
+ if (/^\d+$/.test(value)) return Number(value);
263
+ const at = Date.parse(value);
264
+ return Number.isFinite(at) ? Math.max(0, Math.ceil((at - now) / 1e3)) : void 0;
265
+ }
266
+ function resetDelay(reset, now) {
267
+ return reset >= 1e9 ? Math.max(0, reset - Math.floor(now / 1e3)) : reset;
268
+ }
145
269
  function hostOf(url) {
146
270
  try {
147
271
  return new URL(url).host;
@@ -179,7 +303,13 @@ function listTrackers() {
179
303
  var DEFAULT_API = "https://api.github.com";
180
304
  var TARGET = /^[\w.-]+\/[\w.-]+$/;
181
305
  var NUMBER = /^[1-9]\d{0,14}$/;
306
+ var LIST_SUBJECT = "\u0417\u0430\u0434\u0430\u0447\u0438 GitHub";
307
+ var COMMENTS_SUBJECT = "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitHub";
182
308
  function githubTracker() {
309
+ const issues = {
310
+ list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "comments"] },
311
+ comments: { filters: ["updatedAfter"], clientSide: [], order: ["asc"] }
312
+ };
183
313
  return {
184
314
  descriptor: {
185
315
  tracker: "github",
@@ -191,13 +321,15 @@ function githubTracker() {
191
321
  ],
192
322
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: true, statuses: ["open", "in_progress", "done", "declined"] }
193
323
  },
194
- create: (context) => createGithubAdapter(context)
324
+ create: (context) => createGithubAdapter(context, issues),
325
+ issues
195
326
  };
196
327
  }
197
- function createGithubAdapter({ connection, fetch }) {
328
+ function createGithubAdapter({ connection, fetch }, capabilities) {
198
329
  const target = requireTarget(connection.target, "owner/repo \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F GitHub");
199
330
  if (!TARGET.test(target)) throw new TrackerError("invalid_request", "target GitHub \u2014 owner/repo.");
200
- const root = `${baseUrl(connection.url, DEFAULT_API)}/repos/${target}/issues`;
331
+ const api = baseUrl(connection.url, DEFAULT_API);
332
+ const root = `${api}/repos/${target}/issues`;
201
333
  const headers = {
202
334
  accept: "application/vnd.github+json",
203
335
  "x-github-api-version": "2022-11-28",
@@ -213,14 +345,13 @@ function createGithubAdapter({ connection, fetch }) {
213
345
  };
214
346
  return {
215
347
  list: async (query) => {
348
+ assertListQuery(LIST_SUBJECT, capabilities.list, query, ISSUE_FILTERS);
349
+ assertCsvLabels(query.labels);
216
350
  const page = pageOf(query.cursor);
217
- const params = new URLSearchParams({ state: stateFilter(query.status), per_page: String(query.limit), page: String(page) });
218
- const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
219
- const items = (Array.isArray(body) ? body : []).filter((item) => isRecord(item) && item.pull_request === void 0).map(issueOf);
220
- return {
221
- items: query.status === void 0 ? items : items.filter((issue) => query.status?.includes(issue.status.category) === true),
222
- .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {}
223
- };
351
+ const search = needsSearch(query);
352
+ const params = search ? searchParams(target, query, page) : issuesParams(query, page);
353
+ const { body, headers: responseHeaders } = await requestJson(`${search ? `${api}/search/issues` : root}?${params.toString()}`, { fetch, headers });
354
+ return issuePageOf(rowsOf(body, search), query, responseHeaders.get("link") ?? "", page);
224
355
  },
225
356
  get: async (key) => issueOf(await rawOf(key)),
226
357
  create: async (input) => {
@@ -239,8 +370,10 @@ function createGithubAdapter({ connection, fetch }) {
239
370
  return issueOf(response.body);
240
371
  },
241
372
  comments: async (key, query) => {
373
+ assertCommentQuery(COMMENTS_SUBJECT, capabilities.comments, query);
242
374
  const page = pageOf(query.cursor);
243
375
  const params = new URLSearchParams({ per_page: String(query.limit), page: String(page) });
376
+ if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
244
377
  const { body, headers: responseHeaders } = await requestJson(`${root}/${number(key)}/comments?${params.toString()}`, { fetch, headers, missingIsIssue: true });
245
378
  return { items: (Array.isArray(body) ? body : []).map(commentOf), .../rel="next"/.test(responseHeaders.get("link") ?? "") ? { nextCursor: String(page + 1) } : {} };
246
379
  },
@@ -257,6 +390,68 @@ function stateFilter(status) {
257
390
  if (status.every((item) => item === "done" || item === "declined")) return "closed";
258
391
  return "all";
259
392
  }
393
+ function needsSearch(query) {
394
+ return query.search !== void 0 || query.createdAfter !== void 0 || query.createdBefore !== void 0 || query.updatedBefore !== void 0;
395
+ }
396
+ function assertCsvLabels(labels) {
397
+ if (labels?.some((label) => label.includes(",")) === true) throw new TrackerError("invalid_request", `${LIST_SUBJECT}: \u043C\u0435\u0442\u043A\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0437\u0430\u043F\u044F\u0442\u0443\u044E.`);
398
+ }
399
+ function issuesParams(query, page) {
400
+ const params = new URLSearchParams({ state: stateFilter(query.status), per_page: String(query.limit), page: String(page) });
401
+ if (query.labels !== void 0 && query.labels.length > 0) params.set("labels", query.labels.join(","));
402
+ if (query.assignee !== void 0) params.set("assignee", query.assignee ?? "none");
403
+ if (query.author !== void 0) params.set("creator", query.author);
404
+ if (query.updatedAfter !== void 0) params.set("since", isoTime(query.updatedAfter));
405
+ if (query.sort !== void 0) {
406
+ params.set("sort", query.sort.field);
407
+ params.set("direction", query.sort.direction ?? "desc");
408
+ }
409
+ return params;
410
+ }
411
+ function searchParams(target, query, page) {
412
+ const params = new URLSearchParams({ q: searchQuery(target, query), per_page: String(query.limit), page: String(page) });
413
+ const sort = query.sort;
414
+ params.set("sort", sort?.field ?? "created");
415
+ params.set("order", sort?.direction ?? "desc");
416
+ return params;
417
+ }
418
+ function searchQuery(target, query) {
419
+ const parts = [`repo:${target}`, "is:issue"];
420
+ const state = searchStateFilter(query.status);
421
+ if (state !== void 0) parts.push(state);
422
+ for (const label of query.labels ?? []) parts.push(`label:${quoted(label)}`);
423
+ if (query.assignee !== void 0) parts.push(query.assignee === null ? "no:assignee" : `assignee:${quoted(query.assignee)}`);
424
+ if (query.author !== void 0) parts.push(`author:${quoted(query.author)}`);
425
+ const created = timeRangeQualifier("created", query.createdAfter, query.createdBefore);
426
+ if (created !== void 0) parts.push(created);
427
+ const updated = timeRangeQualifier("updated", query.updatedAfter, query.updatedBefore);
428
+ if (updated !== void 0) parts.push(updated);
429
+ if (query.search !== void 0) parts.push(quoted(query.search));
430
+ return parts.join(" ");
431
+ }
432
+ function timeRangeQualifier(field, after, before) {
433
+ if (after !== void 0 && before !== void 0) return `${field}:${isoTime(after)}..${isoTime(before)}`;
434
+ if (after !== void 0) return `${field}:>=${isoTime(after)}`;
435
+ if (before !== void 0) return `${field}:<=${isoTime(before)}`;
436
+ return void 0;
437
+ }
438
+ function rowsOf(body, search) {
439
+ if (!search) return Array.isArray(body) ? body : [];
440
+ return isRecord(body) && Array.isArray(body.items) ? body.items : [];
441
+ }
442
+ function searchStateFilter(status) {
443
+ if (status === void 0) return void 0;
444
+ if (status.every((item) => item === "open" || item === "in_progress")) return "is:open";
445
+ if (status.every((item) => item === "done" || item === "declined")) return "is:closed";
446
+ return void 0;
447
+ }
448
+ function issuePageOf(rows, query, link, page) {
449
+ const items = rows.filter((row) => isRecord(row) && row.pull_request === void 0).map(issueOf);
450
+ return {
451
+ items: query.status === void 0 ? items : items.filter((issue) => query.status?.includes(issue.status.category) === true),
452
+ .../rel="next"/.test(link) ? { nextCursor: String(page + 1) } : {}
453
+ };
454
+ }
260
455
  function composeLabels(plain, priority, status) {
261
456
  const priorityLabel = priority === void 0 ? void 0 : priorityName(GITHUB_PRIORITY_LABELS, priority);
262
457
  return [...plain, ...priorityLabel === void 0 ? [] : [priorityLabel], ...status === "in_progress" ? [GITHUB_IN_PROGRESS_LABEL] : []];
@@ -308,6 +503,16 @@ function commentOf(raw) {
308
503
  var DEFAULT_URL = "https://gitlab.com";
309
504
  var TARGET2 = /^(\d{1,15}|[\w.-]+(\/[\w.-]+)+)$/;
310
505
  var IID = /^[1-9]\d{0,14}$/;
506
+ var ISSUE_CAPABILITIES = {
507
+ list: { filters: ISSUE_FILTERS, clientSide: ["status"], sorts: ["created", "updated", "priority", "title"] },
508
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
509
+ };
510
+ var ORDER_BY = {
511
+ created: "created_at",
512
+ updated: "updated_at",
513
+ priority: "priority",
514
+ title: "title"
515
+ };
311
516
  function gitlabTracker() {
312
517
  return {
313
518
  descriptor: {
@@ -320,7 +525,8 @@ function gitlabTracker() {
320
525
  ],
321
526
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: false, statuses: ["open", "in_progress", "done"] }
322
527
  },
323
- create: (context) => createGitlabAdapter(context)
528
+ create: (context) => createGitlabAdapter(context),
529
+ issues: ISSUE_CAPABILITIES
324
530
  };
325
531
  }
326
532
  function createGitlabAdapter({ connection, fetch }) {
@@ -335,8 +541,26 @@ function createGitlabAdapter({ connection, fetch }) {
335
541
  const currentOf = async (key) => issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, missingIsIssue: true })).body);
336
542
  return {
337
543
  list: async (query) => {
544
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 GitLab", ISSUE_CAPABILITIES.list, query, ISSUE_FILTERS);
338
545
  const page = pageOf2(query.cursor);
339
- const params = new URLSearchParams({ state: stateFilter2(query.status), per_page: String(query.limit), page: String(page), order_by: "created_at", sort: "desc" });
546
+ const params = new URLSearchParams({
547
+ state: stateFilter2(query.status),
548
+ per_page: String(query.limit),
549
+ page: String(page),
550
+ order_by: query.sort === void 0 ? "created_at" : ORDER_BY[query.sort.field] ?? "created_at",
551
+ sort: query.sort?.direction ?? "desc"
552
+ });
553
+ if (query.labels !== void 0 && query.labels.length > 0) {
554
+ if (query.labels.some((label) => label.includes(","))) throw new TrackerError("invalid_request", "\u0417\u0430\u0434\u0430\u0447\u0438 GitLab: \u043C\u0435\u0442\u043A\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0437\u0430\u043F\u044F\u0442\u0443\u044E.");
555
+ params.set("labels", query.labels.join(","));
556
+ }
557
+ if (query.assignee !== void 0) params.set(query.assignee === null ? "assignee_id" : "assignee_username", query.assignee === null ? "None" : query.assignee);
558
+ if (query.author !== void 0) params.set("author_username", query.author);
559
+ if (query.search !== void 0) params.set("search", query.search);
560
+ if (query.createdAfter !== void 0) params.set("created_after", isoTime(query.createdAfter));
561
+ if (query.createdBefore !== void 0) params.set("created_before", isoTime(query.createdBefore));
562
+ if (query.updatedAfter !== void 0) params.set("updated_after", isoTime(query.updatedAfter));
563
+ if (query.updatedBefore !== void 0) params.set("updated_before", isoTime(query.updatedBefore));
340
564
  const { body, headers: responseHeaders } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
341
565
  const items = (Array.isArray(body) ? body : []).map(issueOf2);
342
566
  const next = responseHeaders.get("x-next-page");
@@ -356,11 +580,12 @@ function createGitlabAdapter({ connection, fetch }) {
356
580
  return issueOf2((await requestJson(`${root}/${iid(key)}`, { fetch, headers, method: "PUT", body: patchBody2(patch, current), missingIsIssue: true })).body);
357
581
  },
358
582
  comments: async (key, query) => {
583
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 GitLab", ISSUE_CAPABILITIES.comments, query);
359
584
  const page = pageOf2(query.cursor);
360
- const params = new URLSearchParams({ sort: "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
585
+ const params = new URLSearchParams({ sort: query.order ?? "asc", order_by: "created_at", per_page: String(query.limit), page: String(page) });
361
586
  const { body, headers: responseHeaders } = await requestJson(`${root}/${iid(key)}/notes?${params.toString()}`, { fetch, headers, missingIsIssue: true });
362
587
  const next = responseHeaders.get("x-next-page");
363
- const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true).map(commentOf2);
588
+ const items = (Array.isArray(body) ? body : []).filter((note) => isRecord(note) && note.system !== true && inTimeRange(toTimestamp(note.updated_at), query.updatedAfter, void 0)).map(commentOf2);
364
589
  return { items, ...next === null || next === "" ? {} : { nextCursor: next } };
365
590
  },
366
591
  comment: async (key, body) => commentOf2((await requestJson(`${root}/${iid(key)}/notes`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
@@ -430,6 +655,11 @@ var PROJECT_KEY = /^[A-Z][A-Z0-9_]{0,49}$/;
430
655
  var ISSUE_KEY = /^[A-Z][A-Z0-9_]{0,49}-[1-9]\d{0,14}$/;
431
656
  var FIELDS = "summary,description,status,priority,labels,assignee,created,updated";
432
657
  var CATEGORY_JQL = { open: '"To Do"', in_progress: '"In Progress"', done: "Done" };
658
+ var ISSUE_CAPABILITIES2 = {
659
+ list: { filters: ISSUE_FILTERS, clientSide: [], sorts: ["created", "updated", "priority", "title"] },
660
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
661
+ };
662
+ var JIRA_SORT_FIELDS = { created: "created", updated: "updated", priority: "priority", title: "summary" };
433
663
  function jiraTracker() {
434
664
  return {
435
665
  descriptor: {
@@ -443,7 +673,8 @@ function jiraTracker() {
443
673
  ],
444
674
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: false, statuses: ["open", "in_progress", "done"] }
445
675
  },
446
- create: (context) => createJiraAdapter(context)
676
+ create: (context) => createJiraAdapter(context),
677
+ issues: ISSUE_CAPABILITIES2
447
678
  };
448
679
  }
449
680
  function createJiraAdapter({ connection, fetch }) {
@@ -462,7 +693,8 @@ function createJiraAdapter({ connection, fetch }) {
462
693
  const load = async (value) => toIssue2((await requestJson(`${api}/issue/${key(value)}?fields=${FIELDS}`, { fetch, headers, missingIsIssue: true })).body);
463
694
  return {
464
695
  list: async (query) => {
465
- const jql = listJql(project, query.status);
696
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 Jira", ISSUE_CAPABILITIES2.list, query, ISSUE_FILTERS);
697
+ const jql = listJql(project, query);
466
698
  if (jql === void 0) return { items: [] };
467
699
  if (query.cursor?.startsWith("s:") !== true) {
468
700
  const params2 = new URLSearchParams({ jql, fields: FIELDS, maxResults: String(query.limit) });
@@ -507,23 +739,44 @@ function createJiraAdapter({ connection, fetch }) {
507
739
  return load(value);
508
740
  },
509
741
  comments: async (value, query) => {
742
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 Jira", ISSUE_CAPABILITIES2.comments, query);
510
743
  const startAt = query.cursor === void 0 ? 0 : Number(query.cursor);
511
- const params = new URLSearchParams({ startAt: String(Number.isInteger(startAt) && startAt >= 0 ? startAt : 0), maxResults: String(query.limit), orderBy: "created" });
744
+ const params = new URLSearchParams({ startAt: String(Number.isInteger(startAt) && startAt >= 0 ? startAt : 0), maxResults: String(query.limit), orderBy: query.order === "desc" ? "-created" : "created" });
512
745
  const { body } = await requestJson(`${api}/issue/${key(value)}/comment?${params.toString()}`, { fetch, headers, missingIsIssue: true });
513
746
  const record = isRecord(body) ? body : {};
514
- const items = Array.isArray(record.comments) ? record.comments : [];
747
+ const raw = Array.isArray(record.comments) ? record.comments : [];
748
+ const items = raw.filter((item) => inTimeRange(toTimestamp(isRecord(item) ? item.updated : void 0), query.updatedAfter, void 0));
515
749
  const total = typeof record.total === "number" ? record.total : 0;
516
750
  const at = typeof record.startAt === "number" ? record.startAt : 0;
517
- return { items: items.map(commentOf3), ...at + items.length < total && items.length > 0 ? { nextCursor: String(at + items.length) } : {} };
751
+ return { items: items.map(commentOf3), ...at + raw.length < total && raw.length > 0 ? { nextCursor: String(at + raw.length) } : {} };
518
752
  },
519
753
  comment: async (value, body) => commentOf3((await requestJson(`${api}/issue/${key(value)}/comment`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
520
754
  };
521
755
  }
522
- function listJql(project, status) {
523
- const base = `project = "${project}"`;
524
- if (status === void 0) return `${base} ORDER BY created DESC`;
525
- const names = status.flatMap((item) => CATEGORY_JQL[item] ?? []);
526
- return names.length === 0 ? void 0 : `${base} AND statusCategory in (${names.join(", ")}) ORDER BY created DESC`;
756
+ function listJql(project, query) {
757
+ const clauses = [];
758
+ if (query.status !== void 0) {
759
+ const names = query.status.flatMap((item) => CATEGORY_JQL[item] ?? []);
760
+ if (names.length === 0) return void 0;
761
+ clauses.push(`statusCategory in (${names.join(", ")})`);
762
+ }
763
+ for (const label of query.labels ?? []) clauses.push(`labels = ${quoted(label.replace(/\s+/g, "-"))}`);
764
+ if (query.assignee !== void 0) clauses.push(query.assignee === null ? "assignee is EMPTY" : `assignee = ${quoted(query.assignee)}`);
765
+ if (query.author !== void 0) clauses.push(`reporter = ${quoted(query.author)}`);
766
+ if (query.search !== void 0) clauses.push(`text ~ ${quoted(query.search)}`);
767
+ if (query.createdAfter !== void 0) clauses.push(`created >= ${quoted(jiraTime(query.createdAfter, "down"))}`);
768
+ if (query.createdBefore !== void 0) clauses.push(`created <= ${quoted(jiraTime(query.createdBefore, "up"))}`);
769
+ if (query.updatedAfter !== void 0) clauses.push(`updated >= ${quoted(jiraTime(query.updatedAfter, "down"))}`);
770
+ if (query.updatedBefore !== void 0) clauses.push(`updated <= ${quoted(jiraTime(query.updatedBefore, "up"))}`);
771
+ const sort = query.sort;
772
+ const field = sort === void 0 ? "created" : sort.field;
773
+ const direction = sort?.direction ?? "desc";
774
+ return `${[`project = ${quoted(project)}`, ...clauses].join(" AND ")} ORDER BY ${JIRA_SORT_FIELDS[field]} ${direction.toUpperCase()}`;
775
+ }
776
+ function jiraTime(ms, outward) {
777
+ const at = new Date(outward === "down" ? Math.floor(ms / 6e4) * 6e4 : Math.ceil(ms / 6e4) * 6e4);
778
+ const pad = (value) => String(value).padStart(2, "0");
779
+ return `${String(at.getUTCFullYear())}/${pad(at.getUTCMonth() + 1)}/${pad(at.getUTCDate())} ${pad(at.getUTCHours())}:${pad(at.getUTCMinutes())}`;
527
780
  }
528
781
  function fieldsOf(patch) {
529
782
  return {
@@ -578,6 +831,14 @@ function commentOf3(raw) {
578
831
 
579
832
  // src/tracker/local.lib.ts
580
833
  var STATUS_NAMES = { open: "Open", in_progress: "In progress", done: "Done", declined: "Declined" };
834
+ var ISSUE_CAPABILITIES3 = {
835
+ list: {
836
+ filters: ["status", "labels", "assignee", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
837
+ clientSide: ["labels", "assignee", "search", "createdAfter", "createdBefore", "updatedAfter", "updatedBefore"],
838
+ sorts: []
839
+ },
840
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc"] }
841
+ };
581
842
  function localTracker() {
582
843
  return {
583
844
  descriptor: {
@@ -586,6 +847,7 @@ function localTracker() {
586
847
  fields: [],
587
848
  features: { create: true, update: true, comment: true, priority: true, labels: true, assignee: true, statuses: [...ALL_STATUSES] }
588
849
  },
850
+ issues: ISSUE_CAPABILITIES3,
589
851
  create: (context) => createLocalAdapter(context)
590
852
  };
591
853
  }
@@ -600,9 +862,11 @@ function createLocalAdapter({ store, projectId, author }) {
600
862
  return row;
601
863
  };
602
864
  return {
603
- list: (query) => {
865
+ // list и comments async: отказ проверки запроса — rejected promise, как у остальных адаптеров.
866
+ list: async (query) => {
867
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 Local", ISSUE_CAPABILITIES3.list, query, ISSUE_FILTERS);
604
868
  const page = store.listLocalIssues(projectId, { limit: query.limit, ...query.cursor === void 0 ? {} : { cursor: query.cursor }, ...query.status === void 0 ? {} : { status: query.status } });
605
- return Promise.resolve({ items: page.items.map(toIssue), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } });
869
+ return { items: page.items.filter((row) => matchesIssueQuery(row, query)).map(toIssue), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } };
606
870
  },
607
871
  get: (key) => Promise.resolve(toIssue(mustGet(key))),
608
872
  create: (input) => Promise.resolve(toIssue(store.createLocalIssue(projectId, input))),
@@ -611,13 +875,22 @@ function createLocalAdapter({ store, projectId, author }) {
611
875
  if (row === void 0) throw new TrackerError("unknown_issue", `\u0412 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u043C \u0442\u0440\u0435\u043A\u0435\u0440\u0435 \u043D\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 ${key}.`);
612
876
  return Promise.resolve(toIssue(row));
613
877
  },
614
- comments: (key, query) => {
878
+ comments: async (key, query) => {
879
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 Local", ISSUE_CAPABILITIES3.comments, query);
615
880
  const page = store.listLocalComments(projectId, mustGet(key).number, { limit: query.limit, ...query.cursor === void 0 ? {} : { cursor: query.cursor } });
616
- return Promise.resolve({ items: page.items.map(toComment), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } });
881
+ const items = query.updatedAfter === void 0 ? page.items : page.items.filter((row) => inTimeRange(row.createdAt, query.updatedAfter, void 0));
882
+ return { items: items.map(toComment), ...page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor } };
617
883
  },
618
884
  comment: (key, body) => Promise.resolve(toComment(store.addLocalComment(projectId, mustGet(key).number, body, author)))
619
885
  };
620
886
  }
887
+ function matchesIssueQuery(row, query) {
888
+ if (query.labels !== void 0 && !query.labels.every((label) => row.labels.includes(label))) return false;
889
+ const assignee = query.assignee;
890
+ if (assignee !== void 0 && (assignee === null ? row.assignee !== void 0 : row.assignee !== assignee)) return false;
891
+ if (query.search !== void 0 && !containsText([row.title, row.description], query.search)) return false;
892
+ return inTimeRange(row.createdAt, query.createdAfter, query.createdBefore) && inTimeRange(row.updatedAt, query.updatedAfter, query.updatedBefore);
893
+ }
621
894
  function toIssue(row) {
622
895
  return {
623
896
  key: String(row.number),
@@ -641,6 +914,16 @@ var DEFAULT_API2 = "https://api.sourcecraft.tech";
641
914
  var DEFAULT_WEB = "https://sourcecraft.dev";
642
915
  var TARGET3 = /^[\w.-]+\/[\w.-]+$/;
643
916
  var SLUG = /^[\w-]{1,100}$/;
917
+ var ISSUE_SORT_FIELDS_UPSTREAM = {
918
+ created: "created_at",
919
+ updated: "updated_at",
920
+ priority: "priority",
921
+ title: "title"
922
+ };
923
+ var ISSUE_CAPABILITIES4 = {
924
+ list: { filters: ISSUE_FILTERS, clientSide: ["assignee"], sorts: ["created", "updated", "priority", "title"] },
925
+ comments: { filters: COMMENT_FILTERS, clientSide: COMMENT_FILTERS, order: ["asc", "desc"] }
926
+ };
644
927
  function sourcecraftTracker() {
645
928
  return {
646
929
  descriptor: {
@@ -653,6 +936,7 @@ function sourcecraftTracker() {
653
936
  ],
654
937
  features: { create: true, update: true, comment: true, priority: true, labels: false, assignee: false, statuses: [...ALL_STATUSES] }
655
938
  },
939
+ issues: ISSUE_CAPABILITIES4,
656
940
  create: (context) => createSourceCraftAdapter(context)
657
941
  };
658
942
  }
@@ -670,12 +954,18 @@ function createSourceCraftAdapter({ connection, fetch }) {
670
954
  const toIssue2 = (raw) => issueOf4(raw, web);
671
955
  return {
672
956
  list: async (query) => {
957
+ assertListQuery("\u0417\u0430\u0434\u0430\u0447\u0438 SourceCraft", ISSUE_CAPABILITIES4.list, query, ISSUE_FILTERS);
673
958
  const params = new URLSearchParams({ page_size: String(query.limit) });
674
959
  if (query.cursor !== void 0) params.set("page_token", query.cursor);
960
+ const filter = issueFilterOf(query);
961
+ if (filter !== void 0) params.set("filter", filter);
962
+ if (query.sort !== void 0) params.set("sort_by", `${query.sort.direction === "asc" ? "" : "-"}${issueSortFieldOf(query.sort.field)}`);
675
963
  const { body } = await requestJson(`${root}?${params.toString()}`, { fetch, headers });
676
964
  const record = isRecord(body) ? body : {};
677
- const issues = (Array.isArray(record.issues) ? record.issues : []).map(toIssue2);
678
- return { items: query.status === void 0 ? issues : issues.filter((issue) => query.status?.includes(issue.status.category) === true), ...nextOf(record.next_page_token) };
965
+ let issues = (Array.isArray(record.issues) ? record.issues : []).map(toIssue2);
966
+ if (query.status !== void 0) issues = issues.filter((issue) => query.status?.includes(issue.status.category) === true);
967
+ if (query.assignee === null) issues = issues.filter((issue) => issue.assignee === void 0);
968
+ return { items: issues, ...nextOf(record.next_page_token) };
679
969
  },
680
970
  get: async (key) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, missingIsIssue: true })).body),
681
971
  create: async (input) => {
@@ -684,11 +974,15 @@ function createSourceCraftAdapter({ connection, fetch }) {
684
974
  },
685
975
  update: async (key, patch) => toIssue2((await requestJson(`${root}/${slug(key)}`, { fetch, headers, method: "PATCH", body: patchBody3(patch), missingIsIssue: true })).body),
686
976
  comments: async (key, query) => {
977
+ assertCommentQuery("\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 SourceCraft", ISSUE_CAPABILITIES4.comments, query);
687
978
  const params = new URLSearchParams({ page_size: String(query.limit) });
688
979
  if (query.cursor !== void 0) params.set("page_token", query.cursor);
980
+ if (query.order !== void 0) params.set("sort_by", query.order === "asc" ? "created_at" : "-created_at");
689
981
  const { body } = await requestJson(`${root}/${slug(key)}/comments?${params.toString()}`, { fetch, headers, missingIsIssue: true });
690
982
  const record = isRecord(body) ? body : {};
691
- return { items: (Array.isArray(record.issue_comments) ? record.issue_comments : []).map(commentOf4), ...nextOf(record.next_page_token) };
983
+ const rawComments = Array.isArray(record.issue_comments) ? record.issue_comments : [];
984
+ const filtered = query.updatedAfter === void 0 ? rawComments : rawComments.filter((raw) => inTimeRange(toTimestamp(isRecord(raw) ? raw.updated_at : void 0), query.updatedAfter, void 0));
985
+ return { items: filtered.map(commentOf4), ...nextOf(record.next_page_token) };
692
986
  },
693
987
  comment: async (key, body) => commentOf4((await requestJson(`${root}/${slug(key)}/comments`, { fetch, headers, method: "POST", body: { body }, missingIsIssue: true })).body)
694
988
  };
@@ -705,6 +999,27 @@ function nextOf(token) {
705
999
  const value = text(token);
706
1000
  return value === void 0 ? {} : { nextCursor: value };
707
1001
  }
1002
+ function issueSortFieldOf(field) {
1003
+ const upstream = ISSUE_SORT_FIELDS_UPSTREAM[field];
1004
+ if (upstream === void 0) throw new TrackerError("unsupported", `\u0417\u0430\u0434\u0430\u0447\u0438 SourceCraft: \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E ${field} \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F.`);
1005
+ return upstream;
1006
+ }
1007
+ function issueFilterOf(query) {
1008
+ const clauses = [];
1009
+ if (query.status !== void 0 && query.status.length > 0) {
1010
+ const slugs = [...new Set(query.status.flatMap((category) => SOURCECRAFT_STATUS_FILTER[category]))];
1011
+ clauses.push(`status in (${slugs.join(", ")})`);
1012
+ }
1013
+ for (const label of query.labels ?? []) clauses.push(`label_slug=${quoted(label)}`);
1014
+ if (typeof query.assignee === "string") clauses.push(`assignee_slug=${quoted(query.assignee)}`);
1015
+ if (query.author !== void 0) clauses.push(`author_slug=${quoted(query.author)}`);
1016
+ if (query.search !== void 0) clauses.push(`title=${quoted(query.search)}`);
1017
+ if (query.createdAfter !== void 0) clauses.push(`created_at>=${quoted(isoTime(query.createdAfter))}`);
1018
+ if (query.createdBefore !== void 0) clauses.push(`created_at<=${quoted(isoTime(query.createdBefore))}`);
1019
+ if (query.updatedAfter !== void 0) clauses.push(`updated_at>=${quoted(isoTime(query.updatedAfter))}`);
1020
+ if (query.updatedBefore !== void 0) clauses.push(`updated_at<=${quoted(isoTime(query.updatedBefore))}`);
1021
+ return clauses.length === 0 ? void 0 : clauses.join(" and ");
1022
+ }
708
1023
  function issueOf4(raw, web) {
709
1024
  const item = isRecord(raw) ? raw : {};
710
1025
  const status = isRecord(item.status) ? item.status : {};
@@ -794,17 +1109,22 @@ function deleteTrackerToken(dataDir, projectId, sink = defaultSecretSink) {
794
1109
  }
795
1110
  export {
796
1111
  ALL_STATUSES,
1112
+ COMMENT_FILTERS,
797
1113
  GITHUB_CLOSE_REASONS,
798
1114
  GITHUB_IN_PROGRESS_LABEL,
799
1115
  GITHUB_PRIORITY_LABELS,
800
1116
  GITLAB_IN_PROGRESS_LABEL,
801
1117
  GITLAB_PRIORITY_LABELS,
1118
+ ISSUE_FILTERS,
1119
+ ISSUE_SORT_FIELDS,
802
1120
  JIRA_ISSUE_TYPE,
803
1121
  JIRA_PRIORITIES,
804
1122
  JIRA_PRIORITY_NAMES,
805
1123
  JIRA_STATUS_CATEGORIES,
1124
+ LEGACY_ISSUE_CAPABILITIES,
806
1125
  SOURCECRAFT_PRIORITIES,
807
1126
  SOURCECRAFT_STATUSES,
1127
+ SOURCECRAFT_STATUS_FILTER,
808
1128
  SOURCECRAFT_STATUS_SLUGS,
809
1129
  TRACKER_ERROR_BODY_MAX,
810
1130
  TRACKER_LOCAL_AUTHOR,
@@ -812,20 +1132,28 @@ export {
812
1132
  TRACKER_SECRETS_FILE,
813
1133
  TRACKER_TIMEOUT_MS,
814
1134
  TrackerError,
1135
+ assertCommentQuery,
1136
+ assertListQuery,
815
1137
  baseUrl,
816
1138
  clipText,
1139
+ containsText,
817
1140
  deleteTrackerToken,
818
1141
  findTracker,
819
1142
  githubTracker,
820
1143
  gitlabTracker,
821
1144
  hasTrackerToken,
1145
+ inTimeRange,
822
1146
  isRecord,
1147
+ isoTime,
1148
+ issueCapabilitiesOf,
823
1149
  jiraTracker,
1150
+ keysFor,
824
1151
  listTrackers,
825
1152
  localTracker,
826
1153
  lookup,
827
1154
  nameOf,
828
1155
  priorityName,
1156
+ quoted,
829
1157
  readTrackerToken,
830
1158
  registerBuiltinTrackers,
831
1159
  registerTracker,
@@ -835,6 +1163,7 @@ export {
835
1163
  stringList,
836
1164
  text,
837
1165
  toTimestamp,
1166
+ usedFilters,
838
1167
  writeTrackerToken
839
1168
  };
840
1169
  //# sourceMappingURL=index.js.map