@rubytech/create-maxy 1.0.499 → 1.0.500

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 (109) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  3. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/PLUGIN.md +36 -8
  4. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js +229 -153
  5. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js.map +1 -1
  6. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.d.ts +19 -1
  7. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
  8. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.js +99 -3
  9. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.js.map +1 -1
  10. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts +10 -0
  11. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts.map +1 -0
  12. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js +24 -0
  13. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js.map +1 -0
  14. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts +16 -0
  15. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts.map +1 -0
  16. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js +35 -0
  17. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js.map +1 -0
  18. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/key-register.js +1 -1
  19. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/key-register.js.map +1 -1
  20. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts +13 -0
  21. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -0
  22. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js +41 -0
  23. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js.map +1 -0
  24. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts +9 -0
  25. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts.map +1 -0
  26. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js +16 -0
  27. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js.map +1 -0
  28. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts +15 -0
  29. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts.map +1 -0
  30. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js +11 -0
  31. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js.map +1 -0
  32. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts +10 -0
  33. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts.map +1 -0
  34. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js +39 -0
  35. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js.map +1 -0
  36. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts +9 -0
  37. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts.map +1 -0
  38. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js +33 -0
  39. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js.map +1 -0
  40. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts +18 -0
  41. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts.map +1 -0
  42. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js +59 -0
  43. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js.map +1 -0
  44. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts +10 -0
  45. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts.map +1 -0
  46. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js +39 -0
  47. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js.map +1 -0
  48. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts +12 -0
  49. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts.map +1 -0
  50. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js +28 -0
  51. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js.map +1 -0
  52. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts +15 -0
  53. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts.map +1 -0
  54. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js +11 -0
  55. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js.map +1 -0
  56. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts +16 -0
  57. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts.map +1 -0
  58. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js +39 -0
  59. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js.map +1 -0
  60. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts +13 -0
  61. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts.map +1 -0
  62. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js +49 -0
  63. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js.map +1 -0
  64. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts +7 -0
  65. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts.map +1 -0
  66. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js +15 -0
  67. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js.map +1 -0
  68. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts +14 -0
  69. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts.map +1 -0
  70. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js +11 -0
  71. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js.map +1 -0
  72. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts +9 -0
  73. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts.map +1 -0
  74. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js +40 -0
  75. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js.map +1 -0
  76. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts +13 -0
  77. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts.map +1 -0
  78. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js +34 -0
  79. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js.map +1 -0
  80. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts +14 -0
  81. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts.map +1 -0
  82. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js +18 -0
  83. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js.map +1 -0
  84. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/index.ts +335 -158
  85. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/lib/loop-api.ts +140 -3
  86. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/customer-preferences.ts +60 -0
  87. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback.ts +80 -0
  88. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/key-register.ts +1 -1
  89. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-enquiry.ts +105 -0
  90. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-batch.ts +48 -0
  91. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-request.ts +37 -0
  92. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match.ts +78 -0
  93. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-detail.ts +63 -0
  94. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-search.ts +93 -0
  95. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-detail.ts +70 -0
  96. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-listed.ts +67 -0
  97. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-request.ts +37 -0
  98. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-search.ts +80 -0
  99. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/supplier.ts +120 -0
  100. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/team-availability.ts +42 -0
  101. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-create.ts +36 -0
  102. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-detail.ts +70 -0
  103. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-search.ts +74 -0
  104. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-update.ts +48 -0
  105. package/payload/server/server.js +89 -2
  106. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback-list.ts +0 -54
  107. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-list.ts +0 -52
  108. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/properties-list.ts +0 -52
  109. package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewings-list.ts +0 -62
