@rubytech/create-maxy 1.0.507 → 1.0.509

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.
Files changed (103) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -1
  3. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +18 -0
  4. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -0
  5. package/payload/platform/lib/mcp-stderr-tee/dist/index.js +45 -0
  6. package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -0
  7. package/payload/platform/lib/mcp-stderr-tee/src/index.ts +51 -0
  8. package/payload/platform/lib/mcp-stderr-tee/tsconfig.json +8 -0
  9. package/payload/platform/package.json +2 -2
  10. package/payload/platform/plugins/admin/mcp/dist/index.js +2 -0
  11. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  12. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +2 -0
  13. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  14. package/payload/platform/plugins/contacts/mcp/dist/index.js +2 -0
  15. package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/email/mcp/dist/index.js +2 -0
  17. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/index.js +2 -0
  19. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  20. package/payload/platform/plugins/replicate/mcp/dist/index.js +2 -0
  21. package/payload/platform/plugins/replicate/mcp/dist/index.js.map +1 -1
  22. package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  24. package/payload/platform/plugins/tasks/mcp/dist/index.js +2 -0
  25. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  26. package/payload/platform/plugins/telegram/mcp/dist/index.js +2 -0
  27. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  28. package/payload/platform/plugins/waitlist/mcp/dist/index.js +2 -0
  29. package/payload/platform/plugins/waitlist/mcp/dist/index.js.map +1 -1
  30. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +5 -0
  31. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  32. package/payload/platform/plugins/workflows/mcp/dist/index.js +2 -0
  33. package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
  34. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js +2 -33
  35. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js.map +1 -1
  36. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts.map +1 -1
  37. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js.map +1 -1
  38. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts.map +1 -1
  39. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js.map +1 -1
  40. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
  41. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
  42. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts.map +1 -1
  43. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js.map +1 -1
  44. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts.map +1 -1
  45. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js.map +1 -1
  46. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts.map +1 -1
  47. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js.map +1 -1
  48. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts.map +1 -1
  49. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js +110 -18
  50. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js.map +1 -1
  51. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts.map +1 -1
  52. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js +34 -6
  53. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js.map +1 -1
  54. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts.map +1 -1
  55. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js +59 -16
  56. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js.map +1 -1
  57. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
  58. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js +5 -1
  59. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js.map +1 -1
  60. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts.map +1 -1
  61. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js.map +1 -1
  62. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts.map +1 -1
  63. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js +4 -2
  64. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js.map +1 -1
  65. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts.map +1 -1
  66. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js.map +1 -1
  67. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts.map +1 -1
  68. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js +6 -2
  69. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js.map +1 -1
  70. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-info.d.ts.map +1 -1
  71. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-info.js +16 -4
  72. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-info.js.map +1 -1
  73. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts.map +1 -1
  74. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js.map +1 -1
  75. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts.map +1 -1
  76. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js +63 -18
  77. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js.map +1 -1
  78. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts.map +1 -1
  79. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js +13 -3
  80. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js.map +1 -1
  81. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts.map +1 -1
  82. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js.map +1 -1
  83. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/index.ts +3 -35
  84. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/customer-preferences.ts +6 -0
  85. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback.ts +6 -0
  86. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-enquiry.ts +8 -0
  87. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-batch.ts +5 -0
  88. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-request.ts +5 -0
  89. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match.ts +6 -0
  90. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-detail.ts +203 -21
  91. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-search.ts +99 -12
  92. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-detail.ts +95 -20
  93. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-listed.ts +27 -6
  94. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-request.ts +5 -0
  95. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-search.ts +20 -8
  96. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/supplier.ts +9 -0
  97. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/team-availability.ts +12 -2
  98. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/team-info.ts +50 -9
  99. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-create.ts +5 -0
  100. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-detail.ts +124 -23
  101. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-search.ts +25 -7
  102. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-update.ts +5 -0
  103. package/payload/server/server.js +122 -14
@@ -4,15 +4,25 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /property/residential/{dept} ─────────────────
8
+ // Fields: id, refId, propertyAddress, status, propertyType,
9
+ // dateCreated, price, bedrooms, offerCount, viewingCount,
10
+ // publishedToPortals, publishedToWebsite, publishedToMatching
11
+ // ────────────────────────────────────────────────────────────────
7
12
  interface LoopPropertySummary {
8
- id?: number;
9
- address?: string;
10
- price?: number;
13
+ id: number;
14
+ refId?: string;
15
+ propertyAddress?: string;
11
16
  status?: string;
12
- type?: string;
17
+ propertyType?: string;
18
+ dateCreated?: string;
19
+ price?: number;
13
20
  bedrooms?: number;
14
- bathrooms?: number;
15
- description?: string;
21
+ offerCount?: number;
22
+ viewingCount?: number;
23
+ publishedToPortals?: boolean;
24
+ publishedToWebsite?: boolean;
25
+ publishedToMatching?: boolean;
16
26
  [key: string]: unknown;
17
27
  }
