@mgsoftwarebv/mcp-server-bridge 3.5.9 → 3.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64421,7 +64421,7 @@ var init_DocTypeReader = __esm({
64421
64421
  });
64422
64422
 
64423
64423
  // ../../node_modules/strnum/strnum.js
64424
- function toNumber(str, options = {}) {
64424
+ function toNumber2(str, options = {}) {
64425
64425
  options = Object.assign({}, consider, options);
64426
64426
  if (!str || typeof str !== "string") return str;
64427
64427
  let trimmedStr = str.trim();
@@ -65669,7 +65669,7 @@ function parseValue(val, shouldParse, options) {
65669
65669
  const newval = val.trim();
65670
65670
  if (newval === "true") return true;
65671
65671
  else if (newval === "false") return false;
65672
- else return toNumber(val, options);
65672
+ else return toNumber2(val, options);
65673
65673
  } else {
65674
65674
  if (isExist(val)) {
65675
65675
  return val;
@@ -99324,6 +99324,10 @@ var githubEvents = pgTable(
99324
99324
  {
99325
99325
  id: uuid3().defaultRandom().primaryKey().notNull(),
99326
99326
  createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).defaultNow().notNull(),
99327
+ activityDate: timestamp("activity_date", {
99328
+ withTimezone: true,
99329
+ mode: "string"
99330
+ }).defaultNow().notNull(),
99327
99331
  // Team and project info
99328
99332
  teamId: uuid3("team_id").notNull(),
99329
99333
  projectId: uuid3("project_id"),
@@ -99376,6 +99380,10 @@ var githubEvents = pgTable(
99376
99380
  "btree",
99377
99381
  table.createdAt.desc()
99378
99382
  ),
99383
+ index("idx_github_events_activity_date").using(
99384
+ "btree",
99385
+ table.activityDate.desc()
99386
+ ),
99379
99387
  index("idx_github_events_ticket_id").on(table.ticketId),
99380
99388
  index("idx_github_events_event_type").on(table.eventType),
99381
99389
  index("idx_github_events_team_date").using(
@@ -99383,6 +99391,11 @@ var githubEvents = pgTable(
99383
99391
  table.teamId,
99384
99392
  table.createdAt.desc()
99385
99393
  ),
99394
+ index("idx_github_events_team_activity_date").using(
99395
+ "btree",
99396
+ table.teamId,
99397
+ table.activityDate.desc()
99398
+ ),
99386
99399
  pgPolicy("Team members can view their github events", {
99387
99400
  as: "permissive",
99388
99401
  for: "select",
@@ -101247,6 +101260,8 @@ var teams = pgTable(
101247
101260
  website: text(),
101248
101261
  documentsEmailId: text("documents_email_id").default("generate_inbox(10)"),
101249
101262
  email: text(),
101263
+ billingFromEmail: text("billing_from_email"),
101264
+ billingFromVerified: boolean3("billing_from_verified").default(false).notNull(),
101250
101265
  inboxEmail: text("inbox_email"),
101251
101266
  inboxForwarding: boolean3("inbox_forwarding").default(true),
101252
101267
  baseCurrency: text("base_currency").default("EUR"),
@@ -107203,6 +107218,67 @@ var TOOLS = [
107203
107218
  required: ["workDescription", "estimatedHours"]
107204
107219
  }
107205
107220
  },
107221
+ {
107222
+ name: "get-time-entries",
107223
+ description: "Read and TOTAL tracked time entries (urenregistratie / tracker entries from the agenda timesheet) so you can answer questions like 'how many hours did I work this month for customer X?'. Scoped to your provider team(s). Filter by period (dateFrom/dateTo, inclusive, Europe/Amsterdam), user (defaults to the API key user; pass 'all' for the whole team), project, customer (matches the event's customer OR its project's customer), ticket, derived status and event type. Worked hours per entry = (endTime - startTime), falling back to the tracked duration. All-day markers and deadlines are excluded (those are agenda items \u2014 see get-calendar-items). Returns accurate aggregate totals over ALL matching entries (totalHours, billableHours, nonBillableHours, invoicedHours, entryCount), an optional grouped breakdown, and a page of detailed entries (entriesTruncated indicates more exist than were listed). Use get-projects / get-customers / get-tickets to resolve ids first.",
107224
+ inputSchema: {
107225
+ type: "object",
107226
+ properties: {
107227
+ teamId: teamIdProp,
107228
+ userId: {
107229
+ type: "string",
107230
+ description: "User whose hours to total. Defaults to the API key user ('me'). Pass a user UUID for someone else, or 'all' for every team member."
107231
+ },
107232
+ projectId: { type: "string", description: "Filter by project UUID." },
107233
+ customerId: {
107234
+ type: "string",
107235
+ description: "Filter by customer UUID. Matches entries whose own customer OR whose project's customer is this customer (project link is the common case)."
107236
+ },
107237
+ ticketId: {
107238
+ type: "string",
107239
+ description: "Only entries linked to this ticket (UUID)."
107240
+ },
107241
+ dateFrom: {
107242
+ type: "string",
107243
+ description: "Inclusive period start (YYYY-MM-DD, Europe/Amsterdam)."
107244
+ },
107245
+ dateTo: {
107246
+ type: "string",
107247
+ description: "Inclusive period end (YYYY-MM-DD, Europe/Amsterdam)."
107248
+ },
107249
+ status: {
107250
+ type: "string",
107251
+ enum: ["draft", "approved", "confirmed", "invoiced", "all"],
107252
+ description: "Derived status filter: draft (not confirmed, not invoiced), approved/confirmed (confirmed, not invoiced), invoiced (linked to an invoice), all (default)."
107253
+ },
107254
+ type: {
107255
+ type: "string",
107256
+ enum: [
107257
+ "meeting",
107258
+ "work",
107259
+ "clocked_work",
107260
+ "appointment",
107261
+ "task",
107262
+ "other"
107263
+ ],
107264
+ description: "Filter by timesheet event type. Omit to include all worked-time types (deadlines/all-day markers are always excluded)."
107265
+ },
107266
+ groupBy: {
107267
+ type: "string",
107268
+ enum: ["none", "day", "project", "customer", "user", "ticket"],
107269
+ default: "none",
107270
+ description: "Return an aggregated breakdown by this dimension (in addition to the overall totals). For 'ticket', an entry's hours are attributed in full to each linked ticket."
107271
+ },
107272
+ pageSize: {
107273
+ type: "number",
107274
+ default: 50,
107275
+ maximum: 200,
107276
+ description: "Max detailed entries returned. Totals/groups always cover ALL matching entries regardless of this limit."
107277
+ }
107278
+ },
107279
+ required: []
107280
+ }
107281
+ },
107206
107282
  {
107207
107283
  name: "get-trips",
107208
107284
  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.",
@@ -114024,6 +114100,338 @@ async function handleLogHours(input) {
114024
114100
  responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
114025
114101
  return { content: [{ type: "text", text: responseText }] };
114026
114102
  }
114103
+ var TIME_ENTRY_TYPES = [
114104
+ "meeting",
114105
+ "work",
114106
+ "clocked_work",
114107
+ "appointment",
114108
+ "task",
114109
+ "other"
114110
+ ];
114111
+ var TIME_ENTRY_STATUSES = [
114112
+ "draft",
114113
+ "approved",
114114
+ "confirmed",
114115
+ "invoiced",
114116
+ "all"
114117
+ ];
114118
+ var TIME_ENTRY_GROUP_BY = [
114119
+ "none",
114120
+ "day",
114121
+ "project",
114122
+ "customer",
114123
+ "user",
114124
+ "ticket"
114125
+ ];
114126
+ var TIMEZONE = "Europe/Amsterdam";
114127
+ function textResponse3(text3) {
114128
+ return { content: [{ type: "text", text: text3 }] };
114129
+ }
114130
+ function jsonResponse(payload) {
114131
+ return {
114132
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
114133
+ };
114134
+ }
114135
+ function toNumber(value) {
114136
+ if (value == null) return 0;
114137
+ if (typeof value === "number") return value;
114138
+ const parsed = Number.parseFloat(String(value));
114139
+ return Number.isFinite(parsed) ? parsed : 0;
114140
+ }
114141
+ function hoursFrom(seconds) {
114142
+ return Math.round(toNumber(seconds) / 3600 * 100) / 100;
114143
+ }
114144
+ function deriveEntryStatus(invoiceId, rawStatus) {
114145
+ if (invoiceId) return "invoiced";
114146
+ if (rawStatus === "confirmed") return "approved";
114147
+ return "draft";
114148
+ }
114149
+ function durationSecondsExpr() {
114150
+ const te = schema_exports.timesheetEvents;
114151
+ return sql`CASE WHEN ${te.endTime} IS NOT NULL THEN GREATEST(0, EXTRACT(EPOCH FROM (${te.endTime} - ${te.startTime}))) ELSE COALESCE(${te.trackedDuration}, 0) END`;
114152
+ }
114153
+ function localDateExpr() {
114154
+ return sql`(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})::date`;
114155
+ }
114156
+ function localDateTextExpr() {
114157
+ return sql`to_char(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)}, 'YYYY-MM-DD')`;
114158
+ }
114159
+ function effectiveCustomerIdExpr() {
114160
+ return sql`COALESCE(${schema_exports.timesheetEvents.customerId}, ${schema_exports.projects.customerId})`;
114161
+ }
114162
+ async function buildTimeEntryGroups(groupBy, whereExpr) {
114163
+ const te = schema_exports.timesheetEvents;
114164
+ const totalSecondsSel = sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`;
114165
+ const billableSecondsSel = sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`;
114166
+ const countSel = sql`count(*)::int`;
114167
+ const finish = (rows2) => rows2.sort((a6, b7) => b7.totalHours - a6.totalHours);
114168
+ if (groupBy === "day") {
114169
+ const dayExpr = localDateTextExpr();
114170
+ const rows2 = await db.select({
114171
+ day: sql`${dayExpr}`,
114172
+ totalSeconds: totalSecondsSel,
114173
+ billableSeconds: billableSecondsSel,
114174
+ entryCount: countSel
114175
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(dayExpr);
114176
+ return finish(
114177
+ rows2.map((r6) => ({
114178
+ key: { type: "day", date: r6.day },
114179
+ entryCount: Number(r6.entryCount),
114180
+ totalHours: hoursFrom(r6.totalSeconds),
114181
+ billableHours: hoursFrom(r6.billableSeconds)
114182
+ }))
114183
+ );
114184
+ }
114185
+ if (groupBy === "project") {
114186
+ const rows2 = await db.select({
114187
+ projectId: te.projectId,
114188
+ projectName: schema_exports.projects.name,
114189
+ totalSeconds: totalSecondsSel,
114190
+ billableSeconds: billableSecondsSel,
114191
+ entryCount: countSel
114192
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(te.projectId, schema_exports.projects.name);
114193
+ return finish(
114194
+ rows2.map((r6) => ({
114195
+ key: {
114196
+ type: "project",
114197
+ id: r6.projectId,
114198
+ name: r6.projectId ? r6.projectName : "(no project)"
114199
+ },
114200
+ entryCount: Number(r6.entryCount),
114201
+ totalHours: hoursFrom(r6.totalSeconds),
114202
+ billableHours: hoursFrom(r6.billableSeconds)
114203
+ }))
114204
+ );
114205
+ }
114206
+ if (groupBy === "customer") {
114207
+ const custId = effectiveCustomerIdExpr();
114208
+ const rows2 = await db.select({
114209
+ customerId: sql`${custId}`,
114210
+ customerName: schema_exports.customers.name,
114211
+ totalSeconds: totalSecondsSel,
114212
+ billableSeconds: billableSecondsSel,
114213
+ entryCount: countSel
114214
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(schema_exports.customers, sql`${schema_exports.customers.id} = ${custId}`).where(whereExpr).groupBy(custId, schema_exports.customers.name);
114215
+ return finish(
114216
+ rows2.map((r6) => ({
114217
+ key: {
114218
+ type: "customer",
114219
+ id: r6.customerId,
114220
+ name: r6.customerId ? r6.customerName : "(no customer)"
114221
+ },
114222
+ entryCount: Number(r6.entryCount),
114223
+ totalHours: hoursFrom(r6.totalSeconds),
114224
+ billableHours: hoursFrom(r6.billableSeconds)
114225
+ }))
114226
+ );
114227
+ }
114228
+ if (groupBy === "user") {
114229
+ const rows2 = await db.select({
114230
+ userId: te.userId,
114231
+ userName: schema_exports.users.fullName,
114232
+ totalSeconds: totalSecondsSel,
114233
+ billableSeconds: billableSecondsSel,
114234
+ entryCount: countSel
114235
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(schema_exports.users, eq(te.userId, schema_exports.users.id)).where(whereExpr).groupBy(te.userId, schema_exports.users.fullName);
114236
+ return finish(
114237
+ rows2.map((r6) => ({
114238
+ key: { type: "user", id: r6.userId, name: r6.userName },
114239
+ entryCount: Number(r6.entryCount),
114240
+ totalHours: hoursFrom(r6.totalSeconds),
114241
+ billableHours: hoursFrom(r6.billableSeconds)
114242
+ }))
114243
+ );
114244
+ }
114245
+ const tet = schema_exports.timesheetEventTickets;
114246
+ const rows = await db.select({
114247
+ ticketId: schema_exports.tickets.id,
114248
+ ticketNumber: schema_exports.tickets.ticketNumber,
114249
+ totalSeconds: totalSecondsSel,
114250
+ billableSeconds: billableSecondsSel,
114251
+ entryCount: countSel
114252
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(tet, eq(tet.timesheetEventId, te.id)).leftJoin(schema_exports.tickets, eq(schema_exports.tickets.id, tet.ticketId)).where(whereExpr).groupBy(schema_exports.tickets.id, schema_exports.tickets.ticketNumber);
114253
+ return finish(
114254
+ rows.map((r6) => ({
114255
+ key: {
114256
+ type: "ticket",
114257
+ id: r6.ticketId,
114258
+ ticketNumber: r6.ticketId ? r6.ticketNumber : "(no ticket)"
114259
+ },
114260
+ entryCount: Number(r6.entryCount),
114261
+ totalHours: hoursFrom(r6.totalSeconds),
114262
+ billableHours: hoursFrom(r6.billableSeconds)
114263
+ }))
114264
+ );
114265
+ }
114266
+ async function handleGetTimeEntries(input) {
114267
+ const ctx = getAuthContext();
114268
+ const te = schema_exports.timesheetEvents;
114269
+ const status = input.status ?? "all";
114270
+ if (!TIME_ENTRY_STATUSES.includes(status)) {
114271
+ return textResponse3(
114272
+ `Error: invalid status "${input.status}". Allowed: ${TIME_ENTRY_STATUSES.join(", ")}.`
114273
+ );
114274
+ }
114275
+ const groupBy = input.groupBy ?? "none";
114276
+ if (!TIME_ENTRY_GROUP_BY.includes(groupBy)) {
114277
+ return textResponse3(
114278
+ `Error: invalid groupBy "${input.groupBy}". Allowed: ${TIME_ENTRY_GROUP_BY.join(", ")}.`
114279
+ );
114280
+ }
114281
+ if (input.type && !TIME_ENTRY_TYPES.includes(input.type)) {
114282
+ return textResponse3(
114283
+ `Error: invalid type "${input.type}". Allowed: ${TIME_ENTRY_TYPES.join(", ")}. (Deadlines/all-day markers are agenda items \u2014 use get-calendar-items.)`
114284
+ );
114285
+ }
114286
+ const scope = await resolveTeamScope(input.teamId);
114287
+ if (!scope.ok) return scope.response;
114288
+ if (scope.teamIds.length === 0) {
114289
+ return textResponse3("No accessible teams found.");
114290
+ }
114291
+ if (input.projectId && !scope.projectIds.includes(input.projectId)) {
114292
+ return textResponse3(
114293
+ `Project not found or no access: ${input.projectId}. Call get-projects first.`
114294
+ );
114295
+ }
114296
+ if (input.customerId && !scope.customerIds.includes(input.customerId)) {
114297
+ return textResponse3(
114298
+ `Customer not found or no access: ${input.customerId}. Call get-customers first.`
114299
+ );
114300
+ }
114301
+ const rawUserId = input.userId?.trim();
114302
+ const userId = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
114303
+ const conditions = [
114304
+ inArray(te.teamId, scope.teamIds),
114305
+ eq(te.isDeleted, false),
114306
+ // Worked time only: drop all-day markers / deadlines (~24h agenda blocks
114307
+ // that would otherwise massively inflate the totals).
114308
+ eq(te.allDay, false),
114309
+ sql`${te.type}::text <> 'deadline'`
114310
+ ];
114311
+ if (userId !== "all") conditions.push(eq(te.userId, userId));
114312
+ if (input.projectId) conditions.push(eq(te.projectId, input.projectId));
114313
+ if (input.customerId) {
114314
+ conditions.push(
114315
+ sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId}) = ${input.customerId}`
114316
+ );
114317
+ }
114318
+ if (input.dateFrom) {
114319
+ conditions.push(sql`${localDateExpr()} >= ${input.dateFrom}::date`);
114320
+ }
114321
+ if (input.dateTo) {
114322
+ conditions.push(sql`${localDateExpr()} <= ${input.dateTo}::date`);
114323
+ }
114324
+ if (input.type) conditions.push(sql`${te.type}::text = ${input.type}`);
114325
+ if (input.ticketId) {
114326
+ conditions.push(
114327
+ sql`EXISTS (SELECT 1 FROM ${schema_exports.timesheetEventTickets} WHERE ${schema_exports.timesheetEventTickets.timesheetEventId} = ${te.id} AND ${schema_exports.timesheetEventTickets.ticketId} = ${input.ticketId})`
114328
+ );
114329
+ }
114330
+ if (status === "draft") {
114331
+ conditions.push(sql`(${te.status}::text = 'draft' AND ${te.invoiceId} IS NULL)`);
114332
+ } else if (status === "approved" || status === "confirmed") {
114333
+ conditions.push(
114334
+ sql`(${te.status}::text = 'confirmed' AND ${te.invoiceId} IS NULL)`
114335
+ );
114336
+ } else if (status === "invoiced") {
114337
+ conditions.push(sql`${te.invoiceId} IS NOT NULL`);
114338
+ }
114339
+ const whereExpr = and(...conditions);
114340
+ const [totalsRow] = await db.select({
114341
+ count: sql`count(*)::int`,
114342
+ totalSeconds: sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`,
114343
+ billableSeconds: sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`,
114344
+ invoicedSeconds: sql`COALESCE(SUM(CASE WHEN ${te.invoiceId} IS NOT NULL THEN ${durationSecondsExpr()} ELSE 0 END), 0)`
114345
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr);
114346
+ const totalSeconds = toNumber(totalsRow?.totalSeconds);
114347
+ const billableSeconds = toNumber(totalsRow?.billableSeconds);
114348
+ const invoicedSeconds = toNumber(totalsRow?.invoicedSeconds);
114349
+ const entryCount = Number(totalsRow?.count ?? 0);
114350
+ const pageSize = Math.min(input.pageSize ?? 50, 200);
114351
+ const entryRows = await db.select({
114352
+ id: te.id,
114353
+ title: te.title,
114354
+ description: te.description,
114355
+ type: te.type,
114356
+ rawStatus: te.status,
114357
+ billingStatus: te.billingStatus,
114358
+ invoiceId: te.invoiceId,
114359
+ startTime: te.startTime,
114360
+ endTime: te.endTime,
114361
+ date: sql`${localDateTextExpr()}`,
114362
+ durationSeconds: sql`${durationSecondsExpr()}`,
114363
+ userId: te.userId,
114364
+ userName: schema_exports.users.fullName,
114365
+ projectId: te.projectId,
114366
+ projectName: schema_exports.projects.name,
114367
+ customerId: sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`,
114368
+ customerName: schema_exports.customers.name
114369
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(schema_exports.users, eq(te.userId, schema_exports.users.id)).leftJoin(
114370
+ schema_exports.customers,
114371
+ sql`${schema_exports.customers.id} = COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`
114372
+ ).where(whereExpr).orderBy(desc(te.startTime)).limit(pageSize);
114373
+ const entryIds = entryRows.map((r6) => r6.id);
114374
+ const ticketsByEvent = /* @__PURE__ */ new Map();
114375
+ if (entryIds.length > 0) {
114376
+ const links = await db.select({
114377
+ eventId: schema_exports.timesheetEventTickets.timesheetEventId,
114378
+ ticketId: schema_exports.tickets.id,
114379
+ ticketNumber: schema_exports.tickets.ticketNumber
114380
+ }).from(schema_exports.timesheetEventTickets).innerJoin(
114381
+ schema_exports.tickets,
114382
+ eq(schema_exports.timesheetEventTickets.ticketId, schema_exports.tickets.id)
114383
+ ).where(inArray(schema_exports.timesheetEventTickets.timesheetEventId, entryIds));
114384
+ for (const link of links) {
114385
+ const list = ticketsByEvent.get(link.eventId) ?? [];
114386
+ list.push({ id: link.ticketId, ticketNumber: link.ticketNumber });
114387
+ ticketsByEvent.set(link.eventId, list);
114388
+ }
114389
+ }
114390
+ const entries = entryRows.map((r6) => ({
114391
+ id: r6.id,
114392
+ date: r6.date,
114393
+ startTime: r6.startTime,
114394
+ endTime: r6.endTime,
114395
+ hours: hoursFrom(r6.durationSeconds),
114396
+ title: r6.title,
114397
+ description: r6.description,
114398
+ type: r6.type,
114399
+ status: deriveEntryStatus(r6.invoiceId, r6.rawStatus),
114400
+ billingStatus: r6.billingStatus,
114401
+ invoiced: r6.invoiceId != null,
114402
+ invoiceId: r6.invoiceId,
114403
+ user: r6.userId ? { id: r6.userId, name: r6.userName } : null,
114404
+ project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
114405
+ customer: r6.customerId ? { id: r6.customerId, name: r6.customerName } : null,
114406
+ tickets: ticketsByEvent.get(r6.id) ?? []
114407
+ }));
114408
+ const groups = groupBy === "none" ? void 0 : await buildTimeEntryGroups(groupBy, whereExpr);
114409
+ return jsonResponse({
114410
+ filters: {
114411
+ userId: userId === "all" ? "all" : userId,
114412
+ teamIds: scope.teamIds,
114413
+ projectId: input.projectId ?? null,
114414
+ customerId: input.customerId ?? null,
114415
+ ticketId: input.ticketId ?? null,
114416
+ dateFrom: input.dateFrom ?? null,
114417
+ dateTo: input.dateTo ?? null,
114418
+ status,
114419
+ type: input.type ?? null,
114420
+ timezone: TIMEZONE
114421
+ },
114422
+ totals: {
114423
+ totalHours: hoursFrom(totalSeconds),
114424
+ billableHours: hoursFrom(billableSeconds),
114425
+ nonBillableHours: hoursFrom(totalSeconds - billableSeconds),
114426
+ invoicedHours: hoursFrom(invoicedSeconds),
114427
+ entryCount
114428
+ },
114429
+ groupBy,
114430
+ ...groups ? { groups } : {},
114431
+ entries,
114432
+ entriesTruncated: entryCount > entries.length
114433
+ });
114434
+ }
114027
114435
 
114028
114436
  // ../invoice/src/utils/included-items.ts
114029
114437
  function parseIncludedItems(value) {
@@ -114397,7 +114805,7 @@ var INVOICE_STATUSES = [
114397
114805
  "refunded"
114398
114806
  ];
114399
114807
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
114400
- function textResponse3(text3) {
114808
+ function textResponse4(text3) {
114401
114809
  return { content: [{ type: "text", text: text3 }] };
114402
114810
  }
114403
114811
  function tiptapNote(text3) {
@@ -114515,7 +114923,7 @@ async function resolveInvoiceLineItems(inputs, defaults, teamId) {
114515
114923
  return { items };
114516
114924
  }
114517
114925
  function notDraftResponse(invoice) {
114518
- return textResponse3(
114926
+ return textResponse4(
114519
114927
  `Invoice ${invoice.invoiceNumber ?? invoice.id} has status "${invoice.status}", not "draft". These tools only modify draft invoices \u2014 sent/paid/unpaid/overdue invoices are immutable here.`
114520
114928
  );
114521
114929
  }
@@ -114532,14 +114940,14 @@ Issue: ${invoice.issueDate ? new Date(invoice.issueDate).toLocaleDateString() :
114532
114940
  async function handleGetInvoices(input) {
114533
114941
  const { customerId, status, q: q3, pageSize = 20 } = input;
114534
114942
  if (status && !INVOICE_STATUSES.includes(status)) {
114535
- return textResponse3(
114943
+ return textResponse4(
114536
114944
  `Error: invalid status "${status}". Allowed: ${INVOICE_STATUSES.join(", ")}.`
114537
114945
  );
114538
114946
  }
114539
114947
  const scope = await resolveTeamScope(input.teamId);
114540
114948
  if (!scope.ok) return scope.response;
114541
114949
  if (scope.teamIds.length === 0) {
114542
- return textResponse3("No accessible teams found.");
114950
+ return textResponse4("No accessible teams found.");
114543
114951
  }
114544
114952
  const filters = [inArray(schema_exports.invoices.teamId, scope.teamIds)];
114545
114953
  if (customerId) filters.push(eq(schema_exports.invoices.customerId, customerId));
@@ -114566,7 +114974,7 @@ async function handleGetInvoices(input) {
114566
114974
  createdAt: schema_exports.invoices.createdAt
114567
114975
  }).from(schema_exports.invoices).where(and(...filters)).orderBy(desc(schema_exports.invoices.createdAt)).limit(Math.min(pageSize, 100));
114568
114976
  if (rows.length === 0) {
114569
- return textResponse3("No invoices found.");
114977
+ return textResponse4("No invoices found.");
114570
114978
  }
114571
114979
  const list = rows.map(
114572
114980
  (inv) => `**${inv.invoiceNumber ?? "(draft, no number)"}**
@@ -114576,7 +114984,7 @@ Customer: ${inv.customerName ?? inv.customerId ?? "(none)"}
114576
114984
  Issue date: ${inv.issueDate ? new Date(inv.issueDate).toLocaleDateString() : "-"} | Due: ${inv.dueDate ? new Date(inv.dueDate).toLocaleDateString() : "-"}
114577
114985
  `
114578
114986
  ).join("\n");
114579
- return textResponse3(
114987
+ return textResponse4(
114580
114988
  `Found ${rows.length} invoices:
114581
114989
 
114582
114990
  ${list}
@@ -114585,15 +114993,15 @@ Use \`get-invoice-by-id\` for line items and linked documents. Use \`link-docume
114585
114993
  }
114586
114994
  async function handleGetInvoiceById(input) {
114587
114995
  const { invoiceId } = input;
114588
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
114996
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
114589
114997
  const scope = await resolveTeamScope(input.teamId);
114590
114998
  if (!scope.ok) return scope.response;
114591
114999
  if (scope.teamIds.length === 0) {
114592
- return textResponse3("No accessible teams found.");
115000
+ return textResponse4("No accessible teams found.");
114593
115001
  }
114594
115002
  const invoice = await loadInvoiceByIdentifier(invoiceId, scope.teamIds);
114595
115003
  if (!invoice) {
114596
- return textResponse3(
115004
+ return textResponse4(
114597
115005
  `Invoice ${invoiceId} not found or you don't have access to it.`
114598
115006
  );
114599
115007
  }
@@ -114610,7 +115018,7 @@ async function handleGetInvoiceById(input) {
114610
115018
  );
114611
115019
  const linesText = lineItems.length > 0 ? lineItems.map((line2, i6) => formatLineItemDetail(line2, i6)).join("\n\n") : "(no line items)";
114612
115020
  const docsText = linkedDocs.length > 0 ? linkedDocs.map((d6) => `- ${d6.title} (${d6.type ?? "document"}) \u2014 ${d6.id}`).join("\n") : "(none)";
114613
- return textResponse3(
115021
+ return textResponse4(
114614
115022
  `**Invoice ${invoice.invoiceNumber ?? invoice.id}**
114615
115023
 
114616
115024
  ID: ${invoice.id}
@@ -114632,12 +115040,12 @@ ${docsText}
114632
115040
  }
114633
115041
  async function handleUpdateInvoice(input) {
114634
115042
  const { invoiceId } = input;
114635
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
115043
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
114636
115044
  const resolved = await resolveTeamId(input.teamId);
114637
115045
  if (!resolved.ok) return resolved.response;
114638
115046
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114639
115047
  if (!invoice) {
114640
- return textResponse3(
115048
+ return textResponse4(
114641
115049
  `Invoice ${invoiceId} not found or not owned by this team.`
114642
115050
  );
114643
115051
  }
@@ -114664,7 +115072,7 @@ async function handleUpdateInvoice(input) {
114664
115072
  defaults,
114665
115073
  invoice.teamId
114666
115074
  );
114667
- if (error49) return textResponse3(`Error: ${error49}`);
115075
+ if (error49) return textResponse4(`Error: ${error49}`);
114668
115076
  const totals = computeInvoiceTotals(
114669
115077
  items,
114670
115078
  defaults,
@@ -114677,28 +115085,28 @@ async function handleUpdateInvoice(input) {
114677
115085
  updates.amount = totals.amount;
114678
115086
  }
114679
115087
  if (Object.keys(updates).length === 0) {
114680
- return textResponse3(
115088
+ return textResponse4(
114681
115089
  "No fields to update. Provide at least one of: title, note, internalNote, dueDate, issueDate, lineItems."
114682
115090
  );
114683
115091
  }
114684
115092
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
114685
115093
  const [updated] = await db.update(schema_exports.invoices).set(updates).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114686
- if (!updated) return textResponse3(`Failed to update invoice ${invoiceId}.`);
114687
- return textResponse3(`\u2705 **Draft invoice updated**
115094
+ if (!updated) return textResponse4(`Failed to update invoice ${invoiceId}.`);
115095
+ return textResponse4(`\u2705 **Draft invoice updated**
114688
115096
 
114689
115097
  ${formatInvoiceSummary(updated)}`);
114690
115098
  }
114691
115099
  async function handleUpdateInvoiceLines(input) {
114692
115100
  const { invoiceId, lineItems: patches } = input;
114693
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
115101
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
114694
115102
  if (!patches || patches.length === 0) {
114695
- return textResponse3("Error: `lineItems` must be a non-empty array.");
115103
+ return textResponse4("Error: `lineItems` must be a non-empty array.");
114696
115104
  }
114697
115105
  const resolved = await resolveTeamId(input.teamId);
114698
115106
  if (!resolved.ok) return resolved.response;
114699
115107
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114700
115108
  if (!invoice) {
114701
- return textResponse3(
115109
+ return textResponse4(
114702
115110
  `Invoice ${invoiceId} not found or not owned by this team.`
114703
115111
  );
114704
115112
  }
@@ -114712,7 +115120,7 @@ async function handleUpdateInvoiceLines(input) {
114712
115120
  for (const patch of patches) {
114713
115121
  const index2 = patch.index;
114714
115122
  if (index2 < 0 || index2 >= items.length) {
114715
- return textResponse3(
115123
+ return textResponse4(
114716
115124
  `Error: line index ${index2} is out of range (invoice has ${items.length} line(s)).`
114717
115125
  );
114718
115126
  }
@@ -114739,13 +115147,13 @@ async function handleUpdateInvoiceLines(input) {
114739
115147
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
114740
115148
  }).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114741
115149
  if (!updated) {
114742
- return textResponse3(`Failed to update invoice lines for ${invoiceId}.`);
115150
+ return textResponse4(`Failed to update invoice lines for ${invoiceId}.`);
114743
115151
  }
114744
115152
  const changedLines = patches.map((p3) => {
114745
115153
  const line2 = items[p3.index];
114746
115154
  return `[${p3.index}] ${plainTextFromLineItemName(line2.name)}`;
114747
115155
  }).join("\n");
114748
- return textResponse3(
115156
+ return textResponse4(
114749
115157
  `\u2705 **Updated ${updatedCount} line item(s) on draft invoice ${updated.invoiceNumber ?? updated.id}**
114750
115158
 
114751
115159
  ${changedLines}
@@ -114755,13 +115163,13 @@ New total: ${updated.amount} ${updated.currency} (subtotal ${updated.subtotal},
114755
115163
  }
114756
115164
  async function handleAddProductToInvoice(input) {
114757
115165
  const { invoiceId, productId } = input;
114758
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
114759
- if (!productId) return textResponse3("Error: `productId` is required.");
115166
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
115167
+ if (!productId) return textResponse4("Error: `productId` is required.");
114760
115168
  const resolved = await resolveTeamId(input.teamId);
114761
115169
  if (!resolved.ok) return resolved.response;
114762
115170
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114763
115171
  if (!invoice) {
114764
- return textResponse3(
115172
+ return textResponse4(
114765
115173
  `Invoice ${invoiceId} not found or not owned by this team.`
114766
115174
  );
114767
115175
  }
@@ -114769,7 +115177,7 @@ async function handleAddProductToInvoice(input) {
114769
115177
  const products = await loadProductsInTeam([productId], invoice.teamId);
114770
115178
  const product = products.get(productId);
114771
115179
  if (!product) {
114772
- return textResponse3(
115180
+ return textResponse4(
114773
115181
  `Product ${productId} not found or not owned by this team.`
114774
115182
  );
114775
115183
  }
@@ -114801,9 +115209,9 @@ async function handleAddProductToInvoice(input) {
114801
115209
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
114802
115210
  }).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114803
115211
  if (!updated) {
114804
- return textResponse3(`Failed to add product to invoice ${invoiceId}.`);
115212
+ return textResponse4(`Failed to add product to invoice ${invoiceId}.`);
114805
115213
  }
114806
- return textResponse3(
115214
+ return textResponse4(
114807
115215
  `\u2705 **Product added to draft invoice ${updated.invoiceNumber ?? updated.id}**
114808
115216
 
114809
115217
  ` + formatLineItemDetail(newItem, items.length - 1) + `
@@ -114815,12 +115223,12 @@ Clause and pricing variant are snapshotted on the line \u2014 later catalog edit
114815
115223
  async function handleLinkDocumentToInvoice(input) {
114816
115224
  const { documentId, invoiceId } = input;
114817
115225
  if (!documentId) {
114818
- return textResponse3("Error: `documentId` is required.");
115226
+ return textResponse4("Error: `documentId` is required.");
114819
115227
  }
114820
115228
  const scope = await resolveTeamScope(input.teamId);
114821
115229
  if (!scope.ok) return scope.response;
114822
115230
  if (scope.teamIds.length === 0) {
114823
- return textResponse3("No accessible teams found.");
115231
+ return textResponse4("No accessible teams found.");
114824
115232
  }
114825
115233
  const [doc] = await db.select({
114826
115234
  id: schema_exports.documents.id,
@@ -114835,24 +115243,24 @@ async function handleLinkDocumentToInvoice(input) {
114835
115243
  )
114836
115244
  ).limit(1);
114837
115245
  if (!doc) {
114838
- return textResponse3(
115246
+ return textResponse4(
114839
115247
  `Document ${documentId} not found or you don't have access to it.`
114840
115248
  );
114841
115249
  }
114842
115250
  if (!invoiceId) {
114843
115251
  await db.update(schema_exports.documents).set({ invoiceId: null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
114844
- return textResponse3(
115252
+ return textResponse4(
114845
115253
  `\u2705 Document "${doc.title}" (${doc.id}) is unlinked from its invoice.`
114846
115254
  );
114847
115255
  }
114848
115256
  const invoice = await findAccessibleInvoice(invoiceId, [doc.teamId]);
114849
115257
  if (!invoice) {
114850
- return textResponse3(
115258
+ return textResponse4(
114851
115259
  `Error: invoice ${invoiceId} not found in team ${doc.teamId}. Use get-invoices to find a valid invoice id.`
114852
115260
  );
114853
115261
  }
114854
115262
  await db.update(schema_exports.documents).set({ invoiceId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
114855
- return textResponse3(
115263
+ return textResponse4(
114856
115264
  `\u2705 **Document linked to invoice!**
114857
115265
 
114858
115266
  Document: ${doc.title} (${doc.id})
@@ -114985,7 +115393,7 @@ ${description ? `Description: ${description}
114985
115393
  ]
114986
115394
  };
114987
115395
  }
114988
- function textResponse4(text3) {
115396
+ function textResponse5(text3) {
114989
115397
  return { content: [{ type: "text", text: text3 }] };
114990
115398
  }
114991
115399
  function memberLabel(m4) {
@@ -114999,7 +115407,7 @@ async function requireTeamOwner2(teamId, userId) {
114999
115407
  eq(schema_exports.usersOnTeam.teamId, teamId)
115000
115408
  )
115001
115409
  ).limit(1);
115002
- return membership?.role === "owner" ? null : textResponse4(OWNER_REQUIRED);
115410
+ return membership?.role === "owner" ? null : textResponse5(OWNER_REQUIRED);
115003
115411
  }
115004
115412
  async function setProjectMemberAccess(params) {
115005
115413
  const { projectId, teamId, memberIds, createdBy } = params;
@@ -115103,7 +115511,7 @@ async function resolveTeamMember(teamId, opts) {
115103
115511
  if (!match) {
115104
115512
  return {
115105
115513
  ok: false,
115106
- response: textResponse4(
115514
+ response: textResponse5(
115107
115515
  `User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
115108
115516
  )
115109
115517
  };
@@ -115116,7 +115524,7 @@ async function resolveTeamMember(teamId, opts) {
115116
115524
  if (matches.length === 0) {
115117
115525
  return {
115118
115526
  ok: false,
115119
- response: textResponse4(
115527
+ response: textResponse5(
115120
115528
  `No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
115121
115529
  )
115122
115530
  };
@@ -115124,7 +115532,7 @@ async function resolveTeamMember(teamId, opts) {
115124
115532
  if (matches.length > 1) {
115125
115533
  return {
115126
115534
  ok: false,
115127
- response: textResponse4(
115535
+ response: textResponse5(
115128
115536
  `Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
115129
115537
  )
115130
115538
  };
@@ -115133,7 +115541,7 @@ async function resolveTeamMember(teamId, opts) {
115133
115541
  }
115134
115542
  return {
115135
115543
  ok: false,
115136
- response: textResponse4(
115544
+ response: textResponse5(
115137
115545
  "Provide either a userId or an email to identify the member."
115138
115546
  )
115139
115547
  };
@@ -115182,7 +115590,7 @@ async function handleUpdateProject(input) {
115182
115590
  if (!resolved.ok) return resolved.response;
115183
115591
  const existing = await loadProjectInTeam(id, resolved.teamId);
115184
115592
  if (!existing) {
115185
- return textResponse4(
115593
+ return textResponse5(
115186
115594
  `Project ${id} not found, or it is not owned by this team.`
115187
115595
  );
115188
115596
  }
@@ -115197,7 +115605,7 @@ async function handleUpdateProject(input) {
115197
115605
  )
115198
115606
  ).limit(1);
115199
115607
  if (dupe) {
115200
- return textResponse4(
115608
+ return textResponse5(
115201
115609
  `A project named "${input.name}" already exists in this team. Choose a different name.`
115202
115610
  );
115203
115611
  }
@@ -115262,7 +115670,7 @@ async function handleUpdateProject(input) {
115262
115670
  customerName: schema_exports.customers.name
115263
115671
  }).from(schema_exports.projects).leftJoin(schema_exports.customers, eq(schema_exports.projects.customerId, schema_exports.customers.id)).where(eq(schema_exports.projects.id, id)).limit(1);
115264
115672
  if (!updated) {
115265
- return textResponse4(`Failed to update project ${id}.`);
115673
+ return textResponse5(`Failed to update project ${id}.`);
115266
115674
  }
115267
115675
  const lines = [
115268
115676
  "\u2705 **Project Updated**",
@@ -115280,7 +115688,7 @@ async function handleUpdateProject(input) {
115280
115688
  if (willRename) {
115281
115689
  lines.push("", "Note: tickets for this project were renumbered.");
115282
115690
  }
115283
- return textResponse4(lines.join("\n"));
115691
+ return textResponse5(lines.join("\n"));
115284
115692
  }
115285
115693
  async function handleGetProjectMembers(input) {
115286
115694
  const { projectId } = input;
@@ -115288,7 +115696,7 @@ async function handleGetProjectMembers(input) {
115288
115696
  if (!resolved.ok) return resolved.response;
115289
115697
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115290
115698
  if (!project) {
115291
- return textResponse4(
115699
+ return textResponse5(
115292
115700
  `Project ${projectId} not found, or it is not owned by this team.`
115293
115701
  );
115294
115702
  }
@@ -115317,7 +115725,7 @@ async function handleGetProjectMembers(input) {
115317
115725
  return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
115318
115726
  }).join("\n");
115319
115727
  const note = state2.projectMemberIds.size === 0 ? "No members are explicitly assigned to this project, so every owner and every unrestricted member can see it." : `${state2.projectMemberIds.size} member(s) are explicitly assigned to this project.`;
115320
- return textResponse4(
115728
+ return textResponse5(
115321
115729
  `**Project members for "${project.name}"** (ID: ${project.id})
115322
115730
 
115323
115731
  ${note}
@@ -115338,7 +115746,7 @@ async function handleSetProjectMembers(input) {
115338
115746
  if (ownerError) return ownerError;
115339
115747
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115340
115748
  if (!project) {
115341
- return textResponse4(
115749
+ return textResponse5(
115342
115750
  `Project ${projectId} not found, or it is not owned by this team.`
115343
115751
  );
115344
115752
  }
@@ -115376,7 +115784,7 @@ async function handleSetProjectMembers(input) {
115376
115784
 
115377
115785
  \u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
115378
115786
  }
115379
- return textResponse4(
115787
+ return textResponse5(
115380
115788
  `\u2705 **Project members updated**
115381
115789
 
115382
115790
  Members with explicit access to this project:
@@ -115392,7 +115800,7 @@ async function handleAddProjectMember(input) {
115392
115800
  if (ownerError) return ownerError;
115393
115801
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115394
115802
  if (!project) {
115395
- return textResponse4(
115803
+ return textResponse5(
115396
115804
  `Project ${projectId} not found, or it is not owned by this team.`
115397
115805
  );
115398
115806
  }
@@ -115403,7 +115811,7 @@ async function handleAddProjectMember(input) {
115403
115811
  if (!member2.ok) return member2.response;
115404
115812
  const state2 = await getProjectAccessState(resolved.teamId, projectId);
115405
115813
  if (state2.projectMemberIds.has(member2.member.userId)) {
115406
- return textResponse4(
115814
+ return textResponse5(
115407
115815
  `${memberLabel(member2.member)} already has explicit access to this project.`
115408
115816
  );
115409
115817
  }
@@ -115418,7 +115826,7 @@ async function handleAddProjectMember(input) {
115418
115826
  if (wasUnrestricted) {
115419
115827
  text3 += "\n\n\u26A0\uFE0F This member previously had no access restrictions (they could see all projects). They are now restricted to ONLY the projects explicitly assigned to them. Grant any other projects they still need with add-project-member, or remove all their assignments to restore full visibility.";
115420
115828
  }
115421
- return textResponse4(text3);
115829
+ return textResponse5(text3);
115422
115830
  }
115423
115831
  async function handleRemoveProjectMember(input) {
115424
115832
  const ctx = getAuthContext();
@@ -115429,7 +115837,7 @@ async function handleRemoveProjectMember(input) {
115429
115837
  if (ownerError) return ownerError;
115430
115838
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115431
115839
  if (!project) {
115432
- return textResponse4(
115840
+ return textResponse5(
115433
115841
  `Project ${projectId} not found, or it is not owned by this team.`
115434
115842
  );
115435
115843
  }
@@ -115440,7 +115848,7 @@ async function handleRemoveProjectMember(input) {
115440
115848
  if (!member2.ok) return member2.response;
115441
115849
  const state2 = await getProjectAccessState(resolved.teamId, projectId);
115442
115850
  if (!state2.projectMemberIds.has(member2.member.userId)) {
115443
- return textResponse4(
115851
+ return textResponse5(
115444
115852
  `${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
115445
115853
  );
115446
115854
  }
@@ -115456,7 +115864,7 @@ async function handleRemoveProjectMember(input) {
115456
115864
  if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
115457
115865
  text3 += "\n\nThis was the member's last project assignment, so their access restrictions were cleared \u2014 they can see all projects in the team again (default behavior).";
115458
115866
  }
115459
- return textResponse4(text3);
115867
+ return textResponse5(text3);
115460
115868
  }
115461
115869
  async function loadProjectForCleanup(projectId, teamId) {
115462
115870
  const accessibleTeamIds = await getAccessibleTeamIds(teamId);
@@ -115484,25 +115892,25 @@ async function countProjectDependencies(projectId) {
115484
115892
  }
115485
115893
  async function handleArchiveProject(input) {
115486
115894
  const { projectId, reason } = input;
115487
- if (!projectId) return textResponse4("Error: `projectId` is required.");
115895
+ if (!projectId) return textResponse5("Error: `projectId` is required.");
115488
115896
  const resolved = await resolveTeamId(input.teamId);
115489
115897
  if (!resolved.ok) return resolved.response;
115490
115898
  const project = await loadProjectForCleanup(projectId, resolved.teamId);
115491
115899
  if (!project) {
115492
- return textResponse4(
115900
+ return textResponse5(
115493
115901
  `Project ${projectId} not found, or it is not owned by this team.`
115494
115902
  );
115495
115903
  }
115496
115904
  const state2 = getProjectArchiveState(project.settings);
115497
115905
  if (state2.archived) {
115498
- return textResponse4(
115906
+ return textResponse5(
115499
115907
  `Project "${project.name}" (${project.id}) is already archived${state2.archivedAt ? ` (since ${state2.archivedAt})` : ""}.`
115500
115908
  );
115501
115909
  }
115502
115910
  const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
115503
115911
  const nextSettings = withArchiveSettings(project.settings, archivedAt, reason);
115504
115912
  await db.update(schema_exports.projects).set({ settings: nextSettings, updatedAt: sql`now()` }).where(eq(schema_exports.projects.id, project.id));
115505
- return textResponse4(
115913
+ return textResponse5(
115506
115914
  `\u2705 **Project archived**
115507
115915
 
115508
115916
  Project: ${project.name}
@@ -115520,21 +115928,21 @@ Note: the archive flag is stored in \`projects.settings.archivedAt\`; the dashbo
115520
115928
  async function handleDeleteProject(input) {
115521
115929
  const ctx = getAuthContext();
115522
115930
  const { projectId, confirmEmptyOnly } = input;
115523
- if (!projectId) return textResponse4("Error: `projectId` is required.");
115931
+ if (!projectId) return textResponse5("Error: `projectId` is required.");
115524
115932
  const resolved = await resolveTeamId(input.teamId);
115525
115933
  if (!resolved.ok) return resolved.response;
115526
115934
  const ownerError = await requireTeamOwner2(resolved.teamId, ctx.userId);
115527
115935
  if (ownerError) return ownerError;
115528
115936
  const project = await loadProjectForCleanup(projectId, resolved.teamId);
115529
115937
  if (!project) {
115530
- return textResponse4(
115938
+ return textResponse5(
115531
115939
  `Project ${projectId} not found, or it is not owned by this team.`
115532
115940
  );
115533
115941
  }
115534
115942
  const deps = await countProjectDependencies(project.id);
115535
115943
  const summary = formatProjectDependencies(deps);
115536
115944
  if (!isProjectEmpty(deps)) {
115537
- return textResponse4(
115945
+ return textResponse5(
115538
115946
  `\u{1F6AB} **Delete blocked** \u2014 project "${project.name}" (${project.id}) is not empty.
115539
115947
 
115540
115948
  Dependencies: ${summary}.
@@ -115543,12 +115951,12 @@ A hard delete would orphan these records, so it is not allowed. Use archive-proj
115543
115951
  );
115544
115952
  }
115545
115953
  if (confirmEmptyOnly !== true) {
115546
- return textResponse4(
115954
+ return textResponse5(
115547
115955
  `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).`
115548
115956
  );
115549
115957
  }
115550
115958
  await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
115551
- return textResponse4(
115959
+ return textResponse5(
115552
115960
  `\u2705 **Project deleted**
115553
115961
 
115554
115962
  Project: ${project.name}
@@ -115601,7 +116009,7 @@ var PRODUCT_COLUMNS2 = {
115601
116009
  createdAt: schema_exports.invoiceProducts.createdAt,
115602
116010
  updatedAt: schema_exports.invoiceProducts.updatedAt
115603
116011
  };
115604
- function textResponse5(text3) {
116012
+ function textResponse6(text3) {
115605
116013
  return { content: [{ type: "text", text: text3 }] };
115606
116014
  }
115607
116015
  function formatPrice(p3) {
@@ -115647,14 +116055,14 @@ async function handleGetProducts(input) {
115647
116055
  const { q: q3, currency, pageSize = 20 } = input;
115648
116056
  const status = input.status ?? "active";
115649
116057
  if (!PRODUCT_STATUSES.includes(status)) {
115650
- return textResponse5(
116058
+ return textResponse6(
115651
116059
  `Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
115652
116060
  );
115653
116061
  }
115654
116062
  const scope = await resolveTeamScope(input.teamId);
115655
116063
  if (!scope.ok) return scope.response;
115656
116064
  if (scope.teamIds.length === 0) {
115657
- return textResponse5("No accessible teams found.");
116065
+ return textResponse6("No accessible teams found.");
115658
116066
  }
115659
116067
  const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
115660
116068
  if (status === "active") {
@@ -115677,11 +116085,11 @@ async function handleGetProducts(input) {
115677
116085
  asc(schema_exports.invoiceProducts.name)
115678
116086
  ).limit(Math.min(pageSize, 100));
115679
116087
  if (rows.length === 0) {
115680
- return textResponse5(
116088
+ return textResponse6(
115681
116089
  `No products found${status !== "all" ? ` (status: ${status})` : ""}.`
115682
116090
  );
115683
116091
  }
115684
- return textResponse5(
116092
+ return textResponse6(
115685
116093
  `Found ${rows.length} product(s):
115686
116094
 
115687
116095
  ${rows.map(formatProduct).join("\n")}`
@@ -115689,11 +116097,11 @@ ${rows.map(formatProduct).join("\n")}`
115689
116097
  }
115690
116098
  async function handleGetProductById(input) {
115691
116099
  const { productId } = input;
115692
- if (!productId) return textResponse5("Error: `productId` is required.");
116100
+ if (!productId) return textResponse6("Error: `productId` is required.");
115693
116101
  const scope = await resolveTeamScope(input.teamId);
115694
116102
  if (!scope.ok) return scope.response;
115695
116103
  if (scope.teamIds.length === 0) {
115696
- return textResponse5("No accessible teams found.");
116104
+ return textResponse6("No accessible teams found.");
115697
116105
  }
115698
116106
  const [row] = await db.select(PRODUCT_COLUMNS2).from(schema_exports.invoiceProducts).where(
115699
116107
  and(
@@ -115702,11 +116110,11 @@ async function handleGetProductById(input) {
115702
116110
  )
115703
116111
  ).limit(1);
115704
116112
  if (!row) {
115705
- return textResponse5(
116113
+ return textResponse6(
115706
116114
  `Product ${productId} not found or you don't have access to it.`
115707
116115
  );
115708
116116
  }
115709
- return textResponse5(formatProduct(row));
116117
+ return textResponse6(formatProduct(row));
115710
116118
  }
115711
116119
  async function loadProductInTeam(productId, teamId) {
115712
116120
  const accessibleTeamIds = await getAccessibleTeamIds(teamId);
@@ -115721,10 +116129,10 @@ async function loadProductInTeam(productId, teamId) {
115721
116129
  async function handleCreateProduct(input) {
115722
116130
  const { name: name21, description, price, currency, unit } = input;
115723
116131
  if (!name21 || name21.trim().length === 0) {
115724
- return textResponse5("Error: `name` is required.");
116132
+ return textResponse6("Error: `name` is required.");
115725
116133
  }
115726
116134
  const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
115727
- if (enumError) return textResponse5(enumError);
116135
+ if (enumError) return textResponse6(enumError);
115728
116136
  const resolved = await resolveTeamId(input.teamId);
115729
116137
  if (!resolved.ok) return resolved.response;
115730
116138
  const [created] = await db.insert(schema_exports.invoiceProducts).values({
@@ -115744,8 +116152,8 @@ async function handleCreateProduct(input) {
115744
116152
  isActive: true,
115745
116153
  lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
115746
116154
  }).returning(PRODUCT_COLUMNS2);
115747
- if (!created) return textResponse5("Failed to create product.");
115748
- return textResponse5(
116155
+ if (!created) return textResponse6("Failed to create product.");
116156
+ return textResponse6(
115749
116157
  `\u2705 **Product created**
115750
116158
 
115751
116159
  ${formatProduct(created)}`
@@ -115753,21 +116161,21 @@ ${formatProduct(created)}`
115753
116161
  }
115754
116162
  async function handleUpdateProduct(input) {
115755
116163
  const { productId } = input;
115756
- if (!productId) return textResponse5("Error: `productId` is required.");
116164
+ if (!productId) return textResponse6("Error: `productId` is required.");
115757
116165
  const resolved = await resolveTeamId(input.teamId);
115758
116166
  if (!resolved.ok) return resolved.response;
115759
116167
  const existing = await loadProductInTeam(productId, resolved.teamId);
115760
116168
  if (!existing) {
115761
- return textResponse5(
116169
+ return textResponse6(
115762
116170
  `Product ${productId} not found, or it is not owned by this team.`
115763
116171
  );
115764
116172
  }
115765
116173
  const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
115766
- if (enumError) return textResponse5(enumError);
116174
+ if (enumError) return textResponse6(enumError);
115767
116175
  const updates = {};
115768
116176
  if (input.name !== void 0) {
115769
116177
  if (!input.name || input.name.trim().length === 0) {
115770
- return textResponse5("Error: `name` cannot be empty.");
116178
+ return textResponse6("Error: `name` cannot be empty.");
115771
116179
  }
115772
116180
  updates.name = input.name.trim();
115773
116181
  }
@@ -115788,14 +116196,14 @@ async function handleUpdateProduct(input) {
115788
116196
  updates.clause = serializeProductClause(input.clause);
115789
116197
  }
115790
116198
  if (Object.keys(updates).length === 0) {
115791
- return textResponse5(
116199
+ return textResponse6(
115792
116200
  "No fields to update. Provide at least one of: name, description, price, currency, unit, isActive, billingType, category, includedItems, optional, tier, sortOrder, clause."
115793
116201
  );
115794
116202
  }
115795
116203
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
115796
116204
  const [updated] = await db.update(schema_exports.invoiceProducts).set(updates).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS2);
115797
- if (!updated) return textResponse5(`Failed to update product ${productId}.`);
115798
- return textResponse5(
116205
+ if (!updated) return textResponse6(`Failed to update product ${productId}.`);
116206
+ return textResponse6(
115799
116207
  `\u2705 **Product updated**
115800
116208
 
115801
116209
  ${formatProduct(updated)}
@@ -115804,23 +116212,23 @@ Note: this only affects future invoices/quotes. Existing documents keep their li
115804
116212
  }
115805
116213
  async function handleArchiveProduct(input) {
115806
116214
  const { productId, reason } = input;
115807
- if (!productId) return textResponse5("Error: `productId` is required.");
116215
+ if (!productId) return textResponse6("Error: `productId` is required.");
115808
116216
  const resolved = await resolveTeamId(input.teamId);
115809
116217
  if (!resolved.ok) return resolved.response;
115810
116218
  const existing = await loadProductInTeam(productId, resolved.teamId);
115811
116219
  if (!existing) {
115812
- return textResponse5(
116220
+ return textResponse6(
115813
116221
  `Product ${productId} not found, or it is not owned by this team.`
115814
116222
  );
115815
116223
  }
115816
116224
  if (!existing.isActive) {
115817
- return textResponse5(
116225
+ return textResponse6(
115818
116226
  `Product "${existing.name}" (${existing.id}) is already archived.`
115819
116227
  );
115820
116228
  }
115821
116229
  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_COLUMNS2);
115822
- if (!archived) return textResponse5(`Failed to archive product ${productId}.`);
115823
- return textResponse5(
116230
+ if (!archived) return textResponse6(`Failed to archive product ${productId}.`);
116231
+ return textResponse6(
115824
116232
  `\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
115825
116233
 
115826
116234
  ${formatProduct(archived)}${reason ? `Reason: ${reason}
@@ -116870,7 +117278,7 @@ var QUOTE_STATUSES = [
116870
117278
  "expired"
116871
117279
  ];
116872
117280
  var SAFE_DRAFT_STATUSES = /* @__PURE__ */ new Set(["draft"]);
116873
- function textResponse6(text3) {
117281
+ function textResponse7(text3) {
116874
117282
  return { content: [{ type: "text", text: text3 }] };
116875
117283
  }
116876
117284
  async function loadTemplateDefaults(teamId) {
@@ -117040,14 +117448,14 @@ function tiptapNote2(text3) {
117040
117448
  async function handleGetQuotes(input) {
117041
117449
  const { customerId, status, q: q3, pageSize = 20 } = input;
117042
117450
  if (status && !QUOTE_STATUSES.includes(status)) {
117043
- return textResponse6(
117451
+ return textResponse7(
117044
117452
  `Error: invalid status "${status}". Allowed: ${QUOTE_STATUSES.join(", ")}.`
117045
117453
  );
117046
117454
  }
117047
117455
  const scope = await resolveTeamScope(input.teamId);
117048
117456
  if (!scope.ok) return scope.response;
117049
117457
  if (scope.teamIds.length === 0) {
117050
- return textResponse6("No accessible teams found.");
117458
+ return textResponse7("No accessible teams found.");
117051
117459
  }
117052
117460
  const filters = [inArray(schema_exports.quotations.teamId, scope.teamIds)];
117053
117461
  if (customerId) filters.push(eq(schema_exports.quotations.customerId, customerId));
@@ -117062,10 +117470,10 @@ async function handleGetQuotes(input) {
117062
117470
  }
117063
117471
  const rows = await db.select(QUOTE_COLUMNS).from(schema_exports.quotations).where(and(...filters)).orderBy(desc(schema_exports.quotations.createdAt)).limit(Math.min(pageSize, 100));
117064
117472
  if (rows.length === 0) {
117065
- return textResponse6("No quotes found.");
117473
+ return textResponse7("No quotes found.");
117066
117474
  }
117067
117475
  const note = input.projectId ? "\nNote: `projectId` was ignored \u2014 quotations are not linked to projects." : "";
117068
- return textResponse6(
117476
+ return textResponse7(
117069
117477
  `Found ${rows.length} quote(s):
117070
117478
 
117071
117479
  ${rows.map(formatQuote).join("\n")}${note}`
@@ -117073,10 +117481,10 @@ ${rows.map(formatQuote).join("\n")}${note}`
117073
117481
  }
117074
117482
  async function handleCreateQuote(input) {
117075
117483
  const { customerId } = input;
117076
- if (!customerId) return textResponse6("Error: `customerId` is required.");
117484
+ if (!customerId) return textResponse7("Error: `customerId` is required.");
117077
117485
  const status = input.status ?? "draft";
117078
117486
  if (!SAFE_DRAFT_STATUSES.has(status)) {
117079
- return textResponse6(
117487
+ return textResponse7(
117080
117488
  `Error: this tool only creates draft quotes. Requested status "${status}" is not allowed. Sending/accepting a quote is a manual dashboard action.`
117081
117489
  );
117082
117490
  }
@@ -117099,7 +117507,7 @@ async function handleCreateQuote(input) {
117099
117507
  )
117100
117508
  ).limit(1);
117101
117509
  if (!customer) {
117102
- return textResponse6(
117510
+ return textResponse7(
117103
117511
  `Customer ${customerId} not found or not owned by this team.`
117104
117512
  );
117105
117513
  }
@@ -117109,7 +117517,7 @@ async function handleCreateQuote(input) {
117109
117517
  defaults,
117110
117518
  teamId
117111
117519
  );
117112
- if (error49) return textResponse6(`Error: ${error49}`);
117520
+ if (error49) return textResponse7(`Error: ${error49}`);
117113
117521
  const totals = computeTotals(items, defaults);
117114
117522
  const quotationNumber = await nextQuotationNumber(teamId);
117115
117523
  const template = buildQuoteTemplate(defaults, input.title);
@@ -117184,8 +117592,8 @@ async function handleCreateQuote(input) {
117184
117592
  tax: totals.tax,
117185
117593
  amount: totals.amount
117186
117594
  }).returning(QUOTE_COLUMNS);
117187
- if (!created) return textResponse6("Failed to create quote.");
117188
- return textResponse6(
117595
+ if (!created) return textResponse7("Failed to create quote.");
117596
+ return textResponse7(
117189
117597
  `\u2705 **Draft quote created**
117190
117598
 
117191
117599
  ${formatQuote(created)}
@@ -117203,15 +117611,15 @@ async function loadQuoteInTeam(id, teamId) {
117203
117611
  return row ?? null;
117204
117612
  }
117205
117613
  function notDraftResponse2(quote) {
117206
- return textResponse6(
117614
+ return textResponse7(
117207
117615
  `Quote ${quote.quotationNumber ?? quote.id} has status "${quote.status}", not "draft". These tools only modify draft quotes \u2014 sent/accepted/rejected/expired quotes are immutable here so their product snapshots stay reproducible.`
117208
117616
  );
117209
117617
  }
117210
117618
  async function handleUpdateQuote(input) {
117211
117619
  const { id } = input;
117212
- if (!id) return textResponse6("Error: `id` is required.");
117620
+ if (!id) return textResponse7("Error: `id` is required.");
117213
117621
  if (input.status !== void 0 && !SAFE_DRAFT_STATUSES.has(input.status)) {
117214
- return textResponse6(
117622
+ return textResponse7(
117215
117623
  `Error: status can only stay within {${[...SAFE_DRAFT_STATUSES].join(", ")}}. "${input.status}" (send/accept/reject/expire) must be done manually from the dashboard.`
117216
117624
  );
117217
117625
  }
@@ -117219,7 +117627,7 @@ async function handleUpdateQuote(input) {
117219
117627
  if (!resolved.ok) return resolved.response;
117220
117628
  const quote = await loadQuoteInTeam(id, resolved.teamId);
117221
117629
  if (!quote) {
117222
- return textResponse6(`Quote ${id} not found or not owned by this team.`);
117630
+ return textResponse7(`Quote ${id} not found or not owned by this team.`);
117223
117631
  }
117224
117632
  if (quote.status !== "draft") return notDraftResponse2(quote);
117225
117633
  const defaults = templateDefaultsFromStored(quote.template, quote.currency);
@@ -117239,7 +117647,7 @@ async function handleUpdateQuote(input) {
117239
117647
  defaults,
117240
117648
  quote.teamId
117241
117649
  );
117242
- if (error49) return textResponse6(`Error: ${error49}`);
117650
+ if (error49) return textResponse7(`Error: ${error49}`);
117243
117651
  const totals = computeTotals(items, defaults);
117244
117652
  updates.lineItems = items;
117245
117653
  updates.subtotal = totals.subtotal;
@@ -117251,32 +117659,32 @@ async function handleUpdateQuote(input) {
117251
117659
  });
117252
117660
  }
117253
117661
  if (Object.keys(updates).length === 0) {
117254
- return textResponse6(
117662
+ return textResponse7(
117255
117663
  "No fields to update. Provide at least one of: title, description, validUntil, lineItems."
117256
117664
  );
117257
117665
  }
117258
117666
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
117259
117667
  const [updated] = await db.update(schema_exports.quotations).set(updates).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
117260
- if (!updated) return textResponse6(`Failed to update quote ${id}.`);
117261
- return textResponse6(`\u2705 **Draft quote updated**
117668
+ if (!updated) return textResponse7(`Failed to update quote ${id}.`);
117669
+ return textResponse7(`\u2705 **Draft quote updated**
117262
117670
 
117263
117671
  ${formatQuote(updated)}`);
117264
117672
  }
117265
117673
  async function handleAddProductToQuote(input) {
117266
117674
  const { quoteId, productId } = input;
117267
- if (!quoteId) return textResponse6("Error: `quoteId` is required.");
117268
- if (!productId) return textResponse6("Error: `productId` is required.");
117675
+ if (!quoteId) return textResponse7("Error: `quoteId` is required.");
117676
+ if (!productId) return textResponse7("Error: `productId` is required.");
117269
117677
  const resolved = await resolveTeamId(input.teamId);
117270
117678
  if (!resolved.ok) return resolved.response;
117271
117679
  const quote = await loadQuoteInTeam(quoteId, resolved.teamId);
117272
117680
  if (!quote) {
117273
- return textResponse6(`Quote ${quoteId} not found or not owned by this team.`);
117681
+ return textResponse7(`Quote ${quoteId} not found or not owned by this team.`);
117274
117682
  }
117275
117683
  if (quote.status !== "draft") return notDraftResponse2(quote);
117276
117684
  const products = await loadProductsInTeam2([productId], quote.teamId);
117277
117685
  const product = products.get(productId);
117278
117686
  if (!product) {
117279
- return textResponse6(
117687
+ return textResponse7(
117280
117688
  `Product ${productId} not found or not owned by this team.`
117281
117689
  );
117282
117690
  }
@@ -117306,7 +117714,7 @@ async function handleAddProductToQuote(input) {
117306
117714
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
117307
117715
  }).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
117308
117716
  if (!updated) {
117309
- return textResponse6(`Failed to add product to quote ${quoteId}.`);
117717
+ return textResponse7(`Failed to add product to quote ${quoteId}.`);
117310
117718
  }
117311
117719
  await db.update(schema_exports.invoiceProducts).set({
117312
117720
  usageCount: sql`${schema_exports.invoiceProducts.usageCount} + 1`,
@@ -117322,7 +117730,7 @@ async function handleAddProductToQuote(input) {
117322
117730
  if (meta5.includedItems && meta5.includedItems.length > 0) {
117323
117731
  metaParts.push(`included=[${meta5.includedItems.join(", ")}]`);
117324
117732
  }
117325
- return textResponse6(
117733
+ return textResponse7(
117326
117734
  `\u2705 **Product added to draft quote ${updated.quotationNumber ?? updated.id}**
117327
117735
 
117328
117736
  Line item: ${newItem.name} \xD7 ${newItem.quantity}${newItem.unit ? ` ${newItem.unit}` : ""} @ ${newItem.price} ${snap.currency}
@@ -123057,7 +123465,7 @@ function formatDeleteAttachmentRefusal(reason, context2) {
123057
123465
  }
123058
123466
 
123059
123467
  // src/tools/ticket-attachments.ts
123060
- function textResponse7(text3) {
123468
+ function textResponse8(text3) {
123061
123469
  return { content: [{ type: "text", text: text3 }] };
123062
123470
  }
123063
123471
  async function findAttachment(attachmentId) {
@@ -123130,7 +123538,7 @@ ${url3}`
123130
123538
  async function handleUploadTicketAttachment(input) {
123131
123539
  const ctx = getAuthContext() ?? authContext;
123132
123540
  if (!ctx) {
123133
- return textResponse7("Error: Not authenticated.");
123541
+ return textResponse8("Error: Not authenticated.");
123134
123542
  }
123135
123543
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
123136
123544
  if (!access.ok) return access.response;
@@ -123146,12 +123554,12 @@ async function handleUploadTicketAttachment(input) {
123146
123554
  userId: ctx.userId
123147
123555
  });
123148
123556
  if (!resolved.ok) {
123149
- return textResponse7(resolved.message);
123557
+ return textResponse8(resolved.message);
123150
123558
  }
123151
123559
  const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
123152
123560
  const validationError = validateAttachmentBuffer(buffer2, mimeType);
123153
123561
  if (validationError) {
123154
- return textResponse7(validationError.message);
123562
+ return textResponse8(validationError.message);
123155
123563
  }
123156
123564
  const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
123157
123565
  try {
@@ -123162,7 +123570,7 @@ async function handleUploadTicketAttachment(input) {
123162
123570
  options: { contentType: mimeType, upsert: true }
123163
123571
  });
123164
123572
  } catch (error49) {
123165
- return textResponse7(
123573
+ return textResponse8(
123166
123574
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
123167
123575
  );
123168
123576
  }
@@ -123191,7 +123599,7 @@ async function handleUploadTicketAttachment(input) {
123191
123599
  url3 = signed.url;
123192
123600
  } catch {
123193
123601
  }
123194
- return textResponse7(
123602
+ return textResponse8(
123195
123603
  `\u{1F4CE} **Attached to ${ticket.ticketNumber}**
123196
123604
  File: ${fileName}
123197
123605
  Type: ${mimeType}
@@ -123205,18 +123613,18 @@ ${url3}` : "")
123205
123613
  async function handleDeleteTicketAttachment(input) {
123206
123614
  const ctx = getAuthContext() ?? authContext;
123207
123615
  if (!ctx) {
123208
- return textResponse7("Error: Not authenticated.");
123616
+ return textResponse8("Error: Not authenticated.");
123209
123617
  }
123210
123618
  const inputError = validateDeleteAttachmentInput(input.attachmentId);
123211
123619
  if (inputError) {
123212
- return textResponse7(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
123620
+ return textResponse8(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
123213
123621
  }
123214
123622
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
123215
123623
  if (!access.ok) return access.response;
123216
123624
  const ticket = access.ticket;
123217
123625
  const attachment = await findAttachment(input.attachmentId);
123218
123626
  if (!attachment) {
123219
- return textResponse7(
123627
+ return textResponse8(
123220
123628
  formatDeleteAttachmentRefusal("attachment_not_found", {
123221
123629
  attachmentId: input.attachmentId,
123222
123630
  ticketNumber: ticket.ticketNumber
@@ -123224,7 +123632,7 @@ async function handleDeleteTicketAttachment(input) {
123224
123632
  );
123225
123633
  }
123226
123634
  if (!validateAttachmentBelongsToTicket(attachment.ticketId, ticket.id)) {
123227
- return textResponse7(
123635
+ return textResponse8(
123228
123636
  formatDeleteAttachmentRefusal("wrong_ticket", {
123229
123637
  attachmentId: input.attachmentId,
123230
123638
  ticketNumber: ticket.ticketNumber,
@@ -123236,7 +123644,7 @@ async function handleDeleteTicketAttachment(input) {
123236
123644
  const table = attachment.source === "ticket" ? schema_exports.ticketAttachments : schema_exports.ticketCommentAttachments;
123237
123645
  const [deletedRow] = await db.delete(table).where(eq(table.id, input.attachmentId)).returning({ id: table.id });
123238
123646
  if (!deletedRow) {
123239
- return textResponse7(
123647
+ return textResponse8(
123240
123648
  `Failed to delete attachment ${input.attachmentId}. It may have been removed already.`
123241
123649
  );
123242
123650
  }
@@ -123266,7 +123674,7 @@ async function handleDeleteTicketAttachment(input) {
123266
123674
  fileName: attachment.fileName,
123267
123675
  source: attachment.source
123268
123676
  });
123269
- return textResponse7(JSON.stringify(result, null, 2));
123677
+ return textResponse8(JSON.stringify(result, null, 2));
123270
123678
  }
123271
123679
 
123272
123680
  // src/tools/tiptap-text.ts
@@ -123683,7 +124091,7 @@ function formatTagUsage(usage) {
123683
124091
  }
123684
124092
 
123685
124093
  // src/tools/tag-management.ts
123686
- function textResponse8(text3) {
124094
+ function textResponse9(text3) {
123687
124095
  return { content: [{ type: "text", text: text3 }] };
123688
124096
  }
123689
124097
  var TAG_COLUMNS = {
@@ -123724,24 +124132,24 @@ function scopeFilter(projectId) {
123724
124132
  return projectId === null ? isNull(schema_exports.tags.projectId) : eq(schema_exports.tags.projectId, projectId);
123725
124133
  }
123726
124134
  async function handleUpdateTag(input) {
123727
- if (!input.tagId) return textResponse8("Error: `tagId` is required.");
124135
+ if (!input.tagId) return textResponse9("Error: `tagId` is required.");
123728
124136
  const resolved = await resolveTeamId(input.teamId);
123729
124137
  if (!resolved.ok) return resolved.response;
123730
124138
  const existing = await loadTagInTeam(input.tagId, resolved.teamId);
123731
124139
  if (!existing) {
123732
- return textResponse8(
124140
+ return textResponse9(
123733
124141
  `Tag ${input.tagId} not found, or it is not owned by this team.`
123734
124142
  );
123735
124143
  }
123736
124144
  const renaming = input.name !== void 0;
123737
124145
  const rescoping = input.projectId !== void 0;
123738
124146
  if (!renaming && !rescoping) {
123739
- return textResponse8(
124147
+ return textResponse9(
123740
124148
  "No changes requested. Provide `name` to rename and/or `projectId` (string, or null for a general tag) to change scope."
123741
124149
  );
123742
124150
  }
123743
124151
  if (renaming && !isValidTagName(input.name)) {
123744
- return textResponse8("Error: `name` cannot be empty.");
124152
+ return textResponse9("Error: `name` cannot be empty.");
123745
124153
  }
123746
124154
  const nextName = renaming ? input.name.trim() : existing.name;
123747
124155
  const nextProjectId = rescoping ? input.projectId ?? null : existing.projectId;
@@ -123754,13 +124162,13 @@ async function handleUpdateTag(input) {
123754
124162
  )
123755
124163
  ).limit(1);
123756
124164
  if (collision) {
123757
- return textResponse8(
124165
+ return textResponse9(
123758
124166
  `\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.`
123759
124167
  );
123760
124168
  }
123761
124169
  const [updated] = await db.update(schema_exports.tags).set({ name: nextName, projectId: nextProjectId }).where(eq(schema_exports.tags.id, existing.id)).returning(TAG_COLUMNS);
123762
- if (!updated) return textResponse8(`Failed to update tag ${input.tagId}.`);
123763
- return textResponse8(
124170
+ if (!updated) return textResponse9(`Failed to update tag ${input.tagId}.`);
124171
+ return textResponse9(
123764
124172
  `\u2705 **Tag updated**
123765
124173
 
123766
124174
  ${describeTag(updated)}
@@ -123769,34 +124177,34 @@ Existing ticket/customer/project/transaction tag relations are preserved.`
123769
124177
  );
123770
124178
  }
123771
124179
  async function handleDeleteTag(input) {
123772
- if (!input.tagId) return textResponse8("Error: `tagId` is required.");
124180
+ if (!input.tagId) return textResponse9("Error: `tagId` is required.");
123773
124181
  const mode = input.mode ?? "delete_if_unused";
123774
124182
  const resolved = await resolveTeamId(input.teamId);
123775
124183
  if (!resolved.ok) return resolved.response;
123776
124184
  const existing = await loadTagInTeam(input.tagId, resolved.teamId);
123777
124185
  if (!existing) {
123778
- return textResponse8(
124186
+ return textResponse9(
123779
124187
  `Tag ${input.tagId} not found, or it is not owned by this team.`
123780
124188
  );
123781
124189
  }
123782
124190
  const usage = await getTagUsage(existing.id);
123783
124191
  const total = totalTagUsage(usage);
123784
124192
  if (mode === "archive") {
123785
- return textResponse8(
124193
+ return textResponse9(
123786
124194
  `\u2139\uFE0F Archiving is not supported for team tags: the \`tags\` table has no archived column. ${describeTag(existing)} is used by ${formatTagUsage(usage)}.
123787
124195
 
123788
124196
  Options: use merge-tags to fold it into another tag, or delete it once it is unused (mode: delete_if_unused).`
123789
124197
  );
123790
124198
  }
123791
124199
  if (total > 0) {
123792
- return textResponse8(
124200
+ return textResponse9(
123793
124201
  `\u274C Refusing to delete ${describeTag(existing)}: it is still used by ${formatTagUsage(usage)}. Deleting would strip the tag off those entities.
123794
124202
 
123795
124203
  Use merge-tags to move usage onto another tag first, then delete the (now-empty) tag.`
123796
124204
  );
123797
124205
  }
123798
124206
  await db.delete(schema_exports.tags).where(eq(schema_exports.tags.id, existing.id));
123799
- return textResponse8(
124207
+ return textResponse9(
123800
124208
  `\u2705 **Tag deleted** (was unused): ${describeTag(existing)}`
123801
124209
  );
123802
124210
  }
@@ -123806,7 +124214,7 @@ async function resolveMergeTarget(teamId, input) {
123806
124214
  if (!tag2) {
123807
124215
  return {
123808
124216
  ok: false,
123809
- response: textResponse8(
124217
+ response: textResponse9(
123810
124218
  `Target tag ${input.targetTagId} not found, or it is not owned by this team.`
123811
124219
  )
123812
124220
  };
@@ -123816,7 +124224,7 @@ async function resolveMergeTarget(teamId, input) {
123816
124224
  if (!isValidTagName(input.targetName)) {
123817
124225
  return {
123818
124226
  ok: false,
123819
- response: textResponse8(
124227
+ response: textResponse9(
123820
124228
  "Error: provide either `targetTagId` or a non-empty `targetName`."
123821
124229
  )
123822
124230
  };
@@ -123834,14 +124242,14 @@ async function resolveMergeTarget(teamId, input) {
123834
124242
  }
123835
124243
  const [created] = await db.insert(schema_exports.tags).values({ teamId, name: input.targetName.trim(), projectId: null }).returning(TAG_COLUMNS);
123836
124244
  if (!created) {
123837
- return { ok: false, response: textResponse8("Failed to create target tag.") };
124245
+ return { ok: false, response: textResponse9("Failed to create target tag.") };
123838
124246
  }
123839
124247
  return { ok: true, tag: created, created: true };
123840
124248
  }
123841
124249
  async function handleMergeTags(input) {
123842
124250
  const rawSourceIds = [...new Set(input.sourceTagIds ?? [])].filter(Boolean);
123843
124251
  if (rawSourceIds.length === 0) {
123844
- return textResponse8("Error: `sourceTagIds` must contain at least one tag id.");
124252
+ return textResponse9("Error: `sourceTagIds` must contain at least one tag id.");
123845
124253
  }
123846
124254
  const resolved = await resolveTeamId(input.teamId);
123847
124255
  if (!resolved.ok) return resolved.response;
@@ -123855,7 +124263,7 @@ async function handleMergeTags(input) {
123855
124263
  const foundIds = new Set(sourceTags.map((t8) => t8.id));
123856
124264
  const missing = rawSourceIds.filter((id) => !foundIds.has(id));
123857
124265
  if (missing.length > 0) {
123858
- return textResponse8(
124266
+ return textResponse9(
123859
124267
  `Error: source tag(s) not found or not owned by this team: ${missing.join(", ")}.`
123860
124268
  );
123861
124269
  }
@@ -123863,7 +124271,7 @@ async function handleMergeTags(input) {
123863
124271
  if (!target.ok) return target.response;
123864
124272
  const sourcesToMerge = sourceTags.filter((t8) => t8.id !== target.tag.id);
123865
124273
  if (sourcesToMerge.length === 0) {
123866
- return textResponse8(
124274
+ return textResponse9(
123867
124275
  "Error: nothing to merge \u2014 the only source tag is the same as the target tag."
123868
124276
  );
123869
124277
  }
@@ -123960,7 +124368,7 @@ async function handleMergeTags(input) {
123960
124368
  const movedTotal = results.tickets.moved + results.customers.moved + results.projects.moved + results.transactions.moved;
123961
124369
  const skippedTotal = results.tickets.skipped + results.customers.skipped + results.projects.skipped + results.transactions.skipped;
123962
124370
  const line2 = (label, r6) => `- ${label}: ${r6.moved} moved, ${r6.skipped} skipped (duplicate)`;
123963
- return textResponse8(
124371
+ return textResponse9(
123964
124372
  `\u2705 **Tags merged** into ${describeTag(target.tag)}${target.created ? " (newly created)" : ""}
123965
124373
 
123966
124374
  Sources (${sourcesToMerge.length}): ${sourcesToMerge.map((t8) => `${t8.name} (${t8.id})`).join(", ")}
@@ -124346,15 +124754,15 @@ function attemptedLockedFields(update) {
124346
124754
  // src/tools/trips.ts
124347
124755
  var TRIP_TYPES = ["private", "business"];
124348
124756
  var BILLING_TYPES2 = TRIP_BILLING_TYPES;
124349
- function textResponse9(text3) {
124757
+ function textResponse10(text3) {
124350
124758
  return { content: [{ type: "text", text: text3 }] };
124351
124759
  }
124352
- function jsonResponse(payload) {
124760
+ function jsonResponse2(payload) {
124353
124761
  return {
124354
124762
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
124355
124763
  };
124356
124764
  }
124357
- function toNumber2(value) {
124765
+ function toNumber3(value) {
124358
124766
  if (value == null) return 0;
124359
124767
  if (typeof value === "number") return value;
124360
124768
  const parsed = Number.parseFloat(String(value));
@@ -124367,9 +124775,9 @@ function formatTrip(t8) {
124367
124775
  startLocation: t8.startLocation,
124368
124776
  endLocation: t8.endLocation,
124369
124777
  tripType: t8.tripType,
124370
- distance: t8.distance != null ? toNumber2(t8.distance) : null,
124371
- odometerStart: t8.odometerStart != null ? toNumber2(t8.odometerStart) : null,
124372
- odometerEnd: t8.odometerEnd != null ? toNumber2(t8.odometerEnd) : null,
124778
+ distance: t8.distance != null ? toNumber3(t8.distance) : null,
124779
+ odometerStart: t8.odometerStart != null ? toNumber3(t8.odometerStart) : null,
124780
+ odometerEnd: t8.odometerEnd != null ? toNumber3(t8.odometerEnd) : null,
124373
124781
  billingType: t8.billingType,
124374
124782
  rate: t8.rate,
124375
124783
  amount: t8.amount,
@@ -124402,19 +124810,19 @@ var TRIP_RELATIONS = {
124402
124810
  };
124403
124811
  async function handleGetTrips(input) {
124404
124812
  if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
124405
- return textResponse9(
124813
+ return textResponse10(
124406
124814
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
124407
124815
  );
124408
124816
  }
124409
124817
  if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
124410
- return textResponse9(
124818
+ return textResponse10(
124411
124819
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
124412
124820
  );
124413
124821
  }
124414
124822
  const scope = await resolveTeamScope(input.teamId);
124415
124823
  if (!scope.ok) return scope.response;
124416
124824
  if (scope.teamIds.length === 0) {
124417
- return textResponse9("No accessible teams found.");
124825
+ return textResponse10("No accessible teams found.");
124418
124826
  }
124419
124827
  const filters = [inArray(schema_exports.trips.teamId, scope.teamIds)];
124420
124828
  if (input.dateFrom) filters.push(gte(schema_exports.trips.date, input.dateFrom));
@@ -124446,7 +124854,7 @@ async function handleGetTrips(input) {
124446
124854
  });
124447
124855
  const totals = rows.reduce(
124448
124856
  (acc, t8) => {
124449
- const distance = toNumber2(t8.distance);
124857
+ const distance = toNumber3(t8.distance);
124450
124858
  const amount = t8.amount ?? 0;
124451
124859
  if (t8.tripType === "business") acc.businessKm += distance;
124452
124860
  else acc.privateKm += distance;
@@ -124456,7 +124864,7 @@ async function handleGetTrips(input) {
124456
124864
  },
124457
124865
  { businessKm: 0, privateKm: 0, totalKm: 0, totalAmount: 0 }
124458
124866
  );
124459
- return jsonResponse({
124867
+ return jsonResponse2({
124460
124868
  count: rows.length,
124461
124869
  totals: {
124462
124870
  businessKm: round23(totals.businessKm),
@@ -124516,20 +124924,20 @@ async function validateInvoice(invoiceId, teamId) {
124516
124924
  }
124517
124925
  async function handleCreateTrip(input) {
124518
124926
  const ctx = getAuthContext();
124519
- if (!input.date) return textResponse9("Error: `date` (YYYY-MM-DD) is required.");
124927
+ if (!input.date) return textResponse10("Error: `date` (YYYY-MM-DD) is required.");
124520
124928
  if (!input.startLocation || !input.endLocation) {
124521
- return textResponse9(
124929
+ return textResponse10(
124522
124930
  "Error: `startLocation` and `endLocation` are required."
124523
124931
  );
124524
124932
  }
124525
124933
  if (!input.tripType || !TRIP_TYPES.includes(input.tripType)) {
124526
- return textResponse9(
124934
+ return textResponse10(
124527
124935
  `Error: \`tripType\` is required and must be one of: ${TRIP_TYPES.join(", ")}.`
124528
124936
  );
124529
124937
  }
124530
124938
  const billingType = input.billingType ?? "not_billable";
124531
124939
  if (!BILLING_TYPES2.includes(billingType)) {
124532
- return textResponse9(
124940
+ return textResponse10(
124533
124941
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
124534
124942
  );
124535
124943
  }
@@ -124541,7 +124949,7 @@ async function handleCreateTrip(input) {
124541
124949
  customerId: input.customerId,
124542
124950
  vehicleId: input.vehicleId
124543
124951
  });
124544
- if (linkError) return textResponse9(`Error: ${linkError}`);
124952
+ if (linkError) return textResponse10(`Error: ${linkError}`);
124545
124953
  if (!input.allowDuplicate) {
124546
124954
  const dupFilters = [
124547
124955
  eq(schema_exports.trips.teamId, teamId),
@@ -124558,8 +124966,8 @@ async function handleCreateTrip(input) {
124558
124966
  }
124559
124967
  const [dup] = await db.select({ id: schema_exports.trips.id, distance: schema_exports.trips.distance }).from(schema_exports.trips).where(and(...dupFilters)).limit(1);
124560
124968
  if (dup) {
124561
- return textResponse9(
124562
- `\u26A0\uFE0F A matching trip already exists for ${input.date} (${input.startLocation} \u2192 ${input.endLocation}): trip ${dup.id}${dup.distance != null ? ` (${toNumber2(dup.distance)} km)` : ""}. Not creating a duplicate. Use update-trip to adjust it, or re-call create-trip with allowDuplicate: true to record a second trip anyway.`
124969
+ return textResponse10(
124970
+ `\u26A0\uFE0F A matching trip already exists for ${input.date} (${input.startLocation} \u2192 ${input.endLocation}): trip ${dup.id}${dup.distance != null ? ` (${toNumber3(dup.distance)} km)` : ""}. Not creating a duplicate. Use update-trip to adjust it, or re-call create-trip with allowDuplicate: true to record a second trip anyway.`
124563
124971
  );
124564
124972
  }
124565
124973
  }
@@ -124588,7 +124996,7 @@ async function handleCreateTrip(input) {
124588
124996
  vehicleId: input.vehicleId ?? null,
124589
124997
  snapshotId: input.snapshotId ?? null
124590
124998
  }).returning({ id: schema_exports.trips.id });
124591
- if (!created) return textResponse9("Failed to create trip.");
124999
+ if (!created) return textResponse10("Failed to create trip.");
124592
125000
  const trip = await loadTripInTeams(created.id, [teamId]);
124593
125001
  return {
124594
125002
  content: [
@@ -124603,14 +125011,14 @@ ${JSON.stringify(formatTrip(trip), null, 2)}`
124603
125011
  }
124604
125012
  async function handleUpdateTrip(input) {
124605
125013
  const ctx = getAuthContext();
124606
- if (!input.id) return textResponse9("Error: `id` is required.");
125014
+ if (!input.id) return textResponse10("Error: `id` is required.");
124607
125015
  if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
124608
- return textResponse9(
125016
+ return textResponse10(
124609
125017
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
124610
125018
  );
124611
125019
  }
124612
125020
  if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
124613
- return textResponse9(
125021
+ return textResponse10(
124614
125022
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
124615
125023
  );
124616
125024
  }
@@ -124619,7 +125027,7 @@ async function handleUpdateTrip(input) {
124619
125027
  const accessibleTeamIds = await getAccessibleTeamIds(resolved.teamId);
124620
125028
  const existing = await loadTripInTeams(input.id, accessibleTeamIds);
124621
125029
  if (!existing) {
124622
- return textResponse9(
125030
+ return textResponse10(
124623
125031
  `Trip ${input.id} not found or you don't have access to it. Call get-trips to find a valid id.`
124624
125032
  );
124625
125033
  }
@@ -124630,7 +125038,7 @@ async function handleUpdateTrip(input) {
124630
125038
  input
124631
125039
  );
124632
125040
  if (attempted.length > 0) {
124633
- return textResponse9(
125041
+ return textResponse10(
124634
125042
  `Error: trip ${input.id} is invoiced${existing.invoiceId ? ` (invoice ${existing.invoiceId})` : ""}. Financial/distance fields are locked: ${attempted.join(", ")}. Re-call with allowInvoicedOverride: true to change them anyway, or only update project/customer/notes/vehicle links.`
124635
125043
  );
124636
125044
  }
@@ -124640,10 +125048,10 @@ async function handleUpdateTrip(input) {
124640
125048
  customerId: input.customerId ?? void 0,
124641
125049
  vehicleId: input.vehicleId ?? void 0
124642
125050
  });
124643
- if (linkError) return textResponse9(`Error: ${linkError}`);
125051
+ if (linkError) return textResponse10(`Error: ${linkError}`);
124644
125052
  if (input.invoiceId) {
124645
125053
  const invoiceError = await validateInvoice(input.invoiceId, teamId);
124646
- if (invoiceError) return textResponse9(`Error: ${invoiceError}`);
125054
+ if (invoiceError) return textResponse10(`Error: ${invoiceError}`);
124647
125055
  }
124648
125056
  const updates = {};
124649
125057
  if (input.date !== void 0) updates.date = input.date;
@@ -124680,7 +125088,7 @@ async function handleUpdateTrip(input) {
124680
125088
  if (input.isInvoiced !== void 0) updates.isInvoiced = input.isInvoiced;
124681
125089
  if (input.amount === void 0 && (input.distance !== void 0 || input.rate !== void 0 || input.billingType !== void 0)) {
124682
125090
  const nextBilling = input.billingType ?? existing.billingType;
124683
- const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ? toNumber2(existing.distance) : null;
125091
+ const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ? toNumber3(existing.distance) : null;
124684
125092
  const nextRate = input.rate !== void 0 ? input.rate : existing.rate;
124685
125093
  const derived = deriveTripAmount({
124686
125094
  billingType: nextBilling,
@@ -124691,7 +125099,7 @@ async function handleUpdateTrip(input) {
124691
125099
  if (derived != null) updates.amount = derived;
124692
125100
  }
124693
125101
  if (Object.keys(updates).length === 0) {
124694
- return textResponse9(
125102
+ return textResponse10(
124695
125103
  "No fields to update. Provide at least one editable field."
124696
125104
  );
124697
125105
  }
@@ -124718,7 +125126,7 @@ async function handleGetVehicles(input) {
124718
125126
  const scope = await resolveTeamScope(input.teamId);
124719
125127
  if (!scope.ok) return scope.response;
124720
125128
  if (scope.teamIds.length === 0) {
124721
- return textResponse9("No accessible teams found.");
125129
+ return textResponse10("No accessible teams found.");
124722
125130
  }
124723
125131
  const filters = [inArray(schema_exports.vehicles.teamId, scope.teamIds)];
124724
125132
  if (input.q) filters.push(ilike(schema_exports.vehicles.name, `%${input.q}%`));
@@ -124729,13 +125137,13 @@ async function handleGetVehicles(input) {
124729
125137
  currentOdometer: schema_exports.vehicles.currentOdometer,
124730
125138
  teamId: schema_exports.vehicles.teamId
124731
125139
  }).from(schema_exports.vehicles).where(and(...filters)).orderBy(asc(schema_exports.vehicles.name)).limit(Math.min(input.pageSize ?? 50, 200));
124732
- return jsonResponse({
125140
+ return jsonResponse2({
124733
125141
  count: rows.length,
124734
125142
  vehicles: rows.map((v2) => ({
124735
125143
  id: v2.id,
124736
125144
  name: v2.name,
124737
125145
  licensePlate: v2.licensePlate,
124738
- currentOdometer: v2.currentOdometer != null ? toNumber2(v2.currentOdometer) : null
125146
+ currentOdometer: v2.currentOdometer != null ? toNumber3(v2.currentOdometer) : null
124739
125147
  }))
124740
125148
  });
124741
125149
  }
@@ -124744,7 +125152,7 @@ async function handleGetTripTemplates(input) {
124744
125152
  const scope = await resolveTeamScope(input.teamId);
124745
125153
  if (!scope.ok) return scope.response;
124746
125154
  if (scope.teamIds.length === 0) {
124747
- return textResponse9("No accessible teams found.");
125155
+ return textResponse10("No accessible teams found.");
124748
125156
  }
124749
125157
  const filters = [inArray(schema_exports.tripTemplates.teamId, scope.teamIds)];
124750
125158
  const userId = input.userId ?? ctx.userId;
@@ -124768,26 +125176,26 @@ async function handleGetTripTemplates(input) {
124768
125176
  vehicleId: schema_exports.tripTemplates.vehicleId,
124769
125177
  notes: schema_exports.tripTemplates.notes
124770
125178
  }).from(schema_exports.tripTemplates).where(and(...filters)).orderBy(asc(schema_exports.tripTemplates.name)).limit(Math.min(input.pageSize ?? 50, 200));
124771
- return jsonResponse({
125179
+ return jsonResponse2({
124772
125180
  count: rows.length,
124773
125181
  templates: rows.map((t8) => ({
124774
125182
  ...t8,
124775
- distance: t8.distance != null ? toNumber2(t8.distance) : null,
124776
- returnDistance: t8.returnDistance != null ? toNumber2(t8.returnDistance) : null
125183
+ distance: t8.distance != null ? toNumber3(t8.distance) : null,
125184
+ returnDistance: t8.returnDistance != null ? toNumber3(t8.returnDistance) : null
124777
125185
  }))
124778
125186
  });
124779
125187
  }
124780
125188
  async function handleGetFrequentTripsForProject(input) {
124781
125189
  const ctx = getAuthContext();
124782
125190
  if (!input.projectId) {
124783
- return textResponse9("Error: `projectId` is required.");
125191
+ return textResponse10("Error: `projectId` is required.");
124784
125192
  }
124785
125193
  const resolved = await resolveTeamId(input.teamId);
124786
125194
  if (!resolved.ok) return resolved.response;
124787
125195
  const teamId = resolved.teamId;
124788
125196
  const projectIds = await getAccessibleProjectIds(ctx.userId, teamId);
124789
125197
  if (!projectIds.includes(input.projectId)) {
124790
- return textResponse9(
125198
+ return textResponse10(
124791
125199
  `Project not found or no access: ${input.projectId}. Call get-projects first.`
124792
125200
  );
124793
125201
  }
@@ -124814,7 +125222,7 @@ async function handleGetFrequentTripsForProject(input) {
124814
125222
  schema_exports.trips.endLocation,
124815
125223
  schema_exports.trips.tripType
124816
125224
  ).orderBy(desc(sql`count(*)`), desc(sql`max(${schema_exports.trips.date})`)).limit(limitN);
124817
- return jsonResponse({
125225
+ return jsonResponse2({
124818
125226
  count: groups.length,
124819
125227
  daysBack,
124820
125228
  frequentTrips: groups.map((g6) => ({
@@ -124822,7 +125230,7 @@ async function handleGetFrequentTripsForProject(input) {
124822
125230
  endLocation: g6.endLocation,
124823
125231
  tripType: g6.tripType,
124824
125232
  count: g6.count,
124825
- avgDistance: g6.avgDistance != null ? round23(toNumber2(g6.avgDistance)) : null,
125233
+ avgDistance: g6.avgDistance != null ? round23(toNumber3(g6.avgDistance)) : null,
124826
125234
  lastUsedDate: g6.lastUsedDate
124827
125235
  }))
124828
125236
  });
@@ -125490,6 +125898,10 @@ function createMcpServer() {
125490
125898
  );
125491
125899
  case "log-hours":
125492
125900
  return await handleLogHours(asToolArgs(toolArgs));
125901
+ case "get-time-entries":
125902
+ return await handleGetTimeEntries(
125903
+ asToolArgs(toolArgs)
125904
+ );
125493
125905
  case "get-github-file":
125494
125906
  return await handleGetGithubFile(asToolArgs(toolArgs));
125495
125907
  case "list-github-directory":