@rubytech/create-maxy-code 0.1.45 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +8 -16
- package/package.json +1 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts +0 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +2 -5
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +2 -8
- package/payload/platform/lib/graph-mcp/dist/index.js +1 -4
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/index.ts +1 -4
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +7 -6
- package/payload/platform/lib/graph-search/dist/index.d.ts +5 -5
- package/payload/platform/lib/graph-search/dist/index.js +4 -4
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +6 -7
- package/payload/platform/lib/graph-search/src/index.ts +5 -5
- package/payload/platform/lib/graph-trash/dist/index.d.ts +8 -12
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +8 -12
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +8 -12
- package/payload/platform/lib/graph-write/dist/index.d.ts +8 -9
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +9 -10
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +9 -10
- package/payload/platform/lib/mcp-eager/dist/index.d.ts +4 -4
- package/payload/platform/lib/mcp-eager/dist/index.js +4 -4
- package/payload/platform/lib/mcp-eager/src/index.ts +4 -4
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +56 -38
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +42 -40
- package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +7 -9
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +10 -12
- package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +3 -3
- package/payload/platform/plugins/docs/references/deployment.md +0 -14
- package/payload/platform/plugins/docs/references/platform.md +2 -2
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +5 -5
- package/payload/platform/plugins/loop/PLUGIN.md +67 -12
- package/payload/platform/plugins/loop/mcp/dist/index.js +42 -16
- package/payload/platform/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts +12 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/platform/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/platform/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/platform/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/platform/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/platform/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/platform/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +7 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-not-applicable.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +4 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js +8 -9
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js.map +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.d.ts +2 -2
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.js +2 -2
- package/payload/platform/scripts/admin-persist-audit.ts +4 -10
- package/payload/platform/scripts/conversation-id-allowlist.txt +0 -1
- package/payload/platform/scripts/lib/resolve-account-dir.sh +0 -2
- package/payload/platform/scripts/resume-tunnel.sh +2 -9
- package/payload/platform/scripts/seed-neo4j.sh +26 -104
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +53 -7
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -5
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +13 -17
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +10 -2
- package/payload/platform/templates/agents/admin/SOUL.md +23 -1
- package/payload/premium-plugins/real-agent/agents/buyer-enquiry/SOUL.md +10 -0
- package/payload/premium-plugins/real-agent/agents/compliance.md +1 -1
- package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
- package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +67 -12
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +42 -16
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.d.ts +12 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/server/public/assets/admin-SyUHjvNs.js +216 -0
- package/payload/server/public/assets/{data-BxpOR0eM.js → data-DwQ7ZEyy.js} +1 -1
- package/payload/server/public/assets/{graph-CrmxRSeb.js → graph-CwZkD6GX.js} +1 -1
- package/payload/server/public/assets/graph-labels-CVkmOHhw.js +1 -0
- package/payload/server/public/assets/page-CQ7ELqGs.js +1 -0
- package/payload/server/public/assets/page-SOCqJGlV.js +50 -0
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +9 -46
- package/payload/platform/plugins/docs/references/migration-guide.md +0 -90
- package/payload/platform/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/platform/scripts/component-knowledgedoc-backfill.ts +0 -214
- package/payload/platform/scripts/migrate-import.sh +0 -437
- package/payload/platform/scripts/taskmaster-export.sh +0 -388
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/server/public/assets/admin-DVZcLuYu.js +0 -216
- package/payload/server/public/assets/graph-labels-C8A6BmtP.js +0 -1
- package/payload/server/public/assets/page-D5wAeFU0.js +0 -50
- package/payload/server/public/assets/page-DR5NU6S0.js +0 -1
|
@@ -58,18 +58,19 @@ const ENDPOINT_MAP: EndpointMapping[] = [
|
|
|
58
58
|
{ method: 'post', path: '/marketing/rentals/matching/{id}/information', tool: 'loop-marketing-match-request', source: 'tools/marketing-match-request.ts', fragment: '/marketing/rentals' },
|
|
59
59
|
{ method: 'post', path: '/marketing/matching/{id}/callback', tool: 'loop-marketing-match-request', source: 'tools/marketing-match-request.ts', fragment: '/matching/' },
|
|
60
60
|
{ method: 'post', path: '/marketing/rentals/matching/{id}/callback', tool: 'loop-marketing-match-request', source: 'tools/marketing-match-request.ts', fragment: '/marketing/rentals' },
|
|
61
|
-
// Marketing —
|
|
61
|
+
// Marketing — seller enquiry (1)
|
|
62
62
|
{ method: 'post', path: '/marketing/enquiries/seller', tool: 'loop-marketing-enquiry', source: 'tools/marketing-enquiry.ts', fragment: '/marketing/enquiries/seller' },
|
|
63
|
-
|
|
64
|
-
{ method: 'put', path: '/marketing/enquiries/auto-responder/{id}/
|
|
65
|
-
{ method: 'put', path: '/marketing/enquiries/auto-responder/{id}/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{ method: 'post', path: '/property/residential/sales/{id}/
|
|
69
|
-
{ method: 'post', path: '/property/residential/sales/{id}/
|
|
70
|
-
{ method: 'post', path: '/property/residential/
|
|
71
|
-
{ method: 'post', path: '/property/residential/lettings/{id}/
|
|
72
|
-
{ method: 'post', path: '/property/residential/lettings/{id}/
|
|
63
|
+
// Marketing — auto-responder (3) — Task 103 split into loop-auto-responder
|
|
64
|
+
{ method: 'put', path: '/marketing/enquiries/auto-responder/{id}/answers/{key}', tool: 'loop-auto-responder', source: 'tools/auto-responder.ts', fragment: '/marketing/enquiries/auto-responder/' },
|
|
65
|
+
{ method: 'put', path: '/marketing/enquiries/auto-responder/{id}/details/{key}', tool: 'loop-auto-responder', source: 'tools/auto-responder.ts', fragment: '/marketing/enquiries/auto-responder/' },
|
|
66
|
+
{ method: 'put', path: '/marketing/enquiries/auto-responder/{id}/refer/{key}', tool: 'loop-auto-responder', source: 'tools/auto-responder.ts', fragment: '/marketing/enquiries/auto-responder/' },
|
|
67
|
+
// Property intent (6) — Task 103 split loop-property-request into three intent-clear tools
|
|
68
|
+
{ method: 'post', path: '/property/residential/sales/{id}/viewing', tool: 'loop-property-viewing', source: 'tools/property-intent.ts', fragment: '/property/residential/' },
|
|
69
|
+
{ method: 'post', path: '/property/residential/sales/{id}/call-back', tool: 'loop-property-callback', source: 'tools/property-intent.ts', fragment: '/property/residential/' },
|
|
70
|
+
{ method: 'post', path: '/property/residential/sales/{id}/information', tool: 'loop-property-information', source: 'tools/property-intent.ts', fragment: '/property/residential/' },
|
|
71
|
+
{ method: 'post', path: '/property/residential/lettings/{id}/viewing', tool: 'loop-property-viewing', source: 'tools/property-intent.ts', fragment: '/property/residential/' },
|
|
72
|
+
{ method: 'post', path: '/property/residential/lettings/{id}/call-back', tool: 'loop-property-callback', source: 'tools/property-intent.ts', fragment: '/property/residential/' },
|
|
73
|
+
{ method: 'post', path: '/property/residential/lettings/{id}/information', tool: 'loop-property-information', source: 'tools/property-intent.ts', fragment: '/property/residential/' },
|
|
73
74
|
// Supplier (3)
|
|
74
75
|
{ method: 'post', path: '/supplier/maintenance/{code}/{jobid}/complete', tool: 'loop-supplier', source: 'tools/supplier.ts', fragment: '/supplier/maintenance/' },
|
|
75
76
|
{ method: 'put', path: '/supplier/maintenance/{code}/{quoteid}/quote-for-job', tool: 'loop-supplier', source: 'tools/supplier.ts', fragment: '/quote-for-job' },
|
|
@@ -10,7 +10,8 @@ import { keyList } from "./tools/key-list.js";
|
|
|
10
10
|
import { propertySearch } from "./tools/property-search.js";
|
|
11
11
|
import { propertyDetail } from "./tools/property-detail.js";
|
|
12
12
|
import { propertyListed } from "./tools/property-listed.js";
|
|
13
|
-
import {
|
|
13
|
+
import { propertySold } from "./tools/property-sold.js";
|
|
14
|
+
import { propertyViewing, propertyCallback, propertyInformation } from "./tools/property-intent.js";
|
|
14
15
|
import { peopleSearch } from "./tools/people-search.js";
|
|
15
16
|
import { peopleDetail } from "./tools/people-detail.js";
|
|
16
17
|
import { viewingSearch } from "./tools/viewing-search.js";
|
|
@@ -24,6 +25,7 @@ import { marketingMatchDetail } from "./tools/marketing-match.js";
|
|
|
24
25
|
import { marketingMatchBatch } from "./tools/marketing-match-batch.js";
|
|
25
26
|
import { marketingMatchRequest } from "./tools/marketing-match-request.js";
|
|
26
27
|
import { marketingEnquiry } from "./tools/marketing-enquiry.js";
|
|
28
|
+
import { autoResponder } from "./tools/auto-responder.js";
|
|
27
29
|
import { customerPreferences } from "./tools/customer-preferences.js";
|
|
28
30
|
import { supplier } from "./tools/supplier.js";
|
|
29
31
|
import { closeDriver } from "./lib/neo4j.js";
|
|
@@ -194,11 +196,10 @@ server.tool(
|
|
|
194
196
|
|
|
195
197
|
server.tool(
|
|
196
198
|
"loop-property-listed",
|
|
197
|
-
"Get properties listed on a specific channel (Rightmove, Zoopla, OnTheMarket, website).
|
|
199
|
+
"Get properties currently listed on a specific channel (Rightmove, Zoopla, OnTheMarket, website). For exchanged/completed properties use loop-property-sold.",
|
|
198
200
|
{
|
|
199
201
|
channel: z.enum(["rightmove", "onTheMarket", "zoopla", "website"]).describe("Listing channel"),
|
|
200
202
|
department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
|
|
201
|
-
includeSold: z.boolean().optional().describe("Include sold gallery (default: false)"),
|
|
202
203
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
203
204
|
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
204
205
|
},
|
|
@@ -206,19 +207,63 @@ server.tool(
|
|
|
206
207
|
);
|
|
207
208
|
|
|
208
209
|
server.tool(
|
|
209
|
-
"loop-property-
|
|
210
|
-
"
|
|
210
|
+
"loop-property-sold",
|
|
211
|
+
"Get exchanged or completed properties on a specific channel. Powers canvassing from recent sales and sold-gallery rendering.",
|
|
212
|
+
{
|
|
213
|
+
channel: z.enum(["rightmove", "onTheMarket", "zoopla", "website"]).describe("Listing channel"),
|
|
214
|
+
teamName: z.string().optional().describe("Query a specific team only"),
|
|
215
|
+
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
216
|
+
},
|
|
217
|
+
toolHandler(async (p) => propertySold(p as Parameters<typeof propertySold>[0]))
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Property-level intent tools (Task 103 split — replaces loop-property-request).
|
|
221
|
+
// Each intent is a distinct operator action so the agent's tool surface
|
|
222
|
+
// preserves intent fidelity.
|
|
223
|
+
|
|
224
|
+
server.tool(
|
|
225
|
+
"loop-property-viewing",
|
|
226
|
+
"Request a viewing for a property. Use when the operator says 'book a viewing', 'arrange a viewing', or 'ask to see the property'.",
|
|
227
|
+
{
|
|
228
|
+
teamName: z.string().min(1).describe("Team to submit the request through"),
|
|
229
|
+
propertyId: z.number().int().describe("Property ID"),
|
|
230
|
+
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
231
|
+
name: z.string().optional().describe("Requester name"),
|
|
232
|
+
email: z.string().optional().describe("Requester email"),
|
|
233
|
+
phone: z.string().optional().describe("Requester phone"),
|
|
234
|
+
message: z.string().optional().describe("Additional message"),
|
|
235
|
+
},
|
|
236
|
+
toolHandler(async (p) => propertyViewing(p as Parameters<typeof propertyViewing>[0]))
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
server.tool(
|
|
240
|
+
"loop-property-callback",
|
|
241
|
+
"Request a callback about a property. Use when the operator says 'ask them to call', 'request a callback', or 'have someone ring back'.",
|
|
242
|
+
{
|
|
243
|
+
teamName: z.string().min(1).describe("Team to submit the request through"),
|
|
244
|
+
propertyId: z.number().int().describe("Property ID"),
|
|
245
|
+
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
246
|
+
name: z.string().optional().describe("Requester name"),
|
|
247
|
+
email: z.string().optional().describe("Requester email"),
|
|
248
|
+
phone: z.string().optional().describe("Requester phone"),
|
|
249
|
+
message: z.string().optional().describe("Additional message"),
|
|
250
|
+
},
|
|
251
|
+
toolHandler(async (p) => propertyCallback(p as Parameters<typeof propertyCallback>[0]))
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
server.tool(
|
|
255
|
+
"loop-property-information",
|
|
256
|
+
"Request more information about a property. Use when the operator says 'ask for details', 'request the brochure', or 'find out more'.",
|
|
211
257
|
{
|
|
212
258
|
teamName: z.string().min(1).describe("Team to submit the request through"),
|
|
213
259
|
propertyId: z.number().int().describe("Property ID"),
|
|
214
260
|
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
215
|
-
action: z.enum(["viewing", "call-back", "information"]).describe("Request type"),
|
|
216
261
|
name: z.string().optional().describe("Requester name"),
|
|
217
262
|
email: z.string().optional().describe("Requester email"),
|
|
218
263
|
phone: z.string().optional().describe("Requester phone"),
|
|
219
264
|
message: z.string().optional().describe("Additional message"),
|
|
220
265
|
},
|
|
221
|
-
toolHandler(async (p) =>
|
|
266
|
+
toolHandler(async (p) => propertyInformation(p as Parameters<typeof propertyInformation>[0]))
|
|
222
267
|
);
|
|
223
268
|
|
|
224
269
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -377,22 +422,28 @@ server.tool(
|
|
|
377
422
|
|
|
378
423
|
server.tool(
|
|
379
424
|
"loop-marketing-enquiry",
|
|
380
|
-
"
|
|
425
|
+
"Submit a seller-side marketing enquiry to Loop. For inbound applicant auto-responder workflows use loop-auto-responder.",
|
|
381
426
|
{
|
|
382
427
|
teamName: z.string().min(1).describe("Team to submit through"),
|
|
383
|
-
|
|
384
|
-
"seller-enquiry", "autoresponder-get", "autoresponder-answers",
|
|
385
|
-
"autoresponder-details", "autoresponder-refer",
|
|
386
|
-
]).describe("Enquiry action"),
|
|
387
|
-
sellerEnquiryData: z.record(z.string(), z.unknown()).optional().describe("Seller enquiry request body (for seller-enquiry)"),
|
|
388
|
-
autoResponderId: z.number().int().optional().describe("Auto-responder enquiry ID"),
|
|
389
|
-
autoResponderKey: z.string().optional().describe("Auto-responder key (UUID)"),
|
|
390
|
-
answers: z.array(z.unknown()).optional().describe("Auto-responder answers array"),
|
|
391
|
-
details: z.record(z.string(), z.unknown()).optional().describe("Auto-responder details object"),
|
|
428
|
+
sellerEnquiryData: z.record(z.string(), z.unknown()).optional().describe("Seller enquiry request body"),
|
|
392
429
|
},
|
|
393
430
|
toolHandler(async (p) => marketingEnquiry(p as Parameters<typeof marketingEnquiry>[0]))
|
|
394
431
|
);
|
|
395
432
|
|
|
433
|
+
server.tool(
|
|
434
|
+
"loop-auto-responder",
|
|
435
|
+
"Read or progress an inbound enquiry through Loop's auto-responder (the surface behind Loop's 40% enquiry-efficiency claim). action=get reads questions+details; answers submits applicant replies; details updates the enquiry record; refer hands it to another team.",
|
|
436
|
+
{
|
|
437
|
+
teamName: z.string().min(1).describe("Team for this operation"),
|
|
438
|
+
action: z.enum(["get", "answers", "details", "refer"]).describe("Auto-responder action"),
|
|
439
|
+
autoResponderId: z.number().int().describe("Auto-responder enquiry ID"),
|
|
440
|
+
autoResponderKey: z.string().describe("Auto-responder key (UUID issued at enquiry creation)"),
|
|
441
|
+
answers: z.array(z.unknown()).optional().describe("Applicant answers (required for action=answers)"),
|
|
442
|
+
details: z.record(z.string(), z.unknown()).optional().describe("Enquiry details (for action=details)"),
|
|
443
|
+
},
|
|
444
|
+
toolHandler(async (p) => autoResponder(p as Parameters<typeof autoResponder>[0]))
|
|
445
|
+
);
|
|
446
|
+
|
|
396
447
|
// ─────────────────────────────────────────────────────────────────
|
|
397
448
|
// Customer Tools (1) — permission: customer
|
|
398
449
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { loopGet, loopPut, withTeamKey } from "../lib/loop-api.js";
|
|
2
|
+
|
|
3
|
+
type AutoResponderAction = "get" | "answers" | "details" | "refer";
|
|
4
|
+
|
|
5
|
+
// Loop API V2: enquiry auto-responder endpoints (Loop's stated 40% efficiency lever)
|
|
6
|
+
// GET /marketing/enquiries/auto-responder/{id}/{key} — read questions + details
|
|
7
|
+
// PUT /marketing/enquiries/auto-responder/{id}/answers/{key} — submit applicant answers
|
|
8
|
+
// PUT /marketing/enquiries/auto-responder/{id}/details/{key} — update enquiry details
|
|
9
|
+
// PUT /marketing/enquiries/auto-responder/{id}/refer/{key} — refer enquiry to another team
|
|
10
|
+
// `id` is the enquiry id, `key` is the UUID handed out at enquiry creation.
|
|
11
|
+
|
|
12
|
+
interface LoopBooleanResponse {
|
|
13
|
+
success?: boolean;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface LoopAutoResponderDetails {
|
|
18
|
+
id?: number;
|
|
19
|
+
questions?: unknown[];
|
|
20
|
+
enquiryDetails?: unknown;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function autoResponder(params: {
|
|
25
|
+
accountId: string;
|
|
26
|
+
teamName: string;
|
|
27
|
+
action: AutoResponderAction;
|
|
28
|
+
autoResponderId: number;
|
|
29
|
+
autoResponderKey: string;
|
|
30
|
+
answers?: unknown[];
|
|
31
|
+
details?: Record<string, unknown>;
|
|
32
|
+
}): Promise<string> {
|
|
33
|
+
const { accountId, teamName, action, autoResponderId: id, autoResponderKey: key } = params;
|
|
34
|
+
|
|
35
|
+
if (action === "get") {
|
|
36
|
+
const result = await withTeamKey<LoopAutoResponderDetails>(
|
|
37
|
+
accountId, teamName, "marketing", "loop-auto-responder",
|
|
38
|
+
async (apiKey) => loopGet<LoopAutoResponderDetails>(
|
|
39
|
+
apiKey, `/marketing/enquiries/auto-responder/${id}/${key}`,
|
|
40
|
+
"loop-auto-responder", teamName
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
return JSON.stringify(result, null, 2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (action === "answers") {
|
|
47
|
+
await withTeamKey<LoopBooleanResponse>(
|
|
48
|
+
accountId, teamName, "marketing", "loop-auto-responder",
|
|
49
|
+
async (apiKey) => loopPut<LoopBooleanResponse>(
|
|
50
|
+
apiKey, `/marketing/enquiries/auto-responder/${id}/answers/${key}`,
|
|
51
|
+
params.answers ?? [], "loop-auto-responder", teamName
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
return `Auto-responder answers submitted for enquiry ${id} via team "${teamName}".`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (action === "details") {
|
|
58
|
+
await withTeamKey<LoopBooleanResponse>(
|
|
59
|
+
accountId, teamName, "marketing", "loop-auto-responder",
|
|
60
|
+
async (apiKey) => loopPut<LoopBooleanResponse>(
|
|
61
|
+
apiKey, `/marketing/enquiries/auto-responder/${id}/details/${key}`,
|
|
62
|
+
params.details ?? {}, "loop-auto-responder", teamName
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
return `Auto-responder details updated for enquiry ${id} via team "${teamName}".`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (action === "refer") {
|
|
69
|
+
await withTeamKey<LoopBooleanResponse>(
|
|
70
|
+
accountId, teamName, "marketing", "loop-auto-responder",
|
|
71
|
+
async (apiKey) => loopPut<LoopBooleanResponse>(
|
|
72
|
+
apiKey, `/marketing/enquiries/auto-responder/${id}/refer/${key}`,
|
|
73
|
+
{}, "loop-auto-responder", teamName
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
return `Enquiry ${id} referred via team "${teamName}".`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(`Unknown auto-responder action: ${action}`);
|
|
80
|
+
}
|
|
@@ -1,113 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loopPost, withTeamKey } from "../lib/loop-api.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Loop API V2: POST /marketing/enquiries/seller
|
|
4
|
+
// Submit a seller-side marketing enquiry. Auto-responder endpoints used to
|
|
5
|
+
// live in this tool but were split into `loop-auto-responder` for intent
|
|
6
|
+
// fidelity (Task 103).
|
|
4
7
|
|
|
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
|
-
// ────────────────────────────────────────────────────────────────
|
|
13
8
|
interface LoopBooleanResponse {
|
|
14
9
|
success?: boolean;
|
|
15
10
|
[key: string]: unknown;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
interface LoopAutoResponderDetails {
|
|
19
|
-
id?: number;
|
|
20
|
-
questions?: unknown[];
|
|
21
|
-
enquiryDetails?: unknown;
|
|
22
|
-
[key: string]: unknown;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
13
|
export async function marketingEnquiry(params: {
|
|
26
14
|
accountId: string;
|
|
27
15
|
teamName: string;
|
|
28
|
-
action: EnquiryAction;
|
|
29
|
-
// For seller-enquiry
|
|
30
16
|
sellerEnquiryData?: Record<string, unknown>;
|
|
31
|
-
// For autoresponder actions
|
|
32
|
-
autoResponderId?: number;
|
|
33
|
-
autoResponderKey?: string;
|
|
34
|
-
// For autoresponder-answers
|
|
35
|
-
answers?: unknown[];
|
|
36
|
-
// For autoresponder-details
|
|
37
|
-
details?: Record<string, unknown>;
|
|
38
17
|
}): Promise<string> {
|
|
39
|
-
const { accountId, teamName,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
return `Seller enquiry submitted via team "${teamName}".`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!params.autoResponderId || !params.autoResponderKey) {
|
|
55
|
-
throw new Error("autoResponderId and autoResponderKey are required for autoresponder actions.");
|
|
56
|
-
}
|
|
57
|
-
const id = params.autoResponderId;
|
|
58
|
-
const key = params.autoResponderKey;
|
|
59
|
-
|
|
60
|
-
if (action === "autoresponder-get") {
|
|
61
|
-
const result = await withTeamKey<LoopAutoResponderDetails>(
|
|
62
|
-
accountId, teamName, "marketing", "loop-marketing-enquiry",
|
|
63
|
-
async (apiKey) => {
|
|
64
|
-
return loopGet<LoopAutoResponderDetails>(
|
|
65
|
-
apiKey, `/marketing/enquiries/auto-responder/${id}/${key}`,
|
|
66
|
-
"loop-marketing-enquiry", teamName
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
return JSON.stringify(result, null, 2);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (action === "autoresponder-answers") {
|
|
74
|
-
await withTeamKey<LoopBooleanResponse>(
|
|
75
|
-
accountId, teamName, "marketing", "loop-marketing-enquiry",
|
|
76
|
-
async (apiKey) => {
|
|
77
|
-
return loopPut<LoopBooleanResponse>(
|
|
78
|
-
apiKey, `/marketing/enquiries/auto-responder/${id}/answers/${key}`,
|
|
79
|
-
params.answers ?? [], "loop-marketing-enquiry", teamName
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
);
|
|
83
|
-
return `Auto-responder answers submitted for enquiry ${id} via team "${teamName}".`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (action === "autoresponder-details") {
|
|
87
|
-
await withTeamKey<LoopBooleanResponse>(
|
|
88
|
-
accountId, teamName, "marketing", "loop-marketing-enquiry",
|
|
89
|
-
async (apiKey) => {
|
|
90
|
-
return loopPut<LoopBooleanResponse>(
|
|
91
|
-
apiKey, `/marketing/enquiries/auto-responder/${id}/details/${key}`,
|
|
92
|
-
params.details ?? {}, "loop-marketing-enquiry", teamName
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
);
|
|
96
|
-
return `Auto-responder details updated for enquiry ${id} via team "${teamName}".`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (action === "autoresponder-refer") {
|
|
100
|
-
await withTeamKey<LoopBooleanResponse>(
|
|
101
|
-
accountId, teamName, "marketing", "loop-marketing-enquiry",
|
|
102
|
-
async (apiKey) => {
|
|
103
|
-
return loopPut<LoopBooleanResponse>(
|
|
104
|
-
apiKey, `/marketing/enquiries/auto-responder/${id}/refer/${key}`,
|
|
105
|
-
{}, "loop-marketing-enquiry", teamName
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
);
|
|
109
|
-
return `Enquiry ${id} referred via team "${teamName}".`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
throw new Error(`Unknown action: ${action}`);
|
|
18
|
+
const { accountId, teamName, sellerEnquiryData } = params;
|
|
19
|
+
await withTeamKey<LoopBooleanResponse>(
|
|
20
|
+
accountId, teamName, "marketing", "loop-marketing-enquiry",
|
|
21
|
+
async (apiKey) => loopPost<LoopBooleanResponse>(
|
|
22
|
+
apiKey, "/marketing/enquiries/seller",
|
|
23
|
+
sellerEnquiryData ?? {}, "loop-marketing-enquiry", teamName
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
return `Seller enquiry submitted via team "${teamName}".`;
|
|
113
27
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { loopPost, withTeamKey } from "../lib/loop-api.js";
|
|
2
|
+
|
|
3
|
+
type Department = "sales" | "lettings";
|
|
4
|
+
|
|
5
|
+
// Loop API V2: distinct operator-intent endpoints (post-Task 103 split).
|
|
6
|
+
// POST /property/residential/{dept}/{id}/viewing — request a viewing
|
|
7
|
+
// POST /property/residential/{dept}/{id}/call-back — request a callback
|
|
8
|
+
// POST /property/residential/{dept}/{id}/information — request more information
|
|
9
|
+
// Each endpoint takes the same {name?, email?, phone?, message?} body but is
|
|
10
|
+
// surfaced as a separate tool so operator intent is preserved on the agent's
|
|
11
|
+
// tool-call surface (replaces the prior bundled loop-property-request).
|
|
12
|
+
|
|
13
|
+
interface LoopBooleanResponse {
|
|
14
|
+
success?: boolean;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function submitIntent(
|
|
19
|
+
toolName: string,
|
|
20
|
+
action: "viewing" | "call-back" | "information",
|
|
21
|
+
params: {
|
|
22
|
+
accountId: string;
|
|
23
|
+
teamName: string;
|
|
24
|
+
propertyId: number;
|
|
25
|
+
department: Department;
|
|
26
|
+
name?: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
phone?: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
},
|
|
31
|
+
successLabel: string
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const { accountId, teamName, propertyId, department, ...body } = params;
|
|
34
|
+
await withTeamKey<LoopBooleanResponse>(
|
|
35
|
+
accountId,
|
|
36
|
+
teamName,
|
|
37
|
+
"properties",
|
|
38
|
+
toolName,
|
|
39
|
+
async (apiKey) => {
|
|
40
|
+
const path = `/property/residential/${department}/${propertyId}/${action}`;
|
|
41
|
+
return loopPost<LoopBooleanResponse>(apiKey, path, body, toolName, teamName);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
return `${successLabel} submitted for property ${propertyId} (${department}) via team "${teamName}".`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function propertyViewing(params: {
|
|
48
|
+
accountId: string;
|
|
49
|
+
teamName: string;
|
|
50
|
+
propertyId: number;
|
|
51
|
+
department: Department;
|
|
52
|
+
name?: string;
|
|
53
|
+
email?: string;
|
|
54
|
+
phone?: string;
|
|
55
|
+
message?: string;
|
|
56
|
+
}): Promise<string> {
|
|
57
|
+
return submitIntent("loop-property-viewing", "viewing", params, "Viewing request");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function propertyCallback(params: {
|
|
61
|
+
accountId: string;
|
|
62
|
+
teamName: string;
|
|
63
|
+
propertyId: number;
|
|
64
|
+
department: Department;
|
|
65
|
+
name?: string;
|
|
66
|
+
email?: string;
|
|
67
|
+
phone?: string;
|
|
68
|
+
message?: string;
|
|
69
|
+
}): Promise<string> {
|
|
70
|
+
return submitIntent("loop-property-callback", "call-back", params, "Callback request");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function propertyInformation(params: {
|
|
74
|
+
accountId: string;
|
|
75
|
+
teamName: string;
|
|
76
|
+
propertyId: number;
|
|
77
|
+
department: Department;
|
|
78
|
+
name?: string;
|
|
79
|
+
email?: string;
|
|
80
|
+
phone?: string;
|
|
81
|
+
message?: string;
|
|
82
|
+
}): Promise<string> {
|
|
83
|
+
return submitIntent("loop-property-information", "information", params, "Information request");
|
|
84
|
+
}
|
|
@@ -39,11 +39,10 @@ export async function propertyListed(params: {
|
|
|
39
39
|
accountId: string;
|
|
40
40
|
channel: Channel;
|
|
41
41
|
department?: Department;
|
|
42
|
-
includeSold?: boolean;
|
|
43
42
|
teamName?: string;
|
|
44
43
|
limit?: number;
|
|
45
44
|
}): Promise<string> {
|
|
46
|
-
const { accountId, channel, department = "both",
|
|
45
|
+
const { accountId, channel, department = "both", teamName, limit } = params;
|
|
47
46
|
|
|
48
47
|
const result = await aggregateAcrossTeams<LoopPropertyListing>(
|
|
49
48
|
accountId,
|
|
@@ -60,12 +59,6 @@ export async function propertyListed(params: {
|
|
|
60
59
|
if (Array.isArray(data)) all.push(...data);
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
if (includeSold) {
|
|
64
|
-
const soldPath = `/property/residential/sold/${channel}`;
|
|
65
|
-
const soldData = await loopGet<LoopPropertyListing[]>(apiKey, soldPath, "loop-property-listed", team);
|
|
66
|
-
if (Array.isArray(soldData)) all.push(...soldData);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
62
|
return all;
|
|
70
63
|
},
|
|
71
64
|
{ teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aggregateAcrossTeams,
|
|
3
|
+
formatAggregationResult,
|
|
4
|
+
loopGet,
|
|
5
|
+
} from "../lib/loop-api.js";
|
|
6
|
+
|
|
7
|
+
// Loop API V2: GET /property/residential/sold/{channel}
|
|
8
|
+
// Distinct from /listed/{channel} — returns exchanged or completed properties
|
|
9
|
+
// (Loop swagger summary: "USE THIS TO POWER WEBSITE SOLD GALLERIES").
|
|
10
|
+
// Use case: canvassing letters built from recent sales in a target postcode.
|
|
11
|
+
|
|
12
|
+
interface LoopPropertyListing {
|
|
13
|
+
listingId?: number;
|
|
14
|
+
propertyId?: number;
|
|
15
|
+
propertyRefId?: string;
|
|
16
|
+
channel?: string;
|
|
17
|
+
propertyAddress?: string;
|
|
18
|
+
displayAddress?: string;
|
|
19
|
+
status?: string;
|
|
20
|
+
propertyType?: string;
|
|
21
|
+
dateLaunched?: string;
|
|
22
|
+
price?: number;
|
|
23
|
+
bedrooms?: number;
|
|
24
|
+
bathrooms?: number;
|
|
25
|
+
receptionRooms?: number;
|
|
26
|
+
shortDescription?: string;
|
|
27
|
+
features?: string[];
|
|
28
|
+
images?: { url: string }[];
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Channel = "rightmove" | "onTheMarket" | "zoopla" | "website";
|
|
33
|
+
|
|
34
|
+
export async function propertySold(params: {
|
|
35
|
+
accountId: string;
|
|
36
|
+
channel: Channel;
|
|
37
|
+
teamName?: string;
|
|
38
|
+
limit?: number;
|
|
39
|
+
}): Promise<string> {
|
|
40
|
+
const { accountId, channel, teamName, limit } = params;
|
|
41
|
+
|
|
42
|
+
const result = await aggregateAcrossTeams<LoopPropertyListing>(
|
|
43
|
+
accountId,
|
|
44
|
+
"properties",
|
|
45
|
+
"loop-property-sold",
|
|
46
|
+
async (apiKey, team) => {
|
|
47
|
+
const path = `/property/residential/sold/${channel}`;
|
|
48
|
+
const data = await loopGet<LoopPropertyListing[]>(apiKey, path, "loop-property-sold", team);
|
|
49
|
+
return Array.isArray(data) ? data : [];
|
|
50
|
+
},
|
|
51
|
+
{ teamName, limitPerTeam: limit ?? 50, limitTotal: limit ?? 200 }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return formatAggregationResult(
|
|
55
|
+
result,
|
|
56
|
+
(p) => {
|
|
57
|
+
const addr = p.displayAddress ?? p.propertyAddress ?? "Unknown address";
|
|
58
|
+
const id = p.propertyId != null ? ` [ID: ${p.propertyId}]` : "";
|
|
59
|
+
const price = p.price ? ` — £${p.price.toLocaleString("en-GB")}` : "";
|
|
60
|
+
const beds = p.bedrooms ? ` ${p.bedrooms}bed` : "";
|
|
61
|
+
const propType = p.propertyType ? ` (${p.propertyType})` : "";
|
|
62
|
+
const status = p.status ? ` [${p.status}]` : "";
|
|
63
|
+
return `- ${addr}${id}${price}${beds}${propType}${status}`;
|
|
64
|
+
},
|
|
65
|
+
`${channel} sold`
|
|
66
|
+
);
|
|
67
|
+
}
|