@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 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":