@mgsoftwarebv/mcp-server-bridge 3.5.17 → 3.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +614 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -107843,6 +107843,59 @@ var TOOLS = [
|
|
|
107843
107843
|
},
|
|
107844
107844
|
required: ["id"]
|
|
107845
107845
|
}
|
|
107846
|
+
},
|
|
107847
|
+
{
|
|
107848
|
+
name: "get-user-activity-report",
|
|
107849
|
+
description: "Daily execution/activity report for a single user (or the whole team) over a date range \u2014 built for standups and end-of-day summaries like 'what did Prince do today?'. Aggregates, per requested timezone day boundaries, the user's ticket changes (created/status/assignee/priority/type/tag/GitHub events from the audit log), comments, attachments, time entries (worked hours) and organized calendar items into one chronological timeline plus summary counts. The user is matched as the ACTOR who performed each action (distinct from a ticket's assignee/requester). Scoped to your provider team(s); pass a user UUID for a colleague, 'me' for yourself (default), or 'all' for the whole team. Each activity carries the ticket UUID + ticket number + title, project, a plain-English summary and the actor. Returns explicit `limitations` (e.g. invalid timezone fallback, unknown user, project filter not applicable to calendar items). Also accepts the alias name get_user_activity_report. Use get-projects to resolve a projectId and get-teams to resolve a teamId first.",
|
|
107850
|
+
inputSchema: {
|
|
107851
|
+
type: "object",
|
|
107852
|
+
properties: {
|
|
107853
|
+
teamId: teamIdProp,
|
|
107854
|
+
userId: {
|
|
107855
|
+
type: "string",
|
|
107856
|
+
description: "User to report on (the actor). Defaults to the API key user ('me'). Pass a user UUID for a colleague, or 'all' for every accessible team member."
|
|
107857
|
+
},
|
|
107858
|
+
dateFrom: {
|
|
107859
|
+
type: "string",
|
|
107860
|
+
description: "Inclusive start day (YYYY-MM-DD) in `timezone`. Defaults to today. If only dateTo is given, dateFrom mirrors it."
|
|
107861
|
+
},
|
|
107862
|
+
dateTo: {
|
|
107863
|
+
type: "string",
|
|
107864
|
+
description: "Inclusive end day (YYYY-MM-DD) in `timezone`. Defaults to dateFrom (or today). For a single-day report pass the same value as dateFrom or omit it."
|
|
107865
|
+
},
|
|
107866
|
+
timezone: {
|
|
107867
|
+
type: "string",
|
|
107868
|
+
default: "Europe/Amsterdam",
|
|
107869
|
+
description: "IANA timezone (e.g. Europe/Amsterdam) used to compute day boundaries and local timestamps. Invalid values fall back to Europe/Amsterdam with a note in `limitations`."
|
|
107870
|
+
},
|
|
107871
|
+
include: {
|
|
107872
|
+
type: "array",
|
|
107873
|
+
items: {
|
|
107874
|
+
type: "string",
|
|
107875
|
+
enum: [
|
|
107876
|
+
"tickets",
|
|
107877
|
+
"comments",
|
|
107878
|
+
"status_changes",
|
|
107879
|
+
"attachments",
|
|
107880
|
+
"time_entries",
|
|
107881
|
+
"calendar_items"
|
|
107882
|
+
]
|
|
107883
|
+
},
|
|
107884
|
+
description: "Activity categories to include. Defaults to all six. 'tickets' = ticket lifecycle/audit events (created, tag, GitHub, generic updates); 'status_changes' = status/assignee/priority/type/project transitions; 'comments', 'attachments', 'time_entries' (worked hours), 'calendar_items' (agenda events organized by the user)."
|
|
107885
|
+
},
|
|
107886
|
+
projectId: {
|
|
107887
|
+
type: "string",
|
|
107888
|
+
description: "Restrict ticket-linked activity (and time entries) to this project UUID. Does not apply to calendar items, which are then omitted (noted in `limitations`)."
|
|
107889
|
+
},
|
|
107890
|
+
pageSize: {
|
|
107891
|
+
type: "number",
|
|
107892
|
+
default: 200,
|
|
107893
|
+
maximum: 500,
|
|
107894
|
+
description: "Max activities returned in the timeline. Summary counts always cover ALL matching activity regardless of this limit."
|
|
107895
|
+
}
|
|
107896
|
+
},
|
|
107897
|
+
required: []
|
|
107898
|
+
}
|
|
107846
107899
|
}
|
|
107847
107900
|
];
|
|
107848
107901
|
var RESOURCES = [
|
|
@@ -127169,6 +127222,562 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
|
|
|
127169
127222
|
};
|
|
127170
127223
|
}
|
|
127171
127224
|
|
|
127225
|
+
// src/tools/user-activity-report.ts
|
|
127226
|
+
var DEFAULT_TIMEZONE = "Europe/Amsterdam";
|
|
127227
|
+
var DEFAULT_PAGE_SIZE = 200;
|
|
127228
|
+
var MAX_PAGE_SIZE = 500;
|
|
127229
|
+
var MAX_ROWS_PER_SOURCE = 2e3;
|
|
127230
|
+
var COMMENT_PREVIEW_LENGTH = 140;
|
|
127231
|
+
var ACTIVITY_INCLUDE_VALUES = [
|
|
127232
|
+
"tickets",
|
|
127233
|
+
"comments",
|
|
127234
|
+
"status_changes",
|
|
127235
|
+
"attachments",
|
|
127236
|
+
"time_entries",
|
|
127237
|
+
"calendar_items"
|
|
127238
|
+
];
|
|
127239
|
+
var STATUS_CHANGE_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
|
|
127240
|
+
"status_change",
|
|
127241
|
+
"status_changed",
|
|
127242
|
+
"assignment",
|
|
127243
|
+
"assignee_changed",
|
|
127244
|
+
"priority_change",
|
|
127245
|
+
"priority_changed",
|
|
127246
|
+
"type_change",
|
|
127247
|
+
"type_changed",
|
|
127248
|
+
"project_changed"
|
|
127249
|
+
]);
|
|
127250
|
+
var COMMENT_ACTIVITY_TYPES = /* @__PURE__ */ new Set([
|
|
127251
|
+
"comment_added",
|
|
127252
|
+
"comment_internal_added",
|
|
127253
|
+
"comment_updated",
|
|
127254
|
+
"comment_deleted"
|
|
127255
|
+
]);
|
|
127256
|
+
function resolveIncludes(input) {
|
|
127257
|
+
if (input == null || input.length === 0) {
|
|
127258
|
+
return { includes: new Set(ACTIVITY_INCLUDE_VALUES), invalid: [] };
|
|
127259
|
+
}
|
|
127260
|
+
const includes = /* @__PURE__ */ new Set();
|
|
127261
|
+
const invalid = [];
|
|
127262
|
+
for (const raw of input) {
|
|
127263
|
+
const value = String(raw).trim();
|
|
127264
|
+
if (ACTIVITY_INCLUDE_VALUES.includes(value)) {
|
|
127265
|
+
includes.add(value);
|
|
127266
|
+
} else if (value.length > 0) {
|
|
127267
|
+
invalid.push(value);
|
|
127268
|
+
}
|
|
127269
|
+
}
|
|
127270
|
+
return { includes, invalid };
|
|
127271
|
+
}
|
|
127272
|
+
function isValidIsoDate2(value) {
|
|
127273
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
127274
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
127275
|
+
if (Number.isNaN(parsed.getTime())) return false;
|
|
127276
|
+
return parsed.toISOString().slice(0, 10) === value;
|
|
127277
|
+
}
|
|
127278
|
+
function isValidTimeZone(timeZone) {
|
|
127279
|
+
if (!timeZone) return false;
|
|
127280
|
+
try {
|
|
127281
|
+
new Intl.DateTimeFormat("en-US", { timeZone });
|
|
127282
|
+
return true;
|
|
127283
|
+
} catch {
|
|
127284
|
+
return false;
|
|
127285
|
+
}
|
|
127286
|
+
}
|
|
127287
|
+
function todayInTimeZone(timeZone, now2 = /* @__PURE__ */ new Date()) {
|
|
127288
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
127289
|
+
timeZone,
|
|
127290
|
+
year: "numeric",
|
|
127291
|
+
month: "2-digit",
|
|
127292
|
+
day: "2-digit"
|
|
127293
|
+
}).format(now2);
|
|
127294
|
+
}
|
|
127295
|
+
function resolveDayWindow(args2) {
|
|
127296
|
+
const today = todayInTimeZone(args2.timezone, args2.now);
|
|
127297
|
+
const from = args2.dateFrom ?? args2.dateTo ?? today;
|
|
127298
|
+
const to = args2.dateTo ?? args2.dateFrom ?? today;
|
|
127299
|
+
return { from, to };
|
|
127300
|
+
}
|
|
127301
|
+
function classifyTicketActivity(activityType) {
|
|
127302
|
+
if (COMMENT_ACTIVITY_TYPES.has(activityType)) return null;
|
|
127303
|
+
if (STATUS_CHANGE_ACTIVITY_TYPES.has(activityType)) return "status_changes";
|
|
127304
|
+
return "tickets";
|
|
127305
|
+
}
|
|
127306
|
+
function humanizeActivityType(activityType) {
|
|
127307
|
+
const text3 = activityType.replace(/_/g, " ").trim();
|
|
127308
|
+
if (text3.length === 0) return "Activity";
|
|
127309
|
+
return text3.charAt(0).toUpperCase() + text3.slice(1);
|
|
127310
|
+
}
|
|
127311
|
+
function summarizeTicketActivity(activityType, oldValue, newValue) {
|
|
127312
|
+
const oldV = oldValue && oldValue.trim() !== "" ? oldValue.trim() : null;
|
|
127313
|
+
const newV = newValue && newValue.trim() !== "" ? newValue.trim() : null;
|
|
127314
|
+
switch (activityType) {
|
|
127315
|
+
case "created":
|
|
127316
|
+
return "Created the ticket";
|
|
127317
|
+
case "updated":
|
|
127318
|
+
return "Updated the ticket";
|
|
127319
|
+
case "status_change":
|
|
127320
|
+
case "status_changed":
|
|
127321
|
+
if (oldV && newV) return `Changed status from ${oldV} to ${newV}`;
|
|
127322
|
+
if (newV) return `Changed status to ${newV}`;
|
|
127323
|
+
return "Changed status";
|
|
127324
|
+
case "assignment":
|
|
127325
|
+
case "assignee_changed":
|
|
127326
|
+
if (!oldV && newV) return `Assigned to ${newV}`;
|
|
127327
|
+
if (oldV && !newV) return `Unassigned from ${oldV}`;
|
|
127328
|
+
if (newV) return `Reassigned to ${newV}`;
|
|
127329
|
+
return "Changed assignment";
|
|
127330
|
+
case "priority_change":
|
|
127331
|
+
case "priority_changed":
|
|
127332
|
+
if (oldV && newV) return `Changed priority from ${oldV} to ${newV}`;
|
|
127333
|
+
if (newV) return `Changed priority to ${newV}`;
|
|
127334
|
+
return "Changed priority";
|
|
127335
|
+
case "type_change":
|
|
127336
|
+
case "type_changed":
|
|
127337
|
+
if (oldV && newV) return `Changed type from ${oldV} to ${newV}`;
|
|
127338
|
+
if (newV) return `Changed type to ${newV}`;
|
|
127339
|
+
return "Changed type";
|
|
127340
|
+
case "project_changed":
|
|
127341
|
+
if (oldV && newV) return `Moved project from ${oldV} to ${newV}`;
|
|
127342
|
+
return "Changed project";
|
|
127343
|
+
case "tag_added":
|
|
127344
|
+
return newV ? `Added tag ${newV}` : "Added a tag";
|
|
127345
|
+
case "tag_removed":
|
|
127346
|
+
return oldV ? `Removed tag ${oldV}` : "Removed a tag";
|
|
127347
|
+
case "sequence_changed":
|
|
127348
|
+
return "Reordered the ticket";
|
|
127349
|
+
case "github_commit_linked":
|
|
127350
|
+
return "Linked a GitHub commit";
|
|
127351
|
+
case "github_pr_opened":
|
|
127352
|
+
return "Opened a GitHub pull request";
|
|
127353
|
+
case "github_pr_merged":
|
|
127354
|
+
return "Merged a GitHub pull request";
|
|
127355
|
+
case "github_pr_closed":
|
|
127356
|
+
return "Closed a GitHub pull request";
|
|
127357
|
+
default:
|
|
127358
|
+
return humanizeActivityType(activityType);
|
|
127359
|
+
}
|
|
127360
|
+
}
|
|
127361
|
+
function truncate(text3, max) {
|
|
127362
|
+
const clean = text3.replace(/\s+/g, " ").trim();
|
|
127363
|
+
if (clean.length <= max) return clean;
|
|
127364
|
+
return `${clean.slice(0, Math.max(0, max - 1)).trimEnd()}\u2026`;
|
|
127365
|
+
}
|
|
127366
|
+
function timeZoneOffset(date10, timeZone) {
|
|
127367
|
+
try {
|
|
127368
|
+
const name21 = new Intl.DateTimeFormat("en-US", {
|
|
127369
|
+
timeZone,
|
|
127370
|
+
timeZoneName: "longOffset"
|
|
127371
|
+
}).formatToParts(date10).find((p3) => p3.type === "timeZoneName")?.value;
|
|
127372
|
+
const match = name21?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
127373
|
+
return match?.[1] ?? "+00:00";
|
|
127374
|
+
} catch {
|
|
127375
|
+
return "+00:00";
|
|
127376
|
+
}
|
|
127377
|
+
}
|
|
127378
|
+
function formatIsoWithOffset(date10, timeZone) {
|
|
127379
|
+
const parts = Object.fromEntries(
|
|
127380
|
+
new Intl.DateTimeFormat("en-US", {
|
|
127381
|
+
timeZone,
|
|
127382
|
+
year: "numeric",
|
|
127383
|
+
month: "2-digit",
|
|
127384
|
+
day: "2-digit",
|
|
127385
|
+
hour: "2-digit",
|
|
127386
|
+
minute: "2-digit",
|
|
127387
|
+
second: "2-digit",
|
|
127388
|
+
hour12: false
|
|
127389
|
+
}).formatToParts(date10).map((p3) => [p3.type, p3.value])
|
|
127390
|
+
);
|
|
127391
|
+
const hour2 = parts.hour === "24" ? "00" : parts.hour;
|
|
127392
|
+
const local = `${parts.year}-${parts.month}-${parts.day}T${hour2}:${parts.minute}:${parts.second}`;
|
|
127393
|
+
return `${local}${timeZoneOffset(date10, timeZone)}`;
|
|
127394
|
+
}
|
|
127395
|
+
function textResponse13(text3) {
|
|
127396
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
127397
|
+
}
|
|
127398
|
+
function jsonResponse3(payload) {
|
|
127399
|
+
return {
|
|
127400
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
127401
|
+
};
|
|
127402
|
+
}
|
|
127403
|
+
function toNumber4(value) {
|
|
127404
|
+
if (value == null) return 0;
|
|
127405
|
+
if (typeof value === "number") return value;
|
|
127406
|
+
const parsed = Number.parseFloat(String(value));
|
|
127407
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
127408
|
+
}
|
|
127409
|
+
function hoursFrom2(seconds) {
|
|
127410
|
+
return Math.round(toNumber4(seconds) / 3600 * 100) / 100;
|
|
127411
|
+
}
|
|
127412
|
+
function utcIsoExpr(column) {
|
|
127413
|
+
return sql`to_char(${column} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`;
|
|
127414
|
+
}
|
|
127415
|
+
async function handleGetUserActivityReport(input) {
|
|
127416
|
+
const ctx = getAuthContext();
|
|
127417
|
+
const limitations = [];
|
|
127418
|
+
const { includes, invalid } = resolveIncludes(input.include);
|
|
127419
|
+
for (const value of invalid) {
|
|
127420
|
+
limitations.push(`Ignored unknown include value "${value}".`);
|
|
127421
|
+
}
|
|
127422
|
+
if (includes.size === 0) {
|
|
127423
|
+
return textResponse13(
|
|
127424
|
+
`Error: \`include\` contained no valid categories. Allowed: ${ACTIVITY_INCLUDE_VALUES.join(", ")}.`
|
|
127425
|
+
);
|
|
127426
|
+
}
|
|
127427
|
+
let timezone = input.timezone?.trim() || DEFAULT_TIMEZONE;
|
|
127428
|
+
if (!isValidTimeZone(timezone)) {
|
|
127429
|
+
limitations.push(
|
|
127430
|
+
`Invalid timezone "${input.timezone}"; fell back to ${DEFAULT_TIMEZONE}.`
|
|
127431
|
+
);
|
|
127432
|
+
timezone = DEFAULT_TIMEZONE;
|
|
127433
|
+
}
|
|
127434
|
+
if (input.dateFrom && !isValidIsoDate2(input.dateFrom)) {
|
|
127435
|
+
return textResponse13(
|
|
127436
|
+
`Error: invalid dateFrom "${input.dateFrom}". Use YYYY-MM-DD.`
|
|
127437
|
+
);
|
|
127438
|
+
}
|
|
127439
|
+
if (input.dateTo && !isValidIsoDate2(input.dateTo)) {
|
|
127440
|
+
return textResponse13(
|
|
127441
|
+
`Error: invalid dateTo "${input.dateTo}". Use YYYY-MM-DD.`
|
|
127442
|
+
);
|
|
127443
|
+
}
|
|
127444
|
+
const window2 = resolveDayWindow({
|
|
127445
|
+
dateFrom: input.dateFrom,
|
|
127446
|
+
dateTo: input.dateTo,
|
|
127447
|
+
timezone
|
|
127448
|
+
});
|
|
127449
|
+
if (window2.from > window2.to) {
|
|
127450
|
+
return textResponse13(
|
|
127451
|
+
`Error: dateFrom (${window2.from}) is after dateTo (${window2.to}).`
|
|
127452
|
+
);
|
|
127453
|
+
}
|
|
127454
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
127455
|
+
if (!scope.ok) return scope.response;
|
|
127456
|
+
if (scope.teamIds.length === 0) {
|
|
127457
|
+
return textResponse13("No accessible teams found.");
|
|
127458
|
+
}
|
|
127459
|
+
if (input.projectId && !scope.projectIds.includes(input.projectId)) {
|
|
127460
|
+
return textResponse13(
|
|
127461
|
+
`Project not found or no access: ${input.projectId}. Call get-projects first.`
|
|
127462
|
+
);
|
|
127463
|
+
}
|
|
127464
|
+
const rawUserId = input.userId?.trim();
|
|
127465
|
+
const userFilter = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
|
|
127466
|
+
const isAll = userFilter === "all";
|
|
127467
|
+
let user;
|
|
127468
|
+
if (isAll) {
|
|
127469
|
+
user = { id: "all", name: "All team members" };
|
|
127470
|
+
} else {
|
|
127471
|
+
const [row] = await db.select({ id: schema_exports.users.id, name: schema_exports.users.fullName }).from(schema_exports.users).where(eq(schema_exports.users.id, userFilter)).limit(1);
|
|
127472
|
+
if (!row) {
|
|
127473
|
+
limitations.push(
|
|
127474
|
+
`User ${userFilter} was not found; reporting any team-scoped activity recorded for that id.`
|
|
127475
|
+
);
|
|
127476
|
+
user = { id: userFilter, name: null };
|
|
127477
|
+
} else {
|
|
127478
|
+
user = { id: row.id, name: row.name };
|
|
127479
|
+
}
|
|
127480
|
+
}
|
|
127481
|
+
const dayWindow = (column) => sql`(${column} AT TIME ZONE ${timezone})::date BETWEEN ${window2.from}::date AND ${window2.to}::date`;
|
|
127482
|
+
const ticketAccess = and(
|
|
127483
|
+
buildTicketAccessPredicate(
|
|
127484
|
+
scope.teamIds,
|
|
127485
|
+
scope.projectIds,
|
|
127486
|
+
scope.customerIds
|
|
127487
|
+
),
|
|
127488
|
+
eq(schema_exports.tickets.isDeleted, false)
|
|
127489
|
+
);
|
|
127490
|
+
const activities2 = [];
|
|
127491
|
+
const ticketsTouched = /* @__PURE__ */ new Set();
|
|
127492
|
+
let commentsAdded = 0;
|
|
127493
|
+
let statusChanges = 0;
|
|
127494
|
+
let attachmentsAdded = 0;
|
|
127495
|
+
let timeEntrySeconds = 0;
|
|
127496
|
+
const pushTicketTouched = (ticketId) => {
|
|
127497
|
+
if (ticketId) ticketsTouched.add(ticketId);
|
|
127498
|
+
};
|
|
127499
|
+
if (includes.has("tickets") || includes.has("status_changes")) {
|
|
127500
|
+
const ta = schema_exports.ticketActivity;
|
|
127501
|
+
const conditions = [ticketAccess, dayWindow(ta.createdAt)];
|
|
127502
|
+
if (!isAll) conditions.push(eq(ta.userId, userFilter));
|
|
127503
|
+
if (input.projectId)
|
|
127504
|
+
conditions.push(eq(schema_exports.tickets.projectId, input.projectId));
|
|
127505
|
+
const rows = await db.select({
|
|
127506
|
+
ts: utcIsoExpr(ta.createdAt),
|
|
127507
|
+
activityType: ta.activityType,
|
|
127508
|
+
oldValue: ta.oldValue,
|
|
127509
|
+
newValue: ta.newValue,
|
|
127510
|
+
actorId: ta.userId,
|
|
127511
|
+
actorName: schema_exports.users.fullName,
|
|
127512
|
+
ticketId: schema_exports.tickets.id,
|
|
127513
|
+
ticketNumber: schema_exports.tickets.ticketNumber,
|
|
127514
|
+
ticketTitle: schema_exports.tickets.title,
|
|
127515
|
+
projectId: schema_exports.projects.id,
|
|
127516
|
+
projectName: schema_exports.projects.name
|
|
127517
|
+
}).from(ta).innerJoin(schema_exports.tickets, eq(schema_exports.tickets.id, ta.ticketId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, ta.userId)).leftJoin(schema_exports.projects, eq(schema_exports.projects.id, schema_exports.tickets.projectId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
|
|
127518
|
+
if (rows.length >= MAX_ROWS_PER_SOURCE) {
|
|
127519
|
+
limitations.push(
|
|
127520
|
+
`Ticket activity capped at ${MAX_ROWS_PER_SOURCE} rows; counts may be partial. Narrow the date range.`
|
|
127521
|
+
);
|
|
127522
|
+
}
|
|
127523
|
+
for (const r6 of rows) {
|
|
127524
|
+
const bucket = classifyTicketActivity(r6.activityType);
|
|
127525
|
+
if (!bucket || !includes.has(bucket)) continue;
|
|
127526
|
+
pushTicketTouched(r6.ticketId);
|
|
127527
|
+
if (bucket === "status_changes") statusChanges += 1;
|
|
127528
|
+
activities2.push({
|
|
127529
|
+
timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
|
|
127530
|
+
timestampUtc: r6.ts,
|
|
127531
|
+
type: `ticket_${r6.activityType}`,
|
|
127532
|
+
ticketId: r6.ticketId,
|
|
127533
|
+
ticketNumber: r6.ticketNumber,
|
|
127534
|
+
ticketTitle: r6.ticketTitle,
|
|
127535
|
+
project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
|
|
127536
|
+
summary: summarizeTicketActivity(
|
|
127537
|
+
r6.activityType,
|
|
127538
|
+
r6.oldValue,
|
|
127539
|
+
r6.newValue
|
|
127540
|
+
),
|
|
127541
|
+
actor: { id: r6.actorId, name: r6.actorName },
|
|
127542
|
+
meta: {
|
|
127543
|
+
activityType: r6.activityType,
|
|
127544
|
+
from: r6.oldValue ?? null,
|
|
127545
|
+
to: r6.newValue ?? null
|
|
127546
|
+
}
|
|
127547
|
+
});
|
|
127548
|
+
}
|
|
127549
|
+
}
|
|
127550
|
+
if (includes.has("comments")) {
|
|
127551
|
+
const tc = schema_exports.ticketComments;
|
|
127552
|
+
const conditions = [ticketAccess, dayWindow(tc.createdAt)];
|
|
127553
|
+
if (!isAll) conditions.push(eq(tc.userId, userFilter));
|
|
127554
|
+
if (input.projectId)
|
|
127555
|
+
conditions.push(eq(schema_exports.tickets.projectId, input.projectId));
|
|
127556
|
+
const rows = await db.select({
|
|
127557
|
+
ts: utcIsoExpr(tc.createdAt),
|
|
127558
|
+
content: tc.content,
|
|
127559
|
+
isInternal: tc.isInternal,
|
|
127560
|
+
actorId: tc.userId,
|
|
127561
|
+
actorUserName: tc.userName,
|
|
127562
|
+
actorName: schema_exports.users.fullName,
|
|
127563
|
+
ticketId: schema_exports.tickets.id,
|
|
127564
|
+
ticketNumber: schema_exports.tickets.ticketNumber,
|
|
127565
|
+
ticketTitle: schema_exports.tickets.title,
|
|
127566
|
+
projectId: schema_exports.projects.id,
|
|
127567
|
+
projectName: schema_exports.projects.name
|
|
127568
|
+
}).from(tc).innerJoin(schema_exports.tickets, eq(schema_exports.tickets.id, tc.ticketId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, tc.userId)).leftJoin(schema_exports.projects, eq(schema_exports.projects.id, schema_exports.tickets.projectId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
|
|
127569
|
+
if (rows.length >= MAX_ROWS_PER_SOURCE) {
|
|
127570
|
+
limitations.push(
|
|
127571
|
+
`Comments capped at ${MAX_ROWS_PER_SOURCE} rows; counts may be partial. Narrow the date range.`
|
|
127572
|
+
);
|
|
127573
|
+
}
|
|
127574
|
+
for (const r6 of rows) {
|
|
127575
|
+
commentsAdded += 1;
|
|
127576
|
+
pushTicketTouched(r6.ticketId);
|
|
127577
|
+
activities2.push({
|
|
127578
|
+
timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
|
|
127579
|
+
timestampUtc: r6.ts,
|
|
127580
|
+
type: r6.isInternal ? "ticket_internal_comment" : "ticket_comment",
|
|
127581
|
+
ticketId: r6.ticketId,
|
|
127582
|
+
ticketNumber: r6.ticketNumber,
|
|
127583
|
+
ticketTitle: r6.ticketTitle,
|
|
127584
|
+
project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
|
|
127585
|
+
summary: `Added ${r6.isInternal ? "internal " : ""}comment: ${truncate(
|
|
127586
|
+
r6.content,
|
|
127587
|
+
COMMENT_PREVIEW_LENGTH
|
|
127588
|
+
)}`,
|
|
127589
|
+
actor: { id: r6.actorId, name: r6.actorName ?? r6.actorUserName },
|
|
127590
|
+
meta: { isInternal: r6.isInternal }
|
|
127591
|
+
});
|
|
127592
|
+
}
|
|
127593
|
+
}
|
|
127594
|
+
if (includes.has("attachments")) {
|
|
127595
|
+
const collectAttachments = async (table, type) => {
|
|
127596
|
+
const conditions = [ticketAccess, dayWindow(table.createdAt)];
|
|
127597
|
+
if (!isAll) conditions.push(eq(table.userId, userFilter));
|
|
127598
|
+
if (input.projectId)
|
|
127599
|
+
conditions.push(eq(schema_exports.tickets.projectId, input.projectId));
|
|
127600
|
+
const rows = await db.select({
|
|
127601
|
+
ts: utcIsoExpr(table.createdAt),
|
|
127602
|
+
fileName: table.fileName,
|
|
127603
|
+
actorId: table.userId,
|
|
127604
|
+
actorName: schema_exports.users.fullName,
|
|
127605
|
+
ticketId: schema_exports.tickets.id,
|
|
127606
|
+
ticketNumber: schema_exports.tickets.ticketNumber,
|
|
127607
|
+
ticketTitle: schema_exports.tickets.title,
|
|
127608
|
+
projectId: schema_exports.projects.id,
|
|
127609
|
+
projectName: schema_exports.projects.name
|
|
127610
|
+
}).from(table).innerJoin(schema_exports.tickets, eq(schema_exports.tickets.id, table.ticketId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, table.userId)).leftJoin(
|
|
127611
|
+
schema_exports.projects,
|
|
127612
|
+
eq(schema_exports.projects.id, schema_exports.tickets.projectId)
|
|
127613
|
+
).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
|
|
127614
|
+
for (const r6 of rows) {
|
|
127615
|
+
attachmentsAdded += 1;
|
|
127616
|
+
pushTicketTouched(r6.ticketId);
|
|
127617
|
+
activities2.push({
|
|
127618
|
+
timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
|
|
127619
|
+
timestampUtc: r6.ts,
|
|
127620
|
+
type,
|
|
127621
|
+
ticketId: r6.ticketId,
|
|
127622
|
+
ticketNumber: r6.ticketNumber,
|
|
127623
|
+
ticketTitle: r6.ticketTitle,
|
|
127624
|
+
project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
|
|
127625
|
+
summary: `Uploaded attachment ${r6.fileName}`,
|
|
127626
|
+
actor: { id: r6.actorId, name: r6.actorName },
|
|
127627
|
+
meta: { fileName: r6.fileName }
|
|
127628
|
+
});
|
|
127629
|
+
}
|
|
127630
|
+
};
|
|
127631
|
+
await collectAttachments(schema_exports.ticketAttachments, "ticket_attachment");
|
|
127632
|
+
await collectAttachments(
|
|
127633
|
+
schema_exports.ticketCommentAttachments,
|
|
127634
|
+
"ticket_comment_attachment"
|
|
127635
|
+
);
|
|
127636
|
+
}
|
|
127637
|
+
if (includes.has("time_entries")) {
|
|
127638
|
+
const te = schema_exports.timesheetEvents;
|
|
127639
|
+
const durationSeconds = sql`CASE WHEN ${te.endTime} IS NOT NULL THEN GREATEST(0, EXTRACT(EPOCH FROM (${te.endTime} - ${te.startTime}))) ELSE COALESCE(${te.trackedDuration}, 0) END`;
|
|
127640
|
+
const conditions = [
|
|
127641
|
+
inArray(te.teamId, scope.teamIds),
|
|
127642
|
+
eq(te.isDeleted, false),
|
|
127643
|
+
eq(te.allDay, false),
|
|
127644
|
+
sql`${te.type}::text <> 'deadline'`,
|
|
127645
|
+
dayWindow(te.startTime)
|
|
127646
|
+
];
|
|
127647
|
+
if (!isAll) conditions.push(eq(te.userId, userFilter));
|
|
127648
|
+
if (input.projectId) conditions.push(eq(te.projectId, input.projectId));
|
|
127649
|
+
const rows = await db.select({
|
|
127650
|
+
id: te.id,
|
|
127651
|
+
ts: utcIsoExpr(te.startTime),
|
|
127652
|
+
title: te.title,
|
|
127653
|
+
type: te.type,
|
|
127654
|
+
billingStatus: te.billingStatus,
|
|
127655
|
+
durationSeconds,
|
|
127656
|
+
actorId: te.userId,
|
|
127657
|
+
actorName: schema_exports.users.fullName,
|
|
127658
|
+
projectId: te.projectId,
|
|
127659
|
+
projectName: schema_exports.projects.name
|
|
127660
|
+
}).from(te).leftJoin(schema_exports.projects, eq(schema_exports.projects.id, te.projectId)).leftJoin(schema_exports.users, eq(schema_exports.users.id, te.userId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
|
|
127661
|
+
const entryIds = rows.map((r6) => r6.id);
|
|
127662
|
+
const ticketsByEvent = /* @__PURE__ */ new Map();
|
|
127663
|
+
if (entryIds.length > 0) {
|
|
127664
|
+
const links = await db.select({
|
|
127665
|
+
eventId: schema_exports.timesheetEventTickets.timesheetEventId,
|
|
127666
|
+
ticketId: schema_exports.tickets.id,
|
|
127667
|
+
ticketNumber: schema_exports.tickets.ticketNumber,
|
|
127668
|
+
ticketTitle: schema_exports.tickets.title
|
|
127669
|
+
}).from(schema_exports.timesheetEventTickets).innerJoin(
|
|
127670
|
+
schema_exports.tickets,
|
|
127671
|
+
eq(schema_exports.timesheetEventTickets.ticketId, schema_exports.tickets.id)
|
|
127672
|
+
).where(
|
|
127673
|
+
inArray(schema_exports.timesheetEventTickets.timesheetEventId, entryIds)
|
|
127674
|
+
);
|
|
127675
|
+
for (const link of links) {
|
|
127676
|
+
const list = ticketsByEvent.get(link.eventId) ?? [];
|
|
127677
|
+
list.push({
|
|
127678
|
+
id: link.ticketId,
|
|
127679
|
+
ticketNumber: link.ticketNumber,
|
|
127680
|
+
title: link.ticketTitle
|
|
127681
|
+
});
|
|
127682
|
+
ticketsByEvent.set(link.eventId, list);
|
|
127683
|
+
}
|
|
127684
|
+
}
|
|
127685
|
+
for (const r6 of rows) {
|
|
127686
|
+
const seconds = toNumber4(r6.durationSeconds);
|
|
127687
|
+
timeEntrySeconds += seconds;
|
|
127688
|
+
const hours = hoursFrom2(seconds);
|
|
127689
|
+
const linked = ticketsByEvent.get(r6.id) ?? [];
|
|
127690
|
+
for (const t8 of linked) ticketsTouched.add(t8.id);
|
|
127691
|
+
const primary = linked[0] ?? null;
|
|
127692
|
+
const projectLabel = r6.projectName ? ` on ${r6.projectName}` : "";
|
|
127693
|
+
const titleLabel = r6.title ? ` \u2014 ${r6.title}` : "";
|
|
127694
|
+
activities2.push({
|
|
127695
|
+
timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
|
|
127696
|
+
timestampUtc: r6.ts,
|
|
127697
|
+
type: "time_entry",
|
|
127698
|
+
ticketId: primary?.id ?? null,
|
|
127699
|
+
ticketNumber: primary?.ticketNumber ?? null,
|
|
127700
|
+
ticketTitle: primary?.title ?? null,
|
|
127701
|
+
project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
|
|
127702
|
+
summary: `Logged ${hours}h${projectLabel}${titleLabel}`,
|
|
127703
|
+
actor: { id: r6.actorId, name: r6.actorName },
|
|
127704
|
+
meta: {
|
|
127705
|
+
hours,
|
|
127706
|
+
eventType: r6.type,
|
|
127707
|
+
billingStatus: r6.billingStatus,
|
|
127708
|
+
linkedTickets: linked.map((t8) => ({
|
|
127709
|
+
id: t8.id,
|
|
127710
|
+
ticketNumber: t8.ticketNumber
|
|
127711
|
+
}))
|
|
127712
|
+
}
|
|
127713
|
+
});
|
|
127714
|
+
}
|
|
127715
|
+
}
|
|
127716
|
+
if (includes.has("calendar_items")) {
|
|
127717
|
+
if (input.projectId) {
|
|
127718
|
+
limitations.push(
|
|
127719
|
+
"calendar_items were omitted because a projectId filter was set (agenda events have no project link)."
|
|
127720
|
+
);
|
|
127721
|
+
} else {
|
|
127722
|
+
const ae2 = schema_exports.agendaEvents;
|
|
127723
|
+
const conditions = [
|
|
127724
|
+
inArray(ae2.teamId, scope.teamIds),
|
|
127725
|
+
eq(ae2.isDeleted, false),
|
|
127726
|
+
dayWindow(ae2.startTime)
|
|
127727
|
+
];
|
|
127728
|
+
if (!isAll) conditions.push(eq(ae2.organizerId, userFilter));
|
|
127729
|
+
const rows = await db.select({
|
|
127730
|
+
ts: utcIsoExpr(ae2.startTime),
|
|
127731
|
+
title: ae2.title,
|
|
127732
|
+
status: ae2.status,
|
|
127733
|
+
actorId: ae2.organizerId,
|
|
127734
|
+
actorName: schema_exports.users.fullName
|
|
127735
|
+
}).from(ae2).leftJoin(schema_exports.users, eq(schema_exports.users.id, ae2.organizerId)).where(and(...conditions)).limit(MAX_ROWS_PER_SOURCE);
|
|
127736
|
+
for (const r6 of rows) {
|
|
127737
|
+
activities2.push({
|
|
127738
|
+
timestamp: formatIsoWithOffset(new Date(r6.ts), timezone),
|
|
127739
|
+
timestampUtc: r6.ts,
|
|
127740
|
+
type: "calendar_item",
|
|
127741
|
+
ticketId: null,
|
|
127742
|
+
ticketNumber: null,
|
|
127743
|
+
ticketTitle: null,
|
|
127744
|
+
project: null,
|
|
127745
|
+
summary: `Calendar: ${r6.title}`,
|
|
127746
|
+
actor: { id: r6.actorId, name: r6.actorName },
|
|
127747
|
+
meta: { status: r6.status }
|
|
127748
|
+
});
|
|
127749
|
+
}
|
|
127750
|
+
}
|
|
127751
|
+
}
|
|
127752
|
+
activities2.sort((a6, b7) => a6.timestampUtc.localeCompare(b7.timestampUtc));
|
|
127753
|
+
const pageSize = Math.min(
|
|
127754
|
+
Math.max(1, input.pageSize ?? DEFAULT_PAGE_SIZE),
|
|
127755
|
+
MAX_PAGE_SIZE
|
|
127756
|
+
);
|
|
127757
|
+
const pagedActivities = activities2.slice(0, pageSize);
|
|
127758
|
+
return jsonResponse3({
|
|
127759
|
+
user,
|
|
127760
|
+
period: { from: window2.from, to: window2.to, timezone },
|
|
127761
|
+
filters: {
|
|
127762
|
+
teamIds: scope.teamIds,
|
|
127763
|
+
userId: isAll ? "all" : userFilter,
|
|
127764
|
+
projectId: input.projectId ?? null,
|
|
127765
|
+
include: [...includes]
|
|
127766
|
+
},
|
|
127767
|
+
summary: {
|
|
127768
|
+
ticketsTouched: ticketsTouched.size,
|
|
127769
|
+
commentsAdded,
|
|
127770
|
+
statusChanges,
|
|
127771
|
+
attachmentsAdded,
|
|
127772
|
+
timeEntriesHours: hoursFrom2(timeEntrySeconds),
|
|
127773
|
+
activitiesTotal: activities2.length
|
|
127774
|
+
},
|
|
127775
|
+
activities: pagedActivities,
|
|
127776
|
+
activitiesTruncated: activities2.length > pagedActivities.length,
|
|
127777
|
+
limitations
|
|
127778
|
+
});
|
|
127779
|
+
}
|
|
127780
|
+
|
|
127172
127781
|
// src/server.ts
|
|
127173
127782
|
var SERVER_VERSION = "3.5.12";
|
|
127174
127783
|
function createMcpServer() {
|
|
@@ -127414,6 +128023,11 @@ function createMcpServer() {
|
|
|
127414
128023
|
return await handleGetTimeEntries(
|
|
127415
128024
|
asToolArgs(toolArgs)
|
|
127416
128025
|
);
|
|
128026
|
+
case "get-user-activity-report":
|
|
128027
|
+
case "get_user_activity_report":
|
|
128028
|
+
return await handleGetUserActivityReport(
|
|
128029
|
+
asToolArgs(toolArgs)
|
|
128030
|
+
);
|
|
127417
128031
|
case "create-time-entries":
|
|
127418
128032
|
return await handleCreateTimeEntries(
|
|
127419
128033
|
asToolArgs(toolArgs)
|