@mgsoftwarebv/mcp-server-bridge 3.5.13 → 3.5.15
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 +396 -2
- 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
|
@@ -94095,7 +94095,7 @@ var projects = pgTable(
|
|
|
94095
94095
|
// Basic project management extensions
|
|
94096
94096
|
color: text().default("#3B82F6").notNull(),
|
|
94097
94097
|
projectCode: text("project_code"),
|
|
94098
|
-
settings: jsonb().default({}),
|
|
94098
|
+
settings: jsonb().$type().default({}),
|
|
94099
94099
|
// Customer portal fields
|
|
94100
94100
|
ownedByCustomer: boolean3("owned_by_customer").default(false),
|
|
94101
94101
|
internal: boolean3().default(false).notNull(),
|
|
@@ -101312,7 +101312,11 @@ var teams = pgTable(
|
|
|
101312
101312
|
// classifier allow-list filter. Default 30 mirrors `DEFAULT_TOOL_BUDGET_MAX`
|
|
101313
101313
|
// in apps/api/src/ai/floris/skills/budget.ts. Read-resolver lands in
|
|
101314
101314
|
// A5-impl.b; per-team toggles in A5-impl.c.
|
|
101315
|
-
florisConfig: jsonb("floris_config").$type().default({ tool_budget_max: 30 }).notNull()
|
|
101315
|
+
florisConfig: jsonb("floris_config").$type().default({ tool_budget_max: 30 }).notNull(),
|
|
101316
|
+
// Per-team time-tracking policy (ticket 2026-REFRO-140). Currently holds the
|
|
101317
|
+
// agent kill-switch `blockAgentDrafts`. Default `{}`; the resolver treats a
|
|
101318
|
+
// missing flag as "block" so new teams are safe by default.
|
|
101319
|
+
timeTrackingPolicy: jsonb("time_tracking_policy").$type().default({}).notNull()
|
|
101316
101320
|
},
|
|
101317
101321
|
(table) => [
|
|
101318
101322
|
unique("teams_documents_email_id_key").on(table.documentsEmailId),
|
|
@@ -107400,6 +107404,80 @@ var TOOLS = [
|
|
|
107400
107404
|
required: ["entries"]
|
|
107401
107405
|
}
|
|
107402
107406
|
},
|
|
107407
|
+
{
|
|
107408
|
+
name: "delete-time-entries",
|
|
107409
|
+
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.",
|
|
107410
|
+
inputSchema: {
|
|
107411
|
+
type: "object",
|
|
107412
|
+
properties: {
|
|
107413
|
+
teamId: teamIdProp,
|
|
107414
|
+
id: {
|
|
107415
|
+
type: "string",
|
|
107416
|
+
description: "A single timesheet event id (UUID) to delete."
|
|
107417
|
+
},
|
|
107418
|
+
ids: {
|
|
107419
|
+
type: "array",
|
|
107420
|
+
items: { type: "string" },
|
|
107421
|
+
description: "Multiple timesheet event ids (UUIDs) to delete in one call (max 100). Combined with `id` if both are given."
|
|
107422
|
+
},
|
|
107423
|
+
allowInvoicedOverride: {
|
|
107424
|
+
type: "boolean",
|
|
107425
|
+
default: false,
|
|
107426
|
+
description: "When true, also delete confirmed and/or invoiced entries. Default false skips them (safer)."
|
|
107427
|
+
}
|
|
107428
|
+
},
|
|
107429
|
+
required: []
|
|
107430
|
+
}
|
|
107431
|
+
},
|
|
107432
|
+
{
|
|
107433
|
+
name: "update-time-entry",
|
|
107434
|
+
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.",
|
|
107435
|
+
inputSchema: {
|
|
107436
|
+
type: "object",
|
|
107437
|
+
properties: {
|
|
107438
|
+
teamId: teamIdProp,
|
|
107439
|
+
id: {
|
|
107440
|
+
type: "string",
|
|
107441
|
+
description: "Timesheet event id (UUID) to update."
|
|
107442
|
+
},
|
|
107443
|
+
date: {
|
|
107444
|
+
type: "string",
|
|
107445
|
+
description: "New calendar date (YYYY-MM-DD, Europe/Amsterdam). Moves the line to 09:00 on this day."
|
|
107446
|
+
},
|
|
107447
|
+
hours: {
|
|
107448
|
+
type: "number",
|
|
107449
|
+
description: "New worked hours for the line (> 0, <= 24)."
|
|
107450
|
+
},
|
|
107451
|
+
description: {
|
|
107452
|
+
type: "string",
|
|
107453
|
+
description: "New line description (updates both the line title and description)."
|
|
107454
|
+
},
|
|
107455
|
+
projectId: {
|
|
107456
|
+
type: ["string", "null"],
|
|
107457
|
+
description: "New project UUID, or null to clear the project link."
|
|
107458
|
+
},
|
|
107459
|
+
ticketId: {
|
|
107460
|
+
type: ["string", "null"],
|
|
107461
|
+
description: "Replace the linked ticket (UUID), or null to unlink all tickets."
|
|
107462
|
+
},
|
|
107463
|
+
status: {
|
|
107464
|
+
type: "string",
|
|
107465
|
+
enum: ["draft", "confirmed", "submitted"],
|
|
107466
|
+
description: "New status. 'submitted' is treated as 'confirmed'."
|
|
107467
|
+
},
|
|
107468
|
+
billable: {
|
|
107469
|
+
type: "boolean",
|
|
107470
|
+
description: "New billable flag. true -> to_bill, false -> unbillable."
|
|
107471
|
+
},
|
|
107472
|
+
allowInvoicedOverride: {
|
|
107473
|
+
type: "boolean",
|
|
107474
|
+
default: false,
|
|
107475
|
+
description: "Allow editing locked fields (date/hours/status/billable) on a confirmed/invoiced entry."
|
|
107476
|
+
}
|
|
107477
|
+
},
|
|
107478
|
+
required: ["id"]
|
|
107479
|
+
}
|
|
107480
|
+
},
|
|
107403
107481
|
{
|
|
107404
107482
|
name: "get-trips",
|
|
107405
107483
|
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.",
|
|
@@ -114307,6 +114385,43 @@ async function handleListGithubDirectory(input) {
|
|
|
114307
114385
|
}
|
|
114308
114386
|
|
|
114309
114387
|
// src/tools/hours.ts
|
|
114388
|
+
async function assertAgentMayLogHours(args2) {
|
|
114389
|
+
const [team] = await db.select({ policy: schema_exports.teams.timeTrackingPolicy }).from(schema_exports.teams).where(eq(schema_exports.teams.id, args2.teamId)).limit(1);
|
|
114390
|
+
let projectPolicy = {};
|
|
114391
|
+
if (args2.projectId) {
|
|
114392
|
+
const [project] = await db.select({ settings: schema_exports.projects.settings }).from(schema_exports.projects).where(eq(schema_exports.projects.id, args2.projectId)).limit(1);
|
|
114393
|
+
const settings = project?.settings;
|
|
114394
|
+
if (settings?.timeTracking && typeof settings.timeTracking === "object") {
|
|
114395
|
+
projectPolicy = settings.timeTracking;
|
|
114396
|
+
}
|
|
114397
|
+
}
|
|
114398
|
+
const teamBlocksAgents = team?.policy?.blockAgentDrafts ?? true;
|
|
114399
|
+
const projectAllowAgents = projectPolicy.allowAgents;
|
|
114400
|
+
const allowAgents = projectAllowAgents === void 0 || projectAllowAgents === null ? !teamBlocksAgents : Boolean(projectAllowAgents);
|
|
114401
|
+
const enabled = projectPolicy.enabled !== false;
|
|
114402
|
+
const requireTicket = projectPolicy.requireTicket === true;
|
|
114403
|
+
const allowedTicketStatuses = Array.isArray(projectPolicy.allowedTicketStatuses) && projectPolicy.allowedTicketStatuses.length > 0 ? projectPolicy.allowedTicketStatuses : null;
|
|
114404
|
+
if (!enabled) {
|
|
114405
|
+
throw new Error(
|
|
114406
|
+
"Blocked by time-tracking policy: time tracking is disabled for this project. No hours can be logged."
|
|
114407
|
+
);
|
|
114408
|
+
}
|
|
114409
|
+
if (!allowAgents) {
|
|
114410
|
+
throw new Error(
|
|
114411
|
+
"Blocked by time-tracking policy: agents/automation may not log draft hours here. Ask a human to enable 'allow agents' on the project (or the team's agent time-tracking setting) before logging time."
|
|
114412
|
+
);
|
|
114413
|
+
}
|
|
114414
|
+
if (requireTicket && !args2.hasTicket) {
|
|
114415
|
+
throw new Error(
|
|
114416
|
+
"Blocked by time-tracking policy: this project requires every time entry to be linked to a ticket. Pass a ticketId."
|
|
114417
|
+
);
|
|
114418
|
+
}
|
|
114419
|
+
if (allowedTicketStatuses && args2.hasTicket && args2.ticketStatus && !allowedTicketStatuses.includes(args2.ticketStatus)) {
|
|
114420
|
+
throw new Error(
|
|
114421
|
+
`Blocked by time-tracking policy: time can only be booked on tickets with status: ${allowedTicketStatuses.join(", ")}. This ticket is "${args2.ticketStatus}".`
|
|
114422
|
+
);
|
|
114423
|
+
}
|
|
114424
|
+
}
|
|
114310
114425
|
async function handleLogHours(input) {
|
|
114311
114426
|
const ctx = getAuthContext();
|
|
114312
114427
|
const {
|
|
@@ -114367,6 +114482,12 @@ async function handleLogHours(input) {
|
|
|
114367
114482
|
if (!resolved.ok) return resolved.response;
|
|
114368
114483
|
insertTeamId = resolved.teamId;
|
|
114369
114484
|
}
|
|
114485
|
+
await assertAgentMayLogHours({
|
|
114486
|
+
teamId: insertTeamId,
|
|
114487
|
+
projectId: project?.id ?? null,
|
|
114488
|
+
hasTicket: Boolean(ticket?.id),
|
|
114489
|
+
ticketStatus: ticket?.status ?? null
|
|
114490
|
+
});
|
|
114370
114491
|
const durationSeconds = Math.round(estimatedHours * 3600);
|
|
114371
114492
|
const now2 = /* @__PURE__ */ new Date();
|
|
114372
114493
|
let agendaEntry = null;
|
|
@@ -115087,6 +115208,271 @@ async function handleCreateTimeEntries(input) {
|
|
|
115087
115208
|
}
|
|
115088
115209
|
});
|
|
115089
115210
|
}
|
|
115211
|
+
var MAX_DELETE_ENTRIES = 100;
|
|
115212
|
+
function rowDurationSeconds(row) {
|
|
115213
|
+
if (row.startTime && row.endTime) {
|
|
115214
|
+
const seconds = (new Date(row.endTime).getTime() - new Date(row.startTime).getTime()) / 1e3;
|
|
115215
|
+
return Math.max(0, Math.round(seconds));
|
|
115216
|
+
}
|
|
115217
|
+
return Math.max(0, Math.round(toNumber(row.trackedDuration)));
|
|
115218
|
+
}
|
|
115219
|
+
function rowDurationHours(row) {
|
|
115220
|
+
return Math.round(rowDurationSeconds(row) / 3600 * 100) / 100;
|
|
115221
|
+
}
|
|
115222
|
+
function isInvoicedRow(row) {
|
|
115223
|
+
return row.invoiceId != null || row.billingStatus === "billed" || row.billingStatus === "draft_billed";
|
|
115224
|
+
}
|
|
115225
|
+
async function handleDeleteTimeEntries(input) {
|
|
115226
|
+
const te = schema_exports.timesheetEvents;
|
|
115227
|
+
const ids = [
|
|
115228
|
+
...new Set(
|
|
115229
|
+
[input.id, ...Array.isArray(input.ids) ? input.ids : []].filter(
|
|
115230
|
+
(value) => typeof value === "string" && value.trim().length > 0
|
|
115231
|
+
).map((value) => value.trim())
|
|
115232
|
+
)
|
|
115233
|
+
];
|
|
115234
|
+
if (ids.length === 0) {
|
|
115235
|
+
return textResponse3(
|
|
115236
|
+
"Error: provide `id` or a non-empty `ids` array of timesheet event ids. Use get-time-entries to find ids."
|
|
115237
|
+
);
|
|
115238
|
+
}
|
|
115239
|
+
if (ids.length > MAX_DELETE_ENTRIES) {
|
|
115240
|
+
return textResponse3(
|
|
115241
|
+
`Error: too many ids (${ids.length}). Max ${MAX_DELETE_ENTRIES} per call.`
|
|
115242
|
+
);
|
|
115243
|
+
}
|
|
115244
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
115245
|
+
if (!scope.ok) return scope.response;
|
|
115246
|
+
if (scope.teamIds.length === 0) {
|
|
115247
|
+
return textResponse3("No accessible teams found.");
|
|
115248
|
+
}
|
|
115249
|
+
const deleted = [];
|
|
115250
|
+
const skipped = [];
|
|
115251
|
+
const errors = [];
|
|
115252
|
+
for (const id of ids) {
|
|
115253
|
+
try {
|
|
115254
|
+
const [row] = await db.select({
|
|
115255
|
+
id: te.id,
|
|
115256
|
+
title: te.title,
|
|
115257
|
+
status: te.status,
|
|
115258
|
+
invoiceId: te.invoiceId,
|
|
115259
|
+
billingStatus: te.billingStatus,
|
|
115260
|
+
isDeleted: te.isDeleted,
|
|
115261
|
+
startTime: te.startTime,
|
|
115262
|
+
endTime: te.endTime,
|
|
115263
|
+
trackedDuration: te.trackedDuration,
|
|
115264
|
+
date: sql`${localDateTextExpr()}`
|
|
115265
|
+
}).from(te).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds))).limit(1);
|
|
115266
|
+
if (!row) {
|
|
115267
|
+
errors.push({
|
|
115268
|
+
id,
|
|
115269
|
+
message: "Not found or no access. Call get-time-entries to find a valid id."
|
|
115270
|
+
});
|
|
115271
|
+
continue;
|
|
115272
|
+
}
|
|
115273
|
+
const status = row.status === "confirmed" ? "confirmed" : "draft";
|
|
115274
|
+
const invoiced = isInvoicedRow(row);
|
|
115275
|
+
if (row.isDeleted) {
|
|
115276
|
+
skipped.push({ id, reason: "already_deleted", status, invoiced });
|
|
115277
|
+
continue;
|
|
115278
|
+
}
|
|
115279
|
+
const confirmed = status === "confirmed";
|
|
115280
|
+
if ((invoiced || confirmed) && !input.allowInvoicedOverride) {
|
|
115281
|
+
skipped.push({
|
|
115282
|
+
id,
|
|
115283
|
+
reason: invoiced ? "invoiced" : "confirmed",
|
|
115284
|
+
status,
|
|
115285
|
+
invoiced
|
|
115286
|
+
});
|
|
115287
|
+
continue;
|
|
115288
|
+
}
|
|
115289
|
+
await db.update(te).set({ isDeleted: true, deletedAt: sql`NOW()`, updatedAt: sql`NOW()` }).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds)));
|
|
115290
|
+
deleted.push({
|
|
115291
|
+
id,
|
|
115292
|
+
date: row.date ?? null,
|
|
115293
|
+
hours: rowDurationHours(row),
|
|
115294
|
+
title: row.title
|
|
115295
|
+
});
|
|
115296
|
+
} catch (error49) {
|
|
115297
|
+
errors.push({
|
|
115298
|
+
id,
|
|
115299
|
+
message: error49 instanceof Error ? error49.message : String(error49)
|
|
115300
|
+
});
|
|
115301
|
+
}
|
|
115302
|
+
}
|
|
115303
|
+
return jsonResponse({
|
|
115304
|
+
teamScope: scope.teamIds,
|
|
115305
|
+
deleted,
|
|
115306
|
+
skipped,
|
|
115307
|
+
errors,
|
|
115308
|
+
totals: {
|
|
115309
|
+
deletedCount: deleted.length,
|
|
115310
|
+
skippedCount: skipped.length,
|
|
115311
|
+
errorCount: errors.length,
|
|
115312
|
+
deletedHours: Math.round(deleted.reduce((sum, d6) => sum + d6.hours, 0) * 100) / 100
|
|
115313
|
+
}
|
|
115314
|
+
});
|
|
115315
|
+
}
|
|
115316
|
+
async function handleUpdateTimeEntry(input) {
|
|
115317
|
+
const te = schema_exports.timesheetEvents;
|
|
115318
|
+
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
115319
|
+
if (!id) return textResponse3("Error: `id` is required.");
|
|
115320
|
+
let mappedStatus;
|
|
115321
|
+
if (input.status !== void 0) {
|
|
115322
|
+
const resolvedStatus = mapCreateStatus(input.status);
|
|
115323
|
+
if (resolvedStatus === null) {
|
|
115324
|
+
return textResponse3(
|
|
115325
|
+
`Error: invalid status "${input.status}". Allowed: draft, confirmed (submitted is treated as confirmed).`
|
|
115326
|
+
);
|
|
115327
|
+
}
|
|
115328
|
+
mappedStatus = resolvedStatus;
|
|
115329
|
+
}
|
|
115330
|
+
const newDate = input.date !== void 0 ? String(input.date).trim() : void 0;
|
|
115331
|
+
if (newDate !== void 0 && !isValidIsoDate(newDate)) {
|
|
115332
|
+
return textResponse3("Error: invalid `date`; expected YYYY-MM-DD.");
|
|
115333
|
+
}
|
|
115334
|
+
if (input.hours !== void 0) {
|
|
115335
|
+
const hours = toNumber(input.hours);
|
|
115336
|
+
if (!(hours > 0)) {
|
|
115337
|
+
return textResponse3("Error: `hours` must be a number greater than 0.");
|
|
115338
|
+
}
|
|
115339
|
+
if (hours > 24) {
|
|
115340
|
+
return textResponse3("Error: `hours` must not exceed 24 for a single day.");
|
|
115341
|
+
}
|
|
115342
|
+
}
|
|
115343
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
115344
|
+
if (!scope.ok) return scope.response;
|
|
115345
|
+
if (scope.teamIds.length === 0) {
|
|
115346
|
+
return textResponse3("No accessible teams found.");
|
|
115347
|
+
}
|
|
115348
|
+
const [existing] = await db.select({
|
|
115349
|
+
id: te.id,
|
|
115350
|
+
status: te.status,
|
|
115351
|
+
invoiceId: te.invoiceId,
|
|
115352
|
+
billingStatus: te.billingStatus,
|
|
115353
|
+
isDeleted: te.isDeleted,
|
|
115354
|
+
startTime: te.startTime,
|
|
115355
|
+
endTime: te.endTime,
|
|
115356
|
+
trackedDuration: te.trackedDuration
|
|
115357
|
+
}).from(te).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds))).limit(1);
|
|
115358
|
+
if (!existing || existing.isDeleted) {
|
|
115359
|
+
return textResponse3(
|
|
115360
|
+
`Time entry ${id} not found or no access. Call get-time-entries to find a valid id.`
|
|
115361
|
+
);
|
|
115362
|
+
}
|
|
115363
|
+
const invoiced = isInvoicedRow(existing);
|
|
115364
|
+
const confirmed = existing.status === "confirmed";
|
|
115365
|
+
if ((invoiced || confirmed) && !input.allowInvoicedOverride) {
|
|
115366
|
+
const attempted = [];
|
|
115367
|
+
if (input.date !== void 0) attempted.push("date");
|
|
115368
|
+
if (input.hours !== void 0) attempted.push("hours");
|
|
115369
|
+
if (input.billable !== void 0) attempted.push("billable");
|
|
115370
|
+
if (input.status !== void 0) attempted.push("status");
|
|
115371
|
+
if (attempted.length > 0) {
|
|
115372
|
+
return textResponse3(
|
|
115373
|
+
`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.`
|
|
115374
|
+
);
|
|
115375
|
+
}
|
|
115376
|
+
}
|
|
115377
|
+
if (input.projectId) {
|
|
115378
|
+
if (!scope.projectIds.includes(input.projectId)) {
|
|
115379
|
+
return textResponse3(
|
|
115380
|
+
`Project not found or no access: ${input.projectId}. Call get-projects first.`
|
|
115381
|
+
);
|
|
115382
|
+
}
|
|
115383
|
+
}
|
|
115384
|
+
if (input.ticketId) {
|
|
115385
|
+
const [ticket] = await db.select({
|
|
115386
|
+
id: schema_exports.tickets.id,
|
|
115387
|
+
teamId: schema_exports.tickets.teamId,
|
|
115388
|
+
projectId: schema_exports.tickets.projectId,
|
|
115389
|
+
customerId: schema_exports.tickets.customerId
|
|
115390
|
+
}).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, input.ticketId)).limit(1);
|
|
115391
|
+
if (!ticket) {
|
|
115392
|
+
return textResponse3(
|
|
115393
|
+
`Ticket not found: ${input.ticketId}. Call get-tickets first.`
|
|
115394
|
+
);
|
|
115395
|
+
}
|
|
115396
|
+
let hasAccess = scope.teamIds.includes(ticket.teamId);
|
|
115397
|
+
if (!hasAccess && ticket.projectId) {
|
|
115398
|
+
hasAccess = scope.projectIds.includes(ticket.projectId);
|
|
115399
|
+
}
|
|
115400
|
+
if (!hasAccess && ticket.customerId) {
|
|
115401
|
+
hasAccess = scope.customerIds.includes(ticket.customerId);
|
|
115402
|
+
}
|
|
115403
|
+
if (!hasAccess) {
|
|
115404
|
+
return textResponse3(
|
|
115405
|
+
`No access to ticket: ${input.ticketId}. Call get-tickets first.`
|
|
115406
|
+
);
|
|
115407
|
+
}
|
|
115408
|
+
}
|
|
115409
|
+
const updates = { updatedAt: sql`NOW()` };
|
|
115410
|
+
if (input.description !== void 0) {
|
|
115411
|
+
const description = String(input.description).trim();
|
|
115412
|
+
if (!description) {
|
|
115413
|
+
return textResponse3("Error: `description` cannot be empty.");
|
|
115414
|
+
}
|
|
115415
|
+
updates.title = description;
|
|
115416
|
+
updates.description = description;
|
|
115417
|
+
}
|
|
115418
|
+
if (input.projectId !== void 0) updates.projectId = input.projectId;
|
|
115419
|
+
if (input.billable !== void 0) {
|
|
115420
|
+
updates.billingStatus = input.billable ? "to_bill" : "unbillable";
|
|
115421
|
+
}
|
|
115422
|
+
if (mappedStatus !== void 0) updates.status = mappedStatus;
|
|
115423
|
+
if (newDate !== void 0 || input.hours !== void 0) {
|
|
115424
|
+
const durationSeconds = input.hours !== void 0 ? Math.round(toNumber(input.hours) * 3600) : rowDurationSeconds(existing);
|
|
115425
|
+
const startExpr = newDate !== void 0 ? sql`(${`${newDate} ${DEFAULT_START_HOUR}`}::timestamp AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})` : sql`${existing.startTime}::timestamptz`;
|
|
115426
|
+
const endExpr = sql`(${startExpr} + ${`${durationSeconds} seconds`}::interval)`;
|
|
115427
|
+
updates.startTime = startExpr;
|
|
115428
|
+
updates.endTime = endExpr;
|
|
115429
|
+
updates.trackedDuration = durationSeconds;
|
|
115430
|
+
updates.isTracked = true;
|
|
115431
|
+
}
|
|
115432
|
+
const hasFieldUpdate = Object.keys(updates).length > 1;
|
|
115433
|
+
if (!hasFieldUpdate && input.ticketId === void 0) {
|
|
115434
|
+
return textResponse3(
|
|
115435
|
+
"No fields to update. Provide at least one of date, hours, description, projectId, ticketId, status, billable."
|
|
115436
|
+
);
|
|
115437
|
+
}
|
|
115438
|
+
if (hasFieldUpdate) {
|
|
115439
|
+
await db.update(te).set(updates).where(and(eq(te.id, id), inArray(te.teamId, scope.teamIds)));
|
|
115440
|
+
}
|
|
115441
|
+
if (input.ticketId !== void 0) {
|
|
115442
|
+
await db.delete(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.timesheetEventId, id));
|
|
115443
|
+
if (input.ticketId) {
|
|
115444
|
+
await db.insert(schema_exports.timesheetEventTickets).values({ timesheetEventId: id, ticketId: input.ticketId }).onConflictDoNothing();
|
|
115445
|
+
}
|
|
115446
|
+
}
|
|
115447
|
+
const [row] = await db.select({
|
|
115448
|
+
id: te.id,
|
|
115449
|
+
title: te.title,
|
|
115450
|
+
description: te.description,
|
|
115451
|
+
status: te.status,
|
|
115452
|
+
invoiceId: te.invoiceId,
|
|
115453
|
+
billingStatus: te.billingStatus,
|
|
115454
|
+
startTime: te.startTime,
|
|
115455
|
+
endTime: te.endTime,
|
|
115456
|
+
trackedDuration: te.trackedDuration,
|
|
115457
|
+
projectId: te.projectId,
|
|
115458
|
+
date: sql`${localDateTextExpr()}`
|
|
115459
|
+
}).from(te).where(eq(te.id, id)).limit(1);
|
|
115460
|
+
const links = await db.select({ ticketId: schema_exports.timesheetEventTickets.ticketId }).from(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.timesheetEventId, id));
|
|
115461
|
+
return jsonResponse({
|
|
115462
|
+
updated: {
|
|
115463
|
+
id,
|
|
115464
|
+
date: row?.date ?? null,
|
|
115465
|
+
hours: row ? rowDurationHours(row) : 0,
|
|
115466
|
+
title: row?.title ?? null,
|
|
115467
|
+
description: row?.description ?? null,
|
|
115468
|
+
status: row?.status === "confirmed" ? "confirmed" : "draft",
|
|
115469
|
+
invoiced: row ? isInvoicedRow(row) : false,
|
|
115470
|
+
billingStatus: row?.billingStatus ?? null,
|
|
115471
|
+
projectId: row?.projectId ?? null,
|
|
115472
|
+
ticketIds: links.map((link) => link.ticketId)
|
|
115473
|
+
}
|
|
115474
|
+
});
|
|
115475
|
+
}
|
|
115090
115476
|
|
|
115091
115477
|
// ../invoice/src/utils/included-items.ts
|
|
115092
115478
|
function parseIncludedItems(value) {
|
|
@@ -126565,6 +126951,14 @@ function createMcpServer() {
|
|
|
126565
126951
|
return await handleCreateTimeEntries(
|
|
126566
126952
|
asToolArgs(toolArgs)
|
|
126567
126953
|
);
|
|
126954
|
+
case "delete-time-entries":
|
|
126955
|
+
return await handleDeleteTimeEntries(
|
|
126956
|
+
asToolArgs(toolArgs)
|
|
126957
|
+
);
|
|
126958
|
+
case "update-time-entry":
|
|
126959
|
+
return await handleUpdateTimeEntry(
|
|
126960
|
+
asToolArgs(toolArgs)
|
|
126961
|
+
);
|
|
126568
126962
|
case "get-github-file":
|
|
126569
126963
|
return await handleGetGithubFile(asToolArgs(toolArgs));
|
|
126570
126964
|
case "list-github-directory":
|