@mgsoftwarebv/mcp-server-bridge 3.0.0 → 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';
8
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
+ });
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
+ );
116
+ process.exit(1);
117
+ }
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
+ );
15
122
  process.exit(1);
16
123
  }
17
- var supabase = createClient(supabaseUrl, supabaseKey);
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,267 +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
- const { data: ticketData, error } = await supabase.from("tickets").select(`
663
- *,
664
- projects:project_id(id, name),
665
- customers:customer_id(id, name),
666
- assignee:assignee_id(id, full_name, email),
667
- requester:requester_id(id, full_name, email)
668
- `).eq("id", id).single();
669
- if (error) {
670
- if (error.code === "PGRST116") {
671
- throw new Error(`Ticket not found: ${id}`);
672
- }
673
- throw new Error(error.message || JSON.stringify(error));
674
- }
675
- let hasAccess = false;
676
- if (teamIds.includes(ticketData.team_id)) hasAccess = true;
677
- if (!hasAccess && ticketData.project_id && projectIds.includes(ticketData.project_id)) hasAccess = true;
678
- if (!hasAccess && ticketData.customer_id && customerIds.includes(ticketData.customer_id)) hasAccess = true;
679
- if (!hasAccess) {
680
- throw new Error("Access denied: You do not have permission to view this ticket");
681
- }
682
- const data = ticketData;
683
- const { data: attachments, error: attachmentsError } = await supabase.from("ticket_attachments").select(`
684
- id,
685
- file_name,
686
- file_size,
687
- mime_type,
688
- storage_key,
689
- created_at,
690
- users:user_id(id, full_name)
691
- `).eq("ticket_id", id).order("created_at", { ascending: true });
692
- if (attachmentsError) {
693
- console.error("Error fetching attachments:", attachmentsError);
694
- }
695
- const { data: comments, error: commentsError } = await supabase.from("ticket_comments").select(`
696
- id,
697
- content,
698
- created_at,
699
- user_id
700
- `).eq("ticket_id", id).order("created_at", { ascending: true });
701
- if (commentsError) {
702
- console.error("Error fetching comments:", commentsError);
703
- }
704
- const commentUserIds = [...new Set(comments?.map((c) => c.user_id).filter(Boolean) || [])];
705
- const commentUserMap = /* @__PURE__ */ new Map();
706
- if (commentUserIds.length > 0) {
707
- const { data: commentUsers } = await supabase.from("users").select("id, full_name").in("id", commentUserIds);
708
- commentUsers?.forEach((u) => commentUserMap.set(u.id, u));
709
- }
710
- const commentIds = comments?.map((c) => c.id) || [];
711
- let commentAttachments = [];
712
- if (commentIds.length > 0) {
713
- const { data: commAttachments, error: commAttachmentsError } = await supabase.from("ticket_comment_attachments").select(`
714
- id,
715
- comment_id,
716
- file_name,
717
- file_size,
718
- mime_type,
719
- storage_key,
720
- created_at
721
- `).in("comment_id", commentIds);
722
- if (commAttachmentsError) {
723
- console.error("Error fetching comment attachments:", commAttachmentsError);
724
- } else {
725
- commentAttachments = commAttachments || [];
726
- }
727
- }
728
- const content = [{
729
- type: "text",
730
- 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:**
731
1013
 
732
- **${data.ticket_number}**: ${data.title}
733
- Status: ${data.status}
734
- Priority: ${data.priority}
735
- Type: ${data.type}
736
- ${data.description ? `Description: ${data.description}
737
- ` : ""}${data.projects?.name ? `Project: ${data.projects.name}
738
- ` : ""}${data.customers?.name ? `Customer: ${data.customers.name}
739
- ` : ""}${data.assignee?.full_name ? `Assignee: ${data.assignee.full_name}
740
- ` : ""}Requester: ${data.requester?.full_name || "Unknown"}
741
- Created: ${new Date(data.created_at).toLocaleDateString()}
742
- ${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 ? `
743
1025
  \u{1F4CE} Attachments: ${attachments.length}
744
- ` : ""}${comments && comments.length > 0 ? `\u{1F4AC} Comments: ${comments.length}
1026
+ ` : ""}${comments.length > 0 ? `\u{1F4AC} Comments: ${comments.length}
745
1027
  ` : ""}`
746
- }];
747
- if (attachments && attachments.length > 0) {
748
- console.error(`\u{1F4CE} Processing ${attachments.length} ticket attachments...`);
749
- for (const attachment of attachments) {
750
- if (isImageFile(attachment.mime_type)) {
751
- console.error(`\u{1F5BC}\uFE0F Downloading image: ${attachment.file_name}`);
752
- const base64Data = await downloadImageAsBase64(attachment.storage_key);
753
- if (base64Data) {
754
- content.push({
755
- type: "image",
756
- data: base64Data,
757
- mimeType: attachment.mime_type
758
- });
759
- content.push({
760
- type: "text",
761
- text: `
762
- \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()})
763
1050
  `
764
- });
765
- }
766
- }
767
- }
1051
+ });
768
1052
  }
769
- if (commentAttachments.length > 0) {
770
- console.error(`\u{1F4CE} Processing ${commentAttachments.length} comment attachments...`);
771
- for (const attachment of commentAttachments) {
772
- if (isImageFile(attachment.mime_type)) {
773
- console.error(`\u{1F5BC}\uFE0F Downloading comment image: ${attachment.file_name}`);
774
- const base64Data = await downloadImageAsBase64(attachment.storage_key);
775
- if (base64Data) {
776
- const comment = comments?.find((c) => c.id === attachment.comment_id);
777
- content.push({
778
- type: "image",
779
- data: base64Data,
780
- mimeType: attachment.mime_type
781
- });
782
- content.push({
783
- type: "text",
784
- text: `
785
- \u{1F4F8} **Image from comment** by ${commentUserMap.get(comment?.user_id)?.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)
786
1082
  ` + (comment?.content ? `Comment text: "${comment.content.substring(0, 100)}${comment.content.length > 100 ? "..." : ""}"
787
1083
  ` : "")
788
- });
789
- }
790
- }
791
- }
1084
+ });
792
1085
  }
793
- console.error(`\u2705 Returning ticket with ${content.filter((c) => c.type === "image").length} images`);
794
- return { content };
795
1086
  }
796
- case "create-ticket": {
797
- const { title, description, status = "open", priority = "medium", type = "task", projectId, customerId } = args2;
798
- const year = (/* @__PURE__ */ new Date()).getFullYear();
799
- let resolvedTeamId = authContext.teamId;
800
- let resolvedCustomerId = customerId;
801
- let projectAbbreviation = "";
802
- if (projectId) {
803
- const { data: project } = await supabase.from("projects").select("name, team_id, customer_id").eq("id", projectId).single();
804
- if (project) {
805
- if (project.team_id) {
806
- resolvedTeamId = project.team_id;
807
- }
808
- if (!resolvedCustomerId && project.customer_id) {
809
- resolvedCustomerId = project.customer_id;
810
- }
811
- if (project.name) {
812
- const name2 = project.name.toUpperCase().replace(/[^A-Z0-9\s]/g, "");
813
- const words = name2.split(/\s+/).filter(Boolean);
814
- if (words.length >= 2) {
815
- projectAbbreviation = words.slice(0, 2).map((w) => w.substring(0, 3)).join("").substring(0, 5);
816
- } else if (words.length === 1 && words[0]) {
817
- projectAbbreviation = words[0].substring(0, 5);
818
- }
819
- }
820
- }
821
- }
822
- let ticketNumber;
823
- if (projectId && projectAbbreviation) {
824
- const pattern = `${year}-${projectAbbreviation}-%`;
825
- 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);
826
- let nextSequence = 1;
827
- if (existingTickets && existingTickets.length > 0 && existingTickets[0]?.ticket_number) {
828
- const parts = existingTickets[0].ticket_number.split("-");
829
- if (parts.length === 3) {
830
- const lastSeq = parseInt(parts[2], 10);
831
- if (!isNaN(lastSeq)) nextSequence = lastSeq + 1;
832
- }
833
- }
834
- ticketNumber = `${year}-${projectAbbreviation}-${String(nextSequence).padStart(3, "0")}`;
835
- } else {
836
- const { count } = await supabase.from("tickets").select("*", { count: "exact", head: true }).eq("team_id", resolvedTeamId);
837
- 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);
838
1127
  }
839
- const { data, error } = await supabase.from("tickets").insert({
840
- team_id: resolvedTeamId,
841
- ticket_number: ticketNumber,
842
- title,
843
- description,
844
- status,
845
- priority,
846
- type,
847
- project_id: projectId || null,
848
- customer_id: resolvedCustomerId || null,
849
- requester_id: authContext.userId
850
- }).select().single();
851
- if (error) throw error;
852
- return {
853
- content: [{
854
- type: "text",
855
- 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!**
856
1171
 
857
1172
  Ticket Number: **${ticketNumber}**
858
1173
  Title: ${title}
@@ -860,143 +1175,179 @@ Status: ${status}
860
1175
  Priority: ${priority}
861
1176
  Type: ${type}
862
1177
  `
