@mgsoftwarebv/mcp-server-bridge 3.3.0 → 3.3.2
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 +830 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import Stream, { Readable, Writable, Duplex } from 'stream';
|
|
|
6
6
|
import { performance } from 'perf_hooks';
|
|
7
7
|
import os, { platform, release, homedir } from 'os';
|
|
8
8
|
import fs, { ReadStream, lstatSync, fstatSync, readFileSync, promises } from 'fs';
|
|
9
|
-
import { join, dirname, sep as sep$1, normalize } from 'path';
|
|
9
|
+
import { basename, join, dirname, sep as sep$1, normalize } from 'path';
|
|
10
10
|
import fs2, { readFile } from 'fs/promises';
|
|
11
11
|
import { Buffer as Buffer$1 } from 'buffer';
|
|
12
12
|
import { request as request$2 } from 'http';
|
|
@@ -23574,7 +23574,14 @@ var init_quotations = __esm({
|
|
|
23574
23574
|
withTimezone: true,
|
|
23575
23575
|
mode: "string"
|
|
23576
23576
|
}),
|
|
23577
|
-
compiledHash: text("compiled_hash")
|
|
23577
|
+
compiledHash: text("compiled_hash"),
|
|
23578
|
+
// Manual, terminal lock: once set, signatures can no longer be cleared,
|
|
23579
|
+
// edited, or added. Set when the provider clicks "Definitief maken".
|
|
23580
|
+
finalizedAt: timestamp("finalized_at", {
|
|
23581
|
+
withTimezone: true,
|
|
23582
|
+
mode: "string"
|
|
23583
|
+
}),
|
|
23584
|
+
finalizedBy: uuid3("finalized_by")
|
|
23578
23585
|
},
|
|
23579
23586
|
(table) => [
|
|
23580
23587
|
index("idx_documents_team_id").on(table.teamId),
|
|
@@ -23595,6 +23602,11 @@ var init_quotations = __esm({
|
|
|
23595
23602
|
foreignColumns: [users.id],
|
|
23596
23603
|
name: "documents_user_id_fkey"
|
|
23597
23604
|
}).onDelete("set null"),
|
|
23605
|
+
foreignKey({
|
|
23606
|
+
columns: [table.finalizedBy],
|
|
23607
|
+
foreignColumns: [users.id],
|
|
23608
|
+
name: "documents_finalized_by_fkey"
|
|
23609
|
+
}).onDelete("set null"),
|
|
23598
23610
|
foreignKey({
|
|
23599
23611
|
columns: [table.templateId],
|
|
23600
23612
|
foreignColumns: [documentTemplates.id],
|
|
@@ -23707,7 +23719,15 @@ var init_quotations = __esm({
|
|
|
23707
23719
|
recipientEmail: text("recipient_email"),
|
|
23708
23720
|
message: text(),
|
|
23709
23721
|
expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }),
|
|
23710
|
-
signedAt: timestamp("signed_at", { withTimezone: true, mode: "string" })
|
|
23722
|
+
signedAt: timestamp("signed_at", { withTimezone: true, mode: "string" }),
|
|
23723
|
+
// Which signer slot this signature belongs to (mirrors
|
|
23724
|
+
// document_signatures.signature_data.signerId for direct querying).
|
|
23725
|
+
signerSlotId: text("signer_slot_id"),
|
|
23726
|
+
// Hash of the private edit token returned to the signer's browser so an
|
|
23727
|
+
// anonymous signer can later replace their own signature.
|
|
23728
|
+
editTokenHash: text("edit_token_hash"),
|
|
23729
|
+
// Set when a logged-in (portal) user signs, enabling identity-based edits.
|
|
23730
|
+
signerUserId: uuid3("signer_user_id")
|
|
23711
23731
|
},
|
|
23712
23732
|
(table) => [
|
|
23713
23733
|
index("idx_document_signing_requests_document_id").on(table.documentId),
|
|
@@ -23732,6 +23752,11 @@ var init_quotations = __esm({
|
|
|
23732
23752
|
columns: [table.createdBy],
|
|
23733
23753
|
foreignColumns: [users.id],
|
|
23734
23754
|
name: "document_signing_requests_created_by_fkey"
|
|
23755
|
+
}).onDelete("set null"),
|
|
23756
|
+
foreignKey({
|
|
23757
|
+
columns: [table.signerUserId],
|
|
23758
|
+
foreignColumns: [users.id],
|
|
23759
|
+
name: "document_signing_requests_signer_user_id_fkey"
|
|
23735
23760
|
}).onDelete("set null")
|
|
23736
23761
|
]
|
|
23737
23762
|
);
|
|
@@ -106926,6 +106951,41 @@ var TOOLS = [
|
|
|
106926
106951
|
required: ["attachmentId"]
|
|
106927
106952
|
}
|
|
106928
106953
|
},
|
|
106954
|
+
{
|
|
106955
|
+
name: "upload-ticket-attachment",
|
|
106956
|
+
description: "Attach a file (image or document) to a ticket. Provide exactly ONE source: `filePath` (absolute local path), `imageUrl` (public URL to download), or `base64Data` (raw or data: URI). To push an image the user pasted into Cursor chat onto the ticket: when the message is sent, Cursor writes the pasted image into the workspace `assets/` folder as `image-<uuid>.png` \u2014 locate the newest `assets/image-*.png` and pass its absolute path as `filePath`. Allowed types: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV. Max 25 MB. Returns the attachment id and a 1-hour download URL.",
|
|
106957
|
+
inputSchema: {
|
|
106958
|
+
type: "object",
|
|
106959
|
+
properties: {
|
|
106960
|
+
teamId: teamIdProp,
|
|
106961
|
+
ticketId: {
|
|
106962
|
+
type: "string",
|
|
106963
|
+
description: "Ticket ID (UUID) to attach the file to."
|
|
106964
|
+
},
|
|
106965
|
+
filePath: {
|
|
106966
|
+
type: "string",
|
|
106967
|
+
description: "Absolute local path to the file. For a pasted image, use the newest workspace assets/image-*.png."
|
|
106968
|
+
},
|
|
106969
|
+
imageUrl: {
|
|
106970
|
+
type: "string",
|
|
106971
|
+
description: "Public URL to download and attach."
|
|
106972
|
+
},
|
|
106973
|
+
base64Data: {
|
|
106974
|
+
type: "string",
|
|
106975
|
+
description: "Base64 file contents (raw or a data: URI)."
|
|
106976
|
+
},
|
|
106977
|
+
fileName: {
|
|
106978
|
+
type: "string",
|
|
106979
|
+
description: "Optional file name override (defaults to the source's basename)."
|
|
106980
|
+
},
|
|
106981
|
+
mimeType: {
|
|
106982
|
+
type: "string",
|
|
106983
|
+
description: "Optional MIME override; needed for base64Data without a data: prefix."
|
|
106984
|
+
}
|
|
106985
|
+
},
|
|
106986
|
+
required: ["ticketId"]
|
|
106987
|
+
}
|
|
106988
|
+
},
|
|
106929
106989
|
{
|
|
106930
106990
|
name: "get-customers",
|
|
106931
106991
|
description: "Get customers with optional search",
|
|
@@ -106989,6 +107049,109 @@ var TOOLS = [
|
|
|
106989
107049
|
required: ["name"]
|
|
106990
107050
|
}
|
|
106991
107051
|
},
|
|
107052
|
+
{
|
|
107053
|
+
name: "update-project",
|
|
107054
|
+
description: "Update an existing project's fields (name, description, customer, rate, currency, billable, estimate, internal). Only provided fields change. Renaming a project renumbers its tickets. There is no project 'status' field. Find the project id via get-projects.",
|
|
107055
|
+
inputSchema: {
|
|
107056
|
+
type: "object",
|
|
107057
|
+
properties: {
|
|
107058
|
+
teamId: teamIdProp,
|
|
107059
|
+
id: { type: "string", description: "Project ID" },
|
|
107060
|
+
name: { type: "string" },
|
|
107061
|
+
description: { type: ["string", "null"] },
|
|
107062
|
+
customerId: {
|
|
107063
|
+
type: ["string", "null"],
|
|
107064
|
+
description: "Customer ID to link, or null to unlink."
|
|
107065
|
+
},
|
|
107066
|
+
rate: { type: ["number", "null"], description: "Hourly rate" },
|
|
107067
|
+
currency: { type: ["string", "null"] },
|
|
107068
|
+
billable: { type: "boolean" },
|
|
107069
|
+
estimate: {
|
|
107070
|
+
type: ["number", "null"],
|
|
107071
|
+
description: "Estimated hours/units"
|
|
107072
|
+
},
|
|
107073
|
+
internal: {
|
|
107074
|
+
type: "boolean",
|
|
107075
|
+
description: "Whether this is an internal (no-customer) project"
|
|
107076
|
+
}
|
|
107077
|
+
},
|
|
107078
|
+
required: ["id"]
|
|
107079
|
+
}
|
|
107080
|
+
},
|
|
107081
|
+
{
|
|
107082
|
+
name: "get-project-members",
|
|
107083
|
+
description: "List the members explicitly assigned to a project (member_project_access) plus the full team roster with each member's effective access. Use the returned userIds with set/add/remove-project-member. Access model: owners and members with no restrictions see ALL projects; once a member is explicitly assigned to ANY project they can ONLY see their explicitly-assigned projects.",
|
|
107084
|
+
inputSchema: {
|
|
107085
|
+
type: "object",
|
|
107086
|
+
properties: {
|
|
107087
|
+
teamId: teamIdProp,
|
|
107088
|
+
projectId: { type: "string", description: "Project ID" }
|
|
107089
|
+
},
|
|
107090
|
+
required: ["projectId"]
|
|
107091
|
+
}
|
|
107092
|
+
},
|
|
107093
|
+
{
|
|
107094
|
+
name: "set-project-members",
|
|
107095
|
+
description: "Replace the COMPLETE set of members explicitly assigned to a project. Members not listed lose their explicit assignment; pass an empty list to clear all assignments (the project then reverts to being visible to every owner and unrestricted member). Identify members by userIds and/or emails (each must be a member of the team). Requires team OWNER privileges. Warning: assigning a member who previously had no restrictions limits them to ONLY their explicitly-assigned projects.",
|
|
107096
|
+
inputSchema: {
|
|
107097
|
+
type: "object",
|
|
107098
|
+
properties: {
|
|
107099
|
+
teamId: teamIdProp,
|
|
107100
|
+
projectId: { type: "string", description: "Project ID" },
|
|
107101
|
+
userIds: {
|
|
107102
|
+
type: "array",
|
|
107103
|
+
items: { type: "string" },
|
|
107104
|
+
description: "User IDs to assign to the project"
|
|
107105
|
+
},
|
|
107106
|
+
emails: {
|
|
107107
|
+
type: "array",
|
|
107108
|
+
items: { type: "string" },
|
|
107109
|
+
description: "Member emails to assign (resolved to user IDs)"
|
|
107110
|
+
}
|
|
107111
|
+
},
|
|
107112
|
+
required: ["projectId"]
|
|
107113
|
+
}
|
|
107114
|
+
},
|
|
107115
|
+
{
|
|
107116
|
+
name: "add-project-member",
|
|
107117
|
+
description: "Grant a single member explicit access to a project, preserving existing assignments. Identify the member by userId or email. Requires team OWNER privileges. Warning: if the member previously had no restrictions (saw all projects), granting this first assignment restricts them to ONLY their explicitly-assigned projects.",
|
|
107118
|
+
inputSchema: {
|
|
107119
|
+
type: "object",
|
|
107120
|
+
properties: {
|
|
107121
|
+
teamId: teamIdProp,
|
|
107122
|
+
projectId: { type: "string", description: "Project ID" },
|
|
107123
|
+
userId: {
|
|
107124
|
+
type: "string",
|
|
107125
|
+
description: "User ID of the member to add"
|
|
107126
|
+
},
|
|
107127
|
+
email: {
|
|
107128
|
+
type: "string",
|
|
107129
|
+
description: "Email of the member to add (alternative to userId)"
|
|
107130
|
+
}
|
|
107131
|
+
},
|
|
107132
|
+
required: ["projectId"]
|
|
107133
|
+
}
|
|
107134
|
+
},
|
|
107135
|
+
{
|
|
107136
|
+
name: "remove-project-member",
|
|
107137
|
+
description: "Remove a single member's explicit access to a project, preserving their other assignments. Identify the member by userId or email. Requires team OWNER privileges. If this was the member's only assignment, their restrictions are cleared and they can see all projects again.",
|
|
107138
|
+
inputSchema: {
|
|
107139
|
+
type: "object",
|
|
107140
|
+
properties: {
|
|
107141
|
+
teamId: teamIdProp,
|
|
107142
|
+
projectId: { type: "string", description: "Project ID" },
|
|
107143
|
+
userId: {
|
|
107144
|
+
type: "string",
|
|
107145
|
+
description: "User ID of the member to remove"
|
|
107146
|
+
},
|
|
107147
|
+
email: {
|
|
107148
|
+
type: "string",
|
|
107149
|
+
description: "Email of the member to remove (alternative to userId)"
|
|
107150
|
+
}
|
|
107151
|
+
},
|
|
107152
|
+
required: ["projectId"]
|
|
107153
|
+
}
|
|
107154
|
+
},
|
|
106992
107155
|
{
|
|
106993
107156
|
name: "list-documents",
|
|
106994
107157
|
description: "List quote/proposal/deliverables documents with optional filtering by type, status, customer, linked invoice, or a title search. Returns document ids for use with get-document / update-document.",
|
|
@@ -108262,6 +108425,7 @@ var signatureBlockSchema = external_exports5.object({
|
|
|
108262
108425
|
name: external_exports5.string().optional(),
|
|
108263
108426
|
company: external_exports5.string().optional(),
|
|
108264
108427
|
role: external_exports5.string().optional(),
|
|
108428
|
+
optional: external_exports5.boolean().optional(),
|
|
108265
108429
|
signedBy: external_exports5.string().optional(),
|
|
108266
108430
|
signedAt: external_exports5.string().optional(),
|
|
108267
108431
|
signatureType: external_exports5.enum(["drawn", "typed"]).optional(),
|
|
@@ -113123,6 +113287,479 @@ ${description ? `Description: ${description}
|
|
|
113123
113287
|
]
|
|
113124
113288
|
};
|
|
113125
113289
|
}
|
|
113290
|
+
function textResponse(text3) {
|
|
113291
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
113292
|
+
}
|
|
113293
|
+
function memberLabel(m4) {
|
|
113294
|
+
return m4.fullName || m4.email || m4.userId;
|
|
113295
|
+
}
|
|
113296
|
+
var OWNER_REQUIRED = "Only team owners can manage project members. Ask a team owner to run this action (or use an owner's API key).";
|
|
113297
|
+
async function requireTeamOwner(teamId, userId) {
|
|
113298
|
+
const [membership] = await db.select({ role: schema_exports.usersOnTeam.role }).from(schema_exports.usersOnTeam).where(
|
|
113299
|
+
and(
|
|
113300
|
+
eq(schema_exports.usersOnTeam.userId, userId),
|
|
113301
|
+
eq(schema_exports.usersOnTeam.teamId, teamId)
|
|
113302
|
+
)
|
|
113303
|
+
).limit(1);
|
|
113304
|
+
return membership?.role === "owner" ? null : textResponse(OWNER_REQUIRED);
|
|
113305
|
+
}
|
|
113306
|
+
async function setProjectMemberAccess(params) {
|
|
113307
|
+
const { projectId, teamId, memberIds, createdBy } = params;
|
|
113308
|
+
const baseFilter = and(
|
|
113309
|
+
eq(schema_exports.memberProjectAccess.projectId, projectId),
|
|
113310
|
+
eq(schema_exports.memberProjectAccess.teamId, teamId)
|
|
113311
|
+
);
|
|
113312
|
+
if (memberIds.length > 0) {
|
|
113313
|
+
await db.delete(schema_exports.memberProjectAccess).where(
|
|
113314
|
+
and(baseFilter, notInArray(schema_exports.memberProjectAccess.userId, memberIds))
|
|
113315
|
+
);
|
|
113316
|
+
await db.insert(schema_exports.memberProjectAccess).values(
|
|
113317
|
+
memberIds.map((userId) => ({
|
|
113318
|
+
userId,
|
|
113319
|
+
teamId,
|
|
113320
|
+
projectId,
|
|
113321
|
+
hasAccess: true,
|
|
113322
|
+
createdBy,
|
|
113323
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
113324
|
+
}))
|
|
113325
|
+
).onConflictDoUpdate({
|
|
113326
|
+
target: [
|
|
113327
|
+
schema_exports.memberProjectAccess.userId,
|
|
113328
|
+
schema_exports.memberProjectAccess.teamId,
|
|
113329
|
+
schema_exports.memberProjectAccess.projectId
|
|
113330
|
+
],
|
|
113331
|
+
set: {
|
|
113332
|
+
hasAccess: true,
|
|
113333
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
113334
|
+
createdBy
|
|
113335
|
+
}
|
|
113336
|
+
});
|
|
113337
|
+
} else {
|
|
113338
|
+
await db.delete(schema_exports.memberProjectAccess).where(baseFilter);
|
|
113339
|
+
}
|
|
113340
|
+
}
|
|
113341
|
+
function generateProjectAbbreviation(projectName) {
|
|
113342
|
+
if (!projectName || projectName.trim() === "") {
|
|
113343
|
+
return "PROJ";
|
|
113344
|
+
}
|
|
113345
|
+
const cleanName = projectName.toUpperCase().replace(/[^A-Z0-9\s]/g, "");
|
|
113346
|
+
const words = cleanName.split(" ").filter((word) => word.length > 0);
|
|
113347
|
+
if (words.length === 0) {
|
|
113348
|
+
const cleaned = projectName.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
113349
|
+
const abbr = cleaned.substring(0, 5);
|
|
113350
|
+
return abbr.length < 2 ? "PROJ" : abbr;
|
|
113351
|
+
}
|
|
113352
|
+
let abbreviation = "";
|
|
113353
|
+
if (words.length === 1) {
|
|
113354
|
+
const word = words[0];
|
|
113355
|
+
if (word && word.length <= 5) {
|
|
113356
|
+
abbreviation = word;
|
|
113357
|
+
} else if (word) {
|
|
113358
|
+
abbreviation = word.substring(
|
|
113359
|
+
0,
|
|
113360
|
+
word.substring(3, 4).match(/[AEIOU]/) ? 3 : 5
|
|
113361
|
+
);
|
|
113362
|
+
}
|
|
113363
|
+
} else if (words.length === 2) {
|
|
113364
|
+
const firstWord = words[0];
|
|
113365
|
+
const secondWord = words[1];
|
|
113366
|
+
if (firstWord && secondWord) {
|
|
113367
|
+
abbreviation = firstWord.substring(0, 3) + secondWord.substring(0, 2);
|
|
113368
|
+
}
|
|
113369
|
+
} else {
|
|
113370
|
+
for (let i6 = 0; i6 < Math.min(words.length, 6); i6++) {
|
|
113371
|
+
const word = words[i6];
|
|
113372
|
+
if (word && (word.length > 2 || i6 === 0)) {
|
|
113373
|
+
abbreviation += word.charAt(0);
|
|
113374
|
+
}
|
|
113375
|
+
}
|
|
113376
|
+
if (abbreviation.length < 3 && words.length > 0) {
|
|
113377
|
+
const firstWord = words[0];
|
|
113378
|
+
if (firstWord) {
|
|
113379
|
+
abbreviation = firstWord.substring(0, 3) + abbreviation.substring(1);
|
|
113380
|
+
}
|
|
113381
|
+
}
|
|
113382
|
+
}
|
|
113383
|
+
if (abbreviation.length === 0) {
|
|
113384
|
+
abbreviation = "PROJ";
|
|
113385
|
+
} else if (abbreviation.length > 8) {
|
|
113386
|
+
abbreviation = abbreviation.substring(0, 8);
|
|
113387
|
+
}
|
|
113388
|
+
if (abbreviation.length < 2) {
|
|
113389
|
+
abbreviation += "X";
|
|
113390
|
+
}
|
|
113391
|
+
return abbreviation;
|
|
113392
|
+
}
|
|
113393
|
+
async function getTeamRoster(teamId) {
|
|
113394
|
+
return db.select({
|
|
113395
|
+
userId: schema_exports.usersOnTeam.userId,
|
|
113396
|
+
role: schema_exports.usersOnTeam.role,
|
|
113397
|
+
fullName: schema_exports.users.fullName,
|
|
113398
|
+
email: schema_exports.users.email
|
|
113399
|
+
}).from(schema_exports.usersOnTeam).innerJoin(schema_exports.users, eq(schema_exports.usersOnTeam.userId, schema_exports.users.id)).where(eq(schema_exports.usersOnTeam.teamId, teamId));
|
|
113400
|
+
}
|
|
113401
|
+
async function resolveTeamMember(teamId, opts) {
|
|
113402
|
+
const roster = await getTeamRoster(teamId);
|
|
113403
|
+
if (opts.userId) {
|
|
113404
|
+
const match = roster.find((m4) => m4.userId === opts.userId);
|
|
113405
|
+
if (!match) {
|
|
113406
|
+
return {
|
|
113407
|
+
ok: false,
|
|
113408
|
+
response: textResponse(
|
|
113409
|
+
`User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
|
|
113410
|
+
)
|
|
113411
|
+
};
|
|
113412
|
+
}
|
|
113413
|
+
return { ok: true, member: match };
|
|
113414
|
+
}
|
|
113415
|
+
if (opts.email) {
|
|
113416
|
+
const needle = opts.email.trim().toLowerCase();
|
|
113417
|
+
const matches = roster.filter((m4) => (m4.email ?? "").toLowerCase() === needle);
|
|
113418
|
+
if (matches.length === 0) {
|
|
113419
|
+
return {
|
|
113420
|
+
ok: false,
|
|
113421
|
+
response: textResponse(
|
|
113422
|
+
`No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
|
|
113423
|
+
)
|
|
113424
|
+
};
|
|
113425
|
+
}
|
|
113426
|
+
if (matches.length > 1) {
|
|
113427
|
+
return {
|
|
113428
|
+
ok: false,
|
|
113429
|
+
response: textResponse(
|
|
113430
|
+
`Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
|
|
113431
|
+
)
|
|
113432
|
+
};
|
|
113433
|
+
}
|
|
113434
|
+
return { ok: true, member: matches[0] };
|
|
113435
|
+
}
|
|
113436
|
+
return {
|
|
113437
|
+
ok: false,
|
|
113438
|
+
response: textResponse(
|
|
113439
|
+
"Provide either a userId or an email to identify the member."
|
|
113440
|
+
)
|
|
113441
|
+
};
|
|
113442
|
+
}
|
|
113443
|
+
async function loadProjectInTeam(projectId, teamId) {
|
|
113444
|
+
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
113445
|
+
const [row] = await db.select({
|
|
113446
|
+
id: schema_exports.projects.id,
|
|
113447
|
+
name: schema_exports.projects.name,
|
|
113448
|
+
description: schema_exports.projects.description,
|
|
113449
|
+
customerId: schema_exports.projects.customerId,
|
|
113450
|
+
rate: schema_exports.projects.rate,
|
|
113451
|
+
currency: schema_exports.projects.currency,
|
|
113452
|
+
billable: schema_exports.projects.billable,
|
|
113453
|
+
estimate: schema_exports.projects.estimate,
|
|
113454
|
+
internal: schema_exports.projects.internal,
|
|
113455
|
+
ownedByCustomer: schema_exports.projects.ownedByCustomer,
|
|
113456
|
+
teamId: schema_exports.projects.teamId
|
|
113457
|
+
}).from(schema_exports.projects).where(eq(schema_exports.projects.id, projectId)).limit(1);
|
|
113458
|
+
if (!row || !row.teamId || !accessibleTeamIds.includes(row.teamId)) {
|
|
113459
|
+
return null;
|
|
113460
|
+
}
|
|
113461
|
+
return row;
|
|
113462
|
+
}
|
|
113463
|
+
async function getProjectAccessState(teamId, projectId) {
|
|
113464
|
+
const rows = await db.select({
|
|
113465
|
+
userId: schema_exports.memberProjectAccess.userId,
|
|
113466
|
+
projectId: schema_exports.memberProjectAccess.projectId,
|
|
113467
|
+
hasAccess: schema_exports.memberProjectAccess.hasAccess
|
|
113468
|
+
}).from(schema_exports.memberProjectAccess).where(eq(schema_exports.memberProjectAccess.teamId, teamId));
|
|
113469
|
+
const restrictedUserIds = /* @__PURE__ */ new Set();
|
|
113470
|
+
const projectMemberIds = /* @__PURE__ */ new Set();
|
|
113471
|
+
const rowCountByUser = /* @__PURE__ */ new Map();
|
|
113472
|
+
for (const r6 of rows) {
|
|
113473
|
+
restrictedUserIds.add(r6.userId);
|
|
113474
|
+
rowCountByUser.set(r6.userId, (rowCountByUser.get(r6.userId) ?? 0) + 1);
|
|
113475
|
+
if (r6.projectId === projectId && r6.hasAccess) {
|
|
113476
|
+
projectMemberIds.add(r6.userId);
|
|
113477
|
+
}
|
|
113478
|
+
}
|
|
113479
|
+
return { restrictedUserIds, projectMemberIds, rowCountByUser };
|
|
113480
|
+
}
|
|
113481
|
+
async function handleUpdateProject(input) {
|
|
113482
|
+
const { id } = input;
|
|
113483
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113484
|
+
if (!resolved.ok) return resolved.response;
|
|
113485
|
+
const existing = await loadProjectInTeam(id, resolved.teamId);
|
|
113486
|
+
if (!existing) {
|
|
113487
|
+
return textResponse(
|
|
113488
|
+
`Project ${id} not found, or it is not owned by this team.`
|
|
113489
|
+
);
|
|
113490
|
+
}
|
|
113491
|
+
const owningTeamId = existing.teamId;
|
|
113492
|
+
const willRename = input.name !== void 0 && input.name !== existing.name;
|
|
113493
|
+
if (willRename) {
|
|
113494
|
+
const [dupe] = await db.select({ id: schema_exports.projects.id }).from(schema_exports.projects).where(
|
|
113495
|
+
and(
|
|
113496
|
+
eq(schema_exports.projects.teamId, owningTeamId),
|
|
113497
|
+
eq(schema_exports.projects.name, input.name),
|
|
113498
|
+
sql`${schema_exports.projects.id} != ${id}`
|
|
113499
|
+
)
|
|
113500
|
+
).limit(1);
|
|
113501
|
+
if (dupe) {
|
|
113502
|
+
return textResponse(
|
|
113503
|
+
`A project named "${input.name}" already exists in this team. Choose a different name.`
|
|
113504
|
+
);
|
|
113505
|
+
}
|
|
113506
|
+
}
|
|
113507
|
+
const oldRate = existing.rate;
|
|
113508
|
+
const oldInternal = existing.internal;
|
|
113509
|
+
const newRate = input.rate !== void 0 ? input.rate : existing.rate;
|
|
113510
|
+
const newInternal = input.internal !== void 0 ? input.internal : existing.internal;
|
|
113511
|
+
await db.update(schema_exports.projects).set({
|
|
113512
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
113513
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
113514
|
+
...input.customerId !== void 0 ? { customerId: input.customerId } : {},
|
|
113515
|
+
...input.rate !== void 0 ? { rate: input.rate } : {},
|
|
113516
|
+
...input.currency !== void 0 ? { currency: input.currency } : {},
|
|
113517
|
+
...input.billable !== void 0 ? { billable: input.billable } : {},
|
|
113518
|
+
...input.estimate !== void 0 ? { estimate: input.estimate } : {},
|
|
113519
|
+
...input.internal !== void 0 ? { internal: input.internal } : {},
|
|
113520
|
+
updatedAt: sql`now()`
|
|
113521
|
+
}).where(eq(schema_exports.projects.id, id));
|
|
113522
|
+
if (willRename) {
|
|
113523
|
+
const abbreviation = generateProjectAbbreviation(input.name);
|
|
113524
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
113525
|
+
await db.execute(sql`
|
|
113526
|
+
UPDATE tickets
|
|
113527
|
+
SET
|
|
113528
|
+
ticket_number = ${currentYear} || '-' || ${abbreviation} || '-' || LPAD(numbered.rn::text, 3, '0'),
|
|
113529
|
+
updated_at = NOW()
|
|
113530
|
+
FROM (
|
|
113531
|
+
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at ASC) AS rn
|
|
113532
|
+
FROM tickets
|
|
113533
|
+
WHERE project_id = ${id}
|
|
113534
|
+
) AS numbered
|
|
113535
|
+
WHERE tickets.id = numbered.id
|
|
113536
|
+
`);
|
|
113537
|
+
}
|
|
113538
|
+
const wasBillable = (oldRate ?? 0) > 0 && !(oldInternal ?? false);
|
|
113539
|
+
const isBillable = (newRate ?? 0) > 0 && !(newInternal ?? false);
|
|
113540
|
+
if (!wasBillable && isBillable) {
|
|
113541
|
+
await db.update(schema_exports.timesheetEvents).set({ billingStatus: "to_bill", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
113542
|
+
and(
|
|
113543
|
+
eq(schema_exports.timesheetEvents.projectId, id),
|
|
113544
|
+
eq(schema_exports.timesheetEvents.billingStatus, "unbillable"),
|
|
113545
|
+
eq(schema_exports.timesheetEvents.isDeleted, false)
|
|
113546
|
+
)
|
|
113547
|
+
);
|
|
113548
|
+
} else if (wasBillable && !isBillable) {
|
|
113549
|
+
await db.update(schema_exports.timesheetEvents).set({ billingStatus: "unbillable", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
113550
|
+
and(
|
|
113551
|
+
eq(schema_exports.timesheetEvents.projectId, id),
|
|
113552
|
+
eq(schema_exports.timesheetEvents.billingStatus, "to_bill"),
|
|
113553
|
+
eq(schema_exports.timesheetEvents.isDeleted, false)
|
|
113554
|
+
)
|
|
113555
|
+
);
|
|
113556
|
+
}
|
|
113557
|
+
const [updated] = await db.select({
|
|
113558
|
+
id: schema_exports.projects.id,
|
|
113559
|
+
name: schema_exports.projects.name,
|
|
113560
|
+
description: schema_exports.projects.description,
|
|
113561
|
+
rate: schema_exports.projects.rate,
|
|
113562
|
+
currency: schema_exports.projects.currency,
|
|
113563
|
+
internal: schema_exports.projects.internal,
|
|
113564
|
+
customerName: schema_exports.customers.name
|
|
113565
|
+
}).from(schema_exports.projects).leftJoin(schema_exports.customers, eq(schema_exports.projects.customerId, schema_exports.customers.id)).where(eq(schema_exports.projects.id, id)).limit(1);
|
|
113566
|
+
if (!updated) {
|
|
113567
|
+
return textResponse(`Failed to update project ${id}.`);
|
|
113568
|
+
}
|
|
113569
|
+
const lines = [
|
|
113570
|
+
"\u2705 **Project Updated**",
|
|
113571
|
+
"",
|
|
113572
|
+
`Name: ${updated.name} (ID: ${updated.id})`
|
|
113573
|
+
];
|
|
113574
|
+
if (updated.description) lines.push(`Description: ${updated.description}`);
|
|
113575
|
+
if (updated.customerName) lines.push(`Customer: ${updated.customerName}`);
|
|
113576
|
+
if (updated.rate != null) {
|
|
113577
|
+
lines.push(
|
|
113578
|
+
`Rate: ${updated.rate}${updated.currency ? ` ${updated.currency}` : ""}`
|
|
113579
|
+
);
|
|
113580
|
+
}
|
|
113581
|
+
lines.push(`Internal: ${updated.internal ? "yes" : "no"}`);
|
|
113582
|
+
if (willRename) {
|
|
113583
|
+
lines.push("", "Note: tickets for this project were renumbered.");
|
|
113584
|
+
}
|
|
113585
|
+
return textResponse(lines.join("\n"));
|
|
113586
|
+
}
|
|
113587
|
+
async function handleGetProjectMembers(input) {
|
|
113588
|
+
const { projectId } = input;
|
|
113589
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113590
|
+
if (!resolved.ok) return resolved.response;
|
|
113591
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113592
|
+
if (!project) {
|
|
113593
|
+
return textResponse(
|
|
113594
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113595
|
+
);
|
|
113596
|
+
}
|
|
113597
|
+
const [roster, state2] = await Promise.all([
|
|
113598
|
+
getTeamRoster(resolved.teamId),
|
|
113599
|
+
getProjectAccessState(resolved.teamId, projectId)
|
|
113600
|
+
]);
|
|
113601
|
+
const rosterById = new Map(roster.map((m4) => [m4.userId, m4]));
|
|
113602
|
+
const explicitList = [...state2.projectMemberIds].map((uid2) => {
|
|
113603
|
+
const m4 = rosterById.get(uid2);
|
|
113604
|
+
return `- ${m4 ? memberLabel(m4) : uid2} (userId: ${uid2})`;
|
|
113605
|
+
}).join("\n") || " (none)";
|
|
113606
|
+
const rosterList = roster.map((m4) => {
|
|
113607
|
+
const isOwner = m4.role === "owner";
|
|
113608
|
+
const restricted = state2.restrictedUserIds.has(m4.userId);
|
|
113609
|
+
let access;
|
|
113610
|
+
if (isOwner) {
|
|
113611
|
+
access = "all projects (owner)";
|
|
113612
|
+
} else if (!restricted) {
|
|
113613
|
+
access = "all projects (no restrictions)";
|
|
113614
|
+
} else if (state2.projectMemberIds.has(m4.userId)) {
|
|
113615
|
+
access = "HAS access to this project";
|
|
113616
|
+
} else {
|
|
113617
|
+
access = "NO access to this project";
|
|
113618
|
+
}
|
|
113619
|
+
return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
|
|
113620
|
+
}).join("\n");
|
|
113621
|
+
const note = state2.projectMemberIds.size === 0 ? "No members are explicitly assigned to this project, so every owner and every unrestricted member can see it." : `${state2.projectMemberIds.size} member(s) are explicitly assigned to this project.`;
|
|
113622
|
+
return textResponse(
|
|
113623
|
+
`**Project members for "${project.name}"** (ID: ${project.id})
|
|
113624
|
+
|
|
113625
|
+
${note}
|
|
113626
|
+
|
|
113627
|
+
Explicitly assigned members:
|
|
113628
|
+
${explicitList}
|
|
113629
|
+
|
|
113630
|
+
Team roster (use these userIds with set/add/remove-project-member):
|
|
113631
|
+
${rosterList}`
|
|
113632
|
+
);
|
|
113633
|
+
}
|
|
113634
|
+
async function handleSetProjectMembers(input) {
|
|
113635
|
+
const ctx = authContext;
|
|
113636
|
+
const { projectId } = input;
|
|
113637
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113638
|
+
if (!resolved.ok) return resolved.response;
|
|
113639
|
+
const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
|
|
113640
|
+
if (ownerError) return ownerError;
|
|
113641
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113642
|
+
if (!project) {
|
|
113643
|
+
return textResponse(
|
|
113644
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113645
|
+
);
|
|
113646
|
+
}
|
|
113647
|
+
const before = await getProjectAccessState(resolved.teamId, projectId);
|
|
113648
|
+
const memberIds = /* @__PURE__ */ new Set();
|
|
113649
|
+
for (const userId of input.userIds ?? []) {
|
|
113650
|
+
const r6 = await resolveTeamMember(resolved.teamId, { userId });
|
|
113651
|
+
if (!r6.ok) return r6.response;
|
|
113652
|
+
memberIds.add(r6.member.userId);
|
|
113653
|
+
}
|
|
113654
|
+
for (const email5 of input.emails ?? []) {
|
|
113655
|
+
const r6 = await resolveTeamMember(resolved.teamId, { email: email5 });
|
|
113656
|
+
if (!r6.ok) return r6.response;
|
|
113657
|
+
memberIds.add(r6.member.userId);
|
|
113658
|
+
}
|
|
113659
|
+
await setProjectMemberAccess({
|
|
113660
|
+
projectId,
|
|
113661
|
+
teamId: resolved.teamId,
|
|
113662
|
+
memberIds: [...memberIds],
|
|
113663
|
+
createdBy: ctx.userId
|
|
113664
|
+
});
|
|
113665
|
+
const roster = await getTeamRoster(resolved.teamId);
|
|
113666
|
+
const rosterById = new Map(roster.map((m4) => [m4.userId, m4]));
|
|
113667
|
+
const list = [...memberIds].map((uid2) => {
|
|
113668
|
+
const m4 = rosterById.get(uid2);
|
|
113669
|
+
return `- ${m4 ? memberLabel(m4) : uid2} (userId: ${uid2})`;
|
|
113670
|
+
}).join("\n") || " (none \u2014 all explicit assignments cleared; the project is visible to every owner and unrestricted member)";
|
|
113671
|
+
const newlyRestricted = [...memberIds].filter(
|
|
113672
|
+
(uid2) => !before.restrictedUserIds.has(uid2)
|
|
113673
|
+
);
|
|
113674
|
+
let warning2 = "";
|
|
113675
|
+
if (newlyRestricted.length > 0) {
|
|
113676
|
+
const names = newlyRestricted.map((uid2) => memberLabel(rosterById.get(uid2) ?? { fullName: null, email: null, userId: uid2 })).join(", ");
|
|
113677
|
+
warning2 = `
|
|
113678
|
+
|
|
113679
|
+
\u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
|
|
113680
|
+
}
|
|
113681
|
+
return textResponse(
|
|
113682
|
+
`\u2705 **Project members updated**
|
|
113683
|
+
|
|
113684
|
+
Members with explicit access to this project:
|
|
113685
|
+
${list}${warning2}`
|
|
113686
|
+
);
|
|
113687
|
+
}
|
|
113688
|
+
async function handleAddProjectMember(input) {
|
|
113689
|
+
const ctx = authContext;
|
|
113690
|
+
const { projectId } = input;
|
|
113691
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113692
|
+
if (!resolved.ok) return resolved.response;
|
|
113693
|
+
const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
|
|
113694
|
+
if (ownerError) return ownerError;
|
|
113695
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113696
|
+
if (!project) {
|
|
113697
|
+
return textResponse(
|
|
113698
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113699
|
+
);
|
|
113700
|
+
}
|
|
113701
|
+
const member2 = await resolveTeamMember(resolved.teamId, {
|
|
113702
|
+
userId: input.userId,
|
|
113703
|
+
email: input.email
|
|
113704
|
+
});
|
|
113705
|
+
if (!member2.ok) return member2.response;
|
|
113706
|
+
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
113707
|
+
if (state2.projectMemberIds.has(member2.member.userId)) {
|
|
113708
|
+
return textResponse(
|
|
113709
|
+
`${memberLabel(member2.member)} already has explicit access to this project.`
|
|
113710
|
+
);
|
|
113711
|
+
}
|
|
113712
|
+
const wasUnrestricted = !state2.restrictedUserIds.has(member2.member.userId);
|
|
113713
|
+
await setProjectMemberAccess({
|
|
113714
|
+
projectId,
|
|
113715
|
+
teamId: resolved.teamId,
|
|
113716
|
+
memberIds: [...state2.projectMemberIds, member2.member.userId],
|
|
113717
|
+
createdBy: ctx.userId
|
|
113718
|
+
});
|
|
113719
|
+
let text3 = `\u2705 Added ${memberLabel(member2.member)} (userId: ${member2.member.userId}) to the project.`;
|
|
113720
|
+
if (wasUnrestricted) {
|
|
113721
|
+
text3 += "\n\n\u26A0\uFE0F This member previously had no access restrictions (they could see all projects). They are now restricted to ONLY the projects explicitly assigned to them. Grant any other projects they still need with add-project-member, or remove all their assignments to restore full visibility.";
|
|
113722
|
+
}
|
|
113723
|
+
return textResponse(text3);
|
|
113724
|
+
}
|
|
113725
|
+
async function handleRemoveProjectMember(input) {
|
|
113726
|
+
const ctx = authContext;
|
|
113727
|
+
const { projectId } = input;
|
|
113728
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113729
|
+
if (!resolved.ok) return resolved.response;
|
|
113730
|
+
const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
|
|
113731
|
+
if (ownerError) return ownerError;
|
|
113732
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113733
|
+
if (!project) {
|
|
113734
|
+
return textResponse(
|
|
113735
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113736
|
+
);
|
|
113737
|
+
}
|
|
113738
|
+
const member2 = await resolveTeamMember(resolved.teamId, {
|
|
113739
|
+
userId: input.userId,
|
|
113740
|
+
email: input.email
|
|
113741
|
+
});
|
|
113742
|
+
if (!member2.ok) return member2.response;
|
|
113743
|
+
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
113744
|
+
if (!state2.projectMemberIds.has(member2.member.userId)) {
|
|
113745
|
+
return textResponse(
|
|
113746
|
+
`${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
|
|
113747
|
+
);
|
|
113748
|
+
}
|
|
113749
|
+
await setProjectMemberAccess({
|
|
113750
|
+
projectId,
|
|
113751
|
+
teamId: resolved.teamId,
|
|
113752
|
+
memberIds: [...state2.projectMemberIds].filter(
|
|
113753
|
+
(uid2) => uid2 !== member2.member.userId
|
|
113754
|
+
),
|
|
113755
|
+
createdBy: ctx.userId
|
|
113756
|
+
});
|
|
113757
|
+
let text3 = `\u2705 Removed ${memberLabel(member2.member)} (userId: ${member2.member.userId}) from the project.`;
|
|
113758
|
+
if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
|
|
113759
|
+
text3 += "\n\nThis was the member's last project assignment, so their access restrictions were cleared \u2014 they can see all projects in the team again (default behavior).";
|
|
113760
|
+
}
|
|
113761
|
+
return textResponse(text3);
|
|
113762
|
+
}
|
|
113126
113763
|
|
|
113127
113764
|
// src/tools/session-completion.ts
|
|
113128
113765
|
init_drizzle_orm();
|
|
@@ -114027,6 +114664,7 @@ ${JSON.stringify(teams2)}`
|
|
|
114027
114664
|
|
|
114028
114665
|
// src/tools/ticket-attachments.ts
|
|
114029
114666
|
init_drizzle_orm();
|
|
114667
|
+
init_auth();
|
|
114030
114668
|
init_db2();
|
|
114031
114669
|
|
|
114032
114670
|
// ../../node_modules/@aws-sdk/middleware-expect-continue/dist-es/index.js
|
|
@@ -119487,6 +120125,52 @@ async function loadAccessibleTicket(requestedTeamId, ticketId) {
|
|
|
119487
120125
|
}
|
|
119488
120126
|
|
|
119489
120127
|
// src/tools/ticket-attachments.ts
|
|
120128
|
+
var ALLOWED_IMAGE_TYPES = [
|
|
120129
|
+
"image/jpeg",
|
|
120130
|
+
"image/png",
|
|
120131
|
+
"image/gif",
|
|
120132
|
+
"image/webp"
|
|
120133
|
+
];
|
|
120134
|
+
var ALLOWED_DOCUMENT_TYPES = [
|
|
120135
|
+
"application/pdf",
|
|
120136
|
+
"application/msword",
|
|
120137
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
120138
|
+
"application/vnd.ms-excel",
|
|
120139
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
120140
|
+
"application/vnd.ms-powerpoint",
|
|
120141
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
120142
|
+
"text/plain",
|
|
120143
|
+
"text/csv"
|
|
120144
|
+
];
|
|
120145
|
+
var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
120146
|
+
...ALLOWED_IMAGE_TYPES,
|
|
120147
|
+
...ALLOWED_DOCUMENT_TYPES
|
|
120148
|
+
]);
|
|
120149
|
+
var MAX_FILE_SIZE = 25 * 1024 * 1024;
|
|
120150
|
+
var EXT_MIME = {
|
|
120151
|
+
jpg: "image/jpeg",
|
|
120152
|
+
jpeg: "image/jpeg",
|
|
120153
|
+
png: "image/png",
|
|
120154
|
+
gif: "image/gif",
|
|
120155
|
+
webp: "image/webp",
|
|
120156
|
+
pdf: "application/pdf",
|
|
120157
|
+
doc: "application/msword",
|
|
120158
|
+
txt: "text/plain",
|
|
120159
|
+
csv: "text/csv",
|
|
120160
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
120161
|
+
xls: "application/vnd.ms-excel",
|
|
120162
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
120163
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
120164
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
120165
|
+
};
|
|
120166
|
+
function textResponse2(text3) {
|
|
120167
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
120168
|
+
}
|
|
120169
|
+
function mimeFromName(name21) {
|
|
120170
|
+
if (!name21) return null;
|
|
120171
|
+
const ext = name21.split(/[?#]/)[0]?.split(".").pop()?.toLowerCase();
|
|
120172
|
+
return ext && EXT_MIME[ext] ? EXT_MIME[ext] : null;
|
|
120173
|
+
}
|
|
119490
120174
|
async function findAttachment(attachmentId) {
|
|
119491
120175
|
const [ticketAtt] = await db.select({
|
|
119492
120176
|
ticketId: schema_exports.ticketAttachments.ticketId,
|
|
@@ -119554,6 +120238,127 @@ ${url3}`
|
|
|
119554
120238
|
]
|
|
119555
120239
|
};
|
|
119556
120240
|
}
|
|
120241
|
+
async function handleUploadTicketAttachment(input) {
|
|
120242
|
+
const ctx = authContext;
|
|
120243
|
+
const sources = [input.filePath, input.imageUrl, input.base64Data].filter(
|
|
120244
|
+
(v2) => typeof v2 === "string" && v2.trim().length > 0
|
|
120245
|
+
);
|
|
120246
|
+
if (sources.length === 0) {
|
|
120247
|
+
return textResponse2(
|
|
120248
|
+
"Provide exactly one source: filePath (absolute local path), imageUrl, or base64Data."
|
|
120249
|
+
);
|
|
120250
|
+
}
|
|
120251
|
+
if (sources.length > 1) {
|
|
120252
|
+
return textResponse2(
|
|
120253
|
+
"Provide only one source (filePath, imageUrl, or base64Data), not several."
|
|
120254
|
+
);
|
|
120255
|
+
}
|
|
120256
|
+
const access = await loadAccessibleTicket(input.teamId, input.ticketId);
|
|
120257
|
+
if (!access.ok) return access.response;
|
|
120258
|
+
const ticket = access.ticket;
|
|
120259
|
+
let buffer2;
|
|
120260
|
+
let fileName = input.fileName?.trim() ?? "";
|
|
120261
|
+
let mimeType = input.mimeType?.trim() ?? "";
|
|
120262
|
+
try {
|
|
120263
|
+
if (input.filePath) {
|
|
120264
|
+
buffer2 = await readFile(input.filePath);
|
|
120265
|
+
if (!fileName) fileName = basename(input.filePath) || "attachment";
|
|
120266
|
+
if (!mimeType) {
|
|
120267
|
+
mimeType = mimeFromName(fileName) ?? "application/octet-stream";
|
|
120268
|
+
}
|
|
120269
|
+
} else if (input.imageUrl) {
|
|
120270
|
+
const res = await fetch(input.imageUrl);
|
|
120271
|
+
if (!res.ok) {
|
|
120272
|
+
return textResponse2(
|
|
120273
|
+
`Could not download from URL: HTTP ${res.status}.`
|
|
120274
|
+
);
|
|
120275
|
+
}
|
|
120276
|
+
const headerType = res.headers.get("content-type")?.split(";")[0]?.trim();
|
|
120277
|
+
buffer2 = Buffer.from(await res.arrayBuffer());
|
|
120278
|
+
if (!fileName) {
|
|
120279
|
+
fileName = input.imageUrl.split("/").pop()?.split(/[?#]/)[0] || `attachment_${Date.now()}`;
|
|
120280
|
+
}
|
|
120281
|
+
if (!mimeType) {
|
|
120282
|
+
mimeType = (headerType && headerType !== "application/octet-stream" ? headerType : null) ?? mimeFromName(input.imageUrl) ?? mimeFromName(fileName) ?? "application/octet-stream";
|
|
120283
|
+
}
|
|
120284
|
+
} else {
|
|
120285
|
+
let b64 = input.base64Data;
|
|
120286
|
+
const dataUri = b64.match(/^data:([^;]+);base64,(.*)$/s);
|
|
120287
|
+
if (dataUri) {
|
|
120288
|
+
if (!mimeType) mimeType = dataUri[1] ?? "";
|
|
120289
|
+
b64 = dataUri[2] ?? "";
|
|
120290
|
+
} else if (b64.includes(",")) {
|
|
120291
|
+
b64 = b64.split(",")[1] || b64;
|
|
120292
|
+
}
|
|
120293
|
+
buffer2 = Buffer.from(b64, "base64");
|
|
120294
|
+
if (!fileName) fileName = `attachment_${Date.now()}`;
|
|
120295
|
+
if (!mimeType) {
|
|
120296
|
+
mimeType = mimeFromName(fileName) ?? "application/octet-stream";
|
|
120297
|
+
}
|
|
120298
|
+
}
|
|
120299
|
+
} catch (error49) {
|
|
120300
|
+
return textResponse2(
|
|
120301
|
+
`Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
120302
|
+
);
|
|
120303
|
+
}
|
|
120304
|
+
if (buffer2.byteLength === 0) {
|
|
120305
|
+
return textResponse2("The file is empty (0 bytes); nothing to upload.");
|
|
120306
|
+
}
|
|
120307
|
+
if (buffer2.byteLength > MAX_FILE_SIZE) {
|
|
120308
|
+
return textResponse2(
|
|
120309
|
+
`File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
|
|
120310
|
+
1
|
|
120311
|
+
)} MB). Max: 25 MB.`
|
|
120312
|
+
);
|
|
120313
|
+
}
|
|
120314
|
+
if (!ALLOWED_MIME_TYPES.has(mimeType)) {
|
|
120315
|
+
return textResponse2(
|
|
120316
|
+
`Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
|
|
120317
|
+
);
|
|
120318
|
+
}
|
|
120319
|
+
const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
|
|
120320
|
+
try {
|
|
120321
|
+
await storage.upload({
|
|
120322
|
+
bucket: "vault",
|
|
120323
|
+
path: storageKey,
|
|
120324
|
+
body: buffer2,
|
|
120325
|
+
options: { contentType: mimeType, upsert: true }
|
|
120326
|
+
});
|
|
120327
|
+
} catch (error49) {
|
|
120328
|
+
return textResponse2(
|
|
120329
|
+
`Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
120330
|
+
);
|
|
120331
|
+
}
|
|
120332
|
+
const [row] = await db.insert(schema_exports.ticketAttachments).values({
|
|
120333
|
+
ticketId: ticket.id,
|
|
120334
|
+
teamId: ticket.teamId,
|
|
120335
|
+
userId: ctx.userId,
|
|
120336
|
+
fileName,
|
|
120337
|
+
fileSize: buffer2.byteLength,
|
|
120338
|
+
mimeType,
|
|
120339
|
+
storageKey
|
|
120340
|
+
}).returning({ id: schema_exports.ticketAttachments.id });
|
|
120341
|
+
let url3 = "";
|
|
120342
|
+
try {
|
|
120343
|
+
const signed = await storage.createSignedUrl({
|
|
120344
|
+
bucket: "vault",
|
|
120345
|
+
path: storageKey,
|
|
120346
|
+
expiresIn: 3600
|
|
120347
|
+
});
|
|
120348
|
+
url3 = signed.url;
|
|
120349
|
+
} catch {
|
|
120350
|
+
}
|
|
120351
|
+
return textResponse2(
|
|
120352
|
+
`\u{1F4CE} **Attached to ${ticket.ticketNumber}**
|
|
120353
|
+
File: ${fileName}
|
|
120354
|
+
Type: ${mimeType}
|
|
120355
|
+
Size: ${Math.round(buffer2.byteLength / 1024)}KB
|
|
120356
|
+
Attachment id: ${row?.id}` + (url3 ? `
|
|
120357
|
+
|
|
120358
|
+
Download URL (valid 1 hour):
|
|
120359
|
+
${url3}` : "")
|
|
120360
|
+
);
|
|
120361
|
+
}
|
|
119557
120362
|
|
|
119558
120363
|
// src/tools/ticket-comments.ts
|
|
119559
120364
|
init_drizzle_orm();
|
|
@@ -120252,6 +121057,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request3) => {
|
|
|
120252
121057
|
return await handleGetTicketAttachment(
|
|
120253
121058
|
asToolArgs(toolArgs)
|
|
120254
121059
|
);
|
|
121060
|
+
case "upload-ticket-attachment":
|
|
121061
|
+
return await handleUploadTicketAttachment(
|
|
121062
|
+
asToolArgs(toolArgs)
|
|
121063
|
+
);
|
|
120255
121064
|
case "get-customers":
|
|
120256
121065
|
return await handleGetCustomers(asToolArgs(toolArgs));
|
|
120257
121066
|
case "create-customer":
|
|
@@ -120260,6 +121069,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request3) => {
|
|
|
120260
121069
|
return await handleGetProjects(asToolArgs(toolArgs));
|
|
120261
121070
|
case "create-project":
|
|
120262
121071
|
return await handleCreateProject(asToolArgs(toolArgs));
|
|
121072
|
+
case "update-project":
|
|
121073
|
+
return await handleUpdateProject(asToolArgs(toolArgs));
|
|
121074
|
+
case "get-project-members":
|
|
121075
|
+
return await handleGetProjectMembers(
|
|
121076
|
+
asToolArgs(toolArgs)
|
|
121077
|
+
);
|
|
121078
|
+
case "set-project-members":
|
|
121079
|
+
return await handleSetProjectMembers(
|
|
121080
|
+
asToolArgs(toolArgs)
|
|
121081
|
+
);
|
|
121082
|
+
case "add-project-member":
|
|
121083
|
+
return await handleAddProjectMember(
|
|
121084
|
+
asToolArgs(toolArgs)
|
|
121085
|
+
);
|
|
121086
|
+
case "remove-project-member":
|
|
121087
|
+
return await handleRemoveProjectMember(
|
|
121088
|
+
asToolArgs(toolArgs)
|
|
121089
|
+
);
|
|
120263
121090
|
case "list-documents":
|
|
120264
121091
|
return await handleListDocuments(asToolArgs(toolArgs));
|
|
120265
121092
|
case "get-document":
|