@mgsoftwarebv/mcp-server-bridge 3.5.22 → 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
@@ -106143,6 +106143,20 @@ var TOOLS = [
106143
106143
  items: { type: "string" },
106144
106144
  description: "Filter by tag IDs"
106145
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
+ },
106146
106160
  pageSize: { type: "number", default: 20, maximum: 100 }
106147
106161
  },
106148
106162
  required: []
@@ -106156,6 +106170,20 @@ var TOOLS = [
106156
106170
  properties: {
106157
106171
  teamId: teamIdProp,
106158
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
+ },
106159
106187
  priority: {
106160
106188
  type: "string",
106161
106189
  enum: ["low", "medium", "high", "critical"]
@@ -106194,6 +106222,9 @@ var TOOLS = [
106194
106222
  description: "When assigneeId='all', group the items per assignee."
106195
106223
  },
106196
106224
  projectId: { type: "string" },
106225
+ projectTag: { type: "string" },
106226
+ projectTags: { type: "array", items: { type: "string" } },
106227
+ projectTagIds: { type: "array", items: { type: "string" } },
106197
106228
  priority: {
106198
106229
  type: "string",
106199
106230
  enum: ["low", "medium", "high", "critical"]
@@ -106232,7 +106263,10 @@ var TOOLS = [
106232
106263
  type: "boolean",
106233
106264
  default: true,
106234
106265
  description: "Include the unassigned bucket in the overview."
106235
- }
106266
+ },
106267
+ projectTag: { type: "string" },
106268
+ projectTags: { type: "array", items: { type: "string" } },
106269
+ projectTagIds: { type: "array", items: { type: "string" } }
106236
106270
  },
106237
106271
  required: []
106238
106272
  }
@@ -106867,13 +106901,27 @@ var TOOLS = [
106867
106901
  },