863
- }]
864
- };
865
1178
  }
866
- case "get-customers": {
867
- const { q, pageSize = 20 } = args2;
868
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
869
- if (customerIds.length === 0) {
870
- return {
871
- content: [{
872
- type: "text",
873
- text: "No customers found or no access to any customers."
874
- }]
875
- };
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."
876
1192
  }
877
- let query = supabase.from("customers").select("id, name, email, website, created_at").in("id", customerIds).limit(Math.min(pageSize, 100));
878
- if (q) query = query.or(`name.ilike.%${q}%,email.ilike.%${q}%`);
879
- const { data, error } = await query.order("name");
880
- if (error) throw error;
881
- return {
882
- content: [{
883
- type: "text",
884
- 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:
885
1218
 
886
- ${data?.map(
887
- (customer) => `**${customer.name}**
888
- ${customer.email ? `Email: ${customer.email}
889
- ` : ""}${customer.website ? `Website: ${customer.website}
890
- ` : ""}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()}
891
1224
  `
892
- ).join("\n") || "No customers found."}`
893
- }]
894
- };
1225
+ ).join("\n") || "No customers found."}`
895
1226
  }
896
- case "create-customer": {
897
- const { name: name2, email, website } = args2;
898
- const { data, error } = await supabase.from("customers").insert({
899
- team_id: authContext.teamId,
900
- name: name2,
901
- email: email || null,
902
- website: website || null,
903
- user_id: authContext.userId
904
- }).select().single();
905
- if (error) throw error;
906
- return {
907
- content: [{
908
- type: "text",
909
- 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!**
910
1244
 
911
- Name: ${name2}
1245
+ Name: ${name}
912
1246
  ${email ? `Email: ${email}
913
1247
  ` : ""}${website ? `Website: ${website}
914
1248
  ` : ""}`
915
- }]
916
- };
917
1249
  }
918
- case "get-projects": {
919
- const { customerId, q, pageSize = 20 } = args2;
920
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
921
- if (projectIds.length === 0) {
922
- return {
923
- content: [{
924
- type: "text",
925
- text: "No projects found or no access to any projects."
926
- }]
927
- };
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."
928
1263
  }
929
- let query = supabase.from("projects").select(`
930
- id,
931
- name,
932
- description,
933
- customer_id,
934
- created_at
935
- `).in("id", projectIds).limit(Math.min(pageSize, 100));
936
- if (customerId) query = query.eq("customer_id", customerId);
937
- if (q) query = query.ilike("name", `%${q}%`);
938
- const { data, error } = await query.order("name");
939
- if (error) throw error;
940
- return {
941
- content: [{
942
- type: "text",
943
- 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:
944
1282
 
945
- ${data?.map(
946
- (project) => `**${project.name}** (ID: ${project.id})
947
- ${project.description ? `Description: ${project.description}
948
- ` : ""}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()}
949
1287
  `
950
- ).join("\n") || "No projects found."}`
951
- }]
952
- };
1288
+ ).join("\n") || "No projects found."}`
953
1289
  }
954
- case "create-project": {
955
- const { name: name2, description, customerId, status = "active" } = args2;
956
- const { data, error } = await supabase.from("projects").insert({
957
- team_id: authContext.teamId,
958
- name: name2,
959
- description: description || null,
960
- customer_id: customerId || null,
961
- status,
962
- user_id: authContext.userId
963
- }).select().single();
964
- if (error) throw error;
965
- return {
966
- content: [{
967
- type: "text",
968
- 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!**
969
1307
 
970
- Name: ${name2}
971
- Status: ${status}
1308
+ Name: ${name}
972
1309
  ${description ? `Description: ${description}
973
1310
  ` : ""}`
974
- }]
975
- };
976
1311
  }
