@mgsoftwarebv/mcp-server-bridge 3.5.12 → 3.5.14
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/README.md +2 -0
- package/dist/index.js +468 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,8 @@ Optional PDF compilation: pass `compilePdf: true` to trigger the `compile-docume
|
|
|
130
130
|
- **log-hours** — log work hours as a draft agenda event (see the `/hours` Cursor command)
|
|
131
131
|
- **create-time-entries** — create one or more individual dated hour lines (one timesheet event per date), single or bulk via `entries[]`. Top-level `projectId`/`ticketId`/`userId`/`description`/`billable`/`status` are shared defaults each entry can override. Skips duplicates (same user/team/date/project/description) unless `allowDuplicate`, and reports created/skipped/errored lines plus `totalHours`. Use this instead of `log-hours` when you have separate dated lines (e.g. "8h on 19/6, 8h on 16/6").
|
|
132
132
|
- **get-time-entries** — read and total tracked time entries (urenregistratie), filterable by period, user, project, customer, ticket, status and type; returns accurate aggregate totals (total/billable/non-billable/invoiced hours) plus an optional grouped breakdown (day/project/customer/user/ticket). Answers questions like "how many hours did I work this month for customer X?"
|
|
133
|
+
- **delete-time-entries** — soft-delete one or more existing hour lines by `id`/`ids[]` (e.g. to drop a wrongly-logged lump entry before re-creating dated lines). Only draft, non-invoiced entries are removed unless `allowInvoicedOverride`; confirmed/invoiced ones are skipped. Reports deleted/skipped/errored ids plus `deletedHours`.
|
|
134
|
+
- **update-time-entry** — correct a single hour line by `id`: change `date`/`hours` (re-times the line), `description`, `projectId`, `ticketId`, `status` or `billable`. On confirmed/invoiced entries the financial/time fields are locked unless `allowInvoicedOverride`; description and project/ticket links stay editable.
|
|
133
135
|
|
|
134
136
|
### GitHub Code Exploration
|
|
135
137
|
- **get-github-file** — read one file from the GitHub repo linked to a project
|
package/dist/index.js
CHANGED
|
@@ -106586,6 +106586,24 @@ var TOOLS = [
|
|
|
106586
106586
|
required: ["id"]
|
|
106587
106587
|
}
|
|
106588
106588
|
},
|
|
106589
|
+
{
|
|
106590
|
+
name: "get-document-link",
|
|
106591
|
+
description: "Get a safe, usable link for a document (use after create-document to share it). Never leaks an auth/session URL. Choose `linkType`:\n- dashboard (default): internal editor deep link for logged-in Refront users, plus the current PDF status and a public link when the document is already shared publicly.\n- pdf_signed (alias: pdf): a temporary signed PDF download URL when a compiled PDF exists; if it is still compiling it returns pdfStatus 'pending' with pdfUrl null and triggers a (re)compile instead of failing.\n- public_share: enables a public customer share (token + visibility=public, recompiling only when no fresh PDF exists) and returns the /p/<token> link. Only use when the document may be shared publicly. Revoke later from the dashboard.\nReturns a JSON object: { documentId, title, status, dashboardUrl, pdfStatus (ready|stale|pending|unavailable), pdfUrl, publicShareUrl, expiresAt }. Respects team/provider access; draft/private documents never expose a public share URL unless they are actually shared.",
|
|
106592
|
+
inputSchema: {
|
|
106593
|
+
type: "object",
|
|
106594
|
+
properties: {
|
|
106595
|
+
teamId: teamIdProp,
|
|
106596
|
+
documentId: { type: "string", description: "Document ID (UUID)" },
|
|
106597
|
+
linkType: {
|
|
106598
|
+
type: "string",
|
|
106599
|
+
enum: ["dashboard", "pdf_signed", "pdf", "public_share"],
|
|
106600
|
+
default: "dashboard",
|
|
106601
|
+
description: "dashboard (default) = editor URL + PDF status; pdf_signed (alias pdf) = temporary signed PDF download URL; public_share = enable + return the public /p/<token> link."
|
|
106602
|
+
}
|
|
106603
|
+
},
|
|
106604
|
+
required: ["documentId"]
|
|
106605
|
+
}
|
|
106606
|
+
},
|
|
106589
106607
|
{
|
|
106590
106608
|
name: "create-document",
|
|
106591
106609
|
description: 'Create a document (proposal, report, deliverables/opleverdocument, ...) from an array of content blocks. Use type \'deliverables\' for a document describing what was delivered for a customer; optionally pass `invoiceId` to link it to an invoice so it can be sent along as a PDF attachment. Each block is `{ id?, type, data }` (ids are auto-generated when omitted). Supported block types and their `data` shapes:\n- cover: { title, subtitle?, showLogo: bool, showDate: bool, clientName?, clientCompany?, date? }\n- toc: { title, maxDepth: 1-6 }\n- heading: { text, level: 1|2|3, numbered: bool }\n- text: { content: TipTapDoc | plain string (auto-converted; supports paragraphs and "- " bullet lists) }\n- callout: { variant: "info"|"warning"|"tip"|"important", title?, content: TipTapDoc | string }\n- table: { title?, headers: string[], rows: string[][] }\n- chart: { title?, chartType: "bar"|"line"|"pie", points: [{label, value}], seriesName?, series?: [{name, color?, points: [{label, value}]}], unit? ("\u20AC"|"%"|...), showValues: bool, color? }. Multi-series (bar/line only): extra series values are matched to the primary points by index; a legend is rendered automatically.\n- pricing: { title?, items: [{description, quantity?, unit?, price}], currency: "EUR", showTotal: bool, vatRate?, includeVat: bool }\n- timeline: { title?, phases: [{name, description?, duration, deliverables?: string[]}] }\n- two_column: { left: TipTapDoc|string, right: TipTapDoc|string, ratio?: "50_50"|"60_40"|"40_60"|"70_30"|"30_70" }\n- divider: { style: "page_break"|"line"|"space" }\n- signature: { title?, description?, signers: [{id, label, name?, company?, role?}] }\nAll text content runs through a humanizer that strips AI-sounding patterns (em-dashes, clich\xE9s, filler phrases); control it with `humanize`. The result includes ready-to-use links (internal dashboard editor URL + PDF status). Use get-document-link afterwards for a PDF download link or to publish a public customer share link.',
|
|
@@ -107382,6 +107400,80 @@ var TOOLS = [
|
|
|
107382
107400
|
required: ["entries"]
|
|
107383
107401
|
}
|
|
107384
107402
|
},
|
|
107403
|
+
{
|
|
107404
|
+
name: "delete-time-entries",
|
|
107405
|
+
description: "Delete one or more existing tracked time entries (urenregels / timesheet events) by id \u2014 e.g. to remove a wrongly-logged lump entry before re-creating it as separate dated lines with create-time-entries. Soft-deletes so the lines disappear from the agenda / urenoverzicht. Pass a single `id` or a list of `ids` (max 100). Safety: only draft, non-invoiced entries are deleted by default; confirmed or invoiced entries are skipped unless `allowInvoicedOverride` is true. Processing is per-id: the result reports deleted ids, skipped ids (reason: confirmed / invoiced / already_deleted) and per-id errors (not found / no access), plus deletedHours. Use get-time-entries to find ids first.",
|
|
107406
|
+
inputSchema: {
|
|
107407
|
+
type: "object",
|
|
107408
|
+
properties: {
|
|
107409
|
+
teamId: teamIdProp,
|
|
107410
|
+
id: {
|
|
107411
|
+
type: "string",
|
|
107412
|
+
description: "A single timesheet event id (UUID) to delete."
|
|
107413
|
+
},
|
|
107414
|
+
ids: {
|
|
107415
|
+
type: "array",
|
|
107416
|
+
items: { type: "string" },
|
|
107417
|
+
description: "Multiple timesheet event ids (UUIDs) to delete in one call (max 100). Combined with `id` if both are given."
|
|
107418
|
+
},
|
|
107419
|
+
allowInvoicedOverride: {
|
|
107420
|
+
type: "boolean",
|
|
107421
|
+
default: false,
|
|
107422
|
+
description: "When true, also delete confirmed and/or invoiced entries. Default false skips them (safer)."
|
|
107423
|
+
}
|
|
107424
|
+
},
|
|
107425
|
+
required: []
|
|
107426
|
+
}
|
|
107427
|
+
},
|
|
107428
|
+
{
|
|
107429
|
+
name: "update-time-entry",
|
|
107430
|
+
description: "Update a single existing tracked time entry (urenregel / timesheet event) by id: correct its date, hours, description, project link, ticket link, status or billable flag. Changing date and/or hours re-times the line (date moves it to 09:00 Europe/Amsterdam, hours sets its duration). Safety: on a confirmed or invoiced entry the financial/time fields (date, hours, status, billable) are locked unless `allowInvoicedOverride` is true; description and project/ticket links stay editable. Use get-time-entries to find the id and get-projects / get-tickets to resolve links. To remove a line use delete-time-entries; to create new lines use create-time-entries.",
|
|
107431
|
+
inputSchema: {
|
|
107432
|
+
type: "object",
|
|
107433
|
+
properties: {
|
|
107434
|
+
teamId: teamIdProp,
|
|
107435
|
+
id: {
|
|
107436
|
+
type: "string",
|
|
107437
|
+
description: "Timesheet event id (UUID) to update."
|
|
107438
|
+
},
|
|
107439
|
+
date: {
|
|
107440
|
+
type: "string",
|
|
107441
|
+
description: "New calendar date (YYYY-MM-DD, Europe/Amsterdam). Moves the line to 09:00 on this day."
|
|
107442
|
+
},
|
|
107443
|
+
hours: {
|
|
107444
|
+
type: "number",
|
|
107445
|
+
description: "New worked hours for the line (> 0, <= 24)."
|
|
107446
|
+
},
|
|
107447
|
+
description: {
|
|
107448
|
+
type: "string",
|
|
107449
|
+
description: "New line description (updates both the line title and description)."
|
|
107450
|
+
},
|
|
107451
|
+
projectId: {
|
|
107452
|
+
type: ["string", "null"],
|
|
107453
|
+
description: "New project UUID, or null to clear the project link."
|
|
107454
|
+
},
|
|
107455
|
+
ticketId: {
|
|
107456
|
+
type: ["string", "null"],
|
|
107457
|
+
description: "Replace the linked ticket (UUID), or null to unlink all tickets."
|
|
107458
|
+
},
|
|
107459
|
+
status: {
|
|
107460
|
+
type: "string",
|
|
107461
|
+
enum: ["draft", "confirmed", "submitted"],
|
|
107462
|
+
description: "New status. 'submitted' is treated as 'confirmed'."
|
|
107463
|
+
},
|
|
107464
|
+
billable: {
|
|
107465
|
+
type: "boolean",
|
|
107466
|
+
description: "New billable flag. true -> to_bill, false -> unbillable."
|
|
107467
|
+
},
|
|
107468
|
+
allowInvoicedOverride: {
|
|
107469
|
+
type: "boolean",
|
|
107470
|
+
default: false,
|
|
107471
|
+
description: "Allow editing locked fields (date/hours/status/billable) on a confirmed/invoiced entry."
|
|
107472
|
+
}
|
|
107473
|
+
},
|
|
107474
|
+
required: ["id"]
|
|
107475
|
+
}
|
|
107476
|
+
},
|
|
107385
107477
|
{
|
|
107386
107478
|
name: "get-trips",
|
|
107387
107479
|
description: "List trips / kilometer registration entries (rides) scoped to your provider team(s), with optional filters by period (dateFrom/dateTo), user, project, customer, trip type (business/private), billing type, and invoiced status. Returns each trip's id, date, start/end location, distance (km), odometer readings, trip type, billing type, rate/amount, linked user/project/customer/invoice/vehicle, plus aggregate business/private/total km and total amount.",
|
|
@@ -109730,6 +109822,59 @@ function ensureTipTapFormat(text3) {
|
|
|
109730
109822
|
if (isTipTapJson(text3)) return text3;
|
|
109731
109823
|
return JSON.stringify(plainTextToTipTap(text3));
|
|
109732
109824
|
}
|
|
109825
|
+
|
|
109826
|
+
// src/tools/document-links.ts
|
|
109827
|
+
function derivePdfStatus(doc) {
|
|
109828
|
+
const currentHash = computeDocumentContentHash({
|
|
109829
|
+
blocks: doc.blocks,
|
|
109830
|
+
branding: doc.branding,
|
|
109831
|
+
pageSize: doc.pageSize,
|
|
109832
|
+
locale: doc.locale,
|
|
109833
|
+
title: doc.title
|
|
109834
|
+
});
|
|
109835
|
+
if (doc.filePath) {
|
|
109836
|
+
if (doc.compiledHash && doc.compiledHash === currentHash) {
|
|
109837
|
+
return { status: "ready", currentHash };
|
|
109838
|
+
}
|
|
109839
|
+
return { status: "stale", currentHash };
|
|
109840
|
+
}
|
|
109841
|
+
if (doc.compiledHash) return { status: "pending", currentHash };
|
|
109842
|
+
return { status: "unavailable", currentHash };
|
|
109843
|
+
}
|
|
109844
|
+
function getDashboardBaseUrl() {
|
|
109845
|
+
return (process.env.DASHBOARD_URL || "https://app.refront.nl").replace(
|
|
109846
|
+
/\/+$/,
|
|
109847
|
+
""
|
|
109848
|
+
);
|
|
109849
|
+
}
|
|
109850
|
+
function buildDashboardUrl(documentId) {
|
|
109851
|
+
return `${getDashboardBaseUrl()}/quotations/documents?editDocument=true&documentId=${encodeURIComponent(
|
|
109852
|
+
documentId
|
|
109853
|
+
)}`;
|
|
109854
|
+
}
|
|
109855
|
+
function buildPublicShareUrl(token) {
|
|
109856
|
+
return `${getDashboardBaseUrl()}/p/${encodeURIComponent(token)}`;
|
|
109857
|
+
}
|
|
109858
|
+
function isPubliclyShared(doc) {
|
|
109859
|
+
return !!doc.token && doc.shareVisibility === "public";
|
|
109860
|
+
}
|
|
109861
|
+
function formatDocumentLinksSummary(doc) {
|
|
109862
|
+
const { status: pdfStatus } = derivePdfStatus(doc);
|
|
109863
|
+
const lines = [
|
|
109864
|
+
"\u{1F517} Links:",
|
|
109865
|
+
`- Dashboard: ${buildDashboardUrl(doc.id)}`,
|
|
109866
|
+
`- PDF status: ${pdfStatus}`
|
|
109867
|
+
];
|
|
109868
|
+
if (isPubliclyShared(doc) && doc.token) {
|
|
109869
|
+
lines.push(`- Public share: ${buildPublicShareUrl(doc.token)}`);
|
|
109870
|
+
}
|
|
109871
|
+
lines.push(
|
|
109872
|
+
"- Use get-document-link (linkType pdf_signed for a temporary PDF download URL, public_share to publish a public link)."
|
|
109873
|
+
);
|
|
109874
|
+
return lines.join("\n");
|
|
109875
|
+
}
|
|
109876
|
+
|
|
109877
|
+
// src/tools/documents.ts
|
|
109733
109878
|
var DOCUMENT_TYPES = [
|
|
109734
109879
|
"proposal",
|
|
109735
109880
|
"plan_of_action",
|
|
@@ -109771,7 +109916,6 @@ var PDF_STATUS_LABEL = {
|
|
|
109771
109916
|
};
|
|
109772
109917
|
function buildLinksSection(doc, opts) {
|
|
109773
109918
|
let status = computePdfStatus(doc);
|
|
109774
|
-
if (opts?.compileTriggered && status !== "ready") status = "pending";
|
|
109775
109919
|
const lines = [
|
|
109776
109920
|
"Links:",
|
|
109777
109921
|
`- Dashboard (internal, login required): ${documentDashboardUrl(doc.id)}`,
|
|
@@ -109872,7 +110016,10 @@ var TRIGGER_API_URL = process.env.TRIGGER_API_URL || "https://trigger-dev.mgsoft
|
|
|
109872
110016
|
async function maybeTriggerPdfCompile(documentId, teamId) {
|
|
109873
110017
|
const secret = process.env.TRIGGER_SECRET_KEY;
|
|
109874
110018
|
if (!secret) {
|
|
109875
|
-
return
|
|
110019
|
+
return {
|
|
110020
|
+
triggered: false,
|
|
110021
|
+
message: "PDF: niet gecompileerd (TRIGGER_SECRET_KEY ontbreekt); de dashboard-editor compileert bij openen/delen."
|
|
110022
|
+
};
|
|
109876
110023
|
}
|
|
109877
110024
|
try {
|
|
109878
110025
|
const response = await fetch(
|
|
@@ -109888,12 +110035,21 @@ async function maybeTriggerPdfCompile(documentId, teamId) {
|
|
|
109888
110035
|
);
|
|
109889
110036
|
if (!response.ok) {
|
|
109890
110037
|
const body = await response.text();
|
|
109891
|
-
return
|
|
110038
|
+
return {
|
|
110039
|
+
triggered: false,
|
|
110040
|
+
message: `PDF: compile-trigger mislukt (HTTP ${response.status}): ${body.slice(0, 200)}`
|
|
110041
|
+
};
|
|
109892
110042
|
}
|
|
109893
110043
|
const json5 = await response.json();
|
|
109894
|
-
return
|
|
110044
|
+
return {
|
|
110045
|
+
triggered: true,
|
|
110046
|
+
message: `PDF: compile-document-pdf getriggerd (run ${json5.id ?? "?"}).`
|
|
110047
|
+
};
|
|
109895
110048
|
} catch (error49) {
|
|
109896
|
-
return
|
|
110049
|
+
return {
|
|
110050
|
+
triggered: false,
|
|
110051
|
+
message: `PDF: compile-trigger mislukt: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
110052
|
+
};
|
|
109897
110053
|
}
|
|
109898
110054
|
}
|
|
109899
110055
|
async function applyBlockOps(existing, ops, humanize) {
|
|
@@ -110078,7 +110234,7 @@ Type: ${doc.type} | Status: ${doc.status} | Locale: ${doc.locale}
|
|
|
110078
110234
|
Team: ${doc.teamId}${doc.customerId ? ` | Customer: ${doc.customerId}` : ""}${doc.invoiceId ? ` | Invoice: ${doc.invoiceId}` : ""}
|
|
110079
110235
|
Blocks (${blocks.length}): ${blockTypeSummary(blocks) || "none"}
|
|
110080
110236
|
|
|
110081
|
-
${
|
|
110237
|
+
${formatDocumentLinksSummary(doc)}
|
|
110082
110238
|
|
|
110083
110239
|
Blocks JSON:
|
|
110084
110240
|
\`\`\`json
|
|
@@ -110265,22 +110421,19 @@ async function handleCreateDocument(input) {
|
|
|
110265
110421
|
content: [{ type: "text", text: "Error: document insert failed." }]
|
|
110266
110422
|
};
|
|
110267
110423
|
}
|
|
110268
|
-
const
|
|
110269
|
-
const
|
|
110270
|
-
|
|
110271
|
-
|
|
110272
|
-
|
|
110273
|
-
|
|
110274
|
-
|
|
110275
|
-
|
|
110276
|
-
|
|
110277
|
-
|
|
110278
|
-
|
|
110279
|
-
|
|
110280
|
-
|
|
110281
|
-
},
|
|
110282
|
-
{ compileTriggered: compilePdf && Boolean(process.env.TRIGGER_SECRET_KEY) }
|
|
110283
|
-
);
|
|
110424
|
+
const pdfResult = compilePdf ? await maybeTriggerPdfCompile(created.id, resolved.teamId) : null;
|
|
110425
|
+
const linksSummary = formatDocumentLinksSummary({
|
|
110426
|
+
id: created.id,
|
|
110427
|
+
title: title.trim(),
|
|
110428
|
+
blocks: humanized.blocks,
|
|
110429
|
+
branding: {},
|
|
110430
|
+
pageSize: "a4",
|
|
110431
|
+
locale,
|
|
110432
|
+
filePath: null,
|
|
110433
|
+
compiledHash: null,
|
|
110434
|
+
token: null,
|
|
110435
|
+
shareVisibility: "private"
|
|
110436
|
+
});
|
|
110284
110437
|
return {
|
|
110285
110438
|
content: [
|
|
110286
110439
|
{
|
|
@@ -110294,10 +110447,10 @@ Team: ${resolved.teamId}${customerId ? ` | Customer: ${customerId}` : ""}
|
|
|
110294
110447
|
${invoiceLine ? `${invoiceLine}
|
|
110295
110448
|
` : ""}Blocks (${humanized.blocks.length}): ${blockTypeSummary(humanized.blocks)}
|
|
110296
110449
|
|
|
110297
|
-
${humanized.summary}${
|
|
110298
|
-
${
|
|
110450
|
+
${humanized.summary}${pdfResult ? `
|
|
110451
|
+
${pdfResult.message}` : ""}
|
|
110299
110452
|
|
|
110300
|
-
`
|
|
110453
|
+
${linksSummary}`
|
|
110301
110454
|
}
|
|
110302
110455
|
]
|
|
110303
110456
|
};
|
|
@@ -110459,23 +110612,20 @@ async function handleUpdateDocument(input) {
|
|
|
110459
110612
|
};
|
|
110460
110613
|
}
|
|
110461
110614
|
await db.update(schema_exports.documents).set(patch).where(eq(schema_exports.documents.id, doc.id));
|
|
110462
|
-
const
|
|
110463
|
-
const
|
|
110464
|
-
|
|
110465
|
-
|
|
110466
|
-
|
|
110467
|
-
|
|
110468
|
-
|
|
110469
|
-
|
|
110470
|
-
|
|
110471
|
-
|
|
110472
|
-
|
|
110473
|
-
|
|
110474
|
-
|
|
110475
|
-
|
|
110476
|
-
},
|
|
110477
|
-
{ compileTriggered: compilePdf && Boolean(process.env.TRIGGER_SECRET_KEY) }
|
|
110478
|
-
);
|
|
110615
|
+
const pdfResult = compilePdf ? await maybeTriggerPdfCompile(doc.id, doc.teamId) : null;
|
|
110616
|
+
const linksSummary = formatDocumentLinksSummary({
|
|
110617
|
+
id: doc.id,
|
|
110618
|
+
title: patch.title ?? doc.title,
|
|
110619
|
+
status: patch.status ?? doc.status,
|
|
110620
|
+
blocks: "blocks" in patch ? patch.blocks : doc.blocks,
|
|
110621
|
+
branding: doc.branding,
|
|
110622
|
+
pageSize: doc.pageSize,
|
|
110623
|
+
locale: doc.locale,
|
|
110624
|
+
filePath: doc.filePath,
|
|
110625
|
+
compiledHash: doc.compiledHash,
|
|
110626
|
+
token: doc.token,
|
|
110627
|
+
shareVisibility: doc.shareVisibility
|
|
110628
|
+
});
|
|
110479
110629
|
return {
|
|
110480
110630
|
content: [
|
|
110481
110631
|
{
|
|
@@ -110485,10 +110635,10 @@ async function handleUpdateDocument(input) {
|
|
|
110485
110635
|
ID: ${doc.id}
|
|
110486
110636
|
${changeLines.map((l4) => `- ${l4}`).join("\n")}${humanizeSummary ? `
|
|
110487
110637
|
|
|
110488
|
-
${humanizeSummary}` : ""}${
|
|
110489
|
-
${
|
|
110638
|
+
${humanizeSummary}` : ""}${pdfResult ? `
|
|
110639
|
+
${pdfResult.message}` : ""}
|
|
110490
110640
|
|
|
110491
|
-
`
|
|
110641
|
+
${linksSummary}`
|
|
110492
110642
|
}
|
|
110493
110643
|
]
|
|
110494
110644
|
};
|
|
@@ -115011,6 +115161,271 @@ async function handleCreateTimeEntries(input) {
|
|
|
115011
115161
|
}
|
|
115012
115162
|
});
|
|
115013
115163
|
}
|
|
115164
|
+
var MAX_DELETE_ENTRIES = 100;
|
|
115165
|
+
function rowDurationSeconds(row) {
|
|
115166
|
+
if (row.startTime && row.endTime) {
|
|
115167
|
+
const seconds = (new Date(row.endTime).getTime() - new Date(row.startTime).getTime()) / 1e3;
|
|
115168
|
+
return Math.max(0, Math.round(seconds));
|
|
115169
|
+
}
|
|
115170
|
+
return Math.max(0, Math.round(toNumber(row.trackedDuration)));
|
|
115171
|
+
}
|
|
115172
|
+
function rowDurationHours(row) {
|
|
115173
|
+
return Math.round(rowDurationSeconds(row) / 3600 * 100) / 100;
|
|
115174
|
+
}
|
|
115175
|
+
function isInvoicedRow(row) {
|
|
115176
|
+
return row.invoiceId != null || row.billingStatus === "billed" || row.billingStatus === "draft_billed";
|
|
115177
|
+
}
|
|
115178
|
+
async function handleDeleteTimeEntries(input) {
|
|
115179
|
+
const te = schema_exports.timesheetEvents;
|
|
115180
|
+
const ids = [
|
|
115181
|
+
...new Set(
|
|
115182
|
+
[input.id, ...Array.isArray(input.ids) ? input.ids : []].filter(
|
|
115183
|
+
(value) => typeof value === "string" && value.trim().length > 0
|
|
115184
|
+
).map((value) => value.trim())
|
|
115185
|
+
)
|
|
115186
|
+
];
|
|
115187
|
+
if (ids.length === 0) {
|
|
115188
|
+
return textResponse3(
|
|
115189
|
+
"Error: provide `id` or a non-empty `ids` array of timesheet event ids. Use get-time-entries to find ids."
|
|
115190
|
+
);
|
|
115191
|
+
}
|
|
115192
|
+
if (ids.length > MAX_DELETE_ENTRIES) {
|
|
115193
|
+
return textResponse3(
|
|
115194
|
+
`Error: too many ids (${ids.length}). Max ${MAX_DELETE_ENTRIES} per call.`
|
|
115195
|
+
);
|
|
115196
|
+
}
|
|
115197
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
115198
|
+
if (!scope.ok) return scope.response;
|
|
115199
|
+
if (scope.teamIds.length === 0) {
|
|
115200
|
+
return textResponse3("No accessible teams found.");
|
|
115201
|
+
}
|
|
115202
|
+
const deleted = [];
|
|
115203
|
+
const skipped = [];
|
|
115204
|
+
const errors = [];
|
|
115205
|
+
for (const id of ids) {
|
|
115206
|
+
try {
|
|
115207
|
+
const [row] = await db.select({
|
|
115208
|
+
id: te.id,
|
|
115209
|
+
title: te.title,
|
|
115210
|
+
status: te.status,
|
|
115211
|
+
invoiceId: te.invoiceId,
|
|
115212
|
+
billingStatus: te.billingStatus,
|
|
115213
|
+
isDeleted: te.isDeleted,
|
|
115214
|
+
startTime: te.startTime,
|
|
115215
|
+
endTime: te.endTime,
|
|
115216
|
+
trackedDuration: te.trackedDuration,
|
|
115217
|
+
date: sql`${localDateTextExpr()}`
|
|
115218
|
+
}).from(te).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds))).limit(1);
|
|
115219
|
+
if (!row) {
|
|
115220
|
+
errors.push({
|
|
115221
|
+
id,
|
|
115222
|
+
message: "Not found or no access. Call get-time-entries to find a valid id."
|
|
115223
|
+
});
|
|
115224
|
+
continue;
|
|
115225
|
+
}
|
|
115226
|
+
const status = row.status === "confirmed" ? "confirmed" : "draft";
|
|
115227
|
+
const invoiced = isInvoicedRow(row);
|
|
115228
|
+
if (row.isDeleted) {
|
|
115229
|
+
skipped.push({ id, reason: "already_deleted", status, invoiced });
|
|
115230
|
+
continue;
|
|
115231
|
+
}
|
|
115232
|
+
const confirmed = status === "confirmed";
|
|
115233
|
+
if ((invoiced || confirmed) && !input.allowInvoicedOverride) {
|
|
115234
|
+
skipped.push({
|
|
115235
|
+
id,
|
|
115236
|
+
reason: invoiced ? "invoiced" : "confirmed",
|
|
115237
|
+
status,
|
|
115238
|
+
invoiced
|
|
115239
|
+
});
|
|
115240
|
+
continue;
|
|
115241
|
+
}
|
|
115242
|
+
await db.update(te).set({ isDeleted: true, deletedAt: sql`NOW()`, updatedAt: sql`NOW()` }).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds)));
|
|
115243
|
+
deleted.push({
|
|
115244
|
+
id,
|
|
115245
|
+
date: row.date ?? null,
|
|
115246
|
+
hours: rowDurationHours(row),
|
|
115247
|
+
title: row.title
|
|
115248
|
+
});
|
|
115249
|
+
} catch (error49) {
|
|
115250
|
+
errors.push({
|
|
115251
|
+
id,
|
|
115252
|
+
message: error49 instanceof Error ? error49.message : String(error49)
|
|
115253
|
+
});
|
|
115254
|
+
}
|
|
115255
|
+
}
|
|
115256
|
+
return jsonResponse({
|
|
115257
|
+
teamScope: scope.teamIds,
|
|
115258
|
+
deleted,
|
|
115259
|
+
skipped,
|
|
115260
|
+
errors,
|
|
115261
|
+
totals: {
|
|
115262
|
+
deletedCount: deleted.length,
|
|
115263
|
+
skippedCount: skipped.length,
|
|
115264
|
+
errorCount: errors.length,
|
|
115265
|
+
deletedHours: Math.round(deleted.reduce((sum, d6) => sum + d6.hours, 0) * 100) / 100
|
|
115266
|
+
}
|
|
115267
|
+
});
|
|
115268
|
+
}
|
|
115269
|
+
async function handleUpdateTimeEntry(input) {
|
|
115270
|
+
const te = schema_exports.timesheetEvents;
|
|
115271
|
+
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
115272
|
+
if (!id) return textResponse3("Error: `id` is required.");
|
|
115273
|
+
let mappedStatus;
|
|
115274
|
+
if (input.status !== void 0) {
|
|
115275
|
+
const resolvedStatus = mapCreateStatus(input.status);
|
|
115276
|
+
if (resolvedStatus === null) {
|
|
115277
|
+
return textResponse3(
|
|
115278
|
+
`Error: invalid status "${input.status}". Allowed: draft, confirmed (submitted is treated as confirmed).`
|
|
115279
|
+
);
|
|
115280
|
+
}
|
|
115281
|
+
mappedStatus = resolvedStatus;
|
|
115282
|
+
}
|
|
115283
|
+
const newDate = input.date !== void 0 ? String(input.date).trim() : void 0;
|
|
115284
|
+
if (newDate !== void 0 && !isValidIsoDate(newDate)) {
|
|
115285
|
+
return textResponse3("Error: invalid `date`; expected YYYY-MM-DD.");
|
|
115286
|
+
}
|
|
115287
|
+
if (input.hours !== void 0) {
|
|
115288
|
+
const hours = toNumber(input.hours);
|
|
115289
|
+
if (!(hours > 0)) {
|
|
115290
|
+
return textResponse3("Error: `hours` must be a number greater than 0.");
|
|
115291
|
+
}
|
|
115292
|
+
if (hours > 24) {
|
|
115293
|
+
return textResponse3("Error: `hours` must not exceed 24 for a single day.");
|
|
115294
|
+
}
|
|
115295
|
+
}
|
|
115296
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
115297
|
+
if (!scope.ok) return scope.response;
|
|
115298
|
+
if (scope.teamIds.length === 0) {
|
|
115299
|
+
return textResponse3("No accessible teams found.");
|
|
115300
|
+
}
|
|
115301
|
+
const [existing] = await db.select({
|
|
115302
|
+
id: te.id,
|
|
115303
|
+
status: te.status,
|
|
115304
|
+
invoiceId: te.invoiceId,
|
|
115305
|
+
billingStatus: te.billingStatus,
|
|
115306
|
+
isDeleted: te.isDeleted,
|
|
115307
|
+
startTime: te.startTime,
|
|
115308
|
+
endTime: te.endTime,
|
|
115309
|
+
trackedDuration: te.trackedDuration
|
|
115310
|
+
}).from(te).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds))).limit(1);
|
|
115311
|
+
if (!existing || existing.isDeleted) {
|
|
115312
|
+
return textResponse3(
|
|
115313
|
+
`Time entry ${id} not found or no access. Call get-time-entries to find a valid id.`
|
|
115314
|
+
);
|
|
115315
|
+
}
|
|
115316
|
+
const invoiced = isInvoicedRow(existing);
|
|
115317
|
+
const confirmed = existing.status === "confirmed";
|
|
115318
|
+
if ((invoiced || confirmed) && !input.allowInvoicedOverride) {
|
|
115319
|
+
const attempted = [];
|
|
115320
|
+
if (input.date !== void 0) attempted.push("date");
|
|
115321
|
+
if (input.hours !== void 0) attempted.push("hours");
|
|
115322
|
+
if (input.billable !== void 0) attempted.push("billable");
|
|
115323
|
+
if (input.status !== void 0) attempted.push("status");
|
|
115324
|
+
if (attempted.length > 0) {
|
|
115325
|
+
return textResponse3(
|
|
115326
|
+
`Error: time entry ${id} is ${invoiced ? "invoiced" : "confirmed"}. Locked fields: ${attempted.join(", ")}. Re-call with allowInvoicedOverride: true to change them anyway, or only update description / project / ticket links.`
|
|
115327
|
+
);
|
|
115328
|
+
}
|
|
115329
|
+
}
|
|
115330
|
+
if (input.projectId) {
|
|
115331
|
+
if (!scope.projectIds.includes(input.projectId)) {
|
|
115332
|
+
return textResponse3(
|
|
115333
|
+
`Project not found or no access: ${input.projectId}. Call get-projects first.`
|
|
115334
|
+
);
|
|
115335
|
+
}
|
|
115336
|
+
}
|
|
115337
|
+
if (input.ticketId) {
|
|
115338
|
+
const [ticket] = await db.select({
|
|
115339
|
+
id: schema_exports.tickets.id,
|
|
115340
|
+
teamId: schema_exports.tickets.teamId,
|
|
115341
|
+
projectId: schema_exports.tickets.projectId,
|
|
115342
|
+
customerId: schema_exports.tickets.customerId
|
|
115343
|
+
}).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, input.ticketId)).limit(1);
|
|
115344
|
+
if (!ticket) {
|
|
115345
|
+
return textResponse3(
|
|
115346
|
+
`Ticket not found: ${input.ticketId}. Call get-tickets first.`
|
|
115347
|
+
);
|
|
115348
|
+
}
|
|
115349
|
+
let hasAccess = scope.teamIds.includes(ticket.teamId);
|
|
115350
|
+
if (!hasAccess && ticket.projectId) {
|
|
115351
|
+
hasAccess = scope.projectIds.includes(ticket.projectId);
|
|
115352
|
+
}
|
|
115353
|
+
if (!hasAccess && ticket.customerId) {
|
|
115354
|
+
hasAccess = scope.customerIds.includes(ticket.customerId);
|
|
115355
|
+
}
|
|
115356
|
+
if (!hasAccess) {
|
|
115357
|
+
return textResponse3(
|
|
115358
|
+
`No access to ticket: ${input.ticketId}. Call get-tickets first.`
|
|
115359
|
+
);
|
|
115360
|
+
}
|
|
115361
|
+
}
|
|
115362
|
+
const updates = { updatedAt: sql`NOW()` };
|
|
115363
|
+
if (input.description !== void 0) {
|
|
115364
|
+
const description = String(input.description).trim();
|
|
115365
|
+
if (!description) {
|
|
115366
|
+
return textResponse3("Error: `description` cannot be empty.");
|
|
115367
|
+
}
|
|
115368
|
+
updates.title = description;
|
|
115369
|
+
updates.description = description;
|
|
115370
|
+
}
|
|
115371
|
+
if (input.projectId !== void 0) updates.projectId = input.projectId;
|
|
115372
|
+
if (input.billable !== void 0) {
|
|
115373
|
+
updates.billingStatus = input.billable ? "to_bill" : "unbillable";
|
|
115374
|
+
}
|
|
115375
|
+
if (mappedStatus !== void 0) updates.status = mappedStatus;
|
|
115376
|
+
if (newDate !== void 0 || input.hours !== void 0) {
|
|
115377
|
+
const durationSeconds = input.hours !== void 0 ? Math.round(toNumber(input.hours) * 3600) : rowDurationSeconds(existing);
|
|
115378
|
+
const startExpr = newDate !== void 0 ? sql`(${`${newDate} ${DEFAULT_START_HOUR}`}::timestamp AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})` : sql`${existing.startTime}::timestamptz`;
|
|
115379
|
+
const endExpr = sql`(${startExpr} + ${`${durationSeconds} seconds`}::interval)`;
|
|
115380
|
+
updates.startTime = startExpr;
|
|
115381
|
+
updates.endTime = endExpr;
|
|
115382
|
+
updates.trackedDuration = durationSeconds;
|
|
115383
|
+
updates.isTracked = true;
|
|
115384
|
+
}
|
|
115385
|
+
const hasFieldUpdate = Object.keys(updates).length > 1;
|
|
115386
|
+
if (!hasFieldUpdate && input.ticketId === void 0) {
|
|
115387
|
+
return textResponse3(
|
|
115388
|
+
"No fields to update. Provide at least one of date, hours, description, projectId, ticketId, status, billable."
|
|
115389
|
+
);
|
|
115390
|
+
}
|
|
115391
|
+
if (hasFieldUpdate) {
|
|
115392
|
+
await db.update(te).set(updates).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds)));
|
|
115393
|
+
}
|
|
115394
|
+
if (input.ticketId !== void 0) {
|
|
115395
|
+
await db.delete(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.timesheetEventId, id));
|
|
115396
|
+
if (input.ticketId) {
|
|
115397
|
+
await db.insert(schema_exports.timesheetEventTickets).values({ timesheetEventId: id, ticketId: input.ticketId }).onConflictDoNothing();
|
|
115398
|
+
}
|
|
115399
|
+
}
|
|
115400
|
+
const [row] = await db.select({
|
|
115401
|
+
id: te.id,
|
|
115402
|
+
title: te.title,
|
|
115403
|
+
description: te.description,
|
|
115404
|
+
status: te.status,
|
|
115405
|
+
invoiceId: te.invoiceId,
|
|
115406
|
+
billingStatus: te.billingStatus,
|
|
115407
|
+
startTime: te.startTime,
|
|
115408
|
+
endTime: te.endTime,
|
|
115409
|
+
trackedDuration: te.trackedDuration,
|
|
115410
|
+
projectId: te.projectId,
|
|
115411
|
+
date: sql`${localDateTextExpr()}`
|
|
115412
|
+
}).from(te).where(eq(te.id, id)).limit(1);
|
|
115413
|
+
const links = await db.select({ ticketId: schema_exports.timesheetEventTickets.ticketId }).from(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.timesheetEventId, id));
|
|
115414
|
+
return jsonResponse({
|
|
115415
|
+
updated: {
|
|
115416
|
+
id,
|
|
115417
|
+
date: row?.date ?? null,
|
|
115418
|
+
hours: row ? rowDurationHours(row) : 0,
|
|
115419
|
+
title: row?.title ?? null,
|
|
115420
|
+
description: row?.description ?? null,
|
|
115421
|
+
status: row?.status === "confirmed" ? "confirmed" : "draft",
|
|
115422
|
+
invoiced: row ? isInvoicedRow(row) : false,
|
|
115423
|
+
billingStatus: row?.billingStatus ?? null,
|
|
115424
|
+
projectId: row?.projectId ?? null,
|
|
115425
|
+
ticketIds: links.map((link) => link.ticketId)
|
|
115426
|
+
}
|
|
115427
|
+
});
|
|
115428
|
+
}
|
|
115014
115429
|
|
|
115015
115430
|
// ../invoice/src/utils/included-items.ts
|
|
115016
115431
|
function parseIncludedItems(value) {
|
|
@@ -126489,6 +126904,14 @@ function createMcpServer() {
|
|
|
126489
126904
|
return await handleCreateTimeEntries(
|
|
126490
126905
|
asToolArgs(toolArgs)
|
|
126491
126906
|
);
|
|
126907
|
+
case "delete-time-entries":
|
|
126908
|
+
return await handleDeleteTimeEntries(
|
|
126909
|
+
asToolArgs(toolArgs)
|
|
126910
|
+
);
|
|
126911
|
+
case "update-time-entry":
|
|
126912
|
+
return await handleUpdateTimeEntry(
|
|
126913
|
+
asToolArgs(toolArgs)
|
|
126914
|
+
);
|
|
126492
126915
|
case "get-github-file":
|
|
126493
126916
|
return await handleGetGithubFile(asToolArgs(toolArgs));
|
|
126494
126917
|
case "list-github-directory":
|