@mgsoftwarebv/mcp-server-bridge 2.23.4 → 3.0.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.
package/dist/index.js CHANGED
@@ -2,45 +2,176 @@
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { ListToolsRequestSchema, ListResourcesRequestSchema, CallToolRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
- import { createClient } from '@supabase/supabase-js';
6
- import { createHash } from 'crypto';
7
5
  import { Octokit } from '@octokit/rest';
6
+ import { createHash } from 'crypto';
7
+ import { eq, or, ilike, and, desc, asc, inArray, sql } from 'drizzle-orm';
8
+ import { createJobDb } from '@refront/db/job-client';
9
+ import * as schema from '@refront/db/schema';
10
+ import { createStorageClient } from '@refront/storage';
11
+
12
+ var _client = null;
13
+ function getClient() {
14
+ if (!_client) {
15
+ if (!process.env.DATABASE_PRIMARY_POOLER_URL) {
16
+ throw new Error(
17
+ "DATABASE_PRIMARY_POOLER_URL is not set. Pass --database-url=... or export the env var before starting the MCP bridge."
18
+ );
19
+ }
20
+ _client = createJobDb();
21
+ }
22
+ return _client;
23
+ }
24
+ var db = new Proxy({}, {
25
+ get(_target, prop) {
26
+ const real = getClient().db;
27
+ const value = Reflect.get(real, prop, real);
28
+ return typeof value === "function" ? value.bind(real) : value;
29
+ }
30
+ });
31
+ async function getAccessibleTeamIds(teamId) {
32
+ const rows = await db.select({ id: schema.teams.id }).from(schema.teams).where(
33
+ or(eq(schema.teams.id, teamId), eq(schema.teams.parentTeamId, teamId))
34
+ );
35
+ const ids = rows.map((r) => r.id);
36
+ return ids.length > 0 ? ids : [teamId];
37
+ }
38
+ async function getAccessibleProjectIds(userId, teamId) {
39
+ try {
40
+ const rows = await db.execute(
41
+ sql`SELECT project_id FROM get_accessible_project_ids(${userId}::uuid, ${teamId}::uuid)`
42
+ );
43
+ return rows.map((r) => r.project_id).filter(Boolean);
44
+ } catch (error) {
45
+ console.error("\u274C Error getting accessible project IDs:", error);
46
+ return [];
47
+ }
48
+ }
49
+ async function getAccessibleCustomerIds(teamId) {
50
+ const teamIds = await getAccessibleTeamIds(teamId);
51
+ const ownCustomers = await db.select({ id: schema.customers.id }).from(schema.customers).where(
52
+ teamIds.length === 1 ? eq(schema.customers.teamId, teamIds[0]) : sql`${schema.customers.teamId} = ANY(${teamIds}::uuid[])`
53
+ );
54
+ const sharedCustomers = await db.select({ customerId: schema.customerSharedTeams.customerId }).from(schema.customerSharedTeams).where(eq(schema.customerSharedTeams.teamId, teamId));
55
+ return [
56
+ .../* @__PURE__ */ new Set([
57
+ ...ownCustomers.map((c) => c.id),
58
+ ...sharedCustomers.map((c) => c.customerId)
59
+ ])
60
+ ];
61
+ }
62
+ async function resolveAiSessionId(prefix, teamIds) {
63
+ if (teamIds.length === 0) return null;
64
+ const rows = await db.select({ id: schema.aiSessions.id }).from(schema.aiSessions).where(
65
+ and(
66
+ teamIds.length === 1 ? eq(schema.aiSessions.teamId, teamIds[0]) : sql`${schema.aiSessions.teamId} = ANY(${teamIds}::uuid[])`,
67
+ sql`${schema.aiSessions.id}::text LIKE ${`${prefix}%`}`
68
+ )
69
+ ).limit(1);
70
+ return rows[0]?.id ?? null;
71
+ }
72
+ var _storage = null;
73
+ function buildClient() {
74
+ const endpoint = process.env.R2_ENDPOINT;
75
+ const accessKeyId = process.env.R2_ACCESS_KEY_ID;
76
+ const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
77
+ if (!endpoint || !accessKeyId || !secretAccessKey) {
78
+ throw new Error(
79
+ "R2 storage is not configured. Set R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY."
80
+ );
81
+ }
82
+ return createStorageClient({
83
+ endpoint,
84
+ accessKeyId,
85
+ secretAccessKey,
86
+ publicDomain: process.env.R2_PUBLIC_DOMAIN || void 0,
87
+ publicBuckets: [
88
+ "vault",
89
+ "avatars",
90
+ "team-logos",
91
+ "blog-images",
92
+ "customer-assets"
93
+ ]
94
+ });
95
+ }
96
+ var storage = new Proxy({}, {
97
+ get(_target, prop) {
98
+ if (!_storage) _storage = buildClient();
99
+ return Reflect.get(_storage, prop, _storage);
100
+ }
101
+ });
8
102
 
103
+ // src/index.ts
9
104
  var args = process.argv.slice(2);
10
- var apiKey = args.find((arg) => arg.startsWith("--api-key="))?.split("=")[1] || process.env.MG_TICKETS_API_KEY;
11
- var supabaseUrl = args.find((arg) => arg.startsWith("--supabase-url="))?.split("=")[1] || process.env.SUPABASE_URL || "https://supabase.refront.nl";
12
- var supabaseKey = args.find((arg) => arg.startsWith("--supabase-key="))?.split("=")[1] || process.env.SUPABASE_SERVICE_ROLE_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3NzIwMzE4MzUsImV4cCI6MTkyOTcxMTgzNX0.M0glA9Ry0v87f-WzLKjLfS2Or4141b8c6Czoj1m5fjE";
105
+ function readArg(name) {
106
+ const prefix = `--${name}=`;
107
+ const hit = args.find((a) => a.startsWith(prefix));
108
+ return hit ? hit.slice(prefix.length) : void 0;
109
+ }
110
+ var apiKey = readArg("api-key") ?? process.env.MG_TICKETS_API_KEY;
111
+ var databaseUrl = readArg("database-url") ?? process.env.DATABASE_PRIMARY_POOLER_URL ?? process.env.DATABASE_URL;
13
112
  if (!apiKey) {
14
- console.error("\u274C API key is required. Use --api-key=your_key or set MG_TICKETS_API_KEY environment variable");
113
+ console.error(
114
+ "\u274C API key is required. Use --api-key=your_key or set MG_TICKETS_API_KEY environment variable"
115
+ );
15
116
  process.exit(1);
16
117
  }
17
- var supabase = createClient(supabaseUrl, supabaseKey);
118
+ if (!databaseUrl) {
119
+ console.error(
120
+ "\u274C Database URL is required. Use --database-url=postgresql://... or set DATABASE_PRIMARY_POOLER_URL (or DATABASE_URL) environment variable."
121
+ );
122
+ process.exit(1);
123
+ }
124
+ process.env.DATABASE_PRIMARY_POOLER_URL = databaseUrl;
125
+ function asToolArgs(input) {
126
+ return input ?? {};
127
+ }
18
128
  function roundToNearest15Minutes(minutes) {
19
129
  if (minutes <= 0) return 0;
20
130
  return Math.round(minutes / 15) * 15;
21
131
  }
22
- async function getAccessibleTeamIds(teamId) {
23
- const { data: accessibleTeams } = await supabase.from("teams").select("id").or(`id.eq.${teamId},parent_team_id.eq.${teamId}`);
24
- return accessibleTeams?.map((t) => t.id) || [teamId];
132
+ function isImageFile(mimeType) {
133
+ return mimeType.startsWith("image/") && ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"].includes(
134
+ mimeType
135
+ );
25
136
  }
26
- async function getAccessibleProjectIds(userId, teamId) {
27
- const { data, error } = await supabase.rpc("get_accessible_project_ids", {
28
- p_user_id: userId,
29
- p_team_id: teamId
30
- });
31
- if (error) {
32
- console.error("\u274C Error getting accessible project IDs:", error);
33
- return [];
137
+ async function downloadImageAsBase64(storageKey) {
138
+ try {
139
+ let signedUrl;
140
+ try {
141
+ const { url } = await storage.createSignedUrl({
142
+ bucket: "vault",
143
+ path: storageKey,
144
+ expiresIn: 3600
145
+ });
146
+ signedUrl = url;
147
+ } catch (err) {
148
+ console.error(`Failed to create signed URL for ${storageKey}:`, err);
149
+ return null;
150
+ }
151
+ const response = await fetch(signedUrl);
152
+ if (!response.ok) {
153
+ console.error(
154
+ `Failed to download file ${storageKey}: ${response.status}`
155
+ );
156
+ return null;
157
+ }
158
+ const arrayBuffer = await response.arrayBuffer();
159
+ return Buffer.from(arrayBuffer).toString("base64");
160
+ } catch (error) {
161
+ console.error(`Error downloading image ${storageKey}:`, error);
162
+ return null;
34
163
  }
35
- return data?.map((row) => row.project_id) || [];
36
164
  }
37
- async function getAccessibleCustomerIds(teamId) {
38
- const teamIds = await getAccessibleTeamIds(teamId);
39
- const { data: ownCustomers } = await supabase.from("customers").select("id").in("team_id", teamIds);
40
- const ownCustomerIds = ownCustomers?.map((c) => c.id) || [];
41
- const { data: sharedCustomers } = await supabase.from("customer_shared_teams").select("customer_id").eq("team_id", teamId);
42
- const sharedCustomerIds = sharedCustomers?.map((c) => c.customer_id) || [];
43
- return [.../* @__PURE__ */ new Set([...ownCustomerIds, ...sharedCustomerIds])];
165
+ function buildTicketAccessPredicate(teamIds, projectIds, customerIds) {
166
+ const branches = [];
167
+ if (teamIds.length > 0) branches.push(inArray(schema.tickets.teamId, teamIds));
168
+ if (projectIds.length > 0)
169
+ branches.push(inArray(schema.tickets.projectId, projectIds));
170
+ if (customerIds.length > 0)
171
+ branches.push(inArray(schema.tickets.customerId, customerIds));
172
+ if (branches.length === 0) return sql`false`;
173
+ if (branches.length === 1) return branches[0];
174
+ return or(...branches);
44
175
  }
45
176
  async function validateApiKey(key) {
46
177
  if (!key.startsWith("mid_") || key.length !== 68) {
@@ -50,17 +181,25 @@ async function validateApiKey(key) {
50
181
  try {
51
182
  const keyHash = createHash("sha256").update(key).digest("hex");
52
183
  console.error(`\u{1F50D} Validating API key hash: ${keyHash.substring(0, 16)}...`);
53
- const { data: apiKeyData, error } = await supabase.from("api_keys").select("id, user_id, team_id, scopes, last_used_at").eq("key_hash", keyHash).single();
54
- if (error || !apiKeyData) {
55
- console.error("\u274C API key not found or invalid:", error?.message);
184
+ const [apiKeyData] = await db.select({
185
+ id: schema.apiKeys.id,
186
+ userId: schema.apiKeys.userId,
187
+ teamId: schema.apiKeys.teamId,
188
+ scopes: schema.apiKeys.scopes,
189
+ lastUsedAt: schema.apiKeys.lastUsedAt
190
+ }).from(schema.apiKeys).where(eq(schema.apiKeys.keyHash, keyHash)).limit(1);
191
+ if (!apiKeyData) {
192
+ console.error("\u274C API key not found or invalid");
56
193
  return null;
57
194
  }
58
- await supabase.from("api_keys").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", apiKeyData.id);
59
- console.error(`\u2705 API key validated for user ${apiKeyData.user_id} in team ${apiKeyData.team_id}`);
195
+ await db.update(schema.apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema.apiKeys.id, apiKeyData.id));
196
+ console.error(
197
+ `\u2705 API key validated for user ${apiKeyData.userId} in team ${apiKeyData.teamId}`
198
+ );
60
199
  return {
61
- userId: apiKeyData.user_id,
62
- teamId: apiKeyData.team_id,
63
- scopes: apiKeyData.scopes || []
200
+ userId: apiKeyData.userId,
201
+ teamId: apiKeyData.teamId,
202
+ scopes: apiKeyData.scopes ?? []
64
203
  };
65
204
  } catch (error) {
66
205
  console.error("\u{1F4A5} API key validation error:", error);
@@ -68,54 +207,35 @@ async function validateApiKey(key) {
68
207
  }
69
208
  }
70
209
  var authContext = null;
71
- function isImageFile(mimeType) {
72
- return mimeType.startsWith("image/") && ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"].includes(mimeType);
73
- }
74
- async function downloadImageAsBase64(storageKey) {
75
- try {
76
- const { data: urlData, error: urlError } = await supabase.storage.from("vault").createSignedUrl(storageKey, 3600);
77
- if (urlError || !urlData?.signedUrl) {
78
- console.error(`Failed to create signed URL for ${storageKey}:`, urlError);
79
- return null;
80
- }
81
- const response = await fetch(urlData.signedUrl);
82
- if (!response.ok) {
83
- console.error(`Failed to download file ${storageKey}: ${response.status}`);
84
- return null;
85
- }
86
- const arrayBuffer = await response.arrayBuffer();
87
- const buffer = Buffer.from(arrayBuffer);
88
- return buffer.toString("base64");
89
- } catch (error) {
90
- console.error(`Error downloading image ${storageKey}:`, error);
91
- return null;
92
- }
93
- }
94
210
  async function getGithubTokenForProject(projectId, teamId) {
95
211
  try {
96
- const { data: repoData, error: repoError } = await supabase.from("project_github_repositories").select("repository_full_name").eq("project_id", projectId).eq("team_id", teamId).single();
97
- if (repoError || !repoData) {
212
+ const [repoData] = await db.select({
213
+ repositoryFullName: schema.projectGithubRepositories.repositoryFullName
214
+ }).from(schema.projectGithubRepositories).where(
215
+ and(
216
+ eq(schema.projectGithubRepositories.projectId, projectId),
217
+ eq(schema.projectGithubRepositories.teamId, teamId)
218
+ )
219
+ ).limit(1);
220
+ if (!repoData) {
98
221
  console.error(`No GitHub repository linked to project ${projectId}`);
99
222
  return null;
100
223
  }
101
- const { data: appData, error: appError } = await supabase.from("apps").select("config").eq("team_id", teamId).eq("app_id", "github").single();
102
- if (appError || !appData?.config?.access_token) {
224
+ const [appData] = await db.select({ config: schema.apps.config }).from(schema.apps).where(
225
+ and(eq(schema.apps.teamId, teamId), eq(schema.apps.appId, "github"))
226
+ ).limit(1);
227
+ const accessToken = appData?.config?.access_token;
228
+ if (!appData || !accessToken) {
103
229
  console.error(`GitHub app not connected for team ${teamId}`);
104
230
  return null;
105
231
  }
106
- const accessToken = appData.config.access_token;
107
- const repositoryFullName = repoData.repository_full_name;
232
+ const repositoryFullName = repoData.repositoryFullName;
108
233
  const [owner, repo] = repositoryFullName.split("/");
109
234
  if (!owner || !repo) {
110
235
  console.error(`Invalid repository full name: ${repositoryFullName}`);
111
236
  return null;
112
237
  }
113
- return {
114
- token: accessToken,
115
- repositoryFullName,
116
- owner,
117
- repo
118
- };
238
+ return { token: accessToken, repositoryFullName, owner, repo };
119
239
  } catch (error) {
120
240
  console.error("Error getting GitHub token for project:", error);
121
241
  return null;
@@ -124,36 +244,40 @@ async function getGithubTokenForProject(projectId, teamId) {
124
244
  async function transitionToNextPhase(sessionId, currentPhase) {
125
245
  try {
126
246
  const now = /* @__PURE__ */ new Date();
127
- const phaseOrder = ["analysis", "bug_investigation", "development", "communication"];
128
- const { data: allPhases, error: fetchError } = await supabase.from("ai_time_logs").select("*").eq("ai_session_id", sessionId).order("activity_type");
129
- if (fetchError || !allPhases) {
130
- console.error("Failed to fetch phases for transition:", fetchError);
131
- return;
132
- }
247
+ const phaseOrder = [
248
+ "analysis",
249
+ "bug_investigation",
250
+ "development",
251
+ "communication"
252
+ ];
253
+ const allPhases = await db.select().from(schema.aiTimeLogs).where(eq(schema.aiTimeLogs.aiSessionId, sessionId)).orderBy(asc(schema.aiTimeLogs.activityType));
133
254
  let currentPhaseType = currentPhase;
134
255
  if (!currentPhaseType) {
135
256
  const activePhase = allPhases.find((p) => p.status === "in_progress");
136
- currentPhaseType = activePhase?.activity_type;
257
+ currentPhaseType = activePhase?.activityType ?? void 0;
137
258
  }
138
259
  if (!currentPhaseType) {
139
- const analysisPhase = allPhases.find((p) => p.activity_type === "analysis");
140
- if (analysisPhase && analysisPhase.status === "pending" && analysisPhase.estimated_duration_seconds > 0) {
141
- await supabase.from("ai_time_logs").update({
142
- status: "in_progress",
143
- started_at: now.toISOString()
144
- }).eq("id", analysisPhase.id);
260
+ const analysisPhase = allPhases.find(
261
+ (p) => p.activityType === "analysis"
262
+ );
263
+ if (analysisPhase && analysisPhase.status === "pending" && (analysisPhase.estimatedDurationSeconds ?? 0) > 0) {
264
+ await db.update(schema.aiTimeLogs).set({ status: "in_progress", startedAt: now.toISOString() }).where(eq(schema.aiTimeLogs.id, analysisPhase.id));
145
265
  console.error("\u2705 Started analysis phase");
146
266
  }
147
267
  return;
148
268
  }
149
- const currentPhaseRecord = allPhases.find((p) => p.activity_type === currentPhaseType && p.status === "in_progress");
269
+ const currentPhaseRecord = allPhases.find(
270
+ (p) => p.activityType === currentPhaseType && p.status === "in_progress"
271
+ );
150
272
  if (currentPhaseRecord) {
151
- const duration = Math.round((now.getTime() - new Date(currentPhaseRecord.started_at).getTime()) / 1e3);
152
- await supabase.from("ai_time_logs").update({
273
+ const duration = Math.round(
274
+ (now.getTime() - new Date(currentPhaseRecord.startedAt).getTime()) / 1e3
275
+ );
276
+ await db.update(schema.aiTimeLogs).set({
153
277
  status: "completed",
154
- ended_at: now.toISOString(),
155
- duration_seconds: duration
156
- }).eq("id", currentPhaseRecord.id);
278
+ endedAt: now.toISOString(),
279
+ durationSeconds: duration
280
+ }).where(eq(schema.aiTimeLogs.id, currentPhaseRecord.id));
157
281
  console.error(`\u2705 Completed phase: ${currentPhaseType} (${duration}s)`);
158
282
  }
159
283
  const currentIndex = phaseOrder.indexOf(currentPhaseType);
@@ -163,18 +287,19 @@ async function transitionToNextPhase(sessionId, currentPhase) {
163
287
  }
164
288
  for (let i = currentIndex + 1; i < phaseOrder.length; i++) {
165
289
  const nextPhaseType = phaseOrder[i];
166
- const nextPhase = allPhases.find((p) => p.activity_type === nextPhaseType);
290
+ const nextPhase = allPhases.find(
291
+ (p) => p.activityType === nextPhaseType
292
+ );
167
293
  if (!nextPhase) continue;
168
- if (nextPhase.estimated_duration_seconds === 0) {
169
- await supabase.from("ai_time_logs").update({ status: "skipped" }).eq("id", nextPhase.id);
170
- console.error(`\u23ED\uFE0F Skipped phase: ${nextPhaseType} (0 minutes estimated)`);
294
+ if ((nextPhase.estimatedDurationSeconds ?? 0) === 0) {
295
+ await db.update(schema.aiTimeLogs).set({ status: "skipped" }).where(eq(schema.aiTimeLogs.id, nextPhase.id));
296
+ console.error(
297
+ `\u23ED\uFE0F Skipped phase: ${nextPhaseType} (0 minutes estimated)`
298
+ );
171
299
  continue;
172
300
  }
173
301
  if (nextPhase.status === "pending") {
174
- await supabase.from("ai_time_logs").update({
175
- status: "in_progress",
176
- started_at: now.toISOString()
177
- }).eq("id", nextPhase.id);
302
+ await db.update(schema.aiTimeLogs).set({ status: "in_progress", startedAt: now.toISOString() }).where(eq(schema.aiTimeLogs.id, nextPhase.id));
178
303
  console.error(`\u2705 Started next phase: ${nextPhaseType}`);
179
304
  return;
180
305
  }
@@ -187,7 +312,7 @@ async function transitionToNextPhase(sessionId, currentPhase) {
187
312
  var server = new Server(
188
313
  {
189
314
  name: "mg-tickets-mcp-bridge",
190
- version: "2.0.0"
315
+ version: "3.0.0"
191
316
  },
192
317
  {
193
318
  capabilities: {
@@ -203,11 +328,27 @@ var TOOLS = [
203
328
  inputSchema: {
204
329
  type: "object",
205
330
  properties: {
206
- status: { type: "string", enum: ["open", "in_progress", "review", "resolved", "closed", "backlog"] },
207
- priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
331
+ status: {
332
+ type: "string",
333
+ enum: [
334
+ "open",
335
+ "in_progress",
336
+ "review",
337
+ "resolved",
338
+ "closed",
339
+ "backlog"
340
+ ]
341
+ },
342
+ priority: {
343
+ type: "string",
344
+ enum: ["low", "medium", "high", "critical"]
345
+ },
208
346
  projectId: { type: "string" },
209
347
  customerId: { type: "string" },
210
- q: { type: "string", description: "Search query for ticket number, title, or description" },
348
+ q: {
349
+ type: "string",
350
+ description: "Search query for ticket number, title, or description"
351
+ },
211
352
  pageSize: { type: "number", default: 20, maximum: 100 }
212
353
  },
213
354
  required: []
@@ -232,9 +373,35 @@ var TOOLS = [
232
373
  properties: {
233
374
  title: { type: "string", description: "Ticket title" },
234
375
  description: { type: "string" },
235
- status: { type: "string", enum: ["open", "in_progress", "review", "resolved", "closed", "backlog"], default: "open" },
236
- priority: { type: "string", enum: ["low", "medium", "high", "critical"], default: "medium" },
237
- type: { type: "string", enum: ["task", "bug", "feature", "support", "question", "improvement"], default: "task" },
376
+ status: {
377
+ type: "string",
378
+ enum: [
379
+ "open",
380
+ "in_progress",
381
+ "review",
382
+ "resolved",
383
+ "closed",
384
+ "backlog"
385
+ ],
386
+ default: "open"
387
+ },
388
+ priority: {
389
+ type: "string",
390
+ enum: ["low", "medium", "high", "critical"],
391
+ default: "medium"
392
+ },
393
+ type: {
394
+ type: "string",
395
+ enum: [
396
+ "task",
397
+ "bug",
398
+ "feature",
399
+ "support",
400
+ "question",
401
+ "improvement"
402
+ ],
403
+ default: "task"
404
+ },
238
405
  projectId: { type: "string" },
239
406
  customerId: { type: "string" }
240
407
  },
@@ -247,7 +414,10 @@ var TOOLS = [
247
414
  inputSchema: {
248
415
  type: "object",
249
416
  properties: {
250
- q: { type: "string", description: "Search query for customer name or email" },
417
+ q: {
418
+ type: "string",
419
+ description: "Search query for customer name or email"
420
+ },
251
421
  pageSize: { type: "number", default: 20, maximum: 100 }
252
422
  },
253
423
  required: []
@@ -288,12 +458,15 @@ var TOOLS = [
288
458
  name: { type: "string", description: "Project name" },
289
459
  description: { type: "string" },
290
460
  customerId: { type: "string" },
291
- status: { type: "string", enum: ["active", "on_hold", "completed", "cancelled"], default: "active" }
461
+ status: {
462
+ type: "string",
463
+ enum: ["active", "on_hold", "completed", "cancelled"],
464
+ default: "active"
465
+ }
292
466
  },
293
467
  required: ["name"]
294
468
  }
295
469
  },
296
- // === NEW AI SESSION TOOLS ===
297
470
  {
298
471
  name: "start-ai-session-smart",
299
472
  description: "Start a new AI development session with automatic tracking",
@@ -302,7 +475,10 @@ var TOOLS = [
302
475
  properties: {
303
476
  ticketId: { type: "string" },
304
477
  ticketUrl: { type: "string", description: "URL to the ticket" },
305
- cursorSessionId: { type: "string", description: "Cursor session identifier" },
478
+ cursorSessionId: {
479
+ type: "string",
480
+ description: "Cursor session identifier"
481
+ },
306
482
  totalEstimatedMinutes: {
307
483
  type: "number",
308
484
  description: "Total estimated time in minutes (senior dev WITHOUT AI, rounded to 15 min)"
@@ -329,7 +505,12 @@ var TOOLS = [
329
505
  developerFollowUp: { type: "string" },
330
506
  followUpReason: {
331
507
  type: "string",
332
- enum: ["incomplete_result", "wrong_approach", "needs_clarification", "error_in_code"]
508
+ enum: [
509
+ "incomplete_result",
510
+ "wrong_approach",
511
+ "needs_clarification",
512
+ "error_in_code"
513
+ ]
333
514
  },
334
515
  outcome: {
335
516
  type: "string",
@@ -345,7 +526,15 @@ var TOOLS = [
345
526
  description: "Detailed work description generated by AI (2-3 sentences, summarizing all work done in session including follow-ups)"
346
527
  }
347
528
  },
348
- required: ["aiSessionId", "originalPrompt", "aiResponse", "developerFollowUp", "followUpReason", "estimatedMinutes", "workDescription"]
529
+ required: [
530
+ "aiSessionId",
531
+ "originalPrompt",
532
+ "aiResponse",
533
+ "developerFollowUp",
534
+ "followUpReason",
535
+ "estimatedMinutes",
536
+ "workDescription"
537
+ ]
349
538
  }
350
539
  },
351
540
  {
@@ -374,9 +563,15 @@ var TOOLS = [
374
563
  items: {
375
564
  type: "object",
376
565
  properties: {
377
- todoId: { type: "string", description: "Optional external todo ID for tracking" },
566
+ todoId: {
567
+ type: "string",
568
+ description: "Optional external todo ID for tracking"
569
+ },
378
570
  content: { type: "string" },
379
- status: { type: "string", enum: ["pending", "in_progress", "completed", "cancelled"] },
571
+ status: {
572
+ type: "string",
573
+ enum: ["pending", "in_progress", "completed", "cancelled"]
574
+ },
380
575
  estimatedMinutes: { type: "number" }
381
576
  },
382
577
  required: ["content", "status"]
@@ -404,7 +599,11 @@ var TOOLS = [
404
599
  type: "object",
405
600
  properties: {
406
601
  content: { type: "string" },
407
- status: { type: "string", enum: ["pending", "in_progress"], default: "pending" },
602
+ status: {
603
+ type: "string",
604
+ enum: ["pending", "in_progress"],
605
+ default: "pending"
606
+ },
408
607
  estimatedMinutes: { type: "number" },
409
608
  addedInFollowUp: { type: "boolean", default: true }
410
609
  },
@@ -457,7 +656,10 @@ var TOOLS = [
457
656
  type: "object",
458
657
  properties: {
459
658
  aiSessionId: { type: "string" },
460
- customerResponse: { type: "string", description: "Customer response generated by Cursor AI" },
659
+ customerResponse: {
660
+ type: "string",
661
+ description: "Customer response generated by Cursor AI"
662
+ },
461
663
  responseType: {
462
664
  type: "string",
463
665
  enum: ["completion", "progress_update", "needs_clarification"],
@@ -526,17 +728,13 @@ var TOOLS = [
526
728
  required: ["workDescription", "estimatedHours"]
527
729
  }
528
730
  },
529
- // === GITHUB TOOLS ===
530
731
  {
531
732
  name: "get-github-file",
532
733
  description: "Get the contents of a specific file from a GitHub repository. Use this after finding relevant files to read their full content.",
533
734
  inputSchema: {
534
735
  type: "object",
535
736
  properties: {
536
- projectId: {
537
- type: "string",
538
- description: "Project ID (UUID)"
539
- },
737
+ projectId: { type: "string", description: "Project ID (UUID)" },
540
738
  filePath: {
541
739
  type: "string",
542
740
  description: 'Full path to the file in the repository (e.g., "src/components/Button.tsx")'
@@ -555,10 +753,7 @@ var TOOLS = [
555
753
  inputSchema: {
556
754
  type: "object",
557
755
  properties: {
558
- projectId: {
559
- type: "string",
560
- description: "Project ID (UUID)"
561
- },
756
+ projectId: { type: "string", description: "Project ID (UUID)" },
562
757
  directoryPath: {
563
758
  type: "string",
564
759
  description: 'Path to directory (e.g., "src/components"). Use empty string or "/" for root directory.'
@@ -592,276 +787,387 @@ var RESOURCES = [
592
787
  mimeType: "application/json"
593
788
  }
594
789
  ];
595
- server.setRequestHandler(ListToolsRequestSchema, async () => {
596
- return { tools: TOOLS };
597
- });
598
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
599
- return { resources: RESOURCES };
600
- });
790
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
791
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
792
+ resources: RESOURCES
793
+ }));
601
794
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
602
795
  if (!authContext) {
603
796
  return {
604
- content: [{ type: "text", text: "Error: Not authenticated. API key validation failed." }]
797
+ content: [
798
+ {
799
+ type: "text",
800
+ text: "Error: Not authenticated. API key validation failed."
801
+ }
802
+ ]
605
803
  };
606
804
  }
607
805
  const { name, arguments: args2 } = request.params;
608
806
  console.error(`\u{1F6E0}\uFE0F Executing tool: ${name} for team ${authContext.teamId}`);
609
807
  try {
610
808
  switch (name) {
611
- case "get-tickets": {
612
- const { status, priority, projectId, customerId, q, pageSize = 20 } = args2;
613
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
614
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
615
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
616
- let teamFilter = `team_id.in.(${teamIds.join(",")})`;
617
- let projectFilter = projectIds.length > 0 ? `project_id.in.(${projectIds.join(",")})` : null;
618
- let customerFilter = customerIds.length > 0 ? `customer_id.in.(${customerIds.join(",")})` : null;
619
- const filters = [teamFilter, projectFilter, customerFilter].filter(Boolean);
620
- let query = supabase.from("tickets").select(`
621
- id,
622
- ticket_number,
623
- title,
624
- description,
625
- status,
626
- priority,
627
- type,
628
- created_at,
629
- project_id,
630
- customer_id,
631
- projects:project_id(id, name),
632
- customers:customer_id(id, name)
633
- `).or(filters.join(",")).limit(Math.min(pageSize, 100));
634
- if (status) query = query.eq("status", status);
635
- if (priority) query = query.eq("priority", priority);
636
- if (projectId) query = query.eq("project_id", projectId);
637
- if (customerId) query = query.eq("customer_id", customerId);
638
- if (q) query = query.or(`ticket_number.ilike.%${q}%,title.ilike.%${q}%,description.ilike.%${q}%`);
639
- const { data, error } = await query.order("created_at", { ascending: false });
640
- if (error) throw error;
641
- return {
642
- content: [{
643
- type: "text",
644
- text: `Found ${data?.length || 0} tickets:
809
+ case "get-tickets":
810
+ return await handleGetTickets(asToolArgs(args2));
811
+ case "get-ticket-by-id":
812
+ return await handleGetTicketById(asToolArgs(args2));
813
+ case "create-ticket":
814
+ return await handleCreateTicket(asToolArgs(args2));
815
+ case "get-customers":
816
+ return await handleGetCustomers(asToolArgs(args2));
817
+ case "create-customer":
818
+ return await handleCreateCustomer(asToolArgs(args2));
819
+ case "get-projects":
820
+ return await handleGetProjects(asToolArgs(args2));
821
+ case "create-project":
822
+ return await handleCreateProject(asToolArgs(args2));
823
+ case "start-ai-session-smart":
824
+ return await handleStartAiSession(
825
+ asToolArgs(args2)
826
+ );
827
+ case "track-manual-follow-up":
828
+ return await handleTrackManualFollowUp(
829
+ asToolArgs(args2)
830
+ );
831
+ case "get-session-context":
832
+ return await handleGetSessionContext(
833
+ asToolArgs(args2)
834
+ );
835
+ case "sync-session-todos":
836
+ return await handleSyncSessionTodos(
837
+ asToolArgs(args2)
838
+ );
839
+ case "add-follow-up-todos":
840
+ return await handleAddFollowUpTodos(
841
+ asToolArgs(args2)
842
+ );
843
+ case "update-session-status":
844
+ return await handleUpdateSessionStatus(
845
+ asToolArgs(args2)
846
+ );
847
+ case "get-completion-context":
848
+ return await handleGetCompletionContext(
849
+ asToolArgs(args2)
850
+ );
851
+ case "save-customer-response":
852
+ return await handleSaveCustomerResponse(
853
+ asToolArgs(args2)
854
+ );
855
+ case "complete-ai-session":
856
+ return await handleCompleteAiSession(
857
+ asToolArgs(args2)
858
+ );
859
+ case "log-hours":
860
+ return await handleLogHours(asToolArgs(args2));
861
+ case "get-github-file":
862
+ return await handleGetGithubFile(asToolArgs(args2));
863
+ case "list-github-directory":
864
+ return await handleListGithubDirectory(
865
+ asToolArgs(args2)
866
+ );
867
+ default:
868
+ throw new Error(`Unknown tool: ${name}`);
869
+ }
870
+ } catch (error) {
871
+ console.error("\u274C Tool execution error:", error);
872
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error);
873
+ return {
874
+ content: [{ type: "text", text: `Error executing ${name}: ${message}` }]
875
+ };
876
+ }
877
+ });
878
+ async function handleGetTickets(input) {
879
+ const ctx = authContext;
880
+ const { status, priority, projectId, customerId, q, pageSize = 20 } = input;
881
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
882
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
883
+ const customerIds = await getAccessibleCustomerIds(ctx.teamId);
884
+ const accessPredicate = buildTicketAccessPredicate(
885
+ teamIds,
886
+ projectIds,
887
+ customerIds
888
+ );
889
+ const filters = [accessPredicate, eq(schema.tickets.isDeleted, false)];
890
+ if (status) filters.push(eq(schema.tickets.status, status));
891
+ if (priority) filters.push(eq(schema.tickets.priority, priority));
892
+ if (projectId) filters.push(eq(schema.tickets.projectId, projectId));
893
+ if (customerId) filters.push(eq(schema.tickets.customerId, customerId));
894
+ if (q) {
895
+ const pattern = `%${q}%`;
896
+ filters.push(
897
+ or(
898
+ ilike(schema.tickets.ticketNumber, pattern),
899
+ ilike(schema.tickets.title, pattern),
900
+ ilike(schema.tickets.description, pattern)
901
+ )
902
+ );
903
+ }
904
+ const rows = await db.select({
905
+ id: schema.tickets.id,
906
+ ticketNumber: schema.tickets.ticketNumber,
907
+ title: schema.tickets.title,
908
+ description: schema.tickets.description,
909
+ status: schema.tickets.status,
910
+ priority: schema.tickets.priority,
911
+ type: schema.tickets.type,
912
+ createdAt: schema.tickets.createdAt,
913
+ projectId: schema.tickets.projectId,
914
+ customerId: schema.tickets.customerId,
915
+ projectName: schema.projects.name,
916
+ customerName: schema.customers.name
917
+ }).from(schema.tickets).leftJoin(schema.projects, eq(schema.projects.id, schema.tickets.projectId)).leftJoin(
918
+ schema.customers,
919
+ eq(schema.customers.id, schema.tickets.customerId)
920
+ ).where(and(...filters)).orderBy(desc(schema.tickets.createdAt)).limit(Math.min(pageSize, 100));
921
+ return {
922
+ content: [
923
+ {
924
+ type: "text",
925
+ text: `Found ${rows.length} tickets:
645
926
 
646
- ${data?.map(
647
- (ticket) => `**${ticket.ticket_number}**: ${ticket.title}
648
- Status: ${ticket.status} | Priority: ${ticket.priority}
649
- ${ticket.projects?.name ? `Project: ${ticket.projects.name}
650
- ` : ""}${ticket.customers?.name ? `Customer: ${ticket.customers.name}
651
- ` : ""}Created: ${new Date(ticket.created_at).toLocaleDateString()}
927
+ ${rows.map(
928
+ (t) => `**${t.ticketNumber}**: ${t.title}
929
+ Status: ${t.status} | Priority: ${t.priority}
930
+ ${t.projectName ? `Project: ${t.projectName}
931
+ ` : ""}${t.customerName ? `Customer: ${t.customerName}
932
+ ` : ""}Created: ${new Date(t.createdAt).toLocaleDateString()}
652
933
  `
653
- ).join("\n") || "No tickets found."}`
654
- }]
655
- };
934
+ ).join("\n") || "No tickets found."}`
656
935
  }
657
- case "get-ticket-by-id": {
658
- const { id } = args2;
659
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
660
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
661
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
662
- let accessFilter = `team_id.in.(${teamIds.join(",")})`;
663
- if (projectIds.length > 0) accessFilter += `,project_id.in.(${projectIds.join(",")})`;
664
- if (customerIds.length > 0) accessFilter += `,customer_id.in.(${customerIds.join(",")})`;
665
- const { data: ticketData, error } = await supabase.from("tickets").select(`
666
- *,
667
- projects:project_id(id, name),
668
- customers:customer_id(id, name),
669
- assignee:assignee_id(id, full_name, email),
670
- requester:requester_id(id, full_name, email)
671
- `).eq("id", id).single();
672
- if (error) {
673
- if (error.code === "PGRST116") {
674
- throw new Error(`Ticket not found: ${id}`);
675
- }
676
- throw new Error(error.message || JSON.stringify(error));
677
- }
678
- let hasAccess = false;
679
- if (teamIds.includes(ticketData.team_id)) {
680
- hasAccess = true;
681
- }
682
- if (!hasAccess && ticketData.project_id && projectIds.includes(ticketData.project_id)) {
683
- hasAccess = true;
684
- }
685
- if (!hasAccess && ticketData.customer_id && customerIds.includes(ticketData.customer_id)) {
686
- hasAccess = true;
687
- }
688
- if (!hasAccess) {
689
- throw new Error("Access denied: You do not have permission to view this ticket");
690
- }
691
- const data = ticketData;
692
- if (error) {
693
- if (error.code === "PGRST116") {
694
- throw new Error(`Ticket not found or no access: ${id}`);
695
- }
696
- throw new Error(error.message || JSON.stringify(error));
697
- }
698
- const { data: attachments, error: attachmentsError } = await supabase.from("ticket_attachments").select(`
699
- id,
700
- file_name,
701
- file_size,
702
- mime_type,
703
- storage_key,
704
- created_at,
705
- users:user_id(id, full_name)
706
- `).eq("ticket_id", id).order("created_at", { ascending: true });
707
- if (attachmentsError) {
708
- console.error("Error fetching attachments:", attachmentsError);
709
- }
710
- const { data: comments, error: commentsError } = await supabase.from("ticket_comments").select(`
711
- id,
712
- content,
713
- created_at,
714
- users:user_id(id, full_name)
715
- `).eq("ticket_id", id).order("created_at", { ascending: true });
716
- if (commentsError) {
717
- console.error("Error fetching comments:", commentsError);
718
- }
719
- const commentIds = comments?.map((c) => c.id) || [];
720
- let commentAttachments = [];
721
- if (commentIds.length > 0) {
722
- const { data: commAttachments, error: commAttachmentsError } = await supabase.from("ticket_comment_attachments").select(`
723
- id,
724
- comment_id,
725
- file_name,
726
- file_size,
727
- mime_type,
728
- storage_key,
729
- created_at
730
- `).in("comment_id", commentIds);
731
- if (commAttachmentsError) {
732
- console.error("Error fetching comment attachments:", commAttachmentsError);
733
- } else {
734
- commentAttachments = commAttachments || [];
735
- }
736
- }
737
- const content = [{
738
- type: "text",
739
- text: `**Ticket Details:**
936
+ ]
937
+ };
938
+ }
939
+ async function handleGetTicketById(input) {
940
+ const ctx = authContext;
941
+ const { id } = input;
942
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
943
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
944
+ const customerIds = await getAccessibleCustomerIds(ctx.teamId);
945
+ const ticketRow = await db.query.tickets.findFirst({
946
+ where: eq(schema.tickets.id, id),
947
+ with: {
948
+ project: { columns: { id: true, name: true } },
949
+ customer: { columns: { id: true, name: true } },
950
+ assignee: { columns: { id: true, fullName: true, email: true } },
951
+ requester: { columns: { id: true, fullName: true, email: true } }
952
+ }
953
+ });
954
+ if (!ticketRow) {
955
+ throw new Error(`Ticket not found: ${id}`);
956
+ }
957
+ let hasAccess = false;
958
+ if (teamIds.includes(ticketRow.teamId)) hasAccess = true;
959
+ if (!hasAccess && ticketRow.projectId && projectIds.includes(ticketRow.projectId))
960
+ hasAccess = true;
961
+ if (!hasAccess && ticketRow.customerId && customerIds.includes(ticketRow.customerId))
962
+ hasAccess = true;
963
+ if (!hasAccess) {
964
+ throw new Error(
965
+ "Access denied: You do not have permission to view this ticket"
966
+ );
967
+ }
968
+ const attachments = await db.select({
969
+ id: schema.ticketAttachments.id,
970
+ fileName: schema.ticketAttachments.fileName,
971
+ fileSize: schema.ticketAttachments.fileSize,
972
+ mimeType: schema.ticketAttachments.mimeType,
973
+ storageKey: schema.ticketAttachments.storageKey,
974
+ createdAt: schema.ticketAttachments.createdAt,
975
+ uploaderId: schema.ticketAttachments.userId,
976
+ uploaderName: schema.users.fullName
977
+ }).from(schema.ticketAttachments).leftJoin(
978
+ schema.users,
979
+ eq(schema.users.id, schema.ticketAttachments.userId)
980
+ ).where(eq(schema.ticketAttachments.ticketId, id)).orderBy(asc(schema.ticketAttachments.createdAt));
981
+ const comments = await db.select({
982
+ id: schema.ticketComments.id,
983
+ content: schema.ticketComments.content,
984
+ createdAt: schema.ticketComments.createdAt,
985
+ userId: schema.ticketComments.userId
986
+ }).from(schema.ticketComments).where(eq(schema.ticketComments.ticketId, id)).orderBy(asc(schema.ticketComments.createdAt));
987
+ const commentUserIds = [
988
+ ...new Set(
989
+ comments.map((c) => c.userId).filter((v) => Boolean(v))
990
+ )
991
+ ];
992
+ const commentUserMap = /* @__PURE__ */ new Map();
993
+ if (commentUserIds.length > 0) {
994
+ const commentUsers = await db.select({ id: schema.users.id, fullName: schema.users.fullName }).from(schema.users).where(inArray(schema.users.id, commentUserIds));
995
+ commentUsers.forEach((u) => commentUserMap.set(u.id, u));
996
+ }
997
+ const commentIds = comments.map((c) => c.id);
998
+ const commentAttachments = commentIds.length > 0 ? await db.select({
999
+ id: schema.ticketCommentAttachments.id,
1000
+ commentId: schema.ticketCommentAttachments.commentId,
1001
+ fileName: schema.ticketCommentAttachments.fileName,
1002
+ fileSize: schema.ticketCommentAttachments.fileSize,
1003
+ mimeType: schema.ticketCommentAttachments.mimeType,
1004
+ storageKey: schema.ticketCommentAttachments.storageKey,
1005
+ createdAt: schema.ticketCommentAttachments.createdAt
1006
+ }).from(schema.ticketCommentAttachments).where(
1007
+ inArray(schema.ticketCommentAttachments.commentId, commentIds)
1008
+ ) : [];
1009
+ const content = [
1010
+ {
1011
+ type: "text",
1012
+ text: `**Ticket Details:**
740
1013
 
741
- **${data.ticket_number}**: ${data.title}
742
- Status: ${data.status}
743
- Priority: ${data.priority}
744
- Type: ${data.type}
745
- ${data.description ? `Description: ${data.description}
746
- ` : ""}${data.projects?.name ? `Project: ${data.projects.name}
747
- ` : ""}${data.customers?.name ? `Customer: ${data.customers.name}
748
- ` : ""}${data.assignee?.full_name ? `Assignee: ${data.assignee.full_name}
749
- ` : ""}Requester: ${data.requester?.full_name || "Unknown"}
750
- Created: ${new Date(data.created_at).toLocaleDateString()}
751
- ${attachments && attachments.length > 0 ? `
1014
+ **${ticketRow.ticketNumber}**: ${ticketRow.title}
1015
+ Status: ${ticketRow.status}
1016
+ Priority: ${ticketRow.priority}
1017
+ Type: ${ticketRow.type}
1018
+ ${ticketRow.description ? `Description: ${ticketRow.description}
1019
+ ` : ""}${ticketRow.project?.name ? `Project: ${ticketRow.project.name}
1020
+ ` : ""}${ticketRow.customer?.name ? `Customer: ${ticketRow.customer.name}
1021
+ ` : ""}${ticketRow.assignee?.fullName ? `Assignee: ${ticketRow.assignee.fullName}
1022
+ ` : ""}Requester: ${ticketRow.requester?.fullName || "Unknown"}
1023
+ Created: ${new Date(ticketRow.createdAt).toLocaleDateString()}
1024
+ ${attachments.length > 0 ? `
752
1025
  \u{1F4CE} Attachments: ${attachments.length}
753
- ` : ""}${comments && comments.length > 0 ? `\u{1F4AC} Comments: ${comments.length}
1026
+ ` : ""}${comments.length > 0 ? `\u{1F4AC} Comments: ${comments.length}
754
1027
  ` : ""}`
755
- }];
756
- if (attachments && attachments.length > 0) {
757
- console.error(`\u{1F4CE} Processing ${attachments.length} ticket attachments...`);
758
- for (const attachment of attachments) {
759
- if (isImageFile(attachment.mime_type)) {
760
- console.error(`\u{1F5BC}\uFE0F Downloading image: ${attachment.file_name}`);
761
- const base64Data = await downloadImageAsBase64(attachment.storage_key);
762
- if (base64Data) {
763
- content.push({
764
- type: "image",
765
- data: base64Data,
766
- mimeType: attachment.mime_type
767
- });
768
- content.push({
769
- type: "text",
770
- text: `
771
- \u{1F4F8} **Image from ticket**: ${attachment.file_name} (${Math.round(attachment.file_size / 1024)}KB, uploaded by ${attachment.users?.full_name || "Unknown"} on ${new Date(attachment.created_at).toLocaleDateString()})
1028
+ }
1029
+ ];
1030
+ if (attachments.length > 0) {
1031
+ console.error(`\u{1F4CE} Processing ${attachments.length} ticket attachments...`);
1032
+ for (const attachment of attachments) {
1033
+ if (isImageFile(attachment.mimeType)) {
1034
+ console.error(`\u{1F5BC}\uFE0F Downloading image: ${attachment.fileName}`);
1035
+ const base64 = await downloadImageAsBase64(attachment.storageKey);
1036
+ if (base64) {
1037
+ content.push({
1038
+ type: "image",
1039
+ data: base64,
1040
+ mimeType: attachment.mimeType
1041
+ });
1042
+ content.push({
1043
+ type: "text",
1044
+ text: `
1045
+ \u{1F4F8} **Image from ticket**: ${attachment.fileName} (${Math.round(
1046
+ attachment.fileSize / 1024
1047
+ )}KB, uploaded by ${attachment.uploaderName || "Unknown"} on ${new Date(
1048
+ attachment.createdAt
1049
+ ).toLocaleDateString()})
772
1050
  `
773
- });
774
- }
775
- }
776
- }
1051
+ });
777
1052
  }
778
- if (commentAttachments.length > 0) {
779
- console.error(`\u{1F4CE} Processing ${commentAttachments.length} comment attachments...`);
780
- for (const attachment of commentAttachments) {
781
- if (isImageFile(attachment.mime_type)) {
782
- console.error(`\u{1F5BC}\uFE0F Downloading comment image: ${attachment.file_name}`);
783
- const base64Data = await downloadImageAsBase64(attachment.storage_key);
784
- if (base64Data) {
785
- const comment = comments?.find((c) => c.id === attachment.comment_id);
786
- content.push({
787
- type: "image",
788
- data: base64Data,
789
- mimeType: attachment.mime_type
790
- });
791
- content.push({
792
- type: "text",
793
- text: `
794
- \u{1F4F8} **Image from comment** by ${comment?.users?.full_name || "Unknown"} on ${new Date(attachment.created_at).toLocaleDateString()}: ${attachment.file_name} (${Math.round(attachment.file_size / 1024)}KB)
1053
+ }
1054
+ }
1055
+ }
1056
+ if (commentAttachments.length > 0) {
1057
+ console.error(
1058
+ `\u{1F4CE} Processing ${commentAttachments.length} comment attachments...`
1059
+ );
1060
+ for (const attachment of commentAttachments) {
1061
+ if (isImageFile(attachment.mimeType)) {
1062
+ console.error(
1063
+ `\u{1F5BC}\uFE0F Downloading comment image: ${attachment.fileName}`
1064
+ );
1065
+ const base64 = await downloadImageAsBase64(attachment.storageKey);
1066
+ if (base64) {
1067
+ const comment = comments.find((c) => c.id === attachment.commentId);
1068
+ const author = comment?.userId ? commentUserMap.get(comment.userId)?.fullName : null;
1069
+ content.push({
1070
+ type: "image",
1071
+ data: base64,
1072
+ mimeType: attachment.mimeType
1073
+ });
1074
+ content.push({
1075
+ type: "text",
1076
+ text: `
1077
+ \u{1F4F8} **Image from comment** by ${author || "Unknown"} on ${new Date(
1078
+ attachment.createdAt
1079
+ ).toLocaleDateString()}: ${attachment.fileName} (${Math.round(
1080
+ attachment.fileSize / 1024
1081
+ )}KB)
795
1082
  ` + (comment?.content ? `Comment text: "${comment.content.substring(0, 100)}${comment.content.length > 100 ? "..." : ""}"
796
1083
  ` : "")
797
- });
798
- }
799
- }
800
- }
1084
+ });
801
1085
  }
802
- console.error(`\u2705 Returning ticket with ${content.filter((c) => c.type === "image").length} images`);
803
- return { content };
804
1086
  }
805
- case "create-ticket": {
806
- const { title, description, status = "open", priority = "medium", type = "task", projectId, customerId } = args2;
807
- const year = (/* @__PURE__ */ new Date()).getFullYear();
808
- let resolvedTeamId = authContext.teamId;
809
- let resolvedCustomerId = customerId;
810
- let projectAbbreviation = "";
811
- if (projectId) {
812
- const { data: project } = await supabase.from("projects").select("name, team_id, customer_id").eq("id", projectId).single();
813
- if (project) {
814
- if (project.team_id) {
815
- resolvedTeamId = project.team_id;
816
- }
817
- if (!resolvedCustomerId && project.customer_id) {
818
- resolvedCustomerId = project.customer_id;
819
- }
820
- if (project.name) {
821
- const name2 = project.name.toUpperCase().replace(/[^A-Z0-9\s]/g, "");
822
- const words = name2.split(/\s+/).filter(Boolean);
823
- if (words.length >= 2) {
824
- projectAbbreviation = words.slice(0, 2).map((w) => w.substring(0, 3)).join("").substring(0, 5);
825
- } else if (words.length === 1 && words[0]) {
826
- projectAbbreviation = words[0].substring(0, 5);
827
- }
828
- }
829
- }
830
- }
831
- let ticketNumber;
832
- if (projectId && projectAbbreviation) {
833
- const pattern = `${year}-${projectAbbreviation}-%`;
834
- const { data: existingTickets } = await supabase.from("tickets").select("ticket_number").eq("project_id", projectId).like("ticket_number", pattern).order("ticket_number", { ascending: false }).limit(1);
835
- let nextSequence = 1;
836
- if (existingTickets && existingTickets.length > 0 && existingTickets[0]?.ticket_number) {
837
- const parts = existingTickets[0].ticket_number.split("-");
838
- if (parts.length === 3) {
839
- const lastSeq = parseInt(parts[2], 10);
840
- if (!isNaN(lastSeq)) nextSequence = lastSeq + 1;
841
- }
842
- }
843
- ticketNumber = `${year}-${projectAbbreviation}-${String(nextSequence).padStart(3, "0")}`;
844
- } else {
845
- const { count } = await supabase.from("tickets").select("*", { count: "exact", head: true }).eq("team_id", resolvedTeamId);
846
- ticketNumber = `${year}-${String((count || 0) + 1).padStart(3, "0")}`;
1087
+ }
1088
+ }
1089
+ console.error(
1090
+ `\u2705 Returning ticket with ${content.filter((c) => c.type === "image").length} images`
1091
+ );
1092
+ return { content };
1093
+ }
1094
+ async function handleCreateTicket(input) {
1095
+ const ctx = authContext;
1096
+ const {
1097
+ title,
1098
+ description,
1099
+ status = "open",
1100
+ priority = "medium",
1101
+ type = "task",
1102
+ projectId,
1103
+ customerId
1104
+ } = input;
1105
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
1106
+ let resolvedTeamId = ctx.teamId;
1107
+ let resolvedCustomerId = customerId;
1108
+ let projectAbbreviation = "";
1109
+ if (projectId) {
1110
+ const [project] = await db.select({
1111
+ name: schema.projects.name,
1112
+ teamId: schema.projects.teamId,
1113
+ customerId: schema.projects.customerId
1114
+ }).from(schema.projects).where(eq(schema.projects.id, projectId)).limit(1);
1115
+ if (project) {
1116
+ if (project.teamId) resolvedTeamId = project.teamId;
1117
+ if (!resolvedCustomerId && project.customerId) {
1118
+ resolvedCustomerId = project.customerId;
1119
+ }
1120
+ if (project.name) {
1121
+ const upper = project.name.toUpperCase().replace(/[^A-Z0-9\s]/g, "");
1122
+ const words = upper.split(/\s+/).filter(Boolean);
1123
+ if (words.length >= 2) {
1124
+ projectAbbreviation = words.slice(0, 2).map((w) => w.substring(0, 3)).join("").substring(0, 5);
1125
+ } else if (words.length === 1 && words[0]) {
1126
+ projectAbbreviation = words[0].substring(0, 5);
847
1127
  }
848
- const { data, error } = await supabase.from("tickets").insert({
849
- team_id: resolvedTeamId,
850
- ticket_number: ticketNumber,
851
- title,
852
- description,
853
- status,
854
- priority,
855
- type,
856
- project_id: projectId || null,
857
- customer_id: resolvedCustomerId || null,
858
- requester_id: authContext.userId
859
- }).select().single();
860
- if (error) throw error;
861
- return {
862
- content: [{
863
- type: "text",
864
- text: `\u2705 **Ticket Created Successfully!**
1128
+ }
1129
+ }
1130
+ }
1131
+ let ticketNumber;
1132
+ if (projectId && projectAbbreviation) {
1133
+ const pattern = `${year}-${projectAbbreviation}-%`;
1134
+ const [highest] = await db.select({ ticketNumber: schema.tickets.ticketNumber }).from(schema.tickets).where(
1135
+ and(
1136
+ eq(schema.tickets.projectId, projectId),
1137
+ ilike(schema.tickets.ticketNumber, pattern)
1138
+ )
1139
+ ).orderBy(desc(schema.tickets.ticketNumber)).limit(1);
1140
+ let nextSequence = 1;
1141
+ if (highest?.ticketNumber) {
1142
+ const parts = highest.ticketNumber.split("-");
1143
+ if (parts.length === 3 && parts[2]) {
1144
+ const lastSeq = Number.parseInt(parts[2], 10);
1145
+ if (!Number.isNaN(lastSeq)) nextSequence = lastSeq + 1;
1146
+ }
1147
+ }
1148
+ ticketNumber = `${year}-${projectAbbreviation}-${String(nextSequence).padStart(3, "0")}`;
1149
+ } else {
1150
+ const [countRow] = await db.select({ n: sql`count(*)::int` }).from(schema.tickets).where(eq(schema.tickets.teamId, resolvedTeamId));
1151
+ const count = Number(countRow?.n ?? 0);
1152
+ ticketNumber = `${year}-${String(count + 1).padStart(3, "0")}`;
1153
+ }
1154
+ await db.insert(schema.tickets).values({
1155
+ teamId: resolvedTeamId,
1156
+ ticketNumber,
1157
+ title,
1158
+ description: description ?? null,
1159
+ status,
1160
+ priority,
1161
+ type,
1162
+ projectId: projectId ?? null,
1163
+ customerId: resolvedCustomerId ?? null,
1164
+ requesterId: ctx.userId
1165
+ });
1166
+ return {
1167
+ content: [
1168
+ {
1169
+ type: "text",
1170
+ text: `\u2705 **Ticket Created Successfully!**
865
1171
 
866
1172
  Ticket Number: **${ticketNumber}**
867
1173
  Title: ${title}
@@ -869,143 +1175,179 @@ Status: ${status}
869
1175
  Priority: ${priority}
870
1176
  Type: ${type}
871
1177
  `
872
- }]
873
- };
874
1178
  }
875
- case "get-customers": {
876
- const { q, pageSize = 20 } = args2;
877
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
878
- if (customerIds.length === 0) {
879
- return {
880
- content: [{
881
- type: "text",
882
- text: "No customers found or no access to any customers."
883
- }]
884
- };
1179
+ ]
1180
+ };
1181
+ }
1182
+ async function handleGetCustomers(input) {
1183
+ const ctx = authContext;
1184
+ const { q, pageSize = 20 } = input;
1185
+ const customerIds = await getAccessibleCustomerIds(ctx.teamId);
1186
+ if (customerIds.length === 0) {
1187
+ return {
1188
+ content: [
1189
+ {
1190
+ type: "text",
1191
+ text: "No customers found or no access to any customers."
885
1192
  }
886
- let query = supabase.from("customers").select("id, name, email, website, created_at").in("id", customerIds).limit(Math.min(pageSize, 100));
887
- if (q) query = query.or(`name.ilike.%${q}%,email.ilike.%${q}%`);
888
- const { data, error } = await query.order("name");
889
- if (error) throw error;
890
- return {
891
- content: [{
892
- type: "text",
893
- text: `Found ${data?.length || 0} customers:
1193
+ ]
1194
+ };
1195
+ }
1196
+ const filters = [inArray(schema.customers.id, customerIds)];
1197
+ if (q) {
1198
+ const pattern = `%${q}%`;
1199
+ filters.push(
1200
+ or(
1201
+ ilike(schema.customers.name, pattern),
1202
+ ilike(schema.customers.email, pattern)
1203
+ )
1204
+ );
1205
+ }
1206
+ const rows = await db.select({
1207
+ id: schema.customers.id,
1208
+ name: schema.customers.name,
1209
+ email: schema.customers.email,
1210
+ website: schema.customers.website,
1211
+ createdAt: schema.customers.createdAt
1212
+ }).from(schema.customers).where(and(...filters)).orderBy(asc(schema.customers.name)).limit(Math.min(pageSize, 100));
1213
+ return {
1214
+ content: [
1215
+ {
1216
+ type: "text",
1217
+ text: `Found ${rows.length} customers:
894
1218
 
895
- ${data?.map(
896
- (customer) => `**${customer.name}**
897
- ${customer.email ? `Email: ${customer.email}
898
- ` : ""}${customer.website ? `Website: ${customer.website}
899
- ` : ""}Created: ${new Date(customer.created_at).toLocaleDateString()}
1219
+ ${rows.map(
1220
+ (c) => `**${c.name}**
1221
+ ${c.email ? `Email: ${c.email}
1222
+ ` : ""}${c.website ? `Website: ${c.website}
1223
+ ` : ""}Created: ${new Date(c.createdAt).toLocaleDateString()}
900
1224
  `
901
- ).join("\n") || "No customers found."}`
902
- }]
903
- };
1225
+ ).join("\n") || "No customers found."}`
904
1226
  }
905
- case "create-customer": {
906
- const { name: name2, email, website } = args2;
907
- const { data, error } = await supabase.from("customers").insert({
908
- team_id: authContext.teamId,
909
- name: name2,
910
- email: email || null,
911
- website: website || null,
912
- user_id: authContext.userId
913
- }).select().single();
914
- if (error) throw error;
915
- return {
916
- content: [{
917
- type: "text",
918
- text: `\u2705 **Customer Created Successfully!**
1227
+ ]
1228
+ };
1229
+ }
1230
+ async function handleCreateCustomer(input) {
1231
+ const ctx = authContext;
1232
+ const { name, email, website } = input;
1233
+ await db.insert(schema.customers).values({
1234
+ teamId: ctx.teamId,
1235
+ name,
1236
+ email: email ?? "",
1237
+ website: website ?? null
1238
+ });
1239
+ return {
1240
+ content: [
1241
+ {
1242
+ type: "text",
1243
+ text: `\u2705 **Customer Created Successfully!**
919
1244
 
920
- Name: ${name2}
1245
+ Name: ${name}
921
1246
  ${email ? `Email: ${email}
922
1247
  ` : ""}${website ? `Website: ${website}
923
1248
  ` : ""}`
924
- }]
925
- };
926
1249
  }
927
- case "get-projects": {
928
- const { customerId, q, pageSize = 20 } = args2;
929
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
930
- if (projectIds.length === 0) {
931
- return {
932
- content: [{
933
- type: "text",
934
- text: "No projects found or no access to any projects."
935
- }]
936
- };
1250
+ ]
1251
+ };
1252
+ }
1253
+ async function handleGetProjects(input) {
1254
+ const ctx = authContext;
1255
+ const { customerId, q, pageSize = 20 } = input;
1256
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
1257
+ if (projectIds.length === 0) {
1258
+ return {
1259
+ content: [
1260
+ {
1261
+ type: "text",
1262
+ text: "No projects found or no access to any projects."
937
1263
  }
938
- let query = supabase.from("projects").select(`
939
- id,
940
- name,
941
- description,
942
- customer_id,
943
- created_at
944
- `).in("id", projectIds).limit(Math.min(pageSize, 100));
945
- if (customerId) query = query.eq("customer_id", customerId);
946
- if (q) query = query.ilike("name", `%${q}%`);
947
- const { data, error } = await query.order("name");
948
- if (error) throw error;
949
- return {
950
- content: [{
951
- type: "text",
952
- text: `Found ${data?.length || 0} projects:
1264
+ ]
1265
+ };
1266
+ }
1267
+ const filters = [inArray(schema.projects.id, projectIds)];
1268
+ if (customerId) filters.push(eq(schema.projects.customerId, customerId));
1269
+ if (q) filters.push(ilike(schema.projects.name, `%${q}%`));
1270
+ const rows = await db.select({
1271
+ id: schema.projects.id,
1272
+ name: schema.projects.name,
1273
+ description: schema.projects.description,
1274
+ customerId: schema.projects.customerId,
1275
+ createdAt: schema.projects.createdAt
1276
+ }).from(schema.projects).where(and(...filters)).orderBy(asc(schema.projects.name)).limit(Math.min(pageSize, 100));
1277
+ return {
1278
+ content: [
1279
+ {
1280
+ type: "text",
1281
+ text: `Found ${rows.length} projects:
953
1282
 
954
- ${data?.map(
955
- (project) => `**${project.name}** (ID: ${project.id})
956
- ${project.description ? `Description: ${project.description}
957
- ` : ""}Created: ${new Date(project.created_at).toLocaleDateString()}
1283
+ ${rows.map(
1284
+ (p) => `**${p.name}** (ID: ${p.id})
1285
+ ${p.description ? `Description: ${p.description}
1286
+ ` : ""}Created: ${new Date(p.createdAt).toLocaleDateString()}
958
1287
  `
959
- ).join("\n") || "No projects found."}`
960
- }]
961
- };
1288
+ ).join("\n") || "No projects found."}`
962
1289
  }
963
- case "create-project": {
964
- const { name: name2, description, customerId, status = "active" } = args2;
965
- const { data, error } = await supabase.from("projects").insert({
966
- team_id: authContext.teamId,
967
- name: name2,
968
- description: description || null,
969
- customer_id: customerId || null,
970
- status,
971
- user_id: authContext.userId
972
- }).select().single();
973
- if (error) throw error;
974
- return {
975
- content: [{
976
- type: "text",
977
- text: `\u2705 **Project Created Successfully!**
1290
+ ]
1291
+ };
1292
+ }
1293
+ async function handleCreateProject(input) {
1294
+ const ctx = authContext;
1295
+ const { name, description, customerId } = input;
1296
+ await db.insert(schema.projects).values({
1297
+ teamId: ctx.teamId,
1298
+ name,
1299
+ description: description ?? null,
1300
+ customerId: customerId ?? null
1301
+ });
1302
+ return {
1303
+ content: [
1304
+ {
1305
+ type: "text",
1306
+ text: `\u2705 **Project Created Successfully!**
978
1307
 
979
- Name: ${name2}
980
- Status: ${status}
1308
+ Name: ${name}
981
1309
  ${description ? `Description: ${description}
982
1310
  ` : ""}`
983
- }]
984
- };
985
1311
  }
986
- // === AI SESSION TOOLS ===
987
- case "start-ai-session-smart": {
988
- const { ticketId, ticketUrl, cursorSessionId, totalEstimatedMinutes, complexityScore } = args2;
989
- if (!totalEstimatedMinutes) {
990
- throw new Error("totalEstimatedMinutes is required");
991
- }
992
- const roundedMinutes = roundToNearest15Minutes(totalEstimatedMinutes);
993
- const sessionStartTime = /* @__PURE__ */ new Date();
994
- const { data: sessionData, error } = await supabase.from("ai_sessions").insert({
995
- ticket_id: ticketId,
996
- provider_user_id: authContext.userId,
997
- team_id: authContext.teamId,
998
- cursor_session_id: cursorSessionId || null,
999
- ai_time_estimate_minutes: roundedMinutes,
1000
- complexity_score: complexityScore || null,
1001
- status: "in_progress"
1002
- }).select("id, ticket_id, cursor_session_id, created_at").single();
1003
- if (error) throw error;
1004
- const sessionId = `ai-sess-${sessionData.id.substring(0, 8)}`;
1005
- return {
1006
- content: [{
1007
- type: "text",
1008
- text: `\u{1F680} **AI Session Started!**
1312
+ ]
1313
+ };
1314
+ }
1315
+ async function handleStartAiSession(input) {
1316
+ const ctx = authContext;
1317
+ const {
1318
+ ticketId,
1319
+ cursorSessionId,
1320
+ totalEstimatedMinutes,
1321
+ complexityScore
1322
+ } = input;
1323
+ if (!totalEstimatedMinutes) {
1324
+ throw new Error("totalEstimatedMinutes is required");
1325
+ }
1326
+ const roundedMinutes = roundToNearest15Minutes(totalEstimatedMinutes);
1327
+ const sessionStartTime = /* @__PURE__ */ new Date();
1328
+ const [sessionData] = await db.insert(schema.aiSessions).values({
1329
+ ticketId,
1330
+ providerUserId: ctx.userId,
1331
+ teamId: ctx.teamId,
1332
+ cursorSessionId: cursorSessionId ?? null,
1333
+ aiTimeEstimateMinutes: roundedMinutes,
1334
+ complexityScore: complexityScore ?? null,
1335
+ status: "in_progress"
1336
+ }).returning({
1337
+ id: schema.aiSessions.id,
1338
+ ticketId: schema.aiSessions.ticketId,
1339
+ cursorSessionId: schema.aiSessions.cursorSessionId,
1340
+ createdAt: schema.aiSessions.createdAt
1341
+ });
1342
+ if (!sessionData) {
1343
+ throw new Error("Failed to create AI session");
1344
+ }
1345
+ const sessionId = `ai-sess-${sessionData.id.substring(0, 8)}`;
1346
+ return {
1347
+ content: [
1348
+ {
1349
+ type: "text",
1350
+ text: `\u{1F680} **AI Session Started!**
1009
1351
 
1010
1352
  \u{1F194} Session ID: **${sessionId}**
1011
1353
  \u{1F3AB} Ticket: ${ticketId}
@@ -1014,115 +1356,143 @@ ${complexityScore ? `\u{1F3AF} Complexity: ${complexityScore}/10
1014
1356
  ` : ""}\u{1F4C5} Started: ${sessionStartTime.toLocaleString()}
1015
1357
 
1016
1358
  \u{1F4DD} Timetrack entry will be created when you complete the session.`
1017
- }]
1018
- };
1019
1359
  }
1020
- case "track-manual-follow-up": {
1021
- const { aiSessionId, originalPrompt, aiResponse, developerFollowUp, followUpReason, outcome = "success", estimatedMinutes, workDescription } = args2;
1022
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1023
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1024
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id, status, created_at, ai_time_estimate_minutes").in("team_id", teamIds);
1025
- if (sessionError) {
1026
- throw new Error(`Database error: ${sessionError.message}`);
1027
- }
1028
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1029
- if (!session) {
1030
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1031
- }
1032
- const followUpTime = /* @__PURE__ */ new Date();
1033
- const oldEstimate = session.ai_time_estimate_minutes || 60;
1034
- const roundedFollowUpMinutes = roundToNearest15Minutes(estimatedMinutes || 0);
1035
- const newEstimate = oldEstimate + roundedFollowUpMinutes;
1036
- await supabase.from("ai_sessions").update({
1037
- status: "in_progress",
1038
- // Restart active tracking
1039
- ai_time_estimate_minutes: newEstimate
1040
- // Increase estimate based on follow-up work
1041
- // Don't update completed_at - session continues
1042
- }).eq("id", session.id);
1043
- const { data, error } = await supabase.from("manual_follow_ups").insert({
1044
- ai_session_id: session.id,
1045
- developer_id: authContext.userId,
1046
- team_id: authContext.teamId,
1047
- original_prompt: originalPrompt,
1048
- ai_response: aiResponse,
1049
- follow_up_prompt: developerFollowUp,
1050
- follow_up_reason: followUpReason,
1051
- outcome,
1052
- time_spent_minutes: null,
1053
- // Calculated automatically from session timestamps
1054
- resolved_at: outcome === "success" ? (/* @__PURE__ */ new Date()).toISOString() : null
1055
- }).select().single();
1056
- if (error) throw error;
1057
- await supabase.from("ai_time_logs").insert({
1058
- ai_session_id: session.id,
1059
- activity_type: "debugging",
1060
- // Follow-ups are typically debugging/fixing
1061
- description: `Follow-up: ${followUpReason.replace("_", " ")} - ${outcome}`,
1062
- duration_seconds: 0,
1063
- // Duration calculated automatically from timestamps
1064
- productivity_score: outcome === "success" ? 9 : outcome === "partial_success" ? 6 : 4,
1065
- started_at: followUpTime.toISOString()
1066
- });
1067
- const sessionStartTime = new Date(session.created_at);
1068
- const totalMinutesElapsed = Math.round((followUpTime.getTime() - sessionStartTime.getTime()) / 6e4);
1069
- const currentEfficiency = totalMinutesElapsed > 0 ? totalMinutesElapsed / newEstimate : 1;
1070
- await supabase.from("ai_sessions").update({
1071
- efficiency_score: currentEfficiency.toFixed(2),
1072
- actual_time_minutes: totalMinutesElapsed
1073
- }).eq("id", session.id);
1074
- const { data: existingEntries, error: entryError } = await supabase.from("agenda_events").select("id, tracked_duration, title, description, start_time").eq("ai_session_id", session.id).eq("status", "draft").order("created_at", { ascending: false });
1075
- let trackerAction = "";
1076
- let trackerDetails = "";
1077
- const existingEntry = existingEntries && existingEntries.length > 0 ? existingEntries[0] : null;
1078
- if (existingEntries && existingEntries.length > 1) {
1079
- const totalExistingDuration = existingEntries.reduce((sum, entry) => sum + (entry.tracked_duration || 0), 0);
1080
- const duplicateIds = existingEntries.slice(1).map((e) => e.id);
1081
- await supabase.from("agenda_events").delete().in("id", duplicateIds);
1082
- if (totalExistingDuration > (existingEntry?.tracked_duration || 0)) {
1083
- await supabase.from("agenda_events").update({ tracked_duration: totalExistingDuration }).eq("id", existingEntry.id);
1084
- existingEntry.tracked_duration = totalExistingDuration;
1085
- }
1086
- trackerAction = `Consolidated ${existingEntries.length} duplicate entries`;
1087
- }
1088
- if (existingEntry) {
1089
- const newDuration = (existingEntry.tracked_duration || 0) + roundedFollowUpMinutes * 60;
1090
- await supabase.from("agenda_events").update({
1091
- tracked_duration: newDuration,
1092
- end_time: followUpTime.toISOString(),
1093
- title: workDescription,
1094
- description: workDescription
1095
- }).eq("id", existingEntry.id);
1096
- trackerAction = trackerAction || "Updated existing tracker";
1097
- trackerDetails = ` \u2022 Total tracked time: ${Math.round(newDuration / 60)} minutes (+${roundedFollowUpMinutes} min)
1360
+ ]
1361
+ };
1362
+ }
1363
+ async function handleTrackManualFollowUp(input) {
1364
+ const ctx = authContext;
1365
+ const {
1366
+ aiSessionId,
1367
+ originalPrompt,
1368
+ aiResponse,
1369
+ developerFollowUp,
1370
+ followUpReason,
1371
+ outcome = "success",
1372
+ estimatedMinutes,
1373
+ workDescription
1374
+ } = input;
1375
+ const prefix = aiSessionId.replace("ai-sess-", "");
1376
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1377
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1378
+ if (!fullSessionId) {
1379
+ throw new Error(`Session not found: ${aiSessionId}`);
1380
+ }
1381
+ const [session] = await db.select({
1382
+ id: schema.aiSessions.id,
1383
+ status: schema.aiSessions.status,
1384
+ createdAt: schema.aiSessions.createdAt,
1385
+ aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes
1386
+ }).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
1387
+ if (!session) throw new Error(`Session not found: ${aiSessionId}`);
1388
+ const followUpTime = /* @__PURE__ */ new Date();
1389
+ const oldEstimate = session.aiTimeEstimateMinutes ?? 60;
1390
+ const roundedFollowUpMinutes = roundToNearest15Minutes(
1391
+ estimatedMinutes || 0
1392
+ );
1393
+ const newEstimate = oldEstimate + roundedFollowUpMinutes;
1394
+ await db.update(schema.aiSessions).set({
1395
+ status: "in_progress",
1396
+ aiTimeEstimateMinutes: newEstimate
1397
+ }).where(eq(schema.aiSessions.id, session.id));
1398
+ await db.insert(schema.manualFollowUps).values({
1399
+ aiSessionId: session.id,
1400
+ developerId: ctx.userId,
1401
+ teamId: ctx.teamId,
1402
+ originalPrompt,
1403
+ aiResponse,
1404
+ followUpPrompt: developerFollowUp,
1405
+ followUpReason,
1406
+ outcome,
1407
+ timeSpentMinutes: null,
1408
+ resolvedAt: outcome === "success" ? (/* @__PURE__ */ new Date()).toISOString() : null
1409
+ });
1410
+ await db.insert(schema.aiTimeLogs).values({
1411
+ aiSessionId: session.id,
1412
+ activityType: "debugging",
1413
+ description: `Follow-up: ${followUpReason.replace("_", " ")} - ${outcome}`,
1414
+ durationSeconds: 0,
1415
+ productivityScore: outcome === "success" ? 9 : outcome === "partial_success" ? 6 : 4,
1416
+ startedAt: followUpTime.toISOString()
1417
+ });
1418
+ const sessionStartTime = new Date(session.createdAt);
1419
+ const totalMinutesElapsed = Math.round(
1420
+ (followUpTime.getTime() - sessionStartTime.getTime()) / 6e4
1421
+ );
1422
+ const currentEfficiency = totalMinutesElapsed > 0 ? totalMinutesElapsed / newEstimate : 1;
1423
+ await db.update(schema.aiSessions).set({
1424
+ efficiencyScore: currentEfficiency.toFixed(2),
1425
+ actualTimeMinutes: totalMinutesElapsed
1426
+ }).where(eq(schema.aiSessions.id, session.id));
1427
+ const existingEntries = await db.select({
1428
+ id: schema.agendaEvents.id,
1429
+ trackedDuration: schema.agendaEvents.trackedDuration,
1430
+ title: schema.agendaEvents.title,
1431
+ description: schema.agendaEvents.description,
1432
+ startTime: schema.agendaEvents.startTime
1433
+ }).from(schema.agendaEvents).where(
1434
+ and(
1435
+ eq(schema.agendaEvents.aiSessionId, session.id),
1436
+ eq(schema.agendaEvents.status, "draft")
1437
+ )
1438
+ ).orderBy(desc(schema.agendaEvents.createdAt));
1439
+ let trackerAction = "";
1440
+ let trackerDetails = "";
1441
+ let existingEntry = existingEntries[0] ?? null;
1442
+ if (existingEntries.length > 1) {
1443
+ const totalExistingDuration = existingEntries.reduce(
1444
+ (sum, entry) => sum + (entry.trackedDuration ?? 0),
1445
+ 0
1446
+ );
1447
+ const duplicateIds = existingEntries.slice(1).map((e) => e.id);
1448
+ await db.delete(schema.agendaEvents).where(inArray(schema.agendaEvents.id, duplicateIds));
1449
+ if (existingEntry && totalExistingDuration > (existingEntry.trackedDuration ?? 0)) {
1450
+ await db.update(schema.agendaEvents).set({ trackedDuration: totalExistingDuration }).where(eq(schema.agendaEvents.id, existingEntry.id));
1451
+ existingEntry = { ...existingEntry, trackedDuration: totalExistingDuration };
1452
+ }
1453
+ trackerAction = `Consolidated ${existingEntries.length} duplicate entries`;
1454
+ }
1455
+ if (existingEntry) {
1456
+ const newDuration = (existingEntry.trackedDuration ?? 0) + roundedFollowUpMinutes * 60;
1457
+ await db.update(schema.agendaEvents).set({
1458
+ trackedDuration: newDuration,
1459
+ endTime: followUpTime.toISOString(),
1460
+ title: workDescription,
1461
+ description: workDescription
1462
+ }).where(eq(schema.agendaEvents.id, existingEntry.id));
1463
+ trackerAction = trackerAction || "Updated existing tracker";
1464
+ trackerDetails = ` \u2022 Total tracked time: ${Math.round(newDuration / 60)} minutes (+${roundedFollowUpMinutes} min)
1098
1465
  \u2022 Description: ${workDescription}
1099
1466
  `;
1100
- } else {
1101
- const durationSeconds = roundedFollowUpMinutes * 60;
1102
- const startTime = new Date(followUpTime.getTime() - durationSeconds * 1e3);
1103
- await supabase.from("agenda_events").insert({
1104
- team_id: authContext.teamId,
1105
- user_id: authContext.userId,
1106
- ai_session_id: session.id,
1107
- title: workDescription,
1108
- description: workDescription,
1109
- start_time: startTime.toISOString(),
1110
- end_time: followUpTime.toISOString(),
1111
- type: "work",
1112
- status: "draft",
1113
- all_day: false,
1114
- is_tracked: true,
1115
- tracked_duration: durationSeconds
1116
- });
1117
- trackerAction = "Created new tracker";
1118
- trackerDetails = ` \u2022 Tracked time: ${roundedFollowUpMinutes} minutes
1467
+ } else {
1468
+ const durationSeconds = roundedFollowUpMinutes * 60;
1469
+ const startTime = new Date(
1470
+ followUpTime.getTime() - durationSeconds * 1e3
1471
+ );
1472
+ await db.insert(schema.agendaEvents).values({
1473
+ teamId: ctx.teamId,
1474
+ userId: ctx.userId,
1475
+ aiSessionId: session.id,
1476
+ title: workDescription,
1477
+ description: workDescription,
1478
+ startTime: startTime.toISOString(),
1479
+ endTime: followUpTime.toISOString(),
1480
+ type: "work",
1481
+ status: "draft",
1482
+ allDay: false,
1483
+ isTracked: true,
1484
+ trackedDuration: durationSeconds
1485
+ });
1486
+ trackerAction = "Created new tracker";
1487
+ trackerDetails = ` \u2022 Tracked time: ${roundedFollowUpMinutes} minutes
1119
1488
  \u2022 Description: ${workDescription}
1120
1489
  `;
1121
- }
1122
- return {
1123
- content: [{
1124
- type: "text",
1125
- text: `\u{1F504} **Follow-up Tracked & Session Restarted!**
1490
+ }
1491
+ return {
1492
+ content: [
1493
+ {
1494
+ type: "text",
1495
+ text: `\u{1F504} **Follow-up Tracked & Session Restarted!**
1126
1496
 
1127
1497
  \u{1F194} Session: ${aiSessionId} (back to active)
1128
1498
  \u{1F50D} Reason: ${followUpReason.replace("_", " ")}
@@ -1140,197 +1510,222 @@ ${complexityScore ? `\u{1F3AF} Complexity: ${complexityScore}/10
1140
1510
  \u23F1\uFE0F **Tracker Entry: ${trackerAction}**
1141
1511
  ` + trackerDetails + `
1142
1512
  \u26A1 **Time tracking resumed** - continue with confidence!`
1143
- }]
1144
- };
1145
1513
  }
1146
- case "get-session-context": {
1147
- const { aiSessionId, includeTicketData = true, includeTodoProgress = true, includeFollowUpHistory = false } = args2;
1148
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1149
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1150
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select(`
1151
- id,
1152
- ticket_id,
1153
- status,
1154
- ai_time_estimate_minutes,
1155
- actual_time_minutes,
1156
- complexity_score,
1157
- created_at,
1158
- cursor_session_id
1159
- `).in("team_id", teamIds);
1160
- if (sessionError) {
1161
- throw new Error(`Database error: ${sessionError.message}`);
1162
- }
1163
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1164
- if (sessionError || !session) {
1165
- throw new Error(`Session not found: ${aiSessionId}`);
1166
- }
1167
- let context = {
1168
- sessionId: aiSessionId,
1169
- status: session.status,
1170
- timeEstimate: session.ai_time_estimate_minutes,
1171
- actualTime: session.actual_time_minutes,
1172
- complexity: session.complexity_score,
1173
- createdAt: session.created_at
1174
- };
1175
- if (includeTicketData) {
1176
- const { data: ticket } = await supabase.from("tickets").select("id, ticket_number, title, description, status, priority, type").eq("id", session.ticket_id).single();
1177
- context.ticketData = ticket;
1178
- }
1179
- if (includeTodoProgress) {
1180
- const { data: todos } = await supabase.from("ai_todos").select("id, content, status, estimated_minutes, actual_minutes").eq("ai_session_id", session.id).order("sequence_order");
1181
- context.todos = todos || [];
1182
- context.todoProgress = {
1183
- total: todos?.length || 0,
1184
- completed: todos?.filter((t) => t.status === "completed").length || 0,
1185
- inProgress: todos?.filter((t) => t.status === "in_progress").length || 0
1186
- };
1187
- }
1188
- if (includeFollowUpHistory) {
1189
- const { data: followUps } = await supabase.from("manual_follow_ups").select("follow_up_reason, outcome, time_spent_minutes, created_at").eq("ai_session_id", session.id).order("created_at");
1190
- context.followUpHistory = followUps || [];
1191
- }
1192
- return {
1193
- content: [{
1194
- type: "text",
1195
- text: `\u{1F3AF} **Session Context Retrieved**
1514
+ ]
1515
+ };
1516
+ }
1517
+ async function handleGetSessionContext(input) {
1518
+ const ctx = authContext;
1519
+ const {
1520
+ aiSessionId,
1521
+ includeTicketData = true,
1522
+ includeTodoProgress = true,
1523
+ includeFollowUpHistory = false
1524
+ } = input;
1525
+ const prefix = aiSessionId.replace("ai-sess-", "");
1526
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1527
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1528
+ if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
1529
+ const [session] = await db.select({
1530
+ id: schema.aiSessions.id,
1531
+ ticketId: schema.aiSessions.ticketId,
1532
+ status: schema.aiSessions.status,
1533
+ aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
1534
+ actualTimeMinutes: schema.aiSessions.actualTimeMinutes,
1535
+ complexityScore: schema.aiSessions.complexityScore,
1536
+ createdAt: schema.aiSessions.createdAt,
1537
+ cursorSessionId: schema.aiSessions.cursorSessionId
1538
+ }).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
1539
+ if (!session) throw new Error(`Session not found: ${aiSessionId}`);
1540
+ const context = {
1541
+ status: session.status,
1542
+ timeEstimate: session.aiTimeEstimateMinutes,
1543
+ actualTime: session.actualTimeMinutes,
1544
+ complexity: session.complexityScore,
1545
+ createdAt: session.createdAt
1546
+ };
1547
+ if (includeTicketData) {
1548
+ const [ticket] = await db.select({
1549
+ id: schema.tickets.id,
1550
+ ticketNumber: schema.tickets.ticketNumber,
1551
+ title: schema.tickets.title,
1552
+ description: schema.tickets.description,
1553
+ status: schema.tickets.status,
1554
+ priority: schema.tickets.priority,
1555
+ type: schema.tickets.type
1556
+ }).from(schema.tickets).where(eq(schema.tickets.id, session.ticketId)).limit(1);
1557
+ context.ticketData = ticket ?? null;
1558
+ }
1559
+ if (includeTodoProgress) {
1560
+ const todos = await db.select({
1561
+ id: schema.aiTodos.id,
1562
+ content: schema.aiTodos.content,
1563
+ status: schema.aiTodos.status,
1564
+ estimatedMinutes: schema.aiTodos.estimatedMinutes,
1565
+ actualMinutes: schema.aiTodos.actualMinutes
1566
+ }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, session.id)).orderBy(asc(schema.aiTodos.sequenceOrder));
1567
+ context.todos = todos;
1568
+ context.todoProgress = {
1569
+ total: todos.length,
1570
+ completed: todos.filter((t) => t.status === "completed").length,
1571
+ inProgress: todos.filter((t) => t.status === "in_progress").length
1572
+ };
1573
+ }
1574
+ if (includeFollowUpHistory) {
1575
+ const followUps = await db.select({
1576
+ followUpReason: schema.manualFollowUps.followUpReason,
1577
+ outcome: schema.manualFollowUps.outcome,
1578
+ timeSpentMinutes: schema.manualFollowUps.timeSpentMinutes,
1579
+ createdAt: schema.manualFollowUps.createdAt
1580
+ }).from(schema.manualFollowUps).where(eq(schema.manualFollowUps.aiSessionId, session.id)).orderBy(asc(schema.manualFollowUps.createdAt));
1581
+ context.followUpHistory = followUps;
1582
+ }
1583
+ const ticketData = context.ticketData;
1584
+ const todoProgress = context.todoProgress;
1585
+ const followUpHistory = context.followUpHistory;
1586
+ return {
1587
+ content: [
1588
+ {
1589
+ type: "text",
1590
+ text: `\u{1F3AF} **Session Context Retrieved**
1196
1591
 
1197
1592
  Session: ${aiSessionId}
1198
1593
  Status: ${session.status}
1199
- ${context.ticketData ? `Ticket: ${context.ticketData.ticket_number} - ${context.ticketData.title}
1200
- ` : ""}${context.todoProgress ? `Todo Progress: ${context.todoProgress.completed}/${context.todoProgress.total} completed
1201
- ` : ""}${context.followUpHistory ? `Follow-ups: ${context.followUpHistory.length}
1594
+ ${ticketData ? `Ticket: ${ticketData.ticketNumber} - ${ticketData.title}
1595
+ ` : ""}${todoProgress ? `Todo Progress: ${todoProgress.completed}/${todoProgress.total} completed
1596
+ ` : ""}${followUpHistory ? `Follow-ups: ${followUpHistory.length}
1202
1597
  ` : ""}
1203
1598
  \u{1F4CB} Full context preserved for seamless continuation!`
1204
- }]
1205
- };
1206
1599
  }
1207
- case "sync-session-todos": {
1208
- const { aiSessionId, todos, replaceAll = true } = args2;
1209
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1210
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1211
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1212
- if (sessionError) {
1213
- throw new Error(`Database error: ${sessionError.message}`);
1214
- }
1215
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1216
- if (!session) {
1217
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1218
- }
1219
- if (replaceAll) {
1220
- await supabase.from("ai_todos").delete().eq("ai_session_id", session.id);
1221
- }
1222
- if (todos && todos.length > 0) {
1223
- let startSequence = 0;
1224
- if (!replaceAll) {
1225
- const { data: maxTodo } = await supabase.from("ai_todos").select("sequence_order").eq("ai_session_id", session.id).order("sequence_order", { ascending: false }).limit(1).single();
1226
- startSequence = (maxTodo?.sequence_order || 0) + 1;
1227
- }
1228
- const todoInserts = todos.map((todo, index) => ({
1229
- ai_session_id: session.id,
1230
- content: todo.content,
1231
- status: todo.status,
1232
- cursor_todo_id: todo.todoId || null,
1233
- estimated_minutes: todo.estimatedMinutes || null,
1234
- sequence_order: startSequence + index
1235
- }));
1236
- const { error: insertError } = await supabase.from("ai_todos").insert(todoInserts);
1237
- if (insertError) throw insertError;
1238
- }
1239
- let phaseTransition = null;
1240
- const { data: currentTodos } = await supabase.from("ai_todos").select("status").eq("ai_session_id", session.id);
1241
- if (currentTodos && currentTodos.length > 0) {
1242
- const hasInProgress = currentTodos.some((t) => t.status === "in_progress");
1243
- const allCompleted = currentTodos.every((t) => t.status === "completed");
1244
- const { data: currentPhase } = await supabase.from("ai_time_logs").select("activity_type, status").eq("ai_session_id", session.id).eq("status", "in_progress").single();
1245
- if (hasInProgress && currentPhase?.activity_type === "analysis") {
1246
- await transitionToNextPhase(session.id, "analysis");
1247
- phaseTransition = "Analysis completed \u2192 Next phase started (Investigation/Development)";
1248
- }
1249
- if (hasInProgress && currentPhase?.activity_type === "bug_investigation") {
1250
- const completedCount = currentTodos.filter((t) => t.status === "completed").length;
1251
- if (completedCount > 0) {
1252
- await transitionToNextPhase(session.id, "bug_investigation");
1253
- phaseTransition = "Investigation completed \u2192 Development phase started";
1254
- }
1255
- }
1256
- if (allCompleted && currentPhase?.activity_type === "development") {
1257
- await transitionToNextPhase(session.id, "development");
1258
- phaseTransition = "Development completed \u2192 Communication phase started";
1259
- }
1260
- }
1261
- return {
1262
- content: [{
1263
- type: "text",
1264
- text: `\u2705 **Todos ${replaceAll ? "Synced" : "Added"} Successfully!**
1600
+ ]
1601
+ };
1602
+ }
1603
+ async function handleSyncSessionTodos(input) {
1604
+ const ctx = authContext;
1605
+ const { aiSessionId, todos, replaceAll = true } = input;
1606
+ const prefix = aiSessionId.replace("ai-sess-", "");
1607
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1608
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1609
+ if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
1610
+ if (replaceAll) {
1611
+ await db.delete(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId));
1612
+ }
1613
+ if (todos && todos.length > 0) {
1614
+ let startSequence = 0;
1615
+ if (!replaceAll) {
1616
+ const [maxTodo] = await db.select({ sequenceOrder: schema.aiTodos.sequenceOrder }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId)).orderBy(desc(schema.aiTodos.sequenceOrder)).limit(1);
1617
+ startSequence = (maxTodo?.sequenceOrder ?? 0) + 1;
1618
+ }
1619
+ await db.insert(schema.aiTodos).values(
1620
+ todos.map((todo, index) => ({
1621
+ aiSessionId: fullSessionId,
1622
+ content: todo.content,
1623
+ status: todo.status,
1624
+ cursorTodoId: todo.todoId ?? null,
1625
+ estimatedMinutes: todo.estimatedMinutes ?? null,
1626
+ sequenceOrder: startSequence + index
1627
+ }))
1628
+ );
1629
+ }
1630
+ let phaseTransition = null;
1631
+ const currentTodos = await db.select({ status: schema.aiTodos.status }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId));
1632
+ if (currentTodos.length > 0) {
1633
+ const hasInProgress = currentTodos.some((t) => t.status === "in_progress");
1634
+ const allCompleted = currentTodos.every((t) => t.status === "completed");
1635
+ const [currentPhase] = await db.select({
1636
+ activityType: schema.aiTimeLogs.activityType,
1637
+ status: schema.aiTimeLogs.status
1638
+ }).from(schema.aiTimeLogs).where(
1639
+ and(
1640
+ eq(schema.aiTimeLogs.aiSessionId, fullSessionId),
1641
+ eq(schema.aiTimeLogs.status, "in_progress")
1642
+ )
1643
+ ).limit(1);
1644
+ if (hasInProgress && currentPhase?.activityType === "analysis") {
1645
+ await transitionToNextPhase(fullSessionId, "analysis");
1646
+ phaseTransition = "Analysis completed \u2192 Next phase started (Investigation/Development)";
1647
+ }
1648
+ if (hasInProgress && currentPhase?.activityType === "bug_investigation") {
1649
+ const completedCount = currentTodos.filter(
1650
+ (t) => t.status === "completed"
1651
+ ).length;
1652
+ if (completedCount > 0) {
1653
+ await transitionToNextPhase(fullSessionId, "bug_investigation");
1654
+ phaseTransition = "Investigation completed \u2192 Development phase started";
1655
+ }
1656
+ }
1657
+ if (allCompleted && currentPhase?.activityType === "development") {
1658
+ await transitionToNextPhase(fullSessionId, "development");
1659
+ phaseTransition = "Development completed \u2192 Communication phase started";
1660
+ }
1661
+ }
1662
+ return {
1663
+ content: [
1664
+ {
1665
+ type: "text",
1666
+ text: `\u2705 **Todos ${replaceAll ? "Synced" : "Added"} Successfully!**
1265
1667
 
1266
1668
  Session: ${aiSessionId}
1267
1669
  ${replaceAll ? "Synced" : "Added"} ${todos?.length || 0} todos
1268
1670
  ${replaceAll ? "" : "\u2795 Added to existing todo list\n"}${phaseTransition ? `\u{1F504} Phase Transition: ${phaseTransition}
1269
1671
  ` : ""}
1270
1672
  \u{1F4DD} Todo list updated and tracked for progress monitoring!`
1271
- }]
1272
- };
1273
1673
  }
1274
- case "add-follow-up-todos": {
1275
- const { aiSessionId, newTodos, followUpReason } = args2;
1276
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1277
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1278
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1279
- if (sessionError) {
1280
- throw new Error(`Database error: ${sessionError.message}`);
1281
- }
1282
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1283
- if (!session) {
1284
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1285
- }
1286
- if (newTodos && newTodos.length > 0) {
1287
- const { data: maxTodo } = await supabase.from("ai_todos").select("sequence_order").eq("ai_session_id", session.id).order("sequence_order", { ascending: false }).limit(1).single();
1288
- const startSequence = (maxTodo?.sequence_order || 0) + 1;
1289
- const todoInserts = newTodos.map((todo, index) => ({
1290
- ai_session_id: session.id,
1291
- content: `[Follow-up] ${todo.content}`,
1292
- status: todo.status || "pending",
1293
- estimated_minutes: todo.estimatedMinutes || null,
1294
- sequence_order: startSequence + index
1295
- }));
1296
- const { error: insertError } = await supabase.from("ai_todos").insert(todoInserts);
1297
- if (insertError) throw insertError;
1298
- }
1299
- return {
1300
- content: [{
1301
- type: "text",
1302
- text: `\u2705 **Follow-up Todos Added Successfully!**
1674
+ ]
1675
+ };
1676
+ }
1677
+ async function handleAddFollowUpTodos(input) {
1678
+ const ctx = authContext;
1679
+ const { aiSessionId, newTodos, followUpReason } = input;
1680
+ const prefix = aiSessionId.replace("ai-sess-", "");
1681
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1682
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1683
+ if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
1684
+ if (newTodos && newTodos.length > 0) {
1685
+ const [maxTodo] = await db.select({ sequenceOrder: schema.aiTodos.sequenceOrder }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId)).orderBy(desc(schema.aiTodos.sequenceOrder)).limit(1);
1686
+ const startSequence = (maxTodo?.sequenceOrder ?? 0) + 1;
1687
+ await db.insert(schema.aiTodos).values(
1688
+ newTodos.map((todo, index) => ({
1689
+ aiSessionId: fullSessionId,
1690
+ content: `[Follow-up] ${todo.content}`,
1691
+ status: todo.status ?? "pending",
1692
+ estimatedMinutes: todo.estimatedMinutes ?? null,
1693
+ sequenceOrder: startSequence + index
1694
+ }))
1695
+ );
1696
+ }
1697
+ return {
1698
+ content: [
1699
+ {
1700
+ type: "text",
1701
+ text: `\u2705 **Follow-up Todos Added Successfully!**
1303
1702
 
1304
1703
  Session: ${aiSessionId}
1305
1704
  Added ${newTodos?.length || 0} new todos from follow-up
1306
1705
  ${followUpReason ? `Reason: ${followUpReason}
1307
1706
  ` : ""}
1308
1707
  \u{1F4DD} New tasks identified and added to existing workflow!`
1309
- }]
1310
- };
1311
1708
  }
1312
- case "update-session-status": {
1313
- const { aiSessionId, status, actualTimeMinutes, completionNotes } = args2;
1314
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1315
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1316
- const { data: allSessions, error: findError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1317
- if (findError) {
1318
- throw new Error(`Database error: ${findError.message}`);
1319
- }
1320
- const foundSession = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1321
- if (!foundSession) {
1322
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1323
- }
1324
- const { data, error } = await supabase.from("ai_sessions").update({
1325
- status,
1326
- actual_time_minutes: actualTimeMinutes || null,
1327
- completed_at: status === "completed" ? (/* @__PURE__ */ new Date()).toISOString() : null
1328
- }).eq("id", foundSession.id).select().single();
1329
- if (error) throw error;
1330
- return {
1331
- content: [{
1332
- type: "text",
1333
- text: `\u{1F3AF} **Session Status Updated!**
1709
+ ]
1710
+ };
1711
+ }
1712
+ async function handleUpdateSessionStatus(input) {
1713
+ const ctx = authContext;
1714
+ const { aiSessionId, status, actualTimeMinutes, completionNotes } = input;
1715
+ const prefix = aiSessionId.replace("ai-sess-", "");
1716
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1717
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1718
+ if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
1719
+ await db.update(schema.aiSessions).set({
1720
+ status,
1721
+ actualTimeMinutes: actualTimeMinutes ?? null,
1722
+ completedAt: status === "completed" ? (/* @__PURE__ */ new Date()).toISOString() : null
1723
+ }).where(eq(schema.aiSessions.id, fullSessionId));
1724
+ return {
1725
+ content: [
1726
+ {
1727
+ type: "text",
1728
+ text: `\u{1F3AF} **Session Status Updated!**
1334
1729
 
1335
1730
  Session: ${aiSessionId}
1336
1731
  Status: ${status}
@@ -1338,113 +1733,135 @@ ${actualTimeMinutes ? `Actual Time: ${actualTimeMinutes} minutes
1338
1733
  ` : ""}${status === "completed" ? `\u2705 Session completed successfully!
1339
1734
  ` : ""}${completionNotes ? `Notes: ${completionNotes}
1340
1735
  ` : ""}`
1341
- }]
1342
- };
1343
1736
  }
1344
- case "get-completion-context": {
1345
- const { aiSessionId, includeFollowUps = true, includeTimeMetrics = true, includeTodos = true } = args2;
1346
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1347
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1348
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select(`
1349
- id,
1350
- ticket_id,
1351
- ai_time_estimate_minutes,
1352
- actual_time_minutes,
1353
- efficiency_score,
1354
- created_at,
1355
- completed_at,
1356
- status,
1357
- complexity_score
1358
- `).in("team_id", teamIds);
1359
- if (sessionError) {
1360
- throw new Error(`Database error: ${sessionError.message}`);
1361
- }
1362
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1363
- if (!session) {
1364
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1365
- }
1366
- const { data: ticket, error: ticketError } = await supabase.from("tickets").select("ticket_number, title, description, type, priority").eq("id", session.ticket_id).single();
1367
- if (ticketError || !ticket) {
1368
- throw new Error("Ticket not found for session");
1369
- }
1370
- let contextData = {
1371
- session: {
1372
- id: aiSessionId,
1373
- status: session.status,
1374
- complexity: session.complexity_score,
1375
- createdAt: session.created_at,
1376
- completedAt: session.completed_at
1377
- },
1378
- ticket: {
1379
- number: ticket.ticket_number,
1380
- title: ticket.title,
1381
- description: ticket.description,
1382
- type: ticket.type,
1383
- priority: ticket.priority
1384
- }
1385
- };
1386
- if (includeTimeMetrics) {
1387
- const timeSaved = session.ai_time_estimate_minutes && session.actual_time_minutes ? Math.max(0, session.ai_time_estimate_minutes - session.actual_time_minutes) : null;
1388
- contextData.timeMetrics = {
1389
- estimatedMinutes: session.ai_time_estimate_minutes,
1390
- actualMinutes: session.actual_time_minutes,
1391
- timeSaved,
1392
- efficiency: session.efficiency_score,
1393
- sessionDuration: session.completed_at && session.created_at ? Math.round((new Date(session.completed_at).getTime() - new Date(session.created_at).getTime()) / 6e4) : null
1394
- };
1395
- }
1396
- if (includeTodos) {
1397
- const { data: todos } = await supabase.from("ai_todos").select("content, status, estimated_minutes, actual_minutes, completed_at").eq("ai_session_id", session.id).order("created_at", { ascending: true });
1398
- contextData.todos = todos || [];
1399
- }
1400
- if (includeFollowUps) {
1401
- const { data: followUps } = await supabase.from("manual_follow_ups").select("follow_up_reason, outcome, time_spent_minutes, created_at").eq("ai_session_id", session.id).order("created_at", { ascending: true });
1402
- contextData.followUps = followUps || [];
1403
- }
1404
- return {
1405
- content: [{
1406
- type: "text",
1407
- text: `\u{1F4CB} **Completion Context Retrieved!**
1737
+ ]
1738
+ };
1739
+ }
1740
+ async function handleGetCompletionContext(input) {
1741
+ const ctx = authContext;
1742
+ const {
1743
+ aiSessionId,
1744
+ includeFollowUps = true,
1745
+ includeTimeMetrics = true,
1746
+ includeTodos = true
1747
+ } = input;
1748
+ const prefix = aiSessionId.replace("ai-sess-", "");
1749
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1750
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1751
+ if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
1752
+ const [session] = await db.select({
1753
+ id: schema.aiSessions.id,
1754
+ ticketId: schema.aiSessions.ticketId,
1755
+ aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
1756
+ actualTimeMinutes: schema.aiSessions.actualTimeMinutes,
1757
+ efficiencyScore: schema.aiSessions.efficiencyScore,
1758
+ createdAt: schema.aiSessions.createdAt,
1759
+ completedAt: schema.aiSessions.completedAt,
1760
+ status: schema.aiSessions.status,
1761
+ complexityScore: schema.aiSessions.complexityScore
1762
+ }).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
1763
+ if (!session) throw new Error(`Session not found: ${aiSessionId}`);
1764
+ const [ticket] = await db.select({
1765
+ ticketNumber: schema.tickets.ticketNumber,
1766
+ title: schema.tickets.title,
1767
+ description: schema.tickets.description,
1768
+ type: schema.tickets.type,
1769
+ priority: schema.tickets.priority
1770
+ }).from(schema.tickets).where(eq(schema.tickets.id, session.ticketId)).limit(1);
1771
+ if (!ticket) throw new Error("Ticket not found for session");
1772
+ const contextData = {
1773
+ session: {
1774
+ id: aiSessionId,
1775
+ status: session.status,
1776
+ complexity: session.complexityScore,
1777
+ createdAt: session.createdAt,
1778
+ completedAt: session.completedAt
1779
+ },
1780
+ ticket: {
1781
+ number: ticket.ticketNumber,
1782
+ title: ticket.title,
1783
+ description: ticket.description,
1784
+ type: ticket.type,
1785
+ priority: ticket.priority
1786
+ }
1787
+ };
1788
+ if (includeTimeMetrics) {
1789
+ const timeSaved = session.aiTimeEstimateMinutes && session.actualTimeMinutes ? Math.max(
1790
+ 0,
1791
+ session.aiTimeEstimateMinutes - session.actualTimeMinutes
1792
+ ) : null;
1793
+ contextData.timeMetrics = {
1794
+ estimatedMinutes: session.aiTimeEstimateMinutes,
1795
+ actualMinutes: session.actualTimeMinutes,
1796
+ timeSaved,
1797
+ efficiency: session.efficiencyScore,
1798
+ sessionDuration: session.completedAt && session.createdAt ? Math.round(
1799
+ (new Date(session.completedAt).getTime() - new Date(session.createdAt).getTime()) / 6e4
1800
+ ) : null
1801
+ };
1802
+ }
1803
+ if (includeTodos) {
1804
+ const todos = await db.select({
1805
+ content: schema.aiTodos.content,
1806
+ status: schema.aiTodos.status,
1807
+ estimatedMinutes: schema.aiTodos.estimatedMinutes,
1808
+ actualMinutes: schema.aiTodos.actualMinutes,
1809
+ completedAt: schema.aiTodos.completedAt
1810
+ }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, session.id)).orderBy(asc(schema.aiTodos.createdAt));
1811
+ contextData.todos = todos;
1812
+ }
1813
+ if (includeFollowUps) {
1814
+ const followUps = await db.select({
1815
+ followUpReason: schema.manualFollowUps.followUpReason,
1816
+ outcome: schema.manualFollowUps.outcome,
1817
+ timeSpentMinutes: schema.manualFollowUps.timeSpentMinutes,
1818
+ createdAt: schema.manualFollowUps.createdAt
1819
+ }).from(schema.manualFollowUps).where(eq(schema.manualFollowUps.aiSessionId, session.id)).orderBy(asc(schema.manualFollowUps.createdAt));
1820
+ contextData.followUps = followUps;
1821
+ }
1822
+ const todosLen = contextData.todos ?? [];
1823
+ const completedTodos = todosLen.filter((t) => t.status === "completed").length;
1824
+ const followUpsLen = contextData.followUps?.length ?? 0;
1825
+ return {
1826
+ content: [
1827
+ {
1828
+ type: "text",
1829
+ text: `\u{1F4CB} **Completion Context Retrieved!**
1408
1830
 
1409
- \u{1F3AB} **Ticket:** ${ticket.ticket_number} - ${ticket.title}
1831
+ \u{1F3AB} **Ticket:** ${ticket.ticketNumber} - ${ticket.title}
1410
1832
  \u{1F194} **Session:** ${aiSessionId} (${session.status})
1411
- \u23F1\uFE0F **Time:** ${session.actual_time_minutes || "N/A"}/${session.ai_time_estimate_minutes || "N/A"} minutes
1412
- \u{1F4CB} **Todos:** ${contextData.todos?.filter((t) => t.status === "completed").length || 0}/${contextData.todos?.length || 0} completed
1413
- \u{1F504} **Follow-ups:** ${contextData.followUps?.length || 0}
1833
+ \u23F1\uFE0F **Time:** ${session.actualTimeMinutes || "N/A"}/${session.aiTimeEstimateMinutes || "N/A"} minutes
1834
+ \u{1F4CB} **Todos:** ${completedTodos}/${todosLen.length} completed
1835
+ \u{1F504} **Follow-ups:** ${followUpsLen}
1414
1836
 
1415
1837
  \u2705 **Full context ready for Cursor AI to generate customer response!**
1416
1838
 
1417
1839
  **Context Data:**
1418
1840
  \`\`\`json
1419
1841
  ${JSON.stringify(contextData, null, 2)}\`\`\``
1420
- }]
1421
- };
1422
1842
  }
1423
- case "save-customer-response": {
1424
- const { aiSessionId, customerResponse, responseType = "completion" } = args2;
1425
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1426
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1427
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1428
- if (sessionError) {
1429
- throw new Error(`Database error: ${sessionError.message}`);
1430
- }
1431
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1432
- if (!session) {
1433
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1434
- }
1435
- const { data: responseData, error: responseError } = await supabase.from("ai_responses").insert({
1436
- ai_session_id: session.id,
1437
- response_type: responseType,
1438
- content: customerResponse,
1439
- is_ready_for_customer: true,
1440
- provider_approved: false
1441
- // Needs manual approval
1442
- }).select().single();
1443
- if (responseError) throw responseError;
1444
- return {
1445
- content: [{
1446
- type: "text",
1447
- text: `\u{1F4BE} **Customer Response Saved!**
1843
+ ]
1844
+ };
1845
+ }
1846
+ async function handleSaveCustomerResponse(input) {
1847
+ const ctx = authContext;
1848
+ const { aiSessionId, customerResponse, responseType = "completion" } = input;
1849
+ const prefix = aiSessionId.replace("ai-sess-", "");
1850
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1851
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1852
+ if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
1853
+ await db.insert(schema.aiResponses).values({
1854
+ aiSessionId: fullSessionId,
1855
+ responseType,
1856
+ content: customerResponse,
1857
+ isReadyForCustomer: true,
1858
+ providerApproved: false
1859
+ });
1860
+ return {
1861
+ content: [
1862
+ {
1863
+ type: "text",
1864
+ text: `\u{1F4BE} **Customer Response Saved!**
1448
1865
 
1449
1866
  \u{1F194} Session: ${aiSessionId}
1450
1867
  \u{1F4DD} Response Type: ${responseType}
@@ -1456,623 +1873,693 @@ ${JSON.stringify(contextData, null, 2)}\`\`\``
1456
1873
  **Preview:**
1457
1874
  \`\`\`
1458
1875
  ${customerResponse.substring(0, 200)}${customerResponse.length > 200 ? "..." : ""}\`\`\``
1459
- }]
1460
- };
1461
1876
  }
1462
- case "complete-ai-session": {
1463
- const { aiSessionId, workCompleted, technicalSummary, invoiceDescription, efficiencyNotes } = args2;
1464
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1465
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1466
- const { data: allSessions, error: getSessionError } = await supabase.from("ai_sessions").select("id, ticket_id, ai_time_estimate_minutes, created_at").in("team_id", teamIds);
1467
- if (getSessionError) {
1468
- throw new Error(`Database error: ${getSessionError.message}`);
1469
- }
1470
- const existingSession = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1471
- if (!existingSession) {
1472
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1473
- }
1474
- const completionTime = /* @__PURE__ */ new Date();
1475
- const sessionStartTime = new Date(existingSession.created_at);
1476
- const timeSpentMinutes = Math.round((completionTime.getTime() - sessionStartTime.getTime()) / 6e4);
1477
- const { data: session, error: sessionError } = await supabase.from("ai_sessions").update({
1478
- status: "completed",
1479
- actual_time_minutes: timeSpentMinutes,
1480
- completed_at: completionTime.toISOString(),
1481
- efficiency_score: null
1482
- // Will be calculated
1483
- }).eq("id", existingSession.id).select("id, ticket_id, ai_time_estimate_minutes, created_at").single();
1484
- if (sessionError || !session) {
1485
- throw new Error(`Failed to update session: ${aiSessionId}`);
1486
- }
1487
- const efficiencyScore = session.ai_time_estimate_minutes ? timeSpentMinutes / session.ai_time_estimate_minutes : 1;
1488
- await supabase.from("ai_sessions").update({ efficiency_score: efficiencyScore.toFixed(2) }).eq("id", session.id);
1489
- const { data: activePhases } = await supabase.from("ai_time_logs").select("*").eq("ai_session_id", existingSession.id).eq("status", "in_progress");
1490
- for (const phase of activePhases || []) {
1491
- const duration = Math.round(
1492
- (completionTime.getTime() - new Date(phase.started_at).getTime()) / 1e3
1493
- );
1494
- await supabase.from("ai_time_logs").update({
1495
- ended_at: completionTime.toISOString(),
1496
- duration_seconds: duration,
1497
- status: "completed"
1498
- }).eq("id", phase.id);
1499
- }
1500
- await supabase.from("ai_time_logs").update({ status: "skipped" }).eq("ai_session_id", existingSession.id).eq("status", "pending").eq("estimated_duration_seconds", 0);
1501
- const sessionDuration = Math.round(
1502
- (completionTime.getTime() - new Date(session.created_at).getTime()) / 6e4
1503
- );
1504
- const workSummary = `Completed ${workCompleted.length} tasks including: ${workCompleted.slice(0, 3).join(", ")}${workCompleted.length > 3 ? " and more" : ""}.`;
1505
- const { data: ticketInfo } = await supabase.from("tickets").select("ticket_number, title, project_id").eq("id", session.ticket_id).single();
1506
- let completionDescription;
1507
- if (invoiceDescription) {
1508
- completionDescription = `${ticketInfo?.ticket_number || "Ticket"}: ${invoiceDescription}`;
1509
- } else {
1510
- const workDescription = workCompleted.map((task, index) => `${index + 1}. ${task}`).join("\n");
1511
- completionDescription = `${ticketInfo?.ticket_number || "Ticket"}: ${technicalSummary || workSummary}
1877
+ ]
1878
+ };
1879
+ }
1880
+ async function handleCompleteAiSession(input) {
1881
+ const ctx = authContext;
1882
+ const {
1883
+ aiSessionId,
1884
+ workCompleted,
1885
+ technicalSummary,
1886
+ invoiceDescription,
1887
+ efficiencyNotes
1888
+ } = input;
1889
+ const prefix = aiSessionId.replace("ai-sess-", "");
1890
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
1891
+ const fullSessionId = await resolveAiSessionId(prefix, teamIds);
1892
+ if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
1893
+ const [existingSession] = await db.select({
1894
+ id: schema.aiSessions.id,
1895
+ ticketId: schema.aiSessions.ticketId,
1896
+ aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
1897
+ createdAt: schema.aiSessions.createdAt
1898
+ }).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
1899
+ if (!existingSession) {
1900
+ throw new Error(`Session not found: ${aiSessionId}`);
1901
+ }
1902
+ const completionTime = /* @__PURE__ */ new Date();
1903
+ const sessionStartTime = new Date(existingSession.createdAt);
1904
+ const timeSpentMinutes = Math.round(
1905
+ (completionTime.getTime() - sessionStartTime.getTime()) / 6e4
1906
+ );
1907
+ const [session] = await db.update(schema.aiSessions).set({
1908
+ status: "completed",
1909
+ actualTimeMinutes: timeSpentMinutes,
1910
+ completedAt: completionTime.toISOString(),
1911
+ efficiencyScore: null
1912
+ }).where(eq(schema.aiSessions.id, existingSession.id)).returning({
1913
+ id: schema.aiSessions.id,
1914
+ ticketId: schema.aiSessions.ticketId,
1915
+ aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
1916
+ createdAt: schema.aiSessions.createdAt
1917
+ });
1918
+ if (!session) throw new Error(`Failed to update session: ${aiSessionId}`);
1919
+ const efficiencyScore = session.aiTimeEstimateMinutes ? timeSpentMinutes / session.aiTimeEstimateMinutes : 1;
1920
+ await db.update(schema.aiSessions).set({ efficiencyScore: efficiencyScore.toFixed(2) }).where(eq(schema.aiSessions.id, session.id));
1921
+ const activePhases = await db.select().from(schema.aiTimeLogs).where(
1922
+ and(
1923
+ eq(schema.aiTimeLogs.aiSessionId, existingSession.id),
1924
+ eq(schema.aiTimeLogs.status, "in_progress")
1925
+ )
1926
+ );
1927
+ for (const phase of activePhases) {
1928
+ const duration = Math.round(
1929
+ (completionTime.getTime() - new Date(phase.startedAt).getTime()) / 1e3
1930
+ );
1931
+ await db.update(schema.aiTimeLogs).set({
1932
+ endedAt: completionTime.toISOString(),
1933
+ durationSeconds: duration,
1934
+ status: "completed"
1935
+ }).where(eq(schema.aiTimeLogs.id, phase.id));
1936
+ }
1937
+ await db.update(schema.aiTimeLogs).set({ status: "skipped" }).where(
1938
+ and(
1939
+ eq(schema.aiTimeLogs.aiSessionId, existingSession.id),
1940
+ eq(schema.aiTimeLogs.status, "pending"),
1941
+ eq(schema.aiTimeLogs.estimatedDurationSeconds, 0)
1942
+ )
1943
+ );
1944
+ const sessionDuration = Math.round(
1945
+ (completionTime.getTime() - new Date(session.createdAt).getTime()) / 6e4
1946
+ );
1947
+ const workSummary = `Completed ${workCompleted.length} tasks including: ${workCompleted.slice(0, 3).join(", ")}${workCompleted.length > 3 ? " and more" : ""}.`;
1948
+ const [ticketInfo] = await db.select({
1949
+ ticketNumber: schema.tickets.ticketNumber,
1950
+ title: schema.tickets.title,
1951
+ projectId: schema.tickets.projectId
1952
+ }).from(schema.tickets).where(eq(schema.tickets.id, session.ticketId)).limit(1);
1953
+ let completionDescription;
1954
+ if (invoiceDescription) {
1955
+ completionDescription = `${ticketInfo?.ticketNumber || "Ticket"}: ${invoiceDescription}`;
1956
+ } else {
1957
+ const workDescription = workCompleted.map((task, index) => `${index + 1}. ${task}`).join("\n");
1958
+ completionDescription = `${ticketInfo?.ticketNumber || "Ticket"}: ${technicalSummary || workSummary}
1512
1959
 
1513
1960
  Completed work:
1514
1961
  ${workDescription}`;
1515
- }
1516
- const estimatedMinutes = session.ai_time_estimate_minutes || timeSpentMinutes;
1517
- const sessionStart = new Date(session.created_at);
1518
- const estimatedEnd = new Date(sessionStart.getTime() + estimatedMinutes * 6e4);
1519
- const { data: existingAgendaEntries } = await supabase.from("agenda_events").select("id, tracked_duration").eq("ai_session_id", session.id).eq("status", "draft").order("created_at", { ascending: false });
1520
- let agendaEvent = null;
1521
- let agendaError = null;
1522
- let wasUpdated = false;
1523
- let consolidatedCount = 0;
1524
- const existingAgendaEntry = existingAgendaEntries && existingAgendaEntries.length > 0 ? existingAgendaEntries[0] : null;
1525
- if (existingAgendaEntries && existingAgendaEntries.length > 1) {
1526
- const duplicateIds = existingAgendaEntries.slice(1).map((e) => e.id);
1527
- await supabase.from("agenda_events").delete().in("id", duplicateIds);
1528
- consolidatedCount = existingAgendaEntries.length - 1;
1529
- }
1530
- if (existingAgendaEntry) {
1531
- const { data: updated, error: updateError } = await supabase.from("agenda_events").update({
1532
- title: ticketInfo?.title || "Development Work",
1533
- description: completionDescription,
1534
- end_time: estimatedEnd.toISOString(),
1535
- project_id: ticketInfo?.project_id || null,
1536
- ticket_id: session.ticket_id,
1537
- tracked_duration: estimatedMinutes * 60
1538
- // Use AI estimate in seconds for billing
1539
- }).eq("id", existingAgendaEntry.id).select("id").single();
1540
- agendaEvent = updated;
1541
- agendaError = updateError;
1542
- wasUpdated = true;
1543
- } else {
1544
- const { data: created, error: createError } = await supabase.from("agenda_events").insert({
1545
- team_id: authContext.teamId,
1546
- user_id: authContext.userId,
1547
- title: ticketInfo?.title || "Development Work",
1548
- description: completionDescription,
1549
- start_time: sessionStart.toISOString(),
1550
- end_time: estimatedEnd.toISOString(),
1551
- project_id: ticketInfo?.project_id || null,
1552
- ticket_id: session.ticket_id,
1553
- ai_session_id: session.id,
1554
- // Use the actual UUID, not the readable ID
1555
- type: "work",
1556
- status: "draft",
1557
- // Mark as draft for manual approval
1558
- all_day: false,
1559
- is_tracked: true,
1560
- tracked_duration: estimatedMinutes * 60
1561
- // Use AI estimate in seconds for billing
1562
- }).select("id").single();
1563
- agendaEvent = created;
1564
- agendaError = createError;
1565
- }
1566
- if (agendaError) {
1567
- console.error(`\u26A0\uFE0F Failed to ${wasUpdated ? "update" : "create"} agenda event:`, agendaError);
1568
- }
1569
- if (consolidatedCount > 0) {
1570
- console.log(`\u{1F9F9} Cleaned up ${consolidatedCount} duplicate agenda entries for session ${aiSessionId}`);
1571
- }
1572
- let responseText = `\u{1F389} **AI Session Completed Successfully!**
1962
+ }
1963
+ const estimatedMinutes = session.aiTimeEstimateMinutes ?? timeSpentMinutes;
1964
+ const sessionStart = new Date(session.createdAt);
1965
+ const estimatedEnd = new Date(
1966
+ sessionStart.getTime() + estimatedMinutes * 6e4
1967
+ );
1968
+ const existingAgendaEntries = await db.select({
1969
+ id: schema.agendaEvents.id,
1970
+ trackedDuration: schema.agendaEvents.trackedDuration
1971
+ }).from(schema.agendaEvents).where(
1972
+ and(
1973
+ eq(schema.agendaEvents.aiSessionId, session.id),
1974
+ eq(schema.agendaEvents.status, "draft")
1975
+ )
1976
+ ).orderBy(desc(schema.agendaEvents.createdAt));
1977
+ let agendaEventId = null;
1978
+ let wasUpdated = false;
1979
+ let consolidatedCount = 0;
1980
+ const existingAgendaEntry = existingAgendaEntries[0] ?? null;
1981
+ if (existingAgendaEntries.length > 1) {
1982
+ const duplicateIds = existingAgendaEntries.slice(1).map((e) => e.id);
1983
+ await db.delete(schema.agendaEvents).where(inArray(schema.agendaEvents.id, duplicateIds));
1984
+ consolidatedCount = existingAgendaEntries.length - 1;
1985
+ }
1986
+ try {
1987
+ if (existingAgendaEntry) {
1988
+ const [updated] = await db.update(schema.agendaEvents).set({
1989
+ title: ticketInfo?.title || "Development Work",
1990
+ description: completionDescription,
1991
+ endTime: estimatedEnd.toISOString(),
1992
+ projectId: ticketInfo?.projectId ?? null,
1993
+ trackedDuration: estimatedMinutes * 60
1994
+ }).where(eq(schema.agendaEvents.id, existingAgendaEntry.id)).returning({ id: schema.agendaEvents.id });
1995
+ agendaEventId = updated?.id ?? null;
1996
+ wasUpdated = true;
1997
+ } else {
1998
+ const [created] = await db.insert(schema.agendaEvents).values({
1999
+ teamId: ctx.teamId,
2000
+ userId: ctx.userId,
2001
+ title: ticketInfo?.title || "Development Work",
2002
+ description: completionDescription,
2003
+ startTime: sessionStart.toISOString(),
2004
+ endTime: estimatedEnd.toISOString(),
2005
+ projectId: ticketInfo?.projectId ?? null,
2006
+ aiSessionId: session.id,
2007
+ type: "work",
2008
+ status: "draft",
2009
+ allDay: false,
2010
+ isTracked: true,
2011
+ trackedDuration: estimatedMinutes * 60
2012
+ }).returning({ id: schema.agendaEvents.id });
2013
+ agendaEventId = created?.id ?? null;
2014
+ }
2015
+ if (agendaEventId && session.ticketId) {
2016
+ await db.insert(schema.agendaEventTickets).values({
2017
+ agendaEventId,
2018
+ ticketId: session.ticketId
2019
+ }).onConflictDoNothing();
2020
+ }
2021
+ } catch (err) {
2022
+ console.error(
2023
+ `\u26A0\uFE0F Failed to ${wasUpdated ? "update" : "create"} agenda event:`,
2024
+ err
2025
+ );
2026
+ }
2027
+ if (consolidatedCount > 0) {
2028
+ console.log(
2029
+ `\u{1F9F9} Cleaned up ${consolidatedCount} duplicate agenda entries for session ${aiSessionId}`
2030
+ );
2031
+ }
2032
+ let responseText = `\u{1F389} **AI Session Completed Successfully!**
1573
2033
 
1574
2034
  `;
1575
- responseText += `\u{1F194} Session: ${aiSessionId}
2035
+ responseText += `\u{1F194} Session: ${aiSessionId}
1576
2036
  `;
1577
- responseText += `\u{1F4CA} **Performance Summary:**
2037
+ responseText += `\u{1F4CA} **Performance Summary:**
1578
2038
  `;
1579
- responseText += ` \u2022 Tasks Completed: ${workCompleted.length}
2039
+ responseText += ` \u2022 Tasks Completed: ${workCompleted.length}
1580
2040
  `;
1581
- responseText += ` \u2022 Time Spent: ${timeSpentMinutes} minutes
2041
+ responseText += ` \u2022 Time Spent: ${timeSpentMinutes} minutes
1582
2042
  `;
1583
- responseText += ` \u2022 Estimated Time: ${session.ai_time_estimate_minutes || "N/A"} minutes
2043
+ responseText += ` \u2022 Estimated Time: ${session.aiTimeEstimateMinutes || "N/A"} minutes
1584
2044
  `;
1585
- responseText += ` \u2022 Efficiency: ${efficiencyScore < 1 ? "\u{1F680}" : efficiencyScore > 1.5 ? "\u26A0\uFE0F" : "\u23F1\uFE0F"} ${(efficiencyScore * 100).toFixed(0)}%
2045
+ responseText += ` \u2022 Efficiency: ${efficiencyScore < 1 ? "\u{1F680}" : efficiencyScore > 1.5 ? "\u26A0\uFE0F" : "\u23F1\uFE0F"} ${(efficiencyScore * 100).toFixed(0)}%
1586
2046
  `;
1587
- responseText += ` \u2022 Session Duration: ${sessionDuration} minutes
2047
+ responseText += ` \u2022 Session Duration: ${sessionDuration} minutes
1588
2048
 
1589
2049
  `;
1590
- responseText += `\u2705 **Work Completed:**
2050
+ responseText += `\u2705 **Work Completed:**
1591
2051
  `;
1592
- workCompleted.forEach((task, index) => {
1593
- responseText += `${index + 1}. ${task}
2052
+ workCompleted.forEach((task, index) => {
2053
+ responseText += `${index + 1}. ${task}
1594
2054
  `;
1595
- });
1596
- responseText += `
2055
+ });
2056
+ responseText += `
1597
2057
  `;
1598
- if (technicalSummary) {
1599
- responseText += `\u{1F527} **Technical Summary:**
2058
+ if (technicalSummary) {
2059
+ responseText += `\u{1F527} **Technical Summary:**
1600
2060
  ${technicalSummary}
1601
2061
 
1602
2062
  `;
1603
- }
1604
- if (efficiencyNotes) {
1605
- responseText += `\u{1F4C8} **Efficiency Notes:**
2063
+ }
2064
+ if (efficiencyNotes) {
2065
+ responseText += `\u{1F4C8} **Efficiency Notes:**
1606
2066
  ${efficiencyNotes}
1607
2067
 
1608
2068
  `;
1609
- }
1610
- if (agendaEvent) {
1611
- responseText += `\u{1F4C5} **Timetrack Entry ${wasUpdated ? "Updated" : "Created"}:**
2069
+ }
2070
+ if (agendaEventId) {
2071
+ responseText += `\u{1F4C5} **Timetrack Entry ${wasUpdated ? "Updated" : "Created"}:**
1612
2072
  `;
1613
- responseText += ` \u2022 Agenda event ${wasUpdated ? "updated with final" : "created with"} work summary
2073
+ responseText += ` \u2022 Agenda event ${wasUpdated ? "updated with final" : "created with"} work summary
1614
2074
  `;
1615
- responseText += ` \u2022 Status: DRAFT (requires approval in agenda)
2075
+ responseText += ` \u2022 Status: DRAFT (requires approval in agenda)
1616
2076
  `;
1617
- responseText += ` \u2022 Duration: ${estimatedMinutes} minutes
2077
+ responseText += ` \u2022 Duration: ${estimatedMinutes} minutes
1618
2078
  `;
1619
- responseText += ` \u2022 Period: ${sessionStart.toLocaleString()} - ${completionTime.toLocaleString()}
2079
+ responseText += ` \u2022 Period: ${sessionStart.toLocaleString()} - ${completionTime.toLocaleString()}
1620
2080
 
1621
2081
  `;
1622
- }
1623
- responseText += `\u{1F4CB} **Context for Customer Response:**
2082
+ }
2083
+ responseText += `\u{1F4CB} **Context for Customer Response:**
1624
2084
  `;
1625
- responseText += ` \u2022 Use "get-completion-context" to retrieve full context
2085
+ responseText += ` \u2022 Use "get-completion-context" to retrieve full context
1626
2086
  `;
1627
- responseText += ` \u2022 Generate customer-friendly response based on completed work
2087
+ responseText += ` \u2022 Generate customer-friendly response based on completed work
1628
2088
  `;
1629
- responseText += ` \u2022 Focus on business value and customer benefits
2089
+ responseText += ` \u2022 Focus on business value and customer benefits
1630
2090
 
1631
2091
  `;
1632
- responseText += `\u{1F3AF} **Session archived successfully!**`;
1633
- return {
1634
- content: [{
1635
- type: "text",
1636
- text: responseText
1637
- }]
1638
- };
2092
+ responseText += `\u{1F3AF} **Session archived successfully!**`;
2093
+ return { content: [{ type: "text", text: responseText }] };
2094
+ }
2095
+ async function handleLogHours(input) {
2096
+ const ctx = authContext;
2097
+ const {
2098
+ projectId,
2099
+ ticketId,
2100
+ aiSessionId,
2101
+ workDescription,
2102
+ estimatedHours,
2103
+ chatContextSummary
2104
+ } = input;
2105
+ let project = null;
2106
+ let ticket = null;
2107
+ let aiSession = null;
2108
+ if (projectId) {
2109
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
2110
+ if (!projectIds.includes(projectId)) {
2111
+ throw new Error(
2112
+ `Project not found or no access: ${projectId}. Please call get-projects first to find the correct project.`
2113
+ );
2114
+ }
2115
+ const [projectData] = await db.select({
2116
+ id: schema.projects.id,
2117
+ name: schema.projects.name,
2118
+ teamId: schema.projects.teamId
2119
+ }).from(schema.projects).where(eq(schema.projects.id, projectId)).limit(1);
2120
+ if (!projectData) throw new Error(`Project not found: ${projectId}.`);
2121
+ project = projectData;
2122
+ }
2123
+ if (ticketId) {
2124
+ const [ticketData] = await db.select({
2125
+ id: schema.tickets.id,
2126
+ title: schema.tickets.title,
2127
+ status: schema.tickets.status,
2128
+ teamId: schema.tickets.teamId,
2129
+ projectId: schema.tickets.projectId,
2130
+ customerId: schema.tickets.customerId
2131
+ }).from(schema.tickets).where(eq(schema.tickets.id, ticketId)).limit(1);
2132
+ if (!ticketData) {
2133
+ throw new Error(
2134
+ `Ticket not found: ${ticketId}. Please call get-tickets first to find the correct ticket.`
2135
+ );
2136
+ }
2137
+ let hasAccess = false;
2138
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
2139
+ if (teamIds.includes(ticketData.teamId)) hasAccess = true;
2140
+ if (!hasAccess && ticketData.projectId) {
2141
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
2142
+ if (projectIds.includes(ticketData.projectId)) hasAccess = true;
2143
+ }
2144
+ if (!hasAccess && ticketData.customerId) {
2145
+ const customerIds = await getAccessibleCustomerIds(ctx.teamId);
2146
+ if (customerIds.includes(ticketData.customerId)) hasAccess = true;
2147
+ }
2148
+ if (!hasAccess) {
2149
+ throw new Error(
2150
+ `No access to ticket: ${ticketId}. Please call get-tickets first to find the correct ticket.`
2151
+ );
2152
+ }
2153
+ ticket = ticketData;
2154
+ }
2155
+ if (aiSessionId) {
2156
+ const [sessionData] = await db.select({
2157
+ id: schema.aiSessions.id,
2158
+ ticketId: schema.aiSessions.ticketId,
2159
+ status: schema.aiSessions.status
2160
+ }).from(schema.aiSessions).where(eq(schema.aiSessions.id, aiSessionId)).limit(1);
2161
+ if (!sessionData) throw new Error(`AI Session not found: ${aiSessionId}.`);
2162
+ aiSession = sessionData;
2163
+ }
2164
+ const durationSeconds = Math.round(estimatedHours * 3600);
2165
+ const now = /* @__PURE__ */ new Date();
2166
+ let agendaEntry = null;
2167
+ let wasUpdated = false;
2168
+ let consolidatedCount = 0;
2169
+ if (aiSession?.id || ticket?.id) {
2170
+ let existingEntries = [];
2171
+ if (aiSession?.id) {
2172
+ existingEntries = await db.select({
2173
+ id: schema.agendaEvents.id,
2174
+ trackedDuration: schema.agendaEvents.trackedDuration,
2175
+ projectId: schema.agendaEvents.projectId,
2176
+ aiSessionId: schema.agendaEvents.aiSessionId
2177
+ }).from(schema.agendaEvents).where(
2178
+ and(
2179
+ eq(schema.agendaEvents.status, "draft"),
2180
+ eq(schema.agendaEvents.userId, ctx.userId),
2181
+ eq(schema.agendaEvents.aiSessionId, aiSession.id)
2182
+ )
2183
+ ).orderBy(desc(schema.agendaEvents.createdAt));
2184
+ } else if (ticket?.id) {
2185
+ const linkedEvents = await db.select({
2186
+ agendaEventId: schema.agendaEventTickets.agendaEventId
2187
+ }).from(schema.agendaEventTickets).where(eq(schema.agendaEventTickets.ticketId, ticket.id));
2188
+ const eventIds = linkedEvents.map((e) => e.agendaEventId);
2189
+ if (eventIds.length > 0) {
2190
+ existingEntries = await db.select({
2191
+ id: schema.agendaEvents.id,
2192
+ trackedDuration: schema.agendaEvents.trackedDuration,
2193
+ projectId: schema.agendaEvents.projectId,
2194
+ aiSessionId: schema.agendaEvents.aiSessionId
2195
+ }).from(schema.agendaEvents).where(
2196
+ and(
2197
+ inArray(schema.agendaEvents.id, eventIds),
2198
+ eq(schema.agendaEvents.status, "draft"),
2199
+ eq(schema.agendaEvents.userId, ctx.userId)
2200
+ )
2201
+ ).orderBy(desc(schema.agendaEvents.createdAt));
1639
2202
  }
1640
- case "log-hours": {
1641
- const { projectId, ticketId, aiSessionId, workDescription, estimatedHours, chatContextSummary } = args2;
1642
- let project = null;
1643
- let ticket = null;
1644
- let aiSession = null;
1645
- if (projectId) {
1646
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
1647
- if (!projectIds.includes(projectId)) {
1648
- throw new Error(`Project not found or no access: ${projectId}. Please call get-projects first to find the correct project.`);
1649
- }
1650
- const { data: projectData, error: projectError } = await supabase.from("projects").select("id, name, team_id").eq("id", projectId).single();
1651
- if (projectError || !projectData) {
1652
- throw new Error(`Project not found: ${projectId}.`);
1653
- }
1654
- project = projectData;
1655
- }
1656
- if (ticketId) {
1657
- const { data: ticketData, error: ticketError } = await supabase.from("tickets").select("id, title, status, team_id, project_id, customer_id").eq("id", ticketId).single();
1658
- if (ticketError || !ticketData) {
1659
- throw new Error(`Ticket not found: ${ticketId}. Please call get-tickets first to find the correct ticket.`);
1660
- }
1661
- let hasAccess = false;
1662
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1663
- if (teamIds.includes(ticketData.team_id)) {
1664
- hasAccess = true;
1665
- }
1666
- if (!hasAccess && ticketData.project_id) {
1667
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
1668
- if (projectIds.includes(ticketData.project_id)) {
1669
- hasAccess = true;
1670
- }
1671
- }
1672
- if (!hasAccess && ticketData.customer_id) {
1673
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
1674
- if (customerIds.includes(ticketData.customer_id)) {
1675
- hasAccess = true;
1676
- }
1677
- }
1678
- if (!hasAccess) {
1679
- throw new Error(`No access to ticket: ${ticketId}. Please call get-tickets first to find the correct ticket.`);
1680
- }
1681
- ticket = ticketData;
1682
- }
1683
- if (aiSessionId) {
1684
- const { data: sessionData, error: sessionError } = await supabase.from("ai_dev_sessions").select("id, ticket_id, status").eq("id", aiSessionId).single();
1685
- if (sessionError || !sessionData) {
1686
- throw new Error(`AI Session not found: ${aiSessionId}.`);
1687
- }
1688
- aiSession = sessionData;
1689
- }
1690
- const durationSeconds = Math.round(estimatedHours * 3600);
1691
- const now = /* @__PURE__ */ new Date();
1692
- let agendaEntry = null;
1693
- let agendaError = null;
1694
- let wasUpdated = false;
1695
- let consolidatedCount = 0;
1696
- if (aiSession?.id || ticket?.id) {
1697
- let query = supabase.from("agenda_events").select("id, tracked_duration, project_id, ticket_id, ai_session_id").eq("status", "draft").eq("user_id", authContext.userId).order("created_at", { ascending: false });
1698
- if (aiSession?.id) {
1699
- query = query.eq("ai_session_id", aiSession.id);
1700
- } else if (ticket?.id) {
1701
- query = query.eq("ticket_id", ticket.id);
1702
- }
1703
- const { data: existingEntries } = await query;
1704
- if (existingEntries && existingEntries.length > 0) {
1705
- const existingEntry = existingEntries[0];
1706
- if (existingEntries.length > 1) {
1707
- const duplicateIds = existingEntries.slice(1).map((e) => e.id);
1708
- await supabase.from("agenda_events").delete().in("id", duplicateIds);
1709
- consolidatedCount = existingEntries.length - 1;
1710
- }
1711
- const newDuration = (existingEntry.tracked_duration || 0) + durationSeconds;
1712
- const { data: updated, error: updateError } = await supabase.from("agenda_events").update({
1713
- tracked_duration: newDuration,
1714
- end_time: now.toISOString(),
1715
- title: workDescription,
1716
- description: chatContextSummary || workDescription,
1717
- project_id: project?.id || existingEntry.project_id
1718
- }).eq("id", existingEntry.id).select("id, tracked_duration, project_id, ticket_id, ai_session_id").single();
1719
- agendaEntry = updated;
1720
- agendaError = updateError;
1721
- wasUpdated = true;
1722
- }
1723
- }
1724
- if (!agendaEntry) {
1725
- const startTime = new Date(now.getTime() - durationSeconds * 1e3);
1726
- const { data: created, error: createError } = await supabase.from("agenda_events").insert({
1727
- team_id: authContext.teamId,
1728
- user_id: authContext.userId,
1729
- project_id: project?.id || null,
1730
- ticket_id: ticket?.id || null,
1731
- ai_session_id: aiSession?.id || null,
1732
- title: workDescription,
1733
- description: chatContextSummary || workDescription,
1734
- start_time: startTime.toISOString(),
1735
- end_time: now.toISOString(),
1736
- type: "work",
1737
- status: "draft",
1738
- all_day: false,
1739
- is_tracked: true,
1740
- tracked_duration: durationSeconds
1741
- }).select("id, tracked_duration, project_id, ticket_id, ai_session_id").single();
1742
- agendaEntry = created;
1743
- agendaError = createError;
1744
- }
1745
- if (agendaError || !agendaEntry) {
1746
- throw new Error(`Failed to ${wasUpdated ? "update" : "create"} time entry: ${agendaError?.message || "Unknown error"}`);
1747
- }
1748
- let responseText = `\u23F1\uFE0F **Hours ${wasUpdated ? "Added to Existing Entry" : "Logged Successfully"}!**
2203
+ }
2204
+ if (existingEntries.length > 0) {
2205
+ const existingEntry = existingEntries[0];
2206
+ if (existingEntries.length > 1) {
2207
+ const duplicateIds = existingEntries.slice(1).map((e) => e.id);
2208
+ await db.delete(schema.agendaEvents).where(inArray(schema.agendaEvents.id, duplicateIds));
2209
+ consolidatedCount = existingEntries.length - 1;
2210
+ }
2211
+ const newDuration = (existingEntry.trackedDuration ?? 0) + durationSeconds;
2212
+ const [updated] = await db.update(schema.agendaEvents).set({
2213
+ trackedDuration: newDuration,
2214
+ endTime: now.toISOString(),
2215
+ title: workDescription,
2216
+ description: chatContextSummary ?? workDescription,
2217
+ projectId: project?.id ?? existingEntry.projectId
2218
+ }).where(eq(schema.agendaEvents.id, existingEntry.id)).returning({
2219
+ id: schema.agendaEvents.id,
2220
+ trackedDuration: schema.agendaEvents.trackedDuration,
2221
+ projectId: schema.agendaEvents.projectId,
2222
+ aiSessionId: schema.agendaEvents.aiSessionId
2223
+ });
2224
+ agendaEntry = updated ?? null;
2225
+ wasUpdated = true;
2226
+ }
2227
+ }
2228
+ if (!agendaEntry) {
2229
+ const startTime = new Date(now.getTime() - durationSeconds * 1e3);
2230
+ const [created] = await db.insert(schema.agendaEvents).values({
2231
+ teamId: ctx.teamId,
2232
+ userId: ctx.userId,
2233
+ projectId: project?.id ?? null,
2234
+ aiSessionId: aiSession?.id ?? null,
2235
+ title: workDescription,
2236
+ description: chatContextSummary ?? workDescription,
2237
+ startTime: startTime.toISOString(),
2238
+ endTime: now.toISOString(),
2239
+ type: "work",
2240
+ status: "draft",
2241
+ allDay: false,
2242
+ isTracked: true,
2243
+ trackedDuration: durationSeconds
2244
+ }).returning({
2245
+ id: schema.agendaEvents.id,
2246
+ trackedDuration: schema.agendaEvents.trackedDuration,
2247
+ projectId: schema.agendaEvents.projectId,
2248
+ aiSessionId: schema.agendaEvents.aiSessionId
2249
+ });
2250
+ agendaEntry = created ?? null;
2251
+ if (agendaEntry && ticket?.id) {
2252
+ await db.insert(schema.agendaEventTickets).values({ agendaEventId: agendaEntry.id, ticketId: ticket.id }).onConflictDoNothing();
2253
+ }
2254
+ }
2255
+ if (!agendaEntry) {
2256
+ throw new Error(
2257
+ `Failed to ${wasUpdated ? "update" : "create"} time entry`
2258
+ );
2259
+ }
2260
+ let responseText = `\u23F1\uFE0F **Hours ${wasUpdated ? "Added to Existing Entry" : "Logged Successfully"}!**
1749
2261
 
1750
2262
  `;
1751
- if (wasUpdated) {
1752
- responseText += `\u{1F504} **Updated existing draft entry** (avoiding duplicates)
2263
+ if (wasUpdated) {
2264
+ responseText += `\u{1F504} **Updated existing draft entry** (avoiding duplicates)
1753
2265
  `;
1754
- responseText += ` \u2022 New total: ${Math.round((agendaEntry.tracked_duration || 0) / 3600 * 10) / 10}h
2266
+ responseText += ` \u2022 New total: ${Math.round((agendaEntry.trackedDuration ?? 0) / 3600 * 10) / 10}h
1755
2267
 
1756
2268
  `;
1757
- }
1758
- if (consolidatedCount > 0) {
1759
- responseText += `\u{1F9F9} **Cleaned up ${consolidatedCount} duplicate entries**
2269
+ }
2270
+ if (consolidatedCount > 0) {
2271
+ responseText += `\u{1F9F9} **Cleaned up ${consolidatedCount} duplicate entries**
1760
2272
 
1761
2273
  `;
1762
- }
1763
- responseText += `\u{1F4CB} **Entry Details:**
2274
+ }
2275
+ responseText += `\u{1F4CB} **Entry Details:**
1764
2276
  `;
1765
- if (project) {
1766
- responseText += ` \u2022 Project: ${project.name}
2277
+ responseText += ` \u2022 Project: ${project ? project.name : "(No project assigned)"}
1767
2278
  `;
1768
- } else {
1769
- responseText += ` \u2022 Project: (No project assigned)
2279
+ if (ticket) responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
1770
2280
  `;
1771
- }
1772
- if (ticket) {
1773
- responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
2281
+ if (aiSession)
2282
+ responseText += ` \u2022 AI Session: ${aiSession.id} (${aiSession.status})
1774
2283
  `;
1775
- }
1776
- if (aiSession) {
1777
- responseText += ` \u2022 AI Session: ${aiSession.id} (${aiSession.status})
2284
+ responseText += ` \u2022 Description: ${workDescription}
1778
2285
  `;
1779
- }
1780
- responseText += ` \u2022 Description: ${workDescription}
1781
- `;
1782
- responseText += ` \u2022 ${wasUpdated ? "Added" : "Estimated"} Hours: ${estimatedHours}h (${Math.floor(estimatedHours)}h ${Math.round(estimatedHours % 1 * 60)}m)
2286
+ responseText += ` \u2022 ${wasUpdated ? "Added" : "Estimated"} Hours: ${estimatedHours}h (${Math.floor(estimatedHours)}h ${Math.round(estimatedHours % 1 * 60)}m)
1783
2287
  `;
1784
- responseText += ` \u2022 Status: DRAFT (not billed yet)
2288
+ responseText += ` \u2022 Status: DRAFT (not billed yet)
1785
2289
  `;
1786
- responseText += ` \u2022 Entry ID: ${agendaEntry.id}
2290
+ responseText += ` \u2022 Entry ID: ${agendaEntry.id}
1787
2291
 
1788
2292
  `;
1789
- if (chatContextSummary) {
1790
- responseText += `\u{1F4CA} **Work Context:**
2293
+ if (chatContextSummary) {
2294
+ responseText += `\u{1F4CA} **Work Context:**
1791
2295
  `;
1792
- responseText += `${chatContextSummary.substring(0, 200)}${chatContextSummary.length > 200 ? "..." : ""}
2296
+ responseText += `${chatContextSummary.substring(0, 200)}${chatContextSummary.length > 200 ? "..." : ""}
1793
2297
 
1794
2298
  `;
1795
- }
1796
- responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
1797
- return {
1798
- content: [{
2299
+ }
2300
+ responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
2301
+ return { content: [{ type: "text", text: responseText }] };
2302
+ }
2303
+ async function handleGetGithubFile(input) {
2304
+ const ctx = authContext;
2305
+ const { projectId, filePath, ref } = input;
2306
+ const githubInfo = await getGithubTokenForProject(projectId, ctx.teamId);
2307
+ if (!githubInfo) {
2308
+ return {
2309
+ content: [
2310
+ { type: "text", text: "\u274C GitHub not configured for this project." }
2311
+ ]
2312
+ };
2313
+ }
2314
+ try {
2315
+ const octokit = new Octokit({ auth: githubInfo.token });
2316
+ console.error(
2317
+ `\u{1F4C4} Reading file: ${filePath} from ${githubInfo.repositoryFullName}`
2318
+ );
2319
+ const { data } = await octokit.rest.repos.getContent({
2320
+ owner: githubInfo.owner,
2321
+ repo: githubInfo.repo,
2322
+ path: filePath,
2323
+ ref
2324
+ });
2325
+ if (Array.isArray(data) || data.type !== "file") {
2326
+ return {
2327
+ content: [
2328
+ {
1799
2329
  type: "text",
1800
- text: responseText
1801
- }]
1802
- };
1803
- }
1804
- // === GITHUB TOOLS ===
1805
- case "get-github-file": {
1806
- const { projectId, filePath, ref } = args2;
1807
- const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1808
- if (!githubInfo) {
1809
- return {
1810
- content: [{
1811
- type: "text",
1812
- text: "\u274C GitHub not configured for this project."
1813
- }]
1814
- };
1815
- }
1816
- try {
1817
- const octokit = new Octokit({ auth: githubInfo.token });
1818
- console.error(`\u{1F4C4} Reading file: ${filePath} from ${githubInfo.repositoryFullName}`);
1819
- const { data } = await octokit.rest.repos.getContent({
1820
- owner: githubInfo.owner,
1821
- repo: githubInfo.repo,
1822
- path: filePath,
1823
- ref
1824
- });
1825
- if (Array.isArray(data) || data.type !== "file") {
1826
- return {
1827
- content: [{
1828
- type: "text",
1829
- text: `\u274C "${filePath}" is not a file or contains multiple items.`
1830
- }]
1831
- };
2330
+ text: `\u274C "${filePath}" is not a file or contains multiple items.`
1832
2331
  }
1833
- const content = Buffer.from(data.content, "base64").toString("utf-8");
1834
- let responseText = `\u{1F4C4} **File: ${filePath}**
2332
+ ]
2333
+ };
2334
+ }
2335
+ const content = Buffer.from(data.content, "base64").toString("utf-8");
2336
+ let responseText = `\u{1F4C4} **File: ${filePath}**
1835
2337
  `;
1836
- responseText += `Repository: ${githubInfo.repositoryFullName}
2338
+ responseText += `Repository: ${githubInfo.repositoryFullName}
1837
2339
  `;
1838
- responseText += `Size: ${data.size} bytes
2340
+ responseText += `Size: ${data.size} bytes
1839
2341
  `;
1840
- responseText += `URL: ${data.html_url}
2342
+ responseText += `URL: ${data.html_url}
1841
2343
 
1842
2344
  `;
1843
- responseText += `**Content:**
2345
+ responseText += `**Content:**
1844
2346
  \`\`\`
1845
2347
  ${content}
1846
2348
  \`\`\``;
1847
- return {
1848
- content: [{
1849
- type: "text",
1850
- text: responseText
1851
- }]
1852
- };
1853
- } catch (error) {
1854
- console.error("GitHub get file error:", error);
1855
- if (error.status === 404) {
1856
- return {
1857
- content: [{
1858
- type: "text",
1859
- text: `\u274C File not found: ${filePath}`
1860
- }]
1861
- };
1862
- }
1863
- return {
1864
- content: [{
1865
- type: "text",
1866
- text: `\u274C Failed to read file: ${error.message || "Unknown error"}`
1867
- }]
1868
- };
1869
- }
1870
- }
1871
- case "list-github-directory": {
1872
- const { projectId, directoryPath, ref } = args2;
1873
- const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1874
- if (!githubInfo) {
1875
- return {
1876
- content: [{
1877
- type: "text",
1878
- text: "\u274C GitHub not configured for this project."
1879
- }]
1880
- };
1881
- }
1882
- try {
1883
- const octokit = new Octokit({ auth: githubInfo.token });
1884
- const normalizedPath = !directoryPath || directoryPath === "/" ? "" : directoryPath;
1885
- console.error(`\u{1F4C1} Listing directory: ${normalizedPath || "(root)"} in ${githubInfo.repositoryFullName}`);
1886
- const { data } = await octokit.rest.repos.getContent({
1887
- owner: githubInfo.owner,
1888
- repo: githubInfo.repo,
1889
- path: normalizedPath,
1890
- ref
1891
- });
1892
- if (!Array.isArray(data)) {
1893
- return {
1894
- content: [{
1895
- type: "text",
1896
- text: `\u274C "${directoryPath}" is not a directory.`
1897
- }]
1898
- };
2349
+ return { content: [{ type: "text", text: responseText }] };
2350
+ } catch (error) {
2351
+ console.error("GitHub get file error:", error);
2352
+ const status = error?.status;
2353
+ if (status === 404) {
2354
+ return {
2355
+ content: [{ type: "text", text: `\u274C File not found: ${filePath}` }]
2356
+ };
2357
+ }
2358
+ const message = error instanceof Error ? error.message : "Unknown error";
2359
+ return {
2360
+ content: [
2361
+ { type: "text", text: `\u274C Failed to read file: ${message}` }
2362
+ ]
2363
+ };
2364
+ }
2365
+ }
2366
+ async function handleListGithubDirectory(input) {
2367
+ const ctx = authContext;
2368
+ const { projectId, directoryPath, ref } = input;
2369
+ const githubInfo = await getGithubTokenForProject(projectId, ctx.teamId);
2370
+ if (!githubInfo) {
2371
+ return {
2372
+ content: [
2373
+ { type: "text", text: "\u274C GitHub not configured for this project." }
2374
+ ]
2375
+ };
2376
+ }
2377
+ try {
2378
+ const octokit = new Octokit({ auth: githubInfo.token });
2379
+ const normalizedPath = !directoryPath || directoryPath === "/" ? "" : directoryPath;
2380
+ console.error(
2381
+ `\u{1F4C1} Listing directory: ${normalizedPath || "(root)"} in ${githubInfo.repositoryFullName}`
2382
+ );
2383
+ const { data } = await octokit.rest.repos.getContent({
2384
+ owner: githubInfo.owner,
2385
+ repo: githubInfo.repo,
2386
+ path: normalizedPath,
2387
+ ref
2388
+ });
2389
+ if (!Array.isArray(data)) {
2390
+ return {
2391
+ content: [
2392
+ {
2393
+ type: "text",
2394
+ text: `\u274C "${directoryPath}" is not a directory.`
1899
2395
  }
1900
- let responseText = `\u{1F4C1} **Directory: ${directoryPath || "(root)"}**
2396
+ ]
2397
+ };
2398
+ }
2399
+ let responseText = `\u{1F4C1} **Directory: ${directoryPath || "(root)"}**
1901
2400
  `;
1902
- responseText += `Repository: ${githubInfo.repositoryFullName}
2401
+ responseText += `Repository: ${githubInfo.repositoryFullName}
1903
2402
  `;
1904
- responseText += `Items: ${data.length}
2403
+ responseText += `Items: ${data.length}
1905
2404
 
1906
2405
  `;
1907
- const directories = data.filter((item) => item.type === "dir");
1908
- const files = data.filter((item) => item.type === "file");
1909
- if (directories.length > 0) {
1910
- responseText += `**\u{1F4C1} Directories (${directories.length}):**
2406
+ const directories = data.filter((item) => item.type === "dir");
2407
+ const files = data.filter((item) => item.type === "file");
2408
+ if (directories.length > 0) {
2409
+ responseText += `**\u{1F4C1} Directories (${directories.length}):**
1911
2410
  `;
1912
- for (const dir of directories) {
1913
- responseText += ` - ${dir.name}/
2411
+ for (const dir of directories) responseText += ` - ${dir.name}/
1914
2412
  `;
1915
- }
1916
- responseText += `
2413
+ responseText += `
1917
2414
  `;
1918
- }
1919
- if (files.length > 0) {
1920
- responseText += `**\u{1F4C4} Files (${files.length}):**
2415
+ }
2416
+ if (files.length > 0) {
2417
+ responseText += `**\u{1F4C4} Files (${files.length}):**
1921
2418
  `;
1922
- for (const file of files) {
1923
- responseText += ` - ${file.name} (${file.size} bytes)
2419
+ for (const file of files)
2420
+ responseText += ` - ${file.name} (${file.size} bytes)
1924
2421
  `;
1925
- }
1926
- }
1927
- return {
1928
- content: [{
1929
- type: "text",
1930
- text: responseText
1931
- }]
1932
- };
1933
- } catch (error) {
1934
- console.error("GitHub list directory error:", error);
1935
- if (error.status === 404) {
1936
- return {
1937
- content: [{
1938
- type: "text",
1939
- text: `\u274C Directory not found: ${directoryPath}`
1940
- }]
1941
- };
1942
- }
1943
- return {
1944
- content: [{
1945
- type: "text",
1946
- text: `\u274C Failed to list directory: ${error.message || "Unknown error"}`
1947
- }]
1948
- };
1949
- }
1950
- }
1951
- default:
1952
- throw new Error(`Unknown tool: ${name}`);
1953
2422
  }
2423
+ return { content: [{ type: "text", text: responseText }] };
1954
2424
  } catch (error) {
1955
- console.error(`\u274C Tool execution error:`, error);
1956
- const message = error instanceof Error ? error.message : error?.message || error?.details || (typeof error === "string" ? error : JSON.stringify(error));
2425
+ console.error("GitHub list directory error:", error);
2426
+ const status = error?.status;
2427
+ if (status === 404) {
2428
+ return {
2429
+ content: [
2430
+ { type: "text", text: `\u274C Directory not found: ${directoryPath}` }
2431
+ ]
2432
+ };
2433
+ }
2434
+ const message = error instanceof Error ? error.message : "Unknown error";
1957
2435
  return {
1958
- content: [{
1959
- type: "text",
1960
- text: `Error executing ${name}: ${message}`
1961
- }]
2436
+ content: [
2437
+ { type: "text", text: `\u274C Failed to list directory: ${message}` }
2438
+ ]
1962
2439
  };
1963
2440
  }
1964
- });
2441
+ }
1965
2442
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1966
2443
  if (!authContext) {
1967
2444
  return {
1968
- contents: [{
1969
- uri: request.params.uri,
1970
- mimeType: "text/plain",
1971
- text: "Error: Not authenticated. API key validation failed."
1972
- }]
2445
+ contents: [
2446
+ {
2447
+ uri: request.params.uri,
2448
+ mimeType: "text/plain",
2449
+ text: "Error: Not authenticated. API key validation failed."
2450
+ }
2451
+ ]
1973
2452
  };
1974
2453
  }
2454
+ const ctx = authContext;
1975
2455
  const { uri } = request.params;
1976
2456
  console.error(`\u{1F4DA} Reading resource: ${uri}`);
1977
2457
  try {
1978
2458
  switch (uri) {
1979
2459
  case "tickets://recent": {
1980
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1981
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
1982
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
1983
- let teamFilter = `team_id.in.(${teamIds.join(",")})`;
1984
- let projectFilter = projectIds.length > 0 ? `project_id.in.(${projectIds.join(",")})` : null;
1985
- let customerFilter = customerIds.length > 0 ? `customer_id.in.(${customerIds.join(",")})` : null;
1986
- const filters = [teamFilter, projectFilter, customerFilter].filter(Boolean);
1987
- const { data, error } = await supabase.from("tickets").select(`
1988
- id,
1989
- ticket_number,
1990
- title,
1991
- status,
1992
- priority,
1993
- created_at
1994
- `).or(filters.join(",")).order("created_at", { ascending: false }).limit(20);
1995
- if (error) throw error;
2460
+ const teamIds = await getAccessibleTeamIds(ctx.teamId);
2461
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
2462
+ const customerIds = await getAccessibleCustomerIds(ctx.teamId);
2463
+ const accessPredicate = buildTicketAccessPredicate(
2464
+ teamIds,
2465
+ projectIds,
2466
+ customerIds
2467
+ );
2468
+ const rows = await db.select({
2469
+ id: schema.tickets.id,
2470
+ ticketNumber: schema.tickets.ticketNumber,
2471
+ title: schema.tickets.title,
2472
+ status: schema.tickets.status,
2473
+ priority: schema.tickets.priority,
2474
+ createdAt: schema.tickets.createdAt
2475
+ }).from(schema.tickets).where(and(accessPredicate, eq(schema.tickets.isDeleted, false))).orderBy(desc(schema.tickets.createdAt)).limit(20);
1996
2476
  return {
1997
- contents: [{
1998
- uri,
1999
- mimeType: "application/json",
2000
- text: JSON.stringify(data, null, 2)
2001
- }]
2477
+ contents: [
2478
+ {
2479
+ uri,
2480
+ mimeType: "application/json",
2481
+ text: JSON.stringify(rows, null, 2)
2482
+ }
2483
+ ]
2002
2484
  };
2003
2485
  }
2004
2486
  case "customers://all": {
2005
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
2487
+ const customerIds = await getAccessibleCustomerIds(ctx.teamId);
2006
2488
  if (customerIds.length === 0) {
2007
2489
  return {
2008
- contents: [{
2009
- uri,
2010
- mimeType: "application/json",
2011
- text: JSON.stringify([], null, 2)
2012
- }]
2490
+ contents: [
2491
+ { uri, mimeType: "application/json", text: JSON.stringify([], null, 2) }
2492
+ ]
2013
2493
  };
2014
2494
  }
2015
- const { data, error } = await supabase.from("customers").select("id, name, email, website, created_at").in("id", customerIds).order("name").limit(50);
2016
- if (error) throw error;
2495
+ const rows = await db.select({
2496
+ id: schema.customers.id,
2497
+ name: schema.customers.name,
2498
+ email: schema.customers.email,
2499
+ website: schema.customers.website,
2500
+ createdAt: schema.customers.createdAt
2501
+ }).from(schema.customers).where(inArray(schema.customers.id, customerIds)).orderBy(asc(schema.customers.name)).limit(50);
2017
2502
  return {
2018
- contents: [{
2019
- uri,
2020
- mimeType: "application/json",
2021
- text: JSON.stringify(data, null, 2)
2022
- }]
2503
+ contents: [
2504
+ { uri, mimeType: "application/json", text: JSON.stringify(rows, null, 2) }
2505
+ ]
2023
2506
  };
2024
2507
  }
2025
2508
  case "projects://active": {
2026
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
2509
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
2027
2510
  if (projectIds.length === 0) {
2028
2511
  return {
2029
- contents: [{
2030
- uri,
2031
- mimeType: "application/json",
2032
- text: JSON.stringify([], null, 2)
2033
- }]
2512
+ contents: [
2513
+ { uri, mimeType: "application/json", text: JSON.stringify([], null, 2) }
2514
+ ]
2034
2515
  };
2035
2516
  }
2036
- const { data, error } = await supabase.from("projects").select(`
2037
- id,
2038
- name,
2039
- description,
2040
- status,
2041
- created_at,
2042
- customers:customer_id(id, name)
2043
- `).in("id", projectIds).eq("status", "active").order("name").limit(50);
2044
- if (error) throw error;
2517
+ const rows = await db.select({
2518
+ id: schema.projects.id,
2519
+ name: schema.projects.name,
2520
+ description: schema.projects.description,
2521
+ createdAt: schema.projects.createdAt,
2522
+ customerId: schema.projects.customerId,
2523
+ customerName: schema.customers.name
2524
+ }).from(schema.projects).leftJoin(
2525
+ schema.customers,
2526
+ eq(schema.customers.id, schema.projects.customerId)
2527
+ ).where(inArray(schema.projects.id, projectIds)).orderBy(asc(schema.projects.name)).limit(50);
2045
2528
  return {
2046
- contents: [{
2047
- uri,
2048
- mimeType: "application/json",
2049
- text: JSON.stringify(data, null, 2)
2050
- }]
2529
+ contents: [
2530
+ { uri, mimeType: "application/json", text: JSON.stringify(rows, null, 2) }
2531
+ ]
2051
2532
  };
2052
2533
  }
2053
2534
  default:
2054
2535
  throw new Error(`Unknown resource: ${uri}`);
2055
2536
  }
2056
2537
  } catch (error) {
2057
- console.error(`\u274C Resource read error:`, error);
2538
+ console.error("\u274C Resource read error:", error);
2058
2539
  return {
2059
- contents: [{
2060
- uri,
2061
- mimeType: "text/plain",
2062
- text: `Error reading ${uri}: ${error instanceof Error ? error.message : "Unknown error"}`
2063
- }]
2540
+ contents: [
2541
+ {
2542
+ uri,
2543
+ mimeType: "text/plain",
2544
+ text: `Error reading ${uri}: ${error instanceof Error ? error.message : "Unknown error"}`
2545
+ }
2546
+ ]
2064
2547
  };
2065
2548
  }
2066
2549
  });
2067
2550
  async function main() {
2068
- console.error("\u{1F680} Starting MG Tickets MCP Bridge Server...");
2551
+ console.error("\u{1F680} Starting Refront MCP Bridge Server...");
2069
2552
  console.error(`\u{1F511} API Key: ${apiKey?.substring(0, 10)}...`);
2070
2553
  authContext = await validateApiKey(apiKey);
2071
2554
  if (!authContext) {
2072
- console.error("\u274C API key validation failed. Please check your key and try again.");
2555
+ console.error(
2556
+ "\u274C API key validation failed. Please check your key and try again."
2557
+ );
2073
2558
  process.exit(1);
2074
2559
  }
2075
- console.error(`\u2705 Authenticated as user ${authContext.userId} in team ${authContext.teamId}`);
2560
+ console.error(
2561
+ `\u2705 Authenticated as user ${authContext.userId} in team ${authContext.teamId}`
2562
+ );
2076
2563
  console.error(`\u{1F4CB} Available scopes: ${authContext.scopes.join(", ")}`);
2077
2564
  console.error("\u{1F4E1} MCP Bridge Server ready for connections");
2078
2565
  const transport = new StdioServerTransport();