@mgsoftwarebv/mcp-server-bridge 3.5.16 → 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 +619 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -106952,7 +106952,7 @@ var TOOLS = [
|
|
|
106952
106952
|
},
|
|
106953
106953
|
{
|
|
106954
106954
|
name: "send-invoice-to-customer",
|
|
106955
|
-
description: "Email an invoice to the customer via the same generate-invoice \u2192 send-invoice-email pipeline as the dashboard send modal. Supports link-only, PDF attachment, or both (mode).
|
|
106955
|
+
description: "Email an invoice to the customer via the same generate-invoice \u2192 send-invoice-email pipeline as the dashboard send modal. Supports link-only, PDF attachment, or both (mode). Draft invoices are blocked unless allowDraft=true; an already-sent invoice warns unless confirmResend=true. Paid, scheduled, canceled and refunded invoices can NEVER be (re)sent here (no override) \u2014 fetch a copy with get-invoice-link and share it manually. Use dryRun=true to preview recipients/mode without sending. Delivery is asynchronous (Trigger.dev). Custom subject/message are not supported yet \u2014 the standard Refront invoice email template is used.",
|
|
106956
106956
|
inputSchema: {
|
|
106957
106957
|
type: "object",
|
|
106958
106958
|
properties: {
|
|
@@ -106993,7 +106993,7 @@ var TOOLS = [
|
|
|
106993
106993
|
allowDraft: {
|
|
106994
106994
|
type: "boolean",
|
|
106995
106995
|
default: false,
|
|
106996
|
-
description: "Allow sending draft invoices"
|
|
106996
|
+
description: "Allow sending draft invoices (does not affect paid/scheduled/canceled/refunded, which are always blocked)"
|
|
106997
106997
|
},
|
|
106998
106998
|
confirmResend: {
|
|
106999
106999
|
type: "boolean",
|
|
@@ -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 = [
|
|
@@ -121874,8 +121927,7 @@ function buildInvoiceDownloadRouteUrl(params) {
|
|
|
121874
121927
|
}
|
|
121875
121928
|
return `${base}/api/download/invoice?id=${encodeURIComponent(params.invoiceId)}`;
|
|
121876
121929
|
}
|
|
121877
|
-
var
|
|
121878
|
-
"draft",
|
|
121930
|
+
var HARD_BLOCKED_INVOICE_STATUSES = /* @__PURE__ */ new Set([
|
|
121879
121931
|
"scheduled",
|
|
121880
121932
|
"canceled",
|
|
121881
121933
|
"refunded",
|
|
@@ -122098,9 +122150,9 @@ async function handleSendInvoiceToCustomer(input) {
|
|
|
122098
122150
|
`Invoice ${invoice.invoiceNumber ?? invoice.id} is still a draft. Finalize it in the dashboard first, or pass allowDraft=true to override.`
|
|
122099
122151
|
);
|
|
122100
122152
|
}
|
|
122101
|
-
if (
|
|
122153
|
+
if (HARD_BLOCKED_INVOICE_STATUSES.has(invoice.status)) {
|
|
122102
122154
|
return textResponse6(
|
|
122103
|
-
`Invoice ${invoice.invoiceNumber ?? invoice.id} has status "${invoice.status}" and cannot be sent.`
|
|
122155
|
+
`Invoice ${invoice.invoiceNumber ?? invoice.id} has status "${invoice.status}" and cannot be (re)sent via MCP. Use get-invoice-link (linkType pdf or public_payment) to fetch a copy and share it manually.`
|
|
122104
122156
|
);
|
|
122105
122157
|
}
|
|
122106
122158
|
if (invoice.sentAt && !input.confirmResend) {
|
|
@@ -127170,6 +127222,562 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
|
|
|
127170
127222
|
};
|
|
127171
127223
|
}
|
|
127172
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
|
+
|
|
127173
127781
|
// src/server.ts
|
|
127174
127782
|
var SERVER_VERSION = "3.5.12";
|
|
127175
127783
|
function createMcpServer() {
|
|
@@ -127415,6 +128023,11 @@ function createMcpServer() {
|
|
|
127415
128023
|
return await handleGetTimeEntries(
|
|
127416
128024
|
asToolArgs(toolArgs)
|
|
127417
128025
|
);
|
|
128026
|
+
case "get-user-activity-report":
|
|
128027
|
+
case "get_user_activity_report":
|
|
128028
|
+
return await handleGetUserActivityReport(
|
|
128029
|
+
asToolArgs(toolArgs)
|
|
128030
|
+
);
|
|
127418
128031
|
case "create-time-entries":
|
|
127419
128032
|
return await handleCreateTimeEntries(
|
|
127420
128033
|
asToolArgs(toolArgs)
|