@doist/todoist-ai 4.15.1 → 4.16.1

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 (167) hide show
  1. package/dist/filter-helpers.d.ts +1 -1
  2. package/dist/index.d.ts +175 -175
  3. package/dist/index.js +61 -81
  4. package/dist/main.js +15 -23
  5. package/dist/mcp-helpers.d.ts +4 -4
  6. package/dist/mcp-server-6tm7Rhyz.js +2840 -0
  7. package/dist/todoist-tool.d.ts +2 -2
  8. package/dist/tool-helpers.d.ts +1 -1
  9. package/dist/tools/add-comments.d.ts +1 -1
  10. package/dist/tools/add-comments.d.ts.map +1 -1
  11. package/dist/tools/add-projects.d.ts +4 -4
  12. package/dist/tools/add-projects.d.ts.map +1 -1
  13. package/dist/tools/add-sections.d.ts +1 -1
  14. package/dist/tools/add-sections.d.ts.map +1 -1
  15. package/dist/tools/add-tasks.d.ts +4 -4
  16. package/dist/tools/add-tasks.d.ts.map +1 -1
  17. package/dist/tools/complete-tasks.d.ts +1 -1
  18. package/dist/tools/complete-tasks.d.ts.map +1 -1
  19. package/dist/tools/delete-object.d.ts +3 -3
  20. package/dist/tools/delete-object.d.ts.map +1 -1
  21. package/dist/tools/fetch.d.ts +1 -1
  22. package/dist/tools/find-activity.d.ts +5 -5
  23. package/dist/tools/find-activity.d.ts.map +1 -1
  24. package/dist/tools/find-comments.d.ts +2 -2
  25. package/dist/tools/find-comments.d.ts.map +1 -1
  26. package/dist/tools/find-completed-tasks.d.ts +3 -3
  27. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  28. package/dist/tools/find-project-collaborators.d.ts +2 -2
  29. package/dist/tools/find-projects.d.ts +1 -1
  30. package/dist/tools/find-projects.d.ts.map +1 -1
  31. package/dist/tools/find-sections.d.ts +1 -1
  32. package/dist/tools/find-sections.d.ts.map +1 -1
  33. package/dist/tools/find-tasks-by-date.d.ts +1 -1
  34. package/dist/tools/find-tasks-by-date.d.ts.map +1 -1
  35. package/dist/tools/find-tasks.d.ts +3 -3
  36. package/dist/tools/find-tasks.d.ts.map +1 -1
  37. package/dist/tools/get-overview.d.ts +1 -1
  38. package/dist/tools/manage-assignments.d.ts +1 -1
  39. package/dist/tools/search.d.ts +1 -1
  40. package/dist/tools/update-comments.d.ts +4 -4
  41. package/dist/tools/update-comments.d.ts.map +1 -1
  42. package/dist/tools/update-projects.d.ts +1 -1
  43. package/dist/tools/update-projects.d.ts.map +1 -1
  44. package/dist/tools/update-sections.d.ts +4 -4
  45. package/dist/tools/update-sections.d.ts.map +1 -1
  46. package/dist/tools/update-tasks.d.ts +7 -7
  47. package/dist/tools/update-tasks.d.ts.map +1 -1
  48. package/dist/tools/user-info.d.ts +1 -1
  49. package/dist/utils/assignment-validator.d.ts +2 -2
  50. package/dist/utils/response-builders.d.ts +1 -3
  51. package/dist/utils/response-builders.d.ts.map +1 -1
  52. package/dist/utils/test-helpers.d.ts +1 -1
  53. package/dist/utils/user-resolver.d.ts +1 -1
  54. package/package.json +11 -9
  55. package/dist/filter-helpers.js +0 -79
  56. package/dist/mcp-helpers.js +0 -71
  57. package/dist/mcp-server.js +0 -142
  58. package/dist/todoist-tool.js +0 -1
  59. package/dist/tool-helpers.js +0 -125
  60. package/dist/tool-helpers.test.d.ts +0 -2
  61. package/dist/tool-helpers.test.d.ts.map +0 -1
  62. package/dist/tool-helpers.test.js +0 -223
  63. package/dist/tools/__tests__/add-comments.test.d.ts +0 -2
  64. package/dist/tools/__tests__/add-comments.test.d.ts.map +0 -1
  65. package/dist/tools/__tests__/add-comments.test.js +0 -241
  66. package/dist/tools/__tests__/add-projects.test.d.ts +0 -2
  67. package/dist/tools/__tests__/add-projects.test.d.ts.map +0 -1
  68. package/dist/tools/__tests__/add-projects.test.js +0 -174
  69. package/dist/tools/__tests__/add-sections.test.d.ts +0 -2
  70. package/dist/tools/__tests__/add-sections.test.d.ts.map +0 -1
  71. package/dist/tools/__tests__/add-sections.test.js +0 -185
  72. package/dist/tools/__tests__/add-tasks.test.d.ts +0 -2
  73. package/dist/tools/__tests__/add-tasks.test.d.ts.map +0 -1
  74. package/dist/tools/__tests__/add-tasks.test.js +0 -533
  75. package/dist/tools/__tests__/assignment-integration.test.d.ts +0 -2
  76. package/dist/tools/__tests__/assignment-integration.test.d.ts.map +0 -1
  77. package/dist/tools/__tests__/assignment-integration.test.js +0 -428
  78. package/dist/tools/__tests__/complete-tasks.test.d.ts +0 -2
  79. package/dist/tools/__tests__/complete-tasks.test.d.ts.map +0 -1
  80. package/dist/tools/__tests__/complete-tasks.test.js +0 -206
  81. package/dist/tools/__tests__/delete-object.test.d.ts +0 -2
  82. package/dist/tools/__tests__/delete-object.test.d.ts.map +0 -1
  83. package/dist/tools/__tests__/delete-object.test.js +0 -110
  84. package/dist/tools/__tests__/fetch.test.d.ts +0 -2
  85. package/dist/tools/__tests__/fetch.test.d.ts.map +0 -1
  86. package/dist/tools/__tests__/fetch.test.js +0 -279
  87. package/dist/tools/__tests__/find-activity.test.d.ts +0 -2
  88. package/dist/tools/__tests__/find-activity.test.d.ts.map +0 -1
  89. package/dist/tools/__tests__/find-activity.test.js +0 -229
  90. package/dist/tools/__tests__/find-comments.test.d.ts +0 -2
  91. package/dist/tools/__tests__/find-comments.test.d.ts.map +0 -1
  92. package/dist/tools/__tests__/find-comments.test.js +0 -236
  93. package/dist/tools/__tests__/find-completed-tasks.test.d.ts +0 -2
  94. package/dist/tools/__tests__/find-completed-tasks.test.d.ts.map +0 -1
  95. package/dist/tools/__tests__/find-completed-tasks.test.js +0 -324
  96. package/dist/tools/__tests__/find-projects.test.d.ts +0 -2
  97. package/dist/tools/__tests__/find-projects.test.d.ts.map +0 -1
  98. package/dist/tools/__tests__/find-projects.test.js +0 -154
  99. package/dist/tools/__tests__/find-sections.test.d.ts +0 -2
  100. package/dist/tools/__tests__/find-sections.test.d.ts.map +0 -1
  101. package/dist/tools/__tests__/find-sections.test.js +0 -245
  102. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts +0 -2
  103. package/dist/tools/__tests__/find-tasks-by-date.test.d.ts.map +0 -1
  104. package/dist/tools/__tests__/find-tasks-by-date.test.js +0 -528
  105. package/dist/tools/__tests__/find-tasks.test.d.ts +0 -2
  106. package/dist/tools/__tests__/find-tasks.test.d.ts.map +0 -1
  107. package/dist/tools/__tests__/find-tasks.test.js +0 -771
  108. package/dist/tools/__tests__/get-overview.test.d.ts +0 -2
  109. package/dist/tools/__tests__/get-overview.test.d.ts.map +0 -1
  110. package/dist/tools/__tests__/get-overview.test.js +0 -225
  111. package/dist/tools/__tests__/search.test.d.ts +0 -2
  112. package/dist/tools/__tests__/search.test.d.ts.map +0 -1
  113. package/dist/tools/__tests__/search.test.js +0 -206
  114. package/dist/tools/__tests__/update-comments.test.d.ts +0 -2
  115. package/dist/tools/__tests__/update-comments.test.d.ts.map +0 -1
  116. package/dist/tools/__tests__/update-comments.test.js +0 -294
  117. package/dist/tools/__tests__/update-projects.test.d.ts +0 -2
  118. package/dist/tools/__tests__/update-projects.test.d.ts.map +0 -1
  119. package/dist/tools/__tests__/update-projects.test.js +0 -217
  120. package/dist/tools/__tests__/update-sections.test.d.ts +0 -2
  121. package/dist/tools/__tests__/update-sections.test.d.ts.map +0 -1
  122. package/dist/tools/__tests__/update-sections.test.js +0 -169
  123. package/dist/tools/__tests__/update-tasks.test.d.ts +0 -2
  124. package/dist/tools/__tests__/update-tasks.test.d.ts.map +0 -1
  125. package/dist/tools/__tests__/update-tasks.test.js +0 -788
  126. package/dist/tools/__tests__/user-info.test.d.ts +0 -2
  127. package/dist/tools/__tests__/user-info.test.d.ts.map +0 -1
  128. package/dist/tools/__tests__/user-info.test.js +0 -139
  129. package/dist/tools/add-comments.js +0 -79
  130. package/dist/tools/add-projects.js +0 -63
  131. package/dist/tools/add-sections.js +0 -61
  132. package/dist/tools/add-tasks.js +0 -160
  133. package/dist/tools/complete-tasks.js +0 -68
  134. package/dist/tools/delete-object.js +0 -79
  135. package/dist/tools/fetch.js +0 -102
  136. package/dist/tools/find-activity.js +0 -221
  137. package/dist/tools/find-comments.js +0 -143
  138. package/dist/tools/find-completed-tasks.js +0 -161
  139. package/dist/tools/find-project-collaborators.js +0 -151
  140. package/dist/tools/find-projects.js +0 -101
  141. package/dist/tools/find-sections.js +0 -96
  142. package/dist/tools/find-tasks-by-date.js +0 -198
  143. package/dist/tools/find-tasks.js +0 -329
  144. package/dist/tools/get-overview.js +0 -249
  145. package/dist/tools/manage-assignments.js +0 -337
  146. package/dist/tools/search.js +0 -65
  147. package/dist/tools/update-comments.js +0 -82
  148. package/dist/tools/update-projects.js +0 -84
  149. package/dist/tools/update-sections.js +0 -70
  150. package/dist/tools/update-tasks.js +0 -170
  151. package/dist/tools/user-info.js +0 -142
  152. package/dist/utils/assignment-validator.js +0 -253
  153. package/dist/utils/constants.js +0 -45
  154. package/dist/utils/duration-parser.js +0 -96
  155. package/dist/utils/duration-parser.test.d.ts +0 -2
  156. package/dist/utils/duration-parser.test.d.ts.map +0 -1
  157. package/dist/utils/duration-parser.test.js +0 -147
  158. package/dist/utils/labels.js +0 -18
  159. package/dist/utils/priorities.js +0 -20
  160. package/dist/utils/response-builders.js +0 -210
  161. package/dist/utils/sanitize-data.js +0 -37
  162. package/dist/utils/sanitize-data.test.d.ts +0 -2
  163. package/dist/utils/sanitize-data.test.d.ts.map +0 -1
  164. package/dist/utils/sanitize-data.test.js +0 -93
  165. package/dist/utils/test-helpers.js +0 -237
  166. package/dist/utils/tool-names.js +0 -40
  167. package/dist/utils/user-resolver.js +0 -179
