@iflow-mcp/omnifocus-mcp 1.2.3

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 (69) hide show
  1. package/QUERY_TOOL_EXAMPLES.md +298 -0
  2. package/QUERY_TOOL_REFERENCE.md +228 -0
  3. package/README.md +250 -0
  4. package/assets/omnifocus-mcp-logo.png +0 -0
  5. package/cli.cjs +9 -0
  6. package/dist/omnifocustypes.js +48 -0
  7. package/dist/server.js +44 -0
  8. package/dist/tools/definitions/addOmniFocusTask.js +76 -0
  9. package/dist/tools/definitions/addProject.js +61 -0
  10. package/dist/tools/definitions/batchAddItems.js +89 -0
  11. package/dist/tools/definitions/batchRemoveItems.js +74 -0
  12. package/dist/tools/definitions/dumpDatabase.js +259 -0
  13. package/dist/tools/definitions/editItem.js +88 -0
  14. package/dist/tools/definitions/getPerspectiveView.js +107 -0
  15. package/dist/tools/definitions/listPerspectives.js +65 -0
  16. package/dist/tools/definitions/queryOmnifocus.js +190 -0
  17. package/dist/tools/definitions/removeItem.js +80 -0
  18. package/dist/tools/dumpDatabase.js +121 -0
  19. package/dist/tools/dumpDatabaseOptimized.js +192 -0
  20. package/dist/tools/primitives/addOmniFocusTask.js +227 -0
  21. package/dist/tools/primitives/addProject.js +132 -0
  22. package/dist/tools/primitives/batchAddItems.js +166 -0
  23. package/dist/tools/primitives/batchRemoveItems.js +44 -0
  24. package/dist/tools/primitives/editItem.js +443 -0
  25. package/dist/tools/primitives/getPerspectiveView.js +50 -0
  26. package/dist/tools/primitives/listPerspectives.js +34 -0
  27. package/dist/tools/primitives/queryOmnifocus.js +365 -0
  28. package/dist/tools/primitives/queryOmnifocusDebug.js +135 -0
  29. package/dist/tools/primitives/removeItem.js +177 -0
  30. package/dist/types.js +1 -0
  31. package/dist/utils/cacheManager.js +187 -0
  32. package/dist/utils/dateFormatting.js +58 -0
  33. package/dist/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  34. package/dist/utils/omnifocusScripts/listPerspectives.js +59 -0
  35. package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
  36. package/dist/utils/scriptExecution.js +113 -0
  37. package/package.json +37 -0
  38. package/src/omnifocustypes.ts +89 -0
  39. package/src/server.ts +109 -0
  40. package/src/tools/definitions/addOmniFocusTask.ts +80 -0
  41. package/src/tools/definitions/addProject.ts +67 -0
  42. package/src/tools/definitions/batchAddItems.ts +98 -0
  43. package/src/tools/definitions/batchRemoveItems.ts +80 -0
  44. package/src/tools/definitions/dumpDatabase.ts +311 -0
  45. package/src/tools/definitions/editItem.ts +96 -0
  46. package/src/tools/definitions/getPerspectiveView.ts +125 -0
  47. package/src/tools/definitions/listPerspectives.ts +72 -0
  48. package/src/tools/definitions/queryOmnifocus.ts +212 -0
  49. package/src/tools/definitions/removeItem.ts +86 -0
  50. package/src/tools/dumpDatabase.ts +196 -0
  51. package/src/tools/dumpDatabaseOptimized.ts +231 -0
  52. package/src/tools/primitives/addOmniFocusTask.ts +252 -0
  53. package/src/tools/primitives/addProject.ts +156 -0
  54. package/src/tools/primitives/batchAddItems.ts +207 -0
  55. package/src/tools/primitives/batchRemoveItems.ts +64 -0
  56. package/src/tools/primitives/editItem.ts +507 -0
  57. package/src/tools/primitives/getPerspectiveView.ts +71 -0
  58. package/src/tools/primitives/listPerspectives.ts +53 -0
  59. package/src/tools/primitives/queryOmnifocus.ts +394 -0
  60. package/src/tools/primitives/queryOmnifocusDebug.ts +139 -0
  61. package/src/tools/primitives/removeItem.ts +195 -0
  62. package/src/types.ts +107 -0
  63. package/src/utils/cacheManager.ts +234 -0
  64. package/src/utils/dateFormatting.ts +81 -0
  65. package/src/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  66. package/src/utils/omnifocusScripts/listPerspectives.js +59 -0
  67. package/src/utils/omnifocusScripts/omnifocusDump.js +223 -0
  68. package/src/utils/scriptExecution.ts +128 -0
  69. package/tsconfig.json +15 -0
