@mgsoftwarebv/mcp-server-bridge 3.3.8 → 3.4.1

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
@@ -103488,16 +103488,6 @@ async function getUserAccessibleCustomerIds(userId) {
103488
103488
  }
103489
103489
  return [...all];
103490
103490
  }
103491
- async function resolveAiSessionId(prefix, teamIds) {
103492
- if (teamIds.length === 0) return null;
103493
- const rows = await db.select({ id: aiSessions.id }).from(aiSessions).where(
103494
- and(
103495
- teamIds.length === 1 ? eq(aiSessions.teamId, teamIds[0]) : sql`${aiSessions.teamId} = ANY(${teamIds}::uuid[])`,
103496
- sql`${aiSessions.id}::text LIKE ${`${prefix}%`}`
103497
- )
103498
- ).limit(1);
103499
- return rows[0]?.id ?? null;
103500
- }
103501
103491
 
103502
103492
  // src/auth.ts
103503
103493
  var authStorage = new AsyncLocalStorage();
@@ -105378,10 +105368,6 @@ var Server = class extends Protocol {
105378
105368
  function asToolArgs(input) {
105379
105369
  return input ?? {};
105380
105370
  }
105381
- function roundToNearest15Minutes(minutes) {
105382
- if (minutes <= 0) return 0;
105383
- return Math.round(minutes / 15) * 15;
105384
- }
105385
105371
  function buildTicketAccessPredicate(teamIds, projectIds, customerIds) {
105386
105372
  const branches = [];
105387
105373
  if (teamIds.length > 0) branches.push(inArray(schema_exports.tickets.teamId, teamIds));
@@ -106292,239 +106278,103 @@ var TOOLS = [
106292
106278
  }
106293
106279
  },
106294
106280
  {
106295
- name: "start-ai-session-smart",
106296
- description: "Start a new AI development session with automatic tracking",
106281
+ name: "get-products",
106282
+ description: "List catalog products used on invoices AND quotes (the shared `invoice_products` catalog). Each entry includes its ID (UUID), name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats. Editing or archiving a catalog product never changes existing invoices/quotes \u2014 those keep an immutable line-item snapshot; catalog changes only affect documents created afterwards.",
106297
106283
  inputSchema: {
106298
106284
  type: "object",
106299
106285
  properties: {
106300
106286
  teamId: teamIdProp,
106301
- ticketId: { type: "string" },
106302
- ticketUrl: { type: "string", description: "URL to the ticket" },
106303
- cursorSessionId: {
106287
+ q: {
106304
106288
  type: "string",
106305
- description: "Cursor session identifier"
106306
- },
106307
- totalEstimatedMinutes: {
106308
- type: "number",
106309
- description: "Total estimated time in minutes (senior dev WITHOUT AI, rounded to 15 min)"
106289
+ description: "Search query for product name/description"
106310
106290
  },
106311
- complexityScore: {
106312
- type: "number",
106313
- minimum: 1,
106314
- maximum: 10,
106315
- description: "Estimated complexity from 1-10"
106316
- }
106317
- },
106318
- required: ["ticketId", "totalEstimatedMinutes"]
106319
- }
106320
- },
106321
- {
106322
- name: "track-manual-follow-up",
106323
- description: "Track manual follow-up prompt by developer",
106324
- inputSchema: {
106325
- type: "object",
106326
- properties: {
106327
- teamId: teamIdProp,
106328
- aiSessionId: { type: "string" },
106329
- originalPrompt: { type: "string" },
106330
- aiResponse: { type: "string" },
106331
- developerFollowUp: { type: "string" },
106332
- followUpReason: {
106291
+ status: {
106333
106292
  type: "string",
106334
- enum: [
106335
- "incomplete_result",
106336
- "wrong_approach",
106337
- "needs_clarification",
106338
- "error_in_code"
106339
- ]
106293
+ enum: ["active", "archived", "all"],
106294
+ default: "active",
106295
+ description: "Filter by catalog status (archived = isActive false)"
106340
106296
  },
106341
- outcome: {
106297
+ currency: {
106342
106298
  type: "string",
106343
- enum: ["success", "partial_success", "still_failed"],
106344
- default: "success"
106299
+ description: "Filter by currency code (e.g. EUR)"
106345
106300
  },
106346
- estimatedMinutes: {
106347
- type: "number",
106348
- description: "Estimated time needed for this follow-up work (think as senior dev WITHOUT AI, in minutes)"
106349
- },
106350
- workDescription: {
106351
- type: "string",
106352
- description: "Detailed work description generated by AI (2-3 sentences, summarizing all work done in session including follow-ups)"
106353
- }
106354
- },
106355
- required: [
106356
- "aiSessionId",
106357
- "originalPrompt",
106358
- "aiResponse",
106359
- "developerFollowUp",
106360
- "followUpReason",
106361
- "estimatedMinutes",
106362
- "workDescription"
106363
- ]
106364
- }
106365
- },
106366
- {
106367
- name: "get-session-context",
106368
- description: "Get current session context for follow-up continuity",
106369
- inputSchema: {
106370
- type: "object",
106371
- properties: {
106372
- teamId: teamIdProp,
106373
- aiSessionId: { type: "string" },
106374
- includeTicketData: { type: "boolean", default: true },
106375
- includeTodoProgress: { type: "boolean", default: true },
106376
- includeFollowUpHistory: { type: "boolean", default: false }
106301
+ pageSize: { type: "number", default: 20, maximum: 100 }
106377
106302
  },
106378
- required: ["aiSessionId"]
106303
+ required: []
106379
106304
  }
106380
106305
  },
106381
106306
  {
106382
- name: "sync-session-todos",
106383
- description: "Synchronize todo list with AI session (replace existing) or add new todos",
106307
+ name: "get-product-by-id",
106308
+ description: "Get a single catalog product by its ID (UUID), including name, unit price, currency, unit, active/archived flag, configurable flag, and usage stats.",
106384
106309
  inputSchema: {
106385
106310
  type: "object",
106386
106311
  properties: {
106387
106312
  teamId: teamIdProp,
106388
- aiSessionId: { type: "string" },
106389
- todos: {
106390
- type: "array",
106391
- items: {
106392
- type: "object",
106393
- properties: {
106394
- todoId: {
106395
- type: "string",
106396
- description: "Optional external todo ID for tracking"
106397
- },
106398
- content: { type: "string" },
106399
- status: {
106400
- type: "string",
106401
- enum: ["pending", "in_progress", "completed", "cancelled"]
106402
- },
106403
- estimatedMinutes: { type: "number" }
106404
- },
106405
- required: ["content", "status"]
106406
- }
106407
- },
106408
- replaceAll: {
106409
- type: "boolean",
106410
- default: true,
106411
- description: "If true, replace all existing todos. If false, add new todos to existing ones"
106412
- }
106313
+ productId: { type: "string", description: "Product ID (UUID)" }
106413
106314
  },
106414
- required: ["aiSessionId", "todos"]
106315
+ required: ["productId"]
106415
106316
  }
106416
106317
  },
106417
106318
  {
106418
- name: "add-follow-up-todos",
106419
- description: "Add new todos from follow-up (without replacing existing ones)",
106319
+ name: "create-product",
106320
+ description: "Create a catalog product for use on invoices and quotes. Stored in the shared `invoice_products` catalog. This only adds a reusable catalog entry; it does not place the product on any document. Returns the created product with its ID and normalized fields.",
106420
106321
  inputSchema: {
106421
106322
  type: "object",
106422
106323
  properties: {
106423
106324
  teamId: teamIdProp,
106424
- aiSessionId: { type: "string" },
106425
- newTodos: {
106426
- type: "array",
106427
- items: {
106428
- type: "object",
106429
- properties: {
106430
- content: { type: "string" },
106431
- status: {
106432
- type: "string",
106433
- enum: ["pending", "in_progress"],
106434
- default: "pending"
106435
- },
106436
- estimatedMinutes: { type: "number" },
106437
- addedInFollowUp: { type: "boolean", default: true }
106438
- },
106439
- required: ["content"]
106440
- }
106325
+ name: { type: "string", description: "Product name" },
106326
+ description: { type: "string" },
106327
+ price: {
106328
+ type: "number",
106329
+ description: "Unit price (catalog default, excl. quantity)"
106441
106330
  },
106442
- followUpReason: {
106331
+ currency: { type: "string", description: "Currency code (e.g. EUR)" },
106332
+ unit: {
106443
106333
  type: "string",
106444
- description: "Why were these todos added in follow-up"
106334
+ description: "Unit label (e.g. hour, piece, month)"
106445
106335
  }
106446
106336
  },
106447
- required: ["aiSessionId", "newTodos"]
106448
- }
106449
- },
106450
- {
106451
- name: "update-session-status",
106452
- description: "Update AI session status and completion info",
106453
- inputSchema: {
106454
- type: "object",
106455
- properties: {
106456
- teamId: teamIdProp,
106457
- aiSessionId: { type: "string" },
106458
- status: {
106459
- type: "string",
106460
- enum: ["started", "in_progress", "paused", "completed", "failed"]
106461
- },
106462
- actualTimeMinutes: { type: "number" },
106463
- completionNotes: { type: "string" }
106464
- },
106465
- required: ["aiSessionId", "status"]
106466
- }
106467
- },
106468
- {
106469
- name: "get-completion-context",
106470
- description: "Get all context needed for Cursor AI to generate customer response",
106471
- inputSchema: {
106472
- type: "object",
106473
- properties: {
106474
- teamId: teamIdProp,
106475
- aiSessionId: { type: "string" },
106476
- includeFollowUps: { type: "boolean", default: true },
106477
- includeTimeMetrics: { type: "boolean", default: true },
106478
- includeTodos: { type: "boolean", default: true }
106479
- },
106480
- required: ["aiSessionId"]
106337
+ required: ["name"]
106481
106338
  }
106482
106339
  },
106483
106340
  {
106484
- name: "save-customer-response",
106485
- description: "Save customer response generated by Cursor AI",
106341
+ name: "update-product",
106342
+ description: "Update a catalog product's editable fields (name, description, price, currency, unit) or reactivate it (isActive: true). Only provided fields change. IMPORTANT: updates apply only to FUTURE invoices/quotes. Existing/sent/accepted/paid documents keep their immutable line-item snapshot and are never mutated. Find the product id via get-products.",
106486
106343
  inputSchema: {
106487
106344
  type: "object",
106488
106345
  properties: {
106489
106346
  teamId: teamIdProp,
106490
- aiSessionId: { type: "string" },
106491
- customerResponse: {
106492
- type: "string",
106493
- description: "Customer response generated by Cursor AI"
106347
+ productId: { type: "string", description: "Product ID (UUID)" },
106348
+ name: { type: "string" },
106349
+ description: { type: ["string", "null"] },
106350
+ price: {
106351
+ type: ["number", "null"],
106352
+ description: "Unit price (catalog default)"
106494
106353
  },
106495
- responseType: {
106496
- type: "string",
106497
- enum: ["completion", "progress_update", "needs_clarification"],
106498
- default: "completion"
106354
+ currency: { type: ["string", "null"] },
106355
+ unit: { type: ["string", "null"] },
106356
+ isActive: {
106357
+ type: "boolean",
106358
+ description: "Set true to reactivate an archived product"
106499
106359
  }
106500
106360
  },
106501
- required: ["aiSessionId", "customerResponse"]
106361
+ required: ["productId"]
106502
106362
  }
106503
106363
  },
106504
106364
  {
106505
- name: "complete-ai-session",
106506
- description: "Complete AI session with work summary - time calculated automatically",
106365
+ name: "archive-product",
106366
+ description: "Archive (soft-disable) a catalog product so it no longer appears in invoice/quote product pickers. Preferred over deletion: products referenced historically stay safe because invoices/quotes hold their own line-item snapshots. Reactivate later via update-product (isActive: true).",
106507
106367
  inputSchema: {
106508
106368
  type: "object",
106509
106369
  properties: {
106510
106370
  teamId: teamIdProp,
106511
- aiSessionId: { type: "string" },
106512
- workCompleted: {
106513
- type: "array",
106514
- items: { type: "string" },
106515
- description: "List of completed tasks/todos in English"
106516
- },
106517
- technicalSummary: {
106518
- type: "string",
106519
- description: "Technical summary of work done in English"
106520
- },
106521
- invoiceDescription: {
106371
+ productId: { type: "string", description: "Product ID (UUID)" },
106372
+ reason: {
106522
106373
  type: "string",
106523
- description: "Short invoice-friendly description in the language of the ticket (2-3 sentences max, suitable for billing)"
106524
- },
106525
- efficiencyNotes: { type: "string" }
106374
+ description: "Optional note about why it was archived (not persisted)"
106375
+ }
106526
106376
  },
106527
- required: ["aiSessionId", "workCompleted"]
106377
+ required: ["productId"]
106528
106378
  }
106529
106379
  },
106530
106380
  {
@@ -106542,10 +106392,6 @@ var TOOLS = [
106542
106392
  type: "string",
106543
106393
  description: "Ticket ID (UUID) - Optional. Cursor AI should call get-tickets (filtered by project) to try matching chat context to an open ticket. Only include if a clear match is found."
106544
106394
  },
106545
- aiSessionId: {
106546
- type: "string",
106547
- description: "AI Session ID - Optional. If a ticket has an active AI dev session, include this ID to link the hours to that session."
106548
- },
106549
106395
  workDescription: {
106550
106396
  type: "string",
106551
106397
  description: "Short description of the work done (for the tracker entry)"
@@ -112400,7 +112246,6 @@ async function handleLogHours(input) {
112400
112246
  const {
112401
112247
  projectId,
112402
112248
  ticketId,
112403
- aiSessionId,
112404
112249
  workDescription,
112405
112250
  estimatedHours,
112406
112251
  chatContextSummary
@@ -112409,7 +112254,6 @@ async function handleLogHours(input) {
112409
112254
  if (!scope.ok) return scope.response;
112410
112255
  let project = null;
112411
112256
  let ticket = null;
112412
- let aiSession = null;
112413
112257
  if (projectId) {
112414
112258
  if (!scope.projectIds.includes(projectId)) {
112415
112259
  throw new Error(
@@ -112451,15 +112295,6 @@ async function handleLogHours(input) {
112451
112295
  }
112452
112296
  ticket = ticketData;
112453
112297
  }
112454
- if (aiSessionId) {
112455
- const [sessionData] = await db.select({
112456
- id: schema_exports.aiSessions.id,
112457
- ticketId: schema_exports.aiSessions.ticketId,
112458
- status: schema_exports.aiSessions.status
112459
- }).from(schema_exports.aiSessions).where(eq(schema_exports.aiSessions.id, aiSessionId)).limit(1);
112460
- if (!sessionData) throw new Error(`AI Session not found: ${aiSessionId}.`);
112461
- aiSession = sessionData;
112462
- }
112463
112298
  let insertTeamId = ticket?.teamId ?? project?.teamId ?? null;
112464
112299
  if (!insertTeamId) {
112465
112300
  const resolved = await resolveTeamId(input.teamId);
@@ -112471,40 +112306,24 @@ async function handleLogHours(input) {
112471
112306
  let agendaEntry = null;
112472
112307
  let wasUpdated = false;
112473
112308
  let consolidatedCount = 0;
112474
- if (aiSession?.id || ticket?.id) {
112309
+ if (ticket?.id) {
112475
112310
  let existingEntries = [];
112476
- if (aiSession?.id) {
112311
+ const linkedEvents = await db.select({
112312
+ timesheetEventId: schema_exports.timesheetEventTickets.timesheetEventId
112313
+ }).from(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.ticketId, ticket.id));
112314
+ const eventIds = linkedEvents.map((e6) => e6.timesheetEventId);
112315
+ if (eventIds.length > 0) {
112477
112316
  existingEntries = await db.select({
112478
112317
  id: schema_exports.timesheetEvents.id,
112479
112318
  trackedDuration: schema_exports.timesheetEvents.trackedDuration,
112480
- projectId: schema_exports.timesheetEvents.projectId,
112481
- aiSessionId: schema_exports.timesheetEvents.aiSessionId
112319
+ projectId: schema_exports.timesheetEvents.projectId
112482
112320
  }).from(schema_exports.timesheetEvents).where(
112483
112321
  and(
112322
+ inArray(schema_exports.timesheetEvents.id, eventIds),
112484
112323
  eq(schema_exports.timesheetEvents.status, "draft"),
112485
- eq(schema_exports.timesheetEvents.userId, ctx.userId),
112486
- eq(schema_exports.timesheetEvents.aiSessionId, aiSession.id)
112324
+ eq(schema_exports.timesheetEvents.userId, ctx.userId)
112487
112325
  )
112488
112326
  ).orderBy(desc(schema_exports.timesheetEvents.createdAt));
112489
- } else if (ticket?.id) {
112490
- const linkedEvents = await db.select({
112491
- timesheetEventId: schema_exports.timesheetEventTickets.timesheetEventId
112492
- }).from(schema_exports.timesheetEventTickets).where(eq(schema_exports.timesheetEventTickets.ticketId, ticket.id));
112493
- const eventIds = linkedEvents.map((e6) => e6.timesheetEventId);
112494
- if (eventIds.length > 0) {
112495
- existingEntries = await db.select({
112496
- id: schema_exports.timesheetEvents.id,
112497
- trackedDuration: schema_exports.timesheetEvents.trackedDuration,
112498
- projectId: schema_exports.timesheetEvents.projectId,
112499
- aiSessionId: schema_exports.timesheetEvents.aiSessionId
112500
- }).from(schema_exports.timesheetEvents).where(
112501
- and(
112502
- inArray(schema_exports.timesheetEvents.id, eventIds),
112503
- eq(schema_exports.timesheetEvents.status, "draft"),
112504
- eq(schema_exports.timesheetEvents.userId, ctx.userId)
112505
- )
112506
- ).orderBy(desc(schema_exports.timesheetEvents.createdAt));
112507
- }
112508
112327
  }
112509
112328
  if (existingEntries.length > 0) {
112510
112329
  const existingEntry = existingEntries[0];
@@ -112523,8 +112342,7 @@ async function handleLogHours(input) {
112523
112342
  }).where(eq(schema_exports.timesheetEvents.id, existingEntry.id)).returning({
112524
112343
  id: schema_exports.timesheetEvents.id,
112525
112344
  trackedDuration: schema_exports.timesheetEvents.trackedDuration,
112526
- projectId: schema_exports.timesheetEvents.projectId,
112527
- aiSessionId: schema_exports.timesheetEvents.aiSessionId
112345
+ projectId: schema_exports.timesheetEvents.projectId
112528
112346
  });
112529
112347
  agendaEntry = updated ?? null;
112530
112348
  wasUpdated = true;
@@ -112536,7 +112354,6 @@ async function handleLogHours(input) {
112536
112354
  teamId: insertTeamId,
112537
112355
  userId: ctx.userId,
112538
112356
  projectId: project?.id ?? null,
112539
- aiSessionId: aiSession?.id ?? null,
112540
112357
  title: workDescription,
112541
112358
  description: chatContextSummary ?? workDescription,
112542
112359
  startTime: startTime.toISOString(),
@@ -112549,8 +112366,7 @@ async function handleLogHours(input) {
112549
112366
  }).returning({
112550
112367
  id: schema_exports.timesheetEvents.id,
112551
112368
  trackedDuration: schema_exports.timesheetEvents.trackedDuration,
112552
- projectId: schema_exports.timesheetEvents.projectId,
112553
- aiSessionId: schema_exports.timesheetEvents.aiSessionId
112369
+ projectId: schema_exports.timesheetEvents.projectId
112554
112370
  });
112555
112371
  agendaEntry = created ?? null;
112556
112372
  if (agendaEntry && ticket?.id) {
@@ -112581,9 +112397,6 @@ async function handleLogHours(input) {
112581
112397
  `;
112582
112398
  if (ticket)
112583
112399
  responseText += ` \u2022 Ticket: ${ticket.title} (${ticket.status})
112584
- `;
112585
- if (aiSession)
112586
- responseText += ` \u2022 AI Session: ${aiSession.id} (${aiSession.status})
112587
112400
  `;
112588
112401
  responseText += ` \u2022 Description: ${workDescription}
112589
112402
  `;
@@ -113292,868 +113105,201 @@ async function handleRemoveProjectMember(input) {
113292
113105
  return textResponse(text3);
113293
113106
  }
113294
113107
 
113295
- // src/tools/session-completion.ts
113296
- async function handleGetCompletionContext(input) {
113297
- const {
113298
- aiSessionId,
113299
- includeFollowUps = true,
113300
- includeTimeMetrics = true,
113301
- includeTodos = true
113302
- } = input;
113303
- const scope = await resolveTeamScope(input.teamId);
113304
- if (!scope.ok) return scope.response;
113305
- const prefix = aiSessionId.replace("ai-sess-", "");
113306
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
113307
- if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
113308
- const [session] = await db.select({
113309
- id: schema_exports.aiSessions.id,
113310
- ticketId: schema_exports.aiSessions.ticketId,
113311
- aiTimeEstimateMinutes: schema_exports.aiSessions.aiTimeEstimateMinutes,
113312
- actualTimeMinutes: schema_exports.aiSessions.actualTimeMinutes,
113313
- efficiencyScore: schema_exports.aiSessions.efficiencyScore,
113314
- createdAt: schema_exports.aiSessions.createdAt,
113315
- completedAt: schema_exports.aiSessions.completedAt,
113316
- status: schema_exports.aiSessions.status,
113317
- complexityScore: schema_exports.aiSessions.complexityScore
113318
- }).from(schema_exports.aiSessions).where(eq(schema_exports.aiSessions.id, fullSessionId)).limit(1);
113319
- if (!session) throw new Error(`Session not found: ${aiSessionId}`);
113320
- const [ticket] = await db.select({
113321
- ticketNumber: schema_exports.tickets.ticketNumber,
113322
- title: schema_exports.tickets.title,
113323
- description: schema_exports.tickets.description,
113324
- type: schema_exports.tickets.type,
113325
- priority: schema_exports.tickets.priority
113326
- }).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, session.ticketId)).limit(1);
113327
- if (!ticket) throw new Error("Ticket not found for session");
113328
- const contextData = {
113329
- session: {
113330
- id: aiSessionId,
113331
- status: session.status,
113332
- complexity: session.complexityScore,
113333
- createdAt: session.createdAt,
113334
- completedAt: session.completedAt
113335
- },
113336
- ticket: {
113337
- number: ticket.ticketNumber,
113338
- title: ticket.title,
113339
- description: ticket.description,
113340
- type: ticket.type,
113341
- priority: ticket.priority
113342
- }
113343
- };
113344
- if (includeTimeMetrics) {
113345
- const timeSaved = session.aiTimeEstimateMinutes && session.actualTimeMinutes ? Math.max(0, session.aiTimeEstimateMinutes - session.actualTimeMinutes) : null;
113346
- contextData.timeMetrics = {
113347
- estimatedMinutes: session.aiTimeEstimateMinutes,
113348
- actualMinutes: session.actualTimeMinutes,
113349
- timeSaved,
113350
- efficiency: session.efficiencyScore,
113351
- sessionDuration: session.completedAt && session.createdAt ? Math.round(
113352
- (new Date(session.completedAt).getTime() - new Date(session.createdAt).getTime()) / 6e4
113353
- ) : null
113354
- };
113355
- }
113356
- if (includeTodos) {
113357
- const todos3 = await db.select({
113358
- content: schema_exports.aiTodos.content,
113359
- status: schema_exports.aiTodos.status,
113360
- estimatedMinutes: schema_exports.aiTodos.estimatedMinutes,
113361
- actualMinutes: schema_exports.aiTodos.actualMinutes,
113362
- completedAt: schema_exports.aiTodos.completedAt
113363
- }).from(schema_exports.aiTodos).where(eq(schema_exports.aiTodos.aiSessionId, session.id)).orderBy(asc(schema_exports.aiTodos.createdAt));
113364
- contextData.todos = todos3;
113365
- }
113366
- if (includeFollowUps) {
113367
- const followUps = await db.select({
113368
- followUpReason: schema_exports.manualFollowUps.followUpReason,
113369
- outcome: schema_exports.manualFollowUps.outcome,
113370
- timeSpentMinutes: schema_exports.manualFollowUps.timeSpentMinutes,
113371
- createdAt: schema_exports.manualFollowUps.createdAt
113372
- }).from(schema_exports.manualFollowUps).where(eq(schema_exports.manualFollowUps.aiSessionId, session.id)).orderBy(asc(schema_exports.manualFollowUps.createdAt));
113373
- contextData.followUps = followUps;
113374
- }
113375
- const todosLen = contextData.todos ?? [];
113376
- const completedTodos = todosLen.filter(
113377
- (t8) => t8.status === "completed"
113378
- ).length;
113379
- const followUpsLen = contextData.followUps?.length ?? 0;
113380
- return {
113381
- content: [
113382
- {
113383
- type: "text",
113384
- text: `\u{1F4CB} **Completion Context Retrieved!**
113385
-
113386
- \u{1F3AB} **Ticket:** ${ticket.ticketNumber} - ${ticket.title}
113387
- \u{1F194} **Session:** ${aiSessionId} (${session.status})
113388
- \u23F1\uFE0F **Time:** ${session.actualTimeMinutes || "N/A"}/${session.aiTimeEstimateMinutes || "N/A"} minutes
113389
- \u{1F4CB} **Todos:** ${completedTodos}/${todosLen.length} completed
113390
- \u{1F504} **Follow-ups:** ${followUpsLen}
113391
-
113392
- \u2705 **Full context ready for Cursor AI to generate customer response!**
113393
-
113394
- **Context Data:**
113395
- \`\`\`json
113396
- ${JSON.stringify(contextData, null, 2)}\`\`\``
113397
- }
113398
- ]
113399
- };
113108
+ // src/tools/products.ts
113109
+ var PRODUCT_STATUSES = ["active", "archived", "all"];
113110
+ var PRODUCT_COLUMNS = {
113111
+ id: schema_exports.invoiceProducts.id,
113112
+ teamId: schema_exports.invoiceProducts.teamId,
113113
+ name: schema_exports.invoiceProducts.name,
113114
+ description: schema_exports.invoiceProducts.description,
113115
+ price: schema_exports.invoiceProducts.price,
113116
+ currency: schema_exports.invoiceProducts.currency,
113117
+ unit: schema_exports.invoiceProducts.unit,
113118
+ isConfigurable: schema_exports.invoiceProducts.isConfigurable,
113119
+ isActive: schema_exports.invoiceProducts.isActive,
113120
+ usageCount: schema_exports.invoiceProducts.usageCount,
113121
+ lastUsedAt: schema_exports.invoiceProducts.lastUsedAt,
113122
+ createdAt: schema_exports.invoiceProducts.createdAt,
113123
+ updatedAt: schema_exports.invoiceProducts.updatedAt
113124
+ };
113125
+ function textResponse2(text3) {
113126
+ return { content: [{ type: "text", text: text3 }] };
113400
113127
  }
113401
- async function handleSaveCustomerResponse(input) {
113402
- const { aiSessionId, customerResponse, responseType = "completion" } = input;
113403
- const scope = await resolveTeamScope(input.teamId);
113404
- if (!scope.ok) return scope.response;
113405
- const prefix = aiSessionId.replace("ai-sess-", "");
113406
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
113407
- if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
113408
- await db.insert(schema_exports.aiResponses).values({
113409
- aiSessionId: fullSessionId,
113410
- responseType,
113411
- content: customerResponse,
113412
- isReadyForCustomer: true,
113413
- providerApproved: false
113414
- });
113415
- return {
113416
- content: [
113417
- {
113418
- type: "text",
113419
- text: `\u{1F4BE} **Customer Response Saved!**
113420
-
113421
- \u{1F194} Session: ${aiSessionId}
113422
- \u{1F4DD} Response Type: ${responseType}
113423
- \u{1F4C4} Length: ${customerResponse.length} characters
113424
-
113425
- \u2705 **Response ready for provider approval**
113426
- \u{1F50D} Provider can review in AI tab before sending to customer
113427
-
113428
- **Preview:**
113429
- \`\`\`
113430
- ${customerResponse.substring(0, 200)}${customerResponse.length > 200 ? "..." : ""}\`\`\``
113431
- }
113432
- ]
113433
- };
113128
+ function formatPrice(p3) {
113129
+ if (p3.price == null) return "(no price)";
113130
+ return `${p3.price}${p3.currency ? ` ${p3.currency}` : ""}${p3.unit ? ` / ${p3.unit}` : ""}`;
113434
113131
  }
113435
- async function handleCompleteAiSession(input) {
113436
- const ctx = getAuthContext();
113437
- const {
113438
- aiSessionId,
113439
- workCompleted,
113440
- technicalSummary,
113441
- invoiceDescription,
113442
- efficiencyNotes
113443
- } = input;
113444
- const scope = await resolveTeamScope(input.teamId);
113445
- if (!scope.ok) return scope.response;
113446
- const prefix = aiSessionId.replace("ai-sess-", "");
113447
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
113448
- if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
113449
- const [existingSession] = await db.select({
113450
- id: schema_exports.aiSessions.id,
113451
- ticketId: schema_exports.aiSessions.ticketId,
113452
- aiTimeEstimateMinutes: schema_exports.aiSessions.aiTimeEstimateMinutes,
113453
- createdAt: schema_exports.aiSessions.createdAt,
113454
- teamId: schema_exports.aiSessions.teamId
113455
- }).from(schema_exports.aiSessions).where(eq(schema_exports.aiSessions.id, fullSessionId)).limit(1);
113456
- if (!existingSession) {
113457
- throw new Error(`Session not found: ${aiSessionId}`);
113458
- }
113459
- const completionTime = /* @__PURE__ */ new Date();
113460
- const sessionStartTime = new Date(existingSession.createdAt);
113461
- const timeSpentMinutes = Math.round(
113462
- (completionTime.getTime() - sessionStartTime.getTime()) / 6e4
113463
- );
113464
- const [session] = await db.update(schema_exports.aiSessions).set({
113465
- status: "completed",
113466
- actualTimeMinutes: timeSpentMinutes,
113467
- completedAt: completionTime.toISOString(),
113468
- efficiencyScore: null
113469
- }).where(eq(schema_exports.aiSessions.id, existingSession.id)).returning({
113470
- id: schema_exports.aiSessions.id,
113471
- ticketId: schema_exports.aiSessions.ticketId,
113472
- aiTimeEstimateMinutes: schema_exports.aiSessions.aiTimeEstimateMinutes,
113473
- createdAt: schema_exports.aiSessions.createdAt
113474
- });
113475
- if (!session) throw new Error(`Failed to update session: ${aiSessionId}`);
113476
- const efficiencyScore = session.aiTimeEstimateMinutes ? timeSpentMinutes / session.aiTimeEstimateMinutes : 1;
113477
- await db.update(schema_exports.aiSessions).set({ efficiencyScore: efficiencyScore.toFixed(2) }).where(eq(schema_exports.aiSessions.id, session.id));
113478
- const activePhases = await db.select().from(schema_exports.aiTimeLogs).where(
113479
- and(
113480
- eq(schema_exports.aiTimeLogs.aiSessionId, existingSession.id),
113481
- eq(schema_exports.aiTimeLogs.status, "in_progress")
113482
- )
113483
- );
113484
- for (const phase of activePhases) {
113485
- const duration5 = Math.round(
113486
- (completionTime.getTime() - new Date(phase.startedAt).getTime()) / 1e3
113487
- );
113488
- await db.update(schema_exports.aiTimeLogs).set({
113489
- endedAt: completionTime.toISOString(),
113490
- durationSeconds: duration5,
113491
- status: "completed"
113492
- }).where(eq(schema_exports.aiTimeLogs.id, phase.id));
113493
- }
113494
- await db.update(schema_exports.aiTimeLogs).set({ status: "skipped" }).where(
113495
- and(
113496
- eq(schema_exports.aiTimeLogs.aiSessionId, existingSession.id),
113497
- eq(schema_exports.aiTimeLogs.status, "pending"),
113498
- eq(schema_exports.aiTimeLogs.estimatedDurationSeconds, 0)
113499
- )
113500
- );
113501
- const sessionDuration = Math.round(
113502
- (completionTime.getTime() - new Date(session.createdAt).getTime()) / 6e4
113503
- );
113504
- const workSummary = `Completed ${workCompleted.length} tasks including: ${workCompleted.slice(0, 3).join(", ")}${workCompleted.length > 3 ? " and more" : ""}.`;
113505
- const [ticketInfo] = await db.select({
113506
- ticketNumber: schema_exports.tickets.ticketNumber,
113507
- title: schema_exports.tickets.title,
113508
- projectId: schema_exports.tickets.projectId
113509
- }).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, session.ticketId)).limit(1);
113510
- let completionDescription;
113511
- if (invoiceDescription) {
113512
- completionDescription = `${ticketInfo?.ticketNumber || "Ticket"}: ${invoiceDescription}`;
113513
- } else {
113514
- const workDescription = workCompleted.map((task, index2) => `${index2 + 1}. ${task}`).join("\n");
113515
- completionDescription = `${ticketInfo?.ticketNumber || "Ticket"}: ${technicalSummary || workSummary}
113516
-
113517
- Completed work:
113518
- ${workDescription}`;
113519
- }
113520
- const estimatedMinutes = session.aiTimeEstimateMinutes ?? timeSpentMinutes;
113521
- const sessionStart = new Date(session.createdAt);
113522
- const estimatedEnd = new Date(
113523
- sessionStart.getTime() + estimatedMinutes * 6e4
113524
- );
113525
- const existingAgendaEntries = await db.select({
113526
- id: schema_exports.timesheetEvents.id,
113527
- trackedDuration: schema_exports.timesheetEvents.trackedDuration
113528
- }).from(schema_exports.timesheetEvents).where(
113529
- and(
113530
- eq(schema_exports.timesheetEvents.aiSessionId, session.id),
113531
- eq(schema_exports.timesheetEvents.status, "draft")
113532
- )
113533
- ).orderBy(desc(schema_exports.timesheetEvents.createdAt));
113534
- let timesheetEventId = null;
113535
- let wasUpdated = false;
113536
- let consolidatedCount = 0;
113537
- const existingAgendaEntry = existingAgendaEntries[0] ?? null;
113538
- if (existingAgendaEntries.length > 1) {
113539
- const duplicateIds = existingAgendaEntries.slice(1).map((e6) => e6.id);
113540
- await db.delete(schema_exports.timesheetEvents).where(inArray(schema_exports.timesheetEvents.id, duplicateIds));
113541
- consolidatedCount = existingAgendaEntries.length - 1;
113542
- }
113543
- try {
113544
- if (existingAgendaEntry) {
113545
- const [updated] = await db.update(schema_exports.timesheetEvents).set({
113546
- title: ticketInfo?.title || "Development Work",
113547
- description: completionDescription,
113548
- endTime: estimatedEnd.toISOString(),
113549
- projectId: ticketInfo?.projectId ?? null,
113550
- trackedDuration: estimatedMinutes * 60
113551
- }).where(eq(schema_exports.timesheetEvents.id, existingAgendaEntry.id)).returning({ id: schema_exports.timesheetEvents.id });
113552
- timesheetEventId = updated?.id ?? null;
113553
- wasUpdated = true;
113554
- } else {
113555
- const [created] = await db.insert(schema_exports.timesheetEvents).values({
113556
- teamId: existingSession.teamId,
113557
- userId: ctx.userId,
113558
- title: ticketInfo?.title || "Development Work",
113559
- description: completionDescription,
113560
- startTime: sessionStart.toISOString(),
113561
- endTime: estimatedEnd.toISOString(),
113562
- projectId: ticketInfo?.projectId ?? null,
113563
- aiSessionId: session.id,
113564
- type: "work",
113565
- status: "draft",
113566
- allDay: false,
113567
- isTracked: true,
113568
- trackedDuration: estimatedMinutes * 60
113569
- }).returning({ id: schema_exports.timesheetEvents.id });
113570
- timesheetEventId = created?.id ?? null;
113571
- }
113572
- if (timesheetEventId && session.ticketId) {
113573
- await db.insert(schema_exports.timesheetEventTickets).values({
113574
- timesheetEventId,
113575
- ticketId: session.ticketId
113576
- }).onConflictDoNothing();
113577
- }
113578
- } catch (err) {
113579
- console.error(
113580
- `\u26A0\uFE0F Failed to ${wasUpdated ? "update" : "create"} agenda event:`,
113581
- err
113582
- );
113583
- }
113584
- if (consolidatedCount > 0) {
113585
- console.log(
113586
- `\u{1F9F9} Cleaned up ${consolidatedCount} duplicate agenda entries for session ${aiSessionId}`
113587
- );
113588
- }
113589
- let responseText = `\u{1F389} **AI Session Completed Successfully!**
113590
-
113591
- `;
113592
- responseText += `\u{1F194} Session: ${aiSessionId}
113593
- `;
113594
- responseText += `\u{1F4CA} **Performance Summary:**
113595
- `;
113596
- responseText += ` \u2022 Tasks Completed: ${workCompleted.length}
113597
- `;
113598
- responseText += ` \u2022 Time Spent: ${timeSpentMinutes} minutes
113599
- `;
113600
- responseText += ` \u2022 Estimated Time: ${session.aiTimeEstimateMinutes || "N/A"} minutes
113601
- `;
113602
- responseText += ` \u2022 Efficiency: ${efficiencyScore < 1 ? "\u{1F680}" : efficiencyScore > 1.5 ? "\u26A0\uFE0F" : "\u23F1\uFE0F"} ${(efficiencyScore * 100).toFixed(0)}%
113603
- `;
113604
- responseText += ` \u2022 Session Duration: ${sessionDuration} minutes
113605
-
113606
- `;
113607
- responseText += `\u2705 **Work Completed:**
113608
- `;
113609
- workCompleted.forEach((task, index2) => {
113610
- responseText += `${index2 + 1}. ${task}
113611
- `;
113612
- });
113613
- responseText += `
113614
- `;
113615
- if (technicalSummary) {
113616
- responseText += `\u{1F527} **Technical Summary:**
113617
- ${technicalSummary}
113618
-
113619
- `;
113620
- }
113621
- if (efficiencyNotes) {
113622
- responseText += `\u{1F4C8} **Efficiency Notes:**
113623
- ${efficiencyNotes}
113624
-
113625
- `;
113626
- }
113627
- if (timesheetEventId) {
113628
- responseText += `\u{1F4C5} **Timetrack Entry ${wasUpdated ? "Updated" : "Created"}:**
113629
- `;
113630
- responseText += ` \u2022 Agenda event ${wasUpdated ? "updated with final" : "created with"} work summary
113631
- `;
113632
- responseText += ` \u2022 Status: DRAFT (requires approval in agenda)
113633
- `;
113634
- responseText += ` \u2022 Duration: ${estimatedMinutes} minutes
113635
- `;
113636
- responseText += ` \u2022 Period: ${sessionStart.toLocaleString()} - ${completionTime.toLocaleString()}
113637
-
113638
- `;
113639
- }
113640
- responseText += `\u{1F4CB} **Context for Customer Response:**
113641
- `;
113642
- responseText += ` \u2022 Use "get-completion-context" to retrieve full context
113643
- `;
113644
- responseText += ` \u2022 Generate customer-friendly response based on completed work
113645
- `;
113646
- responseText += ` \u2022 Focus on business value and customer benefits
113647
-
113132
+ function formatProduct(p3) {
113133
+ const flags = [p3.isActive ? "active" : "archived"];
113134
+ if (p3.isConfigurable) flags.push("configurable");
113135
+ return `**${p3.name}** (${flags.join(", ")})
113136
+ ID: ${p3.id}
113137
+ Price: ${formatPrice(p3)}
113138
+ ${p3.description ? `Description: ${p3.description}
113139
+ ` : ""}Used: ${p3.usageCount}x${p3.lastUsedAt ? ` (last ${new Date(p3.lastUsedAt).toLocaleDateString()})` : ""}
113648
113140
  `;
113649
- responseText += `\u{1F3AF} **Session archived successfully!**`;
113650
- return { content: [{ type: "text", text: responseText }] };
113651
113141
  }
113652
-
113653
- // src/tools/sessions.ts
113654
- async function transitionToNextPhase(sessionId, currentPhase) {
113655
- try {
113656
- const now2 = /* @__PURE__ */ new Date();
113657
- const phaseOrder = [
113658
- "analysis",
113659
- "bug_investigation",
113660
- "development",
113661
- "communication"
113662
- ];
113663
- const allPhases = await db.select().from(schema_exports.aiTimeLogs).where(eq(schema_exports.aiTimeLogs.aiSessionId, sessionId)).orderBy(asc(schema_exports.aiTimeLogs.activityType));
113664
- let currentPhaseType = currentPhase;
113665
- if (!currentPhaseType) {
113666
- const activePhase = allPhases.find((p3) => p3.status === "in_progress");
113667
- currentPhaseType = activePhase?.activityType ?? void 0;
113668
- }
113669
- if (!currentPhaseType) {
113670
- const analysisPhase = allPhases.find(
113671
- (p3) => p3.activityType === "analysis"
113672
- );
113673
- if (analysisPhase && analysisPhase.status === "pending" && (analysisPhase.estimatedDurationSeconds ?? 0) > 0) {
113674
- await db.update(schema_exports.aiTimeLogs).set({ status: "in_progress", startedAt: now2.toISOString() }).where(eq(schema_exports.aiTimeLogs.id, analysisPhase.id));
113675
- console.error("\u2705 Started analysis phase");
113676
- }
113677
- return;
113678
- }
113679
- const currentPhaseRecord = allPhases.find(
113680
- (p3) => p3.activityType === currentPhaseType && p3.status === "in_progress"
113142
+ async function handleGetProducts(input) {
113143
+ const { q: q3, currency, pageSize = 20 } = input;
113144
+ const status = input.status ?? "active";
113145
+ if (!PRODUCT_STATUSES.includes(status)) {
113146
+ return textResponse2(
113147
+ `Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
113681
113148
  );
113682
- if (currentPhaseRecord) {
113683
- const duration5 = Math.round(
113684
- (now2.getTime() - new Date(currentPhaseRecord.startedAt).getTime()) / 1e3
113685
- );
113686
- await db.update(schema_exports.aiTimeLogs).set({
113687
- status: "completed",
113688
- endedAt: now2.toISOString(),
113689
- durationSeconds: duration5
113690
- }).where(eq(schema_exports.aiTimeLogs.id, currentPhaseRecord.id));
113691
- console.error(`\u2705 Completed phase: ${currentPhaseType} (${duration5}s)`);
113692
- }
113693
- const currentIndex = phaseOrder.indexOf(currentPhaseType);
113694
- if (currentIndex === -1 || currentIndex === phaseOrder.length - 1) {
113695
- console.error("No next phase to transition to");
113696
- return;
113697
- }
113698
- for (let i6 = currentIndex + 1; i6 < phaseOrder.length; i6++) {
113699
- const nextPhaseType = phaseOrder[i6];
113700
- const nextPhase = allPhases.find((p3) => p3.activityType === nextPhaseType);
113701
- if (!nextPhase) continue;
113702
- if ((nextPhase.estimatedDurationSeconds ?? 0) === 0) {
113703
- await db.update(schema_exports.aiTimeLogs).set({ status: "skipped" }).where(eq(schema_exports.aiTimeLogs.id, nextPhase.id));
113704
- console.error(
113705
- `\u23ED\uFE0F Skipped phase: ${nextPhaseType} (0 minutes estimated)`
113706
- );
113707
- continue;
113708
- }
113709
- if (nextPhase.status === "pending") {
113710
- await db.update(schema_exports.aiTimeLogs).set({ status: "in_progress", startedAt: now2.toISOString() }).where(eq(schema_exports.aiTimeLogs.id, nextPhase.id));
113711
- console.error(`\u2705 Started next phase: ${nextPhaseType}`);
113712
- return;
113713
- }
113714
- }
113715
- console.error("All remaining phases skipped or completed");
113716
- } catch (error49) {
113717
- console.error("Error transitioning to next phase:", error49);
113718
- }
113719
- }
113720
- async function handleStartAiSession(input) {
113721
- const ctx = getAuthContext();
113722
- const { ticketId, cursorSessionId, totalEstimatedMinutes, complexityScore } = input;
113723
- if (!totalEstimatedMinutes) {
113724
- throw new Error("totalEstimatedMinutes is required");
113725
113149
  }
113726
113150
  const scope = await resolveTeamScope(input.teamId);
113727
113151
  if (!scope.ok) return scope.response;
113728
- const [ticketRow] = await db.select({
113729
- teamId: schema_exports.tickets.teamId,
113730
- projectId: schema_exports.tickets.projectId,
113731
- customerId: schema_exports.tickets.customerId
113732
- }).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, ticketId)).limit(1);
113733
- if (!ticketRow) {
113734
- throw new Error(
113735
- `Ticket not found: ${ticketId}. Call get-tickets to find the correct ticket.`
113736
- );
113152
+ if (scope.teamIds.length === 0) {
113153
+ return textResponse2("No accessible teams found.");
113737
113154
  }
113738
- const hasTicketAccess = scope.teamIds.includes(ticketRow.teamId) || !!ticketRow.projectId && scope.projectIds.includes(ticketRow.projectId) || !!ticketRow.customerId && scope.customerIds.includes(ticketRow.customerId);
113739
- if (!hasTicketAccess) {
113740
- throw new Error(`No access to ticket: ${ticketId}.`);
113155
+ const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
113156
+ if (status === "active") {
113157
+ filters.push(eq(schema_exports.invoiceProducts.isActive, true));
113158
+ } else if (status === "archived") {
113159
+ filters.push(eq(schema_exports.invoiceProducts.isActive, false));
113741
113160
  }
113742
- const insertTeamId = ticketRow.teamId;
113743
- const roundedMinutes = roundToNearest15Minutes(totalEstimatedMinutes);
113744
- const sessionStartTime = /* @__PURE__ */ new Date();
113745
- const [sessionData] = await db.insert(schema_exports.aiSessions).values({
113746
- ticketId,
113747
- providerUserId: ctx.userId,
113748
- teamId: insertTeamId,
113749
- cursorSessionId: cursorSessionId ?? null,
113750
- aiTimeEstimateMinutes: roundedMinutes,
113751
- complexityScore: complexityScore ?? null,
113752
- status: "in_progress"
113753
- }).returning({
113754
- id: schema_exports.aiSessions.id,
113755
- ticketId: schema_exports.aiSessions.ticketId,
113756
- cursorSessionId: schema_exports.aiSessions.cursorSessionId,
113757
- createdAt: schema_exports.aiSessions.createdAt
113758
- });
113759
- if (!sessionData) {
113760
- throw new Error("Failed to create AI session");
113161
+ if (currency) filters.push(eq(schema_exports.invoiceProducts.currency, currency));
113162
+ if (q3) {
113163
+ filters.push(
113164
+ or(
113165
+ sql`${schema_exports.invoiceProducts.fts} @@ plainto_tsquery('english', ${q3})`,
113166
+ ilike(schema_exports.invoiceProducts.name, `%${q3}%`)
113167
+ )
113168
+ );
113761
113169
  }
113762
- const sessionId = `ai-sess-${sessionData.id.substring(0, 8)}`;
113763
- return {
113764
- content: [
113765
- {
113766
- type: "text",
113767
- text: `\u{1F680} **AI Session Started!**
113768
-
113769
- \u{1F194} Session ID: **${sessionId}**
113770
- \u{1F3AB} Ticket: ${ticketId}
113771
- \u23F1\uFE0F Estimated: ${roundedMinutes} min
113772
- ${complexityScore ? `\u{1F3AF} Complexity: ${complexityScore}/10
113773
- ` : ""}\u{1F4C5} Started: ${sessionStartTime.toLocaleString()}
113170
+ const rows = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(and(...filters)).orderBy(
113171
+ desc(schema_exports.invoiceProducts.usageCount),
113172
+ desc(schema_exports.invoiceProducts.lastUsedAt),
113173
+ asc(schema_exports.invoiceProducts.name)
113174
+ ).limit(Math.min(pageSize, 100));
113175
+ if (rows.length === 0) {
113176
+ return textResponse2(
113177
+ `No products found${status !== "all" ? ` (status: ${status})` : ""}.`
113178
+ );
113179
+ }
113180
+ return textResponse2(
113181
+ `Found ${rows.length} product(s):
113774
113182
 
113775
- \u{1F4DD} Timetrack entry will be created when you complete the session.`
113776
- }
113777
- ]
113778
- };
113183
+ ${rows.map(formatProduct).join("\n")}`
113184
+ );
113779
113185
  }
113780
- async function handleTrackManualFollowUp(input) {
113781
- const ctx = getAuthContext();
113782
- const {
113783
- aiSessionId,
113784
- originalPrompt,
113785
- aiResponse,
113786
- developerFollowUp,
113787
- followUpReason,
113788
- outcome = "success",
113789
- estimatedMinutes,
113790
- workDescription
113791
- } = input;
113186
+ async function handleGetProductById(input) {
113187
+ const { productId } = input;
113188
+ if (!productId) return textResponse2("Error: `productId` is required.");
113792
113189
  const scope = await resolveTeamScope(input.teamId);
113793
113190
  if (!scope.ok) return scope.response;
113794
- const prefix = aiSessionId.replace("ai-sess-", "");
113795
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
113796
- if (!fullSessionId) {
113797
- throw new Error(`Session not found: ${aiSessionId}`);
113798
- }
113799
- const [session] = await db.select({
113800
- id: schema_exports.aiSessions.id,
113801
- status: schema_exports.aiSessions.status,
113802
- createdAt: schema_exports.aiSessions.createdAt,
113803
- aiTimeEstimateMinutes: schema_exports.aiSessions.aiTimeEstimateMinutes,
113804
- teamId: schema_exports.aiSessions.teamId
113805
- }).from(schema_exports.aiSessions).where(eq(schema_exports.aiSessions.id, fullSessionId)).limit(1);
113806
- if (!session) throw new Error(`Session not found: ${aiSessionId}`);
113807
- const followUpTime = /* @__PURE__ */ new Date();
113808
- const oldEstimate = session.aiTimeEstimateMinutes ?? 60;
113809
- const roundedFollowUpMinutes = roundToNearest15Minutes(estimatedMinutes || 0);
113810
- const newEstimate = oldEstimate + roundedFollowUpMinutes;
113811
- await db.update(schema_exports.aiSessions).set({
113812
- status: "in_progress",
113813
- aiTimeEstimateMinutes: newEstimate
113814
- }).where(eq(schema_exports.aiSessions.id, session.id));
113815
- await db.insert(schema_exports.manualFollowUps).values({
113816
- aiSessionId: session.id,
113817
- developerId: ctx.userId,
113818
- teamId: session.teamId,
113819
- originalPrompt,
113820
- aiResponse,
113821
- followUpPrompt: developerFollowUp,
113822
- followUpReason,
113823
- outcome,
113824
- timeSpentMinutes: null,
113825
- resolvedAt: outcome === "success" ? (/* @__PURE__ */ new Date()).toISOString() : null
113826
- });
113827
- await db.insert(schema_exports.aiTimeLogs).values({
113828
- aiSessionId: session.id,
113829
- activityType: "debugging",
113830
- description: `Follow-up: ${followUpReason.replace("_", " ")} - ${outcome}`,
113831
- durationSeconds: 0,
113832
- productivityScore: outcome === "success" ? 9 : outcome === "partial_success" ? 6 : 4,
113833
- startedAt: followUpTime.toISOString()
113834
- });
113835
- const sessionStartTime = new Date(session.createdAt);
113836
- const totalMinutesElapsed = Math.round(
113837
- (followUpTime.getTime() - sessionStartTime.getTime()) / 6e4
113838
- );
113839
- const currentEfficiency = totalMinutesElapsed > 0 ? totalMinutesElapsed / newEstimate : 1;
113840
- await db.update(schema_exports.aiSessions).set({
113841
- efficiencyScore: currentEfficiency.toFixed(2),
113842
- actualTimeMinutes: totalMinutesElapsed
113843
- }).where(eq(schema_exports.aiSessions.id, session.id));
113844
- const existingEntries = await db.select({
113845
- id: schema_exports.timesheetEvents.id,
113846
- trackedDuration: schema_exports.timesheetEvents.trackedDuration,
113847
- title: schema_exports.timesheetEvents.title,
113848
- description: schema_exports.timesheetEvents.description,
113849
- startTime: schema_exports.timesheetEvents.startTime
113850
- }).from(schema_exports.timesheetEvents).where(
113191
+ if (scope.teamIds.length === 0) {
113192
+ return textResponse2("No accessible teams found.");
113193
+ }
113194
+ const [row] = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(
113851
113195
  and(
113852
- eq(schema_exports.timesheetEvents.aiSessionId, session.id),
113853
- eq(schema_exports.timesheetEvents.status, "draft")
113196
+ eq(schema_exports.invoiceProducts.id, productId),
113197
+ inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)
113854
113198
  )
113855
- ).orderBy(desc(schema_exports.timesheetEvents.createdAt));
113856
- let trackerAction = "";
113857
- let trackerDetails = "";
113858
- let existingEntry = existingEntries[0] ?? null;
113859
- if (existingEntries.length > 1) {
113860
- const totalExistingDuration = existingEntries.reduce(
113861
- (sum, entry) => sum + (entry.trackedDuration ?? 0),
113862
- 0
113199
+ ).limit(1);
113200
+ if (!row) {
113201
+ return textResponse2(
113202
+ `Product ${productId} not found or you don't have access to it.`
113863
113203
  );
113864
- const duplicateIds = existingEntries.slice(1).map((e6) => e6.id);
113865
- await db.delete(schema_exports.timesheetEvents).where(inArray(schema_exports.timesheetEvents.id, duplicateIds));
113866
- if (existingEntry && totalExistingDuration > (existingEntry.trackedDuration ?? 0)) {
113867
- await db.update(schema_exports.timesheetEvents).set({ trackedDuration: totalExistingDuration }).where(eq(schema_exports.timesheetEvents.id, existingEntry.id));
113868
- existingEntry = {
113869
- ...existingEntry,
113870
- trackedDuration: totalExistingDuration
113871
- };
113872
- }
113873
- trackerAction = `Consolidated ${existingEntries.length} duplicate entries`;
113874
- }
113875
- if (existingEntry) {
113876
- const newDuration = (existingEntry.trackedDuration ?? 0) + roundedFollowUpMinutes * 60;
113877
- await db.update(schema_exports.timesheetEvents).set({
113878
- trackedDuration: newDuration,
113879
- endTime: followUpTime.toISOString(),
113880
- title: workDescription,
113881
- description: workDescription
113882
- }).where(eq(schema_exports.timesheetEvents.id, existingEntry.id));
113883
- trackerAction = trackerAction || "Updated existing tracker";
113884
- trackerDetails = ` \u2022 Total tracked time: ${Math.round(newDuration / 60)} minutes (+${roundedFollowUpMinutes} min)
113885
- \u2022 Description: ${workDescription}
113886
- `;
113887
- } else {
113888
- const durationSeconds = roundedFollowUpMinutes * 60;
113889
- const startTime = new Date(followUpTime.getTime() - durationSeconds * 1e3);
113890
- await db.insert(schema_exports.timesheetEvents).values({
113891
- teamId: session.teamId,
113892
- userId: ctx.userId,
113893
- aiSessionId: session.id,
113894
- title: workDescription,
113895
- description: workDescription,
113896
- startTime: startTime.toISOString(),
113897
- endTime: followUpTime.toISOString(),
113898
- type: "work",
113899
- status: "draft",
113900
- allDay: false,
113901
- isTracked: true,
113902
- trackedDuration: durationSeconds
113903
- });
113904
- trackerAction = "Created new tracker";
113905
- trackerDetails = ` \u2022 Tracked time: ${roundedFollowUpMinutes} minutes
113906
- \u2022 Description: ${workDescription}
113907
- `;
113908
113204
  }
113909
- return {
113910
- content: [
113911
- {
113912
- type: "text",
113913
- text: `\u{1F504} **Follow-up Tracked & Session Restarted!**
113914
-
113915
- \u{1F194} Session: ${aiSessionId} (back to active)
113916
- \u{1F50D} Reason: ${followUpReason.replace("_", " ")}
113917
- \u2705 Outcome: ${outcome}
113918
-
113919
- \u{1F4CA} **Time Estimate Updated:**
113920
- \u2022 Old estimate: ${oldEstimate} minutes
113921
- \u2022 Follow-up estimate: +${roundedFollowUpMinutes} minutes (rounded to 15min)
113922
- \u2022 New estimate: ${newEstimate} minutes
113923
-
113924
- \u{1F4C8} **Current Progress:**
113925
- \u2022 Total time elapsed: ${totalMinutesElapsed} minutes
113926
- \u2022 Efficiency: ${currentEfficiency < 1 ? "\u{1F680} " : currentEfficiency > 1.5 ? "\u26A0\uFE0F " : "\u23F1\uFE0F "}${(currentEfficiency * 100).toFixed(0)}%
113927
-
113928
- \u23F1\uFE0F **Tracker Entry: ${trackerAction}**
113929
- ` + trackerDetails + `
113930
- \u26A1 **Time tracking resumed** - continue with confidence!`
113931
- }
113932
- ]
113933
- };
113205
+ return textResponse2(formatProduct(row));
113934
113206
  }
113935
- async function handleGetSessionContext(input) {
113936
- const {
113937
- aiSessionId,
113938
- includeTicketData = true,
113939
- includeTodoProgress = true,
113940
- includeFollowUpHistory = false
113941
- } = input;
113942
- const scope = await resolveTeamScope(input.teamId);
113943
- if (!scope.ok) return scope.response;
113944
- const prefix = aiSessionId.replace("ai-sess-", "");
113945
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
113946
- if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
113947
- const [session] = await db.select({
113948
- id: schema_exports.aiSessions.id,
113949
- ticketId: schema_exports.aiSessions.ticketId,
113950
- status: schema_exports.aiSessions.status,
113951
- aiTimeEstimateMinutes: schema_exports.aiSessions.aiTimeEstimateMinutes,
113952
- actualTimeMinutes: schema_exports.aiSessions.actualTimeMinutes,
113953
- complexityScore: schema_exports.aiSessions.complexityScore,
113954
- createdAt: schema_exports.aiSessions.createdAt,
113955
- cursorSessionId: schema_exports.aiSessions.cursorSessionId
113956
- }).from(schema_exports.aiSessions).where(eq(schema_exports.aiSessions.id, fullSessionId)).limit(1);
113957
- if (!session) throw new Error(`Session not found: ${aiSessionId}`);
113958
- const context2 = {
113959
- status: session.status,
113960
- timeEstimate: session.aiTimeEstimateMinutes,
113961
- actualTime: session.actualTimeMinutes,
113962
- complexity: session.complexityScore,
113963
- createdAt: session.createdAt
113964
- };
113965
- if (includeTicketData) {
113966
- const [ticket] = await db.select({
113967
- id: schema_exports.tickets.id,
113968
- ticketNumber: schema_exports.tickets.ticketNumber,
113969
- title: schema_exports.tickets.title,
113970
- description: schema_exports.tickets.description,
113971
- status: schema_exports.tickets.status,
113972
- priority: schema_exports.tickets.priority,
113973
- type: schema_exports.tickets.type
113974
- }).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, session.ticketId)).limit(1);
113975
- context2.ticketData = ticket ?? null;
113976
- }
113977
- if (includeTodoProgress) {
113978
- const todos3 = await db.select({
113979
- id: schema_exports.aiTodos.id,
113980
- content: schema_exports.aiTodos.content,
113981
- status: schema_exports.aiTodos.status,
113982
- estimatedMinutes: schema_exports.aiTodos.estimatedMinutes,
113983
- actualMinutes: schema_exports.aiTodos.actualMinutes
113984
- }).from(schema_exports.aiTodos).where(eq(schema_exports.aiTodos.aiSessionId, session.id)).orderBy(asc(schema_exports.aiTodos.sequenceOrder));
113985
- context2.todos = todos3;
113986
- context2.todoProgress = {
113987
- total: todos3.length,
113988
- completed: todos3.filter((t8) => t8.status === "completed").length,
113989
- inProgress: todos3.filter((t8) => t8.status === "in_progress").length
113990
- };
113991
- }
113992
- if (includeFollowUpHistory) {
113993
- const followUps = await db.select({
113994
- followUpReason: schema_exports.manualFollowUps.followUpReason,
113995
- outcome: schema_exports.manualFollowUps.outcome,
113996
- timeSpentMinutes: schema_exports.manualFollowUps.timeSpentMinutes,
113997
- createdAt: schema_exports.manualFollowUps.createdAt
113998
- }).from(schema_exports.manualFollowUps).where(eq(schema_exports.manualFollowUps.aiSessionId, session.id)).orderBy(asc(schema_exports.manualFollowUps.createdAt));
113999
- context2.followUpHistory = followUps;
114000
- }
114001
- const ticketData = context2.ticketData;
114002
- const todoProgress = context2.todoProgress;
114003
- const followUpHistory = context2.followUpHistory;
114004
- return {
114005
- content: [
114006
- {
114007
- type: "text",
114008
- text: `\u{1F3AF} **Session Context Retrieved**
113207
+ async function loadProductInTeam(productId, teamId) {
113208
+ const accessibleTeamIds = await getAccessibleTeamIds(teamId);
113209
+ const [row] = await db.select(PRODUCT_COLUMNS).from(schema_exports.invoiceProducts).where(
113210
+ and(
113211
+ eq(schema_exports.invoiceProducts.id, productId),
113212
+ inArray(schema_exports.invoiceProducts.teamId, accessibleTeamIds)
113213
+ )
113214
+ ).limit(1);
113215
+ return row ?? null;
113216
+ }
113217
+ async function handleCreateProduct(input) {
113218
+ const { name: name21, description, price, currency, unit } = input;
113219
+ if (!name21 || name21.trim().length === 0) {
113220
+ return textResponse2("Error: `name` is required.");
113221
+ }
113222
+ const resolved = await resolveTeamId(input.teamId);
113223
+ if (!resolved.ok) return resolved.response;
113224
+ const [created] = await db.insert(schema_exports.invoiceProducts).values({
113225
+ teamId: resolved.teamId,
113226
+ name: name21.trim(),
113227
+ description: description ?? null,
113228
+ price: price ?? null,
113229
+ currency: currency ?? null,
113230
+ unit: unit ?? null,
113231
+ isActive: true,
113232
+ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
113233
+ }).returning(PRODUCT_COLUMNS);
113234
+ if (!created) return textResponse2("Failed to create product.");
113235
+ return textResponse2(
113236
+ `\u2705 **Product created**
114009
113237
 
114010
- Session: ${aiSessionId}
114011
- Status: ${session.status}
114012
- ${ticketData ? `Ticket: ${ticketData.ticketNumber} - ${ticketData.title}
114013
- ` : ""}${todoProgress ? `Todo Progress: ${todoProgress.completed}/${todoProgress.total} completed
114014
- ` : ""}${followUpHistory ? `Follow-ups: ${followUpHistory.length}
114015
- ` : ""}
114016
- \u{1F4CB} Full context preserved for seamless continuation!`
114017
- }
114018
- ]
114019
- };
113238
+ ${formatProduct(created)}`
113239
+ );
114020
113240
  }
114021
- async function handleSyncSessionTodos(input) {
114022
- const { aiSessionId, todos: todos3, replaceAll = true } = input;
114023
- const scope = await resolveTeamScope(input.teamId);
114024
- if (!scope.ok) return scope.response;
114025
- const prefix = aiSessionId.replace("ai-sess-", "");
114026
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
114027
- if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
114028
- if (replaceAll) {
114029
- await db.delete(schema_exports.aiTodos).where(eq(schema_exports.aiTodos.aiSessionId, fullSessionId));
114030
- }
114031
- if (todos3 && todos3.length > 0) {
114032
- let startSequence = 0;
114033
- if (!replaceAll) {
114034
- const [maxTodo] = await db.select({ sequenceOrder: schema_exports.aiTodos.sequenceOrder }).from(schema_exports.aiTodos).where(eq(schema_exports.aiTodos.aiSessionId, fullSessionId)).orderBy(desc(schema_exports.aiTodos.sequenceOrder)).limit(1);
114035
- startSequence = (maxTodo?.sequenceOrder ?? 0) + 1;
114036
- }
114037
- await db.insert(schema_exports.aiTodos).values(
114038
- todos3.map((todo, index2) => ({
114039
- aiSessionId: fullSessionId,
114040
- content: todo.content,
114041
- status: todo.status,
114042
- cursorTodoId: todo.todoId ?? null,
114043
- estimatedMinutes: todo.estimatedMinutes ?? null,
114044
- sequenceOrder: startSequence + index2
114045
- }))
113241
+ async function handleUpdateProduct(input) {
113242
+ const { productId } = input;
113243
+ if (!productId) return textResponse2("Error: `productId` is required.");
113244
+ const resolved = await resolveTeamId(input.teamId);
113245
+ if (!resolved.ok) return resolved.response;
113246
+ const existing = await loadProductInTeam(productId, resolved.teamId);
113247
+ if (!existing) {
113248
+ return textResponse2(
113249
+ `Product ${productId} not found, or it is not owned by this team.`
114046
113250
  );
114047
113251
  }
114048
- let phaseTransition = null;
114049
- const currentTodos = await db.select({ status: schema_exports.aiTodos.status }).from(schema_exports.aiTodos).where(eq(schema_exports.aiTodos.aiSessionId, fullSessionId));
114050
- if (currentTodos.length > 0) {
114051
- const hasInProgress = currentTodos.some((t8) => t8.status === "in_progress");
114052
- const allCompleted = currentTodos.every((t8) => t8.status === "completed");
114053
- const [currentPhase] = await db.select({
114054
- activityType: schema_exports.aiTimeLogs.activityType,
114055
- status: schema_exports.aiTimeLogs.status
114056
- }).from(schema_exports.aiTimeLogs).where(
114057
- and(
114058
- eq(schema_exports.aiTimeLogs.aiSessionId, fullSessionId),
114059
- eq(schema_exports.aiTimeLogs.status, "in_progress")
114060
- )
114061
- ).limit(1);
114062
- if (hasInProgress && currentPhase?.activityType === "analysis") {
114063
- await transitionToNextPhase(fullSessionId, "analysis");
114064
- phaseTransition = "Analysis completed \u2192 Next phase started (Investigation/Development)";
114065
- }
114066
- if (hasInProgress && currentPhase?.activityType === "bug_investigation") {
114067
- const completedCount = currentTodos.filter(
114068
- (t8) => t8.status === "completed"
114069
- ).length;
114070
- if (completedCount > 0) {
114071
- await transitionToNextPhase(fullSessionId, "bug_investigation");
114072
- phaseTransition = "Investigation completed \u2192 Development phase started";
114073
- }
114074
- }
114075
- if (allCompleted && currentPhase?.activityType === "development") {
114076
- await transitionToNextPhase(fullSessionId, "development");
114077
- phaseTransition = "Development completed \u2192 Communication phase started";
113252
+ const updates = {};
113253
+ if (input.name !== void 0) {
113254
+ if (!input.name || input.name.trim().length === 0) {
113255
+ return textResponse2("Error: `name` cannot be empty.");
114078
113256
  }
113257
+ updates.name = input.name.trim();
114079
113258
  }
114080
- return {
114081
- content: [
114082
- {
114083
- type: "text",
114084
- text: `\u2705 **Todos ${replaceAll ? "Synced" : "Added"} Successfully!**
114085
-
114086
- Session: ${aiSessionId}
114087
- ${replaceAll ? "Synced" : "Added"} ${todos3?.length || 0} todos
114088
- ${replaceAll ? "" : "\u2795 Added to existing todo list\n"}${phaseTransition ? `\u{1F504} Phase Transition: ${phaseTransition}
114089
- ` : ""}
114090
- \u{1F4DD} Todo list updated and tracked for progress monitoring!`
114091
- }
114092
- ]
114093
- };
114094
- }
114095
- async function handleAddFollowUpTodos(input) {
114096
- const { aiSessionId, newTodos, followUpReason } = input;
114097
- const scope = await resolveTeamScope(input.teamId);
114098
- if (!scope.ok) return scope.response;
114099
- const prefix = aiSessionId.replace("ai-sess-", "");
114100
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
114101
- if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
114102
- if (newTodos && newTodos.length > 0) {
114103
- const [maxTodo] = await db.select({ sequenceOrder: schema_exports.aiTodos.sequenceOrder }).from(schema_exports.aiTodos).where(eq(schema_exports.aiTodos.aiSessionId, fullSessionId)).orderBy(desc(schema_exports.aiTodos.sequenceOrder)).limit(1);
114104
- const startSequence = (maxTodo?.sequenceOrder ?? 0) + 1;
114105
- await db.insert(schema_exports.aiTodos).values(
114106
- newTodos.map((todo, index2) => ({
114107
- aiSessionId: fullSessionId,
114108
- content: `[Follow-up] ${todo.content}`,
114109
- status: todo.status ?? "pending",
114110
- estimatedMinutes: todo.estimatedMinutes ?? null,
114111
- sequenceOrder: startSequence + index2
114112
- }))
113259
+ if (input.description !== void 0) updates.description = input.description;
113260
+ if (input.price !== void 0) updates.price = input.price;
113261
+ if (input.currency !== void 0) updates.currency = input.currency;
113262
+ if (input.unit !== void 0) updates.unit = input.unit;
113263
+ if (input.isActive !== void 0) updates.isActive = input.isActive;
113264
+ if (Object.keys(updates).length === 0) {
113265
+ return textResponse2(
113266
+ "No fields to update. Provide at least one of: name, description, price, currency, unit, isActive."
114113
113267
  );
114114
113268
  }
114115
- return {
114116
- content: [
114117
- {
114118
- type: "text",
114119
- text: `\u2705 **Follow-up Todos Added Successfully!**
113269
+ updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
113270
+ const [updated] = await db.update(schema_exports.invoiceProducts).set(updates).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS);
113271
+ if (!updated) return textResponse2(`Failed to update product ${productId}.`);
113272
+ return textResponse2(
113273
+ `\u2705 **Product updated**
114120
113274
 
114121
- Session: ${aiSessionId}
114122
- Added ${newTodos?.length || 0} new todos from follow-up
114123
- ${followUpReason ? `Reason: ${followUpReason}
114124
- ` : ""}
114125
- \u{1F4DD} New tasks identified and added to existing workflow!`
114126
- }
114127
- ]
114128
- };
113275
+ ${formatProduct(updated)}
113276
+ Note: this only affects future invoices/quotes. Existing documents keep their line-item snapshots.`
113277
+ );
114129
113278
  }
114130
- async function handleUpdateSessionStatus(input) {
114131
- const { aiSessionId, status, actualTimeMinutes, completionNotes } = input;
114132
- const scope = await resolveTeamScope(input.teamId);
114133
- if (!scope.ok) return scope.response;
114134
- const prefix = aiSessionId.replace("ai-sess-", "");
114135
- const fullSessionId = await resolveAiSessionId(prefix, scope.teamIds);
114136
- if (!fullSessionId) throw new Error(`Session not found: ${aiSessionId}`);
114137
- await db.update(schema_exports.aiSessions).set({
114138
- status,
114139
- actualTimeMinutes: actualTimeMinutes ?? null,
114140
- completedAt: status === "completed" ? (/* @__PURE__ */ new Date()).toISOString() : null
114141
- }).where(eq(schema_exports.aiSessions.id, fullSessionId));
114142
- return {
114143
- content: [
114144
- {
114145
- type: "text",
114146
- text: `\u{1F3AF} **Session Status Updated!**
113279
+ async function handleArchiveProduct(input) {
113280
+ const { productId, reason } = input;
113281
+ if (!productId) return textResponse2("Error: `productId` is required.");
113282
+ const resolved = await resolveTeamId(input.teamId);
113283
+ if (!resolved.ok) return resolved.response;
113284
+ const existing = await loadProductInTeam(productId, resolved.teamId);
113285
+ if (!existing) {
113286
+ return textResponse2(
113287
+ `Product ${productId} not found, or it is not owned by this team.`
113288
+ );
113289
+ }
113290
+ if (!existing.isActive) {
113291
+ return textResponse2(
113292
+ `Product "${existing.name}" (${existing.id}) is already archived.`
113293
+ );
113294
+ }
113295
+ const [archived] = await db.update(schema_exports.invoiceProducts).set({ isActive: false, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS);
113296
+ if (!archived) return textResponse2(`Failed to archive product ${productId}.`);
113297
+ return textResponse2(
113298
+ `\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
114147
113299
 
114148
- Session: ${aiSessionId}
114149
- Status: ${status}
114150
- ${actualTimeMinutes ? `Actual Time: ${actualTimeMinutes} minutes
114151
- ` : ""}${status === "completed" ? `\u2705 Session completed successfully!
114152
- ` : ""}${completionNotes ? `Notes: ${completionNotes}
114153
- ` : ""}`
114154
- }
114155
- ]
114156
- };
113300
+ ${formatProduct(archived)}${reason ? `Reason: ${reason}
113301
+ ` : ""}Reactivate it with update-product (isActive: true).`
113302
+ );
114157
113303
  }
114158
113304
 
114159
113305
  // src/tools/teams.ts
@@ -119647,7 +118793,7 @@ var EXT_MIME = {
119647
118793
  ppt: "application/vnd.ms-powerpoint",
119648
118794
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
119649
118795
  };
119650
- function textResponse2(text3) {
118796
+ function textResponse3(text3) {
119651
118797
  return { content: [{ type: "text", text: text3 }] };
119652
118798
  }
119653
118799
  function mimeFromName(name21) {
@@ -119728,12 +118874,12 @@ async function handleUploadTicketAttachment(input) {
119728
118874
  (v2) => typeof v2 === "string" && v2.trim().length > 0
119729
118875
  );
119730
118876
  if (sources.length === 0) {
119731
- return textResponse2(
118877
+ return textResponse3(
119732
118878
  "Provide exactly one source: filePath (absolute local path), imageUrl, or base64Data."
119733
118879
  );
119734
118880
  }
119735
118881
  if (sources.length > 1) {
119736
- return textResponse2(
118882
+ return textResponse3(
119737
118883
  "Provide only one source (filePath, imageUrl, or base64Data), not several."
119738
118884
  );
119739
118885
  }
@@ -119753,7 +118899,7 @@ async function handleUploadTicketAttachment(input) {
119753
118899
  } else if (input.imageUrl) {
119754
118900
  const res = await fetch(input.imageUrl);
119755
118901
  if (!res.ok) {
119756
- return textResponse2(
118902
+ return textResponse3(
119757
118903
  `Could not download from URL: HTTP ${res.status}.`
119758
118904
  );
119759
118905
  }
@@ -119781,22 +118927,22 @@ async function handleUploadTicketAttachment(input) {
119781
118927
  }
119782
118928
  }
119783
118929
  } catch (error49) {
119784
- return textResponse2(
118930
+ return textResponse3(
119785
118931
  `Failed to read the file: ${error49 instanceof Error ? error49.message : String(error49)}`
119786
118932
  );
119787
118933
  }
119788
118934
  if (buffer2.byteLength === 0) {
119789
- return textResponse2("The file is empty (0 bytes); nothing to upload.");
118935
+ return textResponse3("The file is empty (0 bytes); nothing to upload.");
119790
118936
  }
119791
118937
  if (buffer2.byteLength > MAX_FILE_SIZE) {
119792
- return textResponse2(
118938
+ return textResponse3(
119793
118939
  `File too large (${(buffer2.byteLength / 1024 / 1024).toFixed(
119794
118940
  1
119795
118941
  )} MB). Max: 25 MB.`
119796
118942
  );
119797
118943
  }
119798
118944
  if (!ALLOWED_MIME_TYPES.has(mimeType)) {
119799
- return textResponse2(
118945
+ return textResponse3(
119800
118946
  `Unsupported file type: ${mimeType}. Allowed: JPEG, PNG, GIF, WebP, PDF, DOC(X), XLS(X), PPT(X), TXT, CSV.`
119801
118947
  );
119802
118948
  }
@@ -119809,7 +118955,7 @@ async function handleUploadTicketAttachment(input) {
119809
118955
  options: { contentType: mimeType, upsert: true }
119810
118956
  });
119811
118957
  } catch (error49) {
119812
- return textResponse2(
118958
+ return textResponse3(
119813
118959
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
119814
118960
  );
119815
118961
  }
@@ -119832,7 +118978,7 @@ async function handleUploadTicketAttachment(input) {
119832
118978
  url3 = signed.url;
119833
118979
  } catch {
119834
118980
  }
119835
- return textResponse2(
118981
+ return textResponse3(
119836
118982
  `\u{1F4CE} **Attached to ${ticket.ticketNumber}**
119837
118983
  File: ${fileName}
119838
118984
  Type: ${mimeType}
@@ -120976,7 +120122,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
120976
120122
  }
120977
120123
 
120978
120124
  // src/server.ts
120979
- var SERVER_VERSION = "3.3.0";
120125
+ var SERVER_VERSION = "3.5.0";
120980
120126
  function createMcpServer() {
120981
120127
  const server = new Server(
120982
120128
  {
@@ -121100,40 +120246,22 @@ function createMcpServer() {
121100
120246
  return await handleLinkDocumentToInvoice(
121101
120247
  asToolArgs(toolArgs)
121102
120248
  );
121103
- case "start-ai-session-smart":
121104
- return await handleStartAiSession(
121105
- asToolArgs(toolArgs)
121106
- );
121107
- case "track-manual-follow-up":
121108
- return await handleTrackManualFollowUp(
121109
- asToolArgs(toolArgs)
121110
- );
121111
- case "get-session-context":
121112
- return await handleGetSessionContext(
121113
- asToolArgs(toolArgs)
121114
- );
121115
- case "sync-session-todos":
121116
- return await handleSyncSessionTodos(
121117
- asToolArgs(toolArgs)
121118
- );
121119
- case "add-follow-up-todos":
121120
- return await handleAddFollowUpTodos(
121121
- asToolArgs(toolArgs)
121122
- );
121123
- case "update-session-status":
121124
- return await handleUpdateSessionStatus(
120249
+ case "get-products":
120250
+ return await handleGetProducts(asToolArgs(toolArgs));
120251
+ case "get-product-by-id":
120252
+ return await handleGetProductById(
121125
120253
  asToolArgs(toolArgs)
121126
120254
  );
121127
- case "get-completion-context":
121128
- return await handleGetCompletionContext(
120255
+ case "create-product":
120256
+ return await handleCreateProduct(
121129
120257
  asToolArgs(toolArgs)
121130
120258
  );
121131
- case "save-customer-response":
121132
- return await handleSaveCustomerResponse(
120259
+ case "update-product":
120260
+ return await handleUpdateProduct(
121133
120261
  asToolArgs(toolArgs)
121134
120262
  );
121135
- case "complete-ai-session":
121136
- return await handleCompleteAiSession(
120263
+ case "archive-product":
120264
+ return await handleArchiveProduct(
121137
120265
  asToolArgs(toolArgs)
121138
120266
  );
121139
120267
  case "log-hours":