@@ -0,0 +1,93 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopPersonSummary {
8
+ id?: number;
9
+ firstName?: string;
10
+ lastName?: string;
11
+ email?: string;
12
+ phone?: string;
13
+ type?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ type PeopleRole = "buyers" | "sellers" | "renters" | "landlords";
18
+
19
+ export async function peopleSearch(params: {
20
+ accountId: string;
21
+ role?: PeopleRole;
22
+ searchTerm?: string;
23
+ // Role-specific filters (buyers)
24
+ maxPrice?: number;
25
+ minBeds?: number;
26
+ searchAreas?: string;
27
+ propertyTypes?: string;
28
+ // Role-specific filters (sellers/landlords)
29
+ startDate?: string;
30
+ endDate?: string;
31
+ minPrice?: number;
32
+ // Role-specific filters (renters)
33
+ maxRent?: number;
34
+ teamName?: string;
35
+ limit?: number;
36
+ }): Promise<string> {
37
+ const {
38
+ accountId, role, searchTerm,
39
+ maxPrice, minBeds, searchAreas, propertyTypes,
40
+ startDate, endDate, minPrice, maxRent,
41
+ teamName, limit,
42
+ } = params;
43
+
44
+ const result = await aggregateAcrossTeams<LoopPersonSummary>(
45
+ accountId,
46
+ "people",
47
+ "loop-people-search",
48
+ async (apiKey, team) => {
49
+ const qp: string[] = [];
50
+ if (searchTerm) qp.push(`SearchTerm=${encodeURIComponent(searchTerm)}`);
51
+
52
+ if (role === "buyers") {
53
+ if (maxPrice != null) qp.push(`MaxPrice=${maxPrice}`);
54
+ if (minBeds != null) qp.push(`MinBeds=${minBeds}`);
55
+ if (searchAreas) qp.push(`SearchAreas=${encodeURIComponent(searchAreas)}`);
56
+ if (propertyTypes) qp.push(`PropertyTypes=${encodeURIComponent(propertyTypes)}`);
57
+ } else if (role === "sellers" || role === "landlords") {
58
+ if (startDate) qp.push(`StartDate=${encodeURIComponent(startDate)}`);
59
+ if (endDate) qp.push(`EndDate=${encodeURIComponent(endDate)}`);
60
+ if (minPrice != null) qp.push(`MinPrice=${minPrice}`);
61
+ if (maxPrice != null) qp.push(`MaxPrice=${maxPrice}`);
62
+ } else if (role === "renters") {
63
+ if (maxRent != null) qp.push(`MaxRent=${maxRent}`);
64
+ if (minBeds != null) qp.push(`MinBeds=${minBeds}`);
65
+ if (searchAreas) qp.push(`SearchAreas=${encodeURIComponent(searchAreas)}`);
66
+ if (propertyTypes) qp.push(`PropertyTypes=${encodeURIComponent(propertyTypes)}`);
67
+ } else {
68
+ // Generic /people endpoint
69
+ if (startDate) qp.push(`StartDate=${encodeURIComponent(startDate)}`);
70
+ if (endDate) qp.push(`EndDate=${encodeURIComponent(endDate)}`);
71
+ }
72
+
73
+ const query = qp.length > 0 ? `?${qp.join("&")}` : "";
74
+ const basePath = role ? `/people/${role}` : "/people";
75
+ const path = `${basePath}${query}`;
76
+ const data = await loopGet<LoopPersonSummary[]>(apiKey, path, "loop-people-search", team);
77
+ return Array.isArray(data) ? data : [];
78
+ },
79
+ { teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
80
+ );
81
+
82
+ const entityName = role ?? "people";
83
+ return formatAggregationResult(
84
+ 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
+ },
91
+ entityName
92
+ );
93
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopPropertyDetail {
8
+ id?: number;
9
+ address?: string;
10
+ price?: number;
11
+ status?: string;
12
+ type?: string;
13
+ bedrooms?: number;
14
+ bathrooms?: number;
15
+ description?: string;
16
+ receptions?: number;
17
+ tenure?: string;
18
+ councilTax?: string;
19
+ epcRating?: string;
20
+ floorArea?: number;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ type Department = "sales" | "lettings";
25
+
26
+ export async function propertyDetail(params: {
27
+ accountId: string;
28
+ propertyId: number;
29
+ department: Department;
30
+ previewHash?: number;
31
+ teamName?: string;
32
+ }): Promise<string> {
33
+ const { accountId, propertyId, department, previewHash, teamName } = params;
34
+
35
+ const result = await aggregateAcrossTeams<LoopPropertyDetail>(
36
+ accountId,
37
+ "properties",
38
+ "loop-property-detail",
39
+ async (apiKey, team) => {
40
+ const path = previewHash
41
+ ? `/property/residential/${department}/${propertyId}/preview/${previewHash}`
42
+ : `/property/residential/${department}/${propertyId}`;
43
+ const data = await loopGet<LoopPropertyDetail>(apiKey, path, "loop-property-detail", team);
44
+ if (data && typeof data === "object" && !Array.isArray(data)) {
45
+ return [data];
46
+ }
47
+ return [];
48
+ },
49
+ { teamName }
50
+ );
51
+
52
+ return formatAggregationResult(
53
+ result,
54
+ (p) => {
55
+ const lines = [`**${p.address ?? "Unknown address"}**`];
56
+ if (p.price) lines.push(`Price: £${p.price.toLocaleString("en-GB")}`);
57
+ 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}`);
66
+ return lines.join("\n");
67
+ },
68
+ "property details"
69
+ );
70
+ }
@@ -0,0 +1,67 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopPropertyListing {
8
+ id?: number;
9
+ address?: string;
10
+ price?: number;
11
+ status?: string;
12
+ type?: string;
13
+ bedrooms?: number;
14
+ listingUrl?: string;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ type Department = "sales" | "lettings" | "both";
19
+ type Channel = "rightmove" | "onTheMarket" | "zoopla" | "website";
20
+
21
+ export async function propertyListed(params: {
22
+ accountId: string;
23
+ channel: Channel;
24
+ department?: Department;
25
+ includeSold?: boolean;
26
+ teamName?: string;
27
+ limit?: number;
28
+ }): Promise<string> {
29
+ const { accountId, channel, department = "both", includeSold = false, teamName, limit } = params;
30
+
31
+ const result = await aggregateAcrossTeams<LoopPropertyListing>(
32
+ accountId,
33
+ "properties",
34
+ "loop-property-listed",
35
+ async (apiKey, team) => {
36
+ const all: LoopPropertyListing[] = [];
37
+ const depts: string[] =
38
+ department === "both" ? ["sales", "lettings"] : [department];
39
+
40
+ for (const dept of depts) {
41
+ const path = `/property/residential/${dept}/listed/${channel}`;
42
+ const data = await loopGet<LoopPropertyListing[]>(apiKey, path, "loop-property-listed", team);
43
+ if (Array.isArray(data)) all.push(...data);
44
+ }
45
+
46
+ if (includeSold) {
47
+ const soldPath = `/property/residential/sold/${channel}`;
48
+ const soldData = await loopGet<LoopPropertyListing[]>(apiKey, soldPath, "loop-property-listed", team);
49
+ if (Array.isArray(soldData)) all.push(...soldData);
50
+ }
51
+
52
+ return all;
53
+ },
54
+ { teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
55
+ );
56
+
57
+ return formatAggregationResult(
58
+ result,
59
+ (p) => {
60
+ const price = p.price ? ` — £${p.price.toLocaleString("en-GB")}` : "";
61
+ const beds = p.bedrooms ? ` ${p.bedrooms}bed` : "";
62
+ const status = p.status ? ` [${p.status}]` : "";
63
+ return `- ${p.address ?? "Unknown address"}${price}${beds}${status}`;
64
+ },
65
+ `${channel} listings`
66
+ );
67
+ }
@@ -0,0 +1,37 @@
1
+ import { loopPost, withTeamKey } from "../lib/loop-api.js";
2
+
3
+ type Department = "sales" | "lettings";
4
+ type RequestAction = "viewing" | "call-back" | "information";
5
+
6
+ interface LoopBooleanResponse {
7
+ success?: boolean;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export async function propertyRequest(params: {
12
+ accountId: string;
13
+ teamName: string;
14
+ propertyId: number;
15
+ department: Department;
16
+ action: RequestAction;
17
+ name?: string;
18
+ email?: string;
19
+ phone?: string;
20
+ message?: string;
21
+ }): Promise<string> {
22
+ const { accountId, teamName, propertyId, department, action, ...body } = params;
23
+
24
+ const result = await withTeamKey<LoopBooleanResponse>(
25
+ accountId,
26
+ teamName,
27
+ "properties",
28
+ "loop-property-request",
29
+ async (apiKey) => {
30
+ const path = `/property/residential/${department}/${propertyId}/${action}`;
31
+ return loopPost<LoopBooleanResponse>(apiKey, path, body, "loop-property-request", teamName);
32
+ }
33
+ );
34
+
35
+ const actionLabel = action === "call-back" ? "callback" : action;
36
+ return `${actionLabel} request submitted for property ${propertyId} (${department}) via team "${teamName}".`;
37
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopPropertySummary {
8
+ id?: number;
9
+ address?: string;
10
+ price?: number;
11
+ status?: string;
12
+ type?: string;
13
+ bedrooms?: number;
14
+ bathrooms?: number;
15
+ description?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ type Department = "sales" | "lettings" | "both";
20
+
21
+ export async function propertySearch(params: {
22
+ accountId: string;
23
+ department?: Department;
24
+ searchTerm?: string;
25
+ minPrice?: number;
26
+ maxPrice?: number;
27
+ minBedrooms?: number;
28
+ maxBedrooms?: number;
29
+ propertyStatuses?: string;
30
+ propertyTypes?: string;
31
+ teamName?: string;
32
+ limit?: number;
33
+ }): Promise<string> {
34
+ const {
35
+ accountId, department = "both", searchTerm, minPrice, maxPrice,
36
+ minBedrooms, maxBedrooms, propertyStatuses, propertyTypes,
37
+ teamName, limit,
38
+ } = params;
39
+
40
+ const result = await aggregateAcrossTeams<LoopPropertySummary>(
41
+ accountId,
42
+ "properties",
43
+ "loop-property-search",
44
+ async (apiKey, team) => {
45
+ const depts: string[] =
46
+ department === "both" ? ["sales", "lettings"] : [department];
47
+ const all: LoopPropertySummary[] = [];
48
+
49
+ for (const dept of depts) {
50
+ const qp: string[] = [];
51
+ if (searchTerm) qp.push(`SearchTerm=${encodeURIComponent(searchTerm)}`);
52
+ if (minPrice != null) qp.push(`MinPrice=${minPrice}`);
53
+ if (maxPrice != null) qp.push(`MaxPrice=${maxPrice}`);
54
+ if (minBedrooms != null) qp.push(`MinBedrooms=${minBedrooms}`);
55
+ if (maxBedrooms != null) qp.push(`MaxBedrooms=${maxBedrooms}`);
56
+ if (propertyStatuses) qp.push(`PropertyStatuses=${encodeURIComponent(propertyStatuses)}`);
57
+ if (propertyTypes) qp.push(`PropertyTypes=${encodeURIComponent(propertyTypes)}`);
58
+
59
+ const query = qp.length > 0 ? `?${qp.join("&")}` : "";
60
+ const path = `/property/residential/${dept}${query}`;
61
+ const data = await loopGet<LoopPropertySummary[]>(apiKey, path, "loop-property-search", team);
62
+ if (Array.isArray(data)) all.push(...data);
63
+ }
64
+ return all;
65
+ },
66
+ { teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
67
+ );
68
+
69
+ return formatAggregationResult(
70
+ result,
71
+ (p) => {
72
+ const price = p.price ? ` — £${p.price.toLocaleString("en-GB")}` : "";
73
+ const beds = p.bedrooms ? ` ${p.bedrooms}bed` : "";
74
+ const status = p.status ? ` [${p.status}]` : "";
75
+ const propType = p.type ? ` (${p.type})` : "";
76
+ return `- ${p.address ?? "Unknown address"}${price}${beds}${propType}${status}`;
77
+ },
78
+ "properties"
79
+ );
80
+ }
@@ -0,0 +1,120 @@
1
+ import { loopGet, loopPost, loopPut, withTeamKey } from "../lib/loop-api.js";
2
+
3
+ type SupplierAction =
4
+ | "maintenance-jobs"
5
+ | "maintenance-complete"
6
+ | "maintenance-quotes"
7
+ | "maintenance-submit-quote"
8
+ | "board-jobs"
9
+ | "board-complete";
10
+
11
+ interface LoopSupplierResult {
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ interface LoopBooleanResponse {
16
+ success?: boolean;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ export async function supplier(params: {
21
+ accountId: string;
22
+ teamName: string;
23
+ action: SupplierAction;
24
+ code: string;
25
+ jobId?: number;
26
+ // For maintenance-submit-quote
27
+ quoteId?: number;
28
+ quoteData?: Record<string, unknown>;
29
+ // For board-complete
30
+ completionData?: Record<string, unknown>;
31
+ }): Promise<string> {
32
+ const { accountId, teamName, action, code } = params;
33
+
34
+ // jobId is required for all actions except maintenance-submit-quote
35
+ const jobId = params.jobId;
36
+ if (action !== "maintenance-submit-quote" && jobId == null) {
37
+ throw new Error(`jobId is required for action "${action}".`);
38
+ }
39
+
40
+ if (action === "maintenance-jobs") {
41
+ const result = await withTeamKey<LoopSupplierResult>(
42
+ accountId, teamName, "supplier", "loop-supplier",
43
+ async (apiKey) => {
44
+ return loopGet<LoopSupplierResult>(
45
+ apiKey, `/supplier/maintenance/${encodeURIComponent(code)}/${jobId}/job-list`,
46
+ "loop-supplier", teamName
47
+ );
48
+ }
49
+ );
50
+ return JSON.stringify(result, null, 2);
51
+ }
52
+
53
+ if (action === "maintenance-complete") {
54
+ await withTeamKey<LoopBooleanResponse>(
55
+ accountId, teamName, "supplier", "loop-supplier",
56
+ async (apiKey) => {
57
+ return loopPost<LoopBooleanResponse>(
58
+ apiKey, `/supplier/maintenance/${encodeURIComponent(code)}/${jobId}/complete`,
59
+ {}, "loop-supplier", teamName
60
+ );
61
+ }
62
+ );
63
+ return `Maintenance job ${jobId} marked complete via team "${teamName}".`;
64
+ }
65
+
66
+ if (action === "maintenance-quotes") {
67
+ const result = await withTeamKey<LoopSupplierResult>(
68
+ accountId, teamName, "supplier", "loop-supplier",
69
+ async (apiKey) => {
70
+ return loopGet<LoopSupplierResult>(
71
+ apiKey, `/supplier/maintenance/${encodeURIComponent(code)}/${jobId}/quote-list`,
72
+ "loop-supplier", teamName
73
+ );
74
+ }
75
+ );
76
+ return JSON.stringify(result, null, 2);
77
+ }
78
+
79
+ if (action === "maintenance-submit-quote") {
80
+ if (!params.quoteId) throw new Error("quoteId is required for maintenance-submit-quote.");
81
+ await withTeamKey<LoopBooleanResponse>(
82
+ accountId, teamName, "supplier", "loop-supplier",
83
+ async (apiKey) => {
84
+ return loopPut<LoopBooleanResponse>(
85
+ apiKey, `/supplier/maintenance/${encodeURIComponent(code)}/${params.quoteId}/quote-for-job`,
86
+ params.quoteData ?? {}, "loop-supplier", teamName
87
+ );
88
+ }
89
+ );
90
+ return `Quote ${params.quoteId} submitted via team "${teamName}".`;
91
+ }
92
+
93
+ if (action === "board-jobs") {
94
+ const result = await withTeamKey<LoopSupplierResult>(
95
+ accountId, teamName, "supplier", "loop-supplier",
96
+ async (apiKey) => {
97
+ return loopGet<LoopSupplierResult>(
98
+ apiKey, `/supplier/board-contractor/${encodeURIComponent(code)}/${jobId}/job-list`,
99
+ "loop-supplier", teamName
100
+ );
101
+ }
102
+ );
103
+ return JSON.stringify(result, null, 2);
104
+ }
105
+
106
+ if (action === "board-complete") {
107
+ await withTeamKey<LoopBooleanResponse>(
108
+ accountId, teamName, "supplier", "loop-supplier",
109
+ async (apiKey) => {
110
+ return loopPost<LoopBooleanResponse>(
111
+ apiKey, `/supplier/board-contractor/${encodeURIComponent(code)}/${jobId}/complete`,
112
+ params.completionData ?? {}, "loop-supplier", teamName
113
+ );
114
+ }
115
+ );
116
+ return `Board contractor job ${jobId} marked complete via team "${teamName}".`;
117
+ }
118
+
119
+ throw new Error(`Unknown supplier action: ${action}`);
120
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopTimeRange {
8
+ start?: string;
9
+ end?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ export async function teamAvailability(params: {
14
+ accountId: string;
15
+ agentId: string;
16
+ searchDate: string;
17
+ teamName?: string;
18
+ }): Promise<string> {
19
+ const { accountId, agentId, searchDate, teamName } = params;
20
+
21
+ const result = await aggregateAcrossTeams<LoopTimeRange>(
22
+ accountId,
23
+ "team",
24
+ "loop-team-availability",
25
+ async (apiKey, team) => {
26
+ const path = `/team/${encodeURIComponent(agentId)}/availability/${encodeURIComponent(searchDate)}`;
27
+ const data = await loopGet<LoopTimeRange[]>(apiKey, path, "loop-team-availability", team);
28
+ return Array.isArray(data) ? data : [];
29
+ },
30
+ { teamName }
31
+ );
32
+
33
+ return formatAggregationResult(
34
+ result,
35
+ (slot) => {
36
+ const start = slot.start ?? "?";
37
+ const end = slot.end ?? "?";
38
+ return `- ${start} — ${end}`;
39
+ },
40
+ `availability slots for ${searchDate}`
41
+ );
42
+ }
@@ -0,0 +1,36 @@
1
+ import { loopPost, withTeamKey } from "../lib/loop-api.js";
2
+
3
+ type Department = "sales" | "lettings";
4
+
5
+ interface LoopNumberResponse {
6
+ result?: number;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ export async function viewingCreate(params: {
11
+ accountId: string;
12
+ teamName: string;
13
+ department: Department;
14
+ propertyId: number;
15
+ date: string;
16
+ time: string;
17
+ attendeeName: string;
18
+ attendeeEmail?: string;
19
+ attendeePhone?: string;
20
+ }): Promise<string> {
21
+ const { accountId, teamName, department, ...body } = params;
22
+
23
+ const result = await withTeamKey<LoopNumberResponse>(
24
+ accountId,
25
+ teamName,
26
+ "viewings",
27
+ "loop-viewing-create",
28
+ async (apiKey) => {
29
+ const path = `/residential/${department}/viewings`;
30
+ return loopPost<LoopNumberResponse>(apiKey, path, body, "loop-viewing-create", teamName);
31
+ }
32
+ );
33
+
34
+ const viewingId = result.result ?? "unknown";
35
+ return `Viewing created (ID: ${viewingId}) for property ${params.propertyId} on ${params.date} at ${params.time} via team "${teamName}".`;
36
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopViewingDetail {
8
+ id?: number;
9
+ propertyAddress?: string;
10
+ date?: string;
11
+ time?: string;
12
+ status?: string;
13
+ 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;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ type Department = "sales" | "lettings";
26
+
27
+ export async function viewingDetail(params: {
28
+ accountId: string;
29
+ viewingId: number;
30
+ department: Department;
31
+ teamName?: string;
32
+ }): Promise<string> {
33
+ const { accountId, viewingId, department, teamName } = params;
34
+
35
+ const result = await aggregateAcrossTeams<LoopViewingDetail>(
36
+ accountId,
37
+ "viewings",
38
+ "loop-viewing-detail",
39
+ async (apiKey, team) => {
40
+ const path = `/residential/${department}/viewings/${viewingId}`;
41
+ const data = await loopGet<LoopViewingDetail>(apiKey, path, "loop-viewing-detail", team);
42
+ if (data && typeof data === "object" && !Array.isArray(data)) {
43
+ return [data];
44
+ }
45
+ return [];
46
+ },
47
+ { teamName }
48
+ );
49
+
50
+ return formatAggregationResult(
51
+ result,
52
+ (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}`);
56
+ if (v.status) lines.push(`Status: ${v.status}`);
57
+ 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}`);
66
+ return lines.join("\n");
67
+ },
68
+ "viewing details"
69
+ );
70
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopViewingSummary {
8
+ id?: number;
9
+ propertyAddress?: string;
10
+ date?: string;
11
+ time?: string;
12
+ status?: string;
13
+ type?: string;
14
+ attendeeName?: string;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ type Department = "sales" | "lettings" | "both";
19
+
20
+ export async function viewingSearch(params: {
21
+ accountId: string;
22
+ department?: Department;
23
+ searchTerm?: string;
24
+ appointmentStartDate?: string;
25
+ appointmentEndDate?: string;
26
+ status?: string;
27
+ teamName?: string;
28
+ limit?: number;
29
+ }): Promise<string> {
30
+ const {
31
+ accountId, department = "both", searchTerm,
32
+ appointmentStartDate, appointmentEndDate, status,
33
+ teamName, limit,
34
+ } = params;
35
+
36
+ const result = await aggregateAcrossTeams<LoopViewingSummary>(
37
+ accountId,
38
+ "viewings",
39
+ "loop-viewing-search",
40
+ async (apiKey, team) => {
41
+ const depts: string[] =
42
+ department === "both" ? ["sales", "lettings"] : [department];
43
+ const all: LoopViewingSummary[] = [];
44
+
45
+ for (const dept of depts) {
46
+ const qp: string[] = [];
47
+ if (searchTerm) qp.push(`SearchTerm=${encodeURIComponent(searchTerm)}`);
48
+ if (appointmentStartDate) qp.push(`AppointmentStartDate=${encodeURIComponent(appointmentStartDate)}`);
49
+ if (appointmentEndDate) qp.push(`AppointmentEndDate=${encodeURIComponent(appointmentEndDate)}`);
50
+ if (status) qp.push(`Status=${encodeURIComponent(status)}`);
51
+
52
+ const query = qp.length > 0 ? `?${qp.join("&")}` : "";
53
+ const path = `/residential/${dept}/viewings${query}`;
54
+ const data = await loopGet<LoopViewingSummary[]>(apiKey, path, "loop-viewing-search", team);
55
+ if (Array.isArray(data)) all.push(...data);
56
+ }
57
+ return all;
58
+ },
59
+ { teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
60
+ );
61
+
62
+ return formatAggregationResult(
63
+ result,
64
+ (v) => {
65
+ const addr = v.propertyAddress ?? "Unknown property";
66
+ const when = [v.date, v.time].filter(Boolean).join(" ");
67
+ const viewStatus = v.status ? ` [${v.status}]` : "";
68
+ const attendee = v.attendeeName ? ` — ${v.attendeeName}` : "";
69
+ const viewType = v.type ? ` (${v.type})` : "";
70
+ return `- ${addr} — ${when || "No date"}${viewType}${attendee}${viewStatus}`;
71
+ },
72
+ "viewings"
73
+ );
74
+ }