@@ -0,0 +1,195 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execAsync = promisify(exec);
4
+
5
+ // Interface for item removal parameters
6
+ export interface RemoveItemParams {
7
+ id?: string; // ID of the task or project to remove
8
+ name?: string; // Name of the task or project to remove (as fallback if ID not provided)
9
+ itemType: 'task' | 'project'; // Type of item to remove
10
+ }
11
+
12
+ /**
13
+ * Generate pure AppleScript for item removal
14
+ */
15
+ function generateAppleScript(params: RemoveItemParams): string {
16
+ // Sanitize and prepare parameters for AppleScript
17
+ const id = params.id?.replace(/['"\\]/g, '\\$&') || ''; // Escape quotes and backslashes
18
+ const name = params.name?.replace(/['"\\]/g, '\\$&') || '';
19
+ const itemType = params.itemType;
20
+
21
+ // Verify we have at least one identifier
22
+ if (!id && !name) {
23
+ return `return "{\\\"success\\\":false,\\\"error\\\":\\\"Either id or name must be provided\\\"}"`;
24
+ }
25
+
26
+ // Construct AppleScript with error handling
27
+ let script = `
28
+ try
29
+ tell application "OmniFocus"
30
+ tell front document
31
+ -- Find the item to remove
32
+ set foundItem to missing value
33
+ `;
34
+
35
+ // Add ID search if provided
36
+ if (id) {
37
+ if (itemType === 'task') {
38
+ script += `
39
+ -- Try to find task by ID (search in projects first, then inbox)
40
+ try
41
+ set foundItem to first flattened task where id = "${id}"
42
+ end try
43
+
44
+ -- If not found in projects, search in inbox
45
+ if foundItem is missing value then
46
+ try
47
+ set foundItem to first inbox task where id = "${id}"
48
+ end try
49
+ end if
50
+ `;
51
+ } else {
52
+ script += `
53
+ -- Try to find project by ID
54
+ try
55
+ set foundItem to first flattened project where id = "${id}"
56
+ end try
57
+ `;
58
+ }
59
+ }
60
+
61
+ // Add name search if provided (and no ID or as fallback)
62
+ if (!id && name) {
63
+ if (itemType === 'task') {
64
+ script += `
65
+ -- Find task by name (search in projects first, then inbox)
66
+ try
67
+ set foundItem to first flattened task where name = "${name}"
68
+ end try
69
+
70
+ -- If not found in projects, search in inbox
71
+ if foundItem is missing value then
72
+ try
73
+ set foundItem to first inbox task where name = "${name}"
74
+ end try
75
+ end if
76
+ `;
77
+ } else {
78
+ script += `
79
+ -- Find project by name
80
+ try
81
+ set foundItem to first flattened project where name = "${name}"
82
+ end try
83
+ `;
84
+ }
85
+ } else if (id && name) {
86
+ if (itemType === 'task') {
87
+ script += `
88
+ -- If ID search failed, try to find by name as fallback
89
+ if foundItem is missing value then
90
+ try
91
+ set foundItem to first flattened task where name = "${name}"
92
+ end try
93
+ end if
94
+
95
+ -- If still not found, search in inbox
96
+ if foundItem is missing value then
97
+ try
98
+ set foundItem to first inbox task where name = "${name}"
99
+ end try
100
+ end if
101
+ `;
102
+ } else {
103
+ script += `
104
+ -- If ID search failed, try to find project by name as fallback
105
+ if foundItem is missing value then
106
+ try
107
+ set foundItem to first flattened project where name = "${name}"
108
+ end try
109
+ end if
110
+ `;
111
+ }
112
+ }
113
+
114
+ // Add the rest of the script
115
+ script += `
116
+ -- If we found the item, remove it
117
+ if foundItem is not missing value then
118
+ set itemName to name of foundItem
119
+ set itemId to id of foundItem as string
120
+
121
+ -- Delete the item
122
+ delete foundItem
123
+
124
+ -- Return success
125
+ return "{\\\"success\\\":true,\\\"id\\\":\\"" & itemId & "\\",\\\"name\\\":\\"" & itemName & "\\"}"
126
+ else
127
+ -- Item not found
128
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Item not found\\\"}"
129
+ end if
130
+ end tell
131
+ end tell
132
+ on error errorMessage
133
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
134
+ end try
135
+ `;
136
+
137
+ return script;
138
+ }
139
+
140
+ /**
141
+ * Remove a task or project from OmniFocus
142
+ */
143
+ export async function removeItem(params: RemoveItemParams): Promise<{success: boolean, id?: string, name?: string, error?: string}> {
144
+ try {
145
+ // Generate AppleScript
146
+ const script = generateAppleScript(params);
147
+
148
+ console.error("Executing AppleScript for removal...");
149
+ console.error(`Item type: ${params.itemType}, ID: ${params.id || 'not provided'}, Name: ${params.name || 'not provided'}`);
150
+
151
+ // Log a preview of the script for debugging (first few lines)
152
+ const scriptPreview = script.split('\n').slice(0, 10).join('\n') + '\n...';
153
+ console.error("AppleScript preview:\n", scriptPreview);
154
+
155
+ // Execute AppleScript directly
156
+ const { stdout, stderr } = await execAsync(`osascript -e '${script}'`);
157
+
158
+ if (stderr) {
159
+ console.error("AppleScript stderr:", stderr);
160
+ }
161
+
162
+ console.error("AppleScript stdout:", stdout);
163
+
164
+ // Parse the result
165
+ try {
166
+ const result = JSON.parse(stdout);
167
+
168
+ // Return the result
169
+ return {
170
+ success: result.success,
171
+ id: result.id,
172
+ name: result.name,
173
+ error: result.error
174
+ };
175
+ } catch (parseError) {
176
+ console.error("Error parsing AppleScript result:", parseError);
177
+ return {
178
+ success: false,
179
+ error: `Failed to parse result: ${stdout}`
180
+ };
181
+ }
182
+ } catch (error: any) {
183
+ console.error("Error in removeItem execution:", error);
184
+
185
+ // Include more detailed error information
186
+ if (error.message && error.message.includes('syntax error')) {
187
+ console.error("This appears to be an AppleScript syntax error. Review the script generation logic.");
188
+ }
189
+
190
+ return {
191
+ success: false,
192
+ error: error?.message || "Unknown error in removeItem"
193
+ };
194
+ }
195
+ }
package/src/types.ts ADDED
@@ -0,0 +1,107 @@
1
+ export interface OmnifocusTask {
2
+ id: string;
3
+ name: string;
4
+ note: string;
5
+ flagged: boolean;
6
+
7
+ // Status
8
+ completed: boolean;
9
+ completionDate: string | null;
10
+ dropDate: string | null;
11
+ taskStatus: string; // One of Task.Status values
12
+ active: boolean;
13
+
14
+ // Dates
15
+ dueDate: string | null;
16
+ deferDate: string | null;
17
+ estimatedMinutes: number | null;
18
+
19
+ // Organization
20
+ tags: string[]; // Tag IDs
21
+ tagNames: string[]; // Human-readable tag names
22
+ parentId: string | null;
23
+ containingProjectId: string | null;
24
+ projectId: string | null;
25
+
26
+ // Task relationships
27
+ childIds: string[];
28
+ hasChildren: boolean;
29
+ sequential: boolean;
30
+ completedByChildren: boolean;
31
+
32
+ // Recurring task information
33
+ repetitionRule: string | null; // Textual representation of repetition rule
34
+ isRepeating: boolean;
35
+ repetitionMethod: string | null; // Fixed or due-based repetition
36
+
37
+ // Attachments
38
+ attachments: any[]; // FileWrapper representations
39
+ linkedFileURLs: string[];
40
+
41
+ // Notifications
42
+ notifications: any[]; // Task.Notification representations
43
+
44
+ // Settings
45
+ shouldUseFloatingTimeZone: boolean;
46
+ }
47
+
48
+ export interface OmnifocusDatabase {
49
+ exportDate: string;
50
+ tasks: OmnifocusTask[];
51
+ projects: Record<string, OmnifocusProject>;
52
+ folders: Record<string, OmnifocusFolder>;
53
+ tags: Record<string, OmnifocusTag>;
54
+ }
55
+
56
+ export interface OmnifocusProject {
57
+ id: string;
58
+ name: string;
59
+ status: string;
60
+ folderID: string | null;
61
+ sequential: boolean;
62
+ effectiveDueDate: string | null;
63
+ effectiveDeferDate: string | null;
64
+ dueDate: string | null;
65
+ deferDate: string | null;
66
+ completedByChildren: boolean;
67
+ containsSingletonActions: boolean;
68
+ note: string;
69
+ tasks: string[]; // Task IDs
70
+ flagged?: boolean;
71
+ estimatedMinutes?: number | null;
72
+ }
73
+
74
+ export interface OmnifocusFolder {
75
+ id: string;
76
+ name: string;
77
+ parentFolderID: string | null;
78
+ status: string;
79
+ projects: string[]; // Project IDs
80
+ subfolders: string[]; // Subfolder IDs
81
+ }
82
+
83
+ export interface OmnifocusTag {
84
+ id: string;
85
+ name: string;
86
+ parentTagID: string | null;
87
+ active: boolean;
88
+ allowsNextAction: boolean;
89
+ tasks: string[]; // Task IDs
90
+ }
91
+
92
+ export interface OmnifocusPerspective {
93
+ id: string;
94
+ name: string;
95
+ type: 'builtin' | 'custom';
96
+ isBuiltIn: boolean;
97
+ canModify: boolean; // false for built-in perspectives
98
+ // Filter rules for custom perspectives (if applicable)
99
+ filterRules?: {
100
+ availability?: string[];
101
+ tags?: string[];
102
+ projects?: string[];
103
+ flagged?: boolean;
104
+ dueWithin?: number;
105
+ // Additional filter properties as needed
106
+ };
107
+ }
@@ -0,0 +1,234 @@
1
+ import { OmnifocusDatabase } from '../types.js';
2
+ import { executeOmniFocusScript } from './scriptExecution.js';
3
+
4
+ interface CacheEntry {
5
+ data: OmnifocusDatabase;
6
+ timestamp: Date;
7
+ checksum?: string;
8
+ }
9
+
10
+ interface CacheOptions {
11
+ ttlSeconds?: number; // Time to live in seconds
12
+ maxSize?: number; // Maximum cache size in MB
13
+ useChecksum?: boolean; // Whether to use checksum for validation
14
+ }
15
+
16
+ class OmniFocusCacheManager {
17
+ private cache: Map<string, CacheEntry> = new Map();
18
+ private options: Required<CacheOptions>;
19
+ private totalSize: number = 0;
20
+
21
+ constructor(options: CacheOptions = {}) {
22
+ this.options = {
23
+ ttlSeconds: options.ttlSeconds ?? 300, // 5 minutes default
24
+ maxSize: options.maxSize ?? 50, // 50MB default
25
+ useChecksum: options.useChecksum ?? true
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Get cached data if valid, otherwise return null
31
+ */
32
+ async get(key: string): Promise<OmnifocusDatabase | null> {
33
+ const entry = this.cache.get(key);
34
+
35
+ if (!entry) {
36
+ return null;
37
+ }
38
+
39
+ // Check if cache has expired
40
+ const age = Date.now() - entry.timestamp.getTime();
41
+ if (age > this.options.ttlSeconds * 1000) {
42
+ this.cache.delete(key);
43
+ return null;
44
+ }
45
+
46
+ // If using checksums, validate the cache is still current
47
+ if (this.options.useChecksum && entry.checksum) {
48
+ const currentChecksum = await this.getDatabaseChecksum();
49
+ if (currentChecksum !== entry.checksum) {
50
+ this.cache.delete(key);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ return entry.data;
56
+ }
57
+
58
+ /**
59
+ * Store data in cache
60
+ */
61
+ async set(key: string, data: OmnifocusDatabase): Promise<void> {
62
+ // Estimate size (rough approximation)
63
+ const dataSize = JSON.stringify(data).length / (1024 * 1024); // Convert to MB
64
+
65
+ // Check if adding this would exceed max size
66
+ if (this.totalSize + dataSize > this.options.maxSize) {
67
+ this.evictOldest();
68
+ }
69
+
70
+ const checksum = this.options.useChecksum ? await this.getDatabaseChecksum() : undefined;
71
+
72
+ this.cache.set(key, {
73
+ data,
74
+ timestamp: new Date(),
75
+ checksum
76
+ });
77
+
78
+ this.totalSize += dataSize;
79
+ }
80
+
81
+ /**
82
+ * Invalidate specific cache entry
83
+ */
84
+ invalidate(key: string): void {
85
+ this.cache.delete(key);
86
+ }
87
+
88
+ /**
89
+ * Clear all cache entries
90
+ */
91
+ clear(): void {
92
+ this.cache.clear();
93
+ this.totalSize = 0;
94
+ }
95
+
96
+ /**
97
+ * Get cache statistics
98
+ */
99
+ getStats(): {
100
+ entries: number;
101
+ sizeEstimateMB: number;
102
+ oldestEntry: Date | null;
103
+ hitRate: number;
104
+ } {
105
+ let oldestEntry: Date | null = null;
106
+
107
+ this.cache.forEach(entry => {
108
+ if (!oldestEntry || entry.timestamp < oldestEntry) {
109
+ oldestEntry = entry.timestamp;
110
+ }
111
+ });
112
+
113
+ return {
114
+ entries: this.cache.size,
115
+ sizeEstimateMB: this.totalSize,
116
+ oldestEntry,
117
+ hitRate: this.calculateHitRate()
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Get a lightweight checksum of the database state
123
+ */
124
+ private async getDatabaseChecksum(): Promise<string> {
125
+ try {
126
+ const script = `
127
+ (() => {
128
+ try {
129
+ // Get counts and latest modification times as a simple checksum
130
+ const taskCount = flattenedTasks.length;
131
+ const projectCount = flattenedProjects.length;
132
+
133
+ // Get the most recent modification time
134
+ let latestMod = new Date(0);
135
+
136
+ flattenedTasks.forEach(task => {
137
+ if (task.modificationDate && task.modificationDate > latestMod) {
138
+ latestMod = task.modificationDate;
139
+ }
140
+ });
141
+
142
+ flattenedProjects.forEach(project => {
143
+ if (project.modificationDate && project.modificationDate > latestMod) {
144
+ latestMod = project.modificationDate;
145
+ }
146
+ });
147
+
148
+ // Create a simple checksum string
149
+ const checksum = taskCount + "-" + projectCount + "-" + latestMod.getTime();
150
+
151
+ return JSON.stringify({ checksum });
152
+ } catch (error) {
153
+ return JSON.stringify({ checksum: "error-" + Date.now() });
154
+ }
155
+ })();
156
+ `;
157
+
158
+ // Write to temp file and execute
159
+ const fs = await import('fs');
160
+ const tempFile = `/tmp/omnifocus_checksum_${Date.now()}.js`;
161
+ fs.writeFileSync(tempFile, script);
162
+
163
+ const result = await executeOmniFocusScript(tempFile);
164
+ fs.unlinkSync(tempFile);
165
+
166
+ return result.checksum || `fallback-${Date.now()}`;
167
+ } catch (error) {
168
+ console.error('Error getting database checksum:', error);
169
+ return `error-${Date.now()}`;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Evict oldest cache entries when size limit is reached
175
+ */
176
+ private evictOldest(): void {
177
+ let oldestKey: string | null = null;
178
+ let oldestTime = new Date();
179
+
180
+ this.cache.forEach((entry, key) => {
181
+ if (entry.timestamp < oldestTime) {
182
+ oldestTime = entry.timestamp;
183
+ oldestKey = key;
184
+ }
185
+ });
186
+
187
+ if (oldestKey) {
188
+ this.cache.delete(oldestKey);
189
+ // Recalculate total size (simplified for now)
190
+ this.totalSize *= 0.9;
191
+ }
192
+ }
193
+
194
+ // Tracking for hit rate calculation
195
+ private hits = 0;
196
+ private misses = 0;
197
+
198
+ private calculateHitRate(): number {
199
+ const total = this.hits + this.misses;
200
+ if (total === 0) return 0;
201
+ return (this.hits / total) * 100;
202
+ }
203
+
204
+ trackHit(): void {
205
+ this.hits++;
206
+ }
207
+
208
+ trackMiss(): void {
209
+ this.misses++;
210
+ }
211
+ }
212
+
213
+ // Singleton instance
214
+ let cacheManager: OmniFocusCacheManager | null = null;
215
+
216
+ /**
217
+ * Get or create the cache manager instance
218
+ */
219
+ export function getCacheManager(options?: CacheOptions): OmniFocusCacheManager {
220
+ if (!cacheManager) {
221
+ cacheManager = new OmniFocusCacheManager(options);
222
+ }
223
+ return cacheManager;
224
+ }
225
+
226
+ /**
227
+ * Reset the cache manager (useful for testing)
228
+ */
229
+ export function resetCacheManager(): void {
230
+ if (cacheManager) {
231
+ cacheManager.clear();
232
+ }
233
+ cacheManager = null;
234
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Version 2 of date formatting utilities that work around AppleScript restrictions
3
+ * Dates must be constructed outside of tell blocks
4
+ */
5
+
6
+ /**
7
+ * Generate AppleScript to construct a date variable outside tell blocks
8
+ * @param isoDateString - ISO format date string
9
+ * @param varName - Name for the date variable
10
+ * @returns AppleScript code to construct the date
11
+ */
12
+ export function createDateOutsideTellBlock(isoDateString: string, varName: string): string {
13
+ // Parse the ISO date string
14
+ const date = new Date(isoDateString);
15
+
16
+ // Check if the date is valid
17
+ if (isNaN(date.getTime())) {
18
+ throw new Error(`Invalid date string: ${isoDateString}`);
19
+ }
20
+
21
+ // Extract date components
22
+ const year = date.getFullYear();
23
+ const month = date.getMonth() + 1; // JavaScript months are 0-indexed
24
+ const day = date.getDate();
25
+ const hours = date.getHours();
26
+ const minutes = date.getMinutes();
27
+ const seconds = date.getSeconds();
28
+
29
+ // Generate AppleScript to construct date outside tell blocks
30
+ return `copy current date to ${varName}
31
+ set year of ${varName} to ${year}
32
+ set month of ${varName} to ${month}
33
+ set day of ${varName} to ${day}
34
+ set hours of ${varName} to ${hours}
35
+ set minutes of ${varName} to ${minutes}
36
+ set seconds of ${varName} to ${seconds}`;
37
+ }
38
+
39
+ /**
40
+ * Generate the complete AppleScript for date assignments
41
+ * Returns both the pre-tell block code and the in-tell block assignment
42
+ */
43
+ export interface DateAssignmentParts {
44
+ preScript: string; // Code to run before tell blocks
45
+ assignmentScript: string; // Code to run inside tell blocks
46
+ }
47
+
48
+ /**
49
+ * Generate date assignment that works with AppleScript restrictions
50
+ */
51
+ export function generateDateAssignmentV2(
52
+ objectName: string,
53
+ propertyName: string,
54
+ isoDateString: string | undefined
55
+ ): DateAssignmentParts | null {
56
+ if (isoDateString === undefined) {
57
+ return null; // No date change requested
58
+ }
59
+
60
+ if (isoDateString === '') {
61
+ // Clear the date
62
+ return {
63
+ preScript: '',
64
+ assignmentScript: `set ${propertyName} of ${objectName} to missing value`
65
+ };
66
+ }
67
+
68
+ // Generate unique variable name
69
+ const varName = `dateVar${Math.random().toString(36).substr(2, 9)}`;
70
+
71
+ // Generate the date construction (outside tell blocks)
72
+ const preScript = createDateOutsideTellBlock(isoDateString, varName);
73
+
74
+ // Generate the assignment (inside tell blocks)
75
+ const assignmentScript = `set ${propertyName} of ${objectName} to ${varName}`;
76
+
77
+ return {
78
+ preScript,
79
+ assignmentScript
80
+ };
81
+ }