@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
@@ -10,7 +10,7 @@ import { decrypt } from "./crypto.js";
10
10
  // For each key: │
11
11
  // 1. Decrypt API key │
12
12
  // 2. Check permissions │
13
- // 3. HTTP GET → Loop API V2
13
+ // 3. HTTP GET/POST/PUT → Loop API V2
14
14
  // │ │
15
15
  // ▼ │
16
16
  // Promise.allSettled → merge results │
@@ -114,13 +114,116 @@ export async function loopGet<T>(
114
114
  }
115
115
  }
116
116
 
117
+ /**
118
+ * Make a POST request to the Loop API V2.
119
+ * For write operations: create viewings, record feedback, submit enquiries, etc.
120
+ * Returns parsed JSON response. Treats 204 as success with empty result.
121
+ */
122
+ export async function loopPost<T>(
123
+ apiKey: string,
124
+ path: string,
125
+ body: unknown,
126
+ toolName: string,
127
+ teamName: string
128
+ ): Promise<T> {
129
+ const url = `${LOOP_BASE_URL}${path}`;
130
+ const start = Date.now();
131
+
132
+ const response = await fetch(url, {
133
+ method: "POST",
134
+ headers: {
135
+ "X-Api-Key": apiKey,
136
+ "Accept": "application/json",
137
+ "Content-Type": "application/json",
138
+ },
139
+ body: JSON.stringify(body),
140
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
141
+ });
142
+
143
+ const duration = Date.now() - start;
144
+ console.error(
145
+ `[loop] WRITE ${toolName} team=${teamName} endpoint=POST ${path} status=${response.status} duration=${duration}ms`
146
+ );
147
+
148
+ if (response.status === 204) {
149
+ return { success: true } as unknown as T;
150
+ }
151
+
152
+ if (!response.ok) {
153
+ const errorBody = await response.text().catch(() => "");
154
+ throw new LoopApiError(response.status, teamName, path, errorBody);
155
+ }
156
+
157
+ const text = await response.text();
158
+ try {
159
+ return JSON.parse(text) as T;
160
+ } catch {
161
+ throw new Error(
162
+ `Loop returned non-JSON response for team=${teamName} path=${path} ` +
163
+ `(status=${response.status}, length=${text.length})`
164
+ );
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Make a PUT request to the Loop API V2.
170
+ * For update operations: batch matching, submit quotes, update preferences, etc.
171
+ */
172
+ export async function loopPut<T>(
173
+ apiKey: string,
174
+ path: string,
175
+ body: unknown,
176
+ toolName: string,
177
+ teamName: string
178
+ ): Promise<T> {
179
+ const url = `${LOOP_BASE_URL}${path}`;
180
+ const start = Date.now();
181
+
182
+ const response = await fetch(url, {
183
+ method: "PUT",
184
+ headers: {
185
+ "X-Api-Key": apiKey,
186
+ "Accept": "application/json",
187
+ "Content-Type": "application/json",
188
+ },
189
+ body: JSON.stringify(body),
190
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
191
+ });
192
+
193
+ const duration = Date.now() - start;
194
+ console.error(
195
+ `[loop] WRITE ${toolName} team=${teamName} endpoint=PUT ${path} status=${response.status} duration=${duration}ms`
196
+ );
197
+
198
+ if (response.status === 204) {
199
+ return { success: true } as unknown as T;
200
+ }
201
+
202
+ if (!response.ok) {
203
+ const errorBody = await response.text().catch(() => "");
204
+ throw new LoopApiError(response.status, teamName, path, errorBody);
205
+ }
206
+
207
+ const text = await response.text();
208
+ try {
209
+ return JSON.parse(text) as T;
210
+ } catch {
211
+ throw new Error(
212
+ `Loop returned non-JSON response for team=${teamName} path=${path} ` +
213
+ `(status=${response.status}, length=${text.length})`
214
+ );
215
+ }
216
+ }
217
+
117
218
  export class LoopApiError extends Error {
118
219
  constructor(
119
220
  public readonly status: number,
120
221
  public readonly teamName: string,
121
- public readonly path: string
222
+ public readonly path: string,
223
+ public readonly responseBody?: string
122
224
  ) {
123
- super(`Loop API returned ${status} for team=${teamName} path=${path}`);
225
+ const detail = responseBody ? ` — ${responseBody.slice(0, 200)}` : "";
226
+ super(`Loop API returned ${status} for team=${teamName} path=${path}${detail}`);
124
227
  this.name = "LoopApiError";
125
228
  }
126
229
  }
@@ -244,6 +347,40 @@ export async function aggregateAcrossTeams<T>(
244
347
  return { results, failures, skipped, totalTeams: allKeys.length };
245
348
  }
246
349
 
350
+ /**
351
+ * Resolve a single team's API key with permission checking.
352
+ * Handles key loading, decryption, and permission validation.
353
+ * Used for both reads and writes that target a specific team — no fan-out.
354
+ */
355
+ export async function withTeamKey<T>(
356
+ accountId: string,
357
+ teamName: string,
358
+ endpointGroup: string,
359
+ toolName: string,
360
+ execute: (apiKey: string) => Promise<T>
361
+ ): Promise<T> {
362
+ const allKeys = await loadTeamKeys(accountId);
363
+ if (allKeys.length === 0) {
364
+ throw new Error("No Loop teams registered. Use loop-key-register to add team API keys.");
365
+ }
366
+
367
+ const key = allKeys.find((k) => k.teamName === teamName);
368
+ if (!key) {
369
+ const available = allKeys.map((k) => k.teamName).join(", ");
370
+ throw new Error(`Team "${teamName}" not found. Available teams: ${available}`);
371
+ }
372
+
373
+ if (!key.permissions.includes(endpointGroup)) {
374
+ throw new Error(
375
+ `Team "${teamName}" does not have ${endpointGroup} permission. ` +
376
+ `Current permissions: ${key.permissions.join(", ")}`
377
+ );
378
+ }
379
+
380
+ const apiKey = decrypt(key.encryptedKey);
381
+ return execute(apiKey);
382
+ }
383
+
247
384
  /**
248
385
  * Format an aggregation result into a human-readable text response.
249
386
  */
@@ -0,0 +1,60 @@
1
+ import { loopGet, loopPost, withTeamKey } from "../lib/loop-api.js";
2
+
3
+ type PreferencesAction = "read" | "write";
4
+
5
+ interface LoopCustomerPreferences {
6
+ personCode?: number;
7
+ preferences?: Record<string, unknown>;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ interface LoopBooleanResponse {
12
+ success?: boolean;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ export async function customerPreferences(params: {
17
+ accountId: string;
18
+ teamName: string;
19
+ personCode: number;
20
+ action: PreferencesAction;
21
+ preferences?: Record<string, unknown>;
22
+ }): Promise<string> {
23
+ const { accountId, teamName, personCode, action } = params;
24
+
25
+ if (action === "read") {
26
+ const result = await withTeamKey<LoopCustomerPreferences>(
27
+ accountId, teamName, "customer", "loop-customer-preferences",
28
+ async (apiKey) => {
29
+ return loopGet<LoopCustomerPreferences>(
30
+ apiKey, `/customer/preferences/${personCode}`,
31
+ "loop-customer-preferences", teamName
32
+ );
33
+ }
34
+ );
35
+
36
+ if (!result || (Array.isArray(result) && result.length === 0)) {
37
+ return `No preferences found for person ${personCode} via team "${teamName}".`;
38
+ }
39
+ return JSON.stringify(result, null, 2);
40
+ }
41
+
42
+ if (action === "write") {
43
+ if (!params.preferences) {
44
+ throw new Error("preferences object is required when action is 'write'.");
45
+ }
46
+
47
+ await withTeamKey<LoopBooleanResponse>(
48
+ accountId, teamName, "customer", "loop-customer-preferences",
49
+ async (apiKey) => {
50
+ return loopPost<LoopBooleanResponse>(
51
+ apiKey, `/customer/preferences/${personCode}`,
52
+ params.preferences!, "loop-customer-preferences", teamName
53
+ );
54
+ }
55
+ );
56
+ return `Preferences updated for person ${personCode} via team "${teamName}".`;
57
+ }
58
+
59
+ throw new Error(`Unknown action: ${action}. Use "read" or "write".`);
60
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ loopGet,
3
+ loopPost,
4
+ withTeamKey,
5
+ } from "../lib/loop-api.js";
6
+
7
+ type Department = "sales" | "lettings";
8
+
9
+ interface LoopViewingFeedback {
10
+ viewingId?: number;
11
+ propertyAddress?: string;
12
+ date?: string;
13
+ attendeeName?: string;
14
+ feedback?: string;
15
+ rating?: number;
16
+ status?: string;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ interface LoopBooleanResponse {
21
+ success?: boolean;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ export async function feedbackGet(params: {
26
+ accountId: string;
27
+ teamName: string;
28
+ viewingId: number;
29
+ department: Department;
30
+ }): Promise<string> {
31
+ const { accountId, teamName, viewingId, department } = params;
32
+
33
+ const result = await withTeamKey<LoopViewingFeedback>(
34
+ accountId,
35
+ teamName,
36
+ "feedback",
37
+ "loop-feedback-get",
38
+ async (apiKey) => {
39
+ const path = `/feedback/residential/${department}/viewings/${viewingId}`;
40
+ return loopGet<LoopViewingFeedback>(apiKey, path, "loop-feedback-get", teamName);
41
+ }
42
+ );
43
+
44
+ if (!result || (Array.isArray(result) && result.length === 0)) {
45
+ return `No feedback found for viewing ${viewingId} (${department}) via team "${teamName}".`;
46
+ }
47
+
48
+ const f = result;
49
+ const lines = [`**Feedback for viewing ${viewingId}**`];
50
+ if (f.propertyAddress) lines.push(`Property: ${f.propertyAddress}`);
51
+ if (f.date) lines.push(`Date: ${f.date}`);
52
+ if (f.attendeeName) lines.push(`Attendee: ${f.attendeeName}`);
53
+ if (f.rating != null) lines.push(`Rating: ★${f.rating}`);
54
+ if (f.status) lines.push(`Status: ${f.status}`);
55
+ if (f.feedback) lines.push(`Feedback: ${f.feedback}`);
56
+ return lines.join("\n");
57
+ }
58
+
59
+ export async function feedbackSubmit(params: {
60
+ accountId: string;
61
+ teamName: string;
62
+ viewingId: number;
63
+ department: Department;
64
+ feedback: string;
65
+ }): Promise<string> {
66
+ const { accountId, teamName, viewingId, department, feedback } = params;
67
+
68
+ await withTeamKey<LoopBooleanResponse>(
69
+ accountId,
70
+ teamName,
71
+ "feedback",
72
+ "loop-feedback-submit",
73
+ async (apiKey) => {
74
+ const path = `/feedback/residential/${department}/viewings/${viewingId}/feedback`;
75
+ return loopPost<LoopBooleanResponse>(apiKey, path, { result: feedback }, "loop-feedback-submit", teamName);
76
+ }
77
+ );
78
+
79
+ return `Feedback submitted for viewing ${viewingId} (${department}) via team "${teamName}".`;
80
+ }
@@ -10,7 +10,7 @@ interface LoopTeamResponse {
10
10
  [key: string]: unknown;
11
11
  }
12
12
 
13
- const ALL_PERMISSIONS = ["properties", "people", "viewings", "feedback", "team"];
13
+ const ALL_PERMISSIONS = ["properties", "people", "viewings", "feedback", "team", "marketing", "customer", "supplier"];
14
14
 
15
15
  export async function keyRegister(params: {
16
16
  teamName: string;
@@ -0,0 +1,105 @@
1
+ import { loopGet, loopPost, loopPut, withTeamKey } from "../lib/loop-api.js";
2
+
3
+ type EnquiryAction = "seller-enquiry" | "autoresponder-get" | "autoresponder-answers" | "autoresponder-details" | "autoresponder-refer";
4
+
5
+ interface LoopBooleanResponse {
6
+ success?: boolean;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ interface LoopAutoResponderDetails {
11
+ id?: number;
12
+ questions?: unknown[];
13
+ enquiryDetails?: unknown;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export async function marketingEnquiry(params: {
18
+ accountId: string;
19
+ teamName: string;
20
+ action: EnquiryAction;
21
+ // For seller-enquiry
22
+ sellerEnquiryData?: Record<string, unknown>;
23
+ // For autoresponder actions
24
+ autoResponderId?: number;
25
+ autoResponderKey?: string;
26
+ // For autoresponder-answers
27
+ answers?: unknown[];
28
+ // For autoresponder-details
29
+ details?: Record<string, unknown>;
30
+ }): Promise<string> {
31
+ const { accountId, teamName, action } = params;
32
+
33
+ if (action === "seller-enquiry") {
34
+ await withTeamKey<LoopBooleanResponse>(
35
+ accountId, teamName, "marketing", "loop-marketing-enquiry",
36
+ async (apiKey) => {
37
+ return loopPost<LoopBooleanResponse>(
38
+ apiKey, "/marketing/enquiries/seller",
39
+ params.sellerEnquiryData ?? {}, "loop-marketing-enquiry", teamName
40
+ );
41
+ }
42
+ );
43
+ return `Seller enquiry submitted via team "${teamName}".`;
44
+ }
45
+
46
+ if (!params.autoResponderId || !params.autoResponderKey) {
47
+ throw new Error("autoResponderId and autoResponderKey are required for autoresponder actions.");
48
+ }
49
+ const id = params.autoResponderId;
50
+ const key = params.autoResponderKey;
51
+
52
+ if (action === "autoresponder-get") {
53
+ const result = await withTeamKey<LoopAutoResponderDetails>(
54
+ accountId, teamName, "marketing", "loop-marketing-enquiry",
55
+ async (apiKey) => {
56
+ return loopGet<LoopAutoResponderDetails>(
57
+ apiKey, `/marketing/enquiries/auto-responder/${id}/${key}`,
58
+ "loop-marketing-enquiry", teamName
59
+ );
60
+ }
61
+ );
62
+ return JSON.stringify(result, null, 2);
63
+ }
64
+
65
+ if (action === "autoresponder-answers") {
66
+ await withTeamKey<LoopBooleanResponse>(
67
+ accountId, teamName, "marketing", "loop-marketing-enquiry",
68
+ async (apiKey) => {
69
+ return loopPut<LoopBooleanResponse>(
70
+ apiKey, `/marketing/enquiries/auto-responder/${id}/answers/${key}`,
71
+ params.answers ?? [], "loop-marketing-enquiry", teamName
72
+ );
73
+ }
74
+ );
75
+ return `Auto-responder answers submitted for enquiry ${id} via team "${teamName}".`;
76
+ }
77
+
78
+ if (action === "autoresponder-details") {
79
+ await withTeamKey<LoopBooleanResponse>(
80
+ accountId, teamName, "marketing", "loop-marketing-enquiry",
81
+ async (apiKey) => {
82
+ return loopPut<LoopBooleanResponse>(
83
+ apiKey, `/marketing/enquiries/auto-responder/${id}/details/${key}`,
84
+ params.details ?? {}, "loop-marketing-enquiry", teamName
85
+ );
86
+ }
87
+ );
88
+ return `Auto-responder details updated for enquiry ${id} via team "${teamName}".`;
89
+ }
90
+
91
+ if (action === "autoresponder-refer") {
92
+ await withTeamKey<LoopBooleanResponse>(
93
+ accountId, teamName, "marketing", "loop-marketing-enquiry",
94
+ async (apiKey) => {
95
+ return loopPut<LoopBooleanResponse>(
96
+ apiKey, `/marketing/enquiries/auto-responder/${id}/refer/${key}`,
97
+ {}, "loop-marketing-enquiry", teamName
98
+ );
99
+ }
100
+ );
101
+ return `Enquiry ${id} referred via team "${teamName}".`;
102
+ }
103
+
104
+ throw new Error(`Unknown action: ${action}`);
105
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopPut,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopMatchingSummary {
8
+ id?: number;
9
+ address?: string;
10
+ price?: number;
11
+ type?: string;
12
+ bedrooms?: number;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ type Department = "sales" | "lettings";
17
+
18
+ export async function marketingMatchBatch(params: {
19
+ accountId: string;
20
+ propertyIds: number[];
21
+ department: Department;
22
+ teamName?: string;
23
+ }): Promise<string> {
24
+ const { accountId, propertyIds, department, teamName } = params;
25
+
26
+ const result = await aggregateAcrossTeams<LoopMatchingSummary>(
27
+ accountId,
28
+ "marketing",
29
+ "loop-marketing-match-batch",
30
+ async (apiKey, team) => {
31
+ const prefix = department === "lettings" ? "/marketing/rentals" : "/marketing";
32
+ const path = `${prefix}/matching/other-matches`;
33
+ const data = await loopPut<LoopMatchingSummary[]>(apiKey, path, propertyIds, "loop-marketing-match-batch", team);
34
+ return Array.isArray(data) ? data : [];
35
+ },
36
+ { teamName }
37
+ );
38
+
39
+ return formatAggregationResult(
40
+ result,
41
+ (p) => {
42
+ const price = p.price ? ` — £${p.price.toLocaleString("en-GB")}` : "";
43
+ const beds = p.bedrooms ? ` ${p.bedrooms}bed` : "";
44
+ return `- ${p.address ?? "Unknown"}${price}${beds}`;
45
+ },
46
+ "matching properties"
47
+ );
48
+ }
@@ -0,0 +1,37 @@
1
+ import { loopPost, withTeamKey } from "../lib/loop-api.js";
2
+
3
+ type Department = "sales" | "lettings";
4
+ type MatchAction = "viewing" | "information" | "callback";
5
+
6
+ interface LoopBooleanResponse {
7
+ success?: boolean;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export async function marketingMatchRequest(params: {
12
+ accountId: string;
13
+ teamName: string;
14
+ propertyId: number;
15
+ department: Department;
16
+ action: MatchAction;
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
+ await withTeamKey<LoopBooleanResponse>(
25
+ accountId,
26
+ teamName,
27
+ "marketing",
28
+ "loop-marketing-match-request",
29
+ async (apiKey) => {
30
+ const prefix = department === "lettings" ? "/marketing/rentals" : "/marketing";
31
+ const path = `${prefix}/matching/${propertyId}/${action}`;
32
+ return loopPost<LoopBooleanResponse>(apiKey, path, body, "loop-marketing-match-request", teamName);
33
+ }
34
+ );
35
+
36
+ return `${action} request submitted for matching property ${propertyId} (${department}) via team "${teamName}".`;
37
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopMatchingProperty {
8
+ id?: number;
9
+ address?: string;
10
+ price?: number;
11
+ type?: string;
12
+ bedrooms?: number;
13
+ status?: string;
14
+ description?: string;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ interface LoopMatchingTeamProfile {
19
+ name?: string;
20
+ address?: string;
21
+ phone?: string;
22
+ email?: string;
23
+ logoUrl?: string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ type Department = "sales" | "lettings";
28
+
29
+ export async function marketingMatchDetail(params: {
30
+ accountId: string;
31
+ propertyId: number;
32
+ department: Department;
33
+ includeTeamProfile?: boolean;
34
+ teamName?: string;
35
+ }): Promise<string> {
36
+ const { accountId, propertyId, department, includeTeamProfile = false, teamName } = params;
37
+
38
+ const result = await aggregateAcrossTeams<LoopMatchingProperty>(
39
+ accountId,
40
+ "marketing",
41
+ "loop-marketing-match",
42
+ async (apiKey, team) => {
43
+ const prefix = department === "lettings" ? "/marketing/rentals" : "/marketing";
44
+ const path = `${prefix}/matching/${propertyId}`;
45
+ const data = await loopGet<LoopMatchingProperty>(apiKey, path, "loop-marketing-match", team);
46
+ if (data && typeof data === "object" && !Array.isArray(data)) {
47
+ if (includeTeamProfile) {
48
+ const profilePath = `${prefix}/matching/${propertyId}/team-profile`;
49
+ const profile = await loopGet<LoopMatchingTeamProfile>(apiKey, profilePath, "loop-marketing-match", team).catch(() => null);
50
+ if (profile) {
51
+ (data as Record<string, unknown>)._teamProfile = profile;
52
+ }
53
+ }
54
+ return [data];
55
+ }
56
+ return [];
57
+ },
58
+ { teamName }
59
+ );
60
+
61
+ return formatAggregationResult(
62
+ result,
63
+ (p) => {
64
+ const lines = [`**${p.address ?? "Unknown address"}**`];
65
+ if (p.price) lines.push(`Price: £${p.price.toLocaleString("en-GB")}`);
66
+ if (p.type) lines.push(`Type: ${p.type}`);
67
+ if (p.bedrooms) lines.push(`Bedrooms: ${p.bedrooms}`);
68
+ if (p.status) lines.push(`Status: ${p.status}`);
69
+ if (p.description) lines.push(`\n${p.description}`);
70
+ const profile = (p as Record<string, unknown>)._teamProfile as LoopMatchingTeamProfile | undefined;
71
+ if (profile) {
72
+ lines.push(`\n**Team:** ${profile.name ?? ""} — ${profile.address ?? ""} | ${profile.phone ?? ""} | ${profile.email ?? ""}`);
73
+ }
74
+ return lines.join("\n");
75
+ },
76
+ "matching property details"
77
+ );
78
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ aggregateAcrossTeams,
3
+ formatAggregationResult,
4
+ loopGet,
5
+ } from "../lib/loop-api.js";
6
+
7
+ interface LoopPersonDetail {
8
+ id?: number;
9
+ firstName?: string;
10
+ lastName?: string;
11
+ email?: string;
12
+ phone?: string;
13
+ mobile?: string;
14
+ type?: string;
15
+ address?: string;
16
+ postcode?: string;
17
+ notes?: string;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ type PeopleRole = "buyers" | "sellers" | "renters" | "landlords";
22
+
23
+ export async function peopleDetail(params: {
24
+ accountId: string;
25
+ personId: number;
26
+ role?: PeopleRole;
27
+ teamName?: string;
28
+ }): Promise<string> {
29
+ const { accountId, personId, role, teamName } = params;
30
+
31
+ const result = await aggregateAcrossTeams<LoopPersonDetail>(
32
+ accountId,
33
+ "people",
34
+ "loop-people-detail",
35
+ async (apiKey, team) => {
36
+ const basePath = role ? `/people/${role}` : "/people";
37
+ const path = `${basePath}/${personId}`;
38
+ const data = await loopGet<LoopPersonDetail>(apiKey, path, "loop-people-detail", team);
39
+ if (data && typeof data === "object" && !Array.isArray(data)) {
40
+ return [data];
41
+ }
42
+ return [];
43
+ },
44
+ { teamName }
45
+ );
46
+
47
+ return formatAggregationResult(
48
+ 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
+ },
61
+ "person details"
62
+ );
63
+ }