@mgsoftwarebv/mcp-server-bridge 2.23.4 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -154
- package/dist/index.js +1842 -1355
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -2,45 +2,176 @@
|
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { ListToolsRequestSchema, ListResourcesRequestSchema, CallToolRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
-
import { createClient } from '@supabase/supabase-js';
|
|
6
|
-
import { createHash } from 'crypto';
|
|
7
5
|
import { Octokit } from '@octokit/rest';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { eq, or, ilike, and, desc, asc, inArray, sql } from 'drizzle-orm';
|
|
8
|
+
import { createJobDb } from '@refront/db/job-client';
|
|
9
|
+
import * as schema from '@refront/db/schema';
|
|
10
|
+
import { createStorageClient } from '@refront/storage';
|
|
11
|
+
|
|
12
|
+
var _client = null;
|
|
13
|
+
function getClient() {
|
|
14
|
+
if (!_client) {
|
|
15
|
+
if (!process.env.DATABASE_PRIMARY_POOLER_URL) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"DATABASE_PRIMARY_POOLER_URL is not set. Pass --database-url=... or export the env var before starting the MCP bridge."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
_client = createJobDb();
|
|
21
|
+
}
|
|
22
|
+
return _client;
|
|
23
|
+
}
|
|
24
|
+
var db = new Proxy({}, {
|
|
25
|
+
get(_target, prop) {
|
|
26
|
+
const real = getClient().db;
|
|
27
|
+
const value = Reflect.get(real, prop, real);
|
|
28
|
+
return typeof value === "function" ? value.bind(real) : value;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
async function getAccessibleTeamIds(teamId) {
|
|
32
|
+
const rows = await db.select({ id: schema.teams.id }).from(schema.teams).where(
|
|
33
|
+
or(eq(schema.teams.id, teamId), eq(schema.teams.parentTeamId, teamId))
|
|
34
|
+
);
|
|
35
|
+
const ids = rows.map((r) => r.id);
|
|
36
|
+
return ids.length > 0 ? ids : [teamId];
|
|
37
|
+
}
|
|
38
|
+
async function getAccessibleProjectIds(userId, teamId) {
|
|
39
|
+
try {
|
|
40
|
+
const rows = await db.execute(
|
|
41
|
+
sql`SELECT project_id FROM get_accessible_project_ids(${userId}::uuid, ${teamId}::uuid)`
|
|
42
|
+
);
|
|
43
|
+
return rows.map((r) => r.project_id).filter(Boolean);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("\u274C Error getting accessible project IDs:", error);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function getAccessibleCustomerIds(teamId) {
|
|
50
|
+
const teamIds = await getAccessibleTeamIds(teamId);
|
|
51
|
+
const ownCustomers = await db.select({ id: schema.customers.id }).from(schema.customers).where(
|
|
52
|
+
teamIds.length === 1 ? eq(schema.customers.teamId, teamIds[0]) : sql`${schema.customers.teamId} = ANY(${teamIds}::uuid[])`
|
|
53
|
+
);
|
|
54
|
+
const sharedCustomers = await db.select({ customerId: schema.customerSharedTeams.customerId }).from(schema.customerSharedTeams).where(eq(schema.customerSharedTeams.teamId, teamId));
|
|
55
|
+
return [
|
|
56
|
+
.../* @__PURE__ */ new Set([
|
|
57
|
+
...ownCustomers.map((c) => c.id),
|
|
58
|
+
...sharedCustomers.map((c) => c.customerId)
|
|
59
|
+
])
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
async function resolveAiSessionId(prefix, teamIds) {
|
|
63
|
+
if (teamIds.length === 0) return null;
|
|
64
|
+
const rows = await db.select({ id: schema.aiSessions.id }).from(schema.aiSessions).where(
|
|
65
|
+
and(
|
|
66
|
+
teamIds.length === 1 ? eq(schema.aiSessions.teamId, teamIds[0]) : sql`${schema.aiSessions.teamId} = ANY(${teamIds}::uuid[])`,
|
|
67
|
+
sql`${schema.aiSessions.id}::text LIKE ${`${prefix}%`}`
|
|
68
|
+
)
|
|
69
|
+
).limit(1);
|
|
70
|
+
return rows[0]?.id ?? null;
|
|
71
|
+
}
|
|
72
|
+
var _storage = null;
|
|
73
|
+
function buildClient() {
|
|
74
|
+
const endpoint = process.env.R2_ENDPOINT;
|
|
75
|
+
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
|
|
76
|
+
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
|
|
77
|
+
if (!endpoint || !accessKeyId || !secretAccessKey) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"R2 storage is not configured. Set R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return createStorageClient({
|
|
83
|
+
endpoint,
|
|
84
|
+
accessKeyId,
|
|
85
|
+
secretAccessKey,
|
|
86
|
+
publicDomain: process.env.R2_PUBLIC_DOMAIN || void 0,
|
|
87
|
+
publicBuckets: [
|
|
88
|
+
"vault",
|
|
89
|
+
"avatars",
|
|
90
|
+
"team-logos",
|
|
91
|
+
"blog-images",
|
|
92
|
+
"customer-assets"
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
var storage = new Proxy({}, {
|
|
97
|
+
get(_target, prop) {
|
|
98
|
+
if (!_storage) _storage = buildClient();
|
|
99
|
+
return Reflect.get(_storage, prop, _storage);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
8
102
|
|
|
103
|
+
// src/index.ts
|
|
9
104
|
var args = process.argv.slice(2);
|
|
10
|
-
|
|
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
|
+
);
|
|
15
116
|
process.exit(1);
|
|
16
117
|
}
|
|
17
|
-
|
|
118
|
+
if (!databaseUrl) {
|
|
119
|
+
console.error(
|
|
120
|
+
"\u274C Database URL is required. Use --database-url=postgresql://... or set DATABASE_PRIMARY_POOLER_URL (or DATABASE_URL) environment variable."
|
|
121
|
+
);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
process.env.DATABASE_PRIMARY_POOLER_URL = databaseUrl;
|
|
125
|
+
function asToolArgs(input) {
|
|
126
|
+
return input ?? {};
|
|
127
|
+
}
|
|
18
128
|
function roundToNearest15Minutes(minutes) {
|
|
19
129
|
if (minutes <= 0) return 0;
|
|
20
130
|
return Math.round(minutes / 15) * 15;
|
|
21
131
|
}
|
|
22
|
-
|
|
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,276 +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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
commentAttachments = commAttachments || [];
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
const content = [{
|
|
738
|
-
type: "text",
|
|
739
|
-
text: `**Ticket Details:**
|
|
936
|
+
]
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
async function handleGetTicketById(input) {
|
|
940
|
+
const ctx = authContext;
|
|
941
|
+
const { id } = input;
|
|
942
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
943
|
+
const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
|
|
944
|
+
const customerIds = await getAccessibleCustomerIds(ctx.teamId);
|
|
945
|
+
const ticketRow = await db.query.tickets.findFirst({
|
|
946
|
+
where: eq(schema.tickets.id, id),
|
|
947
|
+
with: {
|
|
948
|
+
project: { columns: { id: true, name: true } },
|
|
949
|
+
customer: { columns: { id: true, name: true } },
|
|
950
|
+
assignee: { columns: { id: true, fullName: true, email: true } },
|
|
951
|
+
requester: { columns: { id: true, fullName: true, email: true } }
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
if (!ticketRow) {
|
|
955
|
+
throw new Error(`Ticket not found: ${id}`);
|
|
956
|
+
}
|
|
957
|
+
let hasAccess = false;
|
|
958
|
+
if (teamIds.includes(ticketRow.teamId)) hasAccess = true;
|
|
959
|
+
if (!hasAccess && ticketRow.projectId && projectIds.includes(ticketRow.projectId))
|
|
960
|
+
hasAccess = true;
|
|
961
|
+
if (!hasAccess && ticketRow.customerId && customerIds.includes(ticketRow.customerId))
|
|
962
|
+
hasAccess = true;
|
|
963
|
+
if (!hasAccess) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
"Access denied: You do not have permission to view this ticket"
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const attachments = await db.select({
|
|
969
|
+
id: schema.ticketAttachments.id,
|
|
970
|
+
fileName: schema.ticketAttachments.fileName,
|
|
971
|
+
fileSize: schema.ticketAttachments.fileSize,
|
|
972
|
+
mimeType: schema.ticketAttachments.mimeType,
|
|
973
|
+
storageKey: schema.ticketAttachments.storageKey,
|
|
974
|
+
createdAt: schema.ticketAttachments.createdAt,
|
|
975
|
+
uploaderId: schema.ticketAttachments.userId,
|
|
976
|
+
uploaderName: schema.users.fullName
|
|
977
|
+
}).from(schema.ticketAttachments).leftJoin(
|
|
978
|
+
schema.users,
|
|
979
|
+
eq(schema.users.id, schema.ticketAttachments.userId)
|
|
980
|
+
).where(eq(schema.ticketAttachments.ticketId, id)).orderBy(asc(schema.ticketAttachments.createdAt));
|
|
981
|
+
const comments = await db.select({
|
|
982
|
+
id: schema.ticketComments.id,
|
|
983
|
+
content: schema.ticketComments.content,
|
|
984
|
+
createdAt: schema.ticketComments.createdAt,
|
|
985
|
+
userId: schema.ticketComments.userId
|
|
986
|
+
}).from(schema.ticketComments).where(eq(schema.ticketComments.ticketId, id)).orderBy(asc(schema.ticketComments.createdAt));
|
|
987
|
+
const commentUserIds = [
|
|
988
|
+
...new Set(
|
|
989
|
+
comments.map((c) => c.userId).filter((v) => Boolean(v))
|
|
990
|
+
)
|
|
991
|
+
];
|
|
992
|
+
const commentUserMap = /* @__PURE__ */ new Map();
|
|
993
|
+
if (commentUserIds.length > 0) {
|
|
994
|
+
const commentUsers = await db.select({ id: schema.users.id, fullName: schema.users.fullName }).from(schema.users).where(inArray(schema.users.id, commentUserIds));
|
|
995
|
+
commentUsers.forEach((u) => commentUserMap.set(u.id, u));
|
|
996
|
+
}
|
|
997
|
+
const commentIds = comments.map((c) => c.id);
|
|
998
|
+
const commentAttachments = commentIds.length > 0 ? await db.select({
|
|
999
|
+
id: schema.ticketCommentAttachments.id,
|
|
1000
|
+
commentId: schema.ticketCommentAttachments.commentId,
|
|
1001
|
+
fileName: schema.ticketCommentAttachments.fileName,
|
|
1002
|
+
fileSize: schema.ticketCommentAttachments.fileSize,
|
|
1003
|
+
mimeType: schema.ticketCommentAttachments.mimeType,
|
|
1004
|
+
storageKey: schema.ticketCommentAttachments.storageKey,
|
|
1005
|
+
createdAt: schema.ticketCommentAttachments.createdAt
|
|
1006
|
+
}).from(schema.ticketCommentAttachments).where(
|
|
1007
|
+
inArray(schema.ticketCommentAttachments.commentId, commentIds)
|
|
1008
|
+
) : [];
|
|
1009
|
+
const content = [
|
|
1010
|
+
{
|
|
1011
|
+
type: "text",
|
|
1012
|
+
text: `**Ticket Details:**
|
|
740
1013
|
|
|
741
|
-
**${
|
|
742
|
-
Status: ${
|
|
743
|
-
Priority: ${
|
|
744
|
-
Type: ${
|
|
745
|
-
${
|
|
746
|
-
` : ""}${
|
|
747
|
-
` : ""}${
|
|
748
|
-
` : ""}${
|
|
749
|
-
` : ""}Requester: ${
|
|
750
|
-
Created: ${new Date(
|
|
751
|
-
${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 ? `
|
|
752
1025
|
\u{1F4CE} Attachments: ${attachments.length}
|
|
753
|
-
` : ""}${comments
|
|
1026
|
+
` : ""}${comments.length > 0 ? `\u{1F4AC} Comments: ${comments.length}
|
|
754
1027
|
` : ""}`
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1028
|
+
}
|
|
1029
|
+
];
|
|
1030
|
+
if (attachments.length > 0) {
|
|
1031
|
+
console.error(`\u{1F4CE} Processing ${attachments.length} ticket attachments...`);
|
|
1032
|
+
for (const attachment of attachments) {
|
|
1033
|
+
if (isImageFile(attachment.mimeType)) {
|
|
1034
|
+
console.error(`\u{1F5BC}\uFE0F Downloading image: ${attachment.fileName}`);
|
|
1035
|
+
const base64 = await downloadImageAsBase64(attachment.storageKey);
|
|
1036
|
+
if (base64) {
|
|
1037
|
+
content.push({
|
|
1038
|
+
type: "image",
|
|
1039
|
+
data: base64,
|
|
1040
|
+
mimeType: attachment.mimeType
|
|
1041
|
+
});
|
|
1042
|
+
content.push({
|
|
1043
|
+
type: "text",
|
|
1044
|
+
text: `
|
|
1045
|
+
\u{1F4F8} **Image from ticket**: ${attachment.fileName} (${Math.round(
|
|
1046
|
+
attachment.fileSize / 1024
|
|
1047
|
+
)}KB, uploaded by ${attachment.uploaderName || "Unknown"} on ${new Date(
|
|
1048
|
+
attachment.createdAt
|
|
1049
|
+
).toLocaleDateString()})
|
|
772
1050
|
`
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
1051
|
+
});
|
|
777
1052
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (commentAttachments.length > 0) {
|
|
1057
|
+
console.error(
|
|
1058
|
+
`\u{1F4CE} Processing ${commentAttachments.length} comment attachments...`
|
|
1059
|
+
);
|
|
1060
|
+
for (const attachment of commentAttachments) {
|
|
1061
|
+
if (isImageFile(attachment.mimeType)) {
|
|
1062
|
+
console.error(
|
|
1063
|
+
`\u{1F5BC}\uFE0F Downloading comment image: ${attachment.fileName}`
|
|
1064
|
+
);
|
|
1065
|
+
const base64 = await downloadImageAsBase64(attachment.storageKey);
|
|
1066
|
+
if (base64) {
|
|
1067
|
+
const comment = comments.find((c) => c.id === attachment.commentId);
|
|
1068
|
+
const author = comment?.userId ? commentUserMap.get(comment.userId)?.fullName : null;
|
|
1069
|
+
content.push({
|
|
1070
|
+
type: "image",
|
|
1071
|
+
data: base64,
|
|
1072
|
+
mimeType: attachment.mimeType
|
|
1073
|
+
});
|
|
1074
|
+
content.push({
|
|
1075
|
+
type: "text",
|
|
1076
|
+
text: `
|
|
1077
|
+
\u{1F4F8} **Image from comment** by ${author || "Unknown"} on ${new Date(
|
|
1078
|
+
attachment.createdAt
|
|
1079
|
+
).toLocaleDateString()}: ${attachment.fileName} (${Math.round(
|
|
1080
|
+
attachment.fileSize / 1024
|
|
1081
|
+
)}KB)
|
|
795
1082
|
` + (comment?.content ? `Comment text: "${comment.content.substring(0, 100)}${comment.content.length > 100 ? "..." : ""}"
|
|
796
1083
|
` : "")
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
1084
|
+
});
|
|
801
1085
|
}
|
|
802
|
-
console.error(`\u2705 Returning ticket with ${content.filter((c) => c.type === "image").length} images`);
|
|
803
|
-
return { content };
|
|
804
1086
|
}
|
|
805
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const { count } = await supabase.from("tickets").select("*", { count: "exact", head: true }).eq("team_id", resolvedTeamId);
|
|
846
|
-
ticketNumber = `${year}-${String((count || 0) + 1).padStart(3, "0")}`;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
console.error(
|
|
1090
|
+
`\u2705 Returning ticket with ${content.filter((c) => c.type === "image").length} images`
|
|
1091
|
+
);
|
|
1092
|
+
return { content };
|
|
1093
|
+
}
|
|
1094
|
+
async function handleCreateTicket(input) {
|
|
1095
|
+
const ctx = authContext;
|
|
1096
|
+
const {
|
|
1097
|
+
title,
|
|
1098
|
+
description,
|
|
1099
|
+
status = "open",
|
|
1100
|
+
priority = "medium",
|
|
1101
|
+
type = "task",
|
|
1102
|
+
projectId,
|
|
1103
|
+
customerId
|
|
1104
|
+
} = input;
|
|
1105
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
1106
|
+
let resolvedTeamId = ctx.teamId;
|
|
1107
|
+
let resolvedCustomerId = customerId;
|
|
1108
|
+
let projectAbbreviation = "";
|
|
1109
|
+
if (projectId) {
|
|
1110
|
+
const [project] = await db.select({
|
|
1111
|
+
name: schema.projects.name,
|
|
1112
|
+
teamId: schema.projects.teamId,
|
|
1113
|
+
customerId: schema.projects.customerId
|
|
1114
|
+
}).from(schema.projects).where(eq(schema.projects.id, projectId)).limit(1);
|
|
1115
|
+
if (project) {
|
|
1116
|
+
if (project.teamId) resolvedTeamId = project.teamId;
|
|
1117
|
+
if (!resolvedCustomerId && project.customerId) {
|
|
1118
|
+
resolvedCustomerId = project.customerId;
|
|
1119
|
+
}
|
|
1120
|
+
if (project.name) {
|
|
1121
|
+
const upper = project.name.toUpperCase().replace(/[^A-Z0-9\s]/g, "");
|
|
1122
|
+
const words = upper.split(/\s+/).filter(Boolean);
|
|
1123
|
+
if (words.length >= 2) {
|
|
1124
|
+
projectAbbreviation = words.slice(0, 2).map((w) => w.substring(0, 3)).join("").substring(0, 5);
|
|
1125
|
+
} else if (words.length === 1 && words[0]) {
|
|
1126
|
+
projectAbbreviation = words[0].substring(0, 5);
|
|
847
1127
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
let ticketNumber;
|
|
1132
|
+
if (projectId && projectAbbreviation) {
|
|
1133
|
+
const pattern = `${year}-${projectAbbreviation}-%`;
|
|
1134
|
+
const [highest] = await db.select({ ticketNumber: schema.tickets.ticketNumber }).from(schema.tickets).where(
|
|
1135
|
+
and(
|
|
1136
|
+
eq(schema.tickets.projectId, projectId),
|
|
1137
|
+
ilike(schema.tickets.ticketNumber, pattern)
|
|
1138
|
+
)
|
|
1139
|
+
).orderBy(desc(schema.tickets.ticketNumber)).limit(1);
|
|
1140
|
+
let nextSequence = 1;
|
|
1141
|
+
if (highest?.ticketNumber) {
|
|
1142
|
+
const parts = highest.ticketNumber.split("-");
|
|
1143
|
+
if (parts.length === 3 && parts[2]) {
|
|
1144
|
+
const lastSeq = Number.parseInt(parts[2], 10);
|
|
1145
|
+
if (!Number.isNaN(lastSeq)) nextSequence = lastSeq + 1;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
ticketNumber = `${year}-${projectAbbreviation}-${String(nextSequence).padStart(3, "0")}`;
|
|
1149
|
+
} else {
|
|
1150
|
+
const [countRow] = await db.select({ n: sql`count(*)::int` }).from(schema.tickets).where(eq(schema.tickets.teamId, resolvedTeamId));
|
|
1151
|
+
const count = Number(countRow?.n ?? 0);
|
|
1152
|
+
ticketNumber = `${year}-${String(count + 1).padStart(3, "0")}`;
|
|
1153
|
+
}
|
|
1154
|
+
await db.insert(schema.tickets).values({
|
|
1155
|
+
teamId: resolvedTeamId,
|
|
1156
|
+
ticketNumber,
|
|
1157
|
+
title,
|
|
1158
|
+
description: description ?? null,
|
|
1159
|
+
status,
|
|
1160
|
+
priority,
|
|
1161
|
+
type,
|
|
1162
|
+
projectId: projectId ?? null,
|
|
1163
|
+
customerId: resolvedCustomerId ?? null,
|
|
1164
|
+
requesterId: ctx.userId
|
|
1165
|
+
});
|
|
1166
|
+
return {
|
|
1167
|
+
content: [
|
|
1168
|
+
{
|
|
1169
|
+
type: "text",
|
|
1170
|
+
text: `\u2705 **Ticket Created Successfully!**
|
|
865
1171
|
|
|
866
1172
|
Ticket Number: **${ticketNumber}**
|
|
867
1173
|
Title: ${title}
|
|
@@ -869,143 +1175,179 @@ Status: ${status}
|
|
|
869
1175
|
Priority: ${priority}
|
|
870
1176
|
Type: ${type}
|
|
871
1177
|
`
|
|
872
|
-
}]
|
|
873
|
-
};
|
|
874
1178
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1179
|
+
]
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
async function handleGetCustomers(input) {
|
|
1183
|
+
const ctx = authContext;
|
|
1184
|
+
const { q, pageSize = 20 } = input;
|
|
1185
|
+
const customerIds = await getAccessibleCustomerIds(ctx.teamId);
|
|
1186
|
+
if (customerIds.length === 0) {
|
|
1187
|
+
return {
|
|
1188
|
+
content: [
|
|
1189
|
+
{
|
|
1190
|
+
type: "text",
|
|
1191
|
+
text: "No customers found or no access to any customers."
|
|
885
1192
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1193
|
+
]
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
const filters = [inArray(schema.customers.id, customerIds)];
|
|
1197
|
+
if (q) {
|
|
1198
|
+
const pattern = `%${q}%`;
|
|
1199
|
+
filters.push(
|
|
1200
|
+
or(
|
|
1201
|
+
ilike(schema.customers.name, pattern),
|
|
1202
|
+
ilike(schema.customers.email, pattern)
|
|
1203
|
+
)
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
const rows = await db.select({
|
|
1207
|
+
id: schema.customers.id,
|
|
1208
|
+
name: schema.customers.name,
|
|
1209
|
+
email: schema.customers.email,
|
|
1210
|
+
website: schema.customers.website,
|
|
1211
|
+
createdAt: schema.customers.createdAt
|
|
1212
|
+
}).from(schema.customers).where(and(...filters)).orderBy(asc(schema.customers.name)).limit(Math.min(pageSize, 100));
|
|
1213
|
+
return {
|
|
1214
|
+
content: [
|
|
1215
|
+
{
|
|
1216
|
+
type: "text",
|
|
1217
|
+
text: `Found ${rows.length} customers:
|
|
894
1218
|
|
|
895
|
-
${
|
|
896
|
-
|
|
897
|
-
${
|
|
898
|
-
` : ""}${
|
|
899
|
-
` : ""}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()}
|
|
900
1224
|
`
|
|
901
|
-
|
|
902
|
-
}]
|
|
903
|
-
};
|
|
1225
|
+
).join("\n") || "No customers found."}`
|
|
904
1226
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1227
|
+
]
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
async function handleCreateCustomer(input) {
|
|
1231
|
+
const ctx = authContext;
|
|
1232
|
+
const { name, email, website } = input;
|
|
1233
|
+
await db.insert(schema.customers).values({
|
|
1234
|
+
teamId: ctx.teamId,
|
|
1235
|
+
name,
|
|
1236
|
+
email: email ?? "",
|
|
1237
|
+
website: website ?? null
|
|
1238
|
+
});
|
|
1239
|
+
return {
|
|
1240
|
+
content: [
|
|
1241
|
+
{
|
|
1242
|
+
type: "text",
|
|
1243
|
+
text: `\u2705 **Customer Created Successfully!**
|
|
919
1244
|
|
|
920
|
-
Name: ${
|
|
1245
|
+
Name: ${name}
|
|
921
1246
|
${email ? `Email: ${email}
|
|
922
1247
|
` : ""}${website ? `Website: ${website}
|
|
923
1248
|
` : ""}`
|
|
924
|
-
}]
|
|
925
|
-
};
|
|
926
1249
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1250
|
+
]
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
async function handleGetProjects(input) {
|
|
1254
|
+
const ctx = authContext;
|
|
1255
|
+
const { customerId, q, pageSize = 20 } = input;
|
|
1256
|
+
const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
|
|
1257
|
+
if (projectIds.length === 0) {
|
|
1258
|
+
return {
|
|
1259
|
+
content: [
|
|
1260
|
+
{
|
|
1261
|
+
type: "text",
|
|
1262
|
+
text: "No projects found or no access to any projects."
|
|
937
1263
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1264
|
+
]
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
const filters = [inArray(schema.projects.id, projectIds)];
|
|
1268
|
+
if (customerId) filters.push(eq(schema.projects.customerId, customerId));
|
|
1269
|
+
if (q) filters.push(ilike(schema.projects.name, `%${q}%`));
|
|
1270
|
+
const rows = await db.select({
|
|
1271
|
+
id: schema.projects.id,
|
|
1272
|
+
name: schema.projects.name,
|
|
1273
|
+
description: schema.projects.description,
|
|
1274
|
+
customerId: schema.projects.customerId,
|
|
1275
|
+
createdAt: schema.projects.createdAt
|
|
1276
|
+
}).from(schema.projects).where(and(...filters)).orderBy(asc(schema.projects.name)).limit(Math.min(pageSize, 100));
|
|
1277
|
+
return {
|
|
1278
|
+
content: [
|
|
1279
|
+
{
|
|
1280
|
+
type: "text",
|
|
1281
|
+
text: `Found ${rows.length} projects:
|
|
953
1282
|
|
|
954
|
-
${
|
|
955
|
-
|
|
956
|
-
${
|
|
957
|
-
` : ""}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()}
|
|
958
1287
|
`
|
|
959
|
-
|
|
960
|
-
}]
|
|
961
|
-
};
|
|
1288
|
+
).join("\n") || "No projects found."}`
|
|
962
1289
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1290
|
+
]
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
async function handleCreateProject(input) {
|
|
1294
|
+
const ctx = authContext;
|
|
1295
|
+
const { name, description, customerId } = input;
|
|
1296
|
+
await db.insert(schema.projects).values({
|
|
1297
|
+
teamId: ctx.teamId,
|
|
1298
|
+
name,
|
|
1299
|
+
description: description ?? null,
|
|
1300
|
+
customerId: customerId ?? null
|
|
1301
|
+
});
|
|
1302
|
+
return {
|
|
1303
|
+
content: [
|
|
1304
|
+
{
|
|
1305
|
+
type: "text",
|
|
1306
|
+
text: `\u2705 **Project Created Successfully!**
|
|
978
1307
|
|
|
979
|
-
Name: ${
|
|
980
|
-
Status: ${status}
|
|
1308
|
+
Name: ${name}
|
|
981
1309
|
${description ? `Description: ${description}
|
|
982
1310
|
` : ""}`
|
|
983
|
-
}]
|
|
984
|
-
};
|
|
985
1311
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1312
|
+
]
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
async function handleStartAiSession(input) {
|
|
1316
|
+
const ctx = authContext;
|
|
1317
|
+
const {
|
|
1318
|
+
ticketId,
|
|
1319
|
+
cursorSessionId,
|
|
1320
|
+
totalEstimatedMinutes,
|
|
1321
|
+
complexityScore
|
|
1322
|
+
} = input;
|
|
1323
|
+
if (!totalEstimatedMinutes) {
|
|
1324
|
+
throw new Error("totalEstimatedMinutes is required");
|
|
1325
|
+
}
|
|
1326
|
+
const roundedMinutes = roundToNearest15Minutes(totalEstimatedMinutes);
|
|
1327
|
+
const sessionStartTime = /* @__PURE__ */ new Date();
|
|
1328
|
+
const [sessionData] = await db.insert(schema.aiSessions).values({
|
|
1329
|
+
ticketId,
|
|
1330
|
+
providerUserId: ctx.userId,
|
|
1331
|
+
teamId: ctx.teamId,
|
|
1332
|
+
cursorSessionId: cursorSessionId ?? null,
|
|
1333
|
+
aiTimeEstimateMinutes: roundedMinutes,
|
|
1334
|
+
complexityScore: complexityScore ?? null,
|
|
1335
|
+
status: "in_progress"
|
|
1336
|
+
}).returning({
|
|
1337
|
+
id: schema.aiSessions.id,
|
|
1338
|
+
ticketId: schema.aiSessions.ticketId,
|
|
1339
|
+
cursorSessionId: schema.aiSessions.cursorSessionId,
|
|
1340
|
+
createdAt: schema.aiSessions.createdAt
|
|
1341
|
+
});
|
|
1342
|
+
if (!sessionData) {
|
|
1343
|
+
throw new Error("Failed to create AI session");
|
|
1344
|
+
}
|
|
1345
|
+
const sessionId = `ai-sess-${sessionData.id.substring(0, 8)}`;
|
|
1346
|
+
return {
|
|
1347
|
+
content: [
|
|
1348
|
+
{
|
|
1349
|
+
type: "text",
|
|
1350
|
+
text: `\u{1F680} **AI Session Started!**
|
|
1009
1351
|
|
|
1010
1352
|
\u{1F194} Session ID: **${sessionId}**
|
|
1011
1353
|
\u{1F3AB} Ticket: ${ticketId}
|
|
@@ -1014,115 +1356,143 @@ ${complexityScore ? `\u{1F3AF} Complexity: ${complexityScore}/10
|
|
|
1014
1356
|
` : ""}\u{1F4C5} Started: ${sessionStartTime.toLocaleString()}
|
|
1015
1357
|
|
|
1016
1358
|
\u{1F4DD} Timetrack entry will be created when you complete the session.`
|
|
1017
|
-
}]
|
|
1018
|
-
};
|
|
1019
1359
|
}
|
|
1020
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1360
|
+
]
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
async function handleTrackManualFollowUp(input) {
|
|
1364
|
+
const ctx = authContext;
|
|
1365
|
+
const {
|
|
1366
|
+
aiSessionId,
|
|
1367
|
+
originalPrompt,
|
|
1368
|
+
aiResponse,
|
|
1369
|
+
developerFollowUp,
|
|
1370
|
+
followUpReason,
|
|
1371
|
+
outcome = "success",
|
|
1372
|
+
estimatedMinutes,
|
|
1373
|
+
workDescription
|
|
1374
|
+
} = input;
|
|
1375
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1376
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1377
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1378
|
+
if (!fullSessionId) {
|
|
1379
|
+
throw new Error(`Session not found: ${aiSessionId}`);
|
|
1380
|
+
}
|
|
1381
|
+
const [session] = await db.select({
|
|
1382
|
+
id: schema.aiSessions.id,
|
|
1383
|
+
status: schema.aiSessions.status,
|
|
1384
|
+
createdAt: schema.aiSessions.createdAt,
|
|
1385
|
+
aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes
|
|
1386
|
+
}).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
|
|
1387
|
+
if (!session) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1388
|
+
const followUpTime = /* @__PURE__ */ new Date();
|
|
1389
|
+
const oldEstimate = session.aiTimeEstimateMinutes ?? 60;
|
|
1390
|
+
const roundedFollowUpMinutes = roundToNearest15Minutes(
|
|
1391
|
+
estimatedMinutes || 0
|
|
1392
|
+
);
|
|
1393
|
+
const newEstimate = oldEstimate + roundedFollowUpMinutes;
|
|
1394
|
+
await db.update(schema.aiSessions).set({
|
|
1395
|
+
status: "in_progress",
|
|
1396
|
+
aiTimeEstimateMinutes: newEstimate
|
|
1397
|
+
}).where(eq(schema.aiSessions.id, session.id));
|
|
1398
|
+
await db.insert(schema.manualFollowUps).values({
|
|
1399
|
+
aiSessionId: session.id,
|
|
1400
|
+
developerId: ctx.userId,
|
|
1401
|
+
teamId: ctx.teamId,
|
|
1402
|
+
originalPrompt,
|
|
1403
|
+
aiResponse,
|
|
1404
|
+
followUpPrompt: developerFollowUp,
|
|
1405
|
+
followUpReason,
|
|
1406
|
+
outcome,
|
|
1407
|
+
timeSpentMinutes: null,
|
|
1408
|
+
resolvedAt: outcome === "success" ? (/* @__PURE__ */ new Date()).toISOString() : null
|
|
1409
|
+
});
|
|
1410
|
+
await db.insert(schema.aiTimeLogs).values({
|
|
1411
|
+
aiSessionId: session.id,
|
|
1412
|
+
activityType: "debugging",
|
|
1413
|
+
description: `Follow-up: ${followUpReason.replace("_", " ")} - ${outcome}`,
|
|
1414
|
+
durationSeconds: 0,
|
|
1415
|
+
productivityScore: outcome === "success" ? 9 : outcome === "partial_success" ? 6 : 4,
|
|
1416
|
+
startedAt: followUpTime.toISOString()
|
|
1417
|
+
});
|
|
1418
|
+
const sessionStartTime = new Date(session.createdAt);
|
|
1419
|
+
const totalMinutesElapsed = Math.round(
|
|
1420
|
+
(followUpTime.getTime() - sessionStartTime.getTime()) / 6e4
|
|
1421
|
+
);
|
|
1422
|
+
const currentEfficiency = totalMinutesElapsed > 0 ? totalMinutesElapsed / newEstimate : 1;
|
|
1423
|
+
await db.update(schema.aiSessions).set({
|
|
1424
|
+
efficiencyScore: currentEfficiency.toFixed(2),
|
|
1425
|
+
actualTimeMinutes: totalMinutesElapsed
|
|
1426
|
+
}).where(eq(schema.aiSessions.id, session.id));
|
|
1427
|
+
const existingEntries = await db.select({
|
|
1428
|
+
id: schema.agendaEvents.id,
|
|
1429
|
+
trackedDuration: schema.agendaEvents.trackedDuration,
|
|
1430
|
+
title: schema.agendaEvents.title,
|
|
1431
|
+
description: schema.agendaEvents.description,
|
|
1432
|
+
startTime: schema.agendaEvents.startTime
|
|
1433
|
+
}).from(schema.agendaEvents).where(
|
|
1434
|
+
and(
|
|
1435
|
+
eq(schema.agendaEvents.aiSessionId, session.id),
|
|
1436
|
+
eq(schema.agendaEvents.status, "draft")
|
|
1437
|
+
)
|
|
1438
|
+
).orderBy(desc(schema.agendaEvents.createdAt));
|
|
1439
|
+
let trackerAction = "";
|
|
1440
|
+
let trackerDetails = "";
|
|
1441
|
+
let existingEntry = existingEntries[0] ?? null;
|
|
1442
|
+
if (existingEntries.length > 1) {
|
|
1443
|
+
const totalExistingDuration = existingEntries.reduce(
|
|
1444
|
+
(sum, entry) => sum + (entry.trackedDuration ?? 0),
|
|
1445
|
+
0
|
|
1446
|
+
);
|
|
1447
|
+
const duplicateIds = existingEntries.slice(1).map((e) => e.id);
|
|
1448
|
+
await db.delete(schema.agendaEvents).where(inArray(schema.agendaEvents.id, duplicateIds));
|
|
1449
|
+
if (existingEntry && totalExistingDuration > (existingEntry.trackedDuration ?? 0)) {
|
|
1450
|
+
await db.update(schema.agendaEvents).set({ trackedDuration: totalExistingDuration }).where(eq(schema.agendaEvents.id, existingEntry.id));
|
|
1451
|
+
existingEntry = { ...existingEntry, trackedDuration: totalExistingDuration };
|
|
1452
|
+
}
|
|
1453
|
+
trackerAction = `Consolidated ${existingEntries.length} duplicate entries`;
|
|
1454
|
+
}
|
|
1455
|
+
if (existingEntry) {
|
|
1456
|
+
const newDuration = (existingEntry.trackedDuration ?? 0) + roundedFollowUpMinutes * 60;
|
|
1457
|
+
await db.update(schema.agendaEvents).set({
|
|
1458
|
+
trackedDuration: newDuration,
|
|
1459
|
+
endTime: followUpTime.toISOString(),
|
|
1460
|
+
title: workDescription,
|
|
1461
|
+
description: workDescription
|
|
1462
|
+
}).where(eq(schema.agendaEvents.id, existingEntry.id));
|
|
1463
|
+
trackerAction = trackerAction || "Updated existing tracker";
|
|
1464
|
+
trackerDetails = ` \u2022 Total tracked time: ${Math.round(newDuration / 60)} minutes (+${roundedFollowUpMinutes} min)
|
|
1098
1465
|
\u2022 Description: ${workDescription}
|
|
1099
1466
|
`;
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1467
|
+
} else {
|
|
1468
|
+
const durationSeconds = roundedFollowUpMinutes * 60;
|
|
1469
|
+
const startTime = new Date(
|
|
1470
|
+
followUpTime.getTime() - durationSeconds * 1e3
|
|
1471
|
+
);
|
|
1472
|
+
await db.insert(schema.agendaEvents).values({
|
|
1473
|
+
teamId: ctx.teamId,
|
|
1474
|
+
userId: ctx.userId,
|
|
1475
|
+
aiSessionId: session.id,
|
|
1476
|
+
title: workDescription,
|
|
1477
|
+
description: workDescription,
|
|
1478
|
+
startTime: startTime.toISOString(),
|
|
1479
|
+
endTime: followUpTime.toISOString(),
|
|
1480
|
+
type: "work",
|
|
1481
|
+
status: "draft",
|
|
1482
|
+
allDay: false,
|
|
1483
|
+
isTracked: true,
|
|
1484
|
+
trackedDuration: durationSeconds
|
|
1485
|
+
});
|
|
1486
|
+
trackerAction = "Created new tracker";
|
|
1487
|
+
trackerDetails = ` \u2022 Tracked time: ${roundedFollowUpMinutes} minutes
|
|
1119
1488
|
\u2022 Description: ${workDescription}
|
|
1120
1489
|
`;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
content: [
|
|
1493
|
+
{
|
|
1494
|
+
type: "text",
|
|
1495
|
+
text: `\u{1F504} **Follow-up Tracked & Session Restarted!**
|
|
1126
1496
|
|
|
1127
1497
|
\u{1F194} Session: ${aiSessionId} (back to active)
|
|
1128
1498
|
\u{1F50D} Reason: ${followUpReason.replace("_", " ")}
|
|
@@ -1140,197 +1510,222 @@ ${complexityScore ? `\u{1F3AF} Complexity: ${complexityScore}/10
|
|
|
1140
1510
|
\u23F1\uFE0F **Tracker Entry: ${trackerAction}**
|
|
1141
1511
|
` + trackerDetails + `
|
|
1142
1512
|
\u26A1 **Time tracking resumed** - continue with confidence!`
|
|
1143
|
-
}]
|
|
1144
|
-
};
|
|
1145
1513
|
}
|
|
1146
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1514
|
+
]
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
async function handleGetSessionContext(input) {
|
|
1518
|
+
const ctx = authContext;
|
|
1519
|
+
const {
|
|
1520
|
+
aiSessionId,
|
|
1521
|
+
includeTicketData = true,
|
|
1522
|
+
includeTodoProgress = true,
|
|
1523
|
+
includeFollowUpHistory = false
|
|
1524
|
+
} = input;
|
|
1525
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1526
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1527
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1528
|
+
if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1529
|
+
const [session] = await db.select({
|
|
1530
|
+
id: schema.aiSessions.id,
|
|
1531
|
+
ticketId: schema.aiSessions.ticketId,
|
|
1532
|
+
status: schema.aiSessions.status,
|
|
1533
|
+
aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
|
|
1534
|
+
actualTimeMinutes: schema.aiSessions.actualTimeMinutes,
|
|
1535
|
+
complexityScore: schema.aiSessions.complexityScore,
|
|
1536
|
+
createdAt: schema.aiSessions.createdAt,
|
|
1537
|
+
cursorSessionId: schema.aiSessions.cursorSessionId
|
|
1538
|
+
}).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
|
|
1539
|
+
if (!session) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1540
|
+
const context = {
|
|
1541
|
+
status: session.status,
|
|
1542
|
+
timeEstimate: session.aiTimeEstimateMinutes,
|
|
1543
|
+
actualTime: session.actualTimeMinutes,
|
|
1544
|
+
complexity: session.complexityScore,
|
|
1545
|
+
createdAt: session.createdAt
|
|
1546
|
+
};
|
|
1547
|
+
if (includeTicketData) {
|
|
1548
|
+
const [ticket] = await db.select({
|
|
1549
|
+
id: schema.tickets.id,
|
|
1550
|
+
ticketNumber: schema.tickets.ticketNumber,
|
|
1551
|
+
title: schema.tickets.title,
|
|
1552
|
+
description: schema.tickets.description,
|
|
1553
|
+
status: schema.tickets.status,
|
|
1554
|
+
priority: schema.tickets.priority,
|
|
1555
|
+
type: schema.tickets.type
|
|
1556
|
+
}).from(schema.tickets).where(eq(schema.tickets.id, session.ticketId)).limit(1);
|
|
1557
|
+
context.ticketData = ticket ?? null;
|
|
1558
|
+
}
|
|
1559
|
+
if (includeTodoProgress) {
|
|
1560
|
+
const todos = await db.select({
|
|
1561
|
+
id: schema.aiTodos.id,
|
|
1562
|
+
content: schema.aiTodos.content,
|
|
1563
|
+
status: schema.aiTodos.status,
|
|
1564
|
+
estimatedMinutes: schema.aiTodos.estimatedMinutes,
|
|
1565
|
+
actualMinutes: schema.aiTodos.actualMinutes
|
|
1566
|
+
}).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, session.id)).orderBy(asc(schema.aiTodos.sequenceOrder));
|
|
1567
|
+
context.todos = todos;
|
|
1568
|
+
context.todoProgress = {
|
|
1569
|
+
total: todos.length,
|
|
1570
|
+
completed: todos.filter((t) => t.status === "completed").length,
|
|
1571
|
+
inProgress: todos.filter((t) => t.status === "in_progress").length
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
if (includeFollowUpHistory) {
|
|
1575
|
+
const followUps = await db.select({
|
|
1576
|
+
followUpReason: schema.manualFollowUps.followUpReason,
|
|
1577
|
+
outcome: schema.manualFollowUps.outcome,
|
|
1578
|
+
timeSpentMinutes: schema.manualFollowUps.timeSpentMinutes,
|
|
1579
|
+
createdAt: schema.manualFollowUps.createdAt
|
|
1580
|
+
}).from(schema.manualFollowUps).where(eq(schema.manualFollowUps.aiSessionId, session.id)).orderBy(asc(schema.manualFollowUps.createdAt));
|
|
1581
|
+
context.followUpHistory = followUps;
|
|
1582
|
+
}
|
|
1583
|
+
const ticketData = context.ticketData;
|
|
1584
|
+
const todoProgress = context.todoProgress;
|
|
1585
|
+
const followUpHistory = context.followUpHistory;
|
|
1586
|
+
return {
|
|
1587
|
+
content: [
|
|
1588
|
+
{
|
|
1589
|
+
type: "text",
|
|
1590
|
+
text: `\u{1F3AF} **Session Context Retrieved**
|
|
1196
1591
|
|
|
1197
1592
|
Session: ${aiSessionId}
|
|
1198
1593
|
Status: ${session.status}
|
|
1199
|
-
${
|
|
1200
|
-
` : ""}${
|
|
1201
|
-
` : ""}${
|
|
1594
|
+
${ticketData ? `Ticket: ${ticketData.ticketNumber} - ${ticketData.title}
|
|
1595
|
+
` : ""}${todoProgress ? `Todo Progress: ${todoProgress.completed}/${todoProgress.total} completed
|
|
1596
|
+
` : ""}${followUpHistory ? `Follow-ups: ${followUpHistory.length}
|
|
1202
1597
|
` : ""}
|
|
1203
1598
|
\u{1F4CB} Full context preserved for seamless continuation!`
|
|
1204
|
-
}]
|
|
1205
|
-
};
|
|
1206
1599
|
}
|
|
1207
|
-
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1600
|
+
]
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
async function handleSyncSessionTodos(input) {
|
|
1604
|
+
const ctx = authContext;
|
|
1605
|
+
const { aiSessionId, todos, replaceAll = true } = input;
|
|
1606
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1607
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1608
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1609
|
+
if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1610
|
+
if (replaceAll) {
|
|
1611
|
+
await db.delete(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId));
|
|
1612
|
+
}
|
|
1613
|
+
if (todos && todos.length > 0) {
|
|
1614
|
+
let startSequence = 0;
|
|
1615
|
+
if (!replaceAll) {
|
|
1616
|
+
const [maxTodo] = await db.select({ sequenceOrder: schema.aiTodos.sequenceOrder }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId)).orderBy(desc(schema.aiTodos.sequenceOrder)).limit(1);
|
|
1617
|
+
startSequence = (maxTodo?.sequenceOrder ?? 0) + 1;
|
|
1618
|
+
}
|
|
1619
|
+
await db.insert(schema.aiTodos).values(
|
|
1620
|
+
todos.map((todo, index) => ({
|
|
1621
|
+
aiSessionId: fullSessionId,
|
|
1622
|
+
content: todo.content,
|
|
1623
|
+
status: todo.status,
|
|
1624
|
+
cursorTodoId: todo.todoId ?? null,
|
|
1625
|
+
estimatedMinutes: todo.estimatedMinutes ?? null,
|
|
1626
|
+
sequenceOrder: startSequence + index
|
|
1627
|
+
}))
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
let phaseTransition = null;
|
|
1631
|
+
const currentTodos = await db.select({ status: schema.aiTodos.status }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId));
|
|
1632
|
+
if (currentTodos.length > 0) {
|
|
1633
|
+
const hasInProgress = currentTodos.some((t) => t.status === "in_progress");
|
|
1634
|
+
const allCompleted = currentTodos.every((t) => t.status === "completed");
|
|
1635
|
+
const [currentPhase] = await db.select({
|
|
1636
|
+
activityType: schema.aiTimeLogs.activityType,
|
|
1637
|
+
status: schema.aiTimeLogs.status
|
|
1638
|
+
}).from(schema.aiTimeLogs).where(
|
|
1639
|
+
and(
|
|
1640
|
+
eq(schema.aiTimeLogs.aiSessionId, fullSessionId),
|
|
1641
|
+
eq(schema.aiTimeLogs.status, "in_progress")
|
|
1642
|
+
)
|
|
1643
|
+
).limit(1);
|
|
1644
|
+
if (hasInProgress && currentPhase?.activityType === "analysis") {
|
|
1645
|
+
await transitionToNextPhase(fullSessionId, "analysis");
|
|
1646
|
+
phaseTransition = "Analysis completed \u2192 Next phase started (Investigation/Development)";
|
|
1647
|
+
}
|
|
1648
|
+
if (hasInProgress && currentPhase?.activityType === "bug_investigation") {
|
|
1649
|
+
const completedCount = currentTodos.filter(
|
|
1650
|
+
(t) => t.status === "completed"
|
|
1651
|
+
).length;
|
|
1652
|
+
if (completedCount > 0) {
|
|
1653
|
+
await transitionToNextPhase(fullSessionId, "bug_investigation");
|
|
1654
|
+
phaseTransition = "Investigation completed \u2192 Development phase started";
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
if (allCompleted && currentPhase?.activityType === "development") {
|
|
1658
|
+
await transitionToNextPhase(fullSessionId, "development");
|
|
1659
|
+
phaseTransition = "Development completed \u2192 Communication phase started";
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return {
|
|
1663
|
+
content: [
|
|
1664
|
+
{
|
|
1665
|
+
type: "text",
|
|
1666
|
+
text: `\u2705 **Todos ${replaceAll ? "Synced" : "Added"} Successfully!**
|
|
1265
1667
|
|
|
1266
1668
|
Session: ${aiSessionId}
|
|
1267
1669
|
${replaceAll ? "Synced" : "Added"} ${todos?.length || 0} todos
|
|
1268
1670
|
${replaceAll ? "" : "\u2795 Added to existing todo list\n"}${phaseTransition ? `\u{1F504} Phase Transition: ${phaseTransition}
|
|
1269
1671
|
` : ""}
|
|
1270
1672
|
\u{1F4DD} Todo list updated and tracked for progress monitoring!`
|
|
1271
|
-
}]
|
|
1272
|
-
};
|
|
1273
1673
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
text: `\u2705 **Follow-up Todos Added Successfully!**
|
|
1674
|
+
]
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
async function handleAddFollowUpTodos(input) {
|
|
1678
|
+
const ctx = authContext;
|
|
1679
|
+
const { aiSessionId, newTodos, followUpReason } = input;
|
|
1680
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1681
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1682
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1683
|
+
if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1684
|
+
if (newTodos && newTodos.length > 0) {
|
|
1685
|
+
const [maxTodo] = await db.select({ sequenceOrder: schema.aiTodos.sequenceOrder }).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, fullSessionId)).orderBy(desc(schema.aiTodos.sequenceOrder)).limit(1);
|
|
1686
|
+
const startSequence = (maxTodo?.sequenceOrder ?? 0) + 1;
|
|
1687
|
+
await db.insert(schema.aiTodos).values(
|
|
1688
|
+
newTodos.map((todo, index) => ({
|
|
1689
|
+
aiSessionId: fullSessionId,
|
|
1690
|
+
content: `[Follow-up] ${todo.content}`,
|
|
1691
|
+
status: todo.status ?? "pending",
|
|
1692
|
+
estimatedMinutes: todo.estimatedMinutes ?? null,
|
|
1693
|
+
sequenceOrder: startSequence + index
|
|
1694
|
+
}))
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
content: [
|
|
1699
|
+
{
|
|
1700
|
+
type: "text",
|
|
1701
|
+
text: `\u2705 **Follow-up Todos Added Successfully!**
|
|
1303
1702
|
|
|
1304
1703
|
Session: ${aiSessionId}
|
|
1305
1704
|
Added ${newTodos?.length || 0} new todos from follow-up
|
|
1306
1705
|
${followUpReason ? `Reason: ${followUpReason}
|
|
1307
1706
|
` : ""}
|
|
1308
1707
|
\u{1F4DD} New tasks identified and added to existing workflow!`
|
|
1309
|
-
}]
|
|
1310
|
-
};
|
|
1311
1708
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
type: "text",
|
|
1333
|
-
text: `\u{1F3AF} **Session Status Updated!**
|
|
1709
|
+
]
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
async function handleUpdateSessionStatus(input) {
|
|
1713
|
+
const ctx = authContext;
|
|
1714
|
+
const { aiSessionId, status, actualTimeMinutes, completionNotes } = input;
|
|
1715
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1716
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1717
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1718
|
+
if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1719
|
+
await db.update(schema.aiSessions).set({
|
|
1720
|
+
status,
|
|
1721
|
+
actualTimeMinutes: actualTimeMinutes ?? null,
|
|
1722
|
+
completedAt: status === "completed" ? (/* @__PURE__ */ new Date()).toISOString() : null
|
|
1723
|
+
}).where(eq(schema.aiSessions.id, fullSessionId));
|
|
1724
|
+
return {
|
|
1725
|
+
content: [
|
|
1726
|
+
{
|
|
1727
|
+
type: "text",
|
|
1728
|
+
text: `\u{1F3AF} **Session Status Updated!**
|
|
1334
1729
|
|
|
1335
1730
|
Session: ${aiSessionId}
|
|
1336
1731
|
Status: ${status}
|
|
@@ -1338,113 +1733,135 @@ ${actualTimeMinutes ? `Actual Time: ${actualTimeMinutes} minutes
|
|
|
1338
1733
|
` : ""}${status === "completed" ? `\u2705 Session completed successfully!
|
|
1339
1734
|
` : ""}${completionNotes ? `Notes: ${completionNotes}
|
|
1340
1735
|
` : ""}`
|
|
1341
|
-
}]
|
|
1342
|
-
};
|
|
1343
1736
|
}
|
|
1344
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1737
|
+
]
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
async function handleGetCompletionContext(input) {
|
|
1741
|
+
const ctx = authContext;
|
|
1742
|
+
const {
|
|
1743
|
+
aiSessionId,
|
|
1744
|
+
includeFollowUps = true,
|
|
1745
|
+
includeTimeMetrics = true,
|
|
1746
|
+
includeTodos = true
|
|
1747
|
+
} = input;
|
|
1748
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1749
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1750
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1751
|
+
if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1752
|
+
const [session] = await db.select({
|
|
1753
|
+
id: schema.aiSessions.id,
|
|
1754
|
+
ticketId: schema.aiSessions.ticketId,
|
|
1755
|
+
aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
|
|
1756
|
+
actualTimeMinutes: schema.aiSessions.actualTimeMinutes,
|
|
1757
|
+
efficiencyScore: schema.aiSessions.efficiencyScore,
|
|
1758
|
+
createdAt: schema.aiSessions.createdAt,
|
|
1759
|
+
completedAt: schema.aiSessions.completedAt,
|
|
1760
|
+
status: schema.aiSessions.status,
|
|
1761
|
+
complexityScore: schema.aiSessions.complexityScore
|
|
1762
|
+
}).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
|
|
1763
|
+
if (!session) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1764
|
+
const [ticket] = await db.select({
|
|
1765
|
+
ticketNumber: schema.tickets.ticketNumber,
|
|
1766
|
+
title: schema.tickets.title,
|
|
1767
|
+
description: schema.tickets.description,
|
|
1768
|
+
type: schema.tickets.type,
|
|
1769
|
+
priority: schema.tickets.priority
|
|
1770
|
+
}).from(schema.tickets).where(eq(schema.tickets.id, session.ticketId)).limit(1);
|
|
1771
|
+
if (!ticket) throw new Error("Ticket not found for session");
|
|
1772
|
+
const contextData = {
|
|
1773
|
+
session: {
|
|
1774
|
+
id: aiSessionId,
|
|
1775
|
+
status: session.status,
|
|
1776
|
+
complexity: session.complexityScore,
|
|
1777
|
+
createdAt: session.createdAt,
|
|
1778
|
+
completedAt: session.completedAt
|
|
1779
|
+
},
|
|
1780
|
+
ticket: {
|
|
1781
|
+
number: ticket.ticketNumber,
|
|
1782
|
+
title: ticket.title,
|
|
1783
|
+
description: ticket.description,
|
|
1784
|
+
type: ticket.type,
|
|
1785
|
+
priority: ticket.priority
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
if (includeTimeMetrics) {
|
|
1789
|
+
const timeSaved = session.aiTimeEstimateMinutes && session.actualTimeMinutes ? Math.max(
|
|
1790
|
+
0,
|
|
1791
|
+
session.aiTimeEstimateMinutes - session.actualTimeMinutes
|
|
1792
|
+
) : null;
|
|
1793
|
+
contextData.timeMetrics = {
|
|
1794
|
+
estimatedMinutes: session.aiTimeEstimateMinutes,
|
|
1795
|
+
actualMinutes: session.actualTimeMinutes,
|
|
1796
|
+
timeSaved,
|
|
1797
|
+
efficiency: session.efficiencyScore,
|
|
1798
|
+
sessionDuration: session.completedAt && session.createdAt ? Math.round(
|
|
1799
|
+
(new Date(session.completedAt).getTime() - new Date(session.createdAt).getTime()) / 6e4
|
|
1800
|
+
) : null
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
if (includeTodos) {
|
|
1804
|
+
const todos = await db.select({
|
|
1805
|
+
content: schema.aiTodos.content,
|
|
1806
|
+
status: schema.aiTodos.status,
|
|
1807
|
+
estimatedMinutes: schema.aiTodos.estimatedMinutes,
|
|
1808
|
+
actualMinutes: schema.aiTodos.actualMinutes,
|
|
1809
|
+
completedAt: schema.aiTodos.completedAt
|
|
1810
|
+
}).from(schema.aiTodos).where(eq(schema.aiTodos.aiSessionId, session.id)).orderBy(asc(schema.aiTodos.createdAt));
|
|
1811
|
+
contextData.todos = todos;
|
|
1812
|
+
}
|
|
1813
|
+
if (includeFollowUps) {
|
|
1814
|
+
const followUps = await db.select({
|
|
1815
|
+
followUpReason: schema.manualFollowUps.followUpReason,
|
|
1816
|
+
outcome: schema.manualFollowUps.outcome,
|
|
1817
|
+
timeSpentMinutes: schema.manualFollowUps.timeSpentMinutes,
|
|
1818
|
+
createdAt: schema.manualFollowUps.createdAt
|
|
1819
|
+
}).from(schema.manualFollowUps).where(eq(schema.manualFollowUps.aiSessionId, session.id)).orderBy(asc(schema.manualFollowUps.createdAt));
|
|
1820
|
+
contextData.followUps = followUps;
|
|
1821
|
+
}
|
|
1822
|
+
const todosLen = contextData.todos ?? [];
|
|
1823
|
+
const completedTodos = todosLen.filter((t) => t.status === "completed").length;
|
|
1824
|
+
const followUpsLen = contextData.followUps?.length ?? 0;
|
|
1825
|
+
return {
|
|
1826
|
+
content: [
|
|
1827
|
+
{
|
|
1828
|
+
type: "text",
|
|
1829
|
+
text: `\u{1F4CB} **Completion Context Retrieved!**
|
|
1408
1830
|
|
|
1409
|
-
\u{1F3AB} **Ticket:** ${ticket.
|
|
1831
|
+
\u{1F3AB} **Ticket:** ${ticket.ticketNumber} - ${ticket.title}
|
|
1410
1832
|
\u{1F194} **Session:** ${aiSessionId} (${session.status})
|
|
1411
|
-
\u23F1\uFE0F **Time:** ${session.
|
|
1412
|
-
\u{1F4CB} **Todos:** ${
|
|
1413
|
-
\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}
|
|
1414
1836
|
|
|
1415
1837
|
\u2705 **Full context ready for Cursor AI to generate customer response!**
|
|
1416
1838
|
|
|
1417
1839
|
**Context Data:**
|
|
1418
1840
|
\`\`\`json
|
|
1419
1841
|
${JSON.stringify(contextData, null, 2)}\`\`\``
|
|
1420
|
-
}]
|
|
1421
|
-
};
|
|
1422
1842
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
content: [{
|
|
1446
|
-
type: "text",
|
|
1447
|
-
text: `\u{1F4BE} **Customer Response Saved!**
|
|
1843
|
+
]
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
async function handleSaveCustomerResponse(input) {
|
|
1847
|
+
const ctx = authContext;
|
|
1848
|
+
const { aiSessionId, customerResponse, responseType = "completion" } = input;
|
|
1849
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1850
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1851
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1852
|
+
if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1853
|
+
await db.insert(schema.aiResponses).values({
|
|
1854
|
+
aiSessionId: fullSessionId,
|
|
1855
|
+
responseType,
|
|
1856
|
+
content: customerResponse,
|
|
1857
|
+
isReadyForCustomer: true,
|
|
1858
|
+
providerApproved: false
|
|
1859
|
+
});
|
|
1860
|
+
return {
|
|
1861
|
+
content: [
|
|
1862
|
+
{
|
|
1863
|
+
type: "text",
|
|
1864
|
+
text: `\u{1F4BE} **Customer Response Saved!**
|
|
1448
1865
|
|
|
1449
1866
|
\u{1F194} Session: ${aiSessionId}
|
|
1450
1867
|
\u{1F4DD} Response Type: ${responseType}
|
|
@@ -1456,623 +1873,693 @@ ${JSON.stringify(contextData, null, 2)}\`\`\``
|
|
|
1456
1873
|
**Preview:**
|
|
1457
1874
|
\`\`\`
|
|
1458
1875
|
${customerResponse.substring(0, 200)}${customerResponse.length > 200 ? "..." : ""}\`\`\``
|
|
1459
|
-
}]
|
|
1460
|
-
};
|
|
1461
1876
|
}
|
|
1462
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1877
|
+
]
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
async function handleCompleteAiSession(input) {
|
|
1881
|
+
const ctx = authContext;
|
|
1882
|
+
const {
|
|
1883
|
+
aiSessionId,
|
|
1884
|
+
workCompleted,
|
|
1885
|
+
technicalSummary,
|
|
1886
|
+
invoiceDescription,
|
|
1887
|
+
efficiencyNotes
|
|
1888
|
+
} = input;
|
|
1889
|
+
const prefix = aiSessionId.replace("ai-sess-", "");
|
|
1890
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
1891
|
+
const fullSessionId = await resolveAiSessionId(prefix, teamIds);
|
|
1892
|
+
if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
|
|
1893
|
+
const [existingSession] = await db.select({
|
|
1894
|
+
id: schema.aiSessions.id,
|
|
1895
|
+
ticketId: schema.aiSessions.ticketId,
|
|
1896
|
+
aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
|
|
1897
|
+
createdAt: schema.aiSessions.createdAt
|
|
1898
|
+
}).from(schema.aiSessions).where(eq(schema.aiSessions.id, fullSessionId)).limit(1);
|
|
1899
|
+
if (!existingSession) {
|
|
1900
|
+
throw new Error(`Session not found: ${aiSessionId}`);
|
|
1901
|
+
}
|
|
1902
|
+
const completionTime = /* @__PURE__ */ new Date();
|
|
1903
|
+
const sessionStartTime = new Date(existingSession.createdAt);
|
|
1904
|
+
const timeSpentMinutes = Math.round(
|
|
1905
|
+
(completionTime.getTime() - sessionStartTime.getTime()) / 6e4
|
|
1906
|
+
);
|
|
1907
|
+
const [session] = await db.update(schema.aiSessions).set({
|
|
1908
|
+
status: "completed",
|
|
1909
|
+
actualTimeMinutes: timeSpentMinutes,
|
|
1910
|
+
completedAt: completionTime.toISOString(),
|
|
1911
|
+
efficiencyScore: null
|
|
1912
|
+
}).where(eq(schema.aiSessions.id, existingSession.id)).returning({
|
|
1913
|
+
id: schema.aiSessions.id,
|
|
1914
|
+
ticketId: schema.aiSessions.ticketId,
|
|
1915
|
+
aiTimeEstimateMinutes: schema.aiSessions.aiTimeEstimateMinutes,
|
|
1916
|
+
createdAt: schema.aiSessions.createdAt
|
|
1917
|
+
});
|
|
1918
|
+
if (!session) throw new Error(`Failed to update session: ${aiSessionId}`);
|
|
1919
|
+
const efficiencyScore = session.aiTimeEstimateMinutes ? timeSpentMinutes / session.aiTimeEstimateMinutes : 1;
|
|
1920
|
+
await db.update(schema.aiSessions).set({ efficiencyScore: efficiencyScore.toFixed(2) }).where(eq(schema.aiSessions.id, session.id));
|
|
1921
|
+
const activePhases = await db.select().from(schema.aiTimeLogs).where(
|
|
1922
|
+
and(
|
|
1923
|
+
eq(schema.aiTimeLogs.aiSessionId, existingSession.id),
|
|
1924
|
+
eq(schema.aiTimeLogs.status, "in_progress")
|
|
1925
|
+
)
|
|
1926
|
+
);
|
|
1927
|
+
for (const phase of activePhases) {
|
|
1928
|
+
const duration = Math.round(
|
|
1929
|
+
(completionTime.getTime() - new Date(phase.startedAt).getTime()) / 1e3
|
|
1930
|
+
);
|
|
1931
|
+
await db.update(schema.aiTimeLogs).set({
|
|
1932
|
+
endedAt: completionTime.toISOString(),
|
|
1933
|
+
durationSeconds: duration,
|
|
1934
|
+
status: "completed"
|
|
1935
|
+
}).where(eq(schema.aiTimeLogs.id, phase.id));
|
|
1936
|
+
}
|
|
1937
|
+
await db.update(schema.aiTimeLogs).set({ status: "skipped" }).where(
|
|
1938
|
+
and(
|
|
1939
|
+
eq(schema.aiTimeLogs.aiSessionId, existingSession.id),
|
|
1940
|
+
eq(schema.aiTimeLogs.status, "pending"),
|
|
1941
|
+
eq(schema.aiTimeLogs.estimatedDurationSeconds, 0)
|
|
1942
|
+
)
|
|
1943
|
+
);
|
|
1944
|
+
const sessionDuration = Math.round(
|
|
1945
|
+
(completionTime.getTime() - new Date(session.createdAt).getTime()) / 6e4
|
|
1946
|
+
);
|
|
1947
|
+
const workSummary = `Completed ${workCompleted.length} tasks including: ${workCompleted.slice(0, 3).join(", ")}${workCompleted.length > 3 ? " and more" : ""}.`;
|
|
1948
|
+
const [ticketInfo] = await db.select({
|
|
1949
|
+
ticketNumber: schema.tickets.ticketNumber,
|
|
1950
|
+
title: schema.tickets.title,
|
|
1951
|
+
projectId: schema.tickets.projectId
|
|
1952
|
+
}).from(schema.tickets).where(eq(schema.tickets.id, session.ticketId)).limit(1);
|
|
1953
|
+
let completionDescription;
|
|
1954
|
+
if (invoiceDescription) {
|
|
1955
|
+
completionDescription = `${ticketInfo?.ticketNumber || "Ticket"}: ${invoiceDescription}`;
|
|
1956
|
+
} else {
|
|
1957
|
+
const workDescription = workCompleted.map((task, index) => `${index + 1}. ${task}`).join("\n");
|
|
1958
|
+
completionDescription = `${ticketInfo?.ticketNumber || "Ticket"}: ${technicalSummary || workSummary}
|
|
1512
1959
|
|
|
1513
1960
|
Completed work:
|
|
1514
1961
|
${workDescription}`;
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1962
|
+
}
|
|
1963
|
+
const estimatedMinutes = session.aiTimeEstimateMinutes ?? timeSpentMinutes;
|
|
1964
|
+
const sessionStart = new Date(session.createdAt);
|
|
1965
|
+
const estimatedEnd = new Date(
|
|
1966
|
+
sessionStart.getTime() + estimatedMinutes * 6e4
|
|
1967
|
+
);
|
|
1968
|
+
const existingAgendaEntries = await db.select({
|
|
1969
|
+
id: schema.agendaEvents.id,
|
|
1970
|
+
trackedDuration: schema.agendaEvents.trackedDuration
|
|
1971
|
+
}).from(schema.agendaEvents).where(
|
|
1972
|
+
and(
|
|
1973
|
+
eq(schema.agendaEvents.aiSessionId, session.id),
|
|
1974
|
+
eq(schema.agendaEvents.status, "draft")
|
|
1975
|
+
)
|
|
1976
|
+
).orderBy(desc(schema.agendaEvents.createdAt));
|
|
1977
|
+
let agendaEventId = null;
|
|
1978
|
+
let wasUpdated = false;
|
|
1979
|
+
let consolidatedCount = 0;
|
|
1980
|
+
const existingAgendaEntry = existingAgendaEntries[0] ?? null;
|
|
1981
|
+
if (existingAgendaEntries.length > 1) {
|
|
1982
|
+
const duplicateIds = existingAgendaEntries.slice(1).map((e) => e.id);
|
|
1983
|
+
await db.delete(schema.agendaEvents).where(inArray(schema.agendaEvents.id, duplicateIds));
|
|
1984
|
+
consolidatedCount = existingAgendaEntries.length - 1;
|
|
1985
|
+
}
|
|
1986
|
+
try {
|
|
1987
|
+
if (existingAgendaEntry) {
|
|
1988
|
+
const [updated] = await db.update(schema.agendaEvents).set({
|
|
1989
|
+
title: ticketInfo?.title || "Development Work",
|
|
1990
|
+
description: completionDescription,
|
|
1991
|
+
endTime: estimatedEnd.toISOString(),
|
|
1992
|
+
projectId: ticketInfo?.projectId ?? null,
|
|
1993
|
+
trackedDuration: estimatedMinutes * 60
|
|
1994
|
+
}).where(eq(schema.agendaEvents.id, existingAgendaEntry.id)).returning({ id: schema.agendaEvents.id });
|
|
1995
|
+
agendaEventId = updated?.id ?? null;
|
|
1996
|
+
wasUpdated = true;
|
|
1997
|
+
} else {
|
|
1998
|
+
const [created] = await db.insert(schema.agendaEvents).values({
|
|
1999
|
+
teamId: ctx.teamId,
|
|
2000
|
+
userId: ctx.userId,
|
|
2001
|
+
title: ticketInfo?.title || "Development Work",
|
|
2002
|
+
description: completionDescription,
|
|
2003
|
+
startTime: sessionStart.toISOString(),
|
|
2004
|
+
endTime: estimatedEnd.toISOString(),
|
|
2005
|
+
projectId: ticketInfo?.projectId ?? null,
|
|
2006
|
+
aiSessionId: session.id,
|
|
2007
|
+
type: "work",
|
|
2008
|
+
status: "draft",
|
|
2009
|
+
allDay: false,
|
|
2010
|
+
isTracked: true,
|
|
2011
|
+
trackedDuration: estimatedMinutes * 60
|
|
2012
|
+
}).returning({ id: schema.agendaEvents.id });
|
|
2013
|
+
agendaEventId = created?.id ?? null;
|
|
2014
|
+
}
|
|
2015
|
+
if (agendaEventId && session.ticketId) {
|
|
2016
|
+
await db.insert(schema.agendaEventTickets).values({
|
|
2017
|
+
agendaEventId,
|
|
2018
|
+
ticketId: session.ticketId
|
|
2019
|
+
}).onConflictDoNothing();
|
|
2020
|
+
}
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
console.error(
|
|
2023
|
+
`\u26A0\uFE0F Failed to ${wasUpdated ? "update" : "create"} agenda event:`,
|
|
2024
|
+
err
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
if (consolidatedCount > 0) {
|
|
2028
|
+
console.log(
|
|
2029
|
+
`\u{1F9F9} Cleaned up ${consolidatedCount} duplicate agenda entries for session ${aiSessionId}`
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
let responseText = `\u{1F389} **AI Session Completed Successfully!**
|
|
1573
2033
|
|
|
1574
2034
|
`;
|
|
1575
|
-
|
|
2035
|
+
responseText += `\u{1F194} Session: ${aiSessionId}
|
|
1576
2036
|
`;
|
|
1577
|
-
|
|
2037
|
+
responseText += `\u{1F4CA} **Performance Summary:**
|
|
1578
2038
|
`;
|
|
1579
|
-
|
|
2039
|
+
responseText += ` \u2022 Tasks Completed: ${workCompleted.length}
|
|
1580
2040
|
`;
|
|
1581
|
-
|
|
2041
|
+
responseText += ` \u2022 Time Spent: ${timeSpentMinutes} minutes
|
|
1582
2042
|
`;
|
|
1583
|
-
|
|
2043
|
+
responseText += ` \u2022 Estimated Time: ${session.aiTimeEstimateMinutes || "N/A"} minutes
|
|
1584
2044
|
`;
|
|
1585
|
-
|
|
2045
|
+
responseText += ` \u2022 Efficiency: ${efficiencyScore < 1 ? "\u{1F680}" : efficiencyScore > 1.5 ? "\u26A0\uFE0F" : "\u23F1\uFE0F"} ${(efficiencyScore * 100).toFixed(0)}%
|
|
1586
2046
|
`;
|
|
1587
|
-
|
|
2047
|
+
responseText += ` \u2022 Session Duration: ${sessionDuration} minutes
|
|
1588
2048
|
|
|
1589
2049
|
`;
|
|
1590
|
-
|
|
2050
|
+
responseText += `\u2705 **Work Completed:**
|
|
1591
2051
|
`;
|
|
1592
|
-
|
|
1593
|
-
|
|
2052
|
+
workCompleted.forEach((task, index) => {
|
|
2053
|
+
responseText += `${index + 1}. ${task}
|
|
1594
2054
|
`;
|
|
1595
|
-
|
|
1596
|
-
|
|
2055
|
+
});
|
|
2056
|
+
responseText += `
|
|
1597
2057
|
`;
|
|
1598
|
-
|
|
1599
|
-
|
|
2058
|
+
if (technicalSummary) {
|
|
2059
|
+
responseText += `\u{1F527} **Technical Summary:**
|
|
1600
2060
|
${technicalSummary}
|
|
1601
2061
|
|
|
1602
2062
|
`;
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2063
|
+
}
|
|
2064
|
+
if (efficiencyNotes) {
|
|
2065
|
+
responseText += `\u{1F4C8} **Efficiency Notes:**
|
|
1606
2066
|
${efficiencyNotes}
|
|
1607
2067
|
|
|
1608
2068
|
`;
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2069
|
+
}
|
|
2070
|
+
if (agendaEventId) {
|
|
2071
|
+
responseText += `\u{1F4C5} **Timetrack Entry ${wasUpdated ? "Updated" : "Created"}:**
|
|
1612
2072
|
`;
|
|
1613
|
-
|
|
2073
|
+
responseText += ` \u2022 Agenda event ${wasUpdated ? "updated with final" : "created with"} work summary
|
|
1614
2074
|
`;
|
|
1615
|
-
|
|
2075
|
+
responseText += ` \u2022 Status: DRAFT (requires approval in agenda)
|
|
1616
2076
|
`;
|
|
1617
|
-
|
|
2077
|
+
responseText += ` \u2022 Duration: ${estimatedMinutes} minutes
|
|
1618
2078
|
`;
|
|
1619
|
-
|
|
2079
|
+
responseText += ` \u2022 Period: ${sessionStart.toLocaleString()} - ${completionTime.toLocaleString()}
|
|
1620
2080
|
|
|
1621
2081
|
`;
|
|
1622
|
-
|
|
1623
|
-
|
|
2082
|
+
}
|
|
2083
|
+
responseText += `\u{1F4CB} **Context for Customer Response:**
|
|
1624
2084
|
`;
|
|
1625
|
-
|
|
2085
|
+
responseText += ` \u2022 Use "get-completion-context" to retrieve full context
|
|
1626
2086
|
`;
|
|
1627
|
-
|
|
2087
|
+
responseText += ` \u2022 Generate customer-friendly response based on completed work
|
|
1628
2088
|
`;
|
|
1629
|
-
|
|
2089
|
+
responseText += ` \u2022 Focus on business value and customer benefits
|
|
1630
2090
|
|
|
1631
2091
|
`;
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
2092
|
+
responseText += `\u{1F3AF} **Session archived successfully!**`;
|
|
2093
|
+
return { content: [{ type: "text", text: responseText }] };
|
|
2094
|
+
}
|
|
2095
|
+
async function handleLogHours(input) {
|
|
2096
|
+
const ctx = authContext;
|
|
2097
|
+
const {
|
|
2098
|
+
projectId,
|
|
2099
|
+
ticketId,
|
|
2100
|
+
aiSessionId,
|
|
2101
|
+
workDescription,
|
|
2102
|
+
estimatedHours,
|
|
2103
|
+
chatContextSummary
|
|
2104
|
+
} = input;
|
|
2105
|
+
let project = null;
|
|
2106
|
+
let ticket = null;
|
|
2107
|
+
let aiSession = null;
|
|
2108
|
+
if (projectId) {
|
|
2109
|
+
const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
|
|
2110
|
+
if (!projectIds.includes(projectId)) {
|
|
2111
|
+
throw new Error(
|
|
2112
|
+
`Project not found or no access: ${projectId}. Please call get-projects first to find the correct project.`
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
const [projectData] = await db.select({
|
|
2116
|
+
id: schema.projects.id,
|
|
2117
|
+
name: schema.projects.name,
|
|
2118
|
+
teamId: schema.projects.teamId
|
|
2119
|
+
}).from(schema.projects).where(eq(schema.projects.id, projectId)).limit(1);
|
|
2120
|
+
if (!projectData) throw new Error(`Project not found: ${projectId}.`);
|
|
2121
|
+
project = projectData;
|
|
2122
|
+
}
|
|
2123
|
+
if (ticketId) {
|
|
2124
|
+
const [ticketData] = await db.select({
|
|
2125
|
+
id: schema.tickets.id,
|
|
2126
|
+
title: schema.tickets.title,
|
|
2127
|
+
status: schema.tickets.status,
|
|
2128
|
+
teamId: schema.tickets.teamId,
|
|
2129
|
+
projectId: schema.tickets.projectId,
|
|
2130
|
+
customerId: schema.tickets.customerId
|
|
2131
|
+
}).from(schema.tickets).where(eq(schema.tickets.id, ticketId)).limit(1);
|
|
2132
|
+
if (!ticketData) {
|
|
2133
|
+
throw new Error(
|
|
2134
|
+
`Ticket not found: ${ticketId}. Please call get-tickets first to find the correct ticket.`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
let hasAccess = false;
|
|
2138
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
2139
|
+
if (teamIds.includes(ticketData.teamId)) hasAccess = true;
|
|
2140
|
+
if (!hasAccess && ticketData.projectId) {
|
|
2141
|
+
const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
|
|
2142
|
+
if (projectIds.includes(ticketData.projectId)) hasAccess = true;
|
|
2143
|
+
}
|
|
2144
|
+
if (!hasAccess && ticketData.customerId) {
|
|
2145
|
+
const customerIds = await getAccessibleCustomerIds(ctx.teamId);
|
|
2146
|
+
if (customerIds.includes(ticketData.customerId)) hasAccess = true;
|
|
2147
|
+
}
|
|
2148
|
+
if (!hasAccess) {
|
|
2149
|
+
throw new Error(
|
|
2150
|
+
`No access to ticket: ${ticketId}. Please call get-tickets first to find the correct ticket.`
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
ticket = ticketData;
|
|
2154
|
+
}
|
|
2155
|
+
if (aiSessionId) {
|
|
2156
|
+
const [sessionData] = await db.select({
|
|
2157
|
+
id: schema.aiSessions.id,
|
|
2158
|
+
ticketId: schema.aiSessions.ticketId,
|
|
2159
|
+
status: schema.aiSessions.status
|
|
2160
|
+
}).from(schema.aiSessions).where(eq(schema.aiSessions.id, aiSessionId)).limit(1);
|
|
2161
|
+
if (!sessionData) throw new Error(`AI Session not found: ${aiSessionId}.`);
|
|
2162
|
+
aiSession = sessionData;
|
|
2163
|
+
}
|
|
2164
|
+
const durationSeconds = Math.round(estimatedHours * 3600);
|
|
2165
|
+
const now = /* @__PURE__ */ new Date();
|
|
2166
|
+
let agendaEntry = null;
|
|
2167
|
+
let wasUpdated = false;
|
|
2168
|
+
let consolidatedCount = 0;
|
|
2169
|
+
if (aiSession?.id || ticket?.id) {
|
|
2170
|
+
let existingEntries = [];
|
|
2171
|
+
if (aiSession?.id) {
|
|
2172
|
+
existingEntries = await db.select({
|
|
2173
|
+
id: schema.agendaEvents.id,
|
|
2174
|
+
trackedDuration: schema.agendaEvents.trackedDuration,
|
|
2175
|
+
projectId: schema.agendaEvents.projectId,
|
|
2176
|
+
aiSessionId: schema.agendaEvents.aiSessionId
|
|
2177
|
+
}).from(schema.agendaEvents).where(
|
|
2178
|
+
and(
|
|
2179
|
+
eq(schema.agendaEvents.status, "draft"),
|
|
2180
|
+
eq(schema.agendaEvents.userId, ctx.userId),
|
|
2181
|
+
eq(schema.agendaEvents.aiSessionId, aiSession.id)
|
|
2182
|
+
)
|
|
2183
|
+
).orderBy(desc(schema.agendaEvents.createdAt));
|
|
2184
|
+
} else if (ticket?.id) {
|
|
2185
|
+
const linkedEvents = await db.select({
|
|
2186
|
+
agendaEventId: schema.agendaEventTickets.agendaEventId
|
|
2187
|
+
}).from(schema.agendaEventTickets).where(eq(schema.agendaEventTickets.ticketId, ticket.id));
|
|
2188
|
+
const eventIds = linkedEvents.map((e) => e.agendaEventId);
|
|
2189
|
+
if (eventIds.length > 0) {
|
|
2190
|
+
existingEntries = await db.select({
|
|
2191
|
+
id: schema.agendaEvents.id,
|
|
2192
|
+
trackedDuration: schema.agendaEvents.trackedDuration,
|
|
2193
|
+
projectId: schema.agendaEvents.projectId,
|
|
2194
|
+
aiSessionId: schema.agendaEvents.aiSessionId
|
|
2195
|
+
}).from(schema.agendaEvents).where(
|
|
2196
|
+
and(
|
|
2197
|
+
inArray(schema.agendaEvents.id, eventIds),
|
|
2198
|
+
eq(schema.agendaEvents.status, "draft"),
|
|
2199
|
+
eq(schema.agendaEvents.userId, ctx.userId)
|
|
2200
|
+
)
|
|
2201
|
+
).orderBy(desc(schema.agendaEvents.createdAt));
|
|
1639
2202
|
}
|
|
1640
|
-
|
|
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
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
if (aiSession?.id) {
|
|
1699
|
-
query = query.eq("ai_session_id", aiSession.id);
|
|
1700
|
-
} else if (ticket?.id) {
|
|
1701
|
-
query = query.eq("ticket_id", ticket.id);
|
|
1702
|
-
}
|
|
1703
|
-
const { data: existingEntries } = await query;
|
|
1704
|
-
if (existingEntries && existingEntries.length > 0) {
|
|
1705
|
-
const existingEntry = existingEntries[0];
|
|
1706
|
-
if (existingEntries.length > 1) {
|
|
1707
|
-
const duplicateIds = existingEntries.slice(1).map((e) => e.id);
|
|
1708
|
-
await supabase.from("agenda_events").delete().in("id", duplicateIds);
|
|
1709
|
-
consolidatedCount = existingEntries.length - 1;
|
|
1710
|
-
}
|
|
1711
|
-
const newDuration = (existingEntry.tracked_duration || 0) + durationSeconds;
|
|
1712
|
-
const { data: updated, error: updateError } = await supabase.from("agenda_events").update({
|
|
1713
|
-
tracked_duration: newDuration,
|
|
1714
|
-
end_time: now.toISOString(),
|
|
1715
|
-
title: workDescription,
|
|
1716
|
-
description: chatContextSummary || workDescription,
|
|
1717
|
-
project_id: project?.id || existingEntry.project_id
|
|
1718
|
-
}).eq("id", existingEntry.id).select("id, tracked_duration, project_id, ticket_id, ai_session_id").single();
|
|
1719
|
-
agendaEntry = updated;
|
|
1720
|
-
agendaError = updateError;
|
|
1721
|
-
wasUpdated = true;
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
if (!agendaEntry) {
|
|
1725
|
-
const startTime = new Date(now.getTime() - durationSeconds * 1e3);
|
|
1726
|
-
const { data: created, error: createError } = await supabase.from("agenda_events").insert({
|
|
1727
|
-
team_id: authContext.teamId,
|
|
1728
|
-
user_id: authContext.userId,
|
|
1729
|
-
project_id: project?.id || null,
|
|
1730
|
-
ticket_id: ticket?.id || null,
|
|
1731
|
-
ai_session_id: aiSession?.id || null,
|
|
1732
|
-
title: workDescription,
|
|
1733
|
-
description: chatContextSummary || workDescription,
|
|
1734
|
-
start_time: startTime.toISOString(),
|
|
1735
|
-
end_time: now.toISOString(),
|
|
1736
|
-
type: "work",
|
|
1737
|
-
status: "draft",
|
|
1738
|
-
all_day: false,
|
|
1739
|
-
is_tracked: true,
|
|
1740
|
-
tracked_duration: durationSeconds
|
|
1741
|
-
}).select("id, tracked_duration, project_id, ticket_id, ai_session_id").single();
|
|
1742
|
-
agendaEntry = created;
|
|
1743
|
-
agendaError = createError;
|
|
1744
|
-
}
|
|
1745
|
-
if (agendaError || !agendaEntry) {
|
|
1746
|
-
throw new Error(`Failed to ${wasUpdated ? "update" : "create"} time entry: ${agendaError?.message || "Unknown error"}`);
|
|
1747
|
-
}
|
|
1748
|
-
let responseText = `\u23F1\uFE0F **Hours ${wasUpdated ? "Added to Existing Entry" : "Logged Successfully"}!**
|
|
2203
|
+
}
|
|
2204
|
+
if (existingEntries.length > 0) {
|
|
2205
|
+
const existingEntry = existingEntries[0];
|
|
2206
|
+
if (existingEntries.length > 1) {
|
|
2207
|
+
const duplicateIds = existingEntries.slice(1).map((e) => e.id);
|
|
2208
|
+
await db.delete(schema.agendaEvents).where(inArray(schema.agendaEvents.id, duplicateIds));
|
|
2209
|
+
consolidatedCount = existingEntries.length - 1;
|
|
2210
|
+
}
|
|
2211
|
+
const newDuration = (existingEntry.trackedDuration ?? 0) + durationSeconds;
|
|
2212
|
+
const [updated] = await db.update(schema.agendaEvents).set({
|
|
2213
|
+
trackedDuration: newDuration,
|
|
2214
|
+
endTime: now.toISOString(),
|
|
2215
|
+
title: workDescription,
|
|
2216
|
+
description: chatContextSummary ?? workDescription,
|
|
2217
|
+
projectId: project?.id ?? existingEntry.projectId
|
|
2218
|
+
}).where(eq(schema.agendaEvents.id, existingEntry.id)).returning({
|
|
2219
|
+
id: schema.agendaEvents.id,
|
|
2220
|
+
trackedDuration: schema.agendaEvents.trackedDuration,
|
|
2221
|
+
projectId: schema.agendaEvents.projectId,
|
|
2222
|
+
aiSessionId: schema.agendaEvents.aiSessionId
|
|
2223
|
+
});
|
|
2224
|
+
agendaEntry = updated ?? null;
|
|
2225
|
+
wasUpdated = true;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (!agendaEntry) {
|
|
2229
|
+
const startTime = new Date(now.getTime() - durationSeconds * 1e3);
|
|
2230
|
+
const [created] = await db.insert(schema.agendaEvents).values({
|
|
2231
|
+
teamId: ctx.teamId,
|
|
2232
|
+
userId: ctx.userId,
|
|
2233
|
+
projectId: project?.id ?? null,
|
|
2234
|
+
aiSessionId: aiSession?.id ?? null,
|
|
2235
|
+
title: workDescription,
|
|
2236
|
+
description: chatContextSummary ?? workDescription,
|
|
2237
|
+
startTime: startTime.toISOString(),
|
|
2238
|
+
endTime: now.toISOString(),
|
|
2239
|
+
type: "work",
|
|
2240
|
+
status: "draft",
|
|
2241
|
+
allDay: false,
|
|
2242
|
+
isTracked: true,
|
|
2243
|
+
trackedDuration: durationSeconds
|
|
2244
|
+
}).returning({
|
|
2245
|
+
id: schema.agendaEvents.id,
|
|
2246
|
+
trackedDuration: schema.agendaEvents.trackedDuration,
|
|
2247
|
+
projectId: schema.agendaEvents.projectId,
|
|
2248
|
+
aiSessionId: schema.agendaEvents.aiSessionId
|
|
2249
|
+
});
|
|
2250
|
+
agendaEntry = created ?? null;
|
|
2251
|
+
if (agendaEntry && ticket?.id) {
|
|
2252
|
+
await db.insert(schema.agendaEventTickets).values({ agendaEventId: agendaEntry.id, ticketId: ticket.id }).onConflictDoNothing();
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
if (!agendaEntry) {
|
|
2256
|
+
throw new Error(
|
|
2257
|
+
`Failed to ${wasUpdated ? "update" : "create"} time entry`
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
let responseText = `\u23F1\uFE0F **Hours ${wasUpdated ? "Added to Existing Entry" : "Logged Successfully"}!**
|
|
1749
2261
|
|
|
1750
2262
|
`;
|
|
1751
|
-
|
|
1752
|
-
|
|
2263
|
+
if (wasUpdated) {
|
|
2264
|
+
responseText += `\u{1F504} **Updated existing draft entry** (avoiding duplicates)
|
|
1753
2265
|
`;
|
|
1754
|
-
|
|
2266
|
+
responseText += ` \u2022 New total: ${Math.round((agendaEntry.trackedDuration ?? 0) / 3600 * 10) / 10}h
|
|
1755
2267
|
|
|
1756
2268
|
`;
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2269
|
+
}
|
|
2270
|
+
if (consolidatedCount > 0) {
|
|
2271
|
+
responseText += `\u{1F9F9} **Cleaned up ${consolidatedCount} duplicate entries**
|
|
1760
2272
|
|
|
1761
2273
|
`;
|
|
1762
|
-
|
|
1763
|
-
|
|
2274
|
+
}
|
|
2275
|
+
responseText += `\u{1F4CB} **Entry Details:**
|
|
1764
2276
|
`;
|
|
1765
|
-
|
|
1766
|
-
responseText += ` \u2022 Project: ${project.name}
|
|
2277
|
+
responseText += ` \u2022 Project: ${project ? project.name : "(No project assigned)"}
|
|
1767
2278
|
`;
|
|
1768
|
-
|
|
1769
|
-
responseText += ` \u2022 Project: (No project assigned)
|
|
2279
|
+
if (ticket) responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
|
|
1770
2280
|
`;
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
|
|
2281
|
+
if (aiSession)
|
|
2282
|
+
responseText += ` \u2022 AI Session: ${aiSession.id} (${aiSession.status})
|
|
1774
2283
|
`;
|
|
1775
|
-
|
|
1776
|
-
if (aiSession) {
|
|
1777
|
-
responseText += ` \u2022 AI Session: ${aiSession.id} (${aiSession.status})
|
|
2284
|
+
responseText += ` \u2022 Description: ${workDescription}
|
|
1778
2285
|
`;
|
|
1779
|
-
|
|
1780
|
-
responseText += ` \u2022 Description: ${workDescription}
|
|
1781
|
-
`;
|
|
1782
|
-
responseText += ` \u2022 ${wasUpdated ? "Added" : "Estimated"} Hours: ${estimatedHours}h (${Math.floor(estimatedHours)}h ${Math.round(estimatedHours % 1 * 60)}m)
|
|
2286
|
+
responseText += ` \u2022 ${wasUpdated ? "Added" : "Estimated"} Hours: ${estimatedHours}h (${Math.floor(estimatedHours)}h ${Math.round(estimatedHours % 1 * 60)}m)
|
|
1783
2287
|
`;
|
|
1784
|
-
|
|
2288
|
+
responseText += ` \u2022 Status: DRAFT (not billed yet)
|
|
1785
2289
|
`;
|
|
1786
|
-
|
|
2290
|
+
responseText += ` \u2022 Entry ID: ${agendaEntry.id}
|
|
1787
2291
|
|
|
1788
2292
|
`;
|
|
1789
|
-
|
|
1790
|
-
|
|
2293
|
+
if (chatContextSummary) {
|
|
2294
|
+
responseText += `\u{1F4CA} **Work Context:**
|
|
1791
2295
|
`;
|
|
1792
|
-
|
|
2296
|
+
responseText += `${chatContextSummary.substring(0, 200)}${chatContextSummary.length > 200 ? "..." : ""}
|
|
1793
2297
|
|
|
1794
2298
|
`;
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2299
|
+
}
|
|
2300
|
+
responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
|
|
2301
|
+
return { content: [{ type: "text", text: responseText }] };
|
|
2302
|
+
}
|
|
2303
|
+
async function handleGetGithubFile(input) {
|
|
2304
|
+
const ctx = authContext;
|
|
2305
|
+
const { projectId, filePath, ref } = input;
|
|
2306
|
+
const githubInfo = await getGithubTokenForProject(projectId, ctx.teamId);
|
|
2307
|
+
if (!githubInfo) {
|
|
2308
|
+
return {
|
|
2309
|
+
content: [
|
|
2310
|
+
{ type: "text", text: "\u274C GitHub not configured for this project." }
|
|
2311
|
+
]
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
try {
|
|
2315
|
+
const octokit = new Octokit({ auth: githubInfo.token });
|
|
2316
|
+
console.error(
|
|
2317
|
+
`\u{1F4C4} Reading file: ${filePath} from ${githubInfo.repositoryFullName}`
|
|
2318
|
+
);
|
|
2319
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
2320
|
+
owner: githubInfo.owner,
|
|
2321
|
+
repo: githubInfo.repo,
|
|
2322
|
+
path: filePath,
|
|
2323
|
+
ref
|
|
2324
|
+
});
|
|
2325
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
2326
|
+
return {
|
|
2327
|
+
content: [
|
|
2328
|
+
{
|
|
1799
2329
|
type: "text",
|
|
1800
|
-
text:
|
|
1801
|
-
}]
|
|
1802
|
-
};
|
|
1803
|
-
}
|
|
1804
|
-
// === GITHUB TOOLS ===
|
|
1805
|
-
case "get-github-file": {
|
|
1806
|
-
const { projectId, filePath, ref } = args2;
|
|
1807
|
-
const githubInfo = await getGithubTokenForProject(projectId, authContext.teamId);
|
|
1808
|
-
if (!githubInfo) {
|
|
1809
|
-
return {
|
|
1810
|
-
content: [{
|
|
1811
|
-
type: "text",
|
|
1812
|
-
text: "\u274C GitHub not configured for this project."
|
|
1813
|
-
}]
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
try {
|
|
1817
|
-
const octokit = new Octokit({ auth: githubInfo.token });
|
|
1818
|
-
console.error(`\u{1F4C4} Reading file: ${filePath} from ${githubInfo.repositoryFullName}`);
|
|
1819
|
-
const { data } = await octokit.rest.repos.getContent({
|
|
1820
|
-
owner: githubInfo.owner,
|
|
1821
|
-
repo: githubInfo.repo,
|
|
1822
|
-
path: filePath,
|
|
1823
|
-
ref
|
|
1824
|
-
});
|
|
1825
|
-
if (Array.isArray(data) || data.type !== "file") {
|
|
1826
|
-
return {
|
|
1827
|
-
content: [{
|
|
1828
|
-
type: "text",
|
|
1829
|
-
text: `\u274C "${filePath}" is not a file or contains multiple items.`
|
|
1830
|
-
}]
|
|
1831
|
-
};
|
|
2330
|
+
text: `\u274C "${filePath}" is not a file or contains multiple items.`
|
|
1832
2331
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
2332
|
+
]
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
const content = Buffer.from(data.content, "base64").toString("utf-8");
|
|
2336
|
+
let responseText = `\u{1F4C4} **File: ${filePath}**
|
|
1835
2337
|
`;
|
|
1836
|
-
|
|
2338
|
+
responseText += `Repository: ${githubInfo.repositoryFullName}
|
|
1837
2339
|
`;
|
|
1838
|
-
|
|
2340
|
+
responseText += `Size: ${data.size} bytes
|
|
1839
2341
|
`;
|
|
1840
|
-
|
|
2342
|
+
responseText += `URL: ${data.html_url}
|
|
1841
2343
|
|
|
1842
2344
|
`;
|
|
1843
|
-
|
|
2345
|
+
responseText += `**Content:**
|
|
1844
2346
|
\`\`\`
|
|
1845
2347
|
${content}
|
|
1846
2348
|
\`\`\``;
|
|
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
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
return {
|
|
1894
|
-
content: [{
|
|
1895
|
-
type: "text",
|
|
1896
|
-
text: `\u274C "${directoryPath}" is not a directory.`
|
|
1897
|
-
}]
|
|
1898
|
-
};
|
|
2349
|
+
return { content: [{ type: "text", text: responseText }] };
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
console.error("GitHub get file error:", error);
|
|
2352
|
+
const status = error?.status;
|
|
2353
|
+
if (status === 404) {
|
|
2354
|
+
return {
|
|
2355
|
+
content: [{ type: "text", text: `\u274C File not found: ${filePath}` }]
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2359
|
+
return {
|
|
2360
|
+
content: [
|
|
2361
|
+
{ type: "text", text: `\u274C Failed to read file: ${message}` }
|
|
2362
|
+
]
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
async function handleListGithubDirectory(input) {
|
|
2367
|
+
const ctx = authContext;
|
|
2368
|
+
const { projectId, directoryPath, ref } = input;
|
|
2369
|
+
const githubInfo = await getGithubTokenForProject(projectId, ctx.teamId);
|
|
2370
|
+
if (!githubInfo) {
|
|
2371
|
+
return {
|
|
2372
|
+
content: [
|
|
2373
|
+
{ type: "text", text: "\u274C GitHub not configured for this project." }
|
|
2374
|
+
]
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
try {
|
|
2378
|
+
const octokit = new Octokit({ auth: githubInfo.token });
|
|
2379
|
+
const normalizedPath = !directoryPath || directoryPath === "/" ? "" : directoryPath;
|
|
2380
|
+
console.error(
|
|
2381
|
+
`\u{1F4C1} Listing directory: ${normalizedPath || "(root)"} in ${githubInfo.repositoryFullName}`
|
|
2382
|
+
);
|
|
2383
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
2384
|
+
owner: githubInfo.owner,
|
|
2385
|
+
repo: githubInfo.repo,
|
|
2386
|
+
path: normalizedPath,
|
|
2387
|
+
ref
|
|
2388
|
+
});
|
|
2389
|
+
if (!Array.isArray(data)) {
|
|
2390
|
+
return {
|
|
2391
|
+
content: [
|
|
2392
|
+
{
|
|
2393
|
+
type: "text",
|
|
2394
|
+
text: `\u274C "${directoryPath}" is not a directory.`
|
|
1899
2395
|
}
|
|
1900
|
-
|
|
2396
|
+
]
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
let responseText = `\u{1F4C1} **Directory: ${directoryPath || "(root)"}**
|
|
1901
2400
|
`;
|
|
1902
|
-
|
|
2401
|
+
responseText += `Repository: ${githubInfo.repositoryFullName}
|
|
1903
2402
|
`;
|
|
1904
|
-
|
|
2403
|
+
responseText += `Items: ${data.length}
|
|
1905
2404
|
|
|
1906
2405
|
`;
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2406
|
+
const directories = data.filter((item) => item.type === "dir");
|
|
2407
|
+
const files = data.filter((item) => item.type === "file");
|
|
2408
|
+
if (directories.length > 0) {
|
|
2409
|
+
responseText += `**\u{1F4C1} Directories (${directories.length}):**
|
|
1911
2410
|
`;
|
|
1912
|
-
|
|
1913
|
-
responseText += ` - ${dir.name}/
|
|
2411
|
+
for (const dir of directories) responseText += ` - ${dir.name}/
|
|
1914
2412
|
`;
|
|
1915
|
-
|
|
1916
|
-
responseText += `
|
|
2413
|
+
responseText += `
|
|
1917
2414
|
`;
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2415
|
+
}
|
|
2416
|
+
if (files.length > 0) {
|
|
2417
|
+
responseText += `**\u{1F4C4} Files (${files.length}):**
|
|
1921
2418
|
`;
|
|
1922
|
-
|
|
1923
|
-
|
|
2419
|
+
for (const file of files)
|
|
2420
|
+
responseText += ` - ${file.name} (${file.size} bytes)
|
|
1924
2421
|
`;
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
return {
|
|
1928
|
-
content: [{
|
|
1929
|
-
type: "text",
|
|
1930
|
-
text: responseText
|
|
1931
|
-
}]
|
|
1932
|
-
};
|
|
1933
|
-
} catch (error) {
|
|
1934
|
-
console.error("GitHub list directory error:", error);
|
|
1935
|
-
if (error.status === 404) {
|
|
1936
|
-
return {
|
|
1937
|
-
content: [{
|
|
1938
|
-
type: "text",
|
|
1939
|
-
text: `\u274C Directory not found: ${directoryPath}`
|
|
1940
|
-
}]
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
return {
|
|
1944
|
-
content: [{
|
|
1945
|
-
type: "text",
|
|
1946
|
-
text: `\u274C Failed to list directory: ${error.message || "Unknown error"}`
|
|
1947
|
-
}]
|
|
1948
|
-
};
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
default:
|
|
1952
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1953
2422
|
}
|
|
2423
|
+
return { content: [{ type: "text", text: responseText }] };
|
|
1954
2424
|
} catch (error) {
|
|
1955
|
-
console.error(
|
|
1956
|
-
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";
|
|
1957
2435
|
return {
|
|
1958
|
-
content: [
|
|
1959
|
-
type: "text",
|
|
1960
|
-
|
|
1961
|
-
}]
|
|
2436
|
+
content: [
|
|
2437
|
+
{ type: "text", text: `\u274C Failed to list directory: ${message}` }
|
|
2438
|
+
]
|
|
1962
2439
|
};
|
|
1963
2440
|
}
|
|
1964
|
-
}
|
|
2441
|
+
}
|
|
1965
2442
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1966
2443
|
if (!authContext) {
|
|
1967
2444
|
return {
|
|
1968
|
-
contents: [
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2445
|
+
contents: [
|
|
2446
|
+
{
|
|
2447
|
+
uri: request.params.uri,
|
|
2448
|
+
mimeType: "text/plain",
|
|
2449
|
+
text: "Error: Not authenticated. API key validation failed."
|
|
2450
|
+
}
|
|
2451
|
+
]
|
|
1973
2452
|
};
|
|
1974
2453
|
}
|
|
2454
|
+
const ctx = authContext;
|
|
1975
2455
|
const { uri } = request.params;
|
|
1976
2456
|
console.error(`\u{1F4DA} Reading resource: ${uri}`);
|
|
1977
2457
|
try {
|
|
1978
2458
|
switch (uri) {
|
|
1979
2459
|
case "tickets://recent": {
|
|
1980
|
-
const teamIds = await getAccessibleTeamIds(
|
|
1981
|
-
const projectIds = await getAccessibleProjectIds(
|
|
1982
|
-
const customerIds = await getAccessibleCustomerIds(
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2460
|
+
const teamIds = await getAccessibleTeamIds(ctx.teamId);
|
|
2461
|
+
const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
|
|
2462
|
+
const customerIds = await getAccessibleCustomerIds(ctx.teamId);
|
|
2463
|
+
const accessPredicate = buildTicketAccessPredicate(
|
|
2464
|
+
teamIds,
|
|
2465
|
+
projectIds,
|
|
2466
|
+
customerIds
|
|
2467
|
+
);
|
|
2468
|
+
const rows = await db.select({
|
|
2469
|
+
id: schema.tickets.id,
|
|
2470
|
+
ticketNumber: schema.tickets.ticketNumber,
|
|
2471
|
+
title: schema.tickets.title,
|
|
2472
|
+
status: schema.tickets.status,
|
|
2473
|
+
priority: schema.tickets.priority,
|
|
2474
|
+
createdAt: schema.tickets.createdAt
|
|
2475
|
+
}).from(schema.tickets).where(and(accessPredicate, eq(schema.tickets.isDeleted, false))).orderBy(desc(schema.tickets.createdAt)).limit(20);
|
|
1996
2476
|
return {
|
|
1997
|
-
contents: [
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2477
|
+
contents: [
|
|
2478
|
+
{
|
|
2479
|
+
uri,
|
|
2480
|
+
mimeType: "application/json",
|
|
2481
|
+
text: JSON.stringify(rows, null, 2)
|
|
2482
|
+
}
|
|
2483
|
+
]
|
|
2002
2484
|
};
|
|
2003
2485
|
}
|
|
2004
2486
|
case "customers://all": {
|
|
2005
|
-
const customerIds = await getAccessibleCustomerIds(
|
|
2487
|
+
const customerIds = await getAccessibleCustomerIds(ctx.teamId);
|
|
2006
2488
|
if (customerIds.length === 0) {
|
|
2007
2489
|
return {
|
|
2008
|
-
contents: [
|
|
2009
|
-
uri,
|
|
2010
|
-
|
|
2011
|
-
text: JSON.stringify([], null, 2)
|
|
2012
|
-
}]
|
|
2490
|
+
contents: [
|
|
2491
|
+
{ uri, mimeType: "application/json", text: JSON.stringify([], null, 2) }
|
|
2492
|
+
]
|
|
2013
2493
|
};
|
|
2014
2494
|
}
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2495
|
+
const rows = await db.select({
|
|
2496
|
+
id: schema.customers.id,
|
|
2497
|
+
name: schema.customers.name,
|
|
2498
|
+
email: schema.customers.email,
|
|
2499
|
+
website: schema.customers.website,
|
|
2500
|
+
createdAt: schema.customers.createdAt
|
|
2501
|
+
}).from(schema.customers).where(inArray(schema.customers.id, customerIds)).orderBy(asc(schema.customers.name)).limit(50);
|
|
2017
2502
|
return {
|
|
2018
|
-
contents: [
|
|
2019
|
-
uri,
|
|
2020
|
-
|
|
2021
|
-
text: JSON.stringify(data, null, 2)
|
|
2022
|
-
}]
|
|
2503
|
+
contents: [
|
|
2504
|
+
{ uri, mimeType: "application/json", text: JSON.stringify(rows, null, 2) }
|
|
2505
|
+
]
|
|
2023
2506
|
};
|
|
2024
2507
|
}
|
|
2025
2508
|
case "projects://active": {
|
|
2026
|
-
const projectIds = await getAccessibleProjectIds(
|
|
2509
|
+
const projectIds = await getAccessibleProjectIds(ctx.userId, ctx.teamId);
|
|
2027
2510
|
if (projectIds.length === 0) {
|
|
2028
2511
|
return {
|
|
2029
|
-
contents: [
|
|
2030
|
-
uri,
|
|
2031
|
-
|
|
2032
|
-
text: JSON.stringify([], null, 2)
|
|
2033
|
-
}]
|
|
2512
|
+
contents: [
|
|
2513
|
+
{ uri, mimeType: "application/json", text: JSON.stringify([], null, 2) }
|
|
2514
|
+
]
|
|
2034
2515
|
};
|
|
2035
2516
|
}
|
|
2036
|
-
const
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2517
|
+
const rows = await db.select({
|
|
2518
|
+
id: schema.projects.id,
|
|
2519
|
+
name: schema.projects.name,
|
|
2520
|
+
description: schema.projects.description,
|
|
2521
|
+
createdAt: schema.projects.createdAt,
|
|
2522
|
+
customerId: schema.projects.customerId,
|
|
2523
|
+
customerName: schema.customers.name
|
|
2524
|
+
}).from(schema.projects).leftJoin(
|
|
2525
|
+
schema.customers,
|
|
2526
|
+
eq(schema.customers.id, schema.projects.customerId)
|
|
2527
|
+
).where(inArray(schema.projects.id, projectIds)).orderBy(asc(schema.projects.name)).limit(50);
|
|
2045
2528
|
return {
|
|
2046
|
-
contents: [
|
|
2047
|
-
uri,
|
|
2048
|
-
|
|
2049
|
-
text: JSON.stringify(data, null, 2)
|
|
2050
|
-
}]
|
|
2529
|
+
contents: [
|
|
2530
|
+
{ uri, mimeType: "application/json", text: JSON.stringify(rows, null, 2) }
|
|
2531
|
+
]
|
|
2051
2532
|
};
|
|
2052
2533
|
}
|
|
2053
2534
|
default:
|
|
2054
2535
|
throw new Error(`Unknown resource: ${uri}`);
|
|
2055
2536
|
}
|
|
2056
2537
|
} catch (error) {
|
|
2057
|
-
console.error(
|
|
2538
|
+
console.error("\u274C Resource read error:", error);
|
|
2058
2539
|
return {
|
|
2059
|
-
contents: [
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2540
|
+
contents: [
|
|
2541
|
+
{
|
|
2542
|
+
uri,
|
|
2543
|
+
mimeType: "text/plain",
|
|
2544
|
+
text: `Error reading ${uri}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2545
|
+
}
|
|
2546
|
+
]
|
|
2064
2547
|
};
|
|
2065
2548
|
}
|
|
2066
2549
|
});
|
|
2067
2550
|
async function main() {
|
|
2068
|
-
console.error("\u{1F680} Starting
|
|
2551
|
+
console.error("\u{1F680} Starting Refront MCP Bridge Server...");
|
|
2069
2552
|
console.error(`\u{1F511} API Key: ${apiKey?.substring(0, 10)}...`);
|
|
2070
2553
|
authContext = await validateApiKey(apiKey);
|
|
2071
2554
|
if (!authContext) {
|
|
2072
|
-
console.error(
|
|
2555
|
+
console.error(
|
|
2556
|
+
"\u274C API key validation failed. Please check your key and try again."
|
|
2557
|
+
);
|
|
2073
2558
|
process.exit(1);
|
|
2074
2559
|
}
|
|
2075
|
-
console.error(
|
|
2560
|
+
console.error(
|
|
2561
|
+
`\u2705 Authenticated as user ${authContext.userId} in team ${authContext.teamId}`
|
|
2562
|
+
);
|
|
2076
2563
|
console.error(`\u{1F4CB} Available scopes: ${authContext.scopes.join(", ")}`);
|
|
2077
2564
|
console.error("\u{1F4E1} MCP Bridge Server ready for connections");
|
|
2078
2565
|
const transport = new StdioServerTransport();
|