106868
106902
  {
106869
106903
  name: "get-projects",
106870
- 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.",
106871
106905
  inputSchema: {
106872
106906
  type: "object",
106873
106907
  properties: {
106874
106908
  teamId: teamIdProp,
106875
106909
  customerId: { type: "string", description: "Filter by customer ID" },
106876
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
+ },
106877
106925
  status: {
106878
106926
  type: "string",
106879
106927
  enum: ["active", "archived", "all"],
@@ -106887,7 +106935,7 @@ var TOOLS = [
106887
106935
  },
106888
106936
  {
106889
106937
  name: "create-project",
106890
- 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).",
106891
106939
  inputSchema: {
106892
106940
  type: "object",
106893
106941
  properties: {
@@ -106895,6 +106943,16 @@ var TOOLS = [
106895
106943
  name: { type: "string", description: "Project name" },
106896
106944
  description: { type: "string" },
106897
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
+ },
106898
106956
  status: {
106899
106957
  type: "string",
106900
106958
  enum: ["active", "on_hold", "completed", "cancelled"],
@@ -106906,7 +106964,7 @@ var TOOLS = [
106906
106964
  },
106907
106965
  {
106908
106966
  name: "update-project",
106909
- 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.",
106910
106968
  inputSchema: {
106911
106969
  type: "object",
106912
106970
  properties: {
@@ -106928,11 +106986,83 @@ var TOOLS = [
106928
106986
  internal: {
106929
106987
  type: "boolean",
106930
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"
106931
106999
  }
106932
107000
  },
106933
107001
  required: ["id"]
106934
107002
  }
106935
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
+ },
106936
107066
  {
106937
107067
  name: "archive-project",
106938
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.",
@@ -122886,116 +123016,550 @@ ${JSON.stringify(payload, null, 2)}
122886
123016
  );
122887
123017
  }
122888
123018
 
122889
- // src/tools/project-cleanup-util.ts
122890
- var PROJECT_STATUS_FILTERS = [
122891
- "active",
122892
- "archived",
122893
- "all"
122894
- ];
122895
- var DEPENDENCY_LABELS2 = {
122896
- tickets: "ticket(s)",
122897
- timesheetEvents: "agenda/time entr(ies)",
122898
- timesheetTemplates: "timesheet template(s)",
122899
- trips: "trip(s)",
122900
- tripTemplates: "trip template(s)"
122901
- };
122902
- function getProjectArchiveState(settings) {
122903
- const obj = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
122904
- const archivedAt = typeof obj.archivedAt === "string" && obj.archivedAt.trim().length > 0 ? obj.archivedAt : null;
122905
- const archiveReason = typeof obj.archiveReason === "string" && obj.archiveReason.trim().length > 0 ? obj.archiveReason : null;
122906
- return { archived: archivedAt !== null, archivedAt, archiveReason };
122907
- }
122908
- function withArchiveSettings(settings, archivedAt, reason) {
122909
- const base = settings && typeof settings === "object" && !Array.isArray(settings) ? { ...settings } : {};
122910
- base.archivedAt = archivedAt;
122911
- if (reason && reason.trim().length > 0) {
122912
- base.archiveReason = reason.trim();
122913
- }
122914
- return base;
122915
- }
122916
- function totalProjectDependencies(counts) {
122917
- 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();
122918
123022
  }
122919
- function isProjectEmpty(counts) {
122920
- return totalProjectDependencies(counts) === 0;
123023
+ function formatTagList(tags2) {
123024
+ if (tags2.length === 0) return "";
123025
+ return tags2.map((t9) => t9.name).join(", ");
122921
123026
  }
122922
- function formatProjectDependencies(counts) {
122923
- const parts = Object.keys(DEPENDENCY_LABELS2).filter((key) => counts[key] > 0).map((key) => `${counts[key]} ${DEPENDENCY_LABELS2[key]}`);
122924
- 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;
122925
123041
  }
122926
-
122927
- // src/tools/projects.ts
122928
- async function handleGetProjects(input) {
122929
- const ctx = getAuthContext();
122930
- const { customerId, q: q3, pageSize = 20 } = input;
122931
- const status = input.status ?? "active";
122932
- if (!PROJECT_STATUS_FILTERS.includes(status)) {
122933
- return {
122934
- content: [
122935
- {
122936
- type: "text",
122937
- text: `Error: invalid status "${status}". Allowed: ${PROJECT_STATUS_FILTERS.join(", ")}.`
122938
- }
122939
- ]
122940
- };
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);
122941
123054
  }
122942
- const resolved = await resolveTeamId(input.teamId);
122943
- if (!resolved.ok) return resolved.response;
122944
- const projectIds = await getAccessibleProjectIds(ctx.userId, resolved.teamId);
122945
- if (projectIds.length === 0) {
122946
- return {
122947
- content: [
122948
- {
122949
- type: "text",
122950
- text: "No projects found or no access to any projects."
122951
- }
122952
- ]
122953
- };
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
+ }
122954
123076
  }
122955
- const filters = [inArray(schema_exports.projects.id, projectIds)];
122956
- if (customerId) filters.push(eq(schema_exports.projects.customerId, customerId));
122957
- if (q3) filters.push(ilike(schema_exports.projects.name, `%${q3}%`));
122958
- if (status === "active") {
122959
- filters.push(sql`${schema_exports.projects.settings} ->> 'archivedAt' IS NULL`);
122960
- } else if (status === "archived") {
122961
- 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);
122962
123090
  }
122963
- const rows = await db.select({
122964
- id: schema_exports.projects.id,
122965
- name: schema_exports.projects.name,
122966
- description: schema_exports.projects.description,
122967
- customerId: schema_exports.projects.customerId,
122968
- createdAt: schema_exports.projects.createdAt,
122969
- settings: schema_exports.projects.settings
122970
- }).from(schema_exports.projects).where(and(...filters)).orderBy(asc(schema_exports.projects.name)).limit(Math.min(pageSize, 100));
122971
- return {
122972
- content: [
122973
- {
122974
- type: "text",
122975
- text: `Found ${rows.length} project(s)${status !== "all" ? ` (status: ${status})` : ""}:
122976
-
122977
- ${rows.map((p3) => {
122978
- const archive = getProjectArchiveState(p3.settings);
122979
- return `**${p3.name}** (ID: ${p3.id})${archive.archived ? " \u2014 ARCHIVED" : ""}
122980
- ${p3.description ? `Description: ${p3.description}
122981
- ` : ""}Created: ${new Date(p3.createdAt).toLocaleDateString()}
122982
- ${archive.archived ? `Archived: ${archive.archivedAt}${archive.archiveReason ? ` (${archive.archiveReason})` : ""}
122983
- ` : ""}`;
122984
- }).join("\n") || "No projects found."}`
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);
122985
123099
  }
122986
- ]
122987
- };
122988
- }
122989
- async function handleCreateProject(input) {
122990
- const { name: name21, description, customerId } = input;
122991
- const resolved = await resolveTeamId(input.teamId);
122992
- if (!resolved.ok) return resolved.response;
122993
- await db.insert(schema_exports.projects).values({
122994
- teamId: resolved.teamId,
122995
- name: name21,
122996
- description: description ?? null,
122997
- customerId: customerId ?? null
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
122998
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,
123499
+ name: schema_exports.projects.name,
123500
+ description: schema_exports.projects.description,
123501
+ customerId: schema_exports.projects.customerId,
123502
+ createdAt: schema_exports.projects.createdAt,
123503
+ settings: schema_exports.projects.settings
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));
123506
+ return {
123507
+ content: [
123508
+ {
123509
+ type: "text",
123510
+ text: `Found ${rows.length} project(s)${status !== "all" ? ` (status: ${status})` : ""}:
123511
+
123512
+ ${rows.map((p3) => {
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
+ ` : "";
123517
+ return `**${p3.name}** (ID: ${p3.id})${archive.archived ? " \u2014 ARCHIVED" : ""}
123518
+ ${p3.description ? `Description: ${p3.description}
123519
+ ` : ""}` + labelLine + `Created: ${new Date(p3.createdAt).toLocaleDateString()}
123520
+ ${archive.archived ? `Archived: ${archive.archivedAt}${archive.archiveReason ? ` (${archive.archiveReason})` : ""}
123521
+ ` : ""}`;
123522
+ }).join("\n") || "No projects found."}`
123523
+ }
123524
+ ]
123525
+ };
123526
+ }
123527
+ async function handleCreateProject(input) {
123528
+ const { name: name21, description, customerId, projectTags: projectTags2, projectTagIds } = input;
123529
+ const resolved = await resolveTeamId(input.teamId);
123530
+ if (!resolved.ok) return resolved.response;
123531
+ const [created] = await db.insert(schema_exports.projects).values({
123532
+ teamId: resolved.teamId,
123533
+ name: name21,
123534
+ description: description ?? null,
123535
+ customerId: customerId ?? null
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
+ }
122999
123563
  return {
123000
123564
  content: [
123001
123565
  {
@@ -123003,8 +123567,9 @@ async function handleCreateProject(input) {
123003
123567
  text: `\u2705 **Project Created Successfully!**
123004
123568
 
123005
123569
  Name: ${name21}
123570
+ ID: ${created.id}
123006
123571
  ${description ? `Description: ${description}
123007
- ` : ""}`
123572
+ ` : ""}` + labelLine
123008
123573
  }
123009
123574
  ]
123010
123575
  };
@@ -123276,6 +123841,27 @@ async function handleUpdateProject(input) {
123276
123841
  )
123277
123842
  );
123278
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
+ }
123279
123865
  const [updated] = await db.select({
123280
123866
  id: schema_exports.projects.id,
123281
123867
  name: schema_exports.projects.name,
@@ -123301,6 +123887,13 @@ async function handleUpdateProject(input) {
123301
123887
  );
123302
123888
  }
123303
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
+ }
123304
123897
  if (willRename) {
123305
123898
  lines.push("", "Note: tickets for this project were renumbered.");
123306
123899
  }
@@ -123584,6 +124177,136 @@ Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
123584
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.`
123585
124178
  );
123586
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.");
124246
+ }
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
+ );
124273
+ return textResponse7(
124274
+ `\u2705 **Project label added** to "${projectResult.project.name}"
124275
+
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}"
124306
+
124307
+ Removed: ${syncResult.removed.join(", ") || "none"}`
124308
+ );
124309
+ }
123587
124310
 
