@muzzamil-khan/redmine-agent-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +44 -0
  2. package/dist/config/env.d.ts +12 -0
  3. package/dist/config/env.d.ts.map +1 -0
  4. package/dist/config/env.js +32 -0
  5. package/dist/config/env.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +65 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/mcp/server.d.ts +3 -0
  11. package/dist/mcp/server.d.ts.map +1 -0
  12. package/dist/mcp/server.js +162 -0
  13. package/dist/mcp/server.js.map +1 -0
  14. package/dist/prompts/operationalRules.d.ts +2 -0
  15. package/dist/prompts/operationalRules.d.ts.map +1 -0
  16. package/dist/prompts/operationalRules.js +12 -0
  17. package/dist/prompts/operationalRules.js.map +1 -0
  18. package/dist/prompts/questionFlows.d.ts +8 -0
  19. package/dist/prompts/questionFlows.d.ts.map +1 -0
  20. package/dist/prompts/questionFlows.js +27 -0
  21. package/dist/prompts/questionFlows.js.map +1 -0
  22. package/dist/services/redmine.d.ts +44 -0
  23. package/dist/services/redmine.d.ts.map +1 -0
  24. package/dist/services/redmine.js +139 -0
  25. package/dist/services/redmine.js.map +1 -0
  26. package/dist/types/workflow.d.ts +22 -0
  27. package/dist/types/workflow.d.ts.map +1 -0
  28. package/dist/types/workflow.js +2 -0
  29. package/dist/types/workflow.js.map +1 -0
  30. package/dist/utils/elicitation.d.ts +9 -0
  31. package/dist/utils/elicitation.d.ts.map +1 -0
  32. package/dist/utils/elicitation.js +15 -0
  33. package/dist/utils/elicitation.js.map +1 -0
  34. package/dist/utils/fetchUserLogs.d.ts +41 -0
  35. package/dist/utils/fetchUserLogs.d.ts.map +1 -0
  36. package/dist/utils/fetchUserLogs.js +47 -0
  37. package/dist/utils/fetchUserLogs.js.map +1 -0
  38. package/dist/utils/logTargets.d.ts +23 -0
  39. package/dist/utils/logTargets.d.ts.map +1 -0
  40. package/dist/utils/logTargets.js +33 -0
  41. package/dist/utils/logTargets.js.map +1 -0
  42. package/dist/utils/mcpResponse.d.ts +8 -0
  43. package/dist/utils/mcpResponse.d.ts.map +1 -0
  44. package/dist/utils/mcpResponse.js +11 -0
  45. package/dist/utils/mcpResponse.js.map +1 -0
  46. package/dist/utils/timeframe.d.ts +9 -0
  47. package/dist/utils/timeframe.d.ts.map +1 -0
  48. package/dist/utils/timeframe.js +27 -0
  49. package/dist/utils/timeframe.js.map +1 -0
  50. package/dist/utils/validation.d.ts +2 -0
  51. package/dist/utils/validation.d.ts.map +1 -0
  52. package/dist/utils/validation.js +6 -0
  53. package/dist/utils/validation.js.map +1 -0
  54. package/dist/workflows/addLogCommentWorkflow.d.ts +15 -0
  55. package/dist/workflows/addLogCommentWorkflow.d.ts.map +1 -0
  56. package/dist/workflows/addLogCommentWorkflow.js +230 -0
  57. package/dist/workflows/addLogCommentWorkflow.js.map +1 -0
  58. package/dist/workflows/checkCommentPermissionsWorkflow.d.ts +18 -0
  59. package/dist/workflows/checkCommentPermissionsWorkflow.d.ts.map +1 -0
  60. package/dist/workflows/checkCommentPermissionsWorkflow.js +98 -0
  61. package/dist/workflows/checkCommentPermissionsWorkflow.js.map +1 -0
  62. package/dist/workflows/checkProjectAccessWorkflow.d.ts +17 -0
  63. package/dist/workflows/checkProjectAccessWorkflow.d.ts.map +1 -0
  64. package/dist/workflows/checkProjectAccessWorkflow.js +47 -0
  65. package/dist/workflows/checkProjectAccessWorkflow.js.map +1 -0
  66. package/dist/workflows/getCurrentUserWorkflow.d.ts +3 -0
  67. package/dist/workflows/getCurrentUserWorkflow.d.ts.map +1 -0
  68. package/dist/workflows/getCurrentUserWorkflow.js +17 -0
  69. package/dist/workflows/getCurrentUserWorkflow.js.map +1 -0
  70. package/dist/workflows/getIssueWorkflow.d.ts +7 -0
  71. package/dist/workflows/getIssueWorkflow.d.ts.map +1 -0
  72. package/dist/workflows/getIssueWorkflow.js +32 -0
  73. package/dist/workflows/getIssueWorkflow.js.map +1 -0
  74. package/dist/workflows/getLogsWorkflow.d.ts +11 -0
  75. package/dist/workflows/getLogsWorkflow.d.ts.map +1 -0
  76. package/dist/workflows/getLogsWorkflow.js +61 -0
  77. package/dist/workflows/getLogsWorkflow.js.map +1 -0
  78. package/dist/workflows/getProjectsWorkflow.d.ts +3 -0
  79. package/dist/workflows/getProjectsWorkflow.d.ts.map +1 -0
  80. package/dist/workflows/getProjectsWorkflow.js +17 -0
  81. package/dist/workflows/getProjectsWorkflow.js.map +1 -0
  82. package/dist/workflows/getTickets.d.ts +2 -0
  83. package/dist/workflows/getTickets.d.ts.map +1 -0
  84. package/dist/workflows/getTickets.js +2 -0
  85. package/dist/workflows/getTickets.js.map +1 -0
  86. package/dist/workflows/listMyIssuesWorkflow.d.ts +9 -0
  87. package/dist/workflows/listMyIssuesWorkflow.d.ts.map +1 -0
  88. package/dist/workflows/listMyIssuesWorkflow.js +20 -0
  89. package/dist/workflows/listMyIssuesWorkflow.js.map +1 -0
  90. package/dist/workflows/logTimeWorkflow.d.ts +50 -0
  91. package/dist/workflows/logTimeWorkflow.d.ts.map +1 -0
  92. package/dist/workflows/logTimeWorkflow.js +797 -0
  93. package/dist/workflows/logTimeWorkflow.js.map +1 -0
  94. package/package.json +45 -0
