@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/README.md +115 -154
- package/dist/index.js +1842 -1346
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/dist/index.d.ts +0 -1
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
59
|
-
console.error(
|
|
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.
|
|
62
|
-
teamId: apiKeyData.
|
|
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
|
|
97
|
-
|
|
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
|
|
102
|
-
|
|
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
|
|
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 = [
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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?.
|
|
257
|
+
currentPhaseType = activePhase?.activityType ?? void 0;
|
|
137
258
|
}
|
|
138
259
|
if (!currentPhaseType) {
|
|
139
|
-
const analysisPhase = allPhases.find(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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(
|
|
269
|
+
const currentPhaseRecord = allPhases.find(
|
|
270
|
+
(p) => p.activityType === currentPhaseType && p.status === "in_progress"
|
|
271
|
+
);
|
|
150
272
|
if (currentPhaseRecord) {
|
|
151
|
-
const duration = Math.round(
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
}).eq(
|
|
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(
|
|
290
|
+
const nextPhase = allPhases.find(
|
|
291
|
+
(p) => p.activityType === nextPhaseType
|
|
292
|
+
);
|
|
167
293
|
if (!nextPhase) continue;
|
|
168
|
-
if (nextPhase.
|
|
169
|
-
await
|
|
170
|
-
console.error(
|
|
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
|
|
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: "
|
|
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: {
|
|
207
|
-
|
|
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: {
|
|
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: {
|
|
236
|
-
|
|
237
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: [
|
|
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: [
|
|
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: {
|
|
566
|
+
todoId: {
|
|
567
|
+
type: "string",
|
|
568
|
+
description: "Optional external todo ID for tracking"
|
|
569
|
+
},
|
|
378
570
|
content: { type: "string" },
|
|
379
|
-
status: {
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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: [
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
${
|
|
647
|
-
|
|
648
|
-
Status: ${
|
|
649
|
-
${
|
|
650
|
-
` : ""}${
|
|
651
|
-
` : ""}Created: ${new Date(
|
|
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
|
-
|
|
654
|
-
}]
|
|
655
|
-
};
|
|
934
|
+
).join("\n") || "No tickets found."}`
|
|
656
935
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
**${
|
|
733
|
-
Status: ${
|
|
734
|
-
Priority: ${
|
|
735
|
-
Type: ${
|
|
736
|
-
${
|
|
737
|
-
` : ""}${
|
|
738
|
-
` : ""}${
|
|
739
|
-
` : ""}${
|
|
740
|
-
` : ""}Requester: ${
|
|
741
|
-
Created: ${new Date(
|
|
742
|
-
${attachments
|
|
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
|
|
1026
|
+
` : ""}${comments.length > 0 ? `\u{1F4AC} Comments: ${comments.length}
|
|
745
1027
|
` : ""}`
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
${
|
|
887
|
-
|
|
888
|
-
${
|
|
889
|
-
` : ""}${
|
|
890
|
-
` : ""}Created: ${new Date(
|
|
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
|
-
|
|
893
|
-
}]
|
|
894
|
-
};
|
|
1225
|
+
).join("\n") || "No customers found."}`
|
|
895
1226
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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: ${
|
|
1245
|
+
Name: ${name}
|
|
912
1246
|
${email ? `Email: ${email}
|
|
913
1247
|
` : ""}${website ? `Website: ${website}
|
|
914
1248
|
` : ""}`
|
|
915
|
-
}]
|
|
916
|
-
};
|
|
917
1249
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
${
|
|
946
|
-
|
|
947
|
-
${
|
|
948
|
-
` : ""}Created: ${new Date(
|
|
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
|
-
|
|
951
|
-
}]
|
|
952
|
-
};
|
|
1288
|
+
).join("\n") || "No projects found."}`
|
|
953
1289
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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: ${
|
|
971
|
-
Status: ${status}
|
|
1308
|
+
Name: ${name}
|
|
972
1309
|
${description ? `Description: ${description}
|
|
973
1310
|
` : ""}`
|
|
974
|
-
}]
|
|
975
|
-
};
|
|
976
1311
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
${
|
|
1191
|
-
` : ""}${
|
|
1192
|
-
` : ""}${
|
|
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
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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.
|
|
1831
|
+
\u{1F3AB} **Ticket:** ${ticket.ticketNumber} - ${ticket.title}
|
|
1401
1832
|
\u{1F194} **Session:** ${aiSessionId} (${session.status})
|
|
1402
|
-
\u23F1\uFE0F **Time:** ${session.
|
|
1403
|
-
\u{1F4CB} **Todos:** ${
|
|
1404
|
-
\u{1F504} **Follow-ups:** ${
|
|
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
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
-
|
|
2035
|
+
responseText += `\u{1F194} Session: ${aiSessionId}
|
|
1567
2036
|
`;
|
|
1568
|
-
|
|
2037
|
+
responseText += `\u{1F4CA} **Performance Summary:**
|
|
1569
2038
|
`;
|
|
1570
|
-
|
|
2039
|
+
responseText += ` \u2022 Tasks Completed: ${workCompleted.length}
|
|
1571
2040
|
`;
|
|
1572
|
-
|
|
2041
|
+
responseText += ` \u2022 Time Spent: ${timeSpentMinutes} minutes
|
|
1573
2042
|
`;
|
|
1574
|
-
|
|
2043
|
+
responseText += ` \u2022 Estimated Time: ${session.aiTimeEstimateMinutes || "N/A"} minutes
|
|
1575
2044
|
`;
|
|
1576
|
-
|
|
2045
|
+
responseText += ` \u2022 Efficiency: ${efficiencyScore < 1 ? "\u{1F680}" : efficiencyScore > 1.5 ? "\u26A0\uFE0F" : "\u23F1\uFE0F"} ${(efficiencyScore * 100).toFixed(0)}%
|
|
1577
2046
|
`;
|
|
1578
|
-
|
|
2047
|
+
responseText += ` \u2022 Session Duration: ${sessionDuration} minutes
|
|
1579
2048
|
|
|
1580
2049
|
`;
|
|
1581
|
-
|
|
2050
|
+
responseText += `\u2705 **Work Completed:**
|
|
1582
2051
|
`;
|
|
1583
|
-
|
|
1584
|
-
|
|
2052
|
+
workCompleted.forEach((task, index) => {
|
|
2053
|
+
responseText += `${index + 1}. ${task}
|
|
1585
2054
|
`;
|
|
1586
|
-
|
|
1587
|
-
|
|
2055
|
+
});
|
|
2056
|
+
responseText += `
|
|
1588
2057
|
`;
|
|
1589
|
-
|
|
1590
|
-
|
|
2058
|
+
if (technicalSummary) {
|
|
2059
|
+
responseText += `\u{1F527} **Technical Summary:**
|
|
1591
2060
|
${technicalSummary}
|
|
1592
2061
|
|
|
1593
2062
|
`;
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2063
|
+
}
|
|
2064
|
+
if (efficiencyNotes) {
|
|
2065
|
+
responseText += `\u{1F4C8} **Efficiency Notes:**
|
|
1597
2066
|
${efficiencyNotes}
|
|
1598
2067
|
|
|
1599
2068
|
`;
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
2069
|
+
}
|
|
2070
|
+
if (agendaEventId) {
|
|
2071
|
+
responseText += `\u{1F4C5} **Timetrack Entry ${wasUpdated ? "Updated" : "Created"}:**
|
|
1603
2072
|
`;
|
|
1604
|
-
|
|
2073
|
+
responseText += ` \u2022 Agenda event ${wasUpdated ? "updated with final" : "created with"} work summary
|
|
1605
2074
|
`;
|
|
1606
|
-
|
|
2075
|
+
responseText += ` \u2022 Status: DRAFT (requires approval in agenda)
|
|
1607
2076
|
`;
|
|
1608
|
-
|
|
2077
|
+
responseText += ` \u2022 Duration: ${estimatedMinutes} minutes
|
|
1609
2078
|
`;
|
|
1610
|
-
|
|
2079
|
+
responseText += ` \u2022 Period: ${sessionStart.toLocaleString()} - ${completionTime.toLocaleString()}
|
|
1611
2080
|
|
|
1612
2081
|
`;
|
|
1613
|
-
|
|
1614
|
-
|
|
2082
|
+
}
|
|
2083
|
+
responseText += `\u{1F4CB} **Context for Customer Response:**
|
|
1615
2084
|
`;
|
|
1616
|
-
|
|
2085
|
+
responseText += ` \u2022 Use "get-completion-context" to retrieve full context
|
|
1617
2086
|
`;
|
|
1618
|
-
|
|
2087
|
+
responseText += ` \u2022 Generate customer-friendly response based on completed work
|
|
1619
2088
|
`;
|
|
1620
|
-
|
|
2089
|
+
responseText += ` \u2022 Focus on business value and customer benefits
|
|
1621
2090
|
|
|
1622
2091
|
`;
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
2263
|
+
if (wasUpdated) {
|
|
2264
|
+
responseText += `\u{1F504} **Updated existing draft entry** (avoiding duplicates)
|
|
1744
2265
|
`;
|
|
1745
|
-
|
|
2266
|
+
responseText += ` \u2022 New total: ${Math.round((agendaEntry.trackedDuration ?? 0) / 3600 * 10) / 10}h
|
|
1746
2267
|
|
|
1747
2268
|
`;
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2269
|
+
}
|
|
2270
|
+
if (consolidatedCount > 0) {
|
|
2271
|
+
responseText += `\u{1F9F9} **Cleaned up ${consolidatedCount} duplicate entries**
|
|
1751
2272
|
|
|
1752
2273
|
`;
|
|
1753
|
-
|
|
1754
|
-
|
|
2274
|
+
}
|
|
2275
|
+
responseText += `\u{1F4CB} **Entry Details:**
|
|
1755
2276
|
`;
|
|
1756
|
-
|
|
1757
|
-
responseText += ` \u2022 Project: ${project.name}
|
|
2277
|
+
responseText += ` \u2022 Project: ${project ? project.name : "(No project assigned)"}
|
|
1758
2278
|
`;
|
|
1759
|
-
|
|
1760
|
-
responseText += ` \u2022 Project: (No project assigned)
|
|
2279
|
+
if (ticket) responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
|
|
1761
2280
|
`;
|
|
1762
|
-
|
|
1763
|
-
|
|
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
|
-
|
|
2288
|
+
responseText += ` \u2022 Status: DRAFT (not billed yet)
|
|
1774
2289
|
`;
|
|
1775
|
-
|
|
1776
|
-
`;
|
|
1777
|
-
responseText += ` \u2022 Entry ID: ${agendaEntry.id}
|
|
2290
|
+
responseText += ` \u2022 Entry ID: ${agendaEntry.id}
|
|
1778
2291
|
|
|
1779
2292
|
`;
|
|
1780
|
-
|
|
1781
|
-
|
|
2293
|
+
if (chatContextSummary) {
|
|
2294
|
+
responseText += `\u{1F4CA} **Work Context:**
|
|
1782
2295
|
`;
|
|
1783
|
-
|
|
2296
|
+
responseText += `${chatContextSummary.substring(0, 200)}${chatContextSummary.length > 200 ? "..." : ""}
|
|
1784
2297
|
|
|
1785
2298
|
`;
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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:
|
|
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
|
-
|
|
1825
|
-
|
|
2332
|
+
]
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
const content = Buffer.from(data.content, "base64").toString("utf-8");
|
|
2336
|
+
let responseText = `\u{1F4C4} **File: ${filePath}**
|
|
1826
2337
|
`;
|
|
1827
|
-
|
|
2338
|
+
responseText += `Repository: ${githubInfo.repositoryFullName}
|
|
1828
2339
|
`;
|
|
1829
|
-
|
|
2340
|
+
responseText += `Size: ${data.size} bytes
|
|
1830
2341
|
`;
|
|
1831
|
-
|
|
2342
|
+
responseText += `URL: ${data.html_url}
|
|
1832
2343
|
|
|
1833
2344
|
`;
|
|
1834
|
-
|
|
2345
|
+
responseText += `**Content:**
|
|
1835
2346
|
\`\`\`
|
|
1836
2347
|
${content}
|
|
1837
2348
|
\`\`\``;
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
-
|
|
2396
|
+
]
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
let responseText = `\u{1F4C1} **Directory: ${directoryPath || "(root)"}**
|
|
1892
2400
|
`;
|
|
1893
|
-
|
|
2401
|
+
responseText += `Repository: ${githubInfo.repositoryFullName}
|
|
1894
2402
|
`;
|
|
1895
|
-
|
|
2403
|
+
responseText += `Items: ${data.length}
|
|
1896
2404
|
|
|
1897
2405
|
`;
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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
|
-
|
|
1904
|
-
responseText += ` - ${dir.name}/
|
|
2411
|
+
for (const dir of directories) responseText += ` - ${dir.name}/
|
|
1905
2412
|
`;
|
|
1906
|
-
|
|
1907
|
-
responseText += `
|
|
2413
|
+
responseText += `
|
|
1908
2414
|
`;
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2415
|
+
}
|
|
2416
|
+
if (files.length > 0) {
|
|
2417
|
+
responseText += `**\u{1F4C4} Files (${files.length}):**
|
|
1912
2418
|
`;
|
|
1913
|
-
|
|
1914
|
-
|
|
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(
|
|
1947
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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(
|
|
1972
|
-
const projectIds = await getAccessibleProjectIds(
|
|
1973
|
-
const customerIds = await getAccessibleCustomerIds(
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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(
|
|
2487
|
+
const customerIds = await getAccessibleCustomerIds(ctx.teamId);
|
|
1997
2488
|
if (customerIds.length === 0) {
|
|
1998
2489
|
return {
|
|
1999
|
-
contents: [
|
|
2000
|
-
uri,
|
|
2001
|
-
|
|
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
|
|
2007
|
-
|
|
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
|
-
|
|
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(
|
|
2509
|
+
const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
|
|
2018
2510
|
if (projectIds.length === 0) {
|
|
2019
2511
|
return {
|
|
2020
|
-
contents: [
|
|
2021
|
-
uri,
|
|
2022
|
-
|
|
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
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
-
|
|
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(
|
|
2538
|
+
console.error("\u274C Resource read error:", error);
|
|
2049
2539
|
return {
|
|
2050
|
-
contents: [
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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();
|