@mgsoftwarebv/mcp-server-bridge 3.5.21 → 3.5.23

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
@@ -105607,8 +105607,86 @@ var Server = class extends Protocol {
105607
105607
  };
105608
105608
 
105609
105609
  // src/telemetry.ts
105610
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
105611
+ function isUuid(value) {
105612
+ return UUID_RE.test(value);
105613
+ }
105610
105614
  var MCP_TELEMETRY_CATEGORY = "mcp_tool_call";
105611
105615
  var MCP_TELEMETRY_SOURCE = "mcp";
105616
+ function assignTicketRef(refs, value) {
105617
+ const trimmed = value.trim();
105618
+ if (!trimmed) return;
105619
+ if (isUuid(trimmed)) {
105620
+ refs.ticketId = trimmed;
105621
+ return;
105622
+ }
105623
+ refs.ticketNumber = trimmed;
105624
+ }
105625
+ function collectTicketRefsFromEntity(entity) {
105626
+ const refs = [];
105627
+ if (entity.ticketId) refs.push(entity.ticketId);
105628
+ if (entity.ticketNumber) refs.push(entity.ticketNumber);
105629
+ return refs;
105630
+ }
105631
+ async function resolveTicketRefMap(params) {
105632
+ const uuidRefs = /* @__PURE__ */ new Set();
105633
+ const numberRefs = /* @__PURE__ */ new Set();
105634
+ const allRefs = /* @__PURE__ */ new Set();
105635
+ for (const ref of params.refs) {
105636
+ const trimmed = ref.trim();
105637
+ if (!trimmed) continue;
105638
+ allRefs.add(trimmed);
105639
+ if (isUuid(trimmed)) uuidRefs.add(trimmed);
105640
+ else numberRefs.add(trimmed);
105641
+ }
105642
+ if (allRefs.size === 0) {
105643
+ return { byRef: /* @__PURE__ */ new Map(), unresolved: [] };
105644
+ }
105645
+ const lookup = [];
105646
+ if (uuidRefs.size > 0) {
105647
+ lookup.push(inArray(schema_exports.tickets.id, [...uuidRefs]));
105648
+ }
105649
+ for (const num of numberRefs) {
105650
+ lookup.push(sql`lower(${schema_exports.tickets.ticketNumber}) = lower(${num})`);
105651
+ }
105652
+ const ticketRows = await db.select({
105653
+ id: schema_exports.tickets.id,
105654
+ ticketNumber: schema_exports.tickets.ticketNumber,
105655
+ title: schema_exports.tickets.title,
105656
+ projectId: schema_exports.tickets.projectId
105657
+ }).from(schema_exports.tickets).where(
105658
+ and(
105659
+ inArray(schema_exports.tickets.teamId, params.teamIds),
105660
+ eq(schema_exports.tickets.isDeleted, false),
105661
+ or(...lookup)
105662
+ )
105663
+ );
105664
+ const byRef = /* @__PURE__ */ new Map();
105665
+ for (const t9 of ticketRows) {
105666
+ const info = {
105667
+ id: t9.id,
105668
+ ticketNumber: t9.ticketNumber,
105669
+ title: t9.title,
105670
+ projectId: t9.projectId
105671
+ };
105672
+ byRef.set(t9.id, info);
105673
+ if (t9.ticketNumber) {
105674
+ byRef.set(t9.ticketNumber, info);
105675
+ byRef.set(t9.ticketNumber.toLowerCase(), info);
105676
+ }
105677
+ }
105678
+ const unresolved = [...allRefs].filter(
105679
+ (ref) => !byRef.has(ref) && !byRef.has(ref.toLowerCase())
105680
+ );
105681
+ return { byRef, unresolved };
105682
+ }
105683
+ function lookupResolvedTicket(byRef, entity) {
105684
+ for (const ref of collectTicketRefsFromEntity(entity)) {
105685
+ const hit = byRef.get(ref) ?? byRef.get(ref.toLowerCase());
105686
+ if (hit) return hit;
105687
+ }
105688
+ return void 0;
105689
+ }
105612
105690
  var ENTITY_ID_FIELDS = [
105613
105691
  "ticketId",
105614
105692
  "projectId",
@@ -105647,15 +105725,20 @@ function extractSafeEntityRefs(toolName, args2) {
105647
105725
  if (!args2 || typeof args2 !== "object") return refs;
105648
105726
  for (const field of ENTITY_ID_FIELDS) {
105649
105727
  const value = args2[field];
105650
- if (typeof value === "string" && value.trim()) {
105651
- refs[field] = value.trim();
105728
+ if (typeof value !== "string" || !value.trim()) continue;
105729
+ const trimmed = value.trim();
105730
+ if (field === "ticketId") {
105731
+ assignTicketRef(refs, trimmed);
105732
+ continue;
105652
105733
  }
105734
+ refs[field] = trimmed;
105653
105735
  }
105654
105736
  const rawId = typeof args2.id === "string" ? args2.id.trim() : "";
105655
105737
  if (rawId) {
105656
105738
  const n3 = toolName.toLowerCase();
105657
- if (!refs.ticketId && n3.includes("ticket")) refs.ticketId = rawId;
105658
- else if (!refs.projectId && n3.includes("project")) refs.projectId = rawId;
105739
+ if (!refs.ticketId && !refs.ticketNumber && n3.includes("ticket")) {
105740
+ assignTicketRef(refs, rawId);
105741
+ } else if (!refs.projectId && n3.includes("project")) refs.projectId = rawId;
105659
105742
  else if (!refs.customerId && n3.includes("customer")) refs.customerId = rawId;
105660
105743
  else if (!refs.invoiceId && n3.includes("invoice")) refs.invoiceId = rawId;
105661
105744
  else if (!refs.documentId && n3.includes("document")) refs.documentId = rawId;
@@ -105744,6 +105827,7 @@ async function queryMcpActivityRows(params) {
105744
105827
  durationMs: row.durationMs ?? null,
105745
105828
  entity: {
105746
105829
  ticketId: readString(meta5, "ticketId"),
105830
+ ticketNumber: readString(meta5, "ticketNumber"),
105747
105831
  projectId: readString(meta5, "projectId"),
105748
105832
  customerId: readString(meta5, "customerId"),
105749
105833
  invoiceId: readString(meta5, "invoiceId"),
@@ -106059,6 +106143,20 @@ var TOOLS = [
106059
106143
  items: { type: "string" },
106060
106144
  description: "Filter by tag IDs"
106061
106145
  },
106146
+ projectTag: {
106147
+ type: "string",
106148
+ description: "Filter by project label name \u2014 ticket's project must carry this label"
106149
+ },
106150
+ projectTags: {
106151
+ type: "array",
106152
+ items: { type: "string" },
106153
+ description: "Filter by project label names (OR)"
106154
+ },
106155
+ projectTagIds: {
106156
+ type: "array",
106157
+ items: { type: "string" },
106158
+ description: "Filter by project label tag IDs (OR)"
106159
+ },
106062
106160
  pageSize: { type: "number", default: 20, maximum: 100 }
106063
106161
  },
106064
106162
  required: []
@@ -106072,6 +106170,20 @@ var TOOLS = [
106072
106170
  properties: {
106073
106171
  teamId: teamIdProp,
106074
106172
  projectId: { type: "string", description: "Filter to one project UUID." },
106173
+ projectTag: {
106174
+ type: "string",
106175
+ description: "Filter by project label name on the ticket's project"
106176
+ },
106177
+ projectTags: {
106178
+ type: "array",
106179
+ items: { type: "string" },
106180
+ description: "Filter by project label names (OR)"
106181
+ },
106182
+ projectTagIds: {
106183
+ type: "array",
106184
+ items: { type: "string" },
106185
+ description: "Filter by project label tag IDs (OR)"
106186
+ },
106075
106187
  priority: {
106076
106188
  type: "string",
106077
106189
  enum: ["low", "medium", "high", "critical"]
@@ -106110,6 +106222,9 @@ var TOOLS = [
106110
106222
  description: "When assigneeId='all', group the items per assignee."
106111
106223
  },
106112
106224
  projectId: { type: "string" },
106225
+ projectTag: { type: "string" },
106226
+ projectTags: { type: "array", items: { type: "string" } },
106227
+ projectTagIds: { type: "array", items: { type: "string" } },
106113
106228
  priority: {
106114
106229
  type: "string",
106115
106230
  enum: ["low", "medium", "high", "critical"]
@@ -106148,7 +106263,10 @@ var TOOLS = [
106148
106263
  type: "boolean",
106149
106264
  default: true,
106150
106265
  description: "Include the unassigned bucket in the overview."
106151
- }
106266
+ },
106267
+ projectTag: { type: "string" },
106268
+ projectTags: { type: "array", items: { type: "string" } },
106269
+ projectTagIds: { type: "array", items: { type: "string" } }
106152
106270
  },
106153
106271
  required: []
106154
106272
  }
@@ -106783,13 +106901,27 @@ var TOOLS = [
106783
106901
  },
106784
106902
  {
106785
106903
  name: "get-projects",
106786
- description: "Get projects with optional filtering. Each project includes its ID and, when archived, its archive timestamp/reason. Archived projects are hidden by default; pass status 'archived' or 'all' to include them.",
106904
+ description: "Get projects with optional filtering. Each project includes its ID, project labels (tags on the project itself \u2014 not ticket tags), and when archived, its archive timestamp/reason. Archived projects are hidden by default; pass status 'archived' or 'all' to include them.",
106787
106905
  inputSchema: {
106788
106906
  type: "object",
106789
106907
  properties: {
106790
106908
  teamId: teamIdProp,
106791
106909
  customerId: { type: "string", description: "Filter by customer ID" },
106792
106910
  q: { type: "string", description: "Search query for project name" },
106911
+ projectTag: {
106912
+ type: "string",
106913
+ description: "Filter by a single project label name (case-insensitive)"
106914
+ },
106915
+ projectTags: {
106916
+ type: "array",
106917
+ items: { type: "string" },
106918
+ description: "Filter by project label names (OR)"
106919
+ },
106920
+ projectTagIds: {
106921
+ type: "array",
106922
+ items: { type: "string" },
106923
+ description: "Filter by project label tag IDs (OR)"
106924
+ },
106793
106925
  status: {
106794
106926
  type: "string",
106795
106927
  enum: ["active", "archived", "all"],
@@ -106803,7 +106935,7 @@ var TOOLS = [
106803
106935
  },
106804
106936
  {
106805
106937
  name: "create-project",
106806
- description: "Create a new project",
106938
+ description: "Create a new project. Optionally assign project labels (general team tags for grouping/filtering \u2014 distinct from ticket tags).",
106807
106939
  inputSchema: {
106808
106940
  type: "object",
106809
106941
  properties: {
@@ -106811,6 +106943,16 @@ var TOOLS = [
106811
106943
  name: { type: "string", description: "Project name" },
106812
106944
  description: { type: "string" },
106813
106945
  customerId: { type: "string" },
106946
+ projectTags: {
106947
+ type: "array",
106948
+ items: { type: "string" },
106949
+ description: "Project label names to assign (created if missing)"
106950
+ },
106951
+ projectTagIds: {
106952
+ type: "array",
106953
+ items: { type: "string" },
106954
+ description: "Project label tag IDs to assign"
106955
+ },
106814
106956
  status: {
106815
106957
  type: "string",
106816
106958
  enum: ["active", "on_hold", "completed", "cancelled"],
@@ -106822,7 +106964,7 @@ var TOOLS = [
106822
106964
  },
106823
106965
  {
106824
106966
  name: "update-project",
106825
- 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. To retire a mistakenly-created project use archive-project (reversible) or delete-project (empty projects only). Find the project id via get-projects.",
106967
+ description: "Update an existing project's fields (name, description, customer, rate, currency, billable, estimate, internal, project labels). Only provided fields change. When projectTags/projectTagIds are provided, labels are replaced entirely. Renaming a project renumbers its tickets. To retire a mistakenly-created project use archive-project (reversible) or delete-project (empty projects only). Find the project id via get-projects.",
106826
106968
  inputSchema: {
106827
106969
  type: "object",
106828
106970
  properties: {
@@ -106844,11 +106986,83 @@ var TOOLS = [
106844
106986
  internal: {
106845
106987
  type: "boolean",
106846
106988
  description: "Whether this is an internal (no-customer) project"
106989
+ },
106990
+ projectTags: {
106991
+ type: "array",
106992
+ items: { type: "string" },
106993
+ description: "Replace project labels with these names (creates missing)"
106994
+ },
106995
+ projectTagIds: {
106996
+ type: "array",
106997
+ items: { type: "string" },
106998
+ description: "Replace project labels with these tag IDs"
106847
106999
  }
106848
107000
  },
106849
107001
  required: ["id"]
106850
107002
  }
106851
107003
  },
107004
+ {
107005
+ name: "get-project-tags",
107006
+ description: "List project labels assigned to a project (metadata for grouping/filtering \u2014 not ticket tags).",
107007
+ inputSchema: {
107008
+ type: "object",
107009
+ properties: {
107010
+ teamId: teamIdProp,
107011
+ projectId: { type: "string", description: "Project ID" }
107012
+ },
107013
+ required: ["projectId"]
107014
+ }
107015
+ },
107016
+ {
107017
+ name: "set-project-tags",
107018
+ description: "Replace all project labels on a project. Pass projectTags (names) and/or projectTagIds. Missing names are created as general team tags.",
107019
+ inputSchema: {
107020
+ type: "object",
107021
+ properties: {
107022
+ teamId: teamIdProp,
107023
+ projectId: { type: "string", description: "Project ID" },
107024
+ projectTags: {
107025
+ type: "array",
107026
+ items: { type: "string" },
107027
+ description: "Project label names"
107028
+ },
107029
+ projectTagIds: {
107030
+ type: "array",
107031
+ items: { type: "string" },
107032
+ description: "Project label tag IDs"
107033
+ }
107034
+ },
107035
+ required: ["projectId"]
107036
+ }
107037
+ },
107038
+ {
107039
+ name: "add-project-tag",
107040
+ description: "Add one project label to a project (merge, does not remove existing labels).",
107041
+ inputSchema: {
107042
+ type: "object",
107043
+ properties: {
107044
+ teamId: teamIdProp,
107045
+ projectId: { type: "string", description: "Project ID" },
107046
+ projectTag: { type: "string", description: "Project label name" },
107047
+ projectTagId: { type: "string", description: "Project label tag ID" }
107048
+ },
107049
+ required: ["projectId"]
107050
+ }
107051
+ },
107052
+ {
107053
+ name: "remove-project-tag",
107054
+ description: "Remove one project label from a project.",
107055
+ inputSchema: {
107056
+ type: "object",
107057
+ properties: {
107058
+ teamId: teamIdProp,
107059
+ projectId: { type: "string", description: "Project ID" },
107060
+ projectTag: { type: "string", description: "Project label name" },
107061
+ projectTagId: { type: "string", description: "Project label tag ID" }
107062
+ },
107063
+ required: ["projectId"]
107064
+ }
107065
+ },
106852
107066
  {
106853
107067
  name: "archive-project",
106854
107068
  description: "Safely archive (soft-retire) a project \u2014 the recommended way to clean up a mistakenly-created project. Reversible and non-destructive: it keeps all tickets, hours, trips, documents and other data, and only hides the project from get-projects by default. Use this instead of delete-project whenever a project has any history. Find the project id via get-projects. Note: the archive flag is stored in projects.settings.archivedAt; the dashboard UI does not yet read it, so the project still appears there.",
@@ -108559,7 +108773,7 @@ async function resolveTeamScope(requestedTeamId) {
108559
108773
 
108560
108774
  // src/tools/ticket-access.ts
108561
108775
  var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
108562
- function isUuid(value) {
108776
+ function isUuid2(value) {
108563
108777
  return UUID_REGEX.test(value);
108564
108778
  }
108565
108779
  function notFoundResponse(identifier) {
@@ -108592,7 +108806,7 @@ var ticketLookupFields = {
108592
108806
  title: schema_exports.tickets.title
108593
108807
  };
108594
108808
  async function resolveTicketIdentifier(requestedTeamId, identifier) {
108595
- if (isUuid(identifier)) {
108809
+ if (isUuid2(identifier)) {
108596
108810
  return { ok: true, id: identifier };
108597
108811
  }
108598
108812
  const scope = await resolveTeamScope(requestedTeamId);
@@ -121962,7 +122176,7 @@ var INVOICE_STATUSES = [
121962
122176
  "scheduled",
121963
122177
  "refunded"
121964
122178
  ];
121965
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
122179
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
121966
122180
  function textResponse4(text3) {
121967
122181
  return { content: [{ type: "text", text: text3 }] };
121968
122182
  }
@@ -122023,7 +122237,7 @@ function parseStoredLineItems(value) {
122023
122237
  }
122024
122238
  async function loadInvoiceByIdentifier(identifier, teamIds) {
122025
122239
  if (teamIds.length === 0) return null;
122026
- const byId = UUID_RE.test(identifier);
122240
+ const byId = UUID_RE2.test(identifier);
122027
122241
  const filters = [inArray(schema_exports.invoices.teamId, teamIds)];
122028
122242
  filters.push(
122029
122243
  byId ? eq(schema_exports.invoices.id, identifier) : eq(schema_exports.invoices.invoiceNumber, identifier)
@@ -122802,88 +123016,493 @@ ${JSON.stringify(payload, null, 2)}
122802
123016
  );
122803
123017
  }
122804
123018
 
122805
- // src/tools/project-cleanup-util.ts
122806
- var PROJECT_STATUS_FILTERS = [
122807
- "active",
122808
- "archived",
122809
- "all"
122810
- ];
122811
- var DEPENDENCY_LABELS2 = {
122812
- tickets: "ticket(s)",
122813
- timesheetEvents: "agenda/time entr(ies)",
122814
- timesheetTemplates: "timesheet template(s)",
122815
- trips: "trip(s)",
122816
- tripTemplates: "trip template(s)"
122817
- };
122818
- function getProjectArchiveState(settings) {
122819
- const obj = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
122820
- const archivedAt = typeof obj.archivedAt === "string" && obj.archivedAt.trim().length > 0 ? obj.archivedAt : null;
122821
- const archiveReason = typeof obj.archiveReason === "string" && obj.archiveReason.trim().length > 0 ? obj.archiveReason : null;
122822
- return { archived: archivedAt !== null, archivedAt, archiveReason };
122823
- }
122824
- function withArchiveSettings(settings, archivedAt, reason) {
122825
- const base = settings && typeof settings === "object" && !Array.isArray(settings) ? { ...settings } : {};
122826
- base.archivedAt = archivedAt;
122827
- if (reason && reason.trim().length > 0) {
122828
- base.archiveReason = reason.trim();
122829
- }
122830
- return base;
122831
- }
122832
- function totalProjectDependencies(counts) {
122833
- return counts.tickets + counts.timesheetEvents + counts.timesheetTemplates + counts.trips + counts.tripTemplates;
123019
+ // src/tools/ticket-tags.ts
123020
+ function normalizeTagName(name21) {
123021
+ return name21.toLowerCase().trim();
122834
123022
  }
122835
- function isProjectEmpty(counts) {
122836
- return totalProjectDependencies(counts) === 0;
123023
+ function formatTagList(tags2) {
123024
+ if (tags2.length === 0) return "";
123025
+ return tags2.map((t9) => t9.name).join(", ");
122837
123026
  }
122838
- function formatProjectDependencies(counts) {
122839
- const parts = Object.keys(DEPENDENCY_LABELS2).filter((key) => counts[key] > 0).map((key) => `${counts[key]} ${DEPENDENCY_LABELS2[key]}`);
122840
- return parts.length > 0 ? parts.join(", ") : "no dependencies";
123027
+ async function getTagsForTickets(ticketIds) {
123028
+ const result = /* @__PURE__ */ new Map();
123029
+ if (ticketIds.length === 0) return result;
123030
+ const rows = await db.select({
123031
+ ticketId: schema_exports.ticketTags.ticketId,
123032
+ id: schema_exports.tags.id,
123033
+ name: schema_exports.tags.name
123034
+ }).from(schema_exports.ticketTags).innerJoin(schema_exports.tags, eq(schema_exports.tags.id, schema_exports.ticketTags.tagId)).where(inArray(schema_exports.ticketTags.ticketId, ticketIds)).orderBy(schema_exports.tags.name);
123035
+ for (const row of rows) {
123036
+ const existing = result.get(row.ticketId) ?? [];
123037
+ existing.push({ id: row.id, name: row.name });
123038
+ result.set(row.ticketId, existing);
123039
+ }
123040
+ return result;
122841
123041
  }
122842
-
122843
- // src/tools/projects.ts
122844
- async function handleGetProjects(input) {
122845
- const ctx = getAuthContext();
122846
- const { customerId, q: q3, pageSize = 20 } = input;
122847
- const status = input.status ?? "active";
122848
- if (!PROJECT_STATUS_FILTERS.includes(status)) {
122849
- return {
122850
- content: [
122851
- {
122852
- type: "text",
122853
- text: `Error: invalid status "${status}". Allowed: ${PROJECT_STATUS_FILTERS.join(", ")}.`
122854
- }
122855
- ]
122856
- };
123042
+ async function resolveTagFilterIds(teamId, input) {
123043
+ const ids = new Set(input.tagIds ?? []);
123044
+ const names = [
123045
+ ...input.tag ? [input.tag] : [],
123046
+ ...input.tags ?? []
123047
+ ].map(normalizeTagName).filter(Boolean);
123048
+ if (names.length > 0) {
123049
+ const nameConditions = names.map(
123050
+ (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
123051
+ );
123052
+ const rows = await db.select({ id: schema_exports.tags.id }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
123053
+ for (const row of rows) ids.add(row.id);
122857
123054
  }
122858
- const resolved = await resolveTeamId(input.teamId);
122859
- if (!resolved.ok) return resolved.response;
122860
- const projectIds = await getAccessibleProjectIds(ctx.userId, resolved.teamId);
122861
- if (projectIds.length === 0) {
122862
- return {
122863
- content: [
122864
- {
122865
- type: "text",
122866
- text: "No projects found or no access to any projects."
122867
- }
122868
- ]
122869
- };
123055
+ return [...ids];
123056
+ }
123057
+ async function resolveTags(teamId, input) {
123058
+ const tags2 = [];
123059
+ const errors = [];
123060
+ const seenIds = /* @__PURE__ */ new Set();
123061
+ if (input.tagIds?.length) {
123062
+ const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
123063
+ and(
123064
+ eq(schema_exports.tags.teamId, teamId),
123065
+ inArray(schema_exports.tags.id, input.tagIds)
123066
+ )
123067
+ );
123068
+ for (const row of rows) {
123069
+ if (seenIds.has(row.id)) continue;
123070
+ seenIds.add(row.id);
123071
+ tags2.push(row);
123072
+ }
123073
+ for (const id of input.tagIds) {
123074
+ if (!seenIds.has(id)) errors.push(`Unknown tag ID: ${id}`);
123075
+ }
122870
123076
  }
122871
- const filters = [inArray(schema_exports.projects.id, projectIds)];
122872
- if (customerId) filters.push(eq(schema_exports.projects.customerId, customerId));
122873
- if (q3) filters.push(ilike(schema_exports.projects.name, `%${q3}%`));
122874
- if (status === "active") {
122875
- filters.push(sql`${schema_exports.projects.settings} ->> 'archivedAt' IS NULL`);
122876
- } else if (status === "archived") {
122877
- filters.push(sql`${schema_exports.projects.settings} ->> 'archivedAt' IS NOT NULL`);
123077
+ const rawNames = input.tagNames ?? [];
123078
+ if (rawNames.length === 0) return { tags: tags2, errors };
123079
+ const normalizedNames = [
123080
+ ...new Set(rawNames.map(normalizeTagName).filter(Boolean))
123081
+ ];
123082
+ if (normalizedNames.length === 0) return { tags: tags2, errors };
123083
+ const nameConditions = normalizedNames.map(
123084
+ (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
123085
+ );
123086
+ const existing = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
123087
+ const existingByNorm = /* @__PURE__ */ new Map();
123088
+ for (const tag2 of existing) {
123089
+ existingByNorm.set(normalizeTagName(tag2.name), tag2);
122878
123090
  }
122879
- const rows = await db.select({
122880
- id: schema_exports.projects.id,
123091
+ for (const rawName of rawNames) {
123092
+ const norm = normalizeTagName(rawName);
123093
+ if (!norm) continue;
123094
+ const found = existingByNorm.get(norm);
123095
+ if (found) {
123096
+ if (!seenIds.has(found.id)) {
123097
+ seenIds.add(found.id);
123098
+ tags2.push(found);
123099
+ }
123100
+ continue;
123101
+ }
123102
+ if (!input.createMissing) {
123103
+ errors.push(`Tag not found: ${rawName}`);
123104
+ continue;
123105
+ }
123106
+ try {
123107
+ const [created] = await db.insert(schema_exports.tags).values({
123108
+ teamId,
123109
+ name: norm,
123110
+ projectId: input.projectId ?? null
123111
+ }).returning({ id: schema_exports.tags.id, name: schema_exports.tags.name });
123112
+ if (created) {
123113
+ seenIds.add(created.id);
123114
+ tags2.push(created);
123115
+ existingByNorm.set(norm, created);
123116
+ }
123117
+ } catch {
123118
+ const [retry2] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
123119
+ and(
123120
+ eq(schema_exports.tags.teamId, teamId),
123121
+ sql`lower(${schema_exports.tags.name}) = ${norm}`,
123122
+ input.projectId ? or(
123123
+ eq(schema_exports.tags.projectId, input.projectId),
123124
+ isNull(schema_exports.tags.projectId)
123125
+ ) : isNull(schema_exports.tags.projectId)
123126
+ )
123127
+ ).limit(1);
123128
+ if (retry2 && !seenIds.has(retry2.id)) {
123129
+ seenIds.add(retry2.id);
123130
+ tags2.push(retry2);
123131
+ existingByNorm.set(norm, retry2);
123132
+ } else {
123133
+ errors.push(`Failed to create tag: ${rawName}`);
123134
+ }
123135
+ }
123136
+ }
123137
+ return { tags: tags2, errors };
123138
+ }
123139
+ async function syncTicketTags(ticketId, teamId, tagIds, mode = "replace") {
123140
+ const current = await db.select({ tagId: schema_exports.ticketTags.tagId }).from(schema_exports.ticketTags).where(eq(schema_exports.ticketTags.ticketId, ticketId));
123141
+ const currentIds = new Set(current.map((row) => row.tagId));
123142
+ let targetIds;
123143
+ if (mode === "remove") {
123144
+ targetIds = new Set(
123145
+ [...currentIds].filter((id) => !tagIds.includes(id))
123146
+ );
123147
+ } else if (mode === "merge") {
123148
+ targetIds = /* @__PURE__ */ new Set([...currentIds, ...tagIds]);
123149
+ } else {
123150
+ targetIds = new Set(tagIds);
123151
+ }
123152
+ const toInsert = [...targetIds].filter((id) => !currentIds.has(id));
123153
+ const toDelete = [...currentIds].filter((id) => !targetIds.has(id));
123154
+ if (toDelete.length > 0) {
123155
+ await db.delete(schema_exports.ticketTags).where(
123156
+ and(
123157
+ eq(schema_exports.ticketTags.ticketId, ticketId),
123158
+ inArray(schema_exports.ticketTags.tagId, toDelete)
123159
+ )
123160
+ );
123161
+ }
123162
+ if (toInsert.length > 0) {
123163
+ await db.insert(schema_exports.ticketTags).values(
123164
+ toInsert.map((tagId) => ({
123165
+ ticketId,
123166
+ tagId,
123167
+ teamId
123168
+ }))
123169
+ );
123170
+ }
123171
+ const tagIdsToDescribe = [.../* @__PURE__ */ new Set([...toInsert, ...toDelete])];
123172
+ const tagNamesById = /* @__PURE__ */ new Map();
123173
+ if (tagIdsToDescribe.length > 0) {
123174
+ const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(inArray(schema_exports.tags.id, tagIdsToDescribe));
123175
+ for (const row of rows) tagNamesById.set(row.id, row.name);
123176
+ }
123177
+ return {
123178
+ added: toInsert.map((id) => tagNamesById.get(id) ?? id),
123179
+ removed: toDelete.map((id) => tagNamesById.get(id) ?? id)
123180
+ };
123181
+ }
123182
+
123183
+ // src/tools/project-tags.ts
123184
+ async function getTagsForProjects(projectIds) {
123185
+ const result = /* @__PURE__ */ new Map();
123186
+ if (projectIds.length === 0) return result;
123187
+ const rows = await db.select({
123188
+ projectId: schema_exports.projectTags.projectId,
123189
+ id: schema_exports.tags.id,
123190
+ name: schema_exports.tags.name
123191
+ }).from(schema_exports.projectTags).innerJoin(schema_exports.tags, eq(schema_exports.tags.id, schema_exports.projectTags.tagId)).where(inArray(schema_exports.projectTags.projectId, projectIds)).orderBy(schema_exports.tags.name);
123192
+ for (const row of rows) {
123193
+ const existing = result.get(row.projectId) ?? [];
123194
+ existing.push({ id: row.id, name: row.name });
123195
+ result.set(row.projectId, existing);
123196
+ }
123197
+ return result;
123198
+ }
123199
+ async function resolveProjectTagFilterIds(teamId, input) {
123200
+ const ids = new Set(input.projectTagIds ?? []);
123201
+ const names = [
123202
+ ...input.projectTag ? [input.projectTag] : [],
123203
+ ...input.projectTags ?? []
123204
+ ].map(normalizeTagName).filter(Boolean);
123205
+ if (names.length > 0) {
123206
+ const nameConditions = names.map(
123207
+ (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
123208
+ );
123209
+ const rows = await db.select({ id: schema_exports.tags.id }).from(schema_exports.tags).where(
123210
+ and(
123211
+ eq(schema_exports.tags.teamId, teamId),
123212
+ isNull(schema_exports.tags.projectId),
123213
+ or(...nameConditions)
123214
+ )
123215
+ );
123216
+ for (const row of rows) ids.add(row.id);
123217
+ }
123218
+ if (input.projectTagIds?.length) {
123219
+ const rows = await db.select({ id: schema_exports.tags.id }).from(schema_exports.tags).where(
123220
+ and(
123221
+ eq(schema_exports.tags.teamId, teamId),
123222
+ isNull(schema_exports.tags.projectId),
123223
+ inArray(schema_exports.tags.id, input.projectTagIds)
123224
+ )
123225
+ );
123226
+ for (const row of rows) ids.add(row.id);
123227
+ }
123228
+ return [...ids];
123229
+ }
123230
+ async function resolveProjectTags(teamId, input) {
123231
+ const tags2 = [];
123232
+ const errors = [];
123233
+ const seenIds = /* @__PURE__ */ new Set();
123234
+ if (input.tagIds?.length) {
123235
+ const rows = await db.select({
123236
+ id: schema_exports.tags.id,
123237
+ name: schema_exports.tags.name,
123238
+ projectId: schema_exports.tags.projectId
123239
+ }).from(schema_exports.tags).where(
123240
+ and(
123241
+ eq(schema_exports.tags.teamId, teamId),
123242
+ inArray(schema_exports.tags.id, input.tagIds)
123243
+ )
123244
+ );
123245
+ for (const row of rows) {
123246
+ if (row.projectId) {
123247
+ errors.push(
123248
+ `Tag "${row.name}" is project-scoped (ticket tag) and cannot be used as a project label`
123249
+ );
123250
+ continue;
123251
+ }
123252
+ if (seenIds.has(row.id)) continue;
123253
+ seenIds.add(row.id);
123254
+ tags2.push({ id: row.id, name: row.name });
123255
+ }
123256
+ for (const id of input.tagIds) {
123257
+ if (!seenIds.has(id) && !errors.some((e6) => e6.includes(id))) {
123258
+ errors.push(`Unknown tag ID: ${id}`);
123259
+ }
123260
+ }
123261
+ }
123262
+ const rawNames = input.tagNames ?? [];
123263
+ if (rawNames.length === 0) return { tags: tags2, errors };
123264
+ const normalizedNames = [
123265
+ ...new Set(rawNames.map(normalizeTagName).filter(Boolean))
123266
+ ];
123267
+ if (normalizedNames.length === 0) return { tags: tags2, errors };
123268
+ const nameConditions = normalizedNames.map(
123269
+ (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
123270
+ );
123271
+ const existing = await db.select({
123272
+ id: schema_exports.tags.id,
123273
+ name: schema_exports.tags.name,
123274
+ projectId: schema_exports.tags.projectId
123275
+ }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
123276
+ const existingByNorm = /* @__PURE__ */ new Map();
123277
+ for (const tag2 of existing) {
123278
+ if (tag2.projectId) {
123279
+ existingByNorm.set(normalizeTagName(tag2.name), {
123280
+ id: tag2.id,
123281
+ name: tag2.name,
123282
+ _scoped: true
123283
+ });
123284
+ } else {
123285
+ existingByNorm.set(normalizeTagName(tag2.name), {
123286
+ id: tag2.id,
123287
+ name: tag2.name
123288
+ });
123289
+ }
123290
+ }
123291
+ for (const rawName of rawNames) {
123292
+ const norm = normalizeTagName(rawName);
123293
+ if (!norm) continue;
123294
+ const found = existingByNorm.get(norm);
123295
+ if (found) {
123296
+ if (found._scoped) {
123297
+ errors.push(
123298
+ `Tag "${rawName}" is project-scoped (ticket tag) and cannot be used as a project label`
123299
+ );
123300
+ continue;
123301
+ }
123302
+ if (!seenIds.has(found.id)) {
123303
+ seenIds.add(found.id);
123304
+ tags2.push({ id: found.id, name: found.name });
123305
+ }
123306
+ continue;
123307
+ }
123308
+ if (!input.createMissing) {
123309
+ errors.push(`Project label not found: ${rawName}`);
123310
+ continue;
123311
+ }
123312
+ try {
123313
+ const [created] = await db.insert(schema_exports.tags).values({
123314
+ teamId,
123315
+ name: norm,
123316
+ projectId: null
123317
+ }).returning({ id: schema_exports.tags.id, name: schema_exports.tags.name });
123318
+ if (created) {
123319
+ seenIds.add(created.id);
123320
+ tags2.push(created);
123321
+ existingByNorm.set(norm, created);
123322
+ }
123323
+ } catch {
123324
+ const [retry2] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
123325
+ and(
123326
+ eq(schema_exports.tags.teamId, teamId),
123327
+ sql`lower(${schema_exports.tags.name}) = ${norm}`,
123328
+ isNull(schema_exports.tags.projectId)
123329
+ )
123330
+ ).limit(1);
123331
+ if (retry2 && !seenIds.has(retry2.id)) {
123332
+ seenIds.add(retry2.id);
123333
+ tags2.push(retry2);
123334
+ existingByNorm.set(norm, retry2);
123335
+ } else {
123336
+ errors.push(`Failed to create project label: ${rawName}`);
123337
+ }
123338
+ }
123339
+ }
123340
+ return { tags: tags2, errors };
123341
+ }
123342
+ async function syncProjectTags(projectId, teamId, tagIds, mode = "replace") {
123343
+ const current = await db.select({ tagId: schema_exports.projectTags.tagId }).from(schema_exports.projectTags).where(eq(schema_exports.projectTags.projectId, projectId));
123344
+ const currentIds = new Set(current.map((row) => row.tagId));
123345
+ let targetIds;
123346
+ if (mode === "remove") {
123347
+ targetIds = new Set(
123348
+ [...currentIds].filter((id) => !tagIds.includes(id))
123349
+ );
123350
+ } else if (mode === "merge") {
123351
+ targetIds = /* @__PURE__ */ new Set([...currentIds, ...tagIds]);
123352
+ } else {
123353
+ targetIds = new Set(tagIds);
123354
+ }
123355
+ const toInsert = [...targetIds].filter((id) => !currentIds.has(id));
123356
+ const toDelete = [...currentIds].filter((id) => !targetIds.has(id));
123357
+ if (toDelete.length > 0) {
123358
+ await db.delete(schema_exports.projectTags).where(
123359
+ and(
123360
+ eq(schema_exports.projectTags.projectId, projectId),
123361
+ inArray(schema_exports.projectTags.tagId, toDelete)
123362
+ )
123363
+ );
123364
+ }
123365
+ if (toInsert.length > 0) {
123366
+ await db.insert(schema_exports.projectTags).values(
123367
+ toInsert.map((tagId) => ({
123368
+ projectId,
123369
+ tagId,
123370
+ teamId
123371
+ }))
123372
+ );
123373
+ }
123374
+ const tagIdsToDescribe = [.../* @__PURE__ */ new Set([...toInsert, ...toDelete])];
123375
+ const tagNamesById = /* @__PURE__ */ new Map();
123376
+ if (tagIdsToDescribe.length > 0) {
123377
+ const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(inArray(schema_exports.tags.id, tagIdsToDescribe));
123378
+ for (const row of rows) tagNamesById.set(row.id, row.name);
123379
+ }
123380
+ return {
123381
+ added: toInsert.map((id) => tagNamesById.get(id) ?? id),
123382
+ removed: toDelete.map((id) => tagNamesById.get(id) ?? id)
123383
+ };
123384
+ }
123385
+ function projectTagFilterSql(tagIds, ticketsAlias = schema_exports.tickets) {
123386
+ return sql`EXISTS (
123387
+ SELECT 1 FROM ${schema_exports.projectTags} pt
123388
+ WHERE pt.project_id = ${ticketsAlias.projectId}
123389
+ AND pt.tag_id IN (${sql.join(
123390
+ tagIds.map((id) => sql`${id}`),
123391
+ sql`, `
123392
+ )})
123393
+ )`;
123394
+ }
123395
+
123396
+ // src/tools/project-cleanup-util.ts
123397
+ var PROJECT_STATUS_FILTERS = [
123398
+ "active",
123399
+ "archived",
123400
+ "all"
123401
+ ];
123402
+ var DEPENDENCY_LABELS2 = {
123403
+ tickets: "ticket(s)",
123404
+ timesheetEvents: "agenda/time entr(ies)",
123405
+ timesheetTemplates: "timesheet template(s)",
123406
+ trips: "trip(s)",
123407
+ tripTemplates: "trip template(s)"
123408
+ };
123409
+ function getProjectArchiveState(settings) {
123410
+ const obj = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
123411
+ const archivedAt = typeof obj.archivedAt === "string" && obj.archivedAt.trim().length > 0 ? obj.archivedAt : null;
123412
+ const archiveReason = typeof obj.archiveReason === "string" && obj.archiveReason.trim().length > 0 ? obj.archiveReason : null;
123413
+ return { archived: archivedAt !== null, archivedAt, archiveReason };
123414
+ }
123415
+ function withArchiveSettings(settings, archivedAt, reason) {
123416
+ const base = settings && typeof settings === "object" && !Array.isArray(settings) ? { ...settings } : {};
123417
+ base.archivedAt = archivedAt;
123418
+ if (reason && reason.trim().length > 0) {
123419
+ base.archiveReason = reason.trim();
123420
+ }
123421
+ return base;
123422
+ }
123423
+ function totalProjectDependencies(counts) {
123424
+ return counts.tickets + counts.timesheetEvents + counts.timesheetTemplates + counts.trips + counts.tripTemplates;
123425
+ }
123426
+ function isProjectEmpty(counts) {
123427
+ return totalProjectDependencies(counts) === 0;
123428
+ }
123429
+ function formatProjectDependencies(counts) {
123430
+ const parts = Object.keys(DEPENDENCY_LABELS2).filter((key) => counts[key] > 0).map((key) => `${counts[key]} ${DEPENDENCY_LABELS2[key]}`);
123431
+ return parts.length > 0 ? parts.join(", ") : "no dependencies";
123432
+ }
123433
+
123434
+ // src/tools/projects.ts
123435
+ async function handleGetProjects(input) {
123436
+ const ctx = getAuthContext();
123437
+ const { customerId, q: q3, pageSize = 20, projectTag, projectTags: projectTags2, projectTagIds } = input;
123438
+ const status = input.status ?? "active";
123439
+ if (!PROJECT_STATUS_FILTERS.includes(status)) {
123440
+ return {
123441
+ content: [
123442
+ {
123443
+ type: "text",
123444
+ text: `Error: invalid status "${status}". Allowed: ${PROJECT_STATUS_FILTERS.join(", ")}.`
123445
+ }
123446
+ ]
123447
+ };
123448
+ }
123449
+ const resolved = await resolveTeamId(input.teamId);
123450
+ if (!resolved.ok) return resolved.response;
123451
+ const projectIds = await getAccessibleProjectIds(ctx.userId, resolved.teamId);
123452
+ if (projectIds.length === 0) {
123453
+ return {
123454
+ content: [
123455
+ {
123456
+ type: "text",
123457
+ text: "No projects found or no access to any projects."
123458
+ }
123459
+ ]
123460
+ };
123461
+ }
123462
+ const filters = [inArray(schema_exports.projects.id, projectIds)];
123463
+ if (customerId) filters.push(eq(schema_exports.projects.customerId, customerId));
123464
+ if (q3) filters.push(ilike(schema_exports.projects.name, `%${q3}%`));
123465
+ if (status === "active") {
123466
+ filters.push(sql`${schema_exports.projects.settings} ->> 'archivedAt' IS NULL`);
123467
+ } else if (status === "archived") {
123468
+ filters.push(sql`${schema_exports.projects.settings} ->> 'archivedAt' IS NOT NULL`);
123469
+ }
123470
+ const labelFilterIds = await resolveProjectTagFilterIds(resolved.teamId, {
123471
+ projectTag,
123472
+ projectTags: projectTags2,
123473
+ projectTagIds
123474
+ });
123475
+ if (projectTag || projectTags2?.length || projectTagIds?.length) {
123476
+ if (labelFilterIds.length === 0) {
123477
+ return {
123478
+ content: [
123479
+ {
123480
+ type: "text",
123481
+ text: "No projects found (no matching project labels for the given filter)."
123482
+ }
123483
+ ]
123484
+ };
123485
+ }
123486
+ filters.push(
123487
+ sql`EXISTS (
123488
+ SELECT 1 FROM ${schema_exports.projectTags}
123489
+ WHERE ${schema_exports.projectTags.projectId} = ${schema_exports.projects.id}
123490
+ AND ${schema_exports.projectTags.tagId} IN (${sql.join(
123491
+ labelFilterIds.map((id) => sql`${id}`),
123492
+ sql`, `
123493
+ )})
123494
+ )`
123495
+ );
123496
+ }
123497
+ const rows = await db.select({
123498
+ id: schema_exports.projects.id,
122881
123499
  name: schema_exports.projects.name,
122882
123500
  description: schema_exports.projects.description,
122883
123501
  customerId: schema_exports.projects.customerId,
122884
123502
  createdAt: schema_exports.projects.createdAt,
122885
123503
  settings: schema_exports.projects.settings
122886
123504
  }).from(schema_exports.projects).where(and(...filters)).orderBy(asc(schema_exports.projects.name)).limit(Math.min(pageSize, 100));
123505
+ const tagsByProject = await getTagsForProjects(rows.map((p3) => p3.id));
122887
123506
  return {
122888
123507
  content: [
122889
123508
  {
@@ -122892,9 +123511,12 @@ async function handleGetProjects(input) {
122892
123511
 
122893
123512
  ${rows.map((p3) => {
122894
123513
  const archive = getProjectArchiveState(p3.settings);
123514
+ const labels = tagsByProject.get(p3.id) ?? [];
123515
+ const labelLine = labels.length > 0 ? `Labels: ${formatTagList(labels)} (ids: ${labels.map((t9) => t9.id).join(", ")})
123516
+ ` : "";
122895
123517
  return `**${p3.name}** (ID: ${p3.id})${archive.archived ? " \u2014 ARCHIVED" : ""}
122896
123518
  ${p3.description ? `Description: ${p3.description}
122897
- ` : ""}Created: ${new Date(p3.createdAt).toLocaleDateString()}
123519
+ ` : ""}` + labelLine + `Created: ${new Date(p3.createdAt).toLocaleDateString()}
122898
123520
  ${archive.archived ? `Archived: ${archive.archivedAt}${archive.archiveReason ? ` (${archive.archiveReason})` : ""}
122899
123521
  ` : ""}`;
122900
123522
  }).join("\n") || "No projects found."}`
@@ -122903,15 +123525,41 @@ ${archive.archived ? `Archived: ${archive.archivedAt}${archive.archiveReason ? `
122903
123525
  };
122904
123526
  }
122905
123527
  async function handleCreateProject(input) {
122906
- const { name: name21, description, customerId } = input;
123528
+ const { name: name21, description, customerId, projectTags: projectTags2, projectTagIds } = input;
122907
123529
  const resolved = await resolveTeamId(input.teamId);
122908
123530
  if (!resolved.ok) return resolved.response;
122909
- await db.insert(schema_exports.projects).values({
123531
+ const [created] = await db.insert(schema_exports.projects).values({
122910
123532
  teamId: resolved.teamId,
122911
123533
  name: name21,
122912
123534
  description: description ?? null,
122913
123535
  customerId: customerId ?? null
122914
- });
123536
+ }).returning({ id: schema_exports.projects.id });
123537
+ if (!created) {
123538
+ return textResponse7("Failed to create project.");
123539
+ }
123540
+ let labelLine = "";
123541
+ if (projectTags2?.length || projectTagIds?.length) {
123542
+ const { tags: tags2, errors } = await resolveProjectTags(resolved.teamId, {
123543
+ tagNames: projectTags2,
123544
+ tagIds: projectTagIds,
123545
+ createMissing: true
123546
+ });
123547
+ if (errors.length > 0) {
123548
+ return textResponse7(
123549
+ `Project created (ID: ${created.id}) but label errors:
123550
+ ${errors.join("\n")}`
123551
+ );
123552
+ }
123553
+ if (tags2.length > 0) {
123554
+ await syncProjectTags(
123555
+ created.id,
123556
+ resolved.teamId,
123557
+ tags2.map((t9) => t9.id)
123558
+ );
123559
+ labelLine = `
123560
+ Labels: ${formatTagList(tags2)}`;
123561
+ }
123562
+ }
122915
123563
  return {
122916
123564
  content: [
122917
123565
  {
@@ -122919,8 +123567,9 @@ async function handleCreateProject(input) {
122919
123567
  text: `\u2705 **Project Created Successfully!**
122920
123568
 
122921
123569
  Name: ${name21}
123570
+ ID: ${created.id}
122922
123571
  ${description ? `Description: ${description}
122923
- ` : ""}`
123572
+ ` : ""}` + labelLine
122924
123573
  }
122925
123574
  ]
122926
123575
  };
@@ -123192,6 +123841,27 @@ async function handleUpdateProject(input) {
123192
123841
  )
123193
123842
  );
123194
123843
  }
123844
+ let labelSyncNote = "";
123845
+ if (input.projectTags !== void 0 || input.projectTagIds !== void 0) {
123846
+ const { tags: tags2, errors } = await resolveProjectTags(owningTeamId, {
123847
+ tagNames: input.projectTags,
123848
+ tagIds: input.projectTagIds,
123849
+ createMissing: true
123850
+ });
123851
+ if (errors.length > 0) {
123852
+ return textResponse7(`Project label errors:
123853
+ ${errors.join("\n")}`);
123854
+ }
123855
+ const syncResult = await syncProjectTags(
123856
+ id,
123857
+ owningTeamId,
123858
+ tags2.map((t9) => t9.id)
123859
+ );
123860
+ if (syncResult.added.length || syncResult.removed.length) {
123861
+ labelSyncNote = `
123862
+ Labels updated: +${syncResult.added.join(", ") || "none"} / -${syncResult.removed.join(", ") || "none"}`;
123863
+ }
123864
+ }
123195
123865
  const [updated] = await db.select({
123196
123866
  id: schema_exports.projects.id,
123197
123867
  name: schema_exports.projects.name,
@@ -123217,6 +123887,13 @@ async function handleUpdateProject(input) {
123217
123887
  );
123218
123888
  }
123219
123889
  lines.push(`Internal: ${updated.internal ? "yes" : "no"}`);
123890
+ if (labelSyncNote) lines.push(labelSyncNote.trim());
123891
+ const projectLabels = (await getTagsForProjects([id])).get(id) ?? [];
123892
+ if (projectLabels.length > 0) {
123893
+ lines.push(
123894
+ `Labels: ${formatTagList(projectLabels)} (ids: ${projectLabels.map((t9) => t9.id).join(", ")})`
123895
+ );
123896
+ }
123220
123897
  if (willRename) {
123221
123898
  lines.push("", "Note: tickets for this project were renumbered.");
123222
123899
  }
@@ -123479,25 +124156,155 @@ async function handleDeleteProject(input) {
123479
124156
 
123480
124157
  Dependencies: ${summary}.
123481
124158
 
123482
- A hard delete would orphan these records, so it is not allowed. Use archive-project instead to safely retire this project (reversible, keeps all data).`
123483
- );
123484
- }
123485
- if (confirmEmptyOnly !== true) {
123486
- return textResponse7(
123487
- `Project "${project.name}" (${project.id}) has no dependencies and can be safely deleted. This is a permanent hard delete. Re-run delete-project with confirmEmptyOnly: true to proceed (or use archive-project to keep the record).`
123488
- );
124159
+ A hard delete would orphan these records, so it is not allowed. Use archive-project instead to safely retire this project (reversible, keeps all data).`
124160
+ );
124161
+ }
124162
+ if (confirmEmptyOnly !== true) {
124163
+ return textResponse7(
124164
+ `Project "${project.name}" (${project.id}) has no dependencies and can be safely deleted. This is a permanent hard delete. Re-run delete-project with confirmEmptyOnly: true to proceed (or use archive-project to keep the record).`
124165
+ );
124166
+ }
124167
+ await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
124168
+ return textResponse7(
124169
+ `\u2705 **Project deleted**
124170
+
124171
+ Project: ${project.name}
124172
+ ID: ${project.id}
124173
+ Action: hard delete (empty project)
124174
+ Status: deleted
124175
+ Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
124176
+
124177
+ The project had no tickets, hours, trips, or templates. Any project-scoped config (member access, tags, slack/github links, team rates) was removed with it.`
124178
+ );
124179
+ }
124180
+ async function resolveProjectForTags(projectId, teamId) {
124181
+ const project = await loadProjectInTeam(projectId, teamId);
124182
+ if (!project) {
124183
+ return {
124184
+ ok: false,
124185
+ response: textResponse7(
124186
+ `Project ${projectId} not found, or it is not owned by this team.`
124187
+ )
124188
+ };
124189
+ }
124190
+ return { ok: true, project };
124191
+ }
124192
+ async function handleGetProjectTags(input) {
124193
+ const resolved = await resolveTeamId(input.teamId);
124194
+ if (!resolved.ok) return resolved.response;
124195
+ const projectResult = await resolveProjectForTags(
124196
+ input.projectId,
124197
+ resolved.teamId
124198
+ );
124199
+ if (!projectResult.ok) return projectResult.response;
124200
+ const labels = (await getTagsForProjects([input.projectId])).get(input.projectId) ?? [];
124201
+ if (labels.length === 0) {
124202
+ return textResponse7(
124203
+ `Project "${projectResult.project.name}" (${input.projectId}) has no project labels.`
124204
+ );
124205
+ }
124206
+ return textResponse7(
124207
+ `Project labels for **${projectResult.project.name}** (${input.projectId}):
124208
+
124209
+ ` + labels.map((t9) => `- ${t9.name} (id: ${t9.id})`).join("\n")
124210
+ );
124211
+ }
124212
+ async function handleSetProjectTags(input) {
124213
+ const resolved = await resolveTeamId(input.teamId);
124214
+ if (!resolved.ok) return resolved.response;
124215
+ const projectResult = await resolveProjectForTags(
124216
+ input.projectId,
124217
+ resolved.teamId
124218
+ );
124219
+ if (!projectResult.ok) return projectResult.response;
124220
+ const owningTeamId = projectResult.project.teamId;
124221
+ const { tags: tags2, errors } = await resolveProjectTags(owningTeamId, {
124222
+ tagNames: input.projectTags,
124223
+ tagIds: input.projectTagIds,
124224
+ createMissing: true
124225
+ });
124226
+ if (errors.length > 0) {
124227
+ return textResponse7(`Project label errors:
124228
+ ${errors.join("\n")}`);
124229
+ }
124230
+ const syncResult = await syncProjectTags(
124231
+ input.projectId,
124232
+ owningTeamId,
124233
+ tags2.map((t9) => t9.id)
124234
+ );
124235
+ return textResponse7(
124236
+ `\u2705 **Project labels set** for "${projectResult.project.name}" (${input.projectId})
124237
+
124238
+ Labels: ${formatTagList(tags2) || "(none)"}
124239
+ Added: ${syncResult.added.join(", ") || "none"}
124240
+ Removed: ${syncResult.removed.join(", ") || "none"}`
124241
+ );
124242
+ }
124243
+ async function handleAddProjectTag(input) {
124244
+ if (!input.projectTag && !input.projectTagId) {
124245
+ return textResponse7("Provide projectTag (name) or projectTagId.");
123489
124246
  }
123490
- await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
124247
+ const resolved = await resolveTeamId(input.teamId);
124248
+ if (!resolved.ok) return resolved.response;
124249
+ const projectResult = await resolveProjectForTags(
124250
+ input.projectId,
124251
+ resolved.teamId
124252
+ );
124253
+ if (!projectResult.ok) return projectResult.response;
124254
+ const owningTeamId = projectResult.project.teamId;
124255
+ const { tags: tags2, errors } = await resolveProjectTags(owningTeamId, {
124256
+ tagNames: input.projectTag ? [input.projectTag] : void 0,
124257
+ tagIds: input.projectTagId ? [input.projectTagId] : void 0,
124258
+ createMissing: true
124259
+ });
124260
+ if (errors.length > 0) {
124261
+ return textResponse7(`Project label errors:
124262
+ ${errors.join("\n")}`);
124263
+ }
124264
+ if (tags2.length === 0) {
124265
+ return textResponse7("No matching project label to add.");
124266
+ }
124267
+ const syncResult = await syncProjectTags(
124268
+ input.projectId,
124269
+ owningTeamId,
124270
+ tags2.map((t9) => t9.id),
124271
+ "merge"
124272
+ );
123491
124273
  return textResponse7(
123492
- `\u2705 **Project deleted**
124274
+ `\u2705 **Project label added** to "${projectResult.project.name}"
123493
124275
 
123494
- Project: ${project.name}
123495
- ID: ${project.id}
123496
- Action: hard delete (empty project)
123497
- Status: deleted
123498
- Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
124276
+ Added: ${syncResult.added.join(", ") || tags2.map((t9) => t9.name).join(", ")}`
124277
+ );
124278
+ }
124279
+ async function handleRemoveProjectTag(input) {
124280
+ if (!input.projectTag && !input.projectTagId) {
124281
+ return textResponse7("Provide projectTag (name) or projectTagId.");
124282
+ }
124283
+ const resolved = await resolveTeamId(input.teamId);
124284
+ if (!resolved.ok) return resolved.response;
124285
+ const projectResult = await resolveProjectForTags(
124286
+ input.projectId,
124287
+ resolved.teamId
124288
+ );
124289
+ if (!projectResult.ok) return projectResult.response;
124290
+ const owningTeamId = projectResult.project.teamId;
124291
+ const filterIds = await resolveProjectTagFilterIds(owningTeamId, {
124292
+ projectTag: input.projectTag,
124293
+ projectTagIds: input.projectTagId ? [input.projectTagId] : void 0
124294
+ });
124295
+ if (filterIds.length === 0) {
124296
+ return textResponse7("No matching project label found to remove.");
124297
+ }
124298
+ const syncResult = await syncProjectTags(
124299
+ input.projectId,
124300
+ owningTeamId,
124301
+ filterIds,
124302
+ "remove"
124303
+ );
124304
+ return textResponse7(
124305
+ `\u2705 **Project label removed** from "${projectResult.project.name}"
123499
124306
 
123500
- The project had no tickets, hours, trips, or templates. Any project-scoped config (member access, tags, slack/github links, team rates) was removed with it.`
124307
+ Removed: ${syncResult.removed.join(", ") || "none"}`
123501
124308
  );
123502
124309
  }
123503
124310
 
@@ -125904,170 +126711,6 @@ ${rendered}`
125904
126711
  };
125905
126712
  }
125906
126713
 
125907
- // src/tools/ticket-tags.ts
125908
- function normalizeTagName(name21) {
125909
- return name21.toLowerCase().trim();
125910
- }
125911
- function formatTagList(tags2) {
125912
- if (tags2.length === 0) return "";
125913
- return tags2.map((t9) => t9.name).join(", ");
125914
- }
125915
- async function getTagsForTickets(ticketIds) {
125916
- const result = /* @__PURE__ */ new Map();
125917
- if (ticketIds.length === 0) return result;
125918
- const rows = await db.select({
125919
- ticketId: schema_exports.ticketTags.ticketId,
125920
- id: schema_exports.tags.id,
125921
- name: schema_exports.tags.name
125922
- }).from(schema_exports.ticketTags).innerJoin(schema_exports.tags, eq(schema_exports.tags.id, schema_exports.ticketTags.tagId)).where(inArray(schema_exports.ticketTags.ticketId, ticketIds)).orderBy(schema_exports.tags.name);
125923
- for (const row of rows) {
125924
- const existing = result.get(row.ticketId) ?? [];
125925
- existing.push({ id: row.id, name: row.name });
125926
- result.set(row.ticketId, existing);
125927
- }
125928
- return result;
125929
- }
125930
- async function resolveTagFilterIds(teamId, input) {
125931
- const ids = new Set(input.tagIds ?? []);
125932
- const names = [
125933
- ...input.tag ? [input.tag] : [],
125934
- ...input.tags ?? []
125935
- ].map(normalizeTagName).filter(Boolean);
125936
- if (names.length > 0) {
125937
- const nameConditions = names.map(
125938
- (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
125939
- );
125940
- const rows = await db.select({ id: schema_exports.tags.id }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
125941
- for (const row of rows) ids.add(row.id);
125942
- }
125943
- return [...ids];
125944
- }
125945
- async function resolveTags(teamId, input) {
125946
- const tags2 = [];
125947
- const errors = [];
125948
- const seenIds = /* @__PURE__ */ new Set();
125949
- if (input.tagIds?.length) {
125950
- const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
125951
- and(
125952
- eq(schema_exports.tags.teamId, teamId),
125953
- inArray(schema_exports.tags.id, input.tagIds)
125954
- )
125955
- );
125956
- for (const row of rows) {
125957
- if (seenIds.has(row.id)) continue;
125958
- seenIds.add(row.id);
125959
- tags2.push(row);
125960
- }
125961
- for (const id of input.tagIds) {
125962
- if (!seenIds.has(id)) errors.push(`Unknown tag ID: ${id}`);
125963
- }
125964
- }
125965
- const rawNames = input.tagNames ?? [];
125966
- if (rawNames.length === 0) return { tags: tags2, errors };
125967
- const normalizedNames = [
125968
- ...new Set(rawNames.map(normalizeTagName).filter(Boolean))
125969
- ];
125970
- if (normalizedNames.length === 0) return { tags: tags2, errors };
125971
- const nameConditions = normalizedNames.map(
125972
- (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
125973
- );
125974
- const existing = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
125975
- const existingByNorm = /* @__PURE__ */ new Map();
125976
- for (const tag2 of existing) {
125977
- existingByNorm.set(normalizeTagName(tag2.name), tag2);
125978
- }
125979
- for (const rawName of rawNames) {
125980
- const norm = normalizeTagName(rawName);
125981
- if (!norm) continue;
125982
- const found = existingByNorm.get(norm);
125983
- if (found) {
125984
- if (!seenIds.has(found.id)) {
125985
- seenIds.add(found.id);
125986
- tags2.push(found);
125987
- }
125988
- continue;
125989
- }
125990
- if (!input.createMissing) {
125991
- errors.push(`Tag not found: ${rawName}`);
125992
- continue;
125993
- }
125994
- try {
125995
- const [created] = await db.insert(schema_exports.tags).values({
125996
- teamId,
125997
- name: norm,
125998
- projectId: input.projectId ?? null
125999
- }).returning({ id: schema_exports.tags.id, name: schema_exports.tags.name });
126000
- if (created) {
126001
- seenIds.add(created.id);
126002
- tags2.push(created);
126003
- existingByNorm.set(norm, created);
126004
- }
126005
- } catch {
126006
- const [retry2] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
126007
- and(
126008
- eq(schema_exports.tags.teamId, teamId),
126009
- sql`lower(${schema_exports.tags.name}) = ${norm}`,
126010
- input.projectId ? or(
126011
- eq(schema_exports.tags.projectId, input.projectId),
126012
- isNull(schema_exports.tags.projectId)
126013
- ) : isNull(schema_exports.tags.projectId)
126014
- )
126015
- ).limit(1);
126016
- if (retry2 && !seenIds.has(retry2.id)) {
126017
- seenIds.add(retry2.id);
126018
- tags2.push(retry2);
126019
- existingByNorm.set(norm, retry2);
126020
- } else {
126021
- errors.push(`Failed to create tag: ${rawName}`);
126022
- }
126023
- }
126024
- }
126025
- return { tags: tags2, errors };
126026
- }
126027
- async function syncTicketTags(ticketId, teamId, tagIds, mode = "replace") {
126028
- const current = await db.select({ tagId: schema_exports.ticketTags.tagId }).from(schema_exports.ticketTags).where(eq(schema_exports.ticketTags.ticketId, ticketId));
126029
- const currentIds = new Set(current.map((row) => row.tagId));
126030
- let targetIds;
126031
- if (mode === "remove") {
126032
- targetIds = new Set(
126033
- [...currentIds].filter((id) => !tagIds.includes(id))
126034
- );
126035
- } else if (mode === "merge") {
126036
- targetIds = /* @__PURE__ */ new Set([...currentIds, ...tagIds]);
126037
- } else {
126038
- targetIds = new Set(tagIds);
126039
- }
126040
- const toInsert = [...targetIds].filter((id) => !currentIds.has(id));
126041
- const toDelete = [...currentIds].filter((id) => !targetIds.has(id));
126042
- if (toDelete.length > 0) {
126043
- await db.delete(schema_exports.ticketTags).where(
126044
- and(
126045
- eq(schema_exports.ticketTags.ticketId, ticketId),
126046
- inArray(schema_exports.ticketTags.tagId, toDelete)
126047
- )
126048
- );
126049
- }
126050
- if (toInsert.length > 0) {
126051
- await db.insert(schema_exports.ticketTags).values(
126052
- toInsert.map((tagId) => ({
126053
- ticketId,
126054
- tagId,
126055
- teamId
126056
- }))
126057
- );
126058
- }
126059
- const tagIdsToDescribe = [.../* @__PURE__ */ new Set([...toInsert, ...toDelete])];
126060
- const tagNamesById = /* @__PURE__ */ new Map();
126061
- if (tagIdsToDescribe.length > 0) {
126062
- const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(inArray(schema_exports.tags.id, tagIdsToDescribe));
126063
- for (const row of rows) tagNamesById.set(row.id, row.name);
126064
- }
126065
- return {
126066
- added: toInsert.map((id) => tagNamesById.get(id) ?? id),
126067
- removed: toDelete.map((id) => tagNamesById.get(id) ?? id)
126068
- };
126069
- }
126070
-
126071
126714
  // src/tools/tags.ts
126072
126715
  async function handleGetTags(input) {
126073
126716
  const resolved = await resolveTeamId(input.teamId);
@@ -127392,6 +128035,9 @@ async function handleGetTickets(input) {
127392
128035
  tag: tag2,
127393
128036
  tags: tags2,
127394
128037
  tagIds,
128038
+ projectTag,
128039
+ projectTags: projectTags2,
128040
+ projectTagIds,
127395
128041
  pageSize = 20
127396
128042
  } = input;
127397
128043
  const resolved = await resolveTeamId(input.teamId);
@@ -127451,6 +128097,24 @@ async function handleGetTickets(input) {
127451
128097
  )`
127452
128098
  );
127453
128099
  }
128100
+ const filterProjectLabelIds = await resolveProjectTagFilterIds(teamId, {
128101
+ projectTag,
128102
+ projectTags: projectTags2,
128103
+ projectTagIds
128104
+ });
128105
+ if (projectTag || projectTags2?.length || projectTagIds?.length) {
128106
+ if (filterProjectLabelIds.length === 0) {
128107
+ return {
128108
+ content: [
128109
+ {
128110
+ type: "text",
128111
+ text: "No tickets found (no matching project labels for the given filter)."
128112
+ }
128113
+ ]
128114
+ };
128115
+ }
128116
+ filters.push(projectTagFilterSql(filterProjectLabelIds));
128117
+ }
127454
128118
  const rows = await db.select({
127455
128119
  id: schema_exports.tickets.id,
127456
128120
  ticketNumber: schema_exports.tickets.ticketNumber,
@@ -127863,7 +128527,7 @@ function workStatesToStatuses(workStates) {
127863
128527
  }
127864
128528
  return [...set3];
127865
128529
  }
127866
- async function loadWorkQueue(scope, assignee, filters) {
128530
+ async function loadWorkQueue(scope, assignee, filters, teamId) {
127867
128531
  const conditions = [
127868
128532
  buildTicketAccessPredicate(
127869
128533
  scope.teamIds,
@@ -127894,6 +128558,17 @@ async function loadWorkQueue(scope, assignee, filters) {
127894
128558
  if (filters.projectId) {
127895
128559
  conditions.push(eq(t8.projectId, filters.projectId));
127896
128560
  }
128561
+ const labelFilterIds = await resolveProjectTagFilterIds(teamId, {
128562
+ projectTag: filters.projectTag,
128563
+ projectTags: filters.projectTags,
128564
+ projectTagIds: filters.projectTagIds
128565
+ });
128566
+ if (filters.projectTag || filters.projectTags?.length || filters.projectTagIds?.length) {
128567
+ if (labelFilterIds.length === 0) {
128568
+ return [];
128569
+ }
128570
+ conditions.push(projectTagFilterSql(labelFilterIds, t8));
128571
+ }
127897
128572
  const rows = await db.select({
127898
128573
  id: t8.id,
127899
128574
  ticketNumber: t8.ticketNumber,
@@ -127946,10 +128621,12 @@ async function handleGetMyWorkQueue(input) {
127946
128621
  const ctx = getAuthContext();
127947
128622
  const scope = await resolveTeamScope(input.teamId);
127948
128623
  if (!scope.ok) return scope.response;
128624
+ const teamId = input.teamId ?? ctx.teamId;
127949
128625
  const rows = await loadWorkQueue(
127950
128626
  scope,
127951
128627
  { mode: "one", userId: ctx.userId },
127952
- input
128628
+ input,
128629
+ teamId
127953
128630
  );
127954
128631
  const items = rows.map(serializeItem);
127955
128632
  return jsonResponse3({
@@ -127972,10 +128649,12 @@ async function handleGetAssigneeWorkQueue(input) {
127972
128649
  const rawAssignee = input.assigneeId?.trim();
127973
128650
  const isAll = rawAssignee === "all";
127974
128651
  const targetUserId = !rawAssignee || rawAssignee === "me" ? ctx.userId : rawAssignee;
128652
+ const teamId = input.teamId ?? ctx.teamId;
127975
128653
  const rows = await loadWorkQueue(
127976
128654
  scope,
127977
128655
  isAll ? { mode: "all" } : { mode: "one", userId: targetUserId },
127978
- input
128656
+ input,
128657
+ teamId
127979
128658
  );
127980
128659
  const items = rows.map(serializeItem);
127981
128660
  if (isAll && input.groupByAssignee) {
@@ -128011,11 +128690,13 @@ async function handleGetAssigneeWorkQueue(input) {
128011
128690
  });
128012
128691
  }
128013
128692
  async function handleGetTeamWorkloadOverview(input) {
128693
+ const ctx = getAuthContext();
128014
128694
  const scope = await resolveTeamScope(input.teamId);
128015
128695
  if (!scope.ok) return scope.response;
128016
128696
  const staleDays = input.staleDays ?? 7;
128017
128697
  const upcomingDays = input.upcomingDays ?? 7;
128018
128698
  const includeUnassigned = input.includeUnassigned ?? true;
128699
+ const teamId = input.teamId ?? ctx.teamId;
128019
128700
  const conditions = [
128020
128701
  buildTicketAccessPredicate(
128021
128702
  scope.teamIds,
@@ -128028,6 +128709,21 @@ async function handleGetTeamWorkloadOverview(input) {
128028
128709
  if (!includeUnassigned) {
128029
128710
  conditions.push(sql`${t8.assigneeId} IS NOT NULL`);
128030
128711
  }
128712
+ const labelFilterIds = await resolveProjectTagFilterIds(teamId, {
128713
+ projectTag: input.projectTag,
128714
+ projectTags: input.projectTags,
128715
+ projectTagIds: input.projectTagIds
128716
+ });
128717
+ if (input.projectTag || input.projectTags?.length || input.projectTagIds?.length) {
128718
+ if (labelFilterIds.length === 0) {
128719
+ return jsonResponse3({
128720
+ teamScope: scope.teamIds,
128721
+ config: { staleDays, upcomingDays, includeUnassigned },
128722
+ assignees: []
128723
+ });
128724
+ }
128725
+ conditions.push(projectTagFilterSql(labelFilterIds, t8));
128726
+ }
128031
128727
  const rows = await db.select({
128032
128728
  assigneeId: t8.assigneeId,
128033
128729
  assigneeName: schema_exports.users.fullName,
@@ -128969,11 +129665,13 @@ async function handleGetRefrontMcpActivity(input) {
128969
129665
  );
128970
129666
  }
128971
129667
  const userIds = /* @__PURE__ */ new Set();
128972
- const ticketIds = /* @__PURE__ */ new Set();
129668
+ const ticketRefs = /* @__PURE__ */ new Set();
128973
129669
  const projectIds = /* @__PURE__ */ new Set();
128974
129670
  for (const r6 of rows) {
128975
129671
  if (r6.userId) userIds.add(r6.userId);
128976
- if (r6.entity.ticketId) ticketIds.add(r6.entity.ticketId);
129672
+ for (const ref of collectTicketRefsFromEntity(r6.entity)) {
129673
+ ticketRefs.add(ref);
129674
+ }
128977
129675
  if (r6.entity.projectId) projectIds.add(r6.entity.projectId);
128978
129676
  }
128979
129677
  const usersById = /* @__PURE__ */ new Map();
@@ -128981,34 +129679,27 @@ async function handleGetRefrontMcpActivity(input) {
128981
129679
  const userRows = await db.select({ id: schema_exports.users.id, name: schema_exports.users.fullName }).from(schema_exports.users).where(inArray(schema_exports.users.id, [...userIds]));
128982
129680
  for (const u2 of userRows) usersById.set(u2.id, u2.name);
128983
129681
  }
128984
- const ticketsById = /* @__PURE__ */ new Map();
128985
- if (ticketIds.size > 0) {
128986
- const ticketRows = await db.select({
128987
- id: schema_exports.tickets.id,
128988
- ticketNumber: schema_exports.tickets.ticketNumber,
128989
- title: schema_exports.tickets.title,
128990
- projectId: schema_exports.tickets.projectId
128991
- }).from(schema_exports.tickets).where(
128992
- and(
128993
- inArray(schema_exports.tickets.id, [...ticketIds]),
128994
- inArray(schema_exports.tickets.teamId, scope.teamIds)
128995
- )
129682
+ const { byRef: ticketsByRef, unresolved: unresolvedTicketRefs } = await resolveTicketRefMap({ refs: ticketRefs, teamIds: scope.teamIds });
129683
+ if (unresolvedTicketRefs.length > 0) {
129684
+ const preview = unresolvedTicketRefs.slice(0, 5).join(", ");
129685
+ const suffix = unresolvedTicketRefs.length > 5 ? ` (+${unresolvedTicketRefs.length - 5} more)` : "";
129686
+ limitations.push(
129687
+ `${unresolvedTicketRefs.length} ticket ref(s) could not be resolved: ${preview}${suffix}.`
128996
129688
  );
128997
- for (const t9 of ticketRows) {
128998
- ticketsById.set(t9.id, {
128999
- ticketNumber: t9.ticketNumber,
129000
- title: t9.title,
129001
- projectId: t9.projectId
129002
- });
129003
- if (t9.projectId) projectIds.add(t9.projectId);
129004
- }
129689
+ }
129690
+ for (const info of ticketsByRef.values()) {
129691
+ if (info.projectId) projectIds.add(info.projectId);
129005
129692
  }
129006
129693
  const projectsById = /* @__PURE__ */ new Map();
129007
129694
  if (projectIds.size > 0) {
129008
129695
  const projectRows = await db.select({ id: schema_exports.projects.id, name: schema_exports.projects.name }).from(schema_exports.projects).where(inArray(schema_exports.projects.id, [...projectIds]));
129009
129696
  for (const p3 of projectRows) projectsById.set(p3.id, p3.name);
129010
129697
  }
129011
- const projectOf = (r6) => r6.entity.projectId ?? ticketsById.get(r6.entity.ticketId ?? "")?.projectId ?? null;
129698
+ const projectOf = (r6) => {
129699
+ const ticket = lookupResolvedTicket(ticketsByRef, r6.entity);
129700
+ return r6.entity.projectId ?? ticket?.projectId ?? null;
129701
+ };
129702
+ const canonicalTicketId = (r6) => lookupResolvedTicket(ticketsByRef, r6.entity)?.id ?? r6.entity.ticketId ?? r6.entity.ticketNumber ?? null;
129012
129703
  const byAction = {};
129013
129704
  const byToolMap = /* @__PURE__ */ new Map();
129014
129705
  const byTicketMap = /* @__PURE__ */ new Map();
@@ -129036,11 +129727,9 @@ async function handleGetRefrontMcpActivity(input) {
129036
129727
  tool2.durationSamples += 1;
129037
129728
  }
129038
129729
  byToolMap.set(r6.toolName, tool2);
129039
- if (r6.entity.ticketId) {
129040
- byTicketMap.set(
129041
- r6.entity.ticketId,
129042
- (byTicketMap.get(r6.entity.ticketId) ?? 0) + 1
129043
- );
129730
+ if (canonicalTicketId(r6)) {
129731
+ const key = canonicalTicketId(r6);
129732
+ byTicketMap.set(key, (byTicketMap.get(key) ?? 0) + 1);
129044
129733
  }
129045
129734
  const projectId = projectOf(r6);
129046
129735
  if (projectId) {
@@ -129053,12 +129742,15 @@ async function handleGetRefrontMcpActivity(input) {
129053
129742
  failures: v2.failures,
129054
129743
  avgDurationMs: v2.durationSamples > 0 ? Math.round(v2.durationMsTotal / v2.durationSamples) : null
129055
129744
  })).sort((a6, b7) => b7.count - a6.count).slice(0, TOP_ENTITIES);
129056
- const byTicket = [...byTicketMap.entries()].map(([ticketId, count2]) => ({
129057
- ticketId,
129058
- ticketNumber: ticketsById.get(ticketId)?.ticketNumber ?? null,
129059
- ticketTitle: ticketsById.get(ticketId)?.title ?? null,
129060
- count: count2
129061
- })).sort((a6, b7) => b7.count - a6.count).slice(0, TOP_ENTITIES);
129745
+ const byTicket = [...byTicketMap.entries()].map(([ticketId, count2]) => {
129746
+ const ticket = ticketsByRef.get(ticketId) ?? ticketsByRef.get(ticketId.toLowerCase());
129747
+ return {
129748
+ ticketId: ticket?.id ?? ticketId,
129749
+ ticketNumber: ticket?.ticketNumber ?? null,
129750
+ ticketTitle: ticket?.title ?? null,
129751
+ count: count2
129752
+ };
129753
+ }).sort((a6, b7) => b7.count - a6.count).slice(0, TOP_ENTITIES);
129062
129754
  const byProject = [...byProjectMap.entries()].map(([projectId, count2]) => ({
129063
129755
  projectId,
129064
129756
  projectName: projectsById.get(projectId) ?? null,
@@ -129070,6 +129762,9 @@ async function handleGetRefrontMcpActivity(input) {
129070
129762
  );
129071
129763
  const timeline = rows.slice(0, pageSize).map((r6) => {
129072
129764
  const projectId = projectOf(r6);
129765
+ const ticket = lookupResolvedTicket(ticketsByRef, r6.entity);
129766
+ const ticketId = ticket?.id ?? r6.entity.ticketId ?? null;
129767
+ const ticketNumber = ticket?.ticketNumber ?? r6.entity.ticketNumber ?? null;
129073
129768
  return {
129074
129769
  timestamp: formatIsoWithOffset(new Date(r6.tsUtc), timezone),
129075
129770
  timestampUtc: r6.tsUtc,
@@ -129078,8 +129773,8 @@ async function handleGetRefrontMcpActivity(input) {
129078
129773
  success: r6.success,
129079
129774
  durationMs: r6.durationMs,
129080
129775
  actor: { id: r6.userId, name: r6.userId ? usersById.get(r6.userId) ?? null : null },
129081
- ticketId: r6.entity.ticketId ?? null,
129082
- ticketNumber: r6.entity.ticketId ? ticketsById.get(r6.entity.ticketId)?.ticketNumber ?? null : null,
129776
+ ticketId,
129777
+ ticketNumber,
129083
129778
  project: projectId ? { id: projectId, name: projectsById.get(projectId) ?? null } : null,
129084
129779
  entity: r6.entity,
129085
129780
  ...r6.error ? { error: r6.error } : {}
@@ -129409,7 +130104,7 @@ var DEFAULT_PAGE_SIZE4 = 200;
129409
130104
  var MAX_PAGE_SIZE4 = 500;
129410
130105
  var MAX_TIMELINE_SOURCE = 2e3;
129411
130106
  var COMMENT_PREVIEW_LENGTH2 = 140;
129412
- var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
130107
+ var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
129413
130108
  var STATUS_TRANSITION_TYPES = /* @__PURE__ */ new Set(["status_changed", "status_change"]);
129414
130109
  var DISCLAIMER3 = "Cycle-time and interaction signals are supporting context, not an exact measure of worked time. Time-in-status includes nights/weekends and idle waiting; interpret with ticket complexity, blockers, reviews and dependencies.";
129415
130110
  function textResponse17(text3) {
@@ -129449,9 +130144,9 @@ async function handleGetTicketInteractionTimeline(input) {
129449
130144
  if (scope.teamIds.length === 0) {
129450
130145
  return textResponse17("No accessible teams found.");
129451
130146
  }
129452
- const isUuid2 = UUID_RE2.test(rawTicket);
130147
+ const isUuid3 = UUID_RE3.test(rawTicket);
129453
130148
  const idBranches = [eq(schema_exports.tickets.ticketNumber, rawTicket)];
129454
- if (isUuid2) idBranches.push(eq(schema_exports.tickets.id, rawTicket));
130149
+ if (isUuid3) idBranches.push(eq(schema_exports.tickets.id, rawTicket));
129455
130150
  const [ticket] = await db.select({
129456
130151
  id: schema_exports.tickets.id,
129457
130152
  teamId: schema_exports.tickets.teamId,
@@ -130009,6 +130704,22 @@ function createMcpServer() {
130009
130704
  return await handleCreateProject(asToolArgs(toolArgs));
130010
130705
  case "update-project":
130011
130706
  return await handleUpdateProject(asToolArgs(toolArgs));
130707
+ case "get-project-tags":
130708
+ return await handleGetProjectTags(
130709
+ asToolArgs(toolArgs)
130710
+ );
130711
+ case "set-project-tags":
130712
+ return await handleSetProjectTags(
130713
+ asToolArgs(toolArgs)
130714
+ );
130715
+ case "add-project-tag":
130716
+ return await handleAddProjectTag(
130717
+ asToolArgs(toolArgs)
130718
+ );
130719
+ case "remove-project-tag":
130720
+ return await handleRemoveProjectTag(
130721
+ asToolArgs(toolArgs)
130722
+ );
130012
130723
  case "archive-project":
130013
130724
  return await handleArchiveProject(
130014
130725
  asToolArgs(toolArgs)