@mgsoftwarebv/mcp-server-bridge 3.3.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +622 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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
|
);
|
|
@@ -106989,6 +107014,109 @@ var TOOLS = [
|
|
|
106989
107014
|
required: ["name"]
|
|
106990
107015
|
}
|
|
106991
107016
|
},
|
|
107017
|
+
{
|
|
107018
|
+
name: "update-project",
|
|
107019
|
+
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.",
|
|
107020
|
+
inputSchema: {
|
|
107021
|
+
type: "object",
|
|
107022
|
+
properties: {
|
|
107023
|
+
teamId: teamIdProp,
|
|
107024
|
+
id: { type: "string", description: "Project ID" },
|
|
107025
|
+
name: { type: "string" },
|
|
107026
|
+
description: { type: ["string", "null"] },
|
|
107027
|
+
customerId: {
|
|
107028
|
+
type: ["string", "null"],
|
|
107029
|
+
description: "Customer ID to link, or null to unlink."
|
|
107030
|
+
},
|
|
107031
|
+
rate: { type: ["number", "null"], description: "Hourly rate" },
|
|
107032
|
+
currency: { type: ["string", "null"] },
|
|
107033
|
+
billable: { type: "boolean" },
|
|
107034
|
+
estimate: {
|
|
107035
|
+
type: ["number", "null"],
|
|
107036
|
+
description: "Estimated hours/units"
|
|
107037
|
+
},
|
|
107038
|
+
internal: {
|
|
107039
|
+
type: "boolean",
|
|
107040
|
+
description: "Whether this is an internal (no-customer) project"
|
|
107041
|
+
}
|
|
107042
|
+
},
|
|
107043
|
+
required: ["id"]
|
|
107044
|
+
}
|
|
107045
|
+
},
|
|
107046
|
+
{
|
|
107047
|
+
name: "get-project-members",
|
|
107048
|
+
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.",
|
|
107049
|
+
inputSchema: {
|
|
107050
|
+
type: "object",
|
|
107051
|
+
properties: {
|
|
107052
|
+
teamId: teamIdProp,
|
|
107053
|
+
projectId: { type: "string", description: "Project ID" }
|
|
107054
|
+
},
|
|
107055
|
+
required: ["projectId"]
|
|
107056
|
+
}
|
|
107057
|
+
},
|
|
107058
|
+
{
|
|
107059
|
+
name: "set-project-members",
|
|
107060
|
+
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.",
|
|
107061
|
+
inputSchema: {
|
|
107062
|
+
type: "object",
|
|
107063
|
+
properties: {
|
|
107064
|
+
teamId: teamIdProp,
|
|
107065
|
+
projectId: { type: "string", description: "Project ID" },
|
|
107066
|
+
userIds: {
|
|
107067
|
+
type: "array",
|
|
107068
|
+
items: { type: "string" },
|
|
107069
|
+
description: "User IDs to assign to the project"
|
|
107070
|
+
},
|
|
107071
|
+
emails: {
|
|
107072
|
+
type: "array",
|
|
107073
|
+
items: { type: "string" },
|
|
107074
|
+
description: "Member emails to assign (resolved to user IDs)"
|
|
107075
|
+
}
|
|
107076
|
+
},
|
|
107077
|
+
required: ["projectId"]
|
|
107078
|
+
}
|
|
107079
|
+
},
|
|
107080
|
+
{
|
|
107081
|
+
name: "add-project-member",
|
|
107082
|
+
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.",
|
|
107083
|
+
inputSchema: {
|
|
107084
|
+
type: "object",
|
|
107085
|
+
properties: {
|
|
107086
|
+
teamId: teamIdProp,
|
|
107087
|
+
projectId: { type: "string", description: "Project ID" },
|
|
107088
|
+
userId: {
|
|
107089
|
+
type: "string",
|
|
107090
|
+
description: "User ID of the member to add"
|
|
107091
|
+
},
|
|
107092
|
+
email: {
|
|
107093
|
+
type: "string",
|
|
107094
|
+
description: "Email of the member to add (alternative to userId)"
|
|
107095
|
+
}
|
|
107096
|
+
},
|
|
107097
|
+
required: ["projectId"]
|
|
107098
|
+
}
|
|
107099
|
+
},
|
|
107100
|
+
{
|
|
107101
|
+
name: "remove-project-member",
|
|
107102
|
+
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.",
|
|
107103
|
+
inputSchema: {
|
|
107104
|
+
type: "object",
|
|
107105
|
+
properties: {
|
|
107106
|
+
teamId: teamIdProp,
|
|
107107
|
+
projectId: { type: "string", description: "Project ID" },
|
|
107108
|
+
userId: {
|
|
107109
|
+
type: "string",
|
|
107110
|
+
description: "User ID of the member to remove"
|
|
107111
|
+
},
|
|
107112
|
+
email: {
|
|
107113
|
+
type: "string",
|
|
107114
|
+
description: "Email of the member to remove (alternative to userId)"
|
|
107115
|
+
}
|
|
107116
|
+
},
|
|
107117
|
+
required: ["projectId"]
|
|
107118
|
+
}
|
|
107119
|
+
},
|
|
106992
107120
|
{
|
|
106993
107121
|
name: "list-documents",
|
|
106994
107122
|
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 +108390,7 @@ var signatureBlockSchema = external_exports5.object({
|
|
|
108262
108390
|
name: external_exports5.string().optional(),
|
|
108263
108391
|
company: external_exports5.string().optional(),
|
|
108264
108392
|
role: external_exports5.string().optional(),
|
|
108393
|
+
optional: external_exports5.boolean().optional(),
|
|
108265
108394
|
signedBy: external_exports5.string().optional(),
|
|
108266
108395
|
signedAt: external_exports5.string().optional(),
|
|
108267
108396
|
signatureType: external_exports5.enum(["drawn", "typed"]).optional(),
|
|
@@ -113123,6 +113252,479 @@ ${description ? `Description: ${description}
|
|
|
113123
113252
|
]
|
|
113124
113253
|
};
|
|
113125
113254
|
}
|
|
113255
|
+
function textResponse(text3) {
|
|
113256
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
113257
|
+
}
|
|
113258
|
+
function memberLabel(m4) {
|
|
113259
|
+
return m4.fullName || m4.email || m4.userId;
|
|
113260
|
+
}
|
|
113261
|
+
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).";
|
|
113262
|
+
async function requireTeamOwner(teamId, userId) {
|
|
113263
|
+
const [membership] = await db.select({ role: schema_exports.usersOnTeam.role }).from(schema_exports.usersOnTeam).where(
|
|
113264
|
+
and(
|
|
113265
|
+
eq(schema_exports.usersOnTeam.userId, userId),
|
|
113266
|
+
eq(schema_exports.usersOnTeam.teamId, teamId)
|
|
113267
|
+
)
|
|
113268
|
+
).limit(1);
|
|
113269
|
+
return membership?.role === "owner" ? null : textResponse(OWNER_REQUIRED);
|
|
113270
|
+
}
|
|
113271
|
+
async function setProjectMemberAccess(params) {
|
|
113272
|
+
const { projectId, teamId, memberIds, createdBy } = params;
|
|
113273
|
+
const baseFilter = and(
|
|
113274
|
+
eq(schema_exports.memberProjectAccess.projectId, projectId),
|
|
113275
|
+
eq(schema_exports.memberProjectAccess.teamId, teamId)
|
|
113276
|
+
);
|
|
113277
|
+
if (memberIds.length > 0) {
|
|
113278
|
+
await db.delete(schema_exports.memberProjectAccess).where(
|
|
113279
|
+
and(baseFilter, notInArray(schema_exports.memberProjectAccess.userId, memberIds))
|
|
113280
|
+
);
|
|
113281
|
+
await db.insert(schema_exports.memberProjectAccess).values(
|
|
113282
|
+
memberIds.map((userId) => ({
|
|
113283
|
+
userId,
|
|
113284
|
+
teamId,
|
|
113285
|
+
projectId,
|
|
113286
|
+
hasAccess: true,
|
|
113287
|
+
createdBy,
|
|
113288
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
113289
|
+
}))
|
|
113290
|
+
).onConflictDoUpdate({
|
|
113291
|
+
target: [
|
|
113292
|
+
schema_exports.memberProjectAccess.userId,
|
|
113293
|
+
schema_exports.memberProjectAccess.teamId,
|
|
113294
|
+
schema_exports.memberProjectAccess.projectId
|
|
113295
|
+
],
|
|
113296
|
+
set: {
|
|
113297
|
+
hasAccess: true,
|
|
113298
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
113299
|
+
createdBy
|
|
113300
|
+
}
|
|
113301
|
+
});
|
|
113302
|
+
} else {
|
|
113303
|
+
await db.delete(schema_exports.memberProjectAccess).where(baseFilter);
|
|
113304
|
+
}
|
|
113305
|
+
}
|
|
113306
|
+
function generateProjectAbbreviation(projectName) {
|
|
113307
|
+
if (!projectName || projectName.trim() === "") {
|
|
113308
|
+
return "PROJ";
|
|
113309
|
+
}
|
|
113310
|
+
const cleanName = projectName.toUpperCase().replace(/[^A-Z0-9\s]/g, "");
|
|
113311
|
+
const words = cleanName.split(" ").filter((word) => word.length > 0);
|
|
113312
|
+
if (words.length === 0) {
|
|
113313
|
+
const cleaned = projectName.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
113314
|
+
const abbr = cleaned.substring(0, 5);
|
|
113315
|
+
return abbr.length < 2 ? "PROJ" : abbr;
|
|
113316
|
+
}
|
|
113317
|
+
let abbreviation = "";
|
|
113318
|
+
if (words.length === 1) {
|
|
113319
|
+
const word = words[0];
|
|
113320
|
+
if (word && word.length <= 5) {
|
|
113321
|
+
abbreviation = word;
|
|
113322
|
+
} else if (word) {
|
|
113323
|
+
abbreviation = word.substring(
|
|
113324
|
+
0,
|
|
113325
|
+
word.substring(3, 4).match(/[AEIOU]/) ? 3 : 5
|
|
113326
|
+
);
|
|
113327
|
+
}
|
|
113328
|
+
} else if (words.length === 2) {
|
|
113329
|
+
const firstWord = words[0];
|
|
113330
|
+
const secondWord = words[1];
|
|
113331
|
+
if (firstWord && secondWord) {
|
|
113332
|
+
abbreviation = firstWord.substring(0, 3) + secondWord.substring(0, 2);
|
|
113333
|
+
}
|
|
113334
|
+
} else {
|
|
113335
|
+
for (let i6 = 0; i6 < Math.min(words.length, 6); i6++) {
|
|
113336
|
+
const word = words[i6];
|
|
113337
|
+
if (word && (word.length > 2 || i6 === 0)) {
|
|
113338
|
+
abbreviation += word.charAt(0);
|
|
113339
|
+
}
|
|
113340
|
+
}
|
|
113341
|
+
if (abbreviation.length < 3 && words.length > 0) {
|
|
113342
|
+
const firstWord = words[0];
|
|
113343
|
+
if (firstWord) {
|
|
113344
|
+
abbreviation = firstWord.substring(0, 3) + abbreviation.substring(1);
|
|
113345
|
+
}
|
|
113346
|
+
}
|
|
113347
|
+
}
|
|
113348
|
+
if (abbreviation.length === 0) {
|
|
113349
|
+
abbreviation = "PROJ";
|
|
113350
|
+
} else if (abbreviation.length > 8) {
|
|
113351
|
+
abbreviation = abbreviation.substring(0, 8);
|
|
113352
|
+
}
|
|
113353
|
+
if (abbreviation.length < 2) {
|
|
113354
|
+
abbreviation += "X";
|
|
113355
|
+
}
|
|
113356
|
+
return abbreviation;
|
|
113357
|
+
}
|
|
113358
|
+
async function getTeamRoster(teamId) {
|
|
113359
|
+
return db.select({
|
|
113360
|
+
userId: schema_exports.usersOnTeam.userId,
|
|
113361
|
+
role: schema_exports.usersOnTeam.role,
|
|
113362
|
+
fullName: schema_exports.users.fullName,
|
|
113363
|
+
email: schema_exports.users.email
|
|
113364
|
+
}).from(schema_exports.usersOnTeam).innerJoin(schema_exports.users, eq(schema_exports.usersOnTeam.userId, schema_exports.users.id)).where(eq(schema_exports.usersOnTeam.teamId, teamId));
|
|
113365
|
+
}
|
|
113366
|
+
async function resolveTeamMember(teamId, opts) {
|
|
113367
|
+
const roster = await getTeamRoster(teamId);
|
|
113368
|
+
if (opts.userId) {
|
|
113369
|
+
const match = roster.find((m4) => m4.userId === opts.userId);
|
|
113370
|
+
if (!match) {
|
|
113371
|
+
return {
|
|
113372
|
+
ok: false,
|
|
113373
|
+
response: textResponse(
|
|
113374
|
+
`User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
|
|
113375
|
+
)
|
|
113376
|
+
};
|
|
113377
|
+
}
|
|
113378
|
+
return { ok: true, member: match };
|
|
113379
|
+
}
|
|
113380
|
+
if (opts.email) {
|
|
113381
|
+
const needle = opts.email.trim().toLowerCase();
|
|
113382
|
+
const matches = roster.filter((m4) => (m4.email ?? "").toLowerCase() === needle);
|
|
113383
|
+
if (matches.length === 0) {
|
|
113384
|
+
return {
|
|
113385
|
+
ok: false,
|
|
113386
|
+
response: textResponse(
|
|
113387
|
+
`No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
|
|
113388
|
+
)
|
|
113389
|
+
};
|
|
113390
|
+
}
|
|
113391
|
+
if (matches.length > 1) {
|
|
113392
|
+
return {
|
|
113393
|
+
ok: false,
|
|
113394
|
+
response: textResponse(
|
|
113395
|
+
`Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
|
|
113396
|
+
)
|
|
113397
|
+
};
|
|
113398
|
+
}
|
|
113399
|
+
return { ok: true, member: matches[0] };
|
|
113400
|
+
}
|
|
113401
|
+
return {
|
|
113402
|
+
ok: false,
|
|
113403
|
+
response: textResponse(
|
|
113404
|
+
"Provide either a userId or an email to identify the member."
|
|
113405
|
+
)
|
|
113406
|
+
};
|
|
113407
|
+
}
|
|
113408
|
+
async function loadProjectInTeam(projectId, teamId) {
|
|
113409
|
+
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
113410
|
+
const [row] = await db.select({
|
|
113411
|
+
id: schema_exports.projects.id,
|
|
113412
|
+
name: schema_exports.projects.name,
|
|
113413
|
+
description: schema_exports.projects.description,
|
|
113414
|
+
customerId: schema_exports.projects.customerId,
|
|
113415
|
+
rate: schema_exports.projects.rate,
|
|
113416
|
+
currency: schema_exports.projects.currency,
|
|
113417
|
+
billable: schema_exports.projects.billable,
|
|
113418
|
+
estimate: schema_exports.projects.estimate,
|
|
113419
|
+
internal: schema_exports.projects.internal,
|
|
113420
|
+
ownedByCustomer: schema_exports.projects.ownedByCustomer,
|
|
113421
|
+
teamId: schema_exports.projects.teamId
|
|
113422
|
+
}).from(schema_exports.projects).where(eq(schema_exports.projects.id, projectId)).limit(1);
|
|
113423
|
+
if (!row || !row.teamId || !accessibleTeamIds.includes(row.teamId)) {
|
|
113424
|
+
return null;
|
|
113425
|
+
}
|
|
113426
|
+
return row;
|
|
113427
|
+
}
|
|
113428
|
+
async function getProjectAccessState(teamId, projectId) {
|
|
113429
|
+
const rows = await db.select({
|
|
113430
|
+
userId: schema_exports.memberProjectAccess.userId,
|
|
113431
|
+
projectId: schema_exports.memberProjectAccess.projectId,
|
|
113432
|
+
hasAccess: schema_exports.memberProjectAccess.hasAccess
|
|
113433
|
+
}).from(schema_exports.memberProjectAccess).where(eq(schema_exports.memberProjectAccess.teamId, teamId));
|
|
113434
|
+
const restrictedUserIds = /* @__PURE__ */ new Set();
|
|
113435
|
+
const projectMemberIds = /* @__PURE__ */ new Set();
|
|
113436
|
+
const rowCountByUser = /* @__PURE__ */ new Map();
|
|
113437
|
+
for (const r6 of rows) {
|
|
113438
|
+
restrictedUserIds.add(r6.userId);
|
|
113439
|
+
rowCountByUser.set(r6.userId, (rowCountByUser.get(r6.userId) ?? 0) + 1);
|
|
113440
|
+
if (r6.projectId === projectId && r6.hasAccess) {
|
|
113441
|
+
projectMemberIds.add(r6.userId);
|
|
113442
|
+
}
|
|
113443
|
+
}
|
|
113444
|
+
return { restrictedUserIds, projectMemberIds, rowCountByUser };
|
|
113445
|
+
}
|
|
113446
|
+
async function handleUpdateProject(input) {
|
|
113447
|
+
const { id } = input;
|
|
113448
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113449
|
+
if (!resolved.ok) return resolved.response;
|
|
113450
|
+
const existing = await loadProjectInTeam(id, resolved.teamId);
|
|
113451
|
+
if (!existing) {
|
|
113452
|
+
return textResponse(
|
|
113453
|
+
`Project ${id} not found, or it is not owned by this team.`
|
|
113454
|
+
);
|
|
113455
|
+
}
|
|
113456
|
+
const owningTeamId = existing.teamId;
|
|
113457
|
+
const willRename = input.name !== void 0 && input.name !== existing.name;
|
|
113458
|
+
if (willRename) {
|
|
113459
|
+
const [dupe] = await db.select({ id: schema_exports.projects.id }).from(schema_exports.projects).where(
|
|
113460
|
+
and(
|
|
113461
|
+
eq(schema_exports.projects.teamId, owningTeamId),
|
|
113462
|
+
eq(schema_exports.projects.name, input.name),
|
|
113463
|
+
sql`${schema_exports.projects.id} != ${id}`
|
|
113464
|
+
)
|
|
113465
|
+
).limit(1);
|
|
113466
|
+
if (dupe) {
|
|
113467
|
+
return textResponse(
|
|
113468
|
+
`A project named "${input.name}" already exists in this team. Choose a different name.`
|
|
113469
|
+
);
|
|
113470
|
+
}
|
|
113471
|
+
}
|
|
113472
|
+
const oldRate = existing.rate;
|
|
113473
|
+
const oldInternal = existing.internal;
|
|
113474
|
+
const newRate = input.rate !== void 0 ? input.rate : existing.rate;
|
|
113475
|
+
const newInternal = input.internal !== void 0 ? input.internal : existing.internal;
|
|
113476
|
+
await db.update(schema_exports.projects).set({
|
|
113477
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
113478
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
113479
|
+
...input.customerId !== void 0 ? { customerId: input.customerId } : {},
|
|
113480
|
+
...input.rate !== void 0 ? { rate: input.rate } : {},
|
|
113481
|
+
...input.currency !== void 0 ? { currency: input.currency } : {},
|
|
113482
|
+
...input.billable !== void 0 ? { billable: input.billable } : {},
|
|
113483
|
+
...input.estimate !== void 0 ? { estimate: input.estimate } : {},
|
|
113484
|
+
...input.internal !== void 0 ? { internal: input.internal } : {},
|
|
113485
|
+
updatedAt: sql`now()`
|
|
113486
|
+
}).where(eq(schema_exports.projects.id, id));
|
|
113487
|
+
if (willRename) {
|
|
113488
|
+
const abbreviation = generateProjectAbbreviation(input.name);
|
|
113489
|
+
const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
|
|
113490
|
+
await db.execute(sql`
|
|
113491
|
+
UPDATE tickets
|
|
113492
|
+
SET
|
|
113493
|
+
ticket_number = ${currentYear} || '-' || ${abbreviation} || '-' || LPAD(numbered.rn::text, 3, '0'),
|
|
113494
|
+
updated_at = NOW()
|
|
113495
|
+
FROM (
|
|
113496
|
+
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at ASC) AS rn
|
|
113497
|
+
FROM tickets
|
|
113498
|
+
WHERE project_id = ${id}
|
|
113499
|
+
) AS numbered
|
|
113500
|
+
WHERE tickets.id = numbered.id
|
|
113501
|
+
`);
|
|
113502
|
+
}
|
|
113503
|
+
const wasBillable = (oldRate ?? 0) > 0 && !(oldInternal ?? false);
|
|
113504
|
+
const isBillable = (newRate ?? 0) > 0 && !(newInternal ?? false);
|
|
113505
|
+
if (!wasBillable && isBillable) {
|
|
113506
|
+
await db.update(schema_exports.timesheetEvents).set({ billingStatus: "to_bill", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
113507
|
+
and(
|
|
113508
|
+
eq(schema_exports.timesheetEvents.projectId, id),
|
|
113509
|
+
eq(schema_exports.timesheetEvents.billingStatus, "unbillable"),
|
|
113510
|
+
eq(schema_exports.timesheetEvents.isDeleted, false)
|
|
113511
|
+
)
|
|
113512
|
+
);
|
|
113513
|
+
} else if (wasBillable && !isBillable) {
|
|
113514
|
+
await db.update(schema_exports.timesheetEvents).set({ billingStatus: "unbillable", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
113515
|
+
and(
|
|
113516
|
+
eq(schema_exports.timesheetEvents.projectId, id),
|
|
113517
|
+
eq(schema_exports.timesheetEvents.billingStatus, "to_bill"),
|
|
113518
|
+
eq(schema_exports.timesheetEvents.isDeleted, false)
|
|
113519
|
+
)
|
|
113520
|
+
);
|
|
113521
|
+
}
|
|
113522
|
+
const [updated] = await db.select({
|
|
113523
|
+
id: schema_exports.projects.id,
|
|
113524
|
+
name: schema_exports.projects.name,
|
|
113525
|
+
description: schema_exports.projects.description,
|
|
113526
|
+
rate: schema_exports.projects.rate,
|
|
113527
|
+
currency: schema_exports.projects.currency,
|
|
113528
|
+
internal: schema_exports.projects.internal,
|
|
113529
|
+
customerName: schema_exports.customers.name
|
|
113530
|
+
}).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);
|
|
113531
|
+
if (!updated) {
|
|
113532
|
+
return textResponse(`Failed to update project ${id}.`);
|
|
113533
|
+
}
|
|
113534
|
+
const lines = [
|
|
113535
|
+
"\u2705 **Project Updated**",
|
|
113536
|
+
"",
|
|
113537
|
+
`Name: ${updated.name} (ID: ${updated.id})`
|
|
113538
|
+
];
|
|
113539
|
+
if (updated.description) lines.push(`Description: ${updated.description}`);
|
|
113540
|
+
if (updated.customerName) lines.push(`Customer: ${updated.customerName}`);
|
|
113541
|
+
if (updated.rate != null) {
|
|
113542
|
+
lines.push(
|
|
113543
|
+
`Rate: ${updated.rate}${updated.currency ? ` ${updated.currency}` : ""}`
|
|
113544
|
+
);
|
|
113545
|
+
}
|
|
113546
|
+
lines.push(`Internal: ${updated.internal ? "yes" : "no"}`);
|
|
113547
|
+
if (willRename) {
|
|
113548
|
+
lines.push("", "Note: tickets for this project were renumbered.");
|
|
113549
|
+
}
|
|
113550
|
+
return textResponse(lines.join("\n"));
|
|
113551
|
+
}
|
|
113552
|
+
async function handleGetProjectMembers(input) {
|
|
113553
|
+
const { projectId } = input;
|
|
113554
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113555
|
+
if (!resolved.ok) return resolved.response;
|
|
113556
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113557
|
+
if (!project) {
|
|
113558
|
+
return textResponse(
|
|
113559
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113560
|
+
);
|
|
113561
|
+
}
|
|
113562
|
+
const [roster, state2] = await Promise.all([
|
|
113563
|
+
getTeamRoster(resolved.teamId),
|
|
113564
|
+
getProjectAccessState(resolved.teamId, projectId)
|
|
113565
|
+
]);
|
|
113566
|
+
const rosterById = new Map(roster.map((m4) => [m4.userId, m4]));
|
|
113567
|
+
const explicitList = [...state2.projectMemberIds].map((uid2) => {
|
|
113568
|
+
const m4 = rosterById.get(uid2);
|
|
113569
|
+
return `- ${m4 ? memberLabel(m4) : uid2} (userId: ${uid2})`;
|
|
113570
|
+
}).join("\n") || " (none)";
|
|
113571
|
+
const rosterList = roster.map((m4) => {
|
|
113572
|
+
const isOwner = m4.role === "owner";
|
|
113573
|
+
const restricted = state2.restrictedUserIds.has(m4.userId);
|
|
113574
|
+
let access;
|
|
113575
|
+
if (isOwner) {
|
|
113576
|
+
access = "all projects (owner)";
|
|
113577
|
+
} else if (!restricted) {
|
|
113578
|
+
access = "all projects (no restrictions)";
|
|
113579
|
+
} else if (state2.projectMemberIds.has(m4.userId)) {
|
|
113580
|
+
access = "HAS access to this project";
|
|
113581
|
+
} else {
|
|
113582
|
+
access = "NO access to this project";
|
|
113583
|
+
}
|
|
113584
|
+
return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
|
|
113585
|
+
}).join("\n");
|
|
113586
|
+
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.`;
|
|
113587
|
+
return textResponse(
|
|
113588
|
+
`**Project members for "${project.name}"** (ID: ${project.id})
|
|
113589
|
+
|
|
113590
|
+
${note}
|
|
113591
|
+
|
|
113592
|
+
Explicitly assigned members:
|
|
113593
|
+
${explicitList}
|
|
113594
|
+
|
|
113595
|
+
Team roster (use these userIds with set/add/remove-project-member):
|
|
113596
|
+
${rosterList}`
|
|
113597
|
+
);
|
|
113598
|
+
}
|
|
113599
|
+
async function handleSetProjectMembers(input) {
|
|
113600
|
+
const ctx = authContext;
|
|
113601
|
+
const { projectId } = input;
|
|
113602
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113603
|
+
if (!resolved.ok) return resolved.response;
|
|
113604
|
+
const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
|
|
113605
|
+
if (ownerError) return ownerError;
|
|
113606
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113607
|
+
if (!project) {
|
|
113608
|
+
return textResponse(
|
|
113609
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113610
|
+
);
|
|
113611
|
+
}
|
|
113612
|
+
const before = await getProjectAccessState(resolved.teamId, projectId);
|
|
113613
|
+
const memberIds = /* @__PURE__ */ new Set();
|
|
113614
|
+
for (const userId of input.userIds ?? []) {
|
|
113615
|
+
const r6 = await resolveTeamMember(resolved.teamId, { userId });
|
|
113616
|
+
if (!r6.ok) return r6.response;
|
|
113617
|
+
memberIds.add(r6.member.userId);
|
|
113618
|
+
}
|
|
113619
|
+
for (const email5 of input.emails ?? []) {
|
|
113620
|
+
const r6 = await resolveTeamMember(resolved.teamId, { email: email5 });
|
|
113621
|
+
if (!r6.ok) return r6.response;
|
|
113622
|
+
memberIds.add(r6.member.userId);
|
|
113623
|
+
}
|
|
113624
|
+
await setProjectMemberAccess({
|
|
113625
|
+
projectId,
|
|
113626
|
+
teamId: resolved.teamId,
|
|
113627
|
+
memberIds: [...memberIds],
|
|
113628
|
+
createdBy: ctx.userId
|
|
113629
|
+
});
|
|
113630
|
+
const roster = await getTeamRoster(resolved.teamId);
|
|
113631
|
+
const rosterById = new Map(roster.map((m4) => [m4.userId, m4]));
|
|
113632
|
+
const list = [...memberIds].map((uid2) => {
|
|
113633
|
+
const m4 = rosterById.get(uid2);
|
|
113634
|
+
return `- ${m4 ? memberLabel(m4) : uid2} (userId: ${uid2})`;
|
|
113635
|
+
}).join("\n") || " (none \u2014 all explicit assignments cleared; the project is visible to every owner and unrestricted member)";
|
|
113636
|
+
const newlyRestricted = [...memberIds].filter(
|
|
113637
|
+
(uid2) => !before.restrictedUserIds.has(uid2)
|
|
113638
|
+
);
|
|
113639
|
+
let warning2 = "";
|
|
113640
|
+
if (newlyRestricted.length > 0) {
|
|
113641
|
+
const names = newlyRestricted.map((uid2) => memberLabel(rosterById.get(uid2) ?? { fullName: null, email: null, userId: uid2 })).join(", ");
|
|
113642
|
+
warning2 = `
|
|
113643
|
+
|
|
113644
|
+
\u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
|
|
113645
|
+
}
|
|
113646
|
+
return textResponse(
|
|
113647
|
+
`\u2705 **Project members updated**
|
|
113648
|
+
|
|
113649
|
+
Members with explicit access to this project:
|
|
113650
|
+
${list}${warning2}`
|
|
113651
|
+
);
|
|
113652
|
+
}
|
|
113653
|
+
async function handleAddProjectMember(input) {
|
|
113654
|
+
const ctx = authContext;
|
|
113655
|
+
const { projectId } = input;
|
|
113656
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113657
|
+
if (!resolved.ok) return resolved.response;
|
|
113658
|
+
const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
|
|
113659
|
+
if (ownerError) return ownerError;
|
|
113660
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113661
|
+
if (!project) {
|
|
113662
|
+
return textResponse(
|
|
113663
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113664
|
+
);
|
|
113665
|
+
}
|
|
113666
|
+
const member2 = await resolveTeamMember(resolved.teamId, {
|
|
113667
|
+
userId: input.userId,
|
|
113668
|
+
email: input.email
|
|
113669
|
+
});
|
|
113670
|
+
if (!member2.ok) return member2.response;
|
|
113671
|
+
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
113672
|
+
if (state2.projectMemberIds.has(member2.member.userId)) {
|
|
113673
|
+
return textResponse(
|
|
113674
|
+
`${memberLabel(member2.member)} already has explicit access to this project.`
|
|
113675
|
+
);
|
|
113676
|
+
}
|
|
113677
|
+
const wasUnrestricted = !state2.restrictedUserIds.has(member2.member.userId);
|
|
113678
|
+
await setProjectMemberAccess({
|
|
113679
|
+
projectId,
|
|
113680
|
+
teamId: resolved.teamId,
|
|
113681
|
+
memberIds: [...state2.projectMemberIds, member2.member.userId],
|
|
113682
|
+
createdBy: ctx.userId
|
|
113683
|
+
});
|
|
113684
|
+
let text3 = `\u2705 Added ${memberLabel(member2.member)} (userId: ${member2.member.userId}) to the project.`;
|
|
113685
|
+
if (wasUnrestricted) {
|
|
113686
|
+
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.";
|
|
113687
|
+
}
|
|
113688
|
+
return textResponse(text3);
|
|
113689
|
+
}
|
|
113690
|
+
async function handleRemoveProjectMember(input) {
|
|
113691
|
+
const ctx = authContext;
|
|
113692
|
+
const { projectId } = input;
|
|
113693
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113694
|
+
if (!resolved.ok) return resolved.response;
|
|
113695
|
+
const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
|
|
113696
|
+
if (ownerError) return ownerError;
|
|
113697
|
+
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
113698
|
+
if (!project) {
|
|
113699
|
+
return textResponse(
|
|
113700
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113701
|
+
);
|
|
113702
|
+
}
|
|
113703
|
+
const member2 = await resolveTeamMember(resolved.teamId, {
|
|
113704
|
+
userId: input.userId,
|
|
113705
|
+
email: input.email
|
|
113706
|
+
});
|
|
113707
|
+
if (!member2.ok) return member2.response;
|
|
113708
|
+
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
113709
|
+
if (!state2.projectMemberIds.has(member2.member.userId)) {
|
|
113710
|
+
return textResponse(
|
|
113711
|
+
`${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
|
|
113712
|
+
);
|
|
113713
|
+
}
|
|
113714
|
+
await setProjectMemberAccess({
|
|
113715
|
+
projectId,
|
|
113716
|
+
teamId: resolved.teamId,
|
|
113717
|
+
memberIds: [...state2.projectMemberIds].filter(
|
|
113718
|
+
(uid2) => uid2 !== member2.member.userId
|
|
113719
|
+
),
|
|
113720
|
+
createdBy: ctx.userId
|
|
113721
|
+
});
|
|
113722
|
+
let text3 = `\u2705 Removed ${memberLabel(member2.member)} (userId: ${member2.member.userId}) from the project.`;
|
|
113723
|
+
if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
|
|
113724
|
+
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).";
|
|
113725
|
+
}
|
|
113726
|
+
return textResponse(text3);
|
|
113727
|
+
}
|
|
113126
113728
|
|
|
113127
113729
|
// src/tools/session-completion.ts
|
|
113128
113730
|
init_drizzle_orm();
|
|
@@ -120260,6 +120862,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request3) => {
|
|
|
120260
120862
|
return await handleGetProjects(asToolArgs(toolArgs));
|
|
120261
120863
|
case "create-project":
|
|
120262
120864
|
return await handleCreateProject(asToolArgs(toolArgs));
|
|
120865
|
+
case "update-project":
|
|
120866
|
+
return await handleUpdateProject(asToolArgs(toolArgs));
|
|
120867
|
+
case "get-project-members":
|
|
120868
|
+
return await handleGetProjectMembers(
|
|
120869
|
+
asToolArgs(toolArgs)
|
|
120870
|
+
);
|
|
120871
|
+
case "set-project-members":
|
|
120872
|
+
return await handleSetProjectMembers(
|
|
120873
|
+
asToolArgs(toolArgs)
|
|
120874
|
+
);
|
|
120875
|
+
case "add-project-member":
|
|
120876
|
+
return await handleAddProjectMember(
|
|
120877
|
+
asToolArgs(toolArgs)
|
|
120878
|
+
);
|
|
120879
|
+
case "remove-project-member":
|
|
120880
|
+
return await handleRemoveProjectMember(
|
|
120881
|
+
asToolArgs(toolArgs)
|
|
120882
|
+
);
|
|
120263
120883
|
case "list-documents":
|
|
120264
120884
|
return await handleListDocuments(asToolArgs(toolArgs));
|
|
120265
120885
|
case "get-document":
|