@rubytech/create-maxy 1.0.499 → 1.0.501
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/package.json +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/PLUGIN.md +36 -8
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js +229 -153
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.d.ts +19 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.js +99 -3
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts +10 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js +24 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/customer-preferences.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts +16 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js +35 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/feedback.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/key-register.js +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts +13 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js +41 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-enquiry.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts +9 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js +16 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-batch.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts +15 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js +11 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match-request.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts +10 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js +39 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/marketing-match.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts +9 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js +33 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-detail.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts +18 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js +59 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/people-search.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts +10 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js +39 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-detail.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts +12 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js +28 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-listed.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts +15 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js +11 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-request.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts +16 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js +39 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/property-search.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts +13 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js +49 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/supplier.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts +7 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js +15 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/team-availability.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts +14 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js +11 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-create.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts +9 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js +40 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-detail.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts +13 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js +34 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-search.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts +14 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.d.ts.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js +18 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/dist/tools/viewing-update.js.map +1 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/index.ts +335 -158
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/lib/loop-api.ts +140 -3
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/customer-preferences.ts +60 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback.ts +80 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/key-register.ts +1 -1
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-enquiry.ts +105 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-batch.ts +48 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match-request.ts +37 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/marketing-match.ts +78 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-detail.ts +63 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-search.ts +93 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-detail.ts +70 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-listed.ts +67 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-request.ts +37 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/property-search.ts +80 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/supplier.ts +120 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/team-availability.ts +42 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-create.ts +36 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-detail.ts +70 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-search.ts +74 -0
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewing-update.ts +48 -0
- package/payload/server/server.js +95 -5
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback-list.ts +0 -54
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-list.ts +0 -52
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/properties-list.ts +0 -52
- package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/viewings-list.ts +0 -62
package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/lib/loop-api.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/feedback.ts
ADDED
|
@@ -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
|
+
}
|
package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/key-register.ts
CHANGED
|
@@ -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
|
+
}
|
package/payload/premium-plugins/real-agency/plugins/real-agency-loop/mcp/src/tools/people-detail.ts
ADDED
|
@@ -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
|
+
}
|