@mgsoftwarebv/mcp-server-bridge 3.4.1 → 3.5.0
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 +656 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6170,20 +6170,20 @@ var require_resolve = __commonJS({
|
|
|
6170
6170
|
return false;
|
|
6171
6171
|
}
|
|
6172
6172
|
function countKeys(schema) {
|
|
6173
|
-
let
|
|
6173
|
+
let count2 = 0;
|
|
6174
6174
|
for (const key in schema) {
|
|
6175
6175
|
if (key === "$ref")
|
|
6176
6176
|
return Infinity;
|
|
6177
|
-
|
|
6177
|
+
count2++;
|
|
6178
6178
|
if (SIMPLE_INLINED.has(key))
|
|
6179
6179
|
continue;
|
|
6180
6180
|
if (typeof schema[key] == "object") {
|
|
6181
|
-
(0, util_1.eachItem)(schema[key], (sch) =>
|
|
6181
|
+
(0, util_1.eachItem)(schema[key], (sch) => count2 += countKeys(sch));
|
|
6182
6182
|
}
|
|
6183
|
-
if (
|
|
6183
|
+
if (count2 === Infinity)
|
|
6184
6184
|
return Infinity;
|
|
6185
6185
|
}
|
|
6186
|
-
return
|
|
6186
|
+
return count2;
|
|
6187
6187
|
}
|
|
6188
6188
|
function getFullPath(resolver, id = "", normalize2) {
|
|
6189
6189
|
if (normalize2 !== false)
|
|
@@ -9228,8 +9228,8 @@ var require_contains = __commonJS({
|
|
|
9228
9228
|
cxt.result(valid, () => cxt.reset());
|
|
9229
9229
|
function validateItemsWithCount() {
|
|
9230
9230
|
const schValid = gen.name("_valid");
|
|
9231
|
-
const
|
|
9232
|
-
validateItems(schValid, () => gen.if(schValid, () => checkLimits(
|
|
9231
|
+
const count2 = gen.let("count", 0);
|
|
9232
|
+
validateItems(schValid, () => gen.if(schValid, () => checkLimits(count2)));
|
|
9233
9233
|
}
|
|
9234
9234
|
function validateItems(_valid, block) {
|
|
9235
9235
|
gen.forRange("i", 0, len, (i6) => {
|
|
@@ -9242,16 +9242,16 @@ var require_contains = __commonJS({
|
|
|
9242
9242
|
block();
|
|
9243
9243
|
});
|
|
9244
9244
|
}
|
|
9245
|
-
function checkLimits(
|
|
9246
|
-
gen.code((0, codegen_1._)`${
|
|
9245
|
+
function checkLimits(count2) {
|
|
9246
|
+
gen.code((0, codegen_1._)`${count2}++`);
|
|
9247
9247
|
if (max === void 0) {
|
|
9248
|
-
gen.if((0, codegen_1._)`${
|
|
9248
|
+
gen.if((0, codegen_1._)`${count2} >= ${min}`, () => gen.assign(valid, true).break());
|
|
9249
9249
|
} else {
|
|
9250
|
-
gen.if((0, codegen_1._)`${
|
|
9250
|
+
gen.if((0, codegen_1._)`${count2} > ${max}`, () => gen.assign(valid, false).break());
|
|
9251
9251
|
if (min === 1)
|
|
9252
9252
|
gen.assign(valid, true);
|
|
9253
9253
|
else
|
|
9254
|
-
gen.if((0, codegen_1._)`${
|
|
9254
|
+
gen.if((0, codegen_1._)`${count2} >= ${min}`, () => gen.assign(valid, true));
|
|
9255
9255
|
}
|
|
9256
9256
|
}
|
|
9257
9257
|
}
|
|
@@ -15018,8 +15018,8 @@ var init_az = __esm({
|
|
|
15018
15018
|
});
|
|
15019
15019
|
|
|
15020
15020
|
// ../../node_modules/zod/v4/locales/be.js
|
|
15021
|
-
function getBelarusianPlural(
|
|
15022
|
-
const absCount = Math.abs(
|
|
15021
|
+
function getBelarusianPlural(count2, one, few, many) {
|
|
15022
|
+
const absCount = Math.abs(count2);
|
|
15023
15023
|
const lastDigit = absCount % 10;
|
|
15024
15024
|
const lastTwoDigits = absCount % 100;
|
|
15025
15025
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -16933,8 +16933,8 @@ var init_hu = __esm({
|
|
|
16933
16933
|
});
|
|
16934
16934
|
|
|
16935
16935
|
// ../../node_modules/zod/v4/locales/hy.js
|
|
16936
|
-
function getArmenianPlural(
|
|
16937
|
-
return Math.abs(
|
|
16936
|
+
function getArmenianPlural(count2, one, many) {
|
|
16937
|
+
return Math.abs(count2) === 1 ? one : many;
|
|
16938
16938
|
}
|
|
16939
16939
|
function withDefiniteArticle(word) {
|
|
16940
16940
|
if (!word)
|
|
@@ -19049,8 +19049,8 @@ var init_pt = __esm({
|
|
|
19049
19049
|
});
|
|
19050
19050
|
|
|
19051
19051
|
// ../../node_modules/zod/v4/locales/ru.js
|
|
19052
|
-
function getRussianPlural(
|
|
19053
|
-
const absCount = Math.abs(
|
|
19052
|
+
function getRussianPlural(count2, one, few, many) {
|
|
19053
|
+
const absCount = Math.abs(count2);
|
|
19054
19054
|
const lastDigit = absCount % 10;
|
|
19055
19055
|
const lastTwoDigits = absCount % 100;
|
|
19056
19056
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -59475,9 +59475,9 @@ var init_DefaultRetryToken = __esm({
|
|
|
59475
59475
|
count;
|
|
59476
59476
|
cost;
|
|
59477
59477
|
longPoll;
|
|
59478
|
-
constructor(delay2,
|
|
59478
|
+
constructor(delay2, count2, cost, longPoll) {
|
|
59479
59479
|
this.delay = delay2;
|
|
59480
|
-
this.count =
|
|
59480
|
+
this.count = count2;
|
|
59481
59481
|
this.cost = cost;
|
|
59482
59482
|
this.longPoll = longPoll;
|
|
59483
59483
|
}
|
|
@@ -62418,9 +62418,9 @@ function validateAmpersand(xmlData, i6) {
|
|
|
62418
62418
|
i6++;
|
|
62419
62419
|
return validateNumberAmpersand(xmlData, i6);
|
|
62420
62420
|
}
|
|
62421
|
-
let
|
|
62422
|
-
for (; i6 < xmlData.length; i6++,
|
|
62423
|
-
if (xmlData[i6].match(/\w/) &&
|
|
62421
|
+
let count2 = 0;
|
|
62422
|
+
for (; i6 < xmlData.length; i6++, count2++) {
|
|
62423
|
+
if (xmlData[i6].match(/\w/) && count2 < 20)
|
|
62424
62424
|
continue;
|
|
62425
62425
|
if (xmlData[i6] === ";")
|
|
62426
62426
|
break;
|
|
@@ -65059,8 +65059,8 @@ var init_Matcher = __esm({
|
|
|
65059
65059
|
const siblingKey = namespace ? `${namespace}:${tagName}` : tagName;
|
|
65060
65060
|
const counter = siblings.get(siblingKey) || 0;
|
|
65061
65061
|
let position = 0;
|
|
65062
|
-
for (const
|
|
65063
|
-
position +=
|
|
65062
|
+
for (const count2 of siblings.values()) {
|
|
65063
|
+
position += count2;
|
|
65064
65064
|
}
|
|
65065
65065
|
siblings.set(siblingKey, counter + 1);
|
|
65066
65066
|
const node = {
|
|
@@ -86413,6 +86413,11 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
|
|
|
86413
86413
|
return result;
|
|
86414
86414
|
}
|
|
86415
86415
|
|
|
86416
|
+
// ../../node_modules/drizzle-orm/sql/functions/aggregate.js
|
|
86417
|
+
function count(expression) {
|
|
86418
|
+
return sql`count(${sql.raw("*")})`.mapWith(Number);
|
|
86419
|
+
}
|
|
86420
|
+
|
|
86416
86421
|
// ../../node_modules/postgres/src/query.js
|
|
86417
86422
|
var originCache = /* @__PURE__ */ new Map();
|
|
86418
86423
|
var originStackCache = /* @__PURE__ */ new Map();
|
|
@@ -105727,6 +105732,70 @@ var TOOLS = [
|
|
|
105727
105732
|
required: ["name"]
|
|
105728
105733
|
}
|
|
105729
105734
|
},
|
|
105735
|
+
{
|
|
105736
|
+
name: "update-tag",
|
|
105737
|
+
description: "Rename a team tag and/or change its scope. Provide `name` to rename (a case-insensitive name collision in the same scope is rejected \u2014 use merge-tags instead), and/or `projectId` to re-scope (a project UUID, or null for a general team tag). Existing ticket/customer/project/transaction tag relations are preserved. Find tag ids via get-tags.",
|
|
105738
|
+
inputSchema: {
|
|
105739
|
+
type: "object",
|
|
105740
|
+
properties: {
|
|
105741
|
+
teamId: teamIdProp,
|
|
105742
|
+
tagId: { type: "string", description: "Tag ID (UUID) to update" },
|
|
105743
|
+
name: { type: "string", description: "New tag name" },
|
|
105744
|
+
projectId: {
|
|
105745
|
+
type: ["string", "null"],
|
|
105746
|
+
description: "Project UUID to make it project-specific, or null for a general team tag"
|
|
105747
|
+
}
|
|
105748
|
+
},
|
|
105749
|
+
required: ["tagId"]
|
|
105750
|
+
}
|
|
105751
|
+
},
|
|
105752
|
+
{
|
|
105753
|
+
name: "delete-tag",
|
|
105754
|
+
description: "Delete a team tag SAFELY. With mode 'delete_if_unused' (default) the tag is hard-deleted only when it is not used by any ticket/customer/project/transaction; if it is still used, the call is refused and usage counts are returned (it never strips the tag off entities). Mode 'archive' is not supported (the tags table has no archived column) \u2014 use merge-tags to fold a used tag into another, then delete the empty tag.",
|
|
105755
|
+
inputSchema: {
|
|
105756
|
+
type: "object",
|
|
105757
|
+
properties: {
|
|
105758
|
+
teamId: teamIdProp,
|
|
105759
|
+
tagId: { type: "string", description: "Tag ID (UUID) to delete" },
|
|
105760
|
+
mode: {
|
|
105761
|
+
type: "string",
|
|
105762
|
+
enum: ["delete_if_unused", "archive"],
|
|
105763
|
+
default: "delete_if_unused",
|
|
105764
|
+
description: "delete_if_unused = hard-delete only when unused (default); archive = unsupported, returns guidance"
|
|
105765
|
+
}
|
|
105766
|
+
},
|
|
105767
|
+
required: ["tagId"]
|
|
105768
|
+
}
|
|
105769
|
+
},
|
|
105770
|
+
{
|
|
105771
|
+
name: "merge-tags",
|
|
105772
|
+
description: "Merge one or more duplicate/misspelled source tags into a single target tag. Specify the target by `targetTagId` (existing) or `targetName` (reused if it exists, otherwise created as a general team tag). All ticket/customer/project/transaction relations are moved onto the target without creating duplicates (entities that already have the target keep a single tag). By default the empty source tags are deleted afterwards (set deleteSources=false to keep them). Returns moved/skipped counts per entity type. Find tag ids via get-tags.",
|
|
105773
|
+
inputSchema: {
|
|
105774
|
+
type: "object",
|
|
105775
|
+
properties: {
|
|
105776
|
+
teamId: teamIdProp,
|
|
105777
|
+
sourceTagIds: {
|
|
105778
|
+
type: "array",
|
|
105779
|
+
items: { type: "string" },
|
|
105780
|
+
description: "Tag IDs to merge away (at least one)"
|
|
105781
|
+
},
|
|
105782
|
+
targetTagId: {
|
|
105783
|
+
type: "string",
|
|
105784
|
+
description: "Existing target tag ID (takes precedence over targetName)"
|
|
105785
|
+
},
|
|
105786
|
+
targetName: {
|
|
105787
|
+
type: "string",
|
|
105788
|
+
description: "Target tag name; an existing match is reused, otherwise a general tag is created"
|
|
105789
|
+
},
|
|
105790
|
+
deleteSources: {
|
|
105791
|
+
type: "boolean",
|
|
105792
|
+
default: true,
|
|
105793
|
+
description: "Delete the now-empty source tags after merging"
|
|
105794
|
+
}
|
|
105795
|
+
},
|
|
105796
|
+
required: ["sourceTagIds"]
|
|
105797
|
+
}
|
|
105798
|
+
},
|
|
105730
105799
|
{
|
|
105731
105800
|
name: "get-calendar-items",
|
|
105732
105801
|
description: "List agenda/calendar items (deadlines, meetings, reminders, deliveries) with optional filters by date range, project, ticket, customer, assignee, type, or status. Returns items with id, title, startsAt, endsAt, dueDate, linked ticket/project/customer ids, and status.",
|
|
@@ -105935,13 +106004,19 @@ var TOOLS = [
|
|
|
105935
106004
|
},
|
|
105936
106005
|
{
|
|
105937
106006
|
name: "get-projects",
|
|
105938
|
-
description: "Get projects with optional filtering",
|
|
106007
|
+
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.",
|
|
105939
106008
|
inputSchema: {
|
|
105940
106009
|
type: "object",
|
|
105941
106010
|
properties: {
|
|
105942
106011
|
teamId: teamIdProp,
|
|
105943
106012
|
customerId: { type: "string", description: "Filter by customer ID" },
|
|
105944
106013
|
q: { type: "string", description: "Search query for project name" },
|
|
106014
|
+
status: {
|
|
106015
|
+
type: "string",
|
|
106016
|
+
enum: ["active", "archived", "all"],
|
|
106017
|
+
default: "active",
|
|
106018
|
+
description: "Archive filter: 'active' (default, hides archived), 'archived', or 'all'."
|
|
106019
|
+
},
|
|
105945
106020
|
pageSize: { type: "number", default: 20, maximum: 100 }
|
|
105946
106021
|
},
|
|
105947
106022
|
required: []
|
|
@@ -105968,7 +106043,7 @@ var TOOLS = [
|
|
|
105968
106043
|
},
|
|
105969
106044
|
{
|
|
105970
106045
|
name: "update-project",
|
|
105971
|
-
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.
|
|
106046
|
+
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.",
|
|
105972
106047
|
inputSchema: {
|
|
105973
106048
|
type: "object",
|
|
105974
106049
|
properties: {
|
|
@@ -105995,6 +106070,38 @@ var TOOLS = [
|
|
|
105995
106070
|
required: ["id"]
|
|
105996
106071
|
}
|
|
105997
106072
|
},
|
|
106073
|
+
{
|
|
106074
|
+
name: "archive-project",
|
|
106075
|
+
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.",
|
|
106076
|
+
inputSchema: {
|
|
106077
|
+
type: "object",
|
|
106078
|
+
properties: {
|
|
106079
|
+
teamId: teamIdProp,
|
|
106080
|
+
projectId: { type: "string", description: "Project ID to archive" },
|
|
106081
|
+
reason: {
|
|
106082
|
+
type: "string",
|
|
106083
|
+
description: "Optional note explaining why the project is archived"
|
|
106084
|
+
}
|
|
106085
|
+
},
|
|
106086
|
+
required: ["projectId"]
|
|
106087
|
+
}
|
|
106088
|
+
},
|
|
106089
|
+
{
|
|
106090
|
+
name: "delete-project",
|
|
106091
|
+
description: "Permanently hard-delete a project, but ONLY when it is empty (no tickets, agenda/time entries, timesheet templates, trips, or trip templates). If any such dependencies exist the delete is rejected with a dependency summary \u2014 archive-project instead. Requires team OWNER privileges and confirmEmptyOnly: true as an explicit safety interlock. Prefer archive-project unless you are certain the project should be erased.",
|
|
106092
|
+
inputSchema: {
|
|
106093
|
+
type: "object",
|
|
106094
|
+
properties: {
|
|
106095
|
+
teamId: teamIdProp,
|
|
106096
|
+
projectId: { type: "string", description: "Project ID to delete" },
|
|
106097
|
+
confirmEmptyOnly: {
|
|
106098
|
+
type: "boolean",
|
|
106099
|
+
description: "Must be true to authorise the hard delete of an empty project."
|
|
106100
|
+
}
|
|
106101
|
+
},
|
|
106102
|
+
required: ["projectId"]
|
|
106103
|
+
}
|
|
106104
|
+
},
|
|
105998
106105
|
{
|
|
105999
106106
|
name: "get-project-members",
|
|
106000
106107
|
description: "List the members explicitly assigned to a project (member_project_access) plus the full team roster with each member's effective access. Use the returned userIds with set/add/remove-project-member. Access model: owners and members with no restrictions see ALL projects; once a member is explicitly assigned to ANY project they can ONLY see their explicitly-assigned projects.",
|
|
@@ -107985,7 +108092,7 @@ async function applyHumanizer(blocks, mode) {
|
|
|
107985
108092
|
for (const change of rules.report) {
|
|
107986
108093
|
byRule.set(change.rule, (byRule.get(change.rule) ?? 0) + 1);
|
|
107987
108094
|
}
|
|
107988
|
-
const ruleSummary = [...byRule.entries()].map(([rule,
|
|
108095
|
+
const ruleSummary = [...byRule.entries()].map(([rule, count2]) => `${rule} \xD7${count2}`).join(", ");
|
|
107989
108096
|
lines.push(
|
|
107990
108097
|
`Humanizer (rules): ${rules.report.length} aanpassing(en) \u2014 ${ruleSummary}.`
|
|
107991
108098
|
);
|
|
@@ -112565,10 +112672,59 @@ The document can now be selected as a PDF attachment when sending this invoice f
|
|
|
112565
112672
|
};
|
|
112566
112673
|
}
|
|
112567
112674
|
|
|
112675
|
+
// src/tools/project-cleanup-util.ts
|
|
112676
|
+
var PROJECT_STATUS_FILTERS = [
|
|
112677
|
+
"active",
|
|
112678
|
+
"archived",
|
|
112679
|
+
"all"
|
|
112680
|
+
];
|
|
112681
|
+
var DEPENDENCY_LABELS = {
|
|
112682
|
+
tickets: "ticket(s)",
|
|
112683
|
+
timesheetEvents: "agenda/time entr(ies)",
|
|
112684
|
+
timesheetTemplates: "timesheet template(s)",
|
|
112685
|
+
trips: "trip(s)",
|
|
112686
|
+
tripTemplates: "trip template(s)"
|
|
112687
|
+
};
|
|
112688
|
+
function getProjectArchiveState(settings) {
|
|
112689
|
+
const obj = settings && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
|
112690
|
+
const archivedAt = typeof obj.archivedAt === "string" && obj.archivedAt.trim().length > 0 ? obj.archivedAt : null;
|
|
112691
|
+
const archiveReason = typeof obj.archiveReason === "string" && obj.archiveReason.trim().length > 0 ? obj.archiveReason : null;
|
|
112692
|
+
return { archived: archivedAt !== null, archivedAt, archiveReason };
|
|
112693
|
+
}
|
|
112694
|
+
function withArchiveSettings(settings, archivedAt, reason) {
|
|
112695
|
+
const base = settings && typeof settings === "object" && !Array.isArray(settings) ? { ...settings } : {};
|
|
112696
|
+
base.archivedAt = archivedAt;
|
|
112697
|
+
if (reason && reason.trim().length > 0) {
|
|
112698
|
+
base.archiveReason = reason.trim();
|
|
112699
|
+
}
|
|
112700
|
+
return base;
|
|
112701
|
+
}
|
|
112702
|
+
function totalProjectDependencies(counts) {
|
|
112703
|
+
return counts.tickets + counts.timesheetEvents + counts.timesheetTemplates + counts.trips + counts.tripTemplates;
|
|
112704
|
+
}
|
|
112705
|
+
function isProjectEmpty(counts) {
|
|
112706
|
+
return totalProjectDependencies(counts) === 0;
|
|
112707
|
+
}
|
|
112708
|
+
function formatProjectDependencies(counts) {
|
|
112709
|
+
const parts = Object.keys(DEPENDENCY_LABELS).filter((key) => counts[key] > 0).map((key) => `${counts[key]} ${DEPENDENCY_LABELS[key]}`);
|
|
112710
|
+
return parts.length > 0 ? parts.join(", ") : "no dependencies";
|
|
112711
|
+
}
|
|
112712
|
+
|
|
112568
112713
|
// src/tools/projects.ts
|
|
112569
112714
|
async function handleGetProjects(input) {
|
|
112570
112715
|
const ctx = getAuthContext();
|
|
112571
112716
|
const { customerId, q: q3, pageSize = 20 } = input;
|
|
112717
|
+
const status = input.status ?? "active";
|
|
112718
|
+
if (!PROJECT_STATUS_FILTERS.includes(status)) {
|
|
112719
|
+
return {
|
|
112720
|
+
content: [
|
|
112721
|
+
{
|
|
112722
|
+
type: "text",
|
|
112723
|
+
text: `Error: invalid status "${status}". Allowed: ${PROJECT_STATUS_FILTERS.join(", ")}.`
|
|
112724
|
+
}
|
|
112725
|
+
]
|
|
112726
|
+
};
|
|
112727
|
+
}
|
|
112572
112728
|
const resolved = await resolveTeamId(input.teamId);
|
|
112573
112729
|
if (!resolved.ok) return resolved.response;
|
|
112574
112730
|
const projectIds = await getAccessibleProjectIds(ctx.userId, resolved.teamId);
|
|
@@ -112585,25 +112741,33 @@ async function handleGetProjects(input) {
|
|
|
112585
112741
|
const filters = [inArray(schema_exports.projects.id, projectIds)];
|
|
112586
112742
|
if (customerId) filters.push(eq(schema_exports.projects.customerId, customerId));
|
|
112587
112743
|
if (q3) filters.push(ilike(schema_exports.projects.name, `%${q3}%`));
|
|
112744
|
+
if (status === "active") {
|
|
112745
|
+
filters.push(sql`${schema_exports.projects.settings} ->> 'archivedAt' IS NULL`);
|
|
112746
|
+
} else if (status === "archived") {
|
|
112747
|
+
filters.push(sql`${schema_exports.projects.settings} ->> 'archivedAt' IS NOT NULL`);
|
|
112748
|
+
}
|
|
112588
112749
|
const rows = await db.select({
|
|
112589
112750
|
id: schema_exports.projects.id,
|
|
112590
112751
|
name: schema_exports.projects.name,
|
|
112591
112752
|
description: schema_exports.projects.description,
|
|
112592
112753
|
customerId: schema_exports.projects.customerId,
|
|
112593
|
-
createdAt: schema_exports.projects.createdAt
|
|
112754
|
+
createdAt: schema_exports.projects.createdAt,
|
|
112755
|
+
settings: schema_exports.projects.settings
|
|
112594
112756
|
}).from(schema_exports.projects).where(and(...filters)).orderBy(asc(schema_exports.projects.name)).limit(Math.min(pageSize, 100));
|
|
112595
112757
|
return {
|
|
112596
112758
|
content: [
|
|
112597
112759
|
{
|
|
112598
112760
|
type: "text",
|
|
112599
|
-
text: `Found ${rows.length}
|
|
112761
|
+
text: `Found ${rows.length} project(s)${status !== "all" ? ` (status: ${status})` : ""}:
|
|
112600
112762
|
|
|
112601
|
-
${rows.map(
|
|
112602
|
-
|
|
112763
|
+
${rows.map((p3) => {
|
|
112764
|
+
const archive = getProjectArchiveState(p3.settings);
|
|
112765
|
+
return `**${p3.name}** (ID: ${p3.id})${archive.archived ? " \u2014 ARCHIVED" : ""}
|
|
112603
112766
|
${p3.description ? `Description: ${p3.description}
|
|
112604
112767
|
` : ""}Created: ${new Date(p3.createdAt).toLocaleDateString()}
|
|
112605
|
-
`
|
|
112606
|
-
|
|
112768
|
+
${archive.archived ? `Archived: ${archive.archivedAt}${archive.archiveReason ? ` (${archive.archiveReason})` : ""}
|
|
112769
|
+
` : ""}`;
|
|
112770
|
+
}).join("\n") || "No projects found."}`
|
|
112607
112771
|
}
|
|
112608
112772
|
]
|
|
112609
112773
|
};
|
|
@@ -113104,6 +113268,108 @@ async function handleRemoveProjectMember(input) {
|
|
|
113104
113268
|
}
|
|
113105
113269
|
return textResponse(text3);
|
|
113106
113270
|
}
|
|
113271
|
+
async function loadProjectForCleanup(projectId, teamId) {
|
|
113272
|
+
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
113273
|
+
const [row] = await db.select({
|
|
113274
|
+
id: schema_exports.projects.id,
|
|
113275
|
+
name: schema_exports.projects.name,
|
|
113276
|
+
teamId: schema_exports.projects.teamId,
|
|
113277
|
+
settings: schema_exports.projects.settings
|
|
113278
|
+
}).from(schema_exports.projects).where(eq(schema_exports.projects.id, projectId)).limit(1);
|
|
113279
|
+
if (!row || !row.teamId || !accessibleTeamIds.includes(row.teamId)) {
|
|
113280
|
+
return null;
|
|
113281
|
+
}
|
|
113282
|
+
return { id: row.id, name: row.name, teamId: row.teamId, settings: row.settings };
|
|
113283
|
+
}
|
|
113284
|
+
async function countProjectDependencies(projectId) {
|
|
113285
|
+
const countRows = (table) => db.select({ c: sql`count(*)::int` }).from(table).where(eq(table.projectId, projectId)).then((r6) => r6[0]?.c ?? 0);
|
|
113286
|
+
const [tickets3, timesheetEvents2, timesheetTemplates2, trips2, tripTemplates2] = await Promise.all([
|
|
113287
|
+
countRows(schema_exports.tickets),
|
|
113288
|
+
countRows(schema_exports.timesheetEvents),
|
|
113289
|
+
countRows(schema_exports.timesheetTemplates),
|
|
113290
|
+
countRows(schema_exports.trips),
|
|
113291
|
+
countRows(schema_exports.tripTemplates)
|
|
113292
|
+
]);
|
|
113293
|
+
return { tickets: tickets3, timesheetEvents: timesheetEvents2, timesheetTemplates: timesheetTemplates2, trips: trips2, tripTemplates: tripTemplates2 };
|
|
113294
|
+
}
|
|
113295
|
+
async function handleArchiveProject(input) {
|
|
113296
|
+
const { projectId, reason } = input;
|
|
113297
|
+
if (!projectId) return textResponse("Error: `projectId` is required.");
|
|
113298
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113299
|
+
if (!resolved.ok) return resolved.response;
|
|
113300
|
+
const project = await loadProjectForCleanup(projectId, resolved.teamId);
|
|
113301
|
+
if (!project) {
|
|
113302
|
+
return textResponse(
|
|
113303
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113304
|
+
);
|
|
113305
|
+
}
|
|
113306
|
+
const state2 = getProjectArchiveState(project.settings);
|
|
113307
|
+
if (state2.archived) {
|
|
113308
|
+
return textResponse(
|
|
113309
|
+
`Project "${project.name}" (${project.id}) is already archived${state2.archivedAt ? ` (since ${state2.archivedAt})` : ""}.`
|
|
113310
|
+
);
|
|
113311
|
+
}
|
|
113312
|
+
const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
113313
|
+
const nextSettings = withArchiveSettings(project.settings, archivedAt, reason);
|
|
113314
|
+
await db.update(schema_exports.projects).set({ settings: nextSettings, updatedAt: sql`now()` }).where(eq(schema_exports.projects.id, project.id));
|
|
113315
|
+
return textResponse(
|
|
113316
|
+
`\u2705 **Project archived**
|
|
113317
|
+
|
|
113318
|
+
Project: ${project.name}
|
|
113319
|
+
ID: ${project.id}
|
|
113320
|
+
Action: archived (soft, reversible)
|
|
113321
|
+
Status: archived
|
|
113322
|
+
Timestamp: ${archivedAt}
|
|
113323
|
+
${reason ? `Reason: ${reason}
|
|
113324
|
+
` : ""}
|
|
113325
|
+
Archived projects are hidden from get-projects by default (pass status: 'archived' or 'all' to see them). No tickets, hours, or other data were touched.
|
|
113326
|
+
|
|
113327
|
+
Note: the archive flag is stored in \`projects.settings.archivedAt\`; the dashboard UI does not yet read it, so the project still appears there.`
|
|
113328
|
+
);
|
|
113329
|
+
}
|
|
113330
|
+
async function handleDeleteProject(input) {
|
|
113331
|
+
const ctx = getAuthContext();
|
|
113332
|
+
const { projectId, confirmEmptyOnly } = input;
|
|
113333
|
+
if (!projectId) return textResponse("Error: `projectId` is required.");
|
|
113334
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113335
|
+
if (!resolved.ok) return resolved.response;
|
|
113336
|
+
const ownerError = await requireTeamOwner(resolved.teamId, ctx.userId);
|
|
113337
|
+
if (ownerError) return ownerError;
|
|
113338
|
+
const project = await loadProjectForCleanup(projectId, resolved.teamId);
|
|
113339
|
+
if (!project) {
|
|
113340
|
+
return textResponse(
|
|
113341
|
+
`Project ${projectId} not found, or it is not owned by this team.`
|
|
113342
|
+
);
|
|
113343
|
+
}
|
|
113344
|
+
const deps = await countProjectDependencies(project.id);
|
|
113345
|
+
const summary = formatProjectDependencies(deps);
|
|
113346
|
+
if (!isProjectEmpty(deps)) {
|
|
113347
|
+
return textResponse(
|
|
113348
|
+
`\u{1F6AB} **Delete blocked** \u2014 project "${project.name}" (${project.id}) is not empty.
|
|
113349
|
+
|
|
113350
|
+
Dependencies: ${summary}.
|
|
113351
|
+
|
|
113352
|
+
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).`
|
|
113353
|
+
);
|
|
113354
|
+
}
|
|
113355
|
+
if (confirmEmptyOnly !== true) {
|
|
113356
|
+
return textResponse(
|
|
113357
|
+
`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).`
|
|
113358
|
+
);
|
|
113359
|
+
}
|
|
113360
|
+
await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
|
|
113361
|
+
return textResponse(
|
|
113362
|
+
`\u2705 **Project deleted**
|
|
113363
|
+
|
|
113364
|
+
Project: ${project.name}
|
|
113365
|
+
ID: ${project.id}
|
|
113366
|
+
Action: hard delete (empty project)
|
|
113367
|
+
Status: deleted
|
|
113368
|
+
Timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
113369
|
+
|
|
113370
|
+
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.`
|
|
113371
|
+
);
|
|
113372
|
+
}
|
|
113107
113373
|
|
|
113108
113374
|
// src/tools/products.ts
|
|
113109
113375
|
var PRODUCT_STATUSES = ["active", "archived", "all"];
|
|
@@ -119314,11 +119580,11 @@ async function handleCreateTag(input) {
|
|
|
119314
119580
|
const resolved = await resolveTeamId(input.teamId);
|
|
119315
119581
|
if (!resolved.ok) return resolved.response;
|
|
119316
119582
|
const normalized = normalizeTagName(name21);
|
|
119317
|
-
const
|
|
119583
|
+
const scopeFilter2 = input.projectId ? eq(schema_exports.tags.projectId, input.projectId) : isNull(schema_exports.tags.projectId);
|
|
119318
119584
|
const [existing] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
|
|
119319
119585
|
and(
|
|
119320
119586
|
eq(schema_exports.tags.teamId, resolved.teamId),
|
|
119321
|
-
|
|
119587
|
+
scopeFilter2,
|
|
119322
119588
|
sql`lower(${schema_exports.tags.name}) = ${normalized}`
|
|
119323
119589
|
)
|
|
119324
119590
|
).limit(1);
|
|
@@ -119364,6 +119630,346 @@ ${created.projectId ? `Project ID: ${created.projectId}
|
|
|
119364
119630
|
};
|
|
119365
119631
|
}
|
|
119366
119632
|
|
|
119633
|
+
// src/tools/tag-merge-util.ts
|
|
119634
|
+
function planRelationMerge(sourceRows, targetEntityIds) {
|
|
119635
|
+
const targetSet = new Set(targetEntityIds);
|
|
119636
|
+
const teamByEntity = /* @__PURE__ */ new Map();
|
|
119637
|
+
for (const row of sourceRows) {
|
|
119638
|
+
if (!teamByEntity.has(row.entityId)) {
|
|
119639
|
+
teamByEntity.set(row.entityId, row.teamId);
|
|
119640
|
+
}
|
|
119641
|
+
}
|
|
119642
|
+
const toInsert = [];
|
|
119643
|
+
let skippedDuplicates = 0;
|
|
119644
|
+
for (const [entityId, teamId] of teamByEntity) {
|
|
119645
|
+
if (targetSet.has(entityId)) {
|
|
119646
|
+
skippedDuplicates += 1;
|
|
119647
|
+
} else {
|
|
119648
|
+
toInsert.push({ entityId, teamId });
|
|
119649
|
+
}
|
|
119650
|
+
}
|
|
119651
|
+
return {
|
|
119652
|
+
toInsert,
|
|
119653
|
+
skippedDuplicates,
|
|
119654
|
+
sourceEntityCount: teamByEntity.size
|
|
119655
|
+
};
|
|
119656
|
+
}
|
|
119657
|
+
function isValidTagName(name21) {
|
|
119658
|
+
return typeof name21 === "string" && name21.trim().length > 0;
|
|
119659
|
+
}
|
|
119660
|
+
function totalTagUsage(usage) {
|
|
119661
|
+
return usage.tickets + usage.customers + usage.projects + usage.transactions;
|
|
119662
|
+
}
|
|
119663
|
+
function formatTagUsage(usage) {
|
|
119664
|
+
const parts = [];
|
|
119665
|
+
if (usage.tickets) parts.push(`${usage.tickets} ticket(s)`);
|
|
119666
|
+
if (usage.customers) parts.push(`${usage.customers} customer(s)`);
|
|
119667
|
+
if (usage.projects) parts.push(`${usage.projects} project(s)`);
|
|
119668
|
+
if (usage.transactions) parts.push(`${usage.transactions} transaction(s)`);
|
|
119669
|
+
return parts.length > 0 ? parts.join(", ") : "no entities";
|
|
119670
|
+
}
|
|
119671
|
+
|
|
119672
|
+
// src/tools/tag-management.ts
|
|
119673
|
+
function textResponse4(text3) {
|
|
119674
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
119675
|
+
}
|
|
119676
|
+
var TAG_COLUMNS = {
|
|
119677
|
+
id: schema_exports.tags.id,
|
|
119678
|
+
name: schema_exports.tags.name,
|
|
119679
|
+
teamId: schema_exports.tags.teamId,
|
|
119680
|
+
projectId: schema_exports.tags.projectId,
|
|
119681
|
+
createdAt: schema_exports.tags.createdAt
|
|
119682
|
+
};
|
|
119683
|
+
function describeTag(tag) {
|
|
119684
|
+
return `**${tag.name}** (id: ${tag.id})${tag.projectId ? ` [project-specific: ${tag.projectId}]` : " [general]"}`;
|
|
119685
|
+
}
|
|
119686
|
+
async function loadTagInTeam(tagId, teamId) {
|
|
119687
|
+
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
119688
|
+
const [row] = await db.select(TAG_COLUMNS).from(schema_exports.tags).where(
|
|
119689
|
+
and(
|
|
119690
|
+
eq(schema_exports.tags.id, tagId),
|
|
119691
|
+
inArray(schema_exports.tags.teamId, accessibleTeamIds)
|
|
119692
|
+
)
|
|
119693
|
+
).limit(1);
|
|
119694
|
+
return row ?? null;
|
|
119695
|
+
}
|
|
119696
|
+
async function getTagUsage(tagId) {
|
|
119697
|
+
const [tickets3, customers2, projects2, transactions2] = await Promise.all([
|
|
119698
|
+
db.select({ value: count() }).from(schema_exports.ticketTags).where(eq(schema_exports.ticketTags.tagId, tagId)),
|
|
119699
|
+
db.select({ value: count() }).from(schema_exports.customerTags).where(eq(schema_exports.customerTags.tagId, tagId)),
|
|
119700
|
+
db.select({ value: count() }).from(schema_exports.projectTags).where(eq(schema_exports.projectTags.tagId, tagId)),
|
|
119701
|
+
db.select({ value: count() }).from(schema_exports.transactionTags).where(eq(schema_exports.transactionTags.tagId, tagId))
|
|
119702
|
+
]);
|
|
119703
|
+
return {
|
|
119704
|
+
tickets: Number(tickets3[0]?.value ?? 0),
|
|
119705
|
+
customers: Number(customers2[0]?.value ?? 0),
|
|
119706
|
+
projects: Number(projects2[0]?.value ?? 0),
|
|
119707
|
+
transactions: Number(transactions2[0]?.value ?? 0)
|
|
119708
|
+
};
|
|
119709
|
+
}
|
|
119710
|
+
function scopeFilter(projectId) {
|
|
119711
|
+
return projectId === null ? isNull(schema_exports.tags.projectId) : eq(schema_exports.tags.projectId, projectId);
|
|
119712
|
+
}
|
|
119713
|
+
async function handleUpdateTag(input) {
|
|
119714
|
+
if (!input.tagId) return textResponse4("Error: `tagId` is required.");
|
|
119715
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
119716
|
+
if (!resolved.ok) return resolved.response;
|
|
119717
|
+
const existing = await loadTagInTeam(input.tagId, resolved.teamId);
|
|
119718
|
+
if (!existing) {
|
|
119719
|
+
return textResponse4(
|
|
119720
|
+
`Tag ${input.tagId} not found, or it is not owned by this team.`
|
|
119721
|
+
);
|
|
119722
|
+
}
|
|
119723
|
+
const renaming = input.name !== void 0;
|
|
119724
|
+
const rescoping = input.projectId !== void 0;
|
|
119725
|
+
if (!renaming && !rescoping) {
|
|
119726
|
+
return textResponse4(
|
|
119727
|
+
"No changes requested. Provide `name` to rename and/or `projectId` (string, or null for a general tag) to change scope."
|
|
119728
|
+
);
|
|
119729
|
+
}
|
|
119730
|
+
if (renaming && !isValidTagName(input.name)) {
|
|
119731
|
+
return textResponse4("Error: `name` cannot be empty.");
|
|
119732
|
+
}
|
|
119733
|
+
const nextName = renaming ? input.name.trim() : existing.name;
|
|
119734
|
+
const nextProjectId = rescoping ? input.projectId ?? null : existing.projectId;
|
|
119735
|
+
const [collision] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
|
|
119736
|
+
and(
|
|
119737
|
+
eq(schema_exports.tags.teamId, existing.teamId),
|
|
119738
|
+
scopeFilter(nextProjectId),
|
|
119739
|
+
sql`lower(${schema_exports.tags.name}) = ${normalizeTagName(nextName)}`,
|
|
119740
|
+
ne(schema_exports.tags.id, existing.id)
|
|
119741
|
+
)
|
|
119742
|
+
).limit(1);
|
|
119743
|
+
if (collision) {
|
|
119744
|
+
return textResponse4(
|
|
119745
|
+
`\u274C Cannot update: another tag already uses the name "${collision.name}" (id: ${collision.id}) in this scope. Use merge-tags to combine them instead of renaming.`
|
|
119746
|
+
);
|
|
119747
|
+
}
|
|
119748
|
+
const [updated] = await db.update(schema_exports.tags).set({ name: nextName, projectId: nextProjectId }).where(eq(schema_exports.tags.id, existing.id)).returning(TAG_COLUMNS);
|
|
119749
|
+
if (!updated) return textResponse4(`Failed to update tag ${input.tagId}.`);
|
|
119750
|
+
return textResponse4(
|
|
119751
|
+
`\u2705 **Tag updated**
|
|
119752
|
+
|
|
119753
|
+
${describeTag(updated)}
|
|
119754
|
+
|
|
119755
|
+
Existing ticket/customer/project/transaction tag relations are preserved.`
|
|
119756
|
+
);
|
|
119757
|
+
}
|
|
119758
|
+
async function handleDeleteTag(input) {
|
|
119759
|
+
if (!input.tagId) return textResponse4("Error: `tagId` is required.");
|
|
119760
|
+
const mode = input.mode ?? "delete_if_unused";
|
|
119761
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
119762
|
+
if (!resolved.ok) return resolved.response;
|
|
119763
|
+
const existing = await loadTagInTeam(input.tagId, resolved.teamId);
|
|
119764
|
+
if (!existing) {
|
|
119765
|
+
return textResponse4(
|
|
119766
|
+
`Tag ${input.tagId} not found, or it is not owned by this team.`
|
|
119767
|
+
);
|
|
119768
|
+
}
|
|
119769
|
+
const usage = await getTagUsage(existing.id);
|
|
119770
|
+
const total = totalTagUsage(usage);
|
|
119771
|
+
if (mode === "archive") {
|
|
119772
|
+
return textResponse4(
|
|
119773
|
+
`\u2139\uFE0F Archiving is not supported for team tags: the \`tags\` table has no archived column. ${describeTag(existing)} is used by ${formatTagUsage(usage)}.
|
|
119774
|
+
|
|
119775
|
+
Options: use merge-tags to fold it into another tag, or delete it once it is unused (mode: delete_if_unused).`
|
|
119776
|
+
);
|
|
119777
|
+
}
|
|
119778
|
+
if (total > 0) {
|
|
119779
|
+
return textResponse4(
|
|
119780
|
+
`\u274C Refusing to delete ${describeTag(existing)}: it is still used by ${formatTagUsage(usage)}. Deleting would strip the tag off those entities.
|
|
119781
|
+
|
|
119782
|
+
Use merge-tags to move usage onto another tag first, then delete the (now-empty) tag.`
|
|
119783
|
+
);
|
|
119784
|
+
}
|
|
119785
|
+
await db.delete(schema_exports.tags).where(eq(schema_exports.tags.id, existing.id));
|
|
119786
|
+
return textResponse4(
|
|
119787
|
+
`\u2705 **Tag deleted** (was unused): ${describeTag(existing)}`
|
|
119788
|
+
);
|
|
119789
|
+
}
|
|
119790
|
+
async function resolveMergeTarget(teamId, input) {
|
|
119791
|
+
if (input.targetTagId) {
|
|
119792
|
+
const tag = await loadTagInTeam(input.targetTagId, teamId);
|
|
119793
|
+
if (!tag) {
|
|
119794
|
+
return {
|
|
119795
|
+
ok: false,
|
|
119796
|
+
response: textResponse4(
|
|
119797
|
+
`Target tag ${input.targetTagId} not found, or it is not owned by this team.`
|
|
119798
|
+
)
|
|
119799
|
+
};
|
|
119800
|
+
}
|
|
119801
|
+
return { ok: true, tag, created: false };
|
|
119802
|
+
}
|
|
119803
|
+
if (!isValidTagName(input.targetName)) {
|
|
119804
|
+
return {
|
|
119805
|
+
ok: false,
|
|
119806
|
+
response: textResponse4(
|
|
119807
|
+
"Error: provide either `targetTagId` or a non-empty `targetName`."
|
|
119808
|
+
)
|
|
119809
|
+
};
|
|
119810
|
+
}
|
|
119811
|
+
const normalized = normalizeTagName(input.targetName);
|
|
119812
|
+
const matches = await db.select(TAG_COLUMNS).from(schema_exports.tags).where(
|
|
119813
|
+
and(
|
|
119814
|
+
eq(schema_exports.tags.teamId, teamId),
|
|
119815
|
+
sql`lower(${schema_exports.tags.name}) = ${normalized}`
|
|
119816
|
+
)
|
|
119817
|
+
);
|
|
119818
|
+
if (matches.length > 0) {
|
|
119819
|
+
const general = matches.find((t8) => t8.projectId === null);
|
|
119820
|
+
return { ok: true, tag: general ?? matches[0], created: false };
|
|
119821
|
+
}
|
|
119822
|
+
const [created] = await db.insert(schema_exports.tags).values({ teamId, name: input.targetName.trim(), projectId: null }).returning(TAG_COLUMNS);
|
|
119823
|
+
if (!created) {
|
|
119824
|
+
return { ok: false, response: textResponse4("Failed to create target tag.") };
|
|
119825
|
+
}
|
|
119826
|
+
return { ok: true, tag: created, created: true };
|
|
119827
|
+
}
|
|
119828
|
+
async function handleMergeTags(input) {
|
|
119829
|
+
const rawSourceIds = [...new Set(input.sourceTagIds ?? [])].filter(Boolean);
|
|
119830
|
+
if (rawSourceIds.length === 0) {
|
|
119831
|
+
return textResponse4("Error: `sourceTagIds` must contain at least one tag id.");
|
|
119832
|
+
}
|
|
119833
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
119834
|
+
if (!resolved.ok) return resolved.response;
|
|
119835
|
+
const accessibleTeamIds = await getAccessibleTeamIds(resolved.teamId);
|
|
119836
|
+
const sourceTags = await db.select(TAG_COLUMNS).from(schema_exports.tags).where(
|
|
119837
|
+
and(
|
|
119838
|
+
inArray(schema_exports.tags.id, rawSourceIds),
|
|
119839
|
+
inArray(schema_exports.tags.teamId, accessibleTeamIds)
|
|
119840
|
+
)
|
|
119841
|
+
);
|
|
119842
|
+
const foundIds = new Set(sourceTags.map((t8) => t8.id));
|
|
119843
|
+
const missing = rawSourceIds.filter((id) => !foundIds.has(id));
|
|
119844
|
+
if (missing.length > 0) {
|
|
119845
|
+
return textResponse4(
|
|
119846
|
+
`Error: source tag(s) not found or not owned by this team: ${missing.join(", ")}.`
|
|
119847
|
+
);
|
|
119848
|
+
}
|
|
119849
|
+
const target = await resolveMergeTarget(resolved.teamId, input);
|
|
119850
|
+
if (!target.ok) return target.response;
|
|
119851
|
+
const sourcesToMerge = sourceTags.filter((t8) => t8.id !== target.tag.id);
|
|
119852
|
+
if (sourcesToMerge.length === 0) {
|
|
119853
|
+
return textResponse4(
|
|
119854
|
+
"Error: nothing to merge \u2014 the only source tag is the same as the target tag."
|
|
119855
|
+
);
|
|
119856
|
+
}
|
|
119857
|
+
const sourceIds = sourcesToMerge.map((t8) => t8.id);
|
|
119858
|
+
const targetId = target.tag.id;
|
|
119859
|
+
const deleteSources = input.deleteSources ?? true;
|
|
119860
|
+
const results = await db.transaction(async (tx) => {
|
|
119861
|
+
const ticketSrc = await tx.select({
|
|
119862
|
+
entityId: schema_exports.ticketTags.ticketId,
|
|
119863
|
+
teamId: schema_exports.ticketTags.teamId
|
|
119864
|
+
}).from(schema_exports.ticketTags).where(inArray(schema_exports.ticketTags.tagId, sourceIds));
|
|
119865
|
+
const ticketTgt = await tx.select({ entityId: schema_exports.ticketTags.ticketId }).from(schema_exports.ticketTags).where(eq(schema_exports.ticketTags.tagId, targetId));
|
|
119866
|
+
const ticketPlan = planRelationMerge(
|
|
119867
|
+
ticketSrc,
|
|
119868
|
+
ticketTgt.map((r6) => r6.entityId)
|
|
119869
|
+
);
|
|
119870
|
+
if (ticketPlan.toInsert.length > 0) {
|
|
119871
|
+
await tx.insert(schema_exports.ticketTags).values(
|
|
119872
|
+
ticketPlan.toInsert.map((r6) => ({
|
|
119873
|
+
ticketId: r6.entityId,
|
|
119874
|
+
tagId: targetId,
|
|
119875
|
+
teamId: r6.teamId
|
|
119876
|
+
}))
|
|
119877
|
+
);
|
|
119878
|
+
}
|
|
119879
|
+
await tx.delete(schema_exports.ticketTags).where(inArray(schema_exports.ticketTags.tagId, sourceIds));
|
|
119880
|
+
const customerSrc = await tx.select({
|
|
119881
|
+
entityId: schema_exports.customerTags.customerId,
|
|
119882
|
+
teamId: schema_exports.customerTags.teamId
|
|
119883
|
+
}).from(schema_exports.customerTags).where(inArray(schema_exports.customerTags.tagId, sourceIds));
|
|
119884
|
+
const customerTgt = await tx.select({ entityId: schema_exports.customerTags.customerId }).from(schema_exports.customerTags).where(eq(schema_exports.customerTags.tagId, targetId));
|
|
119885
|
+
const customerPlan = planRelationMerge(
|
|
119886
|
+
customerSrc,
|
|
119887
|
+
customerTgt.map((r6) => r6.entityId)
|
|
119888
|
+
);
|
|
119889
|
+
if (customerPlan.toInsert.length > 0) {
|
|
119890
|
+
await tx.insert(schema_exports.customerTags).values(
|
|
119891
|
+
customerPlan.toInsert.map((r6) => ({
|
|
119892
|
+
customerId: r6.entityId,
|
|
119893
|
+
tagId: targetId,
|
|
119894
|
+
teamId: r6.teamId
|
|
119895
|
+
}))
|
|
119896
|
+
);
|
|
119897
|
+
}
|
|
119898
|
+
await tx.delete(schema_exports.customerTags).where(inArray(schema_exports.customerTags.tagId, sourceIds));
|
|
119899
|
+
const projectSrc = await tx.select({
|
|
119900
|
+
entityId: schema_exports.projectTags.projectId,
|
|
119901
|
+
teamId: schema_exports.projectTags.teamId
|
|
119902
|
+
}).from(schema_exports.projectTags).where(inArray(schema_exports.projectTags.tagId, sourceIds));
|
|
119903
|
+
const projectTgt = await tx.select({ entityId: schema_exports.projectTags.projectId }).from(schema_exports.projectTags).where(eq(schema_exports.projectTags.tagId, targetId));
|
|
119904
|
+
const projectPlan = planRelationMerge(
|
|
119905
|
+
projectSrc,
|
|
119906
|
+
projectTgt.map((r6) => r6.entityId)
|
|
119907
|
+
);
|
|
119908
|
+
if (projectPlan.toInsert.length > 0) {
|
|
119909
|
+
await tx.insert(schema_exports.projectTags).values(
|
|
119910
|
+
projectPlan.toInsert.map((r6) => ({
|
|
119911
|
+
projectId: r6.entityId,
|
|
119912
|
+
tagId: targetId,
|
|
119913
|
+
teamId: r6.teamId
|
|
119914
|
+
}))
|
|
119915
|
+
);
|
|
119916
|
+
}
|
|
119917
|
+
await tx.delete(schema_exports.projectTags).where(inArray(schema_exports.projectTags.tagId, sourceIds));
|
|
119918
|
+
const txnSrc = await tx.select({
|
|
119919
|
+
entityId: schema_exports.transactionTags.transactionId,
|
|
119920
|
+
teamId: schema_exports.transactionTags.teamId
|
|
119921
|
+
}).from(schema_exports.transactionTags).where(inArray(schema_exports.transactionTags.tagId, sourceIds));
|
|
119922
|
+
const txnTgt = await tx.select({ entityId: schema_exports.transactionTags.transactionId }).from(schema_exports.transactionTags).where(eq(schema_exports.transactionTags.tagId, targetId));
|
|
119923
|
+
const txnPlan = planRelationMerge(
|
|
119924
|
+
txnSrc,
|
|
119925
|
+
txnTgt.map((r6) => r6.entityId)
|
|
119926
|
+
);
|
|
119927
|
+
if (txnPlan.toInsert.length > 0) {
|
|
119928
|
+
await tx.insert(schema_exports.transactionTags).values(
|
|
119929
|
+
txnPlan.toInsert.map((r6) => ({
|
|
119930
|
+
transactionId: r6.entityId,
|
|
119931
|
+
tagId: targetId,
|
|
119932
|
+
teamId: r6.teamId
|
|
119933
|
+
}))
|
|
119934
|
+
);
|
|
119935
|
+
}
|
|
119936
|
+
await tx.delete(schema_exports.transactionTags).where(inArray(schema_exports.transactionTags.tagId, sourceIds));
|
|
119937
|
+
if (deleteSources) {
|
|
119938
|
+
await tx.delete(schema_exports.tags).where(inArray(schema_exports.tags.id, sourceIds));
|
|
119939
|
+
}
|
|
119940
|
+
return {
|
|
119941
|
+
tickets: planToResult(ticketPlan),
|
|
119942
|
+
customers: planToResult(customerPlan),
|
|
119943
|
+
projects: planToResult(projectPlan),
|
|
119944
|
+
transactions: planToResult(txnPlan)
|
|
119945
|
+
};
|
|
119946
|
+
});
|
|
119947
|
+
const movedTotal = results.tickets.moved + results.customers.moved + results.projects.moved + results.transactions.moved;
|
|
119948
|
+
const skippedTotal = results.tickets.skipped + results.customers.skipped + results.projects.skipped + results.transactions.skipped;
|
|
119949
|
+
const line2 = (label, r6) => `- ${label}: ${r6.moved} moved, ${r6.skipped} skipped (duplicate)`;
|
|
119950
|
+
return textResponse4(
|
|
119951
|
+
`\u2705 **Tags merged** into ${describeTag(target.tag)}${target.created ? " (newly created)" : ""}
|
|
119952
|
+
|
|
119953
|
+
Sources (${sourcesToMerge.length}): ${sourcesToMerge.map((t8) => `${t8.name} (${t8.id})`).join(", ")}
|
|
119954
|
+
|
|
119955
|
+
Relations moved (duplicates skipped to keep a single tag per entity):
|
|
119956
|
+
${line2("Tickets", results.tickets)}
|
|
119957
|
+
${line2("Customers", results.customers)}
|
|
119958
|
+
${line2("Projects", results.projects)}
|
|
119959
|
+
${line2("Transactions", results.transactions)}
|
|
119960
|
+
|
|
119961
|
+
Totals: ${movedTotal} relation(s) moved, ${skippedTotal} duplicate(s) skipped.
|
|
119962
|
+
Source tags ${deleteSources ? "deleted" : "kept (now empty)"}: ${sourcesToMerge.map((t8) => t8.id).join(", ")}.`
|
|
119963
|
+
);
|
|
119964
|
+
}
|
|
119965
|
+
function planToResult(plan) {
|
|
119966
|
+
return {
|
|
119967
|
+
moved: plan.toInsert.length,
|
|
119968
|
+
skipped: plan.skippedDuplicates,
|
|
119969
|
+
total: plan.sourceEntityCount
|
|
119970
|
+
};
|
|
119971
|
+
}
|
|
119972
|
+
|
|
119367
119973
|
// src/utils/ticket-number.ts
|
|
119368
119974
|
async function isTicketNumberTaken(ticketDb, teamId, ticketNumber, excludeTicketId) {
|
|
119369
119975
|
const conditions = [
|
|
@@ -120170,6 +120776,12 @@ function createMcpServer() {
|
|
|
120170
120776
|
return await handleGetTags(asToolArgs(toolArgs));
|
|
120171
120777
|
case "create-tag":
|
|
120172
120778
|
return await handleCreateTag(asToolArgs(toolArgs));
|
|
120779
|
+
case "update-tag":
|
|
120780
|
+
return await handleUpdateTag(asToolArgs(toolArgs));
|
|
120781
|
+
case "delete-tag":
|
|
120782
|
+
return await handleDeleteTag(asToolArgs(toolArgs));
|
|
120783
|
+
case "merge-tags":
|
|
120784
|
+
return await handleMergeTags(asToolArgs(toolArgs));
|
|
120173
120785
|
case "get-calendar-items":
|
|
120174
120786
|
return await handleGetCalendarItems(
|
|
120175
120787
|
asToolArgs(toolArgs)
|
|
@@ -120208,6 +120820,14 @@ function createMcpServer() {
|
|
|
120208
120820
|
return await handleCreateProject(asToolArgs(toolArgs));
|
|
120209
120821
|
case "update-project":
|
|
120210
120822
|
return await handleUpdateProject(asToolArgs(toolArgs));
|
|
120823
|
+
case "archive-project":
|
|
120824
|
+
return await handleArchiveProject(
|
|
120825
|
+
asToolArgs(toolArgs)
|
|
120826
|
+
);
|
|
120827
|
+
case "delete-project":
|
|
120828
|
+
return await handleDeleteProject(
|
|
120829
|
+
asToolArgs(toolArgs)
|
|
120830
|
+
);
|
|
120211
120831
|
case "get-project-members":
|
|
120212
120832
|
return await handleGetProjectMembers(
|
|
120213
120833
|
asToolArgs(toolArgs)
|