@mgsoftwarebv/mcp-server-bridge 3.4.0 → 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 +982 -47
- 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.",
|
|
@@ -106277,6 +106384,106 @@ var TOOLS = [
|
|
|
106277
106384
|
required: ["documentId", "invoiceId"]
|
|
106278
106385
|
}
|
|
106279
106386
|
},
|
|
106387
|
+
{
|
|
106388
|
+
name: "get-products",
|
|
106389
|
+
description: "List catalog products used on invoices AND quotes (the shared `invoice_products` catalog). Each entry includes its ID (UUID), name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats. Editing or archiving a catalog product never changes existing invoices/quotes \u2014 those keep an immutable line-item snapshot; catalog changes only affect documents created afterwards.",
|
|
106390
|
+
inputSchema: {
|
|
106391
|
+
type: "object",
|
|
106392
|
+
properties: {
|
|
106393
|
+
teamId: teamIdProp,
|
|
106394
|
+
q: {
|
|
106395
|
+
type: "string",
|
|
106396
|
+
description: "Search query for product name/description"
|
|
106397
|
+
},
|
|
106398
|
+
status: {
|
|
106399
|
+
type: "string",
|
|
106400
|
+
enum: ["active", "archived", "all"],
|
|
106401
|
+
default: "active",
|
|
106402
|
+
description: "Filter by catalog status (archived = isActive false)"
|
|
106403
|
+
},
|
|
106404
|
+
currency: {
|
|
106405
|
+
type: "string",
|
|
106406
|
+
description: "Filter by currency code (e.g. EUR)"
|
|
106407
|
+
},
|
|
106408
|
+
pageSize: { type: "number", default: 20, maximum: 100 }
|
|
106409
|
+
},
|
|
106410
|
+
required: []
|
|
106411
|
+
}
|
|
106412
|
+
},
|
|
106413
|
+
{
|
|
106414
|
+
name: "get-product-by-id",
|
|
106415
|
+
description: "Get a single catalog product by its ID (UUID), including name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats.",
|
|
106416
|
+
inputSchema: {
|
|
106417
|
+
type: "object",
|
|
106418
|
+
properties: {
|
|
106419
|
+
teamId: teamIdProp,
|
|
106420
|
+
productId: { type: "string", description: "Product ID (UUID)" }
|
|
106421
|
+
},
|
|
106422
|
+
required: ["productId"]
|
|
106423
|
+
}
|
|
106424
|
+
},
|
|
106425
|
+
{
|
|
106426
|
+
name: "create-product",
|
|
106427
|
+
description: "Create a catalog product for use on invoices and quotes. Stored in the shared `invoice_products` catalog. This only adds a reusable catalog entry; it does not place the product on any document. Returns the created product with its ID and normalized fields.",
|
|
106428
|
+
inputSchema: {
|
|
106429
|
+
type: "object",
|
|
106430
|
+
properties: {
|
|
106431
|
+
teamId: teamIdProp,
|
|
106432
|
+
name: { type: "string", description: "Product name" },
|
|
106433
|
+
description: { type: "string" },
|
|
106434
|
+
price: {
|
|
106435
|
+
type: "number",
|
|
106436
|
+
description: "Unit price (catalog default, excl. quantity)"
|
|
106437
|
+
},
|
|
106438
|
+
currency: { type: "string", description: "Currency code (e.g. EUR)" },
|
|
106439
|
+
unit: {
|
|
106440
|
+
type: "string",
|
|
106441
|
+
description: "Unit label (e.g. hour, piece, month)"
|
|
106442
|
+
}
|
|
106443
|
+
},
|
|
106444
|
+
required: ["name"]
|
|
106445
|
+
}
|
|
106446
|
+
},
|
|
106447
|
+
{
|
|
106448
|
+
name: "update-product",
|
|
106449
|
+
description: "Update a catalog product's editable fields (name, description, price, currency, unit) or reactivate it (isActive: true). Only provided fields change. IMPORTANT: updates apply only to FUTURE invoices/quotes. Existing/sent/accepted/paid documents keep their immutable line-item snapshot and are never mutated. Find the product id via get-products.",
|
|
106450
|
+
inputSchema: {
|
|
106451
|
+
type: "object",
|
|
106452
|
+
properties: {
|
|
106453
|
+
teamId: teamIdProp,
|
|
106454
|
+
productId: { type: "string", description: "Product ID (UUID)" },
|
|
106455
|
+
name: { type: "string" },
|
|
106456
|
+
description: { type: ["string", "null"] },
|
|
106457
|
+
price: {
|
|
106458
|
+
type: ["number", "null"],
|
|
106459
|
+
description: "Unit price (catalog default)"
|
|
106460
|
+
},
|
|
106461
|
+
currency: { type: ["string", "null"] },
|
|
106462
|
+
unit: { type: ["string", "null"] },
|
|
106463
|
+
isActive: {
|
|
106464
|
+
type: "boolean",
|
|
106465
|
+
description: "Set true to reactivate an archived product"
|
|
106466
|
+
}
|
|
106467
|
+
},
|
|
106468
|
+
required: ["productId"]
|
|
106469
|
+
}
|
|
106470
|
+
},
|
|
106471
|
+
{
|
|
106472
|
+
name: "archive-product",
|
|
106473
|
+
description: "Archive (soft-disable) a catalog product so it no longer appears in invoice/quote product pickers. Preferred over deletion: products referenced historically stay safe because invoices/quotes hold their own line-item snapshots. Reactivate later via update-product (isActive: true).",
|
|
106474
|
+
inputSchema: {
|
|
106475
|
+
type: "object",
|
|
106476
|
+
properties: {
|
|
106477
|
+
teamId: teamIdProp,
|
|
106478
|
+
productId: { type: "string", description: "Product ID (UUID)" },
|
|
106479
|
+
reason: {
|
|
106480
|
+
type: "string",
|
|
106481
|
+
description: "Optional note about why it was archived (not persisted)"
|
|
106482
|
+
}
|
|
106483
|
+
},
|
|
106484
|
+
required: ["productId"]
|
|
106485
|
+
}
|
|
106486
|
+
},
|
|
106280
106487
|
{
|
|
106281
106488
|
name: "log-hours",
|
|
106282
106489
|
description: "Analyze current chat conversation and log hours as draft tracker entry. AI analyzes chat context to estimate hours as a senior developer would (without AI assistance). Cursor AI matches workspace name to correct project from list (optional).",
|
|
@@ -107885,7 +108092,7 @@ async function applyHumanizer(blocks, mode) {
|
|
|
107885
108092
|
for (const change of rules.report) {
|
|
107886
108093
|
byRule.set(change.rule, (byRule.get(change.rule) ?? 0) + 1);
|
|
107887
108094
|
}
|
|
107888
|
-
const ruleSummary = [...byRule.entries()].map(([rule,
|
|
108095
|
+
const ruleSummary = [...byRule.entries()].map(([rule, count2]) => `${rule} \xD7${count2}`).join(", ");
|
|
107889
108096
|
lines.push(
|
|
107890
108097
|
`Humanizer (rules): ${rules.report.length} aanpassing(en) \u2014 ${ruleSummary}.`
|
|
107891
108098
|
);
|
|
@@ -112465,10 +112672,59 @@ The document can now be selected as a PDF attachment when sending this invoice f
|
|
|
112465
112672
|
};
|
|
112466
112673
|
}
|
|
112467
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
|
+
|
|
112468
112713
|
// src/tools/projects.ts
|
|
112469
112714
|
async function handleGetProjects(input) {
|
|
112470
112715
|
const ctx = getAuthContext();
|
|
112471
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
|
+
}
|
|
112472
112728
|
const resolved = await resolveTeamId(input.teamId);
|
|
112473
112729
|
if (!resolved.ok) return resolved.response;
|
|
112474
112730
|
const projectIds = await getAccessibleProjectIds(ctx.userId, resolved.teamId);
|
|
@@ -112485,25 +112741,33 @@ async function handleGetProjects(input) {
|
|
|
112485
112741
|
const filters = [inArray(schema_exports.projects.id, projectIds)];
|
|
112486
112742
|
if (customerId) filters.push(eq(schema_exports.projects.customerId, customerId));
|
|
112487
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
|
+
}
|
|
112488
112749
|
const rows = await db.select({
|
|
112489
112750
|
id: schema_exports.projects.id,
|
|
112490
112751
|
name: schema_exports.projects.name,
|
|
112491
112752
|
description: schema_exports.projects.description,
|
|
112492
112753
|
customerId: schema_exports.projects.customerId,
|
|
112493
|
-
createdAt: schema_exports.projects.createdAt
|
|
112754
|
+
createdAt: schema_exports.projects.createdAt,
|
|
112755
|
+
settings: schema_exports.projects.settings
|
|
112494
112756
|
}).from(schema_exports.projects).where(and(...filters)).orderBy(asc(schema_exports.projects.name)).limit(Math.min(pageSize, 100));
|
|
112495
112757
|
return {
|
|
112496
112758
|
content: [
|
|
112497
112759
|
{
|
|
112498
112760
|
type: "text",
|
|
112499
|
-
text: `Found ${rows.length}
|
|
112761
|
+
text: `Found ${rows.length} project(s)${status !== "all" ? ` (status: ${status})` : ""}:
|
|
112500
112762
|
|
|
112501
|
-
${rows.map(
|
|
112502
|
-
|
|
112763
|
+
${rows.map((p3) => {
|
|
112764
|
+
const archive = getProjectArchiveState(p3.settings);
|
|
112765
|
+
return `**${p3.name}** (ID: ${p3.id})${archive.archived ? " \u2014 ARCHIVED" : ""}
|
|
112503
112766
|
${p3.description ? `Description: ${p3.description}
|
|
112504
112767
|
` : ""}Created: ${new Date(p3.createdAt).toLocaleDateString()}
|
|
112505
|
-
`
|
|
112506
|
-
|
|
112768
|
+
${archive.archived ? `Archived: ${archive.archivedAt}${archive.archiveReason ? ` (${archive.archiveReason})` : ""}
|
|
112769
|
+
` : ""}`;
|
|
112770
|
+
}).join("\n") || "No projects found."}`
|
|
112507
112771
|
}
|
|
112508
112772
|
]
|
|
112509
112773
|
};
|
|
@@ -113004,6 +113268,305 @@ async function handleRemoveProjectMember(input) {
|
|
|
113004
113268
|
}
|
|
113005
113269
|
return textResponse(text3);
|
|
113006
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
|
+
}
|
|
113373
|
+
|
|
113374
|
+
// src/tools/products.ts
|
|
113375
|
+
var PRODUCT_STATUSES = ["active", "archived", "all"];
|
|
113376
|
+
var PRODUCT_COLUMNS = {
|
|
113377
|
+
id: schema_exports.invoiceProducts.id,
|
|
113378
|
+
teamId: schema_exports.invoiceProducts.teamId,
|
|
113379
|
+
name: schema_exports.invoiceProducts.name,
|
|
113380
|
+
description: schema_exports.invoiceProducts.description,
|
|
113381
|
+
price: schema_exports.invoiceProducts.price,
|
|
113382
|
+
currency: schema_exports.invoiceProducts.currency,
|
|
113383
|
+
unit: schema_exports.invoiceProducts.unit,
|
|
113384
|
+
isConfigurable: schema_exports.invoiceProducts.isConfigurable,
|
|
113385
|
+
isActive: schema_exports.invoiceProducts.isActive,
|
|
113386
|
+
usageCount: schema_exports.invoiceProducts.usageCount,
|
|
113387
|
+
lastUsedAt: schema_exports.invoiceProducts.lastUsedAt,
|
|
113388
|
+
createdAt: schema_exports.invoiceProducts.createdAt,
|
|
113389
|
+
updatedAt: schema_exports.invoiceProducts.updatedAt
|
|
113390
|
+
};
|
|
113391
|
+
function textResponse2(text3) {
|
|
113392
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
113393
|
+
}
|
|
113394
|
+
function formatPrice(p3) {
|
|
113395
|
+
if (p3.price == null) return "(no price)";
|
|
113396
|
+
return `${p3.price}${p3.currency ? ` ${p3.currency}` : ""}${p3.unit ? ` / ${p3.unit}` : ""}`;
|
|
113397
|
+
}
|
|
113398
|
+
function formatProduct(p3) {
|
|
113399
|
+
const flags = [p3.isActive ? "active" : "archived"];
|
|
113400
|
+
if (p3.isConfigurable) flags.push("configurable");
|
|
113401
|
+
return `**${p3.name}** (${flags.join(", ")})
|
|
113402
|
+
ID: ${p3.id}
|
|
113403
|
+
Price: ${formatPrice(p3)}
|
|
113404
|
+
${p3.description ? `Description: ${p3.description}
|
|
113405
|
+
` : ""}Used: ${p3.usageCount}x${p3.lastUsedAt ? ` (last ${new Date(p3.lastUsedAt).toLocaleDateString()})` : ""}
|
|
113406
|
+
`;
|
|
113407
|
+
}
|
|
113408
|
+
async function handleGetProducts(input) {
|
|
113409
|
+
const { q: q3, currency, pageSize = 20 } = input;
|
|
113410
|
+
const status = input.status ?? "active";
|
|
113411
|
+
if (!PRODUCT_STATUSES.includes(status)) {
|
|
113412
|
+
return textResponse2(
|
|
113413
|
+
`Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
|
|
113414
|
+
);
|
|
113415
|
+
}
|
|
113416
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
113417
|
+
if (!scope.ok) return scope.response;
|
|
113418
|
+
if (scope.teamIds.length === 0) {
|
|
113419
|
+
return textResponse2("No accessible teams found.");
|
|
113420
|
+
}
|
|
113421
|
+
const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
|
|
113422
|
+
if (status === "active") {
|
|
113423
|
+
filters.push(eq(schema_exports.invoiceProducts.isActive, true));
|
|
113424
|
+
} else if (status === "archived") {
|
|
113425
|
+
filters.push(eq(schema_exports.invoiceProducts.isActive, false));
|
|
113426
|
+
}
|
|
113427
|
+
if (currency) filters.push(eq(schema_exports.invoiceProducts.currency, currency));
|
|
113428
|
+
if (q3) {
|
|
113429
|
+
filters.push(
|
|
113430
|
+
or(
|
|
113431
|
+
sql`${schema_exports.invoiceProducts.fts} @@ plainto_tsquery('english', ${q3})`,
|
|
113432
|
+
ilike(schema_exports.invoiceProducts.name, `%${q3}%`)
|
|
113433
|
+
)
|
|
113434
|
+
);
|
|
113435
|
+
}
|
|
113436
|
+
const rows = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(and(...filters)).orderBy(
|
|
113437
|
+
desc(schema_exports.invoiceProducts.usageCount),
|
|
113438
|
+
desc(schema_exports.invoiceProducts.lastUsedAt),
|
|
113439
|
+
asc(schema_exports.invoiceProducts.name)
|
|
113440
|
+
).limit(Math.min(pageSize, 100));
|
|
113441
|
+
if (rows.length === 0) {
|
|
113442
|
+
return textResponse2(
|
|
113443
|
+
`No products found${status !== "all" ? ` (status: ${status})` : ""}.`
|
|
113444
|
+
);
|
|
113445
|
+
}
|
|
113446
|
+
return textResponse2(
|
|
113447
|
+
`Found ${rows.length} product(s):
|
|
113448
|
+
|
|
113449
|
+
${rows.map(formatProduct).join("\n")}`
|
|
113450
|
+
);
|
|
113451
|
+
}
|
|
113452
|
+
async function handleGetProductById(input) {
|
|
113453
|
+
const { productId } = input;
|
|
113454
|
+
if (!productId) return textResponse2("Error: `productId` is required.");
|
|
113455
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
113456
|
+
if (!scope.ok) return scope.response;
|
|
113457
|
+
if (scope.teamIds.length === 0) {
|
|
113458
|
+
return textResponse2("No accessible teams found.");
|
|
113459
|
+
}
|
|
113460
|
+
const [row] = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(
|
|
113461
|
+
and(
|
|
113462
|
+
eq(schema_exports.invoiceProducts.id, productId),
|
|
113463
|
+
inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)
|
|
113464
|
+
)
|
|
113465
|
+
).limit(1);
|
|
113466
|
+
if (!row) {
|
|
113467
|
+
return textResponse2(
|
|
113468
|
+
`Product ${productId} not found or you don't have access to it.`
|
|
113469
|
+
);
|
|
113470
|
+
}
|
|
113471
|
+
return textResponse2(formatProduct(row));
|
|
113472
|
+
}
|
|
113473
|
+
async function loadProductInTeam(productId, teamId) {
|
|
113474
|
+
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
113475
|
+
const [row] = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(
|
|
113476
|
+
and(
|
|
113477
|
+
eq(schema_exports.invoiceProducts.id, productId),
|
|
113478
|
+
inArray(schema_exports.invoiceProducts.teamId, accessibleTeamIds)
|
|
113479
|
+
)
|
|
113480
|
+
).limit(1);
|
|
113481
|
+
return row ?? null;
|
|
113482
|
+
}
|
|
113483
|
+
async function handleCreateProduct(input) {
|
|
113484
|
+
const { name: name21, description, price, currency, unit } = input;
|
|
113485
|
+
if (!name21 || name21.trim().length === 0) {
|
|
113486
|
+
return textResponse2("Error: `name` is required.");
|
|
113487
|
+
}
|
|
113488
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113489
|
+
if (!resolved.ok) return resolved.response;
|
|
113490
|
+
const [created] = await db.insert(schema_exports.invoiceProducts).values({
|
|
113491
|
+
teamId: resolved.teamId,
|
|
113492
|
+
name: name21.trim(),
|
|
113493
|
+
description: description ?? null,
|
|
113494
|
+
price: price ?? null,
|
|
113495
|
+
currency: currency ?? null,
|
|
113496
|
+
unit: unit ?? null,
|
|
113497
|
+
isActive: true,
|
|
113498
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
113499
|
+
}).returning(PRODUCT_COLUMNS);
|
|
113500
|
+
if (!created) return textResponse2("Failed to create product.");
|
|
113501
|
+
return textResponse2(
|
|
113502
|
+
`\u2705 **Product created**
|
|
113503
|
+
|
|
113504
|
+
${formatProduct(created)}`
|
|
113505
|
+
);
|
|
113506
|
+
}
|
|
113507
|
+
async function handleUpdateProduct(input) {
|
|
113508
|
+
const { productId } = input;
|
|
113509
|
+
if (!productId) return textResponse2("Error: `productId` is required.");
|
|
113510
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113511
|
+
if (!resolved.ok) return resolved.response;
|
|
113512
|
+
const existing = await loadProductInTeam(productId, resolved.teamId);
|
|
113513
|
+
if (!existing) {
|
|
113514
|
+
return textResponse2(
|
|
113515
|
+
`Product ${productId} not found, or it is not owned by this team.`
|
|
113516
|
+
);
|
|
113517
|
+
}
|
|
113518
|
+
const updates = {};
|
|
113519
|
+
if (input.name !== void 0) {
|
|
113520
|
+
if (!input.name || input.name.trim().length === 0) {
|
|
113521
|
+
return textResponse2("Error: `name` cannot be empty.");
|
|
113522
|
+
}
|
|
113523
|
+
updates.name = input.name.trim();
|
|
113524
|
+
}
|
|
113525
|
+
if (input.description !== void 0) updates.description = input.description;
|
|
113526
|
+
if (input.price !== void 0) updates.price = input.price;
|
|
113527
|
+
if (input.currency !== void 0) updates.currency = input.currency;
|
|
113528
|
+
if (input.unit !== void 0) updates.unit = input.unit;
|
|
113529
|
+
if (input.isActive !== void 0) updates.isActive = input.isActive;
|
|
113530
|
+
if (Object.keys(updates).length === 0) {
|
|
113531
|
+
return textResponse2(
|
|
113532
|
+
"No fields to update. Provide at least one of: name, description, price, currency, unit, isActive."
|
|
113533
|
+
);
|
|
113534
|
+
}
|
|
113535
|
+
updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
113536
|
+
const [updated] = await db.update(schema_exports.invoiceProducts).set(updates).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS);
|
|
113537
|
+
if (!updated) return textResponse2(`Failed to update product ${productId}.`);
|
|
113538
|
+
return textResponse2(
|
|
113539
|
+
`\u2705 **Product updated**
|
|
113540
|
+
|
|
113541
|
+
${formatProduct(updated)}
|
|
113542
|
+
Note: this only affects future invoices/quotes. Existing documents keep their line-item snapshots.`
|
|
113543
|
+
);
|
|
113544
|
+
}
|
|
113545
|
+
async function handleArchiveProduct(input) {
|
|
113546
|
+
const { productId, reason } = input;
|
|
113547
|
+
if (!productId) return textResponse2("Error: `productId` is required.");
|
|
113548
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
113549
|
+
if (!resolved.ok) return resolved.response;
|
|
113550
|
+
const existing = await loadProductInTeam(productId, resolved.teamId);
|
|
113551
|
+
if (!existing) {
|
|
113552
|
+
return textResponse2(
|
|
113553
|
+
`Product ${productId} not found, or it is not owned by this team.`
|
|
113554
|
+
);
|
|
113555
|
+
}
|
|
113556
|
+
if (!existing.isActive) {
|
|
113557
|
+
return textResponse2(
|
|
113558
|
+
`Product "${existing.name}" (${existing.id}) is already archived.`
|
|
113559
|
+
);
|
|
113560
|
+
}
|
|
113561
|
+
const [archived] = await db.update(schema_exports.invoiceProducts).set({ isActive: false, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS);
|
|
113562
|
+
if (!archived) return textResponse2(`Failed to archive product ${productId}.`);
|
|
113563
|
+
return textResponse2(
|
|
113564
|
+
`\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
|
|
113565
|
+
|
|
113566
|
+
${formatProduct(archived)}${reason ? `Reason: ${reason}
|
|
113567
|
+
` : ""}Reactivate it with update-product (isActive: true).`
|
|
113568
|
+
);
|
|
113569
|
+
}
|
|
113007
113570
|
|
|
113008
113571
|
// src/tools/teams.ts
|
|
113009
113572
|
async function handleGetTeams() {
|
|
@@ -118496,7 +119059,7 @@ var EXT_MIME = {
|
|
|
118496
119059
|
ppt: "application/vnd.ms-powerpoint",
|
|
118497
119060
|
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
118498
119061
|
};
|
|
118499
|
-
function
|
|
119062
|
+
function textResponse3(text3) {
|
|
118500
119063
|
return { content: [{ type: "text", text: text3 }] };
|
|
118501
119064
|
}
|
|
118502
119065
|
function mimeFromName(name21) {
|
|
@@ -118577,12 +119140,12 @@ async function handleUploadTicketAttachment(input) {
|
|
|
118577
119140
|
(v2) => typeof v2 === "string" && v2.trim().length > 0
|
|
118578
119141
|
);
|
|
118579
119142
|
if (sources.length === 0) {
|
|
118580
|
-
return
|
|
119143
|
+
return textResponse3(
|
|
118581
119144
|
"Provide exactly one source: filePath (absolute local path), imageUrl, or base64Data."
|
|
118582
119145
|
);
|
|
118583
119146
|
}
|
|
118584
119147
|
if (sources.length > 1) {
|
|
118585
|
-
return
|
|
119148
|
+
return textResponse3(
|
|
118586
119149
|
"Provide only one source (filePath, imageUrl, or base64Data), not several."
|
|
118587
119150
|
);
|
|
118588
119151
|
}
|
|
@@ -118602,7 +119165,7 @@ async function handleUploadTicketAttachment(input) {
|
|
|
118602
119165
|
} else if (input.imageUrl) {
|
|
118603
119166
|
const res = await fetch(input.imageUrl);
|
|
118604
119167
|
if (!res.ok) {
|
|
118605
|
-
return
|
|
119168
|
+
return textResponse3(
|
|
118606
119169
|
`Could not download from URL: HTTP ${res.status}.`
|
|
118607
119170
|
);
|
|
118608
119171
|
}
|
|
@@ -118630,22 +119193,22 @@ async function handleUploadTicketAttachment(input) {
|
|
|
118630
119193
|
}
|
|
118631
119194
|
}
|
|
118632
119195
|
} catch (error49) {
|
|
118633
|
-
return
|
|
119196
|
+
return textResponse3(
|
|
118634
119197
|
`Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
118635
119198
|
);
|
|
118636
119199
|
}
|
|
118637
119200
|
if (buffer2.byteLength === 0) {
|
|
118638
|
-
return
|
|
119201
|
+
return textResponse3("The file is empty (0 bytes); nothing to upload.");
|
|
118639
119202
|
}
|
|
118640
119203
|
if (buffer2.byteLength > MAX_FILE_SIZE) {
|
|
118641
|
-
return
|
|
119204
|
+
return textResponse3(
|
|
118642
119205
|
`File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
|
|
118643
119206
|
1
|
|
118644
119207
|
)} MB). Max: 25 MB.`
|
|
118645
119208
|
);
|
|
118646
119209
|
}
|
|
118647
119210
|
if (!ALLOWED_MIME_TYPES.has(mimeType)) {
|
|
118648
|
-
return
|
|
119211
|
+
return textResponse3(
|
|
118649
119212
|
`Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
|
|
118650
119213
|
);
|
|
118651
119214
|
}
|
|
@@ -118658,7 +119221,7 @@ async function handleUploadTicketAttachment(input) {
|
|
|
118658
119221
|
options: { contentType: mimeType, upsert: true }
|
|
118659
119222
|
});
|
|
118660
119223
|
} catch (error49) {
|
|
118661
|
-
return
|
|
119224
|
+
return textResponse3(
|
|
118662
119225
|
`Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
118663
119226
|
);
|
|
118664
119227
|
}
|
|
@@ -118681,7 +119244,7 @@ async function handleUploadTicketAttachment(input) {
|
|
|
118681
119244
|
url3 = signed.url;
|
|
118682
119245
|
} catch {
|
|
118683
119246
|
}
|
|
118684
|
-
return
|
|
119247
|
+
return textResponse3(
|
|
118685
119248
|
`\u{1F4CE} **Attached to ${ticket.ticketNumber}**
|
|
118686
119249
|
File: ${fileName}
|
|
118687
119250
|
Type: ${mimeType}
|
|
@@ -119017,11 +119580,11 @@ async function handleCreateTag(input) {
|
|
|
119017
119580
|
const resolved = await resolveTeamId(input.teamId);
|
|
119018
119581
|
if (!resolved.ok) return resolved.response;
|
|
119019
119582
|
const normalized = normalizeTagName(name21);
|
|
119020
|
-
const
|
|
119583
|
+
const scopeFilter2 = input.projectId ? eq(schema_exports.tags.projectId, input.projectId) : isNull(schema_exports.tags.projectId);
|
|
119021
119584
|
const [existing] = await db.select({ id: schema_exports.tags.id, name: schema_exports.tags.name }).from(schema_exports.tags).where(
|
|
119022
119585
|
and(
|
|
119023
119586
|
eq(schema_exports.tags.teamId, resolved.teamId),
|
|
119024
|
-
|
|
119587
|
+
scopeFilter2,
|
|
119025
119588
|
sql`lower(${schema_exports.tags.name}) = ${normalized}`
|
|
119026
119589
|
)
|
|
119027
119590
|
).limit(1);
|
|
@@ -119067,6 +119630,346 @@ ${created.projectId ? `Project ID: ${created.projectId}
|
|
|
119067
119630
|
};
|
|
119068
119631
|
}
|
|
119069
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
|
+
|
|
119070
119973
|
// src/utils/ticket-number.ts
|
|
119071
119974
|
async function isTicketNumberTaken(ticketDb, teamId, ticketNumber, excludeTicketId) {
|
|
119072
119975
|
const conditions = [
|
|
@@ -119825,7 +120728,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
|
|
|
119825
120728
|
}
|
|
119826
120729
|
|
|
119827
120730
|
// src/server.ts
|
|
119828
|
-
var SERVER_VERSION = "3.
|
|
120731
|
+
var SERVER_VERSION = "3.5.0";
|
|
119829
120732
|
function createMcpServer() {
|
|
119830
120733
|
const server = new Server(
|
|
119831
120734
|
{
|
|
@@ -119873,6 +120776,12 @@ function createMcpServer() {
|
|
|
119873
120776
|
return await handleGetTags(asToolArgs(toolArgs));
|
|
119874
120777
|
case "create-tag":
|
|
119875
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));
|
|
119876
120785
|
case "get-calendar-items":
|
|
119877
120786
|
return await handleGetCalendarItems(
|
|
119878
120787
|
asToolArgs(toolArgs)
|
|
@@ -119911,6 +120820,14 @@ function createMcpServer() {
|
|
|
119911
120820
|
return await handleCreateProject(asToolArgs(toolArgs));
|
|
119912
120821
|
case "update-project":
|
|
119913
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
|
+
);
|
|
119914
120831
|
case "get-project-members":
|
|
119915
120832
|
return await handleGetProjectMembers(
|
|
119916
120833
|
asToolArgs(toolArgs)
|
|
@@ -119949,6 +120866,24 @@ function createMcpServer() {
|
|
|
119949
120866
|
return await handleLinkDocumentToInvoice(
|
|
119950
120867
|
asToolArgs(toolArgs)
|
|
119951
120868
|
);
|
|
120869
|
+
case "get-products":
|
|
120870
|
+
return await handleGetProducts(asToolArgs(toolArgs));
|
|
120871
|
+
case "get-product-by-id":
|
|
120872
|
+
return await handleGetProductById(
|
|
120873
|
+
asToolArgs(toolArgs)
|
|
120874
|
+
);
|
|
120875
|
+
case "create-product":
|
|
120876
|
+
return await handleCreateProduct(
|
|
120877
|
+
asToolArgs(toolArgs)
|
|
120878
|
+
);
|
|
120879
|
+
case "update-product":
|
|
120880
|
+
return await handleUpdateProduct(
|
|
120881
|
+
asToolArgs(toolArgs)
|
|
120882
|
+
);
|
|
120883
|
+
case "archive-product":
|
|
120884
|
+
return await handleArchiveProduct(
|
|
120885
|
+
asToolArgs(toolArgs)
|
|
120886
|
+
);
|
|
119952
120887
|
case "log-hours":
|
|
119953
120888
|
return await handleLogHours(asToolArgs(toolArgs));
|
|
119954
120889
|
case "get-github-file":
|