@mgsoftwarebv/mcp-server-bridge 3.5.9 → 3.5.11

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"),
@@ -103452,7 +103467,9 @@ var customerRecurringServices = pgTable(
103452
103467
  mode: "string"
103453
103468
  }),
103454
103469
  // --- Lifecycle ---
103455
- // "active" | "paused" | "cancelled" | "ended"
103470
+ // "draft" | "active" | "paused" | "cancelled" | "ended"
103471
+ // "draft" = created but not yet activated (e.g. via MCP); excluded from
103472
+ // all billing/due queries until a human activates it in the dashboard.
103456
103473
  status: text().$type().default("active").notNull(),
103457
103474
  startDate: timestamp("start_date", { withTimezone: true, mode: "string" }),
103458
103475
  // Planned end of the agreement (before cancellation/ended).
@@ -107203,6 +107220,67 @@ var TOOLS = [
107203
107220
  required: ["workDescription", "estimatedHours"]
107204
107221
  }
107205
107222
  },
107223
+ {
107224
+ name: "get-time-entries",
107225
+ 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.",
107226
+ inputSchema: {
107227
+ type: "object",
107228
+ properties: {
107229
+ teamId: teamIdProp,
107230
+ userId: {
107231
+ type: "string",
107232
+ 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."
107233
+ },
107234
+ projectId: { type: "string", description: "Filter by project UUID." },
107235
+ customerId: {
107236
+ type: "string",
107237
+ description: "Filter by customer UUID. Matches entries whose own customer OR whose project's customer is this customer (project link is the common case)."
107238
+ },
107239
+ ticketId: {
107240
+ type: "string",
107241
+ description: "Only entries linked to this ticket (UUID)."
107242
+ },
107243
+ dateFrom: {
107244
+ type: "string",
107245
+ description: "Inclusive period start (YYYY-MM-DD, Europe/Amsterdam)."
107246
+ },
107247
+ dateTo: {
107248
+ type: "string",
107249
+ description: "Inclusive period end (YYYY-MM-DD, Europe/Amsterdam)."
107250
+ },
107251
+ status: {
107252
+ type: "string",
107253
+ enum: ["draft", "approved", "confirmed", "invoiced", "all"],
107254
+ description: "Derived status filter: draft (not confirmed, not invoiced), approved/confirmed (confirmed, not invoiced), invoiced (linked to an invoice), all (default)."
107255
+ },
107256
+ type: {
107257
+ type: "string",
107258
+ enum: [
107259
+ "meeting",
107260
+ "work",
107261
+ "clocked_work",
107262
+ "appointment",
107263
+ "task",
107264
+ "other"
107265
+ ],
107266
+ description: "Filter by timesheet event type. Omit to include all worked-time types (deadlines/all-day markers are always excluded)."
107267
+ },
107268
+ groupBy: {
107269
+ type: "string",
107270
+ enum: ["none", "day", "project", "customer", "user", "ticket"],
107271
+ default: "none",
107272
+ 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."
107273
+ },
107274
+ pageSize: {
107275
+ type: "number",
107276
+ default: 50,
107277
+ maximum: 200,
107278
+ description: "Max detailed entries returned. Totals/groups always cover ALL matching entries regardless of this limit."
107279
+ }
107280
+ },
107281
+ required: []
107282
+ }
107283
+ },
107206
107284
  {
107207
107285
  name: "get-trips",
107208
107286
  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.",
@@ -107414,7 +107492,7 @@ var TOOLS = [
107414
107492
  customerId: { type: "string", description: "Filter by customer UUID" },
107415
107493
  status: {
107416
107494
  type: "string",
107417
- enum: ["active", "paused", "cancelled", "ended"],
107495
+ enum: ["draft", "active", "paused", "cancelled", "ended"],
107418
107496
  description: "Filter by agreement status"
107419
107497
  }
107420
107498
  }
@@ -107422,7 +107500,7 @@ var TOOLS = [
107422
107500
  },
107423
107501
  {
107424
107502
  name: "create-customer-agreement",
107425
- description: "Create a customer-specific product agreement between catalog and billing. Snapshots price, discount, clause and invoice line text without mutating the catalog product.",
107503
+ description: "Create a customer-specific product agreement as a DRAFT. Snapshots price, discount, clause and invoice line text without mutating the catalog product. Drafts are never invoiced; a human activates them in the dashboard. A draft can still be edited via update-customer-agreement, but once activated it is locked for MCP writes.",
107426
107504
  inputSchema: {
107427
107505
  type: "object",
107428
107506
  properties: {
@@ -107458,7 +107536,7 @@ var TOOLS = [
107458
107536
  },
107459
107537
  {
107460
107538
  name: "update-customer-agreement",
107461
- description: "Update an existing customer product agreement by id.",
107539
+ description: "Update a customer product agreement by id. Allowed ONLY while it is a DRAFT; once activated it is locked and this tool returns an error. It cannot change status (activation is a dashboard action) and there is no delete tool by design.",
107462
107540
  inputSchema: {
107463
107541
  type: "object",
107464
107542
  properties: {
@@ -107479,11 +107557,7 @@ var TOOLS = [
107479
107557
  startDate: { type: "string" },
107480
107558
  endDate: { type: "string" },
107481
107559
  contractPeriodEnd: { type: "string" },
107482
- proRataFirstInvoice: { type: "boolean" },
107483
- status: {
107484
- type: "string",
107485
- enum: ["active", "paused", "cancelled", "ended"]
107486
- }
107560
+ proRataFirstInvoice: { type: "boolean" }
107487
107561
  },
107488
107562
  required: ["id"]
107489
107563
  }
@@ -108777,16 +108851,33 @@ async function handleCreateCustomerAgreement(input) {
108777
108851
  endDate: input.endDate ?? null,
108778
108852
  contractPeriodEnd: input.contractPeriodEnd ?? null,
108779
108853
  proRataFirstInvoice: input.proRataFirstInvoice ?? false,
108780
- status: "active"
108854
+ // MCP-created agreements start as a draft; a human activates them in the
108855
+ // dashboard. Drafts are excluded from every billing/due query.
108856
+ status: "draft"
108781
108857
  }).returning();
108782
- return textResponse2(`Created customer agreement:
108858
+ return textResponse2(
108859
+ `Created customer agreement as **draft**. It will not be invoiced until a human activates it in the dashboard. You can keep editing it via update-customer-agreement while it stays a draft.
108783
108860
 
108784
- ${formatAgreement(row)}`);
108861
+ ${formatAgreement(row)}`
108862
+ );
108785
108863
  }
108786
108864
  async function handleUpdateCustomerAgreement(input) {
108787
108865
  const scope = await resolveTeamScope(input.teamId);
108788
108866
  if (!scope.ok) return scope.response;
108789
- const teamId = scope.teamIds[0];
108867
+ const [existing] = await db.select().from(schema_exports.customerRecurringServices).where(
108868
+ and(
108869
+ eq(schema_exports.customerRecurringServices.id, input.id),
108870
+ inArray(schema_exports.customerRecurringServices.teamId, scope.teamIds)
108871
+ )
108872
+ ).limit(1);
108873
+ if (!existing) {
108874
+ return textResponse2("Agreement not found.");
108875
+ }
108876
+ if (existing.status !== "draft") {
108877
+ return textResponse2(
108878
+ `This agreement is '${existing.status}' and is locked: only drafts can be edited via MCP. Activation and any change to a live agreement happen in the dashboard.`
108879
+ );
108880
+ }
108790
108881
  const patch = {
108791
108882
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
108792
108883
  };
@@ -108810,17 +108901,16 @@ async function handleUpdateCustomerAgreement(input) {
108810
108901
  patch.contractPeriodEnd = input.contractPeriodEnd;
108811
108902
  if (input.proRataFirstInvoice !== void 0)
108812
108903
  patch.proRataFirstInvoice = input.proRataFirstInvoice;
108813
- if (input.status !== void 0) patch.status = input.status;
108814
108904
  const [row] = await db.update(schema_exports.customerRecurringServices).set(patch).where(
108815
108905
  and(
108816
108906
  eq(schema_exports.customerRecurringServices.id, input.id),
108817
- eq(schema_exports.customerRecurringServices.teamId, teamId)
108907
+ inArray(schema_exports.customerRecurringServices.teamId, scope.teamIds)
108818
108908
  )
108819
108909
  ).returning();
108820
108910
  if (!row) {
108821
108911
  return textResponse2("Agreement not found.");
108822
108912
  }
108823
- return textResponse2(`Updated customer agreement:
108913
+ return textResponse2(`Updated draft agreement:
108824
108914
 
108825
108915
  ${formatAgreement(row)}`);
108826
108916
  }
@@ -114024,6 +114114,338 @@ async function handleLogHours(input) {
114024
114114
  responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
114025
114115
  return { content: [{ type: "text", text: responseText }] };
114026
114116
  }
114117
+ var TIME_ENTRY_TYPES = [
114118
+ "meeting",
114119
+ "work",
114120
+ "clocked_work",
114121
+ "appointment",
114122
+ "task",
114123
+ "other"
114124
+ ];
114125
+ var TIME_ENTRY_STATUSES = [
114126
+ "draft",
114127
+ "approved",
114128
+ "confirmed",
114129
+ "invoiced",
114130
+ "all"
114131
+ ];
114132
+ var TIME_ENTRY_GROUP_BY = [
114133
+ "none",
114134
+ "day",
114135
+ "project",
114136
+ "customer",
114137
+ "user",
114138
+ "ticket"
114139
+ ];
114140
+ var TIMEZONE = "Europe/Amsterdam";
114141
+ function textResponse3(text3) {
114142
+ return { content: [{ type: "text", text: text3 }] };
114143
+ }
114144
+ function jsonResponse(payload) {
114145
+ return {
114146
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
114147
+ };
114148
+ }
114149
+ function toNumber(value) {
114150
+ if (value == null) return 0;
114151
+ if (typeof value === "number") return value;
114152
+ const parsed = Number.parseFloat(String(value));
114153
+ return Number.isFinite(parsed) ? parsed : 0;
114154
+ }
114155
+ function hoursFrom(seconds) {
114156
+ return Math.round(toNumber(seconds) / 3600 * 100) / 100;
114157
+ }
114158
+ function deriveEntryStatus(invoiceId, rawStatus) {
114159
+ if (invoiceId) return "invoiced";
114160
+ if (rawStatus === "confirmed") return "approved";
114161
+ return "draft";
114162
+ }
114163
+ function durationSecondsExpr() {
114164
+ const te = schema_exports.timesheetEvents;
114165
+ 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`;
114166
+ }
114167
+ function localDateExpr() {
114168
+ return sql`(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})::date`;
114169
+ }
114170
+ function localDateTextExpr() {
114171
+ return sql`to_char(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)}, 'YYYY-MM-DD')`;
114172
+ }
114173
+ function effectiveCustomerIdExpr() {
114174
+ return sql`COALESCE(${schema_exports.timesheetEvents.customerId}, ${schema_exports.projects.customerId})`;
114175
+ }
114176
+ async function buildTimeEntryGroups(groupBy, whereExpr) {
114177
+ const te = schema_exports.timesheetEvents;
114178
+ const totalSecondsSel = sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`;
114179
+ const billableSecondsSel = sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`;
114180
+ const countSel = sql`count(*)::int`;
114181
+ const finish = (rows2) => rows2.sort((a6, b7) => b7.totalHours - a6.totalHours);
114182
+ if (groupBy === "day") {
114183
+ const dayExpr = localDateTextExpr();
114184
+ const rows2 = await db.select({
114185
+ day: sql`${dayExpr}`,
114186
+ totalSeconds: totalSecondsSel,
114187
+ billableSeconds: billableSecondsSel,
114188
+ entryCount: countSel
114189
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(dayExpr);
114190
+ return finish(
114191
+ rows2.map((r6) => ({
114192
+ key: { type: "day", date: r6.day },
114193
+ entryCount: Number(r6.entryCount),
114194
+ totalHours: hoursFrom(r6.totalSeconds),
114195
+ billableHours: hoursFrom(r6.billableSeconds)
114196
+ }))
114197
+ );
114198
+ }
114199
+ if (groupBy === "project") {
114200
+ const rows2 = await db.select({
114201
+ projectId: te.projectId,
114202
+ projectName: schema_exports.projects.name,
114203
+ totalSeconds: totalSecondsSel,
114204
+ billableSeconds: billableSecondsSel,
114205
+ entryCount: countSel
114206
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(te.projectId, schema_exports.projects.name);
114207
+ return finish(
114208
+ rows2.map((r6) => ({
114209
+ key: {
114210
+ type: "project",
114211
+ id: r6.projectId,
114212
+ name: r6.projectId ? r6.projectName : "(no project)"
114213
+ },
114214
+ entryCount: Number(r6.entryCount),
114215
+ totalHours: hoursFrom(r6.totalSeconds),
114216
+ billableHours: hoursFrom(r6.billableSeconds)
114217
+ }))
114218
+ );
114219
+ }
114220
+ if (groupBy === "customer") {
114221
+ const custId = effectiveCustomerIdExpr();
114222
+ const rows2 = await db.select({
114223
+ customerId: sql`${custId}`,
114224
+ customerName: schema_exports.customers.name,
114225
+ totalSeconds: totalSecondsSel,
114226
+ billableSeconds: billableSecondsSel,
114227
+ entryCount: countSel
114228
+ }).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);
114229
+ return finish(
114230
+ rows2.map((r6) => ({
114231
+ key: {
114232
+ type: "customer",
114233
+ id: r6.customerId,
114234
+ name: r6.customerId ? r6.customerName : "(no customer)"
114235
+ },
114236
+ entryCount: Number(r6.entryCount),
114237
+ totalHours: hoursFrom(r6.totalSeconds),
114238
+ billableHours: hoursFrom(r6.billableSeconds)
114239
+ }))
114240
+ );
114241
+ }
114242
+ if (groupBy === "user") {
114243
+ const rows2 = await db.select({
114244
+ userId: te.userId,
114245
+ userName: schema_exports.users.fullName,
114246
+ totalSeconds: totalSecondsSel,
114247
+ billableSeconds: billableSecondsSel,
114248
+ entryCount: countSel
114249
+ }).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);
114250
+ return finish(
114251
+ rows2.map((r6) => ({
114252
+ key: { type: "user", id: r6.userId, name: r6.userName },
114253
+ entryCount: Number(r6.entryCount),
114254
+ totalHours: hoursFrom(r6.totalSeconds),
114255
+ billableHours: hoursFrom(r6.billableSeconds)
114256
+ }))
114257
+ );
114258
+ }
114259
+ const tet = schema_exports.timesheetEventTickets;
114260
+ const rows = await db.select({
114261
+ ticketId: schema_exports.tickets.id,
114262
+ ticketNumber: schema_exports.tickets.ticketNumber,
114263
+ totalSeconds: totalSecondsSel,
114264
+ billableSeconds: billableSecondsSel,
114265
+ entryCount: countSel
114266
+ }).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);
114267
+ return finish(
114268
+ rows.map((r6) => ({
114269
+ key: {
114270
+ type: "ticket",
114271
+ id: r6.ticketId,
114272
+ ticketNumber: r6.ticketId ? r6.ticketNumber : "(no ticket)"
114273
+ },
114274
+ entryCount: Number(r6.entryCount),
114275
+ totalHours: hoursFrom(r6.totalSeconds),
114276
+ billableHours: hoursFrom(r6.billableSeconds)
114277
+ }))
114278
+ );
114279
+ }
114280
+ async function handleGetTimeEntries(input) {
114281
+ const ctx = getAuthContext();
114282
+ const te = schema_exports.timesheetEvents;
114283
+ const status = input.status ?? "all";
114284
+ if (!TIME_ENTRY_STATUSES.includes(status)) {
114285
+ return textResponse3(
114286
+ `Error: invalid status "${input.status}". Allowed: ${TIME_ENTRY_STATUSES.join(", ")}.`
114287
+ );
114288
+ }
114289
+ const groupBy = input.groupBy ?? "none";
114290
+ if (!TIME_ENTRY_GROUP_BY.includes(groupBy)) {
114291
+ return textResponse3(
114292
+ `Error: invalid groupBy "${input.groupBy}". Allowed: ${TIME_ENTRY_GROUP_BY.join(", ")}.`
114293
+ );
114294
+ }
114295
+ if (input.type && !TIME_ENTRY_TYPES.includes(input.type)) {
114296
+ return textResponse3(
114297
+ `Error: invalid type "${input.type}". Allowed: ${TIME_ENTRY_TYPES.join(", ")}. (Deadlines/all-day markers are agenda items \u2014 use get-calendar-items.)`
114298
+ );
114299
+ }
114300
+ const scope = await resolveTeamScope(input.teamId);
114301
+ if (!scope.ok) return scope.response;
114302
+ if (scope.teamIds.length === 0) {
114303
+ return textResponse3("No accessible teams found.");
114304
+ }
114305
+ if (input.projectId && !scope.projectIds.includes(input.projectId)) {
114306
+ return textResponse3(
114307
+ `Project not found or no access: ${input.projectId}. Call get-projects first.`
114308
+ );
114309
+ }
114310
+ if (input.customerId && !scope.customerIds.includes(input.customerId)) {
114311
+ return textResponse3(
114312
+ `Customer not found or no access: ${input.customerId}. Call get-customers first.`
114313
+ );
114314
+ }
114315
+ const rawUserId = input.userId?.trim();
114316
+ const userId = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
114317
+ const conditions = [
114318
+ inArray(te.teamId, scope.teamIds),
114319
+ eq(te.isDeleted, false),
114320
+ // Worked time only: drop all-day markers / deadlines (~24h agenda blocks
114321
+ // that would otherwise massively inflate the totals).
114322
+ eq(te.allDay, false),
114323
+ sql`${te.type}::text <> 'deadline'`
114324
+ ];
114325
+ if (userId !== "all") conditions.push(eq(te.userId, userId));
114326
+ if (input.projectId) conditions.push(eq(te.projectId, input.projectId));
114327
+ if (input.customerId) {
114328
+ conditions.push(
114329
+ sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId}) = ${input.customerId}`
114330
+ );
114331
+ }
114332
+ if (input.dateFrom) {
114333
+ conditions.push(sql`${localDateExpr()} >= ${input.dateFrom}::date`);
114334
+ }
114335
+ if (input.dateTo) {
114336
+ conditions.push(sql`${localDateExpr()} <= ${input.dateTo}::date`);
114337
+ }
114338
+ if (input.type) conditions.push(sql`${te.type}::text = ${input.type}`);
114339
+ if (input.ticketId) {
114340
+ conditions.push(
114341
+ sql`EXISTS (SELECT 1 FROM ${schema_exports.timesheetEventTickets} WHERE ${schema_exports.timesheetEventTickets.timesheetEventId} = ${te.id} AND ${schema_exports.timesheetEventTickets.ticketId} = ${input.ticketId})`
114342
+ );
114343
+ }
114344
+ if (status === "draft") {
114345
+ conditions.push(sql`(${te.status}::text = 'draft' AND ${te.invoiceId} IS NULL)`);
114346
+ } else if (status === "approved" || status === "confirmed") {
114347
+ conditions.push(
114348
+ sql`(${te.status}::text = 'confirmed' AND ${te.invoiceId} IS NULL)`
114349
+ );
114350
+ } else if (status === "invoiced") {
114351
+ conditions.push(sql`${te.invoiceId} IS NOT NULL`);
114352
+ }
114353
+ const whereExpr = and(...conditions);
114354
+ const [totalsRow] = await db.select({
114355
+ count: sql`count(*)::int`,
114356
+ totalSeconds: sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`,
114357
+ billableSeconds: sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`,
114358
+ invoicedSeconds: sql`COALESCE(SUM(CASE WHEN ${te.invoiceId} IS NOT NULL THEN ${durationSecondsExpr()} ELSE 0 END), 0)`
114359
+ }).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr);
114360
+ const totalSeconds = toNumber(totalsRow?.totalSeconds);
114361
+ const billableSeconds = toNumber(totalsRow?.billableSeconds);
114362
+ const invoicedSeconds = toNumber(totalsRow?.invoicedSeconds);
114363
+ const entryCount = Number(totalsRow?.count ?? 0);
114364
+ const pageSize = Math.min(input.pageSize ?? 50, 200);
114365
+ const entryRows = await db.select({
114366
+ id: te.id,
114367
+ title: te.title,
114368
+ description: te.description,
114369
+ type: te.type,
114370
+ rawStatus: te.status,
114371
+ billingStatus: te.billingStatus,
114372
+ invoiceId: te.invoiceId,
114373
+ startTime: te.startTime,
114374
+ endTime: te.endTime,
114375
+ date: sql`${localDateTextExpr()}`,
114376
+ durationSeconds: sql`${durationSecondsExpr()}`,
114377
+ userId: te.userId,
114378
+ userName: schema_exports.users.fullName,
114379
+ projectId: te.projectId,
114380
+ projectName: schema_exports.projects.name,
114381
+ customerId: sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`,
114382
+ customerName: schema_exports.customers.name
114383
+ }).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(
114384
+ schema_exports.customers,
114385
+ sql`${schema_exports.customers.id} = COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`
114386
+ ).where(whereExpr).orderBy(desc(te.startTime)).limit(pageSize);
114387
+ const entryIds = entryRows.map((r6) => r6.id);
114388
+ const ticketsByEvent = /* @__PURE__ */ new Map();
114389
+ if (entryIds.length > 0) {
114390
+ const links = await db.select({
114391
+ eventId: schema_exports.timesheetEventTickets.timesheetEventId,
114392
+ ticketId: schema_exports.tickets.id,
114393
+ ticketNumber: schema_exports.tickets.ticketNumber
114394
+ }).from(schema_exports.timesheetEventTickets).innerJoin(
114395
+ schema_exports.tickets,
114396
+ eq(schema_exports.timesheetEventTickets.ticketId, schema_exports.tickets.id)
114397
+ ).where(inArray(schema_exports.timesheetEventTickets.timesheetEventId, entryIds));
114398
+ for (const link of links) {
114399
+ const list = ticketsByEvent.get(link.eventId) ?? [];
114400
+ list.push({ id: link.ticketId, ticketNumber: link.ticketNumber });
114401
+ ticketsByEvent.set(link.eventId, list);
114402
+ }
114403
+ }
114404
+ const entries = entryRows.map((r6) => ({
114405
+ id: r6.id,
114406
+ date: r6.date,
114407
+ startTime: r6.startTime,
114408
+ endTime: r6.endTime,
114409
+ hours: hoursFrom(r6.durationSeconds),
114410
+ title: r6.title,
114411
+ description: r6.description,
114412
+ type: r6.type,
114413
+ status: deriveEntryStatus(r6.invoiceId, r6.rawStatus),
114414
+ billingStatus: r6.billingStatus,
114415
+ invoiced: r6.invoiceId != null,
114416
+ invoiceId: r6.invoiceId,
114417
+ user: r6.userId ? { id: r6.userId, name: r6.userName } : null,
114418
+ project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
114419
+ customer: r6.customerId ? { id: r6.customerId, name: r6.customerName } : null,
114420
+ tickets: ticketsByEvent.get(r6.id) ?? []
114421
+ }));
114422
+ const groups = groupBy === "none" ? void 0 : await buildTimeEntryGroups(groupBy, whereExpr);
114423
+ return jsonResponse({
114424
+ filters: {
114425
+ userId: userId === "all" ? "all" : userId,
114426
+ teamIds: scope.teamIds,
114427
+ projectId: input.projectId ?? null,
114428
+ customerId: input.customerId ?? null,
114429
+ ticketId: input.ticketId ?? null,
114430
+ dateFrom: input.dateFrom ?? null,
114431
+ dateTo: input.dateTo ?? null,
114432
+ status,
114433
+ type: input.type ?? null,
114434
+ timezone: TIMEZONE
114435
+ },
114436
+ totals: {
114437
+ totalHours: hoursFrom(totalSeconds),
114438
+ billableHours: hoursFrom(billableSeconds),
114439
+ nonBillableHours: hoursFrom(totalSeconds - billableSeconds),
114440
+ invoicedHours: hoursFrom(invoicedSeconds),
114441
+ entryCount
114442
+ },
114443
+ groupBy,
114444
+ ...groups ? { groups } : {},
114445
+ entries,
114446
+ entriesTruncated: entryCount > entries.length
114447
+ });
114448
+ }
114027
114449
 
114028
114450
  // ../invoice/src/utils/included-items.ts
114029
114451
  function parseIncludedItems(value) {
@@ -114397,7 +114819,7 @@ var INVOICE_STATUSES = [
114397
114819
  "refunded"
114398
114820
  ];
114399
114821
  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) {
114822
+ function textResponse4(text3) {
114401
114823
  return { content: [{ type: "text", text: text3 }] };
114402
114824
  }
114403
114825
  function tiptapNote(text3) {
@@ -114515,7 +114937,7 @@ async function resolveInvoiceLineItems(inputs, defaults, teamId) {
114515
114937
  return { items };
114516
114938
  }
114517
114939
  function notDraftResponse(invoice) {
114518
- return textResponse3(
114940
+ return textResponse4(
114519
114941
  `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
114942
  );
114521
114943
  }
@@ -114532,14 +114954,14 @@ Issue: ${invoice.issueDate ? new Date(invoice.issueDate).toLocaleDateString() :
114532
114954
  async function handleGetInvoices(input) {
114533
114955
  const { customerId, status, q: q3, pageSize = 20 } = input;
114534
114956
  if (status && !INVOICE_STATUSES.includes(status)) {
114535
- return textResponse3(
114957
+ return textResponse4(
114536
114958
  `Error: invalid status "${status}". Allowed: ${INVOICE_STATUSES.join(", ")}.`
114537
114959
  );
114538
114960
  }
114539
114961
  const scope = await resolveTeamScope(input.teamId);
114540
114962
  if (!scope.ok) return scope.response;
114541
114963
  if (scope.teamIds.length === 0) {
114542
- return textResponse3("No accessible teams found.");
114964
+ return textResponse4("No accessible teams found.");
114543
114965
  }
114544
114966
  const filters = [inArray(schema_exports.invoices.teamId, scope.teamIds)];
114545
114967
  if (customerId) filters.push(eq(schema_exports.invoices.customerId, customerId));
@@ -114566,7 +114988,7 @@ async function handleGetInvoices(input) {
114566
114988
  createdAt: schema_exports.invoices.createdAt
114567
114989
  }).from(schema_exports.invoices).where(and(...filters)).orderBy(desc(schema_exports.invoices.createdAt)).limit(Math.min(pageSize, 100));
114568
114990
  if (rows.length === 0) {
114569
- return textResponse3("No invoices found.");
114991
+ return textResponse4("No invoices found.");
114570
114992
  }
114571
114993
  const list = rows.map(
114572
114994
  (inv) => `**${inv.invoiceNumber ?? "(draft, no number)"}**
@@ -114576,7 +114998,7 @@ Customer: ${inv.customerName ?? inv.customerId ?? "(none)"}
114576
114998
  Issue date: ${inv.issueDate ? new Date(inv.issueDate).toLocaleDateString() : "-"} | Due: ${inv.dueDate ? new Date(inv.dueDate).toLocaleDateString() : "-"}
114577
114999
  `
114578
115000
  ).join("\n");
114579
- return textResponse3(
115001
+ return textResponse4(
114580
115002
  `Found ${rows.length} invoices:
114581
115003
 
114582
115004
  ${list}
@@ -114585,15 +115007,15 @@ Use \`get-invoice-by-id\` for line items and linked documents. Use \`link-docume
114585
115007
  }
114586
115008
  async function handleGetInvoiceById(input) {
114587
115009
  const { invoiceId } = input;
114588
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
115010
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
114589
115011
  const scope = await resolveTeamScope(input.teamId);
114590
115012
  if (!scope.ok) return scope.response;
114591
115013
  if (scope.teamIds.length === 0) {
114592
- return textResponse3("No accessible teams found.");
115014
+ return textResponse4("No accessible teams found.");
114593
115015
  }
114594
115016
  const invoice = await loadInvoiceByIdentifier(invoiceId, scope.teamIds);
114595
115017
  if (!invoice) {
114596
- return textResponse3(
115018
+ return textResponse4(
114597
115019
  `Invoice ${invoiceId} not found or you don't have access to it.`
114598
115020
  );
114599
115021
  }
@@ -114610,7 +115032,7 @@ async function handleGetInvoiceById(input) {
114610
115032
  );
114611
115033
  const linesText = lineItems.length > 0 ? lineItems.map((line2, i6) => formatLineItemDetail(line2, i6)).join("\n\n") : "(no line items)";
114612
115034
  const docsText = linkedDocs.length > 0 ? linkedDocs.map((d6) => `- ${d6.title} (${d6.type ?? "document"}) \u2014 ${d6.id}`).join("\n") : "(none)";
114613
- return textResponse3(
115035
+ return textResponse4(
114614
115036
  `**Invoice ${invoice.invoiceNumber ?? invoice.id}**
114615
115037
 
114616
115038
  ID: ${invoice.id}
@@ -114632,12 +115054,12 @@ ${docsText}
114632
115054
  }
114633
115055
  async function handleUpdateInvoice(input) {
114634
115056
  const { invoiceId } = input;
114635
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
115057
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
114636
115058
  const resolved = await resolveTeamId(input.teamId);
114637
115059
  if (!resolved.ok) return resolved.response;
114638
115060
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114639
115061
  if (!invoice) {
114640
- return textResponse3(
115062
+ return textResponse4(
114641
115063
  `Invoice ${invoiceId} not found or not owned by this team.`
114642
115064
  );
114643
115065
  }
@@ -114664,7 +115086,7 @@ async function handleUpdateInvoice(input) {
114664
115086
  defaults,
114665
115087
  invoice.teamId
114666
115088
  );
114667
- if (error49) return textResponse3(`Error: ${error49}`);
115089
+ if (error49) return textResponse4(`Error: ${error49}`);
114668
115090
  const totals = computeInvoiceTotals(
114669
115091
  items,
114670
115092
  defaults,
@@ -114677,28 +115099,28 @@ async function handleUpdateInvoice(input) {
114677
115099
  updates.amount = totals.amount;
114678
115100
  }
114679
115101
  if (Object.keys(updates).length === 0) {
114680
- return textResponse3(
115102
+ return textResponse4(
114681
115103
  "No fields to update. Provide at least one of: title, note, internalNote, dueDate, issueDate, lineItems."
114682
115104
  );
114683
115105
  }
114684
115106
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
114685
115107
  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**
115108
+ if (!updated) return textResponse4(`Failed to update invoice ${invoiceId}.`);
115109
+ return textResponse4(`\u2705 **Draft invoice updated**
114688
115110
 
114689
115111
  ${formatInvoiceSummary(updated)}`);
114690
115112
  }
114691
115113
  async function handleUpdateInvoiceLines(input) {
114692
115114
  const { invoiceId, lineItems: patches } = input;
114693
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
115115
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
114694
115116
  if (!patches || patches.length === 0) {
114695
- return textResponse3("Error: `lineItems` must be a non-empty array.");
115117
+ return textResponse4("Error: `lineItems` must be a non-empty array.");
114696
115118
  }
114697
115119
  const resolved = await resolveTeamId(input.teamId);
114698
115120
  if (!resolved.ok) return resolved.response;
114699
115121
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114700
115122
  if (!invoice) {
114701
- return textResponse3(
115123
+ return textResponse4(
114702
115124
  `Invoice ${invoiceId} not found or not owned by this team.`
114703
115125
  );
114704
115126
  }
@@ -114712,7 +115134,7 @@ async function handleUpdateInvoiceLines(input) {
114712
115134
  for (const patch of patches) {
114713
115135
  const index2 = patch.index;
114714
115136
  if (index2 < 0 || index2 >= items.length) {
114715
- return textResponse3(
115137
+ return textResponse4(
114716
115138
  `Error: line index ${index2} is out of range (invoice has ${items.length} line(s)).`
114717
115139
  );
114718
115140
  }
@@ -114739,13 +115161,13 @@ async function handleUpdateInvoiceLines(input) {
114739
115161
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
114740
115162
  }).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114741
115163
  if (!updated) {
114742
- return textResponse3(`Failed to update invoice lines for ${invoiceId}.`);
115164
+ return textResponse4(`Failed to update invoice lines for ${invoiceId}.`);
114743
115165
  }
114744
115166
  const changedLines = patches.map((p3) => {
114745
115167
  const line2 = items[p3.index];
114746
115168
  return `[${p3.index}] ${plainTextFromLineItemName(line2.name)}`;
114747
115169
  }).join("\n");
114748
- return textResponse3(
115170
+ return textResponse4(
114749
115171
  `\u2705 **Updated ${updatedCount} line item(s) on draft invoice ${updated.invoiceNumber ?? updated.id}**
114750
115172
 
114751
115173
  ${changedLines}
@@ -114755,13 +115177,13 @@ New total: ${updated.amount} ${updated.currency} (subtotal ${updated.subtotal},
114755
115177
  }
114756
115178
  async function handleAddProductToInvoice(input) {
114757
115179
  const { invoiceId, productId } = input;
114758
- if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
114759
- if (!productId) return textResponse3("Error: `productId` is required.");
115180
+ if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
115181
+ if (!productId) return textResponse4("Error: `productId` is required.");
114760
115182
  const resolved = await resolveTeamId(input.teamId);
114761
115183
  if (!resolved.ok) return resolved.response;
114762
115184
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114763
115185
  if (!invoice) {
114764
- return textResponse3(
115186
+ return textResponse4(
114765
115187
  `Invoice ${invoiceId} not found or not owned by this team.`
114766
115188
  );
114767
115189
  }
@@ -114769,7 +115191,7 @@ async function handleAddProductToInvoice(input) {
114769
115191
  const products = await loadProductsInTeam([productId], invoice.teamId);
114770
115192
  const product = products.get(productId);
114771
115193
  if (!product) {
114772
- return textResponse3(
115194
+ return textResponse4(
114773
115195
  `Product ${productId} not found or not owned by this team.`
114774
115196
  );
114775
115197
  }
@@ -114801,9 +115223,9 @@ async function handleAddProductToInvoice(input) {
114801
115223
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
114802
115224
  }).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114803
115225
  if (!updated) {
114804
- return textResponse3(`Failed to add product to invoice ${invoiceId}.`);
115226
+ return textResponse4(`Failed to add product to invoice ${invoiceId}.`);
114805
115227
  }
114806
- return textResponse3(
115228
+ return textResponse4(
114807
115229
  `\u2705 **Product added to draft invoice ${updated.invoiceNumber ?? updated.id}**
114808
115230
 
114809
115231
  ` + formatLineItemDetail(newItem, items.length - 1) + `
@@ -114815,12 +115237,12 @@ Clause and pricing variant are snapshotted on the line \u2014 later catalog edit
114815
115237
  async function handleLinkDocumentToInvoice(input) {
114816
115238
  const { documentId, invoiceId } = input;
114817
115239
  if (!documentId) {
114818
- return textResponse3("Error: `documentId` is required.");
115240
+ return textResponse4("Error: `documentId` is required.");
114819
115241
  }
114820
115242
  const scope = await resolveTeamScope(input.teamId);
114821
115243
  if (!scope.ok) return scope.response;
114822
115244
  if (scope.teamIds.length === 0) {
114823
- return textResponse3("No accessible teams found.");
115245
+ return textResponse4("No accessible teams found.");
114824
115246
  }
114825
115247
  const [doc] = await db.select({
114826
115248
  id: schema_exports.documents.id,
@@ -114835,24 +115257,24 @@ async function handleLinkDocumentToInvoice(input) {
114835
115257
  )
114836
115258
  ).limit(1);
114837
115259
  if (!doc) {
114838
- return textResponse3(
115260
+ return textResponse4(
114839
115261
  `Document ${documentId} not found or you don't have access to it.`
114840
115262
  );
114841
115263
  }
114842
115264
  if (!invoiceId) {
114843
115265
  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(
115266
+ return textResponse4(
114845
115267
  `\u2705 Document "${doc.title}" (${doc.id}) is unlinked from its invoice.`
114846
115268
  );
114847
115269
  }
114848
115270
  const invoice = await findAccessibleInvoice(invoiceId, [doc.teamId]);
114849
115271
  if (!invoice) {
114850
- return textResponse3(
115272
+ return textResponse4(
114851
115273
  `Error: invoice ${invoiceId} not found in team ${doc.teamId}. Use get-invoices to find a valid invoice id.`
114852
115274
  );
114853
115275
  }
114854
115276
  await db.update(schema_exports.documents).set({ invoiceId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
114855
- return textResponse3(
115277
+ return textResponse4(
114856
115278
  `\u2705 **Document linked to invoice!**
114857
115279
 
114858
115280
  Document: ${doc.title} (${doc.id})
@@ -114985,7 +115407,7 @@ ${description ? `Description: ${description}
114985
115407
  ]
114986
115408
  };
114987
115409
  }
114988
- function textResponse4(text3) {
115410
+ function textResponse5(text3) {
114989
115411
  return { content: [{ type: "text", text: text3 }] };
114990
115412
  }
114991
115413
  function memberLabel(m4) {
@@ -114999,7 +115421,7 @@ async function requireTeamOwner2(teamId, userId) {
114999
115421
  eq(schema_exports.usersOnTeam.teamId, teamId)
115000
115422
  )
115001
115423
  ).limit(1);
115002
- return membership?.role === "owner" ? null : textResponse4(OWNER_REQUIRED);
115424
+ return membership?.role === "owner" ? null : textResponse5(OWNER_REQUIRED);
115003
115425
  }
115004
115426
  async function setProjectMemberAccess(params) {
115005
115427
  const { projectId, teamId, memberIds, createdBy } = params;
@@ -115103,7 +115525,7 @@ async function resolveTeamMember(teamId, opts) {
115103
115525
  if (!match) {
115104
115526
  return {
115105
115527
  ok: false,
115106
- response: textResponse4(
115528
+ response: textResponse5(
115107
115529
  `User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
115108
115530
  )
115109
115531
  };
@@ -115116,7 +115538,7 @@ async function resolveTeamMember(teamId, opts) {
115116
115538
  if (matches.length === 0) {
115117
115539
  return {
115118
115540
  ok: false,
115119
- response: textResponse4(
115541
+ response: textResponse5(
115120
115542
  `No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
115121
115543
  )
115122
115544
  };
@@ -115124,7 +115546,7 @@ async function resolveTeamMember(teamId, opts) {
115124
115546
  if (matches.length > 1) {
115125
115547
  return {
115126
115548
  ok: false,
115127
- response: textResponse4(
115549
+ response: textResponse5(
115128
115550
  `Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
115129
115551
  )
115130
115552
  };
@@ -115133,7 +115555,7 @@ async function resolveTeamMember(teamId, opts) {
115133
115555
  }
115134
115556
  return {
115135
115557
  ok: false,
115136
- response: textResponse4(
115558
+ response: textResponse5(
115137
115559
  "Provide either a userId or an email to identify the member."
115138
115560
  )
115139
115561
  };
@@ -115182,7 +115604,7 @@ async function handleUpdateProject(input) {
115182
115604
  if (!resolved.ok) return resolved.response;
115183
115605
  const existing = await loadProjectInTeam(id, resolved.teamId);
115184
115606
  if (!existing) {
115185
- return textResponse4(
115607
+ return textResponse5(
115186
115608
  `Project ${id} not found, or it is not owned by this team.`
115187
115609
  );
115188
115610
  }
@@ -115197,7 +115619,7 @@ async function handleUpdateProject(input) {
115197
115619
  )
115198
115620
  ).limit(1);
115199
115621
  if (dupe) {
115200
- return textResponse4(
115622
+ return textResponse5(
115201
115623
  `A project named "${input.name}" already exists in this team. Choose a different name.`
115202
115624
  );
115203
115625
  }
@@ -115262,7 +115684,7 @@ async function handleUpdateProject(input) {
115262
115684
  customerName: schema_exports.customers.name
115263
115685
  }).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
115686
  if (!updated) {
115265
- return textResponse4(`Failed to update project ${id}.`);
115687
+ return textResponse5(`Failed to update project ${id}.`);
115266
115688
  }
115267
115689
  const lines = [
115268
115690
  "\u2705 **Project Updated**",
@@ -115280,7 +115702,7 @@ async function handleUpdateProject(input) {
115280
115702
  if (willRename) {
115281
115703
  lines.push("", "Note: tickets for this project were renumbered.");
115282
115704
  }
115283
- return textResponse4(lines.join("\n"));
115705
+ return textResponse5(lines.join("\n"));
115284
115706
  }
115285
115707
  async function handleGetProjectMembers(input) {
115286
115708
  const { projectId } = input;
@@ -115288,7 +115710,7 @@ async function handleGetProjectMembers(input) {
115288
115710
  if (!resolved.ok) return resolved.response;
115289
115711
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115290
115712
  if (!project) {
115291
- return textResponse4(
115713
+ return textResponse5(
115292
115714
  `Project ${projectId} not found, or it is not owned by this team.`
115293
115715
  );
115294
115716
  }
@@ -115317,7 +115739,7 @@ async function handleGetProjectMembers(input) {
115317
115739
  return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
115318
115740
  }).join("\n");
115319
115741
  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(
115742
+ return textResponse5(
115321
115743
  `**Project members for "${project.name}"** (ID: ${project.id})
115322
115744
 
115323
115745
  ${note}
@@ -115338,7 +115760,7 @@ async function handleSetProjectMembers(input) {
115338
115760
  if (ownerError) return ownerError;
115339
115761
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115340
115762
  if (!project) {
115341
- return textResponse4(
115763
+ return textResponse5(
115342
115764
  `Project ${projectId} not found, or it is not owned by this team.`
115343
115765
  );
115344
115766
  }
@@ -115376,7 +115798,7 @@ async function handleSetProjectMembers(input) {
115376
115798
 
115377
115799
  \u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
115378
115800
  }
115379
- return textResponse4(
115801
+ return textResponse5(
115380
115802
  `\u2705 **Project members updated**
115381
115803
 
115382
115804
  Members with explicit access to this project:
@@ -115392,7 +115814,7 @@ async function handleAddProjectMember(input) {
115392
115814
  if (ownerError) return ownerError;
115393
115815
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115394
115816
  if (!project) {
115395
- return textResponse4(
115817
+ return textResponse5(
115396
115818
  `Project ${projectId} not found, or it is not owned by this team.`
115397
115819
  );
115398
115820
  }
@@ -115403,7 +115825,7 @@ async function handleAddProjectMember(input) {
115403
115825
  if (!member2.ok) return member2.response;
115404
115826
  const state2 = await getProjectAccessState(resolved.teamId, projectId);
115405
115827
  if (state2.projectMemberIds.has(member2.member.userId)) {
115406
- return textResponse4(
115828
+ return textResponse5(
115407
115829
  `${memberLabel(member2.member)} already has explicit access to this project.`
115408
115830
  );
115409
115831
  }
@@ -115418,7 +115840,7 @@ async function handleAddProjectMember(input) {
115418
115840
  if (wasUnrestricted) {
115419
115841
  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
115842
  }
115421
- return textResponse4(text3);
115843
+ return textResponse5(text3);
115422
115844
  }
115423
115845
  async function handleRemoveProjectMember(input) {
115424
115846
  const ctx = getAuthContext();
@@ -115429,7 +115851,7 @@ async function handleRemoveProjectMember(input) {
115429
115851
  if (ownerError) return ownerError;
115430
115852
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115431
115853
  if (!project) {
115432
- return textResponse4(
115854
+ return textResponse5(
115433
115855
  `Project ${projectId} not found, or it is not owned by this team.`
115434
115856
  );
115435
115857
  }
@@ -115440,7 +115862,7 @@ async function handleRemoveProjectMember(input) {
115440
115862
  if (!member2.ok) return member2.response;
115441
115863
  const state2 = await getProjectAccessState(resolved.teamId, projectId);
115442
115864
  if (!state2.projectMemberIds.has(member2.member.userId)) {
115443
- return textResponse4(
115865
+ return textResponse5(
115444
115866
  `${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
115445
115867
  );
115446
115868
  }
@@ -115456,7 +115878,7 @@ async function handleRemoveProjectMember(input) {
115456
115878
  if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
115457
115879
  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
115880
  }
115459
- return textResponse4(text3);
115881
+ return textResponse5(text3);
115460
115882
  }
115461
115883
  async function loadProjectForCleanup(projectId, teamId) {
115462
115884
  const accessibleTeamIds = await getAccessibleTeamIds(teamId);
@@ -115484,25 +115906,25 @@ async function countProjectDependencies(projectId) {
115484
115906
  }
115485
115907
  async function handleArchiveProject(input) {
115486
115908
  const { projectId, reason } = input;
115487
- if (!projectId) return textResponse4("Error: `projectId` is required.");
115909
+ if (!projectId) return textResponse5("Error: `projectId` is required.");
115488
115910
  const resolved = await resolveTeamId(input.teamId);
115489
115911
  if (!resolved.ok) return resolved.response;
115490
115912
  const project = await loadProjectForCleanup(projectId, resolved.teamId);
115491
115913
  if (!project) {
115492
- return textResponse4(
115914
+ return textResponse5(
115493
115915
  `Project ${projectId} not found, or it is not owned by this team.`
115494
115916
  );
115495
115917
  }
115496
115918
  const state2 = getProjectArchiveState(project.settings);
115497
115919
  if (state2.archived) {
115498
- return textResponse4(
115920
+ return textResponse5(
115499
115921
  `Project "${project.name}" (${project.id}) is already archived${state2.archivedAt ? ` (since ${state2.archivedAt})` : ""}.`
115500
115922
  );
115501
115923
  }
115502
115924
  const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
115503
115925
  const nextSettings = withArchiveSettings(project.settings, archivedAt, reason);
115504
115926
  await db.update(schema_exports.projects).set({ settings: nextSettings, updatedAt: sql`now()` }).where(eq(schema_exports.projects.id, project.id));
115505
- return textResponse4(
115927
+ return textResponse5(
115506
115928
  `\u2705 **Project archived**
115507
115929
 
115508
115930
  Project: ${project.name}
@@ -115520,21 +115942,21 @@ Note: the archive flag is stored in \`projects.settings.archivedAt\`; the dashbo
115520
115942
  async function handleDeleteProject(input) {
115521
115943
  const ctx = getAuthContext();
115522
115944
  const { projectId, confirmEmptyOnly } = input;
115523
- if (!projectId) return textResponse4("Error: `projectId` is required.");
115945
+ if (!projectId) return textResponse5("Error: `projectId` is required.");
115524
115946
  const resolved = await resolveTeamId(input.teamId);
115525
115947
  if (!resolved.ok) return resolved.response;
115526
115948
  const ownerError = await requireTeamOwner2(resolved.teamId, ctx.userId);
115527
115949
  if (ownerError) return ownerError;
115528
115950
  const project = await loadProjectForCleanup(projectId, resolved.teamId);
115529
115951
  if (!project) {
115530
- return textResponse4(
115952
+ return textResponse5(
115531
115953
  `Project ${projectId} not found, or it is not owned by this team.`
115532
115954
  );
115533
115955
  }
115534
115956
  const deps = await countProjectDependencies(project.id);
115535
115957
  const summary = formatProjectDependencies(deps);
115536
115958
  if (!isProjectEmpty(deps)) {
115537
- return textResponse4(
115959
+ return textResponse5(
115538
115960
  `\u{1F6AB} **Delete blocked** \u2014 project "${project.name}" (${project.id}) is not empty.
115539
115961
 
115540
115962
  Dependencies: ${summary}.
@@ -115543,12 +115965,12 @@ A hard delete would orphan these records, so it is not allowed. Use archive-proj
115543
115965
  );
115544
115966
  }
115545
115967
  if (confirmEmptyOnly !== true) {
115546
- return textResponse4(
115968
+ return textResponse5(
115547
115969
  `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
115970
  );
115549
115971
  }
115550
115972
  await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
115551
- return textResponse4(
115973
+ return textResponse5(
115552
115974
  `\u2705 **Project deleted**
115553
115975
 
115554
115976
  Project: ${project.name}
@@ -115601,7 +116023,7 @@ var PRODUCT_COLUMNS2 = {
115601
116023
  createdAt: schema_exports.invoiceProducts.createdAt,
115602
116024
  updatedAt: schema_exports.invoiceProducts.updatedAt
115603
116025
  };
115604
- function textResponse5(text3) {
116026
+ function textResponse6(text3) {
115605
116027
  return { content: [{ type: "text", text: text3 }] };
115606
116028
  }
115607
116029
  function formatPrice(p3) {
@@ -115647,14 +116069,14 @@ async function handleGetProducts(input) {
115647
116069
  const { q: q3, currency, pageSize = 20 } = input;
115648
116070
  const status = input.status ?? "active";
115649
116071
  if (!PRODUCT_STATUSES.includes(status)) {
115650
- return textResponse5(
116072
+ return textResponse6(
115651
116073
  `Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
115652
116074
  );
115653
116075
  }
115654
116076
  const scope = await resolveTeamScope(input.teamId);
115655
116077
  if (!scope.ok) return scope.response;
115656
116078
  if (scope.teamIds.length === 0) {
115657
- return textResponse5("No accessible teams found.");
116079
+ return textResponse6("No accessible teams found.");
115658
116080
  }
115659
116081
  const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
115660
116082
  if (status === "active") {
@@ -115677,11 +116099,11 @@ async function handleGetProducts(input) {
115677
116099
  asc(schema_exports.invoiceProducts.name)
115678
116100
  ).limit(Math.min(pageSize, 100));
115679
116101
  if (rows.length === 0) {
115680
- return textResponse5(
116102
+ return textResponse6(
115681
116103
  `No products found${status !== "all" ? ` (status: ${status})` : ""}.`
115682
116104
  );
115683
116105
  }
115684
- return textResponse5(
116106
+ return textResponse6(
115685
116107
  `Found ${rows.length} product(s):
115686
116108
 
115687
116109
  ${rows.map(formatProduct).join("\n")}`
@@ -115689,11 +116111,11 @@ ${rows.map(formatProduct).join("\n")}`
115689
116111
  }
115690
116112
  async function handleGetProductById(input) {
115691
116113
  const { productId } = input;
115692
- if (!productId) return textResponse5("Error: `productId` is required.");
116114
+ if (!productId) return textResponse6("Error: `productId` is required.");
115693
116115
  const scope = await resolveTeamScope(input.teamId);
115694
116116
  if (!scope.ok) return scope.response;
115695
116117
  if (scope.teamIds.length === 0) {
115696
- return textResponse5("No accessible teams found.");
116118
+ return textResponse6("No accessible teams found.");
115697
116119
  }
115698
116120
  const [row] = await db.select(PRODUCT_COLUMNS2).from(schema_exports.invoiceProducts).where(
115699
116121
  and(
@@ -115702,11 +116124,11 @@ async function handleGetProductById(input) {
115702
116124
  )
115703
116125
  ).limit(1);
115704
116126
  if (!row) {
115705
- return textResponse5(
116127
+ return textResponse6(
115706
116128
  `Product ${productId} not found or you don't have access to it.`
115707
116129
  );
115708
116130
  }
115709
- return textResponse5(formatProduct(row));
116131
+ return textResponse6(formatProduct(row));
115710
116132
  }
115711
116133
  async function loadProductInTeam(productId, teamId) {
115712
116134
  const accessibleTeamIds = await getAccessibleTeamIds(teamId);
@@ -115721,10 +116143,10 @@ async function loadProductInTeam(productId, teamId) {
115721
116143
  async function handleCreateProduct(input) {
115722
116144
  const { name: name21, description, price, currency, unit } = input;
115723
116145
  if (!name21 || name21.trim().length === 0) {
115724
- return textResponse5("Error: `name` is required.");
116146
+ return textResponse6("Error: `name` is required.");
115725
116147
  }
115726
116148
  const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
115727
- if (enumError) return textResponse5(enumError);
116149
+ if (enumError) return textResponse6(enumError);
115728
116150
  const resolved = await resolveTeamId(input.teamId);
115729
116151
  if (!resolved.ok) return resolved.response;
115730
116152
  const [created] = await db.insert(schema_exports.invoiceProducts).values({
@@ -115744,8 +116166,8 @@ async function handleCreateProduct(input) {
115744
116166
  isActive: true,
115745
116167
  lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
115746
116168
  }).returning(PRODUCT_COLUMNS2);
115747
- if (!created) return textResponse5("Failed to create product.");
115748
- return textResponse5(
116169
+ if (!created) return textResponse6("Failed to create product.");
116170
+ return textResponse6(
115749
116171
  `\u2705 **Product created**
115750
116172
 
115751
116173
  ${formatProduct(created)}`
@@ -115753,21 +116175,21 @@ ${formatProduct(created)}`
115753
116175
  }
115754
116176
  async function handleUpdateProduct(input) {
115755
116177
  const { productId } = input;
115756
- if (!productId) return textResponse5("Error: `productId` is required.");
116178
+ if (!productId) return textResponse6("Error: `productId` is required.");
115757
116179
  const resolved = await resolveTeamId(input.teamId);
115758
116180
  if (!resolved.ok) return resolved.response;
115759
116181
  const existing = await loadProductInTeam(productId, resolved.teamId);
115760
116182
  if (!existing) {
115761
- return textResponse5(
116183
+ return textResponse6(
115762
116184
  `Product ${productId} not found, or it is not owned by this team.`
115763
116185
  );
115764
116186
  }
115765
116187
  const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
115766
- if (enumError) return textResponse5(enumError);
116188
+ if (enumError) return textResponse6(enumError);
115767
116189
  const updates = {};
115768
116190
  if (input.name !== void 0) {
115769
116191
  if (!input.name || input.name.trim().length === 0) {
115770
- return textResponse5("Error: `name` cannot be empty.");
116192
+ return textResponse6("Error: `name` cannot be empty.");
115771
116193
  }
115772
116194
  updates.name = input.name.trim();
115773
116195
  }
@@ -115788,14 +116210,14 @@ async function handleUpdateProduct(input) {
115788
116210
  updates.clause = serializeProductClause(input.clause);
115789
116211
  }
115790
116212
  if (Object.keys(updates).length === 0) {
115791
- return textResponse5(
116213
+ return textResponse6(
115792
116214
  "No fields to update. Provide at least one of: name, description, price, currency, unit, isActive, billingType, category, includedItems, optional, tier, sortOrder, clause."
115793
116215
  );
115794
116216
  }
115795
116217
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
115796
116218
  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(
116219
+ if (!updated) return textResponse6(`Failed to update product ${productId}.`);
116220
+ return textResponse6(
115799
116221
  `\u2705 **Product updated**
115800
116222
 
115801
116223
  ${formatProduct(updated)}
@@ -115804,23 +116226,23 @@ Note: this only affects future invoices/quotes. Existing documents keep their li
115804
116226
  }
115805
116227
  async function handleArchiveProduct(input) {
115806
116228
  const { productId, reason } = input;
115807
- if (!productId) return textResponse5("Error: `productId` is required.");
116229
+ if (!productId) return textResponse6("Error: `productId` is required.");
115808
116230
  const resolved = await resolveTeamId(input.teamId);
115809
116231
  if (!resolved.ok) return resolved.response;
115810
116232
  const existing = await loadProductInTeam(productId, resolved.teamId);
115811
116233
  if (!existing) {
115812
- return textResponse5(
116234
+ return textResponse6(
115813
116235
  `Product ${productId} not found, or it is not owned by this team.`
115814
116236
  );
115815
116237
  }
115816
116238
  if (!existing.isActive) {
115817
- return textResponse5(
116239
+ return textResponse6(
115818
116240
  `Product "${existing.name}" (${existing.id}) is already archived.`
115819
116241
  );
115820
116242
  }
115821
116243
  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(
116244
+ if (!archived) return textResponse6(`Failed to archive product ${productId}.`);
116245
+ return textResponse6(
115824
116246
  `\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
115825
116247
 
115826
116248
  ${formatProduct(archived)}${reason ? `Reason: ${reason}
@@ -116870,7 +117292,7 @@ var QUOTE_STATUSES = [
116870
117292
  "expired"
116871
117293
  ];
116872
117294
  var SAFE_DRAFT_STATUSES = /* @__PURE__ */ new Set(["draft"]);
116873
- function textResponse6(text3) {
117295
+ function textResponse7(text3) {
116874
117296
  return { content: [{ type: "text", text: text3 }] };
116875
117297
  }
116876
117298
  async function loadTemplateDefaults(teamId) {
@@ -117040,14 +117462,14 @@ function tiptapNote2(text3) {
117040
117462
  async function handleGetQuotes(input) {
117041
117463
  const { customerId, status, q: q3, pageSize = 20 } = input;
117042
117464
  if (status && !QUOTE_STATUSES.includes(status)) {
117043
- return textResponse6(
117465
+ return textResponse7(
117044
117466
  `Error: invalid status "${status}". Allowed: ${QUOTE_STATUSES.join(", ")}.`
117045
117467
  );
117046
117468
  }
117047
117469
  const scope = await resolveTeamScope(input.teamId);
117048
117470
  if (!scope.ok) return scope.response;
117049
117471
  if (scope.teamIds.length === 0) {
117050
- return textResponse6("No accessible teams found.");
117472
+ return textResponse7("No accessible teams found.");
117051
117473
  }
117052
117474
  const filters = [inArray(schema_exports.quotations.teamId, scope.teamIds)];
117053
117475
  if (customerId) filters.push(eq(schema_exports.quotations.customerId, customerId));
@@ -117062,10 +117484,10 @@ async function handleGetQuotes(input) {
117062
117484
  }
117063
117485
  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
117486
  if (rows.length === 0) {
117065
- return textResponse6("No quotes found.");
117487
+ return textResponse7("No quotes found.");
117066
117488
  }
117067
117489
  const note = input.projectId ? "\nNote: `projectId` was ignored \u2014 quotations are not linked to projects." : "";
117068
- return textResponse6(
117490
+ return textResponse7(
117069
117491
  `Found ${rows.length} quote(s):
117070
117492
 
117071
117493
  ${rows.map(formatQuote).join("\n")}${note}`
@@ -117073,10 +117495,10 @@ ${rows.map(formatQuote).join("\n")}${note}`
117073
117495
  }
117074
117496
  async function handleCreateQuote(input) {
117075
117497
  const { customerId } = input;
117076
- if (!customerId) return textResponse6("Error: `customerId` is required.");
117498
+ if (!customerId) return textResponse7("Error: `customerId` is required.");
117077
117499
  const status = input.status ?? "draft";
117078
117500
  if (!SAFE_DRAFT_STATUSES.has(status)) {
117079
- return textResponse6(
117501
+ return textResponse7(
117080
117502
  `Error: this tool only creates draft quotes. Requested status "${status}" is not allowed. Sending/accepting a quote is a manual dashboard action.`
117081
117503
  );
117082
117504
  }
@@ -117099,7 +117521,7 @@ async function handleCreateQuote(input) {
117099
117521
  )
117100
117522
  ).limit(1);
117101
117523
  if (!customer) {
117102
- return textResponse6(
117524
+ return textResponse7(
117103
117525
  `Customer ${customerId} not found or not owned by this team.`
117104
117526
  );
117105
117527
  }
@@ -117109,7 +117531,7 @@ async function handleCreateQuote(input) {
117109
117531
  defaults,
117110
117532
  teamId
117111
117533
  );
117112
- if (error49) return textResponse6(`Error: ${error49}`);
117534
+ if (error49) return textResponse7(`Error: ${error49}`);
117113
117535
  const totals = computeTotals(items, defaults);
117114
117536
  const quotationNumber = await nextQuotationNumber(teamId);
117115
117537
  const template = buildQuoteTemplate(defaults, input.title);
@@ -117184,8 +117606,8 @@ async function handleCreateQuote(input) {
117184
117606
  tax: totals.tax,
117185
117607
  amount: totals.amount
117186
117608
  }).returning(QUOTE_COLUMNS);
117187
- if (!created) return textResponse6("Failed to create quote.");
117188
- return textResponse6(
117609
+ if (!created) return textResponse7("Failed to create quote.");
117610
+ return textResponse7(
117189
117611
  `\u2705 **Draft quote created**
117190
117612
 
117191
117613
  ${formatQuote(created)}
@@ -117203,15 +117625,15 @@ async function loadQuoteInTeam(id, teamId) {
117203
117625
  return row ?? null;
117204
117626
  }
117205
117627
  function notDraftResponse2(quote) {
117206
- return textResponse6(
117628
+ return textResponse7(
117207
117629
  `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
117630
  );
117209
117631
  }
117210
117632
  async function handleUpdateQuote(input) {
117211
117633
  const { id } = input;
117212
- if (!id) return textResponse6("Error: `id` is required.");
117634
+ if (!id) return textResponse7("Error: `id` is required.");
117213
117635
  if (input.status !== void 0 && !SAFE_DRAFT_STATUSES.has(input.status)) {
117214
- return textResponse6(
117636
+ return textResponse7(
117215
117637
  `Error: status can only stay within {${[...SAFE_DRAFT_STATUSES].join(", ")}}. "${input.status}" (send/accept/reject/expire) must be done manually from the dashboard.`
117216
117638
  );
117217
117639
  }
@@ -117219,7 +117641,7 @@ async function handleUpdateQuote(input) {
117219
117641
  if (!resolved.ok) return resolved.response;
117220
117642
  const quote = await loadQuoteInTeam(id, resolved.teamId);
117221
117643
  if (!quote) {
117222
- return textResponse6(`Quote ${id} not found or not owned by this team.`);
117644
+ return textResponse7(`Quote ${id} not found or not owned by this team.`);
117223
117645
  }
117224
117646
  if (quote.status !== "draft") return notDraftResponse2(quote);
117225
117647
  const defaults = templateDefaultsFromStored(quote.template, quote.currency);
@@ -117239,7 +117661,7 @@ async function handleUpdateQuote(input) {
117239
117661
  defaults,
117240
117662
  quote.teamId
117241
117663
  );
117242
- if (error49) return textResponse6(`Error: ${error49}`);
117664
+ if (error49) return textResponse7(`Error: ${error49}`);
117243
117665
  const totals = computeTotals(items, defaults);
117244
117666
  updates.lineItems = items;
117245
117667
  updates.subtotal = totals.subtotal;
@@ -117251,32 +117673,32 @@ async function handleUpdateQuote(input) {
117251
117673
  });
117252
117674
  }
117253
117675
  if (Object.keys(updates).length === 0) {
117254
- return textResponse6(
117676
+ return textResponse7(
117255
117677
  "No fields to update. Provide at least one of: title, description, validUntil, lineItems."
117256
117678
  );
117257
117679
  }
117258
117680
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
117259
117681
  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**
117682
+ if (!updated) return textResponse7(`Failed to update quote ${id}.`);
117683
+ return textResponse7(`\u2705 **Draft quote updated**
117262
117684
 
117263
117685
  ${formatQuote(updated)}`);
117264
117686
  }
117265
117687
  async function handleAddProductToQuote(input) {
117266
117688
  const { quoteId, productId } = input;
117267
- if (!quoteId) return textResponse6("Error: `quoteId` is required.");
117268
- if (!productId) return textResponse6("Error: `productId` is required.");
117689
+ if (!quoteId) return textResponse7("Error: `quoteId` is required.");
117690
+ if (!productId) return textResponse7("Error: `productId` is required.");
117269
117691
  const resolved = await resolveTeamId(input.teamId);
117270
117692
  if (!resolved.ok) return resolved.response;
117271
117693
  const quote = await loadQuoteInTeam(quoteId, resolved.teamId);
117272
117694
  if (!quote) {
117273
- return textResponse6(`Quote ${quoteId} not found or not owned by this team.`);
117695
+ return textResponse7(`Quote ${quoteId} not found or not owned by this team.`);
117274
117696
  }
117275
117697
  if (quote.status !== "draft") return notDraftResponse2(quote);
117276
117698
  const products = await loadProductsInTeam2([productId], quote.teamId);
117277
117699
  const product = products.get(productId);
117278
117700
  if (!product) {
117279
- return textResponse6(
117701
+ return textResponse7(
117280
117702
  `Product ${productId} not found or not owned by this team.`
117281
117703
  );
117282
117704
  }
@@ -117306,7 +117728,7 @@ async function handleAddProductToQuote(input) {
117306
117728
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
117307
117729
  }).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
117308
117730
  if (!updated) {
117309
- return textResponse6(`Failed to add product to quote ${quoteId}.`);
117731
+ return textResponse7(`Failed to add product to quote ${quoteId}.`);
117310
117732
  }
117311
117733
  await db.update(schema_exports.invoiceProducts).set({
117312
117734
  usageCount: sql`${schema_exports.invoiceProducts.usageCount} + 1`,
@@ -117322,7 +117744,7 @@ async function handleAddProductToQuote(input) {
117322
117744
  if (meta5.includedItems && meta5.includedItems.length > 0) {
117323
117745
  metaParts.push(`included=[${meta5.includedItems.join(", ")}]`);
117324
117746
  }
117325
- return textResponse6(
117747
+ return textResponse7(
117326
117748
  `\u2705 **Product added to draft quote ${updated.quotationNumber ?? updated.id}**
117327
117749
 
117328
117750
  Line item: ${newItem.name} \xD7 ${newItem.quantity}${newItem.unit ? ` ${newItem.unit}` : ""} @ ${newItem.price} ${snap.currency}
@@ -123057,7 +123479,7 @@ function formatDeleteAttachmentRefusal(reason, context2) {
123057
123479
  }
123058
123480
 
123059
123481
  // src/tools/ticket-attachments.ts
123060
- function textResponse7(text3) {
123482
+ function textResponse8(text3) {
123061
123483
  return { content: [{ type: "text", text: text3 }] };
123062
123484
  }
123063
123485
  async function findAttachment(attachmentId) {
@@ -123130,7 +123552,7 @@ ${url3}`
123130
123552
  async function handleUploadTicketAttachment(input) {
123131
123553
  const ctx = getAuthContext() ?? authContext;
123132
123554
  if (!ctx) {
123133
- return textResponse7("Error: Not authenticated.");
123555
+ return textResponse8("Error: Not authenticated.");
123134
123556
  }
123135
123557
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
123136
123558
  if (!access.ok) return access.response;
@@ -123146,12 +123568,12 @@ async function handleUploadTicketAttachment(input) {
123146
123568
  userId: ctx.userId
123147
123569
  });
123148
123570
  if (!resolved.ok) {
123149
- return textResponse7(resolved.message);
123571
+ return textResponse8(resolved.message);
123150
123572
  }
123151
123573
  const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
123152
123574
  const validationError = validateAttachmentBuffer(buffer2, mimeType);
123153
123575
  if (validationError) {
123154
- return textResponse7(validationError.message);
123576
+ return textResponse8(validationError.message);
123155
123577
  }
123156
123578
  const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
123157
123579
  try {
@@ -123162,7 +123584,7 @@ async function handleUploadTicketAttachment(input) {
123162
123584
  options: { contentType: mimeType, upsert: true }
123163
123585
  });
123164
123586
  } catch (error49) {
123165
- return textResponse7(
123587
+ return textResponse8(
123166
123588
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
123167
123589
  );
123168
123590
  }
@@ -123191,7 +123613,7 @@ async function handleUploadTicketAttachment(input) {
123191
123613
  url3 = signed.url;
123192
123614
  } catch {
123193
123615
  }
123194
- return textResponse7(
123616
+ return textResponse8(
123195
123617
  `\u{1F4CE} **Attached to ${ticket.ticketNumber}**
123196
123618
  File: ${fileName}
123197
123619
  Type: ${mimeType}
@@ -123205,18 +123627,18 @@ ${url3}` : "")
123205
123627
  async function handleDeleteTicketAttachment(input) {
123206
123628
  const ctx = getAuthContext() ?? authContext;
123207
123629
  if (!ctx) {
123208
- return textResponse7("Error: Not authenticated.");
123630
+ return textResponse8("Error: Not authenticated.");
123209
123631
  }
123210
123632
  const inputError = validateDeleteAttachmentInput(input.attachmentId);
123211
123633
  if (inputError) {
123212
- return textResponse7(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
123634
+ return textResponse8(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
123213
123635
  }
123214
123636
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
123215
123637
  if (!access.ok) return access.response;
123216
123638
  const ticket = access.ticket;
123217
123639
  const attachment = await findAttachment(input.attachmentId);
123218
123640
  if (!attachment) {
123219
- return textResponse7(
123641
+ return textResponse8(
123220
123642
  formatDeleteAttachmentRefusal("attachment_not_found", {
123221
123643
  attachmentId: input.attachmentId,
123222
123644
  ticketNumber: ticket.ticketNumber
@@ -123224,7 +123646,7 @@ async function handleDeleteTicketAttachment(input) {
123224
123646
  );
123225
123647
  }
123226
123648
  if (!validateAttachmentBelongsToTicket(attachment.ticketId, ticket.id)) {
123227
- return textResponse7(
123649
+ return textResponse8(
123228
123650
  formatDeleteAttachmentRefusal("wrong_ticket", {
123229
123651
  attachmentId: input.attachmentId,
123230
123652
  ticketNumber: ticket.ticketNumber,
@@ -123236,7 +123658,7 @@ async function handleDeleteTicketAttachment(input) {
123236
123658
  const table = attachment.source === "ticket" ? schema_exports.ticketAttachments : schema_exports.ticketCommentAttachments;
123237
123659
  const [deletedRow] = await db.delete(table).where(eq(table.id, input.attachmentId)).returning({ id: table.id });
123238
123660
  if (!deletedRow) {
123239
- return textResponse7(
123661
+ return textResponse8(
123240
123662
  `Failed to delete attachment ${input.attachmentId}. It may have been removed already.`
123241
123663
  );
123242
123664
  }
@@ -123266,7 +123688,7 @@ async function handleDeleteTicketAttachment(input) {
123266
123688
  fileName: attachment.fileName,
123267
123689
  source: attachment.source
123268
123690
  });
123269
- return textResponse7(JSON.stringify(result, null, 2));
123691
+ return textResponse8(JSON.stringify(result, null, 2));
123270
123692
  }
123271
123693
 
123272
123694
  // src/tools/tiptap-text.ts
@@ -123683,7 +124105,7 @@ function formatTagUsage(usage) {
123683
124105
  }
123684
124106
 
123685
124107
  // src/tools/tag-management.ts
123686
- function textResponse8(text3) {
124108
+ function textResponse9(text3) {
123687
124109
  return { content: [{ type: "text", text: text3 }] };
123688
124110
  }
123689
124111
  var TAG_COLUMNS = {
@@ -123724,24 +124146,24 @@ function scopeFilter(projectId) {
123724
124146
  return projectId === null ? isNull(schema_exports.tags.projectId) : eq(schema_exports.tags.projectId, projectId);
123725
124147
  }
123726
124148
  async function handleUpdateTag(input) {
123727
- if (!input.tagId) return textResponse8("Error: `tagId` is required.");
124149
+ if (!input.tagId) return textResponse9("Error: `tagId` is required.");
123728
124150
  const resolved = await resolveTeamId(input.teamId);
123729
124151
  if (!resolved.ok) return resolved.response;
123730
124152
  const existing = await loadTagInTeam(input.tagId, resolved.teamId);
123731
124153
  if (!existing) {
123732
- return textResponse8(
124154
+ return textResponse9(
123733
124155
  `Tag ${input.tagId} not found, or it is not owned by this team.`
123734
124156
  );
123735
124157
  }
123736
124158
  const renaming = input.name !== void 0;
123737
124159
  const rescoping = input.projectId !== void 0;
123738
124160
  if (!renaming && !rescoping) {
123739
- return textResponse8(
124161
+ return textResponse9(
123740
124162
  "No changes requested. Provide `name` to rename and/or `projectId` (string, or null for a general tag) to change scope."
123741
124163
  );
123742
124164
  }
123743
124165
  if (renaming && !isValidTagName(input.name)) {
123744
- return textResponse8("Error: `name` cannot be empty.");
124166
+ return textResponse9("Error: `name` cannot be empty.");
123745
124167
  }
123746
124168
  const nextName = renaming ? input.name.trim() : existing.name;
123747
124169
  const nextProjectId = rescoping ? input.projectId ?? null : existing.projectId;
@@ -123754,13 +124176,13 @@ async function handleUpdateTag(input) {
123754
124176
  )
123755
124177
  ).limit(1);
123756
124178
  if (collision) {
123757
- return textResponse8(
124179
+ return textResponse9(
123758
124180
  `\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
124181
  );
123760
124182
  }
123761
124183
  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(
124184
+ if (!updated) return textResponse9(`Failed to update tag ${input.tagId}.`);
124185
+ return textResponse9(
123764
124186
  `\u2705 **Tag updated**
123765
124187
 
123766
124188
  ${describeTag(updated)}
@@ -123769,34 +124191,34 @@ Existing ticket/customer/project/transaction tag relations are preserved.`
123769
124191
  );
123770
124192
  }
123771
124193
  async function handleDeleteTag(input) {
123772
- if (!input.tagId) return textResponse8("Error: `tagId` is required.");
124194
+ if (!input.tagId) return textResponse9("Error: `tagId` is required.");
123773
124195
  const mode = input.mode ?? "delete_if_unused";
123774
124196
  const resolved = await resolveTeamId(input.teamId);
123775
124197
  if (!resolved.ok) return resolved.response;
123776
124198
  const existing = await loadTagInTeam(input.tagId, resolved.teamId);
123777
124199
  if (!existing) {
123778
- return textResponse8(
124200
+ return textResponse9(
123779
124201
  `Tag ${input.tagId} not found, or it is not owned by this team.`
123780
124202
  );
123781
124203
  }
123782
124204
  const usage = await getTagUsage(existing.id);
123783
124205
  const total = totalTagUsage(usage);
123784
124206
  if (mode === "archive") {
123785
- return textResponse8(
124207
+ return textResponse9(
123786
124208
  `\u2139\uFE0F Archiving is not supported for team tags: the \`tags\` table has no archived column. ${describeTag(existing)} is used by ${formatTagUsage(usage)}.
123787
124209
 
123788
124210
  Options: use merge-tags to fold it into another tag, or delete it once it is unused (mode: delete_if_unused).`
123789
124211
  );
123790
124212
  }
123791
124213
  if (total > 0) {
123792
- return textResponse8(
124214
+ return textResponse9(
123793
124215
  `\u274C Refusing to delete ${describeTag(existing)}: it is still used by ${formatTagUsage(usage)}. Deleting would strip the tag off those entities.
123794
124216
 
123795
124217
  Use merge-tags to move usage onto another tag first, then delete the (now-empty) tag.`
123796
124218
  );
123797
124219
  }
123798
124220
  await db.delete(schema_exports.tags).where(eq(schema_exports.tags.id, existing.id));
123799
- return textResponse8(
124221
+ return textResponse9(
123800
124222
  `\u2705 **Tag deleted** (was unused): ${describeTag(existing)}`
123801
124223
  );
123802
124224
  }
@@ -123806,7 +124228,7 @@ async function resolveMergeTarget(teamId, input) {
123806
124228
  if (!tag2) {
123807
124229
  return {
123808
124230
  ok: false,
123809
- response: textResponse8(
124231
+ response: textResponse9(
123810
124232
  `Target tag ${input.targetTagId} not found, or it is not owned by this team.`
123811
124233
  )
123812
124234
  };
@@ -123816,7 +124238,7 @@ async function resolveMergeTarget(teamId, input) {
123816
124238
  if (!isValidTagName(input.targetName)) {
123817
124239
  return {
123818
124240
  ok: false,
123819
- response: textResponse8(
124241
+ response: textResponse9(
123820
124242
  "Error: provide either `targetTagId` or a non-empty `targetName`."
123821
124243
  )
123822
124244
  };
@@ -123834,14 +124256,14 @@ async function resolveMergeTarget(teamId, input) {
123834
124256
  }
123835
124257
  const [created] = await db.insert(schema_exports.tags).values({ teamId, name: input.targetName.trim(), projectId: null }).returning(TAG_COLUMNS);
123836
124258
  if (!created) {
123837
- return { ok: false, response: textResponse8("Failed to create target tag.") };
124259
+ return { ok: false, response: textResponse9("Failed to create target tag.") };
123838
124260
  }
123839
124261
  return { ok: true, tag: created, created: true };
123840
124262
  }
123841
124263
  async function handleMergeTags(input) {
123842
124264
  const rawSourceIds = [...new Set(input.sourceTagIds ?? [])].filter(Boolean);
123843
124265
  if (rawSourceIds.length === 0) {
123844
- return textResponse8("Error: `sourceTagIds` must contain at least one tag id.");
124266
+ return textResponse9("Error: `sourceTagIds` must contain at least one tag id.");
123845
124267
  }
123846
124268
  const resolved = await resolveTeamId(input.teamId);
123847
124269
  if (!resolved.ok) return resolved.response;
@@ -123855,7 +124277,7 @@ async function handleMergeTags(input) {
123855
124277
  const foundIds = new Set(sourceTags.map((t8) => t8.id));
123856
124278
  const missing = rawSourceIds.filter((id) => !foundIds.has(id));
123857
124279
  if (missing.length > 0) {
123858
- return textResponse8(
124280
+ return textResponse9(
123859
124281
  `Error: source tag(s) not found or not owned by this team: ${missing.join(", ")}.`
123860
124282
  );
123861
124283
  }
@@ -123863,7 +124285,7 @@ async function handleMergeTags(input) {
123863
124285
  if (!target.ok) return target.response;
123864
124286
  const sourcesToMerge = sourceTags.filter((t8) => t8.id !== target.tag.id);
123865
124287
  if (sourcesToMerge.length === 0) {
123866
- return textResponse8(
124288
+ return textResponse9(
123867
124289
  "Error: nothing to merge \u2014 the only source tag is the same as the target tag."
123868
124290
  );
123869
124291
  }
@@ -123960,7 +124382,7 @@ async function handleMergeTags(input) {
123960
124382
  const movedTotal = results.tickets.moved + results.customers.moved + results.projects.moved + results.transactions.moved;
123961
124383
  const skippedTotal = results.tickets.skipped + results.customers.skipped + results.projects.skipped + results.transactions.skipped;
123962
124384
  const line2 = (label, r6) => `- ${label}: ${r6.moved} moved, ${r6.skipped} skipped (duplicate)`;
123963
- return textResponse8(
124385
+ return textResponse9(
123964
124386
  `\u2705 **Tags merged** into ${describeTag(target.tag)}${target.created ? " (newly created)" : ""}
123965
124387
 
123966
124388
  Sources (${sourcesToMerge.length}): ${sourcesToMerge.map((t8) => `${t8.name} (${t8.id})`).join(", ")}
@@ -124346,15 +124768,15 @@ function attemptedLockedFields(update) {
124346
124768
  // src/tools/trips.ts
124347
124769
  var TRIP_TYPES = ["private", "business"];
124348
124770
  var BILLING_TYPES2 = TRIP_BILLING_TYPES;
124349
- function textResponse9(text3) {
124771
+ function textResponse10(text3) {
124350
124772
  return { content: [{ type: "text", text: text3 }] };
124351
124773
  }
124352
- function jsonResponse(payload) {
124774
+ function jsonResponse2(payload) {
124353
124775
  return {
124354
124776
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
124355
124777
  };
124356
124778
  }
124357
- function toNumber2(value) {
124779
+ function toNumber3(value) {
124358
124780
  if (value == null) return 0;
124359
124781
  if (typeof value === "number") return value;
124360
124782
  const parsed = Number.parseFloat(String(value));
@@ -124367,9 +124789,9 @@ function formatTrip(t8) {
124367
124789
  startLocation: t8.startLocation,
124368
124790
  endLocation: t8.endLocation,
124369
124791
  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,
124792
+ distance: t8.distance != null ? toNumber3(t8.distance) : null,
124793
+ odometerStart: t8.odometerStart != null ? toNumber3(t8.odometerStart) : null,
124794
+ odometerEnd: t8.odometerEnd != null ? toNumber3(t8.odometerEnd) : null,
124373
124795
  billingType: t8.billingType,
124374
124796
  rate: t8.rate,
124375
124797
  amount: t8.amount,
@@ -124402,19 +124824,19 @@ var TRIP_RELATIONS = {
124402
124824
  };
124403
124825
  async function handleGetTrips(input) {
124404
124826
  if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
124405
- return textResponse9(
124827
+ return textResponse10(
124406
124828
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
124407
124829
  );
124408
124830
  }
124409
124831
  if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
124410
- return textResponse9(
124832
+ return textResponse10(
124411
124833
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
124412
124834
  );
124413
124835
  }
124414
124836
  const scope = await resolveTeamScope(input.teamId);
124415
124837
  if (!scope.ok) return scope.response;
124416
124838
  if (scope.teamIds.length === 0) {
124417
- return textResponse9("No accessible teams found.");
124839
+ return textResponse10("No accessible teams found.");
124418
124840
  }
124419
124841
  const filters = [inArray(schema_exports.trips.teamId, scope.teamIds)];
124420
124842
  if (input.dateFrom) filters.push(gte(schema_exports.trips.date, input.dateFrom));
@@ -124446,7 +124868,7 @@ async function handleGetTrips(input) {
124446
124868
  });
124447
124869
  const totals = rows.reduce(
124448
124870
  (acc, t8) => {
124449
- const distance = toNumber2(t8.distance);
124871
+ const distance = toNumber3(t8.distance);
124450
124872
  const amount = t8.amount ?? 0;
124451
124873
  if (t8.tripType === "business") acc.businessKm += distance;
124452
124874
  else acc.privateKm += distance;
@@ -124456,7 +124878,7 @@ async function handleGetTrips(input) {
124456
124878
  },
124457
124879
  { businessKm: 0, privateKm: 0, totalKm: 0, totalAmount: 0 }
124458
124880
  );
124459
- return jsonResponse({
124881
+ return jsonResponse2({
124460
124882
  count: rows.length,
124461
124883
  totals: {
124462
124884
  businessKm: round23(totals.businessKm),
@@ -124516,20 +124938,20 @@ async function validateInvoice(invoiceId, teamId) {
124516
124938
  }
124517
124939
  async function handleCreateTrip(input) {
124518
124940
  const ctx = getAuthContext();
124519
- if (!input.date) return textResponse9("Error: `date` (YYYY-MM-DD) is required.");
124941
+ if (!input.date) return textResponse10("Error: `date` (YYYY-MM-DD) is required.");
124520
124942
  if (!input.startLocation || !input.endLocation) {
124521
- return textResponse9(
124943
+ return textResponse10(
124522
124944
  "Error: `startLocation` and `endLocation` are required."
124523
124945
  );
124524
124946
  }
124525
124947
  if (!input.tripType || !TRIP_TYPES.includes(input.tripType)) {
124526
- return textResponse9(
124948
+ return textResponse10(
124527
124949
  `Error: \`tripType\` is required and must be one of: ${TRIP_TYPES.join(", ")}.`
124528
124950
  );
124529
124951
  }
124530
124952
  const billingType = input.billingType ?? "not_billable";
124531
124953
  if (!BILLING_TYPES2.includes(billingType)) {
124532
- return textResponse9(
124954
+ return textResponse10(
124533
124955
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
124534
124956
  );
124535
124957
  }
@@ -124541,7 +124963,7 @@ async function handleCreateTrip(input) {
124541
124963
  customerId: input.customerId,
124542
124964
  vehicleId: input.vehicleId
124543
124965
  });
124544
- if (linkError) return textResponse9(`Error: ${linkError}`);
124966
+ if (linkError) return textResponse10(`Error: ${linkError}`);
124545
124967
  if (!input.allowDuplicate) {
124546
124968
  const dupFilters = [
124547
124969
  eq(schema_exports.trips.teamId, teamId),
@@ -124558,8 +124980,8 @@ async function handleCreateTrip(input) {
124558
124980
  }
124559
124981
  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
124982
  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.`
124983
+ return textResponse10(
124984
+ `\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
124985
  );
124564
124986
  }
124565
124987
  }
@@ -124588,7 +125010,7 @@ async function handleCreateTrip(input) {
124588
125010
  vehicleId: input.vehicleId ?? null,
124589
125011
  snapshotId: input.snapshotId ?? null
124590
125012
  }).returning({ id: schema_exports.trips.id });
124591
- if (!created) return textResponse9("Failed to create trip.");
125013
+ if (!created) return textResponse10("Failed to create trip.");
124592
125014
  const trip = await loadTripInTeams(created.id, [teamId]);
124593
125015
  return {
124594
125016
  content: [
@@ -124603,14 +125025,14 @@ ${JSON.stringify(formatTrip(trip), null, 2)}`
124603
125025
  }
124604
125026
  async function handleUpdateTrip(input) {
124605
125027
  const ctx = getAuthContext();
124606
- if (!input.id) return textResponse9("Error: `id` is required.");
125028
+ if (!input.id) return textResponse10("Error: `id` is required.");
124607
125029
  if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
124608
- return textResponse9(
125030
+ return textResponse10(
124609
125031
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
124610
125032
  );
124611
125033
  }
124612
125034
  if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
124613
- return textResponse9(
125035
+ return textResponse10(
124614
125036
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
124615
125037
  );
124616
125038
  }
@@ -124619,7 +125041,7 @@ async function handleUpdateTrip(input) {
124619
125041
  const accessibleTeamIds = await getAccessibleTeamIds(resolved.teamId);
124620
125042
  const existing = await loadTripInTeams(input.id, accessibleTeamIds);
124621
125043
  if (!existing) {
124622
- return textResponse9(
125044
+ return textResponse10(
124623
125045
  `Trip ${input.id} not found or you don't have access to it. Call get-trips to find a valid id.`
124624
125046
  );
124625
125047
  }
@@ -124630,7 +125052,7 @@ async function handleUpdateTrip(input) {
124630
125052
  input
124631
125053
  );
124632
125054
  if (attempted.length > 0) {
124633
- return textResponse9(
125055
+ return textResponse10(
124634
125056
  `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
125057
  );
124636
125058
  }
@@ -124640,10 +125062,10 @@ async function handleUpdateTrip(input) {
124640
125062
  customerId: input.customerId ?? void 0,
124641
125063
  vehicleId: input.vehicleId ?? void 0
124642
125064
  });
124643
- if (linkError) return textResponse9(`Error: ${linkError}`);
125065
+ if (linkError) return textResponse10(`Error: ${linkError}`);
124644
125066
  if (input.invoiceId) {
124645
125067
  const invoiceError = await validateInvoice(input.invoiceId, teamId);
124646
- if (invoiceError) return textResponse9(`Error: ${invoiceError}`);
125068
+ if (invoiceError) return textResponse10(`Error: ${invoiceError}`);
124647
125069
  }
124648
125070
  const updates = {};
124649
125071
  if (input.date !== void 0) updates.date = input.date;
@@ -124680,7 +125102,7 @@ async function handleUpdateTrip(input) {
124680
125102
  if (input.isInvoiced !== void 0) updates.isInvoiced = input.isInvoiced;
124681
125103
  if (input.amount === void 0 && (input.distance !== void 0 || input.rate !== void 0 || input.billingType !== void 0)) {
124682
125104
  const nextBilling = input.billingType ?? existing.billingType;
124683
- const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ? toNumber2(existing.distance) : null;
125105
+ const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ? toNumber3(existing.distance) : null;
124684
125106
  const nextRate = input.rate !== void 0 ? input.rate : existing.rate;
124685
125107
  const derived = deriveTripAmount({
124686
125108
  billingType: nextBilling,
@@ -124691,7 +125113,7 @@ async function handleUpdateTrip(input) {
124691
125113
  if (derived != null) updates.amount = derived;
124692
125114
  }
124693
125115
  if (Object.keys(updates).length === 0) {
124694
- return textResponse9(
125116
+ return textResponse10(
124695
125117
  "No fields to update. Provide at least one editable field."
124696
125118
  );
124697
125119
  }
@@ -124718,7 +125140,7 @@ async function handleGetVehicles(input) {
124718
125140
  const scope = await resolveTeamScope(input.teamId);
124719
125141
  if (!scope.ok) return scope.response;
124720
125142
  if (scope.teamIds.length === 0) {
124721
- return textResponse9("No accessible teams found.");
125143
+ return textResponse10("No accessible teams found.");
124722
125144
  }
124723
125145
  const filters = [inArray(schema_exports.vehicles.teamId, scope.teamIds)];
124724
125146
  if (input.q) filters.push(ilike(schema_exports.vehicles.name, `%${input.q}%`));
@@ -124729,13 +125151,13 @@ async function handleGetVehicles(input) {
124729
125151
  currentOdometer: schema_exports.vehicles.currentOdometer,
124730
125152
  teamId: schema_exports.vehicles.teamId
124731
125153
  }).from(schema_exports.vehicles).where(and(...filters)).orderBy(asc(schema_exports.vehicles.name)).limit(Math.min(input.pageSize ?? 50, 200));
124732
- return jsonResponse({
125154
+ return jsonResponse2({
124733
125155
  count: rows.length,
124734
125156
  vehicles: rows.map((v2) => ({
124735
125157
  id: v2.id,
124736
125158
  name: v2.name,
124737
125159
  licensePlate: v2.licensePlate,
124738
- currentOdometer: v2.currentOdometer != null ? toNumber2(v2.currentOdometer) : null
125160
+ currentOdometer: v2.currentOdometer != null ? toNumber3(v2.currentOdometer) : null
124739
125161
  }))
124740
125162
  });
124741
125163
  }
@@ -124744,7 +125166,7 @@ async function handleGetTripTemplates(input) {
124744
125166
  const scope = await resolveTeamScope(input.teamId);
124745
125167
  if (!scope.ok) return scope.response;
124746
125168
  if (scope.teamIds.length === 0) {
124747
- return textResponse9("No accessible teams found.");
125169
+ return textResponse10("No accessible teams found.");
124748
125170
  }
124749
125171
  const filters = [inArray(schema_exports.tripTemplates.teamId, scope.teamIds)];
124750
125172
  const userId = input.userId ?? ctx.userId;
@@ -124768,26 +125190,26 @@ async function handleGetTripTemplates(input) {
124768
125190
  vehicleId: schema_exports.tripTemplates.vehicleId,
124769
125191
  notes: schema_exports.tripTemplates.notes
124770
125192
  }).from(schema_exports.tripTemplates).where(and(...filters)).orderBy(asc(schema_exports.tripTemplates.name)).limit(Math.min(input.pageSize ?? 50, 200));
124771
- return jsonResponse({
125193
+ return jsonResponse2({
124772
125194
  count: rows.length,
124773
125195
  templates: rows.map((t8) => ({
124774
125196
  ...t8,
124775
- distance: t8.distance != null ? toNumber2(t8.distance) : null,
124776
- returnDistance: t8.returnDistance != null ? toNumber2(t8.returnDistance) : null
125197
+ distance: t8.distance != null ? toNumber3(t8.distance) : null,
125198
+ returnDistance: t8.returnDistance != null ? toNumber3(t8.returnDistance) : null
124777
125199
  }))
124778
125200
  });
124779
125201
  }
124780
125202
  async function handleGetFrequentTripsForProject(input) {
124781
125203
  const ctx = getAuthContext();
124782
125204
  if (!input.projectId) {
124783
- return textResponse9("Error: `projectId` is required.");
125205
+ return textResponse10("Error: `projectId` is required.");
124784
125206
  }
124785
125207
  const resolved = await resolveTeamId(input.teamId);
124786
125208
  if (!resolved.ok) return resolved.response;
124787
125209
  const teamId = resolved.teamId;
124788
125210
  const projectIds = await getAccessibleProjectIds(ctx.userId, teamId);
124789
125211
  if (!projectIds.includes(input.projectId)) {
124790
- return textResponse9(
125212
+ return textResponse10(
124791
125213
  `Project not found or no access: ${input.projectId}. Call get-projects first.`
124792
125214
  );
124793
125215
  }
@@ -124814,7 +125236,7 @@ async function handleGetFrequentTripsForProject(input) {
124814
125236
  schema_exports.trips.endLocation,
124815
125237
  schema_exports.trips.tripType
124816
125238
  ).orderBy(desc(sql`count(*)`), desc(sql`max(${schema_exports.trips.date})`)).limit(limitN);
124817
- return jsonResponse({
125239
+ return jsonResponse2({
124818
125240
  count: groups.length,
124819
125241
  daysBack,
124820
125242
  frequentTrips: groups.map((g6) => ({
@@ -124822,7 +125244,7 @@ async function handleGetFrequentTripsForProject(input) {
124822
125244
  endLocation: g6.endLocation,
124823
125245
  tripType: g6.tripType,
124824
125246
  count: g6.count,
124825
- avgDistance: g6.avgDistance != null ? round23(toNumber2(g6.avgDistance)) : null,
125247
+ avgDistance: g6.avgDistance != null ? round23(toNumber3(g6.avgDistance)) : null,
124826
125248
  lastUsedDate: g6.lastUsedDate
124827
125249
  }))
124828
125250
  });
@@ -125490,6 +125912,10 @@ function createMcpServer() {
125490
125912
  );
125491
125913
  case "log-hours":
125492
125914
  return await handleLogHours(asToolArgs(toolArgs));
125915
+ case "get-time-entries":
125916
+ return await handleGetTimeEntries(
125917
+ asToolArgs(toolArgs)
125918
+ );
125493
125919
  case "get-github-file":
125494
125920
  return await handleGetGithubFile(asToolArgs(toolArgs));
125495
125921
  case "list-github-directory":