977
- // === AI SESSION TOOLS ===
978
- case "start-ai-session-smart": {
979
- const { ticketId, ticketUrl, cursorSessionId, totalEstimatedMinutes, complexityScore } = args2;
980
- if (!totalEstimatedMinutes) {
981
- throw new Error("totalEstimatedMinutes is required");
982
- }
983
- const roundedMinutes = roundToNearest15Minutes(totalEstimatedMinutes);
984
- const sessionStartTime = /* @__PURE__ */ new Date();
985
- const { data: sessionData, error } = await supabase.from("ai_sessions").insert({
986
- ticket_id: ticketId,
987
- provider_user_id: authContext.userId,
988
- team_id: authContext.teamId,
989
- cursor_session_id: cursorSessionId || null,
990
- ai_time_estimate_minutes: roundedMinutes,
991
- complexity_score: complexityScore || null,
992
- status: "in_progress"
993
- }).select("id, ticket_id, cursor_session_id, created_at").single();
994
- if (error) throw error;
995
- const sessionId = `ai-sess-${sessionData.id.substring(0, 8)}`;
996
- return {
997
- content: [{
998
- type: "text",
999
- 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!**
1000
1351
 
1001
1352
  \u{1F194} Session ID: **${sessionId}**
1002
1353
  \u{1F3AB} Ticket: ${ticketId}
@@ -1005,115 +1356,143 @@ ${complexityScore ? `\u{1F3AF} Complexity: ${complexityScore}/10
1005
1356
  ` : ""}\u{1F4C5} Started: ${sessionStartTime.toLocaleString()}
1006
1357
 
1007
1358
  \u{1F4DD} Timetrack entry will be created when you complete the session.`
1008
- }]
1009
- };
1010
1359
  }
1011
- case "track-manual-follow-up": {
1012
- const { aiSessionId, originalPrompt, aiResponse, developerFollowUp, followUpReason, outcome = "success", estimatedMinutes, workDescription } = args2;
1013
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1014
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1015
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id, status, created_at, ai_time_estimate_minutes").in("team_id", teamIds);
1016
- if (sessionError) {
1017
- throw new Error(`Database error: ${sessionError.message}`);
1018
- }
1019
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1020
- if (!session) {
1021
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1022
- }
1023
- const followUpTime = /* @__PURE__ */ new Date();
1024
- const oldEstimate = session.ai_time_estimate_minutes || 60;
1025
- const roundedFollowUpMinutes = roundToNearest15Minutes(estimatedMinutes || 0);
1026
- const newEstimate = oldEstimate + roundedFollowUpMinutes;
1027
- await supabase.from("ai_sessions").update({
1028
- status: "in_progress",
1029
- // Restart active tracking
1030
- ai_time_estimate_minutes: newEstimate
1031
- // Increase estimate based on follow-up work
1032
- // Don't update completed_at - session continues
1033
- }).eq("id", session.id);
1034
- const { data, error } = await supabase.from("manual_follow_ups").insert({
1035
- ai_session_id: session.id,
1036
- developer_id: authContext.userId,
1037
- team_id: authContext.teamId,
1038
- original_prompt: originalPrompt,
1039
- ai_response: aiResponse,
1040
- follow_up_prompt: developerFollowUp,
1041
- follow_up_reason: followUpReason,
1042
- outcome,
1043
- time_spent_minutes: null,
1044
- // Calculated automatically from session timestamps
1045
- resolved_at: outcome === "success" ? (/* @__PURE__ */ new Date()).toISOString() : null
1046
- }).select().single();
1047
- if (error) throw error;
1048
- await supabase.from("ai_time_logs").insert({
1049
- ai_session_id: session.id,
1050
- activity_type: "debugging",
1051
- // Follow-ups are typically debugging/fixing
1052
- description: `Follow-up: ${followUpReason.replace("_", " ")} - ${outcome}`,
1053
- duration_seconds: 0,
1054
- // Duration calculated automatically from timestamps
1055
- productivity_score: outcome === "success" ? 9 : outcome === "partial_success" ? 6 : 4,
1056
- started_at: followUpTime.toISOString()
1057
- });
1058
- const sessionStartTime = new Date(session.created_at);
1059
- const totalMinutesElapsed = Math.round((followUpTime.getTime() - sessionStartTime.getTime()) / 6e4);
1060
- const currentEfficiency = totalMinutesElapsed > 0 ? totalMinutesElapsed / newEstimate : 1;
1061
- await supabase.from("ai_sessions").update({
1062
- efficiency_score: currentEfficiency.toFixed(2),
1063
- actual_time_minutes: totalMinutesElapsed
1064
- }).eq("id", session.id);
1065
- 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 });
1066
- let trackerAction = "";
1067
- let trackerDetails = "";
1068
- const existingEntry = existingEntries && existingEntries.length > 0 ? existingEntries[0] : null;
1069
- if (existingEntries && existingEntries.length > 1) {
1070
- const totalExistingDuration = existingEntries.reduce((sum, entry) => sum + (entry.tracked_duration || 0), 0);
1071
- const duplicateIds = existingEntries.slice(1).map((e) => e.id);
1072
- await supabase.from("agenda_events").delete().in("id", duplicateIds);
1073
- if (totalExistingDuration > (existingEntry?.tracked_duration || 0)) {
1074
- await supabase.from("agenda_events").update({ tracked_duration: totalExistingDuration }).eq("id", existingEntry.id);
1075
- existingEntry.tracked_duration = totalExistingDuration;
1076
- }
1077
- trackerAction = `Consolidated ${existingEntries.length} duplicate entries`;
1078
- }
1079
- if (existingEntry) {
1080
- const newDuration = (existingEntry.tracked_duration || 0) + roundedFollowUpMinutes * 60;
1081
- await supabase.from("agenda_events").update({
1082
- tracked_duration: newDuration,
1083
- end_time: followUpTime.toISOString(),
1084
- title: workDescription,
1085
- description: workDescription
1086
- }).eq("id", existingEntry.id);
1087
- trackerAction = trackerAction || "Updated existing tracker";
1088
- 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)
1089
1465
  \u2022 Description: ${workDescription}
1090
1466
  `;
1091
- } else {
1092
- const durationSeconds = roundedFollowUpMinutes * 60;
1093
- const startTime = new Date(followUpTime.getTime() - durationSeconds * 1e3);
1094
- await supabase.from("agenda_events").insert({
1095
- team_id: authContext.teamId,
1096
- user_id: authContext.userId,
1097
- ai_session_id: session.id,
1098
- title: workDescription,
1099
- description: workDescription,
1100
- start_time: startTime.toISOString(),
1101
- end_time: followUpTime.toISOString(),
1102
- type: "work",
1103
- status: "draft",
1104
- all_day: false,
1105
- is_tracked: true,
1106
- tracked_duration: durationSeconds
1107
- });
1108
- trackerAction = "Created new tracker";
1109
- 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
1110
1488
  \u2022 Description: ${workDescription}
1111
1489
  `;
1112
- }
1113
- return {
1114
- content: [{
1115
- type: "text",
1116
- 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!**
1117
1496
 
1118
1497
  \u{1F194} Session: ${aiSessionId} (back to active)
1119
1498
  \u{1F50D} Reason: ${followUpReason.replace("_", " ")}
@@ -1131,197 +1510,222 @@ ${complexityScore ? `\u{1F3AF} Complexity: ${complexityScore}/10
1131
1510
  \u23F1\uFE0F **Tracker Entry: ${trackerAction}**
1132
1511
  ` + trackerDetails + `
1133
1512
  \u26A1 **Time tracking resumed** - continue with confidence!`
1134
- }]
1135
- };
1136
1513
  }
1137
- case "get-session-context": {
1138
- const { aiSessionId, includeTicketData = true, includeTodoProgress = true, includeFollowUpHistory = false } = args2;
1139
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1140
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1141
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select(`
1142
- id,
1143
- ticket_id,
1144
- status,
1145
- ai_time_estimate_minutes,
1146
- actual_time_minutes,
1147
- complexity_score,
1148
- created_at,
1149
- cursor_session_id
1150
- `).in("team_id", teamIds);
1151
- if (sessionError) {
1152
- throw new Error(`Database error: ${sessionError.message}`);
1153
- }
1154
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1155
- if (sessionError || !session) {
1156
- throw new Error(`Session not found: ${aiSessionId}`);
1157
- }
1158
- let context = {
1159
- sessionId: aiSessionId,
1160
- status: session.status,
1161
- timeEstimate: session.ai_time_estimate_minutes,
1162
- actualTime: session.actual_time_minutes,
1163
- complexity: session.complexity_score,
1164
- createdAt: session.created_at
1165
- };
1166
- if (includeTicketData) {
1167
- const { data: ticket } = await supabase.from("tickets").select("id, ticket_number, title, description, status, priority, type").eq("id", session.ticket_id).single();
1168
- context.ticketData = ticket;
1169
- }
1170
- if (includeTodoProgress) {
1171
- 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");
1172
- context.todos = todos || [];
1173
- context.todoProgress = {
1174
- total: todos?.length || 0,
1175
- completed: todos?.filter((t) => t.status === "completed").length || 0,
1176
- inProgress: todos?.filter((t) => t.status === "in_progress").length || 0
1177
- };
1178
- }
1179
- if (includeFollowUpHistory) {
1180
- 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");
1181
- context.followUpHistory = followUps || [];
1182
- }
1183
- return {
1184
- content: [{
1185
- type: "text",
1186
- 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**
1187
1591
 
1188
1592
  Session: ${aiSessionId}
1189
1593
  Status: ${session.status}
1190
- ${context.ticketData ? `Ticket: ${context.ticketData.ticket_number} - ${context.ticketData.title}
1191
- ` : ""}${context.todoProgress ? `Todo Progress: ${context.todoProgress.completed}/${context.todoProgress.total} completed
1192
- ` : ""}${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}
1193
1597
  ` : ""}
1194
1598
  \u{1F4CB} Full context preserved for seamless continuation!`
1195
- }]
1196
- };
1197
1599
  }
1198
- case "sync-session-todos": {
1199
- const { aiSessionId, todos, replaceAll = true } = args2;
1200
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1201
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1202
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1203
- if (sessionError) {
1204
- throw new Error(`Database error: ${sessionError.message}`);
1205
- }
1206
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1207
- if (!session) {
1208
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1209
- }
1210
- if (replaceAll) {
1211
- await supabase.from("ai_todos").delete().eq("ai_session_id", session.id);
1212
- }
1213
- if (todos && todos.length > 0) {
1214
- let startSequence = 0;
1215
- if (!replaceAll) {
1216
- 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();
1217
- startSequence = (maxTodo?.sequence_order || 0) + 1;
1218
- }
1219
- const todoInserts = todos.map((todo, index) => ({
1220
- ai_session_id: session.id,
1221
- content: todo.content,
1222
- status: todo.status,
1223
- cursor_todo_id: todo.todoId || null,
1224
- estimated_minutes: todo.estimatedMinutes || null,
1225
- sequence_order: startSequence + index
1226
- }));
1227
- const { error: insertError } = await supabase.from("ai_todos").insert(todoInserts);
1228
- if (insertError) throw insertError;
1229
- }
1230
- let phaseTransition = null;
1231
- const { data: currentTodos } = await supabase.from("ai_todos").select("status").eq("ai_session_id", session.id);
1232
- if (currentTodos && currentTodos.length > 0) {
1233
- const hasInProgress = currentTodos.some((t) => t.status === "in_progress");
1234
- const allCompleted = currentTodos.every((t) => t.status === "completed");
1235
- const { data: currentPhase } = await supabase.from("ai_time_logs").select("activity_type, status").eq("ai_session_id", session.id).eq("status", "in_progress").single();
1236
- if (hasInProgress && currentPhase?.activity_type === "analysis") {
1237
- await transitionToNextPhase(session.id, "analysis");
1238
- phaseTransition = "Analysis completed \u2192 Next phase started (Investigation/Development)";
1239
- }
1240
- if (hasInProgress && currentPhase?.activity_type === "bug_investigation") {
1241
- const completedCount = currentTodos.filter((t) => t.status === "completed").length;
1242
- if (completedCount > 0) {
1243
- await transitionToNextPhase(session.id, "bug_investigation");
1244
- phaseTransition = "Investigation completed \u2192 Development phase started";
1245
- }
1246
- }
1247
- if (allCompleted && currentPhase?.activity_type === "development") {
1248
- await transitionToNextPhase(session.id, "development");
1249
- phaseTransition = "Development completed \u2192 Communication phase started";
1250
- }
1251
- }
1252
- return {
1253
- content: [{
1254
- type: "text",
1255
- 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!**
1256
1667
 
1257
1668
  Session: ${aiSessionId}
1258
1669
  ${replaceAll ? "Synced" : "Added"} ${todos?.length || 0} todos
1259
1670
  ${replaceAll ? "" : "\u2795 Added to existing todo list\n"}${phaseTransition ? `\u{1F504} Phase Transition: ${phaseTransition}
1260
1671
  ` : ""}
1261
1672
  \u{1F4DD} Todo list updated and tracked for progress monitoring!`
1262
- }]
1263
- };
1264
1673
  }