123588
124311
  // src/tools/products.ts
123589
124312
  var PRODUCT_STATUSES = ["active", "archived", "all"];
@@ -125988,170 +126711,6 @@ ${rendered}`
125988
126711
  };
125989
126712
  }
125990
126713
 
125991
- // src/tools/ticket-tags.ts
125992
- function normalizeTagName(name21) {
125993
- return name21.toLowerCase().trim();
125994
- }
125995
- function formatTagList(tags2) {
125996
- if (tags2.length === 0) return "";
125997
- return tags2.map((t9) => t9.name).join(", ");
125998
- }
125999
- async function getTagsForTickets(ticketIds) {
126000
- const result = /* @__PURE__ */ new Map();
126001
- if (ticketIds.length === 0) return result;
126002
- const rows = await db.select({
126003
- ticketId: schema_exports.ticketTags.ticketId,
126004
- id: schema_exports.tags.id,
126005
- name: schema_exports.tags.name
126006
- }).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);
126007
- for (const row of rows) {
126008
- const existing = result.get(row.ticketId) ?? [];
126009
- existing.push({ id: row.id, name: row.name });
126010
- result.set(row.ticketId, existing);
126011
- }
126012
- return result;
126013
- }
126014
- async function resolveTagFilterIds(teamId, input) {
126015
- const ids = new Set(input.tagIds ?? []);
126016
- const names = [
126017
- ...input.tag ? [input.tag] : [],
126018
- ...input.tags ?? []
126019
- ].map(normalizeTagName).filter(Boolean);
126020
- if (names.length > 0) {
126021
- const nameConditions = names.map(
126022
- (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
126023
- );
126024
- const rows = await db.select({ id: schema_exports.tags.id }).from(schema_exports.tags).where(and(eq(schema_exports.tags.teamId, teamId), or(...nameConditions)));
126025
- for (const row of rows) ids.add(row.id);
126026
- }
126027
- return [...ids];
126028
- }
126029
- async function resolveTags(teamId, input) {
126030
- const tags2 = [];
126031
- const errors = [];
126032
- const seenIds = /* @__PURE__ */ new Set();
126033
- if (input.tagIds?.length) {
126034
- const rows = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
126035
- and(
126036
- eq(schema_exports.tags.teamId, teamId),
126037
- inArray(schema_exports.tags.id, input.tagIds)
126038
- )
126039
- );
126040
- for (const row of rows) {
126041
- if (seenIds.has(row.id)) continue;
126042
- seenIds.add(row.id);
126043
- tags2.push(row);
126044
- }
126045
- for (const id of input.tagIds) {
126046
- if (!seenIds.has(id)) errors.push(`Unknown tag ID: ${id}`);
126047
- }
126048
- }
126049
- const rawNames = input.tagNames ?? [];
126050
- if (rawNames.length === 0) return { tags: tags2, errors };
126051
- const normalizedNames = [
126052
- ...new Set(rawNames.map(normalizeTagName).filter(Boolean))
126053
- ];
126054
- if (normalizedNames.length === 0) return { tags: tags2, errors };
126055
- const nameConditions = normalizedNames.map(
126056
- (name21) => sql`lower(${schema_exports.tags.name}) = ${name21}`
126057
- );
126058
- 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)));
126059
- const existingByNorm = /* @__PURE__ */ new Map();
126060
- for (const tag2 of existing) {
126061
- existingByNorm.set(normalizeTagName(tag2.name), tag2);
126062
- }
126063
- for (const rawName of rawNames) {
126064
- const norm = normalizeTagName(rawName);
126065
- if (!norm) continue;
126066
- const found = existingByNorm.get(norm);
126067
- if (found) {
126068
- if (!seenIds.has(found.id)) {
126069
- seenIds.add(found.id);
126070
- tags2.push(found);
126071
- }
126072
- continue;
126073
- }
126074
- if (!input.createMissing) {
126075
- errors.push(`Tag not found: ${rawName}`);
126076
- continue;
126077
- }
126078
- try {
126079
- const [created] = await db.insert(schema_exports.tags).values({
126080
- teamId,
126081
- name: norm,
126082
- projectId: input.projectId ?? null
126083
- }).returning({ id: schema_exports.tags.id, name: schema_exports.tags.name });
126084
- if (created) {
126085
- seenIds.add(created.id);
126086
- tags2.push(created);
126087
- existingByNorm.set(norm, created);
126088
- }
126089
- } catch {
126090
- const [retry2] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
126091
- and(
126092
- eq(schema_exports.tags.teamId, teamId),
126093
- sql`lower(${schema_exports.tags.name}) = ${norm}`,
126094
- input.projectId ? or(
126095
- eq(schema_exports.tags.projectId, input.projectId),
126096
- isNull(schema_exports.tags.projectId)
126097
- ) : isNull(schema_exports.tags.projectId)
126098
- )
126099
- ).limit(1);
126100
- if (retry2 && !seenIds.has(retry2.id)) {
126101
- seenIds.add(retry2.id);
126102
- tags2.push(retry2);
126103
- existingByNorm.set(norm, retry2);
126104
- } else {
126105
- errors.push(`Failed to create tag: ${rawName}`);
126106
- }
126107
- }
126108
- }
126109
- return { tags: tags2, errors };
126110
- }
126111
- async function syncTicketTags(ticketId, teamId, tagIds, mode = "replace") {
126112
- const current = await db.select({ tagId: schema_exports.ticketTags.tagId }).from(schema_exports.ticketTags).where(eq(schema_exports.ticketTags.ticketId, ticketId));
126113
- const currentIds = new Set(current.map((row) => row.tagId));
126114
- let targetIds;
126115
- if (mode === "remove") {
126116
- targetIds = new Set(
126117
- [...currentIds].filter((id) => !tagIds.includes(id))
126118
- );
126119
- } else if (mode === "merge") {
126120
- targetIds = /* @__PURE__ */ new Set([...currentIds, ...tagIds]);
126121
- } else {
126122
- targetIds = new Set(tagIds);
126123
- }
126124
- const toInsert = [...targetIds].filter((id) => !currentIds.has(id));
126125
- const toDelete = [...currentIds].filter((id) => !targetIds.has(id));
126126
- if (toDelete.length > 0) {
126127
- await db.delete(schema_exports.ticketTags).where(
126128
- and(
126129
- eq(schema_exports.ticketTags.ticketId, ticketId),
126130
- inArray(schema_exports.ticketTags.tagId, toDelete)
126131
- )
126132
- );
126133
- }
126134
- if (toInsert.length > 0) {
126135
- await db.insert(schema_exports.ticketTags).values(
126136
- toInsert.map((tagId) => ({
126137
- ticketId,
126138
- tagId,
126139
- teamId
126140
- }))
126141
- );
126142
- }
126143
- const tagIdsToDescribe = [.../* @__PURE__ */ new Set([...toInsert, ...toDelete])];
126144
- const tagNamesById = /* @__PURE__ */ new Map();
126145
- if (tagIdsToDescribe.length > 0) {
126146
- 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));
126147
- for (const row of rows) tagNamesById.set(row.id, row.name);
126148
- }
126149
- return {
126150
- added: toInsert.map((id) => tagNamesById.get(id) ?? id),
126151
- removed: toDelete.map((id) => tagNamesById.get(id) ?? id)
126152
- };
126153
- }
126154
-
126155
126714
  // src/tools/tags.ts
126156
126715
  async function handleGetTags(input) {
126157
126716
  const resolved = await resolveTeamId(input.teamId);
@@ -127476,6 +128035,9 @@ async function handleGetTickets(input) {
127476
128035
  tag: tag2,
127477
128036
  tags: tags2,
127478
128037
  tagIds,
128038
+ projectTag,
128039
+ projectTags: projectTags2,
128040
+ projectTagIds,
127479
128041
  pageSize = 20
127480
128042
  } = input;
127481
128043
  const resolved = await resolveTeamId(input.teamId);
@@ -127535,6 +128097,24 @@ async function handleGetTickets(input) {
127535
128097
  )`
