File size: 6,611 Bytes
c09f67c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import type { AppContext } from "@api/ai/agents/config/shared";
import { tz } from "@date-fns/tz";
import { db } from "@midday/db/client";
import { getTrackerProjects, upsertTrackerEntries } from "@midday/db/queries";
import { getAppUrl } from "@midday/utils/envs";
import { formatDate } from "@midday/utils/format";
import { tool } from "ai";
import { formatDistance, setHours, setMinutes } from "date-fns";
import parseDuration from "parse-duration";
import { z } from "zod";

const createTrackerEntrySchema = z.object({
  projectName: z.string().nullable().optional().describe("Project name"),
  projectId: z.string().nullable().optional().describe("Project ID"),
  duration: z.string().describe("Duration (e.g. '8h', '2h 30m', '480m')"),
  date: z.string().nullable().optional().describe("Date (YYYY-MM-DD)"),
  description: z.string().nullable().optional().describe("Description"),
});

/**
 * Parse duration string to seconds using parse-duration library
 * Supports: "8h", "2h", "30m", "480m", "8.5h" (decimal hours), "2h 30m" (compound)
 */
function parseDurationToSeconds(durationStr: string): number {
  const trimmed = durationStr.trim();

  // Handle bare decimal numbers (e.g., "8.5" treated as hours)
  if (/^\d+\.?\d*$/.test(trimmed)) {
    const hours = Number.parseFloat(trimmed);
    if (!Number.isNaN(hours)) {
      return Math.round(hours * 3600);
    }
  }

  // Use parse-duration for all other formats
  const milliseconds = parseDuration(trimmed);
  if (milliseconds === null || milliseconds === undefined) {
    throw new Error(
      `Invalid duration format: ${durationStr}. Use formats like '8h', '2h 30m', '480m', or '8.5h'`,
    );
  }

  return Math.round(milliseconds / 1000);
}

export const createTrackerEntryTool = tool({
  description:
    "Create a time entry for a tracker project - supports finding projects by name and flexible duration formats.",
  inputSchema: createTrackerEntrySchema,
  execute: async function* (
    { projectName, projectId, duration, date, description },
    executionOptions,
  ) {
    const appContext = executionOptions.experimental_context as AppContext;
    const teamId = appContext.teamId as string;
    const userId = appContext.userId || null;
    const searchProjectName = projectName;

    if (!teamId) {
      yield {
        text: "Unable to create tracker entry: Team ID not found in context.",
      };
      return;
    }

    try {
      // Find project by name or use provided projectId
      let finalProjectId: string | null = projectId || null;

      if (!finalProjectId && searchProjectName) {
        // Search for project by name
        const projectsResult = await getTrackerProjects(db, {
          teamId,
          q: searchProjectName,
          pageSize: 5,
        });

        if (projectsResult.data.length === 0) {
          yield {
            text: `No project found matching "${searchProjectName}". Please check the project name or provide a project ID.`,
          };
          return;
        }

        // Use the first matching project
        finalProjectId = projectsResult.data[0]?.id || null;

        // If multiple matches, mention it but use the first one
        if (projectsResult.data.length > 1 && projectsResult.data[0]) {
          const projectNames = projectsResult.data
            .map((p) => p.name)
            .join(", ");
          yield {
            text: `Multiple projects found matching "${searchProjectName}". Using "${projectsResult.data[0].name}". Other matches: ${projectNames}`,
          };
        }
      }

      if (!finalProjectId) {
        yield {
          text: "Please provide either a projectName or projectId to create a time entry.",
        };
        return;
      }

      // Parse duration
      let durationSeconds: number;
      try {
        durationSeconds = parseDurationToSeconds(duration);
      } catch (error) {
        yield {
          text:
            error instanceof Error ? error.message : "Invalid duration format.",
        };
        return;
      }

      // Determine date (default to today)
      const entryDate = date || new Date().toISOString().split("T")[0];
      if (!entryDate) {
        yield {
          text: "Invalid date format.",
        };
        return;
      }

      // Calculate start and stop times (9 AM in user's timezone + duration)
      const userTimezone = appContext.timezone || "UTC";
      let startTime: Date;

      if (userTimezone && userTimezone !== "UTC") {
        try {
          // Create a date at 9 AM in the user's timezone
          const createTZDate = tz(userTimezone);
          const baseDate = createTZDate(new Date(`${entryDate}T00:00:00`));
          startTime = setMinutes(setHours(baseDate, 9), 0);
        } catch (_error) {
          // Fallback to UTC if timezone conversion fails
          startTime = new Date(`${entryDate}T09:00:00.000Z`);
        }
      } else {
        // Use UTC if no timezone provided
        startTime = new Date(`${entryDate}T09:00:00.000Z`);
      }

      const stopTime = new Date(startTime.getTime() + durationSeconds * 1000);

      const startTimeISO = startTime.toISOString();
      const stopTimeISO = stopTime.toISOString();

      // Create the entry
      const result = await upsertTrackerEntries(db, {
        teamId,
        projectId: finalProjectId,
        start: startTimeISO,
        stop: stopTimeISO,
        dates: [entryDate],
        duration: durationSeconds,
        assignedId: userId,
        description: description || null,
      });

      if (!result || result.length === 0) {
        yield {
          text: "Failed to create tracker entry.",
        };
        return;
      }

      const entry = result[0];
      if (!entry) {
        yield {
          text: "Failed to create tracker entry.",
        };
        return;
      }

      const start = new Date(0);
      const end = new Date(durationSeconds * 1000);
      const formattedDuration = formatDistance(start, end, {
        includeSeconds: false,
      });
      const projectName = entry.trackerProject?.name || "Unknown";

      const response = `Successfully created time entry:\n\n**Project:** ${projectName}\n**Date:** ${formatDate(entryDate)}\n**Duration:** ${formattedDuration}\n**Description:** ${entry.description || "None"}`;

      yield {
        text: response,
        link: {
          text: "View tracker",
          url: `${getAppUrl()}/tracker`,
        },
      };
    } catch (error) {
      yield {
        text: `Failed to create tracker entry: ${error instanceof Error ? error.message : "Unknown error"}`,
      };
    }
  },
});