@@ -1,96 +0,0 @@
1
- /**
2
- * Duration parser utility for converting human-readable duration strings
3
- * to minutes using a restricted, language-neutral syntax.
4
- *
5
- * Supported formats:
6
- * - "2h" (hours only)
7
- * - "90m" (minutes only)
8
- * - "2h30m" (hours + minutes)
9
- * - "1.5h" (decimal hours)
10
- * - Supports optional spaces: "2h 30m"
11
- */
12
- export class DurationParseError extends Error {
13
- constructor(input, reason) {
14
- super(`Invalid duration format "${input}": ${reason}`);
15
- this.name = 'DurationParseError';
16
- }
17
- }
18
- /**
19
- * Parses duration string in restricted syntax to minutes.
20
- * Max duration: 1440 minutes (24 hours)
21
- *
22
- * @param durationStr - Duration string like "2h30m", "45m", "1.5h"
23
- * @returns Parsed duration in minutes
24
- * @throws DurationParseError for invalid formats
25
- */
26
- export function parseDuration(durationStr) {
27
- if (!durationStr || typeof durationStr !== 'string') {
28
- throw new DurationParseError(durationStr, 'Duration must be a non-empty string');
29
- }
30
- // Remove all spaces and convert to lowercase
31
- const normalized = durationStr.trim().toLowerCase().replace(/\s+/g, '');
32
- // Check for empty string after trimming
33
- if (!normalized) {
34
- throw new DurationParseError(durationStr, 'Duration must be a non-empty string');
35
- }
36
- // Validate format with strict ordering: hours must come before minutes
37
- // This regex ensures: optional hours followed by optional minutes, no duplicates
38
- const match = normalized.match(/^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?$/);
39
- if (!match || (!match[1] && !match[2])) {
40
- throw new DurationParseError(durationStr, 'Use format like "2h", "30m", "2h30m", or "1.5h"');
41
- }
42
- let totalMinutes = 0;
43
- const [, hoursStr, minutesStr] = match;
44
- // Parse hours if present
45
- if (hoursStr) {
46
- const hours = Number.parseFloat(hoursStr);
47
- if (Number.isNaN(hours) || hours < 0) {
48
- throw new DurationParseError(durationStr, 'Hours must be a positive number');
49
- }
50
- totalMinutes += hours * 60;
51
- }
52
- // Parse minutes if present
53
- if (minutesStr) {
54
- const minutes = Number.parseFloat(minutesStr);
55
- if (Number.isNaN(minutes) || minutes < 0) {
56
- throw new DurationParseError(durationStr, 'Minutes must be a positive number');
57
- }
58
- // Don't allow decimal minutes
59
- if (minutes % 1 !== 0) {
60
- throw new DurationParseError(durationStr, 'Minutes must be a whole number (use decimal hours instead)');
61
- }
62
- totalMinutes += minutes;
63
- }
64
- // The regex already ensures at least one unit is present
65
- // Round to nearest minute (handles decimal hours)
66
- totalMinutes = Math.round(totalMinutes);
67
- // Validate minimum duration
68
- if (totalMinutes === 0) {
69
- throw new DurationParseError(durationStr, 'Duration must be greater than 0 minutes');
70
- }
71
- // Validate maximum duration (24 hours = 1440 minutes)
72
- if (totalMinutes > 1440) {
73
- throw new DurationParseError(durationStr, 'Duration cannot exceed 24 hours (1440 minutes)');
74
- }
75
- return { minutes: totalMinutes };
76
- }
77
- /**
78
- * Formats minutes back to a human-readable duration string.
79
- * Used when returning task data to LLMs.
80
- *
81
- * @param minutes - Duration in minutes
82
- * @returns Formatted duration string like "2h30m" or "45m"
83
- */
84
- export function formatDuration(minutes) {
85
- if (minutes <= 0)
86
- return '0m';
87
- const hours = Math.floor(minutes / 60);
88
- const remainingMinutes = minutes % 60;
89
- if (hours === 0) {
90
- return `${remainingMinutes}m`;
91
- }
92
- if (remainingMinutes === 0) {
93
- return `${hours}h`;
94
- }
95
- return `${hours}h${remainingMinutes}m`;
96
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=duration-parser.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"duration-parser.test.d.ts","sourceRoot":"","sources":["../../src/utils/duration-parser.test.ts"],"names":[],"mappings":""}
@@ -1,147 +0,0 @@
1
- import { DurationParseError, formatDuration, parseDuration } from './duration-parser.js';
2
- describe('parseDuration', () => {
3
- describe('valid formats', () => {
4
- it('should parse hours only', () => {
5
- expect(parseDuration('2h')).toEqual({ minutes: 120 });
6
- expect(parseDuration('1h')).toEqual({ minutes: 60 });
7
- expect(parseDuration('24h')).toEqual({ minutes: 1440 });
8
- });
9
- it('should parse minutes only', () => {
10
- expect(parseDuration('90m')).toEqual({ minutes: 90 });
11
- expect(parseDuration('45m')).toEqual({ minutes: 45 });
12
- expect(parseDuration('1m')).toEqual({ minutes: 1 });
13
- expect(parseDuration('1440m')).toEqual({ minutes: 1440 });
14
- });
15
- it('should parse hours and minutes combined', () => {
16
- expect(parseDuration('2h30m')).toEqual({ minutes: 150 });
17
- expect(parseDuration('1h45m')).toEqual({ minutes: 105 });
18
- expect(parseDuration('0h30m')).toEqual({ minutes: 30 });
19
- expect(parseDuration('23h59m')).toEqual({ minutes: 1439 });
20
- });
21
- it('should parse decimal hours', () => {
22
- expect(parseDuration('1.5h')).toEqual({ minutes: 90 });
23
- expect(parseDuration('2.25h')).toEqual({ minutes: 135 });
24
- expect(parseDuration('0.5h')).toEqual({ minutes: 30 });
25
- expect(parseDuration('0.75h')).toEqual({ minutes: 45 });
26
- });
27
- it('should handle spaces in input', () => {
28
- expect(parseDuration('2h 30m')).toEqual({ minutes: 150 });
29
- expect(parseDuration(' 1h45m ')).toEqual({ minutes: 105 });
30
- expect(parseDuration(' 2h ')).toEqual({ minutes: 120 });
31
- expect(parseDuration(' 90m ')).toEqual({ minutes: 90 });
32
- });
33
- it('should handle case insensitive input', () => {
34
- expect(parseDuration('2H')).toEqual({ minutes: 120 });
35
- expect(parseDuration('90M')).toEqual({ minutes: 90 });
36
- expect(parseDuration('2H30M')).toEqual({ minutes: 150 });
37
- expect(parseDuration('1.5H')).toEqual({ minutes: 90 });
38
- });
39
- it('should round decimal minutes from decimal hours', () => {
40
- expect(parseDuration('1.33h')).toEqual({ minutes: 80 }); // 1.33 * 60 = 79.8 -> 80
41
- expect(parseDuration('1.67h')).toEqual({ minutes: 100 }); // 1.67 * 60 = 100.2 -> 100
42
- });
43
- });
44
- describe('invalid formats', () => {
45
- it('should throw error for empty or null input', () => {
46
- expect(() => parseDuration('')).toThrow(DurationParseError);
47
- expect(() => parseDuration(' ')).toThrow('Duration must be a non-empty string');
48
- // biome-ignore lint/suspicious/noExplicitAny: Testing error cases with invalid types
49
- expect(() => parseDuration(null)).toThrow('Duration must be a non-empty string');
50
- // biome-ignore lint/suspicious/noExplicitAny: Testing error cases with invalid types
51
- expect(() => parseDuration(undefined)).toThrow('Duration must be a non-empty string');
52
- });
53
- it('should throw error for invalid format', () => {
54
- expect(() => parseDuration('2')).toThrow('Use format like "2h", "30m", "2h30m", or "1.5h"');
55
- expect(() => parseDuration('2hours')).toThrow('Use format like');
56
- expect(() => parseDuration('2h30')).toThrow('Use format like');
57
- expect(() => parseDuration('h30m')).toThrow('Use format like');
58
- expect(() => parseDuration('2x30m')).toThrow('Use format like');
59
- expect(() => parseDuration('2h30s')).toThrow('Use format like');
60
- });
61
- it('should throw error for decimal minutes', () => {
62
- expect(() => parseDuration('90.5m')).toThrow('Minutes must be a whole number');
63
- expect(() => parseDuration('1h30.5m')).toThrow('Minutes must be a whole number');
64
- });
65
- it('should throw error for negative values', () => {
66
- expect(() => parseDuration('-2h')).toThrow('Use format like');
67
- expect(() => parseDuration('-30m')).toThrow('Use format like');
68
- expect(() => parseDuration('2h-30m')).toThrow('Use format like');
69
- });
70
- it('should throw error for zero duration', () => {
71
- expect(() => parseDuration('0h')).toThrow('Duration must be greater than 0 minutes');
72
- expect(() => parseDuration('0m')).toThrow('Duration must be greater than 0 minutes');
73
- expect(() => parseDuration('0h0m')).toThrow('Duration must be greater than 0 minutes');
74
- });
75
- it('should throw error for duration exceeding 24 hours', () => {
76
- expect(() => parseDuration('25h')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
77
- expect(() => parseDuration('1441m')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
78
- expect(() => parseDuration('24h1m')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
79
- expect(() => parseDuration('24.1h')).toThrow('Duration cannot exceed 24 hours (1440 minutes)');
80
- });
81
- it('should throw error for malformed numbers', () => {
82
- expect(() => parseDuration('2.h')).toThrow('Use format like');
83
- expect(() => parseDuration('2h.m')).toThrow('Use format like');
84
- expect(() => parseDuration('2..5h')).toThrow('Use format like');
85
- });
86
- it('should throw error for duplicate units', () => {
87
- expect(() => parseDuration('2h3h')).toThrow('Use format like');
88
- expect(() => parseDuration('30m45m')).toThrow('Use format like');
89
- });
90
- it('should throw error for wrong order (minutes before hours)', () => {
91
- expect(() => parseDuration('30m2h')).toThrow('Use format like');
92
- expect(() => parseDuration('45m1h')).toThrow('Use format like');
93
- });
94
- it('should throw error for invalid mixed formats with correct order', () => {
95
- expect(() => parseDuration('2h30m15h')).toThrow('Use format like');
96
- expect(() => parseDuration('1h2h30m')).toThrow('Use format like');
97
- });
98
- });
99
- describe('edge cases', () => {
100
- it('should handle maximum allowed duration', () => {
101
- expect(parseDuration('24h')).toEqual({ minutes: 1440 });
102
- expect(parseDuration('1440m')).toEqual({ minutes: 1440 });
103
- expect(parseDuration('23h60m')).toEqual({ minutes: 1440 });
104
- });
105
- it('should handle minimum allowed duration', () => {
106
- expect(parseDuration('1m')).toEqual({ minutes: 1 });
107
- expect(parseDuration('0.017h')).toEqual({ minutes: 1 }); // 0.017 * 60 = 1.02 -> 1
108
- });
109
- });
110
- });
111
- describe('formatDuration', () => {
112
- it('should format minutes only', () => {
113
- expect(formatDuration(45)).toBe('45m');
114
- expect(formatDuration(1)).toBe('1m');
115
- expect(formatDuration(59)).toBe('59m');
116
- });
117
- it('should format hours only', () => {
118
- expect(formatDuration(60)).toBe('1h');
119
- expect(formatDuration(120)).toBe('2h');
120
- expect(formatDuration(1440)).toBe('24h');
121
- });
122
- it('should format hours and minutes combined', () => {
123
- expect(formatDuration(90)).toBe('1h30m');
124
- expect(formatDuration(150)).toBe('2h30m');
125
- expect(formatDuration(105)).toBe('1h45m');
126
- expect(formatDuration(1439)).toBe('23h59m');
127
- });
128
- it('should handle edge cases', () => {
129
- expect(formatDuration(0)).toBe('0m');
130
- expect(formatDuration(-5)).toBe('0m');
131
- });
132
- });
133
- describe('round trip parsing and formatting', () => {
134
- const testCases = [
135
- { input: '2h', expectedMinutes: 120, expectedFormat: '2h' },
136
- { input: '45m', expectedMinutes: 45, expectedFormat: '45m' },
137
- { input: '2h30m', expectedMinutes: 150, expectedFormat: '2h30m' },
138
- { input: '1.5h', expectedMinutes: 90, expectedFormat: '1h30m' },
139
- ];
140
- for (const { input, expectedMinutes, expectedFormat } of testCases) {
141
- it(`should parse "${input}" and format back consistently`, () => {
142
- const parsed = parseDuration(input);
143
- expect(parsed.minutes).toBe(expectedMinutes);
144
- expect(formatDuration(parsed.minutes)).toBe(expectedFormat);
145
- });
146
- }
147
- });
@@ -1,18 +0,0 @@
1
- import { z } from 'zod';
2
- const LABELS_OPERATORS = ['and', 'or'];
3
- export const LabelsSchema = {
4
- labels: z.string().array().optional().describe('The labels to filter the tasks by'),
5
- labelsOperator: z
6
- .enum(LABELS_OPERATORS)
7
- .optional()
8
- .describe('The operator to use when filtering by labels. This will dictate whether a task has all labels, or some of them. Default is "or".'),
9
- };
10
- export function generateLabelsFilter(labels = [], labelsOperator = 'or') {
11
- if (labels.length === 0)
12
- return '';
13
- const operator = labelsOperator === 'and' ? ' & ' : ' | ';
14
- // Add @ prefix to labels for Todoist API query
15
- const prefixedLabels = labels.map((label) => (label.startsWith('@') ? label : `@${label}`));
16
- const labelStr = prefixedLabels.join(` ${operator} `);
17
- return `(${labelStr})`;
18
- }
@@ -1,20 +0,0 @@
1
- import { z } from 'zod';
2
- const PRIORITY_VALUES = ['p1', 'p2', 'p3', 'p4'];
3
- export const PrioritySchema = z.enum(PRIORITY_VALUES, {
4
- description: 'Task priority: p1 (highest), p2 (high), p3 (medium), p4 (lowest/default)',
5
- });
6
- export function convertPriorityToNumber(priority) {
7
- // Todoist API uses inverse mapping: p1=4 (highest), p2=3, p3=2, p4=1 (lowest)
8
- const priorityMap = { p1: 4, p2: 3, p3: 2, p4: 1 };
9
- return priorityMap[priority];
10
- }
11
- export function convertNumberToPriority(priority) {
12
- // Convert Todoist API numbers back to our enum
13
- const numberMap = { 4: 'p1', 3: 'p2', 2: 'p3', 1: 'p4' };
14
- return numberMap[priority];
15
- }
16
- export function formatPriorityForDisplay(priority) {
17
- // Convert Todoist API numbers to display format (P1, P2, P3, P4)
18
- const displayMap = { 4: 'P1', 3: 'P2', 2: 'P3', 1: 'P4' };
19
- return displayMap[priority] || '';
20
- }
@@ -1,210 +0,0 @@
1
- import { DisplayLimits } from './constants.js';
2
- import { formatPriorityForDisplay } from './priorities.js';
3
- import { ToolNames } from './tool-names.js';
4
- /**
5
- * Helper function to get date string in YYYY-MM-DD format
6
- */
7
- export function getDateString(date = new Date()) {
8
- const parts = date.toISOString().split('T');
9
- return parts[0] ?? '';
10
- }
11
- const { FIND_TASKS_BY_DATE, ADD_TASKS, UPDATE_TASKS, COMPLETE_TASKS, GET_OVERVIEW } = ToolNames;
12
- /**
13
- * Creates concise, actionable summaries for task operations instead of raw JSON
14
- */
15
- export function summarizeTaskOperation(action, tasks, options = {}) {
16
- const { nextSteps, context, showDetails = false } = options;
17
- const count = tasks.length;
18
- const bits = [];
19
- // Main action summary
20
- const taskOrTasks = count === 1 ? 'task' : 'tasks';
21
- const actionSummary = `${action} ${count} ${taskOrTasks}${context ? ` ${context}` : ''}.`;
22
- bits.push(actionSummary);
23
- // Task details preview (if requested or small batch)
24
- const smallBatchLimit = 5;
25
- if (showDetails || count <= smallBatchLimit) {
26
- const previews = previewTasks(tasks, smallBatchLimit);
27
- if (previews.length > 0) {
28
- const moreInfo = count > smallBatchLimit ? `, +${count - smallBatchLimit} more` : '';
29
- bits.push(`Tasks:\n${previews}${moreInfo}.`);
30
- }
31
- }
32
- // Next steps guidance
33
- if (nextSteps?.length) {
34
- bits.push(formatNextSteps(nextSteps));
35
- }
36
- return bits.join('\n');
37
- }
38
- /**
39
- * Creates batch operation summaries with success/failure breakdown
40
- */
41
- export function summarizeBatch(params) {
42
- const { action, success, total, successItems, failures, nextSteps } = params;
43
- const bits = [];
44
- // Main result summary
45
- const successBit = `${action}: ${success}/${total} successful.`;
46
- bits.push(successBit);
47
- // Success items (if provided and reasonable count)
48
- if (successItems?.length && successItems.length <= 5) {
49
- bits.push(`Completed:\n${successItems.map((item) => ` ${item}`).join('\n')}.`);
50
- }
51
- // Failure details (if any)
52
- if (failures?.length) {
53
- const failureCount = failures.length;
54
- const failureBit = `Failed (${failureCount}):\n${failures
55
- .slice(0, DisplayLimits.MAX_FAILURES_SHOWN)
56
- .map((f) => ` ${f.item} (Error: ${f.error}${f.code ? ` [${f.code}]` : ''})`)
57
- .join('\n')}${failureCount > DisplayLimits.MAX_FAILURES_SHOWN ? `, +${failureCount - DisplayLimits.MAX_FAILURES_SHOWN} more` : ''}.`;
58
- bits.push(failureBit);
59
- }
60
- // Next steps
61
- if (nextSteps?.length) {
62
- bits.push(formatNextSteps(nextSteps));
63
- }
64
- return bits.join('\n');
65
- }
66
- /**
67
- * Formats a single task-like object into a readable preview line
68
- */
69
- function formatTaskPreview(task) {
70
- const content = task.content || task.title || 'Untitled';
71
- const due = task.dueDate ? ` • due ${task.dueDate}` : '';
72
- const priority = task.priority ? ` • ${formatPriorityForDisplay(task.priority)}` : '';
73
- const project = task.projectName ? ` • ${task.projectName}` : '';
74
- const id = task.id ? ` • id=${task.id}` : '';
75
- return ` ${content}${due}${priority}${project}${id}`;
76
- }
77
- /**
78
- * Formats a single project-like object into a readable preview line
79
- */
80
- export function formatProjectPreview(project) {
81
- const isInbox = project.inboxProject ? ' • Inbox' : '';
82
- const isFavorite = project.isFavorite ? ' • ⭐' : '';
83
- const isShared = project.isShared ? ' • Shared' : '';
84
- const viewStyle = project.viewStyle && project.viewStyle !== 'list' ? ` • ${project.viewStyle}` : '';
85
- const id = ` • id=${project.id}`;
86
- return ` ${project.name}${isInbox}${isFavorite}${isShared}${viewStyle}${id}`;
87
- }
88
- /**
89
- * Creates preview lines for task lists
90
- */
91
- export function previewTasks(tasks, limit = 5) {
92
- const previewTasks = tasks.slice(0, limit);
93
- const lines = previewTasks.map(formatTaskPreview).join('\n');
94
- // If we're showing fewer tasks than the total, add an indicator
95
- if (tasks.length > limit) {
96
- const remaining = tasks.length - limit;
97
- return `${lines}\n ... and ${remaining} more task${remaining === 1 ? '' : 's'}`;
98
- }
99
- return lines;
100
- }
101
- /**
102
- * Creates list summaries with counts, filters, and guidance
103
- */
104
- export function summarizeList({ subject, count, limit, nextCursor, filterHints, previewLines, zeroReasonHints, nextSteps, }) {
105
- const bits = [];
106
- // Header with count and pagination info
107
- const header = `${subject}: ${count}${typeof limit === 'number' ? ` (limit ${limit})` : ''}${nextCursor ? ', more available' : ''}.`;
108
- bits.push(header);
109
- // Filter information
110
- if (filterHints?.length) {
111
- bits.push(`Filter: ${filterHints.join('; ')}.`);
112
- }
113
- // Preview of items
114
- if (previewLines?.length) {
115
- bits.push(`Preview:\n${previewLines}`);
116
- }
117
- // Help for empty results
118
- if (!count && zeroReasonHints?.length) {
119
- bits.push(`No results. ${zeroReasonHints.join('; ')}.`);
120
- }
121
- // Next steps guidance
122
- if (nextSteps?.length || nextCursor) {
123
- bits.push(formatNextSteps(nextSteps || [], nextCursor));
124
- }
125
- return bits.join('\n');
126
- }
127
- /**
128
- * Formats next steps array into a consistent "Next:" section
129
- * If nextCursor is provided, adds cursor instruction to the steps
130
- */
131
- export function formatNextSteps(nextSteps, nextCursor) {
132
- const allSteps = [...nextSteps];
133
- if (nextCursor) {
134
- allSteps.push(`Pass cursor '${nextCursor}' to fetch more results.`);
135
- }
136
- return `Next:\n${allSteps.map((step) => `- ${step}`).join('\n')}`;
137
- }
138
- /**
139
- * Helper to generate contextual next steps based on task operations
140
- */
141
- export function generateTaskNextSteps(operation, tasks, context) {
142
- const nextSteps = [];
143
- const count = context?.count ?? tasks.length;
144
- switch (operation.toLowerCase()) {
145
- case 'added':
146
- // Context-aware suggestions for newly added tasks
147
- if (context?.hasToday) {
148
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to review today's updated schedule`);
149
- }
150
- else if (context?.hasOverdue) {
151
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to prioritize past-due items`);
152
- }
153
- else if (context?.projectName) {
154
- nextSteps.push(`Use ${GET_OVERVIEW} with projectId to see ${context.projectName} structure`);
155
- }
156
- else {
157
- nextSteps.push(`Use ${GET_OVERVIEW} to see your updated project organization`);
158
- }
159
- // Time-based suggestions
160
- if (context?.timeOfDay === 'morning') {
161
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to plan your day`);
162
- }
163
- break;
164
- case 'updated':
165
- case 'organized':
166
- if (context?.hasToday) {
167
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to see your prioritized schedule`);
168
- }
169
- else if (context?.hasHighPriority) {
170
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE} with filter to focus on high-priority items`);
171
- }
172
- else {
173
- nextSteps.push(`Use ${GET_OVERVIEW} to see your updated project structure`);
174
- }
175
- break;
176
- case 'completed':
177
- if (context?.timeOfDay === 'evening') {
178
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('tomorrow') to plan upcoming work`);
179
- }
180
- else if (context?.hasOverdue) {
181
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to tackle remaining past-due items`);
182
- }
183
- else {
184
- nextSteps.push(`Use ${FIND_TASKS_BY_DATE}('today') to see remaining work`);
185
- }
186
- break;
187
- case 'listed':
188
- if (context?.isEmptyResult) {
189
- nextSteps.push(`Use ${ADD_TASKS} to add tasks for this timeframe`);
190
- nextSteps.push(`Use ${GET_OVERVIEW} with projectId to see tasks in other projects`);
191
- }
192
- else if (count > 0) {
193
- // Tailor suggestions based on result size
194
- if (count > DisplayLimits.BATCH_OPERATION_THRESHOLD) {
195
- nextSteps.push(`Use ${UPDATE_TASKS} to batch-update priorities or dates`);
196
- nextSteps.push('Consider breaking large tasks into subtasks');
197
- }
198
- else {
199
- nextSteps.push(`Use ${UPDATE_TASKS} to modify priorities or due dates`);
200
- }
201
- nextSteps.push(`Use ${COMPLETE_TASKS} to mark finished tasks`);
202
- // Time-sensitive suggestions
203
- if (context?.hasOverdue) {
204
- nextSteps.push('Focus on overdue items first to get back on track');
205
- }
206
- }
207
- break;
208
- }
209
- return nextSteps;
210
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Removes all null fields, empty objects, and empty arrays from an object recursively.
3
- * This ensures that data sent to agents doesn't include unnecessary empty values.
4
- *
5
- * @param obj - The object to sanitize
6
- * @returns A new object with all null fields, empty objects, and empty arrays removed
7
- */
8
- export function removeNullFields(obj) {
9
- if (obj === null || obj === undefined) {
10
- return obj;
11
- }
12
- if (Array.isArray(obj)) {
13
- return obj.map((item) => removeNullFields(item));
14
- }
15
- if (typeof obj === 'object') {
16
- const sanitized = {};
17
- for (const [key, value] of Object.entries(obj)) {
18
- if (value !== null) {
19
- const cleanedValue = removeNullFields(value);
20
- // Skip empty arrays
21
- if (Array.isArray(cleanedValue) && cleanedValue.length === 0) {
22
- continue;
23
- }
24
- // Skip empty objects
25
- if (cleanedValue !== null &&
26
- typeof cleanedValue === 'object' &&
27
- !Array.isArray(cleanedValue) &&
28
- Object.keys(cleanedValue).length === 0) {
29
- continue;
30
- }
31
- sanitized[key] = cleanedValue;
32
- }
33
- }
34
- return sanitized;
35
- }
36
- return obj;
37
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=sanitize-data.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"sanitize-data.test.d.ts","sourceRoot":"","sources":["../../src/utils/sanitize-data.test.ts"],"names":[],"mappings":""}
@@ -1,93 +0,0 @@
1
- import { removeNullFields } from './sanitize-data.js';
2
- describe('removeNullFields', () => {
3
- it('should remove null fields from objects including nested objects', () => {
4
- const input = {
5
- name: 'John',
6
- age: null,
7
- email: 'john@example.com',
8
- phone: null,
9
- address: {
10
- street: '123 Main St',
11
- city: null,
12
- country: 'USA',
13
- },
14
- };
15
- const result = removeNullFields(input);
16
- expect(result).toEqual({
17
- name: 'John',
18
- email: 'john@example.com',
19
- address: {
20
- street: '123 Main St',
21
- country: 'USA',
22
- },
23
- });
24
- });
25
- it('should handle arrays with null values', () => {
26
- const input = {
27
- items: [
28
- { id: 1, value: 'test' },
29
- { id: 2, value: null },
30
- { id: 3, value: 'another' },
31
- ],
32
- };
33
- const result = removeNullFields(input);
34
- expect(result).toEqual({
35
- items: [{ id: 1, value: 'test' }, { id: 2 }, { id: 3, value: 'another' }],
36
- });
37
- });
38
- it('should handle null objects', () => {
39
- const result = removeNullFields(null);
40
- expect(result).toBeNull();
41
- });
42
- it('should remove empty objects and empty arrays', () => {
43
- const input = {
44
- something: 'hello',
45
- another: {},
46
- yetAnother: [],
47
- };
48
- const result = removeNullFields(input);
49
- expect(result).toEqual({
50
- something: 'hello',
51
- });
52
- });
53
- it('should remove empty objects and empty arrays in nested structures', () => {
54
- const input = {
55
- name: 'Test',
56
- metadata: {},
57
- tags: [],
58
- nested: {
59
- data: 'value',
60
- emptyObj: {},
61
- emptyArr: [],
62
- deepNested: {
63
- anotherEmpty: {},
64
- },
65
- },
66
- items: [
67
- { id: 1, data: 'test', empty: {} },
68
- { id: 2, list: [] },
69
- ],
70
- };
71
- const result = removeNullFields(input);
72
- expect(result).toEqual({
73
- name: 'Test',
74
- nested: {
75
- data: 'value',
76
- },
77
- items: [{ id: 1, data: 'test' }, { id: 2 }],
78
- });
79
- });
80
- it('should keep arrays with values and objects with properties', () => {
81
- const input = {
82
- emptyArray: [],
83
- arrayWithValues: [1, 2, 3],
84
- emptyObject: {},
85
- objectWithProps: { key: 'value' },
86
- };
87
- const result = removeNullFields(input);
88
- expect(result).toEqual({
89
- arrayWithValues: [1, 2, 3],
90
- objectWithProps: { key: 'value' },
91
- });
92
- });
93
- });