1265
- case "add-follow-up-todos": {
1266
- const { aiSessionId, newTodos, followUpReason } = args2;
1267
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1268
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1269
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1270
- if (sessionError) {
1271
- throw new Error(`Database error: ${sessionError.message}`);
1272
- }
1273
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1274
- if (!session) {
1275
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1276
- }
1277
- if (newTodos && newTodos.length > 0) {
1278
- 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();
1279
- const startSequence = (maxTodo?.sequence_order || 0) + 1;
1280
- const todoInserts = newTodos.map((todo, index) => ({
1281
- ai_session_id: session.id,
1282
- content: `[Follow-up] ${todo.content}`,
1283
- status: todo.status || "pending",
1284
- estimated_minutes: todo.estimatedMinutes || null,
1285
- sequence_order: startSequence + index
1286
- }));
1287
- const { error: insertError } = await supabase.from("ai_todos").insert(todoInserts);
1288
- if (insertError) throw insertError;
1289
- }
1290
- return {
1291
- content: [{
1292
- type: "text",
1293
- 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!**
1294
1702
 
1295
1703
  Session: ${aiSessionId}
1296
1704
  Added ${newTodos?.length || 0} new todos from follow-up
1297
1705
  ${followUpReason ? `Reason: ${followUpReason}
1298
1706
  ` : ""}
1299
1707
  \u{1F4DD} New tasks identified and added to existing workflow!`
1300
- }]
1301
- };
1302
1708
  }
1303
- case "update-session-status": {
1304
- const { aiSessionId, status, actualTimeMinutes, completionNotes } = args2;
1305
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1306
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1307
- const { data: allSessions, error: findError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1308
- if (findError) {
1309
- throw new Error(`Database error: ${findError.message}`);
1310
- }
1311
- const foundSession = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1312
- if (!foundSession) {
1313
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1314
- }
1315
- const { data, error } = await supabase.from("ai_sessions").update({
1316
- status,
1317
- actual_time_minutes: actualTimeMinutes || null,
1318
- completed_at: status === "completed" ? (/* @__PURE__ */ new Date()).toISOString() : null
1319
- }).eq("id", foundSession.id).select().single();
1320
- if (error) throw error;
1321
- return {
1322
- content: [{
1323
- type: "text",
1324
- 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!**
1325
1729
 
1326
1730
  Session: ${aiSessionId}
1327
1731
  Status: ${status}
@@ -1329,113 +1733,135 @@ ${actualTimeMinutes ? `Actual Time: ${actualTimeMinutes} minutes
1329
1733
  ` : ""}${status === "completed" ? `\u2705 Session completed successfully!
1330
1734
  ` : ""}${completionNotes ? `Notes: ${completionNotes}
1331
1735
  ` : ""}`
1332
- }]
1333
- };
1334
1736
  }
1335
- case "get-completion-context": {
1336
- const { aiSessionId, includeFollowUps = true, includeTimeMetrics = true, includeTodos = true } = args2;
1337
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1338
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1339
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select(`
1340
- id,
1341
- ticket_id,
1342
- ai_time_estimate_minutes,
1343
- actual_time_minutes,
1344
- efficiency_score,
1345
- created_at,
1346
- completed_at,
1347
- status,
1348
- complexity_score
1349
- `).in("team_id", teamIds);
1350
- if (sessionError) {
1351
- throw new Error(`Database error: ${sessionError.message}`);
1352
- }
1353
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1354
- if (!session) {
1355
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1356
- }
1357
- const { data: ticket, error: ticketError } = await supabase.from("tickets").select("ticket_number, title, description, type, priority").eq("id", session.ticket_id).single();
1358
- if (ticketError || !ticket) {
1359
- throw new Error("Ticket not found for session");
1360
- }
1361
- let contextData = {
1362
- session: {
1363
- id: aiSessionId,
1364
- status: session.status,
1365
- complexity: session.complexity_score,
1366
- createdAt: session.created_at,
1367
- completedAt: session.completed_at
1368
- },
1369
- ticket: {
1370
- number: ticket.ticket_number,
1371
- title: ticket.title,
1372
- description: ticket.description,
1373
- type: ticket.type,
1374
- priority: ticket.priority
1375
- }
1376
- };
1377
- if (includeTimeMetrics) {
1378
- const timeSaved = session.ai_time_estimate_minutes && session.actual_time_minutes ? Math.max(0, session.ai_time_estimate_minutes - session.actual_time_minutes) : null;
1379
- contextData.timeMetrics = {
1380
- estimatedMinutes: session.ai_time_estimate_minutes,
1381
- actualMinutes: session.actual_time_minutes,
1382
- timeSaved,
1383
- efficiency: session.efficiency_score,
1384
- sessionDuration: session.completed_at && session.created_at ? Math.round((new Date(session.completed_at).getTime() - new Date(session.created_at).getTime()) / 6e4) : null
1385
- };
1386
- }
1387
- if (includeTodos) {
1388
- 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 });
1389
- contextData.todos = todos || [];
1390
- }
1391
- if (includeFollowUps) {
1392
- 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 });
1393
- contextData.followUps = followUps || [];
1394
- }
1395
- return {
1396
- content: [{
1397
- type: "text",
1398
- 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!**
1399
1830
 
1400
- \u{1F3AB} **Ticket:** ${ticket.ticket_number} - ${ticket.title}
1831
+ \u{1F3AB} **Ticket:** ${ticket.ticketNumber} - ${ticket.title}
1401
1832
  \u{1F194} **Session:** ${aiSessionId} (${session.status})
1402
- \u23F1\uFE0F **Time:** ${session.actual_time_minutes || "N/A"}/${session.ai_time_estimate_minutes || "N/A"} minutes
1403
- \u{1F4CB} **Todos:** ${contextData.todos?.filter((t) => t.status === "completed").length || 0}/${contextData.todos?.length || 0} completed
1404
- \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}
1405
1836
 
1406
1837
  \u2705 **Full context ready for Cursor AI to generate customer response!**
1407
1838
 
1408
1839
  **Context Data:**
1409
1840
  \`\`\`json
1410
1841
  ${JSON.stringify(contextData, null, 2)}\`\`\``
1411
- }]
1412
- };
1413
1842
  }
1414
- case "save-customer-response": {
1415
- const { aiSessionId, customerResponse, responseType = "completion" } = args2;
1416
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1417
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1418
- const { data: allSessions, error: sessionError } = await supabase.from("ai_sessions").select("id").in("team_id", teamIds);
1419
- if (sessionError) {
1420
- throw new Error(`Database error: ${sessionError.message}`);
1421
- }
1422
- const session = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1423
- if (!session) {
1424
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1425
- }
1426
- const { data: responseData, error: responseError } = await supabase.from("ai_responses").insert({
1427
- ai_session_id: session.id,
1428
- response_type: responseType,
1429
- content: customerResponse,
1430
- is_ready_for_customer: true,
1431
- provider_approved: false
1432
- // Needs manual approval
1433
- }).select().single();
1434
- if (responseError) throw responseError;
1435
- return {
1436
- content: [{
1437
- type: "text",
1438
- 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!**
1439
1865
 
1440
1866
  \u{1F194} Session: ${aiSessionId}
1441
1867
  \u{1F4DD} Response Type: ${responseType}
@@ -1447,623 +1873,693 @@ ${JSON.stringify(contextData, null, 2)}\`\`\``
1447
1873
  **Preview:**
1448
1874
  \`\`\`
1449
1875
  ${customerResponse.substring(0, 200)}${customerResponse.length > 200 ? "..." : ""}\`\`\``
1450
- }]
1451
- };
1452
1876
  }