18
28
 
@@ -69,11 +79,13 @@ export async function propertySearch(params: {
69
79
  return formatAggregationResult(
70
80
  result,
71
81
  (p) => {
82
+ const id = p.id != null ? ` [ID: ${p.id}]` : "";
72
83
  const price = p.price ? ` — £${p.price.toLocaleString("en-GB")}` : "";
73
84
  const beds = p.bedrooms ? ` ${p.bedrooms}bed` : "";
74
85
  const status = p.status ? ` [${p.status}]` : "";
75
- const propType = p.type ? ` (${p.type})` : "";
76
- return `- ${p.address ?? "Unknown address"}${price}${beds}${propType}${status}`;
86
+ const propType = p.propertyType ? ` (${p.propertyType})` : "";
87
+ const offers = p.offerCount ? ` ${p.offerCount} offer${p.offerCount !== 1 ? "s" : ""}` : "";
88
+ return `- ${p.propertyAddress ?? "Unknown address"}${id}${price}${beds}${propType}${status}${offers}`;
77
89
  },
78
90
  "properties"
79
91
  );
@@ -8,6 +8,15 @@ type SupplierAction =
8
8
  | "board-jobs"
9
9
  | "board-complete";
10
10
 
11
+ // ─── Loop API V2: Supplier Endpoints ───────────────────────────
12
+ // /supplier/maintenance/{code}/{jobId}/job-list — GET job list
13
+ // /supplier/maintenance/{code}/{jobId}/complete — POST complete
14
+ // /supplier/maintenance/{code}/{jobId}/quote-list — GET quotes
15
+ // /supplier/maintenance/{code}/{quoteId}/quote-for-job — PUT quote
16
+ // /supplier/board-contractor/{code}/{jobId}/job-list — GET jobs
17
+ // /supplier/board-contractor/{code}/{jobId}/complete — POST complete
18
+ // Uses quoteCode/code (string) and jobId (int64)
19
+ // ────────────────────────────────────────────────────────────────
11
20
  interface LoopSupplierResult {
12
21
  [key: string]: unknown;
13
22
  }
@@ -4,7 +4,12 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /team/{agentId}/availability/{searchDate} ────
8
+ // Returns array of: { agentId, start, end }
9
+ // Times are ISO datetime strings (e.g. "2026-04-10T06:00:00")
10
+ // ────────────────────────────────────────────────────────────────
7
11
  interface LoopTimeRange {
12
+ agentId?: string;
8
13
  start?: string;
9
14
  end?: string;
10
15
  [key: string]: unknown;
@@ -33,10 +38,15 @@ export async function teamAvailability(params: {
33
38
  return formatAggregationResult(
34
39
  result,
35
40
  (slot) => {
36
- const start = slot.start ?? "?";
37
- const end = slot.end ?? "?";
41
+ const start = slot.start ? formatTime(slot.start) : "?";
42
+ const end = slot.end ? formatTime(slot.end) : "?";
38
43
  return `- ${start} — ${end}`;
39
44
  },
40
45
  `availability slots for ${searchDate}`
41
46
  );
42
47
  }
48
+
49
+ function formatTime(iso: string): string {
50
+ const timePart = iso.split("T")[1];
51
+ return timePart ? timePart.slice(0, 5) : iso;
52
+ }
@@ -4,13 +4,39 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /team ────────────────────────────────────────
8
+ // Fields: apiKey (redacted), clientName, teamName,
9
+ // address1, address2, address3, email, phoneNumber, websiteUrl,
10
+ // primaryDarkColour, primaryLightColour, secondaryDarkColour,
11
+ // secondaryLightColour, legalName, websiteContactUrl,
12
+ // websitePropertiesUrl, dateCreated,
13
+ // members[] (each: id, firstName, lastName, email, mobilePhone,
14
+ // jobTitle, signatureImageUrl, avatarImage, avatarMedium,
15
+ // avatarThumbnail, biography, customUrl, twitterUrl)
16
+ // ────────────────────────────────────────────────────────────────
7
17
  interface LoopTeam {
8
- id?: string;
9
- name?: string;
10
- address?: string;
11
- phone?: string;
18
+ clientName?: string;
19
+ teamName?: string;
20
+ address1?: string;
21
+ address2?: string;
22
+ address3?: string;
23
+ email?: string;
24
+ phoneNumber?: string;
25
+ websiteUrl?: string;
26
+ legalName?: string;
27
+ dateCreated?: string;
28
+ members?: LoopTeamMember[];
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ interface LoopTeamMember {
33
+ id: string;
34
+ firstName?: string;
35
+ lastName?: string;
12
36
  email?: string;
13
- agentId?: string;
37
+ mobilePhone?: string;
38
+ jobTitle?: string;
39
+ biography?: string;
14
40
  [key: string]: unknown;
15
41
  }
16
42
 
@@ -43,11 +69,26 @@ export async function teamInfo(params: {
43
69
  return formatAggregationResult(
44
70
  result,
45
71
  (t) => {
46
- const name = t.name ?? "Unknown";
47
- const addr = t.address ? ` ${t.address}` : "";
48
- const phone = t.phone ? ` | ${t.phone}` : "";
72
+ const name = t.teamName ?? t.clientName ?? "Unknown";
73
+ const client = t.clientName && t.clientName !== t.teamName ? ` (${t.clientName})` : "";
74
+ const addr = t.address1 ? ` ${t.address1}` : "";
75
+ const phone = t.phoneNumber ? ` | ${t.phoneNumber}` : "";
49
76
  const email = t.email ? ` | ${t.email}` : "";
50
- return `- **${name}**${addr}${phone}${email}`;
77
+ const web = t.websiteUrl ? ` | ${t.websiteUrl}` : "";
78
+
79
+ const lines = [`- **${name}**${client}${addr}${phone}${email}${web}`];
80
+
81
+ if (t.members?.length) {
82
+ lines.push(` Team members (${t.members.length}):`);
83
+ for (const m of t.members) {
84
+ const mName = [m.firstName, m.lastName].filter(Boolean).join(" ");
85
+ const title = m.jobTitle ? ` (${m.jobTitle})` : "";
86
+ const mEmail = m.email ? ` — ${m.email}` : "";
87
+ lines.push(` - ${mName} [ID: ${m.id}]${title}${mEmail}`);
88
+ }
89
+ }
90
+
91
+ return lines.join("\n");
51
92
  },
52
93
  "teams"
53
94
  );
@@ -2,6 +2,11 @@ import { loopPost, withTeamKey } from "../lib/loop-api.js";
2
2
 
3
3
  type Department = "sales" | "lettings";
4
4
 
5
+ // ─── Loop API V2: POST /residential/{dept}/viewings ────────────
6
+ // Request body: { propertyId, date, time, attendeeName,
7
+ // attendeeEmail?, attendeePhone? }
8
+ // Response: number (the created viewing ID) in data field
9
+ // ────────────────────────────────────────────────────────────────
5
10
  interface LoopNumberResponse {
6
11
  result?: number;
7
12
  [key: string]: unknown;
@@ -4,21 +4,73 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /residential/{dept}/viewings/{id} ────────────
8
+ // Fields: id, dateCreated, dateOfAppointment, dateOfAppointmentEnd,
9
+ // dateBuyerFeedbackGiven, dateSellerFeedbackGiven, dateDone,
10
+ // status, type, comment, buyerFeedback, sellerFeedback,
11
+ // buyerConfirmed, sellerConfirmed, agentConfirmed,
12
+ // property (nested PropertySummary), buyer (nested BuyerSummary),
13
+ // seller (nested SellerSummary),
14
+ // attendingAgent (nested TeamMember), creatingAgent (nested TeamMember),
15
+ // teamId
16
+ // ────────────────────────────────────────────────────────────────
7
17
  interface LoopViewingDetail {
8
- id?: number;
9
- propertyAddress?: string;
10
- date?: string;
11
- time?: string;
18
+ id: number;
19
+ dateCreated?: string;
20
+ dateOfAppointment?: string;
21
+ dateOfAppointmentEnd?: string;
22
+ dateBuyerFeedbackGiven?: string | null;
23
+ dateSellerFeedbackGiven?: string | null;
24
+ dateDone?: string | null;
12
25
  status?: string;
13
26
  type?: string;
14
- attendeeName?: string;
15
- attendeeEmail?: string;
16
- attendeePhone?: string;
17
- notes?: string;
18
- buyerFeedback?: string;
19
- sellerFeedback?: string;
20
- renterFeedback?: string;
21
- landlordFeedback?: string;
27
+ comment?: string | null;
28
+ buyerFeedback?: string | null;
29
+ sellerFeedback?: string | null;
30
+ buyerConfirmed?: boolean;
31
+ sellerConfirmed?: boolean;
32
+ agentConfirmed?: boolean;
33
+ property?: {
34
+ id: number;
35
+ propertyAddress?: string;
36
+ status?: string;
37
+ propertyType?: string;
38
+ price?: number;
39
+ bedrooms?: number;
40
+ [key: string]: unknown;
41
+ };
42
+ buyer?: {
43
+ id: number;
44
+ buyerGroupName?: string;
45
+ maxPrice?: number | null;
46
+ position?: string;
47
+ viewingCount?: number;
48
+ offerCount?: number;
49
+ [key: string]: unknown;
50
+ };
51
+ seller?: {
52
+ id: number;
53
+ sellerGroupName?: string;
54
+ propertyAddress?: string;
55
+ price?: number;
56
+ [key: string]: unknown;
57
+ };
58
+ attendingAgent?: {
59
+ id: string;
60
+ firstName?: string;
61
+ lastName?: string;
62
+ email?: string;
63
+ mobilePhone?: string;
64
+ jobTitle?: string;
65
+ [key: string]: unknown;
66
+ };
67
+ creatingAgent?: {
68
+ id: string;
69
+ firstName?: string;
70
+ lastName?: string;
71
+ [key: string]: unknown;
72
+ };
73
+ teamId?: string;
22
74
  [key: string]: unknown;
23
75
  }
24
76
 
@@ -50,21 +102,70 @@ export async function viewingDetail(params: {
50
102
  return formatAggregationResult(
51
103
  result,
52
104
  (v) => {
53
- const lines = [`**${v.propertyAddress ?? "Unknown property"}**`];
54
- const when = [v.date, v.time].filter(Boolean).join(" ");
55
- if (when) lines.push(`Date/Time: ${when}`);
105
+ const propAddr = v.property?.propertyAddress ?? "Unknown property";
106
+ const lines = [`**${propAddr}** [Viewing ID: ${v.id}]`];
107
+
108
+ // Appointment time
109
+ if (v.dateOfAppointment) {
110
+ const start = formatDateTime(v.dateOfAppointment);
111
+ const end = v.dateOfAppointmentEnd ? formatDateTime(v.dateOfAppointmentEnd) : "";
112
+ lines.push(`Date/Time: ${start}${end ? ` — ${end}` : ""}`);
113
+ }
56
114
  if (v.status) lines.push(`Status: ${v.status}`);
57
115
  if (v.type) lines.push(`Type: ${v.type}`);
58
- if (v.attendeeName) lines.push(`Attendee: ${v.attendeeName}`);
59
- if (v.attendeeEmail) lines.push(`Email: ${v.attendeeEmail}`);
60
- if (v.attendeePhone) lines.push(`Phone: ${v.attendeePhone}`);
61
- if (v.notes) lines.push(`Notes: ${v.notes}`);
62
- if (v.buyerFeedback) lines.push(`Buyer Feedback: ${v.buyerFeedback}`);
63
- if (v.sellerFeedback) lines.push(`Seller Feedback: ${v.sellerFeedback}`);
64
- if (v.renterFeedback) lines.push(`Renter Feedback: ${v.renterFeedback}`);
65
- if (v.landlordFeedback) lines.push(`Landlord Feedback: ${v.landlordFeedback}`);
116
+
117
+ // Confirmation status
118
+ const confirmParts: string[] = [];
119
+ if (v.buyerConfirmed != null) confirmParts.push(`Buyer: ${v.buyerConfirmed ? "yes" : "no"}`);
120
+ if (v.sellerConfirmed != null) confirmParts.push(`Seller: ${v.sellerConfirmed ? "yes" : "no"}`);
121
+ if (v.agentConfirmed != null) confirmParts.push(`Agent: ${v.agentConfirmed ? "yes" : "no"}`);
122
+ if (confirmParts.length) lines.push(`Confirmed ${confirmParts.join(", ")}`);
123
+
124
+ // Property details
125
+ if (v.property) {
126
+ const prop = v.property;
127
+ if (prop.price != null) lines.push(`Property price: £${prop.price.toLocaleString("en-GB")}`);
128
+ if (prop.propertyType) lines.push(`Property type: ${prop.propertyType}`);
129
+ if (prop.bedrooms != null) lines.push(`Bedrooms: ${prop.bedrooms}`);
130
+ if (prop.id) lines.push(`Property ID: ${prop.id}`);
131
+ }
132
+
133
+ // Buyer
134
+ if (v.buyer) {
135
+ const b = v.buyer;
136
+ lines.push(`\n**Buyer:** ${b.buyerGroupName ?? "Unknown"} [ID: ${b.id}]`);
137
+ if (b.maxPrice != null) lines.push(` Max price: £${b.maxPrice.toLocaleString("en-GB")}`);
138
+ if (b.position && b.position !== "none") lines.push(` Position: ${b.position}`);
139
+ }
140
+
141
+ // Seller
142
+ if (v.seller) {
143
+ const s = v.seller;
144
+ lines.push(`\n**Seller:** ${s.sellerGroupName ?? "Unknown"} [ID: ${s.id}]`);
145
+ }
146
+
147
+ // Attending agent
148
+ if (v.attendingAgent) {
149
+ const a = v.attendingAgent;
150
+ const agentName = [a.firstName, a.lastName].filter(Boolean).join(" ");
151
+ const title = a.jobTitle ? ` (${a.jobTitle})` : "";
152
+ const contact = a.email ?? a.mobilePhone ?? "";
153
+ lines.push(`\n**Attending agent:** ${agentName}${title}${contact ? ` — ${contact}` : ""}`);
154
+ }
155
+
156
+ // Feedback
157
+ if (v.buyerFeedback) lines.push(`\nBuyer feedback: ${v.buyerFeedback}`);
158
+ if (v.sellerFeedback) lines.push(`Seller feedback: ${v.sellerFeedback}`);
159
+ if (v.comment) lines.push(`Notes: ${v.comment}`);
160
+
66
161
  return lines.join("\n");
67
162
  },
68
163
  "viewing details"
69
164
  );
70
165
  }
166
+
167
+ function formatDateTime(iso: string): string {
168
+ const [datePart, timePart] = iso.split("T");
169
+ if (!timePart) return datePart;
170
+ return `${datePart} ${timePart.slice(0, 5)}`;
171
+ }
@@ -4,14 +4,22 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /residential/{dept}/viewings ─────────────────
8
+ // Fields: id, propertyId, propertyAddress, status,
9
+ // dateOfAppointment, dateOfAppointmentEnd,
10
+ // attendingAgentId, creatingAgentId, type, buyerName
11
+ // ────────────────────────────────────────────────────────────────
7
12
  interface LoopViewingSummary {
8
- id?: number;
13
+ id: number;
14
+ propertyId?: number;
9
15
  propertyAddress?: string;
10
- date?: string;
11
- time?: string;
12
16
  status?: string;
17
+ dateOfAppointment?: string;
18
+ dateOfAppointmentEnd?: string;
19
+ attendingAgentId?: string;
20
+ creatingAgentId?: string;
13
21
  type?: string;
14
- attendeeName?: string;
22
+ buyerName?: string;
15
23
  [key: string]: unknown;
16
24
  }
17
25
 
@@ -63,12 +71,22 @@ export async function viewingSearch(params: {
63
71
  result,
64
72
  (v) => {
65
73
  const addr = v.propertyAddress ?? "Unknown property";
66
- const when = [v.date, v.time].filter(Boolean).join(" ");
74
+ const id = v.id != null ? ` [ID: ${v.id}]` : "";
75
+ // dateOfAppointment is ISO datetime e.g. "2026-04-10T15:30:00"
76
+ const when = v.dateOfAppointment
77
+ ? formatDateTime(v.dateOfAppointment)
78
+ : "No date";
67
79
  const viewStatus = v.status ? ` [${v.status}]` : "";
68
- const attendee = v.attendeeName ? ` — ${v.attendeeName}` : "";
80
+ const buyer = v.buyerName ? ` — ${v.buyerName}` : "";
69
81
  const viewType = v.type ? ` (${v.type})` : "";
70
- return `- ${addr} — ${when || "No date"}${viewType}${attendee}${viewStatus}`;
82
+ return `- ${addr}${id} — ${when}${viewType}${buyer}${viewStatus}`;
71
83
  },
72
84
  "viewings"
73
85
  );
74
86
  }
87
+
88
+ function formatDateTime(iso: string): string {
89
+ const [datePart, timePart] = iso.split("T");
90
+ if (!timePart) return datePart;
91
+ return `${datePart} ${timePart.slice(0, 5)}`;
92
+ }
@@ -8,6 +8,11 @@ type Department = "sales" | "lettings";
8
8
  type FeedbackParty = "buyer" | "seller" | "renter" | "landlord";
9
9
  type Action = "note" | "feedback";
10
10
 
11
+ // ─── Loop API V2: POST /residential/{dept}/viewings/{id}/{suffix}
12
+ // Suffix: "note" for notes, "{party}-feedback" for feedback
13
+ // Request body: { result: string } (StringResponse format)
14
+ // Response: boolean success
15
+ // ────────────────────────────────────────────────────────────────
11
16
  interface LoopBooleanResponse {
12
17
  success?: boolean;
13
18
  [key: string]: unknown;
@@ -3700,9 +3700,9 @@ async function getRecentMessages(conversationId, limit = 50) {
3700
3700
  try {
3701
3701
  const result = await session.run(
3702
3702
  `MATCH (m:Message {conversationId: $conversationId})
3703
+ WITH m ORDER BY m.createdAt DESC LIMIT $limit
3703
3704
  RETURN m.messageId AS messageId, m.role AS role, m.content AS content, m.createdAt AS createdAt
3704
- ORDER BY m.createdAt ASC
3705
- LIMIT $limit`,
3705
+ ORDER BY m.createdAt ASC`,
3706
3706
  { conversationId, limit: neo4j.int(limit) }
3707
3707
  );
3708
3708
  return result.records.map((r) => ({
@@ -3971,6 +3971,26 @@ async function autoLabelSession(conversationId, userMessage) {
3971
3971
  if (currentEntry) currentEntry.pending = false;
3972
3972
  }
3973
3973
  }
3974
+ async function renameConversation(conversationId, label) {
3975
+ let embedding = null;
3976
+ try {
3977
+ embedding = await embed(label);
3978
+ } catch (err) {
3979
+ console.error(`[persist] manual-label: embedding failed, persisting without: ${err instanceof Error ? err.message : String(err)}`);
3980
+ }
3981
+ const session = getSession();
3982
+ try {
3983
+ await session.run(
3984
+ `MATCH (c:Conversation {conversationId: $conversationId})
3985
+ SET c.name = $label, c.updatedAt = datetime()
3986
+ ${embedding ? ", c.embedding = $embedding" : ""}`,
3987
+ { conversationId, label, ...embedding ? { embedding } : {} }
3988
+ );
3989
+ console.error(`[persist] manual-label: renamed ${conversationId} to "${label}"${embedding ? " (embedded)" : ""}`);
3990
+ } finally {
3991
+ await session.close();
3992
+ }
3993
+ }
3974
3994
  var INITIAL_CONFIDENCE = 0.5;
3975
3995
  var REINFORCEMENT_INCREMENT = 0.15;
3976
3996
  var DECAY_THRESHOLD_DAYS = 14;
@@ -5395,41 +5415,42 @@ function consumeStalledSubagents(sessionKey) {
5395
5415
  return stalls && stalls.length > 0 ? stalls : void 0;
5396
5416
  }
5397
5417
  function getMcpServers(accountId, userId, enabledPlugins) {
5418
+ const LOG_DIR2 = resolve5(ACCOUNTS_DIR, accountId, "logs");
5398
5419
  const servers = {
5399
5420
  "memory": {
5400
5421
  command: "node",
5401
5422
  args: [resolve5(PLATFORM_ROOT4, "plugins/memory/mcp/dist/index.js")],
5402
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, ...userId ? { USER_ID: userId } : {} }
5423
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2, ...userId ? { USER_ID: userId } : {} }
5403
5424
  },
5404
5425
  "contacts": {
5405
5426
  command: "node",
5406
5427
  args: [resolve5(PLATFORM_ROOT4, "plugins/contacts/mcp/dist/index.js")],
5407
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4 }
5428
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2 }
5408
5429
  },
5409
5430
  "whatsapp": {
5410
5431
  command: "node",
5411
5432
  args: [resolve5(PLATFORM_ROOT4, "plugins/whatsapp/mcp/dist/index.js")],
5412
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, PLATFORM_PORT: process.env.PORT ?? "19200" }
5433
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2, PLATFORM_PORT: process.env.PORT ?? "19200" }
5413
5434
  },
5414
5435
  "admin": {
5415
5436
  command: "node",
5416
5437
  args: [resolve5(PLATFORM_ROOT4, "plugins/admin/mcp/dist/index.js")],
5417
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, PLATFORM_PORT: process.env.PORT ?? "19200", ...userId ? { USER_ID: userId } : {} }
5438
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2, PLATFORM_PORT: process.env.PORT ?? "19200", ...userId ? { USER_ID: userId } : {} }
5418
5439
  },
5419
5440
  "scheduling": {
5420
5441
  command: "node",
5421
5442
  args: [resolve5(PLATFORM_ROOT4, "plugins/scheduling/mcp/dist/index.js")],
5422
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4 }
5443
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2 }
5423
5444
  },
5424
5445
  "tasks": {
5425
5446
  command: "node",
5426
5447
  args: [resolve5(PLATFORM_ROOT4, "plugins/tasks/mcp/dist/index.js")],
5427
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4 }
5448
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2 }
5428
5449
  },
5429
5450
  "email": {
5430
5451
  command: "node",
5431
5452
  args: [resolve5(PLATFORM_ROOT4, "plugins/email/mcp/dist/index.js")],
5432
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4 }
5453
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2 }
5433
5454
  },
5434
5455
  // Playwright MCP server — browser automation for browser-specialist.
5435
5456
  // Key matches Claude Code's plugin naming: plugin_{plugin}_{server} so tools
@@ -5446,7 +5467,7 @@ function getMcpServers(accountId, userId, enabledPlugins) {
5446
5467
  servers["telegram"] = {
5447
5468
  command: "node",
5448
5469
  args: [resolve5(PLATFORM_ROOT4, "plugins/telegram/mcp/dist/index.js")],
5449
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4 }
5470
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2 }
5450
5471
  };
5451
5472
  } else {
5452
5473
  console.error("[plugins] telegram MCP: skipped (no TELEGRAM_PUBLIC_BOT_TOKEN)");
@@ -5454,7 +5475,7 @@ function getMcpServers(accountId, userId, enabledPlugins) {
5454
5475
  servers["cloudflare"] = {
5455
5476
  command: "node",
5456
5477
  args: [resolve5(PLATFORM_ROOT4, "plugins/cloudflare/mcp/dist/index.js")],
5457
- env: { PLATFORM_ROOT: PLATFORM_ROOT4, PLATFORM_PORT: process.env.PORT ?? "19200" }
5478
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2, PLATFORM_PORT: process.env.PORT ?? "19200" }
5458
5479
  };
5459
5480
  if (Array.isArray(enabledPlugins) && enabledPlugins.length > 0) {
5460
5481
  const pluginsDir = resolve5(PLATFORM_ROOT4, "plugins");
@@ -5485,7 +5506,7 @@ function getMcpServers(accountId, userId, enabledPlugins) {
5485
5506
  servers[dir] = {
5486
5507
  command: "node",
5487
5508
  args: [mcpEntry],
5488
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4 }
5509
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT4, LOG_DIR: LOG_DIR2 }
5489
5510
  };
5490
5511
  console.log(`[plugins] optional MCP server started: ${dir}`);
5491
5512
  }
@@ -27210,8 +27231,87 @@ async function GET11(req, { params }) {
27210
27231
  }
27211
27232
  }
27212
27233
 
27234
+ // app/api/admin/sessions/[id]/label/route.ts
27235
+ var LABEL_MAX_LENGTH = 200;
27236
+ async function POST23(req, { params }) {
27237
+ const { id: conversationId } = await params;
27238
+ const url2 = new URL(req.url);
27239
+ const sessionKey = url2.searchParams.get("session_key");
27240
+ if (!sessionKey) {
27241
+ return Response.json({ error: "session_key required" }, { status: 400 });
27242
+ }
27243
+ if (!validateSession(sessionKey, "admin")) {
27244
+ return Response.json({ error: "Invalid or expired admin session" }, { status: 401 });
27245
+ }
27246
+ const accountId = getAccountIdForSession(sessionKey);
27247
+ if (!accountId) {
27248
+ return Response.json({ error: "Account not found for session" }, { status: 401 });
27249
+ }
27250
+ const owned = await verifyConversationOwnership(conversationId, accountId);
27251
+ if (!owned) {
27252
+ return Response.json({ error: "Conversation not found" }, { status: 404 });
27253
+ }
27254
+ console.error(`[admin] manual-label: requesting suggestion for ${conversationId}`);
27255
+ try {
27256
+ const messages = await getRecentMessages(conversationId, 50);
27257
+ const userMessages = messages.filter((m) => m.role === "user").map((m) => m.content);
27258
+ if (userMessages.length === 0) {
27259
+ console.error(`[admin] manual-label: haiku failed for ${conversationId} \u2014 no user messages`);
27260
+ return Response.json({ label: null });
27261
+ }
27262
+ const label = await generateSessionLabel(userMessages);
27263
+ if (label) {
27264
+ console.error(`[admin] manual-label: haiku suggested "${label}" for ${conversationId}`);
27265
+ } else {
27266
+ console.error(`[admin] manual-label: haiku failed for ${conversationId} \u2014 null response`);
27267
+ }
27268
+ return Response.json({ label });
27269
+ } catch (err) {
27270
+ console.error(`[admin] manual-label: haiku failed for ${conversationId} \u2014 ${err instanceof Error ? err.message : String(err)}`);
27271
+ return Response.json({ label: null });
27272
+ }
27273
+ }
27274
+ async function PUT(req, { params }) {
27275
+ const { id: conversationId } = await params;
27276
+ let body;
27277
+ try {
27278
+ body = await req.json();
27279
+ } catch {
27280
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
27281
+ }
27282
+ const sessionKey = body.session_key;
27283
+ if (!sessionKey) {
27284
+ return Response.json({ error: "session_key required" }, { status: 400 });
27285
+ }
27286
+ if (!validateSession(sessionKey, "admin")) {
27287
+ return Response.json({ error: "Invalid or expired admin session" }, { status: 401 });
27288
+ }
27289
+ const accountId = getAccountIdForSession(sessionKey);
27290
+ if (!accountId) {
27291
+ return Response.json({ error: "Account not found for session" }, { status: 401 });
27292
+ }
27293
+ const owned = await verifyConversationOwnership(conversationId, accountId);
27294
+ if (!owned) {
27295
+ return Response.json({ error: "Conversation not found" }, { status: 404 });
27296
+ }
27297
+ const label = typeof body.label === "string" ? body.label.trim() : "";
27298
+ if (!label) {
27299
+ return Response.json({ error: "label is required and must be non-empty" }, { status: 400 });
27300
+ }
27301
+ if (label.length > LABEL_MAX_LENGTH) {
27302
+ return Response.json({ error: `label must be ${LABEL_MAX_LENGTH} characters or fewer` }, { status: 400 });
27303
+ }
27304
+ try {
27305
+ await renameConversation(conversationId, label);
27306
+ return Response.json({ ok: true });
27307
+ } catch (err) {
27308
+ console.error(`[persist] manual-label: failed to rename ${conversationId} \u2014 ${err instanceof Error ? err.message : String(err)}`);
27309
+ return Response.json({ error: "Failed to rename session" }, { status: 500 });
27310
+ }
27311
+ }
27312
+
27213
27313
  // app/api/admin/browser/launch/route.ts
27214
- async function POST23() {
27314
+ async function POST24() {
27215
27315
  try {
27216
27316
  const vncOk = await ensureVnc();
27217
27317
  if (!vncOk) {
@@ -27609,7 +27709,7 @@ app.post("/api/admin/file-attach", (c) => POST16(c.req.raw));
27609
27709
  app.get("/api/admin/version", () => GET9());
27610
27710
  app.post("/api/admin/version/upgrade", (c) => POST22(c.req.raw));
27611
27711
  app.get("/api/admin/agents", () => GET8());
27612
- app.post("/api/admin/browser/launch", () => POST23());
27712
+ app.post("/api/admin/browser/launch", () => POST24());
27613
27713
  app.get("/api/admin/sessions", (c) => GET10(c.req.raw));
27614
27714
  app.delete(
27615
27715
  "/api/admin/sessions/:id",
@@ -27619,6 +27719,14 @@ app.get(
27619
27719
  "/api/admin/sessions/:id/messages",
27620
27720
  (c) => GET11(c.req.raw, { params: Promise.resolve({ id: c.req.param("id") }) })
27621
27721
  );
27722
+ app.post(
27723
+ "/api/admin/sessions/:id/label",
27724
+ (c) => POST23(c.req.raw, { params: Promise.resolve({ id: c.req.param("id") }) })
27725
+ );
27726
+ app.put(
27727
+ "/api/admin/sessions/:id/label",
27728
+ (c) => PUT(c.req.raw, { params: Promise.resolve({ id: c.req.param("id") }) })
27729
+ );
27622
27730
  var SAFE_SLUG_RE = /^[a-z][a-z0-9-]{2,49}$/;
27623
27731
  var SAFE_FILENAME_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
27624
27732
  var IMAGE_MIME = {