@@ -0,0 +1,797 @@
1
+ import axios from "axios";
2
+ import { getDefaultTimeComment, getTimeEntryDefaults, requireRedmineEnv, } from "../config/env.js";
3
+ import { getIssues, logTimeEntry } from "../services/redmine.js";
4
+ import { isValidIssueId } from "../utils/validation.js";
5
+ const TODAY = "Today";
6
+ const MULTIPLE = "Multiple";
7
+ /** @deprecated use MULTIPLE — still accepted from older clients */
8
+ const ADD_MULTIPLE = "Add multiple";
9
+ const OTHER = "Other";
10
+ const USE_DEFAULT_COMMENT = "Use default comment";
11
+ function todayIso() {
12
+ return new Date().toISOString().slice(0, 10);
13
+ }
14
+ function isValidDate(s) {
15
+ return /^\d{4}-\d{2}-\d{2}$/.test(s.trim());
16
+ }
17
+ function pairKey(spentOn, issueId) {
18
+ return `${spentOn}:${issueId}`;
19
+ }
20
+ function parsePairKey(key) {
21
+ const m = /^(\d{4}-\d{2}-\d{2}):(\d+)$/.exec(key.trim());
22
+ if (!m)
23
+ return undefined;
24
+ const issueId = Number(m[2]);
25
+ if (!isValidIssueId(issueId))
26
+ return undefined;
27
+ // `m[1]` is guaranteed by the regex when `m` is truthy.
28
+ return { spentOn: m[1], issueId };
29
+ }
30
+ function recentDateOptions(count) {
31
+ const out = [];
32
+ const d = new Date();
33
+ for (let i = 0; i < count; i++) {
34
+ const copy = new Date(d);
35
+ copy.setDate(copy.getDate() - i);
36
+ out.push(copy.toISOString().slice(0, 10));
37
+ }
38
+ return out;
39
+ }
40
+ function parseHours(hours) {
41
+ if (hours === undefined)
42
+ return undefined;
43
+ const n = typeof hours === "string" ? Number(hours) : hours;
44
+ return Number.isNaN(n) || n <= 0 ? undefined : n;
45
+ }
46
+ function expandDateToken(token) {
47
+ const t = token.trim();
48
+ if (t === TODAY || t.toLowerCase() === "today")
49
+ return todayIso();
50
+ if (isValidDate(t))
51
+ return t;
52
+ return null;
53
+ }
54
+ function normalizeDates(input) {
55
+ const raw = [
56
+ ...(input.dates ?? []),
57
+ ...(input.spentOn ? [input.spentOn] : []),
58
+ ];
59
+ const out = [];
60
+ for (const d of raw) {
61
+ const expanded = expandDateToken(d);
62
+ if (expanded)
63
+ out.push(expanded);
64
+ }
65
+ return [...new Set(out)];
66
+ }
67
+ function inferDatesFromPrompt(promptHint) {
68
+ if (!promptHint?.trim())
69
+ return undefined;
70
+ const p = promptHint.toLowerCase();
71
+ const isoDates = [
72
+ ...promptHint.matchAll(/\b(\d{4}-\d{2}-\d{2})\b/g),
73
+ ]
74
+ .map((m) => m[1])
75
+ .filter((d) => typeof d === "string");
76
+ if (isoDates.length > 0)
77
+ return [...new Set(isoDates)];
78
+ const out = [];
79
+ if (/\btoday\b/.test(p))
80
+ out.push(todayIso());
81
+ if (/\byesterday\b/.test(p)) {
82
+ const d = new Date();
83
+ d.setDate(d.getDate() - 1);
84
+ out.push(d.toISOString().slice(0, 10));
85
+ }
86
+ return out.length > 0 ? [...new Set(out)] : undefined;
87
+ }
88
+ function wantsExpandedDates(input) {
89
+ return (input.dates ?? []).some((d) => {
90
+ const t = d.trim();
91
+ return t === MULTIPLE || t === ADD_MULTIPLE;
92
+ });
93
+ }
94
+ function wantsOtherDate(input) {
95
+ return (input.dates ?? []).some((d) => d.trim() === OTHER);
96
+ }
97
+ function parseIssueIdsFromPrompt(promptHint) {
98
+ if (!promptHint?.trim())
99
+ return [];
100
+ const ids = [];
101
+ for (const m of promptHint.matchAll(/#(\d+)/g)) {
102
+ const n = Number(m[1]);
103
+ if (isValidIssueId(n))
104
+ ids.push(n);
105
+ }
106
+ for (const part of promptHint.split(/[,;\s]+/)) {
107
+ const n = Number(part.replace(/^#/, "").trim());
108
+ if (isValidIssueId(n))
109
+ ids.push(n);
110
+ }
111
+ return [...new Set(ids)];
112
+ }
113
+ function normalizeIssueIds(input) {
114
+ const fromPrompt = parseIssueIdsFromPrompt(input.promptHint);
115
+ const ids = [
116
+ ...fromPrompt,
117
+ ...(input.issueIds ?? []),
118
+ ...(input.issueId !== undefined ? [input.issueId] : []),
119
+ ];
120
+ return [...new Set(ids.filter((id) => isValidIssueId(id)))];
121
+ }
122
+ function parseIssueIdToken(token) {
123
+ const t = token.trim();
124
+ const leading = /^#?(\d+)/.exec(t);
125
+ if (!leading) {
126
+ return undefined;
127
+ }
128
+ const n = Number(leading[1]);
129
+ return isValidIssueId(n) ? n : undefined;
130
+ }
131
+ function parseCommaIssueIds(value) {
132
+ const ids = [];
133
+ for (const part of value.split(/[,;]+/)) {
134
+ const n = parseIssueIdToken(part);
135
+ if (n !== undefined) {
136
+ ids.push(n);
137
+ }
138
+ }
139
+ return [...new Set(ids)];
140
+ }
141
+ function normalizeIssueIdList(value) {
142
+ if (value === undefined || value === null) {
143
+ return [];
144
+ }
145
+ if (typeof value === "number" && isValidIssueId(value)) {
146
+ return [value];
147
+ }
148
+ if (typeof value === "string") {
149
+ const t = value.trim();
150
+ if (t === OTHER) {
151
+ return [];
152
+ }
153
+ return parseCommaIssueIds(t);
154
+ }
155
+ if (Array.isArray(value)) {
156
+ const ids = [];
157
+ for (const item of value) {
158
+ if (typeof item === "number" && isValidIssueId(item)) {
159
+ ids.push(item);
160
+ }
161
+ else if (typeof item === "string") {
162
+ const one = parseIssueIdToken(item);
163
+ if (one !== undefined) {
164
+ ids.push(one);
165
+ }
166
+ else {
167
+ ids.push(...parseCommaIssueIds(item));
168
+ }
169
+ }
170
+ }
171
+ return [...new Set(ids)];
172
+ }
173
+ return [];
174
+ }
175
+ function normalizeDateIssuesMap(input, allDates) {
176
+ const out = {};
177
+ for (const [date, raw] of Object.entries(input.dateIssues ?? {})) {
178
+ if (!isValidDate(date)) {
179
+ continue;
180
+ }
181
+ const ids = normalizeIssueIdList(raw);
182
+ if (ids.length > 0) {
183
+ out[date] = ids;
184
+ }
185
+ }
186
+ const globalIds = normalizeIssueIds(input);
187
+ const hasPerDate = Object.keys(input.dateIssues ?? {}).some((d) => isValidDate(d));
188
+ if (globalIds.length > 0 && !hasPerDate) {
189
+ for (const d of allDates) {
190
+ out[d] = [...globalIds];
191
+ }
192
+ }
193
+ else if (globalIds.length > 0 && input.issueIds?.length) {
194
+ for (const d of allDates) {
195
+ if (!out[d]?.length) {
196
+ out[d] = [...globalIds];
197
+ }
198
+ }
199
+ }
200
+ if (input.activeDate &&
201
+ isValidDate(input.activeDate) &&
202
+ (input.issueIds?.length || input.issueId)) {
203
+ const activeIds = normalizeIssueIds({
204
+ ...(input.issueIds !== undefined ? { issueIds: input.issueIds } : {}),
205
+ ...(input.issueId !== undefined ? { issueId: input.issueId } : {}),
206
+ });
207
+ if (activeIds.length > 0) {
208
+ out[input.activeDate] = [
209
+ ...new Set([...(out[input.activeDate] ?? []), ...activeIds]),
210
+ ];
211
+ }
212
+ }
213
+ return out;
214
+ }
215
+ function datesAwaitingIssues(allDates, dateIssues) {
216
+ return allDates.filter((d) => !(dateIssues[d]?.length ?? 0));
217
+ }
218
+ function hasAnyDateIssues(dateIssues, allDates) {
219
+ return allDates.some((d) => (dateIssues[d]?.length ?? 0) > 0);
220
+ }
221
+ function buildAllPairsFromDateIssues(allDates, dateIssues) {
222
+ const pairs = [];
223
+ for (const spentOn of allDates) {
224
+ for (const issueId of dateIssues[spentOn] ?? []) {
225
+ if (isValidIssueId(issueId)) {
226
+ pairs.push({ spentOn, issueId });
227
+ }
228
+ }
229
+ }
230
+ return pairs;
231
+ }
232
+ function normalizeSkippedPairs(input) {
233
+ const set = new Set();
234
+ for (const key of input.skippedPairs ?? []) {
235
+ const p = parsePairKey(key);
236
+ if (p)
237
+ set.add(pairKey(p.spentOn, p.issueId));
238
+ }
239
+ return set;
240
+ }
241
+ function findEntry(entries, spentOn, issueId) {
242
+ return entries.find((e) => e.spentOn === spentOn && e.issueId === issueId && e.hours > 0);
243
+ }
244
+ function isPairComplete(entries, spentOn, issueId) {
245
+ const e = findEntry(entries, spentOn, issueId);
246
+ return e !== undefined && e.comments.trim().length > 0;
247
+ }
248
+ function isPairSkipped(skipped, spentOn, issueId) {
249
+ return skipped.has(pairKey(spentOn, issueId));
250
+ }
251
+ function stepData(step, base) {
252
+ return { step, ...base };
253
+ }
254
+ function customInputPrompt(field, message, data) {
255
+ return {
256
+ success: false,
257
+ needsInput: true,
258
+ question: {
259
+ field,
260
+ question: message,
261
+ },
262
+ data: stepData(data.step, {
263
+ ...data,
264
+ awaitingCustomInput: field,
265
+ hint: "Reply in chat with your value, then retry this tool with that field set.",
266
+ }),
267
+ };
268
+ }
269
+ function previewEntry(draft, activityId, billableHoursFieldId) {
270
+ return {
271
+ issueId: draft.issueId,
272
+ hours: draft.hours,
273
+ spentOn: draft.spentOn,
274
+ activityId,
275
+ billableHoursFieldId,
276
+ billableHours: String(draft.hours),
277
+ comments: draft.comments,
278
+ };
279
+ }
280
+ function datesQuestion() {
281
+ return {
282
+ field: "dates",
283
+ question: "Step 1 — When should time be logged?",
284
+ options: [TODAY, MULTIPLE, OTHER],
285
+ allowMultiple: false,
286
+ };
287
+ }
288
+ function datesExpandedQuestion() {
289
+ return {
290
+ field: "dates",
291
+ question: "Step 1 — Select all dates to log time on",
292
+ options: [...recentDateOptions(14), OTHER],
293
+ allowMultiple: true,
294
+ };
295
+ }
296
+ function issueSubjectsToRecord(subjects) {
297
+ const out = {};
298
+ for (const [id, subject] of Object.entries(subjects)) {
299
+ if (subject.trim()) {
300
+ out[String(id)] = subject;
301
+ }
302
+ }
303
+ return out;
304
+ }
305
+ function pairQuestionTitle(pair, subjects, prefix) {
306
+ const title = subjects[pair.issueId]?.trim();
307
+ const suffix = title ? `: ${title}` : "";
308
+ return `${prefix} #${pair.issueId} on ${pair.spentOn}${suffix}?`;
309
+ }
310
+ async function resolveIssueSubjects(input, issueIds) {
311
+ const map = {};
312
+ for (const [key, subject] of Object.entries(input.issueSubjects ?? {})) {
313
+ const id = Number(key);
314
+ if (isValidIssueId(id) && subject?.trim()) {
315
+ map[id] = subject.trim();
316
+ }
317
+ }
318
+ for (const opt of input.issueOptions ?? []) {
319
+ if (!isValidIssueId(opt.issueId)) {
320
+ continue;
321
+ }
322
+ if (opt.subject?.trim()) {
323
+ map[opt.issueId] = opt.subject.trim();
324
+ }
325
+ else if (opt.label?.trim()) {
326
+ const m = /^#\d+\s*—\s*(.+)$/.exec(opt.label.trim());
327
+ if (m?.[1]) {
328
+ map[opt.issueId] = m[1].trim();
329
+ }
330
+ }
331
+ }
332
+ const needsLookup = issueIds.some((id) => !map[id]);
333
+ if (needsLookup) {
334
+ const loaded = await fetchIssueOptions();
335
+ if (!("error" in loaded)) {
336
+ for (const issue of loaded.issues) {
337
+ map[issue.id] = issue.subject;
338
+ }
339
+ }
340
+ }
341
+ return map;
342
+ }
343
+ function batchDateIssuesQuestions(dates, issues) {
344
+ const options = issues.map((i) => `${i.id} — ${i.subject}`);
345
+ return dates.map((spentOn) => ({
346
+ id: spentOn,
347
+ field: "dateIssues",
348
+ question: `Step 2 — Issues for ${spentOn}? (multi-select; option is id — title, or Other + comma-separated IDs)`,
349
+ options: [...options, OTHER],
350
+ allowMultiple: true,
351
+ }));
352
+ }
353
+ function batchNeedsInput(step, field, title, questions, data) {
354
+ return {
355
+ success: false,
356
+ needsInput: true,
357
+ question: {
358
+ field,
359
+ question: title,
360
+ },
361
+ questions,
362
+ data: stepData(step, {
363
+ ...data,
364
+ batchMode: true,
365
+ batchField: field,
366
+ hint: field === "dateIssues"
367
+ ? `Show all ${questions.length} questions in ONE AskQuestion (id = date YYYY-MM-DD). Retry with dateIssues: { "YYYY-MM-DD": [issueId, ...] } — use comma-separated string for a date if Other.`
368
+ : `Show all ${questions.length} questions in ONE AskQuestion (use question id as answer key). Then retry with ${field} object keyed by id.`,
369
+ }),
370
+ };
371
+ }
372
+ function batchHoursQuestions(pairs, subjects) {
373
+ return pairs.map((p) => {
374
+ const id = pairKey(p.spentOn, p.issueId);
375
+ return {
376
+ id,
377
+ field: "pairHours",
378
+ question: pairQuestionTitle(p, subjects, "Step 3 — Hours for"),
379
+ options: ["1", "2", "4", "8", OTHER],
380
+ };
381
+ });
382
+ }
383
+ function batchCommentQuestions(pairs, defaultComment, subjects) {
384
+ return pairs.map((p) => {
385
+ const id = pairKey(p.spentOn, p.issueId);
386
+ const base = pairQuestionTitle(p, subjects, "Step 4 — Comment for").replace(/\?$/, "");
387
+ return {
388
+ id,
389
+ field: "pairComments",
390
+ question: `${base} (default: "${defaultComment}")?`,
391
+ options: [USE_DEFAULT_COMMENT, OTHER],
392
+ allowMultiple: false,
393
+ };
394
+ });
395
+ }
396
+ function mergePairHours(input) {
397
+ const out = { ...(input.pairHours ?? {}) };
398
+ if (input.activeDate &&
399
+ isValidDate(input.activeDate) &&
400
+ input.activeIssueId &&
401
+ input.hours !== undefined) {
402
+ out[pairKey(input.activeDate, input.activeIssueId)] = input.hours;
403
+ }
404
+ return out;
405
+ }
406
+ function mergePairComments(input) {
407
+ const out = { ...(input.pairComments ?? {}) };
408
+ if (input.activeDate &&
409
+ isValidDate(input.activeDate) &&
410
+ input.activeIssueId) {
411
+ const key = pairKey(input.activeDate, input.activeIssueId);
412
+ if (input.commentChoice === "default") {
413
+ out[key] = "default";
414
+ }
415
+ else if (input.comments?.trim()) {
416
+ out[key] = input.comments.trim();
417
+ }
418
+ }
419
+ return out;
420
+ }
421
+ function pairsAwaitingHours(included, entries, pairHours) {
422
+ return included.filter((p) => {
423
+ const key = pairKey(p.spentOn, p.issueId);
424
+ if (findEntry(entries, p.spentOn, p.issueId)) {
425
+ return false;
426
+ }
427
+ return parseHours(pairHours[key]) === undefined;
428
+ });
429
+ }
430
+ function applyPairHoursToEntries(entries, included, pairHours) {
431
+ const next = entries.map((e) => ({ ...e }));
432
+ for (const p of included) {
433
+ const key = pairKey(p.spentOn, p.issueId);
434
+ const hours = parseHours(pairHours[key]);
435
+ if (hours === undefined) {
436
+ continue;
437
+ }
438
+ const idx = next.findIndex((e) => e.spentOn === p.spentOn && e.issueId === p.issueId);
439
+ const draft = {
440
+ issueId: p.issueId,
441
+ hours,
442
+ spentOn: p.spentOn,
443
+ comments: "",
444
+ };
445
+ if (idx >= 0) {
446
+ next[idx] = draft;
447
+ }
448
+ else {
449
+ next.push(draft);
450
+ }
451
+ }
452
+ return next;
453
+ }
454
+ function pairsAwaitingComments(entries, skipped, pairComments) {
455
+ const out = [];
456
+ for (const e of entries) {
457
+ if (isPairSkipped(skipped, e.spentOn, e.issueId)) {
458
+ continue;
459
+ }
460
+ if (e.hours <= 0) {
461
+ continue;
462
+ }
463
+ const key = pairKey(e.spentOn, e.issueId);
464
+ const answer = pairComments[key]?.trim();
465
+ if (!answer || answer === OTHER) {
466
+ out.push({ spentOn: e.spentOn, issueId: e.issueId });
467
+ continue;
468
+ }
469
+ if (e.comments.trim().length > 0) {
470
+ continue;
471
+ }
472
+ }
473
+ return out;
474
+ }
475
+ function applyPairCommentsToEntries(entries, pairComments) {
476
+ const defaultComment = getDefaultTimeComment();
477
+ return entries.map((e) => {
478
+ const key = pairKey(e.spentOn, e.issueId);
479
+ const answer = pairComments[key]?.trim();
480
+ if (!answer || answer === OTHER) {
481
+ return e;
482
+ }
483
+ if (answer === "default" ||
484
+ answer === USE_DEFAULT_COMMENT ||
485
+ answer.toLowerCase() === "use default comment") {
486
+ return { ...e, comments: defaultComment };
487
+ }
488
+ return { ...e, comments: answer };
489
+ });
490
+ }
491
+ function buildPreviewPayload(drafts) {
492
+ const { activityId, billableHoursFieldId } = getTimeEntryDefaults();
493
+ const preview = drafts.map((d) => previewEntry(d, activityId, billableHoursFieldId));
494
+ const previewLines = drafts.map((d, i) => `${i + 1}. ${d.spentOn} | #${d.issueId} | ${d.hours}h billable | "${d.comments}"`);
495
+ return {
496
+ preview,
497
+ previewLines,
498
+ previewText: previewLines.join("\n"),
499
+ };
500
+ }
501
+ function confirmWithPreviewResponse(drafts, allDates, dateIssues, skippedPairs) {
502
+ const { preview, previewLines, previewText } = buildPreviewPayload(drafts);
503
+ return {
504
+ success: false,
505
+ needsInput: true,
506
+ question: {
507
+ field: "confirmSave",
508
+ question: `Step 5 — Review ${drafts.length} time entr${drafts.length === 1 ? "y" : "ies"} below, then confirm save to Redmine.`,
509
+ options: ["yes", "no"],
510
+ },
511
+ data: stepData("preview", {
512
+ entries: drafts,
513
+ dates: allDates,
514
+ dateIssues,
515
+ skippedPairs,
516
+ count: drafts.length,
517
+ summary: `${drafts.length} time entr${drafts.length === 1 ? "y" : "ies"} ready to log`,
518
+ preview,
519
+ previewLines,
520
+ previewText,
521
+ hint: "Show previewText to the user, then retry with confirmSave: yes to save",
522
+ }),
523
+ };
524
+ }
525
+ async function fetchIssueOptions() {
526
+ try {
527
+ const data = await getIssues({
528
+ assignedToId: "me",
529
+ statusId: "*",
530
+ limit: 25,
531
+ });
532
+ return {
533
+ issues: (data.issues ?? []),
534
+ };
535
+ }
536
+ catch {
537
+ return { error: "Failed to load issues" };
538
+ }
539
+ }
540
+ async function saveDrafts(drafts) {
541
+ const { activityId, billableHoursFieldId } = getTimeEntryDefaults();
542
+ const { baseUrl } = requireRedmineEnv();
543
+ const saved = [];
544
+ for (const draft of drafts) {
545
+ const result = await logTimeEntry({
546
+ issueId: draft.issueId,
547
+ hours: draft.hours,
548
+ spentOn: draft.spentOn,
549
+ activityId,
550
+ billableHoursFieldId,
551
+ billableHours: String(draft.hours),
552
+ comments: draft.comments,
553
+ });
554
+ const entry = result.time_entry;
555
+ saved.push({
556
+ timeEntryId: entry.id,
557
+ issueId: draft.issueId,
558
+ issueUrl: `${baseUrl}/issues/${draft.issueId}`,
559
+ hours: draft.hours,
560
+ spentOn: draft.spentOn,
561
+ billableHours: String(draft.hours),
562
+ comments: draft.comments,
563
+ });
564
+ }
565
+ return {
566
+ success: true,
567
+ data: {
568
+ count: saved.length,
569
+ entries: saved,
570
+ batch: true,
571
+ },
572
+ };
573
+ }
574
+ function resolveAllPendingComments(entries, input) {
575
+ const defaultComment = getDefaultTimeComment();
576
+ const pending = entries.filter((e) => e.hours > 0 && e.comments.trim() === "");
577
+ if (pending.length === 0) {
578
+ return [...entries];
579
+ }
580
+ if (input.commentChoice === "custom") {
581
+ const text = input.comments?.trim();
582
+ if (!text || text === OTHER) {
583
+ return customInputPrompt("comments", "Enter comment for all pending entries, then retry with commentChoice: custom and comments", stepData("comments", { entries, suggestedComment: defaultComment }));
584
+ }
585
+ return entries.map((e) => e.hours > 0 && e.comments.trim() === ""
586
+ ? { ...e, comments: text }
587
+ : e);
588
+ }
589
+ if (input.commentChoice === "default" ||
590
+ input.comments?.trim() === USE_DEFAULT_COMMENT) {
591
+ return entries.map((e) => e.hours > 0 && e.comments.trim() === ""
592
+ ? { ...e, comments: defaultComment }
593
+ : e);
594
+ }
595
+ const text = input.comments?.trim();
596
+ if (text && text !== OTHER) {
597
+ return entries.map((e) => e.hours > 0 && e.comments.trim() === ""
598
+ ? { ...e, comments: text }
599
+ : e);
600
+ }
601
+ return entries;
602
+ }
603
+ function completedDrafts(entries) {
604
+ return entries.filter((e) => e.hours > 0 && e.comments.trim().length > 0);
605
+ }
606
+ function baseState(allDates, dateIssues, entries, skipped, allPairs) {
607
+ return {
608
+ dates: allDates,
609
+ dateIssues,
610
+ entries,
611
+ skippedPairs: [...skipped],
612
+ pendingPairs: allPairs
613
+ .filter((p) => !isPairSkipped(skipped, p.spentOn, p.issueId))
614
+ .filter((p) => !isPairComplete(entries, p.spentOn, p.issueId))
615
+ .map((p) => ({ spentOn: p.spentOn, issueId: p.issueId })),
616
+ };
617
+ }
618
+ export async function logTimeWorkflow(input) {
619
+ const entries = (input.entries ?? []).map((e) => ({ ...e }));
620
+ let dateIssues = {};
621
+ let skipped = normalizeSkippedPairs(input);
622
+ // —— Step 1: dates ——
623
+ let allDates = normalizeDates(input);
624
+ if (allDates.length === 0) {
625
+ const inferred = inferDatesFromPrompt(input.promptHint);
626
+ if (inferred?.length)
627
+ allDates = inferred;
628
+ }
629
+ if (wantsOtherDate(input) && allDates.length === 0) {
630
+ return customInputPrompt("spentOn", "You chose Other — enter the date in chat (YYYY-MM-DD), then retry with spentOn or dates[]", stepData("dates", { dates: input.dates }));
631
+ }
632
+ if (allDates.length === 0) {
633
+ if (wantsExpandedDates(input)) {
634
+ return {
635
+ success: false,
636
+ needsInput: true,
637
+ question: datesExpandedQuestion(),
638
+ data: stepData("dates", {
639
+ hint: "Select one or more dates, or Other for a custom date",
640
+ }),
641
+ };
642
+ }
643
+ return {
644
+ success: false,
645
+ needsInput: true,
646
+ question: datesQuestion(),
647
+ data: stepData("dates", {
648
+ hint: "Pick Today, Multiple, or Other — one choice only",
649
+ }),
650
+ };
651
+ }
652
+ if (wantsExpandedDates(input) && allDates.length < 2) {
653
+ return {
654
+ success: false,
655
+ needsInput: true,
656
+ question: datesExpandedQuestion(),
657
+ data: stepData("dates", {
658
+ dates: allDates,
659
+ hint: "Multiple — select one or more dates (or Other for a custom date)",
660
+ }),
661
+ };
662
+ }
663
+ // —— Step 2: batch issues per date (one panel per date, all on one page) ——
664
+ dateIssues = normalizeDateIssuesMap(input, allDates);
665
+ const issuesAwaiting = datesAwaitingIssues(allDates, dateIssues);
666
+ if (issuesAwaiting.length > 0) {
667
+ const loaded = await fetchIssueOptions();
668
+ const issueList = "error" in loaded ? [] : loaded.issues;
669
+ const showDates = Object.keys(dateIssues).length === 0 ? allDates : issuesAwaiting;
670
+ return batchNeedsInput("issues", "dateIssues", `Step 2 — Select issue(s) for each date (${showDates.length} date(s), answer all)`, batchDateIssuesQuestions(showDates, issueList), {
671
+ dates: allDates,
672
+ dateIssues,
673
+ entries,
674
+ issueOptions: issueList.map((i) => ({
675
+ issueId: i.id,
676
+ subject: i.subject,
677
+ label: `#${i.id} — ${i.subject}`,
678
+ })),
679
+ issueSubjects: issueSubjectsToRecord(Object.fromEntries(issueList.map((i) => [i.id, i.subject]))),
680
+ ...("error" in loaded
681
+ ? {
682
+ hint: "Assigned issues could not be loaded — use Other per date and pass comma-separated IDs in dateIssues",
683
+ }
684
+ : {}),
685
+ });
686
+ }
687
+ if (!hasAnyDateIssues(dateIssues, allDates)) {
688
+ return {
689
+ success: false,
690
+ error: "No issues selected for any date",
691
+ data: { dates: allDates, dateIssues },
692
+ };
693
+ }
694
+ const allPairs = buildAllPairsFromDateIssues(allDates, dateIssues);
695
+ const pairHours = mergePairHours(input);
696
+ const pairComments = mergePairComments(input);
697
+ const gridIssueIds = [...new Set(allPairs.map((p) => p.issueId))];
698
+ const issueSubjectMap = await resolveIssueSubjects(input, gridIssueIds);
699
+ const sharedData = () => ({
700
+ ...baseState(allDates, dateIssues, entries, skipped, allPairs),
701
+ issueSubjects: issueSubjectsToRecord(issueSubjectMap),
702
+ issueOptions: gridIssueIds.map((id) => ({
703
+ issueId: id,
704
+ subject: issueSubjectMap[id] ?? "",
705
+ label: issueSubjectMap[id]
706
+ ? `#${id} — ${issueSubjectMap[id]}`
707
+ : `#${id}`,
708
+ })),
709
+ });
710
+ if (allPairs.length === 0) {
711
+ return {
712
+ success: false,
713
+ error: "No date×task combinations — select issues per date in step 2",
714
+ data: { dates: allDates, dateIssues },
715
+ };
716
+ }
717
+ // —— Step 3: batch hours (all date×task pairs from step 2) ——
718
+ let workingEntries = applyPairHoursToEntries(entries, allPairs, pairHours);
719
+ const hoursAwaiting = pairsAwaitingHours(allPairs, workingEntries, pairHours);
720
+ if (hoursAwaiting.length > 0) {
721
+ return batchNeedsInput("hours", "pairHours", `Step 3 — Hours for ${hoursAwaiting.length} pair(s) (answer all)`, batchHoursQuestions(hoursAwaiting, issueSubjectMap), {
722
+ ...sharedData(),
723
+ entries: workingEntries,
724
+ });
725
+ }
726
+ entries.length = 0;
727
+ entries.push(...workingEntries);
728
+ // —— Step 4: batch comments (all pairs on one page) ——
729
+ const defaultComment = getDefaultTimeComment();
730
+ workingEntries = applyPairCommentsToEntries(entries, pairComments);
731
+ const commentsAwaiting = pairsAwaitingComments(workingEntries, skipped, pairComments);
732
+ if (commentsAwaiting.length > 0) {
733
+ return batchNeedsInput("comments", "pairComments", `Step 4 — Comment for ${commentsAwaiting.length} pair(s) (answer all; use "${USE_DEFAULT_COMMENT}" or custom text)`, batchCommentQuestions(commentsAwaiting, defaultComment, issueSubjectMap), {
734
+ ...sharedData(),
735
+ entries: workingEntries,
736
+ pairHours,
737
+ suggestedComment: defaultComment,
738
+ });
739
+ }
740
+ entries.length = 0;
741
+ entries.push(...workingEntries);
742
+ // Legacy: single commentChoice applies default to all pending
743
+ if (input.commentChoice === "default" ||
744
+ (input.comments?.trim() === USE_DEFAULT_COMMENT && !Object.keys(pairComments).length)) {
745
+ const batchResolved = resolveAllPendingComments(entries, input);
746
+ if (Array.isArray(batchResolved)) {
747
+ entries.length = 0;
748
+ entries.push(...batchResolved);
749
+ }
750
+ }
751
+ const drafts = completedDrafts(entries);
752
+ if (drafts.length === 0) {
753
+ return {
754
+ success: false,
755
+ error: "No time entries to save — complete hours and comments for selected issues",
756
+ data: { dates: allDates, dateIssues },
757
+ };
758
+ }
759
+ if (!input.confirmSave) {
760
+ return confirmWithPreviewResponse(drafts, allDates, dateIssues, [...skipped]);
761
+ }
762
+ if (input.confirmSave !== "yes") {
763
+ return {
764
+ success: false,
765
+ error: "Time entries not saved (confirmation was not yes)",
766
+ };
767
+ }
768
+ try {
769
+ return await saveDrafts(drafts);
770
+ }
771
+ catch (error) {
772
+ return formatSaveError(error);
773
+ }
774
+ }
775
+ function formatSaveError(error) {
776
+ if (axios.isAxiosError(error)) {
777
+ const status = error.response?.status;
778
+ const errors = error.response?.data;
779
+ if (status === 403) {
780
+ return {
781
+ success: false,
782
+ error: "Forbidden: you do not have permission to log time on one or more issues",
783
+ };
784
+ }
785
+ if (status === 422 && errors?.errors?.length) {
786
+ return {
787
+ success: false,
788
+ error: errors.errors.join("; "),
789
+ };
790
+ }
791
+ }
792
+ return {
793
+ success: false,
794
+ error: "Failed to log time on Redmine",
795
+ };
796
+ }
797
+ //# sourceMappingURL=logTimeWorkflow.js.map