1453
- case "complete-ai-session": {
1454
- const { aiSessionId, workCompleted, technicalSummary, invoiceDescription, efficiencyNotes } = args2;
1455
- const sessionUuid = aiSessionId.replace("ai-sess-", "");
1456
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1457
- const { data: allSessions, error: getSessionError } = await supabase.from("ai_sessions").select("id, ticket_id, ai_time_estimate_minutes, created_at").in("team_id", teamIds);
1458
- if (getSessionError) {
1459
- throw new Error(`Database error: ${getSessionError.message}`);
1460
- }
1461
- const existingSession = allSessions?.find((s) => s.id.toString().startsWith(sessionUuid));
1462
- if (!existingSession) {
1463
- throw new Error(`Session not found: ${aiSessionId} (searched ${allSessions?.length || 0} sessions)`);
1464
- }
1465
- const completionTime = /* @__PURE__ */ new Date();
1466
- const sessionStartTime = new Date(existingSession.created_at);
1467
- const timeSpentMinutes = Math.round((completionTime.getTime() - sessionStartTime.getTime()) / 6e4);
1468
- const { data: session, error: sessionError } = await supabase.from("ai_sessions").update({
1469
- status: "completed",
1470
- actual_time_minutes: timeSpentMinutes,
1471
- completed_at: completionTime.toISOString(),
1472
- efficiency_score: null
1473
- // Will be calculated
1474
- }).eq("id", existingSession.id).select("id, ticket_id, ai_time_estimate_minutes, created_at").single();
1475
- if (sessionError || !session) {
1476
- throw new Error(`Failed to update session: ${aiSessionId}`);
1477
- }
1478
- const efficiencyScore = session.ai_time_estimate_minutes ? timeSpentMinutes / session.ai_time_estimate_minutes : 1;
1479
- await supabase.from("ai_sessions").update({ efficiency_score: efficiencyScore.toFixed(2) }).eq("id", session.id);
1480
- const { data: activePhases } = await supabase.from("ai_time_logs").select("*").eq("ai_session_id", existingSession.id).eq("status", "in_progress");
1481
- for (const phase of activePhases || []) {
1482
- const duration = Math.round(
1483
- (completionTime.getTime() - new Date(phase.started_at).getTime()) / 1e3
1484
- );
1485
- await supabase.from("ai_time_logs").update({
1486
- ended_at: completionTime.toISOString(),
1487
- duration_seconds: duration,
1488
- status: "completed"
1489
- }).eq("id", phase.id);
1490
- }
1491
- await supabase.from("ai_time_logs").update({ status: "skipped" }).eq("ai_session_id", existingSession.id).eq("status", "pending").eq("estimated_duration_seconds", 0);
1492
- const sessionDuration = Math.round(
1493
- (completionTime.getTime() - new Date(session.created_at).getTime()) / 6e4
1494
- );
1495
- const workSummary = `Completed ${workCompleted.length} tasks including: ${workCompleted.slice(0, 3).join(", ")}${workCompleted.length > 3 ? " and more" : ""}.`;
1496
- const { data: ticketInfo } = await supabase.from("tickets").select("ticket_number, title, project_id").eq("id", session.ticket_id).single();
1497
- let completionDescription;
1498
- if (invoiceDescription) {
1499
- completionDescription = `${ticketInfo?.ticket_number || "Ticket"}: ${invoiceDescription}`;
1500
- } else {
1501
- const workDescription = workCompleted.map((task, index) => `${index + 1}. ${task}`).join("\n");
1502
- 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}
1503
1959
 
1504
1960
  Completed work:
1505
1961
  ${workDescription}`;
1506
- }
1507
- const estimatedMinutes = session.ai_time_estimate_minutes || timeSpentMinutes;
1508
- const sessionStart = new Date(session.created_at);
1509
- const estimatedEnd = new Date(sessionStart.getTime() + estimatedMinutes * 6e4);
1510
- 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 });
1511
- let agendaEvent = null;
1512
- let agendaError = null;
1513
- let wasUpdated = false;
1514
- let consolidatedCount = 0;
1515
- const existingAgendaEntry = existingAgendaEntries && existingAgendaEntries.length > 0 ? existingAgendaEntries[0] : null;
1516
- if (existingAgendaEntries && existingAgendaEntries.length > 1) {
1517
- const duplicateIds = existingAgendaEntries.slice(1).map((e) => e.id);
1518
- await supabase.from("agenda_events").delete().in("id", duplicateIds);
1519
- consolidatedCount = existingAgendaEntries.length - 1;
1520
- }
1521
- if (existingAgendaEntry) {
1522
- const { data: updated, error: updateError } = await supabase.from("agenda_events").update({
1523
- title: ticketInfo?.title || "Development Work",
1524
- description: completionDescription,
1525
- end_time: estimatedEnd.toISOString(),
1526
- project_id: ticketInfo?.project_id || null,
1527
- ticket_id: session.ticket_id,
1528
- tracked_duration: estimatedMinutes * 60
1529
- // Use AI estimate in seconds for billing
1530
- }).eq("id", existingAgendaEntry.id).select("id").single();
1531
- agendaEvent = updated;
1532
- agendaError = updateError;
1533
- wasUpdated = true;
1534
- } else {
1535
- const { data: created, error: createError } = await supabase.from("agenda_events").insert({
1536
- team_id: authContext.teamId,
1537
- user_id: authContext.userId,
1538
- title: ticketInfo?.title || "Development Work",
1539
- description: completionDescription,
1540
- start_time: sessionStart.toISOString(),
1541
- end_time: estimatedEnd.toISOString(),
1542
- project_id: ticketInfo?.project_id || null,
1543
- ticket_id: session.ticket_id,
1544
- ai_session_id: session.id,
1545
- // Use the actual UUID, not the readable ID
1546
- type: "work",
1547
- status: "draft",
1548
- // Mark as draft for manual approval
1549
- all_day: false,
1550
- is_tracked: true,
1551
- tracked_duration: estimatedMinutes * 60
1552
- // Use AI estimate in seconds for billing
1553
- }).select("id").single();
1554
- agendaEvent = created;
1555
- agendaError = createError;
1556
- }
1557
- if (agendaError) {
1558
- console.error(`\u26A0\uFE0F Failed to ${wasUpdated ? "update" : "create"} agenda event:`, agendaError);
1559
- }
1560
- if (consolidatedCount > 0) {
1561
- console.log(`\u{1F9F9} Cleaned up ${consolidatedCount} duplicate agenda entries for session ${aiSessionId}`);
1562
- }
1563
- 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!**
1564
2033
 
1565
2034
  `;
1566
- responseText += `\u{1F194} Session: ${aiSessionId}
2035
+ responseText += `\u{1F194} Session: ${aiSessionId}
1567
2036
  `;
1568
- responseText += `\u{1F4CA} **Performance Summary:**
2037
+ responseText += `\u{1F4CA} **Performance Summary:**
1569
2038
  `;
1570
- responseText += ` \u2022 Tasks Completed: ${workCompleted.length}
2039
+ responseText += ` \u2022 Tasks Completed: ${workCompleted.length}
1571
2040
  `;
1572
- responseText += ` \u2022 Time Spent: ${timeSpentMinutes} minutes
2041
+ responseText += ` \u2022 Time Spent: ${timeSpentMinutes} minutes
1573
2042
  `;
1574
- responseText += ` \u2022 Estimated Time: ${session.ai_time_estimate_minutes || "N/A"} minutes
2043
+ responseText += ` \u2022 Estimated Time: ${session.aiTimeEstimateMinutes || "N/A"} minutes
1575
2044
  `;
1576
- 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)}%
1577
2046
  `;
1578
- responseText += ` \u2022 Session Duration: ${sessionDuration} minutes
2047
+ responseText += ` \u2022 Session Duration: ${sessionDuration} minutes
1579
2048
 
1580
2049
  `;
1581
- responseText += `\u2705 **Work Completed:**
2050
+ responseText += `\u2705 **Work Completed:**
1582
2051
  `;
1583
- workCompleted.forEach((task, index) => {
1584
- responseText += `${index + 1}. ${task}
2052
+ workCompleted.forEach((task, index) => {
2053
+ responseText += `${index + 1}. ${task}
1585
2054
  `;
1586
- });
1587
- responseText += `
2055
+ });
2056
+ responseText += `
1588
2057
  `;
1589
- if (technicalSummary) {
1590
- responseText += `\u{1F527} **Technical Summary:**
2058
+ if (technicalSummary) {
2059
+ responseText += `\u{1F527} **Technical Summary:**
1591
2060
  ${technicalSummary}
1592
2061
 
1593
2062
  `;
1594
- }
1595
- if (efficiencyNotes) {
1596
- responseText += `\u{1F4C8} **Efficiency Notes:**
2063
+ }
2064
+ if (efficiencyNotes) {
2065
+ responseText += `\u{1F4C8} **Efficiency Notes:**
1597
2066
  ${efficiencyNotes}
1598
2067
 
1599
2068
  `;
1600
- }
1601
- if (agendaEvent) {
1602
- responseText += `\u{1F4C5} **Timetrack Entry ${wasUpdated ? "Updated" : "Created"}:**
2069
+ }
2070
+ if (agendaEventId) {
2071
+ responseText += `\u{1F4C5} **Timetrack Entry ${wasUpdated ? "Updated" : "Created"}:**
1603
2072
  `;
1604
- 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
1605
2074
  `;
1606
- responseText += ` \u2022 Status: DRAFT (requires approval in agenda)
2075
+ responseText += ` \u2022 Status: DRAFT (requires approval in agenda)
1607
2076
  `;
1608
- responseText += ` \u2022 Duration: ${estimatedMinutes} minutes
2077
+ responseText += ` \u2022 Duration: ${estimatedMinutes} minutes
1609
2078
  `;
1610
- responseText += ` \u2022 Period: ${sessionStart.toLocaleString()} - ${completionTime.toLocaleString()}
2079
+ responseText += ` \u2022 Period: ${sessionStart.toLocaleString()} - ${completionTime.toLocaleString()}
1611
2080
 
1612
2081
  `;
1613
- }
1614
- responseText += `\u{1F4CB} **Context for Customer Response:**
2082
+ }
2083
+ responseText += `\u{1F4CB} **Context for Customer Response:**
1615
2084
  `;
1616
- responseText += ` \u2022 Use "get-completion-context" to retrieve full context
2085
+ responseText += ` \u2022 Use "get-completion-context" to retrieve full context
1617
2086
  `;
1618
- responseText += ` \u2022 Generate customer-friendly response based on completed work
2087
+ responseText += ` \u2022 Generate customer-friendly response based on completed work
1619
2088
  `;
1620
- responseText += ` \u2022 Focus on business value and customer benefits
2089
+ responseText += ` \u2022 Focus on business value and customer benefits
1621
2090
 
1622
2091
  `;
1623
- responseText += `\u{1F3AF} **Session archived successfully!**`;
1624
- return {
1625
- content: [{
1626
- type: "text",
1627
- text: responseText
1628
- }]
1629
- };
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));
1630
2202
  }
1631
- case "log-hours": {
1632
- const { projectId, ticketId, aiSessionId, workDescription, estimatedHours, chatContextSummary } = args2;
1633
- let project = null;
1634
- let ticket = null;
1635
- let aiSession = null;
1636
- if (projectId) {
1637
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
1638
- if (!projectIds.includes(projectId)) {
1639
- throw new Error(`Project not found or no access: ${projectId}. Please call get-projects first to find the correct project.`);
1640
- }
1641
- const { data: projectData, error: projectError } = await supabase.from("projects").select("id, name, team_id").eq("id", projectId).single();
1642
- if (projectError || !projectData) {
1643
- throw new Error(`Project not found: ${projectId}.`);
1644
- }
1645
- project = projectData;
1646
- }
1647
- if (ticketId) {
1648
- const { data: ticketData, error: ticketError } = await supabase.from("tickets").select("id, title, status, team_id, project_id, customer_id").eq("id", ticketId).single();
1649
- if (ticketError || !ticketData) {
1650
- throw new Error(`Ticket not found: ${ticketId}. Please call get-tickets first to find the correct ticket.`);
1651
- }
1652
- let hasAccess = false;
1653
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1654
- if (teamIds.includes(ticketData.team_id)) {
1655
- hasAccess = true;
1656
- }
1657
- if (!hasAccess && ticketData.project_id) {
1658
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
1659
- if (projectIds.includes(ticketData.project_id)) {
1660
- hasAccess = true;
1661
- }
1662
- }
1663
- if (!hasAccess && ticketData.customer_id) {
1664
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
1665
- if (customerIds.includes(ticketData.customer_id)) {
1666
- hasAccess = true;
1667
- }
1668
- }
1669
- if (!hasAccess) {
1670
- throw new Error(`No access to ticket: ${ticketId}. Please call get-tickets first to find the correct ticket.`);
1671
- }
1672
- ticket = ticketData;
1673
- }
1674
- if (aiSessionId) {
1675
- const { data: sessionData, error: sessionError } = await supabase.from("ai_dev_sessions").select("id, ticket_id, status").eq("id", aiSessionId).single();
1676
- if (sessionError || !sessionData) {
1677
- throw new Error(`AI Session not found: ${aiSessionId}.`);
1678
- }
1679
- aiSession = sessionData;
1680
- }
1681
- const durationSeconds = Math.round(estimatedHours * 3600);
1682
- const now = /* @__PURE__ */ new Date();
1683
- let agendaEntry = null;
1684
- let agendaError = null;
1685
- let wasUpdated = false;
1686
- let consolidatedCount = 0;
1687
- if (aiSession?.id || ticket?.id) {
1688
- 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 });
1689
- if (aiSession?.id) {
1690
- query = query.eq("ai_session_id", aiSession.id);
1691
- } else if (ticket?.id) {
1692
- query = query.eq("ticket_id", ticket.id);
1693
- }
1694
- const { data: existingEntries } = await query;
1695
- if (existingEntries && existingEntries.length > 0) {
1696
- const existingEntry = existingEntries[0];
1697
- if (existingEntries.length > 1) {
1698
- const duplicateIds = existingEntries.slice(1).map((e) => e.id);
1699
- await supabase.from("agenda_events").delete().in("id", duplicateIds);
1700
- consolidatedCount = existingEntries.length - 1;
1701
- }
1702
- const newDuration = (existingEntry.tracked_duration || 0) + durationSeconds;
1703
- const { data: updated, error: updateError } = await supabase.from("agenda_events").update({
1704
- tracked_duration: newDuration,
1705
- end_time: now.toISOString(),
1706
- title: workDescription,
1707
- description: chatContextSummary || workDescription,
1708
- project_id: project?.id || existingEntry.project_id
1709
- }).eq("id", existingEntry.id).select("id, tracked_duration, project_id, ticket_id, ai_session_id").single();
1710
- agendaEntry = updated;
1711
- agendaError = updateError;
1712
- wasUpdated = true;
1713
- }
1714
- }
1715
- if (!agendaEntry) {
1716
- const startTime = new Date(now.getTime() - durationSeconds * 1e3);
1717
- const { data: created, error: createError } = await supabase.from("agenda_events").insert({
1718
- team_id: authContext.teamId,
1719
- user_id: authContext.userId,
1720
- project_id: project?.id || null,
1721
- ticket_id: ticket?.id || null,
1722
- ai_session_id: aiSession?.id || null,
1723
- title: workDescription,
1724
- description: chatContextSummary || workDescription,
1725
- start_time: startTime.toISOString(),
1726
- end_time: now.toISOString(),
1727
- type: "work",
1728
- status: "draft",
1729
- all_day: false,
1730
- is_tracked: true,
1731
- tracked_duration: durationSeconds
1732
- }).select("id, tracked_duration, project_id, ticket_id, ai_session_id").single();
1733
- agendaEntry = created;
1734
- agendaError = createError;
1735
- }
1736
- if (agendaError || !agendaEntry) {
1737
- throw new Error(`Failed to ${wasUpdated ? "update" : "create"} time entry: ${agendaError?.message || "Unknown error"}`);
1738
- }
1739
- 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"}!**
1740
2261
 
1741
2262
  `;
1742
- if (wasUpdated) {
1743
- responseText += `\u{1F504} **Updated existing draft entry** (avoiding duplicates)
2263
+ if (wasUpdated) {
2264
+ responseText += `\u{1F504} **Updated existing draft entry** (avoiding duplicates)
1744
2265
  `;
1745
- 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
1746
2267
 
1747
2268
  `;
1748
- }
1749
- if (consolidatedCount > 0) {
1750
- responseText += `\u{1F9F9} **Cleaned up ${consolidatedCount} duplicate entries**
2269
+ }
2270
+ if (consolidatedCount > 0) {
2271
+ responseText += `\u{1F9F9} **Cleaned up ${consolidatedCount} duplicate entries**
1751
2272
 
1752
2273
  `;
1753
- }
1754
- responseText += `\u{1F4CB} **Entry Details:**
2274
+ }
2275
+ responseText += `\u{1F4CB} **Entry Details:**
1755
2276
  `;
1756
- if (project) {
1757
- responseText += ` \u2022 Project: ${project.name}
2277
+ responseText += ` \u2022 Project: ${project ? project.name : "(No project assigned)"}
1758
2278
  `;
1759
- } else {
1760
- responseText += ` \u2022 Project: (No project assigned)
2279
+ if (ticket) responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
1761
2280
  `;
1762
- }
1763
- if (ticket) {
1764
- responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
2281
+ if (aiSession)
2282
+ responseText += ` \u2022 AI Session: ${aiSession.id} (${aiSession.status})
1765
2283
  `;
1766
- }
1767
- if (aiSession) {
1768
- responseText += ` \u2022 AI Session: ${aiSession.id} (${aiSession.status})
2284
+ responseText += ` \u2022 Description: ${workDescription}
1769
2285
  `;
1770
- }
1771
- responseText += ` \u2022 Description: ${workDescription}
2286
+ responseText += ` \u2022 ${wasUpdated ? "Added" : "Estimated"} Hours: ${estimatedHours}h (${Math.floor(estimatedHours)}h ${Math.round(estimatedHours % 1 * 60)}m)
1772
2287
  `;
1773
- responseText += ` \u2022 ${wasUpdated ? "Added" : "Estimated"} Hours: ${estimatedHours}h (${Math.floor(estimatedHours)}h ${Math.round(estimatedHours % 1 * 60)}m)
2288
+ responseText += ` \u2022 Status: DRAFT (not billed yet)
1774
2289
  `;
1775
- responseText += ` \u2022 Status: DRAFT (not billed yet)
1776
- `;
1777
- responseText += ` \u2022 Entry ID: ${agendaEntry.id}
2290
+ responseText += ` \u2022 Entry ID: ${agendaEntry.id}
1778
2291
 
1779
2292
  `;
1780
- if (chatContextSummary) {
1781
- responseText += `\u{1F4CA} **Work Context:**
2293
+ if (chatContextSummary) {
2294
+ responseText += `\u{1F4CA} **Work Context:**
1782
2295
  `;
1783
- responseText += `${chatContextSummary.substring(0, 200)}${chatContextSummary.length > 200 ? "..." : ""}
2296
+ responseText += `${chatContextSummary.substring(0, 200)}${chatContextSummary.length > 200 ? "..." : ""}
1784
2297
 
1785
2298
  `;
1786
- }
1787
- responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
1788
- return {
1789
- 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
+ {
1790
2329
  type: "text",
1791
- text: responseText
1792
- }]
1793
- };
1794
- }
1795
- // === GITHUB TOOLS ===
1796
- case "get-github-file": {
1797
- const { projectId, filePath, ref } = args2;
1798
- const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1799
- if (!githubInfo) {
1800
- return {
1801
- content: [{
1802
- type: "text",
1803
- text: "\u274C GitHub not configured for this project."
1804
- }]
1805
- };
1806
- }
1807
- try {
1808
- const octokit = new Octokit({ auth: githubInfo.token });
1809
- console.error(`\u{1F4C4} Reading file: ${filePath} from ${githubInfo.repositoryFullName}`);
1810
- const { data } = await octokit.rest.repos.getContent({
1811
- owner: githubInfo.owner,
1812
- repo: githubInfo.repo,
1813
- path: filePath,
1814
- ref
1815
- });
1816
- if (Array.isArray(data) || data.type !== "file") {
1817
- return {
1818
- content: [{
1819
- type: "text",
1820
- text: `\u274C "${filePath}" is not a file or contains multiple items.`
1821
- }]
1822
- };
2330
+ text: `\u274C "${filePath}" is not a file or contains multiple items.`
1823
2331
  }
1824
- const content = Buffer.from(data.content, "base64").toString("utf-8");
1825
- 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}**
1826
2337
  `;
1827
- responseText += `Repository: ${githubInfo.repositoryFullName}
2338
+ responseText += `Repository: ${githubInfo.repositoryFullName}
1828
2339
  `;
1829
- responseText += `Size: ${data.size} bytes
2340
+ responseText += `Size: ${data.size} bytes
1830
2341
  `;
1831
- responseText += `URL: ${data.html_url}
2342
+ responseText += `URL: ${data.html_url}
1832
2343
 
1833
2344
  `;
1834
- responseText += `**Content:**
2345
+ responseText += `**Content:**
1835
2346
  \`\`\`
1836
2347
  ${content}
1837
2348
  \`\`\``;
1838
- return {
1839
- content: [{
1840
- type: "text",
1841
- text: responseText
1842
- }]
1843
- };
1844
- } catch (error) {
1845
- console.error("GitHub get file error:", error);
1846
- if (error.status === 404) {
1847
- return {
1848
- content: [{
1849
- type: "text",
1850
- text: `\u274C File not found: ${filePath}`
1851
- }]
1852
- };
1853
- }
1854
- return {
1855
- content: [{
1856
- type: "text",
1857
- text: `\u274C Failed to read file: ${error.message || "Unknown error"}`
1858
- }]
1859
- };
1860
- }
1861
- }
1862
- case "list-github-directory": {
1863
- const { projectId, directoryPath, ref } = args2;
1864
- const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
1865
- if (!githubInfo) {
1866
- return {
1867
- content: [{
1868
- type: "text",
1869
- text: "\u274C GitHub not configured for this project."
1870
- }]
1871
- };
1872
- }
1873
- try {
1874
- const octokit = new Octokit({ auth: githubInfo.token });
1875
- const normalizedPath = !directoryPath || directoryPath === "/" ? "" : directoryPath;
1876
- console.error(`\u{1F4C1} Listing directory: ${normalizedPath || "(root)"} in ${githubInfo.repositoryFullName}`);
1877
- const { data } = await octokit.rest.repos.getContent({
1878
- owner: githubInfo.owner,
1879
- repo: githubInfo.repo,
1880
- path: normalizedPath,
1881
- ref
1882
- });
1883
- if (!Array.isArray(data)) {
1884
- return {
1885
- content: [{
1886
- type: "text",
1887
- text: `\u274C "${directoryPath}" is not a directory.`
1888
- }]
1889
- };
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.`
1890
2395
  }
1891
- let responseText = `\u{1F4C1} **Directory: ${directoryPath || "(root)"}**
2396
+ ]
2397
+ };
2398
+ }
2399
+ let responseText = `\u{1F4C1} **Directory: ${directoryPath || "(root)"}**
1892
2400
  `;