127536
128098
  );
127537
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
+ }
127538
128118
  const rows = await db.select({
127539
128119
  id: schema_exports.tickets.id,
127540
128120
  ticketNumber: schema_exports.tickets.ticketNumber,
@@ -127947,7 +128527,7 @@ function workStatesToStatuses(workStates) {
127947
128527
  }
127948
128528
  return [...set3];
127949
128529
  }
127950
- async function loadWorkQueue(scope, assignee, filters) {
128530
+ async function loadWorkQueue(scope, assignee, filters, teamId) {
127951
128531
  const conditions = [
127952
128532
  buildTicketAccessPredicate(
127953
128533
  scope.teamIds,
@@ -127978,6 +128558,17 @@ async function loadWorkQueue(scope, assignee, filters) {
127978
128558
  if (filters.projectId) {
127979
128559
  conditions.push(eq(t8.projectId, filters.projectId));
127980
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
+ }
127981
128572
  const rows = await db.select({
127982
128573
  id: t8.id,
127983
128574
  ticketNumber: t8.ticketNumber,
@@ -128030,10 +128621,12 @@ async function handleGetMyWorkQueue(input) {
128030
128621
  const ctx = getAuthContext();
128031
128622
  const scope = await resolveTeamScope(input.teamId);
128032
128623
  if (!scope.ok) return scope.response;
128624
+ const teamId = input.teamId ?? ctx.teamId;
128033
128625
  const rows = await loadWorkQueue(
128034
128626
  scope,
128035
128627
  { mode: "one", userId: ctx.userId },
128036
- input
128628
+ input,
128629
+ teamId
128037
128630
  );
128038
128631
  const items = rows.map(serializeItem);
128039
128632
  return jsonResponse3({
@@ -128056,10 +128649,12 @@ async function handleGetAssigneeWorkQueue(input) {
128056
128649
  const rawAssignee = input.assigneeId?.trim();
128057
128650
  const isAll = rawAssignee === "all";
128058
128651
  const targetUserId = !rawAssignee || rawAssignee === "me" ? ctx.userId : rawAssignee;
128652
+ const teamId = input.teamId ?? ctx.teamId;
128059
128653
  const rows = await loadWorkQueue(
128060
128654
  scope,
128061
128655
  isAll ? { mode: "all" } : { mode: "one", userId: targetUserId },
128062
- input
128656
+ input,
128657
+ teamId
128063
128658
  );
128064
128659
  const items = rows.map(serializeItem);
128065
128660
  if (isAll && input.groupByAssignee) {
@@ -128095,11 +128690,13 @@ async function handleGetAssigneeWorkQueue(input) {
128095
128690
  });
128096
128691
  }
128097
128692
  async function handleGetTeamWorkloadOverview(input) {
128693
+ const ctx = getAuthContext();
128098
128694
  const scope = await resolveTeamScope(input.teamId);
128099
128695
  if (!scope.ok) return scope.response;
128100
128696
  const staleDays = input.staleDays ?? 7;
128101
128697
  const upcomingDays = input.upcomingDays ?? 7;
128102
128698
  const includeUnassigned = input.includeUnassigned ?? true;
128699
+ const teamId = input.teamId ?? ctx.teamId;
128103
128700
  const conditions = [
128104
128701
  buildTicketAccessPredicate(
128105
128702
  scope.teamIds,
@@ -128112,6 +128709,21 @@ async function handleGetTeamWorkloadOverview(input) {
128112
128709
  if (!includeUnassigned) {
128113
128710
  conditions.push(sql`${t8.assigneeId} IS NOT NULL`);
128114
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
+ }
128115
128727
  const rows = await db.select({
128116
128728
  assigneeId: t8.assigneeId,
128117
128729
  assigneeName: schema_exports.users.fullName,
@@ -130092,6 +130704,22 @@ function createMcpServer() {
130092
130704
  return await handleCreateProject(asToolArgs(toolArgs));
130093
130705
  case "update-project":
130094
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
+ );
130095
130723
  case "archive-project":
130096
130724
  return await handleArchiveProject(
130097
130725
  asToolArgs(toolArgs)