@rubytech/create-maxy 1.0.507 → 1.0.508

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
@@ -2,6 +2,14 @@ import { loopGet, loopPost, loopPut, withTeamKey } from "../lib/loop-api.js";
2
2
 
3
3
  type EnquiryAction = "seller-enquiry" | "autoresponder-get" | "autoresponder-answers" | "autoresponder-details" | "autoresponder-refer";
4
4
 
5
+ // ─── Loop API V2: Marketing Enquiry Endpoints ──────────────────
6
+ // POST /marketing/enquiries/seller — submit seller enquiry
7
+ // GET /marketing/enquiries/auto-responder/{id}/{key} — read enquiry
8
+ // PUT .../auto-responder/{id}/answers/{key} — submit answers
9
+ // PUT .../auto-responder/{id}/details/{key} — update details
10
+ // PUT .../auto-responder/{id}/refer/{key} — refer enquiry
11
+ // Note: auto-responder uses personCode + key (UUID), not personId
12
+ // ────────────────────────────────────────────────────────────────
5
13
  interface LoopBooleanResponse {
6
14
  success?: boolean;
7
15
  [key: string]: unknown;
@@ -4,6 +4,11 @@ import {
4
4
  loopPut,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: PUT /marketing/matching/other-matches ─────────
8
+ // Request body: array of property IDs
9
+ // Response: array of matching property summaries
10
+ // Schema not verified via curl (depends on matching state)
11
+ // ────────────────────────────────────────────────────────────────
7
12
  interface LoopMatchingSummary {
8
13
  id?: number;
9
14
  address?: string;
@@ -3,6 +3,11 @@ import { loopPost, withTeamKey } from "../lib/loop-api.js";
3
3
  type Department = "sales" | "lettings";
4
4
  type MatchAction = "viewing" | "information" | "callback";
5
5
 
6
+ // ─── Loop API V2: POST /marketing/matching/{id}/{action} ────────
7
+ // Actions: viewing, information, callback
8
+ // Request body: { name?, email?, phone?, message? }
9
+ // Response: boolean success
10
+ // ────────────────────────────────────────────────────────────────
6
11
  interface LoopBooleanResponse {
7
12
  success?: boolean;
8
13
  [key: string]: unknown;
@@ -4,6 +4,12 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /marketing/matching/{id} ─────────────────────
8
+ // Returned HTTP 500 for test property 895155 — may require
9
+ // publishedToMatching: true. Exact response schema not verified.
10
+ // Fields below are from Swagger docs; [key: string]: unknown
11
+ // ensures any actual response fields are accessible.
12
+ // ────────────────────────────────────────────────────────────────
7
13
  interface LoopMatchingProperty {
8
14
  id?: number;
9
15
  address?: string;
@@ -4,17 +4,111 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /people/{id} ─────────────────────────────────
8
+ // Fields: id, title, firstName, lastName, contactNotes,
9
+ // primaryPhone, primaryPhoneType, primaryEmail, primaryEmailType,
10
+ // dateCreated, houseNameOrNumber, houseSecondaryNameOrNumber,
11
+ // street, locality, town, district, county, postcode,
12
+ // buyerProfiles[], sellerProfiles[], renterIds[], landlordIds[],
13
+ // gdpr*, communication*
14
+ // ────────────────────────────────────────────────────────────────
7
15
  interface LoopPersonDetail {
8
- id?: number;
16
+ id: number;
17
+ title?: string;
9
18
  firstName?: string;
10
19
  lastName?: string;
11
- email?: string;
12
- phone?: string;
13
- mobile?: string;
14
- type?: string;
15
- address?: string;
20
+ contactNotes?: string;
21
+ primaryPhone?: string;
22
+ primaryPhoneType?: string;
23
+ primaryEmail?: string;
24
+ primaryEmailType?: string;
25
+ dateCreated?: string;
26
+ houseNameOrNumber?: string;
27
+ houseSecondaryNameOrNumber?: string;
28
+ street?: string;
29
+ locality?: string;
30
+ town?: string;
31
+ district?: string;
32
+ county?: string;
16
33
  postcode?: string;
17
- notes?: string;
34
+ buyerProfiles?: LoopBuyerProfileRef[];
35
+ sellerProfiles?: LoopSellerProfileRef[];
36
+ renterIds?: number[];
37
+ landlordIds?: number[];
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ interface LoopBuyerProfileRef {
42
+ id: number;
43
+ buyerGroupName?: string;
44
+ maxPrice?: number | null;
45
+ minBeds?: number;
46
+ viewingCount?: number;
47
+ offerCount?: number;
48
+ [key: string]: unknown;
49
+ }
50
+
51
+ interface LoopSellerProfileRef {
52
+ id: number;
53
+ sellerGroupName?: string;
54
+ propertyId?: number;
55
+ propertyAddress?: string;
56
+ price?: number;
57
+ propertyStatus?: string;
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ // ─── Loop API V2: /people/buyers/{id} ──────────────────────────
62
+ // Fields: id, buyerGroupName, buyerIsGroup, maxPrice, minBeds,
63
+ // position, requirementNotes, sellingPosition, purchaseReason,
64
+ // financiallyVerified, mortgageOffered, insuranceOffered,
65
+ // people[] (nested PersonSummary), propertyTypes[], searchAreas[],
66
+ // viewings[], offers[]
67
+ // ────────────────────────────────────────────────────────────────
68
+ interface LoopBuyerDetail {
69
+ id: number;
70
+ buyerGroupName?: string;
71
+ buyerIsGroup?: boolean;
72
+ maxPrice?: number | null;
73
+ minBeds?: number;
74
+ position?: string;
75
+ requirementNotes?: string;
76
+ sellingPosition?: string | null;
77
+ purchaseReason?: string | null;
78
+ financiallyVerified?: string | null;
79
+ mortgageOffered?: string | null;
80
+ insuranceOffered?: string | null;
81
+ people?: LoopNestedPerson[];
82
+ propertyTypes?: string[];
83
+ searchAreas?: string[];
84
+ viewings?: Record<string, unknown>[];
85
+ offers?: Record<string, unknown>[];
86
+ [key: string]: unknown;
87
+ }
88
+
89
+ // ─── Loop API V2: /people/sellers/{id} ─────────────────────────
90
+ // Fields: id, sellerGroupName, sellerIsGroup,
91
+ // people[] (nested PersonSummary), property (nested PropertySummary)
92
+ // ────────────────────────────────────────────────────────────────
93
+ interface LoopSellerDetail {
94
+ id: number;
95
+ sellerGroupName?: string;
96
+ sellerIsGroup?: boolean;
97
+ people?: LoopNestedPerson[];
98
+ property?: Record<string, unknown>;
99
+ [key: string]: unknown;
100
+ }
101
+
102
+ interface LoopNestedPerson {
103
+ id: number;
104
+ title?: string;
105
+ firstName?: string;
106
+ lastName?: string;
107
+ contactNotes?: string;
108
+ primaryPhone?: string;
109
+ primaryPhoneType?: string;
110
+ primaryEmail?: string;
111
+ primaryEmailType?: string;
18
112
  [key: string]: unknown;
19
113
  }
20
114
 
@@ -28,14 +122,14 @@ export async function peopleDetail(params: {
28
122
  }): Promise<string> {
29
123
  const { accountId, personId, role, teamName } = params;
30
124
 
31
- const result = await aggregateAcrossTeams<LoopPersonDetail>(
125
+ const result = await aggregateAcrossTeams<Record<string, unknown>>(
32
126
  accountId,
33
127
  "people",
34
128
  "loop-people-detail",
35
129
  async (apiKey, team) => {
36
130
  const basePath = role ? `/people/${role}` : "/people";
37
131
  const path = `${basePath}/${personId}`;
38
- const data = await loopGet<LoopPersonDetail>(apiKey, path, "loop-people-detail", team);
132
+ const data = await loopGet<Record<string, unknown>>(apiKey, path, "loop-people-detail", team);
39
133
  if (data && typeof data === "object" && !Array.isArray(data)) {
40
134
  return [data];
41
135
  }
@@ -46,18 +140,106 @@ export async function peopleDetail(params: {
46
140
 
47
141
  return formatAggregationResult(
48
142
  result,
49
- (p) => {
50
- const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
51
- const lines = [`**${name}**`];
52
- if (p.type) lines.push(`Type: ${p.type}`);
53
- if (p.email) lines.push(`Email: ${p.email}`);
54
- if (p.phone) lines.push(`Phone: ${p.phone}`);
55
- if (p.mobile) lines.push(`Mobile: ${p.mobile}`);
56
- if (p.address) lines.push(`Address: ${p.address}`);
57
- if (p.postcode) lines.push(`Postcode: ${p.postcode}`);
58
- if (p.notes) lines.push(`Notes: ${p.notes}`);
59
- return lines.join("\n");
60
- },
143
+ (record) => formatDetailRecord(record, role),
61
144
  "person details"
62
145
  );
63
146
  }
147
+
148
+ function formatDetailRecord(record: Record<string, unknown>, role?: PeopleRole): string {
149
+ if (role === "buyers") {
150
+ const p = record as unknown as LoopBuyerDetail;
151
+ const lines = [`**${p.buyerGroupName ?? "Unknown"}** [ID: ${p.id}]`];
152
+ if (p.maxPrice != null) lines.push(`Max price: £${p.maxPrice.toLocaleString("en-GB")}`);
153
+ if (p.minBeds) lines.push(`Min beds: ${p.minBeds}`);
154
+ if (p.position && p.position !== "none") lines.push(`Position: ${p.position}`);
155
+ if (p.sellingPosition) lines.push(`Selling position: ${p.sellingPosition}`);
156
+ if (p.purchaseReason) lines.push(`Purchase reason: ${p.purchaseReason}`);
157
+ if (p.financiallyVerified) lines.push(`Financially verified: ${p.financiallyVerified}`);
158
+ if (p.mortgageOffered) lines.push(`Mortgage: ${p.mortgageOffered}`);
159
+ if (p.requirementNotes) lines.push(`Requirements: ${p.requirementNotes}`);
160
+ if (p.propertyTypes?.length) lines.push(`Property types: ${p.propertyTypes.join(", ")}`);
161
+ if (p.searchAreas?.length) lines.push(`Search areas: ${p.searchAreas.join(", ")}`);
162
+ if (p.viewings?.length) lines.push(`Viewings: ${p.viewings.length}`);
163
+ if (p.offers?.length) lines.push(`Offers: ${p.offers.length}`);
164
+
165
+ if (p.people?.length) {
166
+ lines.push(`\n**Contacts (${p.people.length}):**`);
167
+ for (const person of p.people) {
168
+ const name = [person.firstName, person.lastName].filter(Boolean).join(" ");
169
+ const phone = person.primaryPhone ?? "";
170
+ const email = person.primaryEmail ?? "";
171
+ const contact = [phone, email].filter(Boolean).join(" | ");
172
+ lines.push(` - ${name} [ID: ${person.id}]${contact ? ` — ${contact}` : ""}`);
173
+ }
174
+ }
175
+ return lines.join("\n");
176
+ }
177
+
178
+ if (role === "sellers" || role === "landlords") {
179
+ const p = record as unknown as LoopSellerDetail;
180
+ const lines = [`**${p.sellerGroupName ?? "Unknown"}** [ID: ${p.id}]`];
181
+
182
+ if (p.property) {
183
+ const prop = p.property;
184
+ if (prop.propertyAddress) lines.push(`Property: ${prop.propertyAddress}`);
185
+ if (prop.price != null) lines.push(`Price: £${Number(prop.price).toLocaleString("en-GB")}`);
186
+ if (prop.status) lines.push(`Status: ${prop.status}`);
187
+ if (prop.propertyType) lines.push(`Type: ${prop.propertyType}`);
188
+ if (prop.propertyId || prop.id) lines.push(`Property ID: ${prop.propertyId ?? prop.id}`);
189
+ }
190
+
191
+ if (p.people?.length) {
192
+ lines.push(`\n**Contacts (${p.people.length}):**`);
193
+ for (const person of p.people) {
194
+ const name = [person.firstName, person.lastName].filter(Boolean).join(" ");
195
+ const phone = person.primaryPhone ?? "";
196
+ const email = person.primaryEmail ?? "";
197
+ const contact = [phone, email].filter(Boolean).join(" | ");
198
+ lines.push(` - ${name} [ID: ${person.id}]${contact ? ` — ${contact}` : ""}`);
199
+ }
200
+ }
201
+ return lines.join("\n");
202
+ }
203
+
204
+ // Generic /people/{id} — PersonDetail schema
205
+ const p = record as unknown as LoopPersonDetail;
206
+ const title = p.title?.trim() ? `${p.title.trim()} ` : "";
207
+ const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
208
+ const lines = [`**${title}${name}** [ID: ${p.id}]`];
209
+
210
+ if (p.primaryEmail) lines.push(`Email: ${p.primaryEmail}`);
211
+ if (p.primaryPhone) lines.push(`Phone: ${p.primaryPhone}`);
212
+ if (p.dateCreated) lines.push(`Created: ${p.dateCreated.split("T")[0]}`);
213
+
214
+ // Build address from components
215
+ const addrParts = [
216
+ p.houseSecondaryNameOrNumber,
217
+ p.houseNameOrNumber,
218
+ p.street,
219
+ p.locality,
220
+ p.town,
221
+ p.postcode,
222
+ ].filter(Boolean);
223
+ if (addrParts.length) lines.push(`Address: ${addrParts.join(", ")}`);
224
+
225
+ if (p.contactNotes) lines.push(`Notes: ${p.contactNotes}`);
226
+
227
+ if (p.buyerProfiles?.length) {
228
+ lines.push(`\n**Buyer profiles (${p.buyerProfiles.length}):**`);
229
+ for (const bp of p.buyerProfiles) {
230
+ const bpName = bp.buyerGroupName ?? `Buyer ${bp.id}`;
231
+ const price = bp.maxPrice != null ? ` max £${bp.maxPrice.toLocaleString("en-GB")}` : "";
232
+ lines.push(` - ${bpName} [ID: ${bp.id}]${price}`);
233
+ }
234
+ }
235
+ if (p.sellerProfiles?.length) {
236
+ lines.push(`\n**Seller profiles (${p.sellerProfiles.length}):**`);
237
+ for (const sp of p.sellerProfiles) {
238
+ const spName = sp.sellerGroupName ?? `Seller ${sp.id}`;
239
+ const addr = sp.propertyAddress ? ` — ${sp.propertyAddress}` : "";
240
+ lines.push(` - ${spName} [ID: ${sp.id}]${addr}`);
241
+ }
242
+ }
243
+
244
+ return lines.join("\n");
245
+ }
@@ -4,13 +4,69 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /people ──────────────────────────────────────
8
+ // Fields: id, title, firstName, lastName, contactNotes,
9
+ // primaryPhone, primaryPhoneType, primaryPhoneNotes,
10
+ // primaryEmail, primaryEmailType, primaryEmailNotes,
11
+ // gdprGeneralMarketing, gdprPropertyMatching, gdprThirdParties,
12
+ // communicationCall, communicationEmail, communicationPost, communicationText
13
+ // ────────────────────────────────────────────────────────────────
7
14
  interface LoopPersonSummary {
8
- id?: number;
15
+ id: number;
16
+ title?: string;
9
17
  firstName?: string;
10
18
  lastName?: string;
11
- email?: string;
12
- phone?: string;
13
- type?: string;
19
+ contactNotes?: string;
20
+ primaryPhone?: string;
21
+ primaryPhoneType?: string;
22
+ primaryEmail?: string;
23
+ primaryEmailType?: string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ // ─── Loop API V2: /people/buyers ───────────────────────────────
28
+ // Fields: id, buyerGroupName, buyerIsGroup, maxPrice, minBeds,
29
+ // position, sellingPosition, purchaseReason, financiallyVerified,
30
+ // mortgageOffered, insuranceOffered, offerCount, acceptedOfferCount,
31
+ // viewingCount, upcomingViewingCount
32
+ // ────────────────────────────────────────────────────────────────
33
+ interface LoopBuyerSummary {
34
+ id: number;
35
+ buyerGroupName?: string;
36
+ buyerIsGroup?: boolean;
37
+ maxPrice?: number | null;
38
+ minBeds?: number;
39
+ position?: string;
40
+ sellingPosition?: string | null;
41
+ purchaseReason?: string | null;
42
+ financiallyVerified?: string | null;
43
+ mortgageOffered?: string | null;
44
+ insuranceOffered?: string | null;
45
+ offerCount?: number;
46
+ acceptedOfferCount?: number;
47
+ viewingCount?: number;
48
+ upcomingViewingCount?: number;
49
+ [key: string]: unknown;
50
+ }
51
+
52
+ // ─── Loop API V2: /people/sellers ──────────────────────────────
53
+ // Fields: id, sellerGroupName, sellerIsGroup, propertyId,
54
+ // propertyStatus, propertyType, propertyAddress, price,
55
+ // offerCount, acceptedOfferCount, viewingCount, upcomingViewingCount
56
+ // ────────────────────────────────────────────────────────────────
57
+ interface LoopSellerSummary {
58
+ id: number;
59
+ sellerGroupName?: string;
60
+ sellerIsGroup?: boolean;
61
+ propertyId?: number;
62
+ propertyStatus?: string;
63
+ propertyType?: string;
64
+ propertyAddress?: string;
65
+ price?: number;
66
+ offerCount?: number;
67
+ acceptedOfferCount?: number;
68
+ viewingCount?: number;
69
+ upcomingViewingCount?: number;
14
70
  [key: string]: unknown;
15
71
  }
16
72
 
@@ -41,7 +97,9 @@ export async function peopleSearch(params: {
41
97
  teamName, limit,
42
98
  } = params;
43
99
 
44
- const result = await aggregateAcrossTeams<LoopPersonSummary>(
100
+ // Role-specific endpoints return different schemas — use a generic record
101
+ // type for aggregation, then cast per-role in the formatter.
102
+ const result = await aggregateAcrossTeams<Record<string, unknown>>(
45
103
  accountId,
46
104
  "people",
47
105
  "loop-people-search",
@@ -73,7 +131,7 @@ export async function peopleSearch(params: {
73
131
  const query = qp.length > 0 ? `?${qp.join("&")}` : "";
74
132
  const basePath = role ? `/people/${role}` : "/people";
75
133
  const path = `${basePath}${query}`;
76
- const data = await loopGet<LoopPersonSummary[]>(apiKey, path, "loop-people-search", team);
134
+ const data = await loopGet<Record<string, unknown>[]>(apiKey, path, "loop-people-search", team);
77
135
  return Array.isArray(data) ? data : [];
78
136
  },
79
137
  { teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
@@ -82,12 +140,41 @@ export async function peopleSearch(params: {
82
140
  const entityName = role ?? "people";
83
141
  return formatAggregationResult(
84
142
  result,
85
- (p) => {
86
- const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
87
- const contact = p.email ?? p.phone ?? "";
88
- const type = p.type ? ` (${p.type})` : "";
89
- return `- ${name}${contact ? ` <${contact}>` : ""}${type}`;
90
- },
143
+ (record) => formatPersonRecord(record, role),
91
144
  entityName
92
145
  );
93
146
  }
147
+
148
+ function formatPersonRecord(record: Record<string, unknown>, role?: PeopleRole): string {
149
+ if (role === "buyers") {
150
+ const p = record as unknown as LoopBuyerSummary;
151
+ const name = p.buyerGroupName ?? "Unknown";
152
+ const id = p.id != null ? ` [ID: ${p.id}]` : "";
153
+ const price = p.maxPrice != null ? ` — max £${p.maxPrice.toLocaleString("en-GB")}` : "";
154
+ const beds = p.minBeds ? ` ${p.minBeds}+ beds` : "";
155
+ const position = p.position && p.position !== "none" ? ` (${p.position})` : "";
156
+ const viewings = p.viewingCount ? ` | ${p.viewingCount} viewing${p.viewingCount !== 1 ? "s" : ""}` : "";
157
+ const offers = p.offerCount ? ` | ${p.offerCount} offer${p.offerCount !== 1 ? "s" : ""}` : "";
158
+ return `- ${name}${id}${price}${beds}${position}${viewings}${offers}`;
159
+ }
160
+
161
+ if (role === "sellers" || role === "landlords") {
162
+ const p = record as unknown as LoopSellerSummary;
163
+ const name = p.sellerGroupName ?? "Unknown";
164
+ const id = p.id != null ? ` [ID: ${p.id}]` : "";
165
+ const addr = p.propertyAddress ? ` — ${p.propertyAddress}` : "";
166
+ const price = p.price != null ? ` £${p.price.toLocaleString("en-GB")}` : "";
167
+ const status = p.propertyStatus ? ` [${p.propertyStatus}]` : "";
168
+ const viewings = p.viewingCount ? ` | ${p.viewingCount} viewing${p.viewingCount !== 1 ? "s" : ""}` : "";
169
+ const offers = p.offerCount ? ` | ${p.offerCount} offer${p.offerCount !== 1 ? "s" : ""}` : "";
170
+ return `- ${name}${id}${addr}${price}${status}${viewings}${offers}`;
171
+ }
172
+
173
+ // Generic /people or /people/renters — uses PersonSummary schema
174
+ const p = record as unknown as LoopPersonSummary;
175
+ const name = [p.firstName, p.lastName].filter(Boolean).join(" ") || "Unknown";
176
+ const id = p.id != null ? ` [ID: ${p.id}]` : "";
177
+ const contact = p.primaryEmail ?? p.primaryPhone ?? "";
178
+ const title = p.title?.trim() ? `${p.title.trim()} ` : "";
179
+ return `- ${title}${name}${id}${contact ? ` <${contact}>` : ""}`;
180
+ }
@@ -4,20 +4,55 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /property/residential/{dept}/{id} ────────────
8
+ // Fields: id, refId, address_DisplayAddress, address_HouseNameOrNumber,
9
+ // address_HouseSecondaryNameOrNumber, address_Street, address_Locality,
10
+ // address_Town, address_Postcode, status, propertyType, dateCreated,
11
+ // dateInstructed, dateLaunched, dateExchanged, dateCompleted,
12
+ // dateWithdrawn, shortDescription, fullDescription, bathrooms,
13
+ // bedrooms, receptionRooms, price, priceQualifier, latitude,
14
+ // longitude, parking, outsideSpace, councilTaxBand, floorArea,
15
+ // tenure, serviceCharge, groundRent, features[], images[],
16
+ // marketingFlag_*, creatingAgentId
17
+ // ────────────────────────────────────────────────────────────────
7
18
  interface LoopPropertyDetail {
8
- id?: number;
9
- address?: string;
10
- price?: number;
19
+ id: number;
20
+ refId?: string;
21
+ address_DisplayAddress?: string | null;
22
+ address_HouseNameOrNumber?: string;
23
+ address_HouseSecondaryNameOrNumber?: string;
24
+ address_Street?: string;
25
+ address_Locality?: string | null;
26
+ address_Town?: string;
27
+ address_Postcode?: string;
11
28
  status?: string;
12
- type?: string;
13
- bedrooms?: number;
29
+ propertyType?: string;
30
+ dateCreated?: string;
31
+ dateInstructed?: string | null;
32
+ dateLaunched?: string | null;
33
+ dateExchanged?: string | null;
34
+ dateCompleted?: string | null;
35
+ dateWithdrawn?: string | null;
36
+ shortDescription?: string;
37
+ fullDescription?: string;
14
38
  bathrooms?: number;
15
- description?: string;
16
- receptions?: number;
17
- tenure?: string;
18
- councilTax?: string;
19
- epcRating?: string;
39
+ bedrooms?: number;
40
+ receptionRooms?: number;
41
+ price?: number;
42
+ priceQualifier?: string;
43
+ latitude?: number;
44
+ longitude?: number;
45
+ parking?: string;
46
+ outsideSpace?: string;
47
+ councilTaxBand?: string;
20
48
  floorArea?: number;
49
+ tenure?: string;
50
+ serviceCharge?: number;
51
+ groundRent?: number;
52
+ features?: string[];
53
+ images?: { url: string; mimeType?: string }[];
54
+ creatingAgentId?: string;
55
+ isDirectViewingsEnabled?: boolean;
21
56
  [key: string]: unknown;
22
57
  }
23
58
 
@@ -52,17 +87,57 @@ export async function propertyDetail(params: {
52
87
  return formatAggregationResult(
53
88
  result,
54
89
  (p) => {
55
- const lines = [`**${p.address ?? "Unknown address"}**`];
56
- if (p.price) lines.push(`Price: £${p.price.toLocaleString("en-GB")}`);
90
+ // Build display address from components
91
+ const addr = p.address_DisplayAddress
92
+ ?? ([
93
+ p.address_HouseSecondaryNameOrNumber,
94
+ p.address_HouseNameOrNumber,
95
+ p.address_Street,
96
+ p.address_Town,
97
+ p.address_Postcode,
98
+ ].filter(Boolean).join(", ") || "Unknown address");
99
+
100
+ const lines = [`**${addr}** [ID: ${p.id}]`];
101
+ if (p.price != null) {
102
+ const qualifier = p.priceQualifier && p.priceQualifier !== "none" ? ` (${p.priceQualifier})` : "";
103
+ lines.push(`Price: £${p.price.toLocaleString("en-GB")}${qualifier}`);
104
+ }
57
105
  if (p.status) lines.push(`Status: ${p.status}`);
58
- if (p.type) lines.push(`Type: ${p.type}`);
59
- if (p.bedrooms) lines.push(`Bedrooms: ${p.bedrooms}`);
60
- if (p.bathrooms) lines.push(`Bathrooms: ${p.bathrooms}`);
61
- if (p.receptions) lines.push(`Receptions: ${p.receptions}`);
62
- if (p.tenure) lines.push(`Tenure: ${p.tenure}`);
63
- if (p.councilTax) lines.push(`Council Tax: ${p.councilTax}`);
64
- if (p.epcRating) lines.push(`EPC: ${p.epcRating}`);
65
- if (p.description) lines.push(`\n${p.description}`);
106
+ if (p.propertyType) lines.push(`Type: ${p.propertyType}`);
107
+ if (p.bedrooms != null) lines.push(`Bedrooms: ${p.bedrooms}`);
108
+ if (p.bathrooms != null) lines.push(`Bathrooms: ${p.bathrooms}`);
109
+ if (p.receptionRooms != null) lines.push(`Reception rooms: ${p.receptionRooms}`);
110
+ if (p.floorArea) lines.push(`Floor area: ${p.floorArea} sq ft`);
111
+ if (p.tenure && p.tenure !== "None") lines.push(`Tenure: ${p.tenure}`);
112
+ if (p.councilTaxBand) lines.push(`Council Tax: Band ${p.councilTaxBand}`);
113
+ if (p.parking) lines.push(`Parking: ${p.parking}`);
114
+ if (p.outsideSpace) lines.push(`Outside: ${p.outsideSpace}`);
115
+ if (p.serviceCharge) lines.push(`Service charge: £${p.serviceCharge}`);
116
+ if (p.groundRent) lines.push(`Ground rent: £${p.groundRent}`);
117
+
118
+ // Key dates
119
+ const dates: string[] = [];
120
+ if (p.dateInstructed) dates.push(`Instructed: ${p.dateInstructed.split("T")[0]}`);
121
+ if (p.dateLaunched) dates.push(`Launched: ${p.dateLaunched.split("T")[0]}`);
122
+ if (p.dateExchanged) dates.push(`Exchanged: ${p.dateExchanged.split("T")[0]}`);
123
+ if (p.dateCompleted) dates.push(`Completed: ${p.dateCompleted.split("T")[0]}`);
124
+ if (p.dateWithdrawn) dates.push(`Withdrawn: ${p.dateWithdrawn.split("T")[0]}`);
125
+ if (dates.length) lines.push(dates.join(" | "));
126
+
127
+ if (p.features?.length) {
128
+ lines.push(`Features: ${p.features.join(", ")}`);
129
+ }
130
+
131
+ if (p.images?.length) {
132
+ lines.push(`Images: ${p.images.length} photo${p.images.length !== 1 ? "s" : ""}`);
133
+ }
134
+
135
+ // Short description — strip HTML tags for readability
136
+ if (p.shortDescription) {
137
+ const clean = p.shortDescription.replace(/<[^>]+>/g, "").trim();
138
+ if (clean) lines.push(`\n${clean}`);
139
+ }
140
+
66
141
  return lines.join("\n");
67
142
  },
68
143
  "property details"
@@ -4,14 +4,31 @@ import {
4
4
  loopGet,
5
5
  } from "../lib/loop-api.js";
6
6
 
7
+ // ─── Loop API V2: /property/residential/{dept}/listed/{channel} ─
8
+ // Returns full property detail enriched with listing metadata:
9
+ // listingId, propertyId, propertyRefId, channel, displayAddress,
10
+ // propertyAddress, address_* fields, status, propertyType,
11
+ // dateLaunched, price, bedrooms, bathrooms, receptionRooms,
12
+ // shortDescription, features[], images[]
13
+ // ─────────────────────────────────────────────────────────────────
7
14
  interface LoopPropertyListing {
8
- id?: number;
9
- address?: string;
10
- price?: number;
15
+ listingId?: number;
16
+ propertyId?: number;
17
+ propertyRefId?: string;
18
+ channel?: string;
19
+ propertyAddress?: string;
20
+ displayAddress?: string;
11
21
  status?: string;
12
- type?: string;
22
+ propertyType?: string;
23
+ dateLaunched?: string;
24
+ price?: number;
25
+ priceQualifier?: string;
13
26
  bedrooms?: number;
14
- listingUrl?: string;
27
+ bathrooms?: number;
28
+ receptionRooms?: number;
29
+ shortDescription?: string;
30
+ features?: string[];
31
+ images?: { url: string }[];
15
32
  [key: string]: unknown;
16
33
  }
17
34
 
@@ -57,10 +74,14 @@ export async function propertyListed(params: {
57
74
  return formatAggregationResult(
58
75
  result,
59
76
  (p) => {
77
+ const addr = p.displayAddress ?? p.propertyAddress ?? "Unknown address";
78
+ const id = p.propertyId != null ? ` [ID: ${p.propertyId}]` : "";
60
79
  const price = p.price ? ` — £${p.price.toLocaleString("en-GB")}` : "";
61
80
  const beds = p.bedrooms ? ` ${p.bedrooms}bed` : "";
81
+ const propType = p.propertyType ? ` (${p.propertyType})` : "";
62
82
  const status = p.status ? ` [${p.status}]` : "";
63
- return `- ${p.address ?? "Unknown address"}${price}${beds}${status}`;
83
+ const photos = p.images?.length ? ` ${p.images.length} photo${p.images.length !== 1 ? "s" : ""}` : "";
84
+ return `- ${addr}${id}${price}${beds}${propType}${status}${photos}`;
64
85
  },
65
86
  `${channel} listings`
66
87
  );
@@ -3,6 +3,11 @@ import { loopPost, withTeamKey } from "../lib/loop-api.js";
3
3
  type Department = "sales" | "lettings";
4
4
  type RequestAction = "viewing" | "call-back" | "information";
5
5
 
6
+ // ─── Loop API V2: POST /property/residential/{dept}/{id}/{action}
7
+ // Actions: viewing, call-back, information
8
+ // Request body: { name?, email?, phone?, message? }
9
+ // Response: boolean success
10
+ // ────────────────────────────────────────────────────────────────
6
11
  interface LoopBooleanResponse {
7
12
  success?: boolean;
8
13
  [key: string]: unknown;