1893
- responseText += `Repository: ${githubInfo.repositoryFullName}
2401
+ responseText += `Repository: ${githubInfo.repositoryFullName}
1894
2402
  `;
1895
- responseText += `Items: ${data.length}
2403
+ responseText += `Items: ${data.length}
1896
2404
 
1897
2405
  `;
1898
- const directories = data.filter((item) => item.type === "dir");
1899
- const files = data.filter((item) => item.type === "file");
1900
- if (directories.length > 0) {
1901
- 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}):**
1902
2410
  `;
1903
- for (const dir of directories) {
1904
- responseText += ` - ${dir.name}/
2411
+ for (const dir of directories) responseText += ` - ${dir.name}/
1905
2412
  `;
1906
- }
1907
- responseText += `
2413
+ responseText += `
1908
2414
  `;
1909
- }
1910
- if (files.length > 0) {
1911
- responseText += `**\u{1F4C4} Files (${files.length}):**
2415
+ }
2416
+ if (files.length > 0) {
2417
+ responseText += `**\u{1F4C4} Files (${files.length}):**
1912
2418
  `;
1913
- for (const file of files) {
1914
- responseText += ` - ${file.name} (${file.size} bytes)
2419
+ for (const file of files)
2420
+ responseText += ` - ${file.name} (${file.size} bytes)
1915
2421
  `;
1916
- }
1917
- }
1918
- return {
1919
- content: [{
1920
- type: "text",
1921
- text: responseText
1922
- }]
1923
- };
1924
- } catch (error) {
1925
- console.error("GitHub list directory error:", error);
1926
- if (error.status === 404) {
1927
- return {
1928
- content: [{
1929
- type: "text",
1930
- text: `\u274C Directory not found: ${directoryPath}`
1931
- }]
1932
- };
1933
- }
1934
- return {
1935
- content: [{
1936
- type: "text",
1937
- text: `\u274C Failed to list directory: ${error.message || "Unknown error"}`
1938
- }]
1939
- };
1940
- }
1941
- }
1942
- default:
1943
- throw new Error(`Unknown tool: ${name}`);
1944
2422
  }
2423
+ return { content: [{ type: "text", text: responseText }] };
1945
2424
  } catch (error) {
1946
- console.error(`\u274C Tool execution error:`, error);
1947
- 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";
1948
2435
  return {
1949
- content: [{
1950
- type: "text",
1951
- text: `Error executing ${name}: ${message}`
1952
- }]
2436
+ content: [
2437
+ { type: "text", text: `\u274C Failed to list directory: ${message}` }
2438
+ ]
1953
2439
  };
1954
2440
  }
1955
- });
2441
+ }
1956
2442
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1957
2443
  if (!authContext) {
1958
2444
  return {
1959
- contents: [{
1960
- uri: request.params.uri,
1961
- mimeType: "text/plain",
1962
- text: "Error: Not authenticated. API key validation failed."
1963
- }]
2445
+ contents: [
2446
+ {
2447
+ uri: request.params.uri,
2448
+ mimeType: "text/plain",
2449
+ text: "Error: Not authenticated. API key validation failed."
2450
+ }
2451
+ ]
1964
2452
  };
1965
2453
  }
2454
+ const ctx = authContext;
1966
2455
  const { uri } = request.params;
1967
2456
  console.error(`\u{1F4DA} Reading resource: ${uri}`);
1968
2457
  try {
1969
2458
  switch (uri) {
1970
2459
  case "tickets://recent": {
1971
- const teamIds = await getAccessibleTeamIds(authContext.teamId);
1972
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
1973
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
1974
- let teamFilter = `team_id.in.(${teamIds.join(",")})`;
1975
- let projectFilter = projectIds.length > 0 ? `project_id.in.(${projectIds.join(",")})` : null;
1976
- let customerFilter = customerIds.length > 0 ? `customer_id.in.(${customerIds.join(",")})` : null;
1977
- const filters = [teamFilter, projectFilter, customerFilter].filter(Boolean);
1978
- const { data, error } = await supabase.from("tickets").select(`
1979
- id,
1980
- ticket_number,
1981
- title,
1982
- status,
1983
- priority,
1984
- created_at
1985
- `).or(filters.join(",")).order("created_at", { ascending: false }).limit(20);
1986
- 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);
1987
2476
  return {
1988
- contents: [{
1989
- uri,
1990
- mimeType: "application/json",
1991
- text: JSON.stringify(data, null, 2)
1992
- }]
2477
+ contents: [
2478
+ {
2479
+ uri,
2480
+ mimeType: "application/json",
2481
+ text: JSON.stringify(rows, null, 2)
2482
+ }
2483
+ ]
1993
2484
  };
1994
2485
  }
1995
2486
  case "customers://all": {
1996
- const customerIds = await getAccessibleCustomerIds(authContext.teamId);
2487
+ const customerIds = await getAccessibleCustomerIds(ctx.teamId);
1997
2488
  if (customerIds.length === 0) {
1998
2489
  return {
1999
- contents: [{
2000
- uri,
2001
- mimeType: "application/json",
2002
- text: JSON.stringify([], null, 2)
2003
- }]
2490
+ contents: [
2491
+ { uri, mimeType: "application/json", text: JSON.stringify([], null, 2) }
2492
+ ]
2004
2493
  };
2005
2494
  }
2006
- const { data, error } = await supabase.from("customers").select("id, name, email, website, created_at").in("id", customerIds).order("name").limit(50);
2007
- 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);
2008
2502
  return {
2009
- contents: [{
2010
- uri,
2011
- mimeType: "application/json",
2012
- text: JSON.stringify(data, null, 2)
2013
- }]
2503
+ contents: [
2504
+ { uri, mimeType: "application/json", text: JSON.stringify(rows, null, 2) }
2505
+ ]
2014
2506
  };
2015
2507
  }
2016
2508
  case "projects://active": {
2017
- const projectIds = await getAccessibleProjectIds(authContext.userId, authContext.teamId);
2509
+ const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
2018
2510
  if (projectIds.length === 0) {
2019
2511
  return {
2020
- contents: [{
2021
- uri,
2022
- mimeType: "application/json",
2023
- text: JSON.stringify([], null, 2)
2024
- }]
2512
+ contents: [
2513
+ { uri, mimeType: "application/json", text: JSON.stringify([], null, 2) }
2514
+ ]
2025
2515
  };
2026
2516
  }
2027
- const { data, error } = await supabase.from("projects").select(`
2028
- id,
2029
- name,
2030
- description,
2031
- status,
2032
- created_at,
2033
- customers:customer_id(id, name)
2034
- `).in("id", projectIds).eq("status", "active").order("name").limit(50);
2035
- 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);
2036
2528
  return {
2037
- contents: [{
2038
- uri,
2039
- mimeType: "application/json",
2040
- text: JSON.stringify(data, null, 2)
2041
- }]
2529
+ contents: [
2530
+ { uri, mimeType: "application/json", text: JSON.stringify(rows, null, 2) }
2531
+ ]
2042
2532
  };
2043
2533
  }
2044
2534
  default:
2045
2535
  throw new Error(`Unknown resource: ${uri}`);
2046
2536
  }
2047
2537
  } catch (error) {
2048
- console.error(`\u274C Resource read error:`, error);
2538
+ console.error("\u274C Resource read error:", error);
2049
2539
  return {
2050
- contents: [{
2051
- uri,
2052
- mimeType: "text/plain",
2053
- text: `Error reading ${uri}: ${error instanceof Error ? error.message : "Unknown error"}`
2054
- }]
2540
+ contents: [
2541
+ {
2542
+ uri,
2543
+ mimeType: "text/plain",
2544
+ text: `Error reading ${uri}: ${error instanceof Error ? error.message : "Unknown error"}`
2545
+ }
2546
+ ]
2055
2547
  };
2056
2548
  }
2057
2549
  });
2058
2550
  async function main() {
2059
- console.error("\u{1F680} Starting MG Tickets MCP Bridge Server...");
2551
+ console.error("\u{1F680} Starting Refront MCP Bridge Server...");
2060
2552
  console.error(`\u{1F511} API Key: ${apiKey?.substring(0, 10)}...`);
2061
2553
  authContext = await validateApiKey(apiKey);
2062
2554
  if (!authContext) {
2063
- 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
+ );
2064
2558
  process.exit(1);
2065
2559
  }
2066
- 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
+ );
2067
2563
  console.error(`\u{1F4CB} Available scopes: ${authContext.scopes.join(", ")}`);
2068
2564
  console.error("\u{1F4E1} MCP Bridge Server ready for connections");
2069
2565
  const transport = new